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.
- deadcode-2.6.0/README.md → deadcode-2.6.1/PKG-INFO +101 -8
- deadcode-2.6.0/PKG-INFO → deadcode-2.6.1/README.md +68 -41
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/get_unused_names_error_message.py +2 -2
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/merge_overlaping_file_parts.py +4 -4
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/parse_abstract_syntax_tree.py +1 -1
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/parse_arguments.py +7 -5
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/dead_code_visitor.py +73 -15
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/ignore.py +15 -1
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/lines.py +5 -3
- {deadcode-2.6.0 → deadcode-2.6.1}/pyproject.toml +15 -10
- {deadcode-2.6.0 → deadcode-2.6.1}/.gitignore +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/.venv310/bin/activate_this.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/.venv311/bin/activate_this.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/.venv314/bin/activate_this.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/.venv315/bin/activate_this.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/LICENSE +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/__init__.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/__init__.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/find_python_filenames.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/find_unused_names.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/fix_or_show_unused_code.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/get_unused_import_parts.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/actions/remove_file_parts_from_content.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/cli.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/constants.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/data_types.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/__init__.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/add_colors_to_diff.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/fix_indent.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/flatten_lists.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/nested_scopes.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/print_ast.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/utils/print_safely.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/__init__.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/code_item.py +7 -7
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/module_exports.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/noqa.py +0 -0
- {deadcode-2.6.0 → deadcode-2.6.1}/deadcode/visitor/utils.py +0 -0
- {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
|
[](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.
|
|
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
|
[](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.
|
|
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
|
|
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
|
|
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,
|
|
19
|
-
line_start_s,
|
|
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
|
-
|
|
31
|
-
line_start_s,
|
|
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(
|
|
230
|
-
item
|
|
231
|
-
|
|
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 (
|
|
392
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
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
|
-
(
|
|
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.
|
|
3
|
+
version = "2.6.1"
|
|
4
4
|
authors = [
|
|
5
5
|
{name = "Albertas Gimbutas", email = "albertasgim@gmail.com"},
|
|
6
6
|
]
|
|
7
|
-
description = "Find and
|
|
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 ::
|
|
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
|
|
31
|
-
"mypy
|
|
30
|
+
"ruff",
|
|
31
|
+
"mypy",
|
|
32
32
|
"pip-audit",
|
|
33
33
|
"deadcode",
|
|
34
|
-
"diff-cover
|
|
34
|
+
"diff-cover",
|
|
35
35
|
"pre-commit",
|
|
36
|
-
"pytest
|
|
37
|
-
"pytest-cov
|
|
38
|
-
"pytest-xdist
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
-
'
|
|
15
|
+
'error_code',
|
|
16
|
+
'filename',
|
|
19
17
|
'inherits_from',
|
|
20
|
-
'name_line',
|
|
21
|
-
'name_column',
|
|
22
18
|
'message',
|
|
23
|
-
'
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|