Skip to content

Commit f150211

Browse files
lorenzwalthertasottile
authored andcommitted
add support for R via renv
1 parent b193d9e commit f150211

File tree

15 files changed

+443
-4
lines changed

15 files changed

+443
-4
lines changed

azure-pipelines.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ jobs:
2626
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin"
2727
Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin"
2828
displayName: Add strawberry perl to PATH
29+
- task: PowerShell@2
30+
inputs:
31+
filePath: "testing/get-r.ps1"
32+
displayName: install R
2933
- template: job--python-tox.yml@asottile
3034
parameters:
3135
toxenvs: [py37]
@@ -42,6 +46,8 @@ jobs:
4246
testing/get-swift.sh
4347
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
4448
displayName: install swift
49+
- bash: testing/get-r.sh
50+
displayName: install R
4551
- template: job--python-tox.yml@asottile
4652
parameters:
4753
toxenvs: [pypy3, py36, py37, py38, py39]
@@ -56,3 +62,5 @@ jobs:
5662
testing/get-swift.sh
5763
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
5864
displayName: install swift
65+
- bash: testing/get-r.sh
66+
displayName: install R

pre_commit/languages/all.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pre_commit.languages import perl
1717
from pre_commit.languages import pygrep
1818
from pre_commit.languages import python
19+
from pre_commit.languages import r
1920
from pre_commit.languages import ruby
2021
from pre_commit.languages import rust
2122
from pre_commit.languages import script
@@ -52,6 +53,7 @@ class Language(NamedTuple):
5253
'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
5354
'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
5455
'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
56+
'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
5557
'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
5658
'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
5759
'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

pre_commit/languages/r.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import contextlib
2+
import os
3+
import shlex
4+
import shutil
5+
from typing import Generator
6+
from typing import Sequence
7+
from typing import Tuple
8+
9+
from pre_commit.envcontext import envcontext
10+
from pre_commit.envcontext import PatchesT
11+
from pre_commit.hook import Hook
12+
from pre_commit.languages import helpers
13+
from pre_commit.prefix import Prefix
14+
from pre_commit.util import clean_path_on_failure
15+
from pre_commit.util import cmd_output_b
16+
17+
ENVIRONMENT_DIR = 'renv'
18+
get_default_version = helpers.basic_get_default_version
19+
healthy = helpers.basic_healthy
20+
21+
22+
def get_env_patch(venv: str) -> PatchesT:
23+
return (
24+
('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
25+
)
26+
27+
28+
@contextlib.contextmanager
29+
def in_env(
30+
prefix: Prefix,
31+
language_version: str,
32+
) -> Generator[None, None, None]:
33+
envdir = _get_env_dir(prefix, language_version)
34+
with envcontext(get_env_patch(envdir)):
35+
yield
36+
37+
38+
def _get_env_dir(prefix: Prefix, version: str) -> str:
39+
return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
40+
41+
42+
def _prefix_if_file_entry(
43+
entry: Sequence[str],
44+
prefix: Prefix,
45+
) -> Sequence[str]:
46+
if entry[1] == '-e':
47+
return entry[1:]
48+
else:
49+
return (prefix.path(entry[1]),)
50+
51+
52+
def _entry_validate(entry: Sequence[str]) -> None:
53+
"""
54+
Allowed entries:
55+
# Rscript -e expr
56+
# Rscript path/to/file
57+
"""
58+
if entry[0] != 'Rscript':
59+
raise ValueError('entry must start with `Rscript`.')
60+
61+
if entry[1] == '-e':
62+
if len(entry) > 3:
63+
raise ValueError('You can supply at most one expression.')
64+
elif len(entry) > 2:
65+
raise ValueError(
66+
'The only valid syntax is `Rscript -e {expr}`',
67+
'or `Rscript path/to/hook/script`',
68+
)
69+
70+
71+
def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]:
72+
opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
73+
entry = shlex.split(hook.entry)
74+
_entry_validate(entry)
75+
76+
return (
77+
*entry[:1], *opts,
78+
*_prefix_if_file_entry(entry, hook.prefix),
79+
*hook.args,
80+
)
81+
82+
83+
def install_environment(
84+
prefix: Prefix,
85+
version: str,
86+
additional_dependencies: Sequence[str],
87+
) -> None:
88+
env_dir = _get_env_dir(prefix, version)
89+
with clean_path_on_failure(env_dir):
90+
os.makedirs(env_dir, exist_ok=True)
91+
path_desc_source = prefix.path('DESCRIPTION')
92+
if os.path.exists(path_desc_source):
93+
shutil.copy(path_desc_source, env_dir)
94+
shutil.copy(prefix.path('renv.lock'), env_dir)
95+
cmd_output_b(
96+
'Rscript', '--vanilla', '-e',
97+
"""\
98+
missing_pkgs <- setdiff(
99+
"renv", unname(installed.packages()[, "Package"])
100+
)
101+
options(
102+
repos = c(CRAN = "https://cran.rstudio.com"),
103+
renv.consent = TRUE
104+
)
105+
install.packages(missing_pkgs)
106+
renv::activate()
107+
renv::restore()
108+
activate_statement <- paste0(
109+
'renv::activate("', file.path(getwd()), '"); '
110+
)
111+
writeLines(activate_statement, 'activate.R')
112+
is_package <- tryCatch(
113+
suppressWarnings(
114+
unname(read.dcf('DESCRIPTION')[,'Type'] == "Package")
115+
),
116+
error = function(...) FALSE
117+
)
118+
if (is_package) {
119+
renv::install(normalizePath('.'))
120+
}
121+
""",
122+
cwd=env_dir,
123+
)
124+
if additional_dependencies:
125+
cmd_output_b(
126+
'Rscript', '-e',
127+
'renv::install(commandArgs(trailingOnly = TRUE))',
128+
*additional_dependencies,
129+
cwd=env_dir,
130+
)
131+
132+
133+
def run_hook(
134+
hook: Hook,
135+
file_args: Sequence[str],
136+
color: bool,
137+
) -> Tuple[int, bytes]:
138+
with in_env(hook.prefix, hook.language_version):
139+
return helpers.run_xargs(
140+
hook, _cmd_from_hook(hook), file_args, color=color,
141+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"R": {
3+
"Version": "4.0.3",
4+
"Repositories": [
5+
{
6+
"Name": "CRAN",
7+
"URL": "https://cran.rstudio.com"
8+
}
9+
]
10+
},
11+
"Packages": {
12+
"renv": {
13+
"Package": "renv",
14+
"Version": "0.12.5",
15+
"Source": "Repository",
16+
"Repository": "CRAN",
17+
"Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
18+
}
19+
}
20+
}

pre_commit/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def _git_cmd(*args: str) -> None:
189189
LOCAL_RESOURCES = (
190190
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
191191
'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
192-
'environment.yml', 'Makefile.PL',
192+
'environment.yml', 'Makefile.PL', 'renv.lock',
193193
)
194194

195195
def make_local(self, deps: Sequence[str]) -> str:

testing/gen-languages-all

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import sys
33

44
LANGUAGES = [
5-
'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang',
6-
'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift',
7-
'system',
5+
'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang',
6+
'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script',
7+
'swift', 'system',
88
]
99
FIELDS = [
1010
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',

testing/get-r.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
$dir = $Env:Temp
2+
$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe"
3+
$outputR = "$dir\R-win.exe"
4+
$wcR = New-Object System.Net.WebClient
5+
$wcR.DownloadFile($urlR, $outputR)
6+
Start-Process -FilePath $outputR -ArgumentList "/S /v/qn"

testing/get-r.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
sudo apt install r-base
3+
# create empty folder for user library.
4+
# necessary for non-root users who have
5+
# never installed an R package before.
6+
# Alternatively, we require the renv
7+
# package to be installed already, then we can
8+
# omit that.
9+
Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)'
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# parsing file
2+
- id: parse-file-no-opts-no-args
3+
name: Say hi
4+
entry: Rscript parse-file-no-opts-no-args.R
5+
language: r
6+
types: [r]
7+
- id: parse-file-no-opts-args
8+
name: Say hi
9+
entry: Rscript parse-file-no-opts-args.R
10+
args: [--no-cache]
11+
language: r
12+
types: [r]
13+
## parsing expr
14+
- id: parse-expr-no-opts-no-args-1
15+
name: Say hi
16+
entry: Rscript -e '1+1'
17+
language: r
18+
types: [r]
19+
- id: parse-expr-args-in-entry-2
20+
name: Say hi
21+
entry: Rscript -e '1+1' -e '3' --no-cache3
22+
language: r
23+
types: [r]
24+
# real world
25+
- id: hello-world
26+
name: Say hi
27+
entry: Rscript hello-world.R
28+
args: [blibla]
29+
language: r
30+
types: [r]
31+
- id: hello-world-inline
32+
name: Say hi
33+
entry: |
34+
Rscript -e
35+
'stopifnot(
36+
packageVersion("rprojroot") == "1.0",
37+
packageVersion("gli.clu") == "0.0.0.9000"
38+
)
39+
cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ")
40+
'
41+
args: ['Hi-there']
42+
language: r
43+
types: [r]
44+
- id: additional-deps
45+
name: Check additional deps
46+
entry: Rscript additional-deps.R
47+
language: r
48+
types: [r]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Package: gli.clu
2+
Title: What the Package Does (One Line, Title Case)
3+
Type: Package
4+
Version: 0.0.0.9000
5+
Authors@R:
6+
person(given = "First",
7+
family = "Last",
8+
role = c("aut", "cre"),
9+
email = "first.last@example.com",
10+
comment = c(ORCID = "YOUR-ORCID-ID"))
11+
Description: What the package does (one paragraph).
12+
License: `use_mit_license()`, `use_gpl3_license()` or friends to
13+
pick a license
14+
Encoding: UTF-8
15+
LazyData: true
16+
Roxygen: list(markdown = TRUE)
17+
RoxygenNote: 7.1.1
18+
Imports:
19+
rprojroot

0 commit comments

Comments
 (0)