From d41ad5fc3aca240652bf8a7a7134d9435c92bc00 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Fri, 21 Aug 2026 19:08:40 -0500 Subject: [PATCH 1/2] [py] raise typed WebDriver errors for BiDi from a generated error-code map --- .gitignore | 1 + py/BUILD.bazel | 8 +- py/generate_bidi_protocol.py | 24 +++++- py/private/generate_bidi_protocol.bzl | 6 +- py/selenium/webdriver/common/_bidi/errors.py | 84 +++++++++++++++++++ .../webdriver/common/_bidi/transport.py | 14 +++- .../webdriver/common/bidi_transport_tests.py | 41 ++++++++- 7 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 py/selenium/webdriver/common/_bidi/errors.py diff --git a/.gitignore b/.gitignore index 27c43c074baba..c6e6698cb2599 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ py/selenium/webdriver/common/_bidi/* !py/selenium/webdriver/common/_bidi/serialization.py !py/selenium/webdriver/common/_bidi/transport.py !py/selenium/webdriver/common/_bidi/domain.py +!py/selenium/webdriver/common/_bidi/errors.py py/selenium/webdriver/common/devtools/**/* !py/selenium/webdriver/common/devtools/util.py py/selenium/webdriver/common/linux/ diff --git a/py/BUILD.bazel b/py/BUILD.bazel index 158dc912d61ac..3d26e5f4c9100 100644 --- a/py/BUILD.bazel +++ b/py/BUILD.bazel @@ -813,12 +813,18 @@ py_library( srcs = [":create-bidi-protocol-src"] + [ "selenium/webdriver/common/_bidi/__init__.py", "selenium/webdriver/common/_bidi/domain.py", + "selenium/webdriver/common/_bidi/errors.py", "selenium/webdriver/common/_bidi/serialization.py", "selenium/webdriver/common/_bidi/transport.py", ], imports = ["."], visibility = ["//visibility:public"], - deps = [":exceptions"], + # :remote for errorhandler's error-code tables, which errors.py reconciles against + # rather than keeping a second copy of them. + deps = [ + ":exceptions", + ":remote", + ], ) py_test_suite( diff --git a/py/generate_bidi_protocol.py b/py/generate_bidi_protocol.py index 7579891da0d02..dffd32ce0153c 100644 --- a/py/generate_bidi_protocol.py +++ b/py/generate_bidi_protocol.py @@ -1049,15 +1049,35 @@ def render_module(mod: ModuleIR) -> str: return "\n\n\n".join(blocks) + "\n" +def exception_class_name(code: str) -> str: + """The Python exception class name for a BiDi wire error code.""" + return "".join(word.capitalize() for word in re.split(r"[^a-zA-Z0-9]+", code) if word) + "Exception" + + +def render_error_codes(schema: Schema) -> str: + """Render the wire-code to exception-name map. + + Deliberately pure data: no selenium imports, so the mapping to real exception + classes (which has to reconcile with the classic ones) stays hand-written glue. + """ + codes = schema.types["ErrorCode"]["values"] + docstring = '"""Wire error codes from the BiDi ``ErrorCode`` enum, mapped to exception class names."""' + entries = [f" {lit(code)}: {lit(exception_class_name(code))}," for code in codes] + table = "\n".join(["EXCEPTION_NAMES = {", *entries, "}"]) + return "\n\n\n".join([_HEADER, docstring, table]) + "\n" + + def render_all(schema_path: str) -> dict[str, str]: - """Render every domain module; returns {filename: contents}. + """Render every domain module plus the error-code map; returns {filename: contents}. The package ``__init__.py`` is hand-written (it carries only the package docstring), so it is intentionally not emitted here. """ schema = Schema(json.loads(Path(schema_path).read_text(encoding="utf-8"))) modules = [build_module(schema, domain) for domain in schema.domains()] - return {f"{mod.filename}.py": render_module(mod) for mod in modules} + rendered = {f"{mod.filename}.py": render_module(mod) for mod in modules} + rendered["error_codes.py"] = render_error_codes(schema) + return rendered def main() -> None: diff --git a/py/private/generate_bidi_protocol.bzl b/py/private/generate_bidi_protocol.bzl index cebe2a51ffad7..5e1c0f52db026 100644 --- a/py/private/generate_bidi_protocol.bzl +++ b/py/private/generate_bidi_protocol.bzl @@ -5,9 +5,11 @@ already-projected, binding-neutral schema JSON and emits the generated domain mo The hand-written runtime (``serialization``/``transport``/``domain``) is not produced here. """ -# Generated domain modules, snake_case (one per BiDi domain in the schema). The -# package ``__init__.py`` is hand-written and checked in, so it is not generated here. +# Generated domain modules, snake_case (one per BiDi domain in the schema), plus the +# domain-less error-code map. The package ``__init__.py`` is hand-written and checked +# in, so it is not generated here. _MODULES = [ + "error_codes", "bluetooth", "browser", "browsing_context", diff --git a/py/selenium/webdriver/common/_bidi/errors.py b/py/selenium/webdriver/common/_bidi/errors.py new file mode 100644 index 0000000000000..94037742f282a --- /dev/null +++ b/py/selenium/webdriver/common/_bidi/errors.py @@ -0,0 +1,84 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Exception classes for BiDi wire error codes. + +Hand-written (not generated). The generated ``error_codes`` map is pure data; the +reconciliation with Selenium's classic exceptions has to happen here, because it needs +the real classes. + +Codes the classic WebDriver error handler already types keep that class, so +``except NoSuchElementException`` catches a BiDi failure and a classic one alike, even +where the classic name does not follow from the wire code (``"no such alert"`` is +``NoAlertPresentException``, ``"unable to capture screen"`` is ``ScreenshotException``). +The rest are minted here as ``WebDriverException`` subclasses. + +This is internal, unsupported implementation. See +https://www.selenium.dev/documentation/warnings/bidi-implementation/ +""" + +from __future__ import annotations + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common._bidi.error_codes import EXCEPTION_NAMES +from selenium.webdriver.remote.errorhandler import ErrorCode, ExceptionMapping + + +def _classic_exception(code: str) -> type[WebDriverException] | None: + """The classic exception for a wire code, or None where the code is untyped. + + Resolved through the error handler's own tables rather than a second copy of them, + so a class the handler retypes later follows here without an edit. A code the handler + resolves to bare ``WebDriverException`` counts as untyped: a minted subclass is + strictly more specific and still caught by anyone catching the base. + """ + for name in dir(ErrorCode): + codes = getattr(ErrorCode, name) + if not isinstance(codes, list) or code not in codes: + continue + classic = getattr(ExceptionMapping, name, None) + if classic is not None and classic is not WebDriverException: + return classic + return None + + +def _mint(name: str) -> type[WebDriverException]: + return type(name, (WebDriverException,), {"__module__": __name__, "__doc__": f"Raised for the BiDi {name}."}) + + +_EXCEPTIONS: dict[str, type[WebDriverException]] = { + code: _classic_exception(code) or _mint(name) for code, name in EXCEPTION_NAMES.items() +} + +_BY_NAME: dict[str, type[WebDriverException]] = {exc.__name__: exc for exc in _EXCEPTIONS.values()} + + +def __getattr__(name: str) -> type[WebDriverException]: + """Expose each exception by class name, so a minted one can be imported and caught.""" + try: + return _BY_NAME[name] + except KeyError: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None + + +def exception_for(code: str | None) -> type[WebDriverException]: + """The exception class for a wire error code, falling back for an unrecognized one. + + An error the remote end reports must surface as that error even when the code is one + this schema does not declare, so an unknown code is never a serialization failure. + """ + return _EXCEPTIONS.get(code, WebDriverException) if code else WebDriverException diff --git a/py/selenium/webdriver/common/_bidi/transport.py b/py/selenium/webdriver/common/_bidi/transport.py index 24dc1b596837a..34c0bfa595e04 100644 --- a/py/selenium/webdriver/common/_bidi/transport.py +++ b/py/selenium/webdriver/common/_bidi/transport.py @@ -29,7 +29,7 @@ from typing import Any -from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common._bidi.errors import exception_for class Transport: @@ -52,7 +52,15 @@ def connection(self) -> Any: def execute(self, cmd: str, params: Any = None, result: Any = None) -> Any: reply = self._connection.send_cmd(cmd, params.as_json() if params is not None else {}) if "error" in reply: - message = reply.get("message") - raise WebDriverException(f"{reply['error']}: {message}" if message else reply["error"]) + raise self._error(reply) value = reply["result"] return result.from_json(value) if result is not None else value + + @staticmethod + def _error(reply: dict) -> Exception: + code = reply["error"] + # The class carries the code, so the message need not repeat it — except where the + # remote sent no message, which would otherwise leave nothing to read. + message = reply.get("message") or code + stacktrace = reply.get("stacktrace") + return exception_for(code)(message, stacktrace=stacktrace.split("\n") if stacktrace else None) diff --git a/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py b/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py index a742d551b41d9..1e1234a4d9c37 100644 --- a/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py +++ b/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py @@ -28,7 +28,8 @@ import pytest -from selenium.common.exceptions import WebDriverException +from selenium.common.exceptions import NoSuchFrameException, WebDriverException +from selenium.webdriver.common._bidi import errors from selenium.webdriver.common._bidi.domain import Domain from selenium.webdriver.common._bidi.serialization import Record, meta, register from selenium.webdriver.common._bidi.transport import Transport @@ -51,13 +52,14 @@ class DrivingConnection: Records the outbound frame and returns a canned reply envelope, so the seam is tested against the real send/reply contract rather than a mock of it. Set - ``error`` (and optionally ``message``) to return an error envelope instead. + ``error`` (and optionally ``message``/``stacktrace``) to return an error envelope instead. """ - def __init__(self, reply=None, error=None, message=None): + def __init__(self, reply=None, error=None, message=None, stacktrace=None): self.reply = reply self.error = error self.message = message + self.stacktrace = stacktrace self.sent = None def send_cmd(self, method, params): @@ -66,6 +68,8 @@ def send_cmd(self, method, params): envelope = {"error": self.error} if self.message is not None: envelope["message"] = self.message + if self.stacktrace is not None: + envelope["stacktrace"] = self.stacktrace return envelope return {"result": self.reply} @@ -117,7 +121,7 @@ def test_execute_with_no_result_type_returns_the_raw_reply(): def test_execute_raises_when_the_reply_carries_an_error(): connection = DrivingConnection(error="unknown command", message="no such command") - with pytest.raises(WebDriverException, match=r"unknown command: no such command"): + with pytest.raises(WebDriverException, match=r"no such command"): Transport(connection).execute("bad.command", params=Params(context="c"), result=Result) @@ -129,6 +133,35 @@ def test_execute_error_without_a_message_raises_with_just_the_error(): assert exc_info.value.msg == "unknown command" +def test_a_shared_error_code_raises_the_classic_exception(): + connection = DrivingConnection(error="no such frame", message="it is gone") + + with pytest.raises(NoSuchFrameException, match=r"it is gone"): + Transport(connection).execute("bad.command", result=Result) + + +def test_a_bidi_only_error_code_raises_a_bidi_specific_exception(): + connection = DrivingConnection(error="no such user context", message="nope") + + with pytest.raises(errors.NoSuchUserContextException, match=r"nope"): + Transport(connection).execute("bad.command", result=Result) + + +def test_an_error_code_this_schema_does_not_declare_still_raises(): + connection = DrivingConnection(error="brand new code", message="from a newer browser") + + with pytest.raises(WebDriverException, match=r"from a newer browser"): + Transport(connection).execute("bad.command", result=Result) + + +def test_a_wire_stacktrace_is_carried_onto_the_exception(): + connection = DrivingConnection(error="unknown error", message="boom", stacktrace="a\nb") + + with pytest.raises(WebDriverException) as exc_info: + Transport(connection).execute("bad.command", result=Result) + assert exc_info.value.stacktrace == ["a", "b"] + + def test_execute_treats_the_presence_of_error_as_an_error_not_its_truthiness(): connection = DrivingConnection(error="") From baae3556a238408835cc530216ec8a6ed066fd97 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 25 Aug 2026 14:08:07 -0500 Subject: [PATCH 2/2] [py] generate BiDi error classes instead of minting them at import --- .gitignore | 1 - py/BUILD.bazel | 12 ++- py/generate_bidi_protocol.py | 83 +++++++++++++++--- py/private/generate_bidi_protocol.bzl | 4 +- py/selenium/webdriver/common/_bidi/errors.py | 84 ------------------- .../webdriver/common/bidi_transport_tests.py | 4 +- 6 files changed, 81 insertions(+), 107 deletions(-) delete mode 100644 py/selenium/webdriver/common/_bidi/errors.py diff --git a/.gitignore b/.gitignore index c6e6698cb2599..27c43c074baba 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,6 @@ py/selenium/webdriver/common/_bidi/* !py/selenium/webdriver/common/_bidi/serialization.py !py/selenium/webdriver/common/_bidi/transport.py !py/selenium/webdriver/common/_bidi/domain.py -!py/selenium/webdriver/common/_bidi/errors.py py/selenium/webdriver/common/devtools/**/* !py/selenium/webdriver/common/devtools/util.py py/selenium/webdriver/common/linux/ diff --git a/py/BUILD.bazel b/py/BUILD.bazel index 3d26e5f4c9100..7427cc55f5edf 100644 --- a/py/BUILD.bazel +++ b/py/BUILD.bazel @@ -789,11 +789,14 @@ generate_bidi_protocol( # Build-graph tool: the generator invoked by the rule above (schema + output dir # passed as arguments). No baked args, so it is reusable in the exec configuration. +# Depends on :remote because rendering reads errorhandler's error-code tables to decide +# which BiDi codes reuse a classic exception rather than declaring a new one. py_binary( name = "generate-bidi-protocol-tool", srcs = ["generate_bidi_protocol.py"], main = "generate_bidi_protocol.py", visibility = ["//visibility:private"], + deps = [":remote"], ) # `bazel run //py:generate-bidi-protocol` regenerates the checked-out tree for local @@ -805,6 +808,7 @@ py_binary( data = ["//javascript/selenium-webdriver:create-bidi-src_schema"], main = "generate_bidi_protocol.py", visibility = ["//visibility:public"], + deps = [":remote"], ) py_library( @@ -813,18 +817,12 @@ py_library( srcs = [":create-bidi-protocol-src"] + [ "selenium/webdriver/common/_bidi/__init__.py", "selenium/webdriver/common/_bidi/domain.py", - "selenium/webdriver/common/_bidi/errors.py", "selenium/webdriver/common/_bidi/serialization.py", "selenium/webdriver/common/_bidi/transport.py", ], imports = ["."], visibility = ["//visibility:public"], - # :remote for errorhandler's error-code tables, which errors.py reconciles against - # rather than keeping a second copy of them. - deps = [ - ":exceptions", - ":remote", - ], + deps = [":exceptions"], ) py_test_suite( diff --git a/py/generate_bidi_protocol.py b/py/generate_bidi_protocol.py index dffd32ce0153c..9fe0cd4d7e9ab 100644 --- a/py/generate_bidi_protocol.py +++ b/py/generate_bidi_protocol.py @@ -1054,21 +1054,82 @@ def exception_class_name(code: str) -> str: return "".join(word.capitalize() for word in re.split(r"[^a-zA-Z0-9]+", code) if word) + "Exception" -def render_error_codes(schema: Schema) -> str: - """Render the wire-code to exception-name map. +_ERRORS_DOCSTRING = '''"""Exception classes for BiDi wire error codes. - Deliberately pure data: no selenium imports, so the mapping to real exception - classes (which has to reconcile with the classic ones) stays hand-written glue. +Codes the classic WebDriver error handler already types keep that class, so +``except NoSuchElementException`` catches a BiDi failure and a classic one alike, even +where the classic name does not follow from the wire code (``"no such alert"`` is +``NoAlertPresentException``, ``"unable to capture screen"`` is ``ScreenshotException``). +The rest are declared here as ``WebDriverException`` subclasses. + +This is internal, unsupported implementation. See +https://www.selenium.dev/documentation/warnings/bidi-implementation/ +"""''' + + +def _classic_exceptions() -> dict[str, type]: + """Wire code to the classic exception the error handler already raises for it. + + Read from the handler's own tables rather than a second copy of them, so a class it + retypes later follows here on the next build. A code the handler resolves to bare + ``WebDriverException`` counts as untyped: a subclass declared in the generated module + is strictly more specific and still caught by anyone catching the base. + """ + from selenium.common.exceptions import WebDriverException + from selenium.webdriver.remote.errorhandler import ErrorCode, ExceptionMapping + + classic: dict[str, type] = {} + for name in dir(ErrorCode): + codes = getattr(ErrorCode, name) + mapped = getattr(ExceptionMapping, name, None) + if not isinstance(codes, list) or mapped is None or mapped is WebDriverException: + continue + for code in codes: + if isinstance(code, str): + classic.setdefault(code, mapped) + return classic + + +def render_errors(schema: Schema) -> str: + """Render the exception classes for the BiDi ``ErrorCode`` enum. + + Emitted as real class statements rather than synthesized at import, so the classes + type-check, autocomplete and document like every other exception in the bindings. """ - codes = schema.types["ErrorCode"]["values"] - docstring = '"""Wire error codes from the BiDi ``ErrorCode`` enum, mapped to exception class names."""' - entries = [f" {lit(code)}: {lit(exception_class_name(code))}," for code in codes] - table = "\n".join(["EXCEPTION_NAMES = {", *entries, "}"]) - return "\n\n\n".join([_HEADER, docstring, table]) + "\n" + classic = _classic_exceptions() + resolved = {code: classic.get(code) for code in schema.types["ErrorCode"]["values"]} + + imported = sorted({exc.__name__ for exc in resolved.values() if exc is not None} | {"WebDriverException"}) + import_block = "\n".join(["from selenium.common.exceptions import (", *(f" {n}," for n in imported), ")"]) + + declarations = [ + f'class {exception_class_name(code)}(WebDriverException):\n """Raised for the BiDi {lit(code)} error."""' + for code, exc in resolved.items() + if exc is None + ] + + entries = [ + f" {lit(code)}: {exc.__name__ if exc is not None else exception_class_name(code)}," + for code, exc in resolved.items() + ] + table = "\n".join(["EXCEPTIONS: dict[str, type[WebDriverException]] = {", *entries, "}"]) + + lookup = '''def exception_for(code: str | None) -> type[WebDriverException]: + """The exception class for a wire error code, falling back for an unrecognized one. + + An error the remote end reports must surface as that error even when the code is one + this schema does not declare, so an unknown code is never a serialization failure. + """ + return EXCEPTIONS.get(code, WebDriverException) if code else WebDriverException''' + + blocks = [_HEADER, _ERRORS_DOCSTRING, "from __future__ import annotations", import_block] + blocks += declarations + blocks += [table, lookup] + return "\n\n\n".join(blocks) + "\n" def render_all(schema_path: str) -> dict[str, str]: - """Render every domain module plus the error-code map; returns {filename: contents}. + """Render every domain module plus the error classes; returns {filename: contents}. The package ``__init__.py`` is hand-written (it carries only the package docstring), so it is intentionally not emitted here. @@ -1076,7 +1137,7 @@ def render_all(schema_path: str) -> dict[str, str]: schema = Schema(json.loads(Path(schema_path).read_text(encoding="utf-8"))) modules = [build_module(schema, domain) for domain in schema.domains()] rendered = {f"{mod.filename}.py": render_module(mod) for mod in modules} - rendered["error_codes.py"] = render_error_codes(schema) + rendered["errors.py"] = render_errors(schema) return rendered diff --git a/py/private/generate_bidi_protocol.bzl b/py/private/generate_bidi_protocol.bzl index 5e1c0f52db026..c3fc6e898f28a 100644 --- a/py/private/generate_bidi_protocol.bzl +++ b/py/private/generate_bidi_protocol.bzl @@ -6,10 +6,10 @@ The hand-written runtime (``serialization``/``transport``/``domain``) is not pro """ # Generated domain modules, snake_case (one per BiDi domain in the schema), plus the -# domain-less error-code map. The package ``__init__.py`` is hand-written and checked +# domain-less error classes. The package ``__init__.py`` is hand-written and checked # in, so it is not generated here. _MODULES = [ - "error_codes", + "errors", "bluetooth", "browser", "browsing_context", diff --git a/py/selenium/webdriver/common/_bidi/errors.py b/py/selenium/webdriver/common/_bidi/errors.py deleted file mode 100644 index 94037742f282a..0000000000000 --- a/py/selenium/webdriver/common/_bidi/errors.py +++ /dev/null @@ -1,84 +0,0 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -"""Exception classes for BiDi wire error codes. - -Hand-written (not generated). The generated ``error_codes`` map is pure data; the -reconciliation with Selenium's classic exceptions has to happen here, because it needs -the real classes. - -Codes the classic WebDriver error handler already types keep that class, so -``except NoSuchElementException`` catches a BiDi failure and a classic one alike, even -where the classic name does not follow from the wire code (``"no such alert"`` is -``NoAlertPresentException``, ``"unable to capture screen"`` is ``ScreenshotException``). -The rest are minted here as ``WebDriverException`` subclasses. - -This is internal, unsupported implementation. See -https://www.selenium.dev/documentation/warnings/bidi-implementation/ -""" - -from __future__ import annotations - -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.common._bidi.error_codes import EXCEPTION_NAMES -from selenium.webdriver.remote.errorhandler import ErrorCode, ExceptionMapping - - -def _classic_exception(code: str) -> type[WebDriverException] | None: - """The classic exception for a wire code, or None where the code is untyped. - - Resolved through the error handler's own tables rather than a second copy of them, - so a class the handler retypes later follows here without an edit. A code the handler - resolves to bare ``WebDriverException`` counts as untyped: a minted subclass is - strictly more specific and still caught by anyone catching the base. - """ - for name in dir(ErrorCode): - codes = getattr(ErrorCode, name) - if not isinstance(codes, list) or code not in codes: - continue - classic = getattr(ExceptionMapping, name, None) - if classic is not None and classic is not WebDriverException: - return classic - return None - - -def _mint(name: str) -> type[WebDriverException]: - return type(name, (WebDriverException,), {"__module__": __name__, "__doc__": f"Raised for the BiDi {name}."}) - - -_EXCEPTIONS: dict[str, type[WebDriverException]] = { - code: _classic_exception(code) or _mint(name) for code, name in EXCEPTION_NAMES.items() -} - -_BY_NAME: dict[str, type[WebDriverException]] = {exc.__name__: exc for exc in _EXCEPTIONS.values()} - - -def __getattr__(name: str) -> type[WebDriverException]: - """Expose each exception by class name, so a minted one can be imported and caught.""" - try: - return _BY_NAME[name] - except KeyError: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None - - -def exception_for(code: str | None) -> type[WebDriverException]: - """The exception class for a wire error code, falling back for an unrecognized one. - - An error the remote end reports must surface as that error even when the code is one - this schema does not declare, so an unknown code is never a serialization failure. - """ - return _EXCEPTIONS.get(code, WebDriverException) if code else WebDriverException diff --git a/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py b/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py index 1e1234a4d9c37..770997210a153 100644 --- a/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py +++ b/py/test/unit/selenium/webdriver/common/bidi_transport_tests.py @@ -29,8 +29,8 @@ import pytest from selenium.common.exceptions import NoSuchFrameException, WebDriverException -from selenium.webdriver.common._bidi import errors from selenium.webdriver.common._bidi.domain import Domain +from selenium.webdriver.common._bidi.errors import NoSuchUserContextException from selenium.webdriver.common._bidi.serialization import Record, meta, register from selenium.webdriver.common._bidi.transport import Transport @@ -143,7 +143,7 @@ def test_a_shared_error_code_raises_the_classic_exception(): def test_a_bidi_only_error_code_raises_a_bidi_specific_exception(): connection = DrivingConnection(error="no such user context", message="nope") - with pytest.raises(errors.NoSuchUserContextException, match=r"nope"): + with pytest.raises(NoSuchUserContextException, match=r"nope"): Transport(connection).execute("bad.command", result=Result)