selenium-query 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {selenium_query-0.1.4 → selenium_query-0.1.5}/PKG-INFO +1 -3
  2. {selenium_query-0.1.4 → selenium_query-0.1.5}/pyproject.toml +1 -4
  3. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_asserter_getter.py +2 -2
  4. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_basics/action_getter.py +9 -3
  5. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_basics/base_getter.py +25 -9
  6. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_basics/generic_value_getter.py +1 -1
  7. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_mapper_getter.py +15 -8
  8. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/getter.py +32 -1
  9. {selenium_query-0.1.4 → selenium_query-0.1.5}/README.md +0 -0
  10. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/__init__.py +0 -0
  11. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_any_all_getters.py +0 -0
  12. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_basics/__init__.py +0 -0
  13. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_basics/boolean_getter.py +0 -0
  14. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_filter_getter.py +0 -0
  15. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_negation_transmitter.py +0 -0
  16. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_order_getter.py +0 -0
  17. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_types_and_tools.py +0 -0
  18. {selenium_query-0.1.4 → selenium_query-0.1.5}/selenium_query/_values_getter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: selenium-query
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Python equivalent of jQuery, to write selenium tests.
5
5
  License: GPL-3.0-or-later
6
6
  Author: Frédéric Zinelli
@@ -13,8 +13,6 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
- Provides-Extra: pmt
17
- Requires-Dist: pmt-tools (>=0.1.2) ; extra == "pmt"
18
16
  Requires-Dist: selenium (>=4.36)
19
17
  Description-Content-Type: text/markdown
20
18
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "selenium-query"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Python equivalent of jQuery, to write selenium tests."
5
5
  license = { text = "GPL-3.0-or-later" }
6
6
  authors = [
@@ -12,9 +12,6 @@ dependencies = [
12
12
  "selenium (>=4.36)"
13
13
  ]
14
14
 
15
- [project.optional-dependencies]
16
- pmt = [ "pmt-tools>=0.1.2" ]
17
-
18
15
 
19
16
  [tool.setuptools]
20
17
  packages = ["selenium_query"]
@@ -75,7 +75,7 @@ class AsserterGetter(Getter, accessor="check"):
75
75
  def exists(self, exp=True):
76
76
  """ Usable for non existing list of elements """
77
77
  actual = super().exists() # Already handle the negation!
78
- msg = self._with_msg(f" {actual} should { ' not'*self._negate }be {exp}\n\nOn: {self}")
78
+ msg = self._with_msg(f" {actual} should { 'not '*self._negate }be {exp}\n\nOn: {self}")
79
79
  assert actual == exp, msg
80
80
  return self
81
81
 
@@ -84,7 +84,7 @@ class AsserterGetter(Getter, accessor="check"):
84
84
  """ Usable for non existing list of elements """
85
85
  actual = super().count() # Already handle the negation!
86
86
  msg = self._with_msg(
87
- f" {actual} should { ' not'*self._negate }be {n}\n\nWrong number of children on: {self}"
87
+ f" {actual} should { 'not '*self._negate }be {n}\n\nWrong number of children on: {self}"
88
88
  )
89
89
  assert actual == n, msg
90
90
  return self
@@ -42,6 +42,12 @@ class ActionGetter(BaseGetter):
42
42
  Selenium default is 250 ms.
43
43
  """
44
44
 
45
+ AUTO_SHIFT: ClassVar[float] = 0
46
+ """
47
+ Systematic shift value to use for `scroll_to` (helps when the targeted element is inside an
48
+ iframe, for example).
49
+ """
50
+
45
51
 
46
52
  @staticmethod
47
53
  @contextmanager
@@ -249,7 +255,7 @@ class ActionGetter(BaseGetter):
249
255
  )
250
256
 
251
257
  def pause(self, seconds: Union[float, int, None]=None):
252
- return self._apply("pause", self._duplicate_value(seconds or self.DURATION))
258
+ return self._apply("pause", self._duplicate_value(seconds or self.DURATION/1000))
253
259
 
254
260
  def release(self, other:Optional[GetterOrCss]=None, *, current=False):
255
261
  return self._apply("release", self._this_or_that(other, current))
@@ -291,16 +297,16 @@ class ActionGetter(BaseGetter):
291
297
  WARNING: this action CANNOT be chained with others, because it is applied directly with JS.
292
298
  """
293
299
  elt = self if other is None else self._getter_or_css(other)
300
+
294
301
  # Cannot scroll to different elements, considering how the actions are handled, so forbid it:
295
302
  elt.check("Getters.scroll_to usage requires instances with exactly one element").count(1)
296
-
297
303
  assert not self._actions and not ActionGetter._context_action, (
298
304
  "scroll_to(...) cannot be chained with other actions, because it interacts directly with "
299
305
  "the page, through the JS executor."
300
306
  )
301
307
 
302
308
  rect = elt.get.rect
303
- center_y = rect['y'] + rect['height']/2 + shift
309
+ center_y = rect['y'] + rect['height']/2 + shift + self.AUTO_SHIFT
304
310
 
305
311
  self.run_js(f"""
306
312
  var centerY={ center_y }, H=innerHeight, Y=scrollY
@@ -1,10 +1,10 @@
1
1
  """
2
2
  Kind of jQuery like usage, but for Selenium logistic/elements...
3
3
  """
4
-
4
+ import datetime as dt
5
5
  from dataclasses import dataclass, field
6
6
  from functools import wraps
7
- from typing import Callable, ClassVar, List, TYPE_CHECKING, Tuple
7
+ from typing import Callable, ClassVar, List, TYPE_CHECKING, Tuple, Union
8
8
 
9
9
  from selenium.webdriver.common.by import By
10
10
  from selenium.webdriver.remote.webdriver import WebDriver
@@ -73,11 +73,13 @@ class BaseGetter:
73
73
  return ( self.__class__.from_(self, [elt]) for elt in self.elements )
74
74
 
75
75
 
76
- def __getitem__(self, idx:int):
77
- return self.__class__.from_(self, [self.elements[idx]])
76
+ def __getitem__(self, idx:Union[int,slice]):
77
+ if isinstance(idx, int):
78
+ return self.__class__.from_(self, [self.elements[idx]])
79
+ return self.__class__.from_(self, self.elements[idx])
78
80
 
79
81
 
80
- def find(self, css_selector:str, *, in_page=False) -> 'Getter':
82
+ def find(self, css_selector:str, *, in_page=False, as_:str="") -> 'Getter':
81
83
  """
82
84
  Find all children element matching the given css selector.
83
85
  If @in_page is true, starts searching from the top of the page instead of starting
@@ -96,7 +98,8 @@ class BaseGetter:
96
98
  children = elt.find_elements(By.CSS_SELECTOR, css_selector)
97
99
  elements.extend(children)
98
100
 
99
- full_css_path = f'{self._full_css} -> "{css_selector}"' if self._full_css else f'"{css_selector}"'
101
+ designation = f'"{ as_ or css_selector }"'
102
+ full_css_path = f'{self._full_css} -> {designation}' if self._full_css else designation
100
103
 
101
104
  return self.__class__.from_(self, elements, css_selector, full_css_path)
102
105
 
@@ -127,17 +130,29 @@ class BaseGetter:
127
130
  return getattr(self.driver, method)(code, *args)
128
131
 
129
132
 
130
- def wait_for_element(self, css_selector):
133
+ def wait_for_element(self, css_selector, as_=""):
131
134
  if self._actions:
132
135
  raise ValueError(f"Cannot use `wait_until(...)` on objects with defined ActionChains.")
133
136
 
137
+ date = None
138
+ delta = dt.timedelta(seconds=0.05)
139
+
134
140
  def finder(_):
135
- elt = self.find(css_selector, in_page=True)
136
- return elt.exists() and elt
141
+ nonlocal date
142
+
143
+ elt = self.find(css_selector, in_page=True, as_=as_)
144
+ found = elt.exists() and elt
145
+ if found:
146
+ if not date:
147
+ date = dt.datetime.now()
148
+ if dt.datetime.now() - date > delta:
149
+ return found
150
+ return False
137
151
 
138
152
  return self.waiter.until(finder)
139
153
 
140
154
 
155
+
141
156
  def wait_until(self, bool_provider):
142
157
  if self._actions:
143
158
  raise ValueError(f"Cannot use `wait_until(...)` on objects with defined ActionChains.")
@@ -157,6 +172,7 @@ class BaseGetter:
157
172
  """
158
173
  for elt in self.elements:
159
174
  elt.clear()
175
+ return self
160
176
 
161
177
 
162
178
  #-----------------------------------------------------
@@ -28,7 +28,7 @@ def should_contain(exp:Expected, act:Actual, with_neg:bool):
28
28
  return f"{act!r} should { ' not'*with_neg }contain {exp!r}"
29
29
 
30
30
  def should_be(exp:Expected, act:Actual, with_neg:bool):
31
- return f"{act!r} should { ' not'*with_neg }be {exp!r}"
31
+ return f"{act!r} should { 'not '*with_neg }be {exp!r}"
32
32
 
33
33
 
34
34
 
@@ -3,7 +3,7 @@ from types import MethodType
3
3
  from typing import Any, Dict, List, Optional, Tuple
4
4
 
5
5
 
6
- from ._types_and_tools import MAPPER_GETTER_FOLLOW_UP, TGetter
6
+ from ._types_and_tools import MAPPER_GETTER_FOLLOW_UP, _GetterSubClassesAsProperties, TGetter
7
7
  from ._basics.base_getter import forbid_negation
8
8
  from .getter import Getter
9
9
 
@@ -94,13 +94,18 @@ class MapperGetter(Getter, accessor="map"):
94
94
 
95
95
 
96
96
  @classmethod
97
- def from_(cls, getter:Getter, _css_selector=None, _full_css=None):
98
- return super().from_(getter, [*getter], _css_selector, _full_css)
97
+ def from_(cls, getter:Getter, elements=None, _css_selector=None, _full_css=None, merged=False):
98
+ if elements is not None and not merged:
99
+ raise ValueError("Cannot pass an @elements list to MapperGetter.from_, unless @merge is True")
100
+ elements_as_getters = elements or [*getter]
101
+ return super().from_(getter, elements_as_getters, _css_selector, _full_css)
99
102
 
100
103
 
101
- def __call__(self, css_selector:str=None):
104
+ def __call__(self, css_selector:str=None, as_=""):
102
105
  if css_selector is not None:
103
- self.elements = [g.find(css_selector) for g in self.elements]
106
+ self.elements = [g.find(css_selector, as_=as_) for g in self.elements]
107
+ if as_:
108
+ raise ValueError("Cannot use @as_ without @css_selector argument")
104
109
  return self
105
110
 
106
111
 
@@ -113,11 +118,13 @@ class MapperGetter(Getter, accessor="map"):
113
118
 
114
119
 
115
120
  def __getattribute__(self, k:str):
116
- _getattr = super().__getattribute__
117
121
  if k in _FORBIDDEN_FOLLOW_UP:
118
122
  raise ValueError(f"Cannot use {k} on MapperGetter objects.")
119
123
 
120
- if _getattr('subclass').is_getter_property(k):
124
+ _getattr = super().__getattribute__
125
+
126
+ sub_class:_GetterSubClassesAsProperties = _getattr('subclass')
127
+ if sub_class.is_getter_property(k):
121
128
  return MapperTransmitter(driver=None, waiter=None, _source=self, _prop=k)
122
129
 
123
130
  if k in MAPPER_GETTER_FOLLOW_UP:
@@ -147,7 +154,7 @@ class MapperGetter(Getter, accessor="map"):
147
154
  def _apply_caller(self, bound_methods:List[MethodType]):
148
155
  def call(*a, zip_args=True, non_existent_at_idxs=(), **kw):
149
156
  """
150
- If @zip_args=False, directly transfer *args and ù**kwargs to the bound methods, without
157
+ If @zip_args=False, directly transfer *args and **kwargs to the bound methods, without
151
158
  applying any zipping/mapping logic on values given as lists.
152
159
 
153
160
  If @non_existent_at_idxs is given, it must be a tuple of indices. The bound method at these
@@ -1,4 +1,4 @@
1
- from typing import ClassVar
1
+ from typing import ClassVar, List
2
2
 
3
3
  from ._basics.base_getter import BaseGetter
4
4
  from ._basics.boolean_getter import BooleanGetter
@@ -25,8 +25,39 @@ class Getter(ActionGetter, BooleanGetter, BaseGetter):
25
25
  if accessor:
26
26
  setattr(cls.subclass, accessor, cls)
27
27
 
28
+
29
+ @classmethod
30
+ def merge_cls(cls, *getters:'Getter', mapped=False, as_=None) -> 'Getter':
31
+ """
32
+ Merge collections of Getters together. Any MApperGetter object is "flattened" with the
33
+ other elements.
34
+
35
+ If @mapped is True, Build a MapperGetter if the source elements.
36
+ If @designation is given, it will replace the internal path/css selectors informations.
37
+ """
38
+ flat: List[Getter] = []
39
+ for g in getters:
40
+ if isinstance(g, cls.subclass.map):
41
+ flat.extend(g.elements)
42
+ else:
43
+ flat.append(g)
44
+
45
+ if mapped:
46
+ return cls.subclass.map.from_(flat[0], flat, as_, as_, merged=True)
47
+
48
+ elements = [e for g in flat for e in g.elements]
49
+ return cls.from_(flat[0], elements, as_, as_)
50
+
51
+ def merge(self, *others:'Getter', mapped=False, as_=None) -> 'Getter':
52
+ """
53
+ Merge collections of Getters together.
54
+ """
55
+ return self.merge_cls(self, *others, as_=as_, mapped=mapped)
56
+
57
+
28
58
  #------------------------------------------------------------------------------------------
29
59
 
60
+
30
61
  @property
31
62
  def all(self):
32
63
  return self.subclass.all.from_(self)
File without changes