diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..088a066e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: main + +on: + push: + branches: [main, test-me-*] + tags: '*' + pull_request: + +jobs: + main-windows: + uses: asottile/workflows/.github/workflows/tox.yml@v1.5.0 + with: + env: '["py38"]' + os: windows-latest + main-linux: + uses: asottile/workflows/.github/workflows/tox.yml@v1.5.0 + with: + env: '["py38", "py39", "py310", "py311"]' + os: ubuntu-latest diff --git a/.gitignore b/.gitignore index 32c2fec0..4f6c5b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,4 @@ .*.sw[a-z] .coverage .tox -.venv.touch -/.mypy_cache -/.pytest_cache -/venv* -coverage-html dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4639bc6b..315c420a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,41 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-docstring-first - - id: check-json - - id: check-added-large-files - id: check-yaml - id: debug-statements - - id: name-tests-test - id: double-quote-string-fixer + - id: name-tests-test - id: requirements-txt-fixer -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports==1.7.0] -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.7 +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.5.0 hooks: - - id: autopep8 -- repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 + - id: setup-cfg-fmt +- repo: https://github.com/asottile/reorder-python-imports + rev: v3.13.0 hooks: - id: reorder-python-imports - args: [--py3-plus] + args: [--py38-plus, --add-import, 'from __future__ import annotations'] +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v3.17.0 hooks: - id: pyupgrade - args: [--py36-plus] -- repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.1 + args: [--py38-plus] +- repo: https://github.com/hhatto/autopep8 + rev: v2.3.1 hooks: - - id: add-trailing-comma - args: [--py36-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 hooks: - - id: setup-cfg-fmt + - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v1.11.2 hooks: - id: mypy - additional_dependencies: [types-all] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 56906603..b71169bb 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,6 +3,8 @@ 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. @@ -38,7 +40,13 @@ entry: check-executables-have-shebangs language: python types: [text, executable] - stages: [commit, push, manual] + 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]$)' - id: check-json name: check json description: checks json files for parseable syntax. @@ -51,7 +59,8 @@ entry: check-shebang-scripts-are-executable language: python types: [text] - stages: [commit, push, manual] + stages: [pre-commit, pre-push, manual] + minimum_pre_commit_version: 3.2.0 - id: pretty-format-json name: pretty format json description: sets a standard for formatting json files. @@ -106,6 +115,7 @@ 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. @@ -130,7 +140,8 @@ entry: end-of-file-fixer language: python types: [text] - stages: [commit, push, manual] + stages: [pre-commit, pre-push, manual] + minimum_pre_commit_version: 3.2.0 - 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. @@ -144,7 +155,7 @@ language: python types: [text] - id: fix-encoding-pragma - name: fix python encoding pragma + name: fix python encoding pragma (deprecated) description: 'adds # -*- coding: utf-8 -*- to the top of python files.' language: python entry: fix-encoding-pragma @@ -155,6 +166,12 @@ 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. @@ -162,8 +179,8 @@ language: python types: [text] - id: name-tests-test - name: tests should end in _test.py - description: this verifies that test files are named correctly. + name: python tests naming + description: verifies that test files are named correctly. entry: name-tests-test language: python files: (^|/)tests/.+\.py$ @@ -178,7 +195,7 @@ description: sorts entries in requirements.txt. entry: requirements-txt-fixer language: python - files: requirements.*\.txt$ + files: (requirements|constraints).*\.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. @@ -191,4 +208,5 @@ entry: trailing-whitespace-fixer language: python types: [text] - stages: [commit, push, manual] + stages: [pre-commit, pre-push, manual] + minimum_pre_commit_version: 3.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e72022..e1655740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,117 @@ +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 +================== + +### Features +- `name-tests-test`: updated display text. + - #713 PR by @asottile. +- `check-docstring-first`: make output more parsable. + - #748 PR by @asottile. +- `check-merge-conflict`: make output more parsable. + - #748 PR by @asottile. +- `debug-statements`: make output more parsable. + - #748 PR by @asottile. + +### Fixes +- `check-merge-conflict`: fix detection of `======` conflict marker on windows. + - #748 PR by @asottile. + +### Updating +- Drop python<3.7. + - #719 PR by @asottile. +- Changed default branch from `master` to `main`. + - #744 PR by @asottile. + 4.1.0 - 2021-12-22 ================== diff --git a/README.md b/README.md index c5a199e1..c0f678fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -[![Build Status](https://asottile.visualstudio.com/asottile/_apis/build/status/pre-commit.pre-commit-hooks?branchName=master)](https://asottile.visualstudio.com/asottile/_build/latest?definitionId=17&branchName=master) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/17/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=17&branchName=master) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit-hooks/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit-hooks/master) +[![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) +[![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 ================ @@ -16,7 +15,7 @@ Add this to your `.pre-commit-config.yaml` ```yaml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 # Use the ref you want to point at + rev: v5.0.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -52,11 +51,15 @@ 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,21 +116,35 @@ 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. +#### `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. +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 #### `fix-encoding-pragma` + +_Deprecated since py2 is EOL - use [pyupgrade](https://github.com/asottile/pyupgrade) instead._ + 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 list of target files as input to it. -Note that this hook WILL remove blank lines and does NOT respect any comments. - #### `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}` @@ -137,13 +154,15 @@ Replaces or checks mixed line ending. - `no` - Checks if there is any mixed line ending without modifying any file. #### `name-tests-test` -Assert that files in tests/ end in `_test.py`. - - Use `args: ['--django']` to match `test*.py` instead. +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` #### `no-commit-to-branch` Protect specific branches from direct checkins. - - Use `args: [--branch, staging, --branch, master]` to set the branch. - Both `master` and `main` are protected by default if no branch argument is set. + - Use `args: [--branch, staging, --branch, main]` to set the branch. + Both `main` and `master` are protected by default if no branch argument is set. - `-b` / `--branch` may be specified multiple times to protect multiple branches. - `-p` / `--pattern` can be used to protect branches that match a supplied regex @@ -167,7 +186,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 removes incorrect entry for `pkg-resources==0.0.0` +Sorts entries in requirements.txt and constraints.txt and removes incorrect entry for `pkg-resources==0.0.0` #### `sort-simple-yaml` Sorts simple YAML files which consist only of top-level diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 58dc61dd..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,23 +0,0 @@ -trigger: - branches: - include: [master, test-me-*] - tags: - include: ['*'] - -resources: - repositories: - - repository: asottile - type: github - endpoint: github - name: asottile/azure-pipeline-templates - ref: refs/tags/v2.1.0 - -jobs: -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38] - os: windows -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [pypy3, py36, py37, py38] - os: linux diff --git a/pre_commit_hooks/check_added_large_files.py b/pre_commit_hooks/check_added_large_files.py index e1beb4ef..9e0619b0 100644 --- a/pre_commit_hooks/check_added_large_files.py +++ b/pre_commit_hooks/check_added_large_files.py @@ -1,16 +1,16 @@ +from __future__ import annotations + import argparse import math import os import subprocess -from typing import Optional from typing import Sequence -from typing import Set from pre_commit_hooks.util import added_files from pre_commit_hooks.util import zsplit -def filter_lfs_files(filenames: Set[str]) -> None: # pragma: no cover (lfs) +def filter_lfs_files(filenames: set[str]) -> None: # pragma: no cover (lfs) """Remove files tracked by git-lfs from the set.""" if not filenames: return @@ -46,7 +46,7 @@ def find_large_added_files( filenames_filtered &= added_files() for filename in filenames_filtered: - kb = int(math.ceil(os.stat(filename).st_size / 1024)) + kb = math.ceil(os.stat(filename).st_size / 1024) if kb > maxkb: print(f'{filename} ({kb} KB) exceeds {maxkb} KB.') retv = 1 @@ -54,7 +54,7 @@ def find_large_added_files( return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( 'filenames', nargs='*', diff --git a/pre_commit_hooks/check_ast.py b/pre_commit_hooks/check_ast.py index ab5661dc..fdac3617 100644 --- a/pre_commit_hooks/check_ast.py +++ b/pre_commit_hooks/check_ast.py @@ -1,13 +1,14 @@ +from __future__ import annotations + import argparse import ast import platform import sys import traceback -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_builtin_literals.py b/pre_commit_hooks/check_builtin_literals.py index 3fbae3e8..d3054aa0 100644 --- a/pre_commit_hooks/check_builtin_literals.py +++ b/pre_commit_hooks/check_builtin_literals.py @@ -1,10 +1,9 @@ +from __future__ import annotations + import argparse import ast -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Set BUILTIN_TYPES = { @@ -27,10 +26,10 @@ class Call(NamedTuple): class Visitor(ast.NodeVisitor): def __init__( self, - ignore: Optional[Sequence[str]] = None, + ignore: Sequence[str] | None = None, allow_dict_kwargs: bool = True, ) -> None: - self.builtin_type_calls: List[Call] = [] + self.builtin_type_calls: list[Call] = [] self.ignore = set(ignore) if ignore else set() self.allow_dict_kwargs = allow_dict_kwargs @@ -56,9 +55,9 @@ def visit_Call(self, node: ast.Call) -> None: def check_file( filename: str, - ignore: Optional[Sequence[str]] = None, + ignore: Sequence[str] | None = None, allow_dict_kwargs: bool = True, -) -> List[Call]: +) -> list[Call]: with open(filename, 'rb') as f: tree = ast.parse(f.read(), filename=filename) visitor = Visitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs) @@ -66,11 +65,11 @@ def check_file( return visitor.builtin_type_calls -def parse_ignore(value: str) -> Set[str]: +def parse_ignore(value: str) -> set[str]: return set(value.split(',')) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') parser.add_argument('--ignore', type=parse_ignore, default=set()) diff --git a/pre_commit_hooks/check_byte_order_marker.py b/pre_commit_hooks/check_byte_order_marker.py index fda05e84..59cc5612 100644 --- a/pre_commit_hooks/check_byte_order_marker.py +++ b/pre_commit_hooks/check_byte_order_marker.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +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) diff --git a/pre_commit_hooks/check_case_conflict.py b/pre_commit_hooks/check_case_conflict.py index c3f39db5..33a13f1b 100644 --- a/pre_commit_hooks/check_case_conflict.py +++ b/pre_commit_hooks/check_case_conflict.py @@ -1,15 +1,15 @@ +from __future__ import annotations + import argparse from typing import Iterable from typing import Iterator -from typing import Optional from typing import Sequence -from typing import Set from pre_commit_hooks.util import added_files from pre_commit_hooks.util import cmd_output -def lower_set(iterable: Iterable[str]) -> Set[str]: +def lower_set(iterable: Iterable[str]) -> set[str]: return {x.lower() for x in iterable} @@ -21,7 +21,7 @@ def parents(file: str) -> Iterator[str]: path_parts.pop() -def directories_for(files: Set[str]) -> Set[str]: +def directories_for(files: set[str]) -> set[str]: return {parent for file in files for parent in parents(file)} @@ -56,7 +56,7 @@ def find_conflicting_filenames(filenames: Sequence[str]) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( 'filenames', nargs='*', diff --git a/pre_commit_hooks/check_docstring_first.py b/pre_commit_hooks/check_docstring_first.py index 875c0fba..d55f08a5 100644 --- a/pre_commit_hooks/check_docstring_first.py +++ b/pre_commit_hooks/check_docstring_first.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import argparse import io import tokenize from tokenize import tokenize as tokenize_tokenize -from typing import Optional from typing import Sequence NON_CODE_TOKENS = frozenset(( @@ -27,13 +28,13 @@ def check_docstring_first(src: bytes, filename: str = '') -> int: if tok_type == tokenize.STRING and scol == 0: if found_docstring_line is not None: print( - f'{filename}:{sline} Multiple module docstrings ' + f'{filename}:{sline}: Multiple module docstrings ' f'(first docstring on line {found_docstring_line}).', ) return 1 elif found_code_line is not None: print( - f'{filename}:{sline} Module docstring appears after code ' + f'{filename}:{sline}: Module docstring appears after code ' f'(code seen on line {found_code_line}).', ) return 1 @@ -45,7 +46,7 @@ def check_docstring_first(src: bytes, filename: str = '') -> int: return 0 -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py index 34af5cac..7c984d39 100644 --- a/pre_commit_hooks/check_executables_have_shebangs.py +++ b/pre_commit_hooks/check_executables_have_shebangs.py @@ -1,13 +1,12 @@ """Check that executable text files have a shebang.""" +from __future__ import annotations + import argparse import shlex import sys from typing import Generator -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Set from pre_commit_hooks.util import cmd_output from pre_commit_hooks.util import zsplit @@ -15,8 +14,11 @@ EXECUTABLE_VALUES = frozenset(('1', '3', '5', '7')) -def check_executables(paths: List[str]) -> int: - if sys.platform == 'win32': # pragma: win32 cover +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 return _check_git_filemode(paths) else: # pragma: win32 no cover retv = 0 @@ -33,7 +35,7 @@ class GitLsFile(NamedTuple): filename: str -def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile, None, None]: +def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile]: outs = cmd_output('git', 'ls-files', '-z', '--stage', '--', *paths) for out in zsplit(outs): metadata, filename = out.split('\t') @@ -42,7 +44,7 @@ def git_ls_files(paths: Sequence[str]) -> Generator[GitLsFile, None, None]: def _check_git_filemode(paths: Sequence[str]) -> int: - seen: Set[str] = set() + seen: set[str] = set() for ls_file in git_ls_files(paths): is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:]) if is_executable and not has_shebang(ls_file.filename): @@ -71,7 +73,7 @@ def _message(path: str) -> None: ) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_json.py b/pre_commit_hooks/check_json.py index 96ba024e..6a679fee 100644 --- a/pre_commit_hooks/check_json.py +++ b/pre_commit_hooks/check_json.py @@ -1,16 +1,14 @@ +from __future__ import annotations + import argparse import json from typing import Any -from typing import Dict -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple def raise_duplicate_keys( - ordered_pairs: List[Tuple[str, Any]], -) -> Dict[str, Any]: + ordered_pairs: list[tuple[str, Any]], +) -> dict[str, Any]: d = {} for key, val in ordered_pairs: if key in d: @@ -20,7 +18,7 @@ def raise_duplicate_keys( return d -def main(argv: Optional[Sequence[str]] = None) -> int: +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) diff --git a/pre_commit_hooks/check_merge_conflict.py b/pre_commit_hooks/check_merge_conflict.py index aed1e9ca..15ec284a 100644 --- a/pre_commit_hooks/check_merge_conflict.py +++ b/pre_commit_hooks/check_merge_conflict.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import argparse import os.path -from typing import Optional from typing import Sequence from pre_commit_hooks.util import cmd_output @@ -9,6 +10,7 @@ CONFLICT_PATTERNS = [ b'<<<<<<< ', b'======= ', + b'=======\r\n', b'=======\n', b'>>>>>>> ', ] @@ -26,7 +28,7 @@ def is_in_merge() -> bool: ) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') parser.add_argument('--assume-in-merge', action='store_true') @@ -38,12 +40,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: retcode = 0 for filename in args.filenames: with open(filename, 'rb') as inputfile: - for i, line in enumerate(inputfile): + for i, line in enumerate(inputfile, start=1): for pattern in CONFLICT_PATTERNS: if line.startswith(pattern): print( - f'Merge conflict string "{pattern.decode()}" ' - f'found in {filename}:{i + 1}', + f'{filename}:{i}: Merge conflict string ' + f'{pattern.strip().decode()!r} found', ) retcode = 1 diff --git a/pre_commit_hooks/check_shebang_scripts_are_executable.py b/pre_commit_hooks/check_shebang_scripts_are_executable.py index 50bc9c0c..621696ce 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -1,18 +1,17 @@ """Check that text files with a shebang are executable.""" +from __future__ import annotations + import argparse import shlex import sys -from typing import List -from typing import Optional from typing import Sequence -from typing import Set from pre_commit_hooks.check_executables_have_shebangs import EXECUTABLE_VALUES from pre_commit_hooks.check_executables_have_shebangs import git_ls_files from pre_commit_hooks.check_executables_have_shebangs import has_shebang -def check_shebangs(paths: List[str]) -> int: +def check_shebangs(paths: list[str]) -> int: # Cannot optimize on non-executability here if we intend this check to # work on win32 -- and that's where problems caused by non-executability # (elsewhere) are most likely to arise from. @@ -20,7 +19,7 @@ def check_shebangs(paths: List[str]) -> int: def _check_git_filemode(paths: Sequence[str]) -> int: - seen: Set[str] = set() + seen: set[str] = set() for ls_file in git_ls_files(paths): is_executable = any(b in EXECUTABLE_VALUES for b in ls_file.mode[-3:]) if not is_executable and has_shebang(ls_file.filename): @@ -35,13 +34,15 @@ 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 not supposed to be executable, double-check its shebang ' f'is wanted.\n', file=sys.stderr, ) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_symlinks.py b/pre_commit_hooks/check_symlinks.py index db3b4fea..a85c82a1 100644 --- a/pre_commit_hooks/check_symlinks.py +++ b/pre_commit_hooks/check_symlinks.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import argparse import os.path -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description='Checks for broken symlinks.') parser.add_argument('filenames', nargs='*', help='Filenames to check') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_toml.py b/pre_commit_hooks/check_toml.py index f623e688..0407371e 100644 --- a/pre_commit_hooks/check_toml.py +++ b/pre_commit_hooks/check_toml.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import argparse -from typing import Optional +import sys from typing import Sequence -import toml +if sys.version_info >= (3, 11): # pragma: >=3.11 cover + import tomllib +else: # pragma: <3.11 cover + import tomli as tomllib -def main(argv: Optional[Sequence[str]] = None) -> int: +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) @@ -13,8 +18,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: retval = 0 for filename in args.filenames: try: - toml.load(filename) - except toml.TomlDecodeError as exc: + with open(filename, mode='rb') as fp: + tomllib.load(fp) + except tomllib.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 1c77b9a3..68639bd9 100644 --- a/pre_commit_hooks/check_vcs_permalinks.py +++ b/pre_commit_hooks/check_vcs_permalinks.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import re import sys -from typing import List -from typing import Optional from typing import Pattern from typing import Sequence @@ -15,7 +15,7 @@ def _get_pattern(domain: str) -> Pattern[bytes]: return re.compile(regex.encode()) -def _check_filename(filename: str, patterns: List[Pattern[bytes]]) -> int: +def _check_filename(filename: str, patterns: list[Pattern[bytes]]) -> int: retv = 0 with open(filename, 'rb') as f: for i, line in enumerate(f, 1): @@ -28,7 +28,7 @@ def _check_filename(filename: str, patterns: List[Pattern[bytes]]) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') parser.add_argument( diff --git a/pre_commit_hooks/check_xml.py b/pre_commit_hooks/check_xml.py index 0fa6bb23..c256af9b 100644 --- a/pre_commit_hooks/check_xml.py +++ b/pre_commit_hooks/check_xml.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import argparse import xml.sax.handler -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='XML filenames to check.') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/check_yaml.py b/pre_commit_hooks/check_yaml.py index 5e86b73a..01873921 100644 --- a/pre_commit_hooks/check_yaml.py +++ b/pre_commit_hooks/check_yaml.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import argparse from typing import Any from typing import Generator from typing import NamedTuple -from typing import Optional from typing import Sequence import ruamel.yaml @@ -10,7 +11,7 @@ yaml = ruamel.yaml.YAML(typ='safe') -def _exhaust(gen: Generator[str, None, None]) -> None: +def _exhaust(gen: Generator[str]) -> None: for _ in gen: pass @@ -36,7 +37,7 @@ class Key(NamedTuple): } -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( '-m', '--multi', '--allow-multiple-documents', action='store_true', @@ -45,7 +46,7 @@ def main(argv: Optional[Sequence[str]] = 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 contstructs ' + 'A syntax-only check enables extensions and unsafe constructs ' '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 f78d6d62..cf544c7b 100644 --- a/pre_commit_hooks/debug_statement_hook.py +++ b/pre_commit_hooks/debug_statement_hook.py @@ -1,13 +1,14 @@ +from __future__ import annotations + import argparse import ast import traceback -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence DEBUG_STATEMENTS = { + 'bpdb', 'ipdb', 'pdb', 'pdbr', @@ -29,7 +30,7 @@ class Debug(NamedTuple): class DebugStatementParser(ast.NodeVisitor): def __init__(self) -> None: - self.breakpoints: List[Debug] = [] + self.breakpoints: list[Debug] = [] def visit_Import(self, node: ast.Import) -> None: for name in node.names: @@ -65,12 +66,12 @@ def check_file(filename: str) -> int: visitor.visit(ast_obj) for bp in visitor.breakpoints: - print(f'{filename}:{bp.line}:{bp.col} - {bp.name} {bp.reason}') + print(f'{filename}:{bp.line}:{bp.col}: {bp.name} {bp.reason}') return int(bool(visitor.breakpoints)) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to run') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/destroyed_symlinks.py b/pre_commit_hooks/destroyed_symlinks.py index a3f122ff..f2569085 100644 --- a/pre_commit_hooks/destroyed_symlinks.py +++ b/pre_commit_hooks/destroyed_symlinks.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import shlex import subprocess -from typing import List -from typing import Optional from typing import Sequence from pre_commit_hooks.util import cmd_output @@ -13,8 +13,8 @@ PERMS_NONEXIST = '000000' -def find_destroyed_symlinks(files: Sequence[str]) -> List[str]: - destroyed_links: List[str] = [] +def find_destroyed_symlinks(files: Sequence[str]) -> list[str]: + destroyed_links: list[str] = [] if not files: return destroyed_links for line in zsplit( @@ -66,7 +66,7 @@ def find_destroyed_symlinks(files: Sequence[str]) -> List[str]: return destroyed_links -def main(argv: Optional[Sequence[str]] = None) -> int: +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) @@ -76,11 +76,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: for destroyed_link in destroyed_links: print(f'- {destroyed_link}') print('You should unstage affected files:') - print( - '\tgit reset HEAD -- {}'.format( - ' '.join(shlex.quote(link) for link in destroyed_links), - ), - ) + print(f'\tgit reset HEAD -- {shlex.join(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 ba1d789a..4f59d9cf 100644 --- a/pre_commit_hooks/detect_aws_credentials.py +++ b/pre_commit_hooks/detect_aws_credentials.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import argparse import configparser import os -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Set class BadFile(NamedTuple): @@ -13,7 +12,7 @@ class BadFile(NamedTuple): key: str -def get_aws_cred_files_from_env() -> Set[str]: +def get_aws_cred_files_from_env() -> set[str]: """Extract credential file paths from environment variables.""" return { os.environ[env_var] @@ -25,7 +24,7 @@ def get_aws_cred_files_from_env() -> Set[str]: } -def get_aws_secrets_from_env() -> Set[str]: +def get_aws_secrets_from_env() -> set[str]: """Extract AWS secrets from environment variables.""" keys = set() for env_var in ( @@ -36,7 +35,7 @@ def get_aws_secrets_from_env() -> Set[str]: return keys -def get_aws_secrets_from_file(credentials_file: str) -> Set[str]: +def get_aws_secrets_from_file(credentials_file: str) -> set[str]: """Extract AWS secrets from configuration files. Read an ini-style configuration file and return a set with all found AWS @@ -69,8 +68,8 @@ def get_aws_secrets_from_file(credentials_file: str) -> Set[str]: def check_file_for_aws_keys( filenames: Sequence[str], - keys: Set[bytes], -) -> List[BadFile]: + keys: set[bytes], +) -> list[BadFile]: """Check if files contain AWS secrets. Return a list of all files containing AWS secrets and keys found, with all @@ -90,7 +89,7 @@ def check_file_for_aws_keys( return bad_files -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='+', help='Filenames to run') parser.add_argument( @@ -119,7 +118,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: # of files to to gather AWS secrets from. credential_files |= get_aws_cred_files_from_env() - keys: Set[str] = set() + keys: set[str] = set() for credential_file in credential_files: keys |= get_aws_secrets_from_file(credential_file) diff --git a/pre_commit_hooks/detect_private_key.py b/pre_commit_hooks/detect_private_key.py index 18f95394..cd51f901 100644 --- a/pre_commit_hooks/detect_private_key.py +++ b/pre_commit_hooks/detect_private_key.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence BLACKLIST = [ @@ -16,7 +17,7 @@ ] -def main(argv: Optional[Sequence[str]] = None) -> int: +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) diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py index 40e8821d..a30dce92 100644 --- a/pre_commit_hooks/end_of_file_fixer.py +++ b/pre_commit_hooks/end_of_file_fixer.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import argparse import os from typing import IO -from typing import Optional from typing import Sequence @@ -48,7 +49,7 @@ def fix_file(file_obj: IO[bytes]) -> int: return 0 -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index 392e2261..02bdbccf 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -9,12 +9,13 @@ this hook on that file should reduce the instances of git merge conflicts and keep the file nicely ordered. """ +from __future__ import annotations + import argparse from typing import Any from typing import Callable from typing import IO from typing import Iterable -from typing import Optional from typing import Sequence PASS = 0 @@ -23,7 +24,7 @@ def sort_file_contents( f: IO[bytes], - key: Optional[Callable[[bytes], Any]], + key: Callable[[bytes], Any] | None, *, unique: bool = False, ) -> int: @@ -36,7 +37,10 @@ def sort_file_contents( after = sorted(lines, key=key) before_string = b''.join(before) - after_string = b'\n'.join(after) + b'\n' + after_string = b'\n'.join(after) + + if after_string: + after_string += b'\n' if before_string == after_string: return PASS @@ -47,7 +51,7 @@ def sort_file_contents( return FAIL -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='+', help='Files to sort') parser.add_argument( diff --git a/pre_commit_hooks/fix_byte_order_marker.py b/pre_commit_hooks/fix_byte_order_marker.py index 51b94e16..22a49909 100644 --- a/pre_commit_hooks/fix_byte_order_marker.py +++ b/pre_commit_hooks/fix_byte_order_marker.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +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) diff --git a/pre_commit_hooks/fix_encoding_pragma.py b/pre_commit_hooks/fix_encoding_pragma.py index c704774f..eee67058 100644 --- a/pre_commit_hooks/fix_encoding_pragma.py +++ b/pre_commit_hooks/fix_encoding_pragma.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import argparse +import sys from typing import IO from typing import NamedTuple -from typing import Optional from typing import Sequence DEFAULT_PRAGMA = b'# -*- coding: utf-8 -*-' @@ -26,7 +28,7 @@ class ExpectedContents(NamedTuple): # True: has exactly the coding pragma expected # False: missing coding pragma entirely # None: has a coding pragma, but it does not match - pragma_status: Optional[bool] + pragma_status: bool | None ending: bytes @property @@ -55,7 +57,7 @@ def _get_expected_contents( rest = second_line + rest if potential_coding.rstrip(b'\r\n') == expected_pragma: - pragma_status: Optional[bool] = True + pragma_status: bool | None = True elif has_coding(potential_coding): pragma_status = None else: @@ -105,7 +107,14 @@ def _normalize_pragma(pragma: str) -> bytes: return pragma.encode().rstrip() -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: + print( + 'warning: this hook is deprecated and will be removed in a future ' + 'release because py2 is EOL. instead, use ' + 'https://github.com/asottile/pyupgrade', + file=sys.stderr, + ) + parser = argparse.ArgumentParser( 'Fixes the encoding pragma of python files', ) diff --git a/pre_commit_hooks/forbid_new_submodules.py b/pre_commit_hooks/forbid_new_submodules.py index 02758089..b806cad2 100644 --- a/pre_commit_hooks/forbid_new_submodules.py +++ b/pre_commit_hooks/forbid_new_submodules.py @@ -1,12 +1,13 @@ +from __future__ import annotations + import argparse import os -from typing import Optional from typing import Sequence from pre_commit_hooks.util import cmd_output -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/mixed_line_ending.py b/pre_commit_hooks/mixed_line_ending.py index 4e07ed96..0328e865 100644 --- a/pre_commit_hooks/mixed_line_ending.py +++ b/pre_commit_hooks/mixed_line_ending.py @@ -1,7 +1,7 @@ +from __future__ import annotations + import argparse import collections -from typing import Dict -from typing import Optional from typing import Sequence @@ -25,7 +25,7 @@ def fix_filename(filename: str, fix: str) -> int: with open(filename, 'rb') as f: contents = f.read() - counts: Dict[bytes, int] = collections.defaultdict(int) + counts: dict[bytes, int] = collections.defaultdict(int) for line in contents.splitlines(True): for ending in ALL_ENDINGS: @@ -62,7 +62,7 @@ def fix_filename(filename: str, fix: str) -> int: return other_endings -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( '-f', '--fix', diff --git a/pre_commit_hooks/no_commit_to_branch.py b/pre_commit_hooks/no_commit_to_branch.py index db848507..741f7267 100644 --- a/pre_commit_hooks/no_commit_to_branch.py +++ b/pre_commit_hooks/no_commit_to_branch.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import argparse import re from typing import AbstractSet -from typing import Optional from typing import Sequence from pre_commit_hooks.util import CalledProcessError @@ -23,7 +24,7 @@ def is_on_branch( ) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( '-b', '--branch', action='append', diff --git a/pre_commit_hooks/pretty_format_json.py b/pre_commit_hooks/pretty_format_json.py index 33ad5a1c..5c0292bf 100644 --- a/pre_commit_hooks/pretty_format_json.py +++ b/pre_commit_hooks/pretty_format_json.py @@ -1,13 +1,11 @@ +from __future__ import annotations + import argparse import json import sys from difflib import unified_diff -from typing import List from typing import Mapping -from typing import Optional from typing import Sequence -from typing import Tuple -from typing import Union def _get_pretty_format( @@ -17,7 +15,7 @@ def _get_pretty_format( sort_keys: bool = True, top_keys: Sequence[str] = (), ) -> str: - def pairs_first(pairs: Sequence[Tuple[str, str]]) -> Mapping[str, str]: + def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]: before = [pair for pair in pairs if pair[0] in top_keys] before = sorted(before, key=lambda x: top_keys.index(x[0])) after = [pair for pair in pairs if pair[0] not in top_keys] @@ -38,7 +36,7 @@ def _autofix(filename: str, new_contents: str) -> None: f.write(new_contents) -def parse_num_to_int(s: str) -> Union[int, str]: +def parse_num_to_int(s: str) -> int | str: """Convert string numbers to int, leaving strings as is.""" try: return int(s) @@ -46,7 +44,7 @@ def parse_num_to_int(s: str) -> Union[int, str]: return s -def parse_topkeys(s: str) -> List[str]: +def parse_topkeys(s: str) -> list[str]: return s.split(',') @@ -57,7 +55,7 @@ def get_diff(source: str, target: str, file: str) -> str: return ''.join(diff) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( '--autofix', @@ -117,16 +115,20 @@ def main(argv: Optional[Sequence[str]] = None) -> int: f'Input File {json_file} is not a valid JSON, consider using ' f'check-json', ) - return 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 + 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()) + + status = 1 return status diff --git a/pre_commit_hooks/removed.py b/pre_commit_hooks/removed.py index 236cbf83..6f6c7b72 100644 --- a/pre_commit_hooks/removed.py +++ b/pre_commit_hooks/removed.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import sys -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: argv = argv if argv is not None else sys.argv[1:] hookid, new_hookid, url = argv[:3] raise SystemExit( diff --git a/pre_commit_hooks/requirements_txt_fixer.py b/pre_commit_hooks/requirements_txt_fixer.py index 63f891f7..07b57e18 100644 --- a/pre_commit_hooks/requirements_txt_fixer.py +++ b/pre_commit_hooks/requirements_txt_fixer.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import re from typing import IO -from typing import List -from typing import Optional from typing import Sequence @@ -15,8 +15,8 @@ class Requirement: UNTIL_SEP = re.compile(rb'[^;\s]+') def __init__(self) -> None: - self.value: Optional[bytes] = None - self.comments: List[bytes] = [] + self.value: bytes | None = None + self.comments: list[bytes] = [] @property def name(self) -> bytes: @@ -36,7 +36,7 @@ def name(self) -> bytes: return name[:m.start()] - def __lt__(self, requirement: 'Requirement') -> bool: + def __lt__(self, requirement: Requirement) -> bool: # \n means top of file comment, so always return True, # otherwise just do a string comparison with value. assert self.value is not None, self.value @@ -45,6 +45,11 @@ def __lt__(self, requirement: 'Requirement') -> bool: 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: @@ -61,9 +66,9 @@ def append_value(self, value: bytes) -> None: def fix_requirements(f: IO[bytes]) -> int: - requirements: List[Requirement] = [] + requirements: list[Requirement] = [] before = list(f) - after: List[bytes] = [] + after: list[bytes] = [] before_string = b''.join(before) @@ -110,13 +115,20 @@ 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 != b'pkg-resources==0.0.0\n' + if req.value not in [ + b'pkg-resources==0.0.0\n', + 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 - after.append(requirement.value) + if prev is None or requirement.value != prev.value: + after.append(requirement.value) + prev = requirement after.extend(rest) after_string = b''.join(after) @@ -130,7 +142,7 @@ def fix_requirements(f: IO[bytes]) -> int: return FAIL -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/sort_simple_yaml.py b/pre_commit_hooks/sort_simple_yaml.py index 39f683e4..116b5c19 100644 --- a/pre_commit_hooks/sort_simple_yaml.py +++ b/pre_commit_hooks/sort_simple_yaml.py @@ -17,16 +17,16 @@ In other words, we don't sort deeper than the top layer, and might corrupt complicated YAML files. """ +from __future__ import annotations + import argparse -from typing import List -from typing import Optional from typing import Sequence QUOTES = ["'", '"'] -def sort(lines: List[str]) -> List[str]: +def sort(lines: list[str]) -> list[str]: """Sort a YAML file in alphabetical order, keeping blocks together. :param lines: array of strings (without newlines) @@ -44,7 +44,7 @@ def sort(lines: List[str]) -> List[str]: return new_lines -def parse_block(lines: List[str], header: bool = False) -> List[str]: +def parse_block(lines: list[str], header: bool = False) -> list[str]: """Parse and return a single block, popping off the start of `lines`. If parsing a header block, we stop after we reach a line that is not a @@ -60,7 +60,7 @@ def parse_block(lines: List[str], header: bool = False) -> List[str]: return block_lines -def parse_blocks(lines: List[str]) -> List[List[str]]: +def parse_blocks(lines: list[str]) -> list[list[str]]: """Parse and return all possible blocks, popping off the start of `lines`. :param lines: list of lines @@ -77,7 +77,7 @@ def parse_blocks(lines: List[str]) -> List[List[str]]: return blocks -def first_key(lines: List[str]) -> str: +def first_key(lines: list[str]) -> str: """Returns a string representing the sort key of a block. The sort key is the first YAML key we encounter, ignoring comments, and @@ -99,7 +99,7 @@ def first_key(lines: List[str]) -> str: return '' # not actually reached in reality -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/string_fixer.py b/pre_commit_hooks/string_fixer.py index a08a5f76..d1b1c4ae 100644 --- a/pre_commit_hooks/string_fixer.py +++ b/pre_commit_hooks/string_fixer.py @@ -1,11 +1,18 @@ +from __future__ import annotations + import argparse import io import re +import sys import tokenize -from typing import List -from typing import Optional from typing 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 + START_QUOTE_RE = re.compile('^[a-zA-Z]*"') @@ -24,7 +31,7 @@ def handle_match(token_text: str) -> str: return token_text -def get_line_offsets_by_line_no(src: str) -> List[int]: +def get_line_offsets_by_line_no(src: str) -> list[int]: # Padded so we can index with line number offsets = [-1, 0] for line in src.splitlines(True): @@ -40,11 +47,17 @@ 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 == tokenize.STRING: + 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: new_text = handle_match(token_text) splitcontents[ line_offsets[srow] + scol: @@ -60,7 +73,7 @@ def fix_strings(filename: str) -> int: return 0 -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index bffb0c42..e7842af7 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -1,30 +1,50 @@ +from __future__ import annotations + import argparse import os.path import re -from typing import Optional from typing import Sequence -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') - parser.add_argument( - '--django', default=False, action='store_true', - help='Use Django-style test naming pattern (test*.py)', + 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', ) args = parser.parse_args(argv) retcode = 0 - test_name_pattern = r'test.*\.py' if args.django else r'.*_test\.py' + reg = re.compile(args.pattern) for filename in args.filenames: base = os.path.basename(filename) if ( - not re.match(test_name_pattern, base) and + not reg.fullmatch(base) and not base == '__init__.py' and not base == 'conftest.py' ): retcode = 1 - print(f'{filename} does not match pattern "{test_name_pattern}"') + print(f'{filename} does not match pattern "{args.pattern}"') return retcode diff --git a/pre_commit_hooks/trailing_whitespace_fixer.py b/pre_commit_hooks/trailing_whitespace_fixer.py index 82faa2dc..84f50671 100644 --- a/pre_commit_hooks/trailing_whitespace_fixer.py +++ b/pre_commit_hooks/trailing_whitespace_fixer.py @@ -1,13 +1,14 @@ +from __future__ import annotations + import argparse import os -from typing import Optional from typing import Sequence def _fix_file( filename: str, is_markdown: bool, - chars: Optional[bytes], + chars: bytes | None, ) -> bool: with open(filename, mode='rb') as file_processed: lines = file_processed.readlines() @@ -24,7 +25,7 @@ def _fix_file( def _process_line( line: bytes, is_markdown: bool, - chars: Optional[bytes], + chars: bytes | None, ) -> bytes: if line[-2:] == b'\r\n': eol = b'\r\n' @@ -40,7 +41,7 @@ def _process_line( return line.rstrip(chars) + eol -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument( '--no-markdown-linebreak-ext', diff --git a/pre_commit_hooks/util.py b/pre_commit_hooks/util.py index 402e33e6..d6c90ae0 100644 --- a/pre_commit_hooks/util.py +++ b/pre_commit_hooks/util.py @@ -1,20 +1,19 @@ +from __future__ import annotations + import subprocess from typing import Any -from typing import List -from typing import Optional -from typing import Set class CalledProcessError(RuntimeError): pass -def added_files() -> Set[str]: +def added_files() -> set[str]: cmd = ('git', 'diff', '--staged', '--name-only', '--diff-filter=A') return set(cmd_output(*cmd).splitlines()) -def cmd_output(*cmd: str, retcode: Optional[int] = 0, **kwargs: Any) -> str: +def cmd_output(*cmd: str, retcode: int | None = 0, **kwargs: Any) -> str: kwargs.setdefault('stdout', subprocess.PIPE) kwargs.setdefault('stderr', subprocess.PIPE) proc = subprocess.Popen(cmd, **kwargs) @@ -25,7 +24,7 @@ def cmd_output(*cmd: str, retcode: Optional[int] = 0, **kwargs: Any) -> str: return stdout -def zsplit(s: str) -> List[str]: +def zsplit(s: str) -> list[str]: s = s.strip('\0') if s: return s.split('\0') diff --git a/setup.cfg b/setup.cfg index 45498ebe..a0d67f82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit_hooks -version = 4.1.0 +version = 5.0.0 description = Some out-of-the-box hooks for pre-commit. long_description = file: README.md long_description_content_type = text/markdown @@ -8,16 +8,11 @@ url = https://github.com/pre-commit/pre-commit-hooks author = Anthony Sottile author_email = asottile@umich.edu license = MIT -license_file = LICENSE +license_files = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - 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 @@ -25,8 +20,8 @@ classifiers = packages = find: install_requires = ruamel.yaml>=0.15 - toml -python_requires = >=3.6.1 + tomli>=1.1.0;python_version<"3.11" +python_requires = >=3.8 [options.packages.find] exclude = @@ -80,7 +75,6 @@ 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/setup.py b/setup.py index 8bf1ba93..3d93aefb 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from setuptools import setup setup() diff --git a/testing/util.py b/testing/util.py index 50437544..2bbbe644 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import subprocess diff --git a/tests/check_added_large_files_test.py b/tests/check_added_large_files_test.py index c16bf5a9..54c4e689 100644 --- a/tests/check_added_large_files_test.py +++ b/tests/check_added_large_files_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shutil import pytest diff --git a/tests/check_ast_test.py b/tests/check_ast_test.py index 686fd116..62439661 100644 --- a/tests/check_ast_test.py +++ b/tests/check_ast_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks.check_ast import main from testing.util import get_resource_path diff --git a/tests/check_builtin_literals_test.py b/tests/check_builtin_literals_test.py index e9367989..1b182573 100644 --- a/tests/check_builtin_literals_test.py +++ b/tests/check_builtin_literals_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import pytest diff --git a/tests/check_byte_order_marker_test.py b/tests/check_byte_order_marker_test.py index 4c402476..909a39bb 100644 --- a/tests/check_byte_order_marker_test.py +++ b/tests/check_byte_order_marker_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks import check_byte_order_marker diff --git a/tests/check_case_conflict_test.py b/tests/check_case_conflict_test.py index d9211b57..a914f452 100644 --- a/tests/check_case_conflict_test.py +++ b/tests/check_case_conflict_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import pytest diff --git a/tests/check_docstring_first_test.py b/tests/check_docstring_first_test.py index ed5c08ef..8bafae82 100644 --- a/tests/check_docstring_first_test.py +++ b/tests/check_docstring_first_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.check_docstring_first import check_docstring_first @@ -15,7 +17,7 @@ b'from __future__ import unicode_literals\n' b'"foo"\n', 1, - '{filename}:2 Module docstring appears after code ' + '{filename}:2: Module docstring appears after code ' '(code seen on line 1).\n', ), # Test double docstring @@ -24,7 +26,7 @@ b'from __future__ import absolute_import\n' b'"fake docstring"\n', 1, - '{filename}:3 Multiple module docstrings ' + '{filename}:3: Multiple module docstrings ' '(first docstring on line 1).\n', ), # Test multiple lines of code above @@ -33,7 +35,7 @@ b'import sys\n' b'"docstring"\n', 1, - '{filename}:3 Module docstring appears after code ' + '{filename}:3: Module docstring appears after code ' '(code seen on line 1).\n', ), # String literals in expressions are ok. diff --git a/tests/check_executables_have_shebangs_test.py b/tests/check_executables_have_shebangs_test.py index 5703eded..82d03e3d 100644 --- a/tests/check_executables_have_shebangs_test.py +++ b/tests/check_executables_have_shebangs_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys diff --git a/tests/check_illegal_windows_names_test.py b/tests/check_illegal_windows_names_test.py new file mode 100644 index 00000000..82d75322 --- /dev/null +++ b/tests/check_illegal_windows_names_test.py @@ -0,0 +1,63 @@ +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_json_test.py b/tests/check_json_test.py index 3ec67f1c..53e1f52d 100644 --- a/tests/check_json_test.py +++ b/tests/check_json_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.check_json import main diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py index 79c1b118..76c4283c 100644 --- a/tests/check_merge_conflict_test.py +++ b/tests/check_merge_conflict_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil @@ -99,8 +101,14 @@ def repository_pending_merge(tmpdir): @pytest.mark.usefixtures('f1_is_a_conflict_file') -def test_merge_conflicts_git(): +def test_merge_conflicts_git(capsys): assert main(['f1']) == 1 + out, _ = capsys.readouterr() + assert out == ( + "f1:1: Merge conflict string '<<<<<<<' found\n" + "f1:3: Merge conflict string '=======' found\n" + "f1:5: Merge conflict string '>>>>>>>' found\n" + ) @pytest.mark.parametrize( @@ -137,7 +145,7 @@ def test_care_when_assumed_merge(tmpdir): assert main([str(f.realpath()), '--assume-in-merge']) == 1 -def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir): +def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys): worktree = tmpdir.join('worktree') cmd_output('git', 'worktree', 'add', str(worktree)) with worktree.as_cwd(): @@ -146,4 +154,4 @@ def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir): ) msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG') assert msg.exists() - test_merge_conflicts_git() + test_merge_conflicts_git(capsys) diff --git a/tests/check_shebang_scripts_are_executable_test.py b/tests/check_shebang_scripts_are_executable_test.py index 9e78b06c..e4bd07ca 100644 --- a/tests/check_shebang_scripts_are_executable_test.py +++ b/tests/check_shebang_scripts_are_executable_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pytest diff --git a/tests/check_symlinks_test.py b/tests/check_symlinks_test.py index 07c11687..e2c2c78f 100644 --- a/tests/check_symlinks_test.py +++ b/tests/check_symlinks_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pytest diff --git a/tests/check_toml_test.py b/tests/check_toml_test.py index c7251eb0..d594f815 100644 --- a/tests/check_toml_test.py +++ b/tests/check_toml_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks.check_toml import main diff --git a/tests/check_vcs_permalinks_test.py b/tests/check_vcs_permalinks_test.py index ad591515..01ce94de 100644 --- a/tests/check_vcs_permalinks_test.py +++ b/tests/check_vcs_permalinks_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks.check_vcs_permalinks import main diff --git a/tests/check_xml_test.py b/tests/check_xml_test.py index 357bad64..767619f1 100644 --- a/tests/check_xml_test.py +++ b/tests/check_xml_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.check_xml import main diff --git a/tests/check_yaml_test.py b/tests/check_yaml_test.py index 1a017a12..54eb16e8 100644 --- a/tests/check_yaml_test.py +++ b/tests/check_yaml_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.check_yaml import main diff --git a/tests/conftest.py b/tests/conftest.py index f92cfc18..807f15b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.util import cmd_output diff --git a/tests/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py index 428421a0..5a8e0bb2 100644 --- a/tests/debug_statement_hook_test.py +++ b/tests/debug_statement_hook_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast from pre_commit_hooks.debug_statement_hook import Debug @@ -53,7 +55,9 @@ def test_non_utf8_file(tmpdir): assert main((str(f_py),)) == 0 -def test_py37_breakpoint(tmpdir): +def test_py37_breakpoint(tmpdir, capsys): f_py = tmpdir.join('f.py') f_py.write('def f():\n breakpoint()\n') assert main((str(f_py),)) == 1 + out, _ = capsys.readouterr() + assert out == f'{f_py}:2:4: breakpoint called\n' diff --git a/tests/destroyed_symlinks_test.py b/tests/destroyed_symlinks_test.py index cde06cfb..39c474a1 100644 --- a/tests/destroyed_symlinks_test.py +++ b/tests/destroyed_symlinks_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import subprocess diff --git a/tests/detect_aws_credentials_test.py b/tests/detect_aws_credentials_test.py index 72125099..afda47a9 100644 --- a/tests/detect_aws_credentials_test.py +++ b/tests/detect_aws_credentials_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest.mock import patch import pytest diff --git a/tests/detect_private_key_test.py b/tests/detect_private_key_test.py index d2c724f0..41f8bae5 100644 --- a/tests/detect_private_key_test.py +++ b/tests/detect_private_key_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.detect_private_key import main diff --git a/tests/end_of_file_fixer_test.py b/tests/end_of_file_fixer_test.py index 60b9e82f..8a5d889e 100644 --- a/tests/end_of_file_fixer_test.py +++ b/tests/end_of_file_fixer_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import pytest diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index 15f11342..49b3b793 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.file_contents_sorter import FAIL @@ -8,7 +10,9 @@ @pytest.mark.parametrize( ('input_s', 'argv', 'expected_retval', 'output'), ( - (b'', [], FAIL, b'\n'), + (b'', [], PASS, b''), + (b'\n', [], FAIL, b''), + (b'\n\n', [], FAIL, b''), (b'lonesome\n', [], PASS, b'lonesome\n'), (b'missing_newline', [], FAIL, b'missing_newline\n'), (b'newline\nmissing', [], FAIL, b'missing\nnewline\n'), diff --git a/tests/fix_byte_order_marker_test.py b/tests/fix_byte_order_marker_test.py index da150e37..d7a65990 100644 --- a/tests/fix_byte_order_marker_test.py +++ b/tests/fix_byte_order_marker_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks import fix_byte_order_marker diff --git a/tests/fix_encoding_pragma_test.py b/tests/fix_encoding_pragma_test.py index f3afa094..98557e98 100644 --- a/tests/fix_encoding_pragma_test.py +++ b/tests/fix_encoding_pragma_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import pytest diff --git a/tests/forbid_new_submodules_test.py b/tests/forbid_new_submodules_test.py index 0326d941..058a3294 100644 --- a/tests/forbid_new_submodules_test.py +++ b/tests/forbid_new_submodules_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import subprocess from unittest import mock diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py index f1c26418..a7e79719 100644 --- a/tests/mixed_line_ending_test.py +++ b/tests/mixed_line_ending_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.mixed_line_ending import main diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py index 9fcb5808..eaae5e62 100644 --- a/tests/no_commit_to_branch_test.py +++ b/tests/no_commit_to_branch_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.no_commit_to_branch import is_on_branch diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py index 7fda23b3..68b6d7a1 100644 --- a/tests/pretty_format_json_test.py +++ b/tests/pretty_format_json_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil @@ -80,6 +82,24 @@ 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/readme_test.py b/tests/readme_test.py index 7df7fcfe..038868d8 100644 --- a/tests/readme_test.py +++ b/tests/readme_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks.check_yaml import yaml diff --git a/tests/removed_test.py b/tests/removed_test.py index d635eb1e..cd669578 100644 --- a/tests/removed_test.py +++ b/tests/removed_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.removed import main diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py index e3c6ed50..c0d2c65d 100644 --- a/tests/requirements_txt_fixer_test.py +++ b/tests/requirements_txt_fixer_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.requirements_txt_fixer import FAIL @@ -66,6 +68,12 @@ 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', @@ -74,6 +82,8 @@ ), (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/sort_simple_yaml_test.py b/tests/sort_simple_yaml_test.py index a682c158..6cbda857 100644 --- a/tests/sort_simple_yaml_test.py +++ b/tests/sort_simple_yaml_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pytest diff --git a/tests/string_fixer_test.py b/tests/string_fixer_test.py index 6ddb0ac8..8eb164c5 100644 --- a/tests/string_fixer_test.py +++ b/tests/string_fixer_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import textwrap import pytest @@ -35,6 +37,12 @@ 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 4df2963f..2b5a0dea 100644 --- a/tests/tests_should_end_in_test_test.py +++ b/tests/tests_should_end_in_test_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit_hooks.tests_should_end_in_test import main @@ -41,3 +43,8 @@ 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/tests/trailing_whitespace_fixer_test.py b/tests/trailing_whitespace_fixer_test.py index bb3b62d4..c07497a2 100644 --- a/tests/trailing_whitespace_fixer_test.py +++ b/tests/trailing_whitespace_fixer_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.trailing_whitespace_fixer import main diff --git a/tests/util_test.py b/tests/util_test.py index 7f488161..92473e59 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit_hooks.util import CalledProcessError diff --git a/tox.ini b/tox.ini index 965eba92..11340f4d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,pypy3,pre-commit +envlist = py,pre-commit [testenv] deps = -rrequirements-dev.txt @@ -11,7 +11,7 @@ setenv = commands = coverage erase coverage run -m pytest {posargs:tests} - coverage report --fail-under 100 + coverage report [testenv:pre-commit] skip_install = true