Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions lightpanda/_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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 <select> element. Preferred over backendNodeId.
backend_node_id: The backend node ID of the <select> 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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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 <select> element. Preferred over backendNodeId.
backend_node_id: The backend node ID of the <select> 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:
Expand All @@ -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:
Expand Down
31 changes: 25 additions & 6 deletions scripts/generate_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
Loading