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.
@@ -0,0 +1,10 @@
1
+
2
+ from ._types_and_tools import center_of
3
+ from .getter import Getter
4
+ from ._any_all_getters import BoolAnyGetter, BoolAllGetter
5
+ from ._values_getter import ValuesGetter
6
+ from ._asserter_getter import AsserterGetter
7
+ from ._filter_getter import FilterGetter
8
+ from ._order_getter import OrderGetter
9
+ from ._mapper_getter import MapperGetter
10
+ from ._negation_transmitter import NotTransmitter
@@ -0,0 +1,17 @@
1
+
2
+ from typing import Callable, ClassVar
3
+
4
+ from .getter import Getter
5
+
6
+
7
+
8
+ # Explicite redeclaration, to register properly the accessor:
9
+ class BoolAllGetter(Getter, accessor="all"):
10
+ pass
11
+
12
+
13
+
14
+ class BoolAnyGetter(BoolAllGetter, accessor="any"):
15
+
16
+ COMBINER: ClassVar[Callable] = any
17
+
@@ -0,0 +1,90 @@
1
+ from textwrap import dedent
2
+ from typing import Any, Callable
3
+
4
+ from selenium.webdriver.remote.webelement import WebElement
5
+
6
+ from ._basics.generic_value_getter import should_be
7
+ from .getter import Getter
8
+
9
+
10
+
11
+
12
+
13
+
14
+ class AsserterGetter(Getter, accessor="check"):
15
+ """
16
+ Asserter version of the Getter (css, data, props, id, has_class, ...), raising
17
+ AssertionError if ever something doesn't match.
18
+ * Assertions are chainable.
19
+ * An extra assertion message (head of default message) can be set using `getter.check(msg)`.
20
+
21
+ The expected values can be given as a unique value all the elements must match, or as a list.
22
+ In the latter,
23
+
24
+ Accessible through `Getter.check`.
25
+ """
26
+
27
+ _msg:str = None
28
+
29
+ def __call__(self, msg:str):
30
+ self._msg = dedent(msg).rstrip().lstrip('\n')
31
+ return self
32
+
33
+ def _with_msg(self, msg:str):
34
+ if not self._msg:
35
+ return msg
36
+ return f"\n\n{ self._msg or '' }\n\n{ msg }"
37
+
38
+ def _check_elements(
39
+ self,
40
+ ref_items,
41
+ value_getter: Callable[[WebElement,str],Any],
42
+ *,
43
+ is_ok: Callable[[Any,Any],bool] = None,
44
+ feedback: Callable[[Any,Any],bool] = None,
45
+ ) -> 'AsserterGetter' :
46
+
47
+ is_ok = self._default_is_ok(is_ok)
48
+ feedback = feedback or should_be
49
+
50
+ bads = [] if self.elements else ('no such element',)
51
+ bads.extend(
52
+ f"Wrong number of expected values for {prop!r}: {len(exp)} should be {len(self.elements)}"
53
+ for prop, exp in ref_items
54
+ if isinstance(exp, (list,tuple)) and len(self.elements) != len(exp)
55
+ )
56
+ for i_elt,element in enumerate(self.elements):
57
+ bads.extend(
58
+ f"{prop}: { feedback(expected, actual, self._negate) }"
59
+ for prop, exp in ref_items
60
+ if not is_ok(
61
+ (expected := exp[i_elt] if isinstance(exp, (tuple, list)) else exp),
62
+ (actual := value_getter(element, prop))
63
+ )
64
+ )
65
+
66
+ if bads:
67
+ data = ',\n '.join(bads)
68
+ msg = self._with_msg(f" { data }\n\nOn: { self }")
69
+ assert False, msg
70
+
71
+ self._msg = None
72
+ return self
73
+
74
+
75
+ def exists(self, exp=True):
76
+ """ Usable for non existing list of elements """
77
+ actual = super().exists() # Already handle the negation!
78
+ msg = self._with_msg(f" {actual} should { ' not'*self._negate }be {exp}\n\nOn: {self}")
79
+ assert actual == exp, msg
80
+ return self
81
+
82
+
83
+ def count(self, n:int):
84
+ """ Usable for non existing list of elements """
85
+ actual = super().count() # Already handle the negation!
86
+ msg = self._with_msg(
87
+ f" {actual} should { ' not'*self._negate }be {n}\n\nWrong number of children on: {self}"
88
+ )
89
+ assert actual == n, msg
90
+ return self
File without changes
@@ -0,0 +1,362 @@
1
+
2
+ from contextlib import contextmanager, nullcontext
3
+ from typing import Any, ClassVar, List, Optional, Union, TYPE_CHECKING
4
+
5
+ from selenium.webdriver import Keys
6
+ from selenium.webdriver.common.action_chains import ActionChains
7
+
8
+ from .._types_and_tools import GetterOrCss, WaitingContext
9
+ from .base_getter import BaseGetter
10
+
11
+ if TYPE_CHECKING:
12
+ from ..getter import Getter
13
+
14
+
15
+
16
+
17
+ class ActionGetter(BaseGetter):
18
+ """
19
+ Relay class, applying automatically the action which are normally `on_element` (or equivalent)
20
+ to all the self.elements. The chain of actions must then be triggered by accessing the `go`
21
+ property (triggers Action.perform() on all elements).
22
+
23
+ Methods which take another target as argument may take a css selector string instead
24
+ of a WebElement: the original method will be called with the equivalent element.
25
+ Note that the cardinality of the elements in the current object and those in the
26
+ targeted ones must be identical.
27
+
28
+ (see: https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/common/action_chains.py)
29
+ """
30
+
31
+ _context_action: ClassVar[Optional[ActionChains]] = None
32
+ """
33
+ Global ActionChains instance used when the `ActionGetter.context_chain_and_go` context
34
+ manager is used.
35
+ """
36
+
37
+
38
+ DURATION: ClassVar[int] = 40
39
+ """
40
+ Base duration (in milliseconds value for selenium actions (mouse move, in between clicks, ...)
41
+ Warning about consecutive clicks/actions: short values might end up in different behaviors.
42
+ Selenium default is 250 ms.
43
+ """
44
+
45
+
46
+ @staticmethod
47
+ @contextmanager
48
+ def swap_duration(duration:int):
49
+ """
50
+ Temporarily change the default duration for all actions. Restore it at the end of the tests.
51
+ """
52
+ old = ActionGetter.DURATION
53
+ ActionGetter.DURATION = duration
54
+ try:
55
+ yield
56
+ finally:
57
+ ActionGetter.DURATION = old
58
+
59
+
60
+ @contextmanager
61
+ def context_chain_and_go(self, duration:int=None):
62
+ """
63
+ With this context manager, the very same ActionChain object is used for every single
64
+ element/Getter object. When resuming executions, the ActionChain is automatically
65
+ triggered.
66
+
67
+ WARNING:
68
+ - `scroll_to` is not usable this way: scroll to an element must be done before, and all
69
+ subsequent moves should be done through other methods.
70
+ - there are no specific checks about other actions performed when several elements exist
71
+ on the same Getter object, so everything will be applied in order, even if it actually
72
+ does not make sense at `perform` time.
73
+ """
74
+ ActionGetter._context_action = ActionChains(
75
+ self.driver,
76
+ duration=self.DURATION if duration is None else duration
77
+ )
78
+ try:
79
+ yield None
80
+ ActionGetter._context_action.perform()
81
+ finally:
82
+ ActionGetter._context_action = None
83
+
84
+
85
+ def __getitem__(self, idx:int):
86
+ """
87
+ Returns a new instance of the current class, with only the element at index @idx.
88
+ If the current instance has actions, the corresponding action object is shared with the
89
+ returned object.
90
+ """
91
+ sub_getter = super().__getitem__(idx)
92
+ if self._actions:
93
+ sub_getter._actions = [self._actions[idx]]
94
+ return sub_getter
95
+
96
+ @property
97
+ def go(self):
98
+ """ Alias for `perform()`. """
99
+ return self.perform()
100
+
101
+ def perform(self) -> 'Getter':
102
+ """
103
+ Apply all the registered actions, then return a fresh Getter holding the very same
104
+ elements, but with a find method usable and no actions registered.
105
+ """
106
+ for action in self._actions:
107
+ action.perform()
108
+ return self
109
+
110
+
111
+ #------------------------------------------------------------------
112
+
113
+ def _this_or_that(self, other:Optional[GetterOrCss], current:bool=False):
114
+ """
115
+ By default apply the actions on the elements of the current instance.
116
+ If `@other` is provided, use the corresponding elements instead.
117
+ If @current=True, this means the current mouse position will be used to perform the
118
+ action, instead of moving to any element before.
119
+ """
120
+ if current and other is not None:
121
+ raise ValueError(
122
+ f"Cannot use the `current` mouse location when another element is targeted ({other=!r})."
123
+ )
124
+ return (
125
+ (None,) * len(self.elements) if current else
126
+ self.elements if not other else
127
+ self._getter_or_css(other).elements
128
+ )
129
+
130
+ def _getter_or_css(self, thing:Optional[GetterOrCss]) -> 'Getter' :
131
+ return thing and (self.find(thing, in_page=True) if isinstance(thing, str) else thing)
132
+
133
+ def _duplicate_value(self, value:Any):
134
+ return (value,) * len(self.elements)
135
+
136
+ def _apply(self, name:str, *args_values:List[Any]):
137
+ """
138
+ @args_values: list of all values to use for each element.
139
+ The lists are provided in the order matching the original method arguments.
140
+ Inside the lists, the values must match the self.elements order.
141
+
142
+ @returns a new ActionGetter instance, holding the new actions.
143
+ THIS INSTANCE MUST BE KEPT/STORED/USED to actually trigger all the registered actions.
144
+ """
145
+ if not self.elements:
146
+ raise ValueError(
147
+ f"Cannot initiate actions on a Getter without known elements.\n{ self }"
148
+ )
149
+
150
+ sizes = [*map(len,args_values)]
151
+ if len(set(sizes)) != 1:
152
+ raise ValueError(f"Mismatched arguments data lengths for {name}: {sizes}.\n{ self }")
153
+
154
+ zipped_args = tuple(zip(*args_values))
155
+ if len(zipped_args) != len(self.elements):
156
+ raise ValueError(
157
+ f"Mismatched arguments lengths for {name}: {len(self.elements)} elements, but found "
158
+ f"{len(zipped_args)} sets of arguments.\n{ self }"
159
+ )
160
+
161
+ if ActionGetter._context_action is not None:
162
+ for args in zipped_args:
163
+ ActionGetter._context_action = getattr(ActionGetter._context_action, name)(*args)
164
+ return self # No need to return a new instance, since no instance states are mutated, here.
165
+
166
+ else:
167
+ actions = self._actions or [
168
+ ActionChains(self.driver, duration=self.DURATION) for _ in self.elements
169
+ ]
170
+ built_actions = [
171
+ getattr(action, name)(*args) for args,action in zip(zipped_args, actions)
172
+ ]
173
+ out = ActionGetter.from_(self)
174
+ out._actions = built_actions
175
+ return out
176
+
177
+
178
+ #-----------------------------------------------------------------------
179
+
180
+
181
+ def reset_actions(self):
182
+ return self._apply("reset_actions")
183
+
184
+ def click(self, other:Optional[GetterOrCss]=None, *, current=False):
185
+ return self._apply("click", self._this_or_that(other, current))
186
+
187
+ def click_and_hold(self, other:Optional[GetterOrCss]=None, *, current=False):
188
+ return self._apply("click_and_hold", self._this_or_that(other, current))
189
+
190
+ def context_click(self, other:Optional[GetterOrCss]=None, *, current=False):
191
+ return self._apply("context_click", self._this_or_that(other, current))
192
+
193
+ def double_click(self, other:Optional[GetterOrCss]=None, *, current=False):
194
+ return self._apply("double_click", self._this_or_that(other, current))
195
+
196
+ def drag_and_drop(
197
+ self,
198
+ target: GetterOrCss,
199
+ get_target: bool=False,
200
+ other_src: Optional[GetterOrCss]=None,
201
+ *,
202
+ current=False,
203
+ ):
204
+ """
205
+ If @get_target is True, returns the Getter of the targeted elements instead of the current
206
+ instance.
207
+ The actions are transferred to the target object (shared object => CAREFUL WITH THAT!!!!)
208
+ """
209
+ targets = self._getter_or_css(target)
210
+ self._apply("drag_and_drop", self._this_or_that(other_src, current), targets.elements)
211
+ if get_target:
212
+ targets._actions = self._actions
213
+ return targets
214
+ return self
215
+
216
+ def drag_and_drop_by_offset(self, xoffset: int, yoffset: int, other_src:Optional[GetterOrCss]=None):
217
+ return self._apply(
218
+ "drag_and_drop_by_offset",
219
+ self._this_or_that(other_src),
220
+ self._duplicate_value(xoffset),
221
+ self._duplicate_value(yoffset),
222
+ )
223
+
224
+ def key_down(self, value: str, other:Optional[GetterOrCss]=None, *, current=False):
225
+ return self._apply("key_down", self._duplicate_value(value), self._this_or_that(other, current))
226
+
227
+ def key_up(self, value: str, other:Optional[GetterOrCss]=None, *, current=False):
228
+ return self._apply("key_up", self._duplicate_value(value), self._this_or_that(other, current))
229
+
230
+ def move_by_offset(self, xoffset: int, yoffset: int, release=False):
231
+ out = self._apply(
232
+ "move_by_offset",
233
+ self._duplicate_value(xoffset), self._duplicate_value(yoffset)
234
+ )
235
+ if release:
236
+ self.release(current=True)
237
+ return out
238
+
239
+ def move_to_element(self, other:Optional[GetterOrCss]=None):
240
+ return self._apply("move_to_element", self._this_or_that(other))
241
+
242
+ move_to = move_to_element
243
+ """ Alias for move_to_element """
244
+
245
+ def move_to_element_with_offset(self, xoffset: int, yoffset: int, other:Optional[GetterOrCss]=None):
246
+ return self._apply(
247
+ "move_to_element_with_offset", self._this_or_that(other),
248
+ self._duplicate_value(xoffset), self._duplicate_value(yoffset)
249
+ )
250
+
251
+ def pause(self, seconds: Union[float, int, None]=None):
252
+ return self._apply("pause", self._duplicate_value(seconds or self.DURATION))
253
+
254
+ def release(self, other:Optional[GetterOrCss]=None, *, current=False):
255
+ return self._apply("release", self._this_or_that(other, current))
256
+
257
+ def send_keys_to_element(self, *keys_to_send: str, other:Optional[GetterOrCss]=None, current=False):
258
+ return self._apply(
259
+ "send_keys_to_element", self._this_or_that(other, current),
260
+ *map(self._duplicate_value, keys_to_send)
261
+ )
262
+
263
+ send_keys = send_keys_to_element
264
+ """
265
+ Alias for send_keys_to_element (because the Getter setup generalize the thing...)
266
+ """
267
+
268
+
269
+ # /!\ Selenium's base Scroll stuff just DOES NOT WORK!???
270
+ #
271
+ # def scroll_to_element(self, other:Optional[GetterOrCss]=None):
272
+ # return self._apply("scroll_to_element", self._this_or_that(other))
273
+
274
+ # def scroll_by_amount(self, delta_x: int, delta_y: int):
275
+ # return self._apply(
276
+ # "scroll_by_amount",
277
+ # self._duplicate_value(delta_x), self._duplicate_value(delta_y)
278
+ # )
279
+
280
+ # def scroll_from_origin(self, scroll_origin: ScrollOrigin, delta_x: int, delta_y: int):
281
+ # raise NotImplementedError("Getting lazy...")
282
+
283
+ def scroll_to(self, other:Optional[GetterOrCss]=None, *, shift:float=0, target_location:str='3/4'):
284
+ """
285
+ Scroll the current/given element into view. @shift may be used to tweak the final scroll
286
+ position, to avoid some targets to go out of the viewport.
287
+
288
+ @shift: Apply a vertical shift to the point considered as the point to center in the page.
289
+ @target_location: Ratio of innerHeight where to place the "center+shift" of the target.
290
+
291
+ WARNING: this action CANNOT be chained with others, because it is applied directly with JS.
292
+ """
293
+ elt = self if other is None else self._getter_or_css(other)
294
+ # Cannot scroll to different elements, considering how the actions are handled, so forbid it:
295
+ elt.check("Getters.scroll_to usage requires instances with exactly one element").count(1)
296
+
297
+ assert not self._actions and not ActionGetter._context_action, (
298
+ "scroll_to(...) cannot be chained with other actions, because it interacts directly with "
299
+ "the page, through the JS executor."
300
+ )
301
+
302
+ rect = elt.get.rect
303
+ center_y = rect['y'] + rect['height']/2 + shift
304
+
305
+ self.run_js(f"""
306
+ var centerY={ center_y }, H=innerHeight, Y=scrollY
307
+ var inBound = Y + H/6 <= centerY && centerY <= Y + H * 5/6
308
+ if(!inBound){{
309
+ scrollTo(0, centerY - H * { target_location } );
310
+ }}
311
+ """)
312
+ return elt
313
+
314
+
315
+ def send_shortcut(
316
+ self,
317
+ keys:str,
318
+ *,
319
+ waiting_context:WaitingContext=None,
320
+ ):
321
+ """
322
+ Takes a string representing a combination of keys "+" separated (spaces are ignored), like:
323
+ "Ctrl+S"
324
+ "Ctrl + Alt + Shift + Enter"
325
+ ...
326
+ The string is split, converted to the related selenium Keys and the combination is
327
+ automatically applied to the element.
328
+
329
+ @waiting_context: If given, must be a context manager function (ALREADY CALLED) to control
330
+ any delay/logic that should ba applied around the shortcut application.
331
+ """
332
+ *modifiers,key = map(to_keys, keys.replace(' ','').split("+"))
333
+ self.scroll_to()
334
+ with waiting_context or nullcontext():
335
+ with self.context_chain_and_go():
336
+ for mod in modifiers:
337
+ self.key_down(mod)
338
+ self.send_keys(key)
339
+ for mod in modifiers:
340
+ self.key_up(mod, current=True)
341
+ # `current` because the shortcut could cause a move in the page.
342
+
343
+
344
+
345
+
346
+ def to_keys(name:str) -> Keys:
347
+ """
348
+ Automatically convert a `Keys` name string to the corresponding value, or return the
349
+ original string.
350
+
351
+ * The input is case insensitive.
352
+ * 'CTRL' is automatically converted to 'CONTROL'
353
+ * 'PLUS' is automatically converted to 'Keys.ADD' (-> numpad +)
354
+ """
355
+ upper = name.upper()
356
+ if name.upper() == 'PLUS':
357
+ return Keys.ADD
358
+ if upper == 'CTRL':
359
+ upper = 'CONTROL'
360
+ return getattr(Keys, upper, name)
361
+
362
+
@@ -0,0 +1,177 @@
1
+ """
2
+ Kind of jQuery like usage, but for Selenium logistic/elements...
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from functools import wraps
7
+ from typing import Callable, ClassVar, List, TYPE_CHECKING, Tuple
8
+
9
+ from selenium.webdriver.common.by import By
10
+ from selenium.webdriver.remote.webdriver import WebDriver
11
+ from selenium.webdriver.support.wait import WebDriverWait
12
+ from selenium.webdriver.remote.webelement import WebElement
13
+ from selenium.webdriver.common.action_chains import ActionChains
14
+
15
+ if TYPE_CHECKING:
16
+ from ..getter import Getter
17
+
18
+
19
+
20
+ def negate_output(method):
21
+ @wraps(method)
22
+ def wrapper(self:'Getter', *a, **kw):
23
+ out = method(self, *a, **kw)
24
+ return self.NEGATE_OUTCOME[self._negate](out)
25
+ return wrapper
26
+
27
+ def forbid_negation(method):
28
+ @wraps(method)
29
+ def wrapper(self:'Getter', *a, **kw):
30
+ if self._negate:
31
+ raise ValueError(
32
+ "The `not_` modifier/property should not be used with the `get` one."
33
+ )
34
+ return method(self, *a, **kw)
35
+ return wrapper
36
+
37
+
38
+ @dataclass
39
+ class BaseGetter:
40
+
41
+ driver: WebDriver = None
42
+ waiter: WebDriverWait = None
43
+ elements: List[WebElement] = field(default_factory=list)
44
+
45
+ _css_selector: str = ""
46
+ _full_css: str = ""
47
+
48
+ _negate: bool = False
49
+
50
+ _actions: List[ActionChains] = field(default_factory=list)
51
+
52
+
53
+ NEGATE_OUTCOME: ClassVar[Tuple[Callable,Callable]] = (lambda _:_), (lambda v: not v)
54
+
55
+
56
+ def __repr__(self):
57
+ return f"{ self.__class__.__name__ }({self._full_css!r}, n_elements={ len(self.elements) })"
58
+
59
+
60
+ @negate_output
61
+ def exists(self):
62
+ return bool(self.elements)
63
+ # __bool__ = exists # Dropped (mostly causing a mess: not possible to check instance definition against None)
64
+
65
+
66
+ @negate_output
67
+ def count(self):
68
+ return len(self.elements)
69
+ __len__ = count
70
+
71
+
72
+ def __iter__(self):
73
+ return ( self.__class__.from_(self, [elt]) for elt in self.elements )
74
+
75
+
76
+ def __getitem__(self, idx:int):
77
+ return self.__class__.from_(self, [self.elements[idx]])
78
+
79
+
80
+ def find(self, css_selector:str, *, in_page=False) -> 'Getter':
81
+ """
82
+ Find all children element matching the given css selector.
83
+ If @in_page is true, starts searching from the top of the page instead of starting
84
+ with the current element/Getter.
85
+ @key: ordering key function, if given.
86
+ """
87
+ if not in_page and not self.elements:
88
+ raise ValueError(
89
+ f"Inconsistent `find` request: the current selection is empty.\n{ self }"
90
+ )
91
+ if in_page:
92
+ elements = self.driver.find_elements(By.CSS_SELECTOR, css_selector)
93
+ else:
94
+ elements = []
95
+ for elt in self.elements:
96
+ children = elt.find_elements(By.CSS_SELECTOR, css_selector)
97
+ elements.extend(children)
98
+
99
+ full_css_path = f'{self._full_css} -> "{css_selector}"' if self._full_css else f'"{css_selector}"'
100
+
101
+ return self.__class__.from_(self, elements, css_selector, full_css_path)
102
+
103
+
104
+ def run_js(self, code:str, *args, async_=False, with_promise=True):
105
+ """
106
+ Use driver.execute_script on the given code.
107
+
108
+ * `@args` are arguments that will be passed in the JS environment (available through the
109
+ `arguments` array).
110
+ * `@async_=False`: if True, JS script will run async?. An error is raised if `with_promise`
111
+ is False and `done` is not present in the code (a call to it is needed to resume execution).
112
+ * `@with_promise=True`: if True and the script is supposed to be run async, `code` is
113
+ expected to be a Promise (aka, no await in the source!) and `'.then(done, console.error)'`
114
+ is automatically added at the end of `code` to consume the promise and resume executions.
115
+ In that case, the `done` call is not needed in the source code.
116
+ """
117
+ method = 'execute_script'
118
+ if async_:
119
+ method = 'execute_async_script'
120
+ if not with_promise and 'done' not in code:
121
+ raise ValueError("Invalid async script: a call to the `done` callback is needed")
122
+ code = f"""
123
+ var done = arguments[arguments.length - 1];
124
+ { code }{ '.then(done, console.error)' * with_promise }
125
+ """
126
+
127
+ return getattr(self.driver, method)(code, *args)
128
+
129
+
130
+ def wait_for_element(self, css_selector):
131
+ if self._actions:
132
+ raise ValueError(f"Cannot use `wait_until(...)` on objects with defined ActionChains.")
133
+
134
+ def finder(_):
135
+ elt = self.find(css_selector, in_page=True)
136
+ return elt.exists() and elt
137
+
138
+ return self.waiter.until(finder)
139
+
140
+
141
+ def wait_until(self, bool_provider):
142
+ if self._actions:
143
+ raise ValueError(f"Cannot use `wait_until(...)` on objects with defined ActionChains.")
144
+ return self.waiter.until(bool_provider)
145
+
146
+ def wait_prompt_or_alert(self):
147
+ def _wait(*_):
148
+ try:
149
+ return self.driver.switch_to.alert
150
+ except:
151
+ return None
152
+ return self.waiter.until(_wait)
153
+
154
+ def clear(self):
155
+ """
156
+ Clear the content of editable and resettable elements (no validation -> raise at runtime).
157
+ """
158
+ for elt in self.elements:
159
+ elt.clear()
160
+
161
+
162
+ #-----------------------------------------------------
163
+
164
+
165
+ @classmethod
166
+ def from_(cls, getter:'Getter', elements=None, _css_selector=None, _full_css=None):
167
+ """
168
+ Build a new Getter instance (from the given class/subclass) based on the current
169
+ object, updating various values on the fly.
170
+ """
171
+ return cls(
172
+ getter.driver,
173
+ getter.waiter,
174
+ getter.elements if elements is None else elements, # no nee dto copy the list: never mutated
175
+ getter._css_selector if _css_selector is None else _css_selector,
176
+ getter._full_css if _full_css is None else _full_css,
177
+ )
@@ -0,0 +1,36 @@
1
+
2
+
3
+ from typing import Any, Callable, ClassVar
4
+ from operator import eq, ne
5
+
6
+ from .base_getter import negate_output
7
+ from .generic_value_getter import ComparerGetter
8
+
9
+
10
+
11
+ class BooleanGetter(ComparerGetter):
12
+ """
13
+ Common/default behavior, equivalent to `Getter.all...`
14
+ """
15
+
16
+ COMBINER: ClassVar[Callable] = all
17
+
18
+ _negate_outside = False
19
+ """ If True, apply a negation inside the COMBINER function (aka, for each element). """
20
+
21
+ def _default_is_ok(self, is_ok: Callable[[Any,Any],bool]):
22
+ if self._negate:
23
+ return (lambda a,b: not is_ok(a,b)) if is_ok else ne
24
+ return is_ok or eq
25
+
26
+ @negate_output
27
+ def _check_elements(
28
+ self, ref_items, value_getter, *, is_ok = None, **_,
29
+ ):
30
+ is_ok = self._default_is_ok(is_ok)
31
+ out = self.COMBINER(
32
+ is_ok(exp, value_getter(element, prop))
33
+ for element in self.elements
34
+ for prop, exp in ref_items
35
+ )
36
+ return not out if self._negate_outside else out