pytest-why 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.
- pytest_why/__init__.py +3 -0
- pytest_why/classifier.py +130 -0
- pytest_why/plugin.py +76 -0
- pytest_why/reporter.py +147 -0
- pytest_why-0.1.0.dist-info/METADATA +108 -0
- pytest_why-0.1.0.dist-info/RECORD +10 -0
- pytest_why-0.1.0.dist-info/WHEEL +5 -0
- pytest_why-0.1.0.dist-info/entry_points.txt +2 -0
- pytest_why-0.1.0.dist-info/licenses/LICENSE +21 -0
- pytest_why-0.1.0.dist-info/top_level.txt +1 -0
pytest_why/__init__.py
ADDED
pytest_why/classifier.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Deterministic pytest failure classification."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
BROWSER_AUTOMATION_HINT = (
|
|
7
|
+
"Browser automation is involved. Verify selectors, waits, page load timing, "
|
|
8
|
+
"and whether the element is visible before interaction."
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _contains_any(text: str, needles: tuple[str, ...]) -> bool:
|
|
13
|
+
lowered = text.lower()
|
|
14
|
+
return any(needle.lower() in lowered for needle in needles)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _with_browser_hint(hint: str, longreprtext: str) -> str:
|
|
18
|
+
browser_terms = (
|
|
19
|
+
"selenium",
|
|
20
|
+
"webdriver",
|
|
21
|
+
"NoSuchElementException",
|
|
22
|
+
"StaleElementReferenceException",
|
|
23
|
+
"playwright",
|
|
24
|
+
"locator",
|
|
25
|
+
"page.",
|
|
26
|
+
)
|
|
27
|
+
if _contains_any(longreprtext, browser_terms):
|
|
28
|
+
return f"{hint} {BROWSER_AUTOMATION_HINT}"
|
|
29
|
+
return hint
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def classify_failure(
|
|
33
|
+
nodeid: str,
|
|
34
|
+
phase: str,
|
|
35
|
+
longreprtext: str,
|
|
36
|
+
duration: Optional[float] = None,
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
"""Classify a pytest failure and return its explanation fields."""
|
|
39
|
+
text = longreprtext or ""
|
|
40
|
+
|
|
41
|
+
is_fixture_failure = phase == "setup" and (
|
|
42
|
+
"ScopeMismatch" in text
|
|
43
|
+
or "recursive dependency involving fixture" in text
|
|
44
|
+
or "recursive fixture dependency" in text
|
|
45
|
+
or ("fixture" in text.lower() and "not found" in text.lower())
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if is_fixture_failure:
|
|
49
|
+
result = {
|
|
50
|
+
"type": "fixture_error",
|
|
51
|
+
"title": "Fixture error",
|
|
52
|
+
"explanation": (
|
|
53
|
+
"Pytest could not prepare the test because a fixture is missing, "
|
|
54
|
+
"has an incompatible scope, or depends on itself."
|
|
55
|
+
),
|
|
56
|
+
"hint": (
|
|
57
|
+
"Check the fixture name, where it is defined, its scope, and its "
|
|
58
|
+
"dependency chain."
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
elif _contains_any(
|
|
62
|
+
text,
|
|
63
|
+
("ImportError", "ModuleNotFoundError", "cannot import name", "No module named"),
|
|
64
|
+
):
|
|
65
|
+
result = {
|
|
66
|
+
"type": "import_error",
|
|
67
|
+
"title": "Import error",
|
|
68
|
+
"explanation": (
|
|
69
|
+
"Python could not import a module or symbol required while collecting "
|
|
70
|
+
"or running this test."
|
|
71
|
+
),
|
|
72
|
+
"hint": (
|
|
73
|
+
"Verify the package is installed, the import path is correct, and the "
|
|
74
|
+
"symbol exists without a circular import."
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
elif _contains_any(
|
|
78
|
+
text,
|
|
79
|
+
("Timeout", "TimeoutError", "timed out", "pytest-timeout", "Failed: Timeout"),
|
|
80
|
+
):
|
|
81
|
+
result = {
|
|
82
|
+
"type": "timeout",
|
|
83
|
+
"title": "Timeout",
|
|
84
|
+
"explanation": (
|
|
85
|
+
"The test exceeded a time limit or waited too long for an operation "
|
|
86
|
+
"to complete."
|
|
87
|
+
),
|
|
88
|
+
"hint": (
|
|
89
|
+
"Find the blocked operation, replace fixed sleeps with bounded waits, "
|
|
90
|
+
"and confirm external dependencies respond."
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
elif _contains_any(
|
|
94
|
+
text,
|
|
95
|
+
(
|
|
96
|
+
"AssertionError",
|
|
97
|
+
"E assert",
|
|
98
|
+
"Differing items",
|
|
99
|
+
"Left contains",
|
|
100
|
+
"Right contains",
|
|
101
|
+
),
|
|
102
|
+
):
|
|
103
|
+
result = {
|
|
104
|
+
"type": "assertion_mismatch",
|
|
105
|
+
"title": "Assertion mismatch",
|
|
106
|
+
"explanation": (
|
|
107
|
+
"The code ran, but the observed value or state did not match what the "
|
|
108
|
+
"test expected."
|
|
109
|
+
),
|
|
110
|
+
"hint": (
|
|
111
|
+
"Compare the expected and actual values near the final assertion, then "
|
|
112
|
+
"trace where they first diverge."
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
else:
|
|
116
|
+
result = {
|
|
117
|
+
"type": "unknown_failure",
|
|
118
|
+
"title": "Unknown failure",
|
|
119
|
+
"explanation": (
|
|
120
|
+
"The failure does not match a known pytest-why pattern, so the "
|
|
121
|
+
"traceback remains the best source of detail."
|
|
122
|
+
),
|
|
123
|
+
"hint": (
|
|
124
|
+
"Start at the last application frame in the traceback and inspect the "
|
|
125
|
+
"inputs and state immediately before the exception."
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
result["hint"] = _with_browser_hint(result["hint"], text)
|
|
130
|
+
return result
|
pytest_why/plugin.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Pytest hooks for pytest-why."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from .classifier import classify_failure
|
|
9
|
+
from .reporter import write_html_report, write_markdown_report
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
13
|
+
group = parser.getgroup("pytest-why")
|
|
14
|
+
group.addoption(
|
|
15
|
+
"--why",
|
|
16
|
+
action="store_true",
|
|
17
|
+
default=False,
|
|
18
|
+
help="Explain failures and write pytest-why Markdown and HTML reports.",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
23
|
+
if config.getoption("--why"):
|
|
24
|
+
config.pluginmanager.register(WhyPlugin(), "pytest-why-runtime")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WhyPlugin:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.failures: List[Dict[str, Any]] = []
|
|
30
|
+
|
|
31
|
+
def _record_failure(self, report: Any, phase: str) -> None:
|
|
32
|
+
if not report.failed:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
duration = getattr(report, "duration", None)
|
|
36
|
+
classification = classify_failure(
|
|
37
|
+
nodeid=report.nodeid,
|
|
38
|
+
phase=phase,
|
|
39
|
+
longreprtext=report.longreprtext,
|
|
40
|
+
duration=duration,
|
|
41
|
+
)
|
|
42
|
+
self.failures.append(
|
|
43
|
+
{
|
|
44
|
+
"nodeid": report.nodeid,
|
|
45
|
+
"phase": phase,
|
|
46
|
+
"duration": duration,
|
|
47
|
+
"longreprtext": report.longreprtext,
|
|
48
|
+
**classification,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
|
|
53
|
+
self._record_failure(report, report.when)
|
|
54
|
+
|
|
55
|
+
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
|
|
56
|
+
self._record_failure(report, "collect")
|
|
57
|
+
|
|
58
|
+
def pytest_terminal_summary(
|
|
59
|
+
self,
|
|
60
|
+
terminalreporter: pytest.TerminalReporter,
|
|
61
|
+
exitstatus: int,
|
|
62
|
+
config: pytest.Config,
|
|
63
|
+
) -> None:
|
|
64
|
+
terminalreporter.section("pytest-why: failure explanations")
|
|
65
|
+
terminalreporter.write_line(f"Total failures: {len(self.failures)}")
|
|
66
|
+
for failure in self.failures:
|
|
67
|
+
terminalreporter.write_line(
|
|
68
|
+
f"{failure['title']}: {failure['nodeid']} ({failure['phase']})"
|
|
69
|
+
)
|
|
70
|
+
terminalreporter.write_line(f" Why: {failure['explanation']}")
|
|
71
|
+
terminalreporter.write_line(f" Hint: {failure['hint']}")
|
|
72
|
+
|
|
73
|
+
report_dir = Path.cwd()
|
|
74
|
+
write_markdown_report(self.failures, report_dir / "pytest-why-report.md")
|
|
75
|
+
write_html_report(self.failures, report_dir / "pytest-why-report.html")
|
|
76
|
+
terminalreporter.write_line("Reports: pytest-why-report.md, pytest-why-report.html")
|
pytest_why/reporter.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Markdown and HTML report writers."""
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Iterable, Mapping, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Failure = Mapping[str, Any]
|
|
9
|
+
PathLike = Union[str, Path]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _duration(failure: Failure) -> str:
|
|
13
|
+
duration = failure.get("duration")
|
|
14
|
+
if duration is None:
|
|
15
|
+
return "n/a"
|
|
16
|
+
return f"{float(duration):.3f}s"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _backtick_fence(content: str) -> str:
|
|
20
|
+
longest_run = 0
|
|
21
|
+
current_run = 0
|
|
22
|
+
for character in content:
|
|
23
|
+
if character == "`":
|
|
24
|
+
current_run += 1
|
|
25
|
+
longest_run = max(longest_run, current_run)
|
|
26
|
+
else:
|
|
27
|
+
current_run = 0
|
|
28
|
+
return "`" * max(3, longest_run + 1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_markdown_report(
|
|
32
|
+
failures: Iterable[Failure],
|
|
33
|
+
path: PathLike = "pytest-why-report.md",
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Write a human-readable Markdown failure report."""
|
|
36
|
+
items = list(failures)
|
|
37
|
+
lines = [
|
|
38
|
+
"# pytest-why failure explanations",
|
|
39
|
+
"",
|
|
40
|
+
f"**Total failures:** {len(items)}",
|
|
41
|
+
"",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for index, failure in enumerate(items, start=1):
|
|
45
|
+
traceback = str(failure.get("longreprtext", "")).rstrip()
|
|
46
|
+
fence = _backtick_fence(traceback)
|
|
47
|
+
lines.extend(
|
|
48
|
+
[
|
|
49
|
+
f"## {index}. `{failure['nodeid']}`",
|
|
50
|
+
"",
|
|
51
|
+
f"- **Phase:** `{failure['phase']}`",
|
|
52
|
+
f"- **Type:** `{failure['type']}` - {failure['title']}",
|
|
53
|
+
f"- **Duration:** {_duration(failure)}",
|
|
54
|
+
"",
|
|
55
|
+
f"**Why:** {failure['explanation']}",
|
|
56
|
+
"",
|
|
57
|
+
f"**Hint:** {failure['hint']}",
|
|
58
|
+
"",
|
|
59
|
+
"<details>",
|
|
60
|
+
"<summary>Raw traceback</summary>",
|
|
61
|
+
"",
|
|
62
|
+
f"{fence}text",
|
|
63
|
+
traceback,
|
|
64
|
+
fence,
|
|
65
|
+
"",
|
|
66
|
+
"</details>",
|
|
67
|
+
"",
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
Path(path).write_text("\n".join(lines), encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_html_report(
|
|
75
|
+
failures: Iterable[Failure],
|
|
76
|
+
path: PathLike = "pytest-why-report.html",
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Write a standalone HTML failure report."""
|
|
79
|
+
items = list(failures)
|
|
80
|
+
cards = []
|
|
81
|
+
for failure in items:
|
|
82
|
+
cards.append(
|
|
83
|
+
"""
|
|
84
|
+
<article class="card">
|
|
85
|
+
<h2>{nodeid}</h2>
|
|
86
|
+
<div class="meta">
|
|
87
|
+
<span>Phase: <code>{phase}</code></span>
|
|
88
|
+
<span>Type: <code>{type}</code> - {title}</span>
|
|
89
|
+
<span>Duration: {duration}</span>
|
|
90
|
+
</div>
|
|
91
|
+
<h3>Why</h3>
|
|
92
|
+
<p>{explanation}</p>
|
|
93
|
+
<h3>Hint</h3>
|
|
94
|
+
<p>{hint}</p>
|
|
95
|
+
<details>
|
|
96
|
+
<summary>Raw traceback</summary>
|
|
97
|
+
<pre>{traceback}</pre>
|
|
98
|
+
</details>
|
|
99
|
+
</article>
|
|
100
|
+
""".format(
|
|
101
|
+
nodeid=escape(str(failure["nodeid"])),
|
|
102
|
+
phase=escape(str(failure["phase"])),
|
|
103
|
+
type=escape(str(failure["type"])),
|
|
104
|
+
title=escape(str(failure["title"])),
|
|
105
|
+
duration=escape(_duration(failure)),
|
|
106
|
+
explanation=escape(str(failure["explanation"])),
|
|
107
|
+
hint=escape(str(failure["hint"])),
|
|
108
|
+
traceback=escape(str(failure.get("longreprtext", ""))),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
document = """<!doctype html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="utf-8">
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
117
|
+
<title>pytest-why failure explanations</title>
|
|
118
|
+
<style>
|
|
119
|
+
:root {{ color-scheme: light dark; }}
|
|
120
|
+
body {{ font-family: system-ui, sans-serif; max-width: 960px; margin: 0 auto;
|
|
121
|
+
padding: 2rem; line-height: 1.5; background: #f6f7f9; color: #18202a; }}
|
|
122
|
+
header {{ margin-bottom: 1.5rem; }}
|
|
123
|
+
.card {{ background: white; border: 1px solid #d9dee5; border-radius: 10px;
|
|
124
|
+
padding: 1.25rem; margin: 1rem 0; box-shadow: 0 2px 8px #0000000d; }}
|
|
125
|
+
.card h2 {{ overflow-wrap: anywhere; margin-top: 0; }}
|
|
126
|
+
.meta {{ display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; color: #4b5563; }}
|
|
127
|
+
code, pre {{ font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }}
|
|
128
|
+
pre {{ overflow: auto; padding: 1rem; border-radius: 6px; background: #111827;
|
|
129
|
+
color: #e5e7eb; white-space: pre-wrap; }}
|
|
130
|
+
summary {{ cursor: pointer; font-weight: 600; }}
|
|
131
|
+
@media (prefers-color-scheme: dark) {{
|
|
132
|
+
body {{ background: #111827; color: #e5e7eb; }}
|
|
133
|
+
.card {{ background: #1f2937; border-color: #374151; }}
|
|
134
|
+
.meta {{ color: #cbd5e1; }}
|
|
135
|
+
}}
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
<header>
|
|
140
|
+
<h1>pytest-why: failure explanations</h1>
|
|
141
|
+
<p><strong>Total failures:</strong> {count}</p>
|
|
142
|
+
</header>
|
|
143
|
+
<main>{cards}</main>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
146
|
+
""".format(count=len(items), cards="".join(cards))
|
|
147
|
+
Path(path).write_text(document, encoding="utf-8")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-why
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pytest plugin that explains failing tests like a senior engineer.
|
|
5
|
+
Author: pytest-why contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.dhirajdas.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/godhiraj-code/pytest-why
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pytest>=7
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# pytest-why
|
|
30
|
+
|
|
31
|
+
> A pytest plugin that explains failing tests like a senior engineer.
|
|
32
|
+
|
|
33
|
+
`pytest-why` turns common pytest failures into concise explanations and practical
|
|
34
|
+
next steps. It runs locally, adds no noise to normal test runs, and creates
|
|
35
|
+
shareable Markdown and HTML reports when you ask for them.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python -m pip install pytest-why
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For local development:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install -e .[dev]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Run pytest with one extra flag:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pytest --why
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Without `--why`, the plugin does nothing and creates no report files.
|
|
58
|
+
|
|
59
|
+
## Viral demo
|
|
60
|
+
|
|
61
|
+
Given:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def test_math():
|
|
65
|
+
assert 2 + 2 == 5
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pytest --why
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Sample output:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
================ pytest-why: failure explanations ================
|
|
78
|
+
Total failures: 1
|
|
79
|
+
Assertion mismatch: test_math.py::test_math (call)
|
|
80
|
+
Why: The code ran, but the observed value or state did not match what the test expected.
|
|
81
|
+
Hint: Compare the expected and actual values near the final assertion, then trace where they first diverge.
|
|
82
|
+
Reports: pytest-why-report.md, pytest-why-report.html
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Reports
|
|
86
|
+
|
|
87
|
+
Each `--why` run writes:
|
|
88
|
+
|
|
89
|
+
- `pytest-why-report.md` for pull requests, issue trackers, and terminals
|
|
90
|
+
- `pytest-why-report.html` for a styled, standalone browser view
|
|
91
|
+
|
|
92
|
+
Both reports include the failing test, pytest phase, classification, duration,
|
|
93
|
+
explanation, hint, and the complete raw traceback.
|
|
94
|
+
|
|
95
|
+
## Supported classifications
|
|
96
|
+
|
|
97
|
+
- **Assertion mismatch**: expected and actual values differ
|
|
98
|
+
- **Import error**: a module or symbol could not be imported
|
|
99
|
+
- **Fixture error**: missing fixtures, scope mismatches, or recursive dependencies
|
|
100
|
+
- **Timeout**: a test or operation exceeded its time limit
|
|
101
|
+
- **Unknown failure**: deterministic fallback with traceback-first guidance
|
|
102
|
+
|
|
103
|
+
Selenium and Playwright tracebacks also receive a focused browser automation
|
|
104
|
+
hint covering selectors, waits, page timing, and element visibility.
|
|
105
|
+
|
|
106
|
+
The MVP is deterministic and local. No LLM or network connection is required.
|
|
107
|
+
|
|
108
|
+
**Stop doomscrolling tracebacks. Run pytest --why.**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pytest_why/__init__.py,sha256=nTl0qlGDZO5YClO-pUfZCXNUPI2FN-4nwQzS9qDcRGg,98
|
|
2
|
+
pytest_why/classifier.py,sha256=owIQGXM8mg8uYPMR4GxzEb0qU8e4m-sy9FQfpehtWC8,4212
|
|
3
|
+
pytest_why/plugin.py,sha256=X-VaWHQEASfHcaQDcGY3aOJc87pKL6bq7IOfhnTQMcY,2547
|
|
4
|
+
pytest_why/reporter.py,sha256=a-nTYF9fRHtQA_un60Z7vmNPQ69C7J4FvQxaBlU4Tis,4844
|
|
5
|
+
pytest_why-0.1.0.dist-info/licenses/LICENSE,sha256=XmPzRh-kKDeQQez3kovjZKId7D2OYf5gZwtxolmjHF0,1080
|
|
6
|
+
pytest_why-0.1.0.dist-info/METADATA,sha256=GTqTROj0AaVYGF9rKuCvo5Bo7IQSrmPGlRsmIU3Cheg,3242
|
|
7
|
+
pytest_why-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
pytest_why-0.1.0.dist-info/entry_points.txt,sha256=t4CtYc-h5dGN-9iRyf81NL-sPSyY7Xj84Y8yEO-wMqw,35
|
|
9
|
+
pytest_why-0.1.0.dist-info/top_level.txt,sha256=n6uxzE_N4YpeRE8ovmIOZFIamBeIQpXEvca5G8FCYA4,11
|
|
10
|
+
pytest_why-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pytest-why contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_why
|