Skip to content

Commit

Permalink
use stubfile to override typing (h/t @bandophahita).
Browse files Browse the repository at this point in the history
  • Loading branch information
perrygoy committed Jul 12, 2024
1 parent 6190e30 commit 986d6c1
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 8 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ split-on-trailing-comma = false
"PLR", # likewise using specific numbers and strings in tests.
]
"__version__.py" = ["D"]
"*.pyi" = [
"PLR0913", # it's not our fault they have too many parameters!
]


################################################################################
Expand Down
28 changes: 20 additions & 8 deletions screenpy_playwright/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Pattern, Tuple, TypedDict, Union

from playwright.sync_api import Locator
from playwright.sync_api import FrameLocator, Locator

from .abilities import BrowseTheWebSynchronously
from .exceptions import TargetingError
Expand Down Expand Up @@ -99,7 +99,7 @@ def __repr__(self) -> str:
__str__ = __repr__


class Target(Locator):
class Target(Locator, FrameLocator):
"""A described element on a webpage.
Uses Playwright's Locator API to describe an element on a webpage, with a
Expand Down Expand Up @@ -148,12 +148,16 @@ def in_frame(self, frame_locator: str) -> Target:

def __getattribute__(self, name: str) -> _Manipulation:
"""Convert a Playwright Locator strategy into a Manipulation."""
if not name.startswith("_") and hasattr(Locator, name):
attr = getattr(Locator, name)
is_private = name.startswith("_")
superclass_has_attr = hasattr(Locator, name) or hasattr(FrameLocator, name)
if not is_private and superclass_has_attr:
attr = getattr(Locator, name) or getattr(FrameLocator, name)
is_property = type(attr) is property
r_type = getattr(attr, "__annotations__", {}).get("return")
is_right_type = r_type is Locator or r_type is FrameLocator
is_right_str = r_type in ["Locator", "FrameLocator"]

if is_property or r_type is Locator or r_type == "Locator":
if is_property or is_right_type or is_right_str:
manipulation = _Manipulation(self, name)
self.manipulations.append(manipulation)
return manipulation
Expand Down Expand Up @@ -211,9 +215,17 @@ def found_by(self, the_actor: Actor) -> Locator:
if manipulation.args is None and manipulation.kwargs is None:
locator = getattr(locator, manipulation.name)
else:
locator = getattr(locator, manipulation.name)(
*manipulation.args, **manipulation.kwargs
)
args = []
for arg in manipulation.args:
arg_to_append = arg
if isinstance(arg, Target):
arg_to_append = arg.found_by(the_actor)
args.append(arg_to_append)
kwargs = {
k: v.found_by(the_actor) if isinstance(v, Target) else v
for k, v in manipulation.kwargs.items()
}
locator = getattr(locator, manipulation.name)(*args, **kwargs)

return locator

Expand Down
122 changes: 122 additions & 0 deletions screenpy_playwright/target.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Used to overwrite type hints for Locator methods from Playwright.
Hopefully this is only temporary. -pgoy 2024-JUL-12
"""

from collections import UserString
from dataclasses import dataclass
from typing import Pattern, TypedDict

from playwright.sync_api import FrameLocator, Locator
from screenpy import Actor as Actor
from typing_extensions import NotRequired, Self, Unpack

from .abilities import BrowseTheWebSynchronously as BrowseTheWebSynchronously
from .exceptions import TargetingError as TargetingError

_ManipulationArgsType = tuple[str | int | None, ...]

class _ManipulationKwargsType(TypedDict):
has_text: NotRequired[str | Pattern[str] | None]
has_not_text: NotRequired[str | Pattern[str] | None]
has: NotRequired[Locator | None]
has_not: NotRequired[Locator | None]
exact: NotRequired[bool | None]
checked: NotRequired[bool | None]
disabled: NotRequired[bool | None]
expanded: NotRequired[bool | None]
include_hidden: NotRequired[bool | None]
level: NotRequired[int | None]
name: NotRequired[str | Pattern[str] | None]
pressed: NotRequired[bool | None]
selected: NotRequired[bool | None]

@dataclass
class _Manipulation(UserString):
def __hash__(self) -> int: ...
def __eq__(self, other: object) -> bool: ...
def __getattr__(self, name: str) -> Target | _Manipulation: ...
def __call__(
self,
*args: Unpack[_ManipulationArgsType],
**kwargs: Unpack[_ManipulationKwargsType],
) -> Target: ...
def get_locator(self) -> str: ...
def __init__(
self,
target: Target,
name: str,
args: _ManipulationArgsType | None = ...,
kwargs: _ManipulationKwargsType | None = ...,
) -> None: ...

class Target(FrameLocator, Locator):
manipulations: list[_Manipulation]
first: Target
last: Target
owner: Target
content_frame: Target
@classmethod
def the(cls, name: str) -> Self: ...
def located_by(self, locator: str) -> Target: ...
def in_frame(self, frame_locator: str) -> Target: ...
def __getattribute__(self, name: str) -> _Manipulation: ...
@property
def target_name(self) -> str: ...
@target_name.setter
def target_name(self, value: str) -> None: ...
def found_by(self, the_actor: Actor) -> Locator: ...
def __init__(self, name: str | None = None) -> None: ...

# overridden Locator methods
def and_(self, locator: Locator | Target) -> Target: ...
def filter(
self,
*,
has_text: str | Pattern | None = None,
has_not_text: str | Pattern | None = None,
has: Locator | Target | None = None,
has_not: Locator | Target | None = None,
) -> Target: ...
def frame_locator(self, selector: str) -> Target: ...
def get_by_alt_text(
self, text: str | Pattern, *, exact: bool | None = None
) -> Target: ...
def get_by_label(
self, text: str | Pattern, *, exact: bool | None = None
) -> Target: ...
def get_by_placeholder(
self, text: str | Pattern, *, exact: bool | None = None
) -> Target: ...
def get_by_role(
self,
role: str,
*,
checked: bool | None = None,
disabled: bool | None = None,
expanded: bool | None = None,
include_hidden: bool | None = None,
level: int | None = None,
name: str | Pattern | None = None,
pressed: bool | None = None,
selected: bool | None = None,
exact: bool | None = None,
) -> Target: ...
def get_by_test_id(self, test_id: str | Pattern) -> Target: ...
def get_by_text(
self, text: str | Pattern, *, exact: bool | None = None
) -> Target: ...
def get_by_title(
self, text: str | Pattern, *, exact: bool | None = None
) -> Target: ...
def locator(
self,
selector: str | Locator,
*,
has_text: str | Pattern | None = None,
has_not_text: str | Pattern | None = None,
has: Locator | Target | None = None,
has_not: Locator | Target | None = None,
) -> Target: ...
def nth(self, index: int) -> Target: ...
def or_(self, locator: Locator | Target) -> Target: ...
2 changes: 2 additions & 0 deletions tests/test_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ def test_can_be_instantiated(self) -> None:
t4 = Target().located_by("test")
t5 = Target()
t6 = Target("test").get_by_label("test", exact=True)
t7 = Target("frame").frame_locator("test").content_frame

assert isinstance(t1, Target)
assert isinstance(t2, Target)
assert isinstance(t3, Target)
assert isinstance(t4, Target)
assert isinstance(t5, Target)
assert isinstance(t6, Target)
assert isinstance(t7, Target)

def test_auto_describe(self) -> None:
t1 = Target().located_by("#yellow")
Expand Down

0 comments on commit 986d6c1

Please sign in to comment.