Skip to content

Commit 67e328b

Browse files
authored
Add more explanation when PNode.load() fails during execution. (#455)
1 parent 3dca093 commit 67e328b

File tree

9 files changed

+70
-3
lines changed

9 files changed

+70
-3
lines changed

docs/source/changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
1010
- {pull}`449` simplifies the code building the plugin manager.
1111
- {pull}`451` improves `collect_command.py` and renames `graph.py` to `dag_command.py`.
1212
- {pull}`454` removes more `.svg`s and replaces them with animations.
13+
- {pull}`455` adds more explanation when {meth}`~pytask.PNode.load` fails during the
14+
execution.
1315

1416
## 0.4.1 - 2023-10-11
1517

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ channels:
55
- nodefaults
66

77
dependencies:
8-
- python >=3.8
8+
- python >=3.8,<3.12
99
- pip
1010
- setuptools_scm
1111
- toml

src/_pytask/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class NodeNotCollectedError(PytaskError):
1414
"""Exception for nodes which could not be collected."""
1515

1616

17+
class NodeLoadError(PytaskError):
18+
"""Exception for nodes whose value could not be loaded."""
19+
20+
1721
class ConfigurationError(PytaskError):
1822
"""Exception during the configuration."""
1923

src/_pytask/execute.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from _pytask.database_utils import update_states_in_database
2020
from _pytask.enums import ShowCapture
2121
from _pytask.exceptions import ExecutionError
22+
from _pytask.exceptions import NodeLoadError
2223
from _pytask.exceptions import NodeNotFoundError
2324
from _pytask.mark import Mark
2425
from _pytask.mark_utils import has_mark
26+
from _pytask.node_protocols import PNode
2527
from _pytask.node_protocols import PPathNode
2628
from _pytask.node_protocols import PTask
2729
from _pytask.outcomes import count_outcomes
@@ -31,6 +33,7 @@
3133
from _pytask.report import ExecutionReport
3234
from _pytask.shared import reduce_node_name
3335
from _pytask.traceback import format_exception_without_traceback
36+
from _pytask.traceback import remove_internal_traceback_frames_from_exception
3437
from _pytask.traceback import remove_traceback_from_exc_info
3538
from _pytask.traceback import render_exc_info
3639
from _pytask.tree_util import tree_leaves
@@ -142,6 +145,16 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None:
142145
raise WouldBeExecuted
143146

144147

148+
def _safe_load(node: PNode, task: PTask) -> Any:
149+
try:
150+
return node.load()
151+
except Exception as e: # noqa: BLE001
152+
e = remove_internal_traceback_frames_from_exception(e)
153+
task_name = getattr(task, "display_name", task.name)
154+
msg = f"Exception while loading node {node.name!r} of task {task_name!r}"
155+
raise NodeLoadError(msg) from e
156+
157+
145158
@hookimpl(trylast=True)
146159
def pytask_execute_task(session: Session, task: PTask) -> bool:
147160
"""Execute task."""
@@ -152,11 +165,11 @@ def pytask_execute_task(session: Session, task: PTask) -> bool:
152165

153166
kwargs = {}
154167
for name, value in task.depends_on.items():
155-
kwargs[name] = tree_map(lambda x: x.load(), value)
168+
kwargs[name] = tree_map(lambda x: _safe_load(x, task), value)
156169

157170
for name, value in task.produces.items():
158171
if name in parameters:
159-
kwargs[name] = tree_map(lambda x: x.load(), value)
172+
kwargs[name] = tree_map(lambda x: _safe_load(x, task), value)
160173

161174
out = task.execute(**kwargs)
162175

tests/test_collect_command.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ def task_example(
512512
assert "Product" in captured
513513

514514

515+
@pytest.mark.end_to_end()
515516
def test_node_protocol_for_custom_nodes(runner, tmp_path):
516517
source = """
517518
from typing_extensions import Annotated
@@ -543,6 +544,7 @@ def task_example(
543544
assert "<Dependency custom>" in result.output
544545

545546

547+
@pytest.mark.end_to_end()
546548
def test_node_protocol_for_custom_nodes_with_paths(runner, tmp_path):
547549
source = """
548550
from typing_extensions import Annotated

tests/test_execute.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ def test_pass_non_task_to_functional_api_that_are_ignored():
774774
@pytest.mark.end_to_end()
775775
def test_multiple_product_annotations(runner, tmp_path):
776776
source = """
777+
from __future__ import annotations
777778
from pytask import Product
778779
from typing_extensions import Annotated
779780
from pathlib import Path
@@ -793,3 +794,42 @@ def task_second(
793794
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
794795
result = runner.invoke(cli, [tmp_path.as_posix()])
795796
assert result.exit_code == ExitCode.OK
797+
798+
799+
@pytest.mark.end_to_end()
800+
def test_errors_during_loading_nodes_have_info(runner, tmp_path):
801+
source = """
802+
from __future__ import annotations
803+
from pathlib import Path
804+
from typing import Any
805+
import attrs
806+
import pickle
807+
808+
@attrs.define
809+
class PickleNode:
810+
name: str
811+
path: Path
812+
813+
def state(self) -> str | None:
814+
if self.path.exists():
815+
return str(self.path.stat().st_mtime)
816+
return None
817+
818+
def load(self) -> Any:
819+
return pickle.loads(self.path.read_bytes())
820+
821+
def save(self, value: Any) -> None:
822+
self.path.write_bytes(pickle.dumps(value))
823+
824+
def task_example(
825+
value=PickleNode(name="node", path=Path(__file__).parent / "file.txt")
826+
): pass
827+
"""
828+
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
829+
tmp_path.joinpath("file.txt").touch()
830+
831+
result = runner.invoke(cli, [tmp_path.as_posix()])
832+
assert result.exit_code == ExitCode.FAILED
833+
assert "task_example.py::task_example" in result.output
834+
assert "Exception while loading node" in result.output
835+
assert "_pytask/execute.py" not in result.output

tests/test_node_protocols.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import pickle
44
import textwrap
55

6+
import pytest
67
from pytask import cli
78
from pytask import ExitCode
89

910

11+
@pytest.mark.end_to_end()
1012
def test_node_protocol_for_custom_nodes(runner, tmp_path):
1113
source = """
1214
from typing_extensions import Annotated
@@ -41,6 +43,7 @@ def task_example(
4143
assert tmp_path.joinpath("out.txt").read_text() == "text"
4244

4345

46+
@pytest.mark.end_to_end()
4447
def test_node_protocol_for_custom_nodes_with_paths(runner, tmp_path):
4548
source = """
4649
from typing_extensions import Annotated

tests/test_path.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def test_insert_missing_modules(
318318
assert not modules
319319

320320

321+
@pytest.mark.unit()
321322
def test_importlib_package(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
322323
"""
323324
Importing a package using --importmode=importlib should not import the

tests/test_typing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import functools
44

5+
import pytest
56
from _pytask.typing import is_task_function
67

78

9+
@pytest.mark.unit()
810
def test_is_task_function():
911
def func():
1012
pass

0 commit comments

Comments
 (0)