Promium 3.8.2__tar.gz → 3.9.0__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 (65) hide show
  1. {promium-3.8.2 → promium-3.9.0}/PKG-INFO +1 -1
  2. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/PKG-INFO +1 -1
  3. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/SOURCES.txt +1 -0
  4. promium-3.9.0/promium/_introspect.py +206 -0
  5. {promium-3.8.2 → promium-3.9.0}/promium/assertions.py +21 -0
  6. {promium-3.8.2 → promium-3.9.0}/promium/plugin.py +10 -7
  7. {promium-3.8.2 → promium-3.9.0}/setup.py +1 -1
  8. {promium-3.8.2 → promium-3.9.0}/tests/test_exceptions.py +0 -38
  9. {promium-3.8.2 → promium-3.9.0}/tests/test_expected_conditions.py +0 -7
  10. {promium-3.8.2 → promium-3.9.0}/LICENSE +0 -0
  11. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/dependency_links.txt +0 -0
  12. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/entry_points.txt +0 -0
  13. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/requires.txt +0 -0
  14. {promium-3.8.2 → promium-3.9.0}/Promium.egg-info/top_level.txt +0 -0
  15. {promium-3.8.2 → promium-3.9.0}/README.rst +0 -0
  16. {promium-3.8.2 → promium-3.9.0}/doc/__init__.py +0 -0
  17. {promium-3.8.2 → promium-3.9.0}/doc/conf.py +0 -0
  18. {promium-3.8.2 → promium-3.9.0}/promium/__init__.py +0 -0
  19. {promium-3.8.2 → promium-3.9.0}/promium/_diag.py +0 -0
  20. {promium-3.8.2 → promium-3.9.0}/promium/base.py +0 -0
  21. {promium-3.8.2 → promium-3.9.0}/promium/browsers.py +0 -0
  22. {promium-3.8.2 → promium-3.9.0}/promium/common.py +0 -0
  23. {promium-3.8.2 → promium-3.9.0}/promium/device_config.py +0 -0
  24. {promium-3.8.2 → promium-3.9.0}/promium/elements/__init__.py +0 -0
  25. {promium-3.8.2 → promium-3.9.0}/promium/elements/checkbox.py +0 -0
  26. {promium-3.8.2 → promium-3.9.0}/promium/elements/drop_downs.py +0 -0
  27. {promium-3.8.2 → promium-3.9.0}/promium/elements/frame.py +0 -0
  28. {promium-3.8.2 → promium-3.9.0}/promium/elements/input_field.py +0 -0
  29. {promium-3.8.2 → promium-3.9.0}/promium/elements/link.py +0 -0
  30. {promium-3.8.2 → promium-3.9.0}/promium/elements/multiselect_listbox.py +0 -0
  31. {promium-3.8.2 → promium-3.9.0}/promium/elements/radio_button.py +0 -0
  32. {promium-3.8.2 → promium-3.9.0}/promium/elements/textarea_field.py +0 -0
  33. {promium-3.8.2 → promium-3.9.0}/promium/elements/upload_button.py +0 -0
  34. {promium-3.8.2 → promium-3.9.0}/promium/exceptions.py +0 -0
  35. {promium-3.8.2 → promium-3.9.0}/promium/expected_conditions.py +0 -0
  36. {promium-3.8.2 → promium-3.9.0}/promium/helpers.py +0 -0
  37. {promium-3.8.2 → promium-3.9.0}/promium/logger.py +0 -0
  38. {promium-3.8.2 → promium-3.9.0}/promium/support/__init__.py +0 -0
  39. {promium-3.8.2 → promium-3.9.0}/promium/support/settings.py +0 -0
  40. {promium-3.8.2 → promium-3.9.0}/promium/test_case.py +0 -0
  41. {promium-3.8.2 → promium-3.9.0}/promium/waits.py +0 -0
  42. {promium-3.8.2 → promium-3.9.0}/pyproject.toml +0 -0
  43. {promium-3.8.2 → promium-3.9.0}/setup.cfg +0 -0
  44. {promium-3.8.2 → promium-3.9.0}/tests/__init__.py +0 -0
  45. {promium-3.8.2 → promium-3.9.0}/tests/config.py +0 -0
  46. {promium-3.8.2 → promium-3.9.0}/tests/conftest.py +0 -0
  47. {promium-3.8.2 → promium-3.9.0}/tests/pages/__init__.py +0 -0
  48. {promium-3.8.2 → promium-3.9.0}/tests/pages/catalog_404_page.py +0 -0
  49. {promium-3.8.2 → promium-3.9.0}/tests/pages/catalog_gallery_page.py +0 -0
  50. {promium-3.8.2 → promium-3.9.0}/tests/settings/__init__.py +0 -0
  51. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_base.py +0 -0
  52. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_chrome.py +0 -0
  53. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_edge.py +0 -0
  54. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_firefox.py +0 -0
  55. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_ie.py +0 -0
  56. {promium-3.8.2 → promium-3.9.0}/tests/settings/test_opera.py +0 -0
  57. {promium-3.8.2 → promium-3.9.0}/tests/test_catalog_404.py +0 -0
  58. {promium-3.8.2 → promium-3.9.0}/tests/test_catalog_gallery.py +0 -0
  59. {promium-3.8.2 → promium-3.9.0}/tests/test_enviroment.py +0 -0
  60. {promium-3.8.2 → promium-3.9.0}/tests/test_helpers.py +0 -0
  61. {promium-3.8.2 → promium-3.9.0}/tests/test_requests.py +0 -0
  62. {promium-3.8.2 → promium-3.9.0}/tests/test_soft_asserts.py +0 -0
  63. {promium-3.8.2 → promium-3.9.0}/tests/test_test_cases.py +0 -0
  64. {promium-3.8.2 → promium-3.9.0}/tests/test_waits.py +0 -0
  65. {promium-3.8.2 → promium-3.9.0}/tests/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Promium
3
- Version: 3.8.2
3
+ Version: 3.9.0
4
4
  Summary: Selenium wrapper for testing Web UI
5
5
  Author: Denis Korytkin, Nataliia Guieva, Roman Zaporozhets, Vladimir Kritov, Oleh Dykusha
6
6
  Project-URL: Home page, https://none
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Promium
3
- Version: 3.8.2
3
+ Version: 3.9.0
4
4
  Summary: Selenium wrapper for testing Web UI
5
5
  Author: Denis Korytkin, Nataliia Guieva, Roman Zaporozhets, Vladimir Kritov, Oleh Dykusha
6
6
  Project-URL: Home page, https://none
@@ -12,6 +12,7 @@ doc/__init__.py
12
12
  doc/conf.py
13
13
  promium/__init__.py
14
14
  promium/_diag.py
15
+ promium/_introspect.py
15
16
  promium/assertions.py
16
17
  promium/base.py
17
18
  promium/browsers.py
@@ -0,0 +1,206 @@
1
+ """AST-based expression introspection for soft_assert.
2
+
3
+ Extracts the source expression from a ``soft_assert(...)`` call,
4
+ substitutes runtime values, and returns a colorized string with
5
+ a diff (for strings and containers) similar to pytest assertion output.
6
+ """
7
+
8
+ import ast
9
+ import difflib
10
+ import inspect
11
+ import os
12
+ import pprint
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ _SENTINEL = object()
18
+
19
+ _COMPARE_OPS = {
20
+ ast.Eq: "==",
21
+ ast.NotEq: "!=",
22
+ ast.Lt: "<",
23
+ ast.LtE: "<=",
24
+ ast.Gt: ">",
25
+ ast.GtE: ">=",
26
+ ast.Is: "is",
27
+ ast.IsNot: "is not",
28
+ ast.In: "in",
29
+ ast.NotIn: "not in",
30
+ }
31
+
32
+ _NO_COLOR = bool(
33
+ os.environ.get("NO_COLOR")
34
+ or os.environ.get("PROMIUM_ASSERTION_PANEL_MONO_COLOR")
35
+ )
36
+ _RED = "" if _NO_COLOR else "\033[31m"
37
+ _GREEN = "" if _NO_COLOR else "\033[32m"
38
+ _YELLOW = "" if _NO_COLOR else "\033[1;33m"
39
+ _BOLD = "" if _NO_COLOR else "\033[1m"
40
+ _WHITE = "" if _NO_COLOR else "\033[1;37m"
41
+ _RESET = "" if _NO_COLOR else "\033[0m"
42
+
43
+
44
+ def _escape_control(s: str) -> str:
45
+ """Replace control characters with \\xNN escapes, keep Unicode as-is."""
46
+ return "".join(
47
+ f"\\x{ord(c):02x}" if ord(c) < 32 or ord(c) == 127 else c for c in s
48
+ )
49
+
50
+
51
+ def _colorize_ndiff(left_lines: list[str], right_lines: list[str]) -> str:
52
+ """Return colorized ndiff output for two sequences of lines."""
53
+ colored = []
54
+ for line in difflib.ndiff(left_lines, right_lines):
55
+ stripped = line.rstrip("\n")
56
+ if line.startswith("- "):
57
+ colored.append(f"{_RED}{stripped}{_RESET}\n")
58
+ elif line.startswith("+ "):
59
+ colored.append(f"{_GREEN}{stripped}{_RESET}\n")
60
+ elif line.startswith("? "):
61
+ colored.append(f"{_YELLOW}{stripped}{_RESET}\n")
62
+ else:
63
+ colored.append(line)
64
+ return "".join(colored)
65
+
66
+
67
+ def extract_call_expression(method_name: str) -> str | None:
68
+ """Extract the first argument source from a call to *method_name*
69
+ and rebuild it with runtime values substituted.
70
+
71
+ Example: ``self.soft_assert(code in var)``
72
+ where code='23', var=[1,2,3] → ``"'23' in [1, 2, 3]"``
73
+ """
74
+ try:
75
+ caller = inspect.currentframe().f_back.f_back
76
+ source = Path(caller.f_code.co_filename).read_text(encoding="utf-8")
77
+
78
+ for node in ast.walk(ast.parse(source)):
79
+ if not (
80
+ isinstance(node, ast.Call)
81
+ and isinstance(node.func, ast.Attribute)
82
+ and node.func.attr == method_name
83
+ and node.args
84
+ and node.lineno
85
+ <= caller.f_lineno
86
+ <= (node.end_lineno or node.lineno)
87
+ ):
88
+ continue
89
+
90
+ expr_node = node.args[0]
91
+ expr_source = ast.get_source_segment(source, expr_node)
92
+ if not expr_source:
93
+ return None
94
+
95
+ def _may_have_side_effect(node: ast.expr) -> bool:
96
+ if isinstance(node, ast.Call):
97
+ return True
98
+ if isinstance(node, ast.Attribute):
99
+ if _may_have_side_effect(node.value):
100
+ return True
101
+ val_src = ast.get_source_segment(source, node.value)
102
+ if val_src:
103
+ try:
104
+ obj = eval(
105
+ val_src, caller.f_globals, caller.f_locals
106
+ ) # noqa: S307
107
+ if isinstance(
108
+ getattr(type(obj), node.attr, None),
109
+ property,
110
+ ):
111
+ return True
112
+ except Exception:
113
+ pass
114
+ return False
115
+ return any(
116
+ _may_have_side_effect(child)
117
+ for child in ast.iter_child_nodes(node)
118
+ if isinstance(child, ast.expr)
119
+ )
120
+
121
+ def _try_eval(operand: ast.expr) -> Any:
122
+ """Return the runtime value or _SENTINEL if eval fails."""
123
+ if _may_have_side_effect(operand):
124
+ return _SENTINEL
125
+ src = ast.get_source_segment(source, operand)
126
+ if not src:
127
+ return _SENTINEL
128
+ try:
129
+ return eval(src, caller.f_globals, caller.f_locals) # noqa: S307
130
+ except Exception:
131
+ return _SENTINEL
132
+
133
+ def _eval_operand(operand: ast.expr) -> str:
134
+ """Return repr(value) if eval succeeds, otherwise source text."""
135
+ val = _try_eval(operand)
136
+ if val is _SENTINEL:
137
+ return ast.get_source_segment(source, operand) or "?"
138
+ r = repr(val)
139
+ return r if len(r) <= 200 else r[:200] + "..."
140
+
141
+ if isinstance(expr_node, ast.Compare):
142
+ left_repr = _eval_operand(expr_node.left)
143
+ parts = [f"{_RED}{left_repr}{_RESET}"]
144
+ for cmp_op, comp in zip(expr_node.ops, expr_node.comparators):
145
+ op_str = _COMPARE_OPS.get(type(cmp_op), "??")
146
+ parts.append(f"{_BOLD}{op_str}{_RESET}")
147
+ parts.append(f"{_GREEN}{_eval_operand(comp)}{_RESET}")
148
+ result = " ".join(parts)
149
+
150
+ if len(expr_node.ops) == 1 and isinstance(
151
+ expr_node.ops[0], (ast.Eq, ast.NotEq)
152
+ ):
153
+ left_val = _try_eval(expr_node.left)
154
+ right_val = _try_eval(expr_node.comparators[0])
155
+ if isinstance(left_val, str) and isinstance(right_val, str):
156
+ esc = _escape_control
157
+ result += "\n" + _colorize_ndiff(
158
+ [esc(left_val) + "\n"],
159
+ [esc(right_val) + "\n"],
160
+ )
161
+ elif (
162
+ left_val is not _SENTINEL
163
+ and right_val is not _SENTINEL
164
+ and isinstance(
165
+ left_val,
166
+ (dict, list, tuple, set, frozenset),
167
+ )
168
+ and isinstance(
169
+ right_val,
170
+ (dict, list, tuple, set, frozenset),
171
+ )
172
+ ):
173
+ fmt = pprint.PrettyPrinter(
174
+ width=1, sort_dicts=False
175
+ ).pformat
176
+ left_lines = fmt(left_val).splitlines(keepends=True)
177
+ right_lines = fmt(right_val).splitlines(keepends=True)
178
+ if left_lines and not left_lines[-1].endswith("\n"):
179
+ left_lines[-1] += "\n"
180
+ if right_lines and not right_lines[-1].endswith("\n"):
181
+ right_lines[-1] += "\n"
182
+ result += "\n" + _colorize_ndiff(left_lines, right_lines)
183
+ return result
184
+
185
+ if isinstance(expr_node, ast.BoolOp):
186
+ op_str = " and " if isinstance(expr_node.op, ast.And) else " or "
187
+ colored_parts = []
188
+ for v in expr_node.values:
189
+ val = _try_eval(v)
190
+ operand_str = _eval_operand(v)
191
+ if val is _SENTINEL:
192
+ colored_parts.append(operand_str)
193
+ elif val:
194
+ colored_parts.append(f"{_GREEN}{operand_str}{_RESET}")
195
+ else:
196
+ colored_parts.append(f"{_RED}{operand_str}{_RESET}")
197
+ return (f"{_BOLD}{op_str}{_RESET}").join(colored_parts)
198
+
199
+ val = _try_eval(expr_node)
200
+ operand_str = _eval_operand(expr_node)
201
+ if val is not _SENTINEL and not val:
202
+ return f"{_RED}{operand_str}{_RESET}"
203
+ return operand_str
204
+ except Exception:
205
+ pass
206
+ return None
@@ -11,6 +11,7 @@ import urllib3
11
11
  from json_checker import Checker, CheckerError
12
12
  from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
13
13
 
14
+ from promium._introspect import _RESET, _WHITE, extract_call_expression
14
15
  from promium.common import upload_screenshot
15
16
 
16
17
 
@@ -154,6 +155,26 @@ class BaseSoftAssertion:
154
155
  return f"\nScreenshot: {upload_screenshot(self.driver)}\n"
155
156
  return ""
156
157
 
158
+ def soft_assert(
159
+ self,
160
+ expr: Any,
161
+ msg: str | None = None,
162
+ with_screen: bool = False,
163
+ hard_assert: bool | None = None,
164
+ ) -> None:
165
+ """Check that the expression is true."""
166
+ if not expr:
167
+ source_expr = extract_call_expression("soft_assert")
168
+ parts = []
169
+ if msg:
170
+ parts.append(f"{_WHITE}{msg}{_RESET}")
171
+ if source_expr:
172
+ parts.append(f"Assertion failed: {source_expr}")
173
+ error = "\n".join(parts)
174
+ self._emit(
175
+ error, with_screen=with_screen, hard_assert_override=hard_assert
176
+ )
177
+
157
178
  def soft_assert_true(
158
179
  self,
159
180
  expr: Any,
@@ -303,17 +303,20 @@ def _render_panel(text: str, title: str) -> str:
303
303
  border_style = "none" if mono_color else "red"
304
304
  buf = io.StringIO()
305
305
  console = Console(file=buf, force_terminal=True, width=width - 5)
306
- msg = (
307
- Text(text.strip(), justify="left")
308
- if mono_color
309
- else Syntax(
310
- text.strip(),
306
+ stripped = text.strip()
307
+ has_ansi = "\033[" in stripped
308
+ if mono_color:
309
+ msg = Text(stripped, justify="left")
310
+ elif has_ansi:
311
+ msg = Text.from_ansi(stripped, no_wrap=False)
312
+ else:
313
+ msg = Syntax(
314
+ stripped,
311
315
  "sql",
312
316
  word_wrap=True,
313
317
  theme="ansi_light",
314
318
  background_color="default",
315
319
  )
316
- )
317
320
  panel = Panel(
318
321
  renderable=msg,
319
322
  title=color_msg(title),
@@ -326,7 +329,7 @@ def _render_panel(text: str, title: str) -> str:
326
329
 
327
330
  @pytest.mark.trylast
328
331
  def pytest_runtest_call(item: pytest.Item) -> None:
329
- separator = "-" * 46
332
+ separator = "-" * 80
330
333
  use_rich = os.getenv("PROMIUM_ASSERTION_PANEL_WIDTH") is not None
331
334
 
332
335
  if hasattr(item.config, "assertion_errors"):
@@ -19,7 +19,7 @@ LONG_DESCRIPTION = Path("README.rst").read_text(encoding="utf-8")
19
19
 
20
20
  setup(
21
21
  name="Promium",
22
- version="3.8.2",
22
+ version="3.9.0",
23
23
  install_requires=REQUIREMENTS,
24
24
  author=(
25
25
  "Denis Korytkin, Nataliia Guieva, "
@@ -158,41 +158,3 @@ def test_promium_timeout_exception_attributes():
158
158
  raise exc
159
159
  assert exc_info.value.message == exc.message
160
160
  assert str(exc_info.value) == exc.message
161
-
162
-
163
- @pytest.mark.unit
164
- def test_zzz_promium_exception_coverage_reload():
165
- """Test to ensure exceptions module is loaded for coverage.
166
-
167
- Named with "zzz_" prefix to run last and avoid breaking other tests.
168
- """
169
- import importlib
170
- import sys
171
-
172
- # Зберігаємо всі імпортовані exception класи
173
- from promium.exceptions import (
174
- BrowserConsoleException,
175
- ElementLocationException,
176
- LocatorException,
177
- PromiumException,
178
- PromiumTimeout,
179
- )
180
-
181
- saved_classes = {
182
- "PromiumException": PromiumException,
183
- "PromiumTimeout": PromiumTimeout,
184
- "LocatorException": LocatorException,
185
- "ElementLocationException": ElementLocationException,
186
- "BrowserConsoleException": BrowserConsoleException,
187
- }
188
-
189
- # Перезавантажуємо для coverage
190
- import promium.exceptions
191
- reloaded_module = importlib.reload(promium.exceptions)
192
-
193
- # Відновлюємо оригінальні класи в модулі
194
- for class_name, original_class in saved_classes.items():
195
- setattr(reloaded_module, class_name, original_class)
196
-
197
- # Відновлюємо в sys.modules
198
- sys.modules["promium.exceptions"] = reloaded_module
@@ -22,13 +22,6 @@ from promium.expected_conditions import (
22
22
  )
23
23
 
24
24
 
25
- @pytest.mark.unit
26
- def test_expected_conditions_coverage():
27
- import importlib
28
- import promium.expected_conditions
29
- importlib.reload(promium.expected_conditions)
30
-
31
-
32
25
  @pytest.fixture
33
26
  def mock_element():
34
27
  """Mock element for testing."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes