Skip to content

Commit ed13a53

Browse files
authored
feat: port layout selectors, filter, circular serialization (microsoft#1293)
Roll driver in preparation for 1.22 release. Ports the following changes: - [x] microsoft/playwright@54dd6d0 (feat(locator): layout options (leftOf, rightOf, above, below, near) (#13821)) - [x] microsoft/playwright@c3cf7ee (feat(layout locators): remove maxDistance option (#14013)) - [x] microsoft/playwright@6931d89 (feat(locators): rename locator.that to locator.filter (#14025))
1 parent 7b09eef commit ed13a53

19 files changed

+1193
-216
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
44

55
| | Linux | macOS | Windows |
66
| :--- | :---: | :---: | :---: |
7-
| Chromium <!-- GEN:chromium-version -->101.0.4951.41<!-- GEN:stop --> ||||
7+
| Chromium <!-- GEN:chromium-version -->102.0.5005.40<!-- GEN:stop --> ||||
88
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> ||||
9-
| Firefox <!-- GEN:firefox-version -->98.0.2<!-- GEN:stop --> ||||
9+
| Firefox <!-- GEN:firefox-version -->99.0.1<!-- GEN:stop --> ||||
1010

1111
## Documentation
1212

playwright/_impl/_frame.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,9 +499,27 @@ async def fill(
499499
await self._channel.send("fill", locals_to_params(locals()))
500500

501501
def locator(
502-
self, selector: str, has_text: Union[str, Pattern] = None, has: Locator = None
502+
self,
503+
selector: str,
504+
has_text: Union[str, Pattern] = None,
505+
has: Locator = None,
506+
left_of: "Locator" = None,
507+
right_of: "Locator" = None,
508+
above: "Locator" = None,
509+
below: "Locator" = None,
510+
near: "Locator" = None,
503511
) -> Locator:
504-
return Locator(self, selector, has_text=has_text, has=has)
512+
return Locator(
513+
self,
514+
selector,
515+
has_text=has_text,
516+
has=has,
517+
left_of=left_of,
518+
right_of=right_of,
519+
above=above,
520+
below=below,
521+
near=near,
522+
)
505523

506524
def frame_locator(self, selector: str) -> FrameLocator:
507525
return FrameLocator(self, selector)

playwright/_impl/_impl_to_api_mapping.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
# limitations under the License.
1414

1515
import inspect
16-
from typing import Any, Callable, Dict, List, Optional
16+
from typing import Any, Callable, Dict, List, Optional, Union
1717

1818
from playwright._impl._api_types import Error
19+
from playwright._impl._map import Map
1920

2021
API_ATTR = "_pw_api_instance_"
2122
IMPL_ATTR = "_pw_impl_instance_"
@@ -36,13 +37,27 @@ def __init__(self) -> None:
3637
def register(self, impl_class: type, api_class: type) -> None:
3738
self._mapping[impl_class] = api_class
3839

39-
def from_maybe_impl(self, obj: Any) -> Any:
40+
def from_maybe_impl(
41+
self, obj: Any, visited: Map[Any, Union[List, Dict]] = Map()
42+
) -> Any:
4043
if not obj:
4144
return obj
4245
if isinstance(obj, dict):
43-
return {name: self.from_maybe_impl(value) for name, value in obj.items()}
46+
if obj in visited:
47+
return visited[obj]
48+
o: Dict = {}
49+
visited[obj] = o
50+
for name, value in obj.items():
51+
o[name] = self.from_maybe_impl(value, visited)
52+
return o
4453
if isinstance(obj, list):
45-
return [self.from_maybe_impl(item) for item in obj]
54+
if obj in visited:
55+
return visited[obj]
56+
a: List = []
57+
visited[obj] = a
58+
for item in obj:
59+
a.append(self.from_maybe_impl(item, visited))
60+
return a
4661
api_class = self._mapping.get(type(obj))
4762
if api_class:
4863
api_instance = getattr(obj, API_ATTR, None)
@@ -68,14 +83,26 @@ def from_impl_list(self, items: List[Any]) -> List[Any]:
6883
def from_impl_dict(self, map: Dict[str, Any]) -> Dict[str, Any]:
6984
return {name: self.from_impl(value) for name, value in map.items()}
7085

71-
def to_impl(self, obj: Any) -> Any:
86+
def to_impl(self, obj: Any, visited: Map[Any, Union[List, Dict]] = Map()) -> Any:
7287
try:
7388
if not obj:
7489
return obj
7590
if isinstance(obj, dict):
76-
return {name: self.to_impl(value) for name, value in obj.items()}
91+
if obj in visited:
92+
return visited[obj]
93+
o: Dict = {}
94+
visited[obj] = o
95+
for name, value in obj.items():
96+
o[name] = self.to_impl(value, visited)
97+
return o
7798
if isinstance(obj, list):
78-
return [self.to_impl(item) for item in obj]
99+
if obj in visited:
100+
return visited[obj]
101+
a: List = []
102+
visited[obj] = a
103+
for item in obj:
104+
a.append(self.to_impl(item, visited))
105+
return a
79106
if isinstance(obj, ImplWrapper):
80107
return obj._impl_obj
81108
return obj

playwright/_impl/_js_handle.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
# limitations under the License.
1414

1515
import math
16+
from dataclasses import dataclass
1617
from datetime import datetime
1718
from typing import TYPE_CHECKING, Any, Dict, List, Optional
1819

19-
from playwright._impl._api_types import Error
2020
from playwright._impl._connection import ChannelOwner, from_channel
21+
from playwright._impl._map import Map
2122

2223
if TYPE_CHECKING: # pragma: no cover
2324
from playwright._impl._element_handle import ElementHandle
@@ -26,6 +27,18 @@
2627
Serializable = Any
2728

2829

30+
@dataclass
31+
class VisitorInfo:
32+
visited: Map[Any, int] = Map()
33+
last_id: int = 0
34+
35+
def visit(self, obj: Any) -> int:
36+
assert obj not in self.visited
37+
self.last_id += 1
38+
self.visited[obj] = self.last_id
39+
return self.last_id
40+
41+
2942
class JSHandle(ChannelOwner):
3043
def __init__(
3144
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
@@ -90,13 +103,13 @@ async def json_value(self) -> Any:
90103
return parse_result(await self._channel.send("jsonValue"))
91104

92105

93-
def serialize_value(value: Any, handles: List[JSHandle], depth: int) -> Any:
106+
def serialize_value(
107+
value: Any, handles: List[JSHandle], visitor_info: VisitorInfo = VisitorInfo()
108+
) -> Any:
94109
if isinstance(value, JSHandle):
95110
h = len(handles)
96111
handles.append(value._channel)
97112
return dict(h=h)
98-
if depth > 100:
99-
raise Error("Maximum argument depth exceeded")
100113
if value is None:
101114
return dict(v="null")
102115
if isinstance(value, float):
@@ -117,30 +130,40 @@ def serialize_value(value: Any, handles: List[JSHandle], depth: int) -> Any:
117130
if isinstance(value, str):
118131
return {"s": value}
119132

133+
if value in visitor_info.visited:
134+
return dict(ref=visitor_info.visited[value])
135+
120136
if isinstance(value, list):
121-
result = list(map(lambda a: serialize_value(a, handles, depth + 1), value))
122-
return dict(a=result)
137+
id = visitor_info.visit(value)
138+
a = []
139+
for e in value:
140+
a.append(serialize_value(e, handles, visitor_info))
141+
return dict(a=a, id=id)
123142

124143
if isinstance(value, dict):
125-
result = []
144+
id = visitor_info.visit(value)
145+
o = []
126146
for name in value:
127-
result.append(
128-
{"k": name, "v": serialize_value(value[name], handles, depth + 1)}
147+
o.append(
148+
{"k": name, "v": serialize_value(value[name], handles, visitor_info)}
129149
)
130-
return dict(o=result)
150+
return dict(o=o, id=id)
131151
return dict(v="undefined")
132152

133153

134154
def serialize_argument(arg: Serializable = None) -> Any:
135155
handles: List[JSHandle] = []
136-
value = serialize_value(arg, handles, 0)
156+
value = serialize_value(arg, handles)
137157
return dict(value=value, handles=handles)
138158

139159

140-
def parse_value(value: Any) -> Any:
160+
def parse_value(value: Any, refs: Dict[int, Any] = {}) -> Any:
141161
if value is None:
142162
return None
143163
if isinstance(value, dict):
164+
if "ref" in value:
165+
return refs[value["ref"]]
166+
144167
if "v" in value:
145168
v = value["v"]
146169
if v == "Infinity":
@@ -158,14 +181,21 @@ def parse_value(value: Any) -> Any:
158181
return v
159182

160183
if "a" in value:
161-
return list(map(lambda e: parse_value(e), value["a"]))
184+
a: List = []
185+
refs[value["id"]] = a
186+
for e in value["a"]:
187+
a.append(parse_value(e, refs))
188+
return a
162189

163190
if "d" in value:
164191
return datetime.fromisoformat(value["d"][:-1])
165192

166193
if "o" in value:
167-
o = value["o"]
168-
return {e["k"]: parse_value(e["v"]) for e in o}
194+
o: Dict = {}
195+
refs[value["id"]] = o
196+
for e in value["o"]:
197+
o[e["k"]] = parse_value(e["v"], refs)
198+
return o
169199

170200
if "n" in value:
171201
return value["n"]

playwright/_impl/_locator.py

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Pattern,
2727
TypeVar,
2828
Union,
29+
cast,
2930
)
3031

3132
from playwright._impl._api_structures import (
@@ -66,7 +67,13 @@ def __init__(
6667
selector: str,
6768
has_text: Union[str, Pattern] = None,
6869
has: "Locator" = None,
70+
left_of: "Locator" = None,
71+
right_of: "Locator" = None,
72+
above: "Locator" = None,
73+
below: "Locator" = None,
74+
near: "Locator" = None,
6975
) -> None:
76+
_params = locals()
7077
self._frame = frame
7178
self._selector = selector
7279
self._loop = frame._loop
@@ -81,10 +88,18 @@ def __init__(
8188
escaped = escape_with_quotes(has_text, '"')
8289
self._selector += f" >> :scope:has-text({escaped})"
8390

84-
if has:
85-
if has._frame != frame:
86-
raise Error('Inner "has" locator must belong to the same frame.')
87-
self._selector += " >> has=" + json.dumps(has._selector)
91+
for inner in ["has", "left_of", "right_of", "above", "below", "near"]:
92+
locator: Optional["Locator"] = cast("Locator", _params.get(inner))
93+
if not locator:
94+
continue
95+
if locator._frame != frame:
96+
raise Error(f'Inner "{inner}" locator must belong to the same frame.')
97+
engine_name = inner
98+
if engine_name == "left_of":
99+
engine_name = "left-of"
100+
elif engine_name == "right_of":
101+
engine_name = "right-of"
102+
self._selector += f" >> {engine_name}=" + json.dumps(locator._selector)
88103

89104
def __repr__(self) -> str:
90105
return f"<Locator frame={self._frame!r} selector={self._selector!r}>"
@@ -200,12 +215,22 @@ def locator(
200215
selector: str,
201216
has_text: Union[str, Pattern] = None,
202217
has: "Locator" = None,
218+
left_of: "Locator" = None,
219+
right_of: "Locator" = None,
220+
above: "Locator" = None,
221+
below: "Locator" = None,
222+
near: "Locator" = None,
203223
) -> "Locator":
204224
return Locator(
205225
self._frame,
206226
f"{self._selector} >> {selector}",
207227
has_text=has_text,
208228
has=has,
229+
left_of=left_of,
230+
right_of=right_of,
231+
above=above,
232+
below=below,
233+
near=near,
209234
)
210235

211236
def frame_locator(self, selector: str) -> "FrameLocator":
@@ -236,16 +261,26 @@ def last(self) -> "Locator":
236261
def nth(self, index: int) -> "Locator":
237262
return Locator(self._frame, f"{self._selector} >> nth={index}")
238263

239-
def that(
264+
def filter(
240265
self,
241266
has_text: Union[str, Pattern] = None,
242267
has: "Locator" = None,
268+
left_of: "Locator" = None,
269+
right_of: "Locator" = None,
270+
above: "Locator" = None,
271+
below: "Locator" = None,
272+
near: "Locator" = None,
243273
) -> "Locator":
244274
return Locator(
245275
self._frame,
246276
self._selector,
247277
has_text=has_text,
248278
has=has,
279+
left_of=left_of,
280+
right_of=right_of,
281+
above=above,
282+
below=below,
283+
near=near,
249284
)
250285

251286
async def focus(self, timeout: float = None) -> None:
@@ -577,13 +612,26 @@ def __init__(self, frame: "Frame", frame_selector: str) -> None:
577612
self._frame_selector = frame_selector
578613

579614
def locator(
580-
self, selector: str, has_text: Union[str, Pattern] = None, has: "Locator" = None
615+
self,
616+
selector: str,
617+
has_text: Union[str, Pattern] = None,
618+
has: "Locator" = None,
619+
left_of: "Locator" = None,
620+
right_of: "Locator" = None,
621+
above: "Locator" = None,
622+
below: "Locator" = None,
623+
near: "Locator" = None,
581624
) -> Locator:
582625
return Locator(
583626
self._frame,
584627
f"{self._frame_selector} >> control=enter-frame >> {selector}",
585628
has_text=has_text,
586629
has=has,
630+
left_of=left_of,
631+
right_of=right_of,
632+
above=above,
633+
below=below,
634+
near=near,
587635
)
588636

589637
def frame_locator(self, selector: str) -> "FrameLocator":

playwright/_impl/_map.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Dict, Generic, Tuple, TypeVar
2+
3+
K = TypeVar("K")
4+
V = TypeVar("V")
5+
6+
7+
class Map(Generic[K, V]):
8+
def __init__(self) -> None:
9+
self._entries: Dict[int, Tuple[K, V]] = {}
10+
11+
def __contains__(self, item: K) -> bool:
12+
return id(item) in self._entries
13+
14+
def __setitem__(self, idx: K, value: V) -> None:
15+
self._entries[id(idx)] = (idx, value)
16+
17+
def __getitem__(self, obj: K) -> V:
18+
return self._entries[id(obj)][1]

0 commit comments

Comments
 (0)