Skip to content

Commit 27a4d07

Browse files
authored
Release v0.0.5 and make it work with pytask v0.0.9. (#5)
1 parent 13f97b7 commit 27a4d07

15 files changed

+337
-73
lines changed

.conda/meta.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ requirements:
2020

2121
run:
2222
- python >=3.6
23-
- pytask >=0.0.7
23+
- pytask >=0.0.9
2424

2525
test:
2626
requires:
2727
- pytest
28+
- pytask-parallel >=0.0.4
2829
- r-base
2930
source_files:
3031
- tox.ini
@@ -34,6 +35,7 @@ test:
3435
- pytask --help
3536
- pytask markers
3637
- pytask clean
38+
- pytask collect
3739

3840
- pytest tests
3941

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ chronological order. Releases follow `semantic versioning <https://semver.org/>`
66
all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask-r>`_.
77

88

9+
0.0.5 - 2020-10-30
10+
------------------
11+
12+
- :gh:`5` makes pytask-r work with pytask v0.0.9.
13+
14+
915
0.0.4 - 2020-10-14
1016
------------------
1117

README.rst

+52-6
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,18 @@ Here is an example where you want to run ``script.r``.
7272
pass
7373
7474
Note that, you need to apply the ``@pytask.mark.r`` marker so that pytask-r handles the
75-
task. The executable script must be the first dependency. Other dependencies can be
76-
added after that.
75+
task.
76+
77+
If you are wondering why the function body is empty, know that pytask-r replaces the
78+
body with a predefined internal function. See the section on implementation details for
79+
more information.
80+
81+
82+
Multiple dependencies and products
83+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
84+
85+
What happens if a task has more dependencies? Using a list, the R script which should be
86+
executed must be found in the first position of the list.
7787

7888
.. code-block:: python
7989
@@ -83,9 +93,31 @@ added after that.
8393
def task_run_r_script():
8494
pass
8595
86-
If you are wondering why the function body is empty, know that pytask-r replaces the
87-
body with a predefined internal function. See the section on implementation details for
88-
more information.
96+
If you use a dictionary to pass dependencies to the task, pytask-r will, first, look
97+
for a ``"source"`` key in the dictionary and, secondly, under the key ``0``.
98+
99+
.. code-block:: python
100+
101+
@pytask.mark.depends_on({"source": "script.r", "input": "input.rds"})
102+
def task_run_r_script():
103+
pass
104+
105+
106+
# or
107+
108+
109+
@pytask.mark.depends_on({0: "script.r", "input": "input.rds"})
110+
def task_run_r_script():
111+
pass
112+
113+
114+
# or two decorators for the function, if you do not assign a name to the input.
115+
116+
117+
@pytask.mark.depends_on({"source": "script.r"})
118+
@pytask.mark.depends_on("input.rds")
119+
def task_run_r_script():
120+
pass
89121
90122
91123
Command Line Arguments
@@ -138,12 +170,26 @@ include the ``@pytask.mark.r`` decorator in the parametrization just like with
138170
@pytask.mark.depends_on("script.r")
139171
@pytask.mark.parametrize(
140172
"produces, r",
141-
[("output_1.rds", ["--vanilla", 1]), ("output_2.rds", ["--vanilla", 2])],
173+
[
174+
("output_1.rds", (["--vanilla", "1"],)),
175+
("output_2.rds", (["--vanilla", "2"],)),
176+
],
142177
)
143178
def task_execute_r_script():
144179
pass
145180
146181
182+
Configuration
183+
-------------
184+
185+
If you want to change the name of the key which identifies the R script, change the
186+
following default configuration in your pytask configuration file.
187+
188+
.. code-block:: ini
189+
190+
r_source_key = source
191+
192+
147193
Implementation Details
148194
----------------------
149195

environment.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ dependencies:
1313
- conda-verify
1414

1515
# Package dependencies
16-
- pytask >= 0.0.7
16+
- pytask >=0.0.9
17+
- pytask-parallel >=0.0.4
1718
- r-base
1819

1920
# Misc

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.0.4
2+
current_version = 0.0.5
33
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))(\-?((dev)?(?P<dev>\d+))?)
44
serialize =
55
{major}.{minor}.{patch}dev{dev}

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
setup(
55
name="pytask-r",
6-
version="0.0.4",
6+
version="0.0.5",
77
packages=find_packages(where="src"),
88
package_dir={"": "src"},
99
entry_points={"pytask": ["pytask_r = pytask_r.plugin"]},

src/pytask_r/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.4"
1+
__version__ = "0.0.5"

src/pytask_r/collect.py

+36-21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import copy
33
import functools
44
import subprocess
5+
from pathlib import Path
56
from typing import Iterable
67
from typing import Optional
78
from typing import Union
@@ -12,7 +13,6 @@
1213
from _pytask.nodes import FilePathNode
1314
from _pytask.nodes import PythonFunctionTask
1415
from _pytask.parametrize import _copy_func
15-
from _pytask.shared import to_list
1616

1717

1818
def r(options: Optional[Union[str, Iterable[str]]] = None):
@@ -31,10 +31,9 @@ def r(options: Optional[Union[str, Iterable[str]]] = None):
3131
return options
3232

3333

34-
def run_r_script(depends_on, r):
34+
def run_r_script(r):
3535
"""Run an R script."""
36-
script = to_list(depends_on)[0]
37-
subprocess.run(["Rscript", script.as_posix(), *r], check=True)
36+
subprocess.run(r, check=True)
3837

3938

4039
@hookimpl
@@ -50,32 +49,37 @@ def pytask_collect_task(session, path, name, obj):
5049
task = PythonFunctionTask.from_path_name_function_session(
5150
path, name, obj, session
5251
)
52+
53+
return task
54+
55+
56+
@hookimpl
57+
def pytask_collect_task_teardown(session, task):
58+
"""Perform some checks."""
59+
if get_specific_markers_from_task(task, "r"):
60+
source = _get_node_from_dictionary(task.depends_on, "source")
61+
if isinstance(source, FilePathNode) and source.value.suffix not in [".r", ".R"]:
62+
raise ValueError(
63+
"The first dependency of an R task must be the executable script."
64+
)
65+
5366
r_function = _copy_func(run_r_script)
5467
r_function.pytaskmark = copy.deepcopy(task.function.pytaskmark)
5568

5669
merged_marks = _merge_all_markers(task)
5770
args = r(*merged_marks.args, **merged_marks.kwargs)
58-
r_function = functools.partial(r_function, r=args)
71+
options = _prepare_cmd_options(session, task, args)
72+
r_function = functools.partial(r_function, r=options)
5973

6074
task.function = r_function
6175

62-
return task
63-
6476

65-
@hookimpl
66-
def pytask_collect_task_teardown(task):
67-
"""Perform some checks.
68-
69-
Remove is task is none check with pytask 0.0.9.
70-
71-
"""
72-
if task is not None and get_specific_markers_from_task(task, "r"):
73-
if isinstance(task.depends_on[0], FilePathNode) and task.depends_on[
74-
0
75-
].value.suffix not in [".r", ".R"]:
76-
raise ValueError(
77-
"The first dependency of an R task must be the executable script."
78-
)
77+
def _get_node_from_dictionary(obj, key, fallback=0):
78+
if isinstance(obj, Path):
79+
pass
80+
elif isinstance(obj, dict):
81+
obj = obj.get(key) or obj.get(fallback)
82+
return obj
7983

8084

8185
def _merge_all_markers(task):
@@ -85,3 +89,14 @@ def _merge_all_markers(task):
8589
for mark_ in r_marks[1:]:
8690
mark = mark.combined_with(mark_)
8791
return mark
92+
93+
94+
def _prepare_cmd_options(session, task, args):
95+
"""Prepare the command line arguments to execute the do-file.
96+
97+
The last entry changes the name of the log file. We take the task id as a name which
98+
is unique and does not cause any errors when parallelizing the execution.
99+
100+
"""
101+
source = _get_node_from_dictionary(task.depends_on, session.config["r_source_key"])
102+
return ["Rscript", source.value.as_posix(), *args]

src/pytask_r/config.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
@hookimpl
6-
def pytask_parse_config(config):
6+
def pytask_parse_config(config, config_from_file):
77
"""Register the r marker."""
88
config["markers"]["r"] = "Tasks which are executed with Rscript."
9+
config["r_source_key"] = config_from_file.get("r_source_key", "source")

tests/conftest.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import shutil
22

33
import pytest
4-
4+
from click.testing import CliRunner
55

66
needs_rscript = pytest.mark.skipif(
77
shutil.which("Rscript") is None, reason="R with Rscript needs to be installed."
88
)
9+
10+
11+
@pytest.fixture()
12+
def runner():
13+
return CliRunner()

tests/test_collect.py

+55-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import pytest
55
from _pytask.mark import Mark
66
from _pytask.nodes import FilePathNode
7+
from pytask_r.collect import _get_node_from_dictionary
78
from pytask_r.collect import _merge_all_markers
9+
from pytask_r.collect import _prepare_cmd_options
810
from pytask_r.collect import pytask_collect_task
911
from pytask_r.collect import pytask_collect_task_teardown
1012
from pytask_r.collect import r
@@ -82,12 +84,61 @@ def test_pytask_collect_task(name, expected):
8284
(["input.rds", "script.R"], ["any_out.rds"], pytest.raises(ValueError)),
8385
],
8486
)
85-
def test_pytask_collect_task_teardown(depends_on, produces, expectation):
87+
@pytest.mark.parametrize("r_source_key", ["source", "script"])
88+
def test_pytask_collect_task_teardown(depends_on, produces, expectation, r_source_key):
89+
session = DummyClass()
90+
session.config = {"r_source_key": r_source_key}
91+
8692
task = DummyClass()
87-
task.depends_on = [FilePathNode(n.split(".")[0], Path(n)) for n in depends_on]
88-
task.produces = [FilePathNode(n.split(".")[0], Path(n)) for n in produces]
93+
task.depends_on = {
94+
i: FilePathNode(n.split(".")[0], Path(n)) for i, n in enumerate(depends_on)
95+
}
96+
task.produces = {
97+
i: FilePathNode(n.split(".")[0], Path(n)) for i, n in enumerate(produces)
98+
}
8999
task.markers = [Mark("r", (), {})]
90100
task.function = task_dummy
101+
task.function.pytaskmark = task.markers
91102

92103
with expectation:
93-
pytask_collect_task_teardown(task)
104+
pytask_collect_task_teardown(session, task)
105+
106+
107+
@pytest.mark.unit
108+
@pytest.mark.parametrize(
109+
"obj, key, expected",
110+
[
111+
(1, "asds", 1),
112+
(1, None, 1),
113+
({"a": 1}, "a", 1),
114+
({0: 1}, "a", 1),
115+
],
116+
)
117+
def test_get_node_from_dictionary(obj, key, expected):
118+
result = _get_node_from_dictionary(obj, key)
119+
assert result == expected
120+
121+
122+
@pytest.mark.unit
123+
@pytest.mark.parametrize(
124+
"args",
125+
[
126+
[],
127+
["a"],
128+
["a", "b"],
129+
],
130+
)
131+
@pytest.mark.parametrize("r_source_key", ["source", "script"])
132+
def test_prepare_cmd_options(args, r_source_key):
133+
session = DummyClass()
134+
session.config = {"r_source_key": r_source_key}
135+
136+
node = DummyClass()
137+
node.value = Path("script.r")
138+
task = DummyClass()
139+
task.depends_on = {r_source_key: node}
140+
task.name = "task"
141+
142+
result = _prepare_cmd_options(session, task, args)
143+
144+
assert result == ["Rscript", "script.r", *args]

0 commit comments

Comments
 (0)