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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ own free port.
Set `options.web_socket_url = True` to also get a WebDriver BiDi session and
use `driver.browsing_context` / `driver.script`. `server.bidi_endpoint`
(`ws://127.0.0.1:<port>/session`) is the raw BiDi WebSocket for clients that
speak the protocol directly. `WebDriverServer` is an alias of `BiDiServer`,
`AsyncWebDriverServer` (alias of `AsyncBiDiServer`) is the asyncio twin, and
speak the protocol directly. `AsyncWebDriverServer` is the asyncio twin, and
`args=["--protocol", "cdp"]` serves CDP on the same port as well.

## Respecting robots.txt
Expand All @@ -127,7 +126,7 @@ through `args=`:

```python
Browser(args=["--obey-robots"]) # also AsyncBrowser
CDPServer(args=["--obey-robots"]) # also BiDiServer, and the async twins
CDPServer(args=["--obey-robots"]) # also WebDriverServer, and the async twins
run_script("saved.js", args=["--obey-robots"])
```

Expand Down
10 changes: 4 additions & 6 deletions lightpanda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@

For Playwright or Puppeteer code, ``CDPServer`` runs the browser's own
Chrome DevTools Protocol server and hands you the endpoint to connect to
(see its docs for an example). For Selenium, ``BiDiServer`` (alias
``WebDriverServer``) serves WebDriver, classic and BiDi, the same way and
hands you the ``command_executor`` URL.
(see its docs for an example). For Selenium, ``WebDriverServer`` serves
WebDriver, classic and BiDi, the same way and hands you the
``command_executor`` URL.

The browser can enforce ``robots.txt``, which is off by default. Pass
``args=["--obey-robots"]`` to any of these and a disallowed request fails
Expand All @@ -37,7 +37,7 @@
__docformat__ = "google"

from .async_browser import AsyncBrowser, AsyncSession, run_script_async
from .bidi import AsyncBiDiServer, AsyncWebDriverServer, BiDiServer, WebDriverServer
from .webdriver import AsyncWebDriverServer, WebDriverServer
from .browser import Browser, PageResult, Session, run_script
from .cdp import AsyncCDPServer, CDPServer
from .errors import LightpandaError, ProcessError, ProtocolError, ScriptError, ToolError
Expand All @@ -52,8 +52,6 @@
"run_script_async",
"CDPServer",
"AsyncCDPServer",
"BiDiServer",
"AsyncBiDiServer",
"WebDriverServer",
"AsyncWebDriverServer",
"LightpandaError",
Expand Down
2 changes: 1 addition & 1 deletion lightpanda/_serve.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Shared shape of the ``lightpanda serve`` wrappers (``CDPServer``, ``BiDiServer``).
"""Shared shape of the ``lightpanda serve`` wrappers (``CDPServer``, ``WebDriverServer``).

A serve wrapper owns one ``lightpanda serve`` process on a localhost port
and hands out endpoints for third-party clients; the subclasses differ only
Expand Down
33 changes: 12 additions & 21 deletions lightpanda/bidi.py → lightpanda/webdriver.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""WebDriver: run ``lightpanda serve --protocol webdriver`` for Selenium and co.

:class:`BiDiServer` spawns the browser's WebDriver server (classic and BiDi)
on a free localhost port and owns the process; :class:`AsyncBiDiServer` is the
asyncio twin. ``WebDriverServer`` / ``AsyncWebDriverServer`` are aliases. See
:class:`BiDiServer` for what the browser serves.
:class:`WebDriverServer` spawns the browser's WebDriver server (classic and BiDi)
on a free localhost port and owns the process; :class:`AsyncWebDriverServer` is the
asyncio twin. See :class:`WebDriverServer` for what the browser serves.
"""

from __future__ import annotations
Expand All @@ -15,17 +14,15 @@


@_documented
class BiDiServer(_ServeProcess):
class WebDriverServer(_ServeProcess):
"""A lightpanda process serving WebDriver, classic and BiDi, on 127.0.0.1.

Also importable as ``WebDriverServer``.

```python
from lightpanda import BiDiServer
from lightpanda import WebDriverServer
from selenium import webdriver
from selenium.webdriver.common.by import By

with BiDiServer() as server:
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")
Expand Down Expand Up @@ -66,26 +63,20 @@ def status(self) -> dict:


@_documented
class AsyncBiDiServer(_AsyncServeProcess[BiDiServer]):
""":class:`BiDiServer` for asyncio: the process is spawned by
class AsyncWebDriverServer(_AsyncServeProcess[WebDriverServer]):
""":class:`WebDriverServer` for asyncio: the process is spawned by
:meth:`start`, called automatically on ``async with`` entry."""

_sync_cls = BiDiServer
_sync_cls = WebDriverServer

@property
def bidi_endpoint(self) -> str:
"""See :attr:`BiDiServer.bidi_endpoint`."""
"""See :attr:`WebDriverServer.bidi_endpoint`."""
return self._started().bidi_endpoint

async def status(self) -> dict:
"""See :meth:`BiDiServer.status`."""
"""See :meth:`WebDriverServer.status`."""
return await asyncio.to_thread(self._started().status)


WebDriverServer = BiDiServer
"""Alias of :class:`BiDiServer`: the same server speaks classic WebDriver too."""

AsyncWebDriverServer = AsyncBiDiServer
"""Alias of :class:`AsyncBiDiServer`."""

__all__ = ["BiDiServer", "AsyncBiDiServer", "WebDriverServer", "AsyncWebDriverServer"]
__all__ = ["WebDriverServer", "AsyncWebDriverServer"]
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest

from lightpanda import AsyncBrowser, BiDiServer, Browser, CDPServer
from lightpanda import AsyncBrowser, Browser, CDPServer, WebDriverServer

BROWSER_CHECKOUT = Path(__file__).parent.parent.parent / "browser"
FIXTURES = Path(__file__).parent / "fixtures"
Expand Down Expand Up @@ -62,6 +62,6 @@ def cdp_server(binary):


@pytest.fixture(scope="session")
def bidi_server(binary):
with BiDiServer(binary=binary) as server:
def webdriver_server(binary):
with WebDriverServer(binary=binary) as server:
yield server
6 changes: 3 additions & 3 deletions tests/test_serve.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Lifecycle shared by the `lightpanda serve` wrappers, tested once per
class pair. Protocol-specific behaviour lives in test_cdp.py / test_bidi.py."""
class pair. Protocol-specific behaviour lives in test_cdp.py / test_webdriver.py."""

import pytest
from conftest import alive

from lightpanda import AsyncBiDiServer, AsyncCDPServer, BiDiServer, CDPServer, LightpandaError
from lightpanda import AsyncCDPServer, AsyncWebDriverServer, CDPServer, LightpandaError, WebDriverServer

PAIRS = [
pytest.param(CDPServer, AsyncCDPServer, "version", "ws_endpoint", id="cdp"),
pytest.param(BiDiServer, AsyncBiDiServer, "status", "bidi_endpoint", id="bidi"),
pytest.param(WebDriverServer, AsyncWebDriverServer, "status", "bidi_endpoint", id="bidi"),
]


Expand Down
28 changes: 14 additions & 14 deletions tests/test_bidi.py → tests/test_webdriver.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""BiDiServer's endpoints and the WebDriver session bootstrap they promise,
"""WebDriverServer's endpoints and the WebDriver session bootstrap they promise,
with the standard library only. A real BiDi client is exercised in
test_bidi_selenium.py; the shared process lifecycle in test_serve.py."""
test_webdriver_selenium.py; the shared process lifecycle in test_serve.py."""

import json
import urllib.error
import urllib.request

from lightpanda import BiDiServer
from lightpanda import WebDriverServer

BIDI_CAPS = {"capabilities": {"alwaysMatch": {"webSocketUrl": True}}}

Expand All @@ -29,26 +29,26 @@ def _with_session(server):
return value, lambda: _request("DELETE", f"{server.http_endpoint}/session/{value['sessionId']}")


def test_endpoints_and_status(bidi_server):
assert bidi_server.port > 0
assert bidi_server.bidi_endpoint == f"ws://127.0.0.1:{bidi_server.port}/session"
assert bidi_server.http_endpoint == f"http://127.0.0.1:{bidi_server.port}"
assert bidi_server.status() == {"ready": True, "message": ""}
def test_endpoints_and_status(webdriver_server):
assert webdriver_server.port > 0
assert webdriver_server.bidi_endpoint == f"ws://127.0.0.1:{webdriver_server.port}/session"
assert webdriver_server.http_endpoint == f"http://127.0.0.1:{webdriver_server.port}"
assert webdriver_server.status() == {"ready": True, "message": ""}


def test_session_bootstrap_advertises_bidi_endpoint(bidi_server):
value, done = _with_session(bidi_server)
def test_session_bootstrap_advertises_bidi_endpoint(webdriver_server):
value, done = _with_session(webdriver_server)
try:
assert value["capabilities"]["browserName"] == "Lightpanda"
assert value["capabilities"]["webSocketUrl"] == f"{bidi_server.bidi_endpoint}/{value['sessionId']}"
assert value["capabilities"]["webSocketUrl"] == f"{webdriver_server.bidi_endpoint}/{value['sessionId']}"
finally:
assert done() == (200, {"value": None})


def test_extra_args_passthrough(bidi_server, binary):
assert _request("GET", f"{bidi_server.http_endpoint}/json/version")[0] == 404 # CDP off by default
def test_extra_args_passthrough(webdriver_server, binary):
assert _request("GET", f"{webdriver_server.http_endpoint}/json/version")[0] == 404 # CDP off by default
args = ["--protocol", "cdp", "--advertise-host", "localhost"]
with BiDiServer(binary=binary, args=args) as server:
with WebDriverServer(binary=binary, args=args) as server:
status, version = _request("GET", f"{server.http_endpoint}/json/version") # --protocol is additive
assert status == 200 and version["Browser"].startswith("Lightpanda")
value, done = _with_session(server)
Expand Down
18 changes: 6 additions & 12 deletions tests/test_bidi_selenium.py → tests/test_webdriver_selenium.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Drive a BiDiServer with Selenium, over classic WebDriver and over BiDi.
"""Drive a WebDriverServer with Selenium, over classic WebDriver and over BiDi.
Selenium is a dev-only dependency: `webdriver.Remote` needs its pip package,
not a driver or browser download, and these tests skip when it is not
installed."""
Expand All @@ -9,15 +9,9 @@
from selenium.webdriver.common.by import By # noqa: E402
from selenium.webdriver.common.options import ArgOptions # noqa: E402

from lightpanda import BiDiServer, WebDriverServer # noqa: E402


def test_webdriver_server_alias():
assert WebDriverServer is BiDiServer


def test_selenium_classic_session(bidi_server, fixture_url):
driver = webdriver.Remote(command_executor=bidi_server.http_endpoint, options=webdriver.ChromeOptions())
def test_selenium_classic_session(webdriver_server, fixture_url):
driver = webdriver.Remote(command_executor=webdriver_server.http_endpoint, options=webdriver.ChromeOptions())
try:
driver.get(f"{fixture_url}/index.html")
assert driver.title == "Fixture Home"
Expand All @@ -36,13 +30,13 @@ def test_selenium_classic_session(bidi_server, fixture_url):
driver.quit() # DELETE /session/<id>


def test_selenium_bidi_session(bidi_server, fixture_url):
def test_selenium_bidi_session(webdriver_server, fixture_url):
options = ArgOptions()
options.web_socket_url = True
driver = webdriver.Remote(command_executor=bidi_server.http_endpoint, options=options)
driver = webdriver.Remote(command_executor=webdriver_server.http_endpoint, options=options)
try:
assert driver.caps["browserName"] == "Lightpanda"
assert driver.caps["webSocketUrl"] == f"{bidi_server.bidi_endpoint}/{driver.session_id}"
assert driver.caps["webSocketUrl"] == f"{webdriver_server.bidi_endpoint}/{driver.session_id}"

assert driver.browsing_context.get_tree() == [] # first BiDi access opens the websocket
context = driver.browsing_context.create(type="tab")
Expand Down
Loading