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)