diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f1fafd2..9478a1fe 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: v4.1.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,34 +10,34 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + rev: v2.0.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.8.2 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.1 + rev: v2.2.3 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 + rev: v1.7.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.971 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 9e835d58..1a6056bd 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,6 +3,7 @@ description: prevents giant files from being committed. entry: check-added-large-files language: python + stages: [commit, push, manual] - id: check-ast name: check python ast description: simply checks whether the files parse as valid python. @@ -163,7 +164,7 @@ types: [text] - id: name-tests-test name: python tests naming - description: this verifies that test files are named correctly. + description: verifies that test files are named correctly. entry: name-tests-test language: python files: (^|/)tests/.+\.py$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e72022..d6e3171d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +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 63212d14..411529a0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,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: v4.3.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -57,6 +57,7 @@ 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. @@ -141,8 +142,10 @@ 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. diff --git a/pre_commit_hooks/check_docstring_first.py b/pre_commit_hooks/check_docstring_first.py index 1744919c..d55f08a5 100644 --- a/pre_commit_hooks/check_docstring_first.py +++ b/pre_commit_hooks/check_docstring_first.py @@ -28,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 diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py index 6b5402ac..d8e4f490 100644 --- a/pre_commit_hooks/check_executables_have_shebangs.py +++ b/pre_commit_hooks/check_executables_have_shebangs.py @@ -15,7 +15,10 @@ def check_executables(paths: list[str]) -> int: - if sys.platform == 'win32': # pragma: win32 cover + 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 diff --git a/pre_commit_hooks/check_merge_conflict.py b/pre_commit_hooks/check_merge_conflict.py index 22031a29..15ec284a 100644 --- a/pre_commit_hooks/check_merge_conflict.py +++ b/pre_commit_hooks/check_merge_conflict.py @@ -10,6 +10,7 @@ CONFLICT_PATTERNS = [ b'<<<<<<< ', b'======= ', + b'=======\r\n', b'=======\n', b'>>>>>>> ', ] @@ -39,12 +40,12 @@ def main(argv: Sequence[str] | None = 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 0f35650b..621696ce 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -34,6 +34,8 @@ 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, diff --git a/pre_commit_hooks/check_toml.py b/pre_commit_hooks/check_toml.py index 88f70865..0407371e 100644 --- a/pre_commit_hooks/check_toml.py +++ b/pre_commit_hooks/check_toml.py @@ -1,9 +1,13 @@ from __future__ import annotations import argparse +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: Sequence[str] | None = None) -> int: @@ -14,8 +18,9 @@ def main(argv: Sequence[str] | None = 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/debug_statement_hook.py b/pre_commit_hooks/debug_statement_hook.py index 00b67983..9ada657a 100644 --- a/pre_commit_hooks/debug_statement_hook.py +++ b/pre_commit_hooks/debug_statement_hook.py @@ -65,7 +65,7 @@ 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)) diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index e1ffe367..e7842af7 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -9,23 +9,42 @@ 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/setup.cfg b/setup.cfg index 5b0f7ca5..d4658e22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit_hooks -version = 4.1.0 +version = 4.3.0 description = Some out-of-the-box hooks for pre-commit. long_description = file: README.md long_description_content_type = text/markdown @@ -13,10 +13,6 @@ 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 @@ -24,7 +20,7 @@ classifiers = packages = find: install_requires = ruamel.yaml>=0.15 - toml + tomli>=1.1.0;python_version<"3.11" python_requires = >=3.7 [options.packages.find] diff --git a/tests/check_docstring_first_test.py b/tests/check_docstring_first_test.py index 079896f1..8bafae82 100644 --- a/tests/check_docstring_first_test.py +++ b/tests/check_docstring_first_test.py @@ -17,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 @@ -26,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 @@ -35,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_merge_conflict_test.py b/tests/check_merge_conflict_test.py index d3322db1..76c4283c 100644 --- a/tests/check_merge_conflict_test.py +++ b/tests/check_merge_conflict_test.py @@ -101,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( @@ -139,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(): @@ -148,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/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py index 349fe89c..5a8e0bb2 100644 --- a/tests/debug_statement_hook_test.py +++ b/tests/debug_statement_hook_test.py @@ -55,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/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py index dc3744b8..2b5a0dea 100644 --- a/tests/tests_should_end_in_test_test.py +++ b/tests/tests_should_end_in_test_test.py @@ -43,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