pytest-imply 0.1.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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ *.egg
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dimitri Staessens <dimitri@ouroboros.rocks>
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,3 @@
1
+ include LICENSE README.md
2
+ recursive-include tests *.py
3
+ recursive-include src *.py
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-imply
3
+ Version: 0.1.0
4
+ Summary: Pytest plugin for test implication — skip tests implied by stronger ones
5
+ Author-email: Dimitri Staessens <dimitri@ouroboros.rocks>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://codeberg.org/o7s/pytest-imply
8
+ Project-URL: Repository, https://codeberg.org/o7s/pytest-imply
9
+ Project-URL: Issues, https://codeberg.org/o7s/pytest-imply/issues
10
+ Keywords: pytest,testing,implication,monotonic,optimization
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pytest>=7.0
28
+ Dynamic: license-file
29
+
30
+ # pytest-imply
31
+
32
+ A pytest plugin for **test implication** — skip tests that are implied
33
+ by stronger ones.
34
+
35
+ ## The problem
36
+
37
+ Parametrized test suites often have an ordered severity axis:
38
+
39
+ ```
40
+ test_count[c100] PASSED 2.58s
41
+ test_count[c500] PASSED 4.62s
42
+ test_count[c1000] PASSED 7.13s
43
+ test_count[c5000] PASSED 27.45s
44
+ test_count[c10000] PASSED 52.90s
45
+ ```
46
+
47
+ If `c10000` passes, the smaller counts will almost certainly pass
48
+ too — running them all wastes CI time.
49
+
50
+ ## The solution
51
+
52
+ **pytest-imply** lets the developer declare implication relationships
53
+ between tests. When a stronger test passes, weaker tests are
54
+ short-circuited with a synthetic PASSED result:
55
+
56
+ ```
57
+ test_count[c10000] PASSED 52.90s
58
+ test_count[c5000] IMPLIED (higher count passed)
59
+ test_count[c1000] IMPLIED (higher count passed)
60
+ test_count[c500] IMPLIED (higher count passed)
61
+ test_count[c100] IMPLIED (higher count passed)
62
+ ```
63
+
64
+ If the strongest test fails, the plugin works downward to find the
65
+ threshold — no time wasted on tests below the passing level.
66
+
67
+ ## Installation
68
+
69
+ ```
70
+ pip install pytest-imply
71
+ ```
72
+
73
+ ## Markers
74
+
75
+ ### `monotonic` — parametrized implication
76
+
77
+ For tests parametrized along an ordered axis:
78
+
79
+ ```python
80
+ @pytest.mark.monotonic("count")
81
+ @pytest.mark.parametrize("count", [100, 500, 1000, 5000, 10000])
82
+ def test_stress(count):
83
+ run_workload(count)
84
+ ```
85
+
86
+ The highest value runs first. If it passes, all lower values are
87
+ implied. If it fails, the next-highest runs, and so on.
88
+
89
+ Use `direction="asc"` to run the smallest first:
90
+
91
+ ```python
92
+ @pytest.mark.monotonic("threshold", direction="asc")
93
+ @pytest.mark.parametrize("threshold", [0.01, 0.1, 0.5, 1.0])
94
+ def test_precision(threshold):
95
+ assert error < threshold
96
+ ```
97
+
98
+ ### `implies` / `implied_by` / `implied_by_any` / `implied_by_all` — named tokens
99
+
100
+ For arbitrary implication relationships between tests:
101
+
102
+ ```python
103
+ @pytest.mark.implies("full_stack_ok")
104
+ def test_integration():
105
+ """Full stack test — if this passes, unit tests are implied."""
106
+ ...
107
+
108
+ @pytest.mark.implied_by("full_stack_ok")
109
+ def test_unit():
110
+ """Implied by passing integration test — no need to run."""
111
+ ...
112
+ ```
113
+
114
+ Use `implied_by_any` for OR semantics (implied if **any** token was
115
+ recorded):
116
+
117
+ ```python
118
+ @pytest.mark.implied_by_any("tcp_ok", "udp_ok")
119
+ def test_loopback():
120
+ """Implied if either transport passed."""
121
+ ...
122
+ ```
123
+
124
+ Use `implied_by_all` for AND semantics (implied only if **all** tokens
125
+ were recorded):
126
+
127
+ ```python
128
+ @pytest.mark.implied_by_all("tcp_ok", "udp_ok")
129
+ def test_both_transports():
130
+ """Implied only if both transports passed."""
131
+ ...
132
+ ```
133
+
134
+ ## Ordering
135
+
136
+ The plugin builds a dependency graph from all implication
137
+ relationships and topologically sorts the test items using Kahn's
138
+ algorithm. This guarantees that implying tests always run before
139
+ their implied dependents. Tests without implication markers keep
140
+ their original collection order.
141
+
142
+ ## Caveats
143
+
144
+ - **Transitive propagation**: when a test is implied (short-circuited),
145
+ its `implies` tokens are still propagated, so downstream tests see
146
+ them. This means chains like A→B→C work as expected.
147
+
148
+ - **Stacked markers**: multiple markers of the same type on one test
149
+ are all honoured. For example, stacking two `@pytest.mark.implies`
150
+ decorators records both sets of tokens.
151
+
152
+ - **Token namespacing**: tokens live in a single flat namespace.
153
+ In large projects, use a prefix convention (e.g.,
154
+ `"mymodule::token"`) to avoid accidental collisions between
155
+ independently-authored test modules.
156
+
157
+ - **Fixtures**: when a test is implied, its fixtures **do not run**.
158
+ If an implied test has a session- or module-scoped fixture whose
159
+ side effects other tests depend on, those tests may break. Only
160
+ mark tests as implied when their fixtures are not needed by other
161
+ tests.
162
+
163
+ - **pytest-xdist**: the plugin does not support parallel workers.
164
+ Implication state is per-process. When xdist is active with
165
+ workers, implication logic is **automatically disabled** and a
166
+ warning is emitted. Use `-p no:imply` to suppress the warning.
167
+
168
+ - **Plugin interoperability**: when a test is implied, the plugin
169
+ returns `True` from `pytest_runtest_protocol`, which tells pytest
170
+ the item is fully handled. Other plugins that wrap the test
171
+ protocol (e.g., `pytest-cov`, `pytest-timeout`) will not see
172
+ implied tests. This is inherent to the short-circuit design.
173
+
174
+ - **Orphan tokens**: if an `implied_by` (or variant) references a
175
+ token that no test provides via `implies`, a warning is emitted
176
+ at collection time.
177
+
178
+ - **Dependency cycles**: if implication markers form a cycle, the
179
+ plugin falls back to original collection order for the affected
180
+ tests and emits a warning.
181
+
182
+ - **xfail interaction**: a test marked `@pytest.mark.xfail` with
183
+ `strict=False` that passes (xpass) **does** record its `implies`
184
+ tokens. With `strict=True`, an xpass is treated as a failure and
185
+ tokens are **not** recorded.
186
+
187
+ ## Disabling implication
188
+
189
+ ### `--no-imply`
190
+
191
+ Disable all implication logic and run every test:
192
+
193
+ ```
194
+ pytest --no-imply
195
+ ```
196
+
197
+ Use this for exhaustive nightly or release-gate runs to verify that
198
+ the developer's implication assumptions still hold.
199
+
200
+ ### `imply_enabled` ini option
201
+
202
+ Disable implication via configuration instead of a CLI flag:
203
+
204
+ ```toml
205
+ [tool.pytest.ini_options]
206
+ imply_enabled = false
207
+ ```
208
+
209
+ ## How it works
210
+
211
+ 1. **Collection** — `pytest_collection_modifyitems` builds the
212
+ dependency graph and topologically sorts items.
213
+ 2. **Execution** — `pytest_runtest_protocol` checks whether a test is
214
+ implied; if so, it emits synthetic `TestReport` objects and
215
+ returns `True`.
216
+ 3. **Recording** — `pytest_runtest_makereport` records tokens and
217
+ monotonic passes after a test succeeds.
218
+ 4. **Reporting** — `pytest_report_teststatus` renders implied tests as
219
+ `IMPLIED (reason)` with a cyan `i` marker.
220
+
221
+ ## License
222
+
223
+ MIT
@@ -0,0 +1,194 @@
1
+ # pytest-imply
2
+
3
+ A pytest plugin for **test implication** — skip tests that are implied
4
+ by stronger ones.
5
+
6
+ ## The problem
7
+
8
+ Parametrized test suites often have an ordered severity axis:
9
+
10
+ ```
11
+ test_count[c100] PASSED 2.58s
12
+ test_count[c500] PASSED 4.62s
13
+ test_count[c1000] PASSED 7.13s
14
+ test_count[c5000] PASSED 27.45s
15
+ test_count[c10000] PASSED 52.90s
16
+ ```
17
+
18
+ If `c10000` passes, the smaller counts will almost certainly pass
19
+ too — running them all wastes CI time.
20
+
21
+ ## The solution
22
+
23
+ **pytest-imply** lets the developer declare implication relationships
24
+ between tests. When a stronger test passes, weaker tests are
25
+ short-circuited with a synthetic PASSED result:
26
+
27
+ ```
28
+ test_count[c10000] PASSED 52.90s
29
+ test_count[c5000] IMPLIED (higher count passed)
30
+ test_count[c1000] IMPLIED (higher count passed)
31
+ test_count[c500] IMPLIED (higher count passed)
32
+ test_count[c100] IMPLIED (higher count passed)
33
+ ```
34
+
35
+ If the strongest test fails, the plugin works downward to find the
36
+ threshold — no time wasted on tests below the passing level.
37
+
38
+ ## Installation
39
+
40
+ ```
41
+ pip install pytest-imply
42
+ ```
43
+
44
+ ## Markers
45
+
46
+ ### `monotonic` — parametrized implication
47
+
48
+ For tests parametrized along an ordered axis:
49
+
50
+ ```python
51
+ @pytest.mark.monotonic("count")
52
+ @pytest.mark.parametrize("count", [100, 500, 1000, 5000, 10000])
53
+ def test_stress(count):
54
+ run_workload(count)
55
+ ```
56
+
57
+ The highest value runs first. If it passes, all lower values are
58
+ implied. If it fails, the next-highest runs, and so on.
59
+
60
+ Use `direction="asc"` to run the smallest first:
61
+
62
+ ```python
63
+ @pytest.mark.monotonic("threshold", direction="asc")
64
+ @pytest.mark.parametrize("threshold", [0.01, 0.1, 0.5, 1.0])
65
+ def test_precision(threshold):
66
+ assert error < threshold
67
+ ```
68
+
69
+ ### `implies` / `implied_by` / `implied_by_any` / `implied_by_all` — named tokens
70
+
71
+ For arbitrary implication relationships between tests:
72
+
73
+ ```python
74
+ @pytest.mark.implies("full_stack_ok")
75
+ def test_integration():
76
+ """Full stack test — if this passes, unit tests are implied."""
77
+ ...
78
+
79
+ @pytest.mark.implied_by("full_stack_ok")
80
+ def test_unit():
81
+ """Implied by passing integration test — no need to run."""
82
+ ...
83
+ ```
84
+
85
+ Use `implied_by_any` for OR semantics (implied if **any** token was
86
+ recorded):
87
+
88
+ ```python
89
+ @pytest.mark.implied_by_any("tcp_ok", "udp_ok")
90
+ def test_loopback():
91
+ """Implied if either transport passed."""
92
+ ...
93
+ ```
94
+
95
+ Use `implied_by_all` for AND semantics (implied only if **all** tokens
96
+ were recorded):
97
+
98
+ ```python
99
+ @pytest.mark.implied_by_all("tcp_ok", "udp_ok")
100
+ def test_both_transports():
101
+ """Implied only if both transports passed."""
102
+ ...
103
+ ```
104
+
105
+ ## Ordering
106
+
107
+ The plugin builds a dependency graph from all implication
108
+ relationships and topologically sorts the test items using Kahn's
109
+ algorithm. This guarantees that implying tests always run before
110
+ their implied dependents. Tests without implication markers keep
111
+ their original collection order.
112
+
113
+ ## Caveats
114
+
115
+ - **Transitive propagation**: when a test is implied (short-circuited),
116
+ its `implies` tokens are still propagated, so downstream tests see
117
+ them. This means chains like A→B→C work as expected.
118
+
119
+ - **Stacked markers**: multiple markers of the same type on one test
120
+ are all honoured. For example, stacking two `@pytest.mark.implies`
121
+ decorators records both sets of tokens.
122
+
123
+ - **Token namespacing**: tokens live in a single flat namespace.
124
+ In large projects, use a prefix convention (e.g.,
125
+ `"mymodule::token"`) to avoid accidental collisions between
126
+ independently-authored test modules.
127
+
128
+ - **Fixtures**: when a test is implied, its fixtures **do not run**.
129
+ If an implied test has a session- or module-scoped fixture whose
130
+ side effects other tests depend on, those tests may break. Only
131
+ mark tests as implied when their fixtures are not needed by other
132
+ tests.
133
+
134
+ - **pytest-xdist**: the plugin does not support parallel workers.
135
+ Implication state is per-process. When xdist is active with
136
+ workers, implication logic is **automatically disabled** and a
137
+ warning is emitted. Use `-p no:imply` to suppress the warning.
138
+
139
+ - **Plugin interoperability**: when a test is implied, the plugin
140
+ returns `True` from `pytest_runtest_protocol`, which tells pytest
141
+ the item is fully handled. Other plugins that wrap the test
142
+ protocol (e.g., `pytest-cov`, `pytest-timeout`) will not see
143
+ implied tests. This is inherent to the short-circuit design.
144
+
145
+ - **Orphan tokens**: if an `implied_by` (or variant) references a
146
+ token that no test provides via `implies`, a warning is emitted
147
+ at collection time.
148
+
149
+ - **Dependency cycles**: if implication markers form a cycle, the
150
+ plugin falls back to original collection order for the affected
151
+ tests and emits a warning.
152
+
153
+ - **xfail interaction**: a test marked `@pytest.mark.xfail` with
154
+ `strict=False` that passes (xpass) **does** record its `implies`
155
+ tokens. With `strict=True`, an xpass is treated as a failure and
156
+ tokens are **not** recorded.
157
+
158
+ ## Disabling implication
159
+
160
+ ### `--no-imply`
161
+
162
+ Disable all implication logic and run every test:
163
+
164
+ ```
165
+ pytest --no-imply
166
+ ```
167
+
168
+ Use this for exhaustive nightly or release-gate runs to verify that
169
+ the developer's implication assumptions still hold.
170
+
171
+ ### `imply_enabled` ini option
172
+
173
+ Disable implication via configuration instead of a CLI flag:
174
+
175
+ ```toml
176
+ [tool.pytest.ini_options]
177
+ imply_enabled = false
178
+ ```
179
+
180
+ ## How it works
181
+
182
+ 1. **Collection** — `pytest_collection_modifyitems` builds the
183
+ dependency graph and topologically sorts items.
184
+ 2. **Execution** — `pytest_runtest_protocol` checks whether a test is
185
+ implied; if so, it emits synthetic `TestReport` objects and
186
+ returns `True`.
187
+ 3. **Recording** — `pytest_runtest_makereport` records tokens and
188
+ monotonic passes after a test succeeds.
189
+ 4. **Reporting** — `pytest_report_teststatus` renders implied tests as
190
+ `IMPLIED (reason)` with a cyan `i` marker.
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "pytest-imply"
3
+ dynamic = ["version"]
4
+ description = "Pytest plugin for test implication — skip tests implied by stronger ones"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Dimitri Staessens", email = "dimitri@ouroboros.rocks" },
9
+ ]
10
+ requires-python = ">=3.8"
11
+ dependencies = ["pytest>=7.0"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Framework :: Pytest",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Software Development :: Testing",
26
+ ]
27
+ keywords = ["pytest", "testing", "implication", "monotonic", "optimization"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://codeberg.org/o7s/pytest-imply"
31
+ Repository = "https://codeberg.org/o7s/pytest-imply"
32
+ Issues = "https://codeberg.org/o7s/pytest-imply/issues"
33
+
34
+ [project.entry-points.pytest11]
35
+ imply = "pytest_imply.plugin"
36
+
37
+ [build-system]
38
+ requires = ["setuptools>=64", "setuptools-scm>=8"]
39
+ build-backend = "setuptools.build_meta"
40
+
41
+ [tool.setuptools_scm]
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """pytest-imply — skip tests implied by stronger ones."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ from pytest_imply.plugin import PytestImplyWarning
6
+
7
+ try:
8
+ __version__ = version("pytest-imply")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
11
+
12
+ __all__ = ["PytestImplyWarning", "__version__"]