4
4
5
5
import inspect
6
6
from functools import partial
7
+ from pathlib import PosixPath
8
+ from pathlib import WindowsPath
7
9
from typing import TYPE_CHECKING
8
10
from typing import Any
9
11
from typing import Callable
10
12
13
+ from pytask import NodeLoadError
14
+ from pytask import PathNode
15
+ from pytask import PNode
16
+ from pytask import PProvisionalNode
11
17
from pytask .tree_util import PyTree
12
- from pytask .tree_util import tree_map
18
+ from pytask .tree_util import tree_map_with_path
19
+ from upath .implementations .local import FilePath
13
20
14
21
if TYPE_CHECKING :
15
22
from concurrent .futures import Future
22
29
from pytask_parallel .wrappers import WrapperResult
23
30
24
31
25
- __all__ = ["create_kwargs_for_task" , "get_module" , "parse_future_result" ]
32
+ __all__ = [
33
+ "create_kwargs_for_task" ,
34
+ "get_module" ,
35
+ "parse_future_result" ,
36
+ "is_local_path" ,
37
+ ]
26
38
27
39
28
40
def parse_future_result (
@@ -32,11 +44,12 @@ def parse_future_result(
32
44
# An exception was raised before the task was executed.
33
45
future_exception = future .exception ()
34
46
if future_exception is not None :
47
+ # Prevent circular import for loky backend.
35
48
from pytask_parallel .wrappers import WrapperResult
36
49
37
50
exc_info = _parse_future_exception (future_exception )
38
51
return WrapperResult (
39
- python_nodes = None ,
52
+ carry_over_products = None ,
40
53
warning_reports = [],
41
54
exc_info = exc_info ,
42
55
stdout = "" ,
@@ -45,17 +58,80 @@ def parse_future_result(
45
58
return future .result ()
46
59
47
60
48
- def create_kwargs_for_task (task : PTask ) -> dict [str , PyTree [Any ]]:
61
+ def _safe_load (
62
+ path : tuple [Any , ...],
63
+ node : PNode | PProvisionalNode ,
64
+ task : PTask ,
65
+ * ,
66
+ is_product : bool ,
67
+ remote : bool ,
68
+ ) -> Any :
69
+ """Load a node and catch exceptions."""
70
+ _rich_traceback_guard = True
71
+ # Get the argument name like "path" or "return" for function returns.
72
+ argument = path [0 ]
73
+
74
+ # Raise an error if a PPathNode with a local path is used as a dependency or product
75
+ # (except as a return value).
76
+ if (
77
+ remote
78
+ and argument != "return"
79
+ and isinstance (node , PathNode )
80
+ and is_local_path (node .path )
81
+ ):
82
+ if is_product :
83
+ msg = (
84
+ f"You cannot use a local path as a product in argument { argument !r} "
85
+ "with a remote backend. Either return the content that should be saved "
86
+ "in the file with a return annotation "
87
+ "(https://tinyurl.com/pytask-return) or use a nonlocal path to store "
88
+ "the file in S3 or their like https://tinyurl.com/pytask-remote."
89
+ )
90
+ raise NodeLoadError (msg )
91
+ msg = (
92
+ f"You cannot use a local path as a dependency in argument { argument !r} "
93
+ "with a remote backend. Upload the file to a remote storage like S3 "
94
+ "and use the remote path instead: https://tinyurl.com/pytask-remote."
95
+ )
96
+ raise NodeLoadError (msg )
97
+
98
+ try :
99
+ return node .load (is_product = is_product )
100
+ except Exception as e : # noqa: BLE001
101
+ msg = f"Exception while loading node { node .name !r} of task { task .name !r} "
102
+ raise NodeLoadError (msg ) from e
103
+
104
+
105
+ def create_kwargs_for_task (task : PTask , * , remote : bool ) -> dict [str , PyTree [Any ]]:
49
106
"""Create kwargs for task function."""
50
107
parameters = inspect .signature (task .function ).parameters
51
108
52
109
kwargs = {}
110
+
53
111
for name , value in task .depends_on .items ():
54
- kwargs [name ] = tree_map (lambda x : x .load (), value )
112
+ kwargs [name ] = tree_map_with_path (
113
+ lambda p , x : _safe_load (
114
+ (name , * p ), # noqa: B023
115
+ x ,
116
+ task ,
117
+ is_product = False ,
118
+ remote = remote ,
119
+ ),
120
+ value ,
121
+ )
55
122
56
123
for name , value in task .produces .items ():
57
124
if name in parameters :
58
- kwargs [name ] = tree_map (lambda x : x .load (), value )
125
+ kwargs [name ] = tree_map_with_path (
126
+ lambda p , x : _safe_load (
127
+ (name , * p ), # noqa: B023
128
+ x ,
129
+ task ,
130
+ is_product = True ,
131
+ remote = remote ,
132
+ ),
133
+ value ,
134
+ )
59
135
60
136
return kwargs
61
137
@@ -84,3 +160,8 @@ def get_module(func: Callable[..., Any], path: Path | None) -> ModuleType:
84
160
if path :
85
161
return inspect .getmodule (func , path .as_posix ())
86
162
return inspect .getmodule (func )
163
+
164
+
165
+ def is_local_path (path : Path ) -> bool :
166
+ """Check if a path is local."""
167
+ return isinstance (path , (FilePath , PosixPath , WindowsPath ))
0 commit comments