1
0
Fork 0

Merging upstream version 3.8.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:51:12 +01:00
parent b7536eb65c
commit a61ae18588
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
7 changed files with 180 additions and 49 deletions

View file

@ -14,7 +14,7 @@ repos:
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0 rev: v3.13.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
@ -24,21 +24,21 @@ repos:
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.2 rev: v3.16.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]
- repo: https://github.com/hhatto/autopep8 - repo: https://github.com/hhatto/autopep8
rev: v2.1.0 rev: v2.3.1
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 7.0.0 rev: 7.1.0
hooks: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0 rev: v1.11.0
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: [types-all] additional_dependencies: [types-pyyaml]
exclude: ^testing/resources/ exclude: ^testing/resources/

View file

@ -1,3 +1,12 @@
3.8.0 - 2024-07-28
==================
### Features
- Implement health checks for `language: r` so environments are recreated if
the system version of R changes.
- #3206 issue by @lorenzwalthert.
- #3265 PR by @lorenzwalthert.
3.7.1 - 2024-05-10 3.7.1 - 2024-05-10
================== ==================

View file

@ -14,13 +14,74 @@ from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET from pre_commit.envcontext import UNSET
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'renv' ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
get_default_version = lang_base.basic_get_default_version get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check
def _execute_vanilla_r_code_as_script(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
) -> str:
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
_, out, _ = cmd_output(
_rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd,
)
return out.rstrip('\n')
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_vanilla_r_code_as_script(
'cat(renv::settings$r.version())',
prefix=prefix, version=version,
cwd=envdir,
)
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_vanilla_r_code_as_script(
'cat(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)
def _write_current_r_version(
envdir: str, prefix: Prefix, version: str,
) -> None:
_execute_vanilla_r_code_as_script(
'renv::settings$r.version(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)
def health_check(prefix: Prefix, version: str) -> str | None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
r_version_installation = _read_installed_version(
envdir=envdir, prefix=prefix, version=version,
)
r_version_current_executable = _read_executable_version(
envdir=envdir, prefix=prefix, version=version,
)
if r_version_installation in {'NULL', ''}:
return (
f'Hooks were installed with an unknown R version. R version for '
f'hook repo now set to {r_version_current_executable}'
)
elif r_version_installation != r_version_current_executable:
return (
f'Hooks were installed for R version {r_version_installation}, '
f'but current R executable has version '
f'{r_version_current_executable}'
)
return None
@contextlib.contextmanager @contextlib.contextmanager
@ -147,14 +208,12 @@ def install_environment(
with _r_code_in_tempfile(r_code_inst_environment) as f: with _r_code_in_tempfile(r_code_inst_environment) as f:
cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies: if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
with in_env(prefix, version): _execute_vanilla_r_code_as_script(
with _r_code_in_tempfile(r_code_inst_add) as f: code=r_code_inst_add, prefix=prefix, version=version,
cmd_output_b( args=additional_dependencies,
_rscript_exec(), *RSCRIPT_OPTS,
f,
*additional_dependencies,
cwd=env_dir, cwd=env_dir,
) )

View file

@ -205,7 +205,7 @@ else: # pragma: no cover
def _handle_readonly( def _handle_readonly(
func: Callable[[str], object], func: Callable[[str], object],
path: str, path: str,
exc: Exception, exc: BaseException,
) -> None: ) -> None:
if ( if (
func in (os.rmdir, os.remove, os.unlink) and func in (os.rmdir, os.remove, os.unlink) and
@ -223,7 +223,7 @@ if sys.version_info < (3, 12): # pragma: <3.12 cover
def _handle_readonly_old( def _handle_readonly_old(
func: Callable[[str], object], func: Callable[[str], object],
path: str, path: str,
excinfo: tuple[type[Exception], Exception, TracebackType], excinfo: tuple[type[BaseException], BaseException, TracebackType],
) -> None: ) -> None:
return _handle_readonly(func, path, excinfo[1]) return _handle_readonly(func, path, excinfo[1])

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 3.7.1 version = 3.8.0
description = A framework for managing and maintaining multi-language pre-commit hooks. description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown

View file

@ -209,36 +209,25 @@ def log_info_mock():
yield mck yield mck
class FakeStream:
def __init__(self):
self.data = io.BytesIO()
def write(self, s):
self.data.write(s)
def flush(self):
pass
class Fixture: class Fixture:
def __init__(self, stream): def __init__(self, stream: io.BytesIO) -> None:
self._stream = stream self._stream = stream
def get_bytes(self): def get_bytes(self) -> bytes:
"""Get the output as-if no encoding occurred""" """Get the output as-if no encoding occurred"""
data = self._stream.data.getvalue() data = self._stream.getvalue()
self._stream.data.seek(0) self._stream.seek(0)
self._stream.data.truncate() self._stream.truncate()
return data.replace(b'\r\n', b'\n') return data.replace(b'\r\n', b'\n')
def get(self): def get(self) -> str:
"""Get the output assuming it was written as UTF-8 bytes""" """Get the output assuming it was written as UTF-8 bytes"""
return self.get_bytes().decode() return self.get_bytes().decode()
@pytest.fixture @pytest.fixture
def cap_out(): def cap_out():
stream = FakeStream() stream = io.BytesIO()
write = functools.partial(output.write, stream=stream) write = functools.partial(output.write, stream=stream)
write_line_b = functools.partial(output.write_line_b, stream=stream) write_line_b = functools.partial(output.write_line_b, stream=stream)
with mock.patch.multiple(output, write=write, write_line_b=write_line_b): with mock.patch.multiple(output, write=write, write_line_b=write_line_b):

View file

@ -1,14 +1,17 @@
from __future__ import annotations from __future__ import annotations
import os.path import os.path
import shutil from unittest import mock
import pytest import pytest
import pre_commit.constants as C
from pre_commit import envcontext from pre_commit import envcontext
from pre_commit import lang_base
from pre_commit.languages import r from pre_commit.languages import r
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.store import _make_local_repo from pre_commit.store import _make_local_repo
from pre_commit.util import resource_text
from pre_commit.util import win_exe from pre_commit.util import win_exe
from testing.language_helpers import run_language from testing.language_helpers import run_language
@ -127,7 +130,8 @@ def test_path_rscript_exec_no_r_home_set():
assert r._rscript_exec() == 'Rscript' assert r._rscript_exec() == 'Rscript'
def test_r_hook(tmp_path): @pytest.fixture
def renv_lock_file(tmp_path):
renv_lock = '''\ renv_lock = '''\
{ {
"R": { "R": {
@ -157,6 +161,12 @@ def test_r_hook(tmp_path):
} }
} }
''' '''
tmp_path.joinpath('renv.lock').write_text(renv_lock)
yield
@pytest.fixture
def description_file(tmp_path):
description = '''\ description = '''\
Package: gli.clu Package: gli.clu
Title: What the Package Does (One Line, Title Case) Title: What the Package Does (One Line, Title Case)
@ -178,27 +188,39 @@ RoxygenNote: 7.1.1
Imports: Imports:
rprojroot rprojroot
''' '''
hello_world_r = '''\ tmp_path.joinpath('DESCRIPTION').write_text(description)
yield
@pytest.fixture
def hello_world_file(tmp_path):
hello_world = '''\
stopifnot( stopifnot(
packageVersion('rprojroot') == '1.0', packageVersion('rprojroot') == '1.0',
packageVersion('gli.clu') == '0.0.0.9000' packageVersion('gli.clu') == '0.0.0.9000'
) )
cat("Hello, World, from R!\n") cat("Hello, World, from R!\n")
''' '''
tmp_path.joinpath('hello-world.R').write_text(hello_world)
yield
tmp_path.joinpath('renv.lock').write_text(renv_lock)
tmp_path.joinpath('DESCRIPTION').write_text(description) @pytest.fixture
tmp_path.joinpath('hello-world.R').write_text(hello_world_r) def renv_folder(tmp_path):
renv_dir = tmp_path.joinpath('renv') renv_dir = tmp_path.joinpath('renv')
renv_dir.mkdir() renv_dir.mkdir()
shutil.copy( activate_r = resource_text('empty_template_activate.R')
os.path.join( renv_dir.joinpath('activate.R').write_text(activate_r)
os.path.dirname(__file__), yield
'../../pre_commit/resources/empty_template_activate.R',
),
renv_dir.joinpath('activate.R'),
)
def test_r_hook(
tmp_path,
renv_lock_file,
description_file,
hello_world_file,
renv_folder,
):
expected = (0, b'Hello, World, from R!\n') expected = (0, b'Hello, World, from R!\n')
assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected
@ -221,3 +243,55 @@ Rscript -e '
args=('hi', 'hello'), args=('hi', 'hello'),
) )
assert ret == (0, b'hi, hello, from R!\n') assert ret == (0, b'hi, hello, from R!\n')
@pytest.fixture
def prefix(tmpdir):
yield Prefix(str(tmpdir))
@pytest.fixture
def installed_environment(
renv_lock_file,
hello_world_file,
renv_folder,
prefix,
):
env_dir = lang_base.environment_dir(
prefix, r.ENVIRONMENT_DIR, r.get_default_version(),
)
r.install_environment(prefix, C.DEFAULT, ())
yield prefix, env_dir
def test_health_check_healthy(installed_environment):
# should be healthy right after creation
prefix, _ = installed_environment
assert r.health_check(prefix, C.DEFAULT) is None
def test_health_check_after_downgrade(installed_environment):
prefix, _ = installed_environment
# pretend the saved installed version is old
with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'):
output = r.health_check(prefix, C.DEFAULT)
assert output is not None
assert output.startswith('Hooks were installed for R version')
@pytest.mark.parametrize('version', ('NULL', 'NA', "''"))
def test_health_check_without_version(prefix, installed_environment, version):
prefix, env_dir = installed_environment
# simulate old pre-commit install by unsetting the installed version
r._execute_vanilla_r_code_as_script(
f'renv::settings$r.version({version})',
prefix=prefix, version=C.DEFAULT, cwd=env_dir,
)
# no R version specified fails as unhealty
msg = 'Hooks were installed with an unknown R version'
check_output = r.health_check(prefix, C.DEFAULT)
assert check_output is not None and check_output.startswith(msg)