From 4199743da414e9cb75be2fc3820423275d92eef9 Mon Sep 17 00:00:00 2001 From: Albertas Gimbutas Date: Sun, 8 Nov 2026 03:09:48 +0200 Subject: [PATCH 1/3] [deadcode_py] Suppress unittest lifecycle methods declared in test files --- deadcode/visitor/ignore.py | 24 +++++++- tests/test_unittest_lifecycle_methods.py | 73 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/test_unittest_lifecycle_methods.py diff --git a/deadcode/visitor/ignore.py b/deadcode/visitor/ignore.py index 4895aff..a8cc99b 100644 --- a/deadcode/visitor/ignore.py +++ b/deadcode/visitor/ignore.py @@ -19,6 +19,18 @@ 'setup_method', 'teardown_method', } +UNITTEST_METHOD_NAMES = { + 'setUp', + 'tearDown', + 'setUpClass', + 'tearDownClass', + 'asyncSetUp', + 'asyncTearDown', +} +UNITTEST_FUNCTION_NAMES = { + 'setUpModule', + 'tearDownModule', +} ERROR_CODES = { 'variable': b'DC01', @@ -88,12 +100,20 @@ def _ignore_import(filename: Path, import_name: str) -> bool: def _ignore_function(filename: Path, function_name: str) -> bool: - return (function_name in PYTEST_FUNCTION_NAMES or function_name.startswith('test_')) and _is_test_file(filename) + # Class-level lifecycle methods such as `def setUpClass(cls)` have no decorator and a first argument other + # than `self`, so `visit_FunctionDef` classifies them as functions and they must be suppressed here as well. + return ( + function_name in PYTEST_FUNCTION_NAMES + or function_name in UNITTEST_METHOD_NAMES + or function_name in UNITTEST_FUNCTION_NAMES + or function_name.startswith('test_') + ) and _is_test_file(filename) def _ignore_method(filename: Path, method_name: str) -> bool: return _is_special_name(method_name) or ( - (method_name in PYTEST_METHOD_NAMES or method_name.startswith('test_')) and _is_test_file(filename) + (method_name in PYTEST_METHOD_NAMES or method_name in UNITTEST_METHOD_NAMES or method_name.startswith('test_')) + and _is_test_file(filename) ) diff --git a/tests/test_unittest_lifecycle_methods.py b/tests/test_unittest_lifecycle_methods.py new file mode 100644 index 0000000..74ed1d8 --- /dev/null +++ b/tests/test_unittest_lifecycle_methods.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import pytest + +from deadcode.cli import main, print_main +from deadcode.utils.base_test_case import BaseTestCase + +SOURCE = b""" +from unittest import TestCase + + +class MyTests(TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def setUpClass(cls): + pass + + def tearDownClass(cls): + pass + + def test_something(self): + pass +""" + +SOURCE_WITH_UNUSED_HELPER = SOURCE + b""" + def unused_helper(self): + pass +""" + + +class TestUnittestLifecycleMethods(BaseTestCase): + def test_lifecycle_methods_in_test_file_are_not_reported(self): + self.files = {'/project/tests/test_sample.py': SOURCE} + + self.assertIsNone(main(['/project/tests/test_sample.py', '--no-color'])) + self.assertEqual(print_main(['/project/tests/test_sample.py', '--no-color']), 0) + + def test_lifecycle_methods_outside_test_file_are_still_reported(self): + self.files = {'/project/sample.py': SOURCE} + + output = main(['/project/sample.py', '--no-color']) + + assert output is not None + self.assertIn('DC04 Method `setUp` is never used', output) + self.assertIn('DC04 Method `tearDown` is never used', output) + self.assertIn('DC02 Function `setUpClass` is never used', output) + self.assertIn('DC02 Function `tearDownClass` is never used', output) + self.assertEqual(print_main(['/project/sample.py', '--no-color']), 1) + + def test_unrelated_unused_method_in_test_file_is_still_reported(self): + self.files = {'/project/tests/test_sample.py': SOURCE_WITH_UNUSED_HELPER} + + output = main(['/project/tests/test_sample.py', '--no-color']) + + assert output is not None + self.assertEqual(len(output.splitlines()), 1) + self.assertTrue(output.endswith('DC04 Method `unused_helper` is never used')) + + +def test_unittest_lifecycle_in_temporary_test_directory_returns_no_findings( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + test_directory = tmp_path / 'tests' + test_directory.mkdir() + (test_directory / 'test_sample.py').write_bytes(SOURCE) + monkeypatch.chdir(tmp_path) + + assert main(['tests', '--no-color']) is None + assert print_main(['tests', '--no-color']) == 0 From 998d3614c64f6cd62eff3a5e793dde722ab5f509 Mon Sep 17 00:00:00 2001 From: Albertas Gimbutas Date: Sun, 8 Nov 2026 03:09:48 +0200 Subject: [PATCH 2/3] [deadcode_py] Document unittest lifecycle suppression in the README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f3ea60f..55e2425 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,10 @@ def foo(): # noqa: DC02 A comment on the first decorator line is still accepted. +## Ignoring unittest lifecycle methods +`unittest` lifecycle methods (`setUp`, `tearDown`, `setUpClass`, `tearDownClass`, `asyncSetUp`, `asyncTearDown`, `setUpModule`, `tearDownModule`) declared in test files are ignored by default, alongside the existing pytest hooks. +Declarations with those names outside test files are still reported as unused code. + ## Contributing Create a feature branch from `main`, open a pull request targeting `main`, and @@ -266,6 +270,7 @@ the public module-level names are exported. - --ignore-bodies-if-inherits-from and --ignore-definitions-if-inherits-from now recognize dotted base classes such as graphene.ObjectType and match their final component. - The option help text and README document the accepted plain, dotted and glob base-name forms. - Render decorator names deterministically, including dotted names, and stop unusual decorators such as `@Class().function()` from crashing or producing memory addresses. + - Stop reporting unittest lifecycle methods declared in test files as unused code (issue #21). - v2.5.0 (2026-11-07): - Bugfixes: - Fix a crash when a decorator has no `id` attribute. From 1ae81734a2b3fd22058bcab7c31c8ce63936eb2e Mon Sep 17 00:00:00 2001 From: Albertas Gimbutas Date: Sun, 8 Nov 2026 03:11:29 +0200 Subject: [PATCH 3/3] [deadcode_py] Format the unittest lifecycle test module --- tests/test_unittest_lifecycle_methods.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_unittest_lifecycle_methods.py b/tests/test_unittest_lifecycle_methods.py index 74ed1d8..e3d9d65 100644 --- a/tests/test_unittest_lifecycle_methods.py +++ b/tests/test_unittest_lifecycle_methods.py @@ -26,10 +26,13 @@ def test_something(self): pass """ -SOURCE_WITH_UNUSED_HELPER = SOURCE + b""" +SOURCE_WITH_UNUSED_HELPER = ( + SOURCE + + b""" def unused_helper(self): pass """ +) class TestUnittestLifecycleMethods(BaseTestCase):