diff options
Diffstat (limited to '')
116 files changed, 718 insertions, 410 deletions
@@ -1,9 +1,6 @@ *.egg-info *.py[co] /.coverage -/.mypy_cache -/.pytest_cache /.tox /dist -/venv* .vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66b50a4..1b93cff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,52 +4,42 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-docstring-first - - id: check-json - id: check-yaml - id: debug-statements + - id: double-quote-string-fixer - id: name-tests-test - id: requirements-txt-fixer - - id: double-quote-string-fixer -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports==1.10.0] -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 - hooks: - - id: autopep8 -- repo: https://github.com/pre-commit/pre-commit - rev: v2.17.0 - hooks: - - id: validate_manifest -- repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.0 hooks: - - id: pyupgrade - args: [--py36-plus] + - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 + rev: v3.0.1 hooks: - id: reorder-python-imports - args: [--py3-plus] + exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) + args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 hooks: - - id: setup-cfg-fmt + - id: pyupgrade + args: [--py37-plus] +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.942 hooks: - id: mypy additional_dependencies: [types-all] exclude: ^testing/resources/ -- repo: meta - hooks: - - id: check-hooks-apply - - id: check-useless-excludes diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cccc6..cd31c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +2.18.1 - 2022-04-02 +=================== + +### Fixes +- Fix regression for `repo: local` hooks running `python<3.7` + - #2324 PR by @asottile. + +2.18.0 - 2022-04-02 +=================== + +### Features +- Keep `GIT_HTTP_PROXY_AUTHMETHOD` in git environ. + - #2272 PR by @VincentBerthier. + - #2271 issue by @VincentBerthier. +- Support both `cs` and `coursier` executables for coursier hooks. + - #2293 PR by @Holzhaus. +- Include more information in errors for `language_version` / + `additional_dependencies` for languages which do not support them. + - #2315 PR by @asottile. +- Have autoupdate preferentially pick tags which look like versions when + there are multiple equivalent tags. + - #2312 PR by @mblayman. + - #2311 issue by @mblayman. +- Upgrade `ruby-build`. + - #2319 PR by @jalessio. +- Add top level `default_install_hook_types` which will be installed when + `--hook-types` is not specified in `pre-commit install`. + - #2322 PR by @asottile. + +### Fixes +- Fix typo in help message for `--from-ref` and `--to-ref`. + - #2266 PR by @leetrout. +- Prioritize binary builds for R dependencies. + - #2277 PR by @lorenzwalthert. +- Fix handling of git worktrees. + - #2252 PR by @daschuer. +- Fix handling of `$R_HOME` for R hooks. + - #2301 PR by @jeff-m-sullivan. + - #2300 issue by @jeff-m-sullivan. +- Fix a rare race condition in change stashing. + - #2323 PR by @asottile. + - #2287 issue by @ian-h-chamberlain. + +### Updating +- Remove python3.6 support. Note that pre-commit still supports running hooks + written in older versions, but pre-commit itself requires python 3.7+. + - #2215 PR by @asottile. +- pre-commit has migrated from the `master` branch to `main`. + - #2302 PR by @asottile. + 2.17.0 - 2022-01-18 =================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76df437..adce08f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,7 @@ language, for example: here are the apis that should be implemented for a language -Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py) +Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py) #### `ENVIRONMENT_DIR` @@ -1,6 +1,6 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=main)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d8cbd11..afb2982 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,6 @@ trigger: branches: - include: [master, test-me-*] + include: [main, test-me-*] tags: include: ['*'] @@ -50,7 +50,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38, py39] + toxenvs: [py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 83bd93c..bda61ee 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.main import main diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 47ebd54..bf4e2e4 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import functools import logging @@ -5,8 +7,6 @@ import re import shlex import sys from typing import Any -from typing import Dict -from typing import Optional from typing import Sequence import cfgv @@ -95,7 +95,7 @@ load_manifest = functools.partial( ) -def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_manifest_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) @@ -116,7 +116,7 @@ META = 'meta' # should inherit from cfgv.Conditional if sha support is dropped class WarnMutableRev(cfgv.ConditionalOptional): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if self.key in dct: @@ -135,7 +135,7 @@ class WarnMutableRev(cfgv.ConditionalOptional): class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -154,7 +154,7 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -183,7 +183,7 @@ class MigrateShaToRev: ensure_absent=True, ) - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) @@ -194,7 +194,7 @@ class MigrateShaToRev: else: self._cond('rev').check(dct) - def apply_default(self, dct: Dict[str, Any]) -> None: + def apply_default(self, dct: dict[str, Any]) -> None: if 'sha' in dct: dct['rev'] = dct.pop('sha') @@ -212,7 +212,7 @@ def _entry(modname: str) -> str: def warn_unknown_keys_root( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') @@ -220,7 +220,7 @@ def warn_unknown_keys_root( def warn_unknown_keys_repo( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning( f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', @@ -253,7 +253,7 @@ _meta = ( class NotAllowed(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if self.key in dct: raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') @@ -336,6 +336,11 @@ CONFIG_SCHEMA = cfgv.Map( 'Config', None, cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.Optional( + 'default_install_hook_types', + cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + ['pre-commit'], + ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), @@ -355,6 +360,7 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.WarnAdditionalKeys( ( 'repos', + 'default_install_hook_types', 'default_language_version', 'default_stages', 'files', @@ -377,7 +383,7 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: +def ordered_load_normalize_legacy_config(contents: str) -> dict[str, Any]: data = yaml_load(contents) if isinstance(data, list): logger.warning( @@ -398,7 +404,7 @@ load_config = functools.partial( ) -def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_config_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) diff --git a/pre_commit/color.py b/pre_commit/color.py index 4ddfdf5..2d6f248 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os import sys diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5cb978e..d5352e5 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,12 +1,10 @@ +from __future__ import annotations + import os.path import re from typing import Any -from typing import Dict -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -29,13 +27,13 @@ from pre_commit.util import yaml_load class RevInfo(NamedTuple): repo: str rev: str - frozen: Optional[str] + frozen: str | None @classmethod - def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': + def from_config(cls, config: dict[str, Any]) -> RevInfo: return cls(config['repo'], config['rev'], None) - def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + def update(self, tags_only: bool, freeze: bool) -> RevInfo: git_cmd = ('git', *git.NO_FS_MONITOR) if tags_only: @@ -61,6 +59,9 @@ class RevInfo(NamedTuple): except CalledProcessError: cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() + else: + if tags_only: + rev = git.get_best_candidate_tag(rev, tmp) frozen = None if freeze: @@ -76,7 +77,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): def _check_hooks_still_exist_at_rev( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], info: RevInfo, store: Store, ) -> None: @@ -101,9 +102,9 @@ REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') def _original_lines( path: str, - rev_infos: List[Optional[RevInfo]], + rev_infos: list[RevInfo | None], retry: bool = False, -) -> Tuple[List[str], List[int]]: +) -> tuple[list[str], list[int]]: """detect `rev:` lines or reformat the file""" with open(path, newline='') as f: original = f.read() @@ -120,7 +121,7 @@ def _original_lines( return _original_lines(path, rev_infos, retry=True) -def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: +def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: lines, idxs = _original_lines(path, rev_infos) for idx, rev_info in zip(idxs, rev_infos): @@ -152,7 +153,7 @@ def autoupdate( """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - rev_infos: List[Optional[RevInfo]] = [] + rev_infos: list[RevInfo | None] = [] changed = False config = load_config(config_file) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 2be6c16..5119f64 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from pre_commit import output diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 7f6d311..6892e09 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,8 +1,7 @@ +from __future__ import annotations + import os.path from typing import Any -from typing import Dict -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit import output @@ -17,9 +16,9 @@ from pre_commit.store import Store def _mark_used_repos( store: Store, - all_repos: Dict[Tuple[str, str], str], - unused_repos: Set[Tuple[str, str]], - repo: Dict[str, Any], + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], ) -> None: if repo['repo'] == META: return diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 90bb33b..18e5e9f 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import argparse import os.path import subprocess import sys -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.commands.run import run from pre_commit.envcontext import envcontext @@ -18,7 +18,7 @@ def _run_legacy( hook_type: str, hook_dir: str, args: Sequence[str], -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): raise SystemExit( f"bug: pre-commit's script is installed in migration mode\n" @@ -69,16 +69,16 @@ def _ns( color: bool, *, all_files: bool = False, - remote_branch: Optional[str] = None, - local_branch: Optional[str] = None, - from_ref: Optional[str] = None, - to_ref: Optional[str] = None, - remote_name: Optional[str] = None, - remote_url: Optional[str] = None, - commit_msg_filename: Optional[str] = None, - checkout_type: Optional[str] = None, - is_squash_merge: Optional[str] = None, - rewrite_command: Optional[str] = None, + remote_branch: str | None = None, + local_branch: str | None = None, + from_ref: str | None = None, + to_ref: str | None = None, + remote_name: str | None = None, + remote_url: str | None = None, + commit_msg_filename: str | None = None, + checkout_type: str | None = None, + is_squash_merge: str | None = None, + rewrite_command: str | None = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -109,7 +109,7 @@ def _pre_push_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: remote_name = args[0] remote_url = args[1] @@ -197,7 +197,7 @@ def _run_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 5f17d9c..08af656 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import logging import os.path -from typing import Sequence from pre_commit.commands.install_uninstall import install from pre_commit.store import Store @@ -14,7 +15,7 @@ def init_templatedir( config_file: str, store: Store, directory: str, - hook_types: Sequence[str], + hook_types: list[str] | None, skip_on_missing_config: bool = True, ) -> int: install( diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 50c6443..5ff6cba 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,14 +1,14 @@ +from __future__ import annotations + import logging import os.path import shlex import shutil import sys -from typing import Optional -from typing import Sequence -from typing import Tuple from pre_commit import git from pre_commit import output +from pre_commit.clientlib import InvalidConfigError from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs @@ -32,11 +32,23 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' +def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]: + if hook_types is not None: + return hook_types + else: + try: + cfg = load_config(cfg_filename) + except InvalidConfigError: + return ['pre-commit'] + else: + return cfg['default_install_hook_types'] + + def _hook_paths( hook_type: str, - git_dir: Optional[str] = None, -) -> Tuple[str, str]: - git_dir = git_dir if git_dir is not None else git.get_git_dir() + git_dir: str | None = None, +) -> tuple[str, str]: + git_dir = git_dir if git_dir is not None else git.get_git_common_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' @@ -54,7 +66,7 @@ def _install_hook_script( hook_type: str, overwrite: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) @@ -103,11 +115,11 @@ def _install_hook_script( def install( config_file: str, store: Store, - hook_types: Sequence[str], + hook_types: list[str] | None, overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> int: if git_dir is None and git.has_core_hookpaths_set(): logger.error( @@ -116,7 +128,7 @@ def install( ) return 1 - for hook_type in hook_types: + for hook_type in _hook_types(config_file, hook_types): _install_hook_script( config_file, hook_type, overwrite=overwrite, @@ -150,7 +162,7 @@ def _uninstall_hook_script(hook_type: str) -> None: output.write_line(f'Restored previous hooks to {hook_path}') -def uninstall(hook_types: Sequence[str]) -> int: - for hook_type in hook_types: +def uninstall(config_file: str, hook_types: list[str] | None) -> int: + for hook_type in _hook_types(config_file, hook_types): _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index fef14cd..c3d0a50 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import textwrap diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f8ced0f..37f989b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import contextlib import functools @@ -9,12 +11,8 @@ import time import unicodedata from typing import Any from typing import Collection -from typing import Dict -from typing import List from typing import MutableMapping from typing import Sequence -from typing import Set -from typing import Tuple from identify.identify import tags_from_path @@ -62,7 +60,7 @@ def filter_by_include_exclude( names: Collection[str], include: str, exclude: str, -) -> List[str]: +) -> list[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return [ filename for filename in names @@ -76,7 +74,7 @@ class Classifier: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) - def _types_for_file(self, filename: str) -> Set[str]: + def _types_for_file(self, filename: str) -> set[str]: return tags_from_path(filename) def by_types( @@ -85,7 +83,7 @@ class Classifier: types: Collection[str], types_or: Collection[str], exclude_types: Collection[str], - ) -> List[str]: + ) -> list[str]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) @@ -100,7 +98,7 @@ class Classifier: ret.append(filename) return ret - def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: + def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) names = self.by_types( @@ -117,7 +115,7 @@ class Classifier: filenames: Collection[str], include: str, exclude: str, - ) -> 'Classifier': + ) -> Classifier: # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex # this also makes improperly quoted shell-based hooks work better @@ -128,7 +126,7 @@ class Classifier: return Classifier(filenames) -def _get_skips(environ: MutableMapping[str, str]) -> Set[str]: +def _get_skips(environ: MutableMapping[str, str]) -> set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -144,12 +142,12 @@ def _subtle_line(s: str, use_color: bool) -> None: def _run_single_hook( classifier: Classifier, hook: Hook, - skips: Set[str], + skips: set[str], cols: int, diff_before: bytes, verbose: bool, use_color: bool, -) -> Tuple[bool, bytes]: +) -> tuple[bool, bytes]: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -271,9 +269,9 @@ def _get_diff() -> bytes: def _run_hooks( - config: Dict[str, Any], + config: dict[str, Any], hooks: Sequence[Hook], - skips: Set[str], + skips: set[str], args: argparse.Namespace, ) -> int: """Actually run the hooks.""" diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 64617c3..82a1617 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -2,6 +2,7 @@ # determine the latest revision? This adds ~200ms from my tests (and is # significantly faster than https:// or http://). For now, periodically # manually updating the revision is fine. +from __future__ import annotations SAMPLE_CONFIG = '''\ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4aee209..ef099f5 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import logging import os.path -from typing import Optional -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -18,7 +18,7 @@ from pre_commit.xargs import xargs logger = logging.getLogger(__name__) -def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]: +def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: # if `ref` is explicitly passed, use it if ref is not None: return repo, ref diff --git a/pre_commit/constants.py b/pre_commit/constants.py index d2f9363..5bc4ae9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 8): # pragma: >=3.8 cover @@ -22,4 +24,10 @@ STAGES = ( 'post-rewrite', ) +HOOK_TYPES = ( + 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', + 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', +) + DEFAULT = 'default' diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 92d975d..4f59560 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import enum import os from typing import Generator from typing import MutableMapping from typing import NamedTuple -from typing import Optional from typing import Tuple from typing import Union @@ -32,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: @contextlib.contextmanager def envcontext( patch: PatchesT, - _env: Optional[MutableMapping[str, str]] = None, + _env: MutableMapping[str, str] | None = None, ) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7e74b95..992f5cd 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -1,9 +1,12 @@ +from __future__ import annotations + import contextlib import functools import os.path import sys import traceback from typing import Generator +from typing import IO import pre_commit.constants as C from pre_commit import output @@ -30,7 +33,7 @@ def _log_and_exit( with contextlib.ExitStack() as ctx: if os.access(storedir, os.W_OK): output.write_line(f'Check the log at {log_path}') - log = ctx.enter_context(open(log_path, 'wb')) + log: IO[bytes] = ctx.enter_context(open(log_path, 'wb')) else: # pragma: win32 no cover output.write_line(f'Failed to write to log at {log_path}') log = sys.stdout.buffer diff --git a/pre_commit/errors.py b/pre_commit/errors.py index f84d3f1..eac34fa 100644 --- a/pre_commit/errors.py +++ b/pre_commit/errors.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class FatalError(RuntimeError): pass diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 55a8eb2..f67a586 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import errno import sys @@ -20,13 +22,11 @@ if sys.platform == 'win32': # pragma: no cover (windows) blocked_cb: Callable[[], None], ) -> Generator[None, None, None]: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: blocked_cb() while True: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK @@ -45,7 +45,6 @@ if sys.platform == 'win32': # pragma: no cover (windows) # The documentation however states: # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) else: # pragma: win32 no cover import fcntl diff --git a/pre_commit/git.py b/pre_commit/git.py index e9ec601..35392b3 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,11 +1,9 @@ +from __future__ import annotations + import logging import os.path import sys -from typing import Dict -from typing import List from typing import MutableMapping -from typing import Optional -from typing import Set from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -18,7 +16,7 @@ logger = logging.getLogger(__name__) NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') -def zsplit(s: str) -> List[str]: +def zsplit(s: str) -> list[str]: s = s.strip('\0') if s: return s.split('\0') @@ -27,8 +25,8 @@ def zsplit(s: str) -> List[str]: def no_git_env( - _env: Optional[MutableMapping[str, str]] = None, -) -> Dict[str, str]: + _env: MutableMapping[str, str] | None = None, +) -> dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -45,6 +43,7 @@ def no_git_env( k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', + 'GIT_HTTP_PROXY_AUTHMETHOD', } } @@ -58,13 +57,15 @@ def get_root() -> str: root = os.path.abspath( cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), ) - git_dir = os.path.abspath(get_git_dir()) + inside_git_dir = cmd_output( + 'git', 'rev-parse', '--is-inside-git-dir', + )[1].strip() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - if os.path.samefile(root, git_dir): + if inside_git_dir != 'false': raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', @@ -73,15 +74,25 @@ def get_root() -> str: def get_git_dir(git_root: str = '.') -> str: - opts = ('--git-common-dir', '--git-dir') - _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) - for line, opt in zip(out.splitlines(), opts): - if line != opt: # pragma: no branch (git < 2.5) - return os.path.normpath(os.path.join(git_root, line)) + opt = '--git-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_dir = out.strip() + if git_dir != opt: + return os.path.normpath(os.path.join(git_root, git_dir)) else: raise AssertionError('unreachable: no git dir') +def get_git_common_dir(git_root: str = '.') -> str: + opt = '--git-common-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_common_dir = out.strip() + if git_common_dir != opt: + return os.path.normpath(os.path.join(git_root, git_common_dir)) + else: # pragma: no cover (git < 2.5) + return get_git_dir(git_root) + + def get_remote_url(git_root: str) -> str: _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) return out.strip() @@ -95,7 +106,7 @@ def is_in_merge_conflict() -> bool: ) -def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: +def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]: # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode() @@ -105,7 +116,7 @@ def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: ] -def get_conflicted_files() -> Set[str]: +def get_conflicted_files() -> set[str]: logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other @@ -126,7 +137,7 @@ def get_conflicted_files() -> Set[str]: return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(cwd: Optional[str] = None) -> List[str]: +def get_staged_files(cwd: str | None = None) -> list[str]: return zsplit( cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -137,7 +148,7 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: ) -def intent_to_add_files() -> List[str]: +def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( 'git', 'status', '--ignore-submodules', '--porcelain', '-z', ) @@ -153,11 +164,11 @@ def intent_to_add_files() -> List[str]: return intent_to_add -def get_all_files() -> List[str]: +def get_all_files() -> list[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(old: str, new: str) -> List[str]: +def get_changed_files(old: str, new: str) -> list[str]: diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') try: _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') @@ -230,3 +241,18 @@ def check_for_cygwin_mismatch() -> None: f' - python {exe_type[is_cygwin_python]}\n' f' - git {exe_type[is_cygwin_git]}\n', ) + + +def get_best_candidate_tag(rev: str, git_repo: str) -> str: + """Get the best tag candidate. + + Multiple tags can exist on a SHA. Sometimes a moving tag is attached + to a version tag. Try to pick the tag that looks like a version. + """ + tags = cmd_output( + 'git', *NO_FS_MONITOR, 'tag', '--points-at', rev, cwd=git_repo, + )[1].splitlines() + for tag in tags: + if '.' in tag: + return tag + return rev diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 82e99c5..202abb3 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import logging import shlex from typing import Any -from typing import Dict from typing import NamedTuple from typing import Sequence -from typing import Tuple from pre_commit.prefix import Prefix @@ -38,11 +38,11 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self) -> Tuple[str, ...]: + def cmd(self) -> tuple[str, ...]: return (*shlex.split(self.entry), *self.args) @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( self.prefix, self.language, @@ -51,7 +51,7 @@ class Hook(NamedTuple): ) @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook: # TODO: have cfgv do this (?) extra_keys = set(dct) - _KEYS if extra_keys: diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 0bcedd6..cfcbf68 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,8 +1,8 @@ +from __future__ import annotations + from typing import Callable from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda @@ -30,7 +30,7 @@ from pre_commit.prefix import Prefix class Language(NamedTuple): name: str # Use `None` for no installation / environment - ENVIRONMENT_DIR: Optional[str] + ENVIRONMENT_DIR: str | None # return a value to replace `'default` for `language_version` get_default_version: Callable[[], str] # return whether the environment is healthy (or should be rebuilt) @@ -38,7 +38,7 @@ class Language(NamedTuple): # install a repository for the given language and language_version install_environment: Callable[[Prefix, str, Sequence[str]], None] # execute a hook and return the exit code and output - run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' + run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] # TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 97e2f69..88ac53f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -86,7 +87,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we # can run them without which is much quicker and produces a better # output. diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 2841467..bb3e0b8 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,14 +1,16 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -26,6 +28,14 @@ def install_environment( helpers.assert_version_default('coursier', version) helpers.assert_no_additional_deps('coursier', additional_dependencies) + # Support both possible executable names (either "cs" or "coursier") + executable = find_executable('cs') or find_executable('coursier') + if executable is None: + raise AssertionError( + 'pre-commit requires system-installed "cs" or "coursier" ' + 'executables in the application search path', + ) + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) channel = prefix.path('.pre-commit-channel') with clean_path_on_failure(envdir): @@ -35,7 +45,7 @@ def install_environment( helpers.run_setup_cmd( prefix, ( - 'cs', + executable, 'install', '--default-channels=false', f'--channel={channel}', @@ -66,6 +76,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 16e7554..65135f8 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os.path import shutil import tempfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -76,7 +77,7 @@ def install_environment( with tempfile.TemporaryDirectory() as dep_tmp: dep, _, version = dep_s.partition(':') if version: - dep_cmd: Tuple[str, ...] = (dep, '--version', version) + dep_cmd: tuple[str, ...] = (dep, '--version', version) else: dep_cmd = (dep,) @@ -104,6 +105,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 644d8d2..af1860c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import hashlib import json import os from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.hook import Hook @@ -76,7 +77,7 @@ def build_docker_image( *, pull: bool, ) -> None: # pragma: win32 no cover - cmd: Tuple[str, ...] = ( + cmd: tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, @@ -105,14 +106,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover +def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover try: return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: return () -def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover +def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', @@ -129,7 +130,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 311d127..ccc1d67 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,6 +16,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 094d2f1..a16e7f0 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -84,6 +85,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index d2b02d2..4cb95af 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,7 +15,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: out = f'{hook.entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 10ebc62..759c268 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os.path import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -95,6 +96,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 276ce16..8080826 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,13 +1,12 @@ +from __future__ import annotations + import multiprocessing import os import random import re from typing import Any -from typing import List -from typing import Optional from typing import overload from typing import Sequence -from typing import Tuple from typing import TYPE_CHECKING import pre_commit.constants as C @@ -32,7 +31,7 @@ def exe_exists(exe: str) -> bool: homedir = os.path.expanduser('~') try: - common: Optional[str] = os.path.commonpath((found, homedir)) + common: str | None = os.path.commonpath((found, homedir)) except ValueError: # on windows, different drives raises ValueError common = None @@ -48,7 +47,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: +def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @@ -58,7 +57,7 @@ def environment_dir(d: None, language_version: str) -> None: ... def environment_dir(d: str, language_version: str) -> str: ... -def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: +def environment_dir(d: str | None, language_version: str) -> str | None: if d is None: return None else: @@ -68,7 +67,8 @@ def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: def assert_version_default(binary: str, version: str) -> None: if version != C.DEFAULT: raise AssertionError( - f'For now, pre-commit requires system-installed {binary}', + f'for now, pre-commit requires system-installed {binary} -- ' + f'you selected `language_version: {version}`', ) @@ -78,8 +78,9 @@ def assert_no_additional_deps( ) -> None: if additional_deps: raise AssertionError( - f'For now, pre-commit does not support ' - f'additional_dependencies for {lang}', + f'for now, pre-commit does not support ' + f'additional_dependencies for {lang} -- ' + f'you selected `additional_dependencies: {additional_deps}`', ) @@ -95,7 +96,7 @@ def no_install( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> 'NoReturn': +) -> NoReturn: raise AssertionError('This type is not installable') @@ -113,7 +114,7 @@ def target_concurrency(hook: Hook) -> int: return 1 -def _shuffled(seq: Sequence[str]) -> List[str]: +def _shuffled(seq: Sequence[str]) -> list[str]: """Deterministically shuffle""" fixed_random = random.Random() fixed_random.seed(FIXED_RANDOM_SEED, version=1) @@ -125,10 +126,10 @@ def _shuffled(seq: Sequence[str]) -> List[str]: def run_xargs( hook: Hook, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], file_args: Sequence[str], **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index f699937..38bdf54 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -85,6 +86,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8dc4e8b..b084e8f 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -122,6 +123,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index bbf5504..0eee258 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import shlex from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -62,6 +63,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index a713c3f..f2758c5 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import argparse import re import sys from typing import NamedTuple -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Tuple from pre_commit import output from pre_commit.hook import Hook @@ -90,12 +90,12 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args, color=color) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( 'grep-like finder using python regexes. Unlike grep, this tool ' diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index faa6029..668ba35 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,12 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys -from typing import Dict from typing import Generator -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -35,7 +34,7 @@ def _version_info(exe: str) -> str: return f'<<error retrieving version from {exe}>>' -def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: +def _read_pyvenv_cfg(filename: str) -> dict[str, str]: ret = {} with open(filename, encoding='UTF-8') as f: for line in f: @@ -65,7 +64,7 @@ def get_env_patch(venv: str) -> PatchesT: def _find_by_py_launcher( version: str, -) -> Optional[str]: # pragma: no cover (windows only) +) -> str | None: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') @@ -77,8 +76,8 @@ def _find_by_py_launcher( return None -def _find_by_sys_executable() -> Optional[str]: - def _norm(path: str) -> Optional[str]: +def _find_by_sys_executable() -> str | None: + def _norm(path: str) -> str | None: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if exe not in {'python', 'pythonw'} and find_executable(exe): @@ -133,7 +132,7 @@ def _sys_executable_matches(version: str) -> bool: return sys.version_info[:len(info)] == info -def norm_version(version: str) -> Optional[str]: +def norm_version(version: str) -> str | None: if version == C.DEFAULT: # use virtualenv's default return None elif _sys_executable_matches(version): # virtualenv defaults to our exe @@ -209,6 +208,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e034e39..c736b38 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os import shlex import shutil from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -58,7 +59,11 @@ def _prefix_if_non_local_file_entry( def _rscript_exec() -> str: - return os.path.join(os.getenv('R_HOME', ''), 'Rscript') + r_home = os.environ.get('R_HOME') + if r_home is None: + return 'Rscript' + else: + return os.path.join(r_home, 'bin', 'Rscript') def _entry_validate(entry: Sequence[str]) -> None: @@ -80,7 +85,7 @@ def _entry_validate(entry: Sequence[str]) -> None: ) -def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: +def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: entry = shlex.split(hook.entry) _entry_validate(entry) @@ -102,9 +107,7 @@ def install_environment( shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - f"""\ + r_code_inst_environment = f"""\ prefix_dir <- {prefix.prefix_dir!r} options( repos = c(CRAN = "https://cran.rstudio.com"), @@ -131,24 +134,41 @@ def install_environment( if (is_package) {{ renv::install(prefix_dir) }} - """, + """ + + cmd_output_b( + _rscript_exec(), '--vanilla', '-e', + _inline_r_setup(r_code_inst_environment), cwd=env_dir, ) if additional_dependencies: + r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' with in_env(prefix, version): cmd_output_b( _rscript_exec(), *RSCRIPT_OPTS, '-e', - 'renv::install(commandArgs(trailingOnly = TRUE))', + _inline_r_setup(r_code_inst_add), *additional_dependencies, cwd=env_dir, ) +def _inline_r_setup(code: str) -> str: + """ + Some behaviour of R cannot be configured via env variables, but can + only be configured via R options once R has started. These are set here. + """ + with_option = f"""\ + options(install.packages.compile.from.source = "never") + {code} + """ + return with_option + + def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs( hook, _cmd_from_hook(hook), file_args, color=color, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 81bc954..ae64492 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import functools import os.path @@ -5,7 +7,6 @@ import shutil import tarfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -146,6 +147,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7ea3f54..39e3628 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Set -from typing import Tuple import toml @@ -39,7 +39,7 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: def _add_dependencies( cargo_toml_path: str, - additional_dependencies: Set[str], + additional_dependencies: set[str], ) -> None: with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) @@ -81,7 +81,7 @@ def install_environment( _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} + packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') @@ -101,6 +101,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index a5e1365..2844b5e 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,6 +15,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 66aadc8..c630953 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -59,6 +60,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 139f45d..9846c98 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,5 +16,5 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index ba05295..1b68fc7 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging from typing import Generator diff --git a/pre_commit/main.py b/pre_commit/main.py index f1e8d03..645e97f 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import argparse import logging import os import sys -from typing import Any -from typing import Optional from typing import Sequence -from typing import Union import pre_commit.constants as C from pre_commit import git @@ -46,34 +45,10 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: ) -class AppendReplaceDefault(argparse.Action): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.appended = False - - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Union[str, Sequence[str], None], - option_string: Optional[str] = None, - ) -> None: - if not self.appended: - setattr(namespace, self.dest, []) - self.appended = True - getattr(namespace, self.dest).append(values) - - def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( - '-t', '--hook-type', choices=( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', - ), - action=AppendReplaceDefault, - default=['pre-commit'], - dest='hook_types', + '-t', '--hook-type', + choices=C.HOOK_TYPES, action='append', dest='hook_types', ) @@ -106,7 +81,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--from-ref', '--source', '-s', help=( - '(for usage with `--from-ref`) -- this option represents the ' + '(for usage with `--to-ref`) -- this option represents the ' 'original ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch you are pushing ' 'to. ' @@ -117,7 +92,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--to-ref', '--origin', '-o', help=( - '(for usage with `--to-ref`) -- this option represents the ' + '(for usage with `--from-ref`) -- this option represents the ' 'destination ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch being pushed. ' 'For `post-checkout` hooks, this represents the branch that is ' @@ -175,7 +150,7 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.repo = os.path.relpath(args.repo) -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:] parser = argparse.ArgumentParser(prog='pre-commit') @@ -197,7 +172,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', help=( - 'Update to the bleeding edge of `master` instead of the latest ' + 'Update to the bleeding edge of `HEAD` instead of the latest ' 'tagged version (the default behavior).' ), ) @@ -399,7 +374,10 @@ def main(argv: Optional[Sequence[str]] = None) -> int: elif args.command == 'try-repo': return try_repo(args) elif args.command == 'uninstall': - return uninstall(hook_types=args.hook_types) + return uninstall( + config_file=args.config, + hook_types=args.hook_types, + ) else: raise NotImplementedError( f'Command {args.command} not implemented.', diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a6eb0e0..b05a705 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence import pre_commit.constants as C @@ -27,7 +28,7 @@ def check_all_hooks_match_files(config_file: 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='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 60870f8..0a8249b 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import argparse import re -from typing import Optional from typing import Sequence from cfgv import apply_defaults @@ -65,7 +66,7 @@ def check_useless_excludes(config_file: 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='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 12eb02f..72ee440 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,11 +1,12 @@ +from __future__ import annotations + import sys -from typing import Optional from typing import Sequence from pre_commit import output -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:] for arg in argv: output.write_line(arg) diff --git a/pre_commit/output.py b/pre_commit/output.py index 24f9d84..4bcf27f 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import sys from typing import Any from typing import IO -from typing import Optional def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: @@ -11,9 +12,9 @@ def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: def write_line_b( - s: Optional[bytes] = None, + s: bytes | None = None, stream: IO[bytes] = sys.stdout.buffer, - logfile_name: Optional[str] = None, + logfile_name: str | None = None, ) -> None: with contextlib.ExitStack() as exit_stack: output_streams = [stream] @@ -28,5 +29,5 @@ def write_line_b( output_stream.flush() -def write_line(s: Optional[str] = None, **kwargs: Any) -> None: +def write_line(s: str | None = None, **kwargs: Any) -> None: write_line_b(s.encode() if s is not None else s, **kwargs) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index d344a1d..3fd3129 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,7 +1,7 @@ +from __future__ import annotations + import os.path from typing import Mapping -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING from identify.identify import parse_shebang_from_file @@ -11,11 +11,11 @@ if TYPE_CHECKING: class ExecutableNotFoundError(OSError): - def to_output(self) -> Tuple[int, bytes, None]: + def to_output(self) -> tuple[int, bytes, None]: return (1, self.args[0].encode(), None) -def parse_filename(filename: str) -> Tuple[str, ...]: +def parse_filename(filename: str) -> tuple[str, ...]: if not os.path.exists(filename): return () else: @@ -23,8 +23,8 @@ def parse_filename(filename: str) -> Tuple[str, ...]: def find_executable( - exe: str, _environ: Optional[Mapping[str, str]] = None, -) -> Optional[str]: + exe: str, _environ: Mapping[str, str] | None = None, +) -> str | None: exe = os.path.normpath(exe) if os.sep in exe: return exe @@ -47,7 +47,7 @@ def find_executable( def normexe(orig: str) -> str: - def _error(msg: str) -> 'NoReturn': + def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): @@ -65,7 +65,7 @@ def normexe(orig: str) -> str: return orig -def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]: +def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 0e3ebbd..f1b28c1 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os.path from typing import NamedTuple -from typing import Tuple class Prefix(NamedTuple): @@ -12,6 +13,6 @@ class Prefix(NamedTuple): def exists(self, *parts: str) -> bool: return os.path.exists(self.path(*parts)) - def star(self, end: str) -> Tuple[str, ...]: + def star(self, end: str) -> tuple[str, ...]: paths = os.listdir(self.prefix_dir) return tuple(path for path in paths if path.endswith(end)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 15827dd..ac5d294 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,13 +1,10 @@ +from __future__ import annotations + import json import logging import os from typing import Any -from typing import Dict -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit.clientlib import load_manifest @@ -33,7 +30,7 @@ def _state_filename(prefix: Prefix, venv: str) -> str: return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') -def _read_state(prefix: Prefix, venv: str) -> Optional[object]: +def _read_state(prefix: Prefix, venv: str) -> object | None: filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None @@ -93,9 +90,9 @@ def _hook_install(hook: Hook) -> None: def _hook( - *hook_dicts: Dict[str, Any], - root_config: Dict[str, Any], -) -> Dict[str, Any]: + *hook_dicts: dict[str, Any], + root_config: dict[str, Any], +) -> dict[str, Any]: ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -140,10 +137,10 @@ def _hook( def _non_cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: language = languages[language_name] # pygrep / script / system / docker_image do not have @@ -164,10 +161,10 @@ def _non_cloned_repository_hooks( def _cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -196,10 +193,10 @@ def _cloned_repository_hooks( def _repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: if repo_config['repo'] in {LOCAL, META}: return _non_cloned_repository_hooks(repo_config, store, root_config) else: @@ -207,8 +204,8 @@ def _repository_hooks( def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: - def _need_installed() -> List[Hook]: - seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() + def _need_installed() -> list[Hook]: + seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not _hook_installed(hook): @@ -224,7 +221,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: _hook_install(hook) -def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: +def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]: return tuple( hook for repo in root_config['repos'] diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz Binary files differindex 01867be..e248c57 100644 --- a/pre_commit/resources/ruby-build.tar.gz +++ b/pre_commit/resources/ruby-build.tar.gz diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index bad004c..83d8a03 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path @@ -64,9 +66,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) yield finally: # Try to apply the patch we saved diff --git a/pre_commit/store.py b/pre_commit/store.py index 27d8553..effebfb 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path @@ -5,10 +7,7 @@ import sqlite3 import tempfile from typing import Callable from typing import Generator -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import file_lock @@ -40,7 +39,7 @@ def _get_default_directory() -> str: class Store: get_default_directory = staticmethod(_get_default_directory) - def __init__(self, directory: Optional[str] = None) -> None: + def __init__(self, directory: str | None = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') self.readonly = ( @@ -92,7 +91,7 @@ class Store: @contextlib.contextmanager def connect( self, - db_path: Optional[str] = None, + db_path: str | None = None, ) -> Generator[sqlite3.Connection, None, None]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< @@ -119,7 +118,7 @@ class Store: ) -> str: repo = self.db_repo_name(repo, deps) - def _get_result() -> Optional[str]: + def _get_result() -> str | None: # Check if we already exist with self.connect() as db: result = db.execute( @@ -239,18 +238,18 @@ class Store: self._create_config_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self) -> List[str]: + def select_all_configs(self) -> list[str]: with self.connect() as db: self._create_config_table(db) rows = db.execute('SELECT path FROM configs').fetchall() return [path for path, in rows] - def delete_configs(self, configs: List[str]) -> None: + def delete_configs(self, configs: list[str]) -> None: with self.connect() as db: rows = [(path,) for path in configs] db.executemany('DELETE FROM configs WHERE path = ?', rows) - def select_all_repos(self) -> List[Tuple[str, str, str]]: + def select_all_repos(self) -> list[tuple[str, str, str]]: with self.connect() as db: return db.execute('SELECT repo, ref, path from repos').fetchall() diff --git a/pre_commit/util.py b/pre_commit/util.py index 6977acb..40c53e5 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import contextlib import errno import functools +import importlib.resources import os.path import shutil import stat @@ -10,24 +13,13 @@ import tempfile from types import TracebackType from typing import Any from typing import Callable -from typing import Dict from typing import Generator from typing import IO -from typing import Optional -from typing import Tuple -from typing import Type import yaml from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: >=3.7 cover - from importlib.resources import open_binary - from importlib.resources import read_text -else: # pragma: <3.7 cover - from importlib_resources import open_binary - from importlib_resources import read_text - Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) @@ -73,11 +65,11 @@ def tmpdir() -> Generator[str, None, None]: def resource_bytesio(filename: str) -> IO[bytes]: - return open_binary('pre_commit.resources', filename) + return importlib.resources.open_binary('pre_commit.resources', filename) def resource_text(filename: str) -> str: - return read_text('pre_commit.resources', filename) + return importlib.resources.read_text('pre_commit.resources', filename) def make_executable(filename: str) -> None: @@ -90,10 +82,10 @@ class CalledProcessError(RuntimeError): def __init__( self, returncode: int, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], expected_returncode: int, stdout: bytes, - stderr: Optional[bytes], + stderr: bytes | None, ) -> None: super().__init__(returncode, cmd, expected_returncode, stdout, stderr) self.returncode = returncode @@ -103,7 +95,7 @@ class CalledProcessError(RuntimeError): self.stderr = stderr def __bytes__(self) -> bytes: - def _indent_or_none(part: Optional[bytes]) -> bytes: + def _indent_or_none(part: bytes | None) -> bytes: if part: return b'\n ' + part.replace(b'\n', b'\n ') else: @@ -121,20 +113,20 @@ class CalledProcessError(RuntimeError): return self.__bytes__().decode() -def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: +def _setdefault_kwargs(kwargs: dict[str, Any]) -> None: for arg in ('stdin', 'stdout', 'stderr'): kwargs.setdefault(arg, subprocess.PIPE) -def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]: +def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: return 1, force_bytes(e).rstrip(b'\n') + b'\n', None def cmd_output_b( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, -) -> Tuple[int, bytes, Optional[bytes]]: +) -> tuple[int, bytes, bytes | None]: _setdefault_kwargs(kwargs) try: @@ -156,7 +148,7 @@ def cmd_output_b( return returncode, stdout_b, stderr_b -def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: +def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) stdout = stdout_b.decode() if stdout_b is not None else None stderr = stderr_b.decode() if stderr_b is not None else None @@ -169,10 +161,10 @@ if os.name != 'nt': # pragma: win32 no cover class Pty: def __init__(self) -> None: - self.r: Optional[int] = None - self.w: Optional[int] = None + self.r: int | None = None + self.w: int | None = None - def __enter__(self) -> 'Pty': + def __enter__(self) -> Pty: self.r, self.w = openpty() # tty flags normally change \n to \r\n @@ -195,18 +187,18 @@ if os.name != 'nt': # pragma: win32 no cover def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.close_w() self.close_r() def cmd_output_p( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, - ) -> Tuple[int, bytes, Optional[bytes]]: + ) -> tuple[int, bytes, bytes | None]: assert retcode is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) @@ -250,7 +242,7 @@ def rmtree(path: str) -> None: def handle_remove_readonly( func: Callable[..., Any], path: str, - exc: Tuple[Type[OSError], OSError, TracebackType], + exc: tuple[type[OSError], OSError, TracebackType], ) -> None: excvalue = exc[1] if ( @@ -265,7 +257,7 @@ def rmtree(path: str) -> None: shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s: str) -> Tuple[int, ...]: +def parse_version(s: str) -> tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 6b0fa20..f2b3421 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import concurrent.futures import contextlib import math @@ -8,11 +10,8 @@ from typing import Any from typing import Callable from typing import Generator from typing import Iterable -from typing import List from typing import MutableMapping -from typing import Optional from typing import Sequence -from typing import Tuple from typing import TypeVar from pre_commit import parse_shebang @@ -23,7 +22,7 @@ TArg = TypeVar('TArg') TRet = TypeVar('TRet') -def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int: +def _environ_size(_env: MutableMapping[str, str] | None = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): @@ -62,8 +61,8 @@ def partition( cmd: Sequence[str], varargs: Sequence[str], target_concurrency: int, - _max_length: Optional[int] = None, -) -> Tuple[Tuple[str, ...], ...]: + _max_length: int | None = None, +) -> tuple[tuple[str, ...], ...]: _max_length = _max_length or _get_platform_max_length() # Generally, we try to partition evenly into at least `target_concurrency` @@ -73,7 +72,7 @@ def partition( cmd = tuple(cmd) ret = [] - ret_cmd: List[str] = [] + ret_cmd: list[str] = [] # Reversed so arguments are in order varargs = list(reversed(varargs)) @@ -115,14 +114,14 @@ def _thread_mapper(maxsize: int) -> Generator[ def xargs( - cmd: Tuple[str, ...], + cmd: tuple[str, ...], varargs: Sequence[str], *, color: bool = False, target_concurrency: int = 1, _max_length: int = _get_platform_max_length(), **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: """A simplified implementation of xargs. color: Make a pty if on a platform that supports it @@ -152,8 +151,8 @@ def xargs( partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( - run_cmd: Tuple[str, ...], - ) -> Tuple[int, bytes, Optional[bytes]]: + run_cmd: tuple[str, ...], + ) -> tuple[int, bytes, bytes | None]: return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.17.0 +version = 2.18.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -13,7 +13,6 @@ 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 @@ -31,8 +30,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources<5.3;python_version<"3.7" -python_requires = >=3.6.1 +python_requires = >=3.7 [options.packages.find] exclude = @@ -1,2 +1,4 @@ +from __future__ import annotations + from setuptools import setup setup() diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 0841094..d5a4377 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index f7def08..ef5a041 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 152cf3c..dfd92c0 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import sys LANGUAGES = [ diff --git a/testing/make-archives b/testing/make-archives index ce098ba..1b825fe 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import gzip import os.path @@ -6,7 +8,6 @@ import shutil import subprocess import tarfile import tempfile -from typing import Optional from typing import Sequence @@ -16,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '8663d2f'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', 'a5ca3e4'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', @@ -69,7 +70,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: return output_path -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 2c23700..964cf83 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,4 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3 files: \.py$ diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py index 9c4368e..40efde3 100644 --- a/testing/resources/python_hooks_repo/foo.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py index 0559271..cff6cad 100644 --- a/testing/resources/python_hooks_repo/setup.py +++ b/testing/resources/python_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py index 9c4368e..40efde3 100644 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py index 0559271..cff6cad 100644 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ b/testing/resources/python_venv_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/util.py b/testing/util.py index 283ed47..0dd1784 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import subprocess diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index e21d5fe..7c74c1b 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:focal RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -10,5 +10,5 @@ RUN : \ ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH RUN : \ - && python3.6 -mvenv /venv \ + && python3 -mvenv /venv \ && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade diff --git a/testing/zipapp/entry b/testing/zipapp/entry index 87f9291..15758d9 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import os.path import shutil import stat @@ -59,10 +61,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/testing/zipapp/make b/testing/zipapp/make index effc812..37b5c35 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import base64 import hashlib diff --git a/testing/zipapp/python b/testing/zipapp/python index 7184a1a..67910fc 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """A shim executable to put dependencies on sys.path""" +from __future__ import annotations + import argparse import os.path import runpy @@ -36,10 +38,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 39a3716..3fb3af5 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import re diff --git a/tests/color_test.py b/tests/color_test.py index 5cd226a..89b4fd3 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from unittest import mock diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7316eb9..3806b0e 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shlex from unittest import mock @@ -101,6 +103,24 @@ def test_rev_info_update_tags_only_does_not_pick_tip(tagged): assert new_info.rev == 'v1.2.3' +def test_rev_info_update_tags_prefers_version_tag(tagged, out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_non_version_tag(out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'latest' + + def test_rev_info_update_freeze_tag(tagged): git_commit(cwd=tagged.path) config = make_config_from_repo(tagged.path, rev=tagged.original_rev) diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 955a6bc..dd8e4a5 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 02b3694..c128e93 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pre_commit.constants as C diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 37b78bc..b0159f8 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import subprocess import sys from unittest import mock diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 4e131df..64bfc8b 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 0b2e248..ae668ac 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re @@ -5,6 +7,7 @@ import re_assert import pre_commit.constants as C from pre_commit import git +from pre_commit.commands.install_uninstall import _hook_types from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -25,6 +28,36 @@ from testing.util import cwd from testing.util import git_commit +def test_hook_types_explicitly_listed(): + assert _hook_types(os.devnull, ['pre-push']) == ['pre-push'] + + +def test_hook_types_default_value_when_not_specified(): + assert _hook_types(os.devnull, None) == ['pre-commit'] + + +def test_hook_types_configured(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: [pre-push]\nrepos: []\n') + + assert _hook_types(str(cfg), None) == ['pre-push'] + + +def test_hook_types_configured_nonsense(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: []\nrepos: []\n') + + # hopefully the user doesn't do this, but the code allows it! + assert _hook_types(str(cfg), None) == [] + + +def test_hook_types_configuration_has_error(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('[') + + assert _hook_types(str(cfg), None) == ['pre-commit'] + + def test_is_not_script(): assert is_our_script('setup.py') is False @@ -59,7 +92,7 @@ def test_install_multiple_hooks_at_once(in_git_dir, store): install(C.CONFIG_FILE, store, hook_types=['pre-commit', 'pre-push']) assert in_git_dir.join('.git/hooks/pre-commit').exists() assert in_git_dir.join('.git/hooks/pre-push').exists() - uninstall(hook_types=['pre-commit', 'pre-push']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit', 'pre-push']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() assert not in_git_dir.join('.git/hooks/pre-push').exists() @@ -77,14 +110,14 @@ def test_install_hooks_dead_symlink(in_git_dir, store): def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 def test_uninstall(in_git_dir, store): assert not in_git_dir.join('.git/hooks/pre-commit').exists() install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() - uninstall(hook_types=['pre-commit']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() @@ -414,7 +447,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): # Now install and uninstall pre-commit assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -449,7 +482,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): pre_commit.write('#!/usr/bin/env bash\necho 1\n') make_executable(pre_commit.strpath) - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 assert pre_commit.exists() @@ -1005,3 +1038,16 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): 'Skipping `pre-commit`.' ) assert expected in output + + +def test_install_uninstall_default_hook_types(in_git_dir, store): + cfg_src = 'default_install_hook_types: [pre-commit, pre-push]\nrepos: []\n' + in_git_dir.join(C.CONFIG_FILE).write(cfg_src) + + assert not install(C.CONFIG_FILE, store, hook_types=None) + assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) + assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) + + assert not uninstall(C.CONFIG_FILE, hook_types=None) + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + assert not in_git_dir.join('.git/hooks/pre-push').exists() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5eddd3..b80244e 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pre_commit.constants as C from pre_commit.commands.migrate_config import migrate_config diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3a6fa2a..085b063 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import shlex import sys diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 8e3a904..cf56e98 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.commands.sample_config import sample_config diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index a157d16..0b2db7e 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re import time diff --git a/tests/conftest.py b/tests/conftest.py index f38f969..b68a1d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import io import logging diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index f9d4dce..c82d326 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from unittest import mock diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index cb76dcf..31c71d2 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import sys diff --git a/tests/git_test.py b/tests/git_test.py index bcb3fd1..b9f524a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest @@ -19,6 +21,20 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected +def test_get_root_in_git_sub_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('.git/objects').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + +def test_get_root_not_in_working_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('..').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + def test_in_exactly_dot_git(in_git_dir): with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): git.get_root() @@ -38,6 +54,22 @@ def test_get_root_bare_worktree(tmpdir): assert git.get_root() == os.path.abspath('.') +def test_get_git_dir(tmpdir): + """Regression test for #1972""" + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + worktree = tmpdir.join('worktree').ensure_dir() + cmd_output('git', 'worktree', 'add', '../worktree', cwd=src) + + with worktree.as_cwd(): + assert git.get_git_dir() == src.ensure_dir( + '.git/worktrees/worktree', + ) + assert git.get_git_common_dir() == src.ensure_dir('.git') + + def test_get_root_worktree_in_git(tmpdir): src = tmpdir.join('src').ensure_dir() cmd_output('git', 'init', str(src)) diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 6faa78f..5023b2a 100644 --- a/tests/languages/conda_test.py +++ b/tests/languages/conda_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit import envcontext diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index ec6bb83..5838761 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import builtins import json import ntpath diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 9a64ed1..9e393cb 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages.golang import guess_go_dir diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index fd9b9a4..259cb97 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import multiprocessing import os.path import sys @@ -86,7 +88,9 @@ def test_assert_no_additional_deps(): helpers.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( - 'For now, pre-commit does not support additional_dependencies for lang' + 'for now, pre-commit does not support additional_dependencies for ' + 'lang -- ' + "you selected `additional_dependencies: ['hmmm']`" ) diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 8e52268..fb5ae71 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import shutil diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index d8bacc4..8420046 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages import pygrep diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8324cac..6160669 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sys from unittest import mock @@ -47,16 +49,16 @@ def test_norm_version_of_default_is_sys_executable(): assert python.norm_version('default') is None -@pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) +@pytest.mark.parametrize('v', ('python3.9', 'python3', 'python')) def test_sys_executable_matches(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert python._sys_executable_matches(v) assert python.norm_version(v) is None @pytest.mark.parametrize('v', ('notpython', 'python3.x')) def test_sys_executable_matches_does_not_match(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert not python._sys_executable_matches(v) @@ -65,7 +67,7 @@ def test_sys_executable_matches_does_not_match(v): ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), ('/usr/bin/python', '/usr/bin/python', None), - ('/usr/bin/python3.6m', '/usr/bin/python3.6m', 'python3.6m'), + ('/usr/bin/python3.7m', '/usr/bin/python3.7m', 'python3.7m'), ('v/bin/python', 'v/bin/pypy', 'pypy'), ), ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 66aa7b3..5bc63b2 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import os.path import pytest +from pre_commit import envcontext from pre_commit.languages import r from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -127,3 +130,14 @@ def test_r_parsing_file_local(tempdir_factory, store): config=config, expect_path_prefix=False, ) + + +def test_rscript_exec_relative_to_r_home(): + expected = os.path.join('r_home_dir', 'bin', 'Rscript') + with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): + assert r._rscript_exec() == expected + + +def test_path_rscript_exec_no_r_home_set(): + with envcontext.envcontext((('R_HOME', envcontext.UNSET),)): + assert r._rscript_exec() == 'Rscript' diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 7dff046..dc55456 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import tarfile from unittest import mock diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index fe68593..dc43a99 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from pre_commit import color diff --git a/tests/main_test.py b/tests/main_test.py index 1ad8d41..a645300 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os.path from unittest import mock @@ -12,20 +14,6 @@ from testing.auto_namedtuple import auto_namedtuple from testing.util import cwd -@pytest.mark.parametrize( - ('argv', 'expected'), - ( - ((), ['f']), - (('--f', 'x'), ['x']), - (('--f', 'x', '--f', 'y'), ['x', 'y']), - ), -) -def test_append_replace_default(argv, expected): - parser = argparse.ArgumentParser() - parser.add_argument('--f', action=main.AppendReplaceDefault, default=['f']) - assert parser.parse_args(argv).f == expected - - def _args(**kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) @@ -170,7 +158,7 @@ def test_init_templatedir(mock_store_dir): assert patch.call_count == 1 assert 'tdir' in patch.call_args[0] - assert patch.call_args[1]['hook_types'] == ['pre-commit'] + assert patch.call_args[1]['hook_types'] is None assert patch.call_args[1]['skip_on_missing_config'] is True diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 06bdd04..63f9715 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 703bd25..15b68b4 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes from pre_commit.util import cmd_output diff --git a/tests/meta_hooks/identity_test.py b/tests/meta_hooks/identity_test.py index 3eff00b..97c20ea 100644 --- a/tests/meta_hooks/identity_test.py +++ b/tests/meta_hooks/identity_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import identity diff --git a/tests/output_test.py b/tests/output_test.py index 1cdacbb..c806829 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io from pre_commit import output diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 0bb19c7..d7acbf5 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 6ce8be1..1eac087 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index 8569ba9..cef6887 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import os.path import shutil -import sys from typing import Any -from typing import Dict from unittest import mock import cfgv @@ -875,7 +875,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): @pytest.fixture def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo - repo_path = get_resource_path('python_hooks_repo') + repo_path = get_resource_path('python3_hooks_repo') manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) hooks = [ dict(hook, additional_dependencies=[repo_path]) for hook in manifest @@ -883,21 +883,27 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} -@pytest.mark.xfail( # pragma: win32 no cover - sys.platform == 'win32', - reason='microsoft/azure-pipelines-image-generation#989', -) def test_local_python_repo(store, local_python_config): - hook = _get_hook(local_python_config, store, 'foo') + hook = _get_hook(local_python_config, store, 'python3-hook') + # language_version should have been adjusted to the interpreter version + assert hook.language_version != C.DEFAULT + ret, out = _hook_run(hook, ('filename',), color=False) + assert ret == 0 + assert _norm_out(out) == b"3\n['filename']\nHello World\n" + + +def test_local_python_repo_python2(store, local_python_config): + local_python_config['hooks'][0]['language_version'] = 'python2' + hook = _get_hook(local_python_config, store, 'python3-hook') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert _norm_out(out) == b"2\n['filename']\nHello World\n" def test_default_language_version(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, 'default_stages': ['commit'], 'repos': [local_python_config], @@ -914,7 +920,7 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, 'default_stages': ['commit'], 'repos': [local_python_config], diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 2e3f620..a91f315 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import os.path import shutil diff --git a/tests/store_test.py b/tests/store_test.py index 5a5d69e..ff671a8 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sqlite3 import stat diff --git a/tests/util_test.py b/tests/util_test.py index 01afbd4..6b00f9f 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import subprocess diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7e83ef5..0530e50 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import concurrent.futures import os import sys import time -from typing import Tuple from unittest import mock import pytest @@ -178,7 +179,7 @@ def test_thread_mapper_concurrency_uses_regular_map(): def test_xargs_propagate_kwargs_to_cmd(): env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} - cmd: Tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd: tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) ret, stdout = xargs.xargs(cmd, ('1',), env=env) @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,pypy3,pre-commit +envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt |