Skip to content

Commit

Permalink
Merge pull request #14 from albertas/feature/add-only-option
Browse files Browse the repository at this point in the history
Add only option which now works only with dry option
  • Loading branch information
albertas committed Jul 12, 2024
2 parents bacf9ac + 9a6a700 commit 3fd16de
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 53 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ deadcode . --fix --dry

To see suggested fixes only for `foo.py` file:
```shell
deadcode . --fix --dry foo.py
deadcode . --fix --dry --only foo.py
```

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

## Release notes
- v2.4.0:
- Add `--only` option that accepts filenames only which will be reflected in the output and modified.
This option can be used with `--fix` and `--fix --dry` options as well as for simple unused code detection without fixing.
- v2.3.2:
- Add `pre-commit` hook support.
- Drop support for Python 3.8 and 3.9 versions, since their ast implementation is lacking features.
- Drop support for Python 3.8 and 3.9 versions, since their `ast` implementation is lacking features.
- v2.3.1:
- Started analysing files in bytes instead of trying to convert them into UTF-8 encoded strings.
- Improved automatic removal of unused imports.
Expand Down
6 changes: 6 additions & 0 deletions deadcode/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
try:
import importlib.metadata
__version__ = importlib.metadata.version(__package__ or __name__)
except ImportError:
import importlib_metadata
__version__ = importlib_metadata.version(__package__ or __name__)
41 changes: 21 additions & 20 deletions deadcode/actions/fix_or_show_unused_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,28 @@ def fix_or_show_unused_code(unused_items: Iterable[CodeItem], args: Args) -> str
updated_file_content_lines = remove_file_parts_from_content(file_content_lines, unused_file_parts)
updated_file_content = b''.join(updated_file_content_lines)
if updated_file_content.strip():
if args.dry and ('__all_files__' in args.dry or _match(filename, args.dry)):
with open(filename, 'rb') as f:
filename_bytes = filename.encode()
diff = diff_bytes(
unified_diff,
f.readlines(),
updated_file_content_lines,
fromfile=filename_bytes,
tofile=filename_bytes,
)
# TODO: consider printing result instantly to save memory
result_chunk = b''.join(diff)
if args.no_color:
result.append(result_chunk)
else:
result.append(add_colors_to_diff(result_chunk))
if not args.only or _match(filename, args.only):
if args.dry:
with open(filename, 'rb') as f:
filename_bytes = filename.encode()
diff = diff_bytes(
unified_diff,
f.readlines(),
updated_file_content_lines,
fromfile=filename_bytes,
tofile=filename_bytes,
)
# TODO: consider printing result instantly to save memory
result_chunk = b''.join(diff)
if args.no_color:
result.append(result_chunk)
else:
result.append(add_colors_to_diff(result_chunk))

elif args.fix:
with open(filename, 'wb') as f:
# TODO: is there a method writelines?
f.write(updated_file_content)
elif args.fix:
with open(filename, 'wb') as f:
# TODO: is there a method writelines?
f.write(updated_file_content)
else:
os.remove(filename)

Expand Down
18 changes: 10 additions & 8 deletions deadcode/actions/get_unused_names_error_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from deadcode.data_types import Args
from deadcode.visitor.code_item import CodeItem
from deadcode.visitor.ignore import _match


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

messages = []
for item in unused_names:
message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
message += item.message or (
f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
)
if args.no_color:
message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
messages.append(message)
if not args.only or _match(item.filename, args.only):
message = f'{item.filename_with_position} \033[91m{item.error_code}\033[0m '
message += item.message or (
f"{item.type_.replace('_', ' ').capitalize()} " f"`\033[1m{item.name}\033[0m` " f"is never used"
)
if args.no_color:
message = message.replace('\033[91m', '').replace('\033[1m', '').replace('\033[0m', '')
messages.append(message)

if args.fix:
message = f"\nRemoved \033[1m{len(unused_names)}\033[0m unused code item{'s' if len(unused_names) > 1 else ''}!"
message = f"\nRemoved \033[1m{len(messages)}\033[0m unused code item{'s' if len(messages) > 1 else ''}!"
if args.no_color:
message = message.replace('\x1b[1m', '').replace('\x1b[0m', '')
messages.append(message)
Expand Down
12 changes: 7 additions & 5 deletions deadcode/actions/parse_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
parser.add_argument(
'--dry',
help='Show changes which would be made in files with --fix option.',
action='store_true',
default=False,
)
parser.add_argument(
'--only',
help='Filenames (or path expressions), that will be reflected in the output and modified.',
nargs='*',
action='append',
default=[['__all_files__']],
default=[],
type=str,
)
parser.add_argument(
Expand Down Expand Up @@ -200,10 +206,6 @@ def parse_arguments(args: Optional[List[str]]) -> Args:
if key in parsed_args:
parsed_args[key].extend(item)

# Show changes for only provided files instead of all
if len(parsed_args['dry']) > 1 or '--dry' not in args:
parsed_args['dry'].remove('__all_files__')

# Do not fix if dry option is provided:
if parsed_args['dry']:
parsed_args['fix'] = False
Expand Down
3 changes: 2 additions & 1 deletion deadcode/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
class Args:
fix: bool = False
verbose: bool = False
dry: Iterable[Pathname] = ()
dry: bool = False
only: Iterable[Pathname] = ()
paths: Iterable[Pathname] = ()
exclude: Iterable[Pathname] = ()
ignore_definitions: Iterable[Pathname] = ()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "deadcode"
version = "2.3.2"
version = "2.4.0"
authors = [
{name = "Albertas Gimbutas", email = "albertasgim@gmail.com"},
]
Expand Down
10 changes: 5 additions & 5 deletions tests/cli_args/test_dry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class UnusedClass:
}

unused_names = main('foo.py --no-color --fix --dry'.split())

self.assertEqual(
unused_names,
(
Expand Down Expand Up @@ -152,12 +153,11 @@ def unused_function():
print("Dont change this file")""",
}

unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', 'foo.py'])
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', '--only', 'foo.py'])
self.assertEqual(
unused_names,
fix_indent(
"""\
bar.py:1:0: DC02 Function `unused_function` is never used
foo.py:1:0: DC03 Class `UnusedClass` is never used
--- foo.py
Expand Down Expand Up @@ -195,10 +195,10 @@ class UnusedClass:
print("Dont change this file")"""
}

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

self.assertFiles(
Expand All @@ -220,7 +220,7 @@ class UnusedClass:
print("Dont change this file")"""
}

unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--fix', '--dry', 'f*.py'])
unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--fix', '--dry', '--only', 'f*.py'])
self.assertEqual(
unused_names,
(
Expand Down
128 changes: 128 additions & 0 deletions tests/cli_args/test_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from deadcode.cli import main
from deadcode.utils.base_test_case import BaseTestCase
from deadcode.utils.fix_indent import fix_indent


class TestOnlyCliOption(BaseTestCase):
def test_output_is_provided_only_for_files_in_only_option(self):
self.files = {
'foo.py': b"""
class UnusedClass:
pass
print("Dont change this file")""",
'bar.py': b"""
def unused_function():
pass
print("Dont change this file")""",
}

unused_names = main('. --only foo.py --no-color'.split())

self.assertEqual(
unused_names,
fix_indent(
"""\
foo.py:1:0: DC03 Class `UnusedClass` is never used"""
),
)

self.assertFiles(
{
'foo.py': b"""
class UnusedClass:
pass
print("Dont change this file")""",
'bar.py': b"""
def unused_function():
pass
print("Dont change this file")""",
}
)

def test_files_are_modified_only_for_files_in_only_option(self):
self.files = {
'foo.py': b"""
class UnusedClass:
pass
print("Dont change this line")""",
'bar.py': b"""
def unused_function():
pass
print("Dont change this file")""",
}

unused_names = main('. --only foo.py --no-color --fix'.split())

self.assertEqual(
unused_names,
fix_indent(
"""\
foo.py:1:0: DC03 Class `UnusedClass` is never used\n
Removed 1 unused code item!"""
),
)

# self.assertFiles(
# {
# 'bar.py': b"""
# def unused_function():
# pass

# print("Dont change this file")""",
# 'foo.py': b"""
# print("Dont change this line")""",
# }
# )

def test_diffs_are_provided_only_for_files_in_only_option(self):
self.files = {
'foo.py': b"""
class UnusedClass:
pass
print("Dont change this file")""",
'bar.py': b"""
def unused_function():
pass
print("Dont change this file")""",
}

unused_names = main(['ignore_names_by_pattern.py', '--no-color', '--dry', '--only', 'foo.py'])
self.assertEqual(
unused_names,
fix_indent(
"""\
foo.py:1:0: DC03 Class `UnusedClass` is never used
--- foo.py
+++ foo.py
@@ -1,4 +1 @@
-class UnusedClass:
- pass
-
print("Dont change this file")
"""
),
)

self.assertFiles(
{
'foo.py': b"""
class UnusedClass:
pass
print("Dont change this file")""",
'bar.py': b"""
def unused_function():
pass
print("Dont change this file")""",
}
)
Loading

0 comments on commit 3fd16de

Please sign in to comment.