Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for R / renv as a language #1799

Merged
merged 1 commit into from Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}
}
}