Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/private/_event_manager.py
10192 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
"""Shared event management helpers for generated WebDriver BiDi modules.
19
20
``EventConfig``, ``_EventWrapper``, and ``_EventManager`` are emitted
21
identically into every generated module that exposes events. Rather than
22
duplicating this logic across those modules, they are defined once here and
23
copied into generated outputs by Bazel.
24
"""
25
26
from __future__ import annotations
27
28
import threading
29
from collections.abc import Callable
30
from dataclasses import dataclass
31
from typing import Any
32
33
from selenium.webdriver.common.bidi.session import Session
34
35
36
@dataclass
37
class EventConfig:
38
"""Configuration for a BiDi event."""
39
40
event_key: str
41
bidi_event: str
42
event_class: type
43
44
45
class _EventWrapper:
46
"""Wrapper to provide event_class attribute for WebSocketConnection callbacks."""
47
48
def __init__(self, bidi_event: str, event_class: type):
49
self.event_class = bidi_event # WebSocket expects the BiDi event name as event_class
50
self._python_class = event_class # Keep reference to Python dataclass for deserialization
51
52
def from_json(self, params: dict) -> Any:
53
"""Deserialize event params into the wrapped Python dataclass.
54
55
Args:
56
params: Raw BiDi event params with camelCase keys.
57
58
Returns:
59
An instance of the dataclass, or the raw dict on failure.
60
"""
61
if self._python_class is None or self._python_class is dict:
62
return params
63
try:
64
# Delegate to a classmethod from_json if the class defines one
65
if hasattr(self._python_class, "from_json") and callable(self._python_class.from_json):
66
return self._python_class.from_json(params)
67
import dataclasses as dc
68
69
snake_params = {self._camel_to_snake(k): v for k, v in params.items()}
70
if dc.is_dataclass(self._python_class):
71
valid_fields = {f.name for f in dc.fields(self._python_class)}
72
filtered = {k: v for k, v in snake_params.items() if k in valid_fields}
73
return self._python_class(**filtered)
74
return self._python_class(**snake_params)
75
except Exception:
76
return params
77
78
@staticmethod
79
def _camel_to_snake(name: str) -> str:
80
result = [name[0].lower()]
81
for char in name[1:]:
82
if char.isupper():
83
result.extend(["_", char.lower()])
84
else:
85
result.append(char)
86
return "".join(result)
87
88
89
class _EventManager:
90
"""Manages event subscriptions and callbacks."""
91
92
def __init__(self, conn, event_configs: dict[str, EventConfig]):
93
self.conn = conn
94
self.event_configs = event_configs
95
self.subscriptions: dict = {}
96
self._event_wrappers = {} # Cache of _EventWrapper objects
97
self._bidi_to_class = {config.bidi_event: config.event_class for config in event_configs.values()}
98
self._available_events = ", ".join(sorted(event_configs.keys()))
99
self._subscription_lock = threading.Lock()
100
101
# Create event wrappers for each event
102
for config in event_configs.values():
103
wrapper = _EventWrapper(config.bidi_event, config.event_class)
104
self._event_wrappers[config.bidi_event] = wrapper
105
106
def validate_event(self, event: str) -> EventConfig:
107
event_config = self.event_configs.get(event)
108
if not event_config:
109
raise ValueError(f"Event '{event}' not found. Available events: {self._available_events}")
110
return event_config
111
112
def subscribe_to_event(self, bidi_event: str, contexts: list[str] | None = None) -> None:
113
"""Subscribe to a BiDi event if not already subscribed."""
114
with self._subscription_lock:
115
if bidi_event not in self.subscriptions:
116
session = Session(self.conn)
117
result = session.subscribe([bidi_event], contexts=contexts)
118
sub_id = result.get("subscription") if isinstance(result, dict) else None
119
self.subscriptions[bidi_event] = {
120
"callbacks": [],
121
"subscription_id": sub_id,
122
}
123
124
def unsubscribe_from_event(self, bidi_event: str) -> None:
125
"""Unsubscribe from a BiDi event if no more callbacks exist."""
126
with self._subscription_lock:
127
entry = self.subscriptions.get(bidi_event)
128
if entry is not None and not entry["callbacks"]:
129
session = Session(self.conn)
130
sub_id = entry.get("subscription_id")
131
if sub_id:
132
session.unsubscribe(subscriptions=[sub_id])
133
else:
134
session.unsubscribe(events=[bidi_event])
135
del self.subscriptions[bidi_event]
136
137
def add_callback_to_tracking(self, bidi_event: str, callback_id: int) -> None:
138
with self._subscription_lock:
139
self.subscriptions[bidi_event]["callbacks"].append(callback_id)
140
141
def remove_callback_from_tracking(self, bidi_event: str, callback_id: int) -> None:
142
with self._subscription_lock:
143
entry = self.subscriptions.get(bidi_event)
144
if entry and callback_id in entry["callbacks"]:
145
entry["callbacks"].remove(callback_id)
146
147
def add_event_handler(self, event: str, callback: Callable, contexts: list[str] | None = None) -> int:
148
event_config = self.validate_event(event)
149
# Use the event wrapper for add_callback
150
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
151
callback_id = self.conn.add_callback(event_wrapper, callback)
152
self.subscribe_to_event(event_config.bidi_event, contexts)
153
self.add_callback_to_tracking(event_config.bidi_event, callback_id)
154
return callback_id
155
156
def remove_event_handler(self, event: str, callback_id: int) -> None:
157
event_config = self.validate_event(event)
158
event_wrapper = self._event_wrappers.get(event_config.bidi_event)
159
self.conn.remove_callback(event_wrapper, callback_id)
160
self.remove_callback_from_tracking(event_config.bidi_event, callback_id)
161
self.unsubscribe_from_event(event_config.bidi_event)
162
163
def clear_event_handlers(self) -> None:
164
"""Clear all event handlers."""
165
with self._subscription_lock:
166
if not self.subscriptions:
167
return
168
session = Session(self.conn)
169
for bidi_event, entry in list(self.subscriptions.items()):
170
event_wrapper = self._event_wrappers.get(bidi_event)
171
callbacks = entry["callbacks"] if isinstance(entry, dict) else entry
172
if event_wrapper:
173
for callback_id in callbacks:
174
self.conn.remove_callback(event_wrapper, callback_id)
175
sub_id = entry.get("subscription_id") if isinstance(entry, dict) else None
176
if sub_id:
177
session.unsubscribe(subscriptions=[sub_id])
178
else:
179
session.unsubscribe(events=[bidi_event])
180
self.subscriptions.clear()
181
182