diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6f5511c..8b87a27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,12 +8,12 @@ on: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.7.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39"]' + env: '["py310"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.7.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39", "py310", "py311", "py312"]' + env: '["py310", "py311", "py312", "py313"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fedfab3..239aa5c 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: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,32 +10,32 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 + rev: v3.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.13.0 + rev: v3.16.0 hooks: - id: reorder-python-imports - args: [--py39-plus, --add-import, 'from __future__ import annotations'] + args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 + rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.21.2 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.3.1 + rev: v2.3.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.19.1 hooks: - id: mypy diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b71169b..275605e 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -12,9 +12,9 @@ language: python types: [python] - id: check-byte-order-marker - name: 'check BOM - deprecated: use fix-byte-order-marker' - description: forbids files which have a utf-8 byte-order marker. - entry: check-byte-order-marker + name: check-byte-order-marker (removed) + description: (removed) use fix-byte-order-marker instead. + entry: pre-commit-hooks-removed check-byte-order-marker fix-byte-order-marker https://github.com/pre-commit/pre-commit-hooks language: python types: [text] - id: check-builtin-literals @@ -29,7 +29,7 @@ entry: check-case-conflict language: python - id: check-docstring-first - name: check docstring is first + name: check docstring is first (deprecated) description: checks a common error of defining a docstring after code. entry: check-docstring-first language: python @@ -155,10 +155,10 @@ language: python types: [text] - id: fix-encoding-pragma - name: fix python encoding pragma (deprecated) - description: 'adds # -*- coding: utf-8 -*- to the top of python files.' + name: fix python encoding pragma (removed) + description: (removed) use pyupgrade instead. + entry: pre-commit-hooks-removed fix-encoding-pragma pyupgrade https://github.com/asottile/pyupgrade language: python - entry: fix-encoding-pragma types: [python] - id: forbid-new-submodules name: forbid new submodules diff --git a/CHANGELOG.md b/CHANGELOG.md index e165574..522925e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +6.0.0 - 2025-08-09 +================== + +## Fixes +- `check-shebang-scripts-are-executable`: improve error message. + - #1115 PR by @homebysix. + +## Migrating +- now requires python >= 3.9. + - #1098 PR by @asottile. +- `file-contents-sorter`: disallow `--unique` and `--ignore-case` at the same + time. + - #1095 PR by @nemacysts. + - #794 issue by @teksturi. +- Removed `check-byte-order-marker` and `fix-encoding-pragma`. + - `check-byte-order-marker`: migrate to `fix-byte-order-marker`. + - `fix-encoding-pragma`: migrate to `pyupgrade`. + - #1034 PR by @mxr. + - #1032 issue by @mxr. + - #522 PR by @jgowdy. + 5.0.0 - 2024-10-05 ================== diff --git a/README.md b/README.md index c0f678f..8432455 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Add this to your `.pre-commit-config.yaml` ```yaml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 # Use the ref you want to point at + rev: v6.0.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -45,9 +45,6 @@ Require literal syntax when initializing empty or zero Python builtin types. #### `check-case-conflict` Check for files with names that would conflict on a case-insensitive filesystem like MacOS HFS+ or Windows FAT. -#### `check-docstring-first` -Checks for a common error of placing code before the docstring. - #### `check-executables-have-shebangs` Checks that non-binary executables have a proper shebang. @@ -129,13 +126,6 @@ The following arguments are available: #### `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) - #### `forbid-new-submodules` Prevent addition of new git submodules. @@ -213,6 +203,9 @@ Trims trailing whitespace. ### Deprecated / replaced hooks - `check-byte-order-marker`: instead use fix-byte-order-marker +- `fix-encoding-pragma`: instead use [`pyupgrade`](https://github.com/asottile/pyupgrade) +- `check-docstring-first`: fundamentally flawed, deprecated without replacement. + ### As a standalone package diff --git a/pre_commit_hooks/check_builtin_literals.py b/pre_commit_hooks/check_builtin_literals.py index 16d59b5..e128eea 100644 --- a/pre_commit_hooks/check_builtin_literals.py +++ b/pre_commit_hooks/check_builtin_literals.py @@ -26,36 +26,37 @@ class Call(NamedTuple): class Visitor(ast.NodeVisitor): def __init__( self, - ignore: Sequence[str] | None = None, + ignore: set[str], allow_dict_kwargs: bool = True, ) -> None: self.builtin_type_calls: list[Call] = [] - self.ignore = set(ignore) if ignore else set() self.allow_dict_kwargs = allow_dict_kwargs + self._disallowed = BUILTIN_TYPES.keys() - ignore def _check_dict_call(self, node: ast.Call) -> bool: return self.allow_dict_kwargs and bool(node.keywords) def visit_Call(self, node: ast.Call) -> None: - if not isinstance(node.func, ast.Name): + if ( # Ignore functions that are object attributes (`foo.bar()`). # Assume that if the user calls `builtins.list()`, they know what # they're doing. - return - if node.func.id not in set(BUILTIN_TYPES).difference(self.ignore): - return - if node.func.id == 'dict' and self._check_dict_call(node): - return - elif node.args: - return - self.builtin_type_calls.append( - Call(node.func.id, node.lineno, node.col_offset), - ) + isinstance(node.func, ast.Name) and + node.func.id in self._disallowed and + (node.func.id != 'dict' or not self._check_dict_call(node)) and + not node.args + ): + self.builtin_type_calls.append( + Call(node.func.id, node.lineno, node.col_offset), + ) + + self.generic_visit(node) def check_file( filename: str, - ignore: Sequence[str] | None = None, + *, + ignore: set[str], allow_dict_kwargs: bool = True, ) -> list[Call]: with open(filename, 'rb') as f: diff --git a/pre_commit_hooks/check_byte_order_marker.py b/pre_commit_hooks/check_byte_order_marker.py deleted file mode 100644 index 4fba4b3..0000000 --- a/pre_commit_hooks/check_byte_order_marker.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import argparse -from collections.abc import Sequence - - -def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument('filenames', nargs='*', help='Filenames to check') - args = parser.parse_args(argv) - - retv = 0 - - for filename in args.filenames: - with open(filename, 'rb') as f: - if f.read(3) == b'\xef\xbb\xbf': - retv = 1 - print(f'{filename}: Has a byte-order marker') - - return retv - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/pre_commit_hooks/check_shebang_scripts_are_executable.py b/pre_commit_hooks/check_shebang_scripts_are_executable.py index aca0f77..937425b 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -36,7 +36,7 @@ def _message(path: str) -> None: 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' If it is not supposed to be executable, double-check its shebang ' f'is wanted.\n', file=sys.stderr, ) diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index bfbeb25..ee26d92 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -12,10 +12,10 @@ conflicts and keep the file nicely ordered. from __future__ import annotations import argparse +from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence from typing import Any -from typing import Callable from typing import IO PASS = 0 @@ -54,18 +54,21 @@ def sort_file_contents( def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='+', help='Files to sort') - parser.add_argument( + + mutex = parser.add_mutually_exclusive_group(required=False) + mutex.add_argument( '--ignore-case', action='store_const', const=bytes.lower, default=None, help='fold lower case to upper case characters', ) - parser.add_argument( + mutex.add_argument( '--unique', action='store_true', help='ensure each line is unique', ) + args = parser.parse_args(argv) retv = PASS diff --git a/pre_commit_hooks/fix_encoding_pragma.py b/pre_commit_hooks/fix_encoding_pragma.py deleted file mode 100644 index 5354104..0000000 --- a/pre_commit_hooks/fix_encoding_pragma.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -import argparse -import sys -from collections.abc import Sequence -from typing import IO -from typing import NamedTuple - -DEFAULT_PRAGMA = b'# -*- coding: utf-8 -*-' - - -def has_coding(line: bytes) -> bool: - if not line.strip(): - return False - return ( - line.lstrip()[:1] == b'#' and ( - b'unicode' in line or - b'encoding' in line or - b'coding:' in line or - b'coding=' in line - ) - ) - - -class ExpectedContents(NamedTuple): - shebang: bytes - rest: bytes - # True: has exactly the coding pragma expected - # False: missing coding pragma entirely - # None: has a coding pragma, but it does not match - pragma_status: bool | None - ending: bytes - - @property - def has_any_pragma(self) -> bool: - return self.pragma_status is not False - - def is_expected_pragma(self, remove: bool) -> bool: - expected_pragma_status = not remove - return self.pragma_status is expected_pragma_status - - -def _get_expected_contents( - first_line: bytes, - second_line: bytes, - rest: bytes, - expected_pragma: bytes, -) -> ExpectedContents: - ending = b'\r\n' if first_line.endswith(b'\r\n') else b'\n' - - if first_line.startswith(b'#!'): - shebang = first_line - potential_coding = second_line - else: - shebang = b'' - potential_coding = first_line - rest = second_line + rest - - if potential_coding.rstrip(b'\r\n') == expected_pragma: - pragma_status: bool | None = True - elif has_coding(potential_coding): - pragma_status = None - else: - pragma_status = False - rest = potential_coding + rest - - return ExpectedContents( - shebang=shebang, rest=rest, pragma_status=pragma_status, ending=ending, - ) - - -def fix_encoding_pragma( - f: IO[bytes], - remove: bool = False, - expected_pragma: bytes = DEFAULT_PRAGMA, -) -> int: - expected = _get_expected_contents( - f.readline(), f.readline(), f.read(), expected_pragma, - ) - - # Special cases for empty files - if not expected.rest.strip(): - # If a file only has a shebang or a coding pragma, remove it - if expected.has_any_pragma or expected.shebang: - f.seek(0) - f.truncate() - f.write(b'') - return 1 - else: - return 0 - - if expected.is_expected_pragma(remove): - return 0 - - # Otherwise, write out the new file - f.seek(0) - f.truncate() - f.write(expected.shebang) - if not remove: - f.write(expected_pragma + expected.ending) - f.write(expected.rest) - - return 1 - - -def _normalize_pragma(pragma: str) -> bytes: - return pragma.encode().rstrip() - - -def main(argv: Sequence[str] | None = None) -> int: - 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', - ) - parser.add_argument('filenames', nargs='*', help='Filenames to fix') - parser.add_argument( - '--pragma', default=DEFAULT_PRAGMA, type=_normalize_pragma, - help=( - f'The encoding pragma to use. ' - f'Default: {DEFAULT_PRAGMA.decode()}' - ), - ) - parser.add_argument( - '--remove', action='store_true', - help='Remove the encoding pragma (Useful in a python3-only codebase)', - ) - args = parser.parse_args(argv) - - retv = 0 - - if args.remove: - fmt = 'Removed encoding pragma from {filename}' - else: - fmt = 'Added `{pragma}` to {filename}' - - for filename in args.filenames: - with open(filename, 'r+b') as f: - file_ret = fix_encoding_pragma( - f, remove=args.remove, expected_pragma=args.pragma, - ) - retv |= file_ret - if file_ret: - print( - fmt.format(pragma=args.pragma.decode(), filename=filename), - ) - - return retv - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/pre_commit_hooks/forbid_articles_in_test_filenames.py b/pre_commit_hooks/forbid_articles_in_test_filenames.py index 1a800bc..0c8fd60 100755 --- a/pre_commit_hooks/forbid_articles_in_test_filenames.py +++ b/pre_commit_hooks/forbid_articles_in_test_filenames.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 +from __future__ import annotations import subprocess import sys from pathlib import Path -FORBIDDEN = {"a", "an", "the"} +FORBIDDEN = {'a', 'an', 'the'} def git_ls_python_files(): result = subprocess.run( - ["git", "ls-files", "*.py"], + ['git', 'ls-files', '*.py'], capture_output=True, text=True, check=True, @@ -20,14 +21,14 @@ def git_ls_python_files(): def is_test_file(path: Path) -> bool: name = path.name return ( - name.startswith("test_") - or name.startswith("tests_") - or name.endswith("_test.py") + name.startswith('test_') or + name.startswith('tests_') or + name.endswith('_test.py') ) def has_forbidden_article(path: Path) -> bool: - parts = path.stem.split("_") + parts = path.stem.split('_') return any(part in FORBIDDEN for part in parts) @@ -37,12 +38,12 @@ def main() -> int: continue if has_forbidden_article(path): - print("ERROR: Forbidden article in test filename:") + print('ERROR: Forbidden article in test filename:') print(path) return 1 return 0 -if __name__ == "__main__": +if __name__ == '__main__': sys.exit(main()) diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index 1f21938..9f3733f 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -1,8 +1,11 @@ from __future__ import annotations + import argparse import os.path import re from collections.abc import Sequence + + def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') diff --git a/setup.cfg b/setup.cfg index 3b7e5ee..d91f439 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit_hooks -version = 5.0.0 +version = 6.0.0 description = Some out-of-the-box hooks for pre-commit. long_description = file: README.md long_description_content_type = text/markdown @@ -10,7 +10,6 @@ author_email = asottile@umich.edu license = MIT license_files = LICENSE classifiers = - License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython @@ -21,7 +20,7 @@ packages = find: install_requires = ruamel.yaml>=0.15 tomli>=1.1.0;python_version<"3.11" -python_requires = >=3.9 +python_requires = >=3.10 [options.packages.find] exclude = @@ -33,7 +32,6 @@ console_scripts = check-added-large-files = pre_commit_hooks.check_added_large_files:main check-ast = pre_commit_hooks.check_ast:main check-builtin-literals = pre_commit_hooks.check_builtin_literals:main - check-byte-order-marker = pre_commit_hooks.check_byte_order_marker:main check-case-conflict = pre_commit_hooks.check_case_conflict:main check-docstring-first = pre_commit_hooks.check_docstring_first:main check-executables-have-shebangs = pre_commit_hooks.check_executables_have_shebangs:main @@ -53,7 +51,6 @@ console_scripts = end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:main file-contents-sorter = pre_commit_hooks.file_contents_sorter:main fix-byte-order-marker = pre_commit_hooks.fix_byte_order_marker:main - fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main mixed-line-ending = pre_commit_hooks.mixed_line_ending:main name-tests-test = pre_commit_hooks.tests_should_end_in_test:main diff --git a/tests/check_builtin_literals_test.py b/tests/check_builtin_literals_test.py index 1b18257..de29063 100644 --- a/tests/check_builtin_literals_test.py +++ b/tests/check_builtin_literals_test.py @@ -38,11 +38,6 @@ t1 = () ''' -@pytest.fixture -def visitor(): - return Visitor() - - @pytest.mark.parametrize( ('expression', 'calls'), [ @@ -85,7 +80,8 @@ def visitor(): ('builtins.tuple()', []), ], ) -def test_non_dict_exprs(visitor, expression, calls): +def test_non_dict_exprs(expression, calls): + visitor = Visitor(ignore=set()) visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls @@ -102,7 +98,8 @@ def test_non_dict_exprs(visitor, expression, calls): ('builtins.dict()', []), ], ) -def test_dict_allow_kwargs_exprs(visitor, expression, calls): +def test_dict_allow_kwargs_exprs(expression, calls): + visitor = Visitor(ignore=set()) visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls @@ -114,17 +111,18 @@ def test_dict_allow_kwargs_exprs(visitor, expression, calls): ('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]), ("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]), ('builtins.dict()', []), + pytest.param('f(dict())', [Call('dict', 1, 2)], id='nested'), ], ) def test_dict_no_allow_kwargs_exprs(expression, calls): - visitor = Visitor(allow_dict_kwargs=False) + visitor = Visitor(ignore=set(), allow_dict_kwargs=False) visitor.visit(ast.parse(expression)) assert visitor.builtin_type_calls == calls def test_ignore_constructors(): visitor = Visitor( - ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'), + ignore={'complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'}, ) visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS)) assert visitor.builtin_type_calls == [] diff --git a/tests/check_byte_order_marker_test.py b/tests/check_byte_order_marker_test.py deleted file mode 100644 index 909a39b..0000000 --- a/tests/check_byte_order_marker_test.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -from pre_commit_hooks import check_byte_order_marker - - -def test_failure(tmpdir): - f = tmpdir.join('f.txt') - f.write_text('ohai', encoding='utf-8-sig') - assert check_byte_order_marker.main((str(f),)) == 1 - - -def test_success(tmpdir): - f = tmpdir.join('f.txt') - f.write_text('ohai', encoding='utf-8') - assert check_byte_order_marker.main((str(f),)) == 0 diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py index 76c4283..64112d7 100644 --- a/tests/check_merge_conflict_test.py +++ b/tests/check_merge_conflict_test.py @@ -27,10 +27,10 @@ def f1_is_a_conflict_file(tmpdir): cmd_output('git', 'clone', str(repo1), str(repo2)) - # Commit in master + # Commit in mainline with repo1.as_cwd(): repo1_f1.write('parent\n') - git_commit('-am', 'master commit2') + git_commit('-am', 'mainline commit2') # Commit in clone and pull with repo2.as_cwd(): @@ -82,10 +82,10 @@ def repository_pending_merge(tmpdir): cmd_output('git', 'clone', str(repo1), str(repo2)) - # Commit in master + # Commit in mainline with repo1.as_cwd(): repo1_f1.write('parent\n') - git_commit('-am', 'master commit2') + git_commit('-am', 'mainline commit2') # Commit in clone and pull without committing with repo2.as_cwd(): @@ -112,7 +112,7 @@ def test_merge_conflicts_git(capsys): @pytest.mark.parametrize( - 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'), + 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> main\n'), ) def test_merge_conflicts_failing(contents, repository_pending_merge): repository_pending_merge.join('f2').write_binary(contents) @@ -150,7 +150,7 @@ def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys): cmd_output('git', 'worktree', 'add', str(worktree)) with worktree.as_cwd(): cmd_output( - 'git', 'pull', '--no-rebase', 'origin', 'master', retcode=None, + 'git', 'pull', '--no-rebase', 'origin', 'HEAD', retcode=None, ) msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG') assert msg.exists() diff --git a/tests/check_vcs_permalinks_test.py b/tests/check_vcs_permalinks_test.py index 01ce94d..324b70c 100644 --- a/tests/check_vcs_permalinks_test.py +++ b/tests/check_vcs_permalinks_test.py @@ -16,9 +16,9 @@ def test_passing(tmpdir): # tags are ok b'https://github.com/asottile/test/blob/1.0.0/foo%20bar#L1\n' # links to files but not line numbers are ok - b'https://github.com/asottile/test/blob/master/foo%20bar\n' + b'https://github.com/asottile/test/blob/main/foo%20bar\n' # regression test for overly-greedy regex - b'https://github.com/ yes / no ? /blob/master/foo#L1\n', + b'https://github.com/ yes / no ? /blob/main/foo#L1\n', ) assert not main((str(f),)) @@ -26,17 +26,15 @@ def test_passing(tmpdir): def test_failing(tmpdir, capsys): with tmpdir.as_cwd(): tmpdir.join('f.txt').write_binary( - b'https://github.com/asottile/test/blob/master/foo#L1\n' - b'https://example.com/asottile/test/blob/master/foo#L1\n' + b'https://github.com/asottile/test/blob/main/foo#L1\n' b'https://example.com/asottile/test/blob/main/foo#L1\n', ) assert main(('f.txt', '--additional-github-domain', 'example.com')) out, _ = capsys.readouterr() assert out == ( - 'f.txt:1:https://github.com/asottile/test/blob/master/foo#L1\n' - 'f.txt:2:https://example.com/asottile/test/blob/master/foo#L1\n' - 'f.txt:3:https://example.com/asottile/test/blob/main/foo#L1\n' + 'f.txt:1:https://github.com/asottile/test/blob/main/foo#L1\n' + 'f.txt:2:https://example.com/asottile/test/blob/main/foo#L1\n' '\n' 'Non-permanent github link detected.\n' 'On any page on github press [y] to load a permalink.\n' diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index 49b3b79..f178ae6 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -67,18 +67,6 @@ from pre_commit_hooks.file_contents_sorter import PASS FAIL, b'Fie\nFoe\nfee\nfum\n', ), - ( - b'fee\nFie\nFoe\nfum\n', - ['--unique', '--ignore-case'], - PASS, - b'fee\nFie\nFoe\nfum\n', - ), - ( - b'fee\nfee\nFie\nFoe\nfum\n', - ['--unique', '--ignore-case'], - FAIL, - b'fee\nFie\nFoe\nfum\n', - ), ), ) def test_integration(input_s, argv, expected_retval, output, tmpdir): @@ -89,3 +77,24 @@ def test_integration(input_s, argv, expected_retval, output, tmpdir): assert path.read_binary() == output assert output_retval == expected_retval + + +@pytest.mark.parametrize( + ('input_s', 'argv'), + ( + ( + b'fee\nFie\nFoe\nfum\n', + ['--unique', '--ignore-case'], + ), + ( + b'fee\nfee\nFie\nFoe\nfum\n', + ['--unique', '--ignore-case'], + ), + ), +) +def test_integration_invalid_args(input_s, argv, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) + + with pytest.raises(SystemExit): + main([str(path)] + argv) diff --git a/tests/fix_encoding_pragma_test.py b/tests/fix_encoding_pragma_test.py deleted file mode 100644 index 98557e9..0000000 --- a/tests/fix_encoding_pragma_test.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import io - -import pytest - -from pre_commit_hooks.fix_encoding_pragma import _normalize_pragma -from pre_commit_hooks.fix_encoding_pragma import fix_encoding_pragma -from pre_commit_hooks.fix_encoding_pragma import main - - -def test_integration_inserting_pragma(tmpdir): - path = tmpdir.join('foo.py') - path.write_binary(b'import httplib\n') - - assert main((str(path),)) == 1 - - assert path.read_binary() == ( - b'# -*- coding: utf-8 -*-\n' - b'import httplib\n' - ) - - -def test_integration_ok(tmpdir): - path = tmpdir.join('foo.py') - path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n') - assert main((str(path),)) == 0 - - -def test_integration_remove(tmpdir): - path = tmpdir.join('foo.py') - path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n') - - assert main((str(path), '--remove')) == 1 - - assert path.read_binary() == b'x = 1\n' - - -def test_integration_remove_ok(tmpdir): - path = tmpdir.join('foo.py') - path.write_binary(b'x = 1\n') - assert main((str(path), '--remove')) == 0 - - -@pytest.mark.parametrize( - 'input_str', - ( - b'', - ( - b'# -*- coding: utf-8 -*-\n' - b'x = 1\n' - ), - ( - b'#!/usr/bin/env python\n' - b'# -*- coding: utf-8 -*-\n' - b'foo = "bar"\n' - ), - ), -) -def test_ok_inputs(input_str): - bytesio = io.BytesIO(input_str) - assert fix_encoding_pragma(bytesio) == 0 - bytesio.seek(0) - assert bytesio.read() == input_str - - -@pytest.mark.parametrize( - ('input_str', 'output'), - ( - ( - b'import httplib\n', - b'# -*- coding: utf-8 -*-\n' - b'import httplib\n', - ), - ( - b'#!/usr/bin/env python\n' - b'x = 1\n', - b'#!/usr/bin/env python\n' - b'# -*- coding: utf-8 -*-\n' - b'x = 1\n', - ), - ( - b'#coding=utf-8\n' - b'x = 1\n', - b'# -*- coding: utf-8 -*-\n' - b'x = 1\n', - ), - ( - b'#!/usr/bin/env python\n' - b'#coding=utf8\n' - b'x = 1\n', - b'#!/usr/bin/env python\n' - b'# -*- coding: utf-8 -*-\n' - b'x = 1\n', - ), - # These should each get truncated - (b'#coding: utf-8\n', b''), - (b'# -*- coding: utf-8 -*-\n', b''), - (b'#!/usr/bin/env python\n', b''), - (b'#!/usr/bin/env python\n#coding: utf8\n', b''), - (b'#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n', b''), - ), -) -def test_not_ok_inputs(input_str, output): - bytesio = io.BytesIO(input_str) - assert fix_encoding_pragma(bytesio) == 1 - bytesio.seek(0) - assert bytesio.read() == output - - -def test_ok_input_alternate_pragma(): - input_s = b'# coding: utf-8\nx = 1\n' - bytesio = io.BytesIO(input_s) - ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8') - assert ret == 0 - bytesio.seek(0) - assert bytesio.read() == input_s - - -def test_not_ok_input_alternate_pragma(): - bytesio = io.BytesIO(b'x = 1\n') - ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8') - assert ret == 1 - bytesio.seek(0) - assert bytesio.read() == b'# coding: utf-8\nx = 1\n' - - -@pytest.mark.parametrize( - ('input_s', 'expected'), - ( - ('# coding: utf-8', b'# coding: utf-8'), - # trailing whitespace - ('# coding: utf-8\n', b'# coding: utf-8'), - ), -) -def test_normalize_pragma(input_s, expected): - assert _normalize_pragma(input_s) == expected - - -def test_integration_alternate_pragma(tmpdir, capsys): - f = tmpdir.join('f.py') - f.write('x = 1\n') - - pragma = '# coding: utf-8' - assert main((str(f), '--pragma', pragma)) == 1 - assert f.read() == '# coding: utf-8\nx = 1\n' - out, _ = capsys.readouterr() - assert out == f'Added `# coding: utf-8` to {str(f)}\n' - - -def test_crlf_ok(tmpdir): - f = tmpdir.join('f.py') - f.write_binary(b'# -*- coding: utf-8 -*-\r\nx = 1\r\n') - assert not main((str(f),)) - - -def test_crfl_adds(tmpdir): - f = tmpdir.join('f.py') - f.write_binary(b'x = 1\r\n') - assert main((str(f),)) - assert f.read_binary() == b'# -*- coding: utf-8 -*-\r\nx = 1\r\n' diff --git a/tests/forbid_articles_in_test_filenames_test.py b/tests/forbid_articles_in_test_filenames_test.py index 95a0b9d..fc4f02f 100644 --- a/tests/forbid_articles_in_test_filenames_test.py +++ b/tests/forbid_articles_in_test_filenames_test.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import subprocess import sys from pathlib import Path -HOOK = Path(__file__).parents[1] / "pre_commit_hooks" / "forbid_articles_in_test_filenames.py" +HOOK = Path(__file__).parents[1] / 'pre_commit_hooks' / 'forbid_articles_in_test_filenames.py' def run_hook(repo_path: Path): @@ -18,39 +20,39 @@ def run_hook(repo_path: Path): def init_git_repo(tmp_path: Path): - subprocess.run(["git", "init"], cwd=tmp_path, check=True) - subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True) - subprocess.run(["git", "config", "user.name", "Test User"], cwd=tmp_path, check=True) + subprocess.run(['git', 'init'], cwd=tmp_path, check=True) + subprocess.run(['git', 'config', 'user.email', 'test@example.com'], cwd=tmp_path, check=True) + subprocess.run(['git', 'config', 'user.name', 'Test User'], cwd=tmp_path, check=True) def git_add_all(tmp_path: Path): - subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) + subprocess.run(['git', 'add', '.'], cwd=tmp_path, check=True) def test_fails_on_forbidden_article_in_test_filename(tmp_path: Path): init_git_repo(tmp_path) - bad_test = tmp_path / "tests_create_an_address.py" - bad_test.write_text("def test_something(): pass\n") + bad_test = tmp_path / 'tests_create_an_address.py' + bad_test.write_text('def test_something(): pass\n') git_add_all(tmp_path) code, output = run_hook(tmp_path) assert code == 1 - assert "ERROR: Forbidden article in test filename:" in output - assert "tests_create_an_address.py" in output + assert 'ERROR: Forbidden article in test filename:' in output + assert 'tests_create_an_address.py' in output def test_passes_on_valid_test_filename(tmp_path: Path): init_git_repo(tmp_path) - good_test = tmp_path / "tests_create_address.py" - good_test.write_text("def test_something(): pass\n") + good_test = tmp_path / 'tests_create_address.py' + good_test.write_text('def test_something(): pass\n') git_add_all(tmp_path) code, output = run_hook(tmp_path) assert code == 0 - assert output == "" \ No newline at end of file + assert output == '' diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py index eaae5e6..7d37e49 100644 --- a/tests/no_commit_to_branch_test.py +++ b/tests/no_commit_to_branch_test.py @@ -11,13 +11,13 @@ from testing.util import git_commit def test_other_branch(temp_git_dir): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', 'anotherbranch') - assert is_on_branch({'master'}) is False + assert is_on_branch({'placeholder'}) is False def test_multi_branch(temp_git_dir): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', 'another/branch') - assert is_on_branch({'master'}) is False + assert is_on_branch({'placeholder'}) is False def test_multi_branch_fail(temp_git_dir): @@ -26,9 +26,10 @@ def test_multi_branch_fail(temp_git_dir): assert is_on_branch({'another/branch'}) is True -def test_master_branch(temp_git_dir): +def test_exact_branch(temp_git_dir): with temp_git_dir.as_cwd(): - assert is_on_branch({'master'}) is True + cmd_output('git', 'checkout', '-b', 'branchname') + assert is_on_branch({'branchname'}) is True def test_main_branch_call(temp_git_dir): @@ -50,11 +51,11 @@ def test_branch_pattern_fail(temp_git_dir): assert is_on_branch(set(), {'another/.*'}) is True -@pytest.mark.parametrize('branch_name', ('master', 'another/branch')) +@pytest.mark.parametrize('branch_name', ('somebranch', 'another/branch')) def test_branch_pattern_multiple_branches_fail(temp_git_dir, branch_name): with temp_git_dir.as_cwd(): cmd_output('git', 'checkout', '-b', branch_name) - assert main(('--branch', 'master', '--pattern', 'another/.*')) + assert main(('--branch', 'somebranch', '--pattern', 'another/.*')) def test_main_default_call(temp_git_dir):