lapsed 2.4.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.
- deadcode/__init__.py +8 -0
- deadcode/actions/__init__.py +0 -0
- deadcode/actions/find_python_filenames.py +57 -0
- deadcode/actions/find_unused_names.py +15 -0
- deadcode/actions/fix_or_show_unused_code.py +72 -0
- deadcode/actions/get_unused_names_error_message.py +37 -0
- deadcode/actions/merge_overlaping_file_parts.py +83 -0
- deadcode/actions/parse_abstract_syntax_tree.py +13 -0
- deadcode/actions/parse_arguments.py +250 -0
- deadcode/actions/parse_tach_config.py +207 -0
- deadcode/actions/remove_file_parts_from_content.py +166 -0
- deadcode/cli.py +46 -0
- deadcode/constants.py +49 -0
- deadcode/data_types.py +46 -0
- deadcode/utils/__init__.py +0 -0
- deadcode/utils/add_colors_to_diff.py +10 -0
- deadcode/utils/base_test_case.py +88 -0
- deadcode/utils/fix_indent.py +70 -0
- deadcode/utils/flatten_lists.py +19 -0
- deadcode/utils/nested_scopes.py +83 -0
- deadcode/utils/print_ast.py +5 -0
- deadcode/visitor/__init__.py +0 -0
- deadcode/visitor/code_item.py +140 -0
- deadcode/visitor/dead_code_visitor.py +563 -0
- deadcode/visitor/ignore.py +135 -0
- deadcode/visitor/lines.py +82 -0
- deadcode/visitor/noqa.py +68 -0
- deadcode/visitor/utils.py +87 -0
- lapsed-2.4.1.dist-info/METADATA +245 -0
- lapsed-2.4.1.dist-info/RECORD +33 -0
- lapsed-2.4.1.dist-info/WHEEL +4 -0
- lapsed-2.4.1.dist-info/entry_points.txt +2 -0
- lapsed-2.4.1.dist-info/licenses/LICENSE +661 -0
deadcode/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import List
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from deadcode.actions.parse_tach_config import load_tach_index
|
|
6
|
+
from deadcode.data_types import Args
|
|
7
|
+
from deadcode.visitor.ignore import _match
|
|
8
|
+
|
|
9
|
+
logger = getLogger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_python_filenames(args: Args) -> List[str]:
|
|
13
|
+
tach_index = load_tach_index(args.tach_config)
|
|
14
|
+
|
|
15
|
+
filenames = []
|
|
16
|
+
paths: List[str] = list(args.paths)
|
|
17
|
+
while paths:
|
|
18
|
+
path = Path(paths.pop())
|
|
19
|
+
|
|
20
|
+
if _match(path, args.exclude):
|
|
21
|
+
if args.verbose:
|
|
22
|
+
logger.info(f'Ignoring: {path}')
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if path.is_file() and path.suffix == '.py' and tach_index.is_unchecked(path):
|
|
26
|
+
if args.verbose:
|
|
27
|
+
logger.info(f'Ignoring (unchecked tach module): {path}')
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if path.is_file() and path.suffix == '.py':
|
|
31
|
+
filenames.append(str(path))
|
|
32
|
+
elif path.is_dir():
|
|
33
|
+
paths += list([str(p) for p in path.glob('*')])
|
|
34
|
+
elif not path.exists():
|
|
35
|
+
# TODO: unify error logging and reporting
|
|
36
|
+
# Maybe a cli flag could be added to stop on error
|
|
37
|
+
logger.error(f'Error: {path} could not be found.')
|
|
38
|
+
|
|
39
|
+
if args.verbose:
|
|
40
|
+
sep = '\n - '
|
|
41
|
+
logger.info(f'Files to be checked for dead code: {sep.join(filenames)}')
|
|
42
|
+
return filenames
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Should show errors, if files do not exist.
|
|
46
|
+
# Could mock directories: os.path.isfile os.walk -> replace this one with rglob
|
|
47
|
+
|
|
48
|
+
# Should use list dir myself.
|
|
49
|
+
|
|
50
|
+
# TODO: Whats the solution for mocking the file system checking?
|
|
51
|
+
# Every single provided file is assumed to exist.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# # TODO: investigate what can be reused from this module
|
|
55
|
+
# Loop over all given paths, abort if any ends with .pyc, add the other given
|
|
56
|
+
# files (even those not ending with .py) and collect all .py files under the
|
|
57
|
+
# given directories.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import List, Iterable
|
|
2
|
+
|
|
3
|
+
from deadcode.data_types import Args, Filename
|
|
4
|
+
from deadcode.visitor.code_item import CodeItem
|
|
5
|
+
from deadcode.visitor.dead_code_visitor import DeadCodeVisitor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_unused_names(
|
|
9
|
+
filenames: List[Filename],
|
|
10
|
+
args: Args,
|
|
11
|
+
) -> Iterable[CodeItem]:
|
|
12
|
+
dead_code_visitor = DeadCodeVisitor(filenames, args)
|
|
13
|
+
dead_code_visitor.visit_abstract_syntax_trees()
|
|
14
|
+
unused_code_items = dead_code_visitor.get_unused_code_items()
|
|
15
|
+
return unused_code_items
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from difflib import diff_bytes, unified_diff
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from deadcode.actions.merge_overlaping_file_parts import merge_overlaping_file_parts
|
|
7
|
+
from deadcode.actions.remove_file_parts_from_content import remove_file_parts_from_content
|
|
8
|
+
from deadcode.data_types import Args
|
|
9
|
+
from deadcode.visitor.code_item import CodeItem
|
|
10
|
+
from deadcode.utils.flatten_lists import flatten_list
|
|
11
|
+
from deadcode.utils.add_colors_to_diff import add_colors_to_diff
|
|
12
|
+
from deadcode.visitor.ignore import _match
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str:
|
|
16
|
+
# Reading and writing should be patched in this file.
|
|
17
|
+
|
|
18
|
+
# Group unused_names by filenames
|
|
19
|
+
# For each filename find file parts, which have to be fixed.
|
|
20
|
+
# And fix those file parts.
|
|
21
|
+
#
|
|
22
|
+
# TODO: trailing commas, trailing white spaces and empty lines
|
|
23
|
+
|
|
24
|
+
filename_to_unused_items = defaultdict(list)
|
|
25
|
+
for unused_item in unused_items:
|
|
26
|
+
filename_to_unused_items[str(unused_item.filename)].append(unused_item)
|
|
27
|
+
|
|
28
|
+
result = []
|
|
29
|
+
|
|
30
|
+
for filename, file_unused_items in filename_to_unused_items.items():
|
|
31
|
+
file_parts = flatten_list([item.code_parts for item in file_unused_items])
|
|
32
|
+
|
|
33
|
+
unused_file_parts = merge_overlaping_file_parts(file_parts)
|
|
34
|
+
|
|
35
|
+
with open(filename, 'rb') as f:
|
|
36
|
+
file_content_lines = f.readlines()
|
|
37
|
+
|
|
38
|
+
updated_file_content_lines = remove_file_parts_from_content(file_content_lines, unused_file_parts)
|
|
39
|
+
updated_file_content = b''.join(updated_file_content_lines)
|
|
40
|
+
if updated_file_content.strip():
|
|
41
|
+
if not args.only or _match(filename, args.only):
|
|
42
|
+
if args.dry:
|
|
43
|
+
with open(filename, 'rb') as f:
|
|
44
|
+
filename_bytes = filename.encode()
|
|
45
|
+
diff = diff_bytes(
|
|
46
|
+
unified_diff,
|
|
47
|
+
f.readlines(),
|
|
48
|
+
updated_file_content_lines,
|
|
49
|
+
fromfile=filename_bytes,
|
|
50
|
+
tofile=filename_bytes,
|
|
51
|
+
)
|
|
52
|
+
# TODO: consider printing result instantly to save memory
|
|
53
|
+
result_chunk = b''.join(diff)
|
|
54
|
+
if args.no_color:
|
|
55
|
+
result.append(result_chunk)
|
|
56
|
+
else:
|
|
57
|
+
result.append(add_colors_to_diff(result_chunk))
|
|
58
|
+
|
|
59
|
+
elif args.fix:
|
|
60
|
+
with open(filename, 'wb') as f:
|
|
61
|
+
# TODO: is there a method writelines?
|
|
62
|
+
f.write(updated_file_content)
|
|
63
|
+
else:
|
|
64
|
+
os.remove(filename)
|
|
65
|
+
|
|
66
|
+
if result:
|
|
67
|
+
return b'\n'.join(result).decode()
|
|
68
|
+
return ''
|
|
69
|
+
|
|
70
|
+
# TODO: update this one: solution is to use read and write operations.
|
|
71
|
+
#
|
|
72
|
+
# Solution is to open file for reading and then for writing.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Iterable, Optional
|
|
2
|
+
|
|
3
|
+
from deadcode.data_types import Args
|
|
4
|
+
from deadcode.visitor.code_item import CodeItem
|
|
5
|
+
from deadcode.visitor.ignore import _match
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) -> Optional[str]:
|
|
9
|
+
unused_names = list(unused_names)
|
|
10
|
+
|
|
11
|
+
if not unused_names:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
if args.quiet:
|
|
15
|
+
return ''
|
|
16
|
+
|
|
17
|
+
if args.count:
|
|
18
|
+
return f'{len(unused_names)}'
|
|
19
|
+
|
|
20
|
+
messages = []
|
|
21
|
+
for item in unused_names:
|
|
22
|
+
if not args.only or _match(item.filename, args.only):
|
|
23
|
+
message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
|
|
24
|
+
message += item.message or (
|
|
25
|
+
f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
|
|
26
|
+
)
|
|
27
|
+
if args.no_color:
|
|
28
|
+
message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
|
|
29
|
+
messages.append(message)
|
|
30
|
+
|
|
31
|
+
if args.fix:
|
|
32
|
+
message = f"\nRemoved \033[1m{len(messages)}\033[0m unused code item{'s' if len(messages) > 1 else ''}!"
|
|
33
|
+
if args.no_color:
|
|
34
|
+
message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
|
|
35
|
+
messages.append(message)
|
|
36
|
+
|
|
37
|
+
return '\n'.join(messages)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
from deadcode.data_types import Part
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def does_include(bigger_part: Part, smaller_part: Part) -> bool:
|
|
6
|
+
line_start_b, line_end_b, col_start_b, col_end_b = bigger_part
|
|
7
|
+
line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
|
|
8
|
+
|
|
9
|
+
starts_later = (line_start_b > line_start_s) or ((line_start_b == line_start_s) and (col_start_b >= col_start_s))
|
|
10
|
+
|
|
11
|
+
ends_faster = (line_end_b < line_end_s) or ((line_end_b == line_end_b) and (col_end_b <= col_end_s))
|
|
12
|
+
|
|
13
|
+
return bool(starts_later and ends_faster)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sort_parts(bigger_part: Part, smaller_part: Part) -> Tuple[Part, Part]:
|
|
17
|
+
"""Returns code part which begins first following by another code part."""
|
|
18
|
+
# TODO: Column should go first (tuple comparison would be possible)
|
|
19
|
+
line_start_b, line_end_b, col_start_b, col_end_b = bigger_part
|
|
20
|
+
line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
|
|
21
|
+
|
|
22
|
+
# Pirmo pradžios eilutė ir stulpelis yra pirmiau.
|
|
23
|
+
if (line_start_s < line_start_b) or ((line_start_s == line_start_b) and (col_start_s < col_start_b)):
|
|
24
|
+
return smaller_part, bigger_part
|
|
25
|
+
return bigger_part, smaller_part
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def does_overlap(bigger_part: Part, smaller_part: Part) -> bool:
|
|
29
|
+
bigger_part, smaller_part = sort_parts(bigger_part, smaller_part)
|
|
30
|
+
|
|
31
|
+
line_start_b, line_end_b, col_start_b, col_end_b = bigger_part
|
|
32
|
+
line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
|
|
33
|
+
|
|
34
|
+
# ar pirmo pabaiga perlipa antro pradžia
|
|
35
|
+
return bool((line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def merge_parts(p1: Part, p2: Part) -> Optional[Part]:
|
|
39
|
+
p1, p2 = sort_parts(p1, p2)
|
|
40
|
+
|
|
41
|
+
line_start1, line_end1, col_start1, col_end1 = p1
|
|
42
|
+
line_start2, line_end2, col_start2, col_end2 = p2
|
|
43
|
+
|
|
44
|
+
if (line_end1 > line_start2) or ((line_end1 == line_start2) and (col_end1 > col_start2)):
|
|
45
|
+
line_start = line_start1
|
|
46
|
+
col_start = col_start1
|
|
47
|
+
if line_end1 > line_end2:
|
|
48
|
+
line_end = line_end1
|
|
49
|
+
col_end = col_end1
|
|
50
|
+
elif line_end2 > line_end1:
|
|
51
|
+
line_end = line_end2
|
|
52
|
+
col_end = col_end2
|
|
53
|
+
else:
|
|
54
|
+
line_end = line_end1
|
|
55
|
+
col_end = max(col_end1, col_end2)
|
|
56
|
+
return Part(line_start, line_end, col_start, col_end)
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def merge_overlaping_file_parts(overlaping_file_parts: List[Part]) -> List[Part]:
|
|
62
|
+
# Make algorithm O(n^2) by checking every single part if overlaps
|
|
63
|
+
|
|
64
|
+
non_overlaping_file_parts: List[Part] = []
|
|
65
|
+
for p1 in sorted(overlaping_file_parts):
|
|
66
|
+
merged_part = None
|
|
67
|
+
merged_with_index = None
|
|
68
|
+
for j, p2 in enumerate(non_overlaping_file_parts):
|
|
69
|
+
if does_include(p2, p1):
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if does_overlap(p2, p1):
|
|
73
|
+
merged_part = merge_parts(p2, p1)
|
|
74
|
+
merged_with_index = j
|
|
75
|
+
break
|
|
76
|
+
else:
|
|
77
|
+
non_overlaping_file_parts.append(p1)
|
|
78
|
+
|
|
79
|
+
if merged_part and merged_with_index is not None:
|
|
80
|
+
non_overlaping_file_parts.pop(merged_with_index)
|
|
81
|
+
non_overlaping_file_parts.append(merged_part)
|
|
82
|
+
|
|
83
|
+
return non_overlaping_file_parts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
from deadcode.data_types import Args, FileContent, AbstractSyntaxTree
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_abstract_syntax_tree(file_content: FileContent, args: Args, filename: str) -> AbstractSyntaxTree:
|
|
7
|
+
"""Did my code coverage just increased? Answer is no: a doc string was ignored :tada:"""
|
|
8
|
+
try:
|
|
9
|
+
return ast.parse(file_content, filename=filename, type_comments=True)
|
|
10
|
+
except: # noqa: E722
|
|
11
|
+
if not args.count and not args.quiet:
|
|
12
|
+
print(f'Error: Failed to parse {filename} file, ignoring it.')
|
|
13
|
+
return ast.Module()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 11):
|
|
7
|
+
import tomllib
|
|
8
|
+
else:
|
|
9
|
+
import tomli as tomllib
|
|
10
|
+
|
|
11
|
+
from deadcode.data_types import Args
|
|
12
|
+
from deadcode.utils.flatten_lists import flatten_lists_of_comma_separated_values
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_arguments(args: Optional[List[str]]) -> Args:
|
|
16
|
+
"""Parses arguments (execution options) for deadcode tool.
|
|
17
|
+
|
|
18
|
+
Arguments for DeadCode can be provided via:
|
|
19
|
+
- via command line
|
|
20
|
+
- via pyproject.toml
|
|
21
|
+
- via function arguments
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if not args:
|
|
25
|
+
args = sys.argv[1:]
|
|
26
|
+
|
|
27
|
+
# Docs: https://docs.python.org/3/library/argparse.html
|
|
28
|
+
parser = argparse.ArgumentParser()
|
|
29
|
+
parser.add_argument('paths', help='Paths where to search for python files', nargs='+')
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
'--fix',
|
|
32
|
+
help='Automatically remove detected unused code expressions from the code base.',
|
|
33
|
+
action='store_true',
|
|
34
|
+
default=False,
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'--dry',
|
|
38
|
+
help='Show changes which would be made in files with --fix option.',
|
|
39
|
+
action='store_true',
|
|
40
|
+
default=False,
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
'--only',
|
|
44
|
+
help='Filenames (or path expressions), that will be reflected in the output and modified.',
|
|
45
|
+
nargs='*',
|
|
46
|
+
action='append',
|
|
47
|
+
default=[],
|
|
48
|
+
type=str,
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
'--exclude',
|
|
52
|
+
help='Filenames (or path expressions), which will be completely skipped without being analysed.',
|
|
53
|
+
nargs='*',
|
|
54
|
+
action='append',
|
|
55
|
+
default=[],
|
|
56
|
+
type=str,
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
'--ignore-names',
|
|
60
|
+
help=(
|
|
61
|
+
'Removes provided list of names from the output. '
|
|
62
|
+
'Regexp expressions to match multiple names can also be provided.'
|
|
63
|
+
),
|
|
64
|
+
nargs='*',
|
|
65
|
+
action='append',
|
|
66
|
+
default=[],
|
|
67
|
+
type=str,
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
'--ignore-bodies-of',
|
|
71
|
+
help='Ignores body of an expression if its name matches any of the provided names.',
|
|
72
|
+
nargs='*',
|
|
73
|
+
action='append',
|
|
74
|
+
default=[],
|
|
75
|
+
type=str,
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
'--ignore-bodies-if-decorated-with',
|
|
79
|
+
help='Ignores body of an expression if its decorated with one of the provided decorator names.',
|
|
80
|
+
nargs='*',
|
|
81
|
+
action='append',
|
|
82
|
+
default=[],
|
|
83
|
+
type=str,
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
'--ignore-bodies-if-inherits-from',
|
|
87
|
+
help='Ignores body of a class if it inherits from any of the provided class names.',
|
|
88
|
+
nargs='*',
|
|
89
|
+
action='append',
|
|
90
|
+
default=[],
|
|
91
|
+
type=str,
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
'--ignore-definitions',
|
|
95
|
+
help=(
|
|
96
|
+
'Ignores definition (including name and body) if a '
|
|
97
|
+
'name of an expression matches any of the provided ones.'
|
|
98
|
+
),
|
|
99
|
+
nargs='*',
|
|
100
|
+
action='append',
|
|
101
|
+
default=[],
|
|
102
|
+
type=str,
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
'--ignore-definitions-if-inherits-from',
|
|
106
|
+
help=(
|
|
107
|
+
'Ignores definition (including name and body) of a class if '
|
|
108
|
+
'it inherits from any of the provided class names.'
|
|
109
|
+
),
|
|
110
|
+
nargs='*',
|
|
111
|
+
action='append',
|
|
112
|
+
default=[],
|
|
113
|
+
type=str,
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
'--ignore-definitions-if-decorated-with',
|
|
117
|
+
help=(
|
|
118
|
+
'Ignores definition (including name and body) of an expression, '
|
|
119
|
+
'which is decorated with any of the provided decorator names.'
|
|
120
|
+
),
|
|
121
|
+
nargs='*',
|
|
122
|
+
action='append',
|
|
123
|
+
default=[],
|
|
124
|
+
type=str,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
'--ignore-if-decorated-with',
|
|
129
|
+
help='Ignores both the name and its definition if its decorated with one of the provided decorator names.',
|
|
130
|
+
nargs='*',
|
|
131
|
+
action='append',
|
|
132
|
+
default=[],
|
|
133
|
+
type=str,
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
'--ignore-if-inherits-from',
|
|
137
|
+
help='Ignores both the name and its definition if the class inerits from the provided class name.',
|
|
138
|
+
nargs='*',
|
|
139
|
+
action='append',
|
|
140
|
+
default=[],
|
|
141
|
+
type=str,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
'--ignore-names-if-inherits-from',
|
|
146
|
+
help='Ignores names of classes, which inherit from provided class names.',
|
|
147
|
+
nargs='*',
|
|
148
|
+
action='append',
|
|
149
|
+
default=[],
|
|
150
|
+
type=str,
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
'--ignore-names-if-decorated-with',
|
|
154
|
+
help='Ignores names of an expression, which is decorated with one of the provided decorator names.',
|
|
155
|
+
nargs='*',
|
|
156
|
+
action='append',
|
|
157
|
+
default=[],
|
|
158
|
+
type=str,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
'--ignore-names-in-files',
|
|
163
|
+
help='Ignores unused names in files, which filenames match provided path expressions.',
|
|
164
|
+
nargs='*',
|
|
165
|
+
action='append',
|
|
166
|
+
default=[],
|
|
167
|
+
type=str,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
'--tach-config',
|
|
172
|
+
help=(
|
|
173
|
+
'Paths to one or more tach.toml files (https://docs.gauge.sh) whose [[interfaces]] '
|
|
174
|
+
'declarations mark names as public API (never reported as unused), and whose '
|
|
175
|
+
'unchecked modules are skipped entirely.'
|
|
176
|
+
),
|
|
177
|
+
nargs='*',
|
|
178
|
+
action='append',
|
|
179
|
+
default=[],
|
|
180
|
+
type=str,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
'--no-color',
|
|
185
|
+
help='Turn off colors in the output',
|
|
186
|
+
action='store_true',
|
|
187
|
+
default=False,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
'--quiet',
|
|
192
|
+
help='Does not output anything. Makefile still fails with exit code 1 if unused names are found.',
|
|
193
|
+
action='store_true',
|
|
194
|
+
default=False,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
'--count',
|
|
199
|
+
help='Provides the count of the detected unused names instead of printing them all out.',
|
|
200
|
+
action='store_true',
|
|
201
|
+
default=False,
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
'-v',
|
|
205
|
+
'--verbose',
|
|
206
|
+
help='Shows logs useful for debuging',
|
|
207
|
+
action='store_true',
|
|
208
|
+
default=False,
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
'--version',
|
|
212
|
+
help='Shows deadcode version',
|
|
213
|
+
action='store_true',
|
|
214
|
+
default=False,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
parsed_args = parser.parse_args(args).__dict__
|
|
218
|
+
|
|
219
|
+
for arg_name, arg_value in parsed_args.items():
|
|
220
|
+
if isinstance(arg_value, list) and arg_name != 'paths':
|
|
221
|
+
parsed_args[arg_name] = flatten_lists_of_comma_separated_values(parsed_args.get(arg_name))
|
|
222
|
+
|
|
223
|
+
# Extend the Args with the values provided in the pyproject.toml
|
|
224
|
+
for key, item in parse_pyproject_toml().items():
|
|
225
|
+
if key in parsed_args:
|
|
226
|
+
parsed_args[key].extend(item)
|
|
227
|
+
|
|
228
|
+
# Do not fix if dry option is provided:
|
|
229
|
+
if parsed_args['dry']:
|
|
230
|
+
parsed_args['fix'] = False
|
|
231
|
+
|
|
232
|
+
return Args(**parsed_args)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def parse_pyproject_toml() -> Dict[str, Any]:
|
|
236
|
+
"""Parse a pyproject toml file, pulling out relevant parts for Black.
|
|
237
|
+
|
|
238
|
+
If parsing fails, will raise a tomllib.TOMLDecodeError.
|
|
239
|
+
Copied from: https://github.com/psf/black/blob/01b8d3d4095ebdb91d0d39012a517931625c63cb/src/black/files.py#LL113C15-L113C15
|
|
240
|
+
"""
|
|
241
|
+
pyproject_toml_filename = 'pyproject.toml'
|
|
242
|
+
if not os.path.isfile(pyproject_toml_filename):
|
|
243
|
+
return {}
|
|
244
|
+
|
|
245
|
+
with open(pyproject_toml_filename, 'rb') as f:
|
|
246
|
+
pyproject_toml = tomllib.load(f)
|
|
247
|
+
|
|
248
|
+
config: Dict[str, Any] = pyproject_toml.get('tool', {}).get('deadcode', {})
|
|
249
|
+
config = {k.replace('--', '').replace('-', '_'): v for k, v in config.items()}
|
|
250
|
+
return config
|