From 6bc9822681c31ae78445fca35775fe281d0bf85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 8 Oct 2026 09:58:42 +0200 Subject: [PATCH 1/2] Link class aliases to their class in the Python reference lightpanda-python added WebDriverServer and AsyncWebDriverServer as aliases of BiDiServer and AsyncBiDiServer. pdoc documents an alias as a variable, so it gets no section, yet collect_links mapped every __all__ name to its own heading id, leaving a link to #webdriverserver with no target and failing the generator. Point an alias at the class it names. --- scripts/generate-python-reference.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/generate-python-reference.py b/scripts/generate-python-reference.py index 01476d4..b249807 100644 --- a/scripts/generate-python-reference.py +++ b/scripts/generate-python-reference.py @@ -363,6 +363,12 @@ def collect_links(module: pdoc.doc.Module, names: list[str]) -> dict[str, str]: links: dict[str, str] = {} for name in names: doc = module.members[name] + target = getattr(lightpanda, name) + if inspect.isclass(target) and target.__name__ != name and target.__name__ in names: + # An alias such as `WebDriverServer = BiDiServer` gets no section + # of its own; point it at the class it names. + links[name] = slug(target.__name__) + continue links[name] = slug(name) if isinstance(doc, pdoc.doc.Class): for member in public_members(doc): From 868055ca27370da93c32f3023f1cb400a7ada4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 8 Oct 2026 10:02:24 +0200 Subject: [PATCH 2/2] Document WebDriverServer instead of the BiDiServer alias pair lightpanda-python drops BiDiServer for WebDriverServer with no alias (lightpanda-io/lightpanda-python#17), so the alias handling in the reference generator has nothing left to resolve: remove it. Point the Python guide's robots.txt note at WebDriverServer and regenerate the reference against that branch. --- scripts/generate-python-reference.py | 6 -- src/content/guides/use-python.mdx | 2 +- src/content/reference/python.mdx | 89 ++++++++++++++-------------- 3 files changed, 44 insertions(+), 53 deletions(-) diff --git a/scripts/generate-python-reference.py b/scripts/generate-python-reference.py index b249807..01476d4 100644 --- a/scripts/generate-python-reference.py +++ b/scripts/generate-python-reference.py @@ -363,12 +363,6 @@ def collect_links(module: pdoc.doc.Module, names: list[str]) -> dict[str, str]: links: dict[str, str] = {} for name in names: doc = module.members[name] - target = getattr(lightpanda, name) - if inspect.isclass(target) and target.__name__ != name and target.__name__ in names: - # An alias such as `WebDriverServer = BiDiServer` gets no section - # of its own; point it at the class it names. - links[name] = slug(target.__name__) - continue links[name] = slug(name) if isinstance(doc, pdoc.doc.Class): for member in public_members(doc): diff --git a/src/content/guides/use-python.mdx b/src/content/guides/use-python.mdx index bbde862..41b797b 100644 --- a/src/content/guides/use-python.mdx +++ b/src/content/guides/use-python.mdx @@ -199,7 +199,7 @@ A request the site disallows then fails rather than being sent: a `Session` call raises `ToolError`, and `run_script` raises `ScriptError`. The same flag works on [`CDPServer`](/reference/python#cdpserver) and -[`BiDiServer`](/reference/python#bidiserver), where a blocked request surfaces +[`WebDriverServer`](/reference/python#webdriverserver), where a blocked request surfaces as a navigation error in Playwright or Selenium rather than as a Python exception. diff --git a/src/content/reference/python.mdx b/src/content/reference/python.mdx index acbc032..a916d00 100644 --- a/src/content/reference/python.mdx +++ b/src/content/reference/python.mdx @@ -175,7 +175,7 @@ List the forms on the page as JSON: each form's `backendNodeId`, `action`, `meth **Arguments:** - - **url:** Optional URL to navigate to before processing. + - **url:** Optional URL to navigate to before processing. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.evaluate [#session-evaluate] @@ -196,7 +196,7 @@ Evaluate JavaScript in the current page context — an escape hatch for page-sid **Arguments:** - **script:** JavaScript run in the page context. A bare trailing expression, or `return` with top-level `await`, is the result. - - **url:** Optional URL to navigate to before evaluating. + - **url:** Optional URL to navigate to before evaluating. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. - **save:** Optional bridge-store key. The evaluate's return value is stored under this name and re-exposed as `lp.` to subsequent evaluates. Objects, arrays, and strings are serialized automatically — no JSON.stringify needed. @@ -288,7 +288,7 @@ Cookies stored in the browser. Defaults to cookies whose domain matches the curr **Arguments:** - - **url:** Restrict output to cookies matching this URL's host. Defaults to the current page. + - **url:** Restrict output to cookies matching this URL's host. Defaults to the current page. An empty string is treated as omitted. - **all:** If true, dump every cookie regardless of host. Overrides `url`. ### Session.get_env [#session-get-env] @@ -390,7 +390,7 @@ Raw HTML for the document or, with `selector`/`backendNodeId`, a single node's o - **backend_node_id:** Optional backend node ID. When set, dump only that node's outerHTML. 0 is treated as omitted. - **max_bytes:** Optional soft cap on output size in bytes. Content is truncated at a UTF-8 boundary and a short '[truncated]' marker is appended past the cap. - **strip:** Optional. Omit element groups from the output: `js` (script, noscript, script preloads), `css` (style, stylesheet links), `ui` (css plus img, picture, video, audio, svg, canvas, iframe), `invisible` (elements an author rule or inline style sets to display:none), `shell` (nav, aside, dialog, page-level header/footer and the matching landmark roles; skipped when that would drop most of the text), `clutter` (keep only the main content, in the manner of reader modes; includes `shell` and `invisible`, and falls back to `shell` when it finds too little). \{"js":true,"css":true\} keeps a page dump small. - - **url:** Optional URL to navigate to before dumping. + - **url:** Optional URL to navigate to before dumping. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.interactive_elements [#session-interactive-elements] @@ -403,7 +403,7 @@ List every visible interactive element on the page as a JSON array: native contr **Arguments:** - - **url:** Optional URL to navigate to before processing. + - **url:** Optional URL to navigate to before processing. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.links [#session-links] @@ -423,7 +423,7 @@ Extract the visible links in the opened page as JSON objects with `text` (anchor **Arguments:** - **limit:** Optional. Return at most this many links, in document order. - - **url:** Optional URL to navigate to before processing. + - **url:** Optional URL to navigate to before processing. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.markdown [#session-markdown] @@ -449,7 +449,7 @@ Render the page (or a subtree) as markdown. Scope with `selector` or `backendNod - **backend_node_id:** Optional backend node ID. Render markdown for just that node's subtree. 0 is treated as omitted. - **max_bytes:** Optional soft cap on output size in bytes. Content is truncated at a UTF-8 boundary and a short '[truncated]' marker is appended past the cap. - **strip:** Optional. Omit element groups from the output; same groups as the html tool's strip. `shell` (page chrome by markup) and `clutter` (keep only the main content, in the manner of reader modes) are the ones that matter for reading; `ui` also drops images. - - **url:** Optional URL to navigate to before rendering. + - **url:** Optional URL to navigate to before rendering. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.node_details [#session-node-details] @@ -518,7 +518,7 @@ Render the page, or one node, as a PNG: the text layout Lightpanda computes, not - **backend_node_id:** Optional backend node ID. When set, render only that node. 0 is treated as omitted. - **full_page:** Render the whole content height instead of one viewport. Defaults to false. - **strip:** Optional. Omit element groups from the render; same groups as the html tool's strip (`js`, `css`, `ui`, `invisible`, `shell`, `clutter`). - - **url:** Optional URL to navigate to before rendering. + - **url:** Optional URL to navigate to before rendering. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.scroll [#session-scroll] @@ -633,7 +633,7 @@ Page metadata as JSON: `jsonLd` (each JSON-LD block as a string), `openGraph`, ` **Arguments:** - - **url:** Optional URL to navigate to before processing. + - **url:** Optional URL to navigate to before processing. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. ### Session.tree [#session-tree] @@ -653,7 +653,7 @@ Semantic outline of the page as indented text: one node per line with its role, **Arguments:** - - **url:** Optional URL to navigate to before fetching the semantic tree. + - **url:** Optional URL to navigate to before fetching the semantic tree. An empty string is treated as omitted. - **timeout:** Optional timeout in milliseconds. Defaults to 10000. - **backend_node_id:** Optional backend node ID to get the tree for a specific element instead of the document root. 0 is treated as omitted. - **max_depth:** Optional maximum depth of the tree to return. Useful for exploring high-level structure first. @@ -1588,40 +1588,37 @@ async def start(self) Spawn the server process. Idempotent. -## BiDiServer [#bidiserver] +## WebDriverServer [#webdriverserver] -A lightpanda process serving WebDriver BiDi on 127.0.0.1. +A lightpanda process serving WebDriver, classic and BiDi, on 127.0.0.1. ```python -from lightpanda import BiDiServer +from lightpanda import WebDriverServer from selenium import webdriver -from selenium.webdriver.common.options import ArgOptions +from selenium.webdriver.common.by import By -options = ArgOptions() -options.web_socket_url = True # ask for a WebDriver BiDi session - -with BiDiServer() as server: - driver = webdriver.Remote(command_executor=server.http_endpoint, options=options) - context = driver.browsing_context.create(type="tab") - driver.browsing_context.navigate(context=context, url="https://example.com", wait="complete") - print(driver.script.execute("() => document.title", context_id=context)["value"]) +with WebDriverServer() as server: + # Selenium requires an options object; Lightpanda accepts any browser's. + driver = webdriver.Remote(command_executor=server.http_endpoint, options=webdriver.ChromeOptions()) + driver.get("https://example.com") + print(driver.find_element(By.CSS_SELECTOR, "h1").text) driver.quit() ``` -[`http_endpoint`](#bidiserver-http-endpoint) is Selenium's ``command_executor``. The browser -serves the BiDi modules (``session``, ``browser``, ``browsingContext``, -``script``, ``input``) over the WebSocket plus the classic session -bootstrap (``GET /status``, ``POST /session`` with the ``webSocketUrl`` -capability, ``DELETE /session/``); other classic WebDriver commands -such as Selenium's ``driver.get`` or ``find_element`` are not served, so -drive the page through ``driver.browsing_context`` and ``driver.script`` -with an explicit context, created first as above. Pass -``args=["--protocol", "cdp"]`` to serve CDP on the same port as well -(``--protocol`` is additive). The process is stopped by [`close()`](#bidiserver-close) / -leaving the ``with`` block, and on Linux also when the interpreter dies. +[`http_endpoint`](#webdriverserver-http-endpoint) is Selenium's ``command_executor``. The browser +serves classic WebDriver over HTTP (navigation, element lookup and +interaction, script execution, actions, cookies) and, for a session +created with the ``webSocketUrl`` capability (Selenium's +``options.web_socket_url = True``), the BiDi modules (``session``, +``browser``, ``browsingContext``, ``script``, ``input``) over the +WebSocket, reached through ``driver.browsing_context`` and +``driver.script``. Pass ``args=["--protocol", "cdp"]`` to serve CDP on the +same port as well (``--protocol`` is additive). The process is stopped by +[`close()`](#webdriverserver-close) / leaving the ``with`` block, and on Linux also when the +interpreter dies. ```python -class BiDiServer( +class WebDriverServer( binary: str | os.PathLike | None = None, env: dict[str, str] | None = None, verbose: bool = False, @@ -1646,7 +1643,7 @@ Spawn the server process. Usable as a context manager (`with`). -### BiDiServer.bidi_endpoint [#bidiserver-bidi-endpoint] +### WebDriverServer.bidi_endpoint [#webdriverserver-bidi-endpoint] ```python bidi_endpoint: str @@ -1661,7 +1658,7 @@ capability. Keep the IP literal: the WebSocket upgrade rejects any ``Origin`` header and only accepts an IP-literal or ``localhost`` host. -### BiDiServer.status [#bidiserver-status] +### WebDriverServer.status [#webdriverserver-status] ```python def status(self) -> dict @@ -1669,7 +1666,7 @@ def status(self) -> dict The ``GET /status`` value, ``{"ready": True, "message": ""}``. -### BiDiServer.port [#bidiserver-port] +### WebDriverServer.port [#webdriverserver-port] ```python port: int @@ -1677,7 +1674,7 @@ port: int *Property.* The port the server listens on. -### BiDiServer.http_endpoint [#bidiserver-http-endpoint] +### WebDriverServer.http_endpoint [#webdriverserver-http-endpoint] ```python http_endpoint: str @@ -1687,7 +1684,7 @@ http_endpoint: str (``browserURL``) and Playwright (``connect_over_cdp`` with an http URL) discover the CDP WebSocket from, and Selenium's ``command_executor``. -### BiDiServer.close [#bidiserver-close] +### WebDriverServer.close [#webdriverserver-close] ```python def close(self) -> None @@ -1695,15 +1692,15 @@ def close(self) -> None Stop the server process. Idempotent. -## AsyncBiDiServer [#asyncbidiserver] +## AsyncWebDriverServer [#asyncwebdriverserver] -[`BiDiServer`](#bidiserver) for asyncio: the process is spawned by -[`start()`](#asyncbidiserver-start), called automatically on ``async with`` entry. +[`WebDriverServer`](#webdriverserver) for asyncio: the process is spawned by +[`start()`](#asyncwebdriverserver-start), called automatically on ``async with`` entry. -Every public method and property of [`BiDiServer`](#bidiserver) exists on `AsyncBiDiServer` with the same name and signature; methods are coroutines to `await`. Only the members `AsyncBiDiServer` adds are listed below. +Every public method and property of [`WebDriverServer`](#webdriverserver) exists on `AsyncWebDriverServer` with the same name and signature; methods are coroutines to `await`. Only the members `AsyncWebDriverServer` adds are listed below. ```python -class AsyncBiDiServer( +class AsyncWebDriverServer( binary: str | os.PathLike | None = None, env: dict[str, str] | None = None, verbose: bool = False, @@ -1712,13 +1709,13 @@ class AsyncBiDiServer( ) ``` -Store the settings; the process is spawned by [`start()`](#asyncbidiserver-start). +Store the settings; the process is spawned by [`start()`](#asyncwebdriverserver-start). Arguments are forwarded to the sync class. Usable as a context manager (`async with`). -### AsyncBiDiServer.start [#asyncbidiserver-start] +### AsyncWebDriverServer.start [#asyncwebdriverserver-start] ```python async def start(self)