deadcode 2.6.0__py3-none-any.whl → 2.6.1__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.
@@ -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']:
@@ -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__(
@@ -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
 
deadcode/visitor/lines.py CHANGED
@@ -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,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: deadcode
3
- Version: 2.6.0
4
- Summary: Find and remove dead code.
3
+ Version: 2.6.1
4
+ Summary: Find and fix unused Python code using command line.
5
5
  Project-URL: homepage, https://github.com/albertas/deadcode
6
6
  Project-URL: documentation, https://deadcode.readthedocs.io/
7
7
  Author-email: Albertas Gimbutas <albertasgim@gmail.com>
8
8
  License-File: LICENSE
9
- Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Operating System :: OS Independent
@@ -20,15 +20,15 @@ Classifier: Programming Language :: Python :: 3.15
20
20
  Requires-Python: >=3.10
21
21
  Requires-Dist: tomli>=2.0.1; python_version < '3.11'
22
22
  Provides-Extra: test
23
- Requires-Dist: diff-cover==10.6.0; extra == 'test'
23
+ Requires-Dist: diff-cover; extra == 'test'
24
24
  Requires-Dist: hatch; extra == 'test'
25
- Requires-Dist: mypy==1.10.1; extra == 'test'
25
+ Requires-Dist: mypy; extra == 'test'
26
26
  Requires-Dist: pip-audit; extra == 'test'
27
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'
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
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  [![Deadcode Logo](https://raw.githubusercontent.com/albertas/deadcode/main/docs/_static/deadcode-logo-readme.png)](https://deadcode.readthedocs.io/)
@@ -108,6 +108,43 @@ ignore-bodies-if-inherits-from = ["BaseModel"]
108
108
  Read the [FastAPI guide](https://deadcode.readthedocs.io/en/latest/fastapi.html) for common use cases, the complete example application,
109
109
  the dependency injection strategy, and the expected output with no wrong detections.
110
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
+
111
148
  ## Pre-commit hook
112
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:
113
150
 
@@ -116,7 +153,7 @@ Create a `.pre-commit-config.yaml` file in the root of your project directory, i
116
153
  # See https://pre-commit.com/hooks.html for more hooks
117
154
  repos:
118
155
  - repo: https://github.com/albertas/deadcode
119
- rev: v2.6.0
156
+ rev: v2.6.1
120
157
  hooks:
121
158
  - id: deadcode
122
159
  ```
@@ -148,6 +185,11 @@ run the same hook while developing deadcode itself, use `make pre-commit`.
148
185
  |`-V`, `--version` | - | Shows `deadcode` version. |
149
186
  |`-v`, `--verbose` | - | Shows verbose logging output. |
150
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
+
151
193
  ### Decorator matching
152
194
  Decorator names are written as in the source, including the leading `@`; a pattern without `@` matches nothing.
153
195
  Entries are glob patterns matched with `fnmatch.fnmatchcase`, so `*`, `?` and `[seq]` apply and `*` also crosses dots
@@ -184,12 +226,6 @@ The status is independent of `--quiet`, `--count`, `--only`, `--dry`, and `--fix
184
226
  A `--fix` run exits `1` on the run that found items and `0` on a subsequent run
185
227
  over the cleaned tree.
186
228
 
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
229
  ## Ignoring checks with noqa comments
194
230
  Inline `# noqa` comments can be used to ignore `deadcode` checks.
195
231
  E.g. unused `Foo` class wont be detected/fixed because `# noqa: DC03` comment is used:
@@ -230,6 +266,11 @@ A comment on the first decorator line is still accepted.
230
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.
231
267
  Declarations with those names outside test files are still reported as unused code.
232
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
+
233
274
  ## Contributing
234
275
 
235
276
  Create a feature branch from `main`, open a pull request targeting `main`, and
@@ -241,6 +282,16 @@ commits are made by the maintainer on `main`.
241
282
  Set `PYTEST_XDIST_WORKERS` to change the worker count (`make test PYTEST_XDIST_WORKERS=2`), or pass
242
283
  `PYTEST_ME_PLEASE="-n 0"` to run serially.
243
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
+
244
295
  ## Rationale
245
296
  [ruff](https://pypi.org/project/ruff/) and
246
297
  [flake8](https://pypi.org/project/flake8/) - don't have rules for unused global
@@ -314,10 +365,18 @@ A checked entry is implemented on `main` and is included in the next release; pu
314
365
  - [ ] Investigate ways of extracting and backporting Python3.10+ `ast` implementation to lower Python versions.
315
366
 
316
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.
317
376
  - **v2.6.0** `2026-11-08`:
318
377
  - Features:
319
378
  - 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/.
379
+ - Documentation is now published on Read the Docs at [deadcode.readthedocs.io](https://deadcode.readthedocs.io/).
321
380
  - Add `-V` as a short alias for `--version`.
322
381
  - `--ignore-definitions-if-decorated-with` now ignores whole decorated definitions
323
382
  - `--ignore-bodies-of` now suppresses the bodies of matching classes and functions
@@ -357,6 +416,7 @@ A checked entry is implemented on `main` and is included in the next release; pu
357
416
  - Rules now sit under Ignoring checks with noqa comments, the Glossary under Command line options, and Decorator matching is more concise.
358
417
  - Python 3.10+ requirement is now shown as a badge in the README
359
418
  - **v2.5.0** `2026-11-07`:
419
+ - Stop Codecov report comments on pull requests with no coverage findings
360
420
  - Bugfixes:
361
421
  - Fix a crash when a decorator has no `id` attribute.
362
422
  - **v2.4.1** `2024-08-09`:
@@ -11,10 +11,10 @@ deadcode/actions/find_python_filenames.py,sha256=Jh4J7asUte1_Ykso00BNITD6odXTFrv
11
11
  deadcode/actions/find_unused_names.py,sha256=odRoDI3lOZjZ4YTM7jgiUAIPtGbVEEmheexPgZSEFtA,497
12
12
  deadcode/actions/fix_or_show_unused_code.py,sha256=RB3BhxEVVqsZqub6wS_TL2ORXPiAjZsQjnsHRM2nt6o,3197
13
13
  deadcode/actions/get_unused_import_parts.py,sha256=zCMwPJLU7uiYih3zVBDMSUJgWAyH4w397lj7qUpwAhw,8870
14
- deadcode/actions/get_unused_names_error_message.py,sha256=dYgA2_f1eVvQqDBXn1cG8DxGnQhXzucGOU7STur2S8Y,1517
15
- deadcode/actions/merge_overlaping_file_parts.py,sha256=q4k_3H5ap3-YYMnTODemkv-nAsh9UMjam6hUIoN21Ew,3121
16
- deadcode/actions/parse_abstract_syntax_tree.py,sha256=oQpvPk3HN_GHRSZpgstH1VZITJHJjX0rRmKhA2gMXXY,607
17
- deadcode/actions/parse_arguments.py,sha256=FIuzKraeCHXikYRF6F8YRZbvFkF02L47nzcVrUgzFmI,7949
14
+ deadcode/actions/get_unused_names_error_message.py,sha256=QqZIcMpw_jY6mjIA962wqgTl0DOzzAfXvl5VADKd7XA,1509
15
+ deadcode/actions/merge_overlaping_file_parts.py,sha256=-d_usozdmCXSuxExikB-A4HYkpYBZSJqHki2ZH5G4p8,3129
16
+ deadcode/actions/parse_abstract_syntax_tree.py,sha256=mtoJv3bb8TMpudYoaJeaST5xfyhFhm59hf9joIDDeZs,631
17
+ deadcode/actions/parse_arguments.py,sha256=ecfQJNrG4lF81aEWEoT23cekavgL96qEw_5SJFS9mb0,8009
18
18
  deadcode/actions/remove_file_parts_from_content.py,sha256=EvqnAi-JbuDEXu-WMY3EFx3b0BoaqAX1MsTquEGrvXE,7495
19
19
  deadcode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  deadcode/utils/add_colors_to_diff.py,sha256=JSEtaHbyojIpCOBa2UNCaKDJr0a9hxkeDlRB4h-YpBI,423
@@ -24,16 +24,16 @@ deadcode/utils/nested_scopes.py,sha256=QknaU_lNmFYNBKCAV4zdwNJhcb31JTho_fGTlJ6y9
24
24
  deadcode/utils/print_ast.py,sha256=ovXtdOSgkjQGAeplFt3_LAv3K2r8iiRGu7c6RrflpUU,87
25
25
  deadcode/utils/print_safely.py,sha256=fCudNf-o9-kuceJOkC3HWYkzqemCPEIfx7yNw6X9e78,912
26
26
  deadcode/visitor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- deadcode/visitor/code_item.py,sha256=WwCfZ0nbSFppwED2oz7i5b6-2FujlTSoAdK5nVWSp1w,2258
28
- deadcode/visitor/dead_code_visitor.py,sha256=HmUnLkFjEmZ09QTk3ytKxSK6ikCI0320xVeOxSbLCko,32076
29
- deadcode/visitor/ignore.py,sha256=dIq1oB5aVCbhVnSeczKHGBz1sFGnC5s1La5ycTjH8Cc,4201
30
- deadcode/visitor/lines.py,sha256=DD3SQlMUFNeRIeCHDCNTv4DP6Hw1VqcqqIh6UYLCJBs,3191
27
+ deadcode/visitor/code_item.py,sha256=2CWfuBCn9uH76kjqtYrbThcM-5DLbKk81JOpJEi714U,2258
28
+ deadcode/visitor/dead_code_visitor.py,sha256=cBxXex2s1U7n240fgaDR8MEHKMHRsdajONxP2Ert4aI,34916
29
+ deadcode/visitor/ignore.py,sha256=yrrsV5tLhrweDLMFeTEMvhRBeh1sQSIdL5yIbSHuRjE,4537
30
+ deadcode/visitor/lines.py,sha256=OhMlmzvm-oN0ZT_3HWNjkdafe8WYrJm8mCWV9-rOytg,3275
31
31
  deadcode/visitor/module_exports.py,sha256=z4r5GImH0eDK8IcHlNFXMNZGu1NtpsE20xzmgyHl6U0,2933
32
32
  deadcode/visitor/noqa.py,sha256=7jM7-c_Ui0RAbzo5K0vdtct9Phm1fSebysHYcFWEJGg,2053
33
33
  deadcode/visitor/utils.py,sha256=miyXmyfoGsIv76cOJGGs6X5kTS0Qz_Z7Rdo6HgBMS8M,3702
34
34
  docs/conf.py,sha256=Rm3i-AFN1ywFcr-U55DpKuLU6-A9D1FpsCQ_qeiXUoY,462
35
- deadcode-2.6.0.dist-info/METADATA,sha256=RF0aRDlgEk9Q_9J8LqkCOQ3hzi737KVy_jJTEVkyrlI,24055
36
- deadcode-2.6.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
- deadcode-2.6.0.dist-info/entry_points.txt,sha256=efJLd6DwsjycmpPFhvnql1ziW6xGx6VtAvZXNlREQxg,53
38
- deadcode-2.6.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
39
- deadcode-2.6.0.dist-info/RECORD,,
35
+ deadcode-2.6.1.dist-info/METADATA,sha256=lNY5aLPhQP3u8bGwnmrVub_1nWB5AfY2UzjPx162FYc,27941
36
+ deadcode-2.6.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
+ deadcode-2.6.1.dist-info/entry_points.txt,sha256=efJLd6DwsjycmpPFhvnql1ziW6xGx6VtAvZXNlREQxg,53
38
+ deadcode-2.6.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
39
+ deadcode-2.6.1.dist-info/RECORD,,