Skip to content

Commit 143aa77

Browse files
authored
Release v0.4.0. (#42)
1 parent 70ca0e8 commit 143aa77

18 files changed

+250
-394
lines changed

.pre-commit-config.yaml

+1-4
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,10 @@ repos:
6161
rev: 'v1.5.1'
6262
hooks:
6363
- id: mypy
64-
args: [
65-
--no-strict-optional,
66-
--ignore-missing-imports,
67-
]
6864
additional_dependencies: [
6965
attrs>=21.3.0,
7066
click,
67+
pytask>=0.4.0,
7168
types-PyYAML,
7269
types-setuptools
7370
]

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ chronological order. Releases follow [semantic versioning](https://semver.org/)
55
releases are available on [PyPI](https://pypi.org/project/pytask-r) and
66
[Anaconda.org](https://anaconda.org/conda-forge/pytask-r).
77

8+
## 0.4.0 - 2023-10-08
9+
10+
- {pull}`42` makes the package compatible with pytask v0.4.0.
11+
812
## 0.3.0 - 2023-xx-xx
913

1014
- {pull}`33` deprecates INI configurations and aligns the plugin with pytask v0.3.

README.md

+5-6
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,10 @@ The `.json` file is stored in the same folder as the task in a `.pytask` directo
9696
To parse the JSON file, you need to install
9797
[jsonlite](https://github.com/jeroen/jsonlite).
9898

99-
You can also pass any other information to your script by using the `@pytask.mark.task`
100-
decorator.
99+
You can also pass any other information to your script by using the `@task` decorator.
101100

102101
```python
103-
@pytask.mark.task(kwargs={"number": 1})
102+
@task(kwargs={"number": 1})
104103
@pytask.mark.r(script="script.r")
105104
@pytask.mark.produces("out.rds")
106105
def task_run_r_script():
@@ -146,20 +145,20 @@ different outputs.
146145
```python
147146
for i in range(2):
148147

149-
@pytask.mark.task
148+
@task
150149
@pytask.mark.r(script=f"script_{i}.r")
151150
@pytask.mark.produces(f"out_{i}.csv")
152151
def task_execute_r_script():
153152
pass
154153
```
155154

156155
If you want to pass different inputs to the same R script, pass these arguments with the
157-
`kwargs` keyword of the `@pytask.mark.task` decorator.
156+
`kwargs` keyword of the `@task` decorator.
158157

159158
```python
160159
for i in range(2):
161160

162-
@pytask.mark.task(kwargs={"i": i})
161+
@task(kwargs={"i": i})
163162
@pytask.mark.r(script="script.r")
164163
@pytask.mark.produces(f"output_{i}.csv")
165164
def task_execute_r_script():

environment.yml

+4-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ name: pytask-r
22

33
channels:
44
- conda-forge
5+
- conda-forge/label/pytask_rc
6+
- conda-forge/label/pytask_parallel_rc
57
- nodefaults
68

79
dependencies:
@@ -10,14 +12,9 @@ dependencies:
1012
- setuptools_scm
1113
- toml
1214

13-
# Conda
14-
- anaconda-client
15-
- conda-build
16-
- conda-verify
17-
1815
# Package dependencies
19-
- pytask >=0.3
20-
- pytask-parallel >=0.3
16+
- pytask >=0.4.0
17+
- pytask-parallel >=0.4.0
2118

2219
- r-base >4
2320
- r-jsonlite

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ ignore_errors = true
2525

2626

2727
[tool.ruff]
28-
target-version = "py37"
28+
target-version = "py38"
2929
select = ["ALL"]
3030
fix = true
3131
extend-ignore = [
@@ -67,7 +67,7 @@ convention = "numpy"
6767

6868
[tool.pytest.ini_options]
6969
# Do not add src since it messes with the loading of pytask-parallel as a plugin.
70-
testpaths = ["test"]
70+
testpaths = ["tests"]
7171
markers = [
7272
"wip: Tests that are work-in-progress.",
7373
"unit: Flag for unit tests which target mainly a single function.",

setup.cfg

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ project_urls =
2626
packages = find:
2727
install_requires =
2828
click
29-
pybaum>=0.1.1
30-
pytask>=0.3
29+
pluggy>=1.0.0
30+
pytask>=0.4.0
3131
python_requires = >=3.8
3232
include_package_data = True
3333
package_dir = =src

src/pytask_r/collect.py

+98-60
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,53 @@
11
"""Collect tasks."""
22
from __future__ import annotations
33

4-
import functools
54
import subprocess
5+
import warnings
66
from pathlib import Path
7-
from types import FunctionType
87
from typing import Any
98

10-
from pytask import depends_on
119
from pytask import has_mark
1210
from pytask import hookimpl
11+
from pytask import is_task_function
1312
from pytask import Mark
14-
from pytask import parse_nodes
15-
from pytask import produces
13+
from pytask import NodeInfo
14+
from pytask import parse_dependencies_from_task_function
15+
from pytask import parse_products_from_task_function
16+
from pytask import PathNode
17+
from pytask import PTask
18+
from pytask import PythonNode
1619
from pytask import remove_marks
1720
from pytask import Session
1821
from pytask import Task
22+
from pytask import TaskWithoutPath
23+
from pytask_r.serialization import create_path_to_serialized
1924
from pytask_r.serialization import SERIALIZERS
2025
from pytask_r.shared import r
21-
from pytask_r.shared import R_SCRIPT_KEY
2226

2327

24-
def run_r_script(script: Path, options: list[str], serialized: Path) -> None:
28+
def run_r_script(
29+
_script: Path, _options: list[str], _serialized: Path, **kwargs: Any # noqa: ARG001
30+
) -> None:
2531
"""Run an R script."""
26-
cmd = ["Rscript", script.as_posix(), *options, str(serialized)]
32+
cmd = ["Rscript", _script.as_posix(), *_options, str(_serialized)]
2733
print("Executing " + " ".join(cmd) + ".") # noqa: T201
2834
subprocess.run(cmd, check=True) # noqa: S603
2935

3036

3137
@hookimpl
3238
def pytask_collect_task(
33-
session: Session, path: Path, name: str, obj: Any
34-
) -> Task | None:
39+
session: Session, path: Path | None, name: str, obj: Any
40+
) -> PTask | None:
3541
"""Perform some checks."""
3642
__tracebackhide__ = True
3743

3844
if (
3945
(name.startswith("task_") or has_mark(obj, "task"))
40-
and callable(obj)
46+
and is_task_function(obj)
4147
and has_mark(obj, "r")
4248
):
49+
# Parse @pytask.mark.r decorator.
4350
obj, marks = remove_marks(obj, "r")
44-
4551
if len(marks) > 1:
4652
raise ValueError(
4753
f"Task {name!r} has multiple @pytask.mark.r marks, but only one is "
@@ -54,40 +60,97 @@ def pytask_collect_task(
5460
default_serializer=session.config["r_serializer"],
5561
default_suffix=session.config["r_suffix"],
5662
)
57-
script, options, _, _ = r(**marks[0].kwargs)
63+
script, options, _, suffix = r(**marks[0].kwargs)
5864

5965
obj.pytask_meta.markers.append(mark)
6066

61-
dependencies = parse_nodes(session, path, name, obj, depends_on)
62-
products = parse_nodes(session, path, name, obj, produces)
67+
# Collect the nodes in @pytask.mark.julia and validate them.
68+
path_nodes = Path.cwd() if path is None else path.parent
6369

64-
markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else []
65-
kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {}
66-
67-
task = Task(
68-
base_name=name,
69-
path=path,
70-
function=_copy_func(run_r_script), # type: ignore[arg-type]
71-
depends_on=dependencies,
72-
produces=products,
73-
markers=markers,
74-
kwargs=kwargs,
75-
)
70+
if isinstance(script, str):
71+
warnings.warn(
72+
"Passing a string to the @pytask.mark.r parameter 'script' is "
73+
"deprecated. Please, use a pathlib.Path instead.",
74+
stacklevel=1,
75+
)
76+
script = Path(script)
7677

7778
script_node = session.hook.pytask_collect_node(
78-
session=session, path=path, node=script
79+
session=session,
80+
path=path_nodes,
81+
node_info=NodeInfo(
82+
arg_name="_script",
83+
path=(),
84+
value=script,
85+
task_path=path,
86+
task_name=name,
87+
),
88+
)
89+
90+
if not (isinstance(script_node, PathNode) and script_node.path.suffix == ".r"):
91+
raise ValueError(
92+
"The 'script' keyword of the @pytask.mark.r decorator must point "
93+
f"to Julia file with the .r suffix, but it is {script_node}."
94+
)
95+
96+
options_node = session.hook.pytask_collect_node(
97+
session=session,
98+
path=path_nodes,
99+
node_info=NodeInfo(
100+
arg_name="_options",
101+
path=(),
102+
value=options,
103+
task_path=path,
104+
task_name=name,
105+
),
106+
)
107+
108+
dependencies = parse_dependencies_from_task_function(
109+
session, path, name, path_nodes, obj
110+
)
111+
products = parse_products_from_task_function(
112+
session, path, name, path_nodes, obj
79113
)
80114

81-
if isinstance(task.depends_on, dict):
82-
task.depends_on[R_SCRIPT_KEY] = script_node
83-
task.attributes["r_keep_dict"] = True
115+
# Add script
116+
dependencies["_script"] = script_node
117+
dependencies["_options"] = options_node
118+
119+
markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else []
120+
121+
task: PTask
122+
if path is None:
123+
task = TaskWithoutPath(
124+
name=name,
125+
function=run_r_script,
126+
depends_on=dependencies,
127+
produces=products,
128+
markers=markers,
129+
)
84130
else:
85-
task.depends_on = {0: task.depends_on, R_SCRIPT_KEY: script_node}
86-
task.attributes["r_keep_dict"] = False
131+
task = Task(
132+
base_name=name,
133+
path=path,
134+
function=run_r_script,
135+
depends_on=dependencies,
136+
produces=products,
137+
markers=markers,
138+
)
87139

88-
task.function = functools.partial(
89-
task.function, script=task.depends_on[R_SCRIPT_KEY].path, options=options
140+
# Add serialized node that depends on the task id.
141+
serialized = create_path_to_serialized(task, suffix) # type: ignore[arg-type]
142+
serialized_node = session.hook.pytask_collect_node(
143+
session=session,
144+
path=path_nodes,
145+
node_info=NodeInfo(
146+
arg_name="_serialized",
147+
path=(),
148+
value=PythonNode(value=serialized),
149+
task_path=path,
150+
task_name=name,
151+
),
90152
)
153+
task.depends_on["_serialized"] = serialized_node
91154

92155
return task
93156
return None
@@ -120,28 +183,3 @@ def _parse_r_mark(
120183

121184
mark = Mark("r", (), parsed_kwargs)
122185
return mark
123-
124-
125-
def _copy_func(func: FunctionType) -> FunctionType:
126-
"""Create a copy of a function.
127-
128-
Based on https://stackoverflow.com/a/13503277/7523785.
129-
130-
Example
131-
-------
132-
>>> def _func(): pass
133-
>>> copied_func = _copy_func(_func)
134-
>>> _func is copied_func
135-
False
136-
137-
"""
138-
new_func = FunctionType(
139-
func.__code__,
140-
func.__globals__,
141-
name=func.__name__,
142-
argdefs=func.__defaults__,
143-
closure=func.__closure__,
144-
)
145-
new_func = functools.update_wrapper(new_func, func)
146-
new_func.__kwdefaults__ = func.__kwdefaults__
147-
return new_func

0 commit comments

Comments
 (0)