diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 8b87a27..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: main - -on: - push: - branches: [main, test-me-*] - tags: '*' - pull_request: - -jobs: - main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 - with: - env: '["py310"]' - os: windows-latest - main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 - with: - env: '["py310", "py311", "py312", "py313"]' - os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4808c9..8804d9d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,32 +10,34 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.2.0 + rev: v1.20.1 hooks: - id: setup-cfg-fmt -- repo: https://github.com/asottile/reorder-python-imports - rev: v3.16.0 +- repo: https://github.com/asottile/reorder_python_imports + rev: v3.0.1 hooks: - id: reorder-python-imports - args: [--py310-plus, --add-import, 'from __future__ import annotations'] + args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v4.0.0 + rev: v2.2.2 hooks: - id: add-trailing-comma + args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 + rev: v2.31.1 hooks: - id: pyupgrade - args: [--py310-plus] -- repo: https://github.com/hhatto/autopep8 - rev: v2.3.2 + args: [--py37-plus] +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 + rev: 4.0.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v0.942 hooks: - id: mypy + additional_dependencies: [types-all] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 275605e..9e835d5 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,8 +3,6 @@ description: prevents giant files from being committed. entry: check-added-large-files language: python - stages: [pre-commit, pre-push, manual] - minimum_pre_commit_version: 3.2.0 - id: check-ast name: check python ast description: simply checks whether the files parse as valid python. @@ -12,9 +10,9 @@ language: python types: [python] - id: check-byte-order-marker - name: check-byte-order-marker (removed) - description: (removed) use fix-byte-order-marker instead. - entry: pre-commit-hooks-removed check-byte-order-marker fix-byte-order-marker https://github.com/pre-commit/pre-commit-hooks + name: 'check BOM - deprecated: use fix-byte-order-marker' + description: forbids files which have a utf-8 byte-order marker. + entry: check-byte-order-marker language: python types: [text] - id: check-builtin-literals @@ -29,7 +27,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 @@ -40,13 +38,7 @@ entry: check-executables-have-shebangs language: python types: [text, executable] - stages: [pre-commit, pre-push, manual] - minimum_pre_commit_version: 3.2.0 -- id: check-illegal-windows-names - name: check illegal windows names - entry: Illegal Windows filenames detected - language: fail - files: '(?i)((^|/)(CON|PRN|AUX|NUL|COM[\d¹²³]|LPT[\d¹²³])(\.|/|$)|[<>:\"\\|?*\x00-\x1F]|/[^/]*[\.\s]/|[^/]*[\.\s]$)' + stages: [commit, push, manual] - id: check-json name: check json description: checks json files for parseable syntax. @@ -59,8 +51,7 @@ entry: check-shebang-scripts-are-executable language: python types: [text] - stages: [pre-commit, pre-push, manual] - minimum_pre_commit_version: 3.2.0 + stages: [commit, push, manual] - id: pretty-format-json name: pretty format json description: sets a standard for formatting json files. @@ -115,7 +106,6 @@ entry: destroyed-symlinks language: python types: [file] - stages: [pre-commit, pre-push, manual] - id: detect-aws-credentials name: detect aws credentials description: detects *your* aws credentials from the aws cli credentials file. @@ -140,8 +130,7 @@ entry: end-of-file-fixer language: python types: [text] - stages: [pre-commit, pre-push, manual] - minimum_pre_commit_version: 3.2.0 + stages: [commit, push, manual] - id: file-contents-sorter name: file contents sorter description: sorts the lines in specified files (defaults to alphabetical). you must provide list of target files as input in your .pre-commit-config.yaml file. @@ -155,10 +144,10 @@ language: python types: [text] - id: fix-encoding-pragma - name: fix python encoding pragma (removed) - description: (removed) use pyupgrade instead. - entry: pre-commit-hooks-removed fix-encoding-pragma pyupgrade https://github.com/asottile/pyupgrade + name: fix python encoding pragma + description: 'adds # -*- coding: utf-8 -*- to the top of python files.' language: python + entry: fix-encoding-pragma types: [python] - id: forbid-new-submodules name: forbid new submodules @@ -166,12 +155,6 @@ language: python entry: forbid-new-submodules types: [directory] -- id: forbid-submodules - name: forbid submodules - description: forbids any submodules in the repository - language: fail - entry: 'submodules are not allowed in this repository:' - types: [directory] - id: mixed-line-ending name: mixed line ending description: replaces or checks mixed line ending. @@ -180,7 +163,7 @@ types: [text] - id: name-tests-test name: python tests naming - description: verifies that test files are named correctly. + description: this verifies that test files are named correctly. entry: name-tests-test language: python files: (^|/)tests/.+\.py$ @@ -195,7 +178,7 @@ description: sorts entries in requirements.txt. entry: requirements-txt-fixer language: python - files: (requirements|constraints).*\.txt$ + files: requirements.*\.txt$ - id: sort-simple-yaml name: sort simple yaml files description: sorts simple yaml files which consist only of top-level keys, preserving comments and blocks. @@ -208,5 +191,4 @@ entry: trailing-whitespace-fixer language: python types: [text] - stages: [pre-commit, pre-push, manual] - minimum_pre_commit_version: 3.2.0 + stages: [commit, push, manual] diff --git a/CHANGELOG.md b/CHANGELOG.md index 522925e..6a2ae68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,115 +1,3 @@ -6.0.0 - 2025-08-09 -================== - -## Fixes -- `check-shebang-scripts-are-executable`: improve error message. - - #1115 PR by @homebysix. - -## Migrating -- now requires python >= 3.9. - - #1098 PR by @asottile. -- `file-contents-sorter`: disallow `--unique` and `--ignore-case` at the same - time. - - #1095 PR by @nemacysts. - - #794 issue by @teksturi. -- Removed `check-byte-order-marker` and `fix-encoding-pragma`. - - `check-byte-order-marker`: migrate to `fix-byte-order-marker`. - - `fix-encoding-pragma`: migrate to `pyupgrade`. - - #1034 PR by @mxr. - - #1032 issue by @mxr. - - #522 PR by @jgowdy. - -5.0.0 - 2024-10-05 -================== - -### Features -- `requirements-txt-fixer`: also remove `pkg_resources==...`. - - #850 PR by @ericfrederich. - - #1030 issue by @ericfrederich. -- `check-illegal-windows-names`: new hook! - - #1044 PR by @ericfrederich. - - #589 issue by @ericfrederich. - - #1049 PR by @Jeffrey-Lim. -- `pretty-format-json`: continue processing even if a file has a json error. - - #1039 PR by @amarvin. - - #1038 issue by @amarvin. - -### Fixes -- `destroyed-symlinks`: set `stages` to `[pre-commit, pre-push, manual]` - - PR #1085 by @AdrianDC. - -### Migrating -- pre-commit-hooks now requires `pre-commit>=3.2.0`. -- use non-deprecated names for `stages`. - - #1093 PR by @asottile. - -4.6.0 - 2024-04-06 -================== - -### Features -- `requirements-txt-fixer`: remove duplicate packages. - - #1014 PR by @vhoulbreque-withings. - - #960 issue @csibe17. - -### Migrating -- `fix-encoding-pragma`: deprecated -- will be removed in 5.0.0. use - [pyupgrade](https://github.com/asottile/pyupgrade) or some other tool. - - #1033 PR by @mxr. - - #1032 issue by @mxr. - -4.5.0 - 2023-10-07 -================== - -### Features -- `requirements-txt-fixer`: also sort `constraints.txt` by default. - - #857 PR by @lev-blit. - - #830 issue by @PLPeeters. -- `debug-statements`: add `bpdb` debugger. - - #942 PR by @mwip. - - #941 issue by @mwip. - -### Fixes -- `file-contents-sorter`: fix sorting an empty file. - - #944 PR by @RoelAdriaans. - - #935 issue by @paduszyk. -- `double-quote-string-fixer`: don't rewrite inside f-strings in 3.12+. - - #973 PR by @asottile. - - #971 issue by @XuehaiPan. - -## Migrating -- now requires python >= 3.8. - - #926 PR by @asottile. - - #927 PR by @asottile. - -4.4.0 - 2022-11-23 -================== - -### Features -- `forbid-submodules`: new hook which outright bans submodules. - - #815 PR by @asottile. - - #707 issue by @ChiefGokhlayeh. - -4.3.0 - 2022-06-07 -================== - -### Features -- `check-executables-have-shebangs`: use `git config core.fileMode` to - determine if it should query `git`. - - #730 PR by @Kurt-von-Laven. -- `name-tests-test`: add `--pytest-test-first` test convention. - - #779 PR by @asottile. - -### Fixes -- `check-shebang-scripts-are-executable`: update windows instructions. - - #774 PR by @mdeweerd. - - #770 issue by @mdeweerd. -- `check-toml`: use stdlib `tomllib` when available. - - #771 PR by @DanielNoord. - - #755 issue by @sognetic. -- `check-added-large-files`: don't run on non-file `stages`. - - #778 PR by @asottile. - - #777 issue by @skyj. - 4.2.0 - 2022-04-06 ================== diff --git a/README.md b/README.md index 8432455..7368831 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![build status](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit-hooks/actions/workflows/main.yml) +[![Build Status](https://asottile.visualstudio.com/asottile/_apis/build/status/pre-commit.pre-commit-hooks?branchName=main)](https://asottile.visualstudio.com/asottile/_build/latest?definitionId=17&branchName=main) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/17/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=17&branchName=main) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit-hooks/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit-hooks/main) pre-commit-hooks @@ -15,7 +16,7 @@ Add this to your `.pre-commit-config.yaml` ```yaml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 # Use the ref you want to point at + rev: v4.2.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -45,18 +46,17 @@ 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. -#### `check-illegal-windows-names` -Check for files that cannot be created on Windows. - #### `check-json` Attempts to load all json files to verify syntax. #### `check-merge-conflict` Check for files that contain merge conflict strings. - - `--assume-in-merge` - Allows running the hook when there is no ongoing merge operation #### `check-shebang-scripts-are-executable` Checks that scripts with shebangs are executable. @@ -113,28 +113,25 @@ This hook replaces double quoted strings with single quoted strings. #### `end-of-file-fixer` Makes sure files end in a newline and only a newline. +#### `fix-byte-order-marker` +removes UTF-8 byte order marker + +#### `fix-encoding-pragma` +Add `# -*- coding: utf-8 -*-` to the top of python files. + - To remove the coding pragma pass `--remove` (useful in a python3-only codebase) + #### `file-contents-sorter` Sort the lines in specified files (defaults to alphabetical). -You must provide the target [`files`](https://pre-commit.com/#config-files) as input. +You must provide list of target files as input to it. Note that this hook WILL remove blank lines and does NOT respect any comments. -All newlines will be converted to line feeds (`\n`). The following arguments are available: - `--ignore-case` - fold lower case to upper case characters. - `--unique` - ensure each line is unique. -#### `fix-byte-order-marker` -removes UTF-8 byte order marker - #### `forbid-new-submodules` Prevent addition of new git submodules. -This is intended as a helper to migrate away from submodules. If you want to -ban them entirely use `forbid-submodules` - -#### `forbid-submodules` -forbids any submodules in the repository. - #### `mixed-line-ending` Replaces or checks mixed line ending. - `--fix={auto,crlf,lf,no}` @@ -144,10 +141,8 @@ Replaces or checks mixed line ending. - `no` - Checks if there is any mixed line ending without modifying any file. #### `name-tests-test` -verifies that test files are named correctly. -- `--pytest` (the default): ensure tests match `.*_test\.py` -- `--pytest-test-first`: ensure tests match `test_.*\.py` -- `--django` / `--unittest`: ensure tests match `test.*\.py` +Assert that files in tests/ end in `_test.py`. + - Use `args: ['--django']` to match `test*.py` instead. #### `no-commit-to-branch` Protect specific branches from direct checkins. @@ -176,7 +171,7 @@ the following commandline options: - `--top-keys comma,separated,keys` - Keys to keep at the top of mappings. #### `requirements-txt-fixer` -Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0` +Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0` #### `sort-simple-yaml` Sorts simple YAML files which consist only of top-level @@ -203,9 +198,6 @@ Trims trailing whitespace. ### Deprecated / replaced hooks - `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/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..117b014 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,23 @@ +trigger: + branches: + include: [main, test-me-*] + tags: + include: ['*'] + +resources: + repositories: + - repository: asottile + type: github + endpoint: github + name: asottile/azure-pipeline-templates + ref: refs/tags/v2.4.0 + +jobs: +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py38] + os: windows +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py37, py38, py39, py310] + os: linux diff --git a/pre_commit_hooks/check_added_large_files.py b/pre_commit_hooks/check_added_large_files.py index e674162..79c8d4e 100644 --- a/pre_commit_hooks/check_added_large_files.py +++ b/pre_commit_hooks/check_added_large_files.py @@ -4,7 +4,7 @@ import argparse import math import os import subprocess -from collections.abc import Sequence +from typing import Sequence from pre_commit_hooks.util import added_files from pre_commit_hooks.util import zsplit @@ -46,7 +46,7 @@ def find_large_added_files( filenames_filtered &= added_files() for filename in filenames_filtered: - kb = math.ceil(os.stat(filename).st_size / 1024) + kb = int(math.ceil(os.stat(filename).st_size / 1024)) if kb > maxkb: print(f'{filename} ({kb} KB) exceeds {maxkb} KB.') retv = 1 diff --git a/pre_commit_hooks/check_ast.py b/pre_commit_hooks/check_ast.py index c1f165b..fdac361 100644 --- a/pre_commit_hooks/check_ast.py +++ b/pre_commit_hooks/check_ast.py @@ -5,7 +5,7 @@ import ast import platform import sys import traceback -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit_hooks/check_builtin_literals.py b/pre_commit_hooks/check_builtin_literals.py index e128eea..d3054aa 100644 --- a/pre_commit_hooks/check_builtin_literals.py +++ b/pre_commit_hooks/check_builtin_literals.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import ast -from collections.abc import Sequence from typing import NamedTuple +from typing import Sequence BUILTIN_TYPES = { @@ -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/check_byte_order_marker.py b/pre_commit_hooks/check_byte_order_marker.py new file mode 100644 index 0000000..59cc561 --- /dev/null +++ b/pre_commit_hooks/check_byte_order_marker.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import argparse +from typing import Sequence + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to check') + args = parser.parse_args(argv) + + retv = 0 + + for filename in args.filenames: + with open(filename, 'rb') as f: + if f.read(3) == b'\xef\xbb\xbf': + retv = 1 + print(f'{filename}: Has a byte-order marker') + + return retv + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/pre_commit_hooks/check_case_conflict.py b/pre_commit_hooks/check_case_conflict.py index 475c91c..33a13f1 100644 --- a/pre_commit_hooks/check_case_conflict.py +++ b/pre_commit_hooks/check_case_conflict.py @@ -1,9 +1,9 @@ from __future__ import annotations import argparse -from collections.abc import Iterable -from collections.abc import Iterator -from collections.abc import Sequence +from typing import Iterable +from typing import Iterator +from typing import Sequence from pre_commit_hooks.util import added_files from pre_commit_hooks.util import cmd_output diff --git a/pre_commit_hooks/check_docstring_first.py b/pre_commit_hooks/check_docstring_first.py index 42fbd15..d55f08a 100644 --- a/pre_commit_hooks/check_docstring_first.py +++ b/pre_commit_hooks/check_docstring_first.py @@ -3,8 +3,8 @@ from __future__ import annotations import argparse import io import tokenize -from collections.abc import Sequence from tokenize import tokenize as tokenize_tokenize +from typing import Sequence NON_CODE_TOKENS = frozenset(( tokenize.COMMENT, tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL, diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py index 707863b..6b5402a 100644 --- a/pre_commit_hooks/check_executables_have_shebangs.py +++ b/pre_commit_hooks/check_executables_have_shebangs.py @@ -4,9 +4,9 @@ from __future__ import annotations import argparse import shlex import sys -from collections.abc import Generator -from collections.abc import Sequence +from typing import Generator from typing import NamedTuple +from typing import Sequence from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import zsplit @@ -15,10 +15,7 @@ EXECUTABLE_VALUES = frozenset(('1', '3', '5', '7')) def check_executables(paths: list[str]) -> int: - fs_tracks_executable_bit = cmd_output( - 'git', 'config', 'core.fileMode', retcode=None, - ).strip() - if fs_tracks_executable_bit == 'false': # pragma: win32 cover + if sys.platform == 'win32': # pragma: win32 cover return _check_git_filemode(paths) else: # pragma: win32 no cover retv = 0 @@ -35,7 +32,7 @@ class GitLsFile(NamedTuple): filename: str -def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile]: +def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile, None, None]: outs = cmd_output('git', 'ls-files', '-z', '--stage', '--', *paths) for out in zsplit(outs): metadata, filename = out.split('\t') diff --git a/pre_commit_hooks/check_json.py b/pre_commit_hooks/check_json.py index 612111c..6a679fe 100644 --- a/pre_commit_hooks/check_json.py +++ b/pre_commit_hooks/check_json.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import json -from collections.abc import Sequence from typing import Any +from typing import Sequence def raise_duplicate_keys( diff --git a/pre_commit_hooks/check_merge_conflict.py b/pre_commit_hooks/check_merge_conflict.py index 54a083e..15ec284 100644 --- a/pre_commit_hooks/check_merge_conflict.py +++ b/pre_commit_hooks/check_merge_conflict.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import os.path -from collections.abc import Sequence +from typing import Sequence from pre_commit_hooks.util import cmd_output diff --git a/pre_commit_hooks/check_shebang_scripts_are_executable.py b/pre_commit_hooks/check_shebang_scripts_are_executable.py index 937425b..0f35650 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -4,7 +4,7 @@ from __future__ import annotations import argparse import shlex import sys -from collections.abc import Sequence +from typing import Sequence from pre_commit_hooks.check_executables_have_shebangs import EXECUTABLE_VALUES from pre_commit_hooks.check_executables_have_shebangs import git_ls_files @@ -34,9 +34,7 @@ def _message(path: str) -> None: f'{path}: has a shebang but is not marked executable!\n' f' If it is supposed to be executable, try: ' f'`chmod +x {shlex.quote(path)}`\n' - f' If on Windows, you may also need to: ' - f'`git add --chmod=+x {shlex.quote(path)}`\n' - f' If it is not supposed to be executable, double-check its shebang ' + f' If it not supposed to be executable, double-check its shebang ' f'is wanted.\n', file=sys.stderr, ) diff --git a/pre_commit_hooks/check_symlinks.py b/pre_commit_hooks/check_symlinks.py index be8a800..a85c82a 100644 --- a/pre_commit_hooks/check_symlinks.py +++ b/pre_commit_hooks/check_symlinks.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import os.path -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit_hooks/check_toml.py b/pre_commit_hooks/check_toml.py index 2105b07..88f7086 100644 --- a/pre_commit_hooks/check_toml.py +++ b/pre_commit_hooks/check_toml.py @@ -1,13 +1,9 @@ from __future__ import annotations import argparse -import sys -from collections.abc import Sequence +from typing import Sequence -if sys.version_info >= (3, 11): # pragma: >=3.11 cover - import tomllib -else: # pragma: <3.11 cover - import tomli as tomllib +import toml def main(argv: Sequence[str] | None = None) -> int: @@ -18,9 +14,8 @@ def main(argv: Sequence[str] | None = None) -> int: retval = 0 for filename in args.filenames: try: - with open(filename, mode='rb') as fp: - tomllib.load(fp) - except tomllib.TOMLDecodeError as exc: + toml.load(filename) + except toml.TomlDecodeError as exc: print(f'{filename}: {exc}') retval = 1 return retval diff --git a/pre_commit_hooks/check_vcs_permalinks.py b/pre_commit_hooks/check_vcs_permalinks.py index 108656a..68639bd 100644 --- a/pre_commit_hooks/check_vcs_permalinks.py +++ b/pre_commit_hooks/check_vcs_permalinks.py @@ -3,8 +3,8 @@ from __future__ import annotations import argparse import re import sys -from collections.abc import Sequence -from re import Pattern +from typing import Pattern +from typing import Sequence def _get_pattern(domain: str) -> Pattern[bytes]: diff --git a/pre_commit_hooks/check_xml.py b/pre_commit_hooks/check_xml.py index ff5536b..c256af9 100644 --- a/pre_commit_hooks/check_xml.py +++ b/pre_commit_hooks/check_xml.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import xml.sax.handler -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit_hooks/check_yaml.py b/pre_commit_hooks/check_yaml.py index c94ea71..250794e 100644 --- a/pre_commit_hooks/check_yaml.py +++ b/pre_commit_hooks/check_yaml.py @@ -1,17 +1,17 @@ from __future__ import annotations import argparse -from collections.abc import Generator -from collections.abc import Sequence from typing import Any +from typing import Generator from typing import NamedTuple +from typing import Sequence import ruamel.yaml yaml = ruamel.yaml.YAML(typ='safe') -def _exhaust(gen: Generator[str]) -> None: +def _exhaust(gen: Generator[str, None, None]) -> None: for _ in gen: pass @@ -46,7 +46,7 @@ def main(argv: Sequence[str] | None = None) -> int: '--unsafe', action='store_true', help=( 'Instead of loading the files, simply parse them for syntax. ' - 'A syntax-only check enables extensions and unsafe constructs ' + 'A syntax-only check enables extensions and unsafe contstructs ' 'which would otherwise be forbidden. Using this option removes ' 'all guarantees of portability to other yaml implementations. ' 'Implies --allow-multiple-documents' diff --git a/pre_commit_hooks/debug_statement_hook.py b/pre_commit_hooks/debug_statement_hook.py index 7e6be95..9ada657 100644 --- a/pre_commit_hooks/debug_statement_hook.py +++ b/pre_commit_hooks/debug_statement_hook.py @@ -3,12 +3,11 @@ from __future__ import annotations import argparse import ast import traceback -from collections.abc import Sequence from typing import NamedTuple +from typing import Sequence DEBUG_STATEMENTS = { - 'bpdb', 'ipdb', 'pdb', 'pdbr', diff --git a/pre_commit_hooks/destroyed_symlinks.py b/pre_commit_hooks/destroyed_symlinks.py index 9bc2589..88253c0 100644 --- a/pre_commit_hooks/destroyed_symlinks.py +++ b/pre_commit_hooks/destroyed_symlinks.py @@ -3,7 +3,7 @@ from __future__ import annotations import argparse import shlex import subprocess -from collections.abc import Sequence +from typing import Sequence from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import zsplit @@ -76,7 +76,11 @@ def main(argv: Sequence[str] | None = None) -> int: for destroyed_link in destroyed_links: print(f'- {destroyed_link}') print('You should unstage affected files:') - print(f'\tgit reset HEAD -- {shlex.join(destroyed_links)}') + print( + '\tgit reset HEAD -- {}'.format( + ' '.join(shlex.quote(link) for link in destroyed_links), + ), + ) print( 'And retry commit. As a long term solution ' 'you may try to explicitly tell git that your ' diff --git a/pre_commit_hooks/detect_aws_credentials.py b/pre_commit_hooks/detect_aws_credentials.py index 8582288..4f59d9c 100644 --- a/pre_commit_hooks/detect_aws_credentials.py +++ b/pre_commit_hooks/detect_aws_credentials.py @@ -3,8 +3,8 @@ from __future__ import annotations import argparse import configparser import os -from collections.abc import Sequence from typing import NamedTuple +from typing import Sequence class BadFile(NamedTuple): diff --git a/pre_commit_hooks/detect_private_key.py b/pre_commit_hooks/detect_private_key.py index 9ad703a..cd51f90 100644 --- a/pre_commit_hooks/detect_private_key.py +++ b/pre_commit_hooks/detect_private_key.py @@ -1,7 +1,7 @@ from __future__ import annotations import argparse -from collections.abc import Sequence +from typing import Sequence BLACKLIST = [ b'BEGIN RSA PRIVATE KEY', diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py index a88425c..a30dce9 100644 --- a/pre_commit_hooks/end_of_file_fixer.py +++ b/pre_commit_hooks/end_of_file_fixer.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import os -from collections.abc import Sequence from typing import IO +from typing import Sequence def fix_file(file_obj: IO[bytes]) -> int: diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index ee26d92..c5691f0 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -12,11 +12,11 @@ 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 +from typing import Iterable +from typing import Sequence PASS = 0 FAIL = 1 @@ -37,10 +37,7 @@ def sort_file_contents( after = sorted(lines, key=key) before_string = b''.join(before) - after_string = b'\n'.join(after) - - if after_string: - after_string += b'\n' + after_string = b'\n'.join(after) + b'\n' if before_string == after_string: return PASS @@ -54,21 +51,18 @@ def sort_file_contents( def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='+', help='Files to sort') - - mutex = parser.add_mutually_exclusive_group(required=False) - mutex.add_argument( + parser.add_argument( '--ignore-case', action='store_const', const=bytes.lower, default=None, help='fold lower case to upper case characters', ) - mutex.add_argument( + parser.add_argument( '--unique', action='store_true', help='ensure each line is unique', ) - args = parser.parse_args(argv) retv = PASS diff --git a/pre_commit_hooks/fix_byte_order_marker.py b/pre_commit_hooks/fix_byte_order_marker.py index 100ffea..22a4990 100644 --- a/pre_commit_hooks/fix_byte_order_marker.py +++ b/pre_commit_hooks/fix_byte_order_marker.py @@ -1,7 +1,7 @@ from __future__ import annotations import argparse -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit_hooks/fix_encoding_pragma.py b/pre_commit_hooks/fix_encoding_pragma.py new file mode 100644 index 0000000..60c71ee --- /dev/null +++ b/pre_commit_hooks/fix_encoding_pragma.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import argparse +from typing import IO +from typing import NamedTuple +from typing import Sequence + +DEFAULT_PRAGMA = b'# -*- coding: utf-8 -*-' + + +def has_coding(line: bytes) -> bool: + if not line.strip(): + return False + return ( + line.lstrip()[:1] == b'#' and ( + b'unicode' in line or + b'encoding' in line or + b'coding:' in line or + b'coding=' in line + ) + ) + + +class ExpectedContents(NamedTuple): + shebang: bytes + rest: bytes + # True: has exactly the coding pragma expected + # False: missing coding pragma entirely + # None: has a coding pragma, but it does not match + pragma_status: bool | None + ending: bytes + + @property + def has_any_pragma(self) -> bool: + return self.pragma_status is not False + + def is_expected_pragma(self, remove: bool) -> bool: + expected_pragma_status = not remove + return self.pragma_status is expected_pragma_status + + +def _get_expected_contents( + first_line: bytes, + second_line: bytes, + rest: bytes, + expected_pragma: bytes, +) -> ExpectedContents: + ending = b'\r\n' if first_line.endswith(b'\r\n') else b'\n' + + if first_line.startswith(b'#!'): + shebang = first_line + potential_coding = second_line + else: + shebang = b'' + potential_coding = first_line + rest = second_line + rest + + if potential_coding.rstrip(b'\r\n') == expected_pragma: + pragma_status: bool | None = True + elif has_coding(potential_coding): + pragma_status = None + else: + pragma_status = False + rest = potential_coding + rest + + return ExpectedContents( + shebang=shebang, rest=rest, pragma_status=pragma_status, ending=ending, + ) + + +def fix_encoding_pragma( + f: IO[bytes], + remove: bool = False, + expected_pragma: bytes = DEFAULT_PRAGMA, +) -> int: + expected = _get_expected_contents( + f.readline(), f.readline(), f.read(), expected_pragma, + ) + + # Special cases for empty files + if not expected.rest.strip(): + # If a file only has a shebang or a coding pragma, remove it + if expected.has_any_pragma or expected.shebang: + f.seek(0) + f.truncate() + f.write(b'') + return 1 + else: + return 0 + + if expected.is_expected_pragma(remove): + return 0 + + # Otherwise, write out the new file + f.seek(0) + f.truncate() + f.write(expected.shebang) + if not remove: + f.write(expected_pragma + expected.ending) + f.write(expected.rest) + + return 1 + + +def _normalize_pragma(pragma: str) -> bytes: + return pragma.encode().rstrip() + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + 'Fixes the encoding pragma of python files', + ) + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + parser.add_argument( + '--pragma', default=DEFAULT_PRAGMA, type=_normalize_pragma, + help=( + f'The encoding pragma to use. ' + f'Default: {DEFAULT_PRAGMA.decode()}' + ), + ) + parser.add_argument( + '--remove', action='store_true', + help='Remove the encoding pragma (Useful in a python3-only codebase)', + ) + args = parser.parse_args(argv) + + retv = 0 + + if args.remove: + fmt = 'Removed encoding pragma from {filename}' + else: + fmt = 'Added `{pragma}` to {filename}' + + for filename in args.filenames: + with open(filename, 'r+b') as f: + file_ret = fix_encoding_pragma( + f, remove=args.remove, expected_pragma=args.pragma, + ) + retv |= file_ret + if file_ret: + print( + fmt.format(pragma=args.pragma.decode(), filename=filename), + ) + + return retv + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/pre_commit_hooks/forbid_new_submodules.py b/pre_commit_hooks/forbid_new_submodules.py index b7a63cd..b806cad 100644 --- a/pre_commit_hooks/forbid_new_submodules.py +++ b/pre_commit_hooks/forbid_new_submodules.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import os -from collections.abc import Sequence +from typing import Sequence from pre_commit_hooks.util import cmd_output diff --git a/pre_commit_hooks/mixed_line_ending.py b/pre_commit_hooks/mixed_line_ending.py index 2fbf067..0328e86 100644 --- a/pre_commit_hooks/mixed_line_ending.py +++ b/pre_commit_hooks/mixed_line_ending.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import collections -from collections.abc import Sequence +from typing import Sequence CRLF = b'\r\n' diff --git a/pre_commit_hooks/no_commit_to_branch.py b/pre_commit_hooks/no_commit_to_branch.py index b0b8b23..741f726 100644 --- a/pre_commit_hooks/no_commit_to_branch.py +++ b/pre_commit_hooks/no_commit_to_branch.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import re -from collections.abc import Sequence from typing import AbstractSet +from typing import Sequence from pre_commit_hooks.util import CalledProcessError from pre_commit_hooks.util import cmd_output diff --git a/pre_commit_hooks/pretty_format_json.py b/pre_commit_hooks/pretty_format_json.py index 501f37f..627a11c 100644 --- a/pre_commit_hooks/pretty_format_json.py +++ b/pre_commit_hooks/pretty_format_json.py @@ -3,9 +3,9 @@ from __future__ import annotations import argparse import json import sys -from collections.abc import Mapping -from collections.abc import Sequence from difflib import unified_diff +from typing import Mapping +from typing import Sequence def _get_pretty_format( @@ -115,20 +115,16 @@ def main(argv: Sequence[str] | None = None) -> int: f'Input File {json_file} is not a valid JSON, consider using ' f'check-json', ) - status = 1 - else: - if contents != pretty_contents: - if args.autofix: - _autofix(json_file, pretty_contents) - else: - diff_output = get_diff( - contents, - pretty_contents, - json_file, - ) - sys.stdout.buffer.write(diff_output.encode()) + return 1 - status = 1 + if contents != pretty_contents: + if args.autofix: + _autofix(json_file, pretty_contents) + else: + diff_output = get_diff(contents, pretty_contents, json_file) + sys.stdout.buffer.write(diff_output.encode()) + + status = 1 return status diff --git a/pre_commit_hooks/removed.py b/pre_commit_hooks/removed.py index fb2b6d9..6f6c7b7 100644 --- a/pre_commit_hooks/removed.py +++ b/pre_commit_hooks/removed.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 8ce8ec6..5884394 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import re -from collections.abc import Sequence from typing import IO +from typing import Sequence PASS = 0 @@ -45,11 +45,6 @@ class Requirement: elif requirement.value == b'\n': return False else: - # if 2 requirements have the same name, the one with comments - # needs to go first (so that when removing duplicates, the one - # with comments is kept) - if self.name == requirement.name: - return bool(self.comments) > bool(requirement.comments) return self.name < requirement.name def is_complete(self) -> bool: @@ -115,20 +110,13 @@ def fix_requirements(f: IO[bytes]) -> int: # which is automatically added by broken pip package under Debian requirements = [ req for req in requirements - if req.value not in [ - b'pkg-resources==0.0.0\n', - b'pkg_resources==0.0.0\n', - ] + if req.value != b'pkg-resources==0.0.0\n' ] - # sort the requirements and remove duplicates - prev = None for requirement in sorted(requirements): after.extend(requirement.comments) assert requirement.value, requirement.value - if prev is None or requirement.value != prev.value: - after.append(requirement.value) - prev = requirement + after.append(requirement.value) after.extend(rest) after_string = b''.join(after) diff --git a/pre_commit_hooks/sort_simple_yaml.py b/pre_commit_hooks/sort_simple_yaml.py index 65e6b7a..116b5c1 100644 --- a/pre_commit_hooks/sort_simple_yaml.py +++ b/pre_commit_hooks/sort_simple_yaml.py @@ -20,7 +20,7 @@ complicated YAML files. from __future__ import annotations import argparse -from collections.abc import Sequence +from typing import Sequence QUOTES = ["'", '"'] diff --git a/pre_commit_hooks/string_fixer.py b/pre_commit_hooks/string_fixer.py index 76eb352..0ef9bc7 100644 --- a/pre_commit_hooks/string_fixer.py +++ b/pre_commit_hooks/string_fixer.py @@ -3,15 +3,8 @@ from __future__ import annotations import argparse import io import re -import sys import tokenize -from collections.abc import Sequence - -if sys.version_info >= (3, 12): # pragma: >=3.12 cover - FSTRING_START = tokenize.FSTRING_START - FSTRING_END = tokenize.FSTRING_END -else: # pragma: <3.12 cover - FSTRING_START = FSTRING_END = -1 +from typing import Sequence START_QUOTE_RE = re.compile('^[a-zA-Z]*"') @@ -47,17 +40,11 @@ def fix_strings(filename: str) -> int: # Basically a mutable string splitcontents = list(contents) - fstring_depth = 0 - # Iterate in reverse so the offsets are always correct tokens_l = list(tokenize.generate_tokens(io.StringIO(contents).readline)) tokens = reversed(tokens_l) for token_type, token_text, (srow, scol), (erow, ecol), _ in tokens: - if token_type == FSTRING_START: # pragma: >=3.12 cover - fstring_depth += 1 - elif token_type == FSTRING_END: # pragma: >=3.12 cover - fstring_depth -= 1 - elif fstring_depth == 0 and token_type == tokenize.STRING: + if token_type == tokenize.STRING: new_text = handle_match(token_text) splitcontents[ line_offsets[srow] + scol: diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index 07af277..e1ffe36 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -3,48 +3,29 @@ from __future__ import annotations import argparse import os.path import re -from collections.abc import Sequence +from typing import Sequence def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') - mutex = parser.add_mutually_exclusive_group() - mutex.add_argument( - '--pytest', - dest='pattern', - action='store_const', - const=r'.*_test\.py', - default=r'.*_test\.py', - help='(the default) ensure tests match %(const)s', - ) - mutex.add_argument( - '--pytest-test-first', - dest='pattern', - action='store_const', - const=r'test_.*\.py', - help='ensure tests match %(const)s', - ) - mutex.add_argument( - '--django', '--unittest', - dest='pattern', - action='store_const', - const=r'test.*\.py', - help='ensure tests match %(const)s', + parser.add_argument( + '--django', default=False, action='store_true', + help='Use Django-style test naming pattern (test*.py)', ) args = parser.parse_args(argv) retcode = 0 - reg = re.compile(args.pattern) + test_name_pattern = r'test.*\.py' if args.django else r'.*_test\.py' for filename in args.filenames: base = os.path.basename(filename) if ( - not reg.fullmatch(base) and + not re.match(test_name_pattern, base) and not base == '__init__.py' and not base == 'conftest.py' ): retcode = 1 - print(f'{filename} does not match pattern "{args.pattern}"') + print(f'{filename} does not match pattern "{test_name_pattern}"') return retcode diff --git a/pre_commit_hooks/trailing_whitespace_fixer.py b/pre_commit_hooks/trailing_whitespace_fixer.py index dab8b14..84f5067 100644 --- a/pre_commit_hooks/trailing_whitespace_fixer.py +++ b/pre_commit_hooks/trailing_whitespace_fixer.py @@ -2,7 +2,7 @@ from __future__ import annotations import argparse import os -from collections.abc import Sequence +from typing import Sequence def _fix_file( diff --git a/setup.cfg b/setup.cfg index d91f439..8247f31 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit_hooks -version = 6.0.0 +version = 4.2.0 description = Some out-of-the-box hooks for pre-commit. long_description = file: README.md long_description_content_type = text/markdown @@ -8,10 +8,15 @@ url = https://github.com/pre-commit/pre-commit-hooks author = Anthony Sottile author_email = asottile@umich.edu license = MIT -license_files = LICENSE +license_file = LICENSE classifiers = + License :: OSI Approved :: MIT License 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 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy @@ -19,8 +24,8 @@ classifiers = packages = find: install_requires = ruamel.yaml>=0.15 - tomli>=1.1.0;python_version<"3.11" -python_requires = >=3.10 + toml +python_requires = >=3.7 [options.packages.find] exclude = @@ -32,6 +37,7 @@ console_scripts = check-added-large-files = pre_commit_hooks.check_added_large_files:main check-ast = pre_commit_hooks.check_ast:main check-builtin-literals = pre_commit_hooks.check_builtin_literals:main + check-byte-order-marker = pre_commit_hooks.check_byte_order_marker:main check-case-conflict = pre_commit_hooks.check_case_conflict:main check-docstring-first = pre_commit_hooks.check_docstring_first:main check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main @@ -51,6 +57,7 @@ console_scripts = end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:main file-contents-sorter = pre_commit_hooks.file_contents_sorter:main fix-byte-order-marker = pre_commit_hooks.fix_byte_order_marker:main + fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main mixed-line-ending = pre_commit_hooks.mixed_line_ending:main name-tests-test = pre_commit_hooks.tests_should_end_in_test:main @@ -72,6 +79,7 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true +no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true 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 == [] diff --git a/tests/check_byte_order_marker_test.py b/tests/check_byte_order_marker_test.py new file mode 100644 index 0000000..909a39b --- /dev/null +++ b/tests/check_byte_order_marker_test.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pre_commit_hooks import check_byte_order_marker + + +def test_failure(tmpdir): + f = tmpdir.join('f.txt') + f.write_text('ohai', encoding='utf-8-sig') + assert check_byte_order_marker.main((str(f),)) == 1 + + +def test_success(tmpdir): + f = tmpdir.join('f.txt') + f.write_text('ohai', encoding='utf-8') + assert check_byte_order_marker.main((str(f),)) == 0 diff --git a/tests/check_illegal_windows_names_test.py b/tests/check_illegal_windows_names_test.py deleted file mode 100644 index 82d7532..0000000 --- a/tests/check_illegal_windows_names_test.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -import os.path -import re - -import pytest - -from pre_commit_hooks.check_yaml import yaml - - -@pytest.fixture(scope='module') -def hook_re(): - here = os.path.dirname(__file__) - with open(os.path.join(here, '..', '.pre-commit-hooks.yaml')) as f: - hook_defs = yaml.load(f) - hook, = ( - hook - for hook in hook_defs - if hook['id'] == 'check-illegal-windows-names' - ) - yield re.compile(hook['files']) - - -@pytest.mark.parametrize( - 's', - ( - pytest.param('aux.txt', id='with ext'), - pytest.param('aux', id='without ext'), - pytest.param('AuX.tXt', id='capitals'), - pytest.param('com7.dat', id='com with digit'), - pytest.param(':', id='bare colon'), - pytest.param('file:Zone.Identifier', id='mid colon'), - pytest.param('path/COM¹.json', id='com with superscript'), - pytest.param('dir/LPT³.toml', id='lpt with superscript'), - pytest.param('with < less than', id='with less than'), - pytest.param('Fast or Slow?.md', id='with question mark'), - pytest.param('with "double" quotes', id='with double quotes'), - pytest.param('with_null\x00byte', id='with null byte'), - pytest.param('ends_with.', id='ends with period'), - pytest.param('ends_with ', id='ends with space'), - pytest.param('ends_with\t', id='ends with tab'), - pytest.param('dir/ends./with.txt', id='directory ends with period'), - pytest.param('dir/ends /with.txt', id='directory ends with space'), - ), -) -def test_check_illegal_windows_names_matches(hook_re, s): - assert hook_re.search(s) - - -@pytest.mark.parametrize( - 's', - ( - pytest.param('README.md', id='standard file'), - pytest.param('foo.aux', id='as ext'), - pytest.param('com.dat', id='com without digit'), - pytest.param('.python-version', id='starts with period'), - pytest.param(' pseudo nan', id='with spaces'), - pytest.param('!@#$%^&;=≤\'~`¡¿€🤗', id='with allowed characters'), - pytest.param('path.to/file.py', id='standard path'), - ), -) -def test_check_illegal_windows_names_does_not_match(hook_re, s): - assert hook_re.search(s) is None diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py index 64112d7..76c4283 100644 --- a/tests/check_merge_conflict_test.py +++ b/tests/check_merge_conflict_test.py @@ -27,10 +27,10 @@ def f1_is_a_conflict_file(tmpdir): cmd_output('git', 'clone', str(repo1), str(repo2)) - # Commit in mainline + # Commit in master with repo1.as_cwd(): repo1_f1.write('parent\n') - git_commit('-am', 'mainline commit2') + git_commit('-am', 'master commit2') # Commit in clone and pull with repo2.as_cwd(): @@ -82,10 +82,10 @@ def repository_pending_merge(tmpdir): cmd_output('git', 'clone', str(repo1), str(repo2)) - # Commit in mainline + # Commit in master with repo1.as_cwd(): repo1_f1.write('parent\n') - git_commit('-am', 'mainline commit2') + git_commit('-am', 'master commit2') # Commit in clone and pull without committing with repo2.as_cwd(): @@ -112,7 +112,7 @@ def test_merge_conflicts_git(capsys): @pytest.mark.parametrize( - 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> main\n'), + 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'), ) def test_merge_conflicts_failing(contents, repository_pending_merge): repository_pending_merge.join('f2').write_binary(contents) @@ -150,7 +150,7 @@ def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys): cmd_output('git', 'worktree', 'add', str(worktree)) with worktree.as_cwd(): cmd_output( - 'git', 'pull', '--no-rebase', 'origin', 'HEAD', retcode=None, + 'git', 'pull', '--no-rebase', 'origin', 'master', retcode=None, ) msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG') assert msg.exists() diff --git a/tests/check_vcs_permalinks_test.py b/tests/check_vcs_permalinks_test.py index 324b70c..01ce94d 100644 --- a/tests/check_vcs_permalinks_test.py +++ b/tests/check_vcs_permalinks_test.py @@ -16,9 +16,9 @@ def test_passing(tmpdir): # tags are ok b'https://github.com/asottile/test/blob/1.0.0/foo%20bar#L1\n' # links to files but not line numbers are ok - b'https://github.com/asottile/test/blob/main/foo%20bar\n' + b'https://github.com/asottile/test/blob/master/foo%20bar\n' # regression test for overly-greedy regex - b'https://github.com/ yes / no ? /blob/main/foo#L1\n', + b'https://github.com/ yes / no ? /blob/master/foo#L1\n', ) assert not main((str(f),)) @@ -26,15 +26,17 @@ def test_passing(tmpdir): def test_failing(tmpdir, capsys): with tmpdir.as_cwd(): tmpdir.join('f.txt').write_binary( - b'https://github.com/asottile/test/blob/main/foo#L1\n' + b'https://github.com/asottile/test/blob/master/foo#L1\n' + b'https://example.com/asottile/test/blob/master/foo#L1\n' b'https://example.com/asottile/test/blob/main/foo#L1\n', ) assert main(('f.txt', '--additional-github-domain', 'example.com')) out, _ = capsys.readouterr() assert out == ( - 'f.txt:1:https://github.com/asottile/test/blob/main/foo#L1\n' - 'f.txt:2:https://example.com/asottile/test/blob/main/foo#L1\n' + 'f.txt:1:https://github.com/asottile/test/blob/master/foo#L1\n' + 'f.txt:2:https://example.com/asottile/test/blob/master/foo#L1\n' + 'f.txt:3:https://example.com/asottile/test/blob/main/foo#L1\n' '\n' 'Non-permanent github link detected.\n' 'On any page on github press [y] to load a permalink.\n' diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index f178ae6..5e79e40 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -10,9 +10,7 @@ from pre_commit_hooks.file_contents_sorter import PASS @pytest.mark.parametrize( ('input_s', 'argv', 'expected_retval', 'output'), ( - (b'', [], PASS, b''), - (b'\n', [], FAIL, b''), - (b'\n\n', [], FAIL, b''), + (b'', [], FAIL, b'\n'), (b'lonesome\n', [], PASS, b'lonesome\n'), (b'missing_newline', [], FAIL, b'missing_newline\n'), (b'newline\nmissing', [], FAIL, b'missing\nnewline\n'), @@ -67,6 +65,18 @@ from pre_commit_hooks.file_contents_sorter import PASS FAIL, b'Fie\nFoe\nfee\nfum\n', ), + ( + b'fee\nFie\nFoe\nfum\n', + ['--unique', '--ignore-case'], + PASS, + b'fee\nFie\nFoe\nfum\n', + ), + ( + b'fee\nfee\nFie\nFoe\nfum\n', + ['--unique', '--ignore-case'], + FAIL, + b'fee\nFie\nFoe\nfum\n', + ), ), ) def test_integration(input_s, argv, expected_retval, output, tmpdir): @@ -77,24 +87,3 @@ def test_integration(input_s, argv, expected_retval, output, tmpdir): assert path.read_binary() == output assert output_retval == expected_retval - - -@pytest.mark.parametrize( - ('input_s', 'argv'), - ( - ( - b'fee\nFie\nFoe\nfum\n', - ['--unique', '--ignore-case'], - ), - ( - b'fee\nfee\nFie\nFoe\nfum\n', - ['--unique', '--ignore-case'], - ), - ), -) -def test_integration_invalid_args(input_s, argv, tmpdir): - path = tmpdir.join('file.txt') - path.write_binary(input_s) - - with pytest.raises(SystemExit): - main([str(path)] + argv) diff --git a/tests/fix_encoding_pragma_test.py b/tests/fix_encoding_pragma_test.py new file mode 100644 index 0000000..98557e9 --- /dev/null +++ b/tests/fix_encoding_pragma_test.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import io + +import pytest + +from pre_commit_hooks.fix_encoding_pragma import _normalize_pragma +from pre_commit_hooks.fix_encoding_pragma import fix_encoding_pragma +from pre_commit_hooks.fix_encoding_pragma import main + + +def test_integration_inserting_pragma(tmpdir): + path = tmpdir.join('foo.py') + path.write_binary(b'import httplib\n') + + assert main((str(path),)) == 1 + + assert path.read_binary() == ( + b'# -*- coding: utf-8 -*-\n' + b'import httplib\n' + ) + + +def test_integration_ok(tmpdir): + path = tmpdir.join('foo.py') + path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n') + assert main((str(path),)) == 0 + + +def test_integration_remove(tmpdir): + path = tmpdir.join('foo.py') + path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n') + + assert main((str(path), '--remove')) == 1 + + assert path.read_binary() == b'x = 1\n' + + +def test_integration_remove_ok(tmpdir): + path = tmpdir.join('foo.py') + path.write_binary(b'x = 1\n') + assert main((str(path), '--remove')) == 0 + + +@pytest.mark.parametrize( + 'input_str', + ( + b'', + ( + b'# -*- coding: utf-8 -*-\n' + b'x = 1\n' + ), + ( + b'#!/usr/bin/env python\n' + b'# -*- coding: utf-8 -*-\n' + b'foo = "bar"\n' + ), + ), +) +def test_ok_inputs(input_str): + bytesio = io.BytesIO(input_str) + assert fix_encoding_pragma(bytesio) == 0 + bytesio.seek(0) + assert bytesio.read() == input_str + + +@pytest.mark.parametrize( + ('input_str', 'output'), + ( + ( + b'import httplib\n', + b'# -*- coding: utf-8 -*-\n' + b'import httplib\n', + ), + ( + b'#!/usr/bin/env python\n' + b'x = 1\n', + b'#!/usr/bin/env python\n' + b'# -*- coding: utf-8 -*-\n' + b'x = 1\n', + ), + ( + b'#coding=utf-8\n' + b'x = 1\n', + b'# -*- coding: utf-8 -*-\n' + b'x = 1\n', + ), + ( + b'#!/usr/bin/env python\n' + b'#coding=utf8\n' + b'x = 1\n', + b'#!/usr/bin/env python\n' + b'# -*- coding: utf-8 -*-\n' + b'x = 1\n', + ), + # These should each get truncated + (b'#coding: utf-8\n', b''), + (b'# -*- coding: utf-8 -*-\n', b''), + (b'#!/usr/bin/env python\n', b''), + (b'#!/usr/bin/env python\n#coding: utf8\n', b''), + (b'#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n', b''), + ), +) +def test_not_ok_inputs(input_str, output): + bytesio = io.BytesIO(input_str) + assert fix_encoding_pragma(bytesio) == 1 + bytesio.seek(0) + assert bytesio.read() == output + + +def test_ok_input_alternate_pragma(): + input_s = b'# coding: utf-8\nx = 1\n' + bytesio = io.BytesIO(input_s) + ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8') + assert ret == 0 + bytesio.seek(0) + assert bytesio.read() == input_s + + +def test_not_ok_input_alternate_pragma(): + bytesio = io.BytesIO(b'x = 1\n') + ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8') + assert ret == 1 + bytesio.seek(0) + assert bytesio.read() == b'# coding: utf-8\nx = 1\n' + + +@pytest.mark.parametrize( + ('input_s', 'expected'), + ( + ('# coding: utf-8', b'# coding: utf-8'), + # trailing whitespace + ('# coding: utf-8\n', b'# coding: utf-8'), + ), +) +def test_normalize_pragma(input_s, expected): + assert _normalize_pragma(input_s) == expected + + +def test_integration_alternate_pragma(tmpdir, capsys): + f = tmpdir.join('f.py') + f.write('x = 1\n') + + pragma = '# coding: utf-8' + assert main((str(f), '--pragma', pragma)) == 1 + assert f.read() == '# coding: utf-8\nx = 1\n' + out, _ = capsys.readouterr() + assert out == f'Added `# coding: utf-8` to {str(f)}\n' + + +def test_crlf_ok(tmpdir): + f = tmpdir.join('f.py') + f.write_binary(b'# -*- coding: utf-8 -*-\r\nx = 1\r\n') + assert not main((str(f),)) + + +def test_crfl_adds(tmpdir): + f = tmpdir.join('f.py') + f.write_binary(b'x = 1\r\n') + assert main((str(f),)) + assert f.read_binary() == b'# -*- coding: utf-8 -*-\r\nx = 1\r\n' diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py index 7d37e49..eaae5e6 100644 --- a/tests/no_commit_to_branch_test.py +++ b/tests/no_commit_to_branch_test.py @@ -11,13 +11,13 @@ from testing.util import git_commit def test_other_branch(temp_git_dir): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', 'anotherbranch') - assert is_on_branch({'placeholder'}) is False + assert is_on_branch({'master'}) is False def test_multi_branch(temp_git_dir): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', 'another/branch') - assert is_on_branch({'placeholder'}) is False + assert is_on_branch({'master'}) is False def test_multi_branch_fail(temp_git_dir): @@ -26,10 +26,9 @@ def test_multi_branch_fail(temp_git_dir): assert is_on_branch({'another/branch'}) is True -def test_exact_branch(temp_git_dir): +def test_master_branch(temp_git_dir): with temp_git_dir.as_cwd(): - cmd_output('git', 'checkout', '-b', 'branchname') - assert is_on_branch({'branchname'}) is True + assert is_on_branch({'master'}) is True def test_main_branch_call(temp_git_dir): @@ -51,11 +50,11 @@ def test_branch_pattern_fail(temp_git_dir): assert is_on_branch(set(), {'another/.*'}) is True -@pytest.mark.parametrize('branch_name', ('somebranch', 'another/branch')) +@pytest.mark.parametrize('branch_name', ('master', 'another/branch')) def test_branch_pattern_multiple_branches_fail(temp_git_dir, branch_name): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', branch_name) - assert main(('--branch', 'somebranch', '--pattern', 'another/.*')) + assert main(('--branch', 'master', '--pattern', 'another/.*')) def test_main_default_call(temp_git_dir): diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py index 68b6d7a..5ded724 100644 --- a/tests/pretty_format_json_test.py +++ b/tests/pretty_format_json_test.py @@ -82,24 +82,6 @@ def test_autofix_main(tmpdir): assert ret == 0 -def test_invalid_main(tmpdir): - srcfile1 = tmpdir.join('not_valid_json.json') - srcfile1.write( - '{\n' - ' // not json\n' - ' "a": "b"\n' - '}', - ) - srcfile2 = tmpdir.join('to_be_json_formatted.json') - srcfile2.write('{ "a": "b" }') - - # it should have skipped the first file and formatted the second one - assert main(['--autofix', str(srcfile1), str(srcfile2)]) == 1 - - # confirm second file was formatted (shouldn't trigger linter again) - assert main([str(srcfile2)]) == 0 - - def test_orderfile_get_pretty_format(): ret = main(( '--top-keys=alist', get_resource_path('pretty_formatted_json.json'), diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index c0d2c65..b725afa 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -68,12 +68,6 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement b'f<=2\n' b'g<2\n', ), - (b'a==1\nb==1\na==1\n', FAIL, b'a==1\nb==1\n'), - ( - b'a==1\nb==1\n#comment about a\na==1\n', - FAIL, - b'#comment about a\na==1\nb==1\n', - ), (b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'), ( b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n', @@ -82,8 +76,6 @@ from pre_commit_hooks.requirements_txt_fixer import Requirement ), (b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'), (b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'), - (b'bar\npkg_resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'), - (b'foo\npkg_resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'), ( b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n', FAIL, diff --git a/tests/string_fixer_test.py b/tests/string_fixer_test.py index 8eb164c..9dd7315 100644 --- a/tests/string_fixer_test.py +++ b/tests/string_fixer_test.py @@ -37,12 +37,6 @@ TESTS = ( 1, ), ('"foo""bar"', "'foo''bar'", 1), - pytest.param( - "f'hello{\"world\"}'", - "f'hello{\"world\"}'", - 0, - id='ignore nested fstrings', - ), ) diff --git a/tests/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py index 2b5a0de..dc3744b 100644 --- a/tests/tests_should_end_in_test_test.py +++ b/tests/tests_should_end_in_test_test.py @@ -43,8 +43,3 @@ def test_main_not_django_fails(): def test_main_django_fails(): ret = main(['--django', 'foo_test.py', 'test_bar.py', 'test_baz.py']) assert ret == 1 - - -def test_main_pytest_test_first(): - assert main(['--pytest-test-first', 'test_foo.py']) == 0 - assert main(['--pytest-test-first', 'foo_test.py']) == 1 diff --git a/tox.ini b/tox.ini index 11340f4..cb2b92a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py,pre-commit +envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt