deadcode 2.2.2__py3-none-any.whl → 2.3.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.
Files changed (65) hide show
  1. deadcode/actions/find_python_filenames.py +6 -6
  2. deadcode/actions/fix_or_show_unused_code.py +66 -0
  3. deadcode/actions/get_unused_names_error_message.py +6 -6
  4. deadcode/actions/merge_overlaping_file_parts.py +2 -7
  5. deadcode/actions/parse_abstract_syntax_tree.py +1 -1
  6. deadcode/actions/parse_arguments.py +98 -80
  7. deadcode/actions/remove_file_parts_from_content.py +36 -19
  8. deadcode/cli.py +7 -6
  9. deadcode/constants.py +36 -36
  10. deadcode/data_types.py +2 -1
  11. deadcode/utils/add_colors_to_diff.py +10 -0
  12. deadcode/{tests/base.py → utils/base_test_case.py} +18 -18
  13. deadcode/utils/fix_indent.py +62 -29
  14. deadcode/utils/flatten_lists.py +2 -2
  15. deadcode/utils/nested_scopes.py +52 -16
  16. deadcode/utils/print_ast.py +1 -1
  17. deadcode/visitor/code_item.py +17 -27
  18. deadcode/visitor/dead_code_visitor.py +96 -197
  19. deadcode/visitor/ignore.py +29 -29
  20. deadcode/visitor/lines.py +3 -3
  21. deadcode/visitor/noqa.py +34 -21
  22. deadcode/visitor/utils.py +1 -1
  23. {deadcode-2.2.2.dist-info → deadcode-2.3.1.dist-info}/METADATA +90 -31
  24. deadcode-2.3.1.dist-info/RECORD +32 -0
  25. {deadcode-2.2.2.dist-info → deadcode-2.3.1.dist-info}/WHEEL +1 -1
  26. call_arg_map_to_parameters_poc.py +0 -173
  27. deadcode/actions/fix_unused_code.py +0 -43
  28. deadcode/tests/actions/test_find_python_filenames.py +0 -36
  29. deadcode/tests/actions/test_merge_overlaping_file_parts.py +0 -62
  30. deadcode/tests/cli_args/test_count.py +0 -0
  31. deadcode/tests/cli_args/test_exclude.py +0 -24
  32. deadcode/tests/cli_args/test_fix.py +0 -528
  33. deadcode/tests/cli_args/test_ignore_bodies_if_inherits_from.py +0 -46
  34. deadcode/tests/cli_args/test_ignore_definitions.py +0 -106
  35. deadcode/tests/cli_args/test_ignore_definitions_if_inherits_from.py +0 -125
  36. deadcode/tests/cli_args/test_ignore_names.py +0 -79
  37. deadcode/tests/cli_args/test_ignore_names_in_files.py +0 -0
  38. deadcode/tests/cli_args/test_no_color.py +0 -0
  39. deadcode/tests/cli_args/test_parsing.py +0 -66
  40. deadcode/tests/cli_args/test_quiet.py +0 -0
  41. deadcode/tests/cli_args/test_verbose.py +0 -0
  42. deadcode/tests/code_item/test_code_item.py +0 -16
  43. deadcode/tests/files/classes.py +0 -15
  44. deadcode/tests/files/functions.py +0 -21
  45. deadcode/tests/files/subdir/another_subdir/__init__.py +0 -1
  46. deadcode/tests/files/subdir/one_more_file.py +0 -2
  47. deadcode/tests/files/variables.py +0 -8
  48. deadcode/tests/fix/test_assign.py +0 -92
  49. deadcode/tests/fix/test_class_def.py +0 -116
  50. deadcode/tests/fix/test_empty_files.py +0 -36
  51. deadcode/tests/fix/test_method_def.py +0 -66
  52. deadcode/tests/fix/test_scope.py +0 -243
  53. deadcode/tests/fix/test_unreachable_code.py +0 -100
  54. deadcode/tests/nested_scope/test_nested_scope.py +0 -85
  55. deadcode/tests/noqa/test_noqa_comments.py +0 -329
  56. deadcode/tests/test_accurate_usage_detection.py +0 -49
  57. deadcode/tests/test_deadcode.py +0 -68
  58. deadcode/tests/test_imports.py +0 -101
  59. deadcode/tests/test_line_number_detection.py +0 -97
  60. deadcode/tests/test_output.py +0 -137
  61. deadcode/tests/test_variable_parsing_from_ast_dump.py +0 -12
  62. deadcode/tests/utils/test_fix_indent.py +0 -10
  63. deadcode-2.2.2.dist-info/RECORD +0 -67
  64. {deadcode-2.2.2.dist-info → deadcode-2.3.1.dist-info}/entry_points.txt +0 -0
  65. {deadcode-2.2.2.dist-info → deadcode-2.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -16,21 +16,21 @@ def find_python_filenames(args: Args) -> List[str]:
16
16
 
17
17
  if _match(path, args.exclude):
18
18
  if args.verbose:
19
- logger.info(f"Ignoring: {path}")
19
+ logger.info(f'Ignoring: {path}')
20
20
  continue
21
21
 
22
- if path.is_file() and path.suffix == ".py":
22
+ if path.is_file() and path.suffix == '.py':
23
23
  filenames.append(str(path))
24
24
  elif path.is_dir():
25
- paths += list([str(p) for p in path.glob("*")])
25
+ paths += list([str(p) for p in path.glob('*')])
26
26
  elif not path.exists():
27
27
  # TODO: unify error logging and reporting
28
28
  # Maybe a cli flag could be added to stop on error
29
- logger.error(f"Error: {path} could not be found.")
29
+ logger.error(f'Error: {path} could not be found.')
30
30
 
31
31
  if args.verbose:
32
- sep = "\n - "
33
- logger.info(f"Files to be checked for dead code: {sep.join(filenames)}")
32
+ sep = '\n - '
33
+ logger.info(f'Files to be checked for dead code: {sep.join(filenames)}')
34
34
  return filenames
35
35
 
36
36
 
@@ -0,0 +1,66 @@
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 args.dry and ('__all_files__' in args.dry or _match(filename, args.dry)):
42
+ with open(filename, 'rb') as f:
43
+ filename_bytes = filename.encode()
44
+ diff = diff_bytes(unified_diff, f.readlines(), updated_file_content_lines,
45
+ fromfile=filename_bytes, tofile=filename_bytes)
46
+ # TODO: consider printing result instantly to save memory
47
+ result_chunk = b''.join(diff)
48
+ if args.no_color:
49
+ result.append(result_chunk)
50
+ else:
51
+ result.append(add_colors_to_diff(result_chunk))
52
+
53
+ elif args.fix:
54
+ with open(filename, 'wb') as f:
55
+ # TODO: is there a method writelines?
56
+ f.write(updated_file_content)
57
+ else:
58
+ os.remove(filename)
59
+
60
+ if result:
61
+ return b'\n'.join(result).decode()
62
+ return ''
63
+
64
+ # TODO: update this one: solution is to use read and write operations.
65
+ #
66
+ # Solution is to open file for reading and then for writing.
@@ -11,25 +11,25 @@ def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args)
11
11
  return None
12
12
 
13
13
  if args.quiet:
14
- return ""
14
+ return ''
15
15
 
16
16
  if args.count:
17
- return f"{len(unused_names)}"
17
+ return f'{len(unused_names)}'
18
18
 
19
19
  messages = []
20
20
  for item in unused_names:
21
- message = f"{item.filename_with_position} \033[91m{item.error_code}\033[0m "
21
+ message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
22
22
  message += item.message or (
23
23
  f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
24
24
  )
25
25
  if args.no_color:
26
- message = message.replace("\033[91m", "").replace("\033[1m", "").replace("\033[0m", "")
26
+ message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
27
27
  messages.append(message)
28
28
 
29
29
  if args.fix:
30
30
  message = f"\nRemoved \033[1m{len(unused_names)}\033[0m unused code item{'s' if len(unused_names) > 1 else ''}!"
31
31
  if args.no_color:
32
- message = message.replace("\x1b[1m", "").replace("\x1b[0m", "")
32
+ message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
33
33
  messages.append(message)
34
34
 
35
- return "\n".join(messages)
35
+ return '\n'.join(messages)
@@ -10,9 +10,7 @@ def does_include(bigger_part: Part, smaller_part: Part) -> bool:
10
10
 
11
11
  ends_faster = (line_end_b < line_end_s) or ((line_end_b == line_end_b) and (col_end_b <= col_end_s))
12
12
 
13
- if starts_later and ends_faster:
14
- return True
15
- return False
13
+ return bool(starts_later and ends_faster)
16
14
 
17
15
 
18
16
  def sort_parts(bigger_part: Part, smaller_part: Part) -> Tuple[Part, Part]:
@@ -34,10 +32,7 @@ def does_overlap(bigger_part: Part, smaller_part: Part) -> bool:
34
32
  line_start_s, line_end_s, col_start_s, col_end_s = smaller_part
35
33
 
36
34
  # ar pirmo pabaiga perlipa antro pradžia
37
- if (line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s)):
38
- return True
39
-
40
- return False
35
+ return bool((line_end_b > line_start_s) or ((line_end_b == line_start_s) and (col_end_b > col_start_s)))
41
36
 
42
37
 
43
38
  def merge_parts(p1: Part, p2: Part) -> Optional[Part]:
@@ -9,5 +9,5 @@ def parse_abstract_syntax_tree(file_content: FileContent, args: Args, filename:
9
9
  return ast.parse(file_content, filename=filename, type_comments=True)
10
10
  except: # noqa: E722
11
11
  if not args.count and not args.quiet:
12
- print(f"Error: Failed to parse {filename} file, ignoring it.")
12
+ print(f'Error: Failed to parse {filename} file, ignoring it.')
13
13
  return ast.Module()
@@ -26,163 +26,173 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
26
26
 
27
27
  # Docs: https://docs.python.org/3/library/argparse.html
28
28
  parser = argparse.ArgumentParser()
29
- parser.add_argument("paths", help="Paths where to search for python files", nargs="+")
29
+ parser.add_argument('paths', help='Paths where to search for python files', nargs='+')
30
30
  parser.add_argument(
31
- "--fix",
32
- help="Automatically remove detected unused code expressions from the code base.",
33
- action="store_true",
31
+ '--fix',
32
+ help='Automatically remove detected unused code expressions from the code base.',
33
+ action='store_true',
34
34
  default=False,
35
35
  )
36
-
37
36
  parser.add_argument(
38
- "-v",
39
- "--verbose",
40
- help="Shows logs useful for debuging",
41
- action="store_true",
42
- default=False,
37
+ '--dry',
38
+ help='Show changes which would be made in files with --fix option.',
39
+ nargs='*',
40
+ action='append',
41
+ default=[['__all_files__']],
42
+ type=str,
43
43
  )
44
44
  parser.add_argument(
45
- "--exclude",
46
- help="Filenames (or path expressions), which will be completely skipped without being analysed.",
47
- nargs="*",
48
- action="append",
45
+ '--exclude',
46
+ help='Filenames (or path expressions), which will be completely skipped without being analysed.',
47
+ nargs='*',
48
+ action='append',
49
49
  default=[],
50
50
  type=str,
51
51
  )
52
52
  parser.add_argument(
53
- "--ignore-names",
53
+ '--ignore-names',
54
54
  help=(
55
- "Removes provided list of names from the output. "
56
- "Regexp expressions to match multiple names can also be provided."
55
+ 'Removes provided list of names from the output. '
56
+ 'Regexp expressions to match multiple names can also be provided.'
57
57
  ),
58
- nargs="*",
59
- action="append",
58
+ nargs='*',
59
+ action='append',
60
60
  default=[],
61
61
  type=str,
62
62
  )
63
63
  parser.add_argument(
64
- "--ignore-bodies-of",
65
- help="Ignores body of an expression if its name matches any of the provided names.",
66
- nargs="*",
67
- action="append",
64
+ '--ignore-bodies-of',
65
+ help='Ignores body of an expression if its name matches any of the provided names.',
66
+ nargs='*',
67
+ action='append',
68
68
  default=[],
69
69
  type=str,
70
70
  )
71
71
  parser.add_argument(
72
- "--ignore-bodies-if-decorated-with",
73
- help="Ignores body of an expression if its decorated with one of the provided decorator names.",
74
- nargs="*",
75
- action="append",
72
+ '--ignore-bodies-if-decorated-with',
73
+ help='Ignores body of an expression if its decorated with one of the provided decorator names.',
74
+ nargs='*',
75
+ action='append',
76
76
  default=[],
77
77
  type=str,
78
78
  )
79
79
  parser.add_argument(
80
- "--ignore-bodies-if-inherits-from",
81
- help="Ignores body of a class if it inherits from any of the provided class names.",
82
- nargs="*",
83
- action="append",
80
+ '--ignore-bodies-if-inherits-from',
81
+ help='Ignores body of a class if it inherits from any of the provided class names.',
82
+ nargs='*',
83
+ action='append',
84
84
  default=[],
85
85
  type=str,
86
86
  )
87
87
  parser.add_argument(
88
- "--ignore-definitions",
88
+ '--ignore-definitions',
89
89
  help=(
90
- "Ignores definition (including name and body) if a "
91
- "name of an expression matches any of the provided ones."
90
+ 'Ignores definition (including name and body) if a '
91
+ 'name of an expression matches any of the provided ones.'
92
92
  ),
93
- nargs="*",
94
- action="append",
93
+ nargs='*',
94
+ action='append',
95
95
  default=[],
96
96
  type=str,
97
97
  )
98
98
  parser.add_argument(
99
- "--ignore-definitions-if-inherits-from",
99
+ '--ignore-definitions-if-inherits-from',
100
100
  help=(
101
- "Ignores definition (including name and body) of a class if "
102
- "it inherits from any of the provided class names."
101
+ 'Ignores definition (including name and body) of a class if '
102
+ 'it inherits from any of the provided class names.'
103
103
  ),
104
- nargs="*",
105
- action="append",
104
+ nargs='*',
105
+ action='append',
106
106
  default=[],
107
107
  type=str,
108
108
  )
109
109
  parser.add_argument(
110
- "--ignore-definitions-if-decorated-with",
110
+ '--ignore-definitions-if-decorated-with',
111
111
  help=(
112
- "Ignores definition (including name and body) of an expression, "
113
- "which is decorated with any of the provided decorator names."
112
+ 'Ignores definition (including name and body) of an expression, '
113
+ 'which is decorated with any of the provided decorator names.'
114
114
  ),
115
- nargs="*",
116
- action="append",
115
+ nargs='*',
116
+ action='append',
117
117
  default=[],
118
118
  type=str,
119
119
  )
120
120
 
121
121
  parser.add_argument(
122
- "--ignore-if-decorated-with",
123
- help="Ignores both the name and its definition if its decorated with one of the provided decorator names.",
124
- nargs="*",
125
- action="append",
122
+ '--ignore-if-decorated-with',
123
+ help='Ignores both the name and its definition if its decorated with one of the provided decorator names.',
124
+ nargs='*',
125
+ action='append',
126
126
  default=[],
127
127
  type=str,
128
128
  )
129
129
  parser.add_argument(
130
- "--ignore-if-inherits-from",
131
- help="Ignores both the name and its definition if the class inerits from the provided class name.",
132
- nargs="*",
133
- action="append",
130
+ '--ignore-if-inherits-from',
131
+ help='Ignores both the name and its definition if the class inerits from the provided class name.',
132
+ nargs='*',
133
+ action='append',
134
134
  default=[],
135
135
  type=str,
136
136
  )
137
137
 
138
138
  parser.add_argument(
139
- "--ignore-names-if-inherits-from",
140
- help="Ignores names of classes, which inherit from provided class names.",
141
- nargs="*",
142
- action="append",
139
+ '--ignore-names-if-inherits-from',
140
+ help='Ignores names of classes, which inherit from provided class names.',
141
+ nargs='*',
142
+ action='append',
143
143
  default=[],
144
144
  type=str,
145
145
  )
146
146
  parser.add_argument(
147
- "--ignore-names-if-decorated-with",
148
- help="Ignores names of an expression, which is decorated with one of the provided decorator names.",
149
- nargs="*",
150
- action="append",
147
+ '--ignore-names-if-decorated-with',
148
+ help='Ignores names of an expression, which is decorated with one of the provided decorator names.',
149
+ nargs='*',
150
+ action='append',
151
151
  default=[],
152
152
  type=str,
153
153
  )
154
154
 
155
155
  parser.add_argument(
156
- "--ignore-names-in-files",
157
- help="Ignores unused names in files, which filenames match provided path expressions.",
158
- nargs="*",
159
- action="append",
156
+ '--ignore-names-in-files',
157
+ help='Ignores unused names in files, which filenames match provided path expressions.',
158
+ nargs='*',
159
+ action='append',
160
160
  default=[],
161
161
  type=str,
162
162
  )
163
+
163
164
  parser.add_argument(
164
- "--no-color",
165
- help="Turn off colors in the output",
166
- action="store_true",
165
+ '--no-color',
166
+ help='Turn off colors in the output',
167
+ action='store_true',
167
168
  default=False,
168
169
  )
170
+
169
171
  parser.add_argument(
170
- "--quiet",
171
- help="Does not output anything. Makefile still fails with exit code 1 if unused names are found.",
172
- action="store_true",
172
+ '--quiet',
173
+ help='Does not output anything. Makefile still fails with exit code 1 if unused names are found.',
174
+ action='store_true',
173
175
  default=False,
174
176
  )
177
+
175
178
  parser.add_argument(
176
- "--count",
177
- help="Provides the count of the detected unused names instead of printing them all out.",
178
- action="store_true",
179
+ '--count',
180
+ help='Provides the count of the detected unused names instead of printing them all out.',
181
+ action='store_true',
182
+ default=False,
183
+ )
184
+ parser.add_argument(
185
+ '-v',
186
+ '--verbose',
187
+ help='Shows logs useful for debuging',
188
+ action='store_true',
179
189
  default=False,
180
190
  )
181
191
 
182
192
  parsed_args = parser.parse_args(args).__dict__
183
193
 
184
194
  for arg_name, arg_value in parsed_args.items():
185
- if isinstance(arg_value, list) and arg_name != "paths":
195
+ if isinstance(arg_value, list) and arg_name != 'paths':
186
196
  parsed_args[arg_name] = flatten_lists_of_comma_separated_values(parsed_args.get(arg_name))
187
197
 
188
198
  # Extend the Args with the values provided in the pyproject.toml
@@ -190,6 +200,14 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
190
200
  if key in parsed_args:
191
201
  parsed_args[key].extend(item)
192
202
 
203
+ # Show changes for only provided files instead of all
204
+ if len(parsed_args['dry']) > 1 or '--dry' not in args:
205
+ parsed_args['dry'].remove('__all_files__')
206
+
207
+ # Do not fix if dry option is provided:
208
+ if parsed_args['dry']:
209
+ parsed_args['fix'] = False
210
+
193
211
  return Args(**parsed_args)
194
212
 
195
213
 
@@ -199,13 +217,13 @@ def parse_pyproject_toml() -> Dict[str, Any]:
199
217
  If parsing fails, will raise a tomllib.TOMLDecodeError.
200
218
  Copied from: https://github.com/psf/black/blob/01b8d3d4095ebdb91d0d39012a517931625c63cb/src/black/files.py#LL113C15-L113C15
201
219
  """
202
- pyproject_toml_filename = "pyproject.toml"
220
+ pyproject_toml_filename = 'pyproject.toml'
203
221
  if not os.path.isfile(pyproject_toml_filename):
204
222
  return {}
205
223
 
206
- with open(pyproject_toml_filename, "rb") as f:
224
+ with open(pyproject_toml_filename, 'rb') as f:
207
225
  pyproject_toml = tomllib.load(f)
208
226
 
209
- config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("deadcode", {})
210
- config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
227
+ config: Dict[str, Any] = pyproject_toml.get('tool', {}).get('deadcode', {})
228
+ config = {k.replace('--', '').replace('-', '_'): v for k, v in config.items()}
211
229
  return config
@@ -3,7 +3,7 @@ from typing import List, Optional, TypeVar
3
3
 
4
4
  from deadcode.data_types import Part
5
5
 
6
- T = TypeVar("T")
6
+ T = TypeVar('T')
7
7
 
8
8
 
9
9
  def list_get(list_: List[T], index: int) -> Optional[T]:
@@ -12,20 +12,20 @@ def list_get(list_: List[T], index: int) -> Optional[T]:
12
12
  return None
13
13
 
14
14
 
15
- def ends_with_semicolon(line: str) -> bool:
16
- return line.strip().endswith(":")
15
+ def ends_with_semicolon(line: bytes) -> bool:
16
+ return line.strip().endswith(b':')
17
17
 
18
18
 
19
- def indentation_is_not_childs(previous_line: str, current_line: str) -> bool:
19
+ def indentation_is_not_childs(previous_line: bytes, current_line: bytes) -> bool:
20
20
  return len(get_indentation(previous_line)) >= len(get_indentation(current_line))
21
21
 
22
22
 
23
- def get_indentation(line: str) -> str:
24
- return str(re.findall("^\s*", line)[0])
23
+ def get_indentation(line: bytes) -> bytes:
24
+ return bytes(re.findall(b'^\\s*', line)[0])
25
25
 
26
26
 
27
- def remove_as_from_end(line: str) -> str:
28
- if not (line_rstrip := line.rstrip()).endswith("as"):
27
+ def remove_as_from_end(line: bytes) -> bytes:
28
+ if not (line_rstrip := line.rstrip()).endswith(b'as'):
29
29
  return line
30
30
 
31
31
  # Is there white spaces before as
@@ -34,7 +34,14 @@ def remove_as_from_end(line: str) -> str:
34
34
  return line
35
35
 
36
36
 
37
- def remove_file_parts_from_content(content_lines: List[str], unused_file_parts: List[Part]) -> List[str]:
37
+ def remove_comma_from_begining(line: bytes) -> bytes:
38
+ if not line.lstrip().startswith(b','):
39
+ return line
40
+
41
+ return line.lstrip()[1:].lstrip()
42
+
43
+
44
+ def remove_file_parts_from_content(content_lines: List[bytes], unused_file_parts: List[Part]) -> List[bytes]:
38
45
  """ """
39
46
  # How should move through the lines of content?
40
47
  updated_content_lines = []
@@ -42,12 +49,12 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
42
49
  # TODO: iterate by line and crop that line if needed.
43
50
  unused_part_index = 0
44
51
 
45
- previous_non_removed_line = ""
52
+ previous_non_removed_line = b''
46
53
  was_block_removed = False
47
54
  next_line_after_removed_block = None
48
- indentation_of_first_removed_line = ""
49
- empty_lines_in_a_row_list: List[str] = []
50
- empty_lines_before_removed_block_list: List[str] = []
55
+ indentation_of_first_removed_line = b''
56
+ empty_lines_in_a_row_list: List[bytes] = []
57
+ empty_lines_before_removed_block_list: List[bytes] = []
51
58
 
52
59
  for current_lineno, line in enumerate(content_lines, start=1):
53
60
  from_line, to_line, from_col, to_col = 0, 0, 0, 0
@@ -63,10 +70,14 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
63
70
  indentation_of_first_removed_line = get_indentation(line)
64
71
 
65
72
  if from_line == to_line:
66
- if (line[:from_col] + line[to_col:]).strip().startswith("="):
73
+ # TODO: this check is a workaround for an assignment expression.
74
+ # Clean-ups have to be applied based on removable expression type.
75
+ if (line[:from_col] + line[to_col:]).strip().startswith(b'='):
67
76
  line = line[:from_col]
68
77
  else:
69
- line = remove_as_from_end(line[:from_col]) + line[to_col:]
78
+ # TODO: should apply `as` removal rule only for particular expression type only
79
+ # as well as comma removal.
80
+ line = remove_as_from_end(line[:from_col]) + remove_comma_from_begining(line[to_col:])
70
81
 
71
82
  unused_part_index += 1
72
83
 
@@ -80,16 +91,22 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
80
91
  else:
81
92
  line = line[:from_col]
82
93
 
83
- if line.strip() and not line.startswith("#"):
94
+ if line.strip() and not line.startswith(b'#'):
84
95
  previous_non_removed_line = line
85
96
  updated_content_lines.append(line)
86
97
 
87
98
  # Is it last line, from the block, which have to be ignored?
88
99
  elif current_lineno == to_line:
89
100
  line = line[to_col:]
101
+
102
+ # Remove trailing comma, if we have removed something.
103
+ # TODO: this should be applied only for specific expression types only.
104
+ if line.lstrip().startswith(b','):
105
+ line = line.lstrip()[1:].lstrip()
106
+
90
107
  unused_part_index += 1
91
108
  # TODO: Add tests for case, when comments are added at the start or end of the removed block
92
- if line.strip() and not line.startswith("#"):
109
+ if line.strip() and not line.startswith(b'#'):
93
110
  updated_content_lines.append(line)
94
111
 
95
112
  empty_lines_before_removed_block_list, empty_lines_in_a_row_list = (
@@ -118,7 +135,7 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
118
135
  if indentation_is_not_childs(
119
136
  previous_line=previous_non_removed_line, current_line=next_line_after_removed_block
120
137
  ):
121
- updated_content_lines.append(f"{indentation_of_first_removed_line}pass\n")
138
+ updated_content_lines.append(indentation_of_first_removed_line + b'pass\n')
122
139
 
123
140
  # Add empty lines
124
141
  if indentation_is_not_childs(
@@ -144,6 +161,6 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
144
161
 
145
162
  if was_block_removed:
146
163
  if ends_with_semicolon(previous_non_removed_line):
147
- updated_content_lines.append(f"{indentation_of_first_removed_line}pass\n")
164
+ updated_content_lines.append(indentation_of_first_removed_line + b'pass\n')
148
165
 
149
166
  return updated_content_lines
deadcode/cli.py CHANGED
@@ -2,7 +2,7 @@ from typing import List, Optional
2
2
 
3
3
  from deadcode.actions.find_python_filenames import find_python_filenames
4
4
  from deadcode.actions.find_unused_names import find_unused_names
5
- from deadcode.actions.fix_unused_code import fix_unused_code
5
+ from deadcode.actions.fix_or_show_unused_code import fix_or_show_unused_code
6
6
  from deadcode.actions.parse_arguments import parse_arguments
7
7
  from deadcode.actions.get_unused_names_error_message import (
8
8
  get_unused_names_error_message,
@@ -19,16 +19,17 @@ def main(
19
19
  # TODO: rename unused_names to unused_code_items
20
20
  unused_names = find_unused_names(filenames=filenames, args=args)
21
21
 
22
- if args.fix and unused_names:
23
- fix_unused_code(unused_names)
22
+ file_diff = None
23
+ if (args.fix or args.dry) and unused_names:
24
+ file_diff = fix_or_show_unused_code(unused_names, args=args)
24
25
 
25
26
  if (error_message := get_unused_names_error_message(unused_names, args=args)) is not None:
26
- return error_message
27
+ return error_message + ('\n\n' + file_diff if file_diff else '')
27
28
 
28
29
  if not args.count and not args.quiet:
29
- print("\033[1mWell done!\033[0m ✨ 🚀 ✨")
30
+ print('\033[1mWell done!\033[0m ✨ 🚀 ✨')
30
31
  return None
31
32
 
32
33
 
33
- if __name__ == "__main__":
34
+ if __name__ == '__main__':
34
35
  main()