Skip to content

Commit

Permalink
[#576][#454] REFACTOR: refactor "pure" entity logic to mixins...
Browse files Browse the repository at this point in the history
+ reuse mixins in core.Element, core.All_, core.Client (new name for core.Context)
+ break down entity.py into separate modules per actual entity implementation
  • Loading branch information
yashaka committed Feb 15, 2025
1 parent 6f1c091 commit f7b6d05
Show file tree
Hide file tree
Showing 33 changed files with 1,616 additions and 1,771 deletions.
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,15 @@ Done:
- collection.shadow_roots based on webelement.shadow_root
- element.frame_context
- Made selene.Element a web.Element instead of core.Element
- broke down core.entity.py into core.* [#454](https://github.com/yashaka/selene/issues/454)

Next:
- make core.Element a base class for web.Element
- ensure query.* and command.* use proper classes
- make web.Element a base class for web.Element
- make device.Element a base class for web.Element + move cross-platform support to base classes
- rename context.py to client.py
- ensure query.* and command.* use proper base classes
- review config options... some will not work for core.Element anymore... should we document this?

### pyperclip as dependency in Selene

Expand Down Expand Up @@ -613,6 +617,32 @@ Providing a brief overview of the modules and how to define your own custom comm

Just "autocomplete" is disabled, methods still work;)

### Removed deprecated methods on ...

browser.element(selector).* (web.Element.*):
- `_execute_script(script_on_self_element_and_args, *extra_args)`
- use `execute_script(script_on_self, *args)` instead

### Removed from selene.core.Browser.*

- properties:
- `.__raw__` (renamed it to `._raw_`)

### Removed from selene.core.Element.*

- methods:
- `_execute_script(script_on_self_element_and_args, *extra_args)`
- properties:
- `.__raw__` (renamed it to `._raw_`)
- support of all js-like config options

### Removed from selene.core.Collection.*

- methods:
- deprecated earlier `collection.filtered_by(condition)` in favor of `collection.by(condition)`
- properties:
- `.__raw__` (renamed it to `._raw_`)

### Fix misleading absence of waiting in slicing behavior

Now this will fail:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
from selene import Element
from selene.core.condition import Condition
from selene import web
from selene.core.condition import Condition, Match
from selene.core.conditions import ElementCondition
from selene.support.conditions.be import * # noqa
from selene.support.conditions.be import not_ as _original_not_ # noqa

not_ = _original_not_

visible_in_viewport: Condition[Element] = ElementCondition.raise_if_not(
visible_in_viewport: Condition[web.Element] = Match(
'is visible in view port',
lambda element: element.execute_script(
by=lambda element: element.execute_script(
'''
var rect = element.getBoundingClientRect();
var rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
'''
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
'''
),
)

not_visible_in_viewport: Condition[Element] = visible_in_viewport.not_
not_visible_in_viewport: Condition[web.Element] = visible_in_viewport.not_
5 changes: 2 additions & 3 deletions examples/custom_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from selene import browser, Browser, have
from selene import browser, Browser, Element, have
from selene.core.condition import Condition
from selene.core.conditions import ElementCondition, BrowserCondition
from selene.core.entity import Element
from tests import resources


Expand All @@ -35,7 +34,7 @@ def fn(entity: Element) -> None:
entity.type('one more').press_enter()
raise AssertionError(f'actual produced todos were: {size}')

return ElementCondition(f'have produced {number} todos', fn)
return Condition(f'have produced {number} todos', fn)


def test_wait_for_produced_todos_v1():
Expand Down
4 changes: 2 additions & 2 deletions selene/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# --- ADVANCED --- #

from selene.core import query, command
from selene.core import query, command, Collection
from selene.core.condition import not_ # just in case

# --- SHARED --- #
Expand All @@ -42,7 +42,7 @@

# --- probably just for Type Hints --- #

from selene.core.entity import Element, Collection
from selene.core._element import Element
from selene.core.condition import Condition
from selene.core.conditions import (
ElementCondition,
Expand Down
24 changes: 24 additions & 0 deletions selene/common/fp.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ def _either_res_or(
return cast(R, AbsentResult(str(fn))), failure


I = TypeVar('I')


def _maybeinstance(instance, type_: Type[I]) -> I | None:
"""An alternative to isinstance(type_instance:=instance, type_) to be used
as maybe_type_instance = _maybeinstance(instance, type_)...
Unfortunately will only work inside `if` clause with mypy...
Yet mypy also may not work in `else` clause in some cases... :(
Kind of, right now, there is no much need in this maybeinstance, because
we can use `isinstance(type_instance:=instance, type_)` directly...
"""
return cast(I, instance) if isinstance(instance, type_) else None


# todo: can we make the callable params generic too? (instead current ... as placeholder)
def _either(
res: Callable[..., R], /, *, or_: Type[E]
Expand All @@ -104,6 +120,14 @@ def _either(
)


def raise_(exception: E) -> None:
raise exception


def throw(exception: E) -> None:
raise exception


def pipe(*functions) -> Optional[Callable[[Any], Any]]:
"""
pipes functions one by one in the provided order
Expand Down
9 changes: 9 additions & 0 deletions selene/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations

from selene.core._element import Element
from selene.core._elements import All
from selene.core.entity import Collection
from selene.core._elements_context import _ElementsContext
from selene.core._client import Client

# TODO: should we break core.* into [core.]model.* and [core.]webdriver.*?
4 changes: 2 additions & 2 deletions selene/core/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
# SOFTWARE.

from __future__ import annotations
from typing import Optional, List, Union, overload
from typing_extensions import Optional, List, Union, overload

from selenium.webdriver.common.actions.wheel_input import ScrollOrigin
from selenium.webdriver.remote.webelement import WebElement

from selenium.webdriver import ActionChains
from selenium.webdriver.common.action_chains import AnyDevice

from selene.core.entity import Element
from selene.core._element import Element
from selene.core.configuration import Config
from selene.common._typing_functions import Query, Command

Expand Down
85 changes: 12 additions & 73 deletions selene/core/_browser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2015-2022 Iakiv Kramarenko
# Copyright (c) 2015 Iakiv Kramarenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand All @@ -22,91 +22,31 @@
from __future__ import annotations

import warnings
from typing import Optional, Union, Tuple
from typing_extensions import Optional, Union, Tuple

from selenium.webdriver.remote.switch_to import SwitchTo
from selenium.webdriver.remote.webdriver import WebDriver

from selene.core._actions import _Actions
from selene.core._client import Client
from selene.core.configuration import Config
from selene.core.entity import WaitingEntity, Element, Collection
from selene.core._entity import _WaitingConfiguredEntity
from selene.core import Collection
from selene.core._element import Element
from selene.core.locator import Locator
from selene.support.webdriver import WebHelper


class Browser(WaitingEntity['Browser']):
def __init__(self, config: Optional[Config] = None):
config = Config() if config is None else config
super().__init__(config)

def with_(self, config: Optional[Config] = None, **config_as_kwargs) -> Browser:
return (
Browser(config)
if config
else Browser(self.config.with_(**config_as_kwargs))
class Browser(Client):
def __init__(self, config: Optional[Config] = None, **kwargs):
warnings.warn(
'selene.core.Browser is deprecated, use selene.core.Client instead',
DeprecationWarning,
)
super().__init__(config=config, **kwargs)

def __str__(self):
return 'browser'

# todo: consider not just building driver but also adjust its size according to config
@property
def driver(self) -> WebDriver:
return self.config.driver

# TODO: consider making it callable (self.__call__() to be shortcut to self.__raw__ ...)

@property
def __raw__(self):
return self.config.driver

# @property
# def actions(self) -> ActionChains:
# """
# It's kind of just a shortcut for pretty low level actions from selenium webdriver
# Yet unsure about this property here:)
# comparing to usual high level Selene API...
# Maybe later it would be better to make own Actions with selene-style retries, etc.
# """
# return ActionChains(self.config.driver)

@property
def _actions(self) -> _Actions:
return _Actions(self.config)

# --- Element builders --- #

# TODO: consider None by default,
# and *args, **kwargs to be able to pass custom things
# to be processed by config.location_strategy
# and by default process none as "element to skip all actions on it"
def element(
self, css_or_xpath_or_by: Union[str, Tuple[str, str], Locator]
) -> Element:
if isinstance(css_or_xpath_or_by, Locator):
return Element(css_or_xpath_or_by, self.config)

by = self.config._selector_or_by_to_by(css_or_xpath_or_by)
# todo: do we need by_to_locator_strategy?

return Element(
Locator(f'{self}.element({by})', lambda: self.driver.find_element(*by)),
self.config,
)

def all(
self, css_or_xpath_or_by: Union[str, Tuple[str, str], Locator]
) -> Collection:
if isinstance(css_or_xpath_or_by, Locator):
return Collection(css_or_xpath_or_by, self.config)

by = self.config._selector_or_by_to_by(css_or_xpath_or_by)

return Collection(
Locator(f'{self}.all({by})', lambda: self.driver.find_elements(*by)),
self.config,
)

# --- High Level Commands--- #

def open(self, relative_or_absolute_url: Optional[str] = None) -> Browser:
Expand All @@ -130,7 +70,6 @@ def quit(self) -> None:
"""
self.driver.quit()

# TODO: consider deprecating, it does not close browser, it closes current tab/window
def close(self) -> Browser:
self.driver.close()
return self
Expand Down
8 changes: 6 additions & 2 deletions selene/core/_browser.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ from selenium.webdriver.remote.webdriver import WebDriver as WebDriver
from selenium.webdriver.remote.webelement import WebElement
from typing import Callable, Iterable, Optional, Tuple, TypeVar, Union, Any, Generic

from selene.core.entity import WaitingEntity, Element, Collection, E
from selene.core._elements_context import E
from selene.core._entity import _WaitingConfiguredEntity
from selene.core import Collection
from selene.core._element import Element

class Browser(WaitingEntity['Browser']):
class Browser(_WaitingConfiguredEntity):
def __init__(self, config: Optional[Config] = ...) -> None: ...
def with_(
self,
Expand Down Expand Up @@ -114,6 +117,7 @@ class Browser(WaitingEntity['Browser']):
selector_to_by_strategy: Callable[[str], Tuple[str, str]] = ...,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
**config_as_kwargs,
) -> Browser: ...
@property
def driver(self) -> WebDriver: ...
Expand Down
Loading

0 comments on commit f7b6d05

Please sign in to comment.