From 8c69ca7a302fdf687a25c61d76752f3012d39146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 1 Oct 2026 11:19:56 +0200 Subject: [PATCH] Document the PageResult attributes in a Returns section The methods annotated `-> PageResult` now say what that result carries: a Google-style `Returns:` section lists `url`, `http_status` and `title`, each described by the tool's outputSchema property. pdoc and the docs site render it as a list under Returns, which is the column the retired hand-written reference page had. The attribute names come from the schema, so generation stops if the browser adds an output property that PageResult does not expose, instead of documenting an attribute that is not there. Closes #6. --- lightpanda/_methods.py | 112 ++++++++++++++++++++++++++++++++++++ scripts/generate_methods.py | 31 ++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/lightpanda/_methods.py b/lightpanda/_methods.py index 1ecc3e5..92c1ed3 100644 --- a/lightpanda/_methods.py +++ b/lightpanda/_methods.py @@ -24,6 +24,13 @@ def click(self, *, selector: str | None = None, backend_node_id: int | None = No Args: selector: CSS selector of the element to click. Preferred over backendNodeId. backend_node_id: The backend node ID of the element to click. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("click", selector=selector, backend_node_id=backend_node_id) def console_logs(self) -> Any: @@ -77,6 +84,13 @@ def fill(self, *, value: str, selector: str | None = None, backend_node_id: int value: The text to fill into the input element. selector: CSS selector of the input element to fill. Preferred over backendNodeId. backend_node_id: The backend node ID of the input element to fill. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("fill", value=value, selector=selector, backend_node_id=backend_node_id) def find_element(self, *, role: str | None = None, name: str | None = None) -> Any: @@ -112,6 +126,13 @@ def goto(self, *, url: str, timeout: int | None = None, wait_until: str | None = url: The URL to navigate to, must be a valid URL. timeout: Optional timeout in milliseconds. Defaults to 10000. wait_until: Event that completes the navigation. Defaults to 'load'. Prefer 'domcontentloaded' followed by waitForSelector on pages whose late scripts (ads) hold 'load' back. Avoid 'done' (full quiescence): on pages with constant background activity it is the slowest choice and can run to the timeout. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("goto", url=url, timeout=timeout, wait_until=wait_until) def hover(self, *, selector: str | None = None, backend_node_id: int | None = None) -> PageResult: @@ -120,6 +141,13 @@ def hover(self, *, selector: str | None = None, backend_node_id: int | None = No Args: selector: CSS selector of the element to hover over. Preferred over backendNodeId. backend_node_id: The backend node ID of the element to hover over. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("hover", selector=selector, backend_node_id=backend_node_id) def html(self, *, selector: str | None = None, backend_node_id: int | None = None, max_bytes: int | None = None, strip: dict | None = None, url: str | None = None, timeout: int | None = None) -> Any: @@ -177,6 +205,13 @@ def press(self, *, key: str, selector: str | None = None, backend_node_id: int | key: The key to press (e.g. 'Enter', 'Tab', 'a'). selector: Optional CSS selector of the element to target. Preferred over backendNodeId. backend_node_id: Optional backend node ID of the element to target. Defaults to the document when neither selector nor backendNodeId is provided; 0 is treated as omitted. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("press", key=key, selector=selector, backend_node_id=backend_node_id) def screenshot(self, *, path: str | None = None, selector: str | None = None, backend_node_id: int | None = None, full_page: bool | None = None, strip: dict | None = None, url: str | None = None, timeout: int | None = None) -> Any: @@ -200,6 +235,13 @@ def scroll(self, *, selector: str | None = None, backend_node_id: int | None = N backend_node_id: Optional: The backend node ID of the element to scroll. If the element is not itself a scroll container, its nearest scrollable ancestor is scrolled instead. If neither this nor selector is given (or it is 0), scrolls the window. x: Optional: The horizontal scroll offset. y: Optional: The vertical scroll offset. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("scroll", selector=selector, backend_node_id=backend_node_id, x=x, y=y) def search(self, *, query: str, timeout: int | None = None) -> Any: @@ -217,6 +259,13 @@ def select_option(self, *, value: str, selector: str | None = None, backend_node value: The value of the option to select. selector: CSS selector of the element. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("selectOption", value=value, selector=selector, backend_node_id=backend_node_id) def set_checked(self, *, checked: bool, selector: str | None = None, backend_node_id: int | None = None) -> PageResult: @@ -226,6 +275,13 @@ def set_checked(self, *, checked: bool, selector: str | None = None, backend_nod checked: Whether to check (true) or uncheck (false) the element. selector: CSS selector of the checkbox or radio input element. Preferred over backendNodeId. backend_node_id: The backend node ID of the checkbox or radio input element. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return self.call("setChecked", checked=checked, selector=selector, backend_node_id=backend_node_id) def structured_data(self, *, url: str | None = None, timeout: int | None = None) -> Any: @@ -281,6 +337,13 @@ async def click(self, *, selector: str | None = None, backend_node_id: int | Non Args: selector: CSS selector of the element to click. Preferred over backendNodeId. backend_node_id: The backend node ID of the element to click. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("click", selector=selector, backend_node_id=backend_node_id) async def console_logs(self) -> Any: @@ -334,6 +397,13 @@ async def fill(self, *, value: str, selector: str | None = None, backend_node_id value: The text to fill into the input element. selector: CSS selector of the input element to fill. Preferred over backendNodeId. backend_node_id: The backend node ID of the input element to fill. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("fill", value=value, selector=selector, backend_node_id=backend_node_id) async def find_element(self, *, role: str | None = None, name: str | None = None) -> Any: @@ -369,6 +439,13 @@ async def goto(self, *, url: str, timeout: int | None = None, wait_until: str | url: The URL to navigate to, must be a valid URL. timeout: Optional timeout in milliseconds. Defaults to 10000. wait_until: Event that completes the navigation. Defaults to 'load'. Prefer 'domcontentloaded' followed by waitForSelector on pages whose late scripts (ads) hold 'load' back. Avoid 'done' (full quiescence): on pages with constant background activity it is the slowest choice and can run to the timeout. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("goto", url=url, timeout=timeout, wait_until=wait_until) async def hover(self, *, selector: str | None = None, backend_node_id: int | None = None) -> PageResult: @@ -377,6 +454,13 @@ async def hover(self, *, selector: str | None = None, backend_node_id: int | Non Args: selector: CSS selector of the element to hover over. Preferred over backendNodeId. backend_node_id: The backend node ID of the element to hover over. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("hover", selector=selector, backend_node_id=backend_node_id) async def html(self, *, selector: str | None = None, backend_node_id: int | None = None, max_bytes: int | None = None, strip: dict | None = None, url: str | None = None, timeout: int | None = None) -> Any: @@ -434,6 +518,13 @@ async def press(self, *, key: str, selector: str | None = None, backend_node_id: key: The key to press (e.g. 'Enter', 'Tab', 'a'). selector: Optional CSS selector of the element to target. Preferred over backendNodeId. backend_node_id: Optional backend node ID of the element to target. Defaults to the document when neither selector nor backendNodeId is provided; 0 is treated as omitted. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("press", key=key, selector=selector, backend_node_id=backend_node_id) async def screenshot(self, *, path: str | None = None, selector: str | None = None, backend_node_id: int | None = None, full_page: bool | None = None, strip: dict | None = None, url: str | None = None, timeout: int | None = None) -> Any: @@ -457,6 +548,13 @@ async def scroll(self, *, selector: str | None = None, backend_node_id: int | No backend_node_id: Optional: The backend node ID of the element to scroll. If the element is not itself a scroll container, its nearest scrollable ancestor is scrolled instead. If neither this nor selector is given (or it is 0), scrolls the window. x: Optional: The horizontal scroll offset. y: Optional: The vertical scroll offset. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("scroll", selector=selector, backend_node_id=backend_node_id, x=x, y=y) async def search(self, *, query: str, timeout: int | None = None) -> Any: @@ -474,6 +572,13 @@ async def select_option(self, *, value: str, selector: str | None = None, backen value: The value of the option to select. selector: CSS selector of the element. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("selectOption", value=value, selector=selector, backend_node_id=backend_node_id) async def set_checked(self, *, checked: bool, selector: str | None = None, backend_node_id: int | None = None) -> PageResult: @@ -483,6 +588,13 @@ async def set_checked(self, *, checked: bool, selector: str | None = None, backe checked: Whether to check (true) or uncheck (false) the element. selector: CSS selector of the checkbox or radio input element. Preferred over backendNodeId. backend_node_id: The backend node ID of the checkbox or radio input element. + + Returns: + PageResult: The sentence above, with these attributes (None when absent): + + - `url`: URL of the page the call left loaded. + - `http_status`: Response status of that page's own document. Absent when no response has arrived. 4xx/5xx means an error page, not the content. + - `title`: Document title. Empty or absent when the page has none. """ return await self.call("setChecked", checked=checked, selector=selector, backend_node_id=backend_node_id) async def structured_data(self, *, url: str | None = None, timeout: int | None = None) -> Any: diff --git a/scripts/generate_methods.py b/scripts/generate_methods.py index a53951c..65770e8 100644 --- a/scripts/generate_methods.py +++ b/scripts/generate_methods.py @@ -7,7 +7,8 @@ descriptions, so IDEs and pdoc show what every argument means. Tools that declare an ``outputSchema`` answer with ``structuredContent``, which ``Session.call`` returns as a ``PageResult``, so those methods are annotated -with it; the rest stay ``Any``. Being real code, the methods are +with it and get a ``Returns:`` section listing its attributes from the +schema's property descriptions; the rest stay ``Any``. Being real code, the methods are visible to IDEs, type checkers, and pdoc alike. Run with a binary available: uv run --no-project python scripts/generate_methods.py @@ -24,7 +25,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from lightpanda import Browser # noqa: E402 -from lightpanda.browser import _SESSION_TOOLS, _snake # noqa: E402 +from lightpanda.browser import _SESSION_TOOLS, PageResult, _snake # noqa: E402 PY_TYPES = {"string": "str", "integer": "int", "number": "float", "boolean": "bool", "object": "dict", "array": "list"} @@ -53,13 +54,29 @@ class {cls}: ''' -def docstring(description: str, args: list[tuple[str, str]]) -> str: +def returns_section(name: str, output_schema: dict) -> str: + """A Google-style ``Returns:`` section for a tool answering with + ``structuredContent``: the ``PageResult`` attributes, described by the + output schema's properties.""" + lines = ["Returns:", " PageResult: The sentence above, with these attributes (None when absent):", ""] + for prop, spec in output_schema.get("properties", {}).items(): + attr = _snake(prop) + if attr not in PageResult.__annotations__: + raise SystemExit(f"tool {name}: output property {prop!r} has no PageResult attribute") + lines.append(f" - `{attr}`: {spec.get('description', '').strip()}") + return "\n".join(lines) + + +def docstring(description: str, args: list[tuple[str, str]], returns: str = "") -> str: """The method docstring: the tool description, then a Google-style - ``Args:`` section built from the schema's property descriptions.""" + ``Args:`` section built from the schema's property descriptions, then + ``returns`` when given.""" text = description.strip() documented = [(arg, desc.strip()) for arg, desc in args if desc.strip()] if documented: text += "\n\nArgs:\n" + "\n".join(f" {arg}: {desc}" for arg, desc in documented) + if returns: + text += "\n\n" + returns body = text.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') lines = body.split("\n") if len(lines) == 1: @@ -100,9 +117,11 @@ def method_source(name: str, spec: dict, is_async: bool = False) -> str: call_args = ", ".join([f'"{name}"'] + forwards) prefix = "async def" if is_async else "def" await_ = "await " if is_async else "" - returns = "PageResult" if spec.get("output_schema") else "Any" + output_schema = spec.get("output_schema") + returns = "PageResult" if output_schema else "Any" lines = [f" {prefix} {snake}({', '.join(params)}) -> {returns}:"] - lines.append(docstring(spec["description"], documented)) + section = returns_section(name, output_schema) if output_schema else "" + lines.append(docstring(spec["description"], documented, section)) lines.append(f" return {await_}self.call({call_args})") return "\n".join(lines)