robotframework-appiumwindows 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,215 +1,271 @@
1
- import time
2
-
3
- from AppiumLibrary.keywords.keywordgroup import KeywordGroup
4
-
5
-
6
- class _WindowsKeywords(KeywordGroup):
7
-
8
- def __init__(self):
9
- super().__init__()
10
-
11
- # Public
12
- def appium_hover(self, locator, start_locator=None, timeout=20, **kwargs):
13
- self._info(f"Appium Hover '{locator}', timeout '{timeout}'")
14
- self._appium_hover_api(start_locator=start_locator, end_locator=locator, timeout=timeout, **kwargs)
15
-
16
- def appium_click_offset(self, locator, x_offset=0, y_offset=0, timeout=20, **kwargs):
17
- self._info(
18
- f"Appium Click Offset '{locator}', (x_offset,y_offset) '({x_offset},{y_offset})', timeout '{timeout}'")
19
- self._appium_click_api(locator=locator, timeout=timeout, x_offset=x_offset, y_offset=y_offset, **kwargs)
20
-
21
- def appium_right_click(self, locator, timeout=20, **kwargs):
22
- self._info(f"Appium Right Click '{locator}', timeout '{timeout}'")
23
- self._appium_click_api(locator=locator, timeout=timeout, button="right", **kwargs)
24
-
25
- def appium_left_click(self, locator, timeout=20, **kwargs):
26
- self._info(f"Appium Left Click '{locator}', timeout '{timeout}'")
27
- self._appium_click_api(locator=locator, timeout=timeout, button="left", **kwargs)
28
-
29
- def appium_double_click(self, locator, timeout=20, **kwargs):
30
- self._info(f"Appium Double Click '{locator}', timeout '{timeout}'")
31
- self._appium_click_api(locator=locator, timeout=timeout, times=2, **kwargs)
32
-
33
- def appium_drag_and_drop(self, start_locator=None, end_locator=None, timeout=20, **kwargs):
34
- self._info(f"Appium Drag And Drop '{start_locator} -> {end_locator}', timeout '{timeout}'")
35
- self._appium_drag_and_drop_api(start_locator=start_locator, end_locator=end_locator, timeout=timeout, **kwargs)
36
-
37
- def appium_drag_and_drop_by_offset(self, x_start, y_start, x_end, y_end):
38
- x_start, y_start, x_end, y_end = (int(x) for x in [x_start, y_start, x_end, y_end])
39
- self._info(f"Appium Drag And Drop By Offset ({x_start}, {y_start}) -> ({x_end}, {y_end})")
40
- self._appium_drag_and_drop_api(start_locator=None, end_locator=None, timeout=1,
41
- startX=x_start, startY=y_start,
42
- endX=x_end, endY=y_end)
43
-
44
- def appium_sendkeys(self, text=None, **kwargs):
45
- self._info(f"Appium Sendkeys '{text}'")
46
- self._appium_keys_api(text=text, **kwargs)
47
-
48
- # Private
49
- def _apply_modifier_keys(self, params: dict, modifier_keys):
50
- """Normalize modifier keys and update params in place."""
51
- if modifier_keys:
52
- if isinstance(modifier_keys, (list, tuple)):
53
- params["modifierKeys"] = [str(k).lower() for k in modifier_keys]
54
- else:
55
- params["modifierKeys"] = str(modifier_keys).lower()
56
-
57
- def _appium_click_api(self, locator, timeout, **kwargs):
58
- """
59
- Perform a click action on a Windows element using Appium Windows Driver.
60
-
61
- References:
62
- https://github.com/appium/appium-windows-driver
63
- https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-click
64
-
65
- Args:
66
- locator (str): Element locator.
67
- timeout (int): Maximum time to retry locating and clicking the element (in seconds).
68
- kwargs (dict): Additional click options.
69
-
70
- Keyword Args:
71
- button (str): Mouse button to click. One of:
72
- - "left" (default)
73
- - "middle"
74
- - "right"
75
- - "back"
76
- - "forward"
77
- modifierKeys (list|str): Keys to hold during the click. One or more of:
78
- - "Shift"
79
- - "Ctrl"
80
- - "Alt"
81
- - "Win"
82
- modifier_keys (list|str): Same as `modifierKeys` (snake_case alias).
83
- x_offset (int): X offset relative to the element's top-left corner. Default: 0.
84
- y_offset (int): Y offset relative to the element's top-left corner. Default: 0.
85
- is_center (bool): If True, click at the element's center. Default: False.
86
- durationMs (int): Duration of the click in milliseconds. Default: 100.
87
- times (int): Number of times to click. Default: 1.
88
- interClickDelayMs (int): Delay between multiple clicks in milliseconds. Default: 100.
89
- post_delay (float): Delay after click action (in seconds). Default: 0.5.
90
-
91
- Raises:
92
- Exception: If the element cannot be found or the click action fails within the timeout.
93
- """
94
- x_offset = int(kwargs.get("x_offset", 0))
95
- y_offset = int(kwargs.get("y_offset", 0))
96
- is_center = bool(kwargs.get("is_center", False))
97
-
98
- click_params = {
99
- "button": str(kwargs.get("button", "left")),
100
- "durationMs": int(kwargs.get("durationMs", 100)),
101
- "times": int(kwargs.get("times", 1)),
102
- "interClickDelayMs": int(kwargs.get("interClickDelayMs", 100)),
103
- }
104
-
105
- self._apply_modifier_keys(click_params, kwargs.get("modifierKeys"))
106
-
107
- def _action():
108
- elements = self._element_find(locator, False, False)
109
- if not elements:
110
- raise Exception(f"Element not found: {locator}")
111
-
112
- driver = self._current_application()
113
- rect = driver.get_window_rect()
114
- e_rect = elements[0].rect
115
-
116
- x = rect['x'] + e_rect['x'] + x_offset
117
- y = rect['y'] + e_rect['y'] + y_offset
118
- if is_center:
119
- x += e_rect['width'] // 2
120
- y += e_rect['height'] // 2
121
-
122
- click_params.update({"x": x, "y": y})
123
- self._info(f"Click params {click_params}")
124
-
125
- driver.execute_script("windows: click", click_params)
126
- time.sleep(0.5)
127
-
128
- self._retry(_action, timeout, f"Failed to perform click action on '{locator}'")
129
-
130
- def _appium_hover_api(self, start_locator, end_locator, timeout, **kwargs):
131
- """
132
- Perform a hover action using Platform-Specific Extensions.
133
- """
134
- hover_params = {
135
- "startX": int(kwargs.get("startX", 0)),
136
- "startY": int(kwargs.get("startY", 0)),
137
- "endX": int(kwargs.get("endX", 0)),
138
- "endY": int(kwargs.get("endY", 0)),
139
- "durationMs": int(kwargs.get("durationMs", 100)),
140
- }
141
-
142
- self._apply_modifier_keys(hover_params, kwargs.get("modifierKeys"))
143
-
144
- def _action():
145
- if start_locator:
146
- start_element = self._element_find(start_locator, True, False)
147
- if start_element:
148
- hover_params["startElementId"] = start_element.id
149
- hover_params.pop("startX", None)
150
- hover_params.pop("startY", None)
151
-
152
- if end_locator:
153
- end_element = self._element_find(end_locator, True, False)
154
- if end_element:
155
- hover_params["endElementId"] = end_element.id
156
- hover_params.pop("endX", None)
157
- hover_params.pop("endY", None)
158
-
159
- self._current_application().execute_script("windows: hover", hover_params)
160
- time.sleep(0.5)
161
-
162
- self._retry(_action, timeout, "Failed to perform hover action")
163
-
164
- def _appium_drag_and_drop_api(self, start_locator, end_locator, timeout, **kwargs):
165
- """
166
- Perform a drag and drop action using Appium Windows Driver.
167
- https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-clickanddrag
168
- """
169
- drag_params = {
170
- "startX": int(kwargs.get("startX", 0)),
171
- "startY": int(kwargs.get("startY", 0)),
172
- "endX": int(kwargs.get("endX", 0)),
173
- "endY": int(kwargs.get("endY", 0)),
174
- "durationMs": int(kwargs.get("durationMs", 5000)),
175
- }
176
-
177
- self._apply_modifier_keys(drag_params, kwargs.get("modifierKeys"))
178
-
179
- def _action():
180
- if start_locator:
181
- start_element = self._element_find(start_locator, True, False)
182
- if start_element:
183
- drag_params["startElementId"] = start_element.id
184
- drag_params.pop("startX", None)
185
- drag_params.pop("startY", None)
186
-
187
- if end_locator:
188
- end_element = self._element_find(end_locator, True, False)
189
- if end_element:
190
- drag_params["endElementId"] = end_element.id
191
- drag_params.pop("endX", None)
192
- drag_params.pop("endY", None)
193
-
194
- self._current_application().execute_script("windows: clickAndDrag", drag_params)
195
- time.sleep(0.5)
196
-
197
- self._retry(_action, timeout, "Failed to perform drag and drop action")
198
-
199
- def _appium_keys_api(self, text, **kwargs):
200
- """
201
- Perform a key input action using Appium Windows Driver.
202
- https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-keys
203
-
204
- @param text:
205
- @param kwargs:
206
- @return:
207
- """
208
- actions = kwargs.get("actions", "")
209
- # pause = int(kwargs.get("pause", 0))
210
- # virtual_key_code = int(kwargs.get("virtualKeyCode", 0))
211
- # down = bool(kwargs.get("down", False))
212
- if not actions:
213
- actions = [{"text": text}]
214
- self._current_application().execute_script("windows: keys", {"actions": actions})
215
- time.sleep(0.5)
1
+ import time
2
+ import os
3
+ import pathlib
4
+ import re
5
+ import ntpath
6
+ import posixpath
7
+
8
+ from AppiumLibrary.keywords.keywordgroup import KeywordGroup
9
+
10
+
11
+ class _WindowsKeywords(KeywordGroup):
12
+
13
+ def __init__(self):
14
+ super().__init__()
15
+
16
+ # Public
17
+ def appium_hover(self, locator, start_locator=None, timeout=20, **kwargs):
18
+ self._info(f"Appium Hover '{locator}', timeout '{timeout}'")
19
+ self._appium_hover_api(start_locator=start_locator, end_locator=locator, timeout=timeout, **kwargs)
20
+
21
+ def appium_click_offset(self, locator, x_offset=0, y_offset=0, timeout=20, **kwargs):
22
+ self._info(
23
+ f"Appium Click Offset '{locator}', (x_offset,y_offset) '({x_offset},{y_offset})', timeout '{timeout}'")
24
+ self._appium_click_api(locator=locator, timeout=timeout, x_offset=x_offset, y_offset=y_offset, **kwargs)
25
+
26
+ def appium_right_click(self, locator, timeout=20, **kwargs):
27
+ self._info(f"Appium Right Click '{locator}', timeout '{timeout}'")
28
+ self._appium_click_api(locator=locator, timeout=timeout, button="right", **kwargs)
29
+
30
+ def appium_left_click(self, locator, timeout=20, **kwargs):
31
+ self._info(f"Appium Left Click '{locator}', timeout '{timeout}'")
32
+ self._appium_click_api(locator=locator, timeout=timeout, button="left", **kwargs)
33
+
34
+ def appium_double_click(self, locator, timeout=20, **kwargs):
35
+ self._info(f"Appium Double Click '{locator}', timeout '{timeout}'")
36
+ self._appium_click_api(locator=locator, timeout=timeout, times=2, **kwargs)
37
+
38
+ def appium_drag_and_drop(self, start_locator=None, end_locator=None, timeout=20, **kwargs):
39
+ self._info(f"Appium Drag And Drop '{start_locator} -> {end_locator}', timeout '{timeout}'")
40
+ self._appium_drag_and_drop_api(start_locator=start_locator, end_locator=end_locator, timeout=timeout, **kwargs)
41
+
42
+ def appium_drag_and_drop_by_offset(self, x_start, y_start, x_end, y_end):
43
+ x_start, y_start, x_end, y_end = (int(x) for x in [x_start, y_start, x_end, y_end])
44
+ self._info(f"Appium Drag And Drop By Offset ({x_start}, {y_start}) -> ({x_end}, {y_end})")
45
+ self._appium_drag_and_drop_api(start_locator=None, end_locator=None, timeout=1,
46
+ startX=x_start, startY=y_start,
47
+ endX=x_end, endY=y_end)
48
+
49
+ def appium_sendkeys(self, text=None, **kwargs):
50
+ self._info(f"Appium Sendkeys '{text}'")
51
+ self._appium_keys_api(text=text, **kwargs)
52
+
53
+ def appium_normalize_path(self, path, sep="\\", case_normalize=False, escape_backtick=True):
54
+ """Normalizes the given path.
55
+ - Collapses redundant separators and up-level references.
56
+ - Set sep to ``/`` to Converts ``\\`` to ``/``
57
+ - Replaces initial ``~`` or ``~user`` by that user's home directory.
58
+ - If ``case_normalize`` is given a true value (see `Boolean arguments`)
59
+ on Windows, converts the path to all lowercase.
60
+ - Converts ``pathlib.Path`` instances to ``str``.
61
+
62
+ Examples:
63
+ | ${path1} = | Appium Normalize Path | abc/ |
64
+ | ${path2} = | Appium Normalize Path | abc/../def |
65
+ | ${path3} = | Appium Normalize Path | abc/./def//ghi |
66
+ | ${path4} = | Appium Normalize Path | ~robot/stuff |
67
+ =>
68
+ - ${path1} = ``abc``
69
+ - ${path2} = ``def``
70
+ - ${path3} = ``abc\\def\\ghi``
71
+ - ${path4} = ``\\home\\robot\\stuff``
72
+
73
+ """
74
+ # Determine strict library to use based on target separator
75
+ if sep == "\\":
76
+ path_module = ntpath
77
+ else:
78
+ path_module = posixpath
79
+
80
+ if isinstance(path, pathlib.Path):
81
+ path = str(path)
82
+
83
+ path = path or "."
84
+ path = os.path.expanduser(path)
85
+
86
+ # If targeting Posix, ensure backslashes are converted to forward slashes
87
+ # BEFORE normalization, because posixpath treats backslash as a filename character.
88
+ if path_module is posixpath:
89
+ path = path.replace("\\", "/")
90
+
91
+ path = path_module.normpath(path)
92
+
93
+ if case_normalize:
94
+ path = path_module.normcase(path)
95
+
96
+ if escape_backtick:
97
+ path = re.sub(r"(?<!`)`(?!`)", "``", path)
98
+
99
+ # Force final separator just in case, though normpath usually handles it.
100
+ # ntpath produces '\', posixpath produces '/'
101
+ # The original code did a final cleaning, we can preserve rstrip.
102
+ return path.rstrip()
103
+
104
+ # Private
105
+ def _apply_modifier_keys(self, params: dict, modifier_keys):
106
+ """Normalize modifier keys and update params in place."""
107
+ if modifier_keys:
108
+ if isinstance(modifier_keys, (list, tuple)):
109
+ params["modifierKeys"] = [str(k).lower() for k in modifier_keys]
110
+ else:
111
+ params["modifierKeys"] = str(modifier_keys).lower()
112
+
113
+ def _appium_click_api(self, locator, timeout, **kwargs):
114
+ """
115
+ Perform a click action on a Windows element using Appium Windows Driver.
116
+
117
+ References:
118
+ https://github.com/appium/appium-windows-driver
119
+ https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-click
120
+
121
+ Args:
122
+ locator (str): Element locator.
123
+ timeout (int): Maximum time to retry locating and clicking the element (in seconds).
124
+ kwargs (dict): Additional click options.
125
+
126
+ Keyword Args:
127
+ button (str): Mouse button to click. One of:
128
+ - "left" (default)
129
+ - "middle"
130
+ - "right"
131
+ - "back"
132
+ - "forward"
133
+ modifierKeys (list|str): Keys to hold during the click. One or more of:
134
+ - "Shift"
135
+ - "Ctrl"
136
+ - "Alt"
137
+ - "Win"
138
+ modifier_keys (list|str): Same as `modifierKeys` (snake_case alias).
139
+ x_offset (int): X offset relative to the element's top-left corner. Default: 0.
140
+ y_offset (int): Y offset relative to the element's top-left corner. Default: 0.
141
+ is_center (bool): If True, click at the element's center. Default: False.
142
+ durationMs (int): Duration of the click in milliseconds. Default: 100.
143
+ times (int): Number of times to click. Default: 1.
144
+ interClickDelayMs (int): Delay between multiple clicks in milliseconds. Default: 100.
145
+ post_delay (float): Delay after click action (in seconds). Default: 0.5.
146
+
147
+ Raises:
148
+ Exception: If the element cannot be found or the click action fails within the timeout.
149
+ """
150
+ x_offset = int(kwargs.get("x_offset", 0))
151
+ y_offset = int(kwargs.get("y_offset", 0))
152
+ is_center = bool(kwargs.get("is_center", False))
153
+
154
+ click_params = {
155
+ "button": str(kwargs.get("button", "left")),
156
+ "durationMs": int(kwargs.get("durationMs", 100)),
157
+ "times": int(kwargs.get("times", 1)),
158
+ "interClickDelayMs": int(kwargs.get("interClickDelayMs", 100)),
159
+ }
160
+
161
+ self._apply_modifier_keys(click_params, kwargs.get("modifierKeys"))
162
+
163
+ def _action():
164
+ elements = self._element_find(locator, False, False)
165
+ if not elements:
166
+ raise Exception(f"Element not found: {locator}")
167
+
168
+ driver = self._current_application()
169
+ rect = driver.get_window_rect()
170
+ e_rect = elements[0].rect
171
+
172
+ x = rect['x'] + e_rect['x'] + x_offset
173
+ y = rect['y'] + e_rect['y'] + y_offset
174
+ if is_center:
175
+ x += e_rect['width'] // 2
176
+ y += e_rect['height'] // 2
177
+
178
+ click_params.update({"x": x, "y": y})
179
+ self._info(f"Click params {click_params}")
180
+
181
+ driver.execute_script("windows: click", click_params)
182
+ time.sleep(0.5)
183
+
184
+ self._retry(_action, timeout, f"Failed to perform click action on '{locator}'")
185
+
186
+ def _appium_hover_api(self, start_locator, end_locator, timeout, **kwargs):
187
+ """
188
+ Perform a hover action using Platform-Specific Extensions.
189
+ """
190
+ hover_params = {
191
+ "startX": int(kwargs.get("startX", 0)),
192
+ "startY": int(kwargs.get("startY", 0)),
193
+ "endX": int(kwargs.get("endX", 0)),
194
+ "endY": int(kwargs.get("endY", 0)),
195
+ "durationMs": int(kwargs.get("durationMs", 100)),
196
+ }
197
+
198
+ self._apply_modifier_keys(hover_params, kwargs.get("modifierKeys"))
199
+
200
+ def _action():
201
+ if start_locator:
202
+ start_element = self._element_find(start_locator, True, False)
203
+ if start_element:
204
+ hover_params["startElementId"] = start_element.id
205
+ hover_params.pop("startX", None)
206
+ hover_params.pop("startY", None)
207
+
208
+ if end_locator:
209
+ end_element = self._element_find(end_locator, True, False)
210
+ if end_element:
211
+ hover_params["endElementId"] = end_element.id
212
+ hover_params.pop("endX", None)
213
+ hover_params.pop("endY", None)
214
+
215
+ self._current_application().execute_script("windows: hover", hover_params)
216
+ time.sleep(0.5)
217
+
218
+ self._retry(_action, timeout, "Failed to perform hover action")
219
+
220
+ def _appium_drag_and_drop_api(self, start_locator, end_locator, timeout, **kwargs):
221
+ """
222
+ Perform a drag and drop action using Appium Windows Driver.
223
+ https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-clickanddrag
224
+ """
225
+ drag_params = {
226
+ "startX": int(kwargs.get("startX", 0)),
227
+ "startY": int(kwargs.get("startY", 0)),
228
+ "endX": int(kwargs.get("endX", 0)),
229
+ "endY": int(kwargs.get("endY", 0)),
230
+ "durationMs": int(kwargs.get("durationMs", 5000)),
231
+ }
232
+
233
+ self._apply_modifier_keys(drag_params, kwargs.get("modifierKeys"))
234
+
235
+ def _action():
236
+ if start_locator:
237
+ start_element = self._element_find(start_locator, True, False)
238
+ if start_element:
239
+ drag_params["startElementId"] = start_element.id
240
+ drag_params.pop("startX", None)
241
+ drag_params.pop("startY", None)
242
+
243
+ if end_locator:
244
+ end_element = self._element_find(end_locator, True, False)
245
+ if end_element:
246
+ drag_params["endElementId"] = end_element.id
247
+ drag_params.pop("endX", None)
248
+ drag_params.pop("endY", None)
249
+
250
+ self._current_application().execute_script("windows: clickAndDrag", drag_params)
251
+ time.sleep(0.5)
252
+
253
+ self._retry(_action, timeout, "Failed to perform drag and drop action")
254
+
255
+ def _appium_keys_api(self, text, **kwargs):
256
+ """
257
+ Perform a key input action using Appium Windows Driver.
258
+ https://github.com/appium/appium-windows-driver?tab=readme-ov-file#windows-keys
259
+
260
+ @param text:
261
+ @param kwargs:
262
+ @return:
263
+ """
264
+ actions = kwargs.get("actions", "")
265
+ # pause = int(kwargs.get("pause", 0))
266
+ # virtual_key_code = int(kwargs.get("virtualKeyCode", 0))
267
+ # down = bool(kwargs.get("down", False))
268
+ if not actions:
269
+ actions = [{"text": text}]
270
+ self._current_application().execute_script("windows: keys", {"actions": actions})
271
+ time.sleep(0.5)
@@ -1,70 +1,71 @@
1
- # -*- coding: utf-8 -*-
2
- import inspect
3
- import functools
4
-
5
- # Internal/private marker attribute names
6
- _RUN_ON_FAILURE_ATTR = "__rof_wrapped__"
7
- _IGNORE_RUN_FAILURE_ATTR = "__rof_ignore__"
8
- _ORIGINAL_METHOD_ATTR = "__original__"
9
-
10
-
11
- def _run_on_failure_decorator(method):
12
- """Decorator to wrap keyword methods with _run_on_failure support."""
13
- if getattr(method, _RUN_ON_FAILURE_ATTR, False):
14
- # Already decorated → skip re-wrapping
15
- return method
16
-
17
- @functools.wraps(method)
18
- def wrapper(*args, **kwargs):
19
- try:
20
- return method(*args, **kwargs)
21
- except Exception:
22
- self = args[0] if args else None
23
- if self and hasattr(self, "_run_on_failure"):
24
- self._run_on_failure()
25
- raise
26
-
27
- setattr(wrapper, _RUN_ON_FAILURE_ATTR, True) # mark as decorated
28
- setattr(wrapper, _ORIGINAL_METHOD_ATTR, method) # keep reference to original
29
- return wrapper
30
-
31
-
32
- def ignore_on_fail(method):
33
- """Decorator to mark methods that should never be wrapped by run_on_failure."""
34
- setattr(method, _IGNORE_RUN_FAILURE_ATTR, True)
35
- return method
36
-
37
-
38
- class KeywordGroupMetaClass(type):
39
- def __new__(cls, clsname, bases, attrs):
40
- for name, method in list(attrs.items()):
41
- if (
42
- not name.startswith('_')
43
- and inspect.isfunction(method)
44
- and not getattr(method, _IGNORE_RUN_FAILURE_ATTR, False)
45
- and not getattr(method, _RUN_ON_FAILURE_ATTR, False)
46
- ):
47
- attrs[name] = _run_on_failure_decorator(method)
48
- return super().__new__(cls, clsname, bases, attrs)
49
-
50
-
51
- class KeywordGroup(metaclass=KeywordGroupMetaClass):
52
-
53
- def _invoke_original(self, method, *args, **kwargs):
54
- """
55
- Call the original (undecorated) implementation of a method.
56
-
57
- Accepts either:
58
- - method name (str), e.g. self._invoke_original("click", el)
59
- - bound method itself, e.g. self._invoke_original(self.click, el)
60
-
61
- Falls back to the current method if undecorated.
62
- Returns None if method not found at all.
63
- """
64
- if isinstance(method, str):
65
- method = getattr(self, method, None)
66
- if method is None:
67
- return None
68
-
69
- original = getattr(method, _ORIGINAL_METHOD_ATTR, method)
70
- return original(self, *args, **kwargs)
1
+ # -*- coding: utf-8 -*-
2
+ import inspect
3
+ import functools
4
+
5
+ # Internal/private marker attribute name
6
+ _RUN_ON_FAILURE_MARKER = "__rof_processed__"
7
+
8
+
9
+ def _run_on_failure_decorator(method):
10
+ """Decorator to wrap keyword methods with _run_on_failure support."""
11
+ if getattr(method, _RUN_ON_FAILURE_MARKER, False):
12
+ # Already decorated or ignored skip re-wrapping
13
+ return method
14
+
15
+ @functools.wraps(method)
16
+ def wrapper(*args, **kwargs):
17
+ try:
18
+ return method(*args, **kwargs)
19
+ except Exception as err:
20
+ self = args[0] if args else None
21
+ if self and hasattr(self, "_run_on_failure"):
22
+ if not getattr(err, "_run_on_failure_executed", False):
23
+ self._run_on_failure()
24
+ err._run_on_failure_executed = True
25
+ raise
26
+
27
+ setattr(wrapper, _RUN_ON_FAILURE_MARKER, True) # mark as decorated
28
+ return wrapper
29
+
30
+
31
+ def ignore_on_fail(method):
32
+ """Decorator to mark methods that should never be wrapped by run_on_failure."""
33
+ setattr(method, _RUN_ON_FAILURE_MARKER, True)
34
+ return method
35
+
36
+
37
+ class KeywordGroupMetaClass(type):
38
+ def __new__(cls, clsname, bases, attrs):
39
+ for name, method in list(attrs.items()):
40
+ if (
41
+ not name.startswith('_')
42
+ and inspect.isfunction(method)
43
+ and not getattr(method, _RUN_ON_FAILURE_MARKER, False)
44
+ ):
45
+ attrs[name] = _run_on_failure_decorator(method)
46
+ return super().__new__(cls, clsname, bases, attrs)
47
+
48
+
49
+ class KeywordGroup(metaclass=KeywordGroupMetaClass):
50
+ def _invoke_original(self, method, *args, **kwargs):
51
+ """
52
+ Call the original (undecorated) implementation of a method.
53
+
54
+ Accepts either:
55
+ - method name (str), e.g. self._invoke_original("click", el)
56
+ - bound method itself, e.g. self._invoke_original(self.click, el)
57
+
58
+ Falls back to the current method if undecorated.
59
+ Returns None if method not found at all.
60
+ """
61
+ if isinstance(method, str):
62
+ method = getattr(self, method, None)
63
+ if method is None:
64
+ return None
65
+
66
+ if hasattr(method, "__wrapped__"):
67
+ # It's a decorated method (function), so we must pass self
68
+ return method.__wrapped__(self, *args, **kwargs)
69
+
70
+ # It's an undecorated bound method, so self is already bound
71
+ return method(*args, **kwargs)