diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cde2bbb8..47ede925 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3.5.3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa2422ea..2d35da72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: isort - repo: https://github.com/hakancelikdev/unimport - rev: 1.1.0 + rev: 1.2.0 hooks: - id: unimport diff --git a/action.yml b/action.yml index a498d044..f31f57c4 100644 --- a/action.yml +++ b/action.yml @@ -10,7 +10,7 @@ inputs: runs: using: "composite" steps: - - run: pip install --upgrade pip && python -m pip install unimport==1.1.0 + - run: pip install --upgrade pip && python -m pip install unimport==1.2.0 shell: bash - run: unimport --color auto --gitignore --ignore-init ${{ inputs.extra_args }} shell: bash diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3a958fee..3f95bf77 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,11 +4,32 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - YYYY-MM-DD -## [1.2.0] - 2023-12-03 +## [1.2.0] - 2023-12-22 ### Changed - Python 3.6 support dropped +- Python 3.7 support dropped + +### Fixed + +- Improved Type Comment Analysis + + > To ensure accurate type comment analysis, a crucial fix was implemented in the code. + > The update involves refining the process by adding a check within ast.AnnAssign and + > ast.arg types. Specifically, the code now includes a verification that + > visit_Constant occurs under an annotation, addressing unnecessary name discovery + > issues for better accuracy in type_comment assessment. + + ```python + import sys + from typing import ( + List, + ) + + + test_list: List[str] = ["spam", "eggs"] # it thought spam and eggs were a ast.Name object. + ``` ## [1.1.0] - 2023-11-17 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3055473f..6b03fe16 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,10 +35,10 @@ $ git rebase upstream/main ## Testing -First, make sure you have at least one of the python versions py3.7, py3.8, py3.9, -py3.10 and py3.11. If not all versions are available, after opening PR, github action -will run the tests for each version, so you can be sure that you wrote the correct code. -You can skip the tox step below. +First, make sure you have at least one of the python versions py3.8, py3.9, py3.10 and +py3.11. If not all versions are available, after opening PR, github action will run the +tests for each version, so you can be sure that you wrote the correct code. You can skip +the tox step below. After typing your codes, you should run the tests by typing the following command. diff --git a/docs/FAQ.md b/docs/FAQ.md deleted file mode 100644 index 012074e9..00000000 --- a/docs/FAQ.md +++ /dev/null @@ -1,113 +0,0 @@ -# FAQ - -## Differences between Autoflake and Unimport - -- Autoflake doesn't always remove the duplicate imports when they are on separate lines. - -Example: - -```py -from os import walk -from os import walk - -use(walk) -``` - -For this snippet, autoflake doesn't change anything, while unimport detects and removes -the _first_ walk import. - -- Autoflake replaces unused imports in compound statements with `pass`, while unimport - detects and imports inside compound statements, if it detects that you are expecting - an `ImportError`, it doesn't remove that particular import. - -```py -try: - from x import y -except ImportError: - ... -``` - -For this snippet autoflake replaces the import statement with `pass.`, while unimport -leaves it as is. - -- Autoflake is not accurate when it comes to star import expansions, while unimport can - detect and expand them accurately. - -```py -from math import * - -use(RANDOM_VAR) -``` - -Running autoflake with --expand-star-import flag on the snippet above turns it into - -```py -from math import RANDOM_VAR -``` - -while unimport simple removes the math import because it is not used. - -- Autoflake doesn't work with multiple star imports, while unimport does. - -from math import _ from os import _ - -use(walk, cos) - -Running unimport on the above snippet with --include-star-imports flag produces the -correct output. - -```py -from math import cos -from os import walk - -use(walk, cos) -``` - -while autoflake simply ignores them. - -- Our outputs are more useful, try using our --check, --diff or --permission commands. - -### Performance - -Unimport < 0.6.8 was much slower than Autoflake == 1.4 (current latest version as of -writing this) but Unimport > 0.6.8 is slightly faster. - -### Reasons to choose autoflake - -- ~~It is faster. When tested, autoflake is 1-4x faster on average.~~ (Unimport is - slightly faster now) -- It removes unused variables which unimport doesn't support, and is not planning to. -- Has a feature that removes duplicate keys on objects. - -### Reasons to choose unimport - -- It does more static analysis to increase the accuracy of choosing the correct imports - to remove. -- Can handle star imports more accurately.(https://github.com/myint/autoflake/pull/18 - describes their approach) -- Works with multiple star imports. -- Removes duplicate imports. -- Has skip_file feature that allows one to skip an entire file. -- Allows configuration via pyproject.toml and setup.cfg files. - -### Overall - -Even though unimport and autoflake has features that are similar, they are not designed -to do the same thing. When you are including one to your project, it is a good idea to -know what your needs are, and decide accordingly. - -## I can already do this with many IDEs, why should I use Unimport? - -### Short answer - -Not everyone works with IDEs and not all IDEs work in all environments. - -### Long answer - -Imagine that you are working with a team where everyone may not have the same coding -environment. For instance, not everyone uses PyCharm or VSCode. In such a scenario, if -you want to apply certain standards, you can use pre-commit. It can be helpful. -Furthermore, if you want to ensure that unnecessary imports are removed before -committing, you should use Unimport with pre-commit. Let's assume that you want to -ensure that there are no unnecessary imports in your entire project. In that case, -Unimport will help you achieve this goal. diff --git a/docs/index.md b/docs/index.md index ab8cb3b3..981220b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,5 +29,5 @@ process with Unimport. - **Documentation** https://unimport.hakancelik.dev/ - **Issues** https://github.com/hakancelikdev/unimport/issues/ -- **Changelog** https://unimport.hakancelik.dev/1.1.0/CHANGELOG/ +- **Changelog** https://unimport.hakancelik.dev/1.2.0/CHANGELOG/ - **Playground** https://playground-unimport.hakancelik.dev/ diff --git a/docs/installation.md b/docs/installation.md index 372e6582..d2362c46 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,4 +1,4 @@ -Unimport requires Python 3.7+ and can be easily installed using most common Python +Unimport requires Python 3.8+ and can be easily installed using most common Python packaging tools. We recommend installing the latest stable release from PyPI with pip: ```shell diff --git a/docs/tutorial/other-useful-features.md b/docs/tutorial/other-useful-features.md index 9f0ff83f..d0659a44 100644 --- a/docs/tutorial/other-useful-features.md +++ b/docs/tutorial/other-useful-features.md @@ -14,8 +14,8 @@ from x import ( # noqa ) ``` -**If version of your python is 3.8+** Unimport support multiple skip like below. _It -doesn't matter which line you put the comment on._ +Unimport support multiple skip like below. _It doesn't matter which line you put the +comment on._ ```python from package import ( diff --git a/docs/tutorial/supported-behaviors.md b/docs/tutorial/supported-behaviors.md index 37aacac0..0be33e4c 100644 --- a/docs/tutorial/supported-behaviors.md +++ b/docs/tutorial/supported-behaviors.md @@ -36,9 +36,7 @@ def test(arg: "List['Dict']") -> None: #### Comments -**This feature is only available for python 3.8.** - -Imports in the example below aren't flag as unused by import. +Imports in the example below aren't flag as unused by unimport. ```python from typing import Any diff --git a/docs/tutorial/use-with-github-action.md b/docs/tutorial/use-with-github-action.md index eaa78522..26a18aab 100644 --- a/docs/tutorial/use-with-github-action.md +++ b/docs/tutorial/use-with-github-action.md @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v3.5.3 - uses: actions/setup-python@v4.6.1 - name: Check unused imports - uses: hakancelikdev/unimport@1.1.0 + uses: hakancelikdev/unimport@1.2.0 with: extra_args: --include src/ ``` diff --git a/mkdocs.yml b/mkdocs.yml index 93fc24f0..721889fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ site_url: https://unimport.hakancelik.dev repo_url: https://github.com/hakancelikdev/unimport repo_name: hakancelikdev/unimport edit_uri: https://github.com/hakancelikdev/unimport/tree/main/docs -copyright: Copyright © 2020 - 2023 Hakan Çelik +copyright: Copyright © 2019 - 2024 Hakan Çelik markdown_extensions: - fenced_code @@ -132,4 +132,3 @@ nav: - Contributing: CONTRIBUTING.md - Changelog: CHANGELOG.md - Authors: AUTHORS.md - - FAQ: FAQ.md diff --git a/pyproject.toml b/pyproject.toml index 6e4499b9..7b0fe2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ skip_gitignore = true [tool.black] line-length = 120 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py38', 'py39', 'py310', 'py311'] [tool.unimport] include_star_import = true diff --git a/setup.cfg b/setup.cfg index fc936f49..b003fe99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -29,10 +28,10 @@ classifiers = project_urls = Documentation = https://unimport.hakancelik.dev/ Issues = https://github.com/hakancelikdev/unimport/issues/ - Changelog = https://unimport.hakancelik.dev/1.1.0/CHANGELOG/ + Changelog = https://unimport.hakancelik.dev/1.2.0/CHANGELOG/ [options] -python_requires = >=3.7, <3.12 +python_requires = >=3.8, <3.12 include_package_data = true zip_safe = true packages = @@ -46,10 +45,8 @@ install_requires = libcst>=0.3.7, <=1.1.0; python_version == '3.10' libcst>=0.3.7, <=1.1.0; python_version == '3.9' libcst>=0.3.0, <=1.1.0; python_version == '3.8' - libcst>=0.3.0, <=1.0.1; python_version == '3.7' - pathspec>=0.10.1, <1; python_version >= '3.7' + pathspec>=0.10.1, <1 toml>=0.9.0, <1 - typing-extensions>=3.7.4, <4; python_version < '3.8' [options.entry_points] console_scripts = diff --git a/src/unimport/__init__.py b/src/unimport/__init__.py index 0c311180..4db71176 100644 --- a/src/unimport/__init__.py +++ b/src/unimport/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.1.0" +__version__ = "1.2.0" __description__ = "A linter, formatter for finding and removing unused import statements." diff --git a/src/unimport/analyzers/decarators.py b/src/unimport/analyzers/decarators.py index 01eb4852..a75bfc72 100644 --- a/src/unimport/analyzers/decarators.py +++ b/src/unimport/analyzers/decarators.py @@ -2,7 +2,6 @@ import re from typing import cast -from unimport import constants as C from unimport import typing as T __all__ = ("generic_visit", "skip_import") @@ -22,11 +21,7 @@ def skip_import(func: T.FunctionT) -> T.FunctionT: @functools.wraps(func) def wrapper(self, node, *args, **kwargs): - if C.PY38_PLUS: - source_segment = "\n".join(self.source.splitlines()[node.lineno - 1 : node.end_lineno]) - else: - source_segment = self.source.splitlines()[node.lineno - 1] - + source_segment = "\n".join(self.source.splitlines()[node.lineno - 1 : node.end_lineno]) skip_comment = bool(re.search(SKIP_IMPORT_COMMENTS_REGEX, source_segment, re.IGNORECASE)) if not any((skip_comment, self.any_import_error)): func(self, node, *args, **kwargs) diff --git a/src/unimport/analyzers/importable.py b/src/unimport/analyzers/importable.py index 0ec19792..9efdebac 100644 --- a/src/unimport/analyzers/importable.py +++ b/src/unimport/analyzers/importable.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import ast -from typing import FrozenSet, List from unimport import constants as C from unimport import typing as T @@ -18,17 +19,14 @@ class ImportableAnalyzer(ast.NodeVisitor): ) def __init__(self) -> None: - self.importable_nodes: List[T.ASTNameType] = [] # nodes on the __all__ list - self.suggestions_nodes: List[T.ASTImportableT] = [] # nodes on the CFN + self.importable_nodes: list[ast.Constant] = [] # nodes on the __all__ list + self.suggestions_nodes: list[T.ASTImportableT] = [] # nodes on the CFN def traverse(self, tree): self.visit(tree) for node in self.importable_nodes: - if isinstance(node, ast.Constant): - Name.register(lineno=node.lineno, name=str(node.value), node=node, is_all=True) - elif isinstance(node, ast.Str): - Name.register(lineno=node.lineno, name=node.s, node=node, is_all=True) + Name.register(lineno=node.lineno, name=node.value, node=node, is_all=True) self.clear() @@ -61,7 +59,7 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: def visit_Assign(self, node: ast.Assign) -> None: if getattr(node.targets[0], "id", None) == "__all__" and isinstance(node.value, (ast.List, ast.Tuple, ast.Set)): for item in node.value.elts: - if isinstance(item, (ast.Constant, ast.Str)): + if isinstance(item, ast.Constant) and isinstance(item.value, str): self.importable_nodes.append(item) for target in node.targets: # we only get assigned names @@ -78,18 +76,18 @@ def visit_Expr(self, node: ast.Expr) -> None: ): if node.value.func.attr == "append": for arg in node.value.args: - if isinstance(arg, (ast.Constant, ast.Str)): + if isinstance(arg, ast.Constant) and isinstance(arg.value, str): self.importable_nodes.append(arg) elif node.value.func.attr == "extend": for arg in node.value.args: if isinstance(arg, ast.List): for item in arg.elts: - if isinstance(item, (ast.Constant, ast.Str)): + if isinstance(item, ast.Constant) and isinstance(item.value, str): self.importable_nodes.append(item) @classmethod - def get_names(cls, package: str) -> FrozenSet[str]: + def get_names(cls, package: str) -> frozenset[str]: if utils.is_std(package): return utils.get_dir(package) @@ -106,16 +104,13 @@ def get_names(cls, package: str) -> FrozenSet[str]: return visitor.get_all() or visitor.get_suggestion() return frozenset() - def get_all(self) -> FrozenSet[str]: + def get_all(self) -> frozenset[str]: names = set() for node in self.importable_nodes: - if isinstance(node, ast.Constant): - names.add(node.value) - elif isinstance(node, ast.Str): - names.add(node.s) + names.add(node.value) return frozenset(names) - def get_suggestion(self) -> FrozenSet[str]: + def get_suggestion(self) -> frozenset[str]: names = set() for node in self.suggestions_nodes: # type: ignore if isinstance(node, ast.Name): diff --git a/src/unimport/analyzers/main.py b/src/unimport/analyzers/main.py index 4edf5cbb..4f1621ce 100644 --- a/src/unimport/analyzers/main.py +++ b/src/unimport/analyzers/main.py @@ -2,7 +2,6 @@ import re from pathlib import Path -from unimport import constants as C from unimport.analyzers.import_statement import ImportAnalyzer from unimport.analyzers.importable import ImportableAnalyzer from unimport.analyzers.name import NameAnalyzer @@ -31,7 +30,7 @@ def traverse(self) -> None: if self.skip_file(): return None - tree = ast.parse(self.source, type_comments=True) if C.PY38_PLUS else ast.parse(self.source) + tree = ast.parse(self.source, type_comments=True) set_tree_parents(tree) # set parents to tree diff --git a/src/unimport/analyzers/name.py b/src/unimport/analyzers/name.py index 309ad784..0a029dd5 100644 --- a/src/unimport/analyzers/name.py +++ b/src/unimport/analyzers/name.py @@ -1,9 +1,10 @@ import ast +import contextlib from unimport import constants as C from unimport import typing as T from unimport.analyzers.decarators import generic_visit -from unimport.analyzers.utils import first_parent_match, get_parents, set_tree_parents +from unimport.analyzers.utils import first_parent_match, set_tree_parents from unimport.statement import Name, Scope __all__ = ("NameAnalyzer",) @@ -20,25 +21,26 @@ def visit_ClassDef(self, node) -> None: def visit_FunctionDef(self, node: T.ASTFunctionT) -> None: Scope.add_current_scope(node) - self._type_comment(node) + if node.type_comment is not None: + self.join_visit(node.type_comment, node, mode="func_type") + self.generic_visit(node) Scope.remove_current_scope() visit_AsyncFunctionDef = visit_FunctionDef - def visit_str_helper(self, value: str, node: ast.AST) -> None: - parent = first_parent_match(node, *C.AST_FUNCTION_TUPLE) - is_annassign_or_arg = any(isinstance(parent, (ast.AnnAssign, ast.arg)) for parent in get_parents(node)) - if is_annassign_or_arg or (parent is not None and parent.returns is node): - self.join_visit(value, node) - - def visit_Str(self, node: ast.Str) -> None: - self.visit_str_helper(node.s, node) - + @generic_visit def visit_Constant(self, node: ast.Constant) -> None: if isinstance(node.value, str): - self.visit_str_helper(node.value, node) + if (first_annassign_or_arg := first_parent_match(node, (ast.AnnAssign, ast.arg))) and isinstance( + first_annassign_or_arg.annotation, ast.Constant + ): + self.join_visit(node.value, node) + elif ( + first_func_parent := first_parent_match(node, *C.AST_FUNCTION_TUPLE) + ) and first_func_parent.returns is node: + self.join_visit(node.value, node) @generic_visit def visit_Name(self, node: ast.Name) -> None: @@ -59,26 +61,16 @@ def visit_Attribute(self, node: ast.Attribute) -> None: @generic_visit def visit_Assign(self, node: ast.Assign) -> None: - self._type_comment(node) + if node.type_comment is not None: + self.join_visit(node.type_comment, node) @generic_visit def visit_arg(self, node: ast.arg) -> None: - self._type_comment(node) + if node.type_comment is not None: + self.join_visit(node.type_comment, node) @generic_visit def visit_Subscript(self, node: ast.Subscript) -> None: - # type_variable - # type_var = List["object"] etc. - - def visit_constant_str(node: T.ASTNameType) -> None: - """Separates the value by node type (str or constant) and gives it - to the visit function.""" - - if isinstance(node, ast.Constant) and isinstance(node.value, str): - self.join_visit(node.value, node) - elif isinstance(node, ast.Str): - self.join_visit(node.s, node) - if ( isinstance(node.value, ast.Attribute) and isinstance(node.value.value, ast.Name) @@ -91,16 +83,14 @@ def visit_constant_str(node: T.ASTNameType) -> None: if isinstance(_slice, ast.Tuple): # type: ignore for elt in _slice.elts: # type: ignore - if isinstance(elt, (ast.Constant, ast.Str)): - visit_constant_str(elt) + if isinstance(elt, ast.Constant) and isinstance(elt.value, str): + self.join_visit(elt.value, elt) else: - if isinstance(_slice, (ast.Constant, ast.Str)): # type: ignore - visit_constant_str(_slice) # type: ignore + if isinstance(_slice, ast.Constant) and isinstance(_slice.value, str): # type: ignore + self.join_visit(_slice.value, _slice) @generic_visit def visit_Call(self, node: ast.Call) -> None: - # type_variable - # cast("type", return_value) if ( ( isinstance(node.func, ast.Attribute) @@ -113,23 +103,12 @@ def visit_Call(self, node: ast.Call) -> None: ): if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str): self.join_visit(node.args[0].value, node.args[0]) - elif isinstance(node.args[0], ast.Str): - self.join_visit(node.args[0].s, node.args[0]) - - def _type_comment(self, node: ast.AST) -> None: - mode = "func_type" if isinstance(node, C.AST_FUNCTION_TUPLE) else "eval" - type_comment = getattr(node, "type_comment", None) - if type_comment is not None: - self.join_visit(type_comment, node, mode=mode) def join_visit(self, value: str, node: ast.AST, *, mode: str = "eval") -> None: """A function that parses the value, copies locations from the node and includes them in self.visit.""" - try: - tree = ast.parse(value, mode=mode, type_comments=True) if C.PY38_PLUS else ast.parse(value, mode=mode) - except SyntaxError: - return None - else: + with contextlib.suppress(SyntaxError): + tree = ast.parse(value, mode=mode, type_comments=True) set_tree_parents(tree, parent=node.parent) # type: ignore for new_node in ast.walk(tree): ast.copy_location(new_node, node) diff --git a/src/unimport/constants.py b/src/unimport/constants.py index 4ae55aad..98239904 100644 --- a/src/unimport/constants.py +++ b/src/unimport/constants.py @@ -8,9 +8,8 @@ "GLOB_PATTERN", "INCLUDE_REGEX_PATTERN", "INIT_FILE_IGNORE_REGEX", - "PY37_PLUS", - "PY38_PLUS", "PY39_PLUS", + "PY310_PLUS", "STDLIB_PATH", "SUBSCRIPT_TYPE_VARIABLE", ) @@ -26,9 +25,8 @@ AST_FUNCTION_TUPLE = (ast.FunctionDef, ast.AsyncFunctionDef) # CONF -PY37_PLUS = sys.version_info >= (3, 7) -PY38_PLUS = sys.version_info >= (3, 8) PY39_PLUS = sys.version_info >= (3, 9) +PY310_PLUS = sys.version_info >= (3, 10) SUBSCRIPT_TYPE_VARIABLE = frozenset( { diff --git a/src/unimport/typing.py b/src/unimport/typing.py index 27496672..00d71acc 100644 --- a/src/unimport/typing.py +++ b/src/unimport/typing.py @@ -7,14 +7,12 @@ "FunctionT", "ASTImportableT", "ASTFunctionT", - "ASTNameType", "CFNT", "CSTImportT", ) ASTImportableT = typing.Union[ast.AsyncFunctionDef, ast.Attribute, ast.ClassDef, ast.FunctionDef, ast.Name, ast.alias] ASTFunctionT = typing.TypeVar("ASTFunctionT", ast.FunctionDef, ast.AsyncFunctionDef) -ASTNameType = typing.Union[ast.Name, ast.Constant] CFNT = typing.TypeVar("CFNT", ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Name) CSTImportT = typing.TypeVar("CSTImportT", cst.Import, cst.ImportFrom) diff --git a/src/unimport/utils.py b/src/unimport/utils.py index 8966b6aa..cb3ab92a 100644 --- a/src/unimport/utils.py +++ b/src/unimport/utils.py @@ -139,11 +139,7 @@ def list_paths( for file_name in file_names: if include_regex.search(str(file_name)) and not exclude_regex.search(str(file_name)): for gitignore_pattern in gitignore_patterns: - match_file = ( - gitignore_pattern.match_file(str(file_name)) - if C.PY37_PLUS - else list(gitignore_pattern.match([str(file_name)])) - ) + match_file = gitignore_pattern.match_file(str(file_name)) if match_file: break else: diff --git a/tests/cases/analyzer/statement/match_statement.py b/tests/cases/analyzer/statement/match_statement.py index efdc5930..b4586965 100644 --- a/tests/cases/analyzer/statement/match_statement.py +++ b/tests/cases/analyzer/statement/match_statement.py @@ -6,9 +6,9 @@ NAMES: List[Name] = [ - Name(lineno=3, name="sort_by", is_all=False), - Name(lineno=4, name="sort_by", is_all=False), Name(lineno=5, name="sort_by", is_all=False), + Name(lineno=6, name="sort_by", is_all=False), + Name(lineno=7, name="sort_by", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [] UNUSED_IMPORTS: List[Union[Import, ImportFrom]] = [] diff --git a/tests/cases/analyzer/style/vertical_dont_add_extra_line.py b/tests/cases/analyzer/style/vertical_dont_add_extra_line.py index c1b3fe3c..19f26e35 100644 --- a/tests/cases/analyzer/style/vertical_dont_add_extra_line.py +++ b/tests/cases/analyzer/style/vertical_dont_add_extra_line.py @@ -9,8 +9,6 @@ Name(lineno=7, name="test_list", is_all=False), Name(lineno=7, name="List", is_all=False), Name(lineno=7, name="str", is_all=False), - Name(lineno=7, name="spam", is_all=False), - Name(lineno=7, name="eggs", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [ Import(lineno=1, column=1, name="sys", package="sys"), diff --git a/tests/cases/analyzer/typing/function_str_arg.py b/tests/cases/analyzer/typing/function_str_arg.py index 7b65dcaa..02dc4e76 100644 --- a/tests/cases/analyzer/typing/function_str_arg.py +++ b/tests/cases/analyzer/typing/function_str_arg.py @@ -9,7 +9,6 @@ Name(lineno=4, name="Dict", is_all=False), Name(lineno=4, name="Literal", is_all=False), Name(lineno=4, name="Dict", is_all=False), - Name(lineno=5, name="pas", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [ ImportFrom( diff --git a/tests/cases/analyzer/typing/type_comment_funcdef.py b/tests/cases/analyzer/typing/type_comment_funcdef.py index 5f7465b5..3559b433 100644 --- a/tests/cases/analyzer/typing/type_comment_funcdef.py +++ b/tests/cases/analyzer/typing/type_comment_funcdef.py @@ -6,13 +6,13 @@ NAMES: List[Name] = [ - Name(lineno=6, name="str", is_all=False), - Name(lineno=6, name="List", is_all=False), - Name(lineno=6, name="str", is_all=False), + Name(lineno=4, name="str", is_all=False), + Name(lineno=4, name="List", is_all=False), + Name(lineno=4, name="str", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [ ImportFrom( - lineno=3, + lineno=1, column=1, name="List", package="typing", diff --git a/tests/cases/analyzer/typing/type_comment_params.py b/tests/cases/analyzer/typing/type_comment_params.py index 5addc7d4..8d7b0af5 100644 --- a/tests/cases/analyzer/typing/type_comment_params.py +++ b/tests/cases/analyzer/typing/type_comment_params.py @@ -5,6 +5,11 @@ __all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"] -NAMES: List[Name] = [] -IMPORTS: List[Union[Import, ImportFrom]] = [] +NAMES: List[Name] = [ + Name(lineno=5, name="List", is_all=False), + Name(lineno=6, name="str", is_all=False), +] +IMPORTS: List[Union[Import, ImportFrom]] = [ + ImportFrom(lineno=1, column=1, name="List", package="typing", star=False, suggestions=[]) +] UNUSED_IMPORTS: List[Union[Import, ImportFrom]] = [] diff --git a/tests/cases/analyzer/typing/type_comments.py b/tests/cases/analyzer/typing/type_comments.py index 2b165218..e461ce7d 100644 --- a/tests/cases/analyzer/typing/type_comments.py +++ b/tests/cases/analyzer/typing/type_comments.py @@ -6,38 +6,17 @@ NAMES: List[Name] = [ - Name(lineno=8, name="Any", is_all=False), - Name(lineno=8, name="str", is_all=False), - Name(lineno=8, name="Union", is_all=False), - Name(lineno=8, name="Tuple", is_all=False), - Name(lineno=8, name="Tuple", is_all=False), - Name(lineno=8, name="str", is_all=False), - Name(lineno=8, name="str", is_all=False), + Name(lineno=6, name="Any", is_all=False), + Name(lineno=6, name="str", is_all=False), + Name(lineno=6, name="Union", is_all=False), + Name(lineno=6, name="Tuple", is_all=False), + Name(lineno=6, name="Tuple", is_all=False), + Name(lineno=6, name="str", is_all=False), + Name(lineno=6, name="str", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [ - ImportFrom( - lineno=3, - column=1, - name="Any", - package="typing", - star=False, - suggestions=[], - ), - ImportFrom( - lineno=4, - column=1, - name="Tuple", - package="typing", - star=False, - suggestions=[], - ), - ImportFrom( - lineno=5, - column=1, - name="Union", - package="typing", - star=False, - suggestions=[], - ), + ImportFrom(lineno=1, column=1, name="Any", package="typing", star=False, suggestions=[]), + ImportFrom(lineno=2, column=1, name="Tuple", package="typing", star=False, suggestions=[]), + ImportFrom(lineno=3, column=1, name="Union", package="typing", star=False, suggestions=[]), ] UNUSED_IMPORTS: List[Union[Import, ImportFrom]] = [] diff --git a/tests/cases/analyzer/typing/type_comments_with_variable.py b/tests/cases/analyzer/typing/type_comments_with_variable.py index 1e6e0644..493ea04a 100644 --- a/tests/cases/analyzer/typing/type_comments_with_variable.py +++ b/tests/cases/analyzer/typing/type_comments_with_variable.py @@ -6,13 +6,13 @@ NAMES: List[Name] = [ - Name(lineno=6, name="List", is_all=False), - Name(lineno=6, name="int", is_all=False), - Name(lineno=6, name="test_variable", is_all=False), + Name(lineno=4, name="List", is_all=False), + Name(lineno=4, name="int", is_all=False), + Name(lineno=4, name="test_variable", is_all=False), ] IMPORTS: List[Union[Import, ImportFrom]] = [ ImportFrom( - lineno=3, + lineno=1, column=1, name="List", package="typing", diff --git a/tests/cases/refactor/statement/match_statement.py b/tests/cases/refactor/statement/match_statement.py index 6cd40b23..7ffcd5f6 100644 --- a/tests/cases/refactor/statement/match_statement.py +++ b/tests/cases/refactor/statement/match_statement.py @@ -1,3 +1,5 @@ +# pytest.mark.skipif(not PY310_PLUS, reason: "this statement is supported above python 3.10") + # https://github.com/hakancelikdev/unimport/issues/291 match sort_by: diff --git a/tests/cases/refactor/typing/function_str_arg.py b/tests/cases/refactor/typing/function_str_arg.py index 2814fa04..66a3a742 100644 --- a/tests/cases/refactor/typing/function_str_arg.py +++ b/tests/cases/refactor/typing/function_str_arg.py @@ -2,4 +2,4 @@ def test(item, when: "Literal['Dict']") -> None: - pas + pass diff --git a/tests/cases/refactor/typing/type_comment_funcdef.py b/tests/cases/refactor/typing/type_comment_funcdef.py index de463857..f15c8dfd 100644 --- a/tests/cases/refactor/typing/type_comment_funcdef.py +++ b/tests/cases/refactor/typing/type_comment_funcdef.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List diff --git a/tests/cases/refactor/typing/type_comment_params.py b/tests/cases/refactor/typing/type_comment_params.py index fbf3d8b1..4e422a98 100644 --- a/tests/cases/refactor/typing/type_comment_params.py +++ b/tests/cases/refactor/typing/type_comment_params.py @@ -1,10 +1,8 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List def x( - f: # type:List, - r: # type:str + f, # type:List, + r # type:str ): pass diff --git a/tests/cases/refactor/typing/type_comments.py b/tests/cases/refactor/typing/type_comments.py index ad05be2e..89e92091 100644 --- a/tests/cases/refactor/typing/type_comments.py +++ b/tests/cases/refactor/typing/type_comments.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import Any from typing import Tuple from typing import Union diff --git a/tests/cases/refactor/typing/type_comments_with_variable.py b/tests/cases/refactor/typing/type_comments_with_variable.py index 430eee22..f034d165 100644 --- a/tests/cases/refactor/typing/type_comments_with_variable.py +++ b/tests/cases/refactor/typing/type_comments_with_variable.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List diff --git a/tests/cases/source/statement/match_statement.py b/tests/cases/source/statement/match_statement.py index 6cd40b23..7ffcd5f6 100644 --- a/tests/cases/source/statement/match_statement.py +++ b/tests/cases/source/statement/match_statement.py @@ -1,3 +1,5 @@ +# pytest.mark.skipif(not PY310_PLUS, reason: "this statement is supported above python 3.10") + # https://github.com/hakancelikdev/unimport/issues/291 match sort_by: diff --git a/tests/cases/source/typing/function_str_arg.py b/tests/cases/source/typing/function_str_arg.py index 2814fa04..66a3a742 100644 --- a/tests/cases/source/typing/function_str_arg.py +++ b/tests/cases/source/typing/function_str_arg.py @@ -2,4 +2,4 @@ def test(item, when: "Literal['Dict']") -> None: - pas + pass diff --git a/tests/cases/source/typing/type_comment_funcdef.py b/tests/cases/source/typing/type_comment_funcdef.py index de463857..f15c8dfd 100644 --- a/tests/cases/source/typing/type_comment_funcdef.py +++ b/tests/cases/source/typing/type_comment_funcdef.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List diff --git a/tests/cases/source/typing/type_comment_params.py b/tests/cases/source/typing/type_comment_params.py index fbf3d8b1..4e422a98 100644 --- a/tests/cases/source/typing/type_comment_params.py +++ b/tests/cases/source/typing/type_comment_params.py @@ -1,10 +1,8 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List def x( - f: # type:List, - r: # type:str + f, # type:List, + r # type:str ): pass diff --git a/tests/cases/source/typing/type_comments.py b/tests/cases/source/typing/type_comments.py index ad05be2e..89e92091 100644 --- a/tests/cases/source/typing/type_comments.py +++ b/tests/cases/source/typing/type_comments.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import Any from typing import Tuple from typing import Union diff --git a/tests/cases/source/typing/type_comments_with_variable.py b/tests/cases/source/typing/type_comments_with_variable.py index 430eee22..f034d165 100644 --- a/tests/cases/source/typing/type_comments_with_variable.py +++ b/tests/cases/source/typing/type_comments_with_variable.py @@ -1,5 +1,3 @@ -# skip; condition: not PY38_PLUS, reason: This feature is only available for python 3.8. - from typing import List diff --git a/tests/cases/test_cases.py b/tests/cases/test_cases.py index faa705d4..cda0e8df 100644 --- a/tests/cases/test_cases.py +++ b/tests/cases/test_cases.py @@ -6,7 +6,7 @@ import pytest from unimport.analyzers import MainAnalyzer -from unimport.constants import PY38_PLUS # noqa using eval expression +from unimport.constants import PY310_PLUS # noqa using eval expression from unimport.refactor import refactor_string from unimport.statement import Import, Name from unimport.utils import list_paths @@ -31,12 +31,10 @@ def test_cases(path: Path, logger): analyzer = importlib.import_module(analyzer_import_path) source = path.read_text() - skip = re.search("# skip; condition: (?P.*), reason: (?P.*)", source, re.IGNORECASE) - if skip: - condition = skip.group("condition") - if condition in ("not PY38_PLUS",) and eval(condition): - reason = skip.group("reason") - pytest.skip(reason, allow_module_level=True) + skip_if = re.search(r"# pytest.mark.skipif\((?P.*), reason: (?P.*)\)", source, re.IGNORECASE) + if skip_if and (condition := skip_if.group("condition")) and condition in ("not PY310_PLUS",): + reason = skip_if.group("reason") + pytest.mark.skipif(False, reason, allow_module_level=True) with contextlib.suppress(SyntaxError): with MainAnalyzer(source=source, include_star_import=True): diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 5c0ad72e..2485d1ae 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import re from pathlib import Path -from typing import List import pytest @@ -92,7 +93,7 @@ def test_parse_config_parse_args_config_setup_cfg(): (["--ignore-init"], True, "ignore_init"), ], ) -def test_parse_config_parse_args(argv: List[str], expected_argv: str, attribute_name: str): +def test_parse_config_parse_args(argv: list[str], expected_argv: str, attribute_name: str): parser = generate_parser() args = parser.parse_args(argv) args.disable_auto_discovery_config = True diff --git a/tests/test_utils.py b/tests/test_utils.py index 68c920e1..a3e7f483 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,7 +6,6 @@ import pytest from tests.utils import refactor, reopenable_temp_file -from unimport import constants as C from unimport import utils from unimport.utils import action_to_bool @@ -30,8 +29,8 @@ def test_list_paths(path, count): @pytest.mark.skipif( - not C.PY37_PLUS or sys.platform == "win32", - reason="Patspec version 0.10.0 and above are only supported for Python 3.7 above.", + sys.platform == "win32", + reason="Patspec version 0.10.1 and above are not supported on Windows", ) def test_list_paths_with_gitignore(): gitignore = textwrap.dedent( diff --git a/tests/utils.py b/tests/utils.py index 7a563d1f..f9c84c81 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import os import tempfile +import typing from contextlib import contextmanager from pathlib import Path -from typing import Iterator, Optional from unimport import utils from unimport.analyzers import MainAnalyzer @@ -11,7 +13,7 @@ @contextmanager -def reopenable_temp_file(content: str, newline: Optional[str] = None) -> Iterator[Path]: +def reopenable_temp_file(content: str, newline: str | None = None) -> typing.Iterator[Path]: """Create a reopenable tempfile to supporting multiple reads/writes. Required to avoid file locking issues on Windows. For more diff --git a/tox.ini b/tox.ini index e5058935..de649fb0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = 3.7, 3.8, 3.9, 3.10, 3.11, pre-commit +envlist = 3.8, 3.9, 3.10, 3.11, pre-commit isolated_build = true [testenv] @@ -7,7 +7,7 @@ install_command = python -m pip install {opts} {packages} extras = test commands = - python -m pytest -v --cov unimport {posargs} + python -m pytest -vv --cov unimport {posargs} [testenv:pre-commit] skip_install = true