pytest-deadfixtures 4.0.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.
@@ -0,0 +1,3 @@
1
+ from .plugin import deadfixtures_ignore
2
+
3
+ __all__ = ["deadfixtures_ignore"]
@@ -0,0 +1,356 @@
1
+ """
2
+ Some functions are basically copy n' paste version of code already in pytest.
3
+ Precisely the get_fixtures, get_used_fixturesdefs and write_docstring functions.
4
+ """
5
+
6
+ import os.path
7
+ from itertools import combinations
8
+ from pathlib import Path
9
+ from textwrap import dedent
10
+ from types import FunctionType
11
+ from typing import Any, Iterable, NamedTuple
12
+
13
+ import _pytest.config
14
+ import _pytest.main
15
+ import pytest
16
+ from _pytest.compat import getlocation
17
+
18
+ DUPLICATE_FIXTURES_HEADLINE = "\n\nYou may have some duplicate fixtures:"
19
+ UNUSED_FIXTURES_FOUND_HEADLINE = (
20
+ "Hey there, I believe the following {count} fixture(s) are not being used:"
21
+ )
22
+ UNUSED_FIXTURES_NOT_FOUND_HEADLINE = "Cool, every declared fixture is being used."
23
+
24
+ IGNORED_FIXTURES_HEADLINE = "Ignored fixture(s) {count}:"
25
+ IGNORED_FIXTURES_ATTR = "_deadfixtures_ignore"
26
+
27
+ EXIT_CODE_ERROR = 11
28
+ EXIT_CODE_SUCCESS = 0
29
+
30
+
31
+ class AvailableFixture(NamedTuple):
32
+ relpath: str
33
+ argname: str
34
+ fixturedef: Any
35
+
36
+
37
+ class CachedFixture(NamedTuple):
38
+ fixturedef: Any
39
+ relpath: str
40
+ result: Any
41
+
42
+
43
+ def pytest_addoption(parser: pytest.Parser) -> None:
44
+ group = parser.getgroup("deadfixtures")
45
+ group.addoption(
46
+ "--dead-fixtures",
47
+ action="store_true",
48
+ dest="deadfixtures",
49
+ default=False,
50
+ help="Show fixtures not being used",
51
+ )
52
+ group.addoption(
53
+ "--dup-fixtures",
54
+ action="store_true",
55
+ dest="showrepeated",
56
+ default=False,
57
+ help="Show duplicated fixtures",
58
+ )
59
+ group.addoption(
60
+ "--show-ignored-fixtures",
61
+ action="store_true",
62
+ default=False,
63
+ help="Show fixtures ignored with `deadfixtures_ignore` mark",
64
+ )
65
+
66
+
67
+ def pytest_cmdline_main(config: pytest.Config) -> int | None:
68
+ if config.option.deadfixtures:
69
+ config.option.show_fixture_doc = config.option.verbose
70
+ config.option.verbose = -1
71
+ return _show_dead_fixtures(config)
72
+ return None
73
+
74
+
75
+ def _show_dead_fixtures(config: pytest.Config) -> int | pytest.ExitCode:
76
+ return _pytest.main.wrap_session(config, show_dead_fixtures)
77
+
78
+
79
+ def commonpath(path1: Path, path2: Path) -> Path | None:
80
+ """Return the common part shared with the other path, or None if there is
81
+ no common part.
82
+
83
+ If one path is relative and one is absolute, returns None.
84
+ """
85
+ try:
86
+ return Path(os.path.commonpath((str(path1), str(path2))))
87
+ except ValueError:
88
+ return None
89
+
90
+
91
+ def bestrelpath(directory: Path, dest: Path) -> str:
92
+ """Return a string which is a relative path from directory to dest such
93
+ that directory/bestrelpath == dest.
94
+
95
+ The paths must be either both absolute or both relative.
96
+
97
+ If no such path can be determined, returns dest.
98
+ """
99
+ assert isinstance(directory, Path)
100
+ assert isinstance(dest, Path)
101
+ if dest == directory:
102
+ return os.curdir
103
+ # Find the longest common directory.
104
+ base = commonpath(directory, dest)
105
+ # Can be the case on Windows for two absolute paths on different drives.
106
+ # Can be the case for two relative paths without common prefix.
107
+ # Can be the case for a relative path and an absolute path.
108
+ if not base:
109
+ return str(dest)
110
+ reldirectory = directory.relative_to(base)
111
+ reldest = dest.relative_to(base)
112
+ return str(Path(*([os.pardir] * len(reldirectory.parts))).joinpath(*reldest.parts))
113
+
114
+
115
+ def get_best_relpath(func, curdir: Path) -> str:
116
+ loc = getlocation(func, str(curdir))
117
+ return bestrelpath(curdir, Path(loc))
118
+
119
+
120
+ def deadfixtures_ignore(func):
121
+ """Decorator to mark fixtures that should be ignored by the plugin."""
122
+ setattr(func, IGNORED_FIXTURES_ATTR, True)
123
+ return func
124
+
125
+
126
+ def is_ignored_fixture(fixturedef) -> bool:
127
+ """Check if a fixture is marked as ignored."""
128
+ return getattr(fixturedef.func, IGNORED_FIXTURES_ATTR, False)
129
+
130
+
131
+ if pytest.version_tuple < (9, 2, 0):
132
+
133
+ def get_all_fixture_defs(session: pytest.Session) -> Iterable[Any]:
134
+ fm = session._fixturemanager
135
+ for fixturedefs in fm._arg2fixturedefs.values():
136
+ yield from fixturedefs
137
+ else:
138
+
139
+ def get_all_fixture_defs(session: pytest.Session) -> Iterable[Any]:
140
+ fm = session._fixturemanager
141
+ return fm._get_all_fixture_defs()
142
+
143
+
144
+ def get_fixtures(session: pytest.Session) -> list[AvailableFixture]:
145
+ available = []
146
+ seen = set()
147
+ curdir = Path.cwd()
148
+
149
+ for fixturedef in get_all_fixture_defs(session):
150
+ loc = getlocation(fixturedef.func, str(curdir))
151
+ if (fixturedef.argname, loc) in seen:
152
+ continue
153
+
154
+ seen.add((fixturedef.argname, loc))
155
+
156
+ module = fixturedef.func.__module__
157
+
158
+ if (
159
+ not module.startswith("_pytest.")
160
+ and not module.startswith("pytest_")
161
+ and "site-packages" not in loc
162
+ and "dist-packages" not in loc
163
+ and "<string>" not in loc
164
+ ):
165
+ available.append(
166
+ AvailableFixture(
167
+ bestrelpath(curdir, Path(loc)), fixturedef.argname, fixturedef
168
+ )
169
+ )
170
+
171
+ available.sort(key=lambda a: a.relpath)
172
+ return available
173
+
174
+
175
+ def get_used_fixturesdefs(session: pytest.Session):
176
+ fixturesdefs = []
177
+ for test_function in session.items:
178
+ try:
179
+ info = test_function._fixtureinfo
180
+ except AttributeError:
181
+ # doctests items have no _fixtureinfo attribute
182
+ continue
183
+ if not info.name2fixturedefs:
184
+ # this test item does not use any fixtures
185
+ continue
186
+
187
+ for _, fixturedefs in sorted(info.name2fixturedefs.items()):
188
+ if fixturedefs is None:
189
+ continue
190
+ fixturesdefs.append(fixturedefs[-1])
191
+ return fixturesdefs
192
+
193
+
194
+ def get_parametrized_fixtures(
195
+ session: pytest.Session, available_fixtures: list[AvailableFixture]
196
+ ):
197
+ params_values = []
198
+ for test_function in session.items:
199
+ try:
200
+ for v in test_function.callspec.params.values():
201
+ if isinstance(v, str):
202
+ params_values.append(v)
203
+ except AttributeError:
204
+ continue
205
+
206
+ return [
207
+ available.fixturedef
208
+ for available in available_fixtures
209
+ if available.fixturedef.argname in params_values
210
+ ]
211
+
212
+
213
+ def write_docstring(tw, doc: str) -> None:
214
+ INDENT = " "
215
+ doc = doc.rstrip()
216
+ if "\n" in doc:
217
+ firstline, rest = doc.split("\n", 1)
218
+ else:
219
+ firstline, rest = doc, ""
220
+
221
+ if firstline.strip():
222
+ tw.line(INDENT + firstline.strip())
223
+
224
+ if rest:
225
+ for line in dedent(rest).split("\n"):
226
+ tw.write(INDENT + line + "\n")
227
+
228
+
229
+ def write_fixtures(tw, fixtures, write_docs: bool) -> None:
230
+ for fixture in fixtures:
231
+ tplt = "Fixture name: {}, location: {}"
232
+ tw.line(tplt.format(fixture.argname, fixture.relpath))
233
+ doc = fixture.fixturedef.func.__doc__ or ""
234
+ if write_docs and doc:
235
+ write_docstring(tw, doc)
236
+
237
+
238
+ cached_fixtures: list[CachedFixture] = []
239
+
240
+
241
+ def pytest_sessionstart(session: pytest.Session) -> None:
242
+ cached_fixtures.clear()
243
+
244
+
245
+ def pytest_fixture_post_finalizer(fixturedef, request):
246
+ if not request.config.getvalue("showrepeated"):
247
+ return
248
+
249
+ if getattr(fixturedef, "cached_result", None):
250
+ curdir = Path.cwd()
251
+ loc = getlocation(fixturedef.func, str(curdir))
252
+
253
+ cached_fixtures.append(
254
+ CachedFixture(
255
+ fixturedef, bestrelpath(curdir, Path(loc)), fixturedef.cached_result[0]
256
+ )
257
+ )
258
+
259
+
260
+ def same_fixture(one, two) -> bool:
261
+ def result_same_type(a, b):
262
+ return isinstance(a.result, type(b.result))
263
+
264
+ def same_result(a, b) -> bool:
265
+ if not a.result or not b.result:
266
+ return False
267
+ if isinstance(a.result, FunctionType):
268
+ return a.result == b.result
269
+ a_result_dict = getattr(a.result, "__dict__", None)
270
+ b_result_dict = getattr(b.result, "__dict__", None)
271
+ if a_result_dict is not None and b_result_dict is not None:
272
+ return a_result_dict == b_result_dict
273
+ return a.result == b.result
274
+
275
+ def same_loc(a, b) -> bool:
276
+ return a.relpath == b.relpath
277
+
278
+ return result_same_type(one, two) and same_result(one, two) and not same_loc(one, two)
279
+
280
+
281
+ def pytest_sessionfinish(
282
+ session: pytest.Session, exitstatus: int | pytest.ExitCode
283
+ ) -> None:
284
+ if exitstatus or not session.config.getvalue("showrepeated"):
285
+ return
286
+
287
+ tw = _pytest.config.create_terminal_writer(session.config)
288
+
289
+ duplicated_fixtures = [
290
+ (a, b) for a, b in combinations(cached_fixtures, 2) if same_fixture(a, b)
291
+ ]
292
+
293
+ if duplicated_fixtures:
294
+ tw.line(DUPLICATE_FIXTURES_HEADLINE, red=True)
295
+ msg = "Fixture name: {}, location: {}"
296
+ for a, b in duplicated_fixtures:
297
+ tw.line(msg.format(a.fixturedef.argname, a.relpath))
298
+ tw.line(msg.format(b.fixturedef.argname, b.relpath))
299
+
300
+
301
+ def show_dead_fixtures(
302
+ config: pytest.Config, session: pytest.Session
303
+ ) -> int | pytest.ExitCode | None:
304
+ session.perform_collect()
305
+ tw = _pytest.config.create_terminal_writer(config)
306
+ show_fixture_doc = config.getvalue("show_fixture_doc")
307
+ show_ignored = config.getvalue("show_ignored_fixtures")
308
+
309
+ used_fixtures = get_used_fixturesdefs(session)
310
+ available_fixtures = get_fixtures(session)
311
+ param_fixtures = get_parametrized_fixtures(session, available_fixtures)
312
+
313
+ curdir = Path.cwd()
314
+
315
+ def _fixture_key(fixturedef):
316
+ return fixturedef.argname, getlocation(fixturedef.func, curdir)
317
+
318
+ used_keys = {_fixture_key(fixturedef) for fixturedef in used_fixtures}
319
+ param_keys = {_fixture_key(fixturedef) for fixturedef in param_fixtures}
320
+
321
+ # Separate ignored and unused fixtures
322
+ ignored_fixtures = [
323
+ fixture
324
+ for fixture in available_fixtures
325
+ if is_ignored_fixture(fixture.fixturedef)
326
+ ]
327
+
328
+ unused_fixtures = [
329
+ fixture
330
+ for fixture in available_fixtures
331
+ if _fixture_key(fixture.fixturedef) not in used_keys
332
+ and _fixture_key(fixture.fixturedef) not in param_keys
333
+ and not is_ignored_fixture(fixture.fixturedef)
334
+ ]
335
+
336
+ tw.line()
337
+ if unused_fixtures:
338
+ tw.line(
339
+ UNUSED_FIXTURES_FOUND_HEADLINE.format(count=len(unused_fixtures)), red=True
340
+ )
341
+ write_fixtures(tw, unused_fixtures, show_fixture_doc)
342
+ else:
343
+ tw.line(UNUSED_FIXTURES_NOT_FOUND_HEADLINE, green=True)
344
+
345
+ # Show ignored fixtures if requested
346
+ if show_ignored and ignored_fixtures:
347
+ tw.line()
348
+ tw.line(
349
+ IGNORED_FIXTURES_HEADLINE.format(count=len(ignored_fixtures)), yellow=True
350
+ )
351
+ write_fixtures(tw, ignored_fixtures, show_fixture_doc)
352
+
353
+ if unused_fixtures:
354
+ return EXIT_CODE_ERROR
355
+ else:
356
+ return EXIT_CODE_SUCCESS
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytest-deadfixtures
3
+ Version: 4.0.0
4
+ Summary: A simple plugin to list unused fixtures in pytest
5
+ Project-URL: Issues, https://github.com/jllorencetti/pytest-deadfixtures/issues
6
+ Project-URL: Source, https://github.com/jllorencetti/pytest-deadfixtures
7
+ Author-email: João Luiz Lorencetti <me@dirtycoder.net>
8
+ Maintainer-email: João Luiz Lorencetti <me@dirtycoder.net>, Christian Hartung <6785871+hartungstenio@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Programming Language :: Python :: Implementation :: CPython
24
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
25
+ Classifier: Topic :: Software Development :: Testing
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: pytest>=9.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # pytest-deadfixtures
31
+
32
+ [![PyPI - Version](https://img.shields.io/pypi/v/pytest-deadfixtures.svg)](https://pypi.org/project/pytest-deadfixtures)
33
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-deadfixtures.svg)](https://pypi.org/project/pytest-deadfixtures)
34
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jllorencetti/pytest-deadfixtures/main.svg)](https://results.pre-commit.ci/latest/github/jllorencetti/pytest-deadfixtures/main)
35
+
36
+ ----
37
+
38
+ A simple plugin to list unused or duplicated fixtures in a pytest suite.
39
+
40
+ ## Features
41
+
42
+ - List unused fixtures in your tests
43
+ - List duplicated fixtures
44
+ - Ignore specific fixtures from analysis with `@deadfixtures_ignore`
45
+
46
+ ## Installation
47
+
48
+ Install `pytest-deadfixtures` from [PyPI](https://pypi.org/project/pytest-deadfixtures/):
49
+
50
+ ```console
51
+ pip install pytest-deadfixtures
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Important
57
+
58
+ The `--dead-fixtures` option does not run your tests and can also be sensitive to
59
+ errors during the pytest collection step. If using it in CI, run it after the
60
+ normal test run:
61
+
62
+ ```yaml
63
+ script:
64
+ - pytest
65
+ - pytest --dead-fixtures
66
+ ```
67
+
68
+ ### Listing unused fixtures
69
+
70
+ Run pytest with the `--dead-fixtures` option:
71
+
72
+ ```console
73
+ pytest --dead-fixtures
74
+ ```
75
+
76
+ ```text
77
+ Hey there, I believe the following fixture(s) are not being used:
78
+ Fixture name: some_fixture, location: test_write_docs_when_verbose.py:5
79
+
80
+ no tests ran
81
+ ```
82
+
83
+ Use verbosity to include fixture docstrings in the report:
84
+
85
+ ```console
86
+ pytest --dead-fixtures -v
87
+ ```
88
+
89
+ ### Listing repeated fixtures
90
+
91
+ The `--dup-fixtures` option compares fixture return values to find possible
92
+ duplicates. Fixtures without a truthy return value are skipped. Treat the report
93
+ as a hint and verify that the fixtures provide the same functionality before
94
+ removing one.
95
+
96
+ Unlike `--dead-fixtures`, this option runs the tests:
97
+
98
+ ```console
99
+ pytest --dup-fixtures
100
+ ```
101
+
102
+ ```text
103
+ tests/test_deadfixtures.py ........
104
+
105
+ You may have some duplicate fixtures:
106
+ Fixture name: someclass_fixture, location: test_repeated_fixtures.py:12
107
+ Fixture name: someclass_samefixture, location: test_repeated_fixtures.py:17
108
+ ```
109
+
110
+ ### Ignoring specific fixtures
111
+
112
+ Use the `@deadfixtures_ignore` decorator to exclude a fixture from the analysis:
113
+
114
+ ```python
115
+ import pytest
116
+
117
+ from pytest_deadfixtures import deadfixtures_ignore
118
+
119
+
120
+ @pytest.fixture
121
+ @deadfixtures_ignore
122
+ def intentionally_unused_fixture():
123
+ """This fixture won't be reported as unused."""
124
+ return "something"
125
+ ```
126
+
127
+ To see which fixtures are ignored, use `--show-ignored-fixtures`:
128
+
129
+ ```console
130
+ pytest --dead-fixtures --show-ignored-fixtures
131
+ ```
132
+
133
+ ```text
134
+ No unused fixtures found. Great job!
135
+
136
+ Ignored fixture(s) 1:
137
+ Fixture name: intentionally_unused_fixture, location: conftest.py:10
138
+ ```
139
+
140
+ ## Projects using it
141
+
142
+ - [wemake-django-template](https://github.com/wemake-services/wemake-django-template)
143
+ - [fands.dev django template](https://github.com/fandsdev/django)
144
+
145
+ ## Contributing
146
+
147
+ Contributions are welcome. Run the tests with [`hatch test`](https://hatch.pypa.io/latest/cli/reference/#hatch-test)
148
+ and check the code with [`hatch check code`](https://hatch.pypa.io/latest/cli/reference/#hatch-check)
149
+ before submitting a pull request.
150
+
151
+ ## License
152
+
153
+ Distributed under the terms of the [MIT License](http://opensource.org/licenses/MIT).
154
+ `pytest-deadfixtures` is free and open source software.
155
+
156
+ ## Issues
157
+
158
+ If you encounter any problems, [file an issue](https://github.com/jllorencetti/pytest-deadfixtures/issues)
159
+ with a detailed description.
@@ -0,0 +1,7 @@
1
+ pytest_deadfixtures/__init__.py,sha256=IhffZzQvwp9lxwBblmXUK9vwjLw8sgEAokEFZvJHChw,75
2
+ pytest_deadfixtures/plugin.py,sha256=iHZsk9lNfPtCqTTdx9h5xWPHJrix4pA8zyFZSg9C4TY,10807
3
+ pytest_deadfixtures-4.0.0.dist-info/METADATA,sha256=MhE6tiTArfeULus5g7fUFnfl1_kZasCjS4LMB_BT8EI,4846
4
+ pytest_deadfixtures-4.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
5
+ pytest_deadfixtures-4.0.0.dist-info/entry_points.txt,sha256=tnHaWCnR8PjAP5gUtCq1xv6r5fXxU-vzobPE3mR7Y5U,53
6
+ pytest_deadfixtures-4.0.0.dist-info/licenses/LICENSE,sha256=6ZFh-GBggddu8SVAu3jMK_xAj7mq_nh7Q8X1gvYpZAA,1088
7
+ pytest_deadfixtures-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ deadfixtures = pytest_deadfixtures.plugin
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 João Luiz Lorencetti
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
13
+ all 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
21
+ THE SOFTWARE.