diff --git a/.github/workflows/check-deadcode-on-python310.yml b/.github/workflows/check-deadcode-on-python310.yml deleted file mode 100644 index f889b0f..0000000 --- a/.github/workflows/check-deadcode-on-python310.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: Checks deadcode package with Python3.10 - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - make .venv - - name: Run checks - run: | - make check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b9c313b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Test + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +env: + FORCE_COLOR: 1 + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Install dependencies + run: | + make .venv + + - name: Run checks + run: | + make check diff --git a/deadcode/actions/find_python_filenames.py b/deadcode/actions/find_python_filenames.py index d073152..f6979a0 100644 --- a/deadcode/actions/find_python_filenames.py +++ b/deadcode/actions/find_python_filenames.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import List from pathlib import Path from deadcode.data_types import Args @@ -8,9 +7,9 @@ logger = getLogger() -def find_python_filenames(args: Args) -> List[str]: +def find_python_filenames(args: Args) -> list[str]: filenames = [] - paths: List[str] = list(args.paths) + paths: list[str] = list(args.paths) while paths: path = Path(paths.pop()) diff --git a/deadcode/actions/find_unused_names.py b/deadcode/actions/find_unused_names.py index 91b8451..7bf9dd3 100644 --- a/deadcode/actions/find_unused_names.py +++ b/deadcode/actions/find_unused_names.py @@ -1,4 +1,4 @@ -from typing import List, Iterable +from collections.abc import Iterable from deadcode.data_types import Args, Filename from deadcode.visitor.code_item import CodeItem @@ -6,7 +6,7 @@ def find_unused_names( - filenames: List[Filename], + filenames: list[Filename], args: Args, ) -> Iterable[CodeItem]: dead_code_visitor = DeadCodeVisitor(filenames, args) diff --git a/deadcode/actions/fix_or_show_unused_code.py b/deadcode/actions/fix_or_show_unused_code.py index 8b2b470..7046d69 100644 --- a/deadcode/actions/fix_or_show_unused_code.py +++ b/deadcode/actions/fix_or_show_unused_code.py @@ -1,6 +1,6 @@ from collections import defaultdict from difflib import diff_bytes, unified_diff -from typing import Iterable +from collections.abc import Iterable import os from deadcode.actions.merge_overlaping_file_parts import merge_overlaping_file_parts diff --git a/deadcode/actions/get_unused_names_error_message.py b/deadcode/actions/get_unused_names_error_message.py index 7565af8..6569b03 100644 --- a/deadcode/actions/get_unused_names_error_message.py +++ b/deadcode/actions/get_unused_names_error_message.py @@ -1,11 +1,11 @@ -from typing import Iterable, Optional +from collections.abc import Iterable from deadcode.data_types import Args from deadcode.visitor.code_item import CodeItem from deadcode.visitor.ignore import _match -def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) -> Optional[str]: +def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) -> str | None: unused_names = list(unused_names) if not unused_names: diff --git a/deadcode/actions/merge_overlaping_file_parts.py b/deadcode/actions/merge_overlaping_file_parts.py index 7bfadb7..bec2e51 100644 --- a/deadcode/actions/merge_overlaping_file_parts.py +++ b/deadcode/actions/merge_overlaping_file_parts.py @@ -1,4 +1,3 @@ -from typing import List, Optional, Tuple from deadcode.data_types import Part @@ -13,7 +12,7 @@ def does_include(bigger_part: Part, smaller_part: Part) -> bool: return bool(starts_later and ends_faster) -def sort_parts(bigger_part: Part, smaller_part: Part) -> Tuple[Part, Part]: +def sort_parts(bigger_part: Part, smaller_part: Part) -> tuple[Part, Part]: """Returns code part which begins first following by another code part.""" # TODO: Column should go first (tuple comparison would be possible) line_start_b, line_end_b, col_start_b, col_end_b = bigger_part @@ -35,7 +34,7 @@ def does_overlap(bigger_part: Part, smaller_part: Part) -> bool: return bool((line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s))) -def merge_parts(p1: Part, p2: Part) -> Optional[Part]: +def merge_parts(p1: Part, p2: Part) -> Part | None: p1, p2 = sort_parts(p1, p2) line_start1, line_end1, col_start1, col_end1 = p1 @@ -58,10 +57,10 @@ def merge_parts(p1: Part, p2: Part) -> Optional[Part]: return None -def merge_overlaping_file_parts(overlaping_file_parts: List[Part]) -> List[Part]: +def merge_overlaping_file_parts(overlaping_file_parts: list[Part]) -> list[Part]: # Make algorithm O(n^2) by checking every single part if overlaps - non_overlaping_file_parts: List[Part] = [] + non_overlaping_file_parts: list[Part] = [] for p1 in sorted(overlaping_file_parts): merged_part = None merged_with_index = None diff --git a/deadcode/actions/parse_arguments.py b/deadcode/actions/parse_arguments.py index d51ecc3..8315108 100644 --- a/deadcode/actions/parse_arguments.py +++ b/deadcode/actions/parse_arguments.py @@ -1,5 +1,5 @@ import argparse -from typing import Any, Dict, List, Optional +from typing import Any import sys import os @@ -12,7 +12,7 @@ from deadcode.utils.flatten_lists import flatten_lists_of_comma_separated_values -def parse_arguments(args: Optional[List[str]]) -> Args: +def parse_arguments(args: list[str] | None) -> Args: """Parses arguments (execution options) for deadcode tool. Arguments for DeadCode can be provided via: @@ -219,7 +219,7 @@ def parse_arguments(args: Optional[List[str]]) -> Args: return Args(**parsed_args) -def parse_pyproject_toml() -> Dict[str, Any]: +def parse_pyproject_toml() -> dict[str, Any]: """Parse a pyproject toml file, pulling out relevant parts for Black. If parsing fails, will raise a tomllib.TOMLDecodeError. @@ -232,6 +232,6 @@ def parse_pyproject_toml() -> Dict[str, Any]: with open(pyproject_toml_filename, 'rb') as f: pyproject_toml = tomllib.load(f) - config: Dict[str, Any] = pyproject_toml.get('tool', {}).get('deadcode', {}) + config: dict[str, Any] = pyproject_toml.get('tool', {}).get('deadcode', {}) config = {k.replace('--', '').replace('-', '_'): v for k, v in config.items()} return config diff --git a/deadcode/actions/remove_file_parts_from_content.py b/deadcode/actions/remove_file_parts_from_content.py index 03cba67..cdb3cd3 100644 --- a/deadcode/actions/remove_file_parts_from_content.py +++ b/deadcode/actions/remove_file_parts_from_content.py @@ -1,12 +1,12 @@ import re -from typing import List, Optional, TypeVar +from typing import TypeVar from deadcode.data_types import Part T = TypeVar('T') -def list_get(list_: List[T], index: int) -> Optional[T]: +def list_get(list_: list[T], index: int) -> T | None: if len(list_) > index: return list_[index] return None @@ -41,7 +41,7 @@ def remove_comma_from_begining(line: bytes) -> bytes: return line.lstrip()[1:].lstrip() -def remove_file_parts_from_content(content_lines: List[bytes], unused_file_parts: List[Part]) -> List[bytes]: +def remove_file_parts_from_content(content_lines: list[bytes], unused_file_parts: list[Part]) -> list[bytes]: """ """ # How should move through the lines of content? updated_content_lines = [] @@ -53,8 +53,8 @@ def remove_file_parts_from_content(content_lines: List[bytes], unused_file_parts was_block_removed = False next_line_after_removed_block = None indentation_of_first_removed_line = b'' - empty_lines_in_a_row_list: List[bytes] = [] - empty_lines_before_removed_block_list: List[bytes] = [] + empty_lines_in_a_row_list: list[bytes] = [] + empty_lines_before_removed_block_list: list[bytes] = [] for current_lineno, line in enumerate(content_lines, start=1): from_line, to_line, from_col, to_col = 0, 0, 0, 0 diff --git a/deadcode/cli.py b/deadcode/cli.py index 4b4f54e..c0e1405 100644 --- a/deadcode/cli.py +++ b/deadcode/cli.py @@ -1,4 +1,3 @@ -from typing import List, Optional import sys from deadcode import __version__ @@ -12,9 +11,8 @@ def main( - command_line_args: Optional[List[str]] = None, -) -> Optional[str]: - + command_line_args: list[str] | None = None, +) -> str | None: if command_line_args and '--version' in command_line_args or '--version' in sys.argv: return __version__ diff --git a/deadcode/constants.py b/deadcode/constants.py index 8839e39..4566dae 100644 --- a/deadcode/constants.py +++ b/deadcode/constants.py @@ -1,4 +1,4 @@ -from typing import Dict, Literal +from typing import Literal UnusedCodeType = Literal[ @@ -33,7 +33,7 @@ ] -ERROR_TYPE_TO_ERROR_CODE: Dict[UnusedCodeType, UnusedCodeErrorCode] = { +ERROR_TYPE_TO_ERROR_CODE: dict[UnusedCodeType, UnusedCodeErrorCode] = { 'variable': 'DC01', 'function': 'DC02', 'class': 'DC03', diff --git a/deadcode/data_types.py b/deadcode/data_types.py index 11bbbba..87b1f17 100644 --- a/deadcode/data_types.py +++ b/deadcode/data_types.py @@ -1,7 +1,8 @@ import ast from dataclasses import dataclass -from typing import Iterable, NamedTuple +from typing import NamedTuple +from collections.abc import Iterable AbstractSyntaxTree = ast.Module # Should be module instead of ast diff --git a/deadcode/utils/base_test_case.py b/deadcode/utils/base_test_case.py index 830a164..a88f8cf 100644 --- a/deadcode/utils/base_test_case.py +++ b/deadcode/utils/base_test_case.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any from unittest import TestCase from unittest.mock import MagicMock, patch @@ -8,7 +8,7 @@ class BaseTestCase(TestCase): - files: Dict[str, bytes] = {} + files: dict[str, bytes] = {} maxDiff = None @@ -17,10 +17,10 @@ def patch(self, path: str) -> MagicMock: self.addCleanup(patcher.stop) return patcher.start() - def _get_filenames(self, *args: Any, **kwargs: Any) -> List[str]: + def _get_filenames(self, *args: Any, **kwargs: Any) -> list[str]: return list(self.files.keys()) - def _read_file_side_effect(self, filename: Union[str, Path], *args: Any, **kwargs: Any) -> MagicMock: + def _read_file_side_effect(self, filename: str | Path, *args: Any, **kwargs: Any) -> MagicMock: mock = MagicMock() mock.filename = str(filename) @@ -35,7 +35,7 @@ def cache_file_content(file_content: bytes) -> int: return mock def setUp(self) -> None: - self.updated_files: Dict[str, bytes] = {} + self.updated_files: dict[str, bytes] = {} self.find_python_filenames_mock = self.patch('deadcode.cli.find_python_filenames') self.find_python_filenames_mock.side_effect = self._get_filenames @@ -50,7 +50,7 @@ def setUp(self) -> None: self.args = Args() - def assertFiles(self, files: Dict[str, bytes], removed: Optional[List[str]] = None) -> None: + def assertFiles(self, files: dict[str, bytes], removed: list[str] | None = None) -> None: expected_removed_files = removed expected_files = files @@ -76,7 +76,7 @@ def assertFiles(self, files: Dict[str, bytes], removed: Optional[List[str]] = No fix_indent(self.updated_files.get(filename) or unchanged_files.get(filename) or ''), ) - def assertUpdatedFiles(self, expected_updated_files: Dict[str, bytes]) -> None: + def assertUpdatedFiles(self, expected_updated_files: dict[str, bytes]) -> None: """Checks if updated files match expected updated files.""" self.assertListEqual(list(expected_updated_files.keys()), list(self.updated_files.keys())) diff --git a/deadcode/utils/fix_indent.py b/deadcode/utils/fix_indent.py index 35a2e76..808fc8e 100644 --- a/deadcode/utils/fix_indent.py +++ b/deadcode/utils/fix_indent.py @@ -1,10 +1,10 @@ import sys -from typing import Optional, TypeVar +from typing import TypeVar T = TypeVar('T') -def fix_indent(doc: T) -> Optional[T]: +def fix_indent(doc: T) -> T | None: """Finds indentation of a first line and removes it from all following lines. Implemented based on inspect.cleandoc by keeping trailing lines. diff --git a/deadcode/utils/flatten_lists.py b/deadcode/utils/flatten_lists.py index a5878ed..513a4b3 100644 --- a/deadcode/utils/flatten_lists.py +++ b/deadcode/utils/flatten_lists.py @@ -1,18 +1,18 @@ -from typing import List, Optional, TypeVar +from typing import TypeVar T = TypeVar('T') def flatten_lists_of_comma_separated_values( - list_of_comma_separated_values: Optional[List[List[str]]], -) -> List[str]: + list_of_comma_separated_values: list[list[str]] | None, +) -> list[str]: """Concatenates lists into one list.""" if not list_of_comma_separated_values: return [] return flatten_list([v.split(',') for v in flatten_list(list_of_comma_separated_values)]) -def flatten_list(list_of_lists: Optional[List[List[T]]]) -> List[T]: +def flatten_list(list_of_lists: list[list[T]] | None) -> list[T]: """Concatenates lists into one list.""" if not list_of_lists: return [] diff --git a/deadcode/utils/nested_scopes.py b/deadcode/utils/nested_scopes.py index 4aef039..99b7d9e 100644 --- a/deadcode/utils/nested_scopes.py +++ b/deadcode/utils/nested_scopes.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any from deadcode.visitor.code_item import CodeItem @@ -15,7 +15,7 @@ class NestedScope: """ def __init__(self) -> None: - self._scopes: Dict[Union[str, CodeItem], Any] = {} + self._scopes: dict[str | CodeItem, Any] = {} def add(self, code_item: CodeItem) -> None: """Adds code item to nested scope.""" @@ -34,7 +34,7 @@ def add(self, code_item: CodeItem) -> None: # > TODO: leaf should be replaced. Is it replaced with new code item? current_scope[code_item] = {} - def get(self, name: str, scope: str) -> Optional[Union[CodeItem, str]]: + def get(self, name: str, scope: str) -> CodeItem | str | None: """Returns CodeItem which matches scoped_name (e.g. package.class.method.variable) from the given scope or None if its not found.""" @@ -45,7 +45,7 @@ def get(self, name: str, scope: str) -> Optional[Union[CodeItem, str]]: # projects.models, billing.models, auth.models: only one root scope called models would be registered. # Create a stack of scopes begining from nearest and following with parent one - scopes: List[Dict[Union[CodeItem, str], Dict[Any, Any]]] = [] + scopes: list[dict[CodeItem | str, dict[Any, Any]]] = [] next_scope = self._scopes for scope_part in scope.split('.'): if scope_part not in next_scope: diff --git a/deadcode/visitor/code_item.py b/deadcode/visitor/code_item.py index 4d0c5f9..6d5b407 100644 --- a/deadcode/visitor/code_item.py +++ b/deadcode/visitor/code_item.py @@ -29,7 +29,6 @@ from pathlib import Path -from typing import List, Optional from deadcode.constants import UnusedCodeType, ERROR_TYPE_TO_ERROR_CODE @@ -60,11 +59,11 @@ def __init__( name: str, type_: UnusedCodeType, filename: Path, - code_parts: Optional[List[Part]] = None, # TODO: I should use a dataclass instead of a tuple for Part. - scope: Optional[str] = None, - inherits_from: Optional[List[str]] = None, - name_line: Optional[int] = None, - name_column: Optional[int] = None, + code_parts: list[Part] | None = None, # TODO: I should use a dataclass instead of a tuple for Part. + scope: str | None = None, + inherits_from: list[str] | None = None, + name_line: int | None = None, + name_column: int | None = None, message: str = '', number_of_uses: int = 0, ): diff --git a/deadcode/visitor/dead_code_visitor.py b/deadcode/visitor/dead_code_visitor.py index 9236eb3..2bad9d8 100644 --- a/deadcode/visitor/dead_code_visitor.py +++ b/deadcode/visitor/dead_code_visitor.py @@ -8,7 +8,8 @@ from pathlib import Path -from typing import Callable, Dict, List, Optional, Set, TextIO, Union, Iterable +from typing import TextIO +from collections.abc import Callable, Iterable from deadcode.constants import UnusedCodeType from deadcode.data_types import Args, Part from deadcode.visitor.code_item import CodeItem @@ -40,47 +41,47 @@ class DeadCodeVisitor(ast.NodeVisitor): """Finds dead code.""" - def __init__(self, filenames: List[str], args: Args) -> None: + def __init__(self, filenames: list[str], args: Args) -> None: self.filenames = filenames self.args = args - self.ignore_decorators: List[str] = [] + self.ignore_decorators: list[str] = [] verbose = False # verbose = args.verbose self.verbose = verbose - self.defined_attrs: List[CodeItem] = LoggingList('attribute', verbose) - self.defined_classes: List[CodeItem] = LoggingList('class', verbose) - self.defined_funcs: List[CodeItem] = LoggingList('function', verbose) - self.defined_imports: List[CodeItem] = LoggingList('import', verbose) - self.defined_methods: List[CodeItem] = LoggingList('method', verbose) - self.defined_props: List[CodeItem] = LoggingList('property', verbose) - self.defined_vars: List[CodeItem] = LoggingList('variable', verbose) - self.unused_file: List[CodeItem] = LoggingList('unused_file', verbose) - self.unreachable_code: List[CodeItem] = LoggingList('unreachable_code', verbose) + self.defined_attrs: list[CodeItem] = LoggingList('attribute', verbose) + self.defined_classes: list[CodeItem] = LoggingList('class', verbose) + self.defined_funcs: list[CodeItem] = LoggingList('function', verbose) + self.defined_imports: list[CodeItem] = LoggingList('import', verbose) + self.defined_methods: list[CodeItem] = LoggingList('method', verbose) + self.defined_props: list[CodeItem] = LoggingList('property', verbose) + self.defined_vars: list[CodeItem] = LoggingList('variable', verbose) + self.unused_file: list[CodeItem] = LoggingList('unused_file', verbose) + self.unreachable_code: list[CodeItem] = LoggingList('unreachable_code', verbose) - self.used_names: Set[str] = LoggingSet('name', self.verbose) + self.used_names: set[str] = LoggingSet('name', self.verbose) self.filename = Path() - self.code: List[str] = [] + self.code: list[str] = [] # Note: scope is a stack containing current module name, class names, function names - self.scope_parts: List[str] = [] + self.scope_parts: list[str] = [] # This flag is used to stop registering code definitions in a code item # during recursive its parsing self.should_ignore_new_definitions = False - self.noqa_lines: Dict[bytes, Set[int]] = {} + self.noqa_lines: dict[bytes, set[int]] = {} self.scopes = NestedScope() @property def scope(self) -> str: return '.'.join(self.scope_parts) - def add_used_name(self, name: str, scope: Optional[str] = None) -> None: + def add_used_name(self, name: str, scope: str | None = None) -> None: # TODO: Usage should be tracked on CodeItem. # TODO: Lets first resolve, how to correctly set the type of a function argument. @@ -161,7 +162,7 @@ def unused_vars(self) -> Iterable[CodeItem]: def unused_attrs(self) -> Iterable[CodeItem]: return _get_unused_items(self.defined_attrs, self.used_names) - def _log(self, *args, file: Optional[TextIO] = None, force: bool = False) -> None: # type: ignore + def _log(self, *args, file: TextIO | None = None, force: bool = False) -> None: # type: ignore if self.verbose or force: file = file or sys.stdout try: @@ -171,7 +172,7 @@ def _log(self, *args, file: Optional[TextIO] = None, force: bool = False) -> Non x = ' '.join(map(str, args)) print(x.encode(), file=file) - def _add_aliases(self, node: Union[ast.Import, ast.ImportFrom]) -> None: + def _add_aliases(self, node: ast.Import | ast.ImportFrom) -> None: """ We delegate to this method instead of using visit_alias() to have access to line numbers and to filter imports from __future__. @@ -193,7 +194,7 @@ def _add_aliases(self, node: Union[ast.Import, ast.ImportFrom]) -> None: if alias is not None: self.add_used_name(name_and_alias.name) - def _handle_conditional_node(self, node: Union[ast.If, ast.IfExp, ast.While], name: str) -> None: + def _handle_conditional_node(self, node: ast.If | ast.IfExp | ast.While, name: str) -> None: if utils.condition_is_always_false(node.test): self._define( self.unreachable_code, @@ -230,12 +231,12 @@ def _handle_conditional_node(self, node: Union[ast.If, ast.IfExp, ast.While], na def _define( self, - collection: List[CodeItem], + collection: list[CodeItem], name: str, first_node: ast.AST, - last_node: Optional[ast.AST] = None, + last_node: ast.AST | None = None, message: str = '', - ignore: Optional[Callable[[Path, str], bool]] = None, + ignore: Callable[[Path, str], bool] | None = None, ) -> None: # TODO: add support for ignore_definitions, ignore_definitions_if_inherits_from options self._log( @@ -359,7 +360,7 @@ def _is_locals_call(node: ast.AST) -> bool: and not node.keywords ) - def get_inherits_from(self, node: ast.AST) -> Optional[List[str]]: + def get_inherits_from(self, node: ast.AST) -> list[str] | None: """Returns a set of base classes from any level in inheritance tree.""" if not (bases_attr := getattr(node, 'bases', None)): @@ -383,7 +384,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: else: self._define(self.defined_classes, node.name, node, ignore=_ignore_class) - def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None: + def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: decorator_names = [utils.get_decorator_name(decorator) for decorator in node.decorator_list] # type: ignore first_arg = node.args.args[0].arg if node.args.args else None @@ -512,7 +513,7 @@ def visit(self, node: ast.AST) -> None: if was_scope_increased: self.scope_parts.pop() - def _handle_ast_list(self, ast_list: List[ast.AST]) -> None: + def _handle_ast_list(self, ast_list: list[ast.AST]) -> None: """ Find unreachable nodes in the given sequence of ast nodes. """ diff --git a/deadcode/visitor/ignore.py b/deadcode/visitor/ignore.py index 6caa6e0..e5c1357 100644 --- a/deadcode/visitor/ignore.py +++ b/deadcode/visitor/ignore.py @@ -1,7 +1,7 @@ import ast from fnmatch import fnmatch, fnmatchcase from pathlib import Path -from typing import Iterable, Set, Union +from collections.abc import Iterable from deadcode.visitor.code_item import CodeItem @@ -36,7 +36,7 @@ } -def _get_unused_items(defined_items: Iterable[CodeItem], used_names: Set[str]) -> Iterable[CodeItem]: +def _get_unused_items(defined_items: Iterable[CodeItem], used_names: set[str]) -> Iterable[CodeItem]: unused_items = [item for item in defined_items if item.name not in used_names] unused_items.sort(key=lambda item: item.name.lower()) return unused_items @@ -46,12 +46,12 @@ def _is_special_name(name: str) -> bool: return name.startswith('__') and name.endswith('__') -def _match(name: Union[str, Path], patterns: Iterable[str], case: bool = True) -> bool: +def _match(name: str | Path, patterns: Iterable[str], case: bool = True) -> bool: func = fnmatchcase if case else fnmatch return any(func(str(name), pattern) for pattern in patterns) -def _match_many(names: Union[Iterable[str], Iterable[Path]], patterns: Iterable[str], case: bool = True) -> bool: +def _match_many(names: Iterable[str] | Iterable[Path], patterns: Iterable[str], case: bool = True) -> bool: return any(_match(name, patterns, case) for name in names) diff --git a/deadcode/visitor/lines.py b/deadcode/visitor/lines.py index d6ccd38..247eb95 100644 --- a/deadcode/visitor/lines.py +++ b/deadcode/visitor/lines.py @@ -1,8 +1,7 @@ import ast -from typing import List, Optional, Union -def _get_last_child_with_lineno(node: ast.AST) -> Optional[ast.AST]: +def _get_last_child_with_lineno(node: ast.AST) -> ast.AST | None: """ Return the last direct child of `node` that has a lineno attribute, or None if `node` has no such children. @@ -26,7 +25,7 @@ def _get_last_child_with_lineno(node: ast.AST) -> Optional[ast.AST]: continue try: - last_field: Union[ast.AST, List[ast.AST]] = getattr(node, name) + last_field: ast.AST | list[ast.AST] = getattr(node, name) except AttributeError: continue diff --git a/deadcode/visitor/noqa.py b/deadcode/visitor/noqa.py index 31501f1..fc0441f 100644 --- a/deadcode/visitor/noqa.py +++ b/deadcode/visitor/noqa.py @@ -1,6 +1,5 @@ from collections import defaultdict import re -from typing import Dict, List, Set NOQA_REGEXP = re.compile( # Use the same regex as flake8 does. @@ -47,12 +46,12 @@ } -def _parse_error_codes(matches_dict: Dict[str, bytes]) -> List[bytes]: +def _parse_error_codes(matches_dict: dict[str, bytes]) -> list[bytes]: # If no error code is specified, add the line to the "all" category. return [c.strip() for c in (matches_dict['codes'] or b'all').split(b',')] -def parse_noqa(code: bytes) -> Dict[bytes, Set[int]]: +def parse_noqa(code: bytes) -> dict[bytes, set[int]]: noqa_lines = defaultdict(set) for lineno, line in enumerate(code.split(b'\n'), start=1): match = NOQA_REGEXP.search(line) @@ -63,6 +62,6 @@ def parse_noqa(code: bytes) -> Dict[bytes, Set[int]]: return noqa_lines -def ignore_line(noqa_lines: Dict[bytes, Set[int]], lineno: int, error_code: bytes) -> bool: +def ignore_line(noqa_lines: dict[bytes, set[int]], lineno: int, error_code: bytes) -> bool: """Check if the reported line is annotated with "# noqa".""" return lineno in noqa_lines[error_code] or lineno in noqa_lines[b'all'] diff --git a/deadcode/visitor/utils.py b/deadcode/visitor/utils.py index 43fc366..74b1e9d 100644 --- a/deadcode/visitor/utils.py +++ b/deadcode/visitor/utils.py @@ -1,5 +1,4 @@ import ast -from typing import Union from deadcode.visitor.code_item import CodeItem from deadcode.constants import UnusedCodeType @@ -51,7 +50,7 @@ def condition_is_always_true(condition: ast.AST) -> bool: # return path -def get_decorator_name(decorator: Union[ast.Call, ast.Attribute]) -> str: +def get_decorator_name(decorator: ast.Call | ast.Attribute) -> str: if isinstance(decorator, ast.Call): decorator = decorator.func # type: ignore parts = [] diff --git a/pyproject.toml b/pyproject.toml index f14a60b..2a0f263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,10 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ - "tomli>=2.0.1" + "tomli>=2.0.1; python_version<'3.11'" ] [project.optional-dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt index 8882e09..dd1c74c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,8 +13,6 @@ certifi==2024.7.4 # httpcore # httpx # requests -cffi==1.16.0 - # via cryptography charset-normalizer==3.3.2 # via requests click==8.1.7 @@ -23,8 +21,6 @@ click==8.1.7 # userpath coverage==7.5.4 # via pytest-cov -cryptography==42.0.8 - # via secretstorage cyclonedx-python-lib==7.5.1 # via pip-audit defusedxml==0.7.1 @@ -42,6 +38,7 @@ filelock==3.15.4 h11==0.14.0 # via httpcore hatch==1.12.0 + # via deadcode (pyproject.toml) hatchling==1.25.0 # via hatch html5lib==1.1 @@ -68,10 +65,6 @@ jaraco-context==5.3.0 # via keyring jaraco-functools==4.0.1 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage keyring==25.2.1 # via hatch license-expression==30.3.0 @@ -87,6 +80,7 @@ more-itertools==10.3.0 msgpack==1.0.8 # via cachecontrol mypy==1.10.1 + # via deadcode (pyproject.toml) mypy-extensions==1.0.0 # via mypy packageurl-python==0.15.3 @@ -107,6 +101,7 @@ pip==24.1.2 pip-api==0.0.34 # via pip-audit pip-audit==2.7.3 + # via deadcode (pyproject.toml) pip-requirements-parser==32.0.1 # via pip-audit platformdirs==4.2.2 @@ -121,15 +116,16 @@ ptyprocess==0.7.0 # via pexpect py-serializable==1.1.0 # via cyclonedx-python-lib -pycparser==2.22 - # via cffi pygments==2.18.0 # via rich pyparsing==3.1.2 # via pip-requirements-parser pytest==8.2.2 - # via pytest-cov + # via + # deadcode (pyproject.toml) + # pytest-cov pytest-cov==5.0.0 + # via deadcode (pyproject.toml) requests==2.32.3 # via # cachecontrol @@ -139,8 +135,7 @@ rich==13.7.1 # hatch # pip-audit ruff==0.5.1 -secretstorage==3.3.3 - # via keyring + # via deadcode (pyproject.toml) shellingham==1.5.4 # via hatch six==1.16.0 @@ -155,6 +150,7 @@ toml==0.10.2 # via pip-audit tomli==2.0.1 # via + # deadcode (pyproject.toml) # coverage # hatchling # mypy diff --git a/requirements.txt b/requirements.txt index 40fa916..9efe771 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml -o requirements.txt -tomli==2.0.1 +# uv pip compile --universal pyproject.toml -o requirements.txt -p 3.10 +tomli==2.0.1 ; python_full_version < '3.11' + # via deadcode (pyproject.toml) diff --git a/tests/files/classes.py b/tests/files/classes.py index 560b220..61747b5 100644 --- a/tests/files/classes.py +++ b/tests/files/classes.py @@ -1,4 +1,4 @@ -class UnusedClass(object): +class UnusedClass: pass