diff --git a/README.md b/README.md index 477449ed2..96631dbd3 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 101.0.4951.41 | ✅ | ✅ | ✅ | +| Chromium 102.0.5005.40 | ✅ | ✅ | ✅ | | WebKit 15.4 | ✅ | ✅ | ✅ | -| Firefox 98.0.2 | ✅ | ✅ | ✅ | +| Firefox 99.0.1 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 471ea7dcb..0ace5023d 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -499,9 +499,27 @@ async def fill( await self._channel.send("fill", locals_to_params(locals())) def locator( - self, selector: str, has_text: Union[str, Pattern] = None, has: Locator = None + self, + selector: str, + has_text: Union[str, Pattern] = None, + has: Locator = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> Locator: - return Locator(self, selector, has_text=has_text, has=has) + return Locator( + self, + selector, + has_text=has_text, + has=has, + left_of=left_of, + right_of=right_of, + above=above, + below=below, + near=near, + ) def frame_locator(self, selector: str) -> FrameLocator: return FrameLocator(self, selector) diff --git a/playwright/_impl/_impl_to_api_mapping.py b/playwright/_impl/_impl_to_api_mapping.py index a9afff31f..f0af8ab3e 100644 --- a/playwright/_impl/_impl_to_api_mapping.py +++ b/playwright/_impl/_impl_to_api_mapping.py @@ -13,9 +13,10 @@ # limitations under the License. import inspect -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Union from playwright._impl._api_types import Error +from playwright._impl._map import Map API_ATTR = "_pw_api_instance_" IMPL_ATTR = "_pw_impl_instance_" @@ -36,13 +37,27 @@ def __init__(self) -> None: def register(self, impl_class: type, api_class: type) -> None: self._mapping[impl_class] = api_class - def from_maybe_impl(self, obj: Any) -> Any: + def from_maybe_impl( + self, obj: Any, visited: Map[Any, Union[List, Dict]] = Map() + ) -> Any: if not obj: return obj if isinstance(obj, dict): - return {name: self.from_maybe_impl(value) for name, value in obj.items()} + if obj in visited: + return visited[obj] + o: Dict = {} + visited[obj] = o + for name, value in obj.items(): + o[name] = self.from_maybe_impl(value, visited) + return o if isinstance(obj, list): - return [self.from_maybe_impl(item) for item in obj] + if obj in visited: + return visited[obj] + a: List = [] + visited[obj] = a + for item in obj: + a.append(self.from_maybe_impl(item, visited)) + return a api_class = self._mapping.get(type(obj)) if api_class: api_instance = getattr(obj, API_ATTR, None) @@ -68,14 +83,26 @@ def from_impl_list(self, items: List[Any]) -> List[Any]: def from_impl_dict(self, map: Dict[str, Any]) -> Dict[str, Any]: return {name: self.from_impl(value) for name, value in map.items()} - def to_impl(self, obj: Any) -> Any: + def to_impl(self, obj: Any, visited: Map[Any, Union[List, Dict]] = Map()) -> Any: try: if not obj: return obj if isinstance(obj, dict): - return {name: self.to_impl(value) for name, value in obj.items()} + if obj in visited: + return visited[obj] + o: Dict = {} + visited[obj] = o + for name, value in obj.items(): + o[name] = self.to_impl(value, visited) + return o if isinstance(obj, list): - return [self.to_impl(item) for item in obj] + if obj in visited: + return visited[obj] + a: List = [] + visited[obj] = a + for item in obj: + a.append(self.to_impl(item, visited)) + return a if isinstance(obj, ImplWrapper): return obj._impl_obj return obj diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index cfa097275..7632cfbdc 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -13,11 +13,12 @@ # limitations under the License. import math +from dataclasses import dataclass from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, List, Optional -from playwright._impl._api_types import Error from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._map import Map if TYPE_CHECKING: # pragma: no cover from playwright._impl._element_handle import ElementHandle @@ -26,6 +27,18 @@ Serializable = Any +@dataclass +class VisitorInfo: + visited: Map[Any, int] = Map() + last_id: int = 0 + + def visit(self, obj: Any) -> int: + assert obj not in self.visited + self.last_id += 1 + self.visited[obj] = self.last_id + return self.last_id + + class JSHandle(ChannelOwner): def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict @@ -90,13 +103,13 @@ async def json_value(self) -> Any: return parse_result(await self._channel.send("jsonValue")) -def serialize_value(value: Any, handles: List[JSHandle], depth: int) -> Any: +def serialize_value( + value: Any, handles: List[JSHandle], visitor_info: VisitorInfo = VisitorInfo() +) -> Any: if isinstance(value, JSHandle): h = len(handles) handles.append(value._channel) return dict(h=h) - if depth > 100: - raise Error("Maximum argument depth exceeded") if value is None: return dict(v="null") if isinstance(value, float): @@ -117,30 +130,40 @@ def serialize_value(value: Any, handles: List[JSHandle], depth: int) -> Any: if isinstance(value, str): return {"s": value} + if value in visitor_info.visited: + return dict(ref=visitor_info.visited[value]) + if isinstance(value, list): - result = list(map(lambda a: serialize_value(a, handles, depth + 1), value)) - return dict(a=result) + id = visitor_info.visit(value) + a = [] + for e in value: + a.append(serialize_value(e, handles, visitor_info)) + return dict(a=a, id=id) if isinstance(value, dict): - result = [] + id = visitor_info.visit(value) + o = [] for name in value: - result.append( - {"k": name, "v": serialize_value(value[name], handles, depth + 1)} + o.append( + {"k": name, "v": serialize_value(value[name], handles, visitor_info)} ) - return dict(o=result) + return dict(o=o, id=id) return dict(v="undefined") def serialize_argument(arg: Serializable = None) -> Any: handles: List[JSHandle] = [] - value = serialize_value(arg, handles, 0) + value = serialize_value(arg, handles) return dict(value=value, handles=handles) -def parse_value(value: Any) -> Any: +def parse_value(value: Any, refs: Dict[int, Any] = {}) -> Any: if value is None: return None if isinstance(value, dict): + if "ref" in value: + return refs[value["ref"]] + if "v" in value: v = value["v"] if v == "Infinity": @@ -158,14 +181,21 @@ def parse_value(value: Any) -> Any: return v if "a" in value: - return list(map(lambda e: parse_value(e), value["a"])) + a: List = [] + refs[value["id"]] = a + for e in value["a"]: + a.append(parse_value(e, refs)) + return a if "d" in value: return datetime.fromisoformat(value["d"][:-1]) if "o" in value: - o = value["o"] - return {e["k"]: parse_value(e["v"]) for e in o} + o: Dict = {} + refs[value["id"]] = o + for e in value["o"]: + o[e["k"]] = parse_value(e["v"], refs) + return o if "n" in value: return value["n"] diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 23f2c16e1..faef55f60 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -26,6 +26,7 @@ Pattern, TypeVar, Union, + cast, ) from playwright._impl._api_structures import ( @@ -66,7 +67,13 @@ def __init__( selector: str, has_text: Union[str, Pattern] = None, has: "Locator" = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> None: + _params = locals() self._frame = frame self._selector = selector self._loop = frame._loop @@ -81,10 +88,18 @@ def __init__( escaped = escape_with_quotes(has_text, '"') self._selector += f" >> :scope:has-text({escaped})" - if has: - if has._frame != frame: - raise Error('Inner "has" locator must belong to the same frame.') - self._selector += " >> has=" + json.dumps(has._selector) + for inner in ["has", "left_of", "right_of", "above", "below", "near"]: + locator: Optional["Locator"] = cast("Locator", _params.get(inner)) + if not locator: + continue + if locator._frame != frame: + raise Error(f'Inner "{inner}" locator must belong to the same frame.') + engine_name = inner + if engine_name == "left_of": + engine_name = "left-of" + elif engine_name == "right_of": + engine_name = "right-of" + self._selector += f" >> {engine_name}=" + json.dumps(locator._selector) def __repr__(self) -> str: return f"" @@ -200,12 +215,22 @@ def locator( selector: str, has_text: Union[str, Pattern] = None, has: "Locator" = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> "Locator": return Locator( self._frame, f"{self._selector} >> {selector}", has_text=has_text, has=has, + left_of=left_of, + right_of=right_of, + above=above, + below=below, + near=near, ) def frame_locator(self, selector: str) -> "FrameLocator": @@ -236,16 +261,26 @@ def last(self) -> "Locator": def nth(self, index: int) -> "Locator": return Locator(self._frame, f"{self._selector} >> nth={index}") - def that( + def filter( self, has_text: Union[str, Pattern] = None, has: "Locator" = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> "Locator": return Locator( self._frame, self._selector, has_text=has_text, has=has, + left_of=left_of, + right_of=right_of, + above=above, + below=below, + near=near, ) async def focus(self, timeout: float = None) -> None: @@ -577,13 +612,26 @@ def __init__(self, frame: "Frame", frame_selector: str) -> None: self._frame_selector = frame_selector def locator( - self, selector: str, has_text: Union[str, Pattern] = None, has: "Locator" = None + self, + selector: str, + has_text: Union[str, Pattern] = None, + has: "Locator" = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> Locator: return Locator( self._frame, f"{self._frame_selector} >> control=enter-frame >> {selector}", has_text=has_text, has=has, + left_of=left_of, + right_of=right_of, + above=above, + below=below, + near=near, ) def frame_locator(self, selector: str) -> "FrameLocator": diff --git a/playwright/_impl/_map.py b/playwright/_impl/_map.py new file mode 100644 index 000000000..d5c2dc5e4 --- /dev/null +++ b/playwright/_impl/_map.py @@ -0,0 +1,18 @@ +from typing import Dict, Generic, Tuple, TypeVar + +K = TypeVar("K") +V = TypeVar("V") + + +class Map(Generic[K, V]): + def __init__(self) -> None: + self._entries: Dict[int, Tuple[K, V]] = {} + + def __contains__(self, item: K) -> bool: + return id(item) in self._entries + + def __setitem__(self, idx: K, value: V) -> None: + self._entries[id(idx)] = (idx, value) + + def __getitem__(self, obj: K) -> V: + return self._entries[id(obj)][1] diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 98916923a..2c182e23e 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -712,8 +712,22 @@ def locator( selector: str, has_text: Union[str, Pattern] = None, has: "Locator" = None, + left_of: "Locator" = None, + right_of: "Locator" = None, + above: "Locator" = None, + below: "Locator" = None, + near: "Locator" = None, ) -> "Locator": - return self._main_frame.locator(selector, has_text=has_text, has=has) + return self._main_frame.locator( + selector, + has_text=has_text, + has=has, + left_of=left_of, + right_of=right_of, + above=above, + below=below, + near=near, + ) def frame_locator(self, selector: str) -> "FrameLocator": return self.main_frame.frame_locator(selector) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 485c0a8f7..e7ffcd5d9 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -2188,9 +2188,9 @@ async def set_input_files( """ElementHandle.set_input_files Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they - are resolved relative to the the current working directory. For empty array, clears the selected files. + are resolved relative to the current working directory. For empty array, clears the selected files. - This method expects [`elementHandle`] to point to an + This method expects `ElementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). However, if the element is inside the `