Skip to content

Support pytask v0.4. #36

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

Merged
merged 11 commits into from
May 19, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ chronological order. Releases follow [semantic versioning](https://semver.org/)
releases are available on [PyPI](https://pypi.org/project/pytask-stata) and
[Anaconda.org](https://anaconda.org/conda-forge/pytask-stata).

## 0.3.0 - 2023-xx-xx
## 0.4.0 - 2023-10-08

- {pull}`36` makes pytask-stata compatible with pytask v0.4.0.

## 0.3.0 - 2023-01-23

- {pull}`24` adds ruff and refurb.
- {pull}`25` adds docformatter.
Expand Down
12 changes: 0 additions & 12 deletions MANIFEST.in

This file was deleted.

25 changes: 0 additions & 25 deletions environment.yml

This file was deleted.

37 changes: 23 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]
build-backend = "setuptools.build_meta"
requires = ["hatchling", "hatch_vcs"]
build-backend = "hatchling.build"

[project]
name = "pytask_stata"
Expand All @@ -15,7 +15,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
]
requires-python = ">=3.8"
dependencies = ["click", "pytask>=0.3,<0.4"]
dependencies = ["click", "pytask>=0.4"]
dynamic = ["version"]

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

[tool.setuptools]
include-package-data = true
package-dir = { "" = "src" }
zip-safe = false
platforms = ["any"]
license-files = ["LICENSE"]
[tool.rye]
managed = true
dev-dependencies = [
"tox-uv>=1.8.2",
]

[tool.hatch.build.hooks.vcs]
version-file = "src/pytask_stata/_version.py"

[tool.hatch.build.targets.sdist]
exclude = ["tests"]
only-packages = true

[tool.hatch.build.targets.wheel]
exclude = ["tests"]
only-packages = true

[tool.setuptools.packages.find]
where = ["src"]
namespaces = false
[tool.hatch.version]
source = "vcs"

[tool.setuptools_scm]
write_to = "src/pytask_stata/_version.py"
[tool.hatch.metadata]
allow-direct-references = true

[tool.mypy]
files = ["src", "tests"]
Expand Down
196 changes: 124 additions & 72 deletions src/pytask_stata/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,40 @@

import functools
import subprocess
from types import FunctionType
from typing import TYPE_CHECKING
import warnings
from pathlib import Path
from typing import Any

from pytask import Mark
from pytask import NodeInfo
from pytask import PathNode
from pytask import PTask
from pytask import PythonNode
from pytask import Session
from pytask import Task
from pytask import depends_on
from pytask import TaskWithoutPath
from pytask import has_mark
from pytask import hookimpl
from pytask import parse_nodes
from pytask import produces
from pytask import is_task_function
from pytask import parse_dependencies_from_task_function
from pytask import parse_products_from_task_function
from pytask import remove_marks

from pytask_stata.shared import convert_task_id_to_name_of_log_file
from pytask_stata.shared import stata

if TYPE_CHECKING:
from pathlib import Path


def run_stata_script(
executable: str, script: Path, options: list[str], log_name: list[str], cwd: Path
_executable: str,
_script: Path,
_options: list[str],
_log_name: str,
_cwd: Path,
) -> None:
"""Run an R script."""
cmd = [executable, "-e", "do", script.as_posix(), *options, *log_name]
cmd = [_executable, "-e", "do", _script.as_posix(), *_options, f"-{_log_name}"]
print("Executing " + " ".join(cmd) + ".") # noqa: T201
subprocess.run(cmd, cwd=cwd, check=True) # noqa: S603
subprocess.run(cmd, cwd=_cwd, check=True) # noqa: S603


@hookimpl
Expand All @@ -43,11 +49,11 @@ def pytask_collect_task(

if (
(name.startswith("task_") or has_mark(obj, "task"))
and callable(obj)
and is_task_function(obj)
and has_mark(obj, "stata")
):
# Parse the @pytask.mark.stata decorator.
obj, marks = remove_marks(obj, "stata")

if len(marks) > 1:
msg = (
f"Task {name!r} has multiple @pytask.mark.stata marks, but only one is "
Expand All @@ -57,50 +63,123 @@ def pytask_collect_task(

mark = _parse_stata_mark(mark=marks[0])
script, options = stata(**marks[0].kwargs)

obj.pytask_meta.markers.append(mark)

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

markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else []
kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {}

task = Task(
base_name=name,
path=path,
function=_copy_func(run_stata_script), # type: ignore[arg-type]
depends_on=dependencies,
produces=products,
markers=markers,
kwargs=kwargs,
)
if isinstance(script, str):
warnings.warn(
"Passing a string to the @pytask.mark.stata parameter 'script' is "
"deprecated. Please, use a pathlib.Path instead.",
stacklevel=1,
)
script = Path(script)

script_node = session.hook.pytask_collect_node(
session=session, path=path, node=script
session=session,
path=path_nodes,
node_info=NodeInfo(
arg_name="script", path=(), value=script, task_path=path, task_name=name
),
)

if isinstance(task.depends_on, dict):
task.depends_on["__script"] = script_node
if not (isinstance(script_node, PathNode) and script_node.path.suffix == ".do"):
msg = (
"The 'script' keyword of the @pytask.mark.stata decorator must point "
f"to a file with the .do suffix, but it is {script_node}."
)
raise ValueError(msg)

options_node = session.hook.pytask_collect_node(
session=session,
path=path_nodes,
node_info=NodeInfo(
arg_name="_options",
path=(),
value=options,
task_path=path,
task_name=name,
),
)

executable_node = session.hook.pytask_collect_node(
session=session,
path=path_nodes,
node_info=NodeInfo(
arg_name="_executable",
path=(),
value=session.config["stata"],
task_path=path,
task_name=name,
),
)

cwd_node = session.hook.pytask_collect_node(
session=session,
path=path_nodes,
node_info=NodeInfo(
arg_name="_cwd",
path=(),
value=path.parent.as_posix(),
task_path=path,
task_name=name,
),
)

dependencies = parse_dependencies_from_task_function(
session, path, name, path_nodes, obj
)
products = parse_products_from_task_function(
session, path, name, path_nodes, obj
)

# Add script
dependencies["_script"] = script_node
dependencies["_options"] = options_node
dependencies["_cwd"] = cwd_node
dependencies["_executable"] = executable_node

partialed = functools.partial(run_stata_script, _cwd=path.parent)
markers = obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else []

task: PTask
if path is None:
task = TaskWithoutPath(
name=name,
function=partialed,
depends_on=dependencies,
produces=products,
markers=markers,
)
else:
task.depends_on = {0: task.depends_on, "__script": script_node}
task = Task(
base_name=name,
path=path,
function=partialed,
depends_on=dependencies,
produces=products,
markers=markers,
)

# Add log_name node that depends on the task id.
if session.config["platform"] == "win32":
log_name = convert_task_id_to_name_of_log_file(task.short_name)
log_name_arg = [f"-{log_name}"]
log_name = convert_task_id_to_name_of_log_file(task)
else:
log_name_arg = []

stata_function = functools.partial(
task.function,
executable=session.config["stata"],
script=task.depends_on["__script"].path,
options=options,
log_name=log_name_arg,
cwd=task.path.parent,
log_name = ""

log_name_node = session.hook.pytask_collect_node(
session=session,
path=path_nodes,
node_info=NodeInfo(
arg_name="_log_name",
path=(),
value=PythonNode(value=log_name),
task_path=path,
task_name=name,
),
)

task.function = stata_function
task.depends_on["_log_name"] = log_name_node

return task
return None
Expand All @@ -109,32 +188,5 @@ def pytask_collect_task(
def _parse_stata_mark(mark: Mark) -> Mark:
"""Parse a Stata mark."""
script, options = stata(**mark.kwargs)

parsed_kwargs = {"script": script or None, "options": options or []}

return Mark("stata", (), parsed_kwargs)


def _copy_func(func: FunctionType) -> FunctionType:
"""Create a copy of a function.

Based on https://stackoverflow.com/a/13503277/7523785.

Example
-------
>>> def _func(): pass
>>> copied_func = _copy_func(_func)
>>> _func is copied_func
False

"""
new_func = FunctionType(
func.__code__,
func.__globals__,
name=func.__name__,
argdefs=func.__defaults__,
closure=func.__closure__,
)
new_func = functools.update_wrapper(new_func, func)
new_func.__kwdefaults__ = func.__kwdefaults__
return new_func
Loading
Loading