Skip to content

Add more test cases for parametrizations. #221

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 12 commits into from
Feb 21, 2022
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
20 changes: 10 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.. image:: https://raw.githubusercontent.com/pytask-dev/pytask/main/docs/source/_static/images/pytask_w_text.png
:target: https://pytask-dev.readthedocs.io/en/latest
:target: https://pytask-dev.readthedocs.io/en/stable
:align: center
:width: 50%
:alt: pytask
Expand Down Expand Up @@ -27,7 +27,7 @@
:target: https://pypi.org/project/pytask

.. image:: https://readthedocs.org/projects/pytask-dev/badge/?version=latest
:target: https://pytask-dev.readthedocs.io/en/latest
:target: https://pytask-dev.readthedocs.io/en/stable

.. image:: https://img.shields.io/github/workflow/status/pytask-dev/pytask/Continuous%20Integration%20Workflow/main
:target: https://github.com/pytask-dev/pytask/actions?query=branch%3Amain
Expand Down Expand Up @@ -57,12 +57,12 @@ projects. Its features include:
do not execute it.

- **Debug mode.** `Jump into the debugger
<https://pytask-dev.readthedocs.io/en/latest/tutorials/how_to_debug.html>`_ if a task
<https://pytask-dev.readthedocs.io/en/stable/tutorials/how_to_debug.html>`_ if a task
fails, get feedback quickly, and be more productive.

- **Select tasks via expressions.** Run only a subset of tasks with `expressions and
marker expressions
<https://pytask-dev.readthedocs.io/en/latest/tutorials/how_to_select_tasks.html>`_
<https://pytask-dev.readthedocs.io/en/stable/tutorials/how_to_select_tasks.html>`_
known from pytest.

- **Easily extensible with plugins**. pytask is built on top of `pluggy
Expand All @@ -73,7 +73,7 @@ projects. Its features include:
<https://github.com/pytask-dev/pytask-r>`_, and `Stata
<https://github.com/pytask-dev/pytask-stata>`_ and more can be found `here
<https://github.com/topics/pytask>`_. Read in `this tutorial
<https://pytask-dev.readthedocs.io/en/latest/tutorials/how_to_use_plugins.html>`_ how
<https://pytask-dev.readthedocs.io/en/stable/tutorials/how_to_use_plugins.html>`_ how
to use and create plugins with a `cookiecutter
<https://github.com/pytask-dev/cookiecutter-pytask-plugin>`_.

Expand Down Expand Up @@ -105,7 +105,7 @@ example, installed via the `Microsoft Store <https://aka.ms/terminal>`_.
To quickly set up a new project, use the `cookiecutter-pytask-project
<https://github.com/pytask-dev/cookiecutter-pytask-project>`_ template or start from
`other templates or example projects
<https://pytask-dev.readthedocs.io/en/latest/how_to_guides/bp_templates_and_projects.html>`_.
<https://pytask-dev.readthedocs.io/en/stable/how_to_guides/bp_templates_and_projects.html>`_.

.. end-installation

Expand Down Expand Up @@ -144,16 +144,16 @@ To execute the task, enter ``pytask`` on the command-line
Documentation
-------------

The documentation can be found under https://pytask-dev.readthedocs.io/en/latest with
`tutorials <https://pytask-dev.readthedocs.io/en/latest/tutorials/index.html>`_ and
The documentation can be found under https://pytask-dev.readthedocs.io/en/stable with
`tutorials <https://pytask-dev.readthedocs.io/en/stable/tutorials/index.html>`_ and
guides for `best practices
<https://pytask-dev.readthedocs.io/en/latest/how_to_guides/index.html>`_.
<https://pytask-dev.readthedocs.io/en/stable/how_to_guides/index.html>`_.


Changes
-------

Consult the `release notes <https://pytask-dev.readthedocs.io/en/latest/changes.html>`_
Consult the `release notes <https://pytask-dev.readthedocs.io/en/stable/changes.html>`_
to find out about what is new.


Expand Down
1 change: 1 addition & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
- :pull:`218` removes ``depends_on`` and ``produces`` from the task function when
parsed.
- :pull:`219` removes some leftovers from pytest in :class:`~_pytask.mark.Mark`.
- :pull:`221` adds more test cases for parametrizations.
- :pull:`222` adds an automated Github Actions job for creating a list pytask plugins.


Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies:
- pydot
- pytest
- pytest-cov
- pytest-xdist
- tox-conda

# Documentation
Expand Down
6 changes: 3 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = pytask
description = In its highest aspirations, pytask tries to be pytest as a build system.
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://pytask-dev.readthedocs.io/en/latest
url = https://pytask-dev.readthedocs.io/en/stable
author = Tobias Raabe
author_email = [email protected]
license = MIT
Expand All @@ -26,8 +26,8 @@ classifiers =
Topic :: Scientific/Engineering
Topic :: Software Development :: Build Tools
project_urls =
Changelog = https://pytask-dev.readthedocs.io/en/latest/changes.html
Documentation = https://pytask-dev.readthedocs.io/en/latest
Changelog = https://pytask-dev.readthedocs.io/en/stable/changes.html
Documentation = https://pytask-dev.readthedocs.io/en/stable
Github = https://github.com/pytask-dev/pytask
Tracker = https://github.com/pytask-dev/pytask/issues

Expand Down
9 changes: 7 additions & 2 deletions src/_pytask/parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,13 @@ def pytask_parametrize_task(

if len(markers) > 1:
raise NotImplementedError(
"Multiple parametrizations are currently not implemented since it is "
"not possible to define products for tasks from a Cartesian product."
"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_parametrizations.html."
)

if has_marker(obj, "task"):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def test_pytask_collect_node_does_not_raise_error_if_path_is_not_normalized(
if is_absolute:
collected_node = tmp_path / collected_node

with warnings.catch_warnings() as record:
with warnings.catch_warnings(record=True) as record:
result = pytask_collect_node(session, task_path, collected_node)
assert not record

Expand Down
2 changes: 1 addition & 1 deletion tests/test_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def test_live_execution_displays_subset_of_table(capsys, tmp_path, n_entries_in_

live_manager.start()
live.update_running_tasks(running_task)
live_manager.stop()
live_manager.stop(transient=False)

captured = capsys.readouterr()
assert "Task" in captured.out
Expand Down
170 changes: 129 additions & 41 deletions tests/test_parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
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 _arg_value_to_id_component
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
Expand Down Expand Up @@ -54,14 +57,7 @@ def test_pytask_generate_tasks_1(session):
def func(i, j): # noqa: U100
pass

names_and_objs = pytask_parametrize_task(session, "func", func)

for (name, func), values in zip(
names_and_objs, itertools.product(range(2), range(2))
):
assert name == f"func[{values[0]}-{values[1]}]"
assert func.keywords["i"] == values[0]
assert func.keywords["j"] == values[1]
pytask_parametrize_task(session, "func", func)


@pytest.mark.integration
Expand All @@ -72,16 +68,7 @@ def test_pytask_generate_tasks_2(session):
def func(i, j, k): # noqa: U100
pass

names_and_objs = pytask_parametrize_task(session, "func", func)

for (name, func), values in zip(
names_and_objs,
[(i, j, k) for i in range(2) for j in range(2) for k in range(2)],
):
assert name == f"func[{values[0]}-{values[1]}-{values[2]}]"
assert func.keywords["i"] == values[0]
assert func.keywords["j"] == values[1]
assert func.keywords["k"] == values[2]
pytask_parametrize_task(session, "func", func)


@pytest.mark.integration
Expand Down Expand Up @@ -109,9 +96,32 @@ def func():
(["i", "j"], ("i", "j")),
],
)
def test_parse_argnames(arg_names, expected):
parsed_argnames = _parse_arg_names(arg_names)
assert parsed_argnames == expected
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, expected",
[
(["a", "b", "c"], [("a",), ("b",), ("c",)]),
([(0, 0), (0, 1), (1, 0)], [(0, 0), (0, 1), (1, 0)]),
([[0, 0], [0, 1], [1, 0]], [(0, 0), (0, 1), (1, 0)]),
({"a": 0, "b": 1}, [("a",), ("b",)]),
([TaskArguments(1, 2)], [(1, 2)]),
([TaskArguments(a=1, b=2)], [(1, 2)]),
([TaskArguments(b=2, a=1)], [(1, 2)]),
],
)
def test_parse_arg_values(arg_values, expected):
parsed_arg_values = _parse_arg_values(arg_values)
assert parsed_arg_values == expected


@pytest.mark.unit
Expand Down Expand Up @@ -267,28 +277,20 @@ def task_func(i):


@pytest.mark.end_to_end
@pytest.mark.xfail(strict=True, reason="Cartesian task product is disabled.")
def test_two_parametrize_w_ids(tmp_path):
tmp_path.joinpath("task_module.py").write_text(
textwrap.dedent(
"""
import pytask
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
"""
)
)
session = main({"paths": tmp_path})
@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 session.exit_code == 0
assert len(session.tasks) == 4
for task, id_ in zip(
session.tasks, ["[1.1-2.1]", "[1.1-2.2]", "[1.2-2.1]", "[1.2-2.2]"]
):
assert id_ in task.name
assert result.exit_code == ExitCode.COLLECTION_FAILED
assert "You cannot apply @pytask.mark.parametrize multiple" in result.output


@pytest.mark.end_to_end
Expand Down Expand Up @@ -430,3 +432,89 @@ def task_example(produces):
session = main({"paths": tmp_path})
assert session.exit_code == 0
assert session.tasks[0].function.__wrapped__.pytaskmark == []


@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 == 0
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)