Files
browser/bindings/python/scripts/generate_methods.py
Adrià Arrufat f4ecf92f3b python: add lightpanda package (sidecar v1)
pip-installable Python API driving the bundled binary over MCP HTTP:
Browser/Session with one generated method per browser tool
(lightpanda/_methods.py, regenerated from tools/list), run_script()
replay, a CLI trampoline entry point, pytest suite against local
fixtures, and pdoc docs generation.
2026-07-23 18:09:21 +02:00

99 lines
3.1 KiB
Python

"""Generate lightpanda/_methods.py from the binary's tools/list schemas.
Emits one concrete, annotated, documented method per browser tool (snake_case,
with the original tool name aliased), all forwarding to ``Session.call``.
Being real code, the methods are visible to IDEs, type checkers, and pdoc
alike. Run from bindings/python with a binary available:
uv run --no-project python scripts/generate_methods.py
CI regenerates and diffs the file to catch tool-schema drift.
"""
from __future__ import annotations
import keyword
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from lightpanda import Browser # noqa: E402
from lightpanda.browser import _SESSION_TOOLS, _snake # noqa: E402
PY_TYPES = {"string": "str", "integer": "int", "number": "float", "boolean": "bool", "object": "dict", "array": "list"}
HEADER = '''\
"""Generated by scripts/generate_methods.py — do not edit.
One method per lightpanda browser tool, signatures derived from the tool
JSON schemas. camelCase aliases mirror the underlying tool names.
"""
from __future__ import annotations
from typing import Any
class SessionMethods:
def call(self, tool: str, **kwargs: Any) -> Any:
raise NotImplementedError
'''
def docstring(text: str) -> str:
body = text.strip().replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
return f' """{body}"""'
def method_source(name: str, spec: dict) -> str:
schema = spec["schema"]
properties = schema.get("properties", {})
required = set(schema.get("required", []))
for prop in properties:
if keyword.iskeyword(prop) or not prop.isidentifier():
raise SystemExit(f"tool {name}: property {prop!r} is not a valid Python parameter")
params = ["self"]
if properties:
params.append("*")
forwards = []
for prop in sorted(properties, key=lambda p: p not in required):
py_type = PY_TYPES.get(properties[prop].get("type", ""), "Any")
if name == "extract" and prop == "schema":
py_type = "str | dict | list"
if prop in required:
params.append(f"{prop}: {py_type}")
else:
params.append(f"{prop}: {py_type} | None = None")
forwards.append(f"{prop}={prop}")
snake = _snake(name)
call_args = ", ".join([f'"{name}"'] + forwards)
lines = [f" def {snake}({', '.join(params)}) -> Any:"]
lines.append(docstring(spec["description"]))
lines.append(f" return self.call({call_args})")
if snake != name:
lines.append("")
lines.append(f" {name} = {snake}")
return "\n".join(lines)
def main() -> None:
out = Path(__file__).parent.parent / "lightpanda" / "_methods.py"
with Browser() as browser:
tools = {k: v for k, v in browser.tools.items() if k not in _SESSION_TOOLS}
blocks = [HEADER]
for name in sorted(tools):
blocks.append(method_source(name, tools[name]))
source = "\n".join(blocks) + "\n"
compile(source, str(out), "exec")
out.write_text(source)
print(f"wrote {out} ({len(tools)} tools)")
if __name__ == "__main__":
main()