deadcode 2.6.0__tar.gz → 2.6.1__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 (39) hide show
  1. deadcode-2.6.0/README.md → deadcode-2.6.1/PKG-INFO +101 -8
  2. deadcode-2.6.0/PKG-INFO → deadcode-2.6.1/README.md +68 -41
  3. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/get_unused_names_error_message.py +2 -2
  4. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/merge_overlaping_file_parts.py +4 -4
  5. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/parse_abstract_syntax_tree.py +1 -1
  6. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/parse_arguments.py +7 -5
  7. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/dead_code_visitor.py +73 -15
  8. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/ignore.py +15 -1
  9. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/lines.py +5 -3
  10. {deadcode-2.6.0 → deadcode-2.6.1}/pyproject.toml +15 -10
  11. {deadcode-2.6.0 → deadcode-2.6.1}/.gitignore +0 -0
  12. {deadcode-2.6.0 → deadcode-2.6.1}/.venv310/bin/activate_this.py +0 -0
  13. {deadcode-2.6.0 → deadcode-2.6.1}/.venv311/bin/activate_this.py +0 -0
  14. {deadcode-2.6.0 → deadcode-2.6.1}/.venv314/bin/activate_this.py +0 -0
  15. {deadcode-2.6.0 → deadcode-2.6.1}/.venv315/bin/activate_this.py +0 -0
  16. {deadcode-2.6.0 → deadcode-2.6.1}/LICENSE +0 -0
  17. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/__init__.py +0 -0
  18. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/__init__.py +0 -0
  19. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/find_python_filenames.py +0 -0
  20. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/find_unused_names.py +0 -0
  21. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/fix_or_show_unused_code.py +0 -0
  22. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/get_unused_import_parts.py +0 -0
  23. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/remove_file_parts_from_content.py +0 -0
  24. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/cli.py +0 -0
  25. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/constants.py +0 -0
  26. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/data_types.py +0 -0
  27. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/__init__.py +0 -0
  28. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/add_colors_to_diff.py +0 -0
  29. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/fix_indent.py +0 -0
  30. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/flatten_lists.py +0 -0
  31. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/nested_scopes.py +0 -0
  32. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/print_ast.py +0 -0
  33. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/print_safely.py +0 -0
  34. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/__init__.py +0 -0
  35. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/code_item.py +7 -7
  36. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/module_exports.py +0 -0
  37. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/noqa.py +0 -0
  38. {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/utils.py +0 -0
  39. {deadcode-2.6.0 → deadcode-2.6.1}/docs/conf.py +0 -0
@@ -1,3 +1,36 @@
1
+ Metadata-Version: 2.5
2
+ Name: deadcode
3
+ Version: 2.6.1
4
+ Summary: Find and fix unused Python code using command line.
5
+ Project-URL: homepage, https://github.com/albertas/deadcode
6
+ Project-URL: documentation, https://deadcode.readthedocs.io/
7
+ Author-email: Albertas Gimbutas <albertasgim@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: tomli>=2.0.1; python_version < '3.11'
22
+ Provides-Extra: test
23
+ Requires-Dist: diff-cover; extra == 'test'
24
+ Requires-Dist: hatch; extra == 'test'
25
+ Requires-Dist: mypy; extra == 'test'
26
+ Requires-Dist: pip-audit; extra == 'test'
27
+ Requires-Dist: pre-commit; extra == 'test'
28
+ Requires-Dist: pytest; extra == 'test'
29
+ Requires-Dist: pytest-cov; extra == 'test'
30
+ Requires-Dist: pytest-xdist; extra == 'test'
31
+ Requires-Dist: ruff; extra == 'test'
32
+ Description-Content-Type: text/markdown
33
+
1
34
  [![Deadcode Logo](https://raw.githubusercontent.com/albertas/deadcode/main/docs/_static/deadcode-logo-readme.png)](https://deadcode.readthedocs.io/)
2
35
 
3
36
  <h2 align="center">Find and Fix Unused Python Code</h2>
@@ -75,6 +108,43 @@ ignore-bodies-if-inherits-from = ["BaseModel"]
75
108
  Read the [FastAPI guide](https://deadcode.readthedocs.io/en/latest/fastapi.html) for common use cases, the complete example application,
76
109
  the dependency injection strategy, and the expected output with no wrong detections.
77
110
 
111
+ ## Using deadcode on a Django project
112
+
113
+ Django registers models, views, admin classes, middleware, and signal handlers through base classes, decorators, settings strings, and URLconfs, so
114
+ framework entry points look unused to a static analyser. Configure deadcode like this:
115
+
116
+ ```toml
117
+ [tool.deadcode]
118
+ ignore-definitions-if-decorated-with = [
119
+ "@django.contrib.admin.*",
120
+ "@django.contrib.auth.*",
121
+ "@django.dispatch.receiver",
122
+ "@django.utils.decorators.method_decorator",
123
+ "@django.views.decorators.http.require_*",
124
+ "@*register.*",
125
+ ]
126
+ ignore-definitions-if-inherits-from = [
127
+ "django.apps.AppConfig",
128
+ "django.contrib.admin.*",
129
+ "django.core.management.base.BaseCommand",
130
+ "django.db.migrations.Migration",
131
+ "django.db.models.Model",
132
+ "django.forms.*Form",
133
+ "django.utils.deprecation.MiddlewareMixin",
134
+ "django.views.*View",
135
+ ]
136
+ ignore-names-in-files = ["*/settings.py", "*/urls.py"]
137
+ ```
138
+
139
+ The example targets Django 5.2 LTS (5.2.19). The Django REST Framework 3.18.3 entries are additions to the same two keys; see the
140
+ [Django REST Framework section of the Django guide](https://deadcode.readthedocs.io/en/latest/django.html#django-rest-framework). The three keys replace the
141
+ older overlapping name, body, and definition options: one inheritance entry hides a framework subclass name and body, and one decorator entry hides a
142
+ decorated definition name and body. deadcode parses `MIDDLEWARE` registrations and resolves absolute-import qualified names, so a registered middleware
143
+ name and a `models.Model` base need no separate name pattern.
144
+
145
+ Read the [Django guide](https://deadcode.readthedocs.io/en/latest/django.html) for the framework areas, the current and legacy
146
+ middleware forms, both import forms, database signals, the Django REST Framework entries, and the expected behaviour.
147
+
78
148
  ## Pre-commit hook
79
149
  Create a `.pre-commit-config.yaml` file in the root of your project directory, if it doesn't exist, and add the following to the file:
80
150
 
@@ -83,7 +153,7 @@ Create a `.pre-commit-config.yaml` file in the root of your project directory, i
83
153
  # See https://pre-commit.com/hooks.html for more hooks
84
154
  repos:
85
155
  - repo: https://github.com/albertas/deadcode
86
- rev: v2.6.0
156
+ rev: v2.6.1
87
157
  hooks:
88
158
  - id: deadcode
89
159
  ```
@@ -115,6 +185,11 @@ run the same hook while developing deadcode itself, use `make pre-commit`.
115
185
  |`-V`, `--version` | - | Shows `deadcode` version. |
116
186
  |`-v`, `--verbose` | - | Shows verbose logging output. |
117
187
 
188
+ ### Glossary
189
+ - `name` - variable, function or class name.
190
+ - `body` - code block which follows after `:` in function or class definition. A body ignore suppresses every definition inside the expression, including nested classes and functions.
191
+ - `definition` - whole class or function definition expression including its name and body.
192
+
118
193
  ### Decorator matching
119
194
  Decorator names are written as in the source, including the leading `@`; a pattern without `@` matches nothing.
120
195
  Entries are glob patterns matched with `fnmatch.fnmatchcase`, so `*`, `?` and `[seq]` apply and `*` also crosses dots
@@ -151,12 +226,6 @@ The status is independent of `--quiet`, `--count`, `--only`, `--dry`, and `--fix
151
226
  A `--fix` run exits `1` on the run that found items and `0` on a subsequent run
152
227
  over the cleaned tree.
153
228
 
154
- ### Glossary
155
- - `name` - variable, function or class name.
156
- - `body` - code block which follows after `:` in function or class definition. A body ignore suppresses every definition inside the expression, including nested classes and functions.
157
- - `definition` - whole class or function definition expression including its name and body.
158
-
159
-
160
229
  ## Ignoring checks with noqa comments
161
230
  Inline `# noqa` comments can be used to ignore `deadcode` checks.
162
231
  E.g. unused `Foo` class wont be detected/fixed because `# noqa: DC03` comment is used:
@@ -197,6 +266,11 @@ A comment on the first decorator line is still accepted.
197
266
  `unittest` lifecycle methods (`setUp`, `tearDown`, `setUpClass`, `tearDownClass`, `asyncSetUp`, `asyncTearDown`, `setUpModule`, `tearDownModule`) declared in test files are ignored by default, alongside the existing pytest hooks.
198
267
  Declarations with those names outside test files are still reported as unused code.
199
268
 
269
+ ## Ignoring Django test lifecycle methods
270
+ Django test lifecycle methods (`setUpTestData`, the `django.test.TestCase` class method invoked by the framework, and the internal hooks `_pre_setup`, `_post_teardown`, `_fixture_setup`, `_fixture_teardown`) declared in test files are ignored by default.
271
+ Declarations with those names outside test files are still reported as unused code.
272
+ Django helpers that your test code invokes itself are not suppressed, so an unreferenced `assertContains`, `assertNumQueries`, `settings`, `modify_settings`, or `captureOnCommitCallbacks` is still reported.
273
+
200
274
  ## Contributing
201
275
 
202
276
  Create a feature branch from `main`, open a pull request targeting `main`, and
@@ -208,6 +282,16 @@ commits are made by the maintainer on `main`.
208
282
  Set `PYTEST_XDIST_WORKERS` to change the worker count (`make test PYTEST_XDIST_WORKERS=2`), or pass
209
283
  `PYTEST_ME_PLEASE="-n 0"` to run serially.
210
284
 
285
+ ## Security
286
+
287
+ The runtime dependencies in `requirements.txt` and the development dependencies in `requirements-dev.txt` are audited for known
288
+ vulnerabilities with `pip-audit`. The `Vulnerability audit` workflow in `.github/workflows/vulnerability-audit.yml` runs on every pull request,
289
+ on every push to `main`, and weekly on a schedule. Run the same audit locally with `make audit`, `make audit-runtime`, or
290
+ `make audit-development`.
291
+
292
+ Read the [security documentation](https://deadcode.readthedocs.io/en/latest/security.html) for how to read and fix a finding. The only accepted
293
+ exception is `filelock==4.1.0` on Python 3.10, where the fix requires Python 3.11 or newer.
294
+
211
295
  ## Rationale
212
296
  [ruff](https://pypi.org/project/ruff/) and
213
297
  [flake8](https://pypi.org/project/flake8/) - don't have rules for unused global
@@ -281,10 +365,18 @@ A checked entry is implemented on `main` and is included in the next release; pu
281
365
  - [ ] Investigate ways of extracting and backporting Python3.10+ `ast` implementation to lower Python versions.
282
366
 
283
367
  ## Release notes
368
+ - next:
369
+ - Features:
370
+ - Audit runtime and development dependencies for known vulnerabilities in CI on every pull request and weekly.
371
+ - Declare the package development status as Beta.
372
+ - Add a Django usage guide covering models, admin, views, forms, middleware, signals and more, plus Django `MIDDLEWARE` registration parsing and Django-aware qualified matching through absolute imports.
373
+ - Ignore Django test lifecycle methods in test files.
374
+ - Django `MIDDLEWARE` registrations are treated as used and absolute-import qualified expressions resolve to their modules.
375
+ - Support boolean flags such as `fix = true` and `dry = true` in `[tool.deadcode]` configuration.
284
376
  - **v2.6.0** `2026-11-08`:
285
377
  - Features:
286
378
  - Add support for Python 3.13, 3.14 and 3.15.
287
- - Documentation is now published on Read the Docs at https://deadcode.readthedocs.io/.
379
+ - Documentation is now published on Read the Docs at [deadcode.readthedocs.io](https://deadcode.readthedocs.io/).
288
380
  - Add `-V` as a short alias for `--version`.
289
381
  - `--ignore-definitions-if-decorated-with` now ignores whole decorated definitions
290
382
  - `--ignore-bodies-of` now suppresses the bodies of matching classes and functions
@@ -324,6 +416,7 @@ A checked entry is implemented on `main` and is included in the next release; pu
324
416
  - Rules now sit under Ignoring checks with noqa comments, the Glossary under Command line options, and Decorator matching is more concise.
325
417
  - Python 3.10+ requirement is now shown as a badge in the README
326
418
  - **v2.5.0** `2026-11-07`:
419
+ - Stop Codecov report comments on pull requests with no coverage findings
327
420
  - Bugfixes:
328
421
  - Fix a crash when a decorator has no `id` attribute.
329
422
  - **v2.4.1** `2024-08-09`:
@@ -1,36 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: deadcode
3
- Version: 2.6.0
4
- Summary: Find and remove dead code.
5
- Project-URL: homepage, https://github.com/albertas/deadcode
6
- Project-URL: documentation, https://deadcode.readthedocs.io/
7
- Author-email: Albertas Gimbutas <albertasgim@gmail.com>
8
- License-File: LICENSE
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: License :: OSI Approved :: GNU Affero General Public License v3
11
- Classifier: Natural Language :: English
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Programming Language :: Python :: 3.14
19
- Classifier: Programming Language :: Python :: 3.15
20
- Requires-Python: >=3.10
21
- Requires-Dist: tomli>=2.0.1; python_version < '3.11'
22
- Provides-Extra: test
23
- Requires-Dist: diff-cover==10.6.0; extra == 'test'
24
- Requires-Dist: hatch; extra == 'test'
25
- Requires-Dist: mypy==1.10.1; extra == 'test'
26
- Requires-Dist: pip-audit; extra == 'test'
27
- Requires-Dist: pre-commit; extra == 'test'
28
- Requires-Dist: pytest-cov==5.0.0; extra == 'test'
29
- Requires-Dist: pytest-xdist==3.8.0; extra == 'test'
30
- Requires-Dist: pytest==9.0.3; extra == 'test'
31
- Requires-Dist: ruff==0.5.1; extra == 'test'
32
- Description-Content-Type: text/markdown
33
-
34
1
  [![Deadcode Logo](https://raw.githubusercontent.com/albertas/deadcode/main/docs/_static/deadcode-logo-readme.png)](https://deadcode.readthedocs.io/)
35
2
 
36
3
  <h2 align="center">Find and Fix Unused Python Code</h2>
@@ -108,6 +75,43 @@ ignore-bodies-if-inherits-from = ["BaseModel"]
108
75
  Read the [FastAPI guide](https://deadcode.readthedocs.io/en/latest/fastapi.html) for common use cases, the complete example application,
109
76
  the dependency injection strategy, and the expected output with no wrong detections.
110
77
 
78
+ ## Using deadcode on a Django project
79
+
80
+ Django registers models, views, admin classes, middleware, and signal handlers through base classes, decorators, settings strings, and URLconfs, so
81
+ framework entry points look unused to a static analyser. Configure deadcode like this:
82
+
83
+ ```toml
84
+ [tool.deadcode]
85
+ ignore-definitions-if-decorated-with = [
86
+ "@django.contrib.admin.*",
87
+ "@django.contrib.auth.*",
88
+ "@django.dispatch.receiver",
89
+ "@django.utils.decorators.method_decorator",
90
+ "@django.views.decorators.http.require_*",
91
+ "@*register.*",
92
+ ]
93
+ ignore-definitions-if-inherits-from = [
94
+ "django.apps.AppConfig",
95
+ "django.contrib.admin.*",
96
+ "django.core.management.base.BaseCommand",
97
+ "django.db.migrations.Migration",
98
+ "django.db.models.Model",
99
+ "django.forms.*Form",
100
+ "django.utils.deprecation.MiddlewareMixin",
101
+ "django.views.*View",
102
+ ]
103
+ ignore-names-in-files = ["*/settings.py", "*/urls.py"]
104
+ ```
105
+
106
+ The example targets Django 5.2 LTS (5.2.19). The Django REST Framework 3.18.3 entries are additions to the same two keys; see the
107
+ [Django REST Framework section of the Django guide](https://deadcode.readthedocs.io/en/latest/django.html#django-rest-framework). The three keys replace the
108
+ older overlapping name, body, and definition options: one inheritance entry hides a framework subclass name and body, and one decorator entry hides a
109
+ decorated definition name and body. deadcode parses `MIDDLEWARE` registrations and resolves absolute-import qualified names, so a registered middleware
110
+ name and a `models.Model` base need no separate name pattern.
111
+
112
+ Read the [Django guide](https://deadcode.readthedocs.io/en/latest/django.html) for the framework areas, the current and legacy
113
+ middleware forms, both import forms, database signals, the Django REST Framework entries, and the expected behaviour.
114
+
111
115
  ## Pre-commit hook
112
116
  Create a `.pre-commit-config.yaml` file in the root of your project directory, if it doesn't exist, and add the following to the file:
113
117
 
@@ -116,7 +120,7 @@ Create a `.pre-commit-config.yaml` file in the root of your project directory, i
116
120
  # See https://pre-commit.com/hooks.html for more hooks
117
121
  repos:
118
122
  - repo: https://github.com/albertas/deadcode
119
- rev: v2.6.0
123
+ rev: v2.6.1
120
124
  hooks:
121
125
  - id: deadcode
122
126
  ```
@@ -148,6 +152,11 @@ run the same hook while developing deadcode itself, use `make pre-commit`.
148
152
  |`-V`, `--version` | - | Shows `deadcode` version. |
149
153
  |`-v`, `--verbose` | - | Shows verbose logging output. |
150
154
 
155
+ ### Glossary
156
+ - `name` - variable, function or class name.
157
+ - `body` - code block which follows after `:` in function or class definition. A body ignore suppresses every definition inside the expression, including nested classes and functions.
158
+ - `definition` - whole class or function definition expression including its name and body.
159
+
151
160
  ### Decorator matching
152
161
  Decorator names are written as in the source, including the leading `@`; a pattern without `@` matches nothing.
153
162
  Entries are glob patterns matched with `fnmatch.fnmatchcase`, so `*`, `?` and `[seq]` apply and `*` also crosses dots
@@ -184,12 +193,6 @@ The status is independent of `--quiet`, `--count`, `--only`, `--dry`, and `--fix
184
193
  A `--fix` run exits `1` on the run that found items and `0` on a subsequent run
185
194
  over the cleaned tree.
186
195
 
187
- ### Glossary
188
- - `name` - variable, function or class name.
189
- - `body` - code block which follows after `:` in function or class definition. A body ignore suppresses every definition inside the expression, including nested classes and functions.
190
- - `definition` - whole class or function definition expression including its name and body.
191
-
192
-
193
196
  ## Ignoring checks with noqa comments
194
197
  Inline `# noqa` comments can be used to ignore `deadcode` checks.
195
198
  E.g. unused `Foo` class wont be detected/fixed because `# noqa: DC03` comment is used:
@@ -230,6 +233,11 @@ A comment on the first decorator line is still accepted.
230
233
  `unittest` lifecycle methods (`setUp`, `tearDown`, `setUpClass`, `tearDownClass`, `asyncSetUp`, `asyncTearDown`, `setUpModule`, `tearDownModule`) declared in test files are ignored by default, alongside the existing pytest hooks.
231
234
  Declarations with those names outside test files are still reported as unused code.
232
235
 
236
+ ## Ignoring Django test lifecycle methods
237
+ Django test lifecycle methods (`setUpTestData`, the `django.test.TestCase` class method invoked by the framework, and the internal hooks `_pre_setup`, `_post_teardown`, `_fixture_setup`, `_fixture_teardown`) declared in test files are ignored by default.
238
+ Declarations with those names outside test files are still reported as unused code.
239
+ Django helpers that your test code invokes itself are not suppressed, so an unreferenced `assertContains`, `assertNumQueries`, `settings`, `modify_settings`, or `captureOnCommitCallbacks` is still reported.
240
+
233
241
  ## Contributing
234
242
 
235
243
  Create a feature branch from `main`, open a pull request targeting `main`, and
@@ -241,6 +249,16 @@ commits are made by the maintainer on `main`.
241
249
  Set `PYTEST_XDIST_WORKERS` to change the worker count (`make test PYTEST_XDIST_WORKERS=2`), or pass
242
250
  `PYTEST_ME_PLEASE="-n 0"` to run serially.
243
251
 
252
+ ## Security
253
+
254
+ The runtime dependencies in `requirements.txt` and the development dependencies in `requirements-dev.txt` are audited for known
255
+ vulnerabilities with `pip-audit`. The `Vulnerability audit` workflow in `.github/workflows/vulnerability-audit.yml` runs on every pull request,
256
+ on every push to `main`, and weekly on a schedule. Run the same audit locally with `make audit`, `make audit-runtime`, or
257
+ `make audit-development`.
258
+
259
+ Read the [security documentation](https://deadcode.readthedocs.io/en/latest/security.html) for how to read and fix a finding. The only accepted
260
+ exception is `filelock==4.1.0` on Python 3.10, where the fix requires Python 3.11 or newer.
261
+
244
262
  ## Rationale
245
263
  [ruff](https://pypi.org/project/ruff/) and
246
264
  [flake8](https://pypi.org/project/flake8/) - don't have rules for unused global
@@ -314,10 +332,18 @@ A checked entry is implemented on `main` and is included in the next release; pu
314
332
  - [ ] Investigate ways of extracting and backporting Python3.10+ `ast` implementation to lower Python versions.
315
333
 
316
334
  ## Release notes
335
+ - next:
336
+ - Features:
337
+ - Audit runtime and development dependencies for known vulnerabilities in CI on every pull request and weekly.
338
+ - Declare the package development status as Beta.
339
+ - Add a Django usage guide covering models, admin, views, forms, middleware, signals and more, plus Django `MIDDLEWARE` registration parsing and Django-aware qualified matching through absolute imports.
340
+ - Ignore Django test lifecycle methods in test files.
341
+ - Django `MIDDLEWARE` registrations are treated as used and absolute-import qualified expressions resolve to their modules.
342
+ - Support boolean flags such as `fix = true` and `dry = true` in `[tool.deadcode]` configuration.
317
343
  - **v2.6.0** `2026-11-08`:
318
344
  - Features:
319
345
  - Add support for Python 3.13, 3.14 and 3.15.
320
- - Documentation is now published on Read the Docs at https://deadcode.readthedocs.io/.
346
+ - Documentation is now published on Read the Docs at [deadcode.readthedocs.io](https://deadcode.readthedocs.io/).
321
347
  - Add `-V` as a short alias for `--version`.
322
348
  - `--ignore-definitions-if-decorated-with` now ignores whole decorated definitions
323
349
  - `--ignore-bodies-of` now suppresses the bodies of matching classes and functions
@@ -357,6 +383,7 @@ A checked entry is implemented on `main` and is included in the next release; pu
357
383
  - Rules now sit under Ignoring checks with noqa comments, the Glossary under Command line options, and Decorator matching is more concise.
358
384
  - Python 3.10+ requirement is now shown as a badge in the README
359
385
  - **v2.5.0** `2026-11-07`:
386
+ - Stop Codecov report comments on pull requests with no coverage findings
360
387
  - Bugfixes:
361
388
  - Fix a crash when a decorator has no `id` attribute.
362
389
  - **v2.4.1** `2024-08-09`:
@@ -26,14 +26,14 @@ def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args)
26
26
  for item in unused_names:
27
27
  message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
28
28
  message += item.message or (
29
- f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
29
+ f'{item.type_.replace("_", " ").capitalize()} `\033[1m{item.name}\033[0m` is never used'
30
30
  )
31
31
  if args.no_color:
32
32
  message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
33
33
  messages.append(message)
34
34
 
35
35
  if args.fix:
36
- message = f"\nRemoved \033[1m{len(messages)}\033[0m unused code item{'s' if len(messages) > 1 else ''}!"
36
+ message = f'\nRemoved \033[1m{len(messages)}\033[0m unused code item{"s" if len(messages) > 1 else ""}!'
37
37
  if args.no_color:
38
38
  message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
39
39
  messages.append(message)
@@ -15,8 +15,8 @@ def does_include(bigger_part: Part, smaller_part: Part) -> bool:
15
15
  def sort_parts(bigger_part: Part, smaller_part: Part) -> tuple[Part, Part]:
16
16
  """Returns code part which begins first following by another code part."""
17
17
  # TODO: Column should go first (tuple comparison would be possible)
18
- line_start_b, line_end_b, col_start_b, col_end_b = bigger_part
19
- line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
18
+ line_start_b, _line_end_b, col_start_b, _col_end_b = bigger_part
19
+ line_start_s, _line_end_s, col_start_s, _col_end_s = smaller_part
20
20
 
21
21
  # Pirmo pradžios eilutė ir stulpelis yra pirmiau.
22
22
  if (line_start_s < line_start_b) or ((line_start_s == line_start_b) and (col_start_s < col_start_b)):
@@ -27,8 +27,8 @@ def sort_parts(bigger_part: Part, smaller_part: Part) -> tuple[Part, Part]:
27
27
  def does_overlap(bigger_part: Part, smaller_part: Part) -> bool:
28
28
  bigger_part, smaller_part = sort_parts(bigger_part, smaller_part)
29
29
 
30
- line_start_b, line_end_b, col_start_b, col_end_b = bigger_part
31
- line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
30
+ _line_start_b, line_end_b, _col_start_b, col_end_b = bigger_part
31
+ line_start_s, _line_end_s, col_start_s, _col_end_s = smaller_part
32
32
 
33
33
  # ar pirmo pabaiga perlipa antro pradžia
34
34
  return bool((line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s)))
@@ -11,4 +11,4 @@ def parse_abstract_syntax_tree(file_content: FileContent, args: Args, filename:
11
11
  except: # noqa: E722
12
12
  if not args.count and not args.quiet:
13
13
  print_safely(f'Error: Failed to parse {filename} file, ignoring it.')
14
- return ast.Module()
14
+ return ast.Module(body=[], type_ignores=[])
@@ -101,8 +101,7 @@ def parse_arguments(args: list[str] | None) -> Args:
101
101
  parser.add_argument(
102
102
  '--ignore-definitions',
103
103
  help=(
104
- 'Ignores definition (including name and body) if a '
105
- 'name of an expression matches any of the provided ones.'
104
+ 'Ignores definition (including name and body) if a name of an expression matches any of the provided ones.'
106
105
  ),
107
106
  nargs='*',
108
107
  action='append',
@@ -226,9 +225,12 @@ def parse_arguments(args: list[str] | None) -> Args:
226
225
  for key, item in parse_pyproject_toml().items():
227
226
  if key not in parsed_args:
228
227
  continue
229
- if isinstance(item, str) and isinstance(parsed_args[key], list):
230
- item = [item]
231
- parsed_args[key].extend(item)
228
+ if isinstance(parsed_args[key], list):
229
+ if isinstance(item, str):
230
+ item = [item]
231
+ parsed_args[key].extend(item)
232
+ elif item:
233
+ parsed_args[key] = item
232
234
 
233
235
  # Do not fix if dry option is provided:
234
236
  if parsed_args['dry']:
@@ -10,7 +10,7 @@ from logging import getLogger
10
10
 
11
11
  from pathlib import Path
12
12
 
13
- from typing import TextIO
13
+ from typing import Any, TextIO, cast
14
14
  from collections.abc import Callable, Iterable
15
15
  from deadcode.constants import UnusedCodeType
16
16
  from deadcode.data_types import Args, Part
@@ -76,6 +76,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
76
76
  self.bare_used_names: dict[str, set[str]] = {}
77
77
  self.wildcard_imports: dict[str, set[str]] = {}
78
78
  self.explicit_imports: dict[str, set[str]] = {}
79
+ self.qualified_imports: dict[str, dict[str, str]] = {}
79
80
 
80
81
  self.filename = Path()
81
82
  self.code: list[str] = []
@@ -149,6 +150,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
149
150
  filename = os.path.basename(file_path)
150
151
  module_name = os.path.splitext(filename)[0]
151
152
  self.module_name = module_name
153
+ self.qualified_imports[self.module_name] = {}
152
154
  self.scope_parts = [module_name]
153
155
 
154
156
  file_content = f.read()
@@ -242,6 +244,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
242
244
  # We can't easily detect when "os.path" is used.
243
245
  name = name_and_alias.name.partition('.')[0]
244
246
  alias = name_and_alias.asname
247
+ self._record_qualified_import(node, name_and_alias)
245
248
  self._define(
246
249
  self.defined_imports,
247
250
  alias or name,
@@ -251,6 +254,32 @@ class DeadCodeVisitor(ast.NodeVisitor):
251
254
  if alias is not None:
252
255
  self.add_used_name(name_and_alias.name)
253
256
 
257
+ def _record_qualified_import(self, node: ast.Import | ast.ImportFrom, name_and_alias: ast.alias) -> None:
258
+ """Record absolute imports so qualified names can be resolved."""
259
+ qualified_imports = self.qualified_imports.setdefault(self.module_name, {})
260
+ if isinstance(node, ast.Import):
261
+ if name_and_alias.asname:
262
+ qualified_imports[name_and_alias.asname] = name_and_alias.name
263
+ else:
264
+ top_level = name_and_alias.name.partition('.')[0]
265
+ qualified_imports[top_level] = top_level
266
+ return
267
+
268
+ if node.level > 0 or node.module is None or node.module == '__future__':
269
+ return
270
+ if name_and_alias.name == '*':
271
+ return
272
+ local_name = name_and_alias.asname or name_and_alias.name
273
+ qualified_imports[local_name] = f'{node.module}.{name_and_alias.name}'
274
+
275
+ def _resolve_qualified_expression(self, expression: str) -> str | None:
276
+ """Resolve the first component of a dotted expression through absolute imports."""
277
+ first_component, separator, remaining = expression.partition('.')
278
+ target = self.qualified_imports.get(self.module_name, {}).get(first_component)
279
+ if target is None:
280
+ return None
281
+ return f'{target}{separator}{remaining}' if separator else target
282
+
254
283
  def _collect_commented_out_code(self, file_content: bytes) -> None:
255
284
  """Collect comments whose text parses as Python code."""
256
285
  try:
@@ -363,7 +392,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
363
392
 
364
393
  def _get_else_linenos(self, node: ast.If | ast.IfExp | ast.While, first_else_node: ast.AST) -> tuple[int, ...]:
365
394
  """Return line numbers of the `else` keyword preceding the first else statement."""
366
- for lineno in range(first_else_node.lineno - 1, node.lineno, -1):
395
+ for lineno in range(int(cast('Any', first_else_node).lineno) - 1, int(cast('Any', node).lineno), -1):
367
396
  if 0 < lineno <= len(self.code) and self.code[lineno - 1].strip().startswith('else:'):
368
397
  return (lineno,)
369
398
  return ()
@@ -388,8 +417,10 @@ class DeadCodeVisitor(ast.NodeVisitor):
388
417
  or _match(self.filename, self.args.ignore_names_in_files)
389
418
  or _match_many(decorator_names, self.args.ignore_names_if_decorated_with)
390
419
  or _match_many(decorator_names, IGNORED_INTERFACE_DECORATOR_NAMES)
391
- or (item_inherits_from := getattr(first_node, 'inherits_from', None))
392
- and _match_many(item_inherits_from, self.args.ignore_names_if_inherits_from)
420
+ or (
421
+ (item_inherits_from := getattr(first_node, 'inherits_from', None))
422
+ and _match_many(item_inherits_from, self.args.ignore_names_if_inherits_from)
423
+ )
393
424
  or self.should_ignore_new_definitions
394
425
  or any(noqa.ignore_line(self.noqa_lines, lineno, ERROR_CODES[type_]) for lineno in linenos)
395
426
  )
@@ -399,19 +430,19 @@ class DeadCodeVisitor(ast.NodeVisitor):
399
430
 
400
431
  first_lineno = lines.get_first_line_number(first_node)
401
432
  last_lineno = lines.get_last_line_number(last_node)
402
- name_line = last_node.lineno
433
+ name_line = int(cast('Any', last_node).lineno)
403
434
 
404
435
  removal_node_end_lineno = getattr(removal_node, 'end_lineno', None)
405
436
  if removal_node is not None and removal_node_end_lineno is not None:
406
437
  line_start = lines.get_first_line_number(removal_node)
407
438
  line_end = removal_node_end_lineno
408
- col_start = removal_node.col_offset
409
- col_end = removal_node.end_col_offset or 0
439
+ col_start = int(cast('Any', removal_node).col_offset)
440
+ col_end = int(cast('Any', removal_node).end_col_offset or 0)
410
441
  else:
411
442
  line_start = first_lineno
412
443
  line_end = last_lineno
413
- col_start = last_node.col_offset
414
- col_end = last_node.end_col_offset or 0
444
+ col_start = int(cast('Any', last_node).col_offset)
445
+ col_end = int(cast('Any', last_node).end_col_offset or 0)
415
446
 
416
447
  inherits_from = getattr(first_node, 'inherits_from', None)
417
448
  code_item = CodeItem(
@@ -421,7 +452,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
421
452
  code_parts=[Part(line_start, line_end, col_start, col_end)],
422
453
  scope=self.scope,
423
454
  name_line=name_line, # TODO: Maybe this should be a property?
424
- name_column=last_node.col_offset, # TODO: Maybe this should be a property?
455
+ name_column=int(cast('Any', last_node).col_offset), # TODO: Maybe this should be a property?
425
456
  message=message,
426
457
  inherits_from=inherits_from,
427
458
  )
@@ -541,8 +572,15 @@ class DeadCodeVisitor(ast.NodeVisitor):
541
572
  )
542
573
 
543
574
  def _get_decorator_names(self, node: ast.AST) -> list[str]:
544
- """Returns decorator names of the given node."""
545
- return [utils.get_decorator_name(decorator) for decorator in getattr(node, 'decorator_list', ())]
575
+ """Returns decorator names of the given node, including resolved absolute imports."""
576
+ decorator_names = []
577
+ for decorator in getattr(node, 'decorator_list', ()):
578
+ name = utils.get_decorator_name(decorator)
579
+ decorator_names.append(name)
580
+ resolved = self._resolve_qualified_expression(name[1:])
581
+ if resolved is not None and '@' + resolved != name:
582
+ decorator_names.append('@' + resolved)
583
+ return decorator_names
546
584
 
547
585
  def get_inherits_from(self, node: ast.AST) -> list[str] | None:
548
586
  """Returns a set of base classes from any level in inheritance tree."""
@@ -550,7 +588,14 @@ class DeadCodeVisitor(ast.NodeVisitor):
550
588
  if not (bases_attr := getattr(node, 'bases', None)):
551
589
  return None
552
590
 
553
- bases = [name for base in bases_attr for name in utils.get_base_class_names(base)]
591
+ bases = []
592
+ for base in bases_attr:
593
+ bases.extend(utils.get_base_class_names(base))
594
+ if isinstance(base, (ast.Name, ast.Attribute)):
595
+ source = ast.unparse(base)
596
+ resolved = self._resolve_qualified_expression(source)
597
+ if resolved is not None and resolved != source:
598
+ bases.append(resolved)
554
599
  inherits_from = bases[:]
555
600
 
556
601
  for base in bases:
@@ -561,7 +606,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
561
606
  return list(dict.fromkeys(inherits_from))
562
607
 
563
608
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
564
- decorator_names = [utils.get_decorator_name(decorator) for decorator in node.decorator_list]
609
+ decorator_names = self._get_decorator_names(node)
565
610
  for decorator in node.decorator_list:
566
611
  if _match(utils.get_decorator_name(decorator), self.ignore_decorators):
567
612
  self._log(f'Ignoring class "{node.name}" (decorator whitelisted)')
@@ -577,7 +622,7 @@ class DeadCodeVisitor(ast.NodeVisitor):
577
622
 
578
623
  def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
579
624
  self._record_annotation_names(node.returns)
580
- decorator_names = [utils.get_decorator_name(decorator) for decorator in node.decorator_list]
625
+ decorator_names = self._get_decorator_names(node)
581
626
 
582
627
  first_arg = node.args.args[0].arg if node.args.args else None
583
628
 
@@ -631,6 +676,19 @@ class DeadCodeVisitor(ast.NodeVisitor):
631
676
  if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
632
677
  self.add_used_name(elt.value)
633
678
 
679
+ if (
680
+ len(node.targets) == 1
681
+ and isinstance(node.targets[0], ast.Name)
682
+ and node.targets[0].id == 'MIDDLEWARE'
683
+ and len(self.scope_parts) == 1
684
+ and isinstance(node.value, (ast.List, ast.Tuple))
685
+ ):
686
+ for elt in node.value.elts:
687
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
688
+ final_component = elt.value.rpartition('.')[2]
689
+ if final_component:
690
+ self.add_used_name(final_component)
691
+
634
692
  def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
635
693
  self._record_annotation_names(node.annotation)
636
694
 
@@ -31,6 +31,13 @@ UNITTEST_FUNCTION_NAMES = {
31
31
  'setUpModule',
32
32
  'tearDownModule',
33
33
  }
34
+ DJANGO_METHOD_NAMES = {
35
+ 'setUpTestData',
36
+ '_pre_setup',
37
+ '_post_teardown',
38
+ '_fixture_setup',
39
+ '_fixture_teardown',
40
+ }
34
41
  # Built-in interface markers: abstract and overriding definitions are never reported, no option needed.
35
42
  IGNORED_INTERFACE_DECORATOR_NAMES: set[str] = {
36
43
  '@abstractmethod',
@@ -111,17 +118,24 @@ def _ignore_import(filename: Path, import_name: str) -> bool:
111
118
  def _ignore_function(filename: Path, function_name: str) -> bool:
112
119
  # Class-level lifecycle methods such as `def setUpClass(cls)` have no decorator and a first argument other
113
120
  # than `self`, so `visit_FunctionDef` classifies them as functions and they must be suppressed here as well.
121
+ # Django lifecycle methods are declared the same way.
114
122
  return (
115
123
  function_name in PYTEST_FUNCTION_NAMES
116
124
  or function_name in UNITTEST_METHOD_NAMES
117
125
  or function_name in UNITTEST_FUNCTION_NAMES
126
+ or function_name in DJANGO_METHOD_NAMES
118
127
  or function_name.startswith('test_')
119
128
  ) and _is_test_file(filename)
120
129
 
121
130
 
122
131
  def _ignore_method(filename: Path, method_name: str) -> bool:
123
132
  return _is_special_name(method_name) or (
124
- (method_name in PYTEST_METHOD_NAMES or method_name in UNITTEST_METHOD_NAMES or method_name.startswith('test_'))
133
+ (
134
+ method_name in PYTEST_METHOD_NAMES
135
+ or method_name in UNITTEST_METHOD_NAMES
136
+ or method_name in DJANGO_METHOD_NAMES
137
+ or method_name.startswith('test_')
138
+ )
125
139
  and _is_test_file(filename)
126
140
  )
127
141
 
@@ -1,5 +1,7 @@
1
1
  import ast
2
2
 
3
+ from typing import Any, cast
4
+
3
5
 
4
6
  def _get_last_child_with_lineno(node: ast.AST) -> ast.AST | None:
5
7
  """
@@ -52,14 +54,14 @@ def get_last_line_number(node: ast.AST) -> int:
52
54
  may lead to a path that ends with a node without line number.
53
55
 
54
56
  """
55
- max_lineno = node.lineno
57
+ max_lineno = int(cast('Any', node).lineno)
56
58
  while True:
57
59
  last_child = _get_last_child_with_lineno(node)
58
60
  if last_child is None:
59
61
  return max_lineno
60
62
  else:
61
63
  try:
62
- max_lineno = max(max_lineno, last_child.lineno)
64
+ max_lineno = max(max_lineno, int(cast('Any', last_child).lineno))
63
65
  except AttributeError:
64
66
  pass
65
67
  node = last_child
@@ -78,4 +80,4 @@ def get_first_line_number(node: ast.AST) -> int:
78
80
  if decorators := getattr(node, 'decorator_list', []):
79
81
  lineno: int = decorators[0].lineno
80
82
  return lineno
81
- return node.lineno
83
+ return int(cast('Any', node).lineno)
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "deadcode"
3
- version = "2.6.0"
3
+ version = "2.6.1"
4
4
  authors = [
5
5
  {name = "Albertas Gimbutas", email = "albertasgim@gmail.com"},
6
6
  ]
7
- description = "Find and remove dead code."
7
+ description = "Find and fix unused Python code using command line."
8
8
  readme = "README.md"
9
9
  requires-python = ">= 3.10"
10
10
  classifiers = [
11
- "Development Status :: 3 - Alpha",
11
+ "Development Status :: 4 - Beta",
12
12
  "License :: OSI Approved :: GNU Affero General Public License v3",
13
13
  "Natural Language :: English",
14
14
  "Operating System :: OS Independent",
@@ -27,15 +27,15 @@ dependencies = [
27
27
  [project.optional-dependencies]
28
28
  test = [
29
29
  "hatch",
30
- "ruff==0.5.1",
31
- "mypy==1.10.1",
30
+ "ruff",
31
+ "mypy",
32
32
  "pip-audit",
33
33
  "deadcode",
34
- "diff-cover==10.6.0",
34
+ "diff-cover",
35
35
  "pre-commit",
36
- "pytest==9.0.3",
37
- "pytest-cov==5.0.0",
38
- "pytest-xdist==3.8.0",
36
+ "pytest",
37
+ "pytest-cov",
38
+ "pytest-xdist",
39
39
  ]
40
40
 
41
41
  [project.scripts]
@@ -75,7 +75,6 @@ ignore = [
75
75
  "D213", # multi line summary second line
76
76
  "COM812", # missing trailing comma
77
77
  "ISC001", # single line implicit string concatenation
78
- "ANN101", # deprecated requirement to annotate self
79
78
  "A001",
80
79
  "ANN001",
81
80
  "ANN002",
@@ -149,6 +148,12 @@ ignore = [
149
148
  "W293",
150
149
  "W605",
151
150
  "UP",
151
+ "CPY001", # no copyright notices in source files
152
+ "FAST001", # FastAPI rules do not apply to the test fixtures
153
+ "FAST002",
154
+ "PLR0917", # consistent with PLR0913
155
+ "RUF102", # DC noqa codes are intentionally not ruff codes
156
+ "SIM905", # consistent with ISC001
152
157
  ]
153
158
 
154
159
  [tool.ruff.lint.per-file-ignores]
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -11,17 +11,17 @@ class CodeItem: # TODO: This should also be a dataclass, because hash and tuple
11
11
  """
12
12
 
13
13
  __slots__ = (
14
- 'name',
15
- 'type_',
16
- 'filename',
17
14
  'code_parts',
18
- 'scope',
15
+ 'error_code',
16
+ 'filename',
19
17
  'inherits_from',
20
- 'name_line',
21
- 'name_column',
22
18
  'message',
23
- 'error_code',
19
+ 'name',
20
+ 'name_column',
21
+ 'name_line',
24
22
  'number_of_uses',
23
+ 'scope',
24
+ 'type_',
25
25
  )
26
26
 
27
27
  def __init__(
File without changes