From 1312af87e5908ac252c0659a60216e96ec5b23bd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Jun 2020 11:11:24 +0200 Subject: Merging upstream version 2.5.1. Signed-off-by: Daniel Baumann --- .pre-commit-config.yaml | 12 +- CHANGELOG.md | 67 +++++++++++ CONTRIBUTING.md | 5 +- azure-pipelines.yml | 4 +- pre_commit/commands/autoupdate.py | 12 +- pre_commit/commands/hook_impl.py | 3 +- pre_commit/commands/migrate_config.py | 4 + pre_commit/commands/run.py | 39 +++++-- pre_commit/constants.py | 4 +- pre_commit/git.py | 3 +- pre_commit/languages/all.py | 4 +- pre_commit/languages/helpers.py | 4 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/python.py | 147 +++++++++++++----------- pre_commit/languages/python_venv.py | 46 -------- pre_commit/languages/ruby.py | 2 + pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 7 +- pre_commit/meta_hooks/check_useless_excludes.py | 7 +- pre_commit/resources/empty_template_go.mod | 0 pre_commit/staged_files_only.py | 9 +- pre_commit/store.py | 9 +- pre_commit/util.py | 3 +- setup.cfg | 4 +- testing/gen-languages-all | 3 +- testing/util.py | 20 +--- tests/commands/autoupdate_test.py | 32 +++++- tests/commands/hook_impl_test.py | 10 +- tests/commands/install_uninstall_test.py | 57 +++++++++ tests/commands/migrate_config_test.py | 7 +- tests/commands/run_test.py | 19 ++- tests/git_test.py | 5 + tests/languages/helpers_test.py | 2 +- tests/languages/python_test.py | 96 +++++++++++++--- tests/repository_test.py | 8 +- tests/store_test.py | 9 +- tox.ini | 1 - 37 files changed, 456 insertions(+), 212 deletions(-) delete mode 100644 pre_commit/languages/python_venv.py create mode 100644 pre_commit/resources/empty_template_go.mod diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b51417d..36d73c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,25 +12,25 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.0 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.1 + rev: v1.5.2 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.2.0 + rev: v2.4.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.1.0 + rev: v2.4.1 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.1.0 + rev: v2.3.0 hooks: - id: reorder-python-imports args: [--py3-plus] @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.8.2 + rev: v1.9.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b83319..375a9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +2.5.1 - 2020-06-09 +================== + +### Fixes +- Prevent infinite recursion of post-checkout on clone + - #1497 PR by @asottile. + - #1496 issue by @admorgan. + +2.5.0 - 2020-06-08 +================== + +### Features +- Expose a `PRE_COMMIT=1` environment variable when running hooks + - #1467 PR by @tech-chad. + - #1426 issue by @lorenzwalthert. + +### Fixes +- Fix `UnicodeDecodeError` on windows when using the `py` launcher to detect + executables with non-ascii characters in the path + - #1474 PR by @asottile. + - #1472 issue by DrFobos. +- Fix `DeprecationWarning` on python3.9 for `random.shuffle` method + - #1480 PR by @asottile. + - #1479 issue by @isidentical. +- Normalize slashes earlier such that global `files` / `exclude` use forward + slashes on windows as well. + - #1494 PR by @asottile. + - #1476 issue by @harrybiddle. + +2.4.0 - 2020-05-11 +================== + +### Features +- Add support for `post-commit` hooks + - #1415 PR by @ModischFabrications. + - #1411 issue by @ModischFabrications. +- Silence pip version warning in python installation error + - #1412 PR by @asottile. +- Improve python `healthy()` when upgrading operating systems. + - #1431 PR by @asottile. + - #1427 issue by @ahonnecke. +- `language: python_venv` is now an alias to `language: python` (and will be + removed in a future version). + - #1431 PR by @asottile. +- Speed up python `healthy()` check. + - #1431 PR by @asottile. +- `pre-commit autoupdate` now tries to maintain quoting style of `rev`. + - #1435 PR by @marcjay. + - #1434 issue by @marcjay. + +### Fixes +- Fix installation of go modules in `repo: local`. + - #1428 PR by @scop. +- Fix committing with unstaged files and a failing `post-checkout` hook. + - #1422 PR by @domodwyer. + - #1418 issue by @domodwyer. +- Fix installation of node hooks with system node installed on freebsd. + - #1443 PR by @asottile. + - #1440 issue by @jockej. +- Fix ruby hooks when `GEM_PATH` is set globally. + - #1442 PR by @tdeo. +- Improve error message when `pre-commit autoupdate` / + `pre-commit migrate-config` are run but the pre-commit configuration is not + valid yaml. + - #1448 PR by @asottile. + - #1447 issue by @rpdelaney. + 2.3.0 - 2020-04-22 ================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b83c82..d70a89d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,8 @@ This is useful for running specific tests. The easiest way to set this up is to run: 1. `tox --devenv venv` (note: requires tox>=3.13) -2. `. venv/bin/activate` +2. `. venv/bin/activate` (or follow the [activation instructions] for your + platform) This will create and put you into a virtualenv which has an editable installation of pre-commit. Hack away! Running `pre-commit` will reflect @@ -144,3 +145,5 @@ This is usually the easiest to implement, most of them look the same as the `node` hook implementation: https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74 + +[activation instructions]: https://virtualenv.pypa.io/en/latest/user_guide.html#activators diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9b385b4..c21843e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v1.0.0 + ref: refs/tags/v2.0.0 jobs: - template: job--pre-commit.yml@asottile @@ -40,7 +40,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38] + toxenvs: [pypy3, py36, py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 8c9fdd7..87f6d53 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,7 +84,9 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) +REV_LINE_RE = re.compile( + r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, +) def _original_lines( @@ -116,15 +118,15 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: continue match = REV_LINE_RE.match(lines[idx]) assert match is not None - new_rev_s = yaml_dump({'rev': rev_info.rev}) + new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3]) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' - elif match[4].strip().startswith('# frozen:'): + elif match[5].strip().startswith('# frozen:'): comment = '' else: - comment = match[4] - lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' + comment = match[5] + lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}' with open(path, 'w', newline='') as f: f.write(''.join(lines)) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 4843fc7..d0e226f 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -150,6 +150,7 @@ def _pre_push_ns( _EXPECTED_ARG_LENGTH_BY_HOOK = { 'commit-msg': 1, 'post-checkout': 3, + 'post-commit': 0, 'pre-commit': 0, 'pre-merge-commit': 0, 'pre-push': 2, @@ -186,7 +187,7 @@ def _run_ns( return _pre_push_ns(color, args, stdin) elif hook_type in {'commit-msg', 'prepare-commit-msg'}: return _ns(hook_type, color, commit_msg_filename=args[0]) - elif hook_type in {'pre-merge-commit', 'pre-commit'}: + elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) elif hook_type == 'post-checkout': return _ns( diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d83b8e9..d580ff1 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -2,6 +2,7 @@ import re import yaml +from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -43,6 +44,9 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: + # ensure that the configuration is a valid pre-commit configuration + load_config(config_file) + with open(config_file) as f: orig_contents = contents = f.read() diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8c8401c..567b7cd 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -72,13 +72,7 @@ def filter_by_include_exclude( class Classifier: - def __init__(self, filenames: Sequence[str]) -> None: - # 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 - # see #1173 - if os.altsep == '/' and os.sep == '\\': - filenames = [f.replace(os.sep, os.altsep) for f in filenames] + def __init__(self, filenames: Collection[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) @@ -105,6 +99,22 @@ class Classifier: names = self.by_types(names, hook.types, hook.exclude_types) return tuple(names) + @classmethod + def from_config( + cls, + filenames: Collection[str], + include: str, + exclude: str, + ) -> '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 + # see #1173 + if os.altsep == '/' and os.sep == '\\': + filenames = [f.replace(os.sep, os.altsep) for f in filenames] + filenames = filter_by_include_exclude(filenames, include, exclude) + return Classifier(filenames) + def _get_skips(environ: EnvironT) -> Set[str]: skips = environ.get('SKIP', '') @@ -221,7 +231,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.hook_stage == 'post-checkout': # no files for post-checkout + # these hooks do not operate on files + if args.hook_stage in {'post-checkout', 'post-commit'}: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -246,10 +257,9 @@ def _run_hooks( """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) - filenames = filter_by_include_exclude( + classifier = Classifier.from_config( _all_filenames(args), config['files'], config['exclude'], ) - classifier = Classifier(filenames) retval = 0 for hook in hooks: retval |= _run_single_hook( @@ -323,6 +333,12 @@ def run( f'`--hook-stage {args.hook_stage}`', ) return 1 + # prevent recursive post-checkout hooks (#1418) + if ( + args.hook_stage == 'post-checkout' and + environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT') + ): + return 0 # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: @@ -340,6 +356,9 @@ def run( if args.checkout_type: environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + # Set pre_commit flag + environ['PRE_COMMIT'] = '1' + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index e2b8e3a..5150fdc 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -17,8 +17,8 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'post-checkout', 'push', + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'post-commit', 'manual', 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/git.py b/pre_commit/git.py index 7e757f2..576bef8 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -158,7 +158,8 @@ def init_repo(path: str, remote: str) -> None: remote = os.path.abspath(remote) env = no_git_env() - cmd_output_b('git', 'init', path, env=env) + # avoid the user's template so that hooks do not recurse + cmd_output_b('git', 'init', '--template=', path, env=env) cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 8f4ffa8..5609631 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -14,7 +14,6 @@ from pre_commit.languages import node from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python -from pre_commit.languages import python_venv from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages import script @@ -49,7 +48,6 @@ languages = { 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 @@ -57,4 +55,6 @@ languages = { 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 # END GENERATED } +# TODO: fully deprecate `python_venv` +languages['python_venv'] = languages['python'] all_languages = sorted(languages) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b5c95e5..01c65ab 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -18,7 +18,7 @@ from pre_commit.xargs import xargs if TYPE_CHECKING: from typing import NoReturn -FIXED_RANDOM_SEED = 1542676186 +FIXED_RANDOM_SEED = 1542676187 def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: @@ -92,7 +92,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]: fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) - random.shuffle(seq, random=fixed_random.random) + fixed_random.shuffle(seq) return seq diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9b636d3..26f4919 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -79,7 +79,7 @@ def install_environment( # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover - envdir = f'\\\\?\\{os.path.normpath(envdir)}' + envdir = fr'\\?\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 85d8281..6f7c900 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -2,8 +2,7 @@ import contextlib import functools import os import sys -from typing import Callable -from typing import ContextManager +from typing import Dict from typing import Generator from typing import Optional from typing import Sequence @@ -26,6 +25,28 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_env' +@functools.lru_cache(maxsize=None) +def _version_info(exe: str) -> str: + prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' + try: + return cmd_output(exe, '-S', '-c', prog)[1].strip() + except CalledProcessError: + return f'<>' + + +def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: + ret = {} + with open(filename) as f: + for line in f: + try: + k, v = line.split('=') + except ValueError: # blank line / comment / etc. + continue + else: + ret[k.strip()] = v.strip() + return ret + + def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' @@ -34,6 +55,7 @@ def bin_dir(venv: str) -> str: def get_env_patch(venv: str) -> PatchesT: return ( + ('PIP_DISABLE_PIP_VERSION_CHECK', '1'), ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), @@ -45,9 +67,10 @@ def _find_by_py_launcher( ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + env = dict(os.environ, PYTHONIOENCODING='UTF-8') try: - cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') - return cmd_output(*cmd)[1].strip() + return cmd_output(*cmd, env=env)[1].strip() except CalledProcessError: pass return None @@ -115,6 +138,9 @@ def _sys_executable_matches(version: str) -> bool: def norm_version(version: str) -> str: + if version == C.DEFAULT: + return os.path.realpath(sys.executable) + # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable @@ -139,70 +165,59 @@ def norm_version(version: str) -> str: return os.path.expanduser(version) -def py_interface( - _dir: str, - _make_venv: Callable[[str, str], None], -) -> Tuple[ - Callable[[Prefix, str], ContextManager[None]], - Callable[[Prefix, str], bool], - Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], - Callable[[Prefix, str, Sequence[str]], None], -]: - @contextlib.contextmanager - def in_env( - prefix: Prefix, - language_version: str, - ) -> Generator[None, None, None]: - envdir = prefix.path(helpers.environment_dir(_dir, language_version)) - with envcontext(get_env_patch(envdir)): - yield - - def healthy(prefix: Prefix, language_version: str) -> bool: - envdir = helpers.environment_dir(_dir, language_version) - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' - py_exe = prefix.path(bin_dir(envdir), exe_name) - with in_env(prefix, language_version): - retcode, _, _ = cmd_output_b( - py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', - cwd='/', - retcode=None, - ) - return retcode == 0 - - def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, - ) -> Tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) - - def install_environment( - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - directory = helpers.environment_dir(_dir, version) - install = ('python', '-mpip', 'install', '.', *additional_dependencies) - - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - if version != C.DEFAULT: - python = norm_version(version) - else: - python = os.path.realpath(sys.executable) - _make_venv(env_dir, python) - with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install) +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield - return in_env, healthy, run_hook, install_environment +def healthy(prefix: Prefix, language_version: str) -> bool: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') -def make_venv(envdir: str, python: str) -> None: - env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) - cmd_output_b(*cmd, env=env, cwd='/') + # created with "old" virtualenv + if not os.path.exists(pyvenv_cfg): + return False + + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) + cfg = _read_pyvenv_cfg(pyvenv_cfg) + + return ( + 'version_info' in cfg and + _version_info(py_exe) == cfg['version_info'] and ( + 'base-executable' not in cfg or + _version_info(cfg['base-executable']) == cfg['version_info'] + ) + ) -_interface = py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + python = norm_version(version) + venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) + + with clean_path_on_failure(envdir): + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> 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/python_venv.py b/pre_commit/languages/python_venv.py deleted file mode 100644 index 5404c8b..0000000 --- a/pre_commit/languages/python_venv.py +++ /dev/null @@ -1,46 +0,0 @@ -import os.path - -from pre_commit.languages import python -from pre_commit.util import CalledProcessError -from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b - -ENVIRONMENT_DIR = 'py_venv' -get_default_version = python.get_default_version - - -def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) - """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs - packages to the incorrect location. Attempt to find the _original_ exe - and invoke `-mvenv` from there. - - See: - - https://github.com/pre-commit/pre-commit/issues/755 - - https://github.com/pypa/virtualenv/issues/1095 - - https://bugs.python.org/issue30811 - """ - try: - prefix_script = 'import sys; print(sys.real_prefix)' - _, prefix, _ = cmd_output(exe, '-c', prefix_script) - prefix = prefix.strip() - except CalledProcessError: - # not created from -mvirtualenv - return exe - - if os.name == 'nt': - expected = os.path.join(prefix, 'python.exe') - else: - expected = os.path.join(prefix, 'bin', os.path.basename(exe)) - - if os.path.exists(expected): - return expected - else: - return exe - - -def make_venv(envdir: str, python: str) -> None: - cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') - - -_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 61241f8..fe524ec 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,6 +9,7 @@ from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -28,6 +29,7 @@ def get_env_patch( ) -> PatchesT: # pragma: win32 no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), + ('GEM_PATH', UNSET), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), ( diff --git a/pre_commit/main.py b/pre_commit/main.py index 790b347..874eb53 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ 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-checkout', + 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index d0244a9..a1e9352 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -11,10 +11,13 @@ from pre_commit.store import Store def check_all_hooks_match_files(config_file: str) -> int: - classifier = Classifier(git.get_all_files()) + config = load_config(config_file) + classifier = Classifier.from_config( + git.get_all_files(), config['files'], config['exclude'], + ) retv = 0 - for hook in all_hooks(load_config(config_file), Store()): + for hook in all_hooks(config, Store()): if hook.always_run or hook.language == 'fail': continue elif not classifier.filenames_for_hook(hook): diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 30b8d81..db6865c 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -28,11 +28,14 @@ def exclude_matches_any( def check_useless_excludes(config_file: str) -> int: config = load_config(config_file) - classifier = Classifier(git.get_all_files()) + filenames = git.get_all_files() + classifier = Classifier.from_config( + filenames, config['files'], config['exclude'], + ) retv = 0 exclude = config['exclude'] - if not exclude_matches_any(classifier.filenames, '', exclude): + if not exclude_matches_any(filenames, '', exclude): print( f'The global exclude pattern {exclude!r} does not match any files', ) diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod new file mode 100644 index 0000000..e69de29 diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 09d323d..6179301 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -56,8 +56,10 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) - # Clear the working directory of unstaged changes - cmd_output_b('git', 'checkout', '--', '.') + # prevent recursive post-checkout hooks (#1418) + no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + try: yield finally: @@ -72,8 +74,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) _git_apply(patch_filename) + logger.info(f'Restored changes from {patch_filename}.') else: # There weren't any staged files so we don't need to do anything diff --git a/pre_commit/store.py b/pre_commit/store.py index 760b37a..6d8c40a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -30,10 +30,11 @@ def _get_default_directory() -> str: `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.environ.get('PRE_COMMIT_HOME') or os.path.join( + ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join( os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'pre-commit', ) + return os.path.realpath(ret) class Store: @@ -182,9 +183,9 @@ class Store: return self._new_repo(repo, ref, deps, clone_strategy) LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', - 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', - 'Makefile.PL', + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', + 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', + 'environment.yml', 'Makefile.PL', ) def make_local(self, deps: Sequence[str]) -> str: diff --git a/pre_commit/util.py b/pre_commit/util.py index 2db579a..0338b37 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -36,10 +36,11 @@ yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) -def yaml_dump(o: Any) -> str: +def yaml_dump(o: Any, **kwargs: Any) -> str: # when python/mypy#1484 is solved, this can be `functools.partial` return yaml.dump( o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + **kwargs, ) diff --git a/setup.cfg b/setup.cfg index 2e69d50..f1ce18d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.3.0 +version = 2.5.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -27,7 +27,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 toml - virtualenv>=15.2 + virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" python_requires = >=3.6.1 diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 6d0b26f..2bff7be 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,8 +3,7 @@ import sys LANGUAGES = [ 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', - 'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', - 'system', + 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/util.py b/testing/util.py index 439bee7..bfe1421 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,20 +45,6 @@ xfailif_windows_no_ruby = pytest.mark.xfail( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -def supports_venv(): # pragma: no cover (platform specific) - try: - __import__('ensurepip') - __import__('venv') - return True - except ImportError: - return False - - -xfailif_no_venv = pytest.mark.xfail( - not supports_venv(), reason='Does not support venv module', -) - - def run_opts( all_files=False, files=(), @@ -103,10 +89,12 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs): +def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) - cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args + cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', *args) + if all_files: # allow skipping `-a` with `all_files=False` + cmd += ('-a',) if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) ret, out, _ = fn(*cmd, **kwargs) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 25161d1..bd89c1d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -414,9 +414,9 @@ def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 1 - assert new_config_writen['repos'][0] == config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 1 + assert new_config_written['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( @@ -429,9 +429,9 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 2 - assert new_config_writen['repos'][0] == local_config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 2 + assert new_config_written['repos'][0] == local_config def test_autoupdate_meta_hooks(tmpdir, store): @@ -474,3 +474,23 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' + + +def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): + fmt = ( + 'repos:\n' + '- repo: {path}\n' + ' rev: "{rev}"\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {path}\n' + " rev: '{rev}'\n" + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) + assert cfg.read() == expected diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index ddf65b7..2fc0146 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -96,6 +96,7 @@ def test_run_legacy_recursive(tmpdir): ('pre-merge-commit', []), ('pre-push', ['branch_name', 'remote_name']), ('commit-msg', ['.git/COMMIT_EDITMSG']), + ('post-commit', []), ('post-checkout', ['old_head', 'new_head', '1']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), @@ -117,7 +118,7 @@ def test_check_args_length_error_too_many_plural(): ) -def test_check_args_length_error_too_many_singluar(): +def test_check_args_length_error_too_many_singular(): with pytest.raises(SystemExit) as excinfo: hook_impl._check_args_length('commit-msg', []) msg, = excinfo.value.args @@ -149,6 +150,13 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_commit(): + ns = hook_impl._run_ns('post-commit', True, (), b'') + assert ns is not None + assert ns.hook_stage == 'post-commit' + assert ns.color is True + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 66b9190..5809a3f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -726,6 +726,32 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_commit_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-commit', + 'name': 'Post commit', + 'entry': 'touch post-commit.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-commit'], + }], + }, + ] + write_config(path, config) + with cwd(path): + _get_commit_output(tempdir_factory) + assert not os.path.exists('post-commit.tmp') + + install(C.CONFIG_FILE, store, hook_types=['post-commit']) + _get_commit_output(tempdir_factory) + assert os.path.exists('post-commit.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ @@ -763,6 +789,37 @@ def test_post_checkout_integration(tempdir_factory, store): assert 'some_file' not in stderr +def test_skips_post_checkout_unstaged_changes(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'entry': 'fail', + 'language': 'fail', + 'always_run': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + _get_commit_output(tempdir_factory) + + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + + # make an unstaged change so staged_files_only fires + open('file', 'a').close() + cmd_output('git', 'add', 'file') + with open('file', 'w') as f: + f.write('unstaged changes') + + retc, out = _get_commit_output(tempdir_factory, all_files=False) + assert retc == 0 + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index efc0d1c..6a049d5 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,7 @@ import pytest import pre_commit.constants as C +from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config @@ -147,10 +148,10 @@ def test_migrate_config_sha_to_rev(tmpdir): @pytest.mark.parametrize('contents', ('', '\n')) -def test_empty_configuration_file_user_error(tmpdir, contents): +def test_migrate_config_invalid_configuration(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - with tmpdir.as_cwd(): - assert not migrate_config(C.CONFIG_FILE) + with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): + migrate_config(C.CONFIG_FILE) # even though the config is invalid, this should be a noop assert cfg.read() == contents diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c51bcff..2461ed5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -939,7 +939,7 @@ def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): tmpdir.join('a/b/c').ensure() with mock.patch.object(os, 'altsep', '/'): with mock.patch.object(os, 'sep', '\\'): - classifier = Classifier((r'a\b\c',)) + classifier = Classifier.from_config((r'a\b\c',), '', '^$') assert classifier.filenames == ['a/b/c'] @@ -947,7 +947,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): with mock.patch.object(os.path, 'lexists', return_value=True): with mock.patch.object(os, 'altsep', None): with mock.patch.object(os, 'sep', '/'): - classifier = Classifier((r'a/b\c',)) + classifier = Classifier.from_config((r'a/b\c',), '', '^$') assert classifier.filenames == [r'a/b\c'] @@ -1022,3 +1022,18 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): run_opts(hook='do_not_commit'), ) assert b'identity-copy' not in printed + + +def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): + environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'} + opts = run_opts(hook_stage='post-checkout') + assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0 + + +def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): + args = run_opts() + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT'] == '1' diff --git a/tests/git_test.py b/tests/git_test.py index e73a6f2..fafd4a6 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -186,3 +186,8 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', } + + +def test_init_repo_no_hooks(tmpdir): + git.init_repo(str(tmpdir), remote='dne') + assert not tmpdir.join('.git/hooks').exists() diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index c52e947..fa493cc 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -78,5 +78,5 @@ def test_target_concurrency_cpu_count_not_implemented(): def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] - expected = ['3', '7', '8', '2', '4', '6', '5', '1', '0', '9'] + expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] assert helpers._shuffled(seq) == expected diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 34c6c7f..c419ad6 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -5,10 +5,23 @@ from unittest import mock import pytest import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +def test_read_pyvenv_cfg(tmpdir): + pyvenv_cfg = tmpdir.join('pyvenv.cfg') + pyvenv_cfg.write( + '# I am a comment\n' + '\n' + 'foo = bar\n' + 'version-info=123\n', + ) + expected = {'foo': 'bar', 'version-info': '123'} + assert python._read_pyvenv_cfg(pyvenv_cfg) == expected + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: nt cover @@ -21,6 +34,10 @@ def test_norm_version_expanduser(): assert result == expected_path +def test_norm_version_of_default_is_sys_executable(): + assert python.norm_version('default') == os.path.realpath(sys.executable) + + @pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) def test_sys_executable_matches(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): @@ -49,27 +66,78 @@ def test_find_by_sys_executable(exe, realpath, expected): assert python._find_by_sys_executable() == expected -def test_healthy_types_py_in_cwd(tmpdir): +@pytest.fixture +def python_dir(tmpdir): with tmpdir.as_cwd(): prefix = tmpdir.join('prefix').ensure_dir() prefix.join('setup.py').write('import setuptools; setuptools.setup()') prefix = Prefix(str(prefix)) - python.install_environment(prefix, C.DEFAULT, ()) + yield prefix, tmpdir - # even if a `types.py` file exists, should still be healthy - tmpdir.join('types.py').ensure() - assert python.healthy(prefix, C.DEFAULT) is True +def test_healthy_default_creator(python_dir): + prefix, tmpdir = python_dir -def test_healthy_python_goes_missing(tmpdir): - with tmpdir.as_cwd(): - prefix = tmpdir.join('prefix').ensure_dir() - prefix.join('setup.py').write('import setuptools; setuptools.setup()') - prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + + # should be healthy right after creation + assert python.healthy(prefix, C.DEFAULT) is True + + # even if a `types.py` file exists, should still be healthy + tmpdir.join('types.py').ensure() + assert python.healthy(prefix, C.DEFAULT) is True + + +def test_healthy_venv_creator(python_dir): + # venv creator produces slightly different pyvenv.cfg + prefix, tmpdir = python_dir + + with envcontext((('VIRTUALENV_CREATOR', 'venv'),)): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' - py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) - os.remove(py_exe) + assert python.healthy(prefix, C.DEFAULT) is True + + +def test_unhealthy_python_goes_missing(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.remove(py_exe) + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_with_version_change(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'w') as f: + f.write('version_info = 1.2.3\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_system_version_changes(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f: + f.write('base-executable = /does/not/exist\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_old_virtualenv(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate "old" virtualenv by deleting this file + os.remove(prefix.path('py_env-default/pyvenv.cfg')) - assert python.healthy(prefix, C.DEFAULT) is False + assert python.healthy(prefix, C.DEFAULT) is False diff --git a/tests/repository_test.py b/tests/repository_test.py index 3c7a637..2ac7886 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,7 +33,7 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift -from testing.util import xfailif_no_venv +from testing.util import xfailif_windows from testing.util import xfailif_windows_no_ruby @@ -163,7 +163,6 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -@xfailif_no_venv def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', @@ -243,6 +242,7 @@ def test_run_a_node_hook(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -252,6 +252,7 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) +@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -534,6 +535,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output +@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) @@ -880,7 +882,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False, diff --git a/tests/store_test.py b/tests/store_test.py index 5866616..6a4e900 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -25,7 +25,8 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit') + expected = os.path.realpath(os.path.expanduser('~/.cache/pre-commit')) + assert ret == expected def test_adheres_to_xdg_specification(): @@ -33,7 +34,8 @@ def test_adheres_to_xdg_specification(): os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, ): ret = _get_default_directory() - assert ret == os.path.join('/tmp/fakehome', 'pre-commit') + expected = os.path.realpath('/tmp/fakehome/pre-commit') + assert ret == expected def test_uses_environment_variable_when_present(): @@ -41,7 +43,8 @@ def test_uses_environment_variable_when_present(): os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() - assert ret == '/tmp/pre_commit_home' + expected = os.path.realpath('/tmp/pre_commit_home') + assert ret == expected def test_store_init(store): diff --git a/tox.ini b/tox.ini index d9f9420..63a3aab 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ commands = coverage erase coverage run -m pytest {posargs:tests} coverage report - pre-commit install [testenv:pre-commit] skip_install = true -- cgit v1.2.3