Skip to content
Open
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
27 changes: 25 additions & 2 deletions packages/sdk-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,30 @@ exclude = ["src/stagehand/_generated"]
preview = true

[tool.ruff.lint]
select = ["ANN", "E", "F", "I", "PYI", "UP"]
select = [
"ANN",
"B006",
"B015",
"B018",
"B904",
"C4",
"E",
"F",
"I",
"PGH",
"PYI",
"RUF018",
"RUF022",
"RUF023",
"RUF039",
"RUF043",
"RUF100",
"T10",
"TID252",
"UP",
"W",
]
external = ["S"]

[tool.ty.environment]
python-version = "3.11"
Expand All @@ -89,6 +112,6 @@ dev = [
"openai>=2,<3",
"pytest==9.0.3",
"pytest-asyncio==1.3.0",
"ruff==0.15.7",
"ruff==0.16.8",
"ty==0.0.82",
]
14 changes: 7 additions & 7 deletions packages/sdk-python/src/stagehand/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,17 @@
"BrowserbaseRegion",
"BrowserbaseSearchResult",
"BrowserbaseSearchResultItem",
"CacheOptions",
"CDPSubscription",
"CacheMetadata",
"CacheOptions",
"CacheStatus",
"CacheTokenSavings",
"Caret",
"CDPSubscription",
"Cookie",
"CookieParam",
"DefaultExtract",
"DomainPolicy",
"DomainPolicyInput",
"DefaultExtract",
"ExternalProxyConfig",
"ExtractResult",
"FileInput",
Expand All @@ -146,15 +146,12 @@
"PageCDPEvent",
"PageDragAndDropRoutePoint",
"PageEventListener",
"ToolsAddedListener",
"ToolsRemovedListener",
"WebMCPToolIdentity",
"PageEventName",
"PagePDFMargin",
"PageScreenshotClip",
"ProtocolLocator",
"RgbaColor",
"Response",
"RgbaColor",
"Scale",
"ScreenshotType",
"Stagehand",
Expand All @@ -165,11 +162,14 @@
"StagehandRuntimeIncompatibleError",
"State",
"TelemetryConfig",
"ToolsAddedListener",
"ToolsRemovedListener",
"Variables",
"WebMCPAnnotation",
"WebMCPInvocation",
"WebMCPInvocationStatus",
"WebMCPTool",
"WebMCPToolIdentity",
"WebMCPToolResponse",
"browserbase",
"local_browser",
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk-python/src/stagehand/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ class StagehandBrowser:
"_attachment",
"_claimed",
"_close_callback",
"_invalidate_callback",
"_terminal_task",
"_context",
"_invalidate_callback",
"_origin",
"_provider",
"_session_id",
"_terminal_task",
)

def __init__(
Expand Down Expand Up @@ -340,7 +340,7 @@ async def _connect_browser(
raise
for cleanup_error in cleanup_errors:
if not isinstance(cleanup_error, Exception):
raise cleanup_error
raise cleanup_error from error
if cleanup_errors:
raise BaseExceptionGroup(
"Browser connection failed and browser cleanup also failed",
Expand Down
8 changes: 4 additions & 4 deletions packages/sdk-python/tests/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,13 +853,13 @@ async def test_browserbase_validation_precedes_api_calls(
_, api_keys = _install_browserbase_client(monkeypatch)
with pytest.raises(ValueError, match="api_key"):
await browserbase.launch(api_key="")
with pytest.raises(ValueError, match="^base_url must not be empty$"):
with pytest.raises(ValueError, match=r"^base_url must not be empty$"):
await browserbase.launch(api_key="api-key", base_url=" ")
with pytest.raises(ValueError, match="^extension_id must not be empty$"):
with pytest.raises(ValueError, match=r"^extension_id must not be empty$"):
await browserbase.launch(api_key="api-key", extension_id=" ")
with pytest.raises(
ValueError,
match="^browser_settings.extension_id must not be empty$",
match=r"^browser_settings.extension_id must not be empty$",
):
await browserbase.launch(
api_key="api-key",
Expand Down Expand Up @@ -1060,7 +1060,7 @@ def create_profile(**_kwargs: object) -> str:

monkeypatch.setattr(browser.tempfile, "mkdtemp", create_profile)

with pytest.raises(RuntimeError, match="Chrome executable.*does not exist"):
with pytest.raises(RuntimeError, match=r"Chrome executable.*does not exist"):
await _launch_local_browser(LocalBrowserLaunchOptions(executable_path="/missing/chrome"))


Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-python/tests/test_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def test_browser_context_serializes_python_cookie_filters() -> None:
context = BrowserContext(cast(RPCClient, recording), _close_browser)

await context.clear_cookies(
name=re.compile("^session$", re.IGNORECASE),
name=re.compile(r"^session$", re.IGNORECASE),
domain="example.com",
)

Expand Down
8 changes: 4 additions & 4 deletions packages/sdk-python/tests/test_browserbase_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ async def test_create_failure_deletes_owned_extension_best_effort(
fake_api.delete_errors = [OSError("cleanup failed")]
with pytest.raises(
BrowserbaseSessionError,
match="^Failed to create a Browserbase session$",
match=r"^Failed to create a Browserbase session$",
) as raised:
await _BrowserbaseSessionClient(fake_api).create_session(BrowserbaseSessionCreateParams())
assert raised.value.__cause__ is None
Expand Down Expand Up @@ -363,20 +363,20 @@ async def test_connect_validates_sanitizes_and_normalizes(
fake_api: FakeBrowserbaseAPI,
) -> None:
client = _BrowserbaseSessionClient(fake_api)
with pytest.raises(BrowserbaseSessionError, match="^A Browserbase session ID is required$"):
with pytest.raises(BrowserbaseSessionError, match=r"^A Browserbase session ID is required$"):
await client.connect_session(" ")
fake_api.retrieve_error = OSError("secret")
with pytest.raises(
BrowserbaseSessionError,
match="^Failed to retrieve the Browserbase session$",
match=r"^Failed to retrieve the Browserbase session$",
) as raised:
await client.connect_session(" session ")
assert raised.value.__cause__ is None
fake_api.retrieve_error = None
fake_api.retrieve_result = (" ", None, None)
with pytest.raises(
BrowserbaseSessionError,
match="^Browserbase session is not available for connection$",
match=r"^Browserbase session is not available for connection$",
):
await client.connect_session(" session ")

Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-python/tests/test_cdp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ async def connect(_: str) -> FakeWebSocket:
monkeypatch.setattr(cdp_client, "_resolve_browser_web_socket_url", resolve)
monkeypatch.setattr(cdp_client, "_connect_web_socket", connect)

with pytest.raises(RuntimeError, match="does not support Extensions.loadUnpacked"):
with pytest.raises(RuntimeError, match=r"does not support Extensions.loadUnpacked"):
await CDPClient.connect(
cdp_url="http://127.0.0.1:9222",
extension_dir="/tmp/stagehand-extension",
Expand Down Expand Up @@ -622,7 +622,7 @@ async def test_installed_extension_discovery_propagates_cdp_command_errors() ->
socket = FakeWebSocket(lambda _: {"error": {"code": -32601, "message": "Method not available"}})
client = CDPClient(socket, "ws://127.0.0.1/devtools/browser/test")
try:
with pytest.raises(RuntimeError, match="Extensions.getExtensions: Method not available"):
with pytest.raises(RuntimeError, match=r"Extensions.getExtensions: Method not available"):
await client._resolve_extension(load_if_not_found=False)
finally:
await client.close()
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-python/tests/test_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ async def test_page_pdf_preserves_omitted_and_empty_options(
async def test_page_on_rejects_unsupported_events_before_subscribing(event: str) -> None:
recording = RecordingRPCClient({})
page = Page(cast(RPCClient, recording), PageRef(page_id="page-1"))
with pytest.raises(ValueError, match='page.on only supports "console" events'):
with pytest.raises(ValueError, match=r'page.on only supports "console" events'):
await page.on(cast(PageEventName, event), lambda _: None)
assert recording.calls == []
assert recording.notifications == {}
Expand Down Expand Up @@ -618,7 +618,7 @@ async def test_unsubscribe_retries_after_page_off_failure() -> None:
page = Page(cast(RPCClient, recording), PageRef(page_id="page-1"))
subscription = await page.on("console", lambda _: None)

with pytest.raises(RuntimeError, match="temporary page.off failure"):
with pytest.raises(RuntimeError, match=r"temporary page.off failure"):
await subscription.unsubscribe()
assert "page.cdp_event" in recording.notifications

Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-python/tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ async def test_response_surfaces_finished_failures_transport_errors_and_malforme
finished_error = await response.finished()
assert isinstance(finished_error, RuntimeError)
assert str(finished_error) == "net::ERR_FAILED"
with pytest.raises(ValueError, match="response.body returned invalid base64"):
with pytest.raises(ValueError, match=r"response.body returned invalid base64"):
await response.body()
with pytest.raises(RuntimeError, match="handle unavailable"):
await response.all_headers()
Expand All @@ -139,7 +139,7 @@ async def test_response_rejects_noncanonical_base64_pad_bits() -> None:
recording = RecordingRPCClient({"response.body": {"body": "Zh==", "base64_encoded": True}})
response = Response(cast(RPCClient, recording), descriptor())

with pytest.raises(ValueError, match="response.body returned invalid base64"):
with pytest.raises(ValueError, match=r"response.body returned invalid base64"):
await response.body()


Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-python/tests/test_rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ async def test_locator_timeout_error_keeps_remote_message_and_details() -> None:
},
})
with pytest.raises(
RPCError, match="locator.count timed out after 20000ms while resolving frame"
RPCError, match=r"locator.count timed out after 20000ms while resolving frame"
) as raised:
await call
assert raised.value.data == {"name": "TimeoutError"}
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk-python/tests/test_stagehand.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def build_rpc_client(


def test_stagehand_constructor_is_private() -> None:
with pytest.raises(TypeError, match="Stagehand.create"):
with pytest.raises(TypeError, match=r"Stagehand.create"):
Stagehand()


Expand Down Expand Up @@ -692,9 +692,9 @@ async def test_close_is_memoized_and_never_closes_browser_or_transport(
assert recording.close_transport_flags == [False]
assert browser.closed is False
assert transport.close_calls == 0
with pytest.raises(RuntimeError, match="Browser context is unavailable.*Stagehand.create"):
with pytest.raises(RuntimeError, match=r"Browser context is unavailable.*Stagehand.create"):
_ = stagehand.browser.context
with pytest.raises(RuntimeError, match="Stagehand is unavailable.*Stagehand.create"):
with pytest.raises(RuntimeError, match=r"Stagehand is unavailable.*Stagehand.create"):
await stagehand.metrics()

next_recording = _recording()
Expand Down
Loading
Loading