Skip to content

Commit 612aca3

Browse files
authored
Support pytask v0.4. (#36)
1 parent ae2334f commit 612aca3

15 files changed

+279
-344
lines changed

CHANGES.md

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

8-
## 0.3.0 - 2023-xx-xx
8+
## 0.4.0 - 2023-10-08
9+
10+
- {pull}`36` makes pytask-stata compatible with pytask v0.4.0.
11+
12+
## 0.3.0 - 2023-01-23
913

1014
- {pull}`24` adds ruff and refurb.
1115
- {pull}`25` adds docformatter.

MANIFEST.in

-12
This file was deleted.

environment.yml

-25
This file was deleted.

pyproject.toml

+23-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["hatchling", "hatch_vcs"]
3+
build-backend = "hatchling.build"
44

55
[project]
66
name = "pytask_stata"
@@ -15,7 +15,7 @@ classifiers = [
1515
"Programming Language :: Python :: 3 :: Only",
1616
]
1717
requires-python = ">=3.8"
18-
dependencies = ["click", "pytask>=0.3,<0.4"]
18+
dependencies = ["click", "pytask>=0.4"]
1919
dynamic = ["version"]
2020

2121
[project.readme]
@@ -36,19 +36,28 @@ Changelog = "https://github.com/pytask-dev/pytask-stata/blob/main/CHANGES.md"
3636
[project.entry-points]
3737
pytask = { pytask_stata = "pytask_stata.plugin" }
3838

39-
[tool.setuptools]
40-
include-package-data = true
41-
package-dir = { "" = "src" }
42-
zip-safe = false
43-
platforms = ["any"]
44-
license-files = ["LICENSE"]
39+
[tool.rye]
40+
managed = true
41+
dev-dependencies = [
42+
"tox-uv>=1.8.2",
43+
]
44+
45+
[tool.hatch.build.hooks.vcs]
46+
version-file = "src/pytask_stata/_version.py"
47+
48+
[tool.hatch.build.targets.sdist]
49+
exclude = ["tests"]
50+
only-packages = true
51+
52+
[tool.hatch.build.targets.wheel]
53+
exclude = ["tests"]
54+
only-packages = true
4555

46-
[tool.setuptools.packages.find]
47-
where = ["src"]
48-
namespaces = false
56+
[tool.hatch.version]
57+
source = "vcs"
4958

50-
[tool.setuptools_scm]
51-
write_to = "src/pytask_stata/_version.py"
59+
[tool.hatch.metadata]
60+
allow-direct-references = true
5261

5362
[tool.mypy]
5463
files = ["src", "tests"]

src/pytask_stata/collect.py

+124-72
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,40 @@
44

55
import functools
66
import subprocess
7-
from types import FunctionType
8-
from typing import TYPE_CHECKING
7+
import warnings
8+
from pathlib import Path
99
from typing import Any
1010

1111
from pytask import Mark
12+
from pytask import NodeInfo
13+
from pytask import PathNode
14+
from pytask import PTask
15+
from pytask import PythonNode
1216
from pytask import Session
1317
from pytask import Task
14-
from pytask import depends_on
18+
from pytask import TaskWithoutPath
1519
from pytask import has_mark
1620
from pytask import hookimpl
17-
from pytask import parse_nodes
18-
from pytask import produces
21+
from pytask import is_task_function
22+
from pytask import parse_dependencies_from_task_function
23+
from pytask import parse_products_from_task_function
1924
from pytask import remove_marks
2025

2126
from pytask_stata.shared import convert_task_id_to_name_of_log_file
2227
from pytask_stata.shared import stata
2328

24-
if TYPE_CHECKING:
25-
from pathlib import Path
26-
2729

2830
def run_stata_script(
29-
executable: str, script: Path, options: list[str], log_name: list[str], cwd: Path
31+
_executable: str,
32+
_script: Path,
33+
_options: list[str],
34+
_log_name: str,
35+
_cwd: Path,
3036
) -> None:
3137
"""Run an R script."""
32-
cmd = [executable, "-e", "do", script.as_posix(), *options, *log_name]
38+
cmd = [_executable, "-e", "do", _script.as_posix(), *_options, f"-{_log_name}"]
3339
print("Executing " + " ".join(cmd) + ".") # noqa: T201
34-
subprocess.run(cmd, cwd=cwd, check=True) # noqa: S603
40+
subprocess.run(cmd, cwd=_cwd, check=True) # noqa: S603
3541

3642

3743
@hookimpl
@@ -43,11 +49,11 @@ def pytask_collect_task(
4349

4450
if (
4551
(name.startswith("task_") or has_mark(obj, "task"))
46-
and callable(obj)
52+
and is_task_function(obj)
4753
and has_mark(obj, "stata")
4854
):
55+
# Parse the @pytask.mark.stata decorator.
4956
obj, marks = remove_marks(obj, "stata")
50-
5157
if len(marks) > 1:
5258
msg = (
5359
f"Task {name!r} has multiple @pytask.mark.stata marks, but only one is "
@@ -57,50 +63,123 @@ def pytask_collect_task(
5763

5864
mark = _parse_stata_mark(mark=marks[0])
5965
script, options = stata(**marks[0].kwargs)
60-
6166
obj.pytask_meta.markers.append(mark)
6267

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

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

7979
script_node = session.hook.pytask_collect_node(
80-
session=session, path=path, node=script
80+
session=session,
81+
path=path_nodes,
82+
node_info=NodeInfo(
83+
arg_name="script", path=(), value=script, task_path=path, task_name=name
84+
),
8185
)
8286

83-
if isinstance(task.depends_on, dict):
84-
task.depends_on["__script"] = script_node
87+
if not (isinstance(script_node, PathNode) and script_node.path.suffix == ".do"):
88+
msg = (
89+
"The 'script' keyword of the @pytask.mark.stata decorator must point "
90+
f"to a file with the .do suffix, but it is {script_node}."
91+
)
92+
raise ValueError(msg)
93+
94+
options_node = session.hook.pytask_collect_node(
95+
session=session,
96+
path=path_nodes,
97+
node_info=NodeInfo(
98+
arg_name="_options",
99+
path=(),
100+
value=options,
101+
task_path=path,
102+
task_name=name,
103+
),
104+
)
105+
106+
executable_node = session.hook.pytask_collect_node(
107+
session=session,
108+
path=path_nodes,
109+
node_info=NodeInfo(
110+
arg_name="_executable",
111+
path=(),
112+
value=session.config["stata"],
113+
task_path=path,
114+
task_name=name,
115+
),
116+
)
117+
118+
cwd_node = session.hook.pytask_collect_node(
119+
session=session,
120+
path=path_nodes,
121+
node_info=NodeInfo(
122+
arg_name="_cwd",
123+
path=(),
124+
value=path.parent.as_posix(),
125+
task_path=path,
126+
task_name=name,
127+
),
128+
)
129+
130+
dependencies = parse_dependencies_from_task_function(
131+
session, path, name, path_nodes, obj
132+
)
133+
products = parse_products_from_task_function(
134+
session, path, name, path_nodes, obj
135+
)
136+
137+
# Add script
138+
dependencies["_script"] = script_node
139+
dependencies["_options"] = options_node
140+
dependencies["_cwd"] = cwd_node
141+
dependencies["_executable"] = executable_node
142+
143+
partialed = functools.partial(run_stata_script, _cwd=path.parent)
144+
markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else []
145+
146+
task: PTask
147+
if path is None:
148+
task = TaskWithoutPath(
149+
name=name,
150+
function=partialed,
151+
depends_on=dependencies,
152+
produces=products,
153+
markers=markers,
154+
)
85155
else:
86-
task.depends_on = {0: task.depends_on, "__script": script_node}
156+
task = Task(
157+
base_name=name,
158+
path=path,
159+
function=partialed,
160+
depends_on=dependencies,
161+
produces=products,
162+
markers=markers,
163+
)
87164

165+
# Add log_name node that depends on the task id.
88166
if session.config["platform"] == "win32":
89-
log_name = convert_task_id_to_name_of_log_file(task.short_name)
90-
log_name_arg = [f"-{log_name}"]
167+
log_name = convert_task_id_to_name_of_log_file(task)
91168
else:
92-
log_name_arg = []
93-
94-
stata_function = functools.partial(
95-
task.function,
96-
executable=session.config["stata"],
97-
script=task.depends_on["__script"].path,
98-
options=options,
99-
log_name=log_name_arg,
100-
cwd=task.path.parent,
169+
log_name = ""
170+
171+
log_name_node = session.hook.pytask_collect_node(
172+
session=session,
173+
path=path_nodes,
174+
node_info=NodeInfo(
175+
arg_name="_log_name",
176+
path=(),
177+
value=PythonNode(value=log_name),
178+
task_path=path,
179+
task_name=name,
180+
),
101181
)
102-
103-
task.function = stata_function
182+
task.depends_on["_log_name"] = log_name_node
104183

105184
return task
106185
return None
@@ -109,32 +188,5 @@ def pytask_collect_task(
109188
def _parse_stata_mark(mark: Mark) -> Mark:
110189
"""Parse a Stata mark."""
111190
script, options = stata(**mark.kwargs)
112-
113191
parsed_kwargs = {"script": script or None, "options": options or []}
114-
115192
return Mark("stata", (), parsed_kwargs)
116-
117-
118-
def _copy_func(func: FunctionType) -> FunctionType:
119-
"""Create a copy of a function.
120-
121-
Based on https://stackoverflow.com/a/13503277/7523785.
122-
123-
Example
124-
-------
125-
>>> def _func(): pass
126-
>>> copied_func = _copy_func(_func)
127-
>>> _func is copied_func
128-
False
129-
130-
"""
131-
new_func = FunctionType(
132-
func.__code__,
133-
func.__globals__,
134-
name=func.__name__,
135-
argdefs=func.__defaults__,
136-
closure=func.__closure__,
137-
)
138-
new_func = functools.update_wrapper(new_func, func)
139-
new_func.__kwdefaults__ = func.__kwdefaults__
140-
return new_func

0 commit comments

Comments
 (0)