Merging upstream version 3.8.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
b7536eb65c
commit
a61ae18588
7 changed files with 180 additions and 49 deletions
|
@ -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/
|
||||||
|
|
|
@ -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
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue