diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b87a27..118cdc9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,10 @@ jobs: main-windows: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py310"]' + env: '["py39"]' os: windows-latest main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py310", "py311", "py312", "py313"]' + env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4808c9..c6d25f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,23 +10,23 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.2.0 + rev: v2.8.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.16.0 + rev: v3.15.0 hooks: - id: reorder-python-imports - args: [--py310-plus, --add-import, 'from __future__ import annotations'] + args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v4.0.0 + rev: v3.2.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 + rev: v3.20.0 hooks: - id: pyupgrade - args: [--py310-plus] + args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: @@ -36,6 +36,6 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v1.17.1 hooks: - id: mypy diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 275605e..a98a1e5 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -29,7 +29,7 @@ entry: check-case-conflict language: python - id: check-docstring-first - name: check docstring is first (deprecated) + name: check docstring is first description: checks a common error of defining a docstring after code. entry: check-docstring-first language: python diff --git a/CHANGELOG.md b/CHANGELOG.md index 522925e..8038ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -6.0.0 - 2025-08-09 +6.0.0 - 2024-08-09 ================== ## Fixes diff --git a/README.md b/README.md index 8432455..9ee1677 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ Require literal syntax when initializing empty or zero Python builtin types. #### `check-case-conflict` Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT. +#### `check-docstring-first` +Checks for a common error of placing code before the docstring. + #### `check-executables-have-shebangs` Checks that non-binary executables have a proper shebang. @@ -204,8 +207,6 @@ Trims trailing whitespace. - `check-byte-order-marker`: instead use fix-byte-order-marker - `fix-encoding-pragma`: instead use [`pyupgrade`](https://github.com/asottile/pyupgrade) -- `check-docstring-first`: fundamentally flawed, deprecated without replacement. - ### As a standalone package diff --git a/pre_commit_hooks/check_builtin_literals.py b/pre_commit_hooks/check_builtin_literals.py index e128eea..16d59b5 100644 --- a/pre_commit_hooks/check_builtin_literals.py +++ b/pre_commit_hooks/check_builtin_literals.py @@ -26,37 +26,36 @@ class Call(NamedTuple): class Visitor(ast.NodeVisitor): def __init__( self, - ignore: set[str], + ignore: Sequence[str] | None = None, allow_dict_kwargs: bool = True, ) -> None: self.builtin_type_calls: list[Call] = [] + self.ignore = set(ignore) if ignore else set() self.allow_dict_kwargs = allow_dict_kwargs - self._disallowed = BUILTIN_TYPES.keys() - ignore def _check_dict_call(self, node: ast.Call) -> bool: return self.allow_dict_kwargs and bool(node.keywords) def visit_Call(self, node: ast.Call) -> None: - if ( + if not isinstance(node.func, ast.Name): # Ignore functions that are object attributes (`foo.bar()`). # Assume that if the user calls `builtins.list()`, they know what # they're doing. - isinstance(node.func, ast.Name) and - node.func.id in self._disallowed and - (node.func.id != 'dict' or not self._check_dict_call(node)) and - not node.args - ): - self.builtin_type_calls.append( - Call(node.func.id, node.lineno, node.col_offset), - ) - - self.generic_visit(node) + return + if node.func.id not in set(BUILTIN_TYPES).difference(self.ignore): + return + if node.func.id == 'dict' and self._check_dict_call(node): + return + elif node.args: + return + self.builtin_type_calls.append( + Call(node.func.id, node.lineno, node.col_offset), + ) def check_file( filename: str, - *, - ignore: set[str], + ignore: Sequence[str] | None = None, allow_dict_kwargs: bool = True, ) -> list[Call]: with open(filename, 'rb') as f: diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index ee26d92..4435d6a 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -12,10 +12,10 @@ conflicts and keep the file nicely ordered. from __future__ import annotations import argparse -from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence from typing import Any +from typing import Callable from typing import IO PASS = 0 diff --git a/setup.cfg b/setup.cfg index d91f439..14f7a91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ packages = find: install_requires = ruamel.yaml>=0.15 tomli>=1.1.0;python_version<"3.11" -python_requires = >=3.10 +python_requires = >=3.9 [options.packages.find] exclude = diff --git a/tests/check_builtin_literals_test.py b/tests/check_builtin_literals_test.py index de29063..1b18257 100644 --- a/tests/check_builtin_literals_test.py +++ b/tests/check_builtin_literals_test.py @@ -38,6 +38,11 @@ t1 = () ''' +@pytest.fixture +def visitor(): + return Visitor() + + @pytest.mark.parametrize( ('expression', 'calls'), [ @@ -80,8 +85,7 @@ t1 = () ('builtins.tuple()', []), ], ) -def test_non_dict_exprs(expression, calls): - visitor = Visitor(ignore=set()) +def test_non_dict_exprs(visitor, expression, calls): visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls @@ -98,8 +102,7 @@ def test_non_dict_exprs(expression, calls): ('builtins.dict()', []), ], ) -def test_dict_allow_kwargs_exprs(expression, calls): - visitor = Visitor(ignore=set()) +def test_dict_allow_kwargs_exprs(visitor, expression, calls): visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls @@ -111,18 +114,17 @@ def test_dict_allow_kwargs_exprs(expression, calls): ('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]), ("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]), ('builtins.dict()', []), - pytest.param('f(dict())', [Call('dict', 1, 2)], id='nested'), ], ) def test_dict_no_allow_kwargs_exprs(expression, calls): - visitor = Visitor(ignore=set(), allow_dict_kwargs=False) + visitor = Visitor(allow_dict_kwargs=False) visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls def test_ignore_constructors(): visitor = Visitor( - ignore={'complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'}, + ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'), ) visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS)) assert visitor.builtin_type_calls == []