diff --git a/docs/source/_static/images/markers.svg b/docs/source/_static/images/markers.svg
index f27a5e7d..5febd4cd 100644
--- a/docs/source/_static/images/markers.svg
+++ b/docs/source/_static/images/markers.svg
@@ -107,9 +107,6 @@
│ pytask.mark.depends_on │ Add dependencies to a task. See this tutorial for more │
│ │ │
-│ pytask.mark.parametrize │ The marker for pytest's way of repeating tasks which is │
-
-│ │ │
│ pytask.mark.persist │ Prevent execution of a task if all products exist and even if │
│ │ something has changed (dependencies, source file, products). │
│ │ This decorator might be useful for expensive tasks where only │
diff --git a/docs/source/_static/md/markers.md b/docs/source/_static/md/markers.md
index 60b3d0df..2ce8c7d7 100644
--- a/docs/source/_static/md/markers.md
+++ b/docs/source/_static/md/markers.md
@@ -10,10 +10,6 @@ $ pytask markers
│ │ tutorial for more information: │
│ │ https://bit.ly/3JlxylS. │
│ │ │
-│ pytask.mark.parametrize │ The marker for pytest's way of │
-│ │ repeating tasks which is explained in │
-│ │ this tutorial: https://bit.ly/3uqZqkk. │
-│ │ │
│ pytask.mark.persist │ Prevent execution of a task if all │
│ │ products exist and even ifsomething has │
│ │ changed (dependencies, source file, │
diff --git a/docs/source/changes.md b/docs/source/changes.md
index 21617804..d7fa682d 100644
--- a/docs/source/changes.md
+++ b/docs/source/changes.md
@@ -9,6 +9,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
- {pull}`323` remove Python 3.7 support and use a new Github action to provide mamba.
- {pull}`387` replaces pony with sqlalchemy.
+- {pull}`391` removes `@pytask.mark.parametrize`.
## 0.3.2 - 2023-06-07
diff --git a/docs/source/how_to_guides/index.md b/docs/source/how_to_guides/index.md
index 29830859..7f3c9aa5 100644
--- a/docs/source/how_to_guides/index.md
+++ b/docs/source/how_to_guides/index.md
@@ -14,7 +14,6 @@ maxdepth: 1
migrating_from_scripts_to_pytask
invoking_pytask_extended
capture_warnings
-repeating_tasks_with_different_inputs_the_pytest_way
how_to_influence_build_order
how_to_write_a_plugin
```
diff --git a/docs/source/how_to_guides/repeating_tasks_with_different_inputs_the_pytest_way.md b/docs/source/how_to_guides/repeating_tasks_with_different_inputs_the_pytest_way.md
deleted file mode 100644
index 7a81c571..00000000
--- a/docs/source/how_to_guides/repeating_tasks_with_different_inputs_the_pytest_way.md
+++ /dev/null
@@ -1,213 +0,0 @@
-# Repeating tasks with different inputs - The pytest way
-
-:::{important}
-This guide shows you how to parametrize tasks with the pytest approach. For the new and
-preferred approach, see this
-{doc}`tutorial <../tutorials/repeating_tasks_with_different_inputs>`.
-:::
-
-Do you want to define a task repeating an action over a range of inputs? Parametrize
-your task function!
-
-:::{hint}
-The process of repeating a function with different inputs is called parametrizations.
-:::
-
-:::{seealso}
-If you want to know more about best practices for parametrizations, check out this
-{doc}`guide <../how_to_guides/bp_scalable_repetitions_of_tasks>` after you have made
-yourself familiar with this tutorial.
-:::
-
-## An example
-
-We reuse the previous example of a task that generates random data and repeat the same
-operation over some seeds to receive multiple, reproducible samples.
-
-First, we write the task for one seed.
-
-```python
-import numpy as np
-import pytask
-
-
-@pytask.mark.produces(BLD / "data_0.pkl")
-def task_create_random_data(produces):
- rng = np.random.default_rng(0)
- ...
-```
-
-In the next step, we repeat the same task over the numbers 0, 1, and 2 and pass them to
-the `seed` argument. We also vary the name of the produced file in every iteration.
-
-```python
-@pytask.mark.parametrize(
- "produces, seed",
- [(BLD / "data_0.pkl", 0), (BLD / "data_1.pkl", 1), (BLD / "data_2.pkl", 2)],
-)
-def task_create_random_data(seed, produces):
- rng = np.random.default_rng(seed)
- ...
-```
-
-The parametrize decorator receives two arguments. The first argument is
-`"produces, seed"` - the signature. It is a comma-separated string where each value
-specifies the name of a task function argument.
-
-:::{seealso}
-The signature is explained in detail {ref}`below `.
-:::
-
-The second argument of the parametrize decorator is a list with one element per
-iteration. Each element must provide one value for each argument name in the signature -
-two in this case.
-
-pytask executes the task function three times and passes the path from the list to the
-argument `produces` and the seed to `seed`.
-
-:::{note}
-If you use `produces` or `depends_on` in the signature of the parametrize decorator, the
-values are handled as if they were attached to the function with
-{func}`@pytask.mark.depends_on ` or
-{func}`@pytask.mark.produces `.
-:::
-
-## Un-parametrized dependencies
-
-To specify a dependency that is the same for all parametrizations, add it with
-{func}`@pytask.mark.depends_on `.
-
-```python
-@pytask.mark.depends_on(SRC / "common_dependency.file")
-@pytask.mark.parametrize(
- "produces, seed",
- [(BLD / "data_0.pkl", 0), (BLD / "data_1.pkl", 1), (BLD / "data_2.pkl", 2)],
-)
-def task_create_random_data(seed, produces):
- rng = np.random.default_rng(seed)
- ...
-```
-
-(parametrize-signature)=
-
-## The signature
-
-pytask allows for three different kinds of formats for the signature.
-
-1. The signature can be a comma-separated string like an entry in a CSV table. Note that
- white space is stripped from each name which you can use to separate the names for
- readability. Here are some examples:
-
- ```python
- "single_argument"
- "first_argument,second_argument"
- "first_argument, second_argument"
- ```
-
-1. The signature can be a tuple of strings where each string is one argument name. Here
- is an example.
-
- ```python
- ("first_argument", "second_argument")
- ```
-
-1. Finally, using a list of strings is also possible.
-
- ```python
- ["first_argument", "second_argument"]
- ```
-
-## The id
-
-Every task has a unique id that can be used to
-{doc}`select it <../tutorials/selecting_tasks>`. The normal id combines the path to the
-module where the task is defined, a double colon, and the name of the task function.
-Here is an example.
-
-```
-../task_example.py::task_example
-```
-
-This behavior would produce duplicate ids for parametrized tasks. Therefore, there exist
-multiple mechanisms to have unique ids.
-
-(auto-generated-ids)=
-
-### Auto-generated ids
-
-pytask construct ids by extending the task name with representations of the values used
-for each iteration. Booleans, floats, integers, and strings enter the task id directly.
-For example, a task function that receives four arguments, `True`, `1.0`, `2`, and
-`"hello"`, one of each dtype, has the following id.
-
-```
-task_example.py::task_example[True-1.0-2-hello]
-```
-
-Arguments with other dtypes cannot be converted to strings and, thus, are replaced with
-a combination of the argument name and the iteration counter.
-
-For example, the following function is parametrized with tuples.
-
-```python
-@pytask.mark.parametrize("i", [(0,), (1,)])
-def task_example(i):
- pass
-```
-
-Since the tuples are not converted to strings, the ids of the two tasks are
-
-```
-task_example.py::task_example[i0]
-task_example.py::task_example[i1]
-```
-
-### User-defined ids
-
-Instead of a function, you can also pass a list or another iterable of id values via
-`ids`.
-
-This code
-
-```python
-@pytask.mark.parametrize("i", [(0,), (1,)], ids=["first", "second"])
-def task_example(i):
- pass
-```
-
-produces these ids
-
-```
-task_example.py::task_example[first] # (0,)
-task_example.py::task_example[second] # (1,)
-```
-
-(how-to-parametrize-a-task-convert-other-objects)=
-
-### Convert other objects
-
-To change the representation of tuples and other objects, you can pass a function to the
-`ids` argument of the {func}`@pytask.mark.parametrize `
-decorator. The function is called for every argument and may return a boolean, number,
-or string, which will be integrated into the id. For every other return, the
-auto-generated value is used.
-
-We can use the hash value to get a unique representation of a tuple.
-
-```python
-def tuple_to_hash(value):
- if isinstance(value, tuple):
- return hash(a)
-
-
-@pytask.mark.parametrize("i", [(0,), (1,)], ids=tuple_to_hash)
-def task_example(i):
- pass
-```
-
-The tasks have the following ids:
-
-```
-task_example.py::task_example[3430018387555] # (0,)
-task_example.py::task_example[3430019387558] # (1,)
-```
diff --git a/docs/source/reference_guides/api.md b/docs/source/reference_guides/api.md
index 6254656b..8924bb13 100644
--- a/docs/source/reference_guides/api.md
+++ b/docs/source/reference_guides/api.md
@@ -47,35 +47,6 @@ by the host or by plugins. The following marks are available by default.
:func:`_pytask.hookspecs.pytask_collect_node` entry-point.
```
-```{eval-rst}
-.. function:: pytask.mark.parametrize(arg_names, arg_values, *, ids)
-
- Parametrize a task function.
-
- Parametrizing a task allows to execute the same task with different arguments.
-
- :type arg_names: str | list[str] | tuple[str, ...]
- :param arg_names:
- The names of the arguments which can either be given as a comma-separated
- string, a tuple of strings, or a list of strings.
- :type arg_values: Iterable[Sequence[Any] | Any]
- :param arg_values:
- The values which correspond to names in ``arg_names``. For one argument, it is a
- single iterable. For multiple argument names it is an iterable of iterables.
- :type ids: None | (Iterable[None | str | float | int | bool] | Callable[..., Any])
- :param ids:
- This argument can either be a list with ids or a function which is called with
- every value passed to the parametrized function.
-
- If you pass an iterable with ids, make sure to only use :obj:`bool`,
- :obj:`float`, :obj:`int`, or :obj:`str` as values which are used to create task
- ids like ``"task_dummpy.py::task_dummy[first_task_id]"``.
-
- If you pass a function, the function receives each value of the parametrization
- and may return a boolean, number, string or None. For the latter, the
- auto-generated value is used.
-```
-
```{eval-rst}
.. function:: pytask.mark.persist()
diff --git a/docs/source/reference_guides/hookspecs.md b/docs/source/reference_guides/hookspecs.md
index 474afcb4..20e958e5 100644
--- a/docs/source/reference_guides/hookspecs.md
+++ b/docs/source/reference_guides/hookspecs.md
@@ -106,21 +106,6 @@ The following hooks traverse directories and collect tasks from files.
```
-## Parametrization
-
-The hooks to parametrize a task are called during the collection when a function is
-collected. Then, the function is duplicated according to the parametrization and the
-duplicates are collected with {func}`pytask_collect_task`.
-
-```{eval-rst}
-.. autofunction:: pytask_parametrize_task
-```
-
-```{eval-rst}
-.. autofunction:: pytask_parametrize_kwarg_to_marker
-
-```
-
## Resolving Dependencies
The following hooks are designed to build a DAG from tasks and dependencies and check
diff --git a/docs/source/tutorials/repeating_tasks_with_different_inputs.md b/docs/source/tutorials/repeating_tasks_with_different_inputs.md
index 760fc37d..9b3c12f3 100644
--- a/docs/source/tutorials/repeating_tasks_with_different_inputs.md
+++ b/docs/source/tutorials/repeating_tasks_with_different_inputs.md
@@ -2,16 +2,6 @@
Do you want to repeat a task over a range of inputs? Loop over your task function!
-:::{important}
-Before v0.2.0, pytask supported only one approach to repeat tasks. It is also called
-parametrizations, and similarly to pytest, it uses a
-{func}`@pytask.mark.parametrize ` decorator. If you want to
-know more about it, you can find it
-{doc}`here <../how_to_guides/repeating_tasks_with_different_inputs_the_pytest_way>`.
-
-Here you find the new and preferred approach.
-:::
-
## An example
We reuse the task from the previous {doc}`tutorial `, which generates
@@ -71,9 +61,40 @@ and the name of the task function. Here is an example.
```
This behavior would produce duplicate ids for parametrized tasks. By default,
-auto-generated ids are used which are explained {ref}`here `.
+auto-generated ids are used.
+
+(auto-generated-ids)=
+
+### Auto-generated ids
+
+pytask construct ids by extending the task name with representations of the values used
+for each iteration. Booleans, floats, integers, and strings enter the task id directly.
+For example, a task function that receives four arguments, `True`, `1.0`, `2`, and
+`"hello"`, one of each dtype, has the following id.
+
+```
+task_data_preparation.py::task_create_random_data[True-1.0-2-hello]
+```
+
+Arguments with other dtypes cannot be converted to strings and, thus, are replaced with
+a combination of the argument name and the iteration counter.
-More powerful are user-defined ids.
+For example, the following function is parametrized with tuples.
+
+```python
+for i in [(0,), (1,)]:
+
+ @pytask.mark.task
+ def task_create_random_data(i=i):
+ pass
+```
+
+Since the tuples are not converted to strings, the ids of the two tasks are
+
+```
+task_data_preparation.py::task_create_random_data[i0]
+task_data_preparation.py::task_create_random_data[i1]
+```
(ids)=
diff --git a/pyproject.toml b/pyproject.toml
index 8dc8092d..d38fcd9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,6 +62,9 @@ extend-ignore = [
"SLF001", # access private members.
"S603",
"S607",
+ # Temporary
+ "TD002",
+ "TD003",
]
diff --git a/src/_pytask/cli.py b/src/_pytask/cli.py
index 7d42e355..4b6dd39e 100644
--- a/src/_pytask/cli.py
+++ b/src/_pytask/cli.py
@@ -66,7 +66,6 @@ def pytask_add_hooks(pm: pluggy.PluginManager) -> None:
from _pytask import mark
from _pytask import nodes
from _pytask import parameters
- from _pytask import parametrize
from _pytask import persist
from _pytask import profile
from _pytask import dag
@@ -89,7 +88,6 @@ def pytask_add_hooks(pm: pluggy.PluginManager) -> None:
pm.register(mark)
pm.register(nodes)
pm.register(parameters)
- pm.register(parametrize)
pm.register(persist)
pm.register(profile)
pm.register(dag)
diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py
index 7fd1dff9..dcb22df3 100644
--- a/src/_pytask/collect.py
+++ b/src/_pytask/collect.py
@@ -6,7 +6,6 @@
import os
import sys
import time
-import warnings
from pathlib import Path
from typing import Any
from typing import Generator
@@ -106,15 +105,6 @@ def pytask_collect_file_protocol(
return flat_reports
-_PARAMETRIZE_DEPRECATION_WARNING = """\
-The @pytask.mark.parametrize decorator is deprecated and will be removed in pytask \
-v0.4. Either upgrade your code to the new syntax explained in \
-https://tinyurl.com/pytask-loops or silence the warning by setting \
-`silence_parametrize_deprecation = true` in your pyproject.toml under \
-[tool.pytask.ini_options] and pin pytask to <0.4.
-"""
-
-
@hookimpl
def pytask_collect_file(
session: Session, path: Path, reports: list[CollectionReport]
@@ -129,26 +119,11 @@ def pytask_collect_file(
if has_mark(obj, "task"):
continue
- if has_mark(obj, "parametrize"):
- if not session.config.get("silence_parametrize_deprecation", False):
- warnings.warn(
- message=_PARAMETRIZE_DEPRECATION_WARNING,
- category=FutureWarning,
- stacklevel=1,
- )
-
- names_and_objects = session.hook.pytask_parametrize_task(
- session=session, name=name, obj=obj
- )
- else:
- names_and_objects = [(name, obj)]
-
- for name_, obj_ in names_and_objects:
- report = session.hook.pytask_collect_task_protocol(
- session=session, reports=reports, path=path, name=name_, obj=obj_
- )
- if report is not None:
- collected_reports.append(report)
+ report = session.hook.pytask_collect_task_protocol(
+ session=session, reports=reports, path=path, name=name, obj=obj
+ )
+ if report is not None:
+ collected_reports.append(report)
return collected_reports
return None
diff --git a/src/_pytask/hookspecs.py b/src/_pytask/hookspecs.py
index 9a49206f..011feb6e 100644
--- a/src/_pytask/hookspecs.py
+++ b/src/_pytask/hookspecs.py
@@ -8,7 +8,6 @@
import pathlib
from typing import Any
-from typing import Callable
from typing import TYPE_CHECKING
import click
@@ -211,27 +210,6 @@ def pytask_collect_log(
"""
-# Hooks to parametrize tasks.
-
-
-@hookspec(firstresult=True)
-def pytask_parametrize_task(
- session: Session, name: str, obj: Any
-) -> list[tuple[str, Callable[..., Any]]]:
- """Generate multiple tasks from name and object with parametrization."""
-
-
-@hookspec
-def pytask_parametrize_kwarg_to_marker(obj: Any, kwargs: dict[Any, Any]) -> None:
- """Add some keyword arguments as markers to object.
-
- This hook moves arguments defined in the parametrization to marks of the same
- function. This allows an argument like ``depends_on`` be transformed to the usual
- ``@pytask.mark.depends_on`` marker which receives special treatment.
-
- """
-
-
# Hooks for resolving dependencies.
diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py
index d4a77047..4dae51e6 100644
--- a/src/_pytask/mark/structures.py
+++ b/src/_pytask/mark/structures.py
@@ -196,11 +196,13 @@ def __getattr__(self, name: str) -> MarkDecorator | Any:
if self.config is not None and name not in self.config["markers"]:
if self.config["strict_markers"]:
raise ValueError(f"Unknown pytask.mark.{name}.")
- # Raise a specific error for common misspellings of "parametrize".
- if name in ("parameterize", "parametrise", "parameterise"):
- warnings.warn(
- f"Unknown {name!r} mark, did you mean 'parametrize'?", stacklevel=1
- )
+
+ if name in ("parametrize", "parameterize", "parametrise", "parameterise"):
+ raise NotImplementedError(
+ "@pytask.mark.parametrize has been removed since pytask v0.4. "
+ "Upgrade your parametrized tasks to the new syntax defined in"
+ "https://tinyurl.com/pytask-loops or revert to v0.3."
+ ) from None
warnings.warn(
f"Unknown pytask.mark.{name} - is this a typo? You can register "
diff --git a/src/_pytask/parametrize.py b/src/_pytask/parametrize.py
deleted file mode 100644
index f3a9c2ca..00000000
--- a/src/_pytask/parametrize.py
+++ /dev/null
@@ -1,390 +0,0 @@
-"""This module contains the code for the parametrize plugin."""
-from __future__ import annotations
-
-import copy
-import functools
-import itertools
-import pprint
-import types
-from typing import Any
-from typing import Callable
-from typing import Iterable
-from typing import Sequence
-
-from _pytask.config import hookimpl
-from _pytask.console import format_strings_as_flat_tree
-from _pytask.console import TASK_ICON
-from _pytask.mark import Mark
-from _pytask.mark import MARK_GEN as mark # noqa: N811
-from _pytask.mark_utils import remove_marks
-from _pytask.parametrize_utils import arg_value_to_id_component
-from _pytask.session import Session
-from _pytask.shared import find_duplicates
-
-
-def parametrize(
- arg_names: str | list[str] | tuple[str, ...],
- arg_values: Iterable[Sequence[Any] | Any],
- *,
- ids: None | (Iterable[None | str | float | int | bool] | Callable[..., Any]) = None,
-) -> tuple[
- str | list[str] | tuple[str, ...],
- Iterable[Sequence[Any] | Any],
- Iterable[None | str | float | int | bool] | Callable[..., Any] | None,
-]:
- """Parametrize a task function.
-
- Parametrizing a task allows to execute the same task with different arguments.
-
- Parameters
- ----------
- arg_names : str | list[str] | tuple[str, ...]
- The names of the arguments which can either be given as a comma-separated
- string, a tuple of strings, or a list of strings.
- arg_values : Iterable[Sequence[Any] | Any]
- The values which correspond to names in ``arg_names``. For one argument, it is a
- single iterable. For multiple argument names it is an iterable of iterables.
- ids
- This argument can either be a list with ids or a function which is called with
- every value passed to the parametrized function.
-
- If you pass an iterable with ids, make sure to only use :obj:`bool`,
- :obj:`float`, :obj:`int`, or :obj:`str` as values which are used to create task
- ids like ``"task_dummpy.py::task_dummy[first_task_id]"``.
-
- If you pass a function, the function receives each value of the parametrization
- and may return a boolean, number, string or None. For the latter, the
- auto-generated value is used.
-
- """
- return arg_names, arg_values, ids
-
-
-@hookimpl
-def pytask_parse_config(config: dict[str, Any]) -> None:
- """Add the marker to the config."""
- config["markers"]["parametrize"] = (
- "The marker for pytest's way of repeating tasks which is explained in this "
- "tutorial: [link https://bit.ly/3uqZqkk]https://bit.ly/3uqZqkk[/]."
- )
-
-
-@hookimpl
-def pytask_parametrize_task(
- session: Session, name: str, obj: Callable[..., Any]
-) -> list[tuple[str, Callable[..., Any]]]:
- """Parametrize a task.
-
- This function takes a single Python function and all parametrize decorators and
- generates multiple instances of the same task with different arguments.
-
- Note that, while a single ``@pytask.mark.parametrize`` is handled like a loop or a
- :func:`zip`, multiple ``@pytask.mark.parametrize`` decorators form a Cartesian
- product.
-
- We cannot raise an error if the function does not use parametrized arguments since
- some plugins will replace functions with their own implementation like pytask-r.
-
- """
- if callable(obj):
- obj, markers = remove_marks(obj, "parametrize") # type: ignore[assignment]
-
- if len(markers) > 1:
- raise NotImplementedError(
- "You cannot apply @pytask.mark.parametrize multiple times to a task. "
- "Use multiple for-loops, itertools.product or a different strategy to "
- "create all combinations of inputs and pass it to a single "
- "@pytask.mark.parametrize.\n\nFor improved readability, consider to "
- "move the creation of inputs into its own function as shown in the "
- "best-practices guide on parametrizations: https://pytask-dev.rtfd.io/"
- "en/stable/how_to_guides/bp_scalable_repetitions_of_tasks.html."
- )
-
- base_arg_names, arg_names, arg_values = _parse_parametrize_markers(
- markers, name
- )
-
- product_arg_names = list(itertools.product(*arg_names))
- product_arg_values = list(itertools.product(*arg_values))
-
- names_and_functions: list[tuple[str, Callable[..., Any]]] = []
- for names, values in zip(product_arg_names, product_arg_values):
- kwargs = dict(
- zip(
- itertools.chain.from_iterable(base_arg_names),
- itertools.chain.from_iterable(values),
- )
- )
-
- # Copy function and attributes to allow in-place changes.
- func = _copy_func(obj) # type: ignore[arg-type]
- func.pytask_meta = copy.deepcopy( # type: ignore[attr-defined]
- obj.pytask_meta # type: ignore[attr-defined]
- )
- # Convert parametrized dependencies and products to decorator.
- session.hook.pytask_parametrize_kwarg_to_marker(obj=func, kwargs=kwargs)
-
- func.pytask_meta.kwargs = { # type: ignore[attr-defined]
- **func.pytask_meta.kwargs, # type: ignore[attr-defined]
- **kwargs,
- }
-
- name_ = f"{name}[{'-'.join(itertools.chain.from_iterable(names))}]"
- names_and_functions.append((name_, func))
-
- all_names = [i[0] for i in names_and_functions]
- duplicates = find_duplicates(all_names)
-
- if duplicates:
- text = format_strings_as_flat_tree(
- duplicates, "Duplicated task ids", TASK_ICON
- )
- raise ValueError(
- "The following ids are duplicated while parametrizing task "
- f"{name!r}.\n\n{text}\n\nIt might be caused by "
- "parametrizing the task with the same combination of arguments "
- "multiple times. Change the arguments or change the ids generated by "
- "the parametrization."
- )
-
- return names_and_functions
- return None
-
-
-def _parse_parametrize_marker(
- marker: Mark, name: str
-) -> tuple[tuple[str, ...], list[tuple[str, ...]], list[tuple[Any, ...]]]:
- """Parse parametrize marker.
-
- Parameters
- ----------
- marker : Mark
- A parametrize mark.
- name : str
- The name of the task function which is parametrized.
-
- Returns
- -------
- base_arg_names : Tuple[str, ...]
- Contains the names of the arguments.
- processed_arg_names : List[Tuple[str, ...]]
- Each tuple in the list represents the processed names of the arguments suffixed
- with a number indicating the iteration.
- processed_arg_values : List[Tuple[Any, ...]]
- Each tuple in the list represents the values of the arguments for each
- iteration.
-
- """
- arg_names, arg_values, ids = parametrize(*marker.args, **marker.kwargs)
-
- parsed_arg_names = _parse_arg_names(arg_names)
- has_single_arg = len(parsed_arg_names) == 1
- parsed_arg_values = _parse_arg_values(arg_values, has_single_arg)
-
- _check_if_n_arg_names_matches_n_arg_values(
- parsed_arg_names, parsed_arg_values, name
- )
-
- expanded_arg_names = _create_parametrize_ids_components(
- parsed_arg_names, parsed_arg_values, ids
- )
-
- return parsed_arg_names, expanded_arg_names, parsed_arg_values
-
-
-def _parse_parametrize_markers(
- markers: list[Mark], name: str
-) -> tuple[
- list[tuple[str, ...]],
- list[list[tuple[str, ...]]],
- list[list[tuple[Any, ...]]],
-]:
- """Parse parametrize markers."""
- parsed_markers = [_parse_parametrize_marker(marker, name) for marker in markers]
- base_arg_names = [i[0] for i in parsed_markers]
- processed_arg_names = [i[1] for i in parsed_markers]
- processed_arg_values = [i[2] for i in parsed_markers]
-
- return base_arg_names, processed_arg_names, processed_arg_values
-
-
-def _parse_arg_names(arg_names: str | list[str] | tuple[str, ...]) -> tuple[str, ...]:
- """Parse arg_names argument of parametrize decorator.
-
- There are three allowed formats:
-
- 1. comma-separated string representation.
- 2. a tuple of strings.
- 3. a list of strings.
-
- All formats are converted to a tuple of strings.
-
- Parameters
- ----------
- arg_names : Union[str, List[str], Tuple[str, ...]]
- The names of the arguments which are parametrized.
-
- Returns
- -------
- out : Tuple[str, ...]
- The parsed arg_names.
-
- Example
- -------
- >>> _parse_arg_names("i")
- ('i',)
- >>> _parse_arg_names("i, j")
- ('i', 'j')
-
- """
- if isinstance(arg_names, str):
- out = tuple(i.strip() for i in arg_names.split(","))
- elif isinstance(arg_names, (tuple, list)):
- out = tuple(arg_names)
- else:
- raise TypeError(
- "The argument 'arg_names' accepts comma-separated strings, tuples and lists"
- f" of strings. It cannot accept {arg_names} with type {type(arg_names)}."
- )
-
- return out
-
-
-def _parse_arg_values(
- arg_values: Iterable[Sequence[Any] | Any], has_single_arg: bool
-) -> list[tuple[Any, ...]]:
- """Parse the values provided for each argument name.
-
- After processing the values, the return is a list where each value is an iteration
- of the parametrization. Each iteration is a tuple of all parametrized arguments.
-
- Example
- -------
- >>> _parse_arg_values(["a", "b", "c"], has_single_arg=True)
- [('a',), ('b',), ('c',)]
- >>> _parse_arg_values([(0, 0), (0, 1), (1, 0)], has_single_arg=False)
- [(0, 0), (0, 1), (1, 0)]
-
- """
- return [
- tuple(i)
- if isinstance(i, Iterable)
- and not isinstance(i, str)
- and not (isinstance(i, dict) and has_single_arg)
- else (i,)
- for i in arg_values
- ]
-
-
-def _check_if_n_arg_names_matches_n_arg_values(
- arg_names: tuple[str, ...], arg_values: list[tuple[Any, ...]], name: str
-) -> None:
- """Check if the number of argument names matches the number of arguments."""
- n_names = len(arg_names)
- n_values = [len(i) for i in arg_values]
- unique_n_values = tuple(set(n_values))
-
- if not all(i == n_names for i in unique_n_values):
- pretty_arg_values = (
- f"{unique_n_values[0]}"
- if len(unique_n_values) == 1
- else " or ".join(map(str, unique_n_values))
- )
- idx_example = [i == n_names for i in n_values].index(False)
- formatted_example = pprint.pformat(arg_values[idx_example])
- raise ValueError(
- f"Task {name!r} is parametrized with {n_names} 'arg_names', {arg_names}, "
- f"but the number of provided 'arg_values' is {pretty_arg_values}. For "
- f"example, here are the values of parametrization no. {idx_example}:"
- f"\n\n{formatted_example}"
- )
-
-
-def _create_parametrize_ids_components(
- arg_names: tuple[str, ...],
- arg_values: list[tuple[Any, ...]],
- ids: None | (Iterable[None | str | float | int | bool] | Callable[..., Any]),
-) -> list[tuple[str, ...]]:
- """Create the ids for each parametrization.
-
- Parameters
- ----------
- arg_names : Tuple[str, ...]
- The names of the arguments of the parametrized function.
- arg_values : List[Tuple[Any, ...]]
- A list of tuples where each tuple is for one run.
- ids
- The ids associated with one parametrization.
-
- Examples
- --------
- >>> _create_parametrize_ids_components(["i"], [(0,), (1,)], None)
- [('0',), ('1',)]
-
- >>> _create_parametrize_ids_components(["i", "j"], [(0, (0,)), (1, (1,))], None)
- [('0', 'j0'), ('1', 'j1')]
-
- """
- if isinstance(ids, Iterable):
- raw_ids = [(id_,) for id_ in ids]
-
- if len(raw_ids) != len(arg_values):
- raise ValueError("The number of ids must match the number of runs.")
-
- if not all(
- isinstance(id_, (bool, int, float, str)) or id_ is None
- for id_ in itertools.chain.from_iterable(raw_ids)
- ):
- raise ValueError(
- "Ids for parametrization can only be of type bool, float, int, str or "
- "None."
- )
-
- parsed_ids: list[tuple[str, ...]] = [
- (str(id_),) for id_ in itertools.chain.from_iterable(raw_ids)
- ]
-
- else:
- parsed_ids = []
- for i, _arg_values in enumerate(arg_values):
- id_components = tuple(
- arg_value_to_id_component(arg_names[j], arg_value, i, ids)
- for j, arg_value in enumerate(_arg_values)
- )
- parsed_ids.append(id_components)
-
- return parsed_ids
-
-
-@hookimpl
-def pytask_parametrize_kwarg_to_marker(obj: Any, kwargs: dict[str, str]) -> None:
- """Add some parametrized keyword arguments as decorator."""
- if callable(obj):
- for marker_name in ("depends_on", "produces"):
- if marker_name in kwargs:
- mark.__getattr__(marker_name)(kwargs.pop(marker_name))(obj)
-
-
-def _copy_func(func: types.FunctionType) -> types.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 = types.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
diff --git a/src/_pytask/parametrize_utils.py b/src/_pytask/parametrize_utils.py
deleted file mode 100644
index f259c7b3..00000000
--- a/src/_pytask/parametrize_utils.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-from typing import Callable
-
-
-def arg_value_to_id_component(
- arg_name: str, arg_value: Any, i: int, id_func: Callable[..., Any] | None
-) -> str:
- """Create id component from the name and value of the argument.
-
- First, transform the value of the argument with a user-defined function if given.
- Otherwise, take the original value. Then, if the value is a :obj:`bool`,
- :obj:`float`, :obj:`int`, or :obj:`str`, cast it to a string. Otherwise, define a
- placeholder value from the name of the argument and the iteration.
-
- Parameters
- ----------
- arg_name : str
- Name of the parametrized function argument.
- arg_value : Any
- Value of the argument.
- i : int
- The ith iteration of the parametrization.
- id_func : Union[Callable[..., Any], None]
- A callable which maps argument values to :obj:`bool`, :obj:`float`, :obj:`int`,
- or :obj:`str` or anything else. Any object with a different dtype than the first
- will be mapped to an auto-generated id component.
-
- Returns
- -------
- id_component : str
- A part of the final parametrized id.
-
- """
- id_component = id_func(arg_value) if id_func is not None else None
- if isinstance(id_component, (bool, float, int, str)):
- id_component = str(id_component)
- elif isinstance(arg_value, (bool, float, int, str)):
- id_component = str(arg_value)
- else:
- id_component = arg_name + str(i)
- return id_component
diff --git a/src/_pytask/task.py b/src/_pytask/task.py
index d71cb668..7b806af7 100644
--- a/src/_pytask/task.py
+++ b/src/_pytask/task.py
@@ -5,7 +5,6 @@
from typing import Any
from _pytask.config import hookimpl
-from _pytask.mark_utils import has_mark
from _pytask.report import CollectionReport
from _pytask.session import Session
from _pytask.task_utils import COLLECTED_TASKS
@@ -39,24 +38,11 @@ def pytask_collect_file(
collected_reports = []
for name, function in name_to_function.items():
- session.hook.pytask_parametrize_kwarg_to_marker(
- obj=function,
- kwargs=function.pytask_meta.kwargs, # type: ignore[attr-defined]
+ report = session.hook.pytask_collect_task_protocol(
+ session=session, reports=reports, path=path, name=name, obj=function
)
-
- if has_mark(function, "parametrize"):
- names_and_objects = session.hook.pytask_parametrize_task(
- session=session, name=name, obj=function
- )
- else:
- names_and_objects = [(name, function)]
-
- for name_, obj_ in names_and_objects:
- report = session.hook.pytask_collect_task_protocol(
- session=session, reports=reports, path=path, name=name_, obj=obj_
- )
- if report is not None:
- collected_reports.append(report)
+ if report is not None:
+ collected_reports.append(report)
return collected_reports
return None
diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py
index e8e6bd4e..ad541c9d 100644
--- a/src/_pytask/task_utils.py
+++ b/src/_pytask/task_utils.py
@@ -9,7 +9,6 @@
from _pytask.mark import Mark
from _pytask.models import CollectionMetadata
-from _pytask.parametrize_utils import arg_value_to_id_component
from _pytask.shared import find_duplicates
@@ -51,6 +50,13 @@ def task(
"""
def wrapper(func: Callable[..., Any]) -> None:
+ for arg, arg_name in ((name, "name"), (id, "id")):
+ if not (isinstance(arg, str) or arg is None):
+ raise ValueError(
+ f"Argument {arg_name!r} of @pytask.mark.task must be a str, but it "
+ f"is {arg!r}."
+ )
+
unwrapped = inspect.unwrap(func)
raw_path = inspect.getfile(unwrapped)
@@ -104,6 +110,15 @@ def parse_collected_tasks_with_task_marker(
else:
collected_tasks[name] = [i[1] for i in parsed_tasks if i[0] == name][0]
+ # TODO: Remove when parsing dependencies and products from all arguments is
+ # implemented.
+ for task in collected_tasks.values():
+ meta = task.pytask_meta # type: ignore[attr-defined]
+ for marker_name in ("depends_on", "produces"):
+ if marker_name in meta.kwargs:
+ value = meta.kwargs.pop(marker_name)
+ meta.markers.append(Mark(marker_name, (value,), {}))
+
return collected_tasks
@@ -169,7 +184,7 @@ def _generate_ids_for_tasks(
id_ = f"{name}[{i}]"
else:
stringified_args = [
- arg_value_to_id_component(
+ _arg_value_to_id_component(
arg_name=parameter,
arg_value=task.pytask_meta.kwargs.get( # type: ignore[attr-defined]
parameter
@@ -183,3 +198,42 @@ def _generate_ids_for_tasks(
id_ = f"{name}[{id_}]"
out[id_] = task
return out
+
+
+def _arg_value_to_id_component(
+ arg_name: str, arg_value: Any, i: int, id_func: Callable[..., Any] | None
+) -> str:
+ """Create id component from the name and value of the argument.
+
+ First, transform the value of the argument with a user-defined function if given.
+ Otherwise, take the original value. Then, if the value is a :obj:`bool`,
+ :obj:`float`, :obj:`int`, or :obj:`str`, cast it to a string. Otherwise, define a
+ placeholder value from the name of the argument and the iteration.
+
+ Parameters
+ ----------
+ arg_name : str
+ Name of the parametrized function argument.
+ arg_value : Any
+ Value of the argument.
+ i : int
+ The ith iteration of the parametrization.
+ id_func : Union[Callable[..., Any], None]
+ A callable which maps argument values to :obj:`bool`, :obj:`float`, :obj:`int`,
+ or :obj:`str` or anything else. Any object with a different dtype than the first
+ will be mapped to an auto-generated id component.
+
+ Returns
+ -------
+ id_component : str
+ A part of the final parametrized id.
+
+ """
+ id_component = id_func(arg_value) if id_func is not None else None
+ if isinstance(id_component, (bool, float, int, str)):
+ id_component = str(id_component)
+ elif isinstance(arg_value, (bool, float, int, str)):
+ id_component = str(arg_value)
+ else:
+ id_component = arg_name + str(i)
+ return id_component
diff --git a/tests/test_collect_command.py b/tests/test_collect_command.py
index 609594f9..eba87f4c 100644
--- a/tests/test_collect_command.py
+++ b/tests/test_collect_command.py
@@ -81,10 +81,12 @@ def test_collect_parametrized_tasks(runner, tmp_path):
source = """
import pytask
- @pytask.mark.depends_on("in.txt")
- @pytask.mark.parametrize("arg, produces", [(0, "out_0.txt"), (1, "out_1.txt")])
- def task_example(arg):
- pass
+ for arg, produces in [(0, "out_0.txt"), (1, "out_1.txt")]:
+
+ @pytask.mark.task
+ @pytask.mark.depends_on("in.txt")
+ def task_example(arg=arg, produces=produces):
+ pass
"""
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
tmp_path.joinpath("in.txt").touch()
diff --git a/tests/test_live.py b/tests/test_live.py
index 851a11a4..abcfbacf 100644
--- a/tests/test_live.py
+++ b/tests/test_live.py
@@ -262,9 +262,11 @@ def test_full_execution_table_is_displayed_at_the_end_of_execution(tmp_path, run
source = """
import pytask
- @pytask.mark.parametrize("produces", [f"{i}.txt" for i in range(4)])
- def task_create_file(produces):
- produces.touch()
+ for produces in [f"{i}.txt" for i in range(4)]:
+
+ @pytask.mark.task
+ def task_create_file(produces=produces):
+ produces.touch()
"""
# Subfolder to reduce task id and be able to check the output later.
tmp_path.joinpath("d").mkdir()
diff --git a/tests/test_mark.py b/tests/test_mark.py
index 2b79367b..32d468b1 100644
--- a/tests/test_mark.py
+++ b/tests/test_mark.py
@@ -172,16 +172,16 @@ def task_no_2():
],
)
def test_keyword_option_parametrize(tmp_path, expr: str, expected_passed: str) -> None:
- tmp_path.joinpath("task_module.py").write_text(
- textwrap.dedent(
- """
- import pytask
- @pytask.mark.parametrize("arg", [None, 1.3, "2-3"])
- def task_func(arg):
- pass
- """
- )
- )
+ source = """
+ import pytask
+
+ for arg in [None, 1.3, "2-3"]:
+
+ @pytask.mark.task
+ def task_func(arg=arg):
+ pass
+ """
+ tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
session = main({"paths": tmp_path, "expression": expr})
assert session.exit_code == ExitCode.OK
diff --git a/tests/test_parametrize.py b/tests/test_parametrize.py
deleted file mode 100644
index 37994a8b..00000000
--- a/tests/test_parametrize.py
+++ /dev/null
@@ -1,531 +0,0 @@
-from __future__ import annotations
-
-import itertools
-import textwrap
-from contextlib import ExitStack as does_not_raise # noqa: N813
-from typing import NamedTuple
-
-import _pytask.parametrize
-import pytask
-import pytest
-from _pytask.parametrize import _check_if_n_arg_names_matches_n_arg_values
-from _pytask.parametrize import _parse_arg_names
-from _pytask.parametrize import _parse_arg_values
-from _pytask.parametrize import _parse_parametrize_markers
-from _pytask.parametrize import pytask_parametrize_task
-from _pytask.pluginmanager import get_plugin_manager
-from pytask import cli
-from pytask import ExitCode
-from pytask import main
-from pytask import Mark
-from pytask import Session
-
-
-@pytest.fixture(scope="module")
-def session():
- pm = get_plugin_manager()
- pm.register(_pytask.parametrize)
- session = Session(hook=pm.hook)
- return session
-
-
-@pytest.mark.integration()
-def test_pytask_generate_tasks_0(session):
- @pytask.mark.parametrize("i", range(2))
- def func(i): # noqa: ARG001, pragma: no cover
- pass
-
- 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].pytask_meta.kwargs["i"] == 0
- assert names_and_objs[1][1].pytask_meta.kwargs["i"] == 1
-
-
-@pytest.mark.integration()
-@pytest.mark.xfail(strict=True, reason="Cartesian task product is disabled.")
-def test_pytask_generate_tasks_1(session):
- @pytask.mark.parametrize("j", range(2))
- @pytask.mark.parametrize("i", range(2))
- def func(i, j): # noqa: ARG001, pragma: no cover
- pass
-
- pytask_parametrize_task(session, "func", func)
-
-
-@pytest.mark.integration()
-@pytest.mark.xfail(strict=True, reason="Cartesian task product is disabled.")
-def test_pytask_generate_tasks_2(session):
- @pytask.mark.parametrize("j, k", itertools.product(range(2), range(2)))
- @pytask.mark.parametrize("i", range(2))
- def func(i, j, k): # noqa: ARG001, pragma: no cover
- pass
-
- pytask_parametrize_task(session, "func", func)
-
-
-@pytest.mark.unit()
-@pytest.mark.parametrize(
- ("arg_names", "expected"),
- [
- ("i", ("i",)),
- ("i,j", ("i", "j")),
- ("i, j", ("i", "j")),
- (("i", "j"), ("i", "j")),
- (["i", "j"], ("i", "j")),
- ],
-)
-def test_parse_arg_names(arg_names, expected):
- parsed_arg_names = _parse_arg_names(arg_names)
- assert parsed_arg_names == expected
-
-
-class TaskArguments(NamedTuple):
- a: int
- b: int
-
-
-@pytest.mark.unit()
-@pytest.mark.parametrize(
- ("arg_values", "has_single_arg", "expected"),
- [
- (["a", "b", "c"], True, [("a",), ("b",), ("c",)]),
- ([(0, 0), (0, 1), (1, 0)], False, [(0, 0), (0, 1), (1, 0)]),
- ([[0, 0], [0, 1], [1, 0]], False, [(0, 0), (0, 1), (1, 0)]),
- ({"a": 0, "b": 1}, False, [("a",), ("b",)]),
- ([{"a": 0, "b": 1}], True, [({"a": 0, "b": 1},)]),
- ([TaskArguments(1, 2)], False, [(1, 2)]),
- ([TaskArguments(a=1, b=2)], False, [(1, 2)]),
- ([TaskArguments(b=2, a=1)], False, [(1, 2)]),
- ],
-)
-def test_parse_arg_values(arg_values, has_single_arg, expected):
- parsed_arg_values = _parse_arg_values(arg_values, has_single_arg)
- assert parsed_arg_values == expected
-
-
-@pytest.mark.unit()
-@pytest.mark.parametrize(
- ("arg_names", "expectation"),
- [
- ("i", does_not_raise()),
- ("i, j", does_not_raise()),
- (("i", "j"), does_not_raise()),
- (["i", "j"], does_not_raise()),
- (range(1, 2), pytest.raises(TypeError)),
- ({"i": None, "j": None}, pytest.raises(TypeError)),
- ({"i", "j"}, pytest.raises(TypeError)),
- ],
-)
-def test_parse_argnames_raise_error(arg_names, expectation):
- with expectation:
- _parse_arg_names(arg_names)
-
-
-@pytest.mark.integration()
-@pytest.mark.parametrize(
- ("markers", "exp_base_arg_names", "exp_arg_names", "exp_arg_values"),
- [
- (
- [
- Mark("parametrize", ("i", range(2)), {}),
- Mark("parametrize", ("j", range(2)), {}),
- ],
- [("i",), ("j",)],
- [[("0",), ("1",)], [("0",), ("1",)]],
- [[(0,), (1,)], [(0,), (1,)]],
- ),
- (
- [Mark("parametrize", ("i", range(3)), {})],
- [("i",)],
- [[("0",), ("1",), ("2",)]],
- [[(0,), (1,), (2,)]],
- ),
- ],
-)
-def test_parse_parametrize_markers(
- markers, exp_base_arg_names, exp_arg_names, exp_arg_values
-):
- base_arg_names, arg_names, arg_values = _parse_parametrize_markers(markers, "task_")
-
- assert base_arg_names == exp_base_arg_names
- assert arg_names == exp_arg_names
- assert arg_values == exp_arg_values
-
-
-@pytest.mark.end_to_end()
-def test_parametrizing_tasks(tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i, produces', [(1, "1.txt"), (2, "2.txt")])
- def task_write_numbers_to_file(produces, i):
- produces.write_text(str(i))
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.OK
- for i in range(1, 3):
- assert tmp_path.joinpath(f"{i}.txt").read_text() == str(i)
-
-
-@pytest.mark.end_to_end()
-def test_parametrizing_dependencies_and_targets(tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i, produces', [(1, "1.txt"), (2, "2.txt")])
- def task_save_numbers(i, produces):
- produces.write_text(str(i))
-
- @pytask.mark.parametrize("depends_on, produces", [
- ("1.txt", "1_out.txt"), ("2.txt", "2_out.txt")
- ])
- def task_save_numbers_again(depends_on, produces):
- produces.write_text(depends_on.read_text())
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.OK
-
-
-@pytest.mark.end_to_end()
-def test_parametrize_iterator(tmp_path):
- """`parametrize` should work with generators."""
- source = """
- import pytask
- def gen():
- yield 1
- yield 2
- yield 3
- @pytask.mark.parametrize('a', gen())
- def task_func(a):
- pass
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
- session = main({"paths": tmp_path})
- assert session.exit_code == ExitCode.OK
- assert len(session.execution_reports) == 3
-
-
-@pytest.mark.end_to_end()
-def test_raise_error_if_function_does_not_use_parametrized_arguments(tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i', range(2))
- def task_func():
- pass
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.FAILED
- assert isinstance(session.execution_reports[0].exc_info[1], TypeError)
- assert isinstance(session.execution_reports[1].exc_info[1], TypeError)
-
-
-@pytest.mark.end_to_end()
-@pytest.mark.parametrize(
- ("arg_values", "ids"),
- [
- (range(2), ["first_trial", "second_trial"]),
- ([True, False], ["first_trial", "second_trial"]),
- ],
-)
-def test_parametrize_w_ids(tmp_path, arg_values, ids):
- tmp_path.joinpath("task_module.py").write_text(
- textwrap.dedent(
- f"""
- import pytask
-
- @pytask.mark.parametrize('i', {arg_values}, ids={ids})
- def task_func(i):
- pass
- """
- )
- )
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.OK
- for task, id_ in zip(session.tasks, ids):
- assert id_ in task.name
-
-
-@pytest.mark.end_to_end()
-def test_two_parametrize_w_ids(runner, tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i', range(2), ids=["2.1", "2.2"])
- @pytask.mark.parametrize('j', range(2), ids=["1.1", "1.2"])
- def task_func(i, j):
- pass
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.COLLECTION_FAILED
- assert "You cannot apply @pytask.mark.parametrize multiple" in result.output
-
-
-@pytest.mark.end_to_end()
-@pytest.mark.parametrize("ids", [["a"], list("abc"), ((1,), (2,)), ({0}, {1})])
-def test_raise_error_for_irregular_ids(tmp_path, ids):
- tmp_path.joinpath("task_module.py").write_text(
- textwrap.dedent(
- f"""
- import pytask
-
- @pytask.mark.parametrize('i', range(2), ids={ids})
- def task_func():
- pass
- """
- )
- )
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.COLLECTION_FAILED
- assert isinstance(session.collection_reports[0].exc_info[1], ValueError)
-
-
-@pytest.mark.end_to_end()
-def test_raise_error_if_parametrization_produces_non_unique_tasks(tmp_path):
- tmp_path.joinpath("task_module.py").write_text(
- textwrap.dedent(
- """
- import pytask
-
- @pytask.mark.parametrize('i', [0, 0])
- def task_func(i):
- pass
- """
- )
- )
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.COLLECTION_FAILED
- assert isinstance(session.collection_reports[0].exc_info[1], ValueError)
-
-
-@pytest.mark.end_to_end()
-@pytest.mark.parametrize(
- ("arg_names", "arg_values", "content"),
- [
- (
- ("i", "j"),
- [1, 2, 3],
- [
- "ValueError",
- "with 2 'arg_names', ('i', 'j'),",
- "'arg_values' is 1.",
- "parametrization no. 0:",
- "(1,)",
- ],
- ),
- (
- ("i", "j"),
- [(1, 2, 3)],
- [
- "ValueError",
- "with 2 'arg_names', ('i', 'j'),",
- "'arg_values' is 3.",
- "parametrization no. 0:",
- "(1, 2, 3)",
- ],
- ),
- (
- ("i", "j"),
- [(1, 2), (1, 2, 3)],
- [
- "ValueError",
- "with 2 'arg_names', ('i', 'j'),",
- "'arg_values' is 2 or 3.",
- "parametrization no. 1:",
- "(1, 2, 3)",
- ],
- ),
- ],
-)
-def test_wrong_number_of_names_and_wrong_number_of_arguments(
- tmp_path, runner, arg_names, arg_values, content
-):
- source = f"""
- import pytask
-
- @pytask.mark.parametrize({arg_names}, {arg_values})
- def task_func():
- pass
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.COLLECTION_FAILED
- for c in content:
- assert c in result.output
-
-
-@pytest.mark.end_to_end()
-def test_generators_are_removed_from_depends_on_produces(tmp_path):
- source = """
- from pathlib import Path
- import pytask
-
- @pytask.mark.parametrize("produces", [
- ((x for x in ["out.txt", "out_2.txt"]),),
- ["in.txt"],
- ])
- def task_example(produces):
- produces = {0: produces} if isinstance(produces, Path) else produces
- for p in produces.values():
- p.write_text("hihi")
- """
- tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
-
- session = main({"paths": tmp_path})
- assert session.exit_code == ExitCode.OK
- assert session.tasks[0].function.pytask_meta.markers == []
-
-
-@pytest.mark.end_to_end()
-def test_parametrizing_tasks_with_namedtuples(runner, tmp_path):
- source = """
- from typing import NamedTuple
- import pytask
- from pathlib import Path
-
-
- class Task(NamedTuple):
- i: int
- produces: Path
-
-
- @pytask.mark.parametrize('i, produces', [
- Task(i=1, produces="1.txt"), Task(produces="2.txt", i=2),
- ])
- def task_write_numbers_to_file(produces, i):
- produces.write_text(str(i))
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.OK
- for i in range(1, 3):
- assert tmp_path.joinpath(f"{i}.txt").read_text() == str(i)
-
-
-@pytest.mark.end_to_end()
-def test_parametrization_with_different_n_of_arg_names_and_arg_values(runner, tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i, produces', [(1, "1.txt"), (2, 3, "2.txt")])
- def task_write_numbers_to_file(produces, i):
- produces.write_text(str(i))
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.COLLECTION_FAILED
- assert "Task 'task_write_numbers_to_file' is parametrized with 2" in result.output
-
-
-@pytest.mark.unit()
-@pytest.mark.parametrize(
- ("arg_names", "arg_values", "name", "expectation"),
- [
- pytest.param(
- ("a",),
- [(1,), (2,)],
- "task_name",
- does_not_raise(),
- id="normal one argument parametrization",
- ),
- pytest.param(
- ("a", "b"),
- [(1, 2), (3, 4)],
- "task_name",
- does_not_raise(),
- id="normal two argument argument parametrization",
- ),
- pytest.param(
- ("a",),
- [(1, 2), (2,)],
- "task_name",
- pytest.raises(ValueError, match="Task 'task_name' is parametrized with 1"),
- id="error with one argument parametrization",
- ),
- pytest.param(
- ("a", "b"),
- [(1, 2), (3, 4, 5)],
- "task_name",
- pytest.raises(ValueError, match="Task 'task_name' is parametrized with 2"),
- id="error with two argument argument parametrization",
- ),
- ],
-)
-def test_check_if_n_arg_names_matches_n_arg_values(
- arg_names, arg_values, name, expectation
-):
- with expectation:
- _check_if_n_arg_names_matches_n_arg_values(arg_names, arg_values, name)
-
-
-@pytest.mark.end_to_end()
-def test_parametrize_with_single_dict(tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i', [{"a": 1}, {"a": 1.0}])
- def task_write_numbers_to_file(i):
- assert i["a"] == 1
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.OK
-
-
-@pytest.mark.end_to_end()
-def test_deprecation_warning_for_parametrizing_tasks(runner, tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i, produces', [(1, "1.txt"), (2, "2.txt")])
- def task_write_numbers_to_file(produces, i):
- produces.write_text(str(i))
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
-
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.OK
- assert "FutureWarning" in result.output
-
-
-@pytest.mark.end_to_end()
-def test_silence_deprecation_warning_for_parametrizing_tasks(runner, tmp_path):
- source = """
- import pytask
-
- @pytask.mark.parametrize('i, produces', [(1, "1.txt"), (2, "2.txt")])
- def task_write_numbers_to_file(produces, i):
- produces.write_text(str(i))
- """
- tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
- tmp_path.joinpath("pyproject.toml").write_text(
- "[tool.pytask.ini_options]\nsilence_parametrize_deprecation = true"
- )
-
- result = runner.invoke(cli, [tmp_path.as_posix()])
-
- assert result.exit_code == ExitCode.OK
- assert "FutureWarning" not in result.output
diff --git a/tests/test_task.py b/tests/test_task.py
index 33905c26..3313fdb7 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -33,35 +33,6 @@ def {func_name}(produces):
assert session.tasks[0].name.endswith(f"task_module.py::{func_name}")
-@pytest.mark.end_to_end()
-@pytest.mark.parametrize("func_name", ["task_example", "func"])
-@pytest.mark.parametrize("task_name", ["the_only_task", None])
-def test_task_with_task_decorator_with_parametrize(tmp_path, func_name, task_name):
- task_decorator_input = f"{task_name!r}" if task_name else task_name
- source = f"""
- import pytask
-
- @pytask.mark.task({task_decorator_input})
- @pytask.mark.parametrize("produces", ["out_1.txt", "out_2.txt"])
- def {func_name}(produces):
- produces.write_text("Hello. It's me.")
- """
- path_to_module = tmp_path.joinpath("task_module.py")
- path_to_module.write_text(textwrap.dedent(source))
-
- session = main({"paths": tmp_path})
-
- assert session.exit_code == ExitCode.OK
-
- file_name = path_to_module.name
- if task_name:
- assert session.tasks[0].name.endswith(f"{file_name}::{task_name}[out_1.txt]")
- assert session.tasks[1].name.endswith(f"{file_name}::{task_name}[out_2.txt]")
- else:
- assert session.tasks[0].name.endswith(f"{file_name}::{func_name}[out_1.txt]")
- assert session.tasks[1].name.endswith(f"{file_name}::{func_name}[out_2.txt]")
-
-
@pytest.mark.end_to_end()
def test_parametrization_in_for_loop(tmp_path, runner):
source = """
@@ -178,7 +149,7 @@ def test_parametrization_in_for_loop_with_ids(tmp_path, runner):
for i in range(2):
@pytask.mark.task(
- "deco_task", id=i, kwargs={"i": i, "produces": f"out_{i}.txt"}
+ "deco_task", id=str(i), kwargs={"i": i, "produces": f"out_{i}.txt"}
)
def example(produces, i):
produces.write_text(str(i))
@@ -429,3 +400,41 @@ def task_example():
assert "task_example[0]" in result.output
assert "task_example[1]" in result.output
assert "Collected 2 tasks" in result.output
+
+
+@pytest.mark.end_to_end()
+@pytest.mark.parametrize(
+ "irregular_id", [1, (1,), [1], {1}, ["a"], list("abc"), ((1,), (2,)), ({0}, {1})]
+)
+def test_raise_errors_for_irregular_ids(runner, tmp_path, irregular_id):
+ source = f"""
+ import pytask
+
+ @pytask.mark.task(id={irregular_id})
+ def task_example():
+ pass
+ """
+ tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
+
+ result = runner.invoke(cli, [tmp_path.as_posix()])
+
+ assert result.exit_code == ExitCode.COLLECTION_FAILED
+ assert "Argument 'id' of @pytask.mark.task" in result.output
+
+
+@pytest.mark.end_to_end()
+@pytest.mark.xfail(reason="Should fail. Mandatory products will fix the issue.")
+def test_raise_error_if_parametrization_produces_non_unique_tasks(tmp_path):
+ source = """
+ import pytask
+
+ for i in [0, 0]:
+ @pytask.mark.task(id=str(i))
+ def task_func(i=i):
+ pass
+ """
+ tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
+ session = main({"paths": tmp_path})
+
+ assert session.exit_code == ExitCode.COLLECTION_FAILED
+ assert isinstance(session.collection_reports[0].exc_info[1], ValueError)
diff --git a/tests/test_parametrize_utils.py b/tests/test_task_utils.py
similarity index 84%
rename from tests/test_parametrize_utils.py
rename to tests/test_task_utils.py
index 8b483208..779cccd1 100644
--- a/tests/test_parametrize_utils.py
+++ b/tests/test_task_utils.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import pytest
-from _pytask.parametrize_utils import arg_value_to_id_component
+from _pytask.task_utils import _arg_value_to_id_component
@pytest.mark.unit()
@@ -22,5 +22,5 @@
],
)
def test_arg_value_to_id_component(arg_name, arg_value, i, id_func, expected):
- result = arg_value_to_id_component(arg_name, arg_value, i, id_func)
+ result = _arg_value_to_id_component(arg_name, arg_value, i, id_func)
assert result == expected