Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: port layout selectors, filter, circular serialization (#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))
  • Loading branch information
rwoll committed May 11, 2022
1 parent 7b09eef commit ed13a53
Show file tree
Hide file tree
Showing 19 changed files with 1,193 additions and 216 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->101.0.4951.41<!-- GEN:stop --> ||||
| Chromium <!-- GEN:chromium-version -->102.0.5005.40<!-- GEN:stop --> ||||
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->98.0.2<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->99.0.1<!-- GEN:stop --> ||||

## Documentation

Expand Down
22 changes: 20 additions & 2 deletions playwright/_impl/_frame.py
Expand Up @@ -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)
Expand Down
41 changes: 34 additions & 7 deletions playwright/_impl/_impl_to_api_mapping.py
Expand Up @@ -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_"
Expand All @@ -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)
Expand All @@ -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
Expand Down
60 changes: 45 additions & 15 deletions playwright/_impl/_js_handle.py
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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":
Expand All @@ -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"]
Expand Down
60 changes: 54 additions & 6 deletions playwright/_impl/_locator.py
Expand Up @@ -26,6 +26,7 @@
Pattern,
TypeVar,
Union,
cast,
)

from playwright._impl._api_structures import (
Expand Down Expand Up @@ -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
Expand All @@ -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"<Locator frame={self._frame!r} selector={self._selector!r}>"
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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":
Expand Down
18 changes: 18 additions & 0 deletions 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]

0 comments on commit ed13a53

Please sign in to comment.