summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml12
-rw-r--r--CHANGELOG.md9
-rw-r--r--pre_commit/languages/r.py77
-rw-r--r--pre_commit/util.py4
-rw-r--r--setup.cfg2
-rw-r--r--tests/conftest.py25
-rw-r--r--tests/languages/r_test.py100
7 files changed, 180 insertions, 49 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6caee40..1a9a8fe 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,7 +14,7 @@ repos:
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
- rev: v3.12.0
+ rev: v3.13.0
hooks:
- id: reorder-python-imports
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
@@ -24,21 +24,21 @@ repos:
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
- rev: v3.15.2
+ rev: v3.16.0
hooks:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/hhatto/autopep8
- rev: v2.1.0
+ rev: v2.3.1
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
- rev: 7.0.0
+ rev: 7.1.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.10.0
+ rev: v1.11.0
hooks:
- id: mypy
- additional_dependencies: [types-all]
+ additional_dependencies: [types-pyyaml]
exclude: ^testing/resources/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81d5b33..49094bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
==================
diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py
index 93b62bd..5d18bf1 100644
--- a/pre_commit/languages/r.py
+++ b/pre_commit/languages/r.py
@@ -14,13 +14,74 @@ from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
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 win_exe
ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
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
@@ -147,16 +208,14 @@ def install_environment(
with _r_code_in_tempfile(r_code_inst_environment) as f:
cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)
+ _write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
- with in_env(prefix, version):
- with _r_code_in_tempfile(r_code_inst_add) as f:
- cmd_output_b(
- _rscript_exec(), *RSCRIPT_OPTS,
- f,
- *additional_dependencies,
- cwd=env_dir,
- )
+ _execute_vanilla_r_code_as_script(
+ code=r_code_inst_add, prefix=prefix, version=version,
+ args=additional_dependencies,
+ cwd=env_dir,
+ )
def _inline_r_setup(code: str) -> str:
diff --git a/pre_commit/util.py b/pre_commit/util.py
index b75c84a..12aa3c0 100644
--- a/pre_commit/util.py
+++ b/pre_commit/util.py
@@ -205,7 +205,7 @@ else: # pragma: no cover
def _handle_readonly(
func: Callable[[str], object],
path: str,
- exc: Exception,
+ exc: BaseException,
) -> None:
if (
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(
func: Callable[[str], object],
path: str,
- excinfo: tuple[type[Exception], Exception, TracebackType],
+ excinfo: tuple[type[BaseException], BaseException, TracebackType],
) -> None:
return _handle_readonly(func, path, excinfo[1])
diff --git a/setup.cfg b/setup.cfg
index 83c09ac..52b7681 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = pre_commit
-version = 3.7.1
+version = 3.8.0
description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md
long_description_content_type = text/markdown
diff --git a/tests/conftest.py b/tests/conftest.py
index 3076171..bd4af9a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -209,36 +209,25 @@ def log_info_mock():
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:
- def __init__(self, stream):
+ def __init__(self, stream: io.BytesIO) -> None:
self._stream = stream
- def get_bytes(self):
+ def get_bytes(self) -> bytes:
"""Get the output as-if no encoding occurred"""
- data = self._stream.data.getvalue()
- self._stream.data.seek(0)
- self._stream.data.truncate()
+ data = self._stream.getvalue()
+ self._stream.seek(0)
+ self._stream.truncate()
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"""
return self.get_bytes().decode()
@pytest.fixture
def cap_out():
- stream = FakeStream()
+ stream = io.BytesIO()
write = functools.partial(output.write, 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):
diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py
index 02c559c..10919e4 100644
--- a/tests/languages/r_test.py
+++ b/tests/languages/r_test.py
@@ -1,14 +1,17 @@
from __future__ import annotations
import os.path
-import shutil
+from unittest import mock
import pytest
+import pre_commit.constants as C
from pre_commit import envcontext
+from pre_commit import lang_base
from pre_commit.languages import r
from pre_commit.prefix import Prefix
from pre_commit.store import _make_local_repo
+from pre_commit.util import resource_text
from pre_commit.util import win_exe
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'
-def test_r_hook(tmp_path):
+@pytest.fixture
+def renv_lock_file(tmp_path):
renv_lock = '''\
{
"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 = '''\
Package: gli.clu
Title: What the Package Does (One Line, Title Case)
@@ -178,27 +188,39 @@ RoxygenNote: 7.1.1
Imports:
rprojroot
'''
- hello_world_r = '''\
+ tmp_path.joinpath('DESCRIPTION').write_text(description)
+ yield
+
+
+@pytest.fixture
+def hello_world_file(tmp_path):
+ hello_world = '''\
stopifnot(
packageVersion('rprojroot') == '1.0',
packageVersion('gli.clu') == '0.0.0.9000'
)
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)
- tmp_path.joinpath('hello-world.R').write_text(hello_world_r)
+
+@pytest.fixture
+def renv_folder(tmp_path):
renv_dir = tmp_path.joinpath('renv')
renv_dir.mkdir()
- shutil.copy(
- os.path.join(
- os.path.dirname(__file__),
- '../../pre_commit/resources/empty_template_activate.R',
- ),
- renv_dir.joinpath('activate.R'),
- )
+ activate_r = resource_text('empty_template_activate.R')
+ renv_dir.joinpath('activate.R').write_text(activate_r)
+ yield
+
+def test_r_hook(
+ tmp_path,
+ renv_lock_file,
+ description_file,
+ hello_world_file,
+ renv_folder,
+):
expected = (0, b'Hello, World, from R!\n')
assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected
@@ -221,3 +243,55 @@ Rscript -e '
args=('hi', 'hello'),
)
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)