Skip to content

Commit

Permalink
Merge pull request #1799 from lorenzwalthert/renv
Browse files Browse the repository at this point in the history
Support for R / renv as a language
  • Loading branch information
asottile committed Mar 7, 2021
2 parents b193d9e + f150211 commit 19bffaa
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 4 deletions.
8 changes: 8 additions & 0 deletions azure-pipelines.yml
Expand Up @@ -26,6 +26,10 @@ jobs:
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin"
Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin"
displayName: Add strawberry perl to PATH
- task: PowerShell@2
inputs:
filePath: "testing/get-r.ps1"
displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [py37]
Expand All @@ -42,6 +46,8 @@ jobs:
testing/get-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift
- bash: testing/get-r.sh
displayName: install R
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy3, py36, py37, py38, py39]
Expand All @@ -56,3 +62,5 @@ jobs:
testing/get-swift.sh
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
displayName: install swift
- bash: testing/get-r.sh
displayName: install R
2 changes: 2 additions & 0 deletions pre_commit/languages/all.py
Expand Up @@ -16,6 +16,7 @@
from pre_commit.languages import perl
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import r
from pre_commit.languages import ruby
from pre_commit.languages import rust
from pre_commit.languages import script
Expand Down Expand Up @@ -52,6 +53,7 @@ class Language(NamedTuple):
'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
'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.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
Expand Down
141 changes: 141 additions & 0 deletions pre_commit/languages/r.py
@@ -0,0 +1,141 @@
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
from pre_commit.hook import Hook
from pre_commit.languages import helpers
from pre_commit.prefix import Prefix
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b

ENVIRONMENT_DIR = 'renv'
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy


def get_env_patch(venv: str) -> PatchesT:
return (
('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
)


@contextlib.contextmanager
def in_env(
prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
envdir = _get_env_dir(prefix, language_version)
with envcontext(get_env_patch(envdir)):
yield


def _get_env_dir(prefix: Prefix, version: str) -> str:
return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))


def _prefix_if_file_entry(
entry: Sequence[str],
prefix: Prefix,
) -> Sequence[str]:
if entry[1] == '-e':
return entry[1:]
else:
return (prefix.path(entry[1]),)


def _entry_validate(entry: Sequence[str]) -> None:
"""
Allowed entries:
# Rscript -e expr
# Rscript path/to/file
"""
if entry[0] != 'Rscript':
raise ValueError('entry must start with `Rscript`.')

if entry[1] == '-e':
if len(entry) > 3:
raise ValueError('You can supply at most one expression.')
elif len(entry) > 2:
raise ValueError(
'The only valid syntax is `Rscript -e {expr}`',
'or `Rscript path/to/hook/script`',
)


def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]:
opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
entry = shlex.split(hook.entry)
_entry_validate(entry)

return (
*entry[:1], *opts,
*_prefix_if_file_entry(entry, hook.prefix),
*hook.args,
)


def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
env_dir = _get_env_dir(prefix, version)
with clean_path_on_failure(env_dir):
os.makedirs(env_dir, exist_ok=True)
path_desc_source = prefix.path('DESCRIPTION')
if os.path.exists(path_desc_source):
shutil.copy(path_desc_source, env_dir)
shutil.copy(prefix.path('renv.lock'), env_dir)
cmd_output_b(
'Rscript', '--vanilla', '-e',
"""\
missing_pkgs <- setdiff(
"renv", unname(installed.packages()[, "Package"])
)
options(
repos = c(CRAN = "https://cran.rstudio.com"),
renv.consent = TRUE
)
install.packages(missing_pkgs)
renv::activate()
renv::restore()
activate_statement <- paste0(
'renv::activate("', file.path(getwd()), '"); '
)
writeLines(activate_statement, 'activate.R')
is_package <- tryCatch(
suppressWarnings(
unname(read.dcf('DESCRIPTION')[,'Type'] == "Package")
),
error = function(...) FALSE
)
if (is_package) {
renv::install(normalizePath('.'))
}
""",
cwd=env_dir,
)
if additional_dependencies:
cmd_output_b(
'Rscript', '-e',
'renv::install(commandArgs(trailingOnly = TRUE))',
*additional_dependencies,
cwd=env_dir,
)


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, _cmd_from_hook(hook), file_args, color=color,
)
20 changes: 20 additions & 0 deletions pre_commit/resources/empty_template_renv.lock
@@ -0,0 +1,20 @@
{
"R": {
"Version": "4.0.3",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cran.rstudio.com"
}
]
},
"Packages": {
"renv": {
"Package": "renv",
"Version": "0.12.5",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
}
}
}
2 changes: 1 addition & 1 deletion pre_commit/store.py
Expand Up @@ -189,7 +189,7 @@ def _git_cmd(*args: str) -> None:
LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
'environment.yml', 'Makefile.PL',
'environment.yml', 'Makefile.PL', 'renv.lock',
)

def make_local(self, deps: Sequence[str]) -> str:
Expand Down
6 changes: 3 additions & 3 deletions testing/gen-languages-all
Expand Up @@ -2,9 +2,9 @@
import sys

LANGUAGES = [
'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang',
'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift',
'system',
'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang',
'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script',
'swift', 'system',
]
FIELDS = [
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',
Expand Down
6 changes: 6 additions & 0 deletions testing/get-r.ps1
@@ -0,0 +1,6 @@
$dir = $Env:Temp
$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe"
$outputR = "$dir\R-win.exe"
$wcR = New-Object System.Net.WebClient
$wcR.DownloadFile($urlR, $outputR)
Start-Process -FilePath $outputR -ArgumentList "/S /v/qn"
9 changes: 9 additions & 0 deletions testing/get-r.sh
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
sudo apt install r-base
# create empty folder for user library.
# necessary for non-root users who have
# never installed an R package before.
# Alternatively, we require the renv
# package to be installed already, then we can
# omit that.
Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)'
48 changes: 48 additions & 0 deletions testing/resources/r_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,48 @@
# parsing file
- id: parse-file-no-opts-no-args
name: Say hi
entry: Rscript parse-file-no-opts-no-args.R
language: r
types: [r]
- id: parse-file-no-opts-args
name: Say hi
entry: Rscript parse-file-no-opts-args.R
args: [--no-cache]
language: r
types: [r]
## parsing expr
- id: parse-expr-no-opts-no-args-1
name: Say hi
entry: Rscript -e '1+1'
language: r
types: [r]
- id: parse-expr-args-in-entry-2
name: Say hi
entry: Rscript -e '1+1' -e '3' --no-cache3
language: r
types: [r]
# real world
- id: hello-world
name: Say hi
entry: Rscript hello-world.R
args: [blibla]
language: r
types: [r]
- id: hello-world-inline
name: Say hi
entry: |
Rscript -e
'stopifnot(
packageVersion("rprojroot") == "1.0",
packageVersion("gli.clu") == "0.0.0.9000"
)
cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ")
'
args: ['Hi-there']
language: r
types: [r]
- id: additional-deps
name: Check additional deps
entry: Rscript additional-deps.R
language: r
types: [r]
19 changes: 19 additions & 0 deletions testing/resources/r_hooks_repo/DESCRIPTION
@@ -0,0 +1,19 @@
Package: gli.clu
Title: What the Package Does (One Line, Title Case)
Type: Package
Version: 0.0.0.9000
Authors@R:
person(given = "First",
family = "Last",
role = c("aut", "cre"),
email = "first.last@example.com",
comment = c(ORCID = "YOUR-ORCID-ID"))
Description: What the package does (one paragraph).
License: `use_mit_license()`, `use_gpl3_license()` or friends to
pick a license
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.1
Imports:
rprojroot
2 changes: 2 additions & 0 deletions testing/resources/r_hooks_repo/additional-deps.R
@@ -0,0 +1,2 @@
suppressPackageStartupMessages(library("cachem"))
cat("OK\n")
5 changes: 5 additions & 0 deletions testing/resources/r_hooks_repo/hello-world.R
@@ -0,0 +1,5 @@
stopifnot(
packageVersion('rprojroot') == '1.0',
packageVersion('gli.clu') == '0.0.0.9000'
)
cat("Hello, World, from R!\n")
27 changes: 27 additions & 0 deletions testing/resources/r_hooks_repo/renv.lock
@@ -0,0 +1,27 @@
{
"R": {
"Version": "4.0.3",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"renv": {
"Package": "renv",
"Version": "0.12.5",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
},
"rprojroot": {
"Package": "rprojroot",
"Version": "1.0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "86704667fe0860e4fec35afdfec137f3"
}
}
}

0 comments on commit 19bffaa

Please sign in to comment.