selenium-query 0.1.0__py3-none-any.whl
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.
- selenium_query/__init__.py +10 -0
- selenium_query/_any_all_getters.py +17 -0
- selenium_query/_asserter_getter.py +90 -0
- selenium_query/_basics/__init__.py +0 -0
- selenium_query/_basics/action_getter.py +362 -0
- selenium_query/_basics/base_getter.py +177 -0
- selenium_query/_basics/boolean_getter.py +36 -0
- selenium_query/_basics/generic_value_getter.py +119 -0
- selenium_query/_filter_getter.py +53 -0
- selenium_query/_mapper_getter.py +226 -0
- selenium_query/_negation_transmitter.py +40 -0
- selenium_query/_order_getter.py +70 -0
- selenium_query/_types_and_tools.py +93 -0
- selenium_query/_values_getter.py +78 -0
- selenium_query/getter.py +61 -0
- selenium_query-0.1.0.dist-info/METADATA +19 -0
- selenium_query-0.1.0.dist-info/RECORD +18 -0
- selenium_query-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Callable, Optional, Sequence, Tuple
|
|
3
|
+
|
|
4
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
5
|
+
|
|
6
|
+
from .._types_and_tools import Expected, Actual
|
|
7
|
+
from .base_getter import BaseGetter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Value getters:
|
|
13
|
+
def element_is_as_method(elt:WebElement, prop:str):
|
|
14
|
+
return getattr(elt, prop)()
|
|
15
|
+
|
|
16
|
+
def element_rect_value(elt:WebElement, prop:str):
|
|
17
|
+
return elt.rect[prop]
|
|
18
|
+
|
|
19
|
+
# is_ok:
|
|
20
|
+
def contains(exp:Expected, act:Actual):
|
|
21
|
+
return act is not None and exp in act
|
|
22
|
+
|
|
23
|
+
def contains_as_list(exp:Expected, act:Actual):
|
|
24
|
+
return act is not None and exp in act.split()
|
|
25
|
+
|
|
26
|
+
# feedback:
|
|
27
|
+
def should_contain(exp:Expected, act:Actual, with_neg:bool):
|
|
28
|
+
return f"{act!r} should { ' not'*with_neg }contain {exp!r}"
|
|
29
|
+
|
|
30
|
+
def should_be(exp:Expected, act:Actual, with_neg:bool):
|
|
31
|
+
return f"{act!r} should { ' not'*with_neg }be {exp!r}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComparerGetter(BaseGetter):
|
|
36
|
+
"""
|
|
37
|
+
Verify that all the elements are matching the given arguments (css, data, props, ...),
|
|
38
|
+
returning a boolean.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def id(self, txt:str):
|
|
42
|
+
expectations = ( ("id",txt), )
|
|
43
|
+
return self._check_elements(expectations, WebElement.get_attribute)
|
|
44
|
+
|
|
45
|
+
def has_class(self, txt:str):
|
|
46
|
+
expectations = ( ("class",txt), )
|
|
47
|
+
return self._check_elements(
|
|
48
|
+
expectations, WebElement.get_attribute,
|
|
49
|
+
is_ok = contains_as_list,
|
|
50
|
+
feedback = should_contain
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def tag_name(self, txt:str):
|
|
54
|
+
expectations = ( ("tag_name",txt), )
|
|
55
|
+
return self._check_elements(expectations, getattr)
|
|
56
|
+
|
|
57
|
+
def text(self, txt:str):
|
|
58
|
+
expectations = ( ("text",txt), )
|
|
59
|
+
return self._check_elements(expectations, getattr)
|
|
60
|
+
|
|
61
|
+
def has_text(self, txt:str):
|
|
62
|
+
expectations = ( ("text",txt), )
|
|
63
|
+
return self._check_elements(expectations, getattr, is_ok=contains, feedback=should_contain)
|
|
64
|
+
|
|
65
|
+
def is_enabled(self, expected:bool=True):
|
|
66
|
+
expectations = ( ("is_enabled",expected), )
|
|
67
|
+
return self._check_elements(expectations, element_is_as_method)
|
|
68
|
+
|
|
69
|
+
def is_selected(self, expected:bool=True):
|
|
70
|
+
expectations = ( ("is_selected",expected), )
|
|
71
|
+
return self._check_elements(expectations, element_is_as_method)
|
|
72
|
+
|
|
73
|
+
def is_displayed(self, expected:bool=True):
|
|
74
|
+
expectations = ( ("is_displayed",expected), )
|
|
75
|
+
return self._check_elements(expectations, element_is_as_method)
|
|
76
|
+
|
|
77
|
+
def css(self, dct=None, **kwargs:Any):
|
|
78
|
+
expectations = self._merge_args(dct,kwargs).items()
|
|
79
|
+
return self._check_elements(expectations, WebElement.value_of_css_property)
|
|
80
|
+
|
|
81
|
+
def data(self, dct=None, **kwargs:Any):
|
|
82
|
+
expectations = {'data'+s: v for s,v in self._merge_args(dct,kwargs).items()}
|
|
83
|
+
return self.props(expectations)
|
|
84
|
+
|
|
85
|
+
def props(self, dct=None, **kwargs:Any):
|
|
86
|
+
expectations = self._merge_args(dct,kwargs).items()
|
|
87
|
+
return self._check_elements(expectations, WebElement.get_attribute)
|
|
88
|
+
|
|
89
|
+
def rect(self, *, x:float=None, y:float=None, width:float=None, height:float=None):
|
|
90
|
+
items = (('x',x), ('y',y), ('width',width), ('height',height))
|
|
91
|
+
expectations = tuple( (prop,exp) for prop,exp in items if exp is not None ),
|
|
92
|
+
return self._check_elements(expectations, element_rect_value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#----------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _merge_args(self, dct:Optional[dict], kwargs:dict):
|
|
99
|
+
if dct:
|
|
100
|
+
kwargs.update(dct)
|
|
101
|
+
return kwargs
|
|
102
|
+
|
|
103
|
+
def _check_elements(
|
|
104
|
+
self,
|
|
105
|
+
ref_items: Sequence[Tuple[str,Expected]],
|
|
106
|
+
value_getter: Callable[[WebElement,str],Any],
|
|
107
|
+
*,
|
|
108
|
+
is_ok: Callable[[Expected,Actual],bool] = None,
|
|
109
|
+
feedback: Callable[[Any,Any],bool] = None,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Method applying the logic of the child class to each of the properties/values/...
|
|
113
|
+
|
|
114
|
+
@ref_items: Sequence/Iterable (NOT a generator) of tuples `(prop_or_attr, expected_value)`.
|
|
115
|
+
@value_getter: Access the value for the given string on the WebElement.
|
|
116
|
+
@is_ok: if needed, compare expected value with the value coming from the value_getter.
|
|
117
|
+
@feedback: build a string for assertion messages (see AssertGetter).
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
4
|
+
|
|
5
|
+
from .getter import Getter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FilterGetter(Getter, accessor="filter"):
|
|
9
|
+
"""
|
|
10
|
+
Accessible through `Getter.filter`.
|
|
11
|
+
Transform the css, data, props, ... methods into filters: each of them now returns a
|
|
12
|
+
new FilterGetter instance whose the elements are only containing those which match the
|
|
13
|
+
desired values.
|
|
14
|
+
|
|
15
|
+
Basic usage:
|
|
16
|
+
|
|
17
|
+
Getter(...).filter.has_class('this') -> FilterGetter
|
|
18
|
+
|
|
19
|
+
FilterGetter instances can use custom predicates when called. The predicate takes a
|
|
20
|
+
Getter object as argument (one per `self.elements`):
|
|
21
|
+
|
|
22
|
+
Getter.filter(predicate) -> FilterGetter
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __call__(self, getter_ok:Callable[[Getter],bool]):
|
|
26
|
+
out = [g.elements[0] for g in self if getter_ok(g)]
|
|
27
|
+
return self.from_(self, elements=out)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _check_elements(
|
|
31
|
+
self,
|
|
32
|
+
ref_items,
|
|
33
|
+
value_getter: Callable[[WebElement,str],Any],
|
|
34
|
+
*,
|
|
35
|
+
is_ok: Callable[[Any,Any],bool] = None,
|
|
36
|
+
**_,
|
|
37
|
+
) -> 'FilterGetter':
|
|
38
|
+
"""
|
|
39
|
+
The method now returns a new Getter instance whose the elements list only contains those
|
|
40
|
+
which match the criteria given to the calling method (css, data, props, ...).
|
|
41
|
+
"""
|
|
42
|
+
is_ok = self._default_is_ok(is_ok)
|
|
43
|
+
out = [
|
|
44
|
+
element
|
|
45
|
+
for element in self.elements
|
|
46
|
+
if all( is_ok(exp, value_getter(element, prop)) for prop, exp in ref_items)
|
|
47
|
+
]
|
|
48
|
+
return self.from_(self, elements=out)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_(cls, getter:'Getter', elements=None, _css_selector=None, _full_css=None):
|
|
52
|
+
return super().from_(getter, elements, _css_selector, (_full_css or getter._full_css)+"[filtered]")
|
|
53
|
+
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from types import MethodType
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from ._types_and_tools import MAPPER_GETTER_FOLLOW_UP, TGetter
|
|
7
|
+
from ._basics.base_getter import forbid_negation
|
|
8
|
+
from .getter import Getter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_FORBIDDEN_FOLLOW_UP = 'go', 'perform', 'scroll_to', 'map', 'not_'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MapperTransmitter(Getter):
|
|
18
|
+
"""
|
|
19
|
+
Extends Getter for auto completion purpose only: the actual interface is different.
|
|
20
|
+
"""
|
|
21
|
+
_source: TGetter = None
|
|
22
|
+
_prop: str = None
|
|
23
|
+
_call_args: Optional[Tuple[Any]] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattribute__(self, k:str):
|
|
27
|
+
_getattr = super().__getattribute__
|
|
28
|
+
|
|
29
|
+
if k in MAPPER_GETTER_FOLLOW_UP:
|
|
30
|
+
source: MapperGetter = _getattr('_source')
|
|
31
|
+
return source._apply(k, transmitter=self)
|
|
32
|
+
|
|
33
|
+
kls = _getattr('__class__')
|
|
34
|
+
is_mapper_getter_attribute = k in _getattr('__dict__') or k in kls.__dict__
|
|
35
|
+
if is_mapper_getter_attribute:
|
|
36
|
+
return _getattr(k)
|
|
37
|
+
|
|
38
|
+
raise ValueError(f"Cannot use `{k}` on { kls.__name__ } objects.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __call__(self, *a, **kw):
|
|
42
|
+
"""
|
|
43
|
+
Relay to standardize TransmitterGetters for `.check(...)` or `.order_by(...)`.
|
|
44
|
+
"""
|
|
45
|
+
self._call_args = a, kw
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _apply(self, mapped:'Getter'):
|
|
50
|
+
out = getattr(mapped, self._prop)
|
|
51
|
+
if self._call_args:
|
|
52
|
+
a, kw = self._call_args
|
|
53
|
+
out = out(*a, **kw)
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(repr=False)
|
|
63
|
+
class MapperGetter(Getter, accessor="map"):
|
|
64
|
+
"""
|
|
65
|
+
Allow to work with a Getter "behaving like a list": the attributes/methods calls are
|
|
66
|
+
transferred to the elements (as Getters) instead of being applied to the current Getter.
|
|
67
|
+
|
|
68
|
+
Some methods or attributes accesses are forbidden on MapperGetter objects:
|
|
69
|
+
'go', 'perform', 'scroll_to', 'map'
|
|
70
|
+
|
|
71
|
+
Any subsequent "Getter" related method call (see `_GETTERS`) whose arguments are lists will
|
|
72
|
+
automatically map the lists elements to each internal `MapperGetter.elements` when transmitting
|
|
73
|
+
the methods call. The length of the lists and the one of `MapperGetter.elements` are check up
|
|
74
|
+
front and have to match, or ValueError is raised.
|
|
75
|
+
|
|
76
|
+
Other methods or property calls are working directly applying to the MapperGetter instance.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
|
|
80
|
+
G = Getter(...)
|
|
81
|
+
G.check.count(3)
|
|
82
|
+
|
|
83
|
+
mapped = G.map('li.that-class') # Equivalent to applying `find` to each element
|
|
84
|
+
mapped.count() == [1,4,6] # Gives the number of `li.that-class` elements
|
|
85
|
+
# held by each of the original G elements.
|
|
86
|
+
|
|
87
|
+
Note this class is really helpful only when testing hierarchical DOM structures, otherwise,
|
|
88
|
+
assertions with different expectations for each elements can already be done through
|
|
89
|
+
AsserterGetter objects, passing in lists or tuples, instead of values.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
elements: List[Getter] = field(default_factory=list)
|
|
93
|
+
""" Override """
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_(cls, getter:Getter, _css_selector=None, _full_css=None):
|
|
98
|
+
return super().from_(getter, [*getter], _css_selector, _full_css)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def __call__(self, css_selector:str=None):
|
|
102
|
+
if css_selector is not None:
|
|
103
|
+
self.elements = [g.find(css_selector) for g in self.elements]
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def __iter__(self):
|
|
109
|
+
yield from self.elements
|
|
110
|
+
|
|
111
|
+
def __getitem__(self, idx:int):
|
|
112
|
+
return self.elements[idx]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def __getattribute__(self, k:str):
|
|
116
|
+
_getattr = super().__getattribute__
|
|
117
|
+
if k in _FORBIDDEN_FOLLOW_UP:
|
|
118
|
+
raise ValueError(f"Cannot use {k} on MapperGetter objects.")
|
|
119
|
+
|
|
120
|
+
if _getattr('subclass').is_getter_property(k):
|
|
121
|
+
return MapperTransmitter(driver=None, waiter=None, _source=self, _prop=k)
|
|
122
|
+
|
|
123
|
+
if k in MAPPER_GETTER_FOLLOW_UP:
|
|
124
|
+
return super().__getattribute__('_apply')(k)
|
|
125
|
+
|
|
126
|
+
# Using "normal" methods/logic: just use the basic "get" logic
|
|
127
|
+
return _getattr(k)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@forbid_negation
|
|
131
|
+
def _apply(self, prop:str, transmitter:Optional[MapperTransmitter]=None):
|
|
132
|
+
if not self.elements:
|
|
133
|
+
raise ValueError(f"Cannot `_apply` on an empty MapperGetter instance.")
|
|
134
|
+
|
|
135
|
+
out = []
|
|
136
|
+
for g in self.elements:
|
|
137
|
+
if transmitter is not None:
|
|
138
|
+
g = transmitter._apply(g)
|
|
139
|
+
out.append(getattr(g, prop))
|
|
140
|
+
|
|
141
|
+
# If the values are actually bound methods, the next operation will be a method call:
|
|
142
|
+
if isinstance(out[0], MethodType):
|
|
143
|
+
return self._apply_caller(out)
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _apply_caller(self, bound_methods:List[MethodType]):
|
|
148
|
+
def call(*a, zip_args=True, non_existent_at_idxs=(), **kw):
|
|
149
|
+
"""
|
|
150
|
+
If @zip_args=False, directly transfer *args and ù**kwargs to the bound methods, without
|
|
151
|
+
applying any zipping/mapping logic on values given as lists.
|
|
152
|
+
|
|
153
|
+
If @non_existent_at_idxs is given, it must be a tuple of indices. The bound method at these
|
|
154
|
+
indices won't be called and their source Getter have to be non existent, otherwise an
|
|
155
|
+
error will be raised.
|
|
156
|
+
"""
|
|
157
|
+
self._validate_non_existent_elements(bound_methods, non_existent_at_idxs)
|
|
158
|
+
|
|
159
|
+
if zip_args:
|
|
160
|
+
args = self._validate_and_standardize_args(a)
|
|
161
|
+
kwargs = self._validate_and_standardize_kwargs(kw)
|
|
162
|
+
out = [
|
|
163
|
+
method.__self__ if i in non_existent_at_idxs else method(*aa, **kkw)
|
|
164
|
+
for i,(method, aa, kkw) in enumerate(zip(bound_methods, args, kwargs))
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
out = [
|
|
169
|
+
method.__self__ if i in non_existent_at_idxs else method(*a,**kw)
|
|
170
|
+
for i,method in enumerate(bound_methods)
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
if isinstance(out[0], Getter):
|
|
174
|
+
# elements are already Getters, no need to convert again:
|
|
175
|
+
return super(self.__class__, self).from_(self, elements=out)
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
return call
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
#------------------------------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _validate_non_existent_elements(self, bound_methods, non_existent_at_idxs):
|
|
185
|
+
if non_existent_at_idxs and (existents := [
|
|
186
|
+
bound_methods[i].__self__
|
|
187
|
+
for i in non_existent_at_idxs if len(bound_methods[i].__self__.elements)
|
|
188
|
+
]):
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"These Getters should have zero elements:\n "+'\n '.join(map(str,existents))
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_and_standardize_args(self, args: Tuple[Any]):
|
|
195
|
+
wrong_indices = [
|
|
196
|
+
i for i,v in enumerate(args)
|
|
197
|
+
if isinstance(v, (list,tuple)) and len(v) != len(self.elements)
|
|
198
|
+
]
|
|
199
|
+
if wrong_indices:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Mismatched list or tuple argument length at indices {wrong_indices} in {args} "
|
|
202
|
+
f"(expected length: {len(self.elements)})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
out_args = tuple(
|
|
206
|
+
tuple( arg[i] if isinstance(arg, (list,tuple)) else arg for arg in args)
|
|
207
|
+
for i in range(len(self.elements))
|
|
208
|
+
)
|
|
209
|
+
return out_args
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_and_standardize_kwargs(self, kw: Dict[str,Any] ):
|
|
213
|
+
lists_or_tuples_kw = tuple((k,v) for k,v in kw.items() if isinstance(v,(list,tuple)))
|
|
214
|
+
if lists_or_tuples_kw and (wrong_keys := [
|
|
215
|
+
k for k,v in lists_or_tuples_kw if len(v) != len(self.elements)
|
|
216
|
+
]):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Mismatched list or tuple value length for kwargs (keys: {wrong_keys}. "
|
|
219
|
+
f"Expected length: {len(self.elements)})"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
out_kw = (kw,)*len(self.elements) if not lists_or_tuples_kw else tuple(
|
|
223
|
+
{k: v[i] if isinstance(v,(tuple,list)) else v for k,v in kw.items()}
|
|
224
|
+
for i in range(len(self.elements))
|
|
225
|
+
)
|
|
226
|
+
return out_kw
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ._types_and_tools import TGetter
|
|
4
|
+
from .getter import Getter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
FORBIDDEN_NOT_FOLLOW_UP = set("""
|
|
8
|
+
map order_by
|
|
9
|
+
""".strip().split())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class NotTransmitter(Getter, accessor='not_'):
|
|
14
|
+
"""
|
|
15
|
+
Extends Getter mostly for auto completion purpose.
|
|
16
|
+
"""
|
|
17
|
+
source: TGetter = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __getattribute__(self, k:str):
|
|
21
|
+
src: TGetter = super().__getattribute__('source')
|
|
22
|
+
out = src.from_(src)
|
|
23
|
+
|
|
24
|
+
if k in FORBIDDEN_NOT_FOLLOW_UP:
|
|
25
|
+
raise ValueError(f"Cannot use `{k}` attribute access after the `not_` attribute.")
|
|
26
|
+
|
|
27
|
+
elif k in ('any', 'all'):
|
|
28
|
+
out._negate_outside = not out._negate_outside
|
|
29
|
+
|
|
30
|
+
elif k != 'not_':
|
|
31
|
+
out._negate = not out._negate
|
|
32
|
+
|
|
33
|
+
return getattr(out, k)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_(cls, getter, elements=None, _css_selector=None, _full_css=None):
|
|
38
|
+
out = super().from_(getter, elements, _css_selector, _full_css)
|
|
39
|
+
out.source = getter
|
|
40
|
+
return out
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Any, Callable, List
|
|
2
|
+
|
|
3
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
4
|
+
|
|
5
|
+
from .getter import Getter
|
|
6
|
+
from ._values_getter import ValuesGetter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OrderGetter(ValuesGetter, accessor="order_by"):
|
|
12
|
+
"""
|
|
13
|
+
Enable to sort the inner `elements` in various ways, returning a new instance of
|
|
14
|
+
the original class.
|
|
15
|
+
|
|
16
|
+
Getter(...).order_by.id -> Getter
|
|
17
|
+
Getter(...).order_by.has_class("xxx") -> Getter
|
|
18
|
+
|
|
19
|
+
It is possible to sort the elements with a custom key function by calling the OrderGetter
|
|
20
|
+
instance. The key function takes a Getter argument:
|
|
21
|
+
|
|
22
|
+
Getter(...).order_by(custom_key_as_getter) -> Getter
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_src_getter: Getter
|
|
26
|
+
|
|
27
|
+
# Autocompletion helpers: override the types WITHOUT assigning any actual value:
|
|
28
|
+
id: Getter
|
|
29
|
+
class_: Getter
|
|
30
|
+
tag_name: Getter
|
|
31
|
+
text: Getter
|
|
32
|
+
rect: Getter
|
|
33
|
+
is_enabled: Getter # WARNING: use as property!!
|
|
34
|
+
is_selected: Getter # WARNING: use as property!!
|
|
35
|
+
is_displayed: Getter # WARNING: use as property!!
|
|
36
|
+
css: Callable[[Getter],Getter]
|
|
37
|
+
data: Callable[[Getter],Getter]
|
|
38
|
+
props: Callable[[Getter],Getter]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_(cls, getter, elements=None, _css_selector=None, _full_css=None):
|
|
43
|
+
orderer = super().from_(getter, elements, _css_selector, _full_css)
|
|
44
|
+
orderer._src_getter = getter
|
|
45
|
+
return orderer
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __call__(self, keyer: Callable[[Getter],Any]):
|
|
49
|
+
keys = [ keyer(g) for g in self ]
|
|
50
|
+
return self.__order(keys)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_values(self, names, value_getter):
|
|
54
|
+
keys = super()._get_values(names, value_getter)
|
|
55
|
+
return self.__order(keys)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def __order(self, keys:List[Any]):
|
|
59
|
+
# Avoid any trouble (output types changing when only one element) + speed up:
|
|
60
|
+
if len(self.elements) == 1:
|
|
61
|
+
return self._src_getter
|
|
62
|
+
|
|
63
|
+
# Standardization if some elements returned list and others returned bare values (this
|
|
64
|
+
# may happen because of the ValueGetter logistic), to avoid crashes during comparisons:
|
|
65
|
+
if 0 < sum(isinstance(v, list) for v in keys) < len(keys):
|
|
66
|
+
keys = [ v if isinstance(v, list) else [v] for v in keys ]
|
|
67
|
+
|
|
68
|
+
by_idx = sorted(range(len(self.elements)), key=keys.__getitem__)
|
|
69
|
+
ordered = [self.elements[i] for i in by_idx]
|
|
70
|
+
return self._src_getter.from_(self._src_getter, elements=ordered)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from dataclasses import dataclass, fields
|
|
3
|
+
from contextlib import _GeneratorContextManager
|
|
4
|
+
from typing import Any, Dict, Literal, Type, TypeVar, Union, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._any_all_getters import BoolAnyGetter, BoolAllGetter
|
|
9
|
+
from ._values_getter import ValuesGetter
|
|
10
|
+
from ._asserter_getter import AsserterGetter
|
|
11
|
+
from ._filter_getter import FilterGetter
|
|
12
|
+
from ._negation_transmitter import NotTransmitter
|
|
13
|
+
from ._order_getter import OrderGetter
|
|
14
|
+
from ._mapper_getter import MapperGetter
|
|
15
|
+
from .getter import Getter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Expected = Any
|
|
20
|
+
Actual = Any
|
|
21
|
+
GetterOrCss = Union['Getter',str]
|
|
22
|
+
Rect = Dict[Literal['x','y','width','height'],float]
|
|
23
|
+
WaitingContext = _GeneratorContextManager[None, None, None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TGetter = TypeVar('TGetter', bound='Getter')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class _GetterSubClassesAsProperties:
|
|
32
|
+
|
|
33
|
+
all: Type['BoolAllGetter'] = None
|
|
34
|
+
any: Type['BoolAnyGetter'] = None
|
|
35
|
+
check: Type['AsserterGetter'] = None
|
|
36
|
+
filter: Type['FilterGetter'] = None
|
|
37
|
+
get: Type['ValuesGetter'] = None
|
|
38
|
+
order_by: Type['OrderGetter'] = None
|
|
39
|
+
map: Type['MapperGetter'] = None
|
|
40
|
+
not_: Type['NotTransmitter'] = None
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def fields(self):
|
|
44
|
+
return {f.name for f in fields(self.__class__)}
|
|
45
|
+
|
|
46
|
+
def is_getter_property(self, prop:str):
|
|
47
|
+
return prop in self.fields
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def center_of(g:'Getter'):
|
|
53
|
+
d = g.get.rect
|
|
54
|
+
return d['x'] + d['width']/2, d['y'] + d['height']/2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
MAPPER_GETTER_FOLLOW_UP = set("""
|
|
59
|
+
exists
|
|
60
|
+
count
|
|
61
|
+
find
|
|
62
|
+
__getitem__
|
|
63
|
+
id
|
|
64
|
+
class_
|
|
65
|
+
has_class
|
|
66
|
+
tag_name
|
|
67
|
+
text
|
|
68
|
+
has_text
|
|
69
|
+
is_enabled
|
|
70
|
+
is_selected
|
|
71
|
+
is_displayed
|
|
72
|
+
css
|
|
73
|
+
data
|
|
74
|
+
props
|
|
75
|
+
rect
|
|
76
|
+
reset_actions
|
|
77
|
+
click
|
|
78
|
+
click_and_hold
|
|
79
|
+
context_click
|
|
80
|
+
double_click
|
|
81
|
+
drag_and_drop
|
|
82
|
+
drag_and_drop_by_offset
|
|
83
|
+
key_down
|
|
84
|
+
key_up
|
|
85
|
+
moved_by_offset
|
|
86
|
+
move_to
|
|
87
|
+
move_to_element
|
|
88
|
+
move_to_element_with_offset
|
|
89
|
+
pause
|
|
90
|
+
release
|
|
91
|
+
send_keys_to_element
|
|
92
|
+
send_keys
|
|
93
|
+
""".split())
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Callable, Collection
|
|
3
|
+
|
|
4
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from ._types_and_tools import Rect
|
|
9
|
+
from ._basics.base_getter import forbid_negation
|
|
10
|
+
from ._basics.generic_value_getter import element_is_as_method
|
|
11
|
+
from .getter import Getter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(repr=False)
|
|
19
|
+
class ValueNameSetter:
|
|
20
|
+
|
|
21
|
+
value_getter: Callable[[WebElement,str],Any]
|
|
22
|
+
name: str = None
|
|
23
|
+
|
|
24
|
+
def __set_name__(self, _, prop):
|
|
25
|
+
self.name = (self.name or prop).rstrip("_") # Stripping because of `class_`
|
|
26
|
+
|
|
27
|
+
def __get__(self, obj:'ValuesGetter', kls):
|
|
28
|
+
return obj._get_values((self.name,), self.value_getter)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValuesGetter(Getter, accessor="get"):
|
|
34
|
+
"""
|
|
35
|
+
Extract the corresponding value(s) from the current Getter. The output type may change,
|
|
36
|
+
depending on the way the call is done and on what Getter object.
|
|
37
|
+
|
|
38
|
+
Depending on the number of elements:
|
|
39
|
+
* If the getter has only one element, the output is the value's) matching that element.
|
|
40
|
+
* If there are several elements, the output is a list of this/those value(s).
|
|
41
|
+
|
|
42
|
+
Depending on the number of values asked for (may happen when using css, data or props):
|
|
43
|
+
* If there are several kind of values asked for, the corresponding output value is a dict
|
|
44
|
+
(hence, a list of dicts, is several elements).
|
|
45
|
+
* If only one kind of value is asked for, the output is the value itself (hence, a list of
|
|
46
|
+
values if several elements).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@forbid_negation
|
|
50
|
+
def _get_values(self, names:Collection[str], value_getter: Callable[[WebElement,str],Any]):
|
|
51
|
+
if len(names)==1:
|
|
52
|
+
name = names[0]
|
|
53
|
+
out = [ value_getter(element, name) for element in self.elements ]
|
|
54
|
+
else:
|
|
55
|
+
out =[
|
|
56
|
+
{ prop: value_getter(element, prop) for prop in names }
|
|
57
|
+
for element in self.elements
|
|
58
|
+
]
|
|
59
|
+
return out if len(self.elements) != 1 else out[0]
|
|
60
|
+
|
|
61
|
+
id: str = ValueNameSetter(WebElement.get_attribute)
|
|
62
|
+
class_: str = ValueNameSetter(WebElement.get_attribute)
|
|
63
|
+
tag_name: str = ValueNameSetter(getattr)
|
|
64
|
+
text: str = ValueNameSetter(getattr)
|
|
65
|
+
rect: Rect = ValueNameSetter(getattr)
|
|
66
|
+
is_enabled: bool = ValueNameSetter(element_is_as_method) # WARNING: use as property!!
|
|
67
|
+
is_selected: bool = ValueNameSetter(element_is_as_method) # WARNING: use as property!!
|
|
68
|
+
is_displayed: bool = ValueNameSetter(element_is_as_method) # WARNING: use as property!!
|
|
69
|
+
|
|
70
|
+
def css(self, *args:str):
|
|
71
|
+
return self._get_values(args, WebElement.value_of_css_property)
|
|
72
|
+
|
|
73
|
+
def data(self, *args:str):
|
|
74
|
+
args = tuple( "data-"+s for s in args )
|
|
75
|
+
return self._get_values(args, WebElement.get_attribute)
|
|
76
|
+
|
|
77
|
+
def props(self, *args:str):
|
|
78
|
+
return self._get_values(args, WebElement.get_attribute)
|