inspect-ai 0.3.75__py3-none-any.whl → 0.3.77__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inspect_ai/_cli/eval.py +16 -0
- inspect_ai/_display/core/results.py +6 -1
- inspect_ai/_eval/eval.py +8 -1
- inspect_ai/_eval/evalset.py +6 -2
- inspect_ai/_eval/registry.py +3 -5
- inspect_ai/_eval/run.py +7 -2
- inspect_ai/_eval/task/run.py +4 -0
- inspect_ai/_util/content.py +3 -0
- inspect_ai/_util/logger.py +3 -0
- inspect_ai/_view/www/dist/assets/index.css +28 -16
- inspect_ai/_view/www/dist/assets/index.js +4811 -4609
- inspect_ai/_view/www/log-schema.json +79 -9
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +22 -4
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +1 -1
- inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
- inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
- inspect_ai/_view/www/src/types/log.d.ts +11 -5
- inspect_ai/log/_recorders/json.py +8 -0
- inspect_ai/log/_transcript.py +13 -4
- inspect_ai/model/_call_tools.py +13 -4
- inspect_ai/model/_chat_message.py +3 -0
- inspect_ai/model/_model.py +5 -1
- inspect_ai/model/_model_output.py +6 -1
- inspect_ai/model/_openai.py +78 -10
- inspect_ai/model/_openai_responses.py +277 -0
- inspect_ai/model/_providers/anthropic.py +134 -75
- inspect_ai/model/_providers/azureai.py +2 -2
- inspect_ai/model/_providers/mistral.py +29 -13
- inspect_ai/model/_providers/openai.py +64 -57
- inspect_ai/model/_providers/openai_responses.py +177 -0
- inspect_ai/model/_providers/openrouter.py +52 -2
- inspect_ai/model/_providers/providers.py +1 -1
- inspect_ai/model/_providers/vertex.py +5 -2
- inspect_ai/tool/__init__.py +6 -0
- inspect_ai/tool/_tool.py +23 -3
- inspect_ai/tool/_tool_call.py +5 -2
- inspect_ai/tool/_tool_support_helpers.py +200 -0
- inspect_ai/tool/_tools/_bash_session.py +119 -0
- inspect_ai/tool/_tools/_computer/_computer.py +1 -1
- inspect_ai/tool/_tools/_text_editor.py +121 -0
- inspect_ai/tool/_tools/_think.py +48 -0
- inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
- inspect_ai/tool/_tools/_web_search.py +1 -1
- inspect_ai/util/_json.py +28 -0
- inspect_ai/util/_sandbox/context.py +16 -7
- inspect_ai/util/_sandbox/docker/config.py +1 -1
- inspect_ai/util/_sandbox/docker/internal.py +3 -3
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/METADATA +5 -2
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/RECORD +56 -80
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/WHEEL +1 -1
- inspect_ai/model/_image.py +0 -15
- inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
- inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
- inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
- inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
- inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
- inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
- inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
- inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
- inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
- inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
- inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
- inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
- inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
- inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info/licenses}/LICENSE +0 -0
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/top_level.txt +0 -0
@@ -1,71 +0,0 @@
|
|
1
|
-
from functools import reduce
|
2
|
-
from typing import Iterable, TypedDict
|
3
|
-
|
4
|
-
from accessibility_tree_node import AccessibilityTreeNode
|
5
|
-
from cdp.a11y import AXNode, AXNodeId
|
6
|
-
from cdp.dom_snapshot import DOMSnapshot, create_snapshot_context
|
7
|
-
from rectangle import Rectangle
|
8
|
-
|
9
|
-
_AccType = tuple[
|
10
|
-
AXNode | None,
|
11
|
-
dict[AXNodeId, AXNode],
|
12
|
-
]
|
13
|
-
|
14
|
-
|
15
|
-
class AccessibilityTree(TypedDict):
|
16
|
-
root: AccessibilityTreeNode
|
17
|
-
nodes: dict[AXNodeId, AccessibilityTreeNode]
|
18
|
-
|
19
|
-
|
20
|
-
def create_accessibility_tree(
|
21
|
-
*,
|
22
|
-
ax_nodes: Iterable[AXNode],
|
23
|
-
dom_snapshot: DOMSnapshot,
|
24
|
-
device_scale_factor: float,
|
25
|
-
window_bounds: Rectangle,
|
26
|
-
) -> AccessibilityTree | None:
|
27
|
-
"""
|
28
|
-
Creates an accessibility tree from the given Chrome DevTools Protocol AX nodes and DOM snapshot.
|
29
|
-
|
30
|
-
Args:
|
31
|
-
ax_nodes (Iterable[AXNode]): An iterable of AXNode objects representing the accessibility nodes.
|
32
|
-
dom_snapshot (DOMSnapshot): A snapshot of the DOM at the time of accessibility tree creation.
|
33
|
-
device_scale_factor (float): The scale factor of the device.
|
34
|
-
window_bounds (Bounds): The bounds of the window.
|
35
|
-
|
36
|
-
Returns:
|
37
|
-
AccessibilityTree: The accessibility tree.
|
38
|
-
"""
|
39
|
-
|
40
|
-
# first make a dict of AXNodeId's to AXNode's and find the root on the way
|
41
|
-
def reducer(acc: _AccType, ax_node: AXNode) -> _AccType:
|
42
|
-
root_node, nodes = acc
|
43
|
-
nodes[ax_node.nodeId] = ax_node
|
44
|
-
return (
|
45
|
-
# TODO: What do we want for multiple roots?
|
46
|
-
root_node or (ax_node if ax_node.parentId is None else None),
|
47
|
-
nodes,
|
48
|
-
)
|
49
|
-
|
50
|
-
initial_acc: _AccType = (None, {}) # The inference engine is weak
|
51
|
-
root_node, nodes = reduce(reducer, ax_nodes, initial_acc)
|
52
|
-
|
53
|
-
if not root_node:
|
54
|
-
return None
|
55
|
-
|
56
|
-
# Now create the AccessibilityTreeNode hierarchy
|
57
|
-
snapshot_context = create_snapshot_context(dom_snapshot)
|
58
|
-
all_accessibility_tree_nodes: dict[AXNodeId, AccessibilityTreeNode] = {}
|
59
|
-
|
60
|
-
return AccessibilityTree(
|
61
|
-
root=AccessibilityTreeNode(
|
62
|
-
ax_node=root_node,
|
63
|
-
ax_nodes=nodes,
|
64
|
-
parent=None,
|
65
|
-
all_accessibility_tree_nodes=all_accessibility_tree_nodes,
|
66
|
-
snapshot_context=snapshot_context,
|
67
|
-
device_scale_factor=device_scale_factor,
|
68
|
-
window_bounds=window_bounds,
|
69
|
-
),
|
70
|
-
nodes=all_accessibility_tree_nodes,
|
71
|
-
)
|
@@ -1,323 +0,0 @@
|
|
1
|
-
from functools import reduce
|
2
|
-
from typing import Optional
|
3
|
-
|
4
|
-
from cdp.a11y import (
|
5
|
-
AXNode,
|
6
|
-
AXNodeId,
|
7
|
-
AXProperty,
|
8
|
-
AXPropertyName,
|
9
|
-
node_has_property,
|
10
|
-
string_from_ax_value,
|
11
|
-
)
|
12
|
-
from cdp.dom_snapshot import (
|
13
|
-
DOMSnapshotContext,
|
14
|
-
bounds_for_node_index,
|
15
|
-
current_url_src_for_node_index,
|
16
|
-
index_for_node_id,
|
17
|
-
text_value_for_node_index,
|
18
|
-
)
|
19
|
-
from rectangle import Rectangle
|
20
|
-
|
21
|
-
# Properties to ignore when printing out the accessibility tree.
|
22
|
-
_IGNORED_AT_PROPERTIES: set[AXPropertyName] = {
|
23
|
-
"focusable",
|
24
|
-
"readonly",
|
25
|
-
"level",
|
26
|
-
"settable",
|
27
|
-
"multiline",
|
28
|
-
"invalid",
|
29
|
-
}
|
30
|
-
|
31
|
-
# The AXRole enum is here (https://chromium.googlesource.com/chromium/chromium/+/HEAD/ui/accessibility/ax_enums.idl#62)
|
32
|
-
_ROLES_IGNORED = {"horizontal_rule", "ignored"}
|
33
|
-
|
34
|
-
# These are noisy and provide no real benefit unless there's a name associated with them
|
35
|
-
_ROLES_REQUIRING_A_NAME = {"generic", "paragraph", "div"}
|
36
|
-
|
37
|
-
# Used for debugging, include the elements bounding rect during output.
|
38
|
-
_INCLUDE_BOUNDS_IN_OUTPUT = False
|
39
|
-
|
40
|
-
|
41
|
-
class AccessibilityTreeNode:
|
42
|
-
def __init__(
|
43
|
-
self,
|
44
|
-
*,
|
45
|
-
ax_node: AXNode,
|
46
|
-
ax_nodes: dict[AXNodeId, AXNode],
|
47
|
-
parent: Optional["AccessibilityTreeNode"],
|
48
|
-
all_accessibility_tree_nodes: dict[AXNodeId, "AccessibilityTreeNode"],
|
49
|
-
snapshot_context: DOMSnapshotContext,
|
50
|
-
device_scale_factor: float,
|
51
|
-
window_bounds: Rectangle,
|
52
|
-
):
|
53
|
-
all_accessibility_tree_nodes[ax_node.nodeId] = self
|
54
|
-
|
55
|
-
self._is_expanded = node_has_property(ax_node, "expanded")
|
56
|
-
self._ax_node = ax_node
|
57
|
-
self._parent = parent
|
58
|
-
self._is_ignored: bool | None = None
|
59
|
-
self._bounds: Rectangle | None = None
|
60
|
-
self._is_visible: bool | None = None
|
61
|
-
self._current_src_url: str | None = None
|
62
|
-
self._input: str | None = None
|
63
|
-
self.__closest_non_ignored_parent: AccessibilityTreeNode | None = None
|
64
|
-
|
65
|
-
dom_node_index = (
|
66
|
-
index_for_node_id(ax_node.backendDOMNodeId, snapshot_context)
|
67
|
-
if ax_node.backendDOMNodeId
|
68
|
-
else None
|
69
|
-
)
|
70
|
-
|
71
|
-
# The following three variables (_bounds, _is_visible, and _current_src_url) depend on
|
72
|
-
# info from the DOM element associated with this AXNode
|
73
|
-
if dom_node_index:
|
74
|
-
self._bounds = (
|
75
|
-
Rectangle(*layout_bounds).scale(1 / device_scale_factor)
|
76
|
-
if (
|
77
|
-
layout_bounds := bounds_for_node_index(
|
78
|
-
dom_node_index, snapshot_context
|
79
|
-
)
|
80
|
-
)
|
81
|
-
is not None
|
82
|
-
else None
|
83
|
-
)
|
84
|
-
self._is_visible = (
|
85
|
-
(self._bounds.has_area and self._bounds.overlaps(window_bounds))
|
86
|
-
if self._bounds
|
87
|
-
else None
|
88
|
-
)
|
89
|
-
self._current_src_url = (
|
90
|
-
current_url_src_for_node_index(dom_node_index, snapshot_context)
|
91
|
-
if self.role == "image"
|
92
|
-
else None
|
93
|
-
)
|
94
|
-
|
95
|
-
if node_has_property(ax_node, "editable"):
|
96
|
-
# Sometimes a node will have it's input stored as 'value' other times we
|
97
|
-
# need to go looking through the DOM tree to find its matching string.
|
98
|
-
self._input = (
|
99
|
-
string_from_ax_value(ax_node.value)
|
100
|
-
if ax_node.value
|
101
|
-
else text_value_for_node_index(dom_node_index, snapshot_context)
|
102
|
-
if dom_node_index is not None
|
103
|
-
else None
|
104
|
-
)
|
105
|
-
|
106
|
-
# this is a little awkward, but the old code ensured that menuitems were visible in given the following condition
|
107
|
-
if self.role == "menuitem" and parent is not None and parent.is_expanded:
|
108
|
-
self._is_visible = True
|
109
|
-
|
110
|
-
# It's important to set all other properties before creating children in case
|
111
|
-
# the children sample parent.xxx
|
112
|
-
self.children = (
|
113
|
-
[
|
114
|
-
AccessibilityTreeNode(
|
115
|
-
ax_node=ax_nodes[child_id],
|
116
|
-
ax_nodes=ax_nodes,
|
117
|
-
parent=self,
|
118
|
-
all_accessibility_tree_nodes=all_accessibility_tree_nodes,
|
119
|
-
snapshot_context=snapshot_context,
|
120
|
-
device_scale_factor=device_scale_factor,
|
121
|
-
window_bounds=window_bounds,
|
122
|
-
)
|
123
|
-
for child_id in ax_node.childIds
|
124
|
-
]
|
125
|
-
if ax_node.childIds
|
126
|
-
else None
|
127
|
-
)
|
128
|
-
|
129
|
-
def __str__(self) -> str:
|
130
|
-
bounds_string = str(self.bounds) if _INCLUDE_BOUNDS_IN_OUTPUT else None
|
131
|
-
|
132
|
-
input_string = f'Current input: "{self._input}"' if self._input else None
|
133
|
-
|
134
|
-
name_string = f'"{self.name}"' if self.name else None
|
135
|
-
# Without bounds we can't click on the element, and therefore the ID
|
136
|
-
# can not be used. Still useful to show these elements though (e.g.)
|
137
|
-
# to let user know about options.
|
138
|
-
id_string = f"[{'*' if not self._bounds or not self._bounds.has_area else self._ax_node.nodeId}]"
|
139
|
-
url_string = self._current_src_url if self._current_src_url else None
|
140
|
-
|
141
|
-
url_string = _maybe_redact_base64_url(self._current_src_url)
|
142
|
-
|
143
|
-
return " ".join(
|
144
|
-
[
|
145
|
-
item
|
146
|
-
for item in [
|
147
|
-
id_string,
|
148
|
-
self.role,
|
149
|
-
name_string,
|
150
|
-
url_string,
|
151
|
-
input_string,
|
152
|
-
self._property_string(),
|
153
|
-
bounds_string,
|
154
|
-
]
|
155
|
-
if item is not None
|
156
|
-
]
|
157
|
-
)
|
158
|
-
|
159
|
-
@property
|
160
|
-
def node_id(self) -> AXNodeId:
|
161
|
-
return self._ax_node.nodeId
|
162
|
-
|
163
|
-
@property
|
164
|
-
def name(self) -> str:
|
165
|
-
return string_from_ax_value(self._ax_node.name)
|
166
|
-
|
167
|
-
@property
|
168
|
-
def is_expanded(self) -> bool:
|
169
|
-
return self._is_expanded
|
170
|
-
|
171
|
-
@property
|
172
|
-
def is_visible(self) -> bool:
|
173
|
-
return self._is_visible or False
|
174
|
-
|
175
|
-
@property
|
176
|
-
def bounds(self) -> Rectangle | None:
|
177
|
-
return self._bounds
|
178
|
-
|
179
|
-
@property
|
180
|
-
def current_src_url(self) -> str | None:
|
181
|
-
return self._current_src_url
|
182
|
-
|
183
|
-
@property
|
184
|
-
def input(self) -> str | None:
|
185
|
-
return self._input
|
186
|
-
|
187
|
-
@property
|
188
|
-
def is_ignored(self) -> bool:
|
189
|
-
# computing _is_ignored is very expensive, do it on demand and remember the result
|
190
|
-
if self._is_ignored is None:
|
191
|
-
self._is_ignored = self._compute_is_ignored()
|
192
|
-
return self._is_ignored
|
193
|
-
|
194
|
-
@property
|
195
|
-
def role(self) -> str:
|
196
|
-
return string_from_ax_value(self._ax_node.role)
|
197
|
-
|
198
|
-
@property
|
199
|
-
def _closest_non_ignored_parent(self) -> Optional["AccessibilityTreeNode"]:
|
200
|
-
if self._parent and self.__closest_non_ignored_parent is None:
|
201
|
-
self.__closest_non_ignored_parent = (
|
202
|
-
None
|
203
|
-
if not self._parent
|
204
|
-
else self._parent
|
205
|
-
if not self._parent.is_ignored
|
206
|
-
else self._parent._closest_non_ignored_parent # pylint: disable=protected-access
|
207
|
-
)
|
208
|
-
return self.__closest_non_ignored_parent
|
209
|
-
|
210
|
-
def render_accessibility_tree(self, indent: int = 0) -> str:
|
211
|
-
"""Returns the string representation of the node and its children."""
|
212
|
-
result = ""
|
213
|
-
if not self.is_ignored:
|
214
|
-
result = " " * indent + str(self) + "\n"
|
215
|
-
indent += 1
|
216
|
-
children_strings = [
|
217
|
-
child.render_accessibility_tree(indent) for child in self.children or []
|
218
|
-
]
|
219
|
-
result = result + "".join([s for s in children_strings if s])
|
220
|
-
return result
|
221
|
-
|
222
|
-
def render_main_content(self) -> str | None:
|
223
|
-
return (
|
224
|
-
main_node._extract_text() # pylint: disable=protected-access
|
225
|
-
if (main_node := self._get_main_content_node())
|
226
|
-
else None
|
227
|
-
)
|
228
|
-
|
229
|
-
def _compute_is_ignored(self) -> bool:
|
230
|
-
if self._ax_node.ignored or not self.is_visible or self.role in _ROLES_IGNORED:
|
231
|
-
return True
|
232
|
-
|
233
|
-
# if the parent is a link/button with a name, and this node is within the parent's bounds, we can ignore it.
|
234
|
-
if (
|
235
|
-
(closest_parent := self._closest_non_ignored_parent)
|
236
|
-
and closest_parent.bounds
|
237
|
-
and self.bounds
|
238
|
-
and (closest_parent.role == "link" or closest_parent.role == "button")
|
239
|
-
and closest_parent.name
|
240
|
-
and self.bounds.within(closest_parent.bounds)
|
241
|
-
):
|
242
|
-
return True
|
243
|
-
|
244
|
-
if bool(self.name): # excludes None or ""
|
245
|
-
# This simplifies things like links/buttons whose contents are sub-elements
|
246
|
-
# e.g.
|
247
|
-
# [158] button "Menu"
|
248
|
-
# [1166] StaticText "Menu"
|
249
|
-
# becomes
|
250
|
-
# [158] button "Menu"
|
251
|
-
return self._parent is not None and self._parent.name == self.name
|
252
|
-
|
253
|
-
return self.role in _ROLES_REQUIRING_A_NAME
|
254
|
-
|
255
|
-
def _get_main_content_node(self) -> Optional["AccessibilityTreeNode"]:
|
256
|
-
if self.role == "main":
|
257
|
-
return self
|
258
|
-
|
259
|
-
return next(
|
260
|
-
(
|
261
|
-
result
|
262
|
-
for child in self.children or []
|
263
|
-
if (result := child._get_main_content_node()) # pylint: disable=protected-access
|
264
|
-
),
|
265
|
-
None,
|
266
|
-
)
|
267
|
-
|
268
|
-
def _extract_text(self):
|
269
|
-
"""
|
270
|
-
Recursively extracts and concatenates text from the accessibility tree nodes.
|
271
|
-
|
272
|
-
This method traverses the accessibility tree starting from the current node,
|
273
|
-
concatenating the text of each node.
|
274
|
-
|
275
|
-
Returns:
|
276
|
-
str: The concatenated text from the accessibility tree nodes, with
|
277
|
-
appropriate spacing based on node roles.
|
278
|
-
"""
|
279
|
-
if not self.is_visible:
|
280
|
-
return ""
|
281
|
-
|
282
|
-
def reducer(acc, child):
|
283
|
-
child_text = child._extract_text() # pylint: disable=protected-access
|
284
|
-
return (
|
285
|
-
acc
|
286
|
-
+ (
|
287
|
-
"\n\n"
|
288
|
-
if child.role
|
289
|
-
in {"paragraph", "note", "row", "div", "heading", "region"}
|
290
|
-
else " "
|
291
|
-
)
|
292
|
-
+ child_text
|
293
|
-
)
|
294
|
-
|
295
|
-
return reduce(reducer, self.children or [], self.name).strip()
|
296
|
-
|
297
|
-
def _property_string(self) -> str:
|
298
|
-
properties = [
|
299
|
-
_prop_name_value_str(node_property)
|
300
|
-
if node_property.value.value
|
301
|
-
else f"{node_property.name}"
|
302
|
-
for node_property in self._ax_node.properties or ()
|
303
|
-
if node_property.name not in _IGNORED_AT_PROPERTIES
|
304
|
-
]
|
305
|
-
return " [" + ", ".join(properties) + "]" if properties else ""
|
306
|
-
|
307
|
-
|
308
|
-
def _prop_name_value_str(node_property: AXProperty) -> str:
|
309
|
-
# pre-commit hook (debug-statements) crashes if value is inlined below
|
310
|
-
value = (
|
311
|
-
_maybe_redact_base64_url(node_property.value.value)
|
312
|
-
if node_property.name == "url" and isinstance(node_property.value.value, str)
|
313
|
-
else node_property.value.value
|
314
|
-
)
|
315
|
-
return f"{node_property.name}: {value}"
|
316
|
-
|
317
|
-
|
318
|
-
def _maybe_redact_base64_url(url: str | None) -> str | None:
|
319
|
-
return (
|
320
|
-
"data:<base64-data-removed>"
|
321
|
-
if url and url.startswith("data:") and "base64" in url
|
322
|
-
else url
|
323
|
-
)
|
@@ -1,279 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Types and pure functional helpers associated with Chrome DevTools Protocol's 'Accessibility' Domain
|
3
|
-
|
4
|
-
https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/
|
5
|
-
"""
|
6
|
-
|
7
|
-
from typing import Literal, NewType, Optional
|
8
|
-
|
9
|
-
from pydantic import BaseModel
|
10
|
-
|
11
|
-
from cdp.dom import DOMBackendNodeId
|
12
|
-
|
13
|
-
# brand these str's so that we don't confuse them with other str's
|
14
|
-
PageFrameId = NewType("PageFrameId", str)
|
15
|
-
AXNodeId = NewType("AXNodeId", str)
|
16
|
-
|
17
|
-
|
18
|
-
AXPropertyName = Literal[
|
19
|
-
"actions",
|
20
|
-
"busy",
|
21
|
-
"disabled",
|
22
|
-
"editable",
|
23
|
-
"focusable",
|
24
|
-
"focused",
|
25
|
-
"hidden",
|
26
|
-
"hiddenRoot",
|
27
|
-
"invalid",
|
28
|
-
"keyshortcuts",
|
29
|
-
"settable",
|
30
|
-
"roledescription",
|
31
|
-
"live",
|
32
|
-
"atomic",
|
33
|
-
"relevant",
|
34
|
-
"root",
|
35
|
-
"autocomplete",
|
36
|
-
"hasPopup",
|
37
|
-
"level",
|
38
|
-
"multiselectable",
|
39
|
-
"orientation",
|
40
|
-
"multiline",
|
41
|
-
"readonly",
|
42
|
-
"required",
|
43
|
-
"valuemin",
|
44
|
-
"valuemax",
|
45
|
-
"valuetext",
|
46
|
-
"checked",
|
47
|
-
"expanded",
|
48
|
-
"modal",
|
49
|
-
"pressed",
|
50
|
-
"selected",
|
51
|
-
"activedescendant",
|
52
|
-
"controls",
|
53
|
-
"describedby",
|
54
|
-
"details",
|
55
|
-
"errormessage",
|
56
|
-
"flowto",
|
57
|
-
"labelledby",
|
58
|
-
"owns",
|
59
|
-
"url",
|
60
|
-
]
|
61
|
-
|
62
|
-
|
63
|
-
AXValueType = Literal[
|
64
|
-
"boolean",
|
65
|
-
"tristate",
|
66
|
-
"booleanOrUndefined",
|
67
|
-
"idref",
|
68
|
-
"idrefList",
|
69
|
-
"integer",
|
70
|
-
"node",
|
71
|
-
"nodeList",
|
72
|
-
"number",
|
73
|
-
"string",
|
74
|
-
"computedString",
|
75
|
-
"token",
|
76
|
-
"tokenList",
|
77
|
-
"domRelation",
|
78
|
-
"role",
|
79
|
-
"internalRole",
|
80
|
-
"valueUndefined",
|
81
|
-
]
|
82
|
-
|
83
|
-
|
84
|
-
class AXRelatedNode(BaseModel, frozen=True):
|
85
|
-
backendDOMNodeId: DOMBackendNodeId
|
86
|
-
"""The BackendNodeId of the related DOM node."""
|
87
|
-
idref: str | None = None
|
88
|
-
"""The IDRef value provided, if any."""
|
89
|
-
text: str | None = None
|
90
|
-
"""The text alternative of this node in the current context."""
|
91
|
-
|
92
|
-
|
93
|
-
AXValueSourceType = Literal[
|
94
|
-
"attribute", "implicit", "stylet", "contents", "placeholder", "relatedElement"
|
95
|
-
]
|
96
|
-
|
97
|
-
AXValueNativeSourceType = Literal[
|
98
|
-
"description",
|
99
|
-
"figcaption",
|
100
|
-
"label",
|
101
|
-
"labelfor",
|
102
|
-
"labelwrapped",
|
103
|
-
"legend",
|
104
|
-
"rubyannotation",
|
105
|
-
"tablecaption",
|
106
|
-
"title",
|
107
|
-
"other",
|
108
|
-
]
|
109
|
-
|
110
|
-
|
111
|
-
class AXValueSource(BaseModel, frozen=True):
|
112
|
-
"""A single source for a computed AX property."""
|
113
|
-
|
114
|
-
type: AXValueSourceType
|
115
|
-
"""What type of source this is."""
|
116
|
-
value: Optional["AXValue"] = None
|
117
|
-
"""The value of this property source."""
|
118
|
-
attribute: str | None = None
|
119
|
-
"""The name of the relevant attribute, if any."""
|
120
|
-
attributeValue: Optional["AXValue"] = None
|
121
|
-
"""The value of the relevant attribute, if any."""
|
122
|
-
superseded: bool | None = None
|
123
|
-
"""Whether this source is superseded by a higher priority source."""
|
124
|
-
nativeSource: AXValueNativeSourceType | None = None
|
125
|
-
"""The native markup source for this value, e.g. a `<label>` element."""
|
126
|
-
nativeSourceValue: Optional["AXValue"] = None
|
127
|
-
"""The value, such as a node or node list, of the native source."""
|
128
|
-
invalid: bool | None = None
|
129
|
-
"""Whether the value for this property is invalid."""
|
130
|
-
invalidReason: str | None = None
|
131
|
-
"""Reason for the value being invalid, if it is."""
|
132
|
-
|
133
|
-
|
134
|
-
class AXValue(BaseModel, frozen=True):
|
135
|
-
"""A single computed AX property."""
|
136
|
-
|
137
|
-
type: AXValueType
|
138
|
-
"""The type of this value."""
|
139
|
-
value: object | None = None
|
140
|
-
"""The computed value of this property."""
|
141
|
-
relatedNodes: tuple[AXRelatedNode, ...] | None = None
|
142
|
-
"""One or more related nodes, if applicable."""
|
143
|
-
sources: tuple[AXValueSource, ...] | None = None
|
144
|
-
"""The sources which contributed to the computation of this property."""
|
145
|
-
|
146
|
-
|
147
|
-
class AXProperty(BaseModel, frozen=True):
|
148
|
-
name: AXPropertyName
|
149
|
-
"""The name of this property."""
|
150
|
-
value: AXValue
|
151
|
-
"""The value of this property."""
|
152
|
-
|
153
|
-
|
154
|
-
# AXNode's ignoredReasons is documented to be a list[AXProperty]. However, that seems to
|
155
|
-
# be erroneous since the namespace for ignored reason names is completely separate from property names.
|
156
|
-
class AXIgnoredReason(BaseModel, frozen=True):
|
157
|
-
# name: Literal[
|
158
|
-
# "uninteresting",
|
159
|
-
# "ariaHiddenSubtree",
|
160
|
-
# "ariaHiddenElement",
|
161
|
-
# "notRendered",
|
162
|
-
# "notVisible",
|
163
|
-
# "emptyAlt",
|
164
|
-
# ]
|
165
|
-
name: str # since this is all undocumented, it's best to be loose
|
166
|
-
"""The name of this ignored reason."""
|
167
|
-
value: AXValue
|
168
|
-
"""The value of this ignored reason."""
|
169
|
-
|
170
|
-
|
171
|
-
class AXNode(BaseModel, frozen=True):
|
172
|
-
"""A node in the accessibility tree."""
|
173
|
-
|
174
|
-
nodeId: AXNodeId
|
175
|
-
"""Unique identifier for this node."""
|
176
|
-
ignored: bool
|
177
|
-
"""Wether this node is ignore3d for accessibility"""
|
178
|
-
ignoredReasons: tuple[AXIgnoredReason, ...] | None = None
|
179
|
-
"""Collection of reasons why this node is hidden."""
|
180
|
-
role: AXValue | None = None
|
181
|
-
"""This `Node`'s role, wether explicit or implicit."""
|
182
|
-
chromeRole: AXValue | None = None
|
183
|
-
"""This `Node`'s Chrome raw role."""
|
184
|
-
name: AXValue | None = None
|
185
|
-
"""The accessible name for this `Node`."""
|
186
|
-
description: AXValue | None = None
|
187
|
-
"""The accessible description for this `Node`."""
|
188
|
-
value: AXValue | None = None
|
189
|
-
"""The value for this `Node`."""
|
190
|
-
properties: tuple[AXProperty, ...] | None = None
|
191
|
-
"""All other properties"""
|
192
|
-
parentId: AXNodeId | None = None
|
193
|
-
"""ID for this node's parent."""
|
194
|
-
childIds: tuple[AXNodeId, ...] | None = None
|
195
|
-
"""IDs for each of this node's child nodes."""
|
196
|
-
backendDOMNodeId: DOMBackendNodeId | None = None
|
197
|
-
"""The backend ID for the associated DOM node, if any."""
|
198
|
-
frameId: PageFrameId | None = None
|
199
|
-
"""The frame ID for the frame associated with this nodes document."""
|
200
|
-
|
201
|
-
|
202
|
-
class AXTree(BaseModel, frozen=True):
|
203
|
-
nodes: tuple[AXNode, ...]
|
204
|
-
|
205
|
-
|
206
|
-
# CDP defines `AXValue.value` as `Any`, so this is a coercion function
|
207
|
-
def string_from_ax_value(value: AXValue | None) -> str:
|
208
|
-
"""
|
209
|
-
Coerces an AXValue to a string.
|
210
|
-
|
211
|
-
Args:
|
212
|
-
value (AXValue | None): The AXValue to be coerced.
|
213
|
-
|
214
|
-
Returns:
|
215
|
-
str: The string representation of the AXValue, or an empty string if the value is None or not a string.
|
216
|
-
"""
|
217
|
-
return value.value if value and isinstance(value.value, str) else ""
|
218
|
-
|
219
|
-
|
220
|
-
def node_has_property(node: AXNode, property_name: AXPropertyName) -> bool:
|
221
|
-
"""
|
222
|
-
Checks if an AXNode has a specific property.
|
223
|
-
|
224
|
-
Args:
|
225
|
-
node (AXNode): The AXNode to check.
|
226
|
-
property_name (AXPropertyName): The name of the property to look for.
|
227
|
-
|
228
|
-
Returns:
|
229
|
-
bool: True if the node has the property, False otherwise.
|
230
|
-
"""
|
231
|
-
return node_property(node, property_name) is not None
|
232
|
-
|
233
|
-
|
234
|
-
def node_property(node: AXNode, property_name: AXPropertyName) -> AXProperty | None:
|
235
|
-
"""
|
236
|
-
Retrieves a specific property from an AXNode.
|
237
|
-
|
238
|
-
Args:
|
239
|
-
node (AXNode): The AXNode to retrieve the property from.
|
240
|
-
property_name (AXPropertyName): The name of the property to retrieve.
|
241
|
-
|
242
|
-
Returns:
|
243
|
-
AXProperty | None: The property if found, otherwise None.
|
244
|
-
"""
|
245
|
-
return next(
|
246
|
-
(prop for prop in node.properties or () if prop.name == property_name), None
|
247
|
-
)
|
248
|
-
|
249
|
-
|
250
|
-
def node_bool_property(node: AXNode, property_name: AXPropertyName) -> bool:
|
251
|
-
"""
|
252
|
-
Retrieves a boolean property from an AXNode.
|
253
|
-
|
254
|
-
Args:
|
255
|
-
node (AXNode): The AXNode to retrieve the property from.
|
256
|
-
property_name (AXPropertyName): The name of the property to retrieve.
|
257
|
-
|
258
|
-
Returns:
|
259
|
-
bool: The boolean value of the property, or False if the property is not found or not a boolean.
|
260
|
-
"""
|
261
|
-
return bool(prop.value) if (prop := node_property(node, property_name)) else False
|
262
|
-
|
263
|
-
|
264
|
-
def node_str_property(node: AXNode, property_name: AXPropertyName) -> str | None:
|
265
|
-
"""
|
266
|
-
Retrieves a str property from an AXNode.
|
267
|
-
|
268
|
-
Args:
|
269
|
-
node (AXNode): The AXNode to retrieve the property from.
|
270
|
-
property_name (AXPropertyName): The name of the property to retrieve.
|
271
|
-
|
272
|
-
Returns:
|
273
|
-
str: The str value of the property, or None if the property is not found or not a str.
|
274
|
-
"""
|
275
|
-
prop = node_property(node, property_name)
|
276
|
-
if prop is None:
|
277
|
-
return None
|
278
|
-
v = prop.value
|
279
|
-
return v.value if isinstance(v.value, str) else None
|