lapsed 2.7.2__tar.gz → 2.9.0__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.
- {lapsed-2.7.2 → lapsed-2.9.0}/PKG-INFO +1 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/find_python_filenames.py +4 -2
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/fix_or_show_unused_code.py +4 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/parse_tach_config.py +13 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/cli.py +13 -2
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/nested_scopes.py +24 -8
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/code_item.py +3 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/dead_code_visitor.py +12 -2
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/ignore.py +9 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/pyproject.toml +1 -1
- {lapsed-2.7.2 → lapsed-2.9.0}/.gitignore +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/LICENSE +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/README.md +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/__init__.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/__init__.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/find_unused_names.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/get_unused_names_error_message.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/merge_overlaping_file_parts.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/parse_abstract_syntax_tree.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/parse_arguments.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/actions/remove_file_parts_from_content.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/constants.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/data_types.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/__init__.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/add_colors_to_diff.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/base_test_case.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/fix_indent.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/flatten_lists.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/utils/print_ast.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/__init__.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/lines.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/noqa.py +0 -0
- {lapsed-2.7.2 → lapsed-2.9.0}/deadcode/visitor/utils.py +0 -0
|
@@ -33,9 +33,11 @@ def find_python_filenames(args: Args) -> List[str]:
|
|
|
33
33
|
continue
|
|
34
34
|
|
|
35
35
|
if path.is_file() and path.suffix == '.py':
|
|
36
|
-
|
|
36
|
+
# .as_posix() (not str()) so filenames stay forward-slash on every
|
|
37
|
+
# OS, matching the paths given on the command line.
|
|
38
|
+
filenames.append(path.as_posix())
|
|
37
39
|
elif path.is_dir():
|
|
38
|
-
paths += list([
|
|
40
|
+
paths += list([p.as_posix() for p in path.glob('*')])
|
|
39
41
|
elif not path.exists():
|
|
40
42
|
# TODO: unify error logging and reporting
|
|
41
43
|
# Maybe a cli flag could be added to stop on error
|
|
@@ -23,7 +23,10 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
|
|
|
23
23
|
|
|
24
24
|
filename_to_unused_items = defaultdict(list)
|
|
25
25
|
for unused_item in unused_items:
|
|
26
|
-
|
|
26
|
+
# .as_posix() (not str()) to match the forward-slash filenames
|
|
27
|
+
# find_python_filenames() produces, which the mocked/real `open()`
|
|
28
|
+
# calls below key off of.
|
|
29
|
+
filename_to_unused_items[unused_item.filename.as_posix()].append(unused_item)
|
|
27
30
|
|
|
28
31
|
result = []
|
|
29
32
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import sys
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
+
from functools import lru_cache
|
|
4
5
|
from logging import getLogger
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict, Iterable, List, Optional
|
|
7
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
7
8
|
|
|
8
9
|
if sys.version_info >= (3, 11):
|
|
9
10
|
import tomllib
|
|
@@ -46,6 +47,9 @@ class TachIndex:
|
|
|
46
47
|
every other tach project it might also belong to. Paths unrelated to any tach project
|
|
47
48
|
(e.g. an explicitly provided directory that lives elsewhere entirely) are unaffected.
|
|
48
49
|
"""
|
|
50
|
+
if not self._configs:
|
|
51
|
+
return False
|
|
52
|
+
|
|
49
53
|
path = path.resolve()
|
|
50
54
|
|
|
51
55
|
related_to_any_config = False
|
|
@@ -89,6 +93,14 @@ class TachIndex:
|
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def load_tach_index(tach_config_paths: Iterable[str]) -> TachIndex:
|
|
96
|
+
# find_python_filenames() and DeadCodeVisitor.__init__() both load the
|
|
97
|
+
# tach index from the same args.tach_config, so cache it per path set to
|
|
98
|
+
# avoid re-parsing (and re-globbing for tach.domain.toml files) twice.
|
|
99
|
+
return _load_tach_index_cached(tuple(tach_config_paths))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@lru_cache(maxsize=None)
|
|
103
|
+
def _load_tach_index_cached(tach_config_paths: Tuple[str, ...]) -> TachIndex:
|
|
92
104
|
configs = []
|
|
93
105
|
for raw_path in tach_config_paths:
|
|
94
106
|
path = Path(raw_path).resolve()
|
|
@@ -33,14 +33,25 @@ def main(
|
|
|
33
33
|
return error_message + ('\n\n' + file_diff if file_diff else '')
|
|
34
34
|
|
|
35
35
|
if not args.count and not args.quiet:
|
|
36
|
-
|
|
36
|
+
try:
|
|
37
|
+
print('\033[1mWell done!\033[0m ✨ 🚀 ✨')
|
|
38
|
+
except UnicodeEncodeError:
|
|
39
|
+
# Some terminals (e.g. Windows' default cp1252 console codepage) cannot
|
|
40
|
+
# encode emoji. Fall back to a plain message rather than crashing.
|
|
41
|
+
print('\033[1mWell done!\033[0m')
|
|
37
42
|
return None
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
def print_main() -> None:
|
|
41
|
-
|
|
46
|
+
is_version_request = '--version' in sys.argv
|
|
47
|
+
|
|
48
|
+
result = main()
|
|
49
|
+
if result:
|
|
42
50
|
print(result)
|
|
43
51
|
|
|
52
|
+
if not is_version_request and result is not None:
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
44
55
|
|
|
45
56
|
if __name__ == '__main__':
|
|
46
57
|
print_main()
|
|
@@ -2,6 +2,15 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
2
2
|
|
|
3
3
|
from deadcode.visitor.code_item import CodeItem
|
|
4
4
|
|
|
5
|
+
# Sentinel key used to store a scope's own CodeItem definitions in a dict
|
|
6
|
+
# separate from its nested child-scope dicts. A scope-part name (e.g. a class
|
|
7
|
+
# or function name) and a CodeItem's name can be the same string (a class
|
|
8
|
+
# named `Bar` nested under module `foo` is both a child scope `foo.Bar` and a
|
|
9
|
+
# CodeItem named `Bar` defined in scope `foo`), so they can't share one dict's
|
|
10
|
+
# key space without ambiguity. `object()` can't equal any string or CodeItem,
|
|
11
|
+
# so it can't collide with either.
|
|
12
|
+
_ITEMS = object()
|
|
13
|
+
|
|
5
14
|
|
|
6
15
|
class NestedScope:
|
|
7
16
|
"""This data structure is used to track what types are defined in each scope.
|
|
@@ -15,7 +24,7 @@ class NestedScope:
|
|
|
15
24
|
"""
|
|
16
25
|
|
|
17
26
|
def __init__(self) -> None:
|
|
18
|
-
self._scopes: Dict[Union[str, CodeItem], Any] = {}
|
|
27
|
+
self._scopes: Dict[Union[str, CodeItem, object], Any] = {}
|
|
19
28
|
|
|
20
29
|
def add(self, code_item: CodeItem) -> None:
|
|
21
30
|
"""Adds code item to nested scope."""
|
|
@@ -31,8 +40,12 @@ class NestedScope:
|
|
|
31
40
|
current_scope[scope_part] = {} # Could use None if type cannot have scope
|
|
32
41
|
current_scope = current_scope[scope_part]
|
|
33
42
|
|
|
34
|
-
#
|
|
35
|
-
current_scope
|
|
43
|
+
# Store this scope's own definitions in their own dict (keyed by
|
|
44
|
+
# _ITEMS) instead of mixing them into current_scope's child-scope
|
|
45
|
+
# keys, so get() below can do a direct O(1) dict lookup by name
|
|
46
|
+
# instead of scanning current_scope.keys() for a match.
|
|
47
|
+
items: Dict[Union[str, CodeItem], CodeItem] = current_scope.setdefault(_ITEMS, {})
|
|
48
|
+
items[code_item] = code_item
|
|
36
49
|
|
|
37
50
|
def get(self, name: str, scope: str) -> Optional[Union[CodeItem, str]]:
|
|
38
51
|
"""Returns CodeItem which matches scoped_name (e.g. package.class.method.variable)
|
|
@@ -45,7 +58,7 @@ class NestedScope:
|
|
|
45
58
|
# projects.models, billing.models, auth.models: only one root scope called models would be registered.
|
|
46
59
|
|
|
47
60
|
# Create a stack of scopes begining from nearest and following with parent one
|
|
48
|
-
scopes: List[Dict[Union[CodeItem,
|
|
61
|
+
scopes: List[Dict[Union[str, CodeItem, object], Any]] = []
|
|
49
62
|
next_scope = self._scopes
|
|
50
63
|
for scope_part in scope.split('.'):
|
|
51
64
|
if scope_part not in next_scope:
|
|
@@ -53,11 +66,14 @@ class NestedScope:
|
|
|
53
66
|
next_scope = next_scope[scope_part]
|
|
54
67
|
scopes.insert(0, next_scope)
|
|
55
68
|
|
|
56
|
-
# Search for definition with provided name in scopes
|
|
69
|
+
# Search for definition with provided name in scopes.
|
|
70
|
+
# CodeItem.__hash__/__eq__ accept plain strings, so a dict.get() with
|
|
71
|
+
# the string name directly finds the matching CodeItem key's value in
|
|
72
|
+
# O(1) average time.
|
|
57
73
|
for current_scope in scopes:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
74
|
+
found_items: Optional[Dict[Union[str, CodeItem], CodeItem]] = current_scope.get(_ITEMS)
|
|
75
|
+
if found_items and (code_item := found_items.get(name)) is not None:
|
|
76
|
+
return code_item
|
|
61
77
|
|
|
62
78
|
return None
|
|
63
79
|
|
|
@@ -92,7 +92,9 @@ class CodeItem: # TODO: This should also be a dataclass, because hash and tuple
|
|
|
92
92
|
|
|
93
93
|
@property
|
|
94
94
|
def filename_with_position(self) -> str:
|
|
95
|
-
|
|
95
|
+
# .as_posix() (not str()) so output is forward-slash on every OS,
|
|
96
|
+
# matching how the path was originally given on the command line.
|
|
97
|
+
filename_with_position = self.filename.as_posix()
|
|
96
98
|
if self.name_line is not None:
|
|
97
99
|
filename_with_position += f':{self.name_line}'
|
|
98
100
|
if self.name_column is not None:
|
|
@@ -87,6 +87,12 @@ class DeadCodeVisitor(ast.NodeVisitor):
|
|
|
87
87
|
self.scopes = NestedScope()
|
|
88
88
|
self.tach_index = load_tach_index(args.tach_config)
|
|
89
89
|
|
|
90
|
+
# Caches getattr(self, 'visit_' + node type name, None) per AST node
|
|
91
|
+
# type, since visit() below is called once per AST node (potentially
|
|
92
|
+
# millions of times) and both the string concatenation and the
|
|
93
|
+
# getattr MRO walk are otherwise repeated for every single node.
|
|
94
|
+
self._visitor_cache: Dict[type, Optional[Callable[[ast.AST], None]]] = {}
|
|
95
|
+
|
|
90
96
|
@property
|
|
91
97
|
def scope(self) -> str:
|
|
92
98
|
return '.'.join(self.scope_parts)
|
|
@@ -510,8 +516,12 @@ class DeadCodeVisitor(ast.NodeVisitor):
|
|
|
510
516
|
self.should_ignore_new_definitions = True
|
|
511
517
|
should_turn_off_ignore_new_definitions = True
|
|
512
518
|
|
|
513
|
-
|
|
514
|
-
|
|
519
|
+
node_type = node.__class__
|
|
520
|
+
try:
|
|
521
|
+
visitor = self._visitor_cache[node_type]
|
|
522
|
+
except KeyError:
|
|
523
|
+
visitor = getattr(self, 'visit_' + node_type.__name__, None)
|
|
524
|
+
self._visitor_cache[node_type] = visitor
|
|
515
525
|
|
|
516
526
|
if visitor:
|
|
517
527
|
visitor(node)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
from fnmatch import fnmatch, fnmatchcase
|
|
3
|
+
from functools import lru_cache
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Iterable, Set, Union
|
|
5
6
|
|
|
@@ -63,14 +64,21 @@ def _is_special_name(name: str) -> bool:
|
|
|
63
64
|
|
|
64
65
|
def _match(name: Union[str, Path], patterns: Iterable[str], case: bool = True) -> bool:
|
|
65
66
|
func = fnmatchcase if case else fnmatch
|
|
66
|
-
|
|
67
|
+
# .as_posix() (not str()) for Path names, so a pattern like '*/tests/*'
|
|
68
|
+
# matches consistently regardless of the OS's native path separator.
|
|
69
|
+
name_str = name.as_posix() if isinstance(name, Path) else name
|
|
70
|
+
return any(func(name_str, pattern) for pattern in patterns)
|
|
67
71
|
|
|
68
72
|
|
|
69
73
|
def _match_many(names: Union[Iterable[str], Iterable[Path]], patterns: Iterable[str], case: bool = True) -> bool:
|
|
70
74
|
return any(_match(name, patterns, case) for name in names)
|
|
71
75
|
|
|
72
76
|
|
|
77
|
+
@lru_cache(maxsize=None)
|
|
73
78
|
def _is_test_file(filename: Path) -> bool:
|
|
79
|
+
# Called once per definition (function/class/method) in a file, so cache
|
|
80
|
+
# per-filename: `filename.resolve()` is a filesystem syscall and the
|
|
81
|
+
# answer never changes for repeated calls with the same file.
|
|
74
82
|
return _match(
|
|
75
83
|
filename.resolve(),
|
|
76
84
|
['*/test/*', '*/tests/*', '*/test*.py', '*[-_]test.py'],
|
|
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
|