Skip to content

Commit 3fd16de

Browse files
authored
Merge pull request #14 from albertas/feature/add-only-option
Add only option which now works only with dry option
2 parents bacf9ac + 9a6a700 commit 3fd16de

10 files changed

+200
-53
lines changed

Diff for: README.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ deadcode . --fix --dry
2828

2929
To see suggested fixes only for `foo.py` file:
3030
```shell
31-
deadcode . --fix --dry foo.py
31+
deadcode . --fix --dry --only foo.py
3232
```
3333

3434
To fix:
@@ -54,7 +54,8 @@ ignore-names-in-files = ["migrations"]
5454
| Option                                    | Type | Meaning |
5555
|-------------------------------------------|------|----------------------------------------------------------------------|
5656
|`--fix` | - | Automatically remove detected unused code expressions from the code base. |
57-
|`--dry` | - or list | Show changes which would be made in files. Shows changes for provided filenames or shows all changes if no filename is specified. |
57+
|`--dry` | - | Show changes which would be made in files. |
58+
|`--only` | list | Filenames (or path expressions), that will be reflected in the output (and modified if needed). |
5859
|`--exclude` | list | Filenames (or path expressions), which will be completely skipped without being analysed. |
5960
|`--ignore-names` | list | Removes provided list of names from the output. Regexp expressions to match multiple names can also be provided, e.g. `*Mixin` will match all classes ending with `Mixin`. |
6061
|`--ignore-names-in-files` | list | Ignores unused names in files, which filenames match provided path expressions. |
@@ -157,13 +158,11 @@ code base is implemented in.
157158
- [ ] Distinguish between definitions with same name, but different files.
158159
- [ ] Repeated application of `deadcode` till the output stops changing.
159160
- [ ] Unreachable code detection and fixing: this should only be scoped for if statements and only limited to primitive variables.
160-
- [x] `--fix --dry [filenames]` - only show whats about to change in the listed filenames.
161161
- [ ] Benchmarking performance with larger projects (time, CPU and memory consumption) in order to optimize.
162162
- [ ] `--fix` could accept a list of filenames as well (only those files would be changed, but the summary could would be full).
163163
(This might be confusing, because filenames, which have to be considered are provided without any flag, --fix is expected to not accept arguments)
164164
- [ ] pre-commit-hook.
165165
- [ ] language server.
166-
- [x] Use only two digits for error codes instead of 3. Two is plenty and it simplifies usage a bit
167166
- [ ] DC10: remove code after terminal statements like `raise`, `return`, `break`, `continue` and comes in the same scope.
168167
- [ ] Add `ignore` and `per-file-ignores` command line and pyproject.toml options, which allows to skip some rules.
169168
- [ ] Make sure that all rules are being skipped by `noqa` comment and all rules react to `noqa: rule_id` comments.
@@ -172,11 +171,15 @@ code base is implemented in.
172171
documentation should cleary demonstrate the behaviour/example that "Schema" means "*.Schema".
173172
- [ ] Redefinition of an existing name makes previous name unreachable, unless it is assigned somehow.
174173
- [ ] Check if file is still valid/parsable after automatic fixing, if not: halt the change and report error.
174+
- [ ] Investigate ways of extracting and backporting Python3.10+ `ast` implementation to lower Python versions.
175175

176176
## Release notes
177+
- v2.4.0:
178+
- Add `--only` option that accepts filenames only which will be reflected in the output and modified.
179+
This option can be used with `--fix` and `--fix --dry` options as well as for simple unused code detection without fixing.
177180
- v2.3.2:
178181
- Add `pre-commit` hook support.
179-
- Drop support for Python 3.8 and 3.9 versions, since their ast implementation is lacking features.
182+
- Drop support for Python 3.8 and 3.9 versions, since their `ast` implementation is lacking features.
180183
- v2.3.1:
181184
- Started analysing files in bytes instead of trying to convert them into UTF-8 encoded strings.
182185
- Improved automatic removal of unused imports.

Diff for: deadcode/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
try:
2+
import importlib.metadata
3+
__version__ = importlib.metadata.version(__package__ or __name__)
4+
except ImportError:
5+
import importlib_metadata
6+
__version__ = importlib_metadata.version(__package__ or __name__)

Diff for: deadcode/actions/fix_or_show_unused_code.py

+21-20
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,28 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
3838
updated_file_content_lines = remove_file_parts_from_content(file_content_lines, unused_file_parts)
3939
updated_file_content = b''.join(updated_file_content_lines)
4040
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(
45-
unified_diff,
46-
f.readlines(),
47-
updated_file_content_lines,
48-
fromfile=filename_bytes,
49-
tofile=filename_bytes,
50-
)
51-
# TODO: consider printing result instantly to save memory
52-
result_chunk = b''.join(diff)
53-
if args.no_color:
54-
result.append(result_chunk)
55-
else:
56-
result.append(add_colors_to_diff(result_chunk))
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))
5758

58-
elif args.fix:
59-
with open(filename, 'wb') as f:
60-
# TODO: is there a method writelines?
61-
f.write(updated_file_content)
59+
elif args.fix:
60+
with open(filename, 'wb') as f:
61+
# TODO: is there a method writelines?
62+
f.write(updated_file_content)
6263
else:
6364
os.remove(filename)
6465

Diff for: deadcode/actions/get_unused_names_error_message.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from deadcode.data_types import Args
44
from deadcode.visitor.code_item import CodeItem
5+
from deadcode.visitor.ignore import _match
56

67

78
def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args) -> Optional[str]:
@@ -18,16 +19,17 @@ def get_unused_names_error_message(unused_names: Iterable[CodeItem], args: Args)
1819

1920
messages = []
2021
for item in unused_names:
21-
message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
22-
message += item.message or (
23-
f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
24-
)
25-
if args.no_color:
26-
message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
27-
messages.append(message)
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)
2830

2931
if args.fix:
30-
message = f"\nRemoved \033[1m{len(unused_names)}\033[0m unused code item{'s' if len(unused_names) > 1 else ''}!"
32+
message = f"\nRemoved \033[1m{len(messages)}\033[0m unused code item{'s' if len(messages) > 1 else ''}!"
3133
if args.no_color:
3234
message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
3335
messages.append(message)

Diff for: deadcode/actions/parse_arguments.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
3636
parser.add_argument(
3737
'--dry',
3838
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.',
3945
nargs='*',
4046
action='append',
41-
default=[['__all_files__']],
47+
default=[],
4248
type=str,
4349
)
4450
parser.add_argument(
@@ -200,10 +206,6 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
200206
if key in parsed_args:
201207
parsed_args[key].extend(item)
202208

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-
207209
# Do not fix if dry option is provided:
208210
if parsed_args['dry']:
209211
parsed_args['fix'] = False

Diff for: deadcode/data_types.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
class Args:
1515
fix: bool = False
1616
verbose: bool = False
17-
dry: Iterable[Pathname] = ()
17+
dry: bool = False
18+
only: Iterable[Pathname] = ()
1819
paths: Iterable[Pathname] = ()
1920
exclude: Iterable[Pathname] = ()
2021
ignore_definitions: Iterable[Pathname] = ()

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "deadcode"
3-
version = "2.3.2"
3+
version = "2.4.0"
44
authors = [
55
{name = "Albertas Gimbutas", email = "[email protected]"},
66
]

Diff for: tests/cli_args/test_dry.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class UnusedClass:
1414
}
1515

1616
unused_names = main('foo.py --no-color --fix --dry'.split())
17+
1718
self.assertEqual(
1819
unused_names,
1920
(
@@ -152,12 +153,11 @@ def unused_function():
152153
print("Dont change this file")""",
153154
}
154155

155-
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', 'foo.py'])
156+
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', '--only', 'foo.py'])
156157
self.assertEqual(
157158
unused_names,
158159
fix_indent(
159160
"""\
160-
bar.py:1:0: DC02 Function `unused_function` is never used
161161
foo.py:1:0: DC03 Class `UnusedClass` is never used
162162
163163
--- foo.py
@@ -195,10 +195,10 @@ class UnusedClass:
195195
print("Dont change this file")"""
196196
}
197197

198-
unused_names = main('foo.py --no-color --fix --dry fooo.py'.split())
198+
unused_names = main('foo.py --no-color --fix --dry --only fooo.py'.split())
199199
self.assertEqual(
200200
unused_names,
201-
'foo.py:1:0: DC03 Class `UnusedClass` is never used',
201+
'',
202202
)
203203

204204
self.assertFiles(
@@ -220,7 +220,7 @@ class UnusedClass:
220220
print("Dont change this file")"""
221221
}
222222

223-
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--fix', '--dry', 'f*.py'])
223+
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--fix', '--dry', '--only', 'f*.py'])
224224
self.assertEqual(
225225
unused_names,
226226
(

Diff for: tests/cli_args/test_only.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from deadcode.cli import main
2+
from deadcode.utils.base_test_case import BaseTestCase
3+
from deadcode.utils.fix_indent import fix_indent
4+
5+
6+
class TestOnlyCliOption(BaseTestCase):
7+
def test_output_is_provided_only_for_files_in_only_option(self):
8+
self.files = {
9+
'foo.py': b"""
10+
class UnusedClass:
11+
pass
12+
13+
print("Dont change this file")""",
14+
'bar.py': b"""
15+
def unused_function():
16+
pass
17+
18+
print("Dont change this file")""",
19+
}
20+
21+
unused_names = main('. --only foo.py --no-color'.split())
22+
23+
self.assertEqual(
24+
unused_names,
25+
fix_indent(
26+
"""\
27+
foo.py:1:0: DC03 Class `UnusedClass` is never used"""
28+
),
29+
)
30+
31+
self.assertFiles(
32+
{
33+
'foo.py': b"""
34+
class UnusedClass:
35+
pass
36+
37+
print("Dont change this file")""",
38+
'bar.py': b"""
39+
def unused_function():
40+
pass
41+
42+
print("Dont change this file")""",
43+
}
44+
)
45+
46+
def test_files_are_modified_only_for_files_in_only_option(self):
47+
self.files = {
48+
'foo.py': b"""
49+
class UnusedClass:
50+
pass
51+
52+
print("Dont change this line")""",
53+
'bar.py': b"""
54+
def unused_function():
55+
pass
56+
57+
print("Dont change this file")""",
58+
}
59+
60+
unused_names = main('. --only foo.py --no-color --fix'.split())
61+
62+
self.assertEqual(
63+
unused_names,
64+
fix_indent(
65+
"""\
66+
foo.py:1:0: DC03 Class `UnusedClass` is never used\n
67+
Removed 1 unused code item!"""
68+
),
69+
)
70+
71+
# self.assertFiles(
72+
# {
73+
# 'bar.py': b"""
74+
# def unused_function():
75+
# pass
76+
77+
# print("Dont change this file")""",
78+
# 'foo.py': b"""
79+
# print("Dont change this line")""",
80+
# }
81+
# )
82+
83+
def test_diffs_are_provided_only_for_files_in_only_option(self):
84+
self.files = {
85+
'foo.py': b"""
86+
class UnusedClass:
87+
pass
88+
89+
print("Dont change this file")""",
90+
'bar.py': b"""
91+
def unused_function():
92+
pass
93+
94+
print("Dont change this file")""",
95+
}
96+
97+
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', '--only', 'foo.py'])
98+
self.assertEqual(
99+
unused_names,
100+
fix_indent(
101+
"""\
102+
foo.py:1:0: DC03 Class `UnusedClass` is never used
103+
104+
--- foo.py
105+
+++ foo.py
106+
@@ -1,4 +1 @@
107+
-class UnusedClass:
108+
- pass
109+
-
110+
print("Dont change this file")
111+
"""
112+
),
113+
)
114+
115+
self.assertFiles(
116+
{
117+
'foo.py': b"""
118+
class UnusedClass:
119+
pass
120+
121+
print("Dont change this file")""",
122+
'bar.py': b"""
123+
def unused_function():
124+
pass
125+
126+
print("Dont change this file")""",
127+
}
128+
)

0 commit comments

Comments
 (0)