deadcode 0.0.2__py3-none-any.whl → 0.1.0__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.
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ from deadcode.data_types import Args
4
+ from deadcode.utils.path_matching import does_path_match_patterns
5
+
6
+
7
+ def find_python_filenames(args: Args):
8
+ """Recursively searches for Python filenames in provided paths."""
9
+ python_filenames = []
10
+ for path in args.paths:
11
+ if does_path_match_patterns(path, args.exclude):
12
+ continue
13
+
14
+ if os.path.isfile(path):
15
+ filename = path
16
+ if os.path.splitext(filename)[1] == ".py":
17
+ python_filenames.append(filename)
18
+ else:
19
+ for path, _, filenames in os.walk(path):
20
+ if does_path_match_patterns(path, args.exclude):
21
+ continue
22
+
23
+ for f in filenames:
24
+ filename = os.path.join(path, f)
25
+ if os.path.splitext(filename)[1] == ".py" and not does_path_match_patterns(filename, args.exclude):
26
+ python_filenames.append(filename)
27
+
28
+ return python_filenames
@@ -0,0 +1,26 @@
1
+ import re
2
+ from typing import Dict
3
+ from collections import defaultdict
4
+
5
+ from deadcode.data_types import Args, Filename, VariableName, AbstractSyntaxTree
6
+
7
+
8
+ def find_unused_names(
9
+ files: Dict[Filename, AbstractSyntaxTree],
10
+ global_names: Dict[VariableName, Filename],
11
+ args: Args,
12
+ ) -> Dict[VariableName, Filename]:
13
+ name_occourencies: Dict[str, int] = defaultdict(int)
14
+ for name in global_names.keys():
15
+ pattern = re.compile(
16
+ rf"FunctionDef\(name=\'{name}\'|ClassDef\(name=\'{name}\'|Name\(id=\'{name}\'|alias\(name=\'{name}\'"
17
+ )
18
+ for abstract_syntax_tree in files.values():
19
+ name_occourencies[name] += len(re.findall(pattern, abstract_syntax_tree))
20
+
21
+ unused_variables = {
22
+ name: filename
23
+ for name, filename in global_names.items()
24
+ if name_occourencies[name] == 1 and name not in args.ignore_names
25
+ }
26
+ return unused_variables
@@ -0,0 +1,19 @@
1
+ from typing import Dict, Optional
2
+
3
+ from deadcode.data_types import Filename, VariableName
4
+ from deadcode.data_types import Args
5
+
6
+
7
+ def get_unused_names_error_message(unused_names: Dict[VariableName, Filename], args: Args) -> Optional[str]:
8
+ if not unused_names:
9
+ return None
10
+
11
+ if args.no_color:
12
+ return "\n".join([f"{filename} DC100 Global {name} is never used" for name, filename in unused_names.items()])
13
+
14
+ return "\n".join(
15
+ [
16
+ f"{filename} \033[91mDC100\033[0m Global \033[1m{name}\033[0m is never used"
17
+ for name, filename in unused_names.items()
18
+ ]
19
+ )
@@ -0,0 +1,16 @@
1
+ import ast
2
+ from typing import Dict
3
+
4
+ from deadcode.data_types import FileContent, Filename, AbstractSyntaxTree
5
+
6
+
7
+ def parse_abstract_syntax_trees(files: Dict[Filename, FileContent]) -> Dict[Filename, AbstractSyntaxTree]:
8
+ """Did my code coverage just increased? Answer is no: a doc string was ignored :tada:"""
9
+ abstract_syntax_trees_for_files = {}
10
+ for filename, file_content in files.items():
11
+ try:
12
+ abstract_syntax_trees_for_files[filename] = ast.dump(ast.parse(file_content)) # Note: indent=1 for debug
13
+ except: # noqa: E722
14
+ print(f"Error: Failed to parse {filename} file, ignoring it.")
15
+ abstract_syntax_trees_for_files[filename] = ""
16
+ return abstract_syntax_trees_for_files
@@ -0,0 +1,88 @@
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
+ "--exclude",
32
+ help="PATHS to files not to analyse at all (comma separated values)",
33
+ nargs="*",
34
+ action="append",
35
+ default=[],
36
+ type=str,
37
+ )
38
+ parser.add_argument(
39
+ "--ignore-names",
40
+ help="NAMES not to report (comma separated values)",
41
+ nargs="*",
42
+ action="append",
43
+ default=[],
44
+ type=str,
45
+ )
46
+ parser.add_argument(
47
+ "--ignore-names-in-files",
48
+ help="PATHS to files not to report NAMES from (comma separated values)",
49
+ nargs="*",
50
+ action="append",
51
+ default=[],
52
+ type=str,
53
+ )
54
+ parser.add_argument(
55
+ "--no-color",
56
+ help="Turn off colors in the output",
57
+ action="store_true",
58
+ default=False,
59
+ )
60
+
61
+ parsed_args = parser.parse_args(args).__dict__
62
+
63
+ for field_name in ["exclude", "ignore_names", "ignore_names_in_files"]:
64
+ parsed_args[field_name] = flatten_lists_of_comma_separated_values(parsed_args.get(field_name))
65
+
66
+ # Extend the Args with the values provided in the pyproject.toml
67
+ for key, item in parse_pyproject_toml().items():
68
+ if key in parsed_args:
69
+ parsed_args[key].extend(item)
70
+
71
+ return Args(**parsed_args)
72
+
73
+
74
+ def parse_pyproject_toml() -> Dict[str, Any]:
75
+ """Parse a pyproject toml file, pulling out relevant parts for Black.
76
+
77
+ If parsing fails, will raise a tomllib.TOMLDecodeError.
78
+ Copied from: https://github.com/psf/black/blob/01b8d3d4095ebdb91d0d39012a517931625c63cb/src/black/files.py#LL113C15-L113C15
79
+ """
80
+ pyproject_toml_filename = "pyproject.toml"
81
+ if not os.path.isfile(pyproject_toml_filename):
82
+ return {}
83
+
84
+ with open(pyproject_toml_filename, "rb") as f:
85
+ pyproject_toml = tomllib.load(f)
86
+ config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("deadcode", {})
87
+ config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
88
+ return config
@@ -0,0 +1,33 @@
1
+ import re
2
+ from typing import Dict
3
+
4
+ from deadcode.utils.path_matching import does_path_match_patterns
5
+ from deadcode.data_types import (
6
+ Args,
7
+ FileContent,
8
+ Filename,
9
+ VariableName,
10
+ FilenameWithPosition,
11
+ )
12
+
13
+
14
+ def parse_global_names(files: Dict[Filename, FileContent], args: Args) -> Dict[VariableName, FilenameWithPosition]:
15
+ patterns = [
16
+ re.compile(r"^(\w+)\s*="),
17
+ re.compile(r"^def\s+(\w+)\s*\(?"),
18
+ re.compile(r"^class\s+(\w+)\s*\(?:?"),
19
+ ]
20
+
21
+ global_variable_names = {}
22
+ for filename, file_content in files.items():
23
+ if does_path_match_patterns(filename, args.ignore_names_in_files):
24
+ continue
25
+
26
+ for pattern in patterns:
27
+ for line_nr, line in enumerate(file_content.split("\n"), 1):
28
+ if global_variable_name_match := re.search(pattern, line):
29
+ variable_name = global_variable_name_match.groups()[0]
30
+ column_nr = global_variable_name_match.regs[-1][0]
31
+ global_variable_names[variable_name] = f"{filename}:{line_nr}:{column_nr}:"
32
+
33
+ return global_variable_names
@@ -0,0 +1,11 @@
1
+ from typing import Dict, List
2
+
3
+ from deadcode.data_types import FileContent, Filename
4
+
5
+
6
+ def read_files(filenames: List[Filename]) -> Dict[Filename, FileContent]:
7
+ files = {}
8
+ for filename in filenames:
9
+ with open(filename, "r") as f:
10
+ files[filename] = f.read()
11
+ return files
deadcode/cli.py CHANGED
@@ -1,137 +1,34 @@
1
- from typing import Dict, List, Optional
2
- from collections import defaultdict
3
- import os
4
- import sys
5
- import re
6
- from fnmatch import fnmatch
1
+ from typing import List, Optional
7
2
 
8
- from deadcode.data_types import Args, FileContent, Filename, Pathname, VariableName
9
- from deadcode.actions.argument_parsing import parse_command_line_arguments
3
+ from deadcode.actions.find_python_filenames import find_python_filenames
4
+ from deadcode.actions.find_unused_names import find_unused_names
5
+ from deadcode.actions.parse_arguments import parse_arguments
6
+ from deadcode.actions.parse_global_names import parse_global_names
7
+ from deadcode.actions.read_files import read_files
10
8
 
9
+ from deadcode.actions.parse_abstract_syntax_trees import parse_abstract_syntax_trees
10
+ from deadcode.actions.get_unused_names_error_message import (
11
+ get_unused_names_error_message,
12
+ )
11
13
 
12
- def does_path_match_patterns(path: Pathname, patterns: Optional[List[Pathname]]) -> bool:
13
- if path != "." and patterns:
14
- for pattern in patterns:
15
- if fnmatch(path, pattern):
16
- return True
17
- return False
18
14
 
19
-
20
- def find_python_filenames(args: Args):
21
- """Recursively searches for Python filenames in provided paths."""
22
- python_filenames = []
23
- for path in args.paths:
24
- if does_path_match_patterns(path, args.exclude):
25
- continue
26
-
27
- if os.path.isfile(path):
28
- filename = path
29
- if os.path.splitext(filename)[1] == ".py":
30
- python_filenames.append(filename)
31
- else:
32
- for path, _, filenames in os.walk(path):
33
- if does_path_match_patterns(path, args.exclude):
34
- continue
35
-
36
- for f in filenames:
37
- filename = os.path.join(path, f)
38
- if (os.path.splitext(filename)[1] == ".py" and
39
- not does_path_match_patterns(filename, args.exclude)):
40
- python_filenames.append(filename)
41
-
42
- return python_filenames
43
-
44
-
45
- def read_files(filenames: List[Filename]) -> Dict[Filename, FileContent]:
46
- files = {}
47
- for filename in filenames:
48
- with open(filename, "r") as f:
49
- files[filename] = f.read()
50
- return files
51
-
52
-
53
- def parse_global_names(
54
- files: Dict[Filename, FileContent], args: Args
55
- ) -> Dict[VariableName, Filename]:
56
- patterns = [
57
- r"^(\w+)\s*=",
58
- r"^def\s+(\w+)\s*\(?",
59
- r"^class\s+(\w+)\s*\(?:?",
60
- ]
61
-
62
- global_variable_names = {}
63
- for filename, file_content in files.items():
64
- if does_path_match_patterns(filename, args.ignore_files):
65
- continue
66
-
67
- for pattern in patterns:
68
- for line_nr, line in enumerate(file_content.split("\n"), 1):
69
- if global_variable_name_in_file := re.findall(pattern, line):
70
- variable_name = global_variable_name_in_file[0]
71
- # TODO: could find the column name and add it to the output.
72
- global_variable_names[variable_name] = f"{filename}:{line_nr}"
73
-
74
- return global_variable_names
75
-
76
-
77
- def remove_comments_from_file_content(file_content: str) -> str:
78
- # TODO: add a unit test for this feature.
79
- file_lines_without_comments = []
80
- for line in file_content:
81
- if "#" in line:
82
- # TODO: should ignore # in strings.
83
- line = line.split("#", 1)[0]
84
- file_lines_without_comments.append(line)
85
- return "".join(file_lines_without_comments)
86
-
87
-
88
- def remove_comments(files: Dict[Filename, FileContent]) -> Dict[Filename, FileContent]:
89
- files_without_comments = {}
90
- for filename, file_content in files.items():
91
- files_without_comments[filename] = remove_comments_from_file_content(file_content)
92
- return files_without_comments
93
-
94
-
95
- def find_unused_names(
96
- files: Dict[str, str], global_names: Dict[VariableName, Filename], args: Args
97
- ) -> Dict[VariableName, Filename]:
98
- name_occourencies = defaultdict(int)
99
- for name in global_names.keys():
100
- for file_content in files.values():
101
- name_occourencies[name] += file_content.count(name)
102
-
103
- unused_variables = {
104
- name: filename
105
- for name, filename in global_names.items()
106
- if name_occourencies[name] == 1 and name not in args.ignore_names
107
- }
108
- return unused_variables
109
-
110
-
111
- def print_unused_names(unused_names: Dict[VariableName, Filename]):
112
- # TODO: add line number to output
113
- for name, filename in unused_names.items():
114
- print(
115
- filename,
116
- "\033[91mF851\033[0m",
117
- f"Global \033[1m{name}\033[0m is never used",
118
- )
119
-
120
-
121
- def main(command_line_args: Optional[List[str]] = None) -> Dict[VariableName, Filename]:
122
- args = parse_command_line_arguments(command_line_args)
15
+ def main(
16
+ command_line_args: Optional[List[str]] = None,
17
+ ) -> Optional[str]:
18
+ args = parse_arguments(command_line_args)
123
19
 
124
20
  python_filenames = find_python_filenames(args=args)
125
21
  files = read_files(python_filenames)
126
- files_without_comments = remove_comments(files)
127
22
 
128
23
  global_names = parse_global_names(files, args=args)
129
- unused_names = find_unused_names(files_without_comments, global_names, args=args)
24
+ abstract_syntax_trees_of_files = parse_abstract_syntax_trees(files)
25
+ unused_names = find_unused_names(abstract_syntax_trees_of_files, global_names, args=args)
26
+
27
+ if error_message := get_unused_names_error_message(unused_names, args=args):
28
+ return error_message
130
29
 
131
- print_unused_names(unused_names)
132
- if unused_names:
133
- sys.exit(1)
134
- return unused_names
30
+ print("\033[1mWell done!\033[0m ✨ 🚀 ✨")
31
+ return None
135
32
 
136
33
 
137
34
  if __name__ == "__main__":
deadcode/data_types.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from dataclasses import dataclass
2
- from typing import List, Optional
2
+ from typing import List
3
3
 
4
4
 
5
+ AbstractSyntaxTree = str
5
6
  FileContent = str
6
7
  Filename = str # Contains full path to existing file
8
+ FilenameWithPosition = str # filename:line:column:
7
9
  Pathname = str # Can contain wildewards
8
10
  VariableName = str
9
11
 
@@ -13,4 +15,5 @@ class Args:
13
15
  paths: List[Pathname]
14
16
  exclude: List[Pathname]
15
17
  ignore_names: List[Pathname]
16
- ignore_files: List[Pathname]
18
+ ignore_names_in_files: List[Pathname]
19
+ no_color: bool
@@ -0,0 +1,15 @@
1
+ class UnusedClass(object):
2
+ pass
3
+
4
+
5
+ class ThisClassIsUsed:
6
+ name = "One"
7
+
8
+
9
+ instance_of_a_used_class = ThisClassIsUsed()
10
+ print(instance_of_a_used_class)
11
+
12
+
13
+ class AnotherUnusedClass:
14
+ def __init__(self):
15
+ pass
@@ -0,0 +1,18 @@
1
+ def unused_function():
2
+ if 2 > 1:
3
+ pass
4
+
5
+
6
+ def this_one_is_used():
7
+ pass
8
+
9
+
10
+ this_one_is_used()
11
+
12
+
13
+ def another_unused_function(arg1: str = "", arg2: str = "Hello") -> None:
14
+ print(arg1, arg2)
15
+
16
+
17
+ # def this_method_is_commented():
18
+ # pass
@@ -0,0 +1,8 @@
1
+ unused_global_variable = True
2
+
3
+ ANOTHER_GLOBAL_VARIABLE = "This variable is unused"
4
+
5
+ third_global_varialbe = 12 * 25
6
+
7
+ THIS_ONE_IS_USED = "World"
8
+ print(THIS_ONE_IS_USED)