Skip to content
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ repos:
--ignore-missing-imports,
]
additional_dependencies: [
types-attrs,
attrs>=19.2.0,
types-click,
types-setuptools
]
Expand Down
11 changes: 9 additions & 2 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ chronological order. Releases follow `semantic versioning <https://semver.org/>`
all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
`Anaconda.org <https://anaconda.org/conda-forge/pytask>`_.

0.2.0 - 2022-xx-xx
------------------

- :pull:`227` implements ``task.kwargs`` as a new way for a task to hold parametrized
arguments. It also implements :class:`_pytask.models.CollectionMetadata` to carry
parametrized arguments to the task class.


0.1.9 - 2022-xx-xx
0.1.9 - 2022-02-23
------------------

- :pull:`197` publishes types, and adds classes and functions to the main namespace.
Expand All @@ -19,7 +26,7 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
- :pull:`222` adds an automated Github Actions job for creating a list pytask plugins.
- :pull:`225` fixes a circular import noticeable in plugins created by :pull:`197`.
- :pull:`226` fixes a bug where the number of items in the live table during the
execution is not exhausted.
execution is not exhausted. (Closes :issue:`223`.)


0.1.8 - 2022-02-07
Expand Down
22 changes: 19 additions & 3 deletions src/_pytask/execute.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""This module contains hook implementations concerning the execution."""
from __future__ import annotations

import inspect
import sys
import time
from typing import Any
Expand Down Expand Up @@ -147,10 +148,25 @@ def pytask_execute_task_setup(session: Session, task: MetaTask) -> None:
node.path.parent.mkdir(parents=True, exist_ok=True)


@hookimpl
def pytask_execute_task(task: MetaTask) -> None:
@hookimpl(trylast=True)
def pytask_execute_task(task: MetaTask) -> bool:
"""Execute task."""
task.execute()
kwargs = {**task.kwargs}

func_arg_names = set(inspect.signature(task.function).parameters)
for arg_name in ("depends_on", "produces"):
if arg_name in func_arg_names:
attribute = getattr(task, arg_name)
kwargs[arg_name] = (
attribute[0].value
if len(attribute) == 1
and 0 in attribute
and not task.keep_dict[arg_name]
else {name: node.value for name, node in attribute.items()}
)

task.execute(**kwargs)
return True


@hookimpl
Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ def pytask_execute_task_setup(session: Session, task: MetaTask) -> None:


@hookspec(firstresult=True)
def pytask_execute_task(session: Session, task: MetaTask) -> Any | None:
def pytask_execute_task(session: Session, task: MetaTask) -> Any:
"""Execute a task."""


Expand Down
14 changes: 14 additions & 0 deletions src/_pytask/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""This module contains code on models, containers and there like."""
from __future__ import annotations

from typing import Any
from typing import Dict

import attr


@attr.s
class CollectionMetadata:
"""A class for carrying metadata from functions to tasks."""

kwargs = attr.ib(factory=dict, type=Dict[str, Any])
31 changes: 11 additions & 20 deletions src/_pytask/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import annotations

import functools
import inspect
import itertools
from abc import ABCMeta
from abc import abstractmethod
Expand Down Expand Up @@ -83,6 +82,8 @@ class MetaTask(MetaNode):
path: Path
function: Callable[..., Any] | None
attributes: dict[Any, Any]
kwargs: dict[str, Any]
keep_dict: dict[str, bool]
_report_sections: list[tuple[str, str, str]]

@abstractmethod
Expand Down Expand Up @@ -116,6 +117,8 @@ class PythonFunctionTask(MetaTask):
"""Dict[str, MetaNode]: A list of products of task."""
markers = attr.ib(factory=list, type="List[Mark]")
"""Optional[List[Mark]]: A list of markers attached to the task function."""
kwargs = attr.ib(factory=dict, type=Dict[str, Any])
"""Dict[str, Any]: A dictionary with keyword arguments supplied to the task."""
keep_dict = attr.ib(factory=dict, type=Dict[str, bool])
"""Dict[str, bool]: Should dictionaries for single nodes be preserved?"""
_report_sections = attr.ib(factory=list, type=List[Tuple[str, str, str]])
Expand Down Expand Up @@ -150,6 +153,11 @@ def from_path_name_function_session(
if marker.name not in ("depends_on", "produces")
]

if hasattr(function, "pytask_meta"):
kwargs = function.pytask_meta.kwargs # type: ignore[attr-defined]
else:
kwargs = {}

return cls(
base_name=name,
name=create_task_name(path, name),
Expand All @@ -158,35 +166,18 @@ def from_path_name_function_session(
depends_on=dependencies,
produces=products,
markers=markers,
kwargs=kwargs,
keep_dict=keep_dictionary,
)

def execute(self) -> None:
def execute(self, **kwargs: Any) -> None:
"""Execute the task."""
kwargs = self._get_kwargs_from_task_for_function()
self.function(**kwargs)

def state(self) -> str:
"""Return the last modified date of the file where the task is defined."""
return str(self.path.stat().st_mtime)

def _get_kwargs_from_task_for_function(self) -> dict[str, Any]:
"""Process dependencies and products to pass them as kwargs to the function."""
func_arg_names = set(inspect.signature(self.function).parameters)
kwargs = {}
for arg_name in ("depends_on", "produces"):
if arg_name in func_arg_names:
attribute = getattr(self, arg_name)
kwargs[arg_name] = (
attribute[0].value
if len(attribute) == 1
and 0 in attribute
and not self.keep_dict[arg_name]
else {name: node.value for name, node in attribute.items()}
)

return kwargs

def add_report_section(self, when: str, key: str, content: str) -> None:
if content:
self._report_sections.append((when, key, content))
Expand Down
13 changes: 5 additions & 8 deletions src/_pytask/parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from _pytask.mark import MARK_GEN as mark # noqa: N811
from _pytask.mark_utils import has_marker
from _pytask.mark_utils import remove_markers_from_func
from _pytask.models import CollectionMetadata
from _pytask.nodes import find_duplicates
from _pytask.session import Session
from _pytask.task_utils import parse_task_marker
Expand Down Expand Up @@ -133,16 +134,12 @@ def pytask_parametrize_task(
# Convert parametrized dependencies and products to decorator.
session.hook.pytask_parametrize_kwarg_to_marker(obj=func, kwargs=kwargs)

# Attach remaining parametrized arguments to the function.
partialed_func = functools.partial(func, **kwargs)
wrapped_func = functools.update_wrapper(partialed_func, func)

# Remove markers from wrapped function since they are not used. See
# https://github.com/pytask-dev/pytask/issues/216.
wrapped_func.func.pytaskmark = [] # type: ignore[attr-defined]
func.pytask_meta = CollectionMetadata( # type: ignore[attr-defined]
kwargs=kwargs
)

name_ = f"{name}[{'-'.join(itertools.chain.from_iterable(names))}]"
names_and_functions.append((name_, wrapped_func))
names_and_functions.append((name_, func))

all_names = [i[0] for i in names_and_functions]
duplicates = find_duplicates(all_names)
Expand Down
18 changes: 2 additions & 16 deletions tests/test_parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def func(i): # noqa: U100
names_and_objs = pytask_parametrize_task(session, "func", func)

assert [i[0] for i in names_and_objs] == ["func[0]", "func[1]"]
assert names_and_objs[0][1].keywords["i"] == 0
assert names_and_objs[1][1].keywords["i"] == 1
assert names_and_objs[0][1].pytask_meta.kwargs["i"] == 0
assert names_and_objs[1][1].pytask_meta.kwargs["i"] == 1


@pytest.mark.integration
Expand All @@ -71,20 +71,6 @@ def func(i, j, k): # noqa: U100
pytask_parametrize_task(session, "func", func)


@pytest.mark.integration
def test_pytask_parametrize_missing_func_args(session):
"""Missing function args with parametrized tasks raise an error during execution."""

@pytask.mark.parametrize("i", range(2))
def func():
pass

names_and_functions = pytask_parametrize_task(session, "func", func)
for _, func in names_and_functions:
with pytest.raises(TypeError):
func()


@pytest.mark.unit
@pytest.mark.parametrize(
"arg_names, expected",
Expand Down