diff --git a/docs/source/changes.md b/docs/source/changes.md index c05fd9eb..be3ae4f5 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -12,6 +12,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and - {pull}`283` fixes an issue with coverage and tests using pexpect + `pdb.set_trace()`. - {pull}`285` implements that pytask does not show the traceback of tasks which are skipped because its previous task failed. Fixes {issue}`284`. +- {pull}`287` changes that all files that are not produced by a task are displayed in + the error message. Fixes {issue}`262`. ## 0.2.3 - 2022-05-30 diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 320bda73..f7380fb7 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -11,6 +11,7 @@ from _pytask.console import console from _pytask.console import create_summary_panel from _pytask.console import create_url_style_for_task +from _pytask.console import format_strings_as_flat_tree from _pytask.console import format_task_id from _pytask.console import unify_styles from _pytask.dag import descending_tasks @@ -27,6 +28,7 @@ from _pytask.report import ExecutionReport from _pytask.session import Session from _pytask.shared import get_first_non_none_value +from _pytask.shared import reduce_node_name from _pytask.traceback import format_exception_without_traceback from _pytask.traceback import remove_traceback_from_exc_info from _pytask.traceback import render_exc_info @@ -167,15 +169,23 @@ def pytask_execute_task(task: Task) -> bool: @hookimpl def pytask_execute_task_teardown(session: Session, task: Task) -> None: - """Check if each produced node was indeed produced.""" + """Check if :class:`_pytask.nodes.FilePathNode` are produced by a task.""" + missing_nodes = [] for product in session.dag.successors(task.name): node = session.dag.nodes[product]["node"] - try: - node.state() - except NodeNotFoundError as e: - raise NodeNotFoundError( - f"{node.name} was not produced by {task.name}." - ) from e + if isinstance(node, FilePathNode): + + try: + node.state() + except NodeNotFoundError: + missing_nodes.append(node) + + if missing_nodes: + paths = [reduce_node_name(i, session.config["paths"]) for i in missing_nodes] + formatted = format_strings_as_flat_tree( + paths, "The task did not produce the following files:\n", "" + ) + raise NodeNotFoundError(formatted) @hookimpl(trylast=True) diff --git a/tests/test_execute.py b/tests/test_execute.py index aa1e80a9..333ba9f5 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -36,6 +36,25 @@ def task_example(): assert isinstance(session.execution_reports[0].exc_info[1], NodeNotFoundError) +@pytest.mark.end_to_end +def test_task_did_not_produce_multiple_nodes_and_all_are_shown(runner, tmp_path): + source = """ + import pytask + + @pytask.mark.produces(["1.txt", "2.txt"]) + 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.FAILED + assert "NodeNotFoundError" in result.output + assert "1.txt" in result.output + assert "2.txt" in result.output + + @pytest.mark.end_to_end def test_node_not_found_in_task_setup(tmp_path): """Test for :class:`_pytask.exceptions.NodeNotFoundError` in task setup.