pax_global_header00006660000000000000000000000064145645531040014521gustar00rootroot0000000000000052 comment=a3ef14b5a08a80b37b065872ccdfe6126a9e8e89 autoflake-2.3.0/000077500000000000000000000000001456455310400134765ustar00rootroot00000000000000autoflake-2.3.0/.git-blame-ignore-revs000066400000000000000000000002731456455310400176000ustar00rootroot00000000000000# added add-trailing-comma, double-quote-string-fixer and autopep8 to pre-commit config 2cd0e24017857bed3d9848bdfbfeac2dbd0d776d # adopted black 1ec059a945b05df24ed6742cd00f4938e15e5c81 autoflake-2.3.0/.github/000077500000000000000000000000001456455310400150365ustar00rootroot00000000000000autoflake-2.3.0/.github/dependabot.yml000066400000000000000000000004051456455310400176650ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: daily open-pull-requests-limit: 99 - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 99 autoflake-2.3.0/.github/release.yml000066400000000000000000000000661456455310400172030ustar00rootroot00000000000000changelog: exclude: authors: - dependabot autoflake-2.3.0/.github/workflows/000077500000000000000000000000001456455310400170735ustar00rootroot00000000000000autoflake-2.3.0/.github/workflows/main.yaml000066400000000000000000000027251456455310400207110ustar00rootroot00000000000000name: Build on: push: branches: - main pull_request: branches: - main jobs: test: strategy: matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" os: - macos - ubuntu - windows name: tests runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v4.1.1 - uses: actions/setup-python@v5.0.0 with: python-version: ${{ matrix.python-version }} - run: pip install . - run: pip install pytest - name: run tests run: pytest lint: name: pre-commit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 - uses: actions/setup-python@v5.0.0 with: python-version: 3.x - name: install pre-commit run: pip install pre-commit - name: install package run: pip install . tomli - name: pre-commit cache uses: actions/cache@v4.0.0 with: path: ~/.cache/pre-commit key: "${{ hashFiles('.pre-commit-config.yaml') }}" - run: pre-commit run --all-files --show-diff-on-failure fuzz: name: fuzz runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 - uses: actions/setup-python@v5.0.0 with: python-version: 3.x - run: pip install . - name: run fuzz run: python test_fuzz.py ./*.py autoflake-2.3.0/.github/workflows/upload-to-pypi.yml000066400000000000000000000007551456455310400225100ustar00rootroot00000000000000name: Upload to PyPI on: push: tags: - "*" workflow_dispatch: jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 - uses: actions/setup-python@v5.0.0 with: python-version: 3.x - run: pip install build twine - run: python -m build --wheel --sdist - run: twine upload --skip-existing dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} autoflake-2.3.0/.gitignore000066400000000000000000000002041456455310400154620ustar00rootroot00000000000000.*.swp .eggs/ *.pyc MANIFEST README.html __pycache__/ .tox/ build/ dist/ htmlcov/ autoflake.egg-info/ /.coverage /pypi_tmp/ /tmp.*/ autoflake-2.3.0/.pre-commit-config.yaml000066400000000000000000000022051456455310400177560ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell - repo: https://github.com/asottile/reorder-python-imports rev: v3.12.0 hooks: - id: reorder-python-imports args: - --py38-plus - --add-import - from __future__ import annotations - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.2.2 hooks: - id: ruff - id: ruff-format - repo: https://github.com/asottile/pyupgrade rev: v3.15.1 hooks: - id: pyupgrade args: - --py38-plus - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: - id: add-trailing-comma args: - --py36-plus - repo: https://github.com/abravalheri/validate-pyproject rev: v0.16 hooks: - id: validate-pyproject - repo: https://github.com/fsouza/mirrors-pyright rev: v1.1.350 hooks: - id: pyright autoflake-2.3.0/.pre-commit-hooks.yaml000066400000000000000000000003561456455310400176410ustar00rootroot00000000000000- id: autoflake name: autoflake entry: autoflake language: python "types": [python] require_serial: true args: - "--in-place" - "--expand-star-imports" - "--remove-duplicate-keys" - "--remove-unused-variables" autoflake-2.3.0/AUTHORS.rst000066400000000000000000000011341456455310400153540ustar00rootroot00000000000000Author ------ - Steven Myint (https://github.com/myint) Contributors ------------ - tell-k (https://github.com/tell-k) - Adhika Setya Pramudita (https://github.com/adhikasp) - Andrew Dassonville (https://github.com/andrewda) - toddrme2178 (https://github.com/toddrme2178) - Sebastián Ramírez (https://github.com/tiangolo) - Charlie Liu (https://github.com/CLiu13) - Nobuhiro Kasai (https://github.com/sh4869) - James Curtin (https://github.com/jamescurtin) - Sargun Dhillon (https://github.com/sargun) - Anton Ogorodnikov (https://github.com/arxell) - das-intensity (https://github.com/das-intensity) autoflake-2.3.0/LICENSE000066400000000000000000000020331456455310400145010ustar00rootroot00000000000000Copyright (C) Steven Myint Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. autoflake-2.3.0/README.md000066400000000000000000000154061456455310400147630ustar00rootroot00000000000000# autoflake [![Build Status](https://github.com/PyCQA/autoflake/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/PyCQA/autoflake/actions/workflows/main.yaml) ## Introduction _autoflake_ removes unused imports and unused variables from Python code. It makes use of [pyflakes](https://pypi.org/pypi/pyflakes) to do this. By default, autoflake only removes unused imports for modules that are part of the standard library. (Other modules may have side effects that make them unsafe to remove automatically.) Removal of unused variables is also disabled by default. autoflake also removes useless ``pass`` statements by default. ## Example Running autoflake on the below example ``` $ autoflake --in-place --remove-unused-variables example.py ``` ```python import math import re import os import random import multiprocessing import grp, pwd, platform import subprocess, sys def foo(): from abc import ABCMeta, WeakSet try: import multiprocessing print(multiprocessing.cpu_count()) except ImportError as exception: print(sys.version) return math.pi ``` results in ```python import math import sys def foo(): try: import multiprocessing print(multiprocessing.cpu_count()) except ImportError: print(sys.version) return math.pi ``` ## Installation ``` $ pip install --upgrade autoflake ``` ## Advanced usage To allow autoflake to remove additional unused imports (other than than those from the standard library), use the ``--imports`` option. It accepts a comma-separated list of names: ``` $ autoflake --imports=django,requests,urllib3 ``` To remove all unused imports (whether or not they are from the standard library), use the ``--remove-all-unused-imports`` option. To remove unused variables, use the ``--remove-unused-variables`` option. Below is the full listing of options: ``` usage: autoflake [-h] [-c | -cd] [-r] [-j n] [--exclude globs] [--imports IMPORTS] [--expand-star-imports] [--remove-all-unused-imports] [--ignore-init-module-imports] [--remove-duplicate-keys] [--remove-unused-variables] [--remove-rhs-for-unused-variables] [--ignore-pass-statements] [--ignore-pass-after-docstring] [--version] [--quiet] [-v] [--stdin-display-name STDIN_DISPLAY_NAME] [--config CONFIG_FILE] [-i | -s] files [files ...] Removes unused imports and unused variables as reported by pyflakes. positional arguments: files files to format options: -h, --help show this help message and exit -c, --check return error code if changes are needed -cd, --check-diff return error code if changes are needed, also display file diffs -r, --recursive drill down directories recursively -j n, --jobs n number of parallel jobs; match CPU count if value is 0 (default: 0) --exclude globs exclude file/directory names that match these comma-separated globs --imports IMPORTS by default, only unused standard library imports are removed; specify a comma-separated list of additional modules/packages --expand-star-imports expand wildcard star imports with undefined names; this only triggers if there is only one star import in the file; this is skipped if there are any uses of `__all__` or `del` in the file --remove-all-unused-imports remove all unused imports (not just those from the standard library) --ignore-init-module-imports exclude __init__.py when removing unused imports --remove-duplicate-keys remove all duplicate keys in objects --remove-unused-variables remove unused variables --remove-rhs-for-unused-variables remove RHS of statements when removing unused variables (unsafe) --ignore-pass-statements ignore all pass statements --ignore-pass-after-docstring ignore pass statements after a newline ending on '"""' --version show program's version number and exit --quiet Suppress output if there are no issues -v, --verbose print more verbose logs (you can repeat `-v` to make it more verbose) --stdin-display-name STDIN_DISPLAY_NAME the name used when processing input from stdin --config CONFIG_FILE Explicitly set the config file instead of auto determining based on file location -i, --in-place make changes to files instead of printing diffs -s, --stdout print changed text to stdout. defaults to true when formatting stdin, or to false otherwise ``` To ignore the file, you can also add a comment to the top of the file: ```python # autoflake: skip_file import os ``` ## Configuration Configure default arguments using a `pyproject.toml` file: ```toml [tool.autoflake] check = true imports = ["django", "requests", "urllib3"] ``` Or a `setup.cfg` file: ```ini [autoflake] check=true imports=django,requests,urllib3 ``` The name of the configuration parameters match the flags (e.g. use the parameter `expand-star-imports` for the flag `--expand-star-imports`). ## Tests To run the unit tests:: ``` $ ./test_autoflake.py ``` There is also a fuzz test, which runs against any collection of given Python files. It tests autoflake against the files and checks how well it does by running pyflakes on the file before and after. The test fails if the pyflakes results change for the worse. (This is done in memory. The actual files are left untouched.):: ``` $ ./test_fuzz.py --verbose ``` ## Excluding specific lines It might be the case that you have some imports for their side effects, even if you are not using them directly in that file. That is common, for example, in Flask based applications. In where you import Python modules (files) that imported a main ``app``, to have them included in the routes. For example: ```python from .endpoints import role, token, user, utils ``` As those imports are not being used directly, if you are using the option ``--remove-all-unused-imports``, they would be removed. To prevent that, without having to exclude the entire file, you can add a ``# noqa`` comment at the end of the line, like: ```python from .endpoints import role, token, user, utils # noqa ``` That line will instruct ``autoflake`` to let that specific line as is. ## Using [pre-commit](https://pre-commit.com) hooks Add the following to your `.pre-commit-config.yaml` ```yaml - repo: https://github.com/PyCQA/autoflake rev: v2.3.0 hooks: - id: autoflake ``` When customizing the arguments, make sure you include `--in-place` in the list of arguments: ```yaml - repo: https://github.com/PyCQA/autoflake rev: v2.2.1 hooks: - id: autoflake args: [--remove-all-unused-imports, --in-place] ``` autoflake-2.3.0/autoflake.py000077500000000000000000001424771456455310400160450ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (C) Steven Myint # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """Removes unused imports and unused variables as reported by pyflakes.""" from __future__ import annotations import ast import collections import difflib import fnmatch import io import logging import os import pathlib import re import signal import string import sys import sysconfig import tokenize from typing import Any from typing import Callable from typing import cast from typing import IO from typing import Iterable from typing import Mapping from typing import MutableMapping from typing import Sequence import pyflakes.api import pyflakes.messages import pyflakes.reporter __version__ = "2.3.0" _LOGGER = logging.getLogger("autoflake") _LOGGER.propagate = False ATOMS = frozenset([tokenize.NAME, tokenize.NUMBER, tokenize.STRING]) EXCEPT_REGEX = re.compile(r"^\s*except [\s,()\w]+ as \w+:$") PYTHON_SHEBANG_REGEX = re.compile(r"^#!.*\bpython[3]?\b\s*$") MAX_PYTHON_FILE_DETECTION_BYTES = 1024 IGNORE_COMMENT_REGEX = re.compile( r"\s*#\s{1,}autoflake:\s{1,}\bskip_file\b", re.MULTILINE, ) def standard_paths() -> Iterable[str]: """Yield paths to standard modules.""" paths = sysconfig.get_paths() path_names = ("stdlib", "platstdlib") for path_name in path_names: # Yield lib paths. if path_name in paths: path = paths[path_name] if os.path.isdir(path): yield from os.listdir(path) # Yield lib-dynload paths. dynload_path = os.path.join(path, "lib-dynload") if os.path.isdir(dynload_path): yield from os.listdir(dynload_path) def standard_package_names() -> Iterable[str]: """Yield standard module names.""" for name in standard_paths(): if name.startswith("_") or "-" in name: continue if "." in name and not name.endswith(("so", "py", "pyc")): continue yield name.split(".")[0] IMPORTS_WITH_SIDE_EFFECTS = {"antigravity", "rlcompleter", "this"} # In case they are built into CPython. BINARY_IMPORTS = { "datetime", "grp", "io", "json", "math", "multiprocessing", "parser", "pwd", "string", "operator", "os", "sys", "time", } SAFE_IMPORTS = ( frozenset(standard_package_names()) - IMPORTS_WITH_SIDE_EFFECTS | BINARY_IMPORTS ) def unused_import_line_numbers( messages: Iterable[pyflakes.messages.Message], ) -> Iterable[int]: """Yield line numbers of unused imports.""" for message in messages: if isinstance(message, pyflakes.messages.UnusedImport): yield message.lineno def unused_import_module_name( messages: Iterable[pyflakes.messages.Message], ) -> Iterable[tuple[int, str]]: """Yield line number and module name of unused imports.""" pattern = re.compile(r"\'(.+?)\'") for message in messages: if isinstance(message, pyflakes.messages.UnusedImport): module_name = pattern.search(str(message)) if module_name: module_name = module_name.group()[1:-1] yield (message.lineno, module_name) def star_import_used_line_numbers( messages: Iterable[pyflakes.messages.Message], ) -> Iterable[int]: """Yield line number of star import usage.""" for message in messages: if isinstance(message, pyflakes.messages.ImportStarUsed): yield message.lineno def star_import_usage_undefined_name( messages: Iterable[pyflakes.messages.Message], ) -> Iterable[tuple[int, str, str]]: """Yield line number, undefined name, and its possible origin module.""" for message in messages: if isinstance(message, pyflakes.messages.ImportStarUsage): undefined_name = message.message_args[0] module_name = message.message_args[1] yield (message.lineno, undefined_name, module_name) def unused_variable_line_numbers( messages: Iterable[pyflakes.messages.Message], ) -> Iterable[int]: """Yield line numbers of unused variables.""" for message in messages: if isinstance(message, pyflakes.messages.UnusedVariable): yield message.lineno def duplicate_key_line_numbers( messages: Iterable[pyflakes.messages.Message], source: str, ) -> Iterable[int]: """Yield line numbers of duplicate keys.""" messages = [ message for message in messages if isinstance(message, pyflakes.messages.MultiValueRepeatedKeyLiteral) ] if messages: # Filter out complex cases. We don't want to bother trying to parse # this stuff and get it right. We can do it on a key-by-key basis. key_to_messages = create_key_to_messages_dict(messages) lines = source.split("\n") for key, messages in key_to_messages.items(): good = True for message in messages: line = lines[message.lineno - 1] key = message.message_args[0] if not dict_entry_has_key(line, key): good = False if good: for message in messages: yield message.lineno def create_key_to_messages_dict( messages: Iterable[pyflakes.messages.MultiValueRepeatedKeyLiteral], ) -> Mapping[Any, Iterable[pyflakes.messages.MultiValueRepeatedKeyLiteral]]: """Return dict mapping the key to list of messages.""" dictionary: dict[ Any, list[pyflakes.messages.MultiValueRepeatedKeyLiteral], ] = collections.defaultdict(list) for message in messages: dictionary[message.message_args[0]].append(message) return dictionary def check(source: str) -> Iterable[pyflakes.messages.Message]: """Return messages from pyflakes.""" reporter = ListReporter() try: pyflakes.api.check(source, filename="", reporter=reporter) except (AttributeError, RecursionError, UnicodeDecodeError): pass return reporter.messages class StubFile: """Stub out file for pyflakes.""" def write(self, *_: Any) -> None: """Stub out.""" class ListReporter(pyflakes.reporter.Reporter): """Accumulate messages in messages list.""" def __init__(self) -> None: """Initialize. Ignore errors from Reporter. """ ignore = StubFile() pyflakes.reporter.Reporter.__init__(self, ignore, ignore) self.messages: list[pyflakes.messages.Message] = [] def flake(self, message: pyflakes.messages.Message) -> None: """Accumulate messages.""" self.messages.append(message) def extract_package_name(line: str) -> str | None: """Return package name in import statement.""" assert "\\" not in line assert "(" not in line assert ")" not in line assert ";" not in line if line.lstrip().startswith(("import", "from")): word = line.split()[1] else: # Ignore doctests. return None package = word.split(".")[0] assert " " not in package return package def multiline_import(line: str, previous_line: str = "") -> bool: """Return True if import is spans multiples lines.""" for symbol in "()": if symbol in line: return True return multiline_statement(line, previous_line) def multiline_statement(line: str, previous_line: str = "") -> bool: """Return True if this is part of a multiline statement.""" for symbol in "\\:;": if symbol in line: return True sio = io.StringIO(line) try: list(tokenize.generate_tokens(sio.readline)) return previous_line.rstrip().endswith("\\") except (SyntaxError, tokenize.TokenError): return True class PendingFix: """Allows a rewrite operation to span multiple lines. In the main rewrite loop, every time a helper function returns a ``PendingFix`` object instead of a string, this object will be called with the following line. """ def __init__(self, line: str) -> None: """Analyse and store the first line.""" self.accumulator = collections.deque([line]) def __call__(self, line: str) -> PendingFix | str: """Process line considering the accumulator. Return self to keep processing the following lines or a string with the final result of all the lines processed at once. """ raise NotImplementedError("Abstract method needs to be overwritten") def _valid_char_in_line(char: str, line: str) -> bool: """Return True if a char appears in the line and is not commented.""" comment_index = line.find("#") char_index = line.find(char) valid_char_in_line = char_index >= 0 and ( comment_index > char_index or comment_index < 0 ) return valid_char_in_line def _top_module(module_name: str) -> str: """Return the name of the top level module in the hierarchy.""" if module_name[0] == ".": return "%LOCAL_MODULE%" return module_name.split(".")[0] def _modules_to_remove( unused_modules: Iterable[str], safe_to_remove: Iterable[str] = SAFE_IMPORTS, ) -> Iterable[str]: """Discard unused modules that are not safe to remove from the list.""" return [x for x in unused_modules if _top_module(x) in safe_to_remove] def _segment_module(segment: str) -> str: """Extract the module identifier inside the segment. It might be the case the segment does not have a module (e.g. is composed just by a parenthesis or line continuation and whitespace). In this scenario we just keep the segment... These characters are not valid in identifiers, so they will never be contained in the list of unused modules anyway. """ return segment.strip(string.whitespace + ",\\()") or segment class FilterMultilineImport(PendingFix): """Remove unused imports from multiline import statements. This class handles both the cases: "from imports" and "direct imports". Some limitations exist (e.g. imports with comments, lines joined by ``;``, etc). In these cases, the statement is left unchanged to avoid problems. """ IMPORT_RE = re.compile(r"\bimport\b\s*") INDENTATION_RE = re.compile(r"^\s*") BASE_RE = re.compile(r"\bfrom\s+([^ ]+)") SEGMENT_RE = re.compile( r"([^,\s]+(?:[\s\\]+as[\s\\]+[^,\s]+)?[,\s\\)]*)", re.M, ) # ^ module + comma + following space (including new line and continuation) IDENTIFIER_RE = re.compile(r"[^,\s]+") def __init__( self, line: str, unused_module: Iterable[str] = (), remove_all_unused_imports: bool = False, safe_to_remove: Iterable[str] = SAFE_IMPORTS, previous_line: str = "", ): """Receive the same parameters as ``filter_unused_import``.""" self.remove: Iterable[str] = unused_module self.parenthesized: bool = "(" in line self.from_, imports = self.IMPORT_RE.split(line, maxsplit=1) match = self.BASE_RE.search(self.from_) self.base = match.group(1) if match else None self.give_up: bool = False if not remove_all_unused_imports: if self.base and _top_module(self.base) not in safe_to_remove: self.give_up = True else: self.remove = _modules_to_remove(self.remove, safe_to_remove) if "\\" in previous_line: # Ignore tricky things like "try: \ import" ... self.give_up = True self.analyze(line) PendingFix.__init__(self, imports) def is_over(self, line: str | None = None) -> bool: """Return True if the multiline import statement is over.""" line = line or self.accumulator[-1] if self.parenthesized: return _valid_char_in_line(")", line) return not _valid_char_in_line("\\", line) def analyze(self, line: str) -> None: """Decide if the statement will be fixed or left unchanged.""" if any(ch in line for ch in ";:#"): self.give_up = True def fix(self, accumulated: Iterable[str]) -> str: """Given a collection of accumulated lines, fix the entire import.""" old_imports = "".join(accumulated) ending = get_line_ending(old_imports) # Split imports into segments that contain the module name + # comma + whitespace and eventual \ ( ) chars segments = [x for x in self.SEGMENT_RE.findall(old_imports) if x] modules = [_segment_module(x) for x in segments] keep = _filter_imports(modules, self.base, self.remove) # Short-circuit if no import was discarded if len(keep) == len(segments): return self.from_ + "import " + "".join(accumulated) fixed = "" if keep: # Since it is very difficult to deal with all the line breaks and # continuations, let's use the code layout that already exists and # just replace the module identifiers inside the first N-1 segments # + the last segment templates = list(zip(modules, segments)) templates = templates[: len(keep) - 1] + templates[-1:] # It is important to keep the last segment, since it might contain # important chars like `)` fixed = "".join( template.replace(module, keep[i]) for i, (module, template) in enumerate(templates) ) # Fix the edge case: inline parenthesis + just one surviving import if self.parenthesized and any(ch not in fixed for ch in "()"): fixed = fixed.strip(string.whitespace + "()") + ending # Replace empty imports with a "pass" statement empty = len(fixed.strip(string.whitespace + "\\(),")) < 1 if empty: match = self.INDENTATION_RE.search(self.from_) assert match is not None indentation = match.group(0) return indentation + "pass" + ending return self.from_ + "import " + fixed def __call__(self, line: str | None = None) -> PendingFix | str: """Accumulate all the lines in the import and then trigger the fix.""" if line: self.accumulator.append(line) self.analyze(line) if not self.is_over(line): return self if self.give_up: return self.from_ + "import " + "".join(self.accumulator) return self.fix(self.accumulator) def _filter_imports( imports: Iterable[str], parent: str | None = None, unused_module: Iterable[str] = (), ) -> Sequence[str]: # We compare full module name (``a.module`` not `module`) to # guarantee the exact same module as detected from pyflakes. sep = "" if parent and parent[-1] == "." else "." def full_name(name: str) -> str: return name if parent is None else parent + sep + name return [x for x in imports if full_name(x) not in unused_module] def filter_from_import(line: str, unused_module: Iterable[str]) -> str: """Parse and filter ``from something import a, b, c``. Return line without unused import modules, or `pass` if all of the module in import is unused. """ (indentation, imports) = re.split( pattern=r"\bimport\b", string=line, maxsplit=1, ) match = re.search( pattern=r"\bfrom\s+([^ ]+)", string=indentation, ) assert match is not None base_module = match.group(1) imports = re.split(pattern=r"\s*,\s*", string=imports.strip()) filtered_imports = _filter_imports(imports, base_module, unused_module) # All of the import in this statement is unused if not filtered_imports: return get_indentation(line) + "pass" + get_line_ending(line) indentation += "import " return indentation + ", ".join(filtered_imports) + get_line_ending(line) def break_up_import(line: str) -> str: """Return line with imports on separate lines.""" assert "\\" not in line assert "(" not in line assert ")" not in line assert ";" not in line assert "#" not in line assert not line.lstrip().startswith("from") newline = get_line_ending(line) if not newline: return line (indentation, imports) = re.split( pattern=r"\bimport\b", string=line, maxsplit=1, ) indentation += "import " assert newline return "".join( [indentation + i.strip() + newline for i in imports.split(",")], ) def filter_code( source: str, additional_imports: Iterable[str] | None = None, expand_star_imports: bool = False, remove_all_unused_imports: bool = False, remove_duplicate_keys: bool = False, remove_unused_variables: bool = False, remove_rhs_for_unused_variables: bool = False, ignore_init_module_imports: bool = False, ) -> Iterable[str]: """Yield code with unused imports removed.""" imports = SAFE_IMPORTS if additional_imports: imports |= frozenset(additional_imports) del additional_imports messages = check(source) if ignore_init_module_imports: marked_import_line_numbers: frozenset[int] = frozenset() else: marked_import_line_numbers = frozenset( unused_import_line_numbers(messages), ) marked_unused_module: dict[int, list[str]] = collections.defaultdict(list) for line_number, module_name in unused_import_module_name(messages): marked_unused_module[line_number].append(module_name) undefined_names: list[str] = [] if expand_star_imports and not ( # See explanations in #18. re.search(r"\b__all__\b", source) or re.search(r"\bdel\b", source) ): marked_star_import_line_numbers = frozenset( star_import_used_line_numbers(messages), ) if len(marked_star_import_line_numbers) > 1: # Auto expanding only possible for single star import marked_star_import_line_numbers = frozenset() else: for line_number, undefined_name, _ in star_import_usage_undefined_name( messages, ): undefined_names.append(undefined_name) if not undefined_names: marked_star_import_line_numbers = frozenset() else: marked_star_import_line_numbers = frozenset() if remove_unused_variables: marked_variable_line_numbers = frozenset( unused_variable_line_numbers(messages), ) else: marked_variable_line_numbers = frozenset() if remove_duplicate_keys: marked_key_line_numbers: frozenset[int] = frozenset( duplicate_key_line_numbers(messages, source), ) else: marked_key_line_numbers = frozenset() line_messages = get_messages_by_line(messages) sio = io.StringIO(source) previous_line = "" result: str | PendingFix = "" for line_number, line in enumerate(sio.readlines(), start=1): if isinstance(result, PendingFix): result = result(line) elif "#" in line: result = line elif line_number in marked_import_line_numbers: result = filter_unused_import( line, unused_module=marked_unused_module[line_number], remove_all_unused_imports=remove_all_unused_imports, imports=imports, previous_line=previous_line, ) elif line_number in marked_variable_line_numbers: result = filter_unused_variable( line, drop_rhs=remove_rhs_for_unused_variables, ) elif line_number in marked_key_line_numbers: result = filter_duplicate_key( line, line_messages[line_number], line_number, marked_key_line_numbers, source, ) elif line_number in marked_star_import_line_numbers: result = filter_star_import(line, undefined_names) else: result = line if not isinstance(result, PendingFix): yield result previous_line = line def get_messages_by_line( messages: Iterable[pyflakes.messages.Message], ) -> Mapping[int, pyflakes.messages.Message]: """Return dictionary that maps line number to message.""" line_messages: dict[int, pyflakes.messages.Message] = {} for message in messages: line_messages[message.lineno] = message return line_messages def filter_star_import( line: str, marked_star_import_undefined_name: Iterable[str], ) -> str: """Return line with the star import expanded.""" undefined_name = sorted(set(marked_star_import_undefined_name)) return re.sub(r"\*", ", ".join(undefined_name), line) def filter_unused_import( line: str, unused_module: Iterable[str], remove_all_unused_imports: bool, imports: Iterable[str], previous_line: str = "", ) -> PendingFix | str: """Return line if used, otherwise return None.""" # Ignore doctests. if line.lstrip().startswith(">"): return line if multiline_import(line, previous_line): filt = FilterMultilineImport( line, unused_module, remove_all_unused_imports, imports, previous_line, ) return filt() is_from_import = line.lstrip().startswith("from") if "," in line and not is_from_import: return break_up_import(line) package = extract_package_name(line) if not remove_all_unused_imports and package is not None and package not in imports: return line if "," in line: assert is_from_import return filter_from_import(line, unused_module) else: # We need to replace import with "pass" in case the import is the # only line inside a block. For example, # "if True:\n import os". In such cases, if the import is # removed, the block will be left hanging with no body. return get_indentation(line) + "pass" + get_line_ending(line) def filter_unused_variable( line: str, previous_line: str = "", drop_rhs: bool = False, ) -> str: """Return line if used, otherwise return None.""" if re.match(EXCEPT_REGEX, line): return re.sub(r" as \w+:$", ":", line, count=1) elif multiline_statement(line, previous_line): return line elif line.count("=") == 1: split_line = line.split("=") assert len(split_line) == 2 value = split_line[1].lstrip() if "," in split_line[0]: return line if is_literal_or_name(value): # Rather than removing the line, replace with it "pass" to avoid # a possible hanging block with no body. value = "pass" + get_line_ending(line) if drop_rhs: return get_indentation(line) + value if drop_rhs: return "" return get_indentation(line) + value else: return line def filter_duplicate_key( line: str, message: pyflakes.messages.Message, line_number: int, marked_line_numbers: Iterable[int], source: str, previous_line: str = "", ) -> str: """Return '' if first occurrence of the key otherwise return `line`.""" if marked_line_numbers and line_number == sorted(marked_line_numbers)[0]: return "" return line def dict_entry_has_key(line: str, key: Any) -> bool: """Return True if `line` is a dict entry that uses `key`. Return False for multiline cases where the line should not be removed by itself. """ if "#" in line: return False result = re.match(r"\s*(.*)\s*:\s*(.*),\s*$", line) if not result: return False try: candidate_key = ast.literal_eval(result.group(1)) except (SyntaxError, ValueError): return False if multiline_statement(result.group(2)): return False return cast(bool, candidate_key == key) def is_literal_or_name(value: str) -> bool: """Return True if value is a literal or a name.""" try: ast.literal_eval(value) return True except (SyntaxError, ValueError): pass if value.strip() in ["dict()", "list()", "set()"]: return True # Support removal of variables on the right side. But make sure # there are no dots, which could mean an access of a property. return re.match(r"^\w+\s*$", value) is not None def useless_pass_line_numbers( source: str, ignore_pass_after_docstring: bool = False, ) -> Iterable[int]: """Yield line numbers of unneeded "pass" statements.""" sio = io.StringIO(source) previous_token_type = None last_pass_row = None last_pass_indentation = None previous_line = "" previous_non_empty_line = "" for token in tokenize.generate_tokens(sio.readline): token_type = token[0] start_row = token[2][0] line = token[4] is_pass = token_type == tokenize.NAME and line.strip() == "pass" # Leading "pass". if ( start_row - 1 == last_pass_row and get_indentation(line) == last_pass_indentation and token_type in ATOMS and not is_pass ): yield start_row - 1 if is_pass: last_pass_row = start_row last_pass_indentation = get_indentation(line) is_trailing_pass = ( previous_token_type != tokenize.INDENT and not previous_line.rstrip().endswith("\\") ) is_pass_after_docstring = previous_non_empty_line.rstrip().endswith( ("'''", '"""'), ) # Trailing "pass". if is_trailing_pass: if is_pass_after_docstring and ignore_pass_after_docstring: continue else: yield start_row previous_token_type = token_type previous_line = line if line.strip(): previous_non_empty_line = line def filter_useless_pass( source: str, ignore_pass_statements: bool = False, ignore_pass_after_docstring: bool = False, ) -> Iterable[str]: """Yield code with useless "pass" lines removed.""" if ignore_pass_statements: marked_lines: frozenset[int] = frozenset() else: try: marked_lines = frozenset( useless_pass_line_numbers( source, ignore_pass_after_docstring, ), ) except (SyntaxError, tokenize.TokenError): marked_lines = frozenset() sio = io.StringIO(source) for line_number, line in enumerate(sio.readlines(), start=1): if line_number not in marked_lines: yield line def get_indentation(line: str) -> str: """Return leading whitespace.""" if line.strip(): non_whitespace_index = len(line) - len(line.lstrip()) return line[:non_whitespace_index] else: return "" def get_line_ending(line: str) -> str: """Return line ending.""" non_whitespace_index = len(line.rstrip()) - len(line) if not non_whitespace_index: return "" else: return line[non_whitespace_index:] def fix_code( source: str, additional_imports: Iterable[str] | None = None, expand_star_imports: bool = False, remove_all_unused_imports: bool = False, remove_duplicate_keys: bool = False, remove_unused_variables: bool = False, remove_rhs_for_unused_variables: bool = False, ignore_init_module_imports: bool = False, ignore_pass_statements: bool = False, ignore_pass_after_docstring: bool = False, ) -> str: """Return code with all filtering run on it.""" if not source: return source if IGNORE_COMMENT_REGEX.search(source): return source # pyflakes does not handle "nonlocal" correctly. if "nonlocal" in source: remove_unused_variables = False filtered_source = None while True: filtered_source = "".join( filter_useless_pass( "".join( filter_code( source, additional_imports=additional_imports, expand_star_imports=expand_star_imports, remove_all_unused_imports=remove_all_unused_imports, remove_duplicate_keys=remove_duplicate_keys, remove_unused_variables=remove_unused_variables, remove_rhs_for_unused_variables=( remove_rhs_for_unused_variables ), ignore_init_module_imports=ignore_init_module_imports, ), ), ignore_pass_statements=ignore_pass_statements, ignore_pass_after_docstring=ignore_pass_after_docstring, ), ) if filtered_source == source: break source = filtered_source return filtered_source def fix_file( filename: str, args: Mapping[str, Any], standard_out: IO[str] | None = None, ) -> int: """Run fix_code() on a file.""" if standard_out is None: standard_out = sys.stdout encoding = detect_encoding(filename) with open_with_encoding(filename, encoding=encoding) as input_file: return _fix_file( input_file, filename, args, args["write_to_stdout"], standard_out, encoding=encoding, ) def _fix_file( input_file: IO[str], filename: str, args: Mapping[str, Any], write_to_stdout: bool, standard_out: IO[str], encoding: str | None = None, ) -> int: source = input_file.read() original_source = source isInitFile = os.path.basename(filename) == "__init__.py" if args["ignore_init_module_imports"] and isInitFile: ignore_init_module_imports = True else: ignore_init_module_imports = False filtered_source = fix_code( source, additional_imports=(args["imports"].split(",") if "imports" in args else None), expand_star_imports=args["expand_star_imports"], remove_all_unused_imports=args["remove_all_unused_imports"], remove_duplicate_keys=args["remove_duplicate_keys"], remove_unused_variables=args["remove_unused_variables"], remove_rhs_for_unused_variables=(args["remove_rhs_for_unused_variables"]), ignore_init_module_imports=ignore_init_module_imports, ignore_pass_statements=args["ignore_pass_statements"], ignore_pass_after_docstring=args["ignore_pass_after_docstring"], ) if original_source != filtered_source: if args["check"]: standard_out.write( f"{filename}: Unused imports/variables detected{os.linesep}", ) return 1 if args["check_diff"]: diff = get_diff_text( io.StringIO(original_source).readlines(), io.StringIO(filtered_source).readlines(), filename, ) standard_out.write("".join(diff)) return 1 if write_to_stdout: standard_out.write(filtered_source) elif args["in_place"]: with open_with_encoding( filename, mode="w", encoding=encoding, ) as output_file: output_file.write(filtered_source) _LOGGER.info("Fixed %s", filename) else: diff = get_diff_text( io.StringIO(original_source).readlines(), io.StringIO(filtered_source).readlines(), filename, ) standard_out.write("".join(diff)) elif write_to_stdout: standard_out.write(filtered_source) else: if (args["check"] or args["check_diff"]) and not args["quiet"]: standard_out.write(f"{filename}: No issues detected!{os.linesep}") else: _LOGGER.debug("Clean %s: nothing to fix", filename) return 0 def open_with_encoding( filename: str, encoding: str | None, mode: str = "r", limit_byte_check: int = -1, ) -> IO[str]: """Return opened file with a specific encoding.""" if not encoding: encoding = detect_encoding(filename, limit_byte_check=limit_byte_check) return open( filename, mode=mode, encoding=encoding, newline="", # Preserve line endings ) def detect_encoding(filename: str, limit_byte_check: int = -1) -> str: """Return file encoding.""" try: with open(filename, "rb") as input_file: encoding = _detect_encoding(input_file.readline) # Check for correctness of encoding. with open_with_encoding(filename, encoding) as input_file: input_file.read(limit_byte_check) return encoding except (LookupError, SyntaxError, UnicodeDecodeError): return "latin-1" def _detect_encoding(readline: Callable[[], bytes]) -> str: """Return file encoding.""" try: encoding = tokenize.detect_encoding(readline)[0] return encoding except (LookupError, SyntaxError, UnicodeDecodeError): return "latin-1" def get_diff_text(old: Sequence[str], new: Sequence[str], filename: str) -> str: """Return text of unified diff between old and new.""" newline = "\n" diff = difflib.unified_diff( old, new, "original/" + filename, "fixed/" + filename, lineterm=newline, ) text = "" for line in diff: text += line # Work around missing newline (http://bugs.python.org/issue2142). if not line.endswith(newline): text += newline + r"\ No newline at end of file" + newline return text def _split_comma_separated(string: str) -> set[str]: """Return a set of strings.""" return {text.strip() for text in string.split(",") if text.strip()} def is_python_file(filename: str) -> bool: """Return True if filename is Python file.""" if filename.endswith(".py"): return True try: with open_with_encoding( filename, None, limit_byte_check=MAX_PYTHON_FILE_DETECTION_BYTES, ) as f: text = f.read(MAX_PYTHON_FILE_DETECTION_BYTES) if not text: return False first_line = text.splitlines()[0] except (OSError, IndexError): return False if not PYTHON_SHEBANG_REGEX.match(first_line): return False return True def is_exclude_file(filename: str, exclude: Iterable[str]) -> bool: """Return True if file matches exclude pattern.""" base_name = os.path.basename(filename) if base_name.startswith("."): return True for pattern in exclude: if fnmatch.fnmatch(base_name, pattern): return True if fnmatch.fnmatch(filename, pattern): return True return False def match_file(filename: str, exclude: Iterable[str]) -> bool: """Return True if file is okay for modifying/recursing.""" if is_exclude_file(filename, exclude): _LOGGER.debug("Skipped %s: matched to exclude pattern", filename) return False if not os.path.isdir(filename) and not is_python_file(filename): return False return True def find_files( filenames: list[str], recursive: bool, exclude: Iterable[str], ) -> Iterable[str]: """Yield filenames.""" while filenames: name = filenames.pop(0) if recursive and os.path.isdir(name): for root, directories, children in os.walk(name): filenames += [ os.path.join(root, f) for f in children if match_file( os.path.join(root, f), exclude, ) ] directories[:] = [ d for d in directories if match_file( os.path.join(root, d), exclude, ) ] else: if not is_exclude_file(name, exclude): yield name else: _LOGGER.debug("Skipped %s: matched to exclude pattern", name) def process_pyproject_toml(toml_file_path: str) -> MutableMapping[str, Any] | None: """Extract config mapping from pyproject.toml file.""" try: import tomllib except ModuleNotFoundError: import tomli as tomllib with open(toml_file_path, "rb") as f: return tomllib.load(f).get("tool", {}).get("autoflake", None) def process_config_file(config_file_path: str) -> MutableMapping[str, Any] | None: """Extract config mapping from config file.""" import configparser reader = configparser.ConfigParser() reader.read(config_file_path) if not reader.has_section("autoflake"): return None return reader["autoflake"] def find_and_process_config(args: Mapping[str, Any]) -> MutableMapping[str, Any] | None: # Configuration file parsers {filename: parser function}. CONFIG_FILES: Mapping[str, Callable[[str], MutableMapping[str, Any] | None]] = { "pyproject.toml": process_pyproject_toml, "setup.cfg": process_config_file, } # Traverse the file tree common to all files given as argument looking for # a configuration file config_path = os.path.commonpath([os.path.abspath(file) for file in args["files"]]) config: Mapping[str, Any] | None = None while True: for config_file, processor in CONFIG_FILES.items(): config_file_path = os.path.join( os.path.join(config_path, config_file), ) if os.path.isfile(config_file_path): config = processor(config_file_path) if config is not None: break if config is not None: break config_path, tail = os.path.split(config_path) if not tail: break return config def merge_configuration_file( flag_args: MutableMapping[str, Any], ) -> tuple[MutableMapping[str, Any], bool]: """Merge configuration from a file into args.""" BOOL_TYPES = { "1": True, "yes": True, "true": True, "on": True, "0": False, "no": False, "false": False, "off": False, } if "config_file" in flag_args: config_file = pathlib.Path(flag_args["config_file"]).resolve() process_method = process_config_file if config_file.suffix == ".toml": process_method = process_pyproject_toml config = process_method(str(config_file)) if not config: _LOGGER.error( "can't parse config file '%s'", config_file, ) return flag_args, False else: config = find_and_process_config(flag_args) BOOL_FLAGS = { "check", "check_diff", "expand_star_imports", "ignore_init_module_imports", "ignore_pass_after_docstring", "ignore_pass_statements", "in_place", "quiet", "recursive", "remove_all_unused_imports", "remove_duplicate_keys", "remove_rhs_for_unused_variables", "remove_unused_variables", "write_to_stdout", } config_args: dict[str, Any] = {} if config is not None: for name, value in config.items(): arg = name.replace("-", "_") if arg in BOOL_FLAGS: # boolean properties if isinstance(value, str): value = BOOL_TYPES.get(value.lower(), value) if not isinstance(value, bool): _LOGGER.error( "'%s' in the config file should be a boolean", name, ) return flag_args, False config_args[arg] = value else: if isinstance(value, list) and all( isinstance(val, str) for val in value ): value = ",".join(str(val) for val in value) if not isinstance(value, str): _LOGGER.error( "'%s' in the config file should be a comma separated" " string or list of strings", name, ) return flag_args, False config_args[arg] = value # merge args that can be merged merged_args = {} mergeable_keys = {"imports", "exclude"} for key in mergeable_keys: values = ( v for v in (config_args.get(key), flag_args.get(key)) if v is not None ) value = ",".join(values) if value != "": merged_args[key] = value default_args = {arg: False for arg in BOOL_FLAGS} return { **default_args, **config_args, **flag_args, **merged_args, }, True def _main( argv: Sequence[str], standard_out: IO[str] | None, standard_error: IO[str] | None, standard_input: IO[str] | None = None, ) -> int: """Return exit status. 0 means no error. """ import argparse parser = argparse.ArgumentParser( description=__doc__, prog="autoflake", argument_default=argparse.SUPPRESS, ) check_group = parser.add_mutually_exclusive_group() check_group.add_argument( "-c", "--check", action="store_true", help="return error code if changes are needed", ) check_group.add_argument( "-cd", "--check-diff", action="store_true", help="return error code if changes are needed, also display file diffs", ) imports_group = parser.add_mutually_exclusive_group() imports_group.add_argument( "--imports", help="by default, only unused standard library " "imports are removed; specify a comma-separated " "list of additional modules/packages", ) imports_group.add_argument( "--remove-all-unused-imports", action="store_true", help="remove all unused imports (not just those from " "the standard library)", ) parser.add_argument( "-r", "--recursive", action="store_true", help="drill down directories recursively", ) parser.add_argument( "-j", "--jobs", type=int, metavar="n", default=0, help="number of parallel jobs; " "match CPU count if value is 0 (default: 0)", ) parser.add_argument( "--exclude", metavar="globs", help="exclude file/directory names that match these " "comma-separated globs", ) parser.add_argument( "--expand-star-imports", action="store_true", help="expand wildcard star imports with undefined " "names; this only triggers if there is only " "one star import in the file; this is skipped if " "there are any uses of `__all__` or `del` in the " "file", ) parser.add_argument( "--ignore-init-module-imports", action="store_true", help="exclude __init__.py when removing unused " "imports", ) parser.add_argument( "--remove-duplicate-keys", action="store_true", help="remove all duplicate keys in objects", ) parser.add_argument( "--remove-unused-variables", action="store_true", help="remove unused variables", ) parser.add_argument( "--remove-rhs-for-unused-variables", action="store_true", help="remove RHS of statements when removing unused " "variables (unsafe)", ) parser.add_argument( "--ignore-pass-statements", action="store_true", help="ignore all pass statements", ) parser.add_argument( "--ignore-pass-after-docstring", action="store_true", help='ignore pass statements after a newline ending on \'"""\'', ) parser.add_argument( "--version", action="version", version="%(prog)s " + __version__, ) parser.add_argument( "--quiet", action="store_true", help="Suppress output if there are no issues", ) parser.add_argument( "-v", "--verbose", action="count", dest="verbosity", default=0, help="print more verbose logs (you can " "repeat `-v` to make it more verbose)", ) parser.add_argument( "--stdin-display-name", dest="stdin_display_name", default="stdin", help="the name used when processing input from stdin", ) parser.add_argument( "--config", dest="config_file", help=( "Explicitly set the config file " "instead of auto determining based on file location" ), ) parser.add_argument("files", nargs="+", help="files to format") output_group = parser.add_mutually_exclusive_group() output_group.add_argument( "-i", "--in-place", action="store_true", help="make changes to files instead of printing diffs", ) output_group.add_argument( "-s", "--stdout", action="store_true", dest="write_to_stdout", help=( "print changed text to stdout. defaults to true " "when formatting stdin, or to false otherwise" ), ) args: MutableMapping[str, Any] = vars(parser.parse_args(argv[1:])) if standard_error is None: _LOGGER.addHandler(logging.NullHandler()) else: _LOGGER.addHandler(logging.StreamHandler(standard_error)) loglevels = [logging.WARNING, logging.INFO, logging.DEBUG] try: loglevel = loglevels[args["verbosity"]] except IndexError: # Too much -v loglevel = loglevels[-1] _LOGGER.setLevel(loglevel) args, success = merge_configuration_file(args) if not success: return 1 if ( args["remove_rhs_for_unused_variables"] and not (args["remove_unused_variables"]) ): _LOGGER.error( "Using --remove-rhs-for-unused-variables only makes sense when " "used with --remove-unused-variables", ) return 1 if "exclude" in args: args["exclude"] = _split_comma_separated(args["exclude"]) else: args["exclude"] = set() if args["jobs"] < 1: worker_count = os.cpu_count() if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 worker_count = min(worker_count, 60) args["jobs"] = worker_count or 1 filenames = list(set(args["files"])) # convert argparse namespace to a dict so that it can be serialized # by multiprocessing exit_status = 0 files = list(find_files(filenames, args["recursive"], args["exclude"])) if ( args["jobs"] == 1 or len(files) == 1 or args["jobs"] == 1 or "-" in files or standard_out is not None ): for name in files: if name == "-" and standard_input is not None: exit_status |= _fix_file( standard_input, args["stdin_display_name"], args=args, write_to_stdout=True, standard_out=standard_out or sys.stdout, ) else: try: exit_status |= fix_file( name, args=args, standard_out=standard_out, ) except OSError as exception: _LOGGER.error(str(exception)) exit_status |= 1 else: import multiprocessing with multiprocessing.Pool(args["jobs"]) as pool: futs = [] for name in files: fut = pool.apply_async(fix_file, args=(name, args)) futs.append(fut) for fut in futs: try: exit_status |= fut.get() except OSError as exception: _LOGGER.error(str(exception)) exit_status |= 1 return exit_status def main() -> int: """Command-line entry point.""" try: # Exit on broken pipe. signal.signal(signal.SIGPIPE, signal.SIG_DFL) except AttributeError: # pragma: no cover # SIGPIPE is not available on Windows. pass try: return _main( sys.argv, standard_out=None, standard_error=sys.stderr, standard_input=sys.stdin, ) except KeyboardInterrupt: # pragma: no cover return 2 # pragma: no cover if __name__ == "__main__": sys.exit(main()) autoflake-2.3.0/pyproject.toml000066400000000000000000000020431456455310400164110ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "autoflake" description = "Removes unused imports and unused variables" license = { text = "MIT" } classifiers = [ "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Quality Assurance", ] keywords = ["clean", "fix", "automatic", "unused", "import"] urls = { Homepage = "https://www.github.com/PyCQA/autoflake" } requires-python = ">=3.8" dependencies = ["pyflakes>=3.0.0", "tomli>=2.0.1;python_version<'3.11'"] dynamic = ["version"] [project.readme] file = "README.md" content-type = "text/markdown" [project.scripts] autoflake = "autoflake:main" [tool.hatch.version] path = "autoflake.py" [tool.hatch.build.targets.sdist] exclude = ["/.github"] [tool.hatch.build] include = [ "/autoflake.py", "/test_autoflake.py", "/LICENSE", "/README.md", ] exclude = ["/.gitignore"] autoflake-2.3.0/test_autoflake.py000077500000000000000000002700321456455310400170710ustar00rootroot00000000000000#!/usr/bin/env python """Test suite for autoflake.""" from __future__ import annotations import contextlib import functools import io import os import re import shutil import subprocess import sys import tempfile import unittest from typing import Any from typing import Iterator from typing import Mapping from typing import Sequence import autoflake ROOT_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) AUTOFLAKE_COMMAND = [ sys.executable, os.path.join( ROOT_DIRECTORY, "autoflake.py", ), ] class UnitTests(unittest.TestCase): """Unit tests.""" def test_imports(self) -> None: self.assertGreater(len(autoflake.SAFE_IMPORTS), 0) def test_unused_import_line_numbers(self) -> None: self.assertEqual( [1], list( autoflake.unused_import_line_numbers( autoflake.check("import os\n"), ), ), ) def test_unused_import_line_numbers_with_from(self) -> None: self.assertEqual( [1], list( autoflake.unused_import_line_numbers( autoflake.check("from os import path\n"), ), ), ) def test_unused_import_line_numbers_with_dot(self) -> None: self.assertEqual( [1], list( autoflake.unused_import_line_numbers( autoflake.check("import os.path\n"), ), ), ) def test_extract_package_name(self) -> None: self.assertEqual("os", autoflake.extract_package_name("import os")) self.assertEqual( "os", autoflake.extract_package_name("from os import path"), ) self.assertEqual( "os", autoflake.extract_package_name("import os.path"), ) def test_extract_package_name_should_ignore_doctest_for_now(self) -> None: self.assertFalse(autoflake.extract_package_name(">>> import os")) def test_standard_package_names(self) -> None: self.assertIn("os", list(autoflake.standard_package_names())) self.assertIn("subprocess", list(autoflake.standard_package_names())) self.assertIn("urllib", list(autoflake.standard_package_names())) self.assertNotIn("autoflake", list(autoflake.standard_package_names())) self.assertNotIn("pep8", list(autoflake.standard_package_names())) def test_get_line_ending(self) -> None: self.assertEqual("\n", autoflake.get_line_ending("\n")) self.assertEqual("\n", autoflake.get_line_ending("abc\n")) self.assertEqual("\t \t\n", autoflake.get_line_ending("abc\t \t\n")) self.assertEqual("", autoflake.get_line_ending("abc")) self.assertEqual("", autoflake.get_line_ending("")) def test_get_indentation(self) -> None: self.assertEqual("", autoflake.get_indentation("")) self.assertEqual(" ", autoflake.get_indentation(" abc")) self.assertEqual(" ", autoflake.get_indentation(" abc \n\t")) self.assertEqual("\t", autoflake.get_indentation("\tabc \n\t")) self.assertEqual(" \t ", autoflake.get_indentation(" \t abc \n\t")) self.assertEqual("", autoflake.get_indentation(" ")) def test_filter_star_import(self) -> None: self.assertEqual( "from math import cos", autoflake.filter_star_import( "from math import *", ["cos"], ), ) self.assertEqual( "from math import cos, sin", autoflake.filter_star_import( "from math import *", ["sin", "cos"], ), ) def test_filter_unused_variable(self) -> None: self.assertEqual( "foo()", autoflake.filter_unused_variable("x = foo()"), ) self.assertEqual( " foo()", autoflake.filter_unused_variable(" x = foo()"), ) def test_filter_unused_variable_with_literal_or_name(self) -> None: self.assertEqual( "pass", autoflake.filter_unused_variable("x = 1"), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = y"), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = {}"), ) def test_filter_unused_variable_with_basic_data_structures(self) -> None: self.assertEqual( "pass", autoflake.filter_unused_variable("x = dict()"), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = list()"), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = set()"), ) def test_filter_unused_variable_should_ignore_multiline(self) -> None: self.assertEqual( "x = foo()\\", autoflake.filter_unused_variable("x = foo()\\"), ) def test_filter_unused_variable_should_multiple_assignments(self) -> None: self.assertEqual( "x = y = foo()", autoflake.filter_unused_variable("x = y = foo()"), ) def test_filter_unused_variable_with_exception(self) -> None: self.assertEqual( "except Exception:", autoflake.filter_unused_variable("except Exception as exception:"), ) self.assertEqual( "except (ImportError, ValueError):", autoflake.filter_unused_variable( "except (ImportError, ValueError) as foo:", ), ) def test_filter_unused_variable_drop_rhs(self) -> None: self.assertEqual( "", autoflake.filter_unused_variable( "x = foo()", drop_rhs=True, ), ) self.assertEqual( "", autoflake.filter_unused_variable( " x = foo()", drop_rhs=True, ), ) def test_filter_unused_variable_with_literal_or_name_drop_rhs(self) -> None: self.assertEqual( "pass", autoflake.filter_unused_variable("x = 1", drop_rhs=True), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = y", drop_rhs=True), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = {}", drop_rhs=True), ) def test_filter_unused_variable_with_basic_data_structures_drop_rhs(self) -> None: self.assertEqual( "pass", autoflake.filter_unused_variable("x = dict()", drop_rhs=True), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = list()", drop_rhs=True), ) self.assertEqual( "pass", autoflake.filter_unused_variable("x = set()", drop_rhs=True), ) def test_filter_unused_variable_should_ignore_multiline_drop_rhs(self) -> None: self.assertEqual( "x = foo()\\", autoflake.filter_unused_variable("x = foo()\\", drop_rhs=True), ) def test_filter_unused_variable_should_multiple_assignments_drop_rhs(self) -> None: self.assertEqual( "x = y = foo()", autoflake.filter_unused_variable("x = y = foo()", drop_rhs=True), ) def test_filter_unused_variable_with_exception_drop_rhs(self) -> None: self.assertEqual( "except Exception:", autoflake.filter_unused_variable( "except Exception as exception:", drop_rhs=True, ), ) self.assertEqual( "except (ImportError, ValueError):", autoflake.filter_unused_variable( "except (ImportError, ValueError) as foo:", drop_rhs=True, ), ) def test_filter_code(self) -> None: self.assertEqual( """\ import os pass os.foo() """, "".join( autoflake.filter_code( """\ import os import re os.foo() """, ), ), ) def test_filter_code_with_indented_import(self) -> None: self.assertEqual( """\ import os if True: pass os.foo() """, "".join( autoflake.filter_code( """\ import os if True: import re os.foo() """, ), ), ) def test_filter_code_with_from(self) -> None: self.assertEqual( """\ pass x = 1 """, "".join( autoflake.filter_code( """\ from os import path x = 1 """, ), ), ) def test_filter_code_with_not_from(self) -> None: self.assertEqual( """\ pass x = 1 """, "".join( autoflake.filter_code( """\ import frommer x = 1 """, remove_all_unused_imports=True, ), ), ) def test_filter_code_with_used_from(self) -> None: self.assertEqual( """\ import frommer print(frommer) """, "".join( autoflake.filter_code( """\ import frommer print(frommer) """, remove_all_unused_imports=True, ), ), ) def test_filter_code_with_ambiguous_from(self) -> None: self.assertEqual( """\ pass """, "".join( autoflake.filter_code( """\ from frommer import abc, frommer, xyz """, remove_all_unused_imports=True, ), ), ) def test_filter_code_should_avoid_inline_except(self) -> None: line = """\ try: from zap import foo except: from zap import bar """ self.assertEqual( line, "".join( autoflake.filter_code( line, remove_all_unused_imports=True, ), ), ) def test_filter_code_should_avoid_escaped_newlines(self) -> None: line = """\ try:\\ from zap import foo except:\\ from zap import bar """ self.assertEqual( line, "".join( autoflake.filter_code( line, remove_all_unused_imports=True, ), ), ) def test_filter_code_with_remove_all_unused_imports(self) -> None: self.assertEqual( """\ pass pass x = 1 """, "".join( autoflake.filter_code( """\ import foo import zap x = 1 """, remove_all_unused_imports=True, ), ), ) def test_filter_code_with_additional_imports(self) -> None: self.assertEqual( """\ pass import zap x = 1 """, "".join( autoflake.filter_code( """\ import foo import zap x = 1 """, additional_imports=["foo", "bar"], ), ), ) def test_filter_code_should_ignore_imports_with_inline_comment(self) -> None: self.assertEqual( """\ from os import path # foo pass from fake_foo import z # foo, foo, zap x = 1 """, "".join( autoflake.filter_code( """\ from os import path # foo from os import path from fake_foo import z # foo, foo, zap x = 1 """, ), ), ) def test_filter_code_should_respect_noqa(self) -> None: self.assertEqual( """\ pass import re # noqa from subprocess import Popen # NOQA x = 1 """, "".join( autoflake.filter_code( """\ from os import path import re # noqa from subprocess import Popen # NOQA x = 1 """, ), ), ) def test_filter_code_expand_star_imports(self) -> None: self.assertEqual( """\ from math import sin sin(1) """, "".join( autoflake.filter_code( """\ from math import * sin(1) """, expand_star_imports=True, ), ), ) self.assertEqual( """\ from math import cos, sin sin(1) cos(1) """, "".join( autoflake.filter_code( """\ from math import * sin(1) cos(1) """, expand_star_imports=True, ), ), ) def test_filter_code_ignore_multiple_star_import(self) -> None: self.assertEqual( """\ from math import * from re import * sin(1) cos(1) """, "".join( autoflake.filter_code( """\ from math import * from re import * sin(1) cos(1) """, expand_star_imports=True, ), ), ) def test_filter_code_with_special_re_symbols_in_key(self) -> None: self.assertEqual( """\ a = { '????': 2, } print(a) """, "".join( autoflake.filter_code( """\ a = { '????': 3, '????': 2, } print(a) """, remove_duplicate_keys=True, ), ), ) def test_multiline_import(self) -> None: self.assertTrue( autoflake.multiline_import( r"""\ import os, \ math, subprocess """, ), ) self.assertFalse( autoflake.multiline_import( """\ import os, math, subprocess """, ), ) self.assertTrue( autoflake.multiline_import( """\ import os, math, subprocess """, previous_line="if: \\\n", ), ) self.assertTrue( autoflake.multiline_import("from os import (path, sep)"), ) def test_multiline_statement(self) -> None: self.assertFalse(autoflake.multiline_statement("x = foo()")) self.assertTrue(autoflake.multiline_statement("x = 1;")) self.assertTrue(autoflake.multiline_statement("import os, \\")) self.assertTrue(autoflake.multiline_statement("foo(")) self.assertTrue( autoflake.multiline_statement( "1", previous_line="x = \\", ), ) def test_break_up_import(self) -> None: self.assertEqual( "import abc\nimport subprocess\nimport math\n", autoflake.break_up_import("import abc, subprocess, math\n"), ) def test_break_up_import_with_indentation(self) -> None: self.assertEqual( " import abc\n import subprocess\n import math\n", autoflake.break_up_import(" import abc, subprocess, math\n"), ) def test_break_up_import_should_do_nothing_on_no_line_ending(self) -> None: self.assertEqual( "import abc, subprocess, math", autoflake.break_up_import("import abc, subprocess, math"), ) def test_filter_from_import_no_remove(self) -> None: self.assertEqual( """\ from foo import abc, subprocess, math\n""", autoflake.filter_from_import( " from foo import abc, subprocess, math\n", unused_module=[], ), ) def test_filter_from_import_remove_module(self) -> None: self.assertEqual( """\ from foo import subprocess, math\n""", autoflake.filter_from_import( " from foo import abc, subprocess, math\n", unused_module=["foo.abc"], ), ) def test_filter_from_import_remove_all(self) -> None: self.assertEqual( " pass\n", autoflake.filter_from_import( " from foo import abc, subprocess, math\n", unused_module=[ "foo.abc", "foo.subprocess", "foo.math", ], ), ) def test_filter_code_multiline_imports(self) -> None: self.assertEqual( r"""\ import os pass import os os.foo() """, "".join( autoflake.filter_code( r"""\ import os import re import os, \ math, subprocess os.foo() """, ), ), ) def test_filter_code_multiline_from_imports(self) -> None: self.assertEqual( r"""\ import os pass from os.path import ( join, ) join('a', 'b') pass os.foo() from os.path import \ isdir isdir('42') """, "".join( autoflake.filter_code( r"""\ import os import re from os.path import ( exists, join, ) join('a', 'b') from os.path import \ abspath, basename, \ commonpath os.foo() from os.path import \ isfile \ , isdir isdir('42') """, ), ), ) def test_filter_code_should_ignore_semicolons(self) -> None: self.assertEqual( r"""\ import os pass import os; import math, subprocess os.foo() """, "".join( autoflake.filter_code( r"""\ import os import re import os; import math, subprocess os.foo() """, ), ), ) def test_filter_code_should_ignore_non_standard_library(self) -> None: self.assertEqual( """\ import os import my_own_module pass from my_package import another_module from my_package import subprocess from my_blah.my_blah_blah import blah os.foo() """, "".join( autoflake.filter_code( """\ import os import my_own_module import re from my_package import another_module from my_package import subprocess from my_blah.my_blah_blah import blah os.foo() """, ), ), ) def test_filter_code_should_ignore_unsafe_imports(self) -> None: self.assertEqual( """\ import rlcompleter pass pass pass print(1) """, "".join( autoflake.filter_code( """\ import rlcompleter import sys import io import os print(1) """, ), ), ) def test_filter_code_should_ignore_docstring(self) -> None: line = """ def foo() -> None: ''' >>> import math ''' """ self.assertEqual(line, "".join(autoflake.filter_code(line))) def test_with_ignore_init_module_imports_flag(self) -> None: # Need a temp directory in order to specify file name as __init__.py temp_directory = tempfile.mkdtemp(dir=".") temp_file = os.path.join(temp_directory, "__init__.py") try: with open(temp_file, "w") as output: output.write("import re\n") p = subprocess.Popen( list(AUTOFLAKE_COMMAND) + ["--ignore-init-module-imports", temp_file], stdout=subprocess.PIPE, ) result = p.communicate()[0].decode("utf-8") self.assertNotIn("import re", result) finally: shutil.rmtree(temp_directory) def test_without_ignore_init_module_imports_flag(self) -> None: # Need a temp directory in order to specify file name as __init__.py temp_directory = tempfile.mkdtemp(dir=".") temp_file = os.path.join(temp_directory, "__init__.py") try: with open(temp_file, "w") as output: output.write("import re\n") p = subprocess.Popen( list(AUTOFLAKE_COMMAND) + [temp_file], stdout=subprocess.PIPE, ) result = p.communicate()[0].decode("utf-8") self.assertIn("import re", result) finally: shutil.rmtree(temp_directory) def test_fix_code(self) -> None: self.assertEqual( """\ import os import math from sys import version os.foo() math.pi x = version """, autoflake.fix_code( """\ import os import re import abc, math, subprocess from sys import exit, version os.foo() math.pi x = version """, ), ) def test_fix_code_with_from_and_as(self) -> None: self.assertEqual( """\ from collections import namedtuple as xyz xyz """, autoflake.fix_code( """\ from collections import defaultdict, namedtuple as xyz xyz """, ), ) self.assertEqual( """\ from collections import namedtuple as xyz xyz """, autoflake.fix_code( """\ from collections import defaultdict as abc, namedtuple as xyz xyz """, ), ) self.assertEqual( """\ from collections import namedtuple namedtuple """, autoflake.fix_code( """\ from collections import defaultdict as abc, namedtuple namedtuple """, ), ) self.assertEqual( """\ """, autoflake.fix_code( """\ from collections import defaultdict as abc, namedtuple as xyz """, ), ) def test_fix_code_with_from_with_and_without_remove_all(self) -> None: code = """\ from x import a as b, c as d """ self.assertEqual( """\ """, autoflake.fix_code(code, remove_all_unused_imports=True), ) self.assertEqual( code, autoflake.fix_code(code, remove_all_unused_imports=False), ) def test_fix_code_with_from_and_depth_module(self) -> None: self.assertEqual( """\ from distutils.version import StrictVersion StrictVersion('1.0.0') """, autoflake.fix_code( """\ from distutils.version import LooseVersion, StrictVersion StrictVersion('1.0.0') """, remove_all_unused_imports=True, ), ) self.assertEqual( """\ from distutils.version import StrictVersion as version version('1.0.0') """, autoflake.fix_code( """\ from distutils.version import LooseVersion, StrictVersion as version version('1.0.0') """, remove_all_unused_imports=True, ), ) def test_fix_code_with_indented_from(self) -> None: self.assertEqual( """\ def z() -> None: from ctypes import POINTER, byref POINTER, byref """, autoflake.fix_code( """\ def z() -> None: from ctypes import c_short, c_uint, c_int, c_long, pointer, POINTER, byref POINTER, byref """, ), ) self.assertEqual( """\ def z() -> None: pass """, autoflake.fix_code( """\ def z() -> None: from ctypes import c_short, c_uint, c_int, c_long, pointer, POINTER, byref """, ), ) def test_fix_code_with_empty_string(self) -> None: self.assertEqual( "", autoflake.fix_code(""), ) def test_fix_code_with_from_and_as_and_escaped_newline(self) -> None: """Make sure stuff after escaped newline is not lost.""" result = autoflake.fix_code( """\ from collections import defaultdict, namedtuple \\ as xyz xyz """, ) # We currently leave lines with escaped newlines as is. But in the # future this we may parse them and remove unused import accordingly. # For now, we'll work around it here. result = re.sub(r" *\\\n *as ", " as ", result) self.assertEqual( """\ from collections import namedtuple as xyz xyz """, autoflake.fix_code(result), ) def test_fix_code_with_unused_variables(self) -> None: self.assertEqual( """\ def main() -> None: y = 11 print(y) """, autoflake.fix_code( """\ def main() -> None: x = 10 y = 11 print(y) """, remove_unused_variables=True, ), ) def test_fix_code_with_unused_variables_drop_rhs(self) -> None: self.assertEqual( """\ def main() -> None: y = 11 print(y) """, autoflake.fix_code( """\ def main() -> None: x = 10 y = 11 print(y) """, remove_unused_variables=True, remove_rhs_for_unused_variables=True, ), ) def test_fix_code_with_unused_variables_should_skip_nonlocal(self) -> None: """pyflakes does not handle nonlocal correctly.""" code = """\ def bar() -> None: x = 1 def foo() -> None: nonlocal x x = 2 """ self.assertEqual( code, autoflake.fix_code( code, remove_unused_variables=True, ), ) def test_fix_code_with_unused_variables_should_skip_nonlocal_drop_rhs( self, ): """pyflakes does not handle nonlocal correctly.""" code = """\ def bar() -> None: x = 1 def foo() -> None: nonlocal x x = 2 """ self.assertEqual( code, autoflake.fix_code( code, remove_unused_variables=True, remove_rhs_for_unused_variables=True, ), ) def test_detect_encoding_with_bad_encoding(self) -> None: with temporary_file("# -*- coding: blah -*-\n") as filename: self.assertEqual( "latin-1", autoflake.detect_encoding(filename), ) def test_fix_code_with_comma_on_right(self) -> None: """pyflakes does not handle nonlocal correctly.""" self.assertEqual( """\ def main() -> None: pass """, autoflake.fix_code( """\ def main() -> None: x = (1, 2, 3) """, remove_unused_variables=True, ), ) def test_fix_code_with_comma_on_right_drop_rhs(self) -> None: """pyflakes does not handle nonlocal correctly.""" self.assertEqual( """\ def main() -> None: pass """, autoflake.fix_code( """\ def main() -> None: x = (1, 2, 3) """, remove_unused_variables=True, remove_rhs_for_unused_variables=True, ), ) def test_fix_code_with_unused_variables_should_skip_multiple(self) -> None: code = """\ def main() -> None: (x, y, z) = (1, 2, 3) print(z) """ self.assertEqual( code, autoflake.fix_code( code, remove_unused_variables=True, ), ) def test_fix_code_with_unused_variables_should_skip_multiple_drop_rhs( self, ): code = """\ def main() -> None: (x, y, z) = (1, 2, 3) print(z) """ self.assertEqual( code, autoflake.fix_code( code, remove_unused_variables=True, remove_rhs_for_unused_variables=True, ), ) def test_fix_code_should_handle_pyflakes_recursion_error_gracefully(self) -> None: code = "x = [{}]".format("+".join(["abc" for _ in range(2000)])) self.assertEqual( code, autoflake.fix_code(code), ) def test_fix_code_with_duplicate_key(self) -> None: self.assertEqual( """\ a = { (0,1): 3, } print(a) """, "".join( autoflake.fix_code( """\ a = { (0,1): 1, (0, 1): 'two', (0,1): 3, } print(a) """, remove_duplicate_keys=True, ), ), ) def test_fix_code_with_duplicate_key_longer(self) -> None: self.assertEqual( """\ { 'a': 0, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'b': 6, } """, "".join( autoflake.fix_code( """\ { 'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'b': 6, } """, remove_duplicate_keys=True, ), ), ) def test_fix_code_with_duplicate_key_with_many_braces(self) -> None: self.assertEqual( """\ a = None {None: {None: None}, } { None: a.b, } """, "".join( autoflake.fix_code( """\ a = None {None: {None: None}, } { None: a.a, None: a.b, } """, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_complex_case_of_duplicate_key(self) -> None: """We only handle simple cases.""" code = """\ a = {(0,1): 1, (0, 1): 'two', (0,1): 3, } print(a) """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_complex_case_of_duplicate_key_comma(self) -> None: """We only handle simple cases.""" code = """\ { 1: {0, }, 1: {2, }, } """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_complex_case_of_duplicate_key_partially( self, ): """We only handle simple cases.""" code = """\ a = {(0,1): 1, (0, 1): 'two', (0,1): 3, (2,3): 4, (2,3): 4, (2,3): 5, } print(a) """ self.assertEqual( """\ a = {(0,1): 1, (0, 1): 'two', (0,1): 3, (2,3): 5, } print(a) """, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_more_cases_of_duplicate_key(self) -> None: """We only handle simple cases.""" code = """\ a = { (0,1): 1, (0, 1): 'two', (0,1): 3, } print(a) """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_duplicate_key_with_comments(self) -> None: """We only handle simple cases.""" code = """\ a = { (0,1) # : f : 1, (0, 1): 'two', (0,1): 3, } print(a) """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) code = """\ { 1: {0, }, 1: #{2, #}, 0 } """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_duplicate_key_with_multiline_key(self) -> None: """We only handle simple cases.""" code = """\ a = { (0,1 ): 1, (0, 1): 'two', (0,1): 3, } print(a) """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_should_ignore_duplicate_key_with_no_comma(self) -> None: """We don't want to delete the line and leave a lone comma.""" code = """\ a = { (0,1) : 1 , (0, 1): 'two', (0,1): 3, } print(a) """ self.assertEqual( code, "".join( autoflake.fix_code( code, remove_duplicate_keys=True, ), ), ) def test_fix_code_keeps_pass_statements(self) -> None: code = """\ if True: pass else: def foo() -> None: \"\"\" A docstring. \"\"\" pass def foo2() -> None: \"\"\" A docstring. \"\"\" pass def foo3() -> None: \"\"\" A docstring. \"\"\" pass def bar() -> None: # abc pass def blah() -> None: 123 pass pass # Nope. pass """ self.assertEqual( code, "".join( autoflake.fix_code( code, ignore_pass_statements=True, ), ), ) def test_fix_code_keeps_passes_after_docstrings(self) -> None: actual = autoflake.fix_code( """\ if True: pass else: def foo() -> None: \"\"\" A docstring. \"\"\" pass def foo2() -> None: \"\"\" A docstring. \"\"\" pass def foo3() -> None: \"\"\" A docstring. \"\"\" pass def bar() -> None: # abc pass def blah() -> None: 123 pass pass # Nope. pass """, ignore_pass_after_docstring=True, ) expected = """\ if True: pass else: def foo() -> None: \"\"\" A docstring. \"\"\" pass def foo2() -> None: \"\"\" A docstring. \"\"\" pass def foo3() -> None: \"\"\" A docstring. \"\"\" pass def bar() -> None: # abc pass def blah() -> None: 123 pass # Nope. """ self.assertEqual(actual, expected) def test_useless_pass_line_numbers(self) -> None: self.assertEqual( [1], list( autoflake.useless_pass_line_numbers( "pass\n", ), ), ) self.assertEqual( [], list( autoflake.useless_pass_line_numbers( "if True:\n pass\n", ), ), ) def test_useless_pass_line_numbers_with_escaped_newline(self) -> None: self.assertEqual( [], list( autoflake.useless_pass_line_numbers( "if True:\\\n pass\n", ), ), ) def test_useless_pass_line_numbers_with_more_complex(self) -> None: self.assertEqual( [6], list( autoflake.useless_pass_line_numbers( """\ if True: pass else: True x = 1 pass """, ), ), ) def test_useless_pass_line_numbers_after_docstring(self) -> None: actual_pass_line_numbers = list( autoflake.useless_pass_line_numbers( """\ @abc.abstractmethod def some_abstract_method() -> None: \"\"\"Some docstring.\"\"\" pass """, ), ) expected_pass_line_numbers = [4] self.assertEqual(expected_pass_line_numbers, actual_pass_line_numbers) def test_useless_pass_line_numbers_keep_pass_after_docstring(self) -> None: actual_pass_line_numbers = list( autoflake.useless_pass_line_numbers( """\ @abc.abstractmethod def some_abstract_method() -> None: \"\"\"Some docstring.\"\"\" pass """, ignore_pass_after_docstring=True, ), ) expected_pass_line_numbers = [] self.assertEqual(expected_pass_line_numbers, actual_pass_line_numbers) def test_filter_useless_pass(self) -> None: self.assertEqual( """\ if True: pass else: True x = 1 """, "".join( autoflake.filter_useless_pass( """\ if True: pass else: True x = 1 pass """, ), ), ) def test_filter_useless_pass_with_syntax_error(self) -> None: source = """\ if True: if True: if True: if True: if True: pass else: True pass pass x = 1 """ self.assertEqual( source, "".join(autoflake.filter_useless_pass(source)), ) def test_filter_useless_pass_more_complex(self) -> None: self.assertEqual( """\ if True: pass else: def foo() -> None: pass # abc def bar() -> None: # abc pass def blah() -> None: 123 pass # Nope. True x = 1 """, "".join( autoflake.filter_useless_pass( """\ if True: pass else: def foo() -> None: pass # abc def bar() -> None: # abc pass def blah() -> None: 123 pass pass # Nope. pass True x = 1 pass """, ), ), ) def test_filter_useless_pass_keep_pass_after_docstring(self) -> None: source = """\ def foo() -> None: \"\"\" This is not a useless 'pass'. \"\"\" pass @abc.abstractmethod def bar() -> None: \"\"\" Also this is not a useless 'pass'. \"\"\" pass """ self.assertEqual( source, "".join( autoflake.filter_useless_pass( source, ignore_pass_after_docstring=True, ), ), ) def test_filter_useless_pass_keeps_pass_statements(self) -> None: source = """\ if True: pass pass pass pass else: pass True x = 1 pass """ self.assertEqual( source, "".join( autoflake.filter_useless_pass( source, ignore_pass_statements=True, ), ), ) def test_filter_useless_paspasss_with_try(self) -> None: self.assertEqual( """\ import os os.foo() try: pass except ImportError: pass """, "".join( autoflake.filter_useless_pass( """\ import os os.foo() try: pass pass except ImportError: pass """, ), ), ) def test_filter_useless_pass_leading_pass(self) -> None: self.assertEqual( """\ if True: pass else: True x = 1 """, "".join( autoflake.filter_useless_pass( """\ if True: pass pass pass pass else: pass True x = 1 pass """, ), ), ) def test_filter_useless_pass_leading_pass_with_number(self) -> None: self.assertEqual( """\ def func11() -> None: 0, 11 / 2 return 1 """, "".join( autoflake.filter_useless_pass( """\ def func11() -> None: pass 0, 11 / 2 return 1 """, ), ), ) def test_filter_useless_pass_leading_pass_with_string(self) -> None: self.assertEqual( """\ def func11() -> None: 'hello' return 1 """, "".join( autoflake.filter_useless_pass( """\ def func11() -> None: pass 'hello' return 1 """, ), ), ) def test_check(self) -> None: self.assertTrue(autoflake.check("import os")) def test_check_with_bad_syntax(self) -> None: self.assertFalse(autoflake.check("foo(")) def test_check_with_unicode(self) -> None: self.assertFalse(autoflake.check('print("∑")')) self.assertTrue(autoflake.check("import os # ∑")) def test_get_diff_text(self) -> None: # We ignore the first two lines since it differs on Python 2.6. self.assertEqual( """\ -foo +bar """, "\n".join( autoflake.get_diff_text(["foo\n"], ["bar\n"], "").split( "\n", )[3:], ), ) def test_get_diff_text_without_newline(self) -> None: # We ignore the first two lines since it differs on Python 2.6. self.assertEqual( """\ -foo \\ No newline at end of file +foo """, "\n".join( autoflake.get_diff_text(["foo"], ["foo\n"], "").split( "\n", )[3:], ), ) def test_is_literal_or_name(self) -> None: self.assertTrue(autoflake.is_literal_or_name("123")) self.assertTrue(autoflake.is_literal_or_name("[1, 2, 3]")) self.assertTrue(autoflake.is_literal_or_name("xyz")) self.assertFalse(autoflake.is_literal_or_name("xyz.prop")) self.assertFalse(autoflake.is_literal_or_name(" ")) def test_is_python_file(self) -> None: self.assertTrue( autoflake.is_python_file( os.path.join(ROOT_DIRECTORY, "autoflake.py"), ), ) with temporary_file("#!/usr/bin/env python", suffix="") as filename: self.assertTrue(autoflake.is_python_file(filename)) with temporary_file("#!/usr/bin/python", suffix="") as filename: self.assertTrue(autoflake.is_python_file(filename)) with temporary_file("#!/usr/bin/python3", suffix="") as filename: self.assertTrue(autoflake.is_python_file(filename)) with temporary_file("#!/usr/bin/pythonic", suffix="") as filename: self.assertFalse(autoflake.is_python_file(filename)) with temporary_file("###!/usr/bin/python", suffix="") as filename: self.assertFalse(autoflake.is_python_file(filename)) self.assertFalse(autoflake.is_python_file(os.devnull)) self.assertFalse(autoflake.is_python_file("/bin/bash")) def test_is_exclude_file(self) -> None: self.assertTrue( autoflake.is_exclude_file( "1.py", ["test*", "1*"], ), ) self.assertFalse( autoflake.is_exclude_file( "2.py", ["test*", "1*"], ), ) # folder glob self.assertTrue( autoflake.is_exclude_file( "test/test.py", ["test/**.py"], ), ) self.assertTrue( autoflake.is_exclude_file( "test/auto_test.py", ["test/*_test.py"], ), ) self.assertFalse( autoflake.is_exclude_file( "test/auto_auto.py", ["test/*_test.py"], ), ) def test_match_file(self) -> None: with temporary_file("", suffix=".py", prefix=".") as filename: self.assertFalse( autoflake.match_file(filename, exclude=[]), msg=filename, ) self.assertFalse(autoflake.match_file(os.devnull, exclude=[])) with temporary_file("", suffix=".py", prefix="") as filename: self.assertTrue( autoflake.match_file(filename, exclude=[]), msg=filename, ) def test_find_files(self) -> None: temp_directory = tempfile.mkdtemp() try: target = os.path.join(temp_directory, "dir") os.mkdir(target) with open(os.path.join(target, "a.py"), "w"): pass exclude = os.path.join(target, "ex") os.mkdir(exclude) with open(os.path.join(exclude, "b.py"), "w"): pass sub = os.path.join(exclude, "sub") os.mkdir(sub) with open(os.path.join(sub, "c.py"), "w"): pass # FIXME: Avoid changing directory. This may interfere with parallel # test runs. cwd = os.getcwd() os.chdir(temp_directory) try: files = list( autoflake.find_files( ["dir"], True, [os.path.join("dir", "ex")], ), ) finally: os.chdir(cwd) file_names = [os.path.basename(f) for f in files] self.assertIn("a.py", file_names) self.assertNotIn("b.py", file_names) self.assertNotIn("c.py", file_names) finally: shutil.rmtree(temp_directory) def test_exclude(self) -> None: temp_directory = tempfile.mkdtemp(dir=".") try: with open(os.path.join(temp_directory, "a.py"), "w") as output: output.write("import re\n") os.mkdir(os.path.join(temp_directory, "d")) with open( os.path.join(temp_directory, "d", "b.py"), "w", ) as output: output.write("import os\n") p = subprocess.Popen( list(AUTOFLAKE_COMMAND) + [temp_directory, "--recursive", "--exclude=a*"], stdout=subprocess.PIPE, ) result = p.communicate()[0].decode("utf-8") self.assertNotIn("import re", result) self.assertIn("import os", result) finally: shutil.rmtree(temp_directory) class SystemTests(unittest.TestCase): """System tests.""" def test_skip_file(self) -> None: skipped_file_file_text = """ # autoflake: skip_file import re import os import my_own_module x = 1 """ with temporary_file(skipped_file_file_text) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", filename, "--stdout"], standard_out=output_file, standard_error=None, ) self.assertEqual( skipped_file_file_text, output_file.getvalue(), ) def test_skip_file_with_shebang_respect(self) -> None: skipped_file_file_text = """ #!/usr/bin/env python3 # autoflake: skip_file import re import os import my_own_module x = 1 """ with temporary_file(skipped_file_file_text) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", filename, "--stdout"], standard_out=output_file, standard_error=None, ) self.assertEqual( skipped_file_file_text, output_file.getvalue(), ) def test_diff(self) -> None: with temporary_file( """\ import re import os import my_own_module x = 1 """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( """\ -import re -import os import my_own_module x = 1 """, "\n".join(output_file.getvalue().split("\n")[3:]), ) def test_diff_with_nonexistent_file(self) -> None: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "nonexistent_file"], standard_out=output_file, standard_error=output_file, ) self.assertIn("no such file", output_file.getvalue().lower()) def test_diff_with_encoding_declaration(self) -> None: with temporary_file( """\ # coding: iso-8859-1 import re import os import my_own_module x = 1 """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( """\ # coding: iso-8859-1 -import re -import os import my_own_module x = 1 """, "\n".join(output_file.getvalue().split("\n")[3:]), ) def test_in_place(self) -> None: with temporary_file( """\ import foo x = foo import subprocess x() try: import os except ImportError: import os """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--in-place", filename], standard_out=output_file, standard_error=None, ) with open(filename) as f: self.assertEqual( """\ import foo x = foo x() try: pass except ImportError: pass """, f.read(), ) def test_check_with_empty_file(self) -> None: line = "" with temporary_file(line) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--check", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( f"{filename}: No issues detected!{os.linesep}", output_file.getvalue(), ) def test_check_correct_file(self) -> None: with temporary_file( """\ import foo x = foo.bar print(x) """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--check", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( f"{filename}: No issues detected!{os.linesep}", output_file.getvalue(), ) def test_check_correct_file_with_quiet(self) -> None: with temporary_file( """\ import foo x = foo.bar print(x) """, ) as filename: output_file = io.StringIO() autoflake._main( argv=[ "my_fake_program", "--check", "--quiet", filename, ], standard_out=output_file, standard_error=None, ) self.assertEqual("", output_file.getvalue()) def test_check_useless_pass(self) -> None: with temporary_file( """\ import foo x = foo import subprocess x() try: pass import os except ImportError: pass import os import sys """, ) as filename: output_file = io.StringIO() exit_status = autoflake._main( argv=["my_fake_program", "--check", filename], standard_out=output_file, standard_error=None, ) self.assertEqual(exit_status, 1) self.assertEqual( f"{filename}: Unused imports/variables detected{os.linesep}", output_file.getvalue(), ) def test_check_with_multiple_files(self) -> None: with temporary_file("import sys") as file1: with temporary_file("import sys") as file2: output_file = io.StringIO() exit_status = autoflake._main( argv=["my_fake_program", "--check", file1, file2], standard_out=output_file, standard_error=None, ) self.assertEqual(exit_status, 1) self.assertEqual( { f"{file1}: Unused imports/variables detected", f"{file2}: Unused imports/variables detected", }, set(output_file.getvalue().strip().split(os.linesep)), ) def test_check_diff_with_empty_file(self) -> None: line = "" with temporary_file(line) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--check-diff", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( f"{filename}: No issues detected!{os.linesep}", output_file.getvalue(), ) def test_check_diff_correct_file(self) -> None: with temporary_file( """\ import foo x = foo.bar print(x) """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--check-diff", filename], standard_out=output_file, standard_error=None, ) self.assertEqual( f"{filename}: No issues detected!{os.linesep}", output_file.getvalue(), ) def test_check_diff_correct_file_with_quiet(self) -> None: with temporary_file( """\ import foo x = foo.bar print(x) """, ) as filename: output_file = io.StringIO() autoflake._main( argv=[ "my_fake_program", "--check-diff", "--quiet", filename, ], standard_out=output_file, standard_error=None, ) self.assertEqual("", output_file.getvalue()) def test_check_diff_useless_pass(self) -> None: with temporary_file( """\ import foo x = foo import subprocess x() try: pass import os except ImportError: pass import os import sys """, ) as filename: output_file = io.StringIO() exit_status = autoflake._main( argv=["my_fake_program", "--check-diff", filename], standard_out=output_file, standard_error=None, ) self.assertEqual(exit_status, 1) self.assertEqual( """\ import foo x = foo -import subprocess x() try: pass - import os except ImportError: pass - import os - import sys """, "\n".join(output_file.getvalue().split("\n")[3:]), ) def test_in_place_with_empty_file(self) -> None: line = "" with temporary_file(line) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--in-place", filename], standard_out=output_file, standard_error=None, ) with open(filename) as f: self.assertEqual(line, f.read()) def test_in_place_with_with_useless_pass(self) -> None: with temporary_file( """\ import foo x = foo import subprocess x() try: pass import os except ImportError: pass import os import sys """, ) as filename: output_file = io.StringIO() autoflake._main( argv=["my_fake_program", "--in-place", filename], standard_out=output_file, standard_error=None, ) with open(filename) as f: self.assertEqual( """\ import foo x = foo x() try: pass except ImportError: pass """, f.read(), ) def test_with_missing_file(self) -> None: output_file = io.StringIO() ignore = StubFile() autoflake._main( argv=["my_fake_program", "--in-place", ".fake"], standard_out=output_file, standard_error=ignore, # type: ignore ) self.assertFalse(output_file.getvalue()) def test_ignore_hidden_directories(self) -> None: with temporary_directory() as directory: with temporary_directory( prefix=".", directory=directory, ) as inner_directory: with temporary_file( """\ import re import os """, directory=inner_directory, ): output_file = io.StringIO() autoflake._main( argv=[ "my_fake_program", "--recursive", directory, ], standard_out=output_file, standard_error=None, ) self.assertEqual( "", output_file.getvalue().strip(), ) def test_in_place_and_stdout(self) -> None: output_file = io.StringIO() self.assertRaises( SystemExit, autoflake._main, argv=["my_fake_program", "--in-place", "--stdout", __file__], standard_out=output_file, standard_error=output_file, ) def test_end_to_end(self) -> None: with temporary_file( """\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os x = os.sep print(x) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--imports=fake_foo,fake_bar", filename, ], stdout=subprocess.PIPE, ) self.assertEqual( """\ -import fake_fake, fake_foo, fake_bar, fake_zoo -import re, os +import fake_fake +import fake_zoo +import os x = os.sep print(x) """, "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) def test_end_to_end_multiple_files(self) -> None: with temporary_file( """\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os x = os.sep print(x) """, ) as filename1: with temporary_file( """\ import os x = os.sep print(x) """, ) as filename2: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--imports=fake_foo,fake_bar", "--check", "--jobs=2", filename1, filename2, ], stdout=subprocess.PIPE, ) status_code = process.wait() self.assertEqual(1, status_code) def test_end_to_end_with_remove_all_unused_imports(self) -> None: with temporary_file( """\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os x = os.sep print(x) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--remove-all", filename, ], stdout=subprocess.PIPE, ) self.assertEqual( """\ -import fake_fake, fake_foo, fake_bar, fake_zoo -import re, os +import os x = os.sep print(x) """, "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) def test_end_to_end_with_remove_duplicate_keys_multiple_lines(self) -> None: with temporary_file( """\ a = { 'b': 456, 'a': 123, 'b': 7834, 'a': 'wow', 'b': 456, 'c': 'hello', 'c': 'hello2', 'b': 'hiya', } print(a) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--remove-duplicate-keys", filename, ], stdout=subprocess.PIPE, ) self.assertEqual( """\ a = { - 'b': 456, - 'a': 123, - 'b': 7834, 'a': 'wow', - 'b': 456, - 'c': 'hello', 'c': 'hello2', 'b': 'hiya', } """, "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) def test_end_to_end_with_remove_duplicate_keys_and_other_errors(self) -> None: with temporary_file( """\ from math import * print(sin(4)) a = { # Hello 'b': 456, 'a': 123, 'b': 7834, 'a': 'wow', 'b': 456, 'c': 'hello', 'c': 'hello2', 'b': 'hiya', } print(a) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--remove-duplicate-keys", filename, ], stdout=subprocess.PIPE, ) self.assertEqual( """\ from math import * print(sin(4)) a = { # Hello - 'b': 456, - 'a': 123, - 'b': 7834, 'a': 'wow', - 'b': 456, - 'c': 'hello', 'c': 'hello2', 'b': 'hiya', } """, "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) def test_end_to_end_with_remove_duplicate_keys_tuple(self) -> None: with temporary_file( """\ a = { (0,1): 1, (0, 1): 'two', (0,1): 3, } print(a) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--remove-duplicate-keys", filename, ], stdout=subprocess.PIPE, ) self.assertEqual( """\ a = { - (0,1): 1, - (0, 1): 'two', (0,1): 3, } print(a) """, "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) def test_end_to_end_with_error(self) -> None: with temporary_file( """\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os x = os.sep print(x) """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ "--imports=fake_foo,fake_bar", "--remove-all", filename, ], stderr=subprocess.PIPE, ) self.assertIn( "not allowed with argument", process.communicate()[1].decode(), ) def test_end_to_end_from_stdin(self) -> None: stdin_data = b"""\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os x = os.sep print(x) """ process = subprocess.Popen( AUTOFLAKE_COMMAND + ["--remove-all", "-"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, ) stdout, _ = process.communicate(stdin_data) self.assertEqual( """\ import os x = os.sep print(x) """, "\n".join(stdout.decode().split(os.linesep)), ) def test_end_to_end_from_stdin_with_in_place(self) -> None: stdin_data = b"""\ import fake_fake, fake_foo, fake_bar, fake_zoo import re, os, sys x = os.sep print(x) """ process = subprocess.Popen( AUTOFLAKE_COMMAND + ["--remove-all", "--in-place", "-"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, ) stdout, _ = process.communicate(stdin_data) self.assertEqual( """\ import os x = os.sep print(x) """, "\n".join(stdout.decode().split(os.linesep)), ) def test_end_to_end_dont_remove_unused_imports_when_not_using_flag(self) -> None: with temporary_file( """\ from . import fake_bar from . import fake_foo fake_foo.fake_function() """, ) as filename: process = subprocess.Popen( AUTOFLAKE_COMMAND + [ filename, ], stdout=subprocess.PIPE, ) self.assertEqual( "", "\n".join(process.communicate()[0].decode().split(os.linesep)[3:]), ) class MultilineFromImportTests(unittest.TestCase): def test_is_over(self) -> None: filt = autoflake.FilterMultilineImport("from . import (\n") self.assertTrue(filt.is_over("module)\n")) self.assertTrue(filt.is_over(" )\n")) self.assertTrue(filt.is_over(" ) # comment\n")) self.assertTrue(filt.is_over("from module import (a, b)\n")) self.assertFalse(filt.is_over("# )")) self.assertFalse(filt.is_over("module\n")) self.assertFalse(filt.is_over("module, \\\n")) self.assertFalse(filt.is_over("\n")) filt = autoflake.FilterMultilineImport("from . import module, \\\n") self.assertTrue(filt.is_over("module\n")) self.assertTrue(filt.is_over("\n")) self.assertTrue(filt.is_over("m1, m2 # comment with \\\n")) self.assertFalse(filt.is_over("m1, m2 \\\n")) self.assertFalse(filt.is_over("m1, m2 \\ #\n")) self.assertFalse(filt.is_over("m1, m2 \\ # comment with \\\n")) self.assertFalse(filt.is_over("\\\n")) # "Multiline" imports that are not really multiline filt = autoflake.FilterMultilineImport( "import os; " "import math, subprocess", ) self.assertTrue(filt.is_over()) unused = () def assert_fix( self, lines: Sequence[str], result: str, remove_all: bool = True, ) -> None: fixer = autoflake.FilterMultilineImport( lines[0], remove_all_unused_imports=remove_all, unused_module=self.unused, ) fixed = functools.reduce( lambda acc, x: acc(x) if isinstance(acc, autoflake.PendingFix) else acc, lines[1:], fixer(), ) self.assertEqual(fixed, result) def test_fix(self) -> None: self.unused = ["third_party.lib" + str(x) for x in (1, 3, 4)] # Example m0 (isort) self.assert_fix( [ "from third_party import (lib1, lib2, lib3,\n", " lib4, lib5, lib6)\n", ], "from third_party import (lib2, lib5, lib6)\n", ) # Example m1(isort) self.assert_fix( [ "from third_party import (lib1,\n", " lib2,\n", " lib3,\n", " lib4,\n", " lib5,\n", " lib6)\n", ], "from third_party import (lib2,\n" " lib5,\n" " lib6)\n", ) # Variation m1(isort) self.assert_fix( [ "from third_party import (lib1\n", " ,lib2\n", " ,lib3\n", " ,lib4\n", " ,lib5\n", " ,lib6)\n", ], "from third_party import (lib2\n" " ,lib5\n" " ,lib6)\n", ) # Example m2 (isort) self.assert_fix( [ "from third_party import \\\n", " lib1, lib2, lib3, \\\n", " lib4, lib5, lib6\n", ], "from third_party import \\\n" " lib2, lib5, lib6\n", ) # Example m3 (isort) self.assert_fix( [ "from third_party import (\n", " lib1,\n", " lib2,\n", " lib3,\n", " lib4,\n", " lib5\n", ")\n", ], "from third_party import (\n" " lib2,\n" " lib5\n" ")\n", ) # Example m4 (isort) self.assert_fix( [ "from third_party import (\n", " lib1, lib2, lib3, lib4,\n", " lib5, lib6)\n", ], "from third_party import (\n" " lib2, lib5, lib6)\n", ) # Example m5 (isort) self.assert_fix( [ "from third_party import (\n", " lib1, lib2, lib3, lib4,\n", " lib5, lib6\n", ")\n", ], "from third_party import (\n" " lib2, lib5, lib6\n" ")\n", ) # Some Deviations self.assert_fix( [ "from third_party import (\n", " lib1\\\n", # only unused + line continuation " ,lib2, \n", " libA\n", # used import with no commas " ,lib3, \n", # leading and trailing commas + unused import " libB, \n", " \\\n", # empty line with continuation " lib4,\n", # unused import with comment ")\n", ], "from third_party import (\n" " lib2\\\n" " ,libA, \n" " libB,\n" ")\n", ) self.assert_fix( [ "from third_party import (\n", " lib1\n", ",\n", " lib2\n", ",\n", " lib3\n", ",\n", " lib4\n", ",\n", " lib5\n", ")\n", ], "from third_party import (\n" " lib2\n" ",\n" " lib5\n" ")\n", ) self.assert_fix( [ "from third_party import (\n", " lib1 \\\n", ", \\\n", " lib2 \\\n", ",\\\n", " lib3\n", ",\n", " lib4\n", ",\n", " lib5 \\\n", ")\n", ], "from third_party import (\n" " lib2 \\\n" ", \\\n" " lib5 \\\n" ")\n", ) def test_indentation(self) -> None: # Some weird indentation examples self.unused = ["third_party.lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ " from third_party import (\n", " lib1, lib2, lib3, lib4,\n", " lib5, lib6\n", ")\n", ], " from third_party import (\n" " lib2, lib5, lib6\n" ")\n", ) self.assert_fix( [ "\tfrom third_party import \\\n", "\t\tlib1, lib2, lib3, \\\n", "\t\tlib4, lib5, lib6\n", ], "\tfrom third_party import \\\n" "\t\tlib2, lib5, lib6\n", ) def test_fix_relative(self) -> None: # Example m0 (isort) self.unused = [".lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "from . import (lib1, lib2, lib3,\n", " lib4, lib5, lib6)\n", ], "from . import (lib2, lib5, lib6)\n", ) # Example m1(isort) self.unused = ["..lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "from .. import (lib1,\n", " lib2,\n", " lib3,\n", " lib4,\n", " lib5,\n", " lib6)\n", ], "from .. import (lib2,\n" " lib5,\n" " lib6)\n", ) # Example m2 (isort) self.unused = ["...lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "from ... import \\\n", " lib1, lib2, lib3, \\\n", " lib4, lib5, lib6\n", ], "from ... import \\\n" " lib2, lib5, lib6\n", ) # Example m3 (isort) self.unused = [".parent.lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "from .parent import (\n", " lib1,\n", " lib2,\n", " lib3,\n", " lib4,\n", " lib5\n", ")\n", ], "from .parent import (\n" " lib2,\n" " lib5\n" ")\n", ) def test_fix_without_from(self) -> None: self.unused = ["lib" + str(x) for x in (1, 3, 4)] # Multiline but not "from" self.assert_fix( [ "import \\\n", " lib1, lib2, lib3 \\\n", " ,lib4, lib5, lib6\n", ], "import \\\n" " lib2, lib5, lib6\n", ) self.assert_fix( [ "import lib1, lib2, lib3, \\\n", " lib4, lib5, lib6\n", ], "import lib2, lib5, lib6\n", ) # Problematic example without "from" self.assert_fix( [ "import \\\n", " lib1,\\\n", " lib2, \\\n", " libA\\\n", # used import with no commas " ,lib3, \\\n", # leading and trailing commas with unused " libB, \\\n", " \\ \n", # empty line with continuation " lib4\\\n", # unused import with comment "\n", ], "import \\\n" " lib2,\\\n" " libA, \\\n" " libB\\\n" "\n", ) self.unused = [f"lib{x}.x.y.z" for x in (1, 3, 4)] self.assert_fix( [ "import \\\n", " lib1.x.y.z \\", " , \\\n", " lib2.x.y.z \\\n", " , \\\n", " lib3.x.y.z \\\n", " , \\\n", " lib4.x.y.z \\\n", " , \\\n", " lib5.x.y.z\n", ], "import \\\n" " lib2.x.y.z \\" " , \\\n" " lib5.x.y.z\n", ) def test_give_up(self) -> None: # Semicolon self.unused = ["lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "import \\\n", " lib1, lib2, lib3, \\\n", " lib4, lib5; import lib6\n", ], "import \\\n" " lib1, lib2, lib3, \\\n" " lib4, lib5; import lib6\n", ) # Comments self.unused = [".lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "from . import ( # comment\n", " lib1,\\\n", # only unused + line continuation " lib2, \n", " libA\n", # used import with no commas " ,lib3, \n", # leading and trailing commas + unused import " libB, \n", " \\ \n", # empty line with continuation " lib4, # noqa \n", # unused import with comment ") ; import sys\n", ], "from . import ( # comment\n" " lib1,\\\n" " lib2, \n" " libA\n" " ,lib3, \n" " libB, \n" " \\ \n" " lib4, # noqa \n" ") ; import sys\n", ) def test_just_one_import_used(self) -> None: self.unused = ["lib2"] self.assert_fix( [ "import \\\n", " lib1\n", ], "import \\\n" " lib1\n", ) self.assert_fix( [ "import \\\n", " lib2\n", ], "pass\n", ) # Example from issue #8 self.unused = ["re.subn"] self.assert_fix( [ "\tfrom re import (subn)\n", ], "\tpass\n", ) def test_just_one_import_left(self) -> None: # Examples from issue #8 self.unused = ["math.sqrt"] self.assert_fix( [ "from math import (\n", " sqrt,\n", " log\n", " )\n", ], "from math import (\n" " log\n" " )\n", ) self.unused = ["module.b"] self.assert_fix( [ "from module import (a, b)\n", ], "from module import a\n", ) self.assert_fix( [ "from module import (a,\n", " b)\n", ], "from module import a\n", ) self.unused = [] self.assert_fix( [ "from re import (subn)\n", ], "from re import (subn)\n", ) def test_no_empty_imports(self) -> None: self.unused = ["lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "import \\\n", " lib1, lib3, \\\n", " lib4 \n", ], "pass \n", ) # Indented parenthesized block self.unused = [".parent.lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "\t\tfrom .parent import (\n", " lib1,\n", " lib3,\n", " lib4,\n", ")\n", ], "\t\tpass\n", ) def test_without_remove_all(self) -> None: self.unused = ["lib" + str(x) for x in (1, 3, 4)] self.assert_fix( [ "import \\\n", " lib1,\\\n", " lib3,\\\n", " lib4\n", ], "import \\\n" " lib1,\\\n" " lib3,\\\n" " lib4\n", remove_all=False, ) self.unused += ["os.path." + x for x in ("dirname", "isdir", "join")] self.assert_fix( [ "from os.path import (\n", " dirname,\n", " isdir,\n", " join,\n", ")\n", ], "pass\n", remove_all=False, ) self.assert_fix( [ "import \\\n", " os.path.dirname, \\\n", " lib1, \\\n", " lib3\n", ], "import \\\n" " lib1, \\\n" " lib3\n", remove_all=False, ) class ConfigFileTest(unittest.TestCase): def setUp(self) -> None: self.tmpdir = tempfile.mkdtemp(prefix="autoflake.") def tearDown(self) -> None: if self.tmpdir: shutil.rmtree(self.tmpdir) def effective_path(self, path: str, is_file: bool = True) -> str: path = os.path.normpath(path) if os.path.isabs(path): raise ValueError("Should not create an absolute test path") effective_path = os.path.sep.join([self.tmpdir, path]) if not effective_path.startswith( f"{self.tmpdir}{os.path.sep}", ) and (effective_path != self.tmpdir or is_file): raise ValueError("Should create a path within the tmp dir only") return effective_path def create_dir(self, path) -> None: effective_path = self.effective_path(path, False) os.makedirs(effective_path, exist_ok=True) def create_file(self, path, contents="") -> None: effective_path = self.effective_path(path) self.create_dir(os.path.split(path)[0]) with open(effective_path, "w") as f: f.write(contents) def with_defaults(self, **kwargs: Any) -> Mapping[str, Any]: return { "check": False, "check_diff": False, "expand_star_imports": False, "ignore_init_module_imports": False, "ignore_pass_after_docstring": False, "ignore_pass_statements": False, "in_place": False, "quiet": False, "recursive": False, "remove_all_unused_imports": False, "remove_duplicate_keys": False, "remove_rhs_for_unused_variables": False, "remove_unused_variables": False, "write_to_stdout": False, **kwargs, } def test_no_config_file(self) -> None: self.create_file("test_me.py") original_args = { "files": [self.effective_path("test_me.py")], } args, success = autoflake.merge_configuration_file(original_args) assert success is True assert args == self.with_defaults(**original_args) def test_non_nested_pyproject_toml_empty(self) -> None: self.create_file("test_me.py") self.create_file("pyproject.toml", '[tool.other]\nprop="value"\n') files = [self.effective_path("test_me.py")] original_args = {"files": files} args, success = autoflake.merge_configuration_file(original_args) assert success is True assert args == self.with_defaults(**original_args) def test_non_nested_pyproject_toml_non_empty(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", "[tool.autoflake]\nexpand-star-imports=true\n", ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=True, ) def test_non_nested_setup_cfg_non_empty(self) -> None: self.create_file("test_me.py") self.create_file( "setup.cfg", "[other]\nexpand-star-imports = yes\n", ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults(files=files) def test_non_nested_setup_cfg_empty(self) -> None: self.create_file("test_me.py") self.create_file( "setup.cfg", "[autoflake]\nexpand-star-imports = yes\n", ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=True, ) def test_nested_file(self) -> None: self.create_file("nested/file/test_me.py") self.create_file( "pyproject.toml", "[tool.autoflake]\nexpand-star-imports=true\n", ) files = [self.effective_path("nested/file/test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=True, ) def test_common_path_nested_file_do_not_load(self) -> None: self.create_file("nested/file/test_me.py") self.create_file("nested/other/test_me.py") self.create_file( "nested/file/pyproject.toml", "[tool.autoflake]\nexpand-star-imports=true\n", ) files = [ self.effective_path("nested/file/test_me.py"), self.effective_path("nested/other/test_me.py"), ] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults(files=files) def test_common_path_nested_file_do_load(self) -> None: self.create_file("nested/file/test_me.py") self.create_file("nested/other/test_me.py") self.create_file( "nested/pyproject.toml", "[tool.autoflake]\nexpand-star-imports=true\n", ) files = [ self.effective_path("nested/file/test_me.py"), self.effective_path("nested/other/test_me.py"), ] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=True, ) def test_common_path_instead_of_common_prefix(self) -> None: """Using common prefix would result in a failure.""" self.create_file("nested/file-foo/test_me.py") self.create_file("nested/file-bar/test_me.py") self.create_file( "nested/file/pyproject.toml", "[tool.autoflake]\nexpand-star-imports=true\n", ) files = [ self.effective_path("nested/file-foo/test_me.py"), self.effective_path("nested/file-bar/test_me.py"), ] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults(files=files) def test_continue_search_if_no_config_found(self) -> None: self.create_file("nested/test_me.py") self.create_file( "nested/pyproject.toml", '[tool.other]\nprop = "value"\n', ) self.create_file( "pyproject.toml", "[tool.autoflake]\nexpand-star-imports = true\n", ) files = [self.effective_path("nested/test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=True, ) def test_stop_search_if_config_found(self) -> None: self.create_file("nested/test_me.py") self.create_file( "nested/pyproject.toml", "[tool.autoflake]\n", ) self.create_file( "pyproject.toml", "[tool.autoflake]\nexpand-star-imports = true\n", ) files = [self.effective_path("nested/test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults(files=files) def test_config_option(self) -> None: with temporary_file( suffix=".ini", contents=("[autoflake]\n" "check = True\n"), ) as temp_config: self.create_file("test_me.py") files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file( { "files": files, "config_file": temp_config, }, ) assert success is True assert args == self.with_defaults( files=files, config_file=temp_config, check=True, ) def test_merge_configuration_file__toml_config_option(self) -> None: with temporary_file( suffix=".toml", contents=("[tool.autoflake]\n" "check = true\n"), ) as temp_config: self.create_file("test_me.py") files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file( { "files": files, "config_file": temp_config, }, ) assert success is True assert args == self.with_defaults( files=files, config_file=temp_config, check=True, ) def test_load_false(self) -> None: self.create_file("test_me.py") self.create_file( "setup.cfg", "[autoflake]\nexpand-star-imports = no\n", ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, expand_star_imports=False, ) def test_list_value_pyproject_toml(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", '[tool.autoflake]\nimports=["my_lib", "other_lib"]\n', ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, imports="my_lib,other_lib", ) def test_list_value_comma_sep_string_pyproject_toml(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", '[tool.autoflake]\nimports="my_lib,other_lib"\n', ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, imports="my_lib,other_lib", ) def test_list_value_setup_cfg(self) -> None: self.create_file("test_me.py") self.create_file( "setup.cfg", "[autoflake]\nimports=my_lib,other_lib\n", ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file({"files": files}) assert success is True assert args == self.with_defaults( files=files, imports="my_lib,other_lib", ) def test_non_bool_value_for_bool_property(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", '[tool.autoflake]\nexpand-star-imports="invalid"\n', ) files = [self.effective_path("test_me.py")] _, success = autoflake.merge_configuration_file({"files": files}) assert success is False def test_non_bool_value_for_bool_property_in_setup_cfg(self) -> None: self.create_file("test_me.py") self.create_file( "setup.cfg", "[autoflake]\nexpand-star-imports=ok\n", ) files = [self.effective_path("test_me.py")] _, success = autoflake.merge_configuration_file({"files": files}) assert success is False def test_non_list_value_for_list_property(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", "[tool.autoflake]\nexclude=true\n", ) files = [self.effective_path("test_me.py")] _, success = autoflake.merge_configuration_file({"files": files}) assert success is False def test_merge_with_cli_set_list_property(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", '[tool.autoflake]\nimports=["my_lib"]\n', ) files = [self.effective_path("test_me.py")] args, success = autoflake.merge_configuration_file( {"files": files, "imports": "other_lib"}, ) assert success is True assert args == self.with_defaults( files=files, imports="my_lib,other_lib", ) def test_merge_prioritizes_flags(self) -> None: self.create_file("test_me.py") self.create_file( "pyproject.toml", "[tool.autoflake]\ncheck = false\n", ) files = [self.effective_path("test_me.py")] flag_args = { "files": files, "imports": "other_lib", "check": True, } args, success = autoflake.merge_configuration_file(flag_args) assert success is True assert args == self.with_defaults( files=files, imports="other_lib", check=True, ) @contextlib.contextmanager def temporary_file( contents: str, directory: str = ".", suffix: str = ".py", prefix: str = "", ) -> Iterator[str]: """Write contents to temporary file and yield it.""" f = tempfile.NamedTemporaryFile( suffix=suffix, prefix=prefix, delete=False, dir=directory, ) try: f.write(contents.encode()) f.close() yield f.name finally: os.remove(f.name) @contextlib.contextmanager def temporary_directory(directory: str = ".", prefix: str = "tmp.") -> Iterator[str]: """Create temporary directory and yield its path.""" temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) try: yield temp_directory finally: shutil.rmtree(temp_directory) class StubFile: """Fake file that ignores everything.""" def write(*_: Any) -> None: """Ignore.""" if __name__ == "__main__": unittest.main() autoflake-2.3.0/test_fuzz.py000077500000000000000000000172531456455310400161200ustar00rootroot00000000000000#!/usr/bin/env python """Test that autoflake performs correctly on arbitrary Python files. This checks that autoflake never introduces incorrect syntax. This is done by doing a syntax check after the autoflake run. The number of Pyflakes warnings is also confirmed to always improve. """ from __future__ import annotations import argparse import os import shlex import subprocess import sys from typing import Sequence import autoflake ROOT_PATH = os.path.abspath(os.path.dirname(__file__)) AUTOFLAKE_BIN = "'{}' '{}'".format( sys.executable, os.path.join(ROOT_PATH, "autoflake.py"), ) if sys.stdout.isatty(): YELLOW = "\x1b[33m" END = "\x1b[0m" else: YELLOW = "" END = "" def colored(text: str, color: str) -> str: """Return color coded text.""" return color + text + END def pyflakes_count(filename: str) -> int: """Return pyflakes error count.""" with autoflake.open_with_encoding( filename, encoding=autoflake.detect_encoding(filename), ) as f: return len(list(autoflake.check(f.read()))) def readlines(filename: str) -> Sequence[str]: """Return contents of file as a list of lines.""" with autoflake.open_with_encoding( filename, encoding=autoflake.detect_encoding(filename), ) as f: return f.readlines() def diff(before: str, after: str) -> str: """Return diff of two files.""" import difflib return "".join( difflib.unified_diff( readlines(before), readlines(after), before, after, ), ) def run( filename: str, command: str, verbose: bool = False, options: list[str] | None = None, ) -> bool: """Run autoflake on file at filename. Return True on success. """ if not options: options = [] import test_autoflake with test_autoflake.temporary_directory() as temp_directory: temp_filename = os.path.join( temp_directory, os.path.basename(filename), ) import shutil shutil.copyfile(filename, temp_filename) if 0 != subprocess.call( shlex.split(command) + ["--in-place", temp_filename] + options, ): sys.stderr.write("autoflake crashed on " + filename + "\n") return False try: file_diff = diff(filename, temp_filename) if verbose: sys.stderr.write(file_diff) if check_syntax(filename): try: check_syntax(temp_filename, raise_error=True) except ( SyntaxError, TypeError, UnicodeDecodeError, ValueError, ) as exception: sys.stderr.write( "autoflake broke " + filename + "\n" + str(exception) + "\n", ) return False before_count = pyflakes_count(filename) after_count = pyflakes_count(temp_filename) if verbose: print("(before, after):", (before_count, after_count)) if file_diff and after_count > before_count: sys.stderr.write("autoflake made " + filename + " worse\n") return False except OSError as exception: sys.stderr.write(str(exception) + "\n") return True def check_syntax(filename: str, raise_error: bool = False) -> bool: """Return True if syntax is okay.""" with autoflake.open_with_encoding( filename, encoding=autoflake.detect_encoding(filename), ) as input_file: try: compile(input_file.read(), "", "exec", dont_inherit=True) return True except (SyntaxError, TypeError, UnicodeDecodeError, ValueError): if raise_error: raise else: return False def process_args() -> argparse.Namespace: """Return processed arguments (options and positional arguments).""" parser = argparse.ArgumentParser() parser.add_argument( "--command", default=AUTOFLAKE_BIN, help="autoflake command (default: %(default)s)", ) parser.add_argument( "--expand-star-imports", action="store_true", help="expand wildcard star imports with undefined " "names", ) parser.add_argument( "--imports", help='pass to the autoflake "--imports" option', ) parser.add_argument( "--remove-all-unused-imports", action="store_true", help='pass "--remove-all-unused-imports" option to ' "autoflake", ) parser.add_argument( "--remove-duplicate-keys", action="store_true", help='pass "--remove-duplicate-keys" option to ' "autoflake", ) parser.add_argument( "--remove-unused-variables", action="store_true", help='pass "--remove-unused-variables" option to ' "autoflake", ) parser.add_argument( "-v", "--verbose", action="store_true", help="print verbose messages", ) parser.add_argument("files", nargs="*", help="files to test against") return parser.parse_args() def check(args: argparse.Namespace) -> bool: """Run recursively run autoflake on directory of files. Return False if the fix results in broken syntax. """ if args.files: dir_paths = args.files else: dir_paths = [path for path in sys.path if os.path.isdir(path)] options = [] if args.expand_star_imports: options.append("--expand-star-imports") if args.imports: options.append("--imports=" + args.imports) if args.remove_all_unused_imports: options.append("--remove-all-unused-imports") if args.remove_duplicate_keys: options.append("--remove-duplicate-keys") if args.remove_unused_variables: options.append("--remove-unused-variables") filenames = dir_paths completed_filenames = set() while filenames: try: name = os.path.realpath(filenames.pop(0)) if not os.path.exists(name): # Invalid symlink. continue if name in completed_filenames: sys.stderr.write( colored( "---> Skipping previously tested " + name + "\n", YELLOW, ), ) continue else: completed_filenames.update(name) if os.path.isdir(name): for root, directories, children in os.walk(name): filenames += [ os.path.join(root, f) for f in children if f.endswith(".py") and not f.startswith(".") ] directories[:] = [d for d in directories if not d.startswith(".")] else: verbose_message = "---> Testing with " + name sys.stderr.write(colored(verbose_message + "\n", YELLOW)) if not run( os.path.join(name), command=args.command, verbose=args.verbose, options=options, ): return False except (UnicodeDecodeError, UnicodeEncodeError) as exception: # Ignore annoying codec problems on Python 2. print(exception, file=sys.stderr) continue return True def main() -> int: """Run main.""" return 0 if check(process_args()) else 1 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: sys.exit(1)