pytest-html-plus 0.5.2__tar.gz → 1.0.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.
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/PKG-INFO +6 -3
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pyproject.toml +13 -3
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/generate_html_report.py +0 -1
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/plugin.py +158 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/LICENSE +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/README.md +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/__init__.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/compute_filter_counts.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/compute_report_metadata.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/extract_link.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/json_merge.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/json_to_xml_converter.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/resolver_driver.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/send_email_report.py +0 -0
- {pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/utils.py +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-html-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Generate Actionable, automatic screenshots, unified Mobile friendly Pytest HTML report in less than 3 seconds — no hooks, merge plugins, no config, xdist-ready.
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: pytest,pytest-plugin,html-report,test-report,screenshot,mobile-report,xdist,allure-alternative,playwright,selenium
|
|
7
8
|
Author: reporterplus
|
|
8
9
|
Maintainer: reporterplus
|
|
9
10
|
Requires-Python: >=3.10,<4.0
|
|
10
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
12
|
Classifier: Framework :: Pytest
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
13
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -17,9 +18,11 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
23
|
Classifier: Topic :: Software Development :: Testing
|
|
22
24
|
Requires-Dist: keyring (>=25.6.0,<26.0.0)
|
|
25
|
+
Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
|
|
23
26
|
Requires-Dist: yagmail (>=0.15.293,<0.16.0)
|
|
24
27
|
Project-URL: Documentation, https://pytest-html-plus.readthedocs.io
|
|
25
28
|
Project-URL: Homepage, https://github.com/reporterplus/pytest-html-plus
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pytest-html-plus"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Generate Actionable, automatic screenshots, unified Mobile friendly Pytest HTML report in less than 3 seconds — no hooks, merge plugins, no config, xdist-ready."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = ["reporterplus"]
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development :: Testing",
|
|
19
19
|
"Topic :: Software Development :: Quality Assurance",
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
|
-
"Development Status ::
|
|
21
|
+
"Development Status :: 5 - Production/Stable",
|
|
22
22
|
]
|
|
23
23
|
keywords = [
|
|
24
24
|
"pytest",
|
|
@@ -42,6 +42,7 @@ urls.Documentation = "https://pytest-html-plus.readthedocs.io"
|
|
|
42
42
|
python = ">=3.10,<4.0"
|
|
43
43
|
yagmail = ">=0.15.293,<0.16.0"
|
|
44
44
|
keyring = ">=25.6.0,<26.0.0"
|
|
45
|
+
tomli = { version = ">=2.0.1,<3.0.0", python = "<3.11" }
|
|
45
46
|
|
|
46
47
|
[tool.poetry.group.dev.dependencies]
|
|
47
48
|
pytest-rerunfailures = "^15.1"
|
|
@@ -64,6 +65,15 @@ build-backend = "poetry.core.masonry.api"
|
|
|
64
65
|
[tool.poetry.plugins."pytest11"]
|
|
65
66
|
pytest_html_plus = "pytest_html_plus.plugin"
|
|
66
67
|
|
|
68
|
+
[tool.pytest-html-plus.profiles.ci]
|
|
69
|
+
generate-xml = true
|
|
70
|
+
xml-report = "final_xml_ci.xml"
|
|
71
|
+
|
|
72
|
+
[tool.pytest-html-plus.profiles.ci-warnings]
|
|
73
|
+
generate-xml = true
|
|
74
|
+
xml-report = "final_xml_ci_warnings.xml"
|
|
75
|
+
html-output = "report_output_warnings"
|
|
76
|
+
|
|
67
77
|
[tool.ruff]
|
|
68
78
|
line-length = 88
|
|
69
79
|
target-version = "py39"
|
|
@@ -84,4 +94,4 @@ select = [
|
|
|
84
94
|
"I", # isort
|
|
85
95
|
"UP", # pyupgrade
|
|
86
96
|
]
|
|
87
|
-
ignore = []
|
|
97
|
+
ignore = []
|
|
@@ -9,6 +9,11 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
+
try:
|
|
13
|
+
import tomllib
|
|
14
|
+
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
|
|
15
|
+
import tomli as tomllib
|
|
16
|
+
|
|
12
17
|
from pytest_html_plus.compute_report_metadata import write_plus_metadata_if_main_worker
|
|
13
18
|
from pytest_html_plus.extract_link import extract_links_from_item
|
|
14
19
|
from pytest_html_plus.generate_html_report import JSONReporter
|
|
@@ -24,6 +29,22 @@ from pytest_html_plus.utils import (
|
|
|
24
29
|
|
|
25
30
|
python_executable = shutil.which("python3") or shutil.which("python")
|
|
26
31
|
test_screenshot_paths = {}
|
|
32
|
+
PROFILE_OPTION = "--plus-profile"
|
|
33
|
+
PROFILE_SECTION = ("tool", "pytest-html-plus", "profiles")
|
|
34
|
+
PROFILE_OPTION_MAP = {
|
|
35
|
+
"json-report": {"flag": "--json-report", "kind": "value"},
|
|
36
|
+
"capture-screenshots": {"flag": "--capture-screenshots", "kind": "value"},
|
|
37
|
+
"html-output": {"flag": "--html-output", "kind": "value"},
|
|
38
|
+
"screenshots": {"flag": "--screenshots", "kind": "value"},
|
|
39
|
+
"plus-email": {"flag": "--plus-email", "kind": "bool"},
|
|
40
|
+
"detect-flake": {"flag": "--detect-flake", "kind": "value"},
|
|
41
|
+
"should-open-report": {"flag": "--should-open-report", "kind": "value"},
|
|
42
|
+
"generate-xml": {"flag": "--generate-xml", "kind": "bool"},
|
|
43
|
+
"xml-report": {"flag": "--xml-report", "kind": "value"},
|
|
44
|
+
"git-branch": {"flag": "--git-branch", "kind": "value"},
|
|
45
|
+
"git-commit": {"flag": "--git-commit", "kind": "value"},
|
|
46
|
+
"rp-env": {"flag": "--rp-env", "kind": "value"},
|
|
47
|
+
}
|
|
27
48
|
|
|
28
49
|
|
|
29
50
|
logger = logging.getLogger()
|
|
@@ -36,6 +57,136 @@ if not logger.handlers:
|
|
|
36
57
|
logger.addHandler(handler)
|
|
37
58
|
|
|
38
59
|
|
|
60
|
+
def _normalize_profile_key(key):
|
|
61
|
+
return key.replace("_", "-")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_pyproject_toml(start_path=None):
|
|
65
|
+
current = Path(start_path or Path.cwd()).resolve()
|
|
66
|
+
for candidate in (current, *current.parents):
|
|
67
|
+
pyproject_file = candidate / "pyproject.toml"
|
|
68
|
+
if pyproject_file.exists():
|
|
69
|
+
return pyproject_file
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_profiles_from_pyproject(start_path=None):
|
|
74
|
+
pyproject_file = _find_pyproject_toml(start_path=start_path)
|
|
75
|
+
if pyproject_file is None:
|
|
76
|
+
raise pytest.UsageError(
|
|
77
|
+
f"{PROFILE_OPTION} requires a pyproject.toml file in the current "
|
|
78
|
+
"directory or a parent directory"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
with pyproject_file.open("rb") as fh:
|
|
83
|
+
pyproject_data = tomllib.load(fh)
|
|
84
|
+
except tomllib.TOMLDecodeError as exc:
|
|
85
|
+
raise pytest.UsageError(f"Invalid TOML in {pyproject_file}: {exc}") from exc
|
|
86
|
+
|
|
87
|
+
profiles = pyproject_data
|
|
88
|
+
for section in PROFILE_SECTION:
|
|
89
|
+
profiles = profiles.get(section, {})
|
|
90
|
+
|
|
91
|
+
if not isinstance(profiles, dict):
|
|
92
|
+
raise pytest.UsageError(
|
|
93
|
+
f"Expected [{'.'.join(PROFILE_SECTION)}] to be a TOML table"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return profiles
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_profile_args(profile_name, start_path=None):
|
|
100
|
+
profiles = _read_profiles_from_pyproject(start_path=start_path)
|
|
101
|
+
profile = profiles.get(profile_name)
|
|
102
|
+
|
|
103
|
+
if profile is None:
|
|
104
|
+
available_profiles = ", ".join(sorted(profiles)) or "none"
|
|
105
|
+
raise pytest.UsageError(
|
|
106
|
+
f"Unknown profile '{profile_name}' for {PROFILE_OPTION}. "
|
|
107
|
+
f"Available profiles: {available_profiles}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not isinstance(profile, dict):
|
|
111
|
+
raise pytest.UsageError(
|
|
112
|
+
f"Profile '{profile_name}' must be defined as a TOML table"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
profile_args = []
|
|
116
|
+
invalid_keys = []
|
|
117
|
+
|
|
118
|
+
for raw_key, value in profile.items():
|
|
119
|
+
key = _normalize_profile_key(raw_key)
|
|
120
|
+
option = PROFILE_OPTION_MAP.get(key)
|
|
121
|
+
|
|
122
|
+
if option is None:
|
|
123
|
+
invalid_keys.append(raw_key)
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
if option["kind"] == "bool":
|
|
127
|
+
if not isinstance(value, bool):
|
|
128
|
+
raise pytest.UsageError(
|
|
129
|
+
f"Profile '{profile_name}' option '{raw_key}' must be true or false"
|
|
130
|
+
)
|
|
131
|
+
if value:
|
|
132
|
+
profile_args.append(option["flag"])
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if not isinstance(value, (str, int, float)):
|
|
136
|
+
raise pytest.UsageError(
|
|
137
|
+
f"Profile '{profile_name}' option '{raw_key}' "
|
|
138
|
+
"must be a string-like value"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
profile_args.append(f"{option['flag']}={value}")
|
|
142
|
+
|
|
143
|
+
if invalid_keys:
|
|
144
|
+
valid_keys = ", ".join(sorted(PROFILE_OPTION_MAP))
|
|
145
|
+
invalid = ", ".join(sorted(invalid_keys))
|
|
146
|
+
raise pytest.UsageError(
|
|
147
|
+
f"Invalid key(s) in profile '{profile_name}': {invalid}. "
|
|
148
|
+
f"Valid keys: {valid_keys}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return profile_args
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def apply_plus_profile_args(args, start_path=None):
|
|
155
|
+
profile_name = None
|
|
156
|
+
remaining_args = []
|
|
157
|
+
skip_next = False
|
|
158
|
+
|
|
159
|
+
for index, arg in enumerate(args):
|
|
160
|
+
if skip_next:
|
|
161
|
+
skip_next = False
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
if arg == PROFILE_OPTION:
|
|
165
|
+
if profile_name is not None:
|
|
166
|
+
raise pytest.UsageError(f"{PROFILE_OPTION} can only be specified once")
|
|
167
|
+
if index + 1 >= len(args):
|
|
168
|
+
raise pytest.UsageError(f"{PROFILE_OPTION} requires a profile name")
|
|
169
|
+
profile_name = args[index + 1]
|
|
170
|
+
skip_next = True
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
if arg.startswith(f"{PROFILE_OPTION}="):
|
|
174
|
+
if profile_name is not None:
|
|
175
|
+
raise pytest.UsageError(f"{PROFILE_OPTION} can only be specified once")
|
|
176
|
+
profile_name = arg.split("=", 1)[1]
|
|
177
|
+
if not profile_name:
|
|
178
|
+
raise pytest.UsageError(f"{PROFILE_OPTION} requires a profile name")
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
remaining_args.append(arg)
|
|
182
|
+
|
|
183
|
+
if not profile_name:
|
|
184
|
+
return list(args)
|
|
185
|
+
|
|
186
|
+
profile_args = _build_profile_args(profile_name, start_path=start_path)
|
|
187
|
+
return profile_args + remaining_args
|
|
188
|
+
|
|
189
|
+
|
|
39
190
|
@pytest.hookimpl(tryfirst=True)
|
|
40
191
|
def pytest_runtest_setup(item):
|
|
41
192
|
if "caplog" not in item.fixturenames:
|
|
@@ -263,6 +414,7 @@ def pytest_sessionstart(session):
|
|
|
263
414
|
|
|
264
415
|
|
|
265
416
|
def pytest_load_initial_conftests(args):
|
|
417
|
+
args[:] = apply_plus_profile_args(args, start_path=Path.cwd())
|
|
266
418
|
if not any(arg.startswith("--capture") for arg in args):
|
|
267
419
|
args.append("--capture=tee-sys")
|
|
268
420
|
|
|
@@ -270,6 +422,12 @@ def pytest_load_initial_conftests(args):
|
|
|
270
422
|
def pytest_addoption(parser):
|
|
271
423
|
group = parser.getgroup("pytest-html-plus", "pytest-html-plus reporting options")
|
|
272
424
|
|
|
425
|
+
group.addoption(
|
|
426
|
+
PROFILE_OPTION,
|
|
427
|
+
action="store",
|
|
428
|
+
default=None,
|
|
429
|
+
help="Load pytest-html-plus options from a named profile in pyproject.toml",
|
|
430
|
+
)
|
|
273
431
|
group.addoption(
|
|
274
432
|
"--json-report",
|
|
275
433
|
action="store",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_html_plus-0.5.2 → pytest_html_plus-1.0.0}/pytest_html_plus/compute_report_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|