Skip to content

Commit 91c9091

Browse files
Handle comments
1 parent 0319ff9 commit 91c9091

11 files changed

Lines changed: 142 additions & 955 deletions

File tree

py/generate_bidi.py

Lines changed: 7 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -591,16 +591,12 @@ def generate_code(self, enhancements: dict[str, Any] | None = None) -> str:
591591
# Collect needed imports to avoid duplicates
592592
needs_command_builder = bool(self.commands)
593593
needs_dataclass = self.commands or self.types or self.events
594-
needs_threading = self.events
595594
needs_callable = self.events
596-
needs_session = self.events
597595

598596
stdlib_imports = []
599597
local_imports = []
600598

601599
# Add imports (field import will be added conditionally after code generation)
602-
if needs_threading:
603-
stdlib_imports.append("import threading")
604600
if needs_callable:
605601
stdlib_imports.append("from collections.abc import Callable")
606602
if needs_dataclass:
@@ -611,9 +607,9 @@ def generate_code(self, enhancements: dict[str, Any] | None = None) -> str:
611607
local_imports.append(
612608
"from selenium.webdriver.common.bidi.common import command_builder"
613609
)
614-
if needs_session:
610+
if self.events:
615611
local_imports.append(
616-
"from selenium.webdriver.common.bidi.session import Session"
612+
"from selenium.webdriver.common.bidi._event_manager import EventConfig, _EventWrapper, _EventManager"
617613
)
618614

619615
code += "\n".join(stdlib_imports) + "\n"
@@ -797,165 +793,11 @@ def generate_code(self, enhancements: dict[str, Any] | None = None) -> str:
797793
"""
798794
code += "\n\n"
799795

800-
# Generate EventConfig and _EventManager for modules with events
801-
if self.events:
802-
# Generate EventConfig dataclass
803-
code += """@dataclass
804-
class EventConfig:
805-
\"\"\"Configuration for a BiDi event.\"\"\"
806-
event_key: str
807-
bidi_event: str
808-
event_class: type
809-
810-
811-
"""
812-
813-
# Generate _EventManager class
814-
code += """class _EventWrapper:
815-
\"\"\"Wrapper to provide event_class attribute for WebSocketConnection callbacks.\"\"\"
816-
def __init__(self, bidi_event: str, event_class: type):
817-
self.event_class = bidi_event # WebSocket expects the BiDi event name as event_class
818-
self._python_class = event_class # Keep reference to Python dataclass for deserialization
819-
820-
def from_json(self, params: dict) -> Any:
821-
\"\"\"Deserialize event params into the wrapped Python dataclass.
822-
823-
Args:
824-
params: Raw BiDi event params with camelCase keys.
825-
826-
Returns:
827-
An instance of the dataclass, or the raw dict on failure.
828-
\"\"\"
829-
if self._python_class is None or self._python_class is dict:
830-
return params
831-
try:
832-
# Delegate to a classmethod from_json if the class defines one
833-
if hasattr(self._python_class, \"from_json\") and callable(
834-
self._python_class.from_json
835-
):
836-
return self._python_class.from_json(params)
837-
import dataclasses as dc
838-
839-
snake_params = {self._camel_to_snake(k): v for k, v in params.items()}
840-
if dc.is_dataclass(self._python_class):
841-
valid_fields = {f.name for f in dc.fields(self._python_class)}
842-
filtered = {k: v for k, v in snake_params.items() if k in valid_fields}
843-
return self._python_class(**filtered)
844-
return self._python_class(**snake_params)
845-
except Exception:
846-
return params
847-
848-
@staticmethod
849-
def _camel_to_snake(name: str) -> str:
850-
result = [name[0].lower()]
851-
for char in name[1:]:
852-
if char.isupper():
853-
result.extend([\"_\", char.lower()])
854-
else:
855-
result.append(char)
856-
return \"\".join(result)
857-
858-
859-
class _EventManager:
860-
\"\"\"Manages event subscriptions and callbacks.\"\"\"
861-
862-
def __init__(self, conn, event_configs: dict[str, EventConfig]):
863-
self.conn = conn
864-
self.event_configs = event_configs
865-
self.subscriptions: dict = {}
866-
self._event_wrappers = {} # Cache of _EventWrapper objects
867-
self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}
868-
self._available_events = ", ".join(sorted(event_configs.keys()))
869-
self._subscription_lock = threading.Lock()
870-
871-
# Create event wrappers for each event
872-
for config in event_configs.values():
873-
wrapper = _EventWrapper(config.bidi_event, config.event_class)
874-
self._event_wrappers[config.bidi_event] = wrapper
875-
876-
def validate_event(self, event: str) -> EventConfig:
877-
event_config = self.event_configs.get(event)
878-
if not event_config:
879-
raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")
880-
return event_config
881-
882-
def subscribe_to_event(self, bidi_event: str, contexts: list[str] | None = None) -> None:
883-
\"\"\"Subscribe to a BiDi event if not already subscribed.\"\"\"
884-
with self._subscription_lock:
885-
if bidi_event not in self.subscriptions:
886-
session = Session(self.conn)
887-
result = session.subscribe([bidi_event], contexts=contexts)
888-
sub_id = (
889-
result.get(\"subscription\") if isinstance(result, dict) else None
890-
)
891-
self.subscriptions[bidi_event] = {
892-
\"callbacks\": [],
893-
\"subscription_id\": sub_id,
894-
}
895-
896-
def unsubscribe_from_event(self, bidi_event: str) -> None:
897-
\"\"\"Unsubscribe from a BiDi event if no more callbacks exist.\"\"\"
898-
with self._subscription_lock:
899-
entry = self.subscriptions.get(bidi_event)
900-
if entry is not None and not entry[\"callbacks\"]:
901-
session = Session(self.conn)
902-
sub_id = entry.get(\"subscription_id\")
903-
if sub_id:
904-
session.unsubscribe(subscriptions=[sub_id])
905-
else:
906-
session.unsubscribe(events=[bidi_event])
907-
del self.subscriptions[bidi_event]
908-
909-
def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:
910-
with self._subscription_lock:
911-
self.subscriptions[bidi_event][\"callbacks\"].append(callback_id)
912-
913-
def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:
914-
with self._subscription_lock:
915-
entry = self.subscriptions.get(bidi_event)
916-
if entry and callback_id in entry[\"callbacks\"]:
917-
entry[\"callbacks\"].remove(callback_id)
918-
919-
def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:
920-
event_config = self.validate_event(event)
921-
# Use the event wrapper for add_callback
922-
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
923-
callback_id = self.conn.add_callback(event_wrapper, callback)
924-
self.subscribe_to_event(event_config.bidi_event, contexts)
925-
self.add_callback_to_tracking(event_config.bidi_event, callback_id)
926-
return callback_id
927-
928-
def remove_event_handler(self, event: str, callback_id: int) -> None:
929-
event_config = self.validate_event(event)
930-
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
931-
self.conn.remove_callback(event_wrapper, callback_id)
932-
self.remove_callback_from_tracking(event_config.bidi_event, callback_id)
933-
self.unsubscribe_from_event(event_config.bidi_event)
934-
935-
def clear_event_handlers(self) -> None:
936-
\"\"\"Clear all event handlers.\"\"\"
937-
with self._subscription_lock:
938-
if not self.subscriptions:
939-
return
940-
session = Session(self.conn)
941-
for bidi_event, entry in list(self.subscriptions.items()):
942-
event_wrapper = self._event_wrappers.get(bidi_event)
943-
callbacks = entry[\"callbacks\"] if isinstance(entry, dict) else entry
944-
if event_wrapper:
945-
for callback_id in callbacks:
946-
self.conn.remove_callback(event_wrapper, callback_id)
947-
sub_id = (
948-
entry.get(\"subscription_id\") if isinstance(entry, dict) else None
949-
)
950-
if sub_id:
951-
session.unsubscribe(subscriptions=[sub_id])
952-
else:
953-
session.unsubscribe(events=[bidi_event])
954-
self.subscriptions.clear()
955-
956-
957-
"""
958-
code += "\n\n"
796+
# EventConfig, _EventWrapper, and _EventManager are imported from
797+
# selenium.webdriver.common.bidi._event_manager (see import section above)
798+
# rather than being duplicated inline in every generated module.
799+
if False: # placeholder to preserve indentation structure
800+
pass
959801

960802
# Generate class
961803
# Convert module name (camelCase or snake_case) to proper class name (PascalCase)

py/private/bidi_enhancements_manifest.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
# convenience NORMAL constant. In the BiDi spec "normal" is the state
8787
# represented by ClientWindowRectState, but exposing it here keeps the
8888
# Python API consistent with the old ClientWindowState enum.
89-
"exclude_types": ["ClientWindowNamedState"],
89+
"exclude_types": ["ClientWindowNamedState", "SetClientWindowStateParameters"],
9090
"extra_dataclasses": [
9191
'''class ClientWindowNamedState:
9292
"""Named states for a browser client window."""
@@ -95,6 +95,18 @@
9595
MAXIMIZED = "maximized"
9696
MINIMIZED = "minimized"
9797
NORMAL = "normal"''',
98+
'''@dataclass
99+
class SetClientWindowStateParameters:
100+
"""SetClientWindowStateParameters.
101+
102+
The ``state`` field is required and must be either a named-state string
103+
(e.g. ``ClientWindowNamedState.MAXIMIZED``) or a
104+
:class:`ClientWindowRectState` instance. ``client_window`` is the ID of
105+
the window to affect.
106+
"""
107+
108+
client_window: Any | None = None
109+
state: Any | None = None''',
98110
],
99111
# Override the generator-produced set_download_behavior so that
100112
# downloadBehavior is never stripped by the generic None filter.
@@ -1141,7 +1153,11 @@ def __init__(self, type: Any | None, value: Any | None) -> None:
11411153
self.value = value
11421154
11431155
def to_bidi_dict(self) -> dict:
1144-
return {"type": self.type, "value": self.value}''',
1156+
return {"type": self.type, "value": self.value}
1157+
1158+
def to_dict(self) -> dict:
1159+
"""Backward-compatible alias for to_bidi_dict()."""
1160+
return self.to_bidi_dict()''',
11451161
'''class SameSite:
11461162
"""SameSite cookie attribute values."""
11471163
@@ -1219,7 +1235,11 @@ def to_bidi_dict(self) -> dict:
12191235
result["sameSite"] = self.same_site
12201236
if self.expiry is not None:
12211237
result["expiry"] = self.expiry
1222-
return result''',
1238+
return result
1239+
1240+
def to_dict(self) -> dict:
1241+
"""Backward-compatible alias for to_bidi_dict()."""
1242+
return self.to_bidi_dict()''',
12231243
# Custom PartialCookie with camelCase serialization
12241244
'''@dataclass
12251245
class PartialCookie:
@@ -1253,7 +1273,11 @@ def to_bidi_dict(self) -> dict:
12531273
result["sameSite"] = self.same_site
12541274
if self.expiry is not None:
12551275
result["expiry"] = self.expiry
1256-
return result''',
1276+
return result
1277+
1278+
def to_dict(self) -> dict:
1279+
"""Backward-compatible alias for to_bidi_dict()."""
1280+
return self.to_bidi_dict()''',
12571281
# BrowsingContextPartitionDescriptor: first positional arg is *context*
12581282
# (the auto-generated dataclass had `type` first, breaking positional
12591283
# usage like BrowsingContextPartitionDescriptor(driver.current_window_handle))
@@ -1270,7 +1294,12 @@ def __init__(self, context: Any = None, type: str = "context") -> None:
12701294
self.type = type
12711295
12721296
def to_bidi_dict(self) -> dict:
1273-
return {"type": "context", "context": self.context}''',
1297+
return {"type": "context", "context": self.context}
1298+
1299+
def to_dict(self) -> dict:
1300+
"""Backward-compatible alias for to_bidi_dict()."""
1301+
return self.to_bidi_dict()''',
1302+
12741303
# StorageKeyPartitionDescriptor with camelCase serialization
12751304
'''@dataclass
12761305
class StorageKeyPartitionDescriptor:
@@ -1287,7 +1316,11 @@ def to_bidi_dict(self) -> dict:
12871316
result["userContext"] = self.user_context
12881317
if self.source_origin is not None:
12891318
result["sourceOrigin"] = self.source_origin
1290-
return result''',
1319+
return result
1320+
1321+
def to_dict(self) -> dict:
1322+
"""Backward-compatible alias for to_bidi_dict()."""
1323+
return self.to_bidi_dict()''',
12911324
],
12921325
# Override the generated Storage class methods (Python's last-definition-
12931326
# wins semantics means these extra_methods shadow the generated ones).
@@ -1335,7 +1368,19 @@ def to_bidi_dict(self) -> dict:
13351368
params = {k: v for k, v in params.items() if v is not None}
13361369
cmd = command_builder("storage.setCookie", params)
13371370
result = self._conn.execute(cmd)
1371+
if isinstance(result, dict):
1372+
pk_raw = result.get("partitionKey")
1373+
pk = (
1374+
PartitionKey(
1375+
user_context=pk_raw.get("userContext"),
1376+
source_origin=pk_raw.get("sourceOrigin"),
1377+
)
1378+
if isinstance(pk_raw, dict)
1379+
else None
1380+
)
1381+
return SetCookieResult(partition_key=pk)
13381382
return result''',
1383+
13391384
''' def delete_cookies(self, filter=None, partition=None):
13401385
"""Execute storage.deleteCookies."""
13411386
if filter and hasattr(filter, "to_bidi_dict"):
@@ -1349,6 +1394,17 @@ def to_bidi_dict(self) -> dict:
13491394
params = {k: v for k, v in params.items() if v is not None}
13501395
cmd = command_builder("storage.deleteCookies", params)
13511396
result = self._conn.execute(cmd)
1397+
if isinstance(result, dict):
1398+
pk_raw = result.get("partitionKey")
1399+
pk = (
1400+
PartitionKey(
1401+
user_context=pk_raw.get("userContext"),
1402+
source_origin=pk_raw.get("sourceOrigin"),
1403+
)
1404+
if isinstance(pk_raw, dict)
1405+
else None
1406+
)
1407+
return DeleteCookiesResult(partition_key=pk)
13521408
return result''',
13531409
],
13541410
},
@@ -1383,7 +1439,11 @@ def to_bidi_dict(self) -> dict:
13831439
result["file"] = self.file
13841440
if self.prompt is not None:
13851441
result["prompt"] = self.prompt
1386-
return result''',
1442+
return result
1443+
1444+
def to_dict(self) -> dict:
1445+
"""Backward-compatible alias for to_bidi_dict()."""
1446+
return self.to_bidi_dict()''',
13871447
],
13881448
},
13891449

py/selenium/webdriver/common/bidi/browser.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,6 @@ class RemoveUserContextParameters:
139139
user_context: Any | None = None
140140

141141

142-
@dataclass
143-
class SetClientWindowStateParameters:
144-
"""SetClientWindowStateParameters."""
145-
146-
client_window: Any | None = None
147-
148-
149142
@dataclass
150143
class ClientWindowRectState:
151144
"""ClientWindowRectState."""
@@ -188,6 +181,19 @@ class ClientWindowNamedState:
188181
MINIMIZED = "minimized"
189182
NORMAL = "normal"
190183

184+
@dataclass
185+
class SetClientWindowStateParameters:
186+
"""SetClientWindowStateParameters.
187+
188+
The ``state`` field is required and must be either a named-state string
189+
(e.g. ``ClientWindowNamedState.MAXIMIZED``) or a
190+
:class:`ClientWindowRectState` instance. ``client_window`` is the ID of
191+
the window to affect.
192+
"""
193+
194+
client_window: Any | None = None
195+
state: Any | None = None
196+
191197
class Browser:
192198
"""WebDriver BiDi browser module."""
193199

0 commit comments

Comments
 (0)