Path: blob/trunk/py/private/bidi_enhancements_manifest.py
11809 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.161718"""19Enhancement manifest for BiDi code generation.2021This file defines custom enhancements applied to generated BiDi modules,22including custom dataclass methods, parameter validation/transformation,23response deserialization, and field extraction.2425All code must be compatible with Python 3.10+.26"""2728from __future__ import annotations2930from typing import Any3132# ============================================================================33# Format Guide34# ============================================================================35# Each module in ENHANCEMENTS specifies enhancement rules for methods:36#37# 'module_name': {38# 'method_name': {39# 'dataclass_methods': { # For dataclass enhancements40# 'ClassName': ['method1', 'method2', ...]41# },42# 'preprocess': { # Pre-processing on parameters43# 'param_name': 'check_serialize_method'44# },45# 'deserialize': { # Deserialize response to typed objects46# 'response_field': 'TypeName',47# },48# 'extract_field': str, # Extract nested field from response49# 'extract_property': str, # Extract property from extracted items50# 'validate': str, # Validation function name51# 'transform': str, # Transformation function name52# }53# }54# ============================================================================5556ENHANCEMENTS: dict[str, dict[str, Any]] = {57"browser": {58# Dataclass custom methods59"__dataclass_methods__": {60"ClientWindowInfo": [61"get_client_window",62"get_state",63"get_width",64"get_height",65"is_active",66"get_x",67"get_y",68],69},70# Method enhancements71"create_user_context": {72"preprocess": {73"proxy": "check_serialize_method",74"unhandled_prompt_behavior": "check_serialize_method",75},76"extract_field": "userContext",77},78"get_client_windows": {79"deserialize": {80"clientWindows": "ClientWindowInfo",81},82},83"get_user_contexts": {84"extract_field": "userContexts",85"extract_property": "userContext",86},87"set_download_behavior": {88"params_override": {89"allowed": "bool",90"destination_folder": "str",91"userContexts": "[*browser.UserContext]",92},93"validate": "validate_download_behavior",94"transform": {95"allowed": "allowed",96"destination_folder": "destination_folder",97"func": "transform_download_params",98"result_param": "download_behavior",99},100},101# Replace the auto-generated ClientWindowNamedState so we can add the102# convenience NORMAL constant. In the BiDi spec "normal" is the state103# represented by ClientWindowRectState, but exposing it here keeps the104# Python API consistent with the old ClientWindowState enum.105"exclude_types": ["ClientWindowNamedState", "SetClientWindowStateParameters"],106"extra_dataclasses": [107'''class ClientWindowNamedState:108"""Named states for a browser client window."""109110FULLSCREEN = "fullscreen"111MAXIMIZED = "maximized"112MINIMIZED = "minimized"113NORMAL = "normal"''',114'''@dataclass115class SetClientWindowStateParameters:116"""SetClientWindowStateParameters.117118The ``state`` field is required and must be either a named-state string119(e.g. ``ClientWindowNamedState.MAXIMIZED``) or a120:class:`ClientWindowRectState` instance. ``client_window`` is the ID of121the window to affect.122"""123124client_window: Any | None = None125state: Any | None = None''',126],127# Override the generator-produced set_download_behavior so that128# downloadBehavior is never stripped by the generic None filter.129# The BiDi spec marks it as required (can be null, but must be present).130"extra_methods": [131''' def set_download_behavior(132self,133allowed: bool | None = None,134destination_folder: str | None = None,135user_contexts: list[Any] | None = None,136):137"""Set the download behavior for the browser.138139Args:140allowed: ``True`` to allow downloads, ``False`` to deny, or ``None``141to reset to browser default (sends ``null`` to the protocol).142destination_folder: Destination folder for downloads. Required when143``allowed=True``. Accepts a string or :class:`pathlib.Path`.144user_contexts: Optional list of user context IDs.145146Raises:147ValueError: If *allowed* is ``True`` and *destination_folder* is148omitted, or ``False`` and *destination_folder* is provided.149"""150validate_download_behavior(151allowed=allowed,152destination_folder=destination_folder,153user_contexts=user_contexts,154)155download_behavior = transform_download_params(allowed, destination_folder)156# downloadBehavior is a REQUIRED field in the BiDi spec (can be null but157# must be present). Do NOT use a generic None-filter on it.158params: dict = {"downloadBehavior": download_behavior}159if user_contexts is not None:160params["userContexts"] = user_contexts161cmd = command_builder("browser.setDownloadBehavior", params)162return self._conn.execute(cmd)''',163''' def set_client_window_state(164self,165client_window: Any | None = None,166state: Any | None = None,167):168"""Set the client window state.169170Args:171client_window: The client window ID to apply the state to.172state: The window state to set. Can be one of:173- A string: "fullscreen", "maximized", "minimized", "normal"174- A ClientWindowRectState object with width, height, x, y175- A dict representing the state176177Raises:178ValueError: If client_window is not provided or state is invalid.179"""180if client_window is None:181raise ValueError("client_window is required")182if state is None:183raise ValueError("state is required")184185# Serialize ClientWindowRectState if needed186state_param = state187if hasattr(state, '__dataclass_fields__'):188# It's a dataclass, convert to dict189state_param = {190k: v for k, v in state.__dict__.items()191if v is not None192}193194params = {195"clientWindow": client_window,196"state": state_param,197}198cmd = command_builder("browser.setClientWindowState", params)199return self._conn.execute(cmd)''',200],201},202"browsingContext": {203# Method enhancements204"exclude_methods": ["set_viewport"],205"create": {206"extract_field": "context",207},208"get_tree": {209"extract_field": "contexts",210"deserialize": {211"contexts": "Info",212},213},214"capture_screenshot": {215"extract_field": "data",216"params_override": {217"context": "str",218"format": "ImageFormat",219"clip": "BoxClipRectangle",220"origin": "str",221},222},223"print": {224"extract_field": "data",225},226"locate_nodes": {227"extract_field": "nodes",228"params_override": {229"context": "str",230"locator": "dict",231"serializationOptions": "dict",232"startNodes": "list",233"maxNodeCount": "int",234},235},236"set_viewport": {237"params_override": {238"context": "str",239"viewport": "dict",240"userContexts": "list",241"devicePixelRatio": "float",242},243},244"extra_methods": [245''' def set_viewport(246self,247context: str | None = None,248viewport: Any = ...,249user_contexts: Any | None = None,250device_pixel_ratio: Any = ...,251):252"""Execute browsingContext.setViewport.253254Uses sentinel defaults so explicit None is serialized for viewport/devicePixelRatio,255while omitted arguments are not sent.256"""257params = {}258if context is not None:259params["context"] = context260if user_contexts is not None:261params["userContexts"] = user_contexts262if viewport is not ...:263params["viewport"] = viewport264if device_pixel_ratio is not ...:265params["devicePixelRatio"] = device_pixel_ratio266267cmd = command_builder("browsingContext.setViewport", params)268result = self._conn.execute(cmd)269return result''',270],271# Non-CDDL download event dataclasses (Chromium-specific)272"extra_dataclasses": [273'''@dataclass274class DownloadWillBeginParams:275"""DownloadWillBeginParams."""276277suggested_filename: str | None = None''',278'''@dataclass279class DownloadCanceledParams:280"""DownloadCanceledParams."""281282status: Any | None = None''',283'''@dataclass284class DownloadParams:285"""DownloadParams - fields shared by all download end event variants."""286287status: str | None = None288context: Any | None = None289navigation: Any | None = None290timestamp: Any | None = None291url: str | None = None292filepath: str | None = None''',293'''@dataclass294class DownloadEndParams:295"""DownloadEndParams - params for browsingContext.downloadEnd event."""296297download_params: DownloadParams | None = None298299@classmethod300def from_json(cls, params: dict) -> DownloadEndParams:301"""Deserialize from BiDi wire-level params dict."""302dp = DownloadParams(303status=params.get("status"),304context=params.get("context"),305navigation=params.get("navigation"),306timestamp=params.get("timestamp"),307url=params.get("url"),308filepath=params.get("filepath"),309)310return cls(download_params=dp)''',311],312# Download events are now in the CDDL spec, so no extra_events needed313},314"log": {315# Make LogLevel an alias for Level so existing code using LogLevel works316"aliases": {"LogLevel": "Level"},317# Replace the minimal CDDL-generated versions with richer ones that have from_json318"exclude_types": ["JavascriptLogEntry"],319"extra_dataclasses": [320'''@dataclass321class ConsoleLogEntry:322"""ConsoleLogEntry - a console log entry from the browser."""323324type_: str | None = None325method: str | None = None326args: list | None = None327level: Any | None = None328text: Any | None = None329source: Any | None = None330timestamp: Any | None = None331stack_trace: Any | None = None332333@classmethod334def from_json(cls, params: dict) -> ConsoleLogEntry:335"""Deserialize from BiDi params dict."""336return cls(337type_=params.get("type"),338method=params.get("method"),339args=params.get("args"),340level=params.get("level"),341text=params.get("text"),342source=params.get("source"),343timestamp=params.get("timestamp"),344stack_trace=params.get("stackTrace"),345)''',346'''@dataclass347class JavascriptLogEntry:348"""JavascriptLogEntry - a JavaScript error log entry from the browser."""349350type_: str | None = None351level: Any | None = None352text: Any | None = None353source: Any | None = None354timestamp: Any | None = None355stacktrace: Any | None = None356357@classmethod358def from_json(cls, params: dict) -> JavascriptLogEntry:359"""Deserialize from BiDi params dict."""360return cls(361type_=params.get("type"),362level=params.get("level"),363text=params.get("text"),364source=params.get("source"),365timestamp=params.get("timestamp"),366stacktrace=params.get("stackTrace"),367)''',368],369# Define Entry union type for log.entryAdded event deserialization370"extra_type_aliases": [371"Entry = GenericLogEntry | ConsoleLogEntry | JavascriptLogEntry",372],373"event_type_aliases": {374"entry_added": "Entry",375},376},377"emulation": {378"exclude_types": ["setNetworkConditionsParameters"],379"extra_dataclasses": [380'''@dataclass381class SetNetworkConditionsParameters:382"""SetNetworkConditionsParameters."""383384network_conditions: Any | None = None385contexts: list[Any] = field(default_factory=list)386user_contexts: list[Any] = field(default_factory=list)387388389# Backward-compatible alias for existing imports390setNetworkConditionsParameters = SetNetworkConditionsParameters''',391],392"extra_methods": [393''' def set_geolocation_override(394self,395coordinates=None,396error=None,397contexts: list[Any] | None = None,398user_contexts: list[Any] | None = None,399):400"""Execute emulation.setGeolocationOverride.401402Sets or clears the geolocation override for specified browsing or user contexts.403404Args:405coordinates: A GeolocationCoordinates instance (or dict) to override the406position, or ``None`` to clear a previously-set override.407error: A GeolocationPositionError instance (or dict) to simulate a408position-unavailable error. Mutually exclusive with *coordinates*.409contexts: List of browsing context IDs to target.410user_contexts: List of user context IDs to target.411"""412params: dict[str, Any] = {}413if coordinates is not None:414if isinstance(coordinates, dict):415coords_dict = coordinates416else:417coords_dict = {}418if coordinates.latitude is not None:419coords_dict["latitude"] = coordinates.latitude420if coordinates.longitude is not None:421coords_dict["longitude"] = coordinates.longitude422if coordinates.accuracy is not None:423coords_dict["accuracy"] = coordinates.accuracy424if coordinates.altitude is not None:425coords_dict["altitude"] = coordinates.altitude426if coordinates.altitude_accuracy is not None:427coords_dict["altitudeAccuracy"] = coordinates.altitude_accuracy428if coordinates.heading is not None:429coords_dict["heading"] = coordinates.heading430if coordinates.speed is not None:431coords_dict["speed"] = coordinates.speed432params["coordinates"] = coords_dict433if error is not None:434if isinstance(error, dict):435params["error"] = error436else:437params["error"] = {438"type": error.type if error.type is not None else "positionUnavailable"439}440if contexts is not None:441params["contexts"] = contexts442if user_contexts is not None:443params["userContexts"] = user_contexts444cmd = command_builder("emulation.setGeolocationOverride", params)445result = self._conn.execute(cmd)446return result''',447''' def set_timezone_override(448self,449timezone=None,450contexts: list[Any] | None = None,451user_contexts: list[Any] | None = None,452):453"""Execute emulation.setTimezoneOverride.454455Sets or clears the timezone override for specified browsing or user contexts.456Pass ``timezone=None`` (or omit it) to clear a previously-set override.457458Args:459timezone: IANA timezone string (e.g. ``"America/New_York"``) or ``None``460to clear the override.461contexts: List of browsing context IDs to target.462user_contexts: List of user context IDs to target.463"""464params: dict[str, Any] = {"timezone": timezone}465if contexts is not None:466params["contexts"] = contexts467if user_contexts is not None:468params["userContexts"] = user_contexts469cmd = command_builder("emulation.setTimezoneOverride", params)470return self._conn.execute(cmd)''',471''' def set_scripting_enabled(472self,473enabled=None,474contexts: list[Any] | None = None,475user_contexts: list[Any] | None = None,476):477"""Execute emulation.setScriptingEnabled.478479Enables or disables scripting for specified browsing or user contexts.480Pass ``enabled=None`` to restore the default behaviour.481482Args:483enabled: ``True`` to enable scripting, ``False`` to disable it, or484``None`` to clear the override.485contexts: List of browsing context IDs to target.486user_contexts: List of user context IDs to target.487"""488params: dict[str, Any] = {"enabled": enabled}489if contexts is not None:490params["contexts"] = contexts491if user_contexts is not None:492params["userContexts"] = user_contexts493cmd = command_builder("emulation.setScriptingEnabled", params)494return self._conn.execute(cmd)''',495''' def set_user_agent_override(496self,497user_agent=None,498contexts: list[Any] | None = None,499user_contexts: list[Any] | None = None,500):501"""Execute emulation.setUserAgentOverride.502503Overrides the User-Agent string for specified browsing or user contexts.504Pass ``user_agent=None`` to clear a previously-set override.505506Args:507user_agent: Custom User-Agent string, or ``None`` to clear the override.508contexts: List of browsing context IDs to target.509user_contexts: List of user context IDs to target.510"""511params: dict[str, Any] = {"userAgent": user_agent}512if contexts is not None:513params["contexts"] = contexts514if user_contexts is not None:515params["userContexts"] = user_contexts516cmd = command_builder("emulation.setUserAgentOverride", params)517return self._conn.execute(cmd)''',518''' def set_screen_orientation_override(519self,520screen_orientation=None,521contexts: list[Any] | None = None,522user_contexts: list[Any] | None = None,523):524"""Execute emulation.setScreenOrientationOverride.525526Sets or clears the screen orientation override for specified browsing or527user contexts.528529Args:530screen_orientation: A :class:`ScreenOrientation` instance (or dict with531``natural`` and ``type`` keys) to lock the orientation, or ``None``532to clear a previously-set override.533contexts: List of browsing context IDs to target.534user_contexts: List of user context IDs to target.535"""536if screen_orientation is None:537so_value = None538elif isinstance(screen_orientation, dict):539so_value = screen_orientation540else:541natural = screen_orientation.natural542orientation_type = screen_orientation.type543so_value = {544"natural": natural.lower() if isinstance(natural, str) else natural,545"type": orientation_type.lower() if isinstance(orientation_type, str) else orientation_type,546}547params: dict[str, Any] = {"screenOrientation": so_value}548if contexts is not None:549params["contexts"] = contexts550if user_contexts is not None:551params["userContexts"] = user_contexts552cmd = command_builder("emulation.setScreenOrientationOverride", params)553return self._conn.execute(cmd)''',554''' def set_network_conditions(555self,556network_conditions=None,557offline: bool | None = None,558contexts: list[Any] | None = None,559user_contexts: list[Any] | None = None,560):561"""Execute emulation.setNetworkConditions.562563Sets or clears network condition emulation for specified browsing or user564contexts.565566Args:567network_conditions: A dict with the raw ``networkConditions`` value568(e.g. ``{"type": "offline"}``), or ``None`` to clear the override.569Mutually exclusive with *offline*.570offline: Convenience bool — ``True`` sets offline conditions,571``False`` clears them (sends ``null``). When provided, this takes572precedence over *network_conditions*.573contexts: List of browsing context IDs to target.574user_contexts: List of user context IDs to target.575"""576if offline is not None:577nc_value = {"type": "offline"} if offline else None578else:579nc_value = network_conditions580params: dict[str, Any] = {"networkConditions": nc_value}581if contexts is not None:582params["contexts"] = contexts583if user_contexts is not None:584params["userContexts"] = user_contexts585cmd = command_builder("emulation.setNetworkConditions", params)586return self._conn.execute(cmd)''',587''' def set_screen_settings_override(588self,589width: int | None = None,590height: int | None = None,591contexts: list[Any] | None = None,592user_contexts: list[Any] | None = None,593):594"""Execute emulation.setScreenSettingsOverride.595596Sets or clears the screen settings override for specified browsing or user597contexts.598599Args:600width: The screen width in pixels, or ``None`` to clear the override.601height: The screen height in pixels, or ``None`` to clear the override.602contexts: List of browsing context IDs to target.603user_contexts: List of user context IDs to target.604"""605screen_area = None606if width is not None or height is not None:607screen_area = {}608if width is not None:609screen_area["width"] = width610if height is not None:611screen_area["height"] = height612params: dict[str, Any] = {"screenArea": screen_area}613if contexts is not None:614params["contexts"] = contexts615if user_contexts is not None:616params["userContexts"] = user_contexts617cmd = command_builder("emulation.setScreenSettingsOverride", params)618return self._conn.execute(cmd)''',619],620},621"script": {622"extra_init_code": [623"self._log_handlers = LogHandlerRegistry(self)",624"self._dom_mutation_handlers = DomMutationRegistry(self)",625],626"extra_dataclasses": [627# The handler registries and payload classes live in the static628# helper module _script_handlers.py (staged via create-bidi-src629# extra_srcs) so the implementation is lintable and unit-testable630# as real code. The import also re-exports the payload classes631# to keep selenium.webdriver.common.bidi.script.<name> importable632# (DomMutation in particular predates the helper module).633"""from selenium.webdriver.common.bidi._script_handlers import (634ConsoleMessage,635DomMutation,636DomMutationRegistry,637LogHandlerRegistry,638PinnedScript,639ScriptError,640ScriptResult,641execute_pinned,642)""",643],644"extra_methods": [645''' def execute(646self, function_declaration: str | PinnedScript, *args, context_id: str | None = None647) -> Any:648"""Execute a function declaration in the browser context.649650Args:651function_declaration: The function as a string, e.g. ``"() => document.title"``,652or a ``PinnedScript`` returned by ``pin()``.653*args: Optional Python values to pass as arguments to the function.654Each value is serialised to a BiDi ``LocalValue`` automatically.655Supported types: ``None``, ``bool``, ``int``, ``float``656(including ``NaN`` and ``Infinity``), ``str``, ``list``,657``dict``, and ``datetime.datetime``.658When a ``PinnedScript`` is given, the single argument is the659code to execute with the pinned source in scope, e.g.660``script.execute(pinned, "return helper();")``.661context_id: The browsing context ID to run in. Defaults to the662driver\'s current window handle when a driver was provided.663664Returns:665The inner RemoteValue result dict, or raises WebDriverException on666exception. When a ``PinnedScript`` is given, returns a667``ScriptResult`` instead and does not raise: failures are reported668through ``ScriptResult.error``.669"""670if isinstance(function_declaration, PinnedScript):671code = args[0] if args else ""672return execute_pinned(self, function_declaration, code, context_id=context_id)673import math as _math674import datetime as _datetime675from selenium.common.exceptions import WebDriverException as _WebDriverException676677def _serialize_arg(value):678"""Serialise a Python value to a BiDi LocalValue dict."""679if value is None:680return {"type": "null"}681if isinstance(value, bool):682return {"type": "boolean", "value": value}683if isinstance(value, _datetime.datetime):684return {"type": "date", "value": value.isoformat()}685if isinstance(value, float):686if _math.isnan(value):687return {"type": "number", "value": "NaN"}688if _math.isinf(value):689return {"type": "number", "value": "Infinity" if value > 0 else "-Infinity"}690return {"type": "number", "value": value}691if isinstance(value, int):692_MAX_SAFE_INT = 9007199254740991693if abs(value) > _MAX_SAFE_INT:694return {"type": "bigint", "value": str(value)}695return {"type": "number", "value": value}696if isinstance(value, str):697return {"type": "string", "value": value}698if isinstance(value, list):699return {"type": "array", "value": [_serialize_arg(v) for v in value]}700if isinstance(value, dict):701return {"type": "object", "value": [[str(k), _serialize_arg(v)] for k, v in value.items()]}702return value703704if context_id is None and self._driver is not None:705try:706context_id = self._driver.current_window_handle707except Exception:708pass709target = {"context": context_id} if context_id else {}710serialized_args = [_serialize_arg(a) for a in args] if args else None711raw = self.call_function(712function_declaration=function_declaration,713await_promise=True,714target=target,715arguments=serialized_args,716)717if isinstance(raw, dict):718if raw.get("type") == "exception":719exc = raw.get("exceptionDetails", {})720msg = exc.get("text", str(exc)) if isinstance(exc, dict) else str(exc)721raise _WebDriverException(msg)722if raw.get("type") == "success":723return raw.get("result")724return raw''',725''' def _add_preload_script(726self,727function_declaration,728arguments=None,729contexts=None,730user_contexts=None,731sandbox=None,732):733"""Add a preload script with validation.734735Args:736function_declaration: The JS function to run on page load.737arguments: Optional list of BiDi arguments.738contexts: Optional list of browsing context IDs.739user_contexts: Optional list of user context IDs.740sandbox: Optional sandbox name.741742Returns:743script_id: The ID of the added preload script (str).744745Raises:746ValueError: If both contexts and user_contexts are specified.747"""748if contexts is not None and user_contexts is not None:749raise ValueError("Cannot specify both contexts and user_contexts")750result = self.add_preload_script(751function_declaration=function_declaration,752arguments=arguments,753contexts=contexts,754user_contexts=user_contexts,755sandbox=sandbox,756)757if isinstance(result, dict):758return result.get("script")759return result''',760''' def _remove_preload_script(self, script_id):761"""Remove a preload script by ID.762763Args:764script_id: The ID of the preload script to remove.765"""766return self.remove_preload_script(script=script_id)''',767''' def pin(self, function_declaration) -> PinnedScript:768"""Pin (add) a preload script that runs on every page load.769770Args:771function_declaration: The JS function to execute on page load.772773Returns:774A ``PinnedScript`` carrying the pinned source. It subclasses775``str`` (the script ID), so it can be used anywhere a script ID776string is expected, and can be passed to ``execute()`` to run777code with the pinned source in scope.778"""779script_id = self._add_preload_script(function_declaration)780return PinnedScript(script_id, source=function_declaration)''',781''' def unpin(self, script_id):782"""Unpin (remove) a previously pinned preload script.783784Args:785script_id: The ID returned by pin().786"""787return self._remove_preload_script(script_id=script_id)''',788''' def _evaluate(789self,790expression,791target,792await_promise,793result_ownership=None,794serialization_options=None,795user_activation=None,796):797"""Evaluate a script expression and return a structured result.798799Args:800expression: The JavaScript expression to evaluate.801target: A dict like {"context": <id>} or {"realm": <id>}.802await_promise: Whether to await a returned promise.803result_ownership: Optional result ownership setting.804serialization_options: Optional serialization options dict.805user_activation: Optional user activation flag.806807Returns:808An object with .realm, .result (dict or None), and .exception_details (or None).809"""810class _EvalResult:811def __init__(self2, realm, result, exception_details):812self2.realm = realm813self2.result = result814self2.exception_details = exception_details815816raw = self.evaluate(817expression=expression,818target=target,819await_promise=await_promise,820result_ownership=result_ownership,821serialization_options=serialization_options,822user_activation=user_activation,823)824if isinstance(raw, dict):825realm = raw.get("realm")826if raw.get("type") == "exception":827exc = raw.get("exceptionDetails")828return _EvalResult(realm=realm, result=None, exception_details=exc)829return _EvalResult(realm=realm, result=raw.get("result"), exception_details=None)830return _EvalResult(realm=None, result=raw, exception_details=None)''',831''' def _call_function(832self,833function_declaration,834await_promise,835target,836arguments=None,837result_ownership=None,838this=None,839user_activation=None,840serialization_options=None,841):842"""Call a function and return a structured result.843844Args:845function_declaration: The JS function string.846await_promise: Whether to await the return value.847target: A dict like {"context": <id>}.848arguments: Optional list of BiDi arguments.849result_ownership: Optional result ownership.850this: Optional \'this\' binding.851user_activation: Optional user activation flag.852serialization_options: Optional serialization options dict.853854Returns:855An object with .result (dict or None) and .exception_details (or None).856"""857class _CallResult:858def __init__(self2, result, exception_details):859self2.result = result860self2.exception_details = exception_details861862raw = self.call_function(863function_declaration=function_declaration,864await_promise=await_promise,865target=target,866arguments=arguments,867result_ownership=result_ownership,868this=this,869user_activation=user_activation,870serialization_options=serialization_options,871)872if isinstance(raw, dict):873if raw.get("type") == "exception":874exc = raw.get("exceptionDetails")875return _CallResult(result=None, exception_details=exc)876if raw.get("type") == "success":877return _CallResult(result=raw.get("result"), exception_details=None)878return _CallResult(result=raw, exception_details=None)''',879''' def _get_realms(self, context=None, type=None):880"""Get all realms, optionally filtered by context and type.881882Args:883context: Optional browsing context ID to filter by.884type: Optional realm type string to filter by (e.g. RealmType.WINDOW).885886Returns:887List of realm info objects with .realm, .origin, .type, .context attributes.888"""889class _RealmInfo:890def __init__(self2, realm, origin, type_, context):891self2.realm = realm892self2.origin = origin893self2.type = type_894self2.context = context895896raw = self.get_realms(context=context, type=type)897realms_list = raw.get("realms", []) if isinstance(raw, dict) else []898result = []899for r in realms_list:900if isinstance(r, dict):901result.append(_RealmInfo(902realm=r.get("realm"),903origin=r.get("origin"),904type_=r.get("type"),905context=r.get("context"),906))907return result''',908''' def _disown(self, handles, target):909"""Disown handles in a browsing context.910911Args:912handles: List of handle strings to disown.913target: A dict like {"context": <id>}.914"""915return self.disown(handles=handles, target=target)''',916''' def add_console_message_handler(self, callback: Callable) -> int:917"""Add a handler for console log messages (log.entryAdded type=console).918919The callback receives the generated ``ConsoleLogEntry`` dataclass.920For payloads carrying source URL and line/column numbers, see921``add_console_handler``.922923Args:924callback: Function called with a ConsoleLogEntry on each console message.925926Returns:927callback_id for use with remove_console_message_handler.928"""929return self._log_handlers.add_handler(callback, category="console", legacy=True)''',930''' def remove_console_message_handler(self, callback_id: int) -> None:931"""Remove a console message handler by callback ID."""932self._log_handlers.remove_handler(callback_id)''',933''' def add_console_handler(self, callback: Callable) -> int:934"""Add a handler for console messages (log.entryAdded type=console).935936Args:937callback: Function called with a ``ConsoleMessage`` carrying938level, text, source URL, line/column numbers and stack trace.939940Returns:941callback_id for use with remove_console_handler.942"""943return self._log_handlers.add_handler(callback, category="console")''',944''' def remove_console_handler(self, callback_id: int) -> None:945"""Remove a console handler by callback ID."""946self._log_handlers.remove_handler(callback_id)''',947''' def clear_console_handlers(self) -> None:948"""Remove all console handlers.949950Clears handlers registered through both ``add_console_handler``951and ``add_console_message_handler``.952"""953self._log_handlers.clear_handlers("console")''',954''' def add_javascript_error_handler(self, callback: Callable) -> int:955"""Add a handler for JavaScript error log messages (log.entryAdded type=javascript).956957The callback receives the generated ``JavascriptLogEntry`` dataclass.958For payloads carrying source URL and line/column numbers, see959``add_error_handler``.960961Args:962callback: Function called with a JavascriptLogEntry on each JS error.963964Returns:965callback_id for use with remove_javascript_error_handler.966"""967return self._log_handlers.add_handler(callback, category="error", legacy=True)''',968''' def remove_javascript_error_handler(self, callback_id: int) -> None:969"""Remove a JavaScript error handler by callback ID."""970self._log_handlers.remove_handler(callback_id)''',971''' def add_error_handler(self, callback: Callable) -> int:972"""Add a handler for JavaScript errors (log.entryAdded type=javascript).973974Args:975callback: Function called with a ``ScriptError`` carrying message,976source URL, line/column numbers and stack trace.977978Returns:979callback_id for use with remove_error_handler.980"""981return self._log_handlers.add_handler(callback, category="error")''',982''' def remove_error_handler(self, callback_id: int) -> None:983"""Remove a JavaScript error handler by callback ID."""984self._log_handlers.remove_handler(callback_id)''',985''' def clear_error_handlers(self) -> None:986"""Remove all JavaScript error handlers.987988Clears handlers registered through both ``add_error_handler``989and ``add_javascript_error_handler``.990"""991self._log_handlers.clear_handlers("error")''',992''' def add_dom_mutation_handler(self, callback: Callable, mutation_types=None) -> int:993"""Add a handler for DOM mutation events.994995Uses a BiDi preload script and channel to observe DOM mutations on996the page. The callback is invoked with a ``DomMutation`` object997describing each observed mutation.998999Args:1000callback: Function called with a ``DomMutation`` on each mutation.1001mutation_types: The mutation types to observe: any of1002``attributes``, ``childList`` and ``characterData``, given as1003a string or an iterable of strings. Defaults to1004``("attributes",)``.10051006Returns:1007callback_id for use with remove_dom_mutation_handler.1008"""1009return self._dom_mutation_handlers.add_handler(callback, mutation_types)''',1010''' def remove_dom_mutation_handler(self, callback_id: int) -> None:1011"""Remove a DOM mutation handler by callback ID."""1012self._dom_mutation_handlers.remove_handler(callback_id)''',1013''' def clear_dom_mutation_handlers(self) -> None:1014"""Remove all DOM mutation handlers."""1015self._dom_mutation_handlers.clear_handlers()''',1016],1017},1018"network": {1019"exclude_types": ["disownDataParameters"],1020# Initialize intercepts tracking list and per-handler intercept map1021"extra_init_code": [1022"self.intercepts: list[Any] = []",1023"self._handler_intercepts: dict[str, Any] = {}",1024"self._request_handlers = RequestHandlerRegistry(self)",1025"self._response_handlers = ResponseHandlerRegistry(self)",1026"self._auth_handlers = AuthHandlerRegistry(self)",1027],1028# Request class wraps a beforeRequestSent event params and provides actions1029"extra_dataclasses": [1030'''@dataclass1031class DisownDataParameters:1032"""DisownDataParameters."""10331034data_type: Any | None = None1035collector: Any | None = None1036request: Any | None = None103710381039# Backward-compatible alias for existing imports1040disownDataParameters = DisownDataParameters''',1041'''class BytesValue:1042"""A string or base64-encoded bytes value used in cookie operations.10431044This corresponds to network.BytesValue in the WebDriver BiDi specification,1045wrapping either a plain string or a base64-encoded binary value.1046"""10471048TYPE_STRING = "string"1049TYPE_BASE64 = "base64"10501051def __init__(self, type: Any | None, value: Any | None) -> None:1052self.type = type1053self.value = value10541055def to_bidi_dict(self) -> dict:1056return {"type": self.type, "value": self.value}''',1057# Request/Response and the handler registries live in the static1058# helper module _network_handlers.py (staged via create-bidi-src1059# extra_srcs) so the implementation is lintable and unit-testable1060# as real code. The import also re-exports Request and Response to1061# keep them importable from selenium.webdriver.common.bidi.network.1062"""from selenium.webdriver.common.bidi._network_handlers import (1063LEGACY_REQUEST_HANDLER_EVENTS,1064AuthenticationRequest,1065AuthHandlerRegistry,1066Request,1067RequestHandlerRegistry,1068Response,1069ResponseHandlerRegistry,1070looks_like_url_glob,1071)""",1072],1073# Override auth_required to use raw dict so _auth_callback receives all1074# fields (including "request") from the BiDi event params. The1075# generated AuthRequiredParameters dataclass only contains "response",1076# losing the "request" field that holds the request ID required to call1077# network.continueWithAuth. extra_events entries appear last in the1078# EVENT_CONFIGS dict literal, so this duplicate key overrides the1079# CDDL-generated entry.1080# Add before_request event (maps to network.beforeRequestSent)1081# Override response_started to use raw dict for the same reason: the1082# generated ResponseStartedParameters dataclass only contains1083# "response", losing "request", "isBlocked" and "intercepts" which the1084# response handler registry needs to reconcile blocked responses.1085"extra_events": [1086{1087"event_key": "auth_required",1088"bidi_event": "network.authRequired",1089"event_class": "dict",1090},1091{1092"event_key": "before_request",1093"bidi_event": "network.beforeRequestSent",1094"event_class": "dict",1095},1096{1097"event_key": "response_started",1098"bidi_event": "network.responseStarted",1099"event_class": "dict",1100},1101],1102"extra_methods": [1103''' def _add_intercept(self, phases=None, url_patterns=None):1104"""Add a low-level network intercept.11051106Args:1107phases: list of intercept phases (default: ["beforeRequestSent"])1108url_patterns: optional URL patterns to filter11091110Returns:1111dict with "intercept" key containing the intercept ID1112"""1113from selenium.webdriver.common.bidi.common import command_builder as _cb11141115if phases is None:1116phases = ["beforeRequestSent"]1117params = {"phases": phases}1118if url_patterns:1119params["urlPatterns"] = url_patterns1120result = self._conn.execute(_cb("network.addIntercept", params))1121if result:1122intercept_id = result.get("intercept")1123if intercept_id and intercept_id not in self.intercepts:1124self.intercepts.append(intercept_id)1125return result''',1126''' def _remove_intercept(self, intercept_id):1127"""Remove a low-level network intercept."""1128from selenium.webdriver.common.bidi.common import command_builder as _cb11291130self._conn.execute(_cb("network.removeIntercept", {"intercept": intercept_id}))1131if intercept_id in self.intercepts:1132self.intercepts.remove(intercept_id)''',1133''' def _canonical_request_handler_event(self, event):1134"""Map public request-handler aliases to supported event keys."""1135event_aliases = {1136"auth_required": "auth_required",1137"before_request": "before_request",1138"before_request_sent": "before_request",1139}1140canonical_event = event_aliases.get(event)1141if canonical_event is None:1142available_events = ", ".join(sorted(event_aliases))1143raise ValueError(1144f"Unsupported request handler event '{event}'. Available events: {available_events}"1145)1146return canonical_event''',1147''' def add_request_handler(self, event=None, callback=None, url_patterns=None):1148"""Add a handler for network requests.11491150Two calling styles are supported.11511152High-level (recommended)::11531154driver.network.add_request_handler(handler)1155driver.network.add_request_handler(["**/api/**"], handler)11561157The handler receives a :class:`Request` and may observe it, mutate it1158via ``set_url``/``set_method``/``set_headers``/``set_cookies``/``set_body``,1159call ``fail()``, or call ``provide_response(...)``. After all matching1160handlers run, Selenium reconciles the outcome (fail > provide_response >1161continue with mutations > continue) and continues the request1162automatically — observers never stall the page. URL patterns are glob1163strings supporting ``*``, ``**`` and ``?`` (default: match everything).1164Returns a string handler ID for ``remove_request_handler(handler_id)``.11651166Legacy (phase-based)::11671168driver.network.add_request_handler("before_request", handler, url_patterns=[...])11691170The callback must call ``request.continue_request()`` itself and1171url_patterns are wire-level UrlPattern dicts. Returns an int callback1172ID for ``remove_request_handler(event, callback_id)``.1173"""1174if callable(event) and callback is None:1175return self._request_handlers.add_handler(url_patterns, event)1176if callable(callback) and event not in LEGACY_REQUEST_HANDLER_EVENTS:1177if not isinstance(event, str) or looks_like_url_glob(event):1178return self._request_handlers.add_handler(event, callback)1179canonical_event = self._canonical_request_handler_event(event)1180phase_map = {1181"before_request": "beforeRequestSent",1182"auth_required": "authRequired",1183}1184phase = phase_map[canonical_event]1185intercept_result = self._add_intercept(phases=[phase], url_patterns=url_patterns)1186intercept_id = intercept_result.get("intercept") if intercept_result else None11871188def _request_callback(params):1189raw = (1190params1191if isinstance(params, dict)1192else (params.__dict__ if hasattr(params, "__dict__") else {})1193)1194request = Request(self._conn, raw)1195callback(request)11961197callback_id = self.add_event_handler(canonical_event, _request_callback)1198if intercept_id:1199self._handler_intercepts[callback_id] = intercept_id1200return callback_id''',1201''' def remove_request_handler(self, event, callback_id=None):1202"""Remove a network request handler and its associated network intercept.12031204Args:1205event: The handler ID string returned by the high-level1206``add_request_handler(callback)`` form, or the event name used1207with the legacy phase-based form.1208callback_id: The int returned by the legacy form. Omit when1209removing a high-level handler by its ID.1210"""1211if callback_id is None:1212self._request_handlers.remove_handler(event)1213return1214canonical_event = self._canonical_request_handler_event(event)1215self.remove_event_handler(canonical_event, callback_id)1216intercept_id = self._handler_intercepts.pop(callback_id, None)1217if intercept_id:1218self._remove_intercept(intercept_id)''',1219''' def clear_request_handlers(self):1220"""Clear all request handlers and remove all tracked intercepts.12211222Response handlers registered via ``add_response_handler``,1223authentication handlers registered via ``add_authentication_handler``1224and extra headers registered via ``add_extra_header`` are preserved;1225use ``clear_response_handlers`` / ``clear_authentication_handlers`` /1226``clear_extra_headers`` to remove those.1227"""1228self._request_handlers.clear()1229self.clear_event_handlers()1230# After clear() the request registry's intercept_ids() only contains1231# the extra-headers intercept, which survives like the other1232# registries' intercepts.1233preserved_intercepts = (1234self._request_handlers.intercept_ids()1235| self._response_handlers.intercept_ids()1236| self._auth_handlers.intercept_ids()1237)1238for intercept_id in list(self.intercepts):1239if intercept_id not in preserved_intercepts:1240self._remove_intercept(intercept_id)1241# clear_event_handlers dropped every subscription, including the1242# other registries'; restore them so their handlers keep working.1243self._request_handlers.resubscribe()1244self._response_handlers.resubscribe()1245self._auth_handlers.resubscribe()''',1246''' def add_response_handler(self, url_patterns=None, callback=None):1247"""Add a handler for network responses.12481249Usage::12501251driver.network.add_response_handler(handler)1252driver.network.add_response_handler(["**/api/**"], handler)12531254The handler receives a :class:`Response` at the ``responseStarted``1255phase and may observe it or mutate it via1256``set_status``/``set_headers``/``set_cookies``/``set_body``. After all1257matching handlers run, Selenium reconciles the outcome — a mutated body1258is delivered via ``network.provideResponse``, other mutations via1259``network.continueResponse`` — and continues the response1260automatically, so observers never stall the page. URL patterns are1261glob strings supporting ``*``, ``**`` and ``?`` (default: match1262everything).12631264Returns:1265A string handler ID for ``remove_response_handler(handler_id)``.1266"""1267if callable(url_patterns) and callback is None:1268return self._response_handlers.add_handler(None, url_patterns)1269if not callable(callback):1270raise TypeError("add_response_handler requires a callable handler")1271return self._response_handlers.add_handler(url_patterns, callback)''',1272''' def remove_response_handler(self, handler_id):1273"""Remove a response handler and its intercept by handler ID.12741275Args:1276handler_id: The ID returned by ``add_response_handler``.1277"""1278self._response_handlers.remove_handler(handler_id)''',1279''' def clear_response_handlers(self):1280"""Clear all response handlers and their intercepts."""1281self._response_handlers.clear()''',1282''' def add_authentication_handler(self, url_patterns=None, callback=None):1283"""Add a handler for authentication challenges.12841285Usage::12861287driver.network.add_authentication_handler(handler)1288driver.network.add_authentication_handler(1289["https://secure-api.example.com/**"], handler1290)12911292The handler receives an :class:`AuthenticationRequest` at the1293``authRequired`` phase and may respond with1294``provide_credentials(username, password)`` or ``cancel()``. After all1295matching handlers run, Selenium reconciles the outcome (cancel >1296provide_credentials > browser default) and continues the challenge1297automatically, so observers never stall the page. URL patterns are1298glob strings supporting ``*``, ``**`` and ``?`` (default: match1299everything).13001301Do not combine with the credentials-only ``add_auth_handler``: both1302would answer the same challenge and the second response fails.13031304Returns:1305A string handler ID for ``remove_authentication_handler(handler_id)``.1306"""1307if callable(url_patterns) and callback is None:1308return self._auth_handlers.add_handler(None, url_patterns)1309if not callable(callback):1310raise TypeError("add_authentication_handler requires a callable handler")1311return self._auth_handlers.add_handler(url_patterns, callback)''',1312''' def remove_authentication_handler(self, handler_id):1313"""Remove an authentication handler and its intercept by handler ID.13141315Args:1316handler_id: The ID returned by ``add_authentication_handler``.1317"""1318self._auth_handlers.remove_handler(handler_id)''',1319''' def clear_authentication_handlers(self):1320"""Clear all authentication handlers and their intercepts."""1321self._auth_handlers.clear()''',1322''' def add_extra_header(self, name, value):1323"""Add a header that is merged into every subsequent request.13241325Usage::13261327driver.network.add_extra_header("x-test", "value")13281329BiDi has no dedicated command for extra headers, so while any extra1330header is set every request is paused at the ``beforeRequestSent``1331phase and continued with the merged headers — this adds a round trip1332per request, so remove the headers when no longer needed. Header1333names are case-insensitive; adding a header replaces any existing1334request header of the same name.13351336Args:1337name: The header name.1338value: The header value.1339"""1340self._request_handlers.set_extra_header(name, value)''',1341''' def remove_extra_header(self, name):1342"""Stop adding an extra header to subsequent requests.13431344Args:1345name: The (case-insensitive) header name passed to1346``add_extra_header``.1347"""1348self._request_handlers.remove_extra_header(name)''',1349''' def clear_extra_headers(self):1350"""Stop adding all extra headers to subsequent requests."""1351self._request_handlers.clear_extra_headers()''',1352''' def add_auth_handler(self, username, password):1353"""Add an auth handler that automatically provides credentials.13541355For callback-based handling with URL scoping and the ability to cancel1356a challenge, prefer ``add_authentication_handler``. Do not combine the1357two: both would answer the same challenge and the second response1358fails.13591360Args:1361username: The username for basic authentication.1362password: The password for basic authentication.13631364Returns:1365callback_id int for later removal via remove_auth_handler.1366"""1367from selenium.webdriver.common.bidi.common import command_builder as _cb13681369# Set up network intercept for authRequired phase1370intercept_result = self._add_intercept(phases=["authRequired"])1371intercept_id = intercept_result.get("intercept") if intercept_result else None13721373def _auth_callback(params):1374raw = (1375params1376if isinstance(params, dict)1377else (params.__dict__ if hasattr(params, "__dict__") else {})1378)1379request_id = (1380raw.get("request", {}).get("request")1381if isinstance(raw, dict)1382else None1383)1384if request_id:1385self._conn.execute(1386_cb(1387"network.continueWithAuth",1388{1389"request": request_id,1390"action": "provideCredentials",1391"credentials": {1392"type": "password",1393"username": username,1394"password": password,1395},1396},1397)1398)13991400callback_id = self.add_event_handler("auth_required", _auth_callback)1401if intercept_id:1402self._handler_intercepts[callback_id] = intercept_id1403return callback_id''',1404''' def remove_auth_handler(self, callback_id):1405"""Remove an auth handler by callback ID and its associated network intercept.14061407Args:1408callback_id: The handler ID returned by add_auth_handler.1409"""1410self.remove_event_handler("auth_required", callback_id)1411intercept_id = self._handler_intercepts.pop(callback_id, None)1412if intercept_id:1413self._remove_intercept(intercept_id)''',1414],1415},1416"storage": {1417# Exclude auto-generated dataclasses that need custom to_bidi_dict()1418# for JSON-over-WebSocket serialization, or custom constructors.1419"exclude_types": [1420"CookieFilter",1421"PartialCookie",1422"BrowsingContextPartitionDescriptor",1423"StorageKeyPartitionDescriptor",1424],1425"extra_dataclasses": [1426# Re-export network types used in cookie operations so they can be1427# imported from selenium.webdriver.common.bidi.storage alongside1428# the storage-specific classes.1429'''class BytesValue:1430"""A string or base64-encoded bytes value used in cookie operations.14311432This corresponds to network.BytesValue in the WebDriver BiDi specification,1433wrapping either a plain string or a base64-encoded binary value.1434"""14351436TYPE_STRING = "string"1437TYPE_BASE64 = "base64"14381439def __init__(self, type: Any | None, value: Any | None) -> None:1440self.type = type1441self.value = value14421443def to_bidi_dict(self) -> dict:1444return {"type": self.type, "value": self.value}14451446def to_dict(self) -> dict:1447"""Backward-compatible alias for to_bidi_dict()."""1448return self.to_bidi_dict()''',1449'''class SameSite:1450"""SameSite cookie attribute values."""14511452STRICT = "strict"1453LAX = "lax"1454NONE = "none"1455DEFAULT = "default"''',1456# Helper: cookie object returned inside a GetCookiesResult response1457'''@dataclass1458class StorageCookie:1459"""A cookie object returned by storage.getCookies."""14601461name: str | None = None1462value: Any | None = None1463domain: str | None = None1464path: str | None = None1465size: Any | None = None1466http_only: bool | None = None1467secure: bool | None = None1468same_site: Any | None = None1469expiry: Any | None = None14701471@classmethod1472def from_bidi_dict(cls, raw: dict) -> StorageCookie:1473"""Deserialize a wire-level cookie dict to a StorageCookie."""1474value_raw = raw.get("value")1475if isinstance(value_raw, dict):1476value: Any = BytesValue(value_raw.get("type"), value_raw.get("value"))1477else:1478value = value_raw1479return cls(1480name=raw.get("name"),1481value=value,1482domain=raw.get("domain"),1483path=raw.get("path"),1484size=raw.get("size"),1485http_only=raw.get("httpOnly"),1486secure=raw.get("secure"),1487same_site=raw.get("sameSite"),1488expiry=raw.get("expiry"),1489)''',1490# Custom CookieFilter with camelCase serialization1491'''@dataclass1492class CookieFilter:1493"""CookieFilter."""14941495name: str | None = None1496value: Any | None = None1497domain: str | None = None1498path: str | None = None1499size: Any | None = None1500http_only: bool | None = None1501secure: bool | None = None1502same_site: Any | None = None1503expiry: Any | None = None15041505def to_bidi_dict(self) -> dict:1506"""Serialize to the BiDi wire-protocol dict."""1507result: dict = {}1508if self.name is not None:1509result["name"] = self.name1510if self.value is not None:1511result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value1512if self.domain is not None:1513result["domain"] = self.domain1514if self.path is not None:1515result["path"] = self.path1516if self.size is not None:1517result["size"] = self.size1518if self.http_only is not None:1519result["httpOnly"] = self.http_only1520if self.secure is not None:1521result["secure"] = self.secure1522if self.same_site is not None:1523result["sameSite"] = self.same_site1524if self.expiry is not None:1525result["expiry"] = self.expiry1526return result15271528def to_dict(self) -> dict:1529"""Backward-compatible alias for to_bidi_dict()."""1530return self.to_bidi_dict()''',1531# Custom PartialCookie with camelCase serialization1532'''@dataclass1533class PartialCookie:1534"""PartialCookie."""15351536name: str | None = None1537value: Any | None = None1538domain: str | None = None1539path: str | None = None1540http_only: bool | None = None1541secure: bool | None = None1542same_site: Any | None = None1543expiry: Any | None = None15441545def to_bidi_dict(self) -> dict:1546"""Serialize to the BiDi wire-protocol dict."""1547result: dict = {}1548if self.name is not None:1549result["name"] = self.name1550if self.value is not None:1551result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value1552if self.domain is not None:1553result["domain"] = self.domain1554if self.path is not None:1555result["path"] = self.path1556if self.http_only is not None:1557result["httpOnly"] = self.http_only1558if self.secure is not None:1559result["secure"] = self.secure1560if self.same_site is not None:1561result["sameSite"] = self.same_site1562if self.expiry is not None:1563result["expiry"] = self.expiry1564return result15651566def to_dict(self) -> dict:1567"""Backward-compatible alias for to_bidi_dict()."""1568return self.to_bidi_dict()''',1569# BrowsingContextPartitionDescriptor: first positional arg is *context*1570# (the auto-generated dataclass had `type` first, breaking positional1571# usage like BrowsingContextPartitionDescriptor(driver.current_window_handle))1572'''class BrowsingContextPartitionDescriptor:1573"""BrowsingContextPartitionDescriptor.15741575The first positional argument is *context* (a browsing-context ID / window1576handle), mirroring how the class is used throughout the test suite:1577``BrowsingContextPartitionDescriptor(driver.current_window_handle)``.1578"""15791580def __init__(self, context: Any = None, type: str = "context") -> None:1581self.context = context1582self.type = type15831584def to_bidi_dict(self) -> dict:1585return {"type": "context", "context": self.context}15861587def to_dict(self) -> dict:1588"""Backward-compatible alias for to_bidi_dict()."""1589return self.to_bidi_dict()''',1590# StorageKeyPartitionDescriptor with camelCase serialization1591'''@dataclass1592class StorageKeyPartitionDescriptor:1593"""StorageKeyPartitionDescriptor."""15941595type: Any | None = "storageKey"1596user_context: str | None = None1597source_origin: str | None = None15981599def to_bidi_dict(self) -> dict:1600"""Serialize to the BiDi wire-protocol dict."""1601result: dict = {"type": "storageKey"}1602if self.user_context is not None:1603result["userContext"] = self.user_context1604if self.source_origin is not None:1605result["sourceOrigin"] = self.source_origin1606return result16071608def to_dict(self) -> dict:1609"""Backward-compatible alias for to_bidi_dict()."""1610return self.to_bidi_dict()''',1611],1612# Override the generated Storage class methods (Python's last-definition-1613# wins semantics means these extra_methods shadow the generated ones).1614"extra_methods": [1615''' def get_cookies(self, filter=None, partition=None):1616"""Execute storage.getCookies and return a GetCookiesResult."""1617if filter and hasattr(filter, "to_bidi_dict"):1618filter = filter.to_bidi_dict()1619if partition and hasattr(partition, "to_bidi_dict"):1620partition = partition.to_bidi_dict()1621params = {1622"filter": filter,1623"partition": partition,1624}1625params = {k: v for k, v in params.items() if v is not None}1626cmd = command_builder("storage.getCookies", params)1627result = self._conn.execute(cmd)1628if result and "cookies" in result:1629cookies = [1630StorageCookie.from_bidi_dict(c)1631for c in result.get("cookies", [])1632if isinstance(c, dict)1633]1634pk_raw = result.get("partitionKey")1635pk = (1636PartitionKey(1637user_context=pk_raw.get("userContext"),1638source_origin=pk_raw.get("sourceOrigin"),1639)1640if isinstance(pk_raw, dict)1641else None1642)1643return GetCookiesResult(cookies=cookies, partition_key=pk)1644return GetCookiesResult(cookies=[], partition_key=None)''',1645''' def set_cookie(self, cookie=None, partition=None):1646"""Execute storage.setCookie."""1647if cookie and hasattr(cookie, "to_bidi_dict"):1648cookie = cookie.to_bidi_dict()1649if partition and hasattr(partition, "to_bidi_dict"):1650partition = partition.to_bidi_dict()1651params = {1652"cookie": cookie,1653"partition": partition,1654}1655params = {k: v for k, v in params.items() if v is not None}1656cmd = command_builder("storage.setCookie", params)1657result = self._conn.execute(cmd)1658if isinstance(result, dict):1659pk_raw = result.get("partitionKey")1660pk = (1661PartitionKey(1662user_context=pk_raw.get("userContext"),1663source_origin=pk_raw.get("sourceOrigin"),1664)1665if isinstance(pk_raw, dict)1666else None1667)1668return SetCookieResult(partition_key=pk)1669return result''',1670''' def delete_cookies(self, filter=None, partition=None):1671"""Execute storage.deleteCookies."""1672if filter and hasattr(filter, "to_bidi_dict"):1673filter = filter.to_bidi_dict()1674if partition and hasattr(partition, "to_bidi_dict"):1675partition = partition.to_bidi_dict()1676params = {1677"filter": filter,1678"partition": partition,1679}1680params = {k: v for k, v in params.items() if v is not None}1681cmd = command_builder("storage.deleteCookies", params)1682result = self._conn.execute(cmd)1683if isinstance(result, dict):1684pk_raw = result.get("partitionKey")1685pk = (1686PartitionKey(1687user_context=pk_raw.get("userContext"),1688source_origin=pk_raw.get("sourceOrigin"),1689)1690if isinstance(pk_raw, dict)1691else None1692)1693return DeleteCookiesResult(partition_key=pk)1694return result''',1695],1696},1697"session": {1698# Override UserPromptHandler to add to_bidi_dict() for JSON serialization1699"exclude_types": ["UserPromptHandler"],1700"extra_dataclasses": [1701'''@dataclass1702class UserPromptHandler:1703"""UserPromptHandler."""17041705alert: Any | None = None1706before_unload: Any | None = None1707confirm: Any | None = None1708default: Any | None = None1709file: Any | None = None1710prompt: Any | None = None17111712def to_bidi_dict(self) -> dict:1713"""Convert to BiDi protocol dict with camelCase keys."""1714result = {}1715if self.alert is not None:1716result["alert"] = self.alert1717if self.before_unload is not None:1718result["beforeUnload"] = self.before_unload1719if self.confirm is not None:1720result["confirm"] = self.confirm1721if self.default is not None:1722result["default"] = self.default1723if self.file is not None:1724result["file"] = self.file1725if self.prompt is not None:1726result["prompt"] = self.prompt1727return result17281729def to_dict(self) -> dict:1730"""Backward-compatible alias for to_bidi_dict()."""1731return self.to_bidi_dict()''',1732],1733},1734"webExtension": {1735# Suppress the raw generated stubs; hand-written versions follow below1736"exclude_methods": ["install", "uninstall"],1737"extra_methods": [1738''' def install(1739self,1740path: str | None = None,1741archive_path: str | None = None,1742base64_value: str | None = None,1743):1744"""Install a web extension.17451746Exactly one of the three keyword arguments must be provided.17471748Args:1749path: Directory path to an unpacked extension (also accepted for1750signed ``.xpi`` / ``.crx`` archive files on Firefox).1751archive_path: File-system path to a packed extension archive.1752base64_value: Base64-encoded extension archive string.17531754Returns:1755The raw result dict from the BiDi ``webExtension.install`` command1756(contains at least an ``"extension"`` key with the extension ID).17571758Raises:1759ValueError: If more than one, or none, of the arguments is provided.1760"""1761provided = [1762k for k, v in {1763"path": path, "archive_path": archive_path, "base64_value": base64_value,1764}.items() if v is not None1765]1766if len(provided) != 1:1767raise ValueError(1768f"Exactly one of path, archive_path, or base64_value must be provided; got: {provided}"1769)1770if path is not None:1771extension_data = {"type": "path", "path": path}1772elif archive_path is not None:1773extension_data = {"type": "archivePath", "path": archive_path}1774else:1775assert base64_value is not None1776extension_data = {"type": "base64", "value": base64_value}1777params = {"extensionData": extension_data}1778cmd = command_builder("webExtension.install", params)1779try:1780return self._conn.execute(cmd)1781except Exception as e:1782if "Method not available" in str(e):1783raise RuntimeError(1784"webExtension.install failed with 'Method not available'. "1785"This likely means that web extension support is disabled. "1786"Enable unsafe extension debugging and/or set options.enable_webextensions "1787"in your WebDriver configuration."1788) from e1789raise''',1790''' def uninstall(self, extension: str | dict):1791"""Uninstall a web extension.17921793Args:1794extension: Either the extension ID string returned by ``install``,1795or the full result dict returned by ``install`` (the1796``"extension"`` value is extracted automatically).17971798Raises:1799ValueError: If extension is not provided or is None.1800"""1801if isinstance(extension, dict):1802extension_id: Any = extension.get("extension")1803else:1804extension_id = extension18051806if extension_id is None:1807raise ValueError("extension parameter is required")18081809params = {"extension": extension_id}1810cmd = command_builder("webExtension.uninstall", params)1811return self._conn.execute(cmd)''',1812],1813},1814"input": {1815# FileDialogInfo needs from_json for event deserialization1816"exclude_types": ["FileDialogInfo", "PointerMoveAction", "PointerDownAction"],1817"extra_dataclasses": [1818'''@dataclass1819class FileDialogInfo:1820"""FileDialogInfo - parameters for the input.fileDialogOpened event."""18211822context: Any | None = None1823element: Any | None = None1824multiple: bool | None = None18251826@classmethod1827def from_json(cls, params: dict) -> FileDialogInfo:1828"""Deserialize event params into FileDialogInfo."""1829return cls(1830context=params.get("context"),1831element=params.get("element"),1832multiple=params.get("multiple"),1833)''',1834'''@dataclass1835class PointerMoveAction:1836"""PointerMoveAction."""18371838type: str = field(default="pointerMove", init=False)1839x: Any | None = None1840y: Any | None = None1841duration: Any | None = None1842origin: Any | None = None1843properties: Any | None = None''',1844'''@dataclass1845class PointerDownAction:1846"""PointerDownAction."""18471848type: str = field(default="pointerDown", init=False)1849button: Any | None = None1850properties: Any | None = None''',1851],1852"extra_methods": [1853''' def add_file_dialog_handler(self, callback) -> int:1854"""Subscribe to the input.fileDialogOpened event.18551856Args:1857callback: Callable invoked with a FileDialogInfo when a file dialog opens.18581859Returns:1860A handler ID that can be passed to remove_file_dialog_handler.1861"""1862return self._event_manager.add_event_handler("file_dialog_opened", callback)18631864def remove_file_dialog_handler(self, handler_id: int) -> None:1865"""Unsubscribe a previously registered file dialog event handler.18661867Args:1868handler_id: The handler ID returned by add_file_dialog_handler.1869"""1870return self._event_manager.remove_event_handler("file_dialog_opened", handler_id)''',1871],1872},1873"permissions": {1874"module_docstring": (1875"WebDriver BiDi permissions module.\n\n"1876"Provides control over browser permission grants during automated tests,\n"1877"as specified by the W3C Permissions specification.\n\n"1878"Typical usage::\n\n"1879" driver.permissions.set_permission('geolocation', 'granted', origin)\n"1880),1881"class_docstrings": {1882"PermissionState": (1883"Permission state constants.\n\n"1884"GRANTED: The permission is granted — the browser will not prompt the user.\n"1885"DENIED: The permission is denied — the browser will block the request.\n"1886"PROMPT: The browser will show a permission prompt (default browser behaviour)."1887),1888"Permissions": (1889"BiDi interface for controlling browser permissions.\n\nAccess via ``driver.permissions``."1890),1891},1892"extra_dataclasses": [1893'''class PermissionDescriptor:1894"""Descriptor identifying a permission by name.18951896Args:1897name: The permission name (e.g. 'geolocation', 'microphone', 'camera').1898"""18991900def __init__(self, name: str) -> None:1901self.name = name19021903def __repr__(self) -> str:1904return f"PermissionDescriptor(name={self.name!r})"''',1905],1906"extra_methods": [1907''' def set_permission(1908self,1909descriptor: "PermissionDescriptor | str",1910state: "PermissionState | str",1911origin: str | None = None,1912user_context: str | None = None,1913*,1914embedded_origin: str | None = None,1915) -> None:1916"""Set a browser permission.19171918Args:1919descriptor: The permission descriptor or permission name as a string.1920state: The desired permission state (granted, denied, or prompt).1921origin: The origin to scope the permission to.1922user_context: Optional user context ID to scope the permission.1923embedded_origin: Keyword-only. Embedded origin for cross-origin1924iframes; scopes the permission to that iframe's origin.19251926Raises:1927ValueError: If *state* is not a valid permission state.1928"""1929state_value = state.value if isinstance(state, PermissionState) else state1930valid_states = {"granted", "denied", "prompt"}1931if state_value not in valid_states:1932raise ValueError(1933f"Invalid permission state: {state_value!r}. "1934f"Must be one of {sorted(valid_states)}"1935)19361937descriptor_dict = {"name": descriptor} if isinstance(descriptor, str) else {"name": descriptor.name}19381939params: dict = {1940"descriptor": descriptor_dict,1941"state": state_value,1942}1943if origin is not None:1944params["origin"] = origin1945if embedded_origin is not None:1946params["embeddedOrigin"] = embedded_origin1947if user_context is not None:1948params["userContext"] = user_context19491950cmd = command_builder("permissions.setPermission", params)1951self._conn.execute(cmd)''',1952],1953},1954"bluetooth": {1955"module_docstring": (1956"WebDriver BiDi bluetooth module.\n\n"1957"Provides a simulation API for Web Bluetooth, allowing tests to fake\n"1958"Bluetooth adapters, nearby peripherals, GATT services, characteristics,\n"1959"and descriptors without physical hardware.\n"1960),1961"class_docstrings": {1962"Bluetooth": (1963"BiDi interface for simulating Web Bluetooth hardware.\n\n"1964"Simulate adapters, peripherals, GATT services, characteristics,\n"1965"and descriptors without physical hardware."1966),1967"RequestDeviceInfo": (1968"Identifies a simulated Bluetooth device returned in a device-request prompt.\n\n"1969"Attributes:\n"1970" id: The internal device identifier.\n"1971" name: The human-readable device name shown in the prompt."1972),1973"SimulateAdapterParameters": (1974"Parameters for simulating a Bluetooth adapter state.\n\n"1975"Attributes:\n"1976" context: The browsing context ID to target.\n"1977" le_supported: Whether the adapter supports Bluetooth Low Energy.\n"1978" state: Adapter power state (e.g. 'powered-on', 'powered-off', 'absent')."1979),1980"SimulatePreconnectedPeripheralParameters": (1981"Parameters for adding a pre-connected simulated peripheral.\n\n"1982"Attributes:\n"1983" context: The browsing context ID to target.\n"1984" address: The Bluetooth device address (e.g. '09:09:09:09:09:09').\n"1985" name: The device name advertised to the page.\n"1986" manufacturer_data: List of manufacturer-specific data records.\n"1987" known_service_uuids: UUIDs of GATT services the device exposes."1988),1989"SimulateAdvertisementParameters": (1990"Parameters for injecting a simulated advertisement packet.\n\n"1991"Attributes:\n"1992" context: The browsing context ID to target.\n"1993" scan_entry: The advertisement scan record to inject."1994),1995"SimulateGattConnectionResponseParameters": (1996"Parameters for simulating a GATT connection response.\n\n"1997"Attributes:\n"1998" context: The browsing context ID to target.\n"1999" address: The address of the peripheral.\n"2000" code: The ATT error code (0 = success)."2001),2002"SimulateCharacteristicParameters": (2003"Parameters for adding a simulated GATT characteristic to a service.\n\n"2004"Attributes:\n"2005" context: The browsing context ID to target.\n"2006" address: The peripheral address.\n"2007" service: The service UUID the characteristic belongs to.\n"2008" characteristic: UUID of the characteristic.\n"2009" properties: Supported operations (read, write, notify, etc.)."2010),2011},2012"command_docstrings": {2013"handle_request_device_prompt": (2014"Dismiss or accept a Bluetooth device-chooser prompt.\n\n"2015"Args:\n"2016" context: The browsing context containing the prompt.\n"2017" prompt: The prompt ID returned in the prompt-opened event."2018),2019"simulate_adapter": (2020"Simulate a Bluetooth adapter in the given browsing context.\n\n"2021"Args:\n"2022" context: The browsing context ID to target.\n"2023" le_supported: Whether Low Energy is supported.\n"2024" state: Adapter state ('powered-on', 'powered-off', 'absent')."2025),2026"disable_simulation": (2027"Disable all Bluetooth simulation in the given context, restoring real behaviour.\n\n"2028"Args:\n"2029" context: The browsing context ID to stop simulating."2030),2031"simulate_preconnected_peripheral": (2032"Register a simulated peripheral as already connected to the adapter.\n\n"2033"Args:\n"2034" context: The browsing context ID to target.\n"2035" address: The Bluetooth device address.\n"2036" name: The device name.\n"2037" manufacturer_data: Manufacturer-specific advertisement data.\n"2038" known_service_uuids: List of GATT service UUIDs the device exposes."2039),2040"simulate_advertisement": (2041"Inject a simulated Bluetooth advertisement packet.\n\n"2042"Args:\n"2043" context: The browsing context ID to target.\n"2044" scan_entry: The advertisement scan record to inject."2045),2046"simulate_gatt_connection_response": (2047"Respond to a pending GATT connection attempt from the page.\n\n"2048"Args:\n"2049" context: The browsing context ID.\n"2050" address: The peripheral address.\n"2051" code: ATT error code (0 = success; non-zero signals failure)."2052),2053"simulate_gatt_disconnection": (2054"Simulate a GATT disconnection for the given peripheral.\n\n"2055"Args:\n"2056" context: The browsing context ID.\n"2057" address: The address of the peripheral to disconnect."2058),2059"simulate_service": (2060"Add a simulated GATT service to a peripheral.\n\n"2061"Args:\n"2062" context: The browsing context ID.\n"2063" address: The peripheral address.\n"2064" uuid: The service UUID."2065),2066"simulate_characteristic": (2067"Add a simulated GATT characteristic to a service.\n\n"2068"Args:\n"2069" context: The browsing context ID.\n"2070" address: The peripheral address.\n"2071" service: The service UUID.\n"2072" characteristic: The characteristic UUID.\n"2073" properties: Supported operations bitmap."2074),2075"simulate_characteristic_response": (2076"Respond to a pending read or write on a simulated characteristic.\n\n"2077"Args:\n"2078" context: The browsing context ID.\n"2079" address: The peripheral address.\n"2080" service: The service UUID.\n"2081" characteristic: The characteristic UUID.\n"2082" code: ATT error code (0 = success).\n"2083" body: The characteristic value bytes (for reads)."2084),2085"simulate_descriptor": (2086"Add a simulated GATT descriptor to a characteristic.\n\n"2087"Args:\n"2088" context: The browsing context ID.\n"2089" address: The peripheral address.\n"2090" service: The service UUID.\n"2091" characteristic: The characteristic UUID.\n"2092" descriptor: The descriptor UUID."2093),2094"simulate_descriptor_response": (2095"Respond to a pending read or write on a simulated descriptor.\n\n"2096"Args:\n"2097" context: The browsing context ID.\n"2098" address: The peripheral address.\n"2099" service: The service UUID.\n"2100" characteristic: The characteristic UUID.\n"2101" descriptor: The descriptor UUID.\n"2102" code: ATT error code (0 = success).\n"2103" body: The descriptor value bytes (for reads)."2104),2105},2106},2107"speculation": {2108"module_docstring": (2109"WebDriver BiDi speculation module.\n\n"2110"Provides events for observing the status of Speculation Rules prefetch\n"2111"requests initiated by the browser (e.g. via <script type='speculationrules'>).\n"2112),2113"class_docstrings": {2114"Speculation": ("BiDi interface for observing Speculation Rules prefetch activity."),2115"PreloadingStatus": (2116"Status values for a speculation-rules prefetch operation.\n\n"2117"PENDING: The prefetch has been queued but not yet attempted.\n"2118"READY: The prefetch succeeded and the resource is cached.\n"2119"SUCCESS: The prefetched navigation was used successfully.\n"2120"FAILURE: The prefetch failed or was cancelled."2121),2122"PrefetchStatusUpdatedParameters": (2123"Event payload emitted when a prefetch status changes.\n\n"2124"Attributes:\n"2125" context: The browsing context ID that owns the speculation rule.\n"2126" url: The URL being prefetched.\n"2127" status: The new prefetch status (see PreloadingStatus)."2128),2129},2130},2131"userAgentClientHints": {2132"module_docstring": (2133"WebDriver BiDi userAgentClientHints module.\n\n"2134"Provides an API for overriding the User-Agent Client Hints reported\n"2135"by the browser, enabling tests to simulate different devices, platforms,\n"2136"and browser brands without changing the actual browser binary.\n"2137),2138"class_docstrings": {2139"UserAgentClientHints": ("BiDi interface for overriding User-Agent Client Hints."),2140"ClientHintsMetadata": (2141"Full set of User-Agent Client Hint values to override.\n\n"2142"Attributes:\n"2143" brands: List of browser brand/version pairs (e.g. [BrandVersion('Chrome', '120')]).\n"2144" full_version_list: Brands with full version strings.\n"2145" platform: Operating system name (e.g. 'Windows', 'macOS').\n"2146" platform_version: OS version string.\n"2147" architecture: CPU architecture (e.g. 'x86', 'arm').\n"2148" model: Device model (primarily for mobile).\n"2149" mobile: True if the UA should appear to be a mobile device.\n"2150" bitness: Pointer-size bitness string ('32' or '64').\n"2151" wow64: True if running a 32-bit process on 64-bit Windows.\n"2152" form_factors: Device form factors (e.g. 'Desktop', 'Phone')."2153),2154"BrandVersion": (2155"A single browser brand entry used in Client Hints brand lists.\n\n"2156"Attributes:\n"2157" brand: The browser/engine brand name (e.g. 'Google Chrome').\n"2158" version: The major or full version string (e.g. '120')."2159),2160},2161},2162}216321642165# ============================================================================2166# Pre-processing Functions2167# ============================================================================216821692170def check_serialize_method(obj: Any) -> Any:2171"""Check if object has to_bidi_dict() method and use it for serialization."""2172if obj and hasattr(obj, "to_bidi_dict"):2173return obj.to_bidi_dict()2174return obj217521762177# ============================================================================2178# Validation Functions2179# ============================================================================218021812182def validate_download_behavior(2183allowed: bool | None,2184destination_folder: str | None,2185user_contexts: Any | None = None,2186) -> None:2187"""Validate download behavior parameters.21882189Args:2190allowed: Whether downloads are allowed2191destination_folder: Destination folder for downloads2192user_contexts: Optional list of user contexts (ignored for validation)21932194Raises:2195ValueError: If parameters are invalid2196"""2197if allowed is True and not destination_folder:2198raise ValueError("destination_folder is required when allowed=True")2199if allowed is False and destination_folder:2200raise ValueError("destination_folder should not be provided when allowed=False")220122022203# ============================================================================2204# Transformation Functions2205# ============================================================================220622072208def transform_download_params(2209allowed: bool | None,2210destination_folder: str | None,2211) -> dict[str, Any]:2212"""Transform download parameters into download_behavior object.22132214Args:2215allowed: Whether downloads are allowed2216destination_folder: Destination folder for downloads22172218Returns:2219Dictionary representing the download_behavior object, or None if allowed is None2220"""2221if allowed is True:2222return {2223"type": "allowed",2224# Convert pathlib.Path (or any path-like) to str so the BiDi2225# protocol always receives a plain JSON string.2226"destinationFolder": (str(destination_folder) if destination_folder is not None else None),2227}2228elif allowed is False:2229return {"type": "denied"}2230else: # None — reset to browser default (sent as JSON null)2231return None223222332234# ============================================================================2235# Dataclass Method Templates2236# ============================================================================22372238DATACLASS_METHOD_TEMPLATES: dict[str, dict[str, str]] = {2239"ClientWindowInfo": {2240"get_client_window": "return self.client_window",2241"get_state": "return self.state",2242"get_width": "return self.width",2243"get_height": "return self.height",2244"is_active": "return self.active",2245"get_x": "return self.x",2246"get_y": "return self.y",2247},2248"BrowsingContext": {2249"add_event_handler": "_add_event_handler_impl",2250"remove_event_handler": "_remove_event_handler_impl",2251},2252}22532254DATACLASS_METHOD_DOCSTRINGS: dict[str, dict[str, str]] = {2255"ClientWindowInfo": {2256"get_client_window": "Get the client window ID.",2257"get_state": "Get the client window state.",2258"get_width": "Get the client window width.",2259"get_height": "Get the client window height.",2260"is_active": "Check if the client window is active.",2261"get_x": "Get the client window X position.",2262"get_y": "Get the client window Y position.",2263},2264"BrowsingContext": {2265"add_event_handler": "Add an event handler for browsing context events.",2266"remove_event_handler": "Remove an event handler by callback ID.",2267},2268}22692270# ============================================================================2271# Event Handler Support for BrowsingContext2272# ============================================================================227322742275def _add_event_handler(2276self,2277event_name: str,2278callback: callable,2279contexts: list[str] | None = None,2280) -> str:2281"""Add an event handler for a browsing context event.22822283Supported events:2284- 'context_created'2285- 'context_destroyed'2286- 'navigation_started'2287- 'navigation_committed'2288- 'navigation_failed'2289- 'dom_content_loaded'2290- 'load'2291- 'fragment_navigated'2292- 'user_prompt_opened'2293- 'user_prompt_closed'2294- 'download_will_begin'2295- 'download_end'2296- 'history_updated'22972298Args:2299self: The module instance this handler is bound to.2300event_name: The name of the event to subscribe to2301callback: Callback function to invoke when event occurs2302contexts: Optional list of context IDs to limit event subscription23032304Returns:2305A callback ID that can be used to unsubscribe the handler2306"""2307if not hasattr(self, "_event_handlers"):2308self._event_handlers = {}2309self._event_callback_id_counter = 023102311# Generate unique callback ID2312self._event_callback_id_counter += 12313callback_id = f"callback_{self._event_callback_id_counter}"23142315# Store the handler2316self._event_handlers[callback_id] = {2317"event": event_name,2318"callback": callback,2319"contexts": contexts,2320}23212322# Subscribe via the driver's event listening mechanism2323if hasattr(self._driver, "_subscribe_event"):2324self._driver._subscribe_event(event_name, callback, contexts)23252326return callback_id232723282329def _remove_event_handler(2330self,2331callback_id: str,2332) -> None:2333"""Remove an event handler by its callback ID.23342335Args:2336self: The module instance this handler is bound to.2337callback_id: The callback ID returned from add_event_handler2338"""2339if not hasattr(self, "_event_handlers"):2340return23412342if callback_id in self._event_handlers:2343handler_info = self._event_handlers[callback_id]23442345# Unsubscribe from the driver2346if hasattr(self._driver, "_unsubscribe_event"):2347self._driver._unsubscribe_event(2348handler_info["event"],2349handler_info["callback"],2350handler_info["contexts"],2351)23522353del self._event_handlers[callback_id]235423552356