From b1915599e2091b96094f8fda27eaca02b31661f3 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sat, 7 Oct 2023 20:09:26 +0200 Subject: [PATCH] Update more parts of the documentation. (#441) --- docs/source/changes.md | 3 +- .../bp_scalable_repetitions_of_tasks.md | 112 ------------------ docs/source/how_to_guides/bp_scaling_tasks.md | 103 ++++++++++++++++ .../bp_structure_of_task_files.md | 33 ++++-- docs/source/how_to_guides/index.md | 2 +- .../how_to_guides/writing_custom_nodes.md | 6 +- .../repeating_tasks_with_different_inputs.md | 6 +- ...ns_of_tasks_1.py => bp_scaling_tasks_1.py} | 9 +- ...ns_of_tasks_2.py => bp_scaling_tasks_2.py} | 11 +- ...ns_of_tasks_3.py => bp_scaling_tasks_3.py} | 0 ...ns_of_tasks_4.py => bp_scaling_tasks_4.py} | 0 .../bp_structure_of_task_files.py | 4 - src/_pytask/collect_utils.py | 4 +- src/_pytask/mark/__init__.pyi | 6 +- src/_pytask/mark/structures.py | 2 +- tests/test_live.py | 22 ---- tests/test_task.py | 22 ++++ tests/test_typing.py | 2 - 18 files changed, 178 insertions(+), 169 deletions(-) delete mode 100644 docs/source/how_to_guides/bp_scalable_repetitions_of_tasks.md create mode 100644 docs/source/how_to_guides/bp_scaling_tasks.md rename docs_src/how_to_guides/{bp_scalable_repetitions_of_tasks_1.py => bp_scaling_tasks_1.py} (56%) rename docs_src/how_to_guides/{bp_scalable_repetitions_of_tasks_2.py => bp_scaling_tasks_2.py} (70%) rename docs_src/how_to_guides/{bp_scalable_repetitions_of_tasks_3.py => bp_scaling_tasks_3.py} (100%) rename docs_src/how_to_guides/{bp_scalable_repetitions_of_tasks_4.py => bp_scaling_tasks_4.py} (100%) diff --git a/docs/source/changes.md b/docs/source/changes.md index ce88c03e..9b92ba2c 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -5,7 +5,7 @@ chronological order. Releases follow [semantic versioning](https://semver.org/) releases are available on [PyPI](https://pypi.org/project/pytask) and [Anaconda.org](https://anaconda.org/conda-forge/pytask). -## 0.4.0 - 2023-xx-xx +## 0.4.0 - 2023-10-07 - {pull}`323` remove Python 3.7 support and use a new Github action to provide mamba. - {pull}`384` allows to parse dependencies from every function argument if `depends_on` @@ -56,6 +56,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and {func}`pytask.is_task_function`. - {pull}`438` clarifies some types. - {pull}`440` refines more types. +- {pull}`441` updates more parts of the documentation. - {pull}`442` allows users to import `from pytask import mark` and use `@mark.skip`. ## 0.3.2 - 2023-06-07 diff --git a/docs/source/how_to_guides/bp_scalable_repetitions_of_tasks.md b/docs/source/how_to_guides/bp_scalable_repetitions_of_tasks.md deleted file mode 100644 index 2df1f694..00000000 --- a/docs/source/how_to_guides/bp_scalable_repetitions_of_tasks.md +++ /dev/null @@ -1,112 +0,0 @@ -# Scalable repetitions of tasks - -This section advises on how to use repetitions to scale your project quickly. - -## TL;DR - -- Loop over dictionaries that map ids to `kwargs` to create multiple tasks. -- Create the dictionary with a separate function. -- Create functions to build intermediate objects like output paths which can be shared - more easily across tasks than the generated values. - -## Scalability - -Parametrizations allow scaling tasks from $1$ to $N$ in a simple way. What is easily -overlooked is that parametrizations usually trigger other parametrizations and the -growth in tasks is more $1$ to $N \cdot M \cdot \dots$ or $1$ to $N^{M \cdot \dots}$. - -This guide lays out a simple, modular, and scalable structure to fight complexity. - -For example, assume we have four datasets with one binary dependent variable and some -independent variables. We fit three models on each data set: a linear model, a logistic -model, and a decision tree. In total, we have $4 \cdot 3 = 12$ tasks. - -First, let us look at the folder and file structure of such a project. - -``` -my_project -├───pyproject.toml -│ -├───src -│ └───my_project -│ ├────config.py -│ │ -│ ├───data -│ │ ├────data_0.csv -│ │ ├────data_1.csv -│ │ ├────data_2.csv -│ │ └────data_3.csv -│ │ -│ ├───data_preparation -│ │ ├────__init__.py -│ │ ├────config.py -│ │ └────task_prepare_data.py -│ │ -│ └───estimation -│ ├────__init__.py -│ ├────config.py -│ └────task_estimate_models.py -│ -│ -├───setup.py -│ -├───.pytask.sqlite3 -│ -└───bld -``` - -The folder structure, the main `config.py` which holds `SRC` and `BLD`, and the tasks -follow the same structure advocated throughout the tutorials. - -What is new are the local configuration files in each subfolder of `my_project`, which -contain objects shared across tasks. For example, `config.py` holds the paths to the -processed data and the names of the data sets. - -```{literalinclude} ../../../docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_1.py -``` - -The task file `task_prepare_data.py` uses these objects to build the parametrization. - -```{literalinclude} ../../../docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_2.py -``` - -All arguments for the loop and the {func}`@task ` decorator is built within -a function to keep the logic in one place and the module's namespace clean. - -Ids are used to make the task {ref}`ids ` more descriptive and to simplify their -selection with {ref}`expressions `. Here is an example of the task ids with -an explicit id. - -``` -# With id -.../my_project/data_preparation/task_prepare_data.py::task_prepare_data[data_0] -``` - -Next, we move to the estimation to see how we can build another parametrization upon the -previous one. - -```{literalinclude} ../../../docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_3.py -``` - -In the local configuration, we define `ESTIMATIONS` which combines the information on -data and model. The dictionary's key can be used as a task id whenever the estimation is -involved. It allows triggering all tasks related to one estimation - estimation, -figures, tables - with one command. - -```console -pytask -k linear_probability_data_0 -``` - -And here is the task file. - -```{literalinclude} ../../../docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_4.py -``` - -Replicating this pattern across a project allows a clean way to define parametrizations. - -## Extending parametrizations - -Some parametrized tasks are costly to run - costly in terms of computing power, memory, -or time. Users often extend parametrizations triggering all parametrizations to be -rerun. Thus, use the {func}`@pytask.mark.persist ` decorator, which -is explained in more detail in this {doc}`tutorial <../tutorials/making_tasks_persist>`. diff --git a/docs/source/how_to_guides/bp_scaling_tasks.md b/docs/source/how_to_guides/bp_scaling_tasks.md new file mode 100644 index 00000000..e30ac448 --- /dev/null +++ b/docs/source/how_to_guides/bp_scaling_tasks.md @@ -0,0 +1,103 @@ +# Scaling tasks + +In any bigger project you quickly come to the point where you stack multiple repetitions +of tasks on top of each other. + +For example, you have one dataset, four different ways to prepare it, and three +statistical models to analyze the data. The cartesian product of all steps combined +comprises twelve differently fitted models. + +Here you find some tips on how to set up your tasks such that you can easily modify the +cartesian product of steps. + +## Scalability + +Let us dive right into the aforementioned example. We start with one dataset `data.csv`. +Then, we will create four different specifications of the data and, finally, fit three +different models to each specification. + +This is the structure of the project. + +``` +my_project +├───pyproject.toml +│ +├───src +│ └───my_project +│ ├────config.py +│ │ +│ ├───data +│ │ └────data.csv +│ │ +│ ├───data_preparation +│ │ ├────__init__.py +│ │ ├────config.py +│ │ └────task_prepare_data.py +│ │ +│ └───estimation +│ ├────__init__.py +│ ├────config.py +│ └────task_estimate_models.py +│ +│ +├───setup.py +│ +├───.pytask.sqlite3 +│ +└───bld +``` + +The folder structure, the main `config.py` which holds `SRC` and `BLD`, and the tasks +follow the same structure advocated throughout the tutorials. + +New are the local configuration files in each subfolder of `my_project`, which contain +objects shared across tasks. For example, `config.py` holds the paths to the processed +data and the names of the data sets. + +```{literalinclude} ../../../docs_src/how_to_guides/bp_scaling_tasks_1.py +``` + +The task file `task_prepare_data.py` uses these objects to build the repetitions. + +```{literalinclude} ../../../docs_src/how_to_guides/bp_scaling_tasks_2.py +``` + +All arguments for the loop and the {func}`@task ` decorator are built +within a function to keep the logic in one place and the module's namespace clean. + +Ids are used to make the task {ref}`ids ` more descriptive and to simplify their +selection with {ref}`expressions `. Here is an example of the task ids with +an explicit id. + +``` +# With id +.../my_project/data_preparation/task_prepare_data.py::task_prepare_data[data_0] +``` + +Next, we move to the estimation to see how we can build another repetition on top. + +```{literalinclude} ../../../docs_src/how_to_guides/bp_scaling_tasks_3.py +``` + +In the local configuration, we define `ESTIMATIONS` which combines the information on +data and model. The dictionary's key can be used as a task id whenever the estimation is +involved. It allows triggering all tasks related to one estimation - estimation, +figures, tables - with one command. + +```console +pytask -k linear_probability_data_0 +``` + +And here is the task file. + +```{literalinclude} ../../../docs_src/how_to_guides/bp_scaling_tasks_4.py +``` + +Replicating this pattern across a project allows a clean way to define repetitions. + +## Extending repetitions + +Some parametrized tasks are costly to run - costly in terms of computing power, memory, +or time. Users often extend repetitions triggering all repetitions to be rerun. Thus, +use the {func}`@pytask.mark.persist ` decorator, which is explained +in more detail in this {doc}`tutorial <../tutorials/making_tasks_persist>`. diff --git a/docs/source/how_to_guides/bp_structure_of_task_files.md b/docs/source/how_to_guides/bp_structure_of_task_files.md index 22138507..d4ab2779 100644 --- a/docs/source/how_to_guides/bp_structure_of_task_files.md +++ b/docs/source/how_to_guides/bp_structure_of_task_files.md @@ -1,16 +1,20 @@ # Structure of task files -This section provides advice on how to structure task files. +This guide presents some best-practices for structuring your task files. You do not have +to follow them to use pytask or to create a reproducible research project. But, if you +are looking for orientation or inspiration, here are some tips. ## TL;DR -- There might be multiple task functions in a task module, but only if the code is still - readable and not too complex and if runtime for all tasks is low. +- Use task modules to separate task functions from another. Separating tasks by the + stages in research project like data management, analysis, plotting is a good start. + Separate further when task modules become crowded. -- A task function should be the first function in a task module. +- Task functions should be at the top of a task module to easily identify what the + module is for. :::{seealso} - The only exception might be for {doc}`repetitions `. + The only exception might be for {doc}`repetitions `. ::: - The purpose of the task function is to handle IO operations like loading and saving @@ -20,8 +24,9 @@ This section provides advice on how to structure task files. - Non-task functions in the task module are {term}`private functions ` and only used within this task module. The functions should not have side-effects. -- Functions used to accomplish tasks in multiple task modules should have their own - module. +- It should never be necessary to import from task modules. So if you need a function in + multiple task modules, put it in a separate module (which does not start with + `task_`). ## Best Practices @@ -29,16 +34,22 @@ This section provides advice on how to structure task files. There are two reasons to split tasks across several modules. -The first reason concerns readability and complexity. Multiple tasks deal with -(slightly) different concepts and, thus, should be split content-wise. Even if tasks -deal with the same concept, they might be very complex on its own and separate modules -help the reader (most likely you or your colleagues) to focus on one thing. +The first reason concerns readability and complexity. Tasks deal with different concepts +and, thus, should be split. Even if tasks deal with the same concept, they might becna +very complex and separate modules help the reader (most likely you or your colleagues) +to focus on one thing. The second reason is about runtime. If a task module is changed, all tasks within the module are re-run. If the runtime of all tasks in the module is high, you wait longer for your tasks to finish or until an error occurs which prolongs your feedback loops and hurts your productivity. +:::{seealso} +Use {func}`@pytask.mark.persist ` if you want to avoid accidentally +triggering an expensive task. It is also explained in [this +tutorial](../tutorials/making_tasks_persist). +::: + ### Structure of the module For the following example, let us assume that the task module contains one task. diff --git a/docs/source/how_to_guides/index.md b/docs/source/how_to_guides/index.md index afe54999..025284eb 100644 --- a/docs/source/how_to_guides/index.md +++ b/docs/source/how_to_guides/index.md @@ -38,5 +38,5 @@ maxdepth: 1 bp_structure_of_a_research_project bp_structure_of_task_files bp_templates_and_projects -bp_scalable_repetitions_of_tasks +bp_scaling_tasks ``` diff --git a/docs/source/how_to_guides/writing_custom_nodes.md b/docs/source/how_to_guides/writing_custom_nodes.md index 698ec51c..bbaadc7c 100644 --- a/docs/source/how_to_guides/writing_custom_nodes.md +++ b/docs/source/how_to_guides/writing_custom_nodes.md @@ -10,8 +10,8 @@ your own to improve your workflows. ## Use-case A typical task operation is to load data like a {class}`pandas.DataFrame` from a pickle -file, transform it, and store it on disk. The usual way would be to use paths to point to -inputs and outputs and call {func}`pandas.read_pickle` and +file, transform it, and store it on disk. The usual way would be to use paths to point +to inputs and outputs and call {func}`pandas.read_pickle` and {meth}`pandas.DataFrame.to_pickle`. ```{literalinclude} ../../../docs_src/how_to_guides/writing_custom_nodes_example_1.py @@ -54,7 +54,7 @@ A custom node needs to follow an interface so that pytask can perform several ac - Load and save values when tasks are executed. This interface is defined by protocols [^structural-subtyping]. A custom node must -follow at least the protocol {class}`pytask.Node` or, even better, +follow at least the protocol {class}`pytask.PNode` or, even better, {class}`pytask.PPathNode` if it is based on a path. The common node for paths, {class}`pytask.PathNode`, follows the protocol {class}`pytask.PPathNode`. diff --git a/docs/source/tutorials/repeating_tasks_with_different_inputs.md b/docs/source/tutorials/repeating_tasks_with_different_inputs.md index d3b467a9..3ff42a1d 100644 --- a/docs/source/tutorials/repeating_tasks_with_different_inputs.md +++ b/docs/source/tutorials/repeating_tasks_with_different_inputs.md @@ -8,8 +8,8 @@ We reuse the task from the previous {doc}`tutorial `, which genera random data and repeat the same operation over several seeds to receive multiple, reproducible samples. -Apply the {func}`@task ` decorator, loop over the function -and supply different seeds and output paths as default arguments of the function. +Apply the {func}`@task ` decorator, loop over the function and supply +different seeds and output paths as default arguments of the function. ::::{tab-set} @@ -355,7 +355,7 @@ for id_, kwargs in ID_TO_KWARGS.items(): ``` The -{doc}`best-practices guide on parametrizations <../how_to_guides/bp_scalable_repetitions_of_tasks>` +{doc}`best-practices guide on parametrizations <../how_to_guides/bp_scaling_tasks>` goes into even more detail on how to scale parametrizations. ## A warning on globals diff --git a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_1.py b/docs_src/how_to_guides/bp_scaling_tasks_1.py similarity index 56% rename from docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_1.py rename to docs_src/how_to_guides/bp_scaling_tasks_1.py index 61678d14..5479b678 100644 --- a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_1.py +++ b/docs_src/how_to_guides/bp_scaling_tasks_1.py @@ -5,11 +5,16 @@ from my_project.config import SRC -DATA = ["data_0", "data_1", "data_2", "data_3"] +DATA = { + "data_0": {"subset": "subset_1"}, + "data_1": {"subset": "subset_2"}, + "data_2": {"subset": "subset_3"}, + "data_3": {"subset": "subset_4"}, +} def path_to_input_data(name: str) -> Path: - return SRC / "data" / f"{name}.csv" + return SRC / "data" / "data.csv" def path_to_processed_data(name: str) -> Path: diff --git a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_2.py b/docs_src/how_to_guides/bp_scaling_tasks_2.py similarity index 70% rename from docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_2.py rename to docs_src/how_to_guides/bp_scaling_tasks_2.py index fefb7c86..d52731a6 100644 --- a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_2.py +++ b/docs_src/how_to_guides/bp_scaling_tasks_2.py @@ -4,6 +4,7 @@ from my_project.data_preparation.config import DATA from my_project.data_preparation.config import path_to_input_data from my_project.data_preparation.config import path_to_processed_data +from pandas import pd from pytask import Product from pytask import task from typing_extensions import Annotated @@ -11,10 +12,11 @@ def _create_parametrization(data: list[str]) -> dict[str, Path]: id_to_kwargs = {} - for data_name in data: + for data_name, kwargs in data.items(): id_to_kwargs[data_name] = { "path_to_input_data": path_to_input_data(data_name), "path_to_processed_data": path_to_processed_data(data_name), + **kwargs, } return id_to_kwargs @@ -27,6 +29,11 @@ def _create_parametrization(data: list[str]) -> dict[str, Path]: @task(id=id_, kwargs=kwargs) def task_prepare_data( - path_to_input_data: Path, path_to_processed_data: Annotated[Path, Product] + path_to_input_data: Path, + subset: str, + path_to_processed_data: Annotated[Path, Product], ) -> None: + df = pd.read_csv(path_to_input_data) ... + subset = df.loc[df["subset"].eq(subset)] + subset.to_pickle(path_to_processed_data) diff --git a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_3.py b/docs_src/how_to_guides/bp_scaling_tasks_3.py similarity index 100% rename from docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_3.py rename to docs_src/how_to_guides/bp_scaling_tasks_3.py diff --git a/docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_4.py b/docs_src/how_to_guides/bp_scaling_tasks_4.py similarity index 100% rename from docs_src/how_to_guides/bp_scalable_repetitions_of_tasks_4.py rename to docs_src/how_to_guides/bp_scaling_tasks_4.py diff --git a/docs_src/how_to_guides/bp_structure_of_task_files.py b/docs_src/how_to_guides/bp_structure_of_task_files.py index 2b79da99..19ab8e7b 100644 --- a/docs_src/how_to_guides/bp_structure_of_task_files.py +++ b/docs_src/how_to_guides/bp_structure_of_task_files.py @@ -20,13 +20,9 @@ def task_prepare_census_data( """ df = pd.read_csv(path_to_raw_census) - df = _clean_data(df) - df = _create_new_variables(df) - perform_general_checks_on_data(df) - df.to_pickle(path_to_census) diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 167a1d9a..20c9bea2 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -236,7 +236,7 @@ def _merge_dictionaries(list_of_dicts: list[dict[Any, Any]]) -> dict[Any, Any]: Hint: You do not need to use 'depends_on' as the argument name since pytask v0.4. \ Every function argument that is not a product is treated as a dependency. Read more \ -about dependencies in the documentation: https://tinyurl.com/yrezszr4. +about dependencies in the documentation: https://tinyurl.com/pytask-deps-prods. """ @@ -368,7 +368,7 @@ def _find_args_with_node_annotation(func: Callable[..., Any]) -> dict[str, PNode - as a default argument for 'produces': 'produces = Path(...)' - '@pytask.mark.produces(Path(...))' (deprecated) -Read more about products in the documentation: https://tinyurl.com/yrezszr4. +Read more about products in the documentation: https://tinyurl.com/pytask-deps-prods. """ diff --git a/src/_pytask/mark/__init__.pyi b/src/_pytask/mark/__init__.pyi index d3b4a196..84d76e41 100644 --- a/src/_pytask/mark/__init__.pyi +++ b/src/_pytask/mark/__init__.pyi @@ -15,21 +15,21 @@ def select_by_mark(session: Session, dag: nx.DiGraph) -> set[str]: ... class MarkGenerator: @deprecated( - "'@pytask.mark.produces' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. To upgrade your project to the new syntax, read the tutorial on product and dependencies: https://tinyurl.com/yrezszr4.", # noqa: E501, PYI053 + "'@pytask.mark.produces' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. To upgrade your project to the new syntax, read the tutorial on product and dependencies: https://tinyurl.com/pytask-deps-prods.", # noqa: E501, PYI053 category=FutureWarning, stacklevel=1, ) @staticmethod def produces(objects: PyTree[str | Path]) -> None: ... @deprecated( - "'@pytask.mark.depends_on' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. To upgrade your project to the new syntax, read the tutorial on product and dependencies: https://tinyurl.com/yrezszr4.", # noqa: E501, PYI053 + "'@pytask.mark.depends_on' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. To upgrade your project to the new syntax, read the tutorial on product and dependencies: https://tinyurl.com/pytask-deps-prods.", # noqa: E501, PYI053 category=FutureWarning, stacklevel=1, ) @staticmethod def depends_on(objects: PyTree[str | Path]) -> None: ... @deprecated( - "'@pytask.mark.task' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. Use '@pytask.task' instead.", # noqa: E501, PYI053 + "'@pytask.mark.task' is deprecated starting pytask v0.4.0 and will be removed in v0.5.0. Use '@task' from 'from pytask import task' instead.", # noqa: E501, PYI053 category=FutureWarning, stacklevel=1, ) diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index d578a52f..d7d2fb7e 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -163,7 +163,7 @@ def store_mark(obj: Callable[..., Any], mark: Mark) -> None: _DEPRECATION_DECORATOR = """'@pytask.mark.{}' is deprecated starting pytask \ v0.4.0 and will be removed in v0.5.0. To upgrade your project to the new syntax, read \ -the tutorial on product and dependencies: https://tinyurl.com/yrezszr4. +the tutorial on product and dependencies: https://tinyurl.com/pytask-deps-prods. """ diff --git a/tests/test_live.py b/tests/test_live.py index a878c645..626dbac5 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -307,25 +307,3 @@ def task_b(): task_names = [name[0][-1] for name in task_names if name] expected = ["a", "b"] if sort_table == "true" else ["b", "a"] assert expected == task_names - - -@pytest.mark.end_to_end() -def test_execute_w_partialed_functions(tmp_path, runner): - """Test with partialed function which make it harder to extract info. - - Info like source line number and the path to the module. - - """ - source = """ - import functools - - def func(): ... - - task_func = functools.partial(func) - - """ - tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source)) - result = runner.invoke(cli, [tmp_path.joinpath("task_module.py").as_posix()]) - - assert result.exit_code == ExitCode.OK - assert "task_func" in result.output diff --git a/tests/test_task.py b/tests/test_task.py index 228e1e3c..136c69f6 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -331,6 +331,28 @@ def func(produces, content): assert tmp_path.joinpath("out.txt").exists() +@pytest.mark.end_to_end() +def test_task_function_with_partialed_args_and_task_decorator(tmp_path, runner): + source = """ + from pytask import task + import functools + from pathlib import Path + + def func(content): + return content + + task_func = task(produces=Path("out.txt"))( + functools.partial(func, content="hello") + ) + """ + 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 "1 Collected errors and tasks" in result.output + + @pytest.mark.end_to_end() def test_parametrized_tasks_without_arguments_in_signature(tmp_path, runner): """This happens when plugins replace the function with its own implementation. diff --git a/tests/test_typing.py b/tests/test_typing.py index d1ef2c1b..a172dffa 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -12,11 +12,9 @@ def func(): assert is_task_function(func) partialed_func = functools.partial(func) - assert is_task_function(partialed_func) assert is_task_function(lambda x: x) partialed_lambda = functools.partial(lambda x: x) - assert is_task_function(partialed_lambda)