Skip to content

CDDL 2 Python generator#16914

Open
AutomatedTester wants to merge 69 commits intotrunkfrom
cddl2py
Open

CDDL 2 Python generator#16914
AutomatedTester wants to merge 69 commits intotrunkfrom
cddl2py

Conversation

@AutomatedTester
Copy link
Member

@AutomatedTester AutomatedTester commented Jan 16, 2026

This generates bidi code based off of the CDDL that we can update from the specification. I expect over time the generation will need other features added.

@selenium-ci selenium-ci added the C-py Python Bindings label Jan 16, 2026
@AutomatedTester AutomatedTester marked this pull request as draft January 16, 2026 10:14
Copilot AI review requested due to automatic review settings February 16, 2026 11:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a CDDL (Concise Data Definition Language) to Python generator for WebDriver BiDi modules. It generates 9 BiDi protocol modules from the W3C specification, replacing hand-written implementations with auto-generated code.

Changes:

  • Adds py/generate_bidi.py - CDDL parser and Python code generator (623 lines)
  • Adds Bazel build integration for code generation
  • Generates 9 BiDi modules (browser, browsing_context, emulation, input, network, script, session, storage, webextension) with 146 type definitions and 52 commands
  • Adds validation tooling and documentation

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
py/generate_bidi.py Core CDDL parser and Python code generator
py/private/generate_bidi.bzl Bazel rule for BiDi code generation
py/BUILD.bazel Integration of generation target
py/requirements.txt Added pycddl dependency
py/selenium/webdriver/common/bidi/*.py Generated BiDi module replacements
py/validate_bidi_modules.py Validation tooling for comparing generated vs hand-written code
common/bidi/spec/local.cddl CDDL specification (1331 lines)
Various .md files Documentation and findings

Copilot AI review requested due to automatic review settings February 17, 2026 10:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

py/validate_bidi_modules.py:1

  • Corrected spelling of 'Analyze' to match class name convention.
#!/usr/bin/env python3

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 10 comments.

Comment on lines +251 to +257
@dataclass
class disownDataParameters:
"""disownDataParameters type type."""

data_type: Any | None = None
collector: Any | None = None
request: Any | None = None
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated dataclass/type names like disownDataParameters start with a lowercase letter, which is inconsistent with Python class naming and the surrounding generated types. This is likely a generator bug in the CDDL-to-Python name mapping; please ensure all generated class/type names use PascalCase (e.g., DisownDataParameters).

Copilot uses AI. Check for mistakes.
Comment on lines 1179 to 1217
def script(self) -> Script:
if not self._websocket_connection:
self._start_bidi()

if not self._script:
self._script = Script(self._websocket_connection, self)

return self._script

def _start_bidi(self) -> None:
if self.caps.get("webSocketUrl"):
ws_url = self.caps.get("webSocketUrl")
else:
raise WebDriverException("Unable to find url to connect to from capabilities")
raise WebDriverException(
"Unable to find url to connect to from capabilities"
)

if not isinstance(self.command_executor, RemoteConnection):
raise WebDriverException("command_executor must be a RemoteConnection instance for BiDi support")
raise WebDriverException(
"command_executor must be a RemoteConnection instance for BiDi support"
)

self._websocket_connection = WebSocketConnection(
ws_url,
self.command_executor.client_config.websocket_timeout,
self.command_executor.client_config.websocket_interval,
)

@property
def network(self) -> Network:
if not self._websocket_connection:
self._start_bidi()

assert self._websocket_connection is not None
if not hasattr(self, "_network") or self._network is None:
assert self._websocket_connection is not None
self._network = Network(self._websocket_connection)

return self._network
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BiDi module properties still instantiate modules with the old constructor signatures (e.g., Script(self._websocket_connection, self)), but the updated/generated BiDi modules now take a single driver argument. This will raise TypeError when accessing driver.script/driver.network/driver.browser/etc. Either keep the BiDi module constructors compatible with WebSocketConnection, or update these property implementations to pass the expected driver and have the modules call driver._websocket_connection.execute(...) internally.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +79
@dataclass
class setNetworkConditionsParameters:
"""setNetworkConditionsParameters type type."""

network_conditions: Any | None = None
contexts: list[Any | None] | None = field(default_factory=list)
user_contexts: list[Any | None] | None = field(default_factory=list)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated dataclass/type names like setNetworkConditionsParameters start with a lowercase letter, which is inconsistent with Python class naming and the surrounding generated types. This is likely a generator bug in the CDDL-to-Python name mapping; please ensure all generated class/type names use PascalCase (e.g., SetNetworkConditionsParameters).

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +27
import argparse
import importlib.util
import logging
import re
import sys
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from textwrap import dedent, indent as tw_indent
from typing import Any, Dict, List, Optional, Set, Tuple

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New code in the generator uses Optional[...]/Union[...] imports and annotations (e.g., Optional[str]), but the repo’s conventions in py/AGENTS.md prefer X | None and avoiding Optional. Please update the generator to follow the same typing style to keep generated code and tooling consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +434 to +451
def execute(
self, driver_command: str, params: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Sends a command to be executed by a command.CommandExecutor.

Args:
driver_command: The name of the command to execute as a string.
driver_command: The name of the command to execute as a string. Can also be a generator
for BiDi protocol commands.
params: A dictionary of named parameters to send with the command.

Returns:
The command's JSON response loaded into a dictionary object.
"""
# Handle BiDi generator commands
if inspect.isgenerator(driver_command):
# BiDi command: use WebSocketConnection directly
return self.command_executor.execute(driver_command)

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() now treats a generator driver_command as a BiDi command and calls self.command_executor.execute(driver_command). In this class, command_executor is typically a RemoteConnection whose execute() requires (command, params), so this will raise TypeError at runtime. BiDi commands should be routed through self._websocket_connection.execute(...) (after ensuring _start_bidi() has initialized it), and the execute() signature/type should be updated accordingly (e.g., accept Generator[dict, dict, dict] in addition to str).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 10 comments.

Comment on lines +86 to +87
class setNetworkConditionsParameters:
"""setNetworkConditionsParameters."""
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name setNetworkConditionsParameters violates Python naming conventions. Class names should use PascalCase, so this should be SetNetworkConditionsParameters.

Suggested change
class setNetworkConditionsParameters:
"""setNetworkConditionsParameters."""
class SetNetworkConditionsParameters:
"""SetNetworkConditionsParameters."""

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 25, 2026 13:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 35 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

py/selenium/webdriver/remote/webdriver.py:1107

  • start_devtools()/bidi_connection() call import_cdp() which imports selenium.webdriver.common.bidi.cdp, but this PR deletes py/selenium/webdriver/common/bidi/cdp.py. That will cause a ModuleNotFoundError the first time devtools/BiDi connection code runs. Either keep/replace the cdp module (and update import_cdp() accordingly) or remove these code paths.

Comment on lines +271 to +276
def get_cookies(self, filter=None, partition=None):
"""Execute storage.getCookies and return a GetCookiesResult."""
if filter and hasattr(filter, "to_bidi_dict"):
filter = filter.to_bidi_dict()
if partition and hasattr(partition, "to_bidi_dict"):
partition = partition.to_bidi_dict()
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storage.get_cookies/set_cookie/delete_cookies are each defined twice in the same class; the later definitions overwrite the earlier ones. This should be consolidated to a single implementation per method (and ideally keep the type-annotated signatures).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 27, 2026 14:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 34 changed files in this pull request and generated 8 comments.

Comment on lines +103 to +105
"""Test ClientWindowNamedState constants."""
assert ClientWindowNamedState.MAXIMIZED == "maximized"
assert ClientWindowNamedState.MINIMIZED == "minimized"
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test removed the assertion for ClientWindowState.FULLSCREEN and ClientWindowState.NORMAL. Since the class was renamed to ClientWindowNamedState, these constants should still be tested if they still exist in the new class. The generated class ClientWindowNamedState only defines FULLSCREEN, MAXIMIZED, and MINIMIZED but not NORMAL. If NORMAL is a valid window state in the BiDi spec, it should be added to the generated class definition.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +56
type: str = field(default="none", init=False)
id: str | None = None
actions: list[Any | None] | None = field(default_factory=list)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dataclass fields use field(default="...", init=False) pattern for discriminator fields. This should use field(default_factory=lambda: "...") when the default is mutable, though strings are immutable so this is acceptable. However, be aware that init=False means these fields cannot be set during initialization, which may not be the intended behavior for all use cases.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +61
def default(self, o):
if dataclasses.is_dataclass(o) and not isinstance(o, type):
result = {}
for f in dataclasses.fields(o):
value = getattr(o, f.name)
if value is None:
continue
camel_key = _snake_to_camel(f.name)
# Flatten PointerCommonProperties fields inline into the parent
if camel_key == "properties" and dataclasses.is_dataclass(value):
for pf in dataclasses.fields(value):
pv = getattr(value, pf.name)
if pv is not None:
result[_snake_to_camel(pf.name)] = pv
else:
result[camel_key] = value
return result
return super().default(o)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _BiDiEncoder.default() method recursively encodes dataclass values, but it doesn't handle the case where nested values might also need encoding (e.g., lists of dataclasses, dicts containing dataclasses). The encoder should recursively process list and dict values to ensure all nested dataclasses are properly converted.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +40
def command_builder(
method: str, params: dict[str, Any]
) -> Generator[dict[str, Any], Any, Any]:
"""Build a BiDi command generator.

Args:
method: The method to execute.
params: The parameters to pass to the method. Default is None.
method: The BiDi method name (e.g., "session.status", "browser.close")
params: The parameters for the command

Yields:
A dictionary representing the BiDi command

Returns:
The response from the command execution.
The result from the BiDi command execution
"""
if params is None:
params = {}

command = {"method": method, "params": params}
cmd = yield command
return cmd
result = yield {"method": method, "params": params}
return result
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command_builder function signature changed from params: dict | None = None to params: dict[str, Any] (required). This is a breaking API change that may affect existing code that calls command_builder(method) without params. The old behavior allowed None as default, which was converted to an empty dict. Consider making params optional with a default of empty dict: params: dict[str, Any] | None = None and adding if params is None: params = {} in the function body to maintain backward compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +250
class _EventWrapper:
"""Wrapper to provide event_class attribute for WebSocketConnection callbacks."""
def __init__(self, bidi_event: str, event_class: type):
self.event_class = bidi_event # WebSocket expects the BiDi event name as event_class
self._python_class = event_class # Keep reference to Python dataclass for deserialization

def from_json(self, params: dict) -> Any:
"""Deserialize event params into the wrapped Python dataclass.

Args:
params: Raw BiDi event params with camelCase keys.

Returns:
An instance of the dataclass, or the raw dict on failure.
"""
if self._python_class is None or self._python_class is dict:
return params
try:
# Delegate to a classmethod from_json if the class defines one
if hasattr(self._python_class, "from_json") and callable(
self._python_class.from_json
):
return self._python_class.from_json(params)
import dataclasses as dc

snake_params = {self._camel_to_snake(k): v for k, v in params.items()}
if dc.is_dataclass(self._python_class):
valid_fields = {f.name for f in dc.fields(self._python_class)}
filtered = {k: v for k, v in snake_params.items() if k in valid_fields}
return self._python_class(**filtered)
return self._python_class(**snake_params)
except Exception:
return params

@staticmethod
def _camel_to_snake(name: str) -> str:
result = [name[0].lower()]
for char in name[1:]:
if char.isupper():
result.extend(["_", char.lower()])
else:
result.append(char)
return "".join(result)


class _EventManager:
"""Manages event subscriptions and callbacks."""

def __init__(self, conn, event_configs: dict[str, EventConfig]):
self.conn = conn
self.event_configs = event_configs
self.subscriptions: dict = {}
self._event_wrappers = {} # Cache of _EventWrapper objects
self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}
self._available_events = ", ".join(sorted(event_configs.keys()))
self._subscription_lock = threading.Lock()

# Create event wrappers for each event
for config in event_configs.values():
wrapper = _EventWrapper(config.bidi_event, config.event_class)
self._event_wrappers[config.bidi_event] = wrapper

def validate_event(self, event: str) -> EventConfig:
event_config = self.event_configs.get(event)
if not event_config:
raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")
return event_config

def subscribe_to_event(self, bidi_event: str, contexts: list[str] | None = None) -> None:
"""Subscribe to a BiDi event if not already subscribed."""
with self._subscription_lock:
if bidi_event not in self.subscriptions:
session = Session(self.conn)
result = session.subscribe([bidi_event], contexts=contexts)
sub_id = (
result.get("subscription") if isinstance(result, dict) else None
)
self.subscriptions[bidi_event] = {
"callbacks": [],
"subscription_id": sub_id,
}

def unsubscribe_from_event(self, bidi_event: str) -> None:
"""Unsubscribe from a BiDi event if no more callbacks exist."""
with self._subscription_lock:
entry = self.subscriptions.get(bidi_event)
if entry is not None and not entry["callbacks"]:
session = Session(self.conn)
sub_id = entry.get("subscription_id")
if sub_id:
session.unsubscribe(subscriptions=[sub_id])
else:
session.unsubscribe(events=[bidi_event])
del self.subscriptions[bidi_event]

def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:
with self._subscription_lock:
self.subscriptions[bidi_event]["callbacks"].append(callback_id)

def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:
with self._subscription_lock:
entry = self.subscriptions.get(bidi_event)
if entry and callback_id in entry["callbacks"]:
entry["callbacks"].remove(callback_id)

def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:
event_config = self.validate_event(event)
# Use the event wrapper for add_callback
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
callback_id = self.conn.add_callback(event_wrapper, callback)
self.subscribe_to_event(event_config.bidi_event, contexts)
self.add_callback_to_tracking(event_config.bidi_event, callback_id)
return callback_id

def remove_event_handler(self, event: str, callback_id: int) -> None:
event_config = self.validate_event(event)
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
self.conn.remove_callback(event_wrapper, callback_id)
self.remove_callback_from_tracking(event_config.bidi_event, callback_id)
self.unsubscribe_from_event(event_config.bidi_event)

def clear_event_handlers(self) -> None:
"""Clear all event handlers."""
with self._subscription_lock:
if not self.subscriptions:
return
session = Session(self.conn)
for bidi_event, entry in list(self.subscriptions.items()):
event_wrapper = self._event_wrappers.get(bidi_event)
callbacks = entry["callbacks"] if isinstance(entry, dict) else entry
if event_wrapper:
for callback_id in callbacks:
self.conn.remove_callback(event_wrapper, callback_id)
sub_id = (
entry.get("subscription_id") if isinstance(entry, dict) else None
)
if sub_id:
session.unsubscribe(subscriptions=[sub_id])
else:
session.unsubscribe(events=[bidi_event])
self.subscriptions.clear()

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _EventManager and _EventWrapper classes are duplicated across multiple generated modules (log.py, network.py, input.py). This is significant code duplication (~200+ lines per module). These classes should be extracted to a shared module (e.g., common.py or a new event_manager.py) and imported by the generated modules to follow the DRY principle and reduce maintenance burden.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 11, 2026 12:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 299 out of 438 changed files in this pull request and generated 24 comments.

@AutomatedTester AutomatedTester marked this pull request as ready for review March 11, 2026 14:16
Copilot AI review requested due to automatic review settings March 11, 2026 14:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 33 changed files in this pull request and generated 13 comments.

Comment on lines 102 to +105
def test_client_window_state_constants(driver):
assert ClientWindowState.FULLSCREEN == "fullscreen"
assert ClientWindowState.MAXIMIZED == "maximized"
assert ClientWindowState.MINIMIZED == "minimized"
assert ClientWindowState.NORMAL == "normal"
"""Test ClientWindowNamedState constants."""
assert ClientWindowNamedState.MAXIMIZED == "maximized"
assert ClientWindowNamedState.MINIMIZED == "minimized"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientWindowNamedState also defines FULLSCREEN and NORMAL, but this test now only asserts MAXIMIZED/MINIMIZED. Please add assertions for the remaining constants so regressions are caught.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AutomatedTester is there a reason you removed the checks for fullscreen and normal?

def remove_user_context(self, user_context: Any | None = None):
"""Execute browser.removeUserContext."""
if user_context is None:
raise TypeError("remove_user_context() missing required argument: {{snake_param!r}}")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeError message contains an unexpanded template placeholder ({{snake_param!r}}), which will be exposed to users. The generator should emit the actual missing parameter name (or drop the manual check and let Python’s own missing-arg TypeError fire).

Suggested change
raise TypeError("remove_user_context() missing required argument: {{snake_param!r}}")
raise TypeError("remove_user_context() missing required argument: 'user_context'")

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case in a few files, looks like the issue is this line in generate_bidi.py:

body += ( f' raise TypeError("{msg} {{{{snake_param!r}}}}")\n' )

Comment on lines +222 to +225
"""Execute session.subscribe."""
if events is None:
raise TypeError("subscribe() missing required argument: {{snake_param!r}}")

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeError message contains an unexpanded template placeholder ({{snake_param!r}}). The generator should substitute the actual parameter name (or avoid generating this manual required-arg check).

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +148
def to_bidi_dict(self) -> dict:
"""Serialize to the BiDi wire-protocol dict."""
result: dict = {}
if self.name is not None:
result["name"] = self.name
if self.value is not None:
result["value"] = self.value.to_dict()
result["value"] = self.value.to_bidi_dict() if hasattr(self.value, "to_bidi_dict") else self.value
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CookieFilter/PartialCookie/BytesValue switched to to_bidi_dict(); if callers were using the previous to_dict() method, this is a breaking change. Consider keeping to_dict() as a deprecated alias to preserve backward compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +436 to +456
def execute(
self, driver_command: str, params: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Sends a command to be executed by a command.CommandExecutor.

Args:
driver_command: The name of the command to execute as a string.
driver_command: The name of the command to execute as a string. Can also be a generator
for BiDi protocol commands.
params: A dictionary of named parameters to send with the command.

Returns:
The command's JSON response loaded into a dictionary object.
"""
# Handle BiDi generator commands
if inspect.isgenerator(driver_command):
# BiDi command: route through the WebSocket connection, not the
# HTTP RemoteConnection which only accepts (command, params) pairs.
if not self._websocket_connection:
self._start_bidi()
assert self._websocket_connection is not None
return self._websocket_connection.execute(driver_command)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() now supports BiDi generator commands, but the type annotation still declares driver_command as str. Please update the signature (and docstring) to accept a generator type as well, so type checkers/IDEs match the actual supported API.

Copilot uses AI. Check for mistakes.
Comment on lines +194 to 198
def new(self, capabilities: Any | None = None):
"""Execute session.new."""
if capabilities is None:
raise TypeError("new() missing required argument: {{snake_param!r}}")

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeError message contains an unexpanded template placeholder ({{snake_param!r}}). The generator should substitute the actual parameter name (or avoid generating this manual required-arg check).

Copilot uses AI. Check for mistakes.
if self.prompt is not None:
result["prompt"] = self.prompt
return result

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming UserPromptHandler.to_dict() to to_bidi_dict() is a breaking change for existing callers. Consider keeping to_dict as a backward-compatible (possibly deprecated) alias that forwards to to_bidi_dict().

Suggested change
def to_dict(self) -> dict:
"""Backward-compatible alias for to_bidi_dict()."""
return self.to_bidi_dict()

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we been alerting users about this work? @AutomatedTester @diemol we might want to release a blog post or something for instances of direct change/removal

Comment on lines +1 to +4
# DO NOT EDIT THIS FILE!
#
# http://www.apache.org/licenses/LICENSE-2.0
# This file is generated from the WebDriver BiDi specification. If you need to make
# changes, edit the generator and regenerate all of the modules.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated BiDi module header removed the standard Selenium Apache 2.0 / SFC license header and replaced it with a short “DO NOT EDIT” banner. Most Python sources in this repo keep the full header (e.g., py/selenium/webdriver/common/action_chains.py:1-16). Please ensure the generator preserves the license header (you can keep the “DO NOT EDIT” notice below it if desired).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we no longer need the licenses?

Copy link
Contributor

@shbenzer shbenzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is genuinely impressive - awesome work @AutomatedTester!

Copilot AI review requested due to automatic review settings March 13, 2026 12:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 33 changed files in this pull request and generated 7 comments.

Comment on lines +85 to +86
class setNetworkConditionsParameters:
"""setNetworkConditionsParameters."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Comment on lines +108 to 110
def uninstall(self, extension: str | dict):
"""Uninstall a web extension.

Comment on lines +26 to +42
def command_builder(
method: str, params: dict[str, Any]
) -> Generator[dict[str, Any], Any, Any]:
"""Build a BiDi command generator.

Args:
method: The method to execute.
params: The parameters to pass to the method. Default is None.
method: The BiDi method name (e.g., "session.status", "browser.close")
params: The parameters for the command

Yields:
A dictionary representing the BiDi command

Returns:
The response from the command execution.
The result from the BiDi command execution
"""
if params is None:
params = {}

command = {"method": method, "params": params}
cmd = yield command
return cmd
result = yield {"method": method, "params": params}
return result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per comment in browser_context, we might want to implement handling for instances where we need messages to contain None. I only know of the browser_context instance, but we might need to handle more in the future

Comment on lines +278 to +279
class disownDataParameters:
"""disownDataParameters."""
Copy link
Contributor

@shbenzer shbenzer Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree w/o copilot, suggested change looks fine per _camel_to_snake()

Copy link
Contributor

@shbenzer shbenzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is the last bidi test failing, hopefully this helps!

"userContexts": user_contexts,
"devicePixelRatio": device_pixel_ratio,
}
params = {k: v for k, v in params.items() if v is not None}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason the test_set_viewport_back_to_default(driver, pages) test fails is that when setting viewport to default, you need to send values of None for viewport and device_pixel_ratio, params = {k: v for k, v in params.items() if v is not None} removes those. In the future, we might need to make a specific case in the manifest for instances in BiDi specs where None needs to be set, but to get all your tests passing for this pr you can just make set_viewport() a specific case

@qodo-code-review
Copy link
Contributor

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Python / Lint / Lint

Failed stage: Run Bazel [❌]

Failed test name: ""

Failure summary:

The action failed during the lint step py:lint because ruff check detected (and auto-fixed)
formatting/lint issues, then exited non-zero, which caused the Bazel/Rake task to fail.
- bazel run
//py:ruff-check ran bazel-bin/py/private/ruff_check and reported: Fixed 18 errors / Found 18 errors
(18 fixed, 0 remaining).
- Because fixes were required (i.e., the checked-in code was not already
compliant), the lint task failed with exit code 1 (rake_tasks/bazel.rb:57 and
rake_tasks/python.rake:180, task TOP => py:lint), leading to ##[error]Process completed with exit
code 1.
- The reported issues include I001 (unsorted-imports) and UP037 (quoted-annotation) across
several files under py/selenium/webdriver/common/bidi/ (e.g., browser.py, log.py, network.py,
script.py, session.py, storage.py, webextension.py).

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

1538:  env:
1539:  GITHUB_TOKEN: ***
1540:  SEL_M2_USER: 
1541:  SEL_M2_PASS: 
1542:  TWINE_PASSWORD: 
1543:  GEM_HOST_API_KEY: 
1544:  NUGET_API_KEY: 
1545:  NODE_AUTH_TOKEN: 
1546:  BAZELISK_GITHUB_TOKEN: ***
1547:  MSYS_NO_PATHCONV: 1
1548:  MSYS2_ARG_CONV_EXCL: *
1549:  ##[endgroup]
1550:  Running ruff check...
1551:  Executing: bazel run //py:ruff-check
1552:  go aborted!
1553:  bazel run //py:ruff-check failed with exit code: 1
1554:  Output: Extracting Bazel installation...
...

1568:  �[32mAnalyzing:�[0m target //py:ruff-check (2 packages loaded, 0 targets configured)
1569:  �[32mAnalyzing:�[0m target //py:ruff-check (80 packages loaded, 11 targets configured)
1570:  �[32mAnalyzing:�[0m target //py:ruff-check (116 packages loaded, 803 targets configured)
1571:  �[32mAnalyzing:�[0m target //py:ruff-check (146 packages loaded, 2610 targets configured)
1572:  �[32mAnalyzing:�[0m target //py:ruff-check (157 packages loaded, 3761 targets configured)
1573:  �[32mINFO: �[0mAnalyzed target //py:ruff-check (164 packages loaded, 7513 targets configured).
1574:  �[32m[1 / 1]�[0m no actions running
1575:  �[32mINFO: �[0mFound 1 target...
1576:  Target //py/private:ruff_check up-to-date:
1577:  bazel-bin/py/private/ruff_check
1578:  �[32mINFO: �[0mElapsed time: 15.255s, Critical Path: 1.00s
1579:  �[32mINFO: �[0m8 processes: 8 internal.
1580:  �[32mINFO: �[0mBuild completed successfully, 8 total actions
1581:  �[32mINFO: �[0mRunning command line: bazel-bin/py/private/ruff_check
1582:  �[0m
1583:  Fixed 18 errors:
1584:  - py/selenium/webdriver/common/bidi/browser.py:
...

1593:  1 × UP037 (quoted-annotation)
1594:  - py/selenium/webdriver/common/bidi/log.py:
1595:  2 × UP037 (quoted-annotation)
1596:  1 × I001 (unsorted-imports)
1597:  - py/selenium/webdriver/common/bidi/network.py:
1598:  1 × I001 (unsorted-imports)
1599:  - py/selenium/webdriver/common/bidi/script.py:
1600:  3 × I001 (unsorted-imports)
1601:  - py/selenium/webdriver/common/bidi/session.py:
1602:  1 × I001 (unsorted-imports)
1603:  - py/selenium/webdriver/common/bidi/storage.py:
1604:  1 × I001 (unsorted-imports)
1605:  1 × UP037 (quoted-annotation)
1606:  - py/selenium/webdriver/common/bidi/webextension.py:
1607:  1 × I001 (unsorted-imports)
1608:  Found 18 errors (18 fixed, 0 remaining).
1609:  /home/runner/work/selenium/selenium/rake_tasks/bazel.rb:57:in `execute'
1610:  /home/runner/work/selenium/selenium/rake_tasks/python.rake:180:in `block in <main>'
1611:  org/jruby/ext/monitor/Monitor.java:82:in `synchronize'
1612:  Tasks: TOP => py:lint
1613:  (See full trace by running task with --trace)
1614:  ##[error]Process completed with exit code 1.
1615:  ##[group]Run ./scripts/github-actions/rerun-failures.sh './go py:lint' 'false'
...

1666:  With the provided path, there will be 1 file uploaded
1667:  Artifact name is valid!
1668:  Root directory input is valid!
1669:  Beginning upload of artifact content to blob storage
1670:  Uploaded bytes 134
1671:  Finished uploading artifact content to blob storage!
1672:  SHA256 digest of uploaded artifact zip is a204c3c89efb28545421ac113db02a4a17cdc5b77a319f4dbc73995f654fd85e
1673:  Finalizing artifact upload
1674:  Artifact test-logs-ubuntu-Lint-.zip successfully finalized. Artifact ID 5925553564
1675:  Artifact test-logs-ubuntu-Lint- has been successfully uploaded! Final size is 134 bytes. Artifact ID is 5925553564
1676:  Artifact download URL: https://github.com/SeleniumHQ/selenium/actions/runs/23092068559/artifacts/5925553564
1677:  ##[group]Run avail=$(df -k "$GITHUB_WORKSPACE" | awk 'NR==2 {printf "%.0f", $4/1024/1024}')
1678:  �[36;1mavail=$(df -k "$GITHUB_WORKSPACE" | awk 'NR==2 {printf "%.0f", $4/1024/1024}')�[0m
1679:  �[36;1mecho "Remaining disk space: ${avail}GB"�[0m
1680:  �[36;1mif [ "$avail" -lt 5 ]; then�[0m
1681:  �[36;1m  echo "::error::Low disk space: ${avail}GB remaining"�[0m
1682:  �[36;1m  exit 1�[0m

Copy link
Contributor

@shbenzer shbenzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responding to Copilot

Comment on lines 102 to +105
def test_client_window_state_constants(driver):
assert ClientWindowState.FULLSCREEN == "fullscreen"
assert ClientWindowState.MAXIMIZED == "maximized"
assert ClientWindowState.MINIMIZED == "minimized"
assert ClientWindowState.NORMAL == "normal"
"""Test ClientWindowNamedState constants."""
assert ClientWindowNamedState.MAXIMIZED == "maximized"
assert ClientWindowNamedState.MINIMIZED == "minimized"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AutomatedTester is there a reason you removed the checks for fullscreen and normal?

Comment on lines +278 to +279
class disownDataParameters:
"""disownDataParameters."""
Copy link
Contributor

@shbenzer shbenzer Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree w/o copilot, suggested change looks fine per _camel_to_snake()

Comment on lines +1 to +4
# DO NOT EDIT THIS FILE!
#
# http://www.apache.org/licenses/LICENSE-2.0
# This file is generated from the WebDriver BiDi specification. If you need to make
# changes, edit the generator and regenerate all of the modules.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we no longer need the licenses?

def remove_user_context(self, user_context: Any | None = None):
"""Execute browser.removeUserContext."""
if user_context is None:
raise TypeError("remove_user_context() missing required argument: {{snake_param!r}}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case in a few files, looks like the issue is this line in generate_bidi.py:

body += ( f' raise TypeError("{msg} {{{{snake_param!r}}}}")\n' )

if self.prompt is not None:
result["prompt"] = self.prompt
return result

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we been alerting users about this work? @AutomatedTester @diemol we might want to release a blog post or something for instances of direct change/removal

Comment on lines +85 to +86
class setNetworkConditionsParameters:
"""setNetworkConditionsParameters."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Comment on lines +26 to +42
def command_builder(
method: str, params: dict[str, Any]
) -> Generator[dict[str, Any], Any, Any]:
"""Build a BiDi command generator.

Args:
method: The method to execute.
params: The parameters to pass to the method. Default is None.
method: The BiDi method name (e.g., "session.status", "browser.close")
params: The parameters for the command

Yields:
A dictionary representing the BiDi command

Returns:
The response from the command execution.
The result from the BiDi command execution
"""
if params is None:
params = {}

command = {"method": method, "params": params}
cmd = yield command
return cmd
result = yield {"method": method, "params": params}
return result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per comment in browser_context, we might want to implement handling for instances where we need messages to contain None. I only know of the browser_context instance, but we might need to handle more in the future

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-py Python Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.