8
8
from pathlib import Path
9
9
from typing import Any
10
10
from typing import Iterable
11
+ from typing import List
11
12
from typing import Union
12
13
13
14
import attr
@@ -45,24 +46,30 @@ def produces(objects: Union[Any, Iterable[Any]]) -> Union[Any, Iterable[Any]]:
45
46
return objects
46
47
47
48
48
- class MetaTask (metaclass = ABCMeta ):
49
- """The base class for tasks ."""
49
+ class MetaNode (metaclass = ABCMeta ):
50
+ """Meta class for nodes ."""
50
51
51
52
@abstractmethod
52
- def execute (self ):
53
- """Execute the task ."""
53
+ def state (self ):
54
+ """Return a value which indicates whether a node has changed or not ."""
54
55
pass
55
56
57
+
58
+ class MetaTask (MetaNode ):
59
+ """The base class for tasks."""
60
+
56
61
@abstractmethod
57
- def state (self ):
58
- """Return a value to check whether the task definition has changed ."""
62
+ def execute (self ):
63
+ """Execute the task."""
59
64
pass
60
65
61
66
62
67
@attr .s
63
68
class PythonFunctionTask (MetaTask ):
64
69
"""The class for tasks which are Python functions."""
65
70
71
+ base_name = attr .ib (type = str )
72
+ """str: The base name of the task."""
66
73
name = attr .ib (type = str )
67
74
"""str: The unique identifier for a task."""
68
75
path = attr .ib (type = Path )
@@ -95,8 +102,9 @@ def from_path_name_function_session(cls, path, name, function, session):
95
102
]
96
103
97
104
return cls (
105
+ base_name = name ,
106
+ name = _create_task_name (path , name ),
98
107
path = path ,
99
- name = path .as_posix () + "::" + name ,
100
108
function = function ,
101
109
depends_on = dependencies ,
102
110
produces = products ,
@@ -134,15 +142,6 @@ def add_report_section(self, when: str, key: str, content: str):
134
142
self ._report_sections .append ((when , key , content ))
135
143
136
144
137
- class MetaNode (metaclass = ABCMeta ):
138
- """Meta class for nodes."""
139
-
140
- @abstractmethod
141
- def state (self ):
142
- """Return a value which indicates whether a node has changed or not."""
143
- pass
144
-
145
-
146
145
@attr .s
147
146
class FilePathNode (MetaNode ):
148
147
"""The class for a node which is a path."""
@@ -153,6 +152,9 @@ class FilePathNode(MetaNode):
153
152
value = attr .ib ()
154
153
"""Any: Value passed to the decorator which can be requested inside the function."""
155
154
155
+ path = attr .ib ()
156
+ """pathlib.Path: Path to the FilePathNode."""
157
+
156
158
@classmethod
157
159
@functools .lru_cache ()
158
160
def from_path (cls , path : pathlib .Path ):
@@ -162,7 +164,7 @@ def from_path(cls, path: pathlib.Path):
162
164
163
165
"""
164
166
path = path .resolve ()
165
- return cls (path .as_posix (), path )
167
+ return cls (path .as_posix (), path , path )
166
168
167
169
def state (self ):
168
170
"""Return the last modified date for file path."""
@@ -280,3 +282,90 @@ def _convert_nodes_to_dictionary(list_of_tuples):
280
282
nodes [node_name ] = tuple_ [0 ]
281
283
282
284
return nodes
285
+
286
+
287
+ def _create_task_name (path : Path , base_name : str ):
288
+ """Create the name of a task from a path and the task's base name.
289
+
290
+ Examples
291
+ --------
292
+ >>> from pathlib import Path
293
+ >>> _create_task_name(Path("module.py"), "task_dummy")
294
+ 'module.py::task_dummy'
295
+
296
+ """
297
+ return path .as_posix () + "::" + base_name
298
+
299
+
300
+ def _relative_to (path : Path , source : Path , include_source : bool = True ):
301
+ """Make a path relative to another path.
302
+
303
+ In contrast to :meth:`pathlib.Path.relative_to`, this function allows to keep the
304
+ name of the source path.
305
+
306
+ Examples
307
+ --------
308
+ >>> from pathlib import Path
309
+ >>> _relative_to(Path("folder", "file.py"), Path("folder")).as_posix()
310
+ 'folder/file.py'
311
+ >>> _relative_to(Path("folder", "file.py"), Path("folder"), False).as_posix()
312
+ 'file.py'
313
+
314
+ """
315
+ return Path (source .name if include_source else "" , path .relative_to (source ))
316
+
317
+
318
+ def _find_closest_ancestor (path : Path , potential_ancestors : List [Path ]):
319
+ """Find the closest ancestor of a path.
320
+
321
+ Examples
322
+ --------
323
+ >>> from pathlib import Path
324
+ >>> _find_closest_ancestor(Path("folder", "file.py"), [Path("folder")]).as_posix()
325
+ 'folder'
326
+
327
+ >>> paths = [Path("folder"), Path("folder", "subfolder")]
328
+ >>> _find_closest_ancestor(Path("folder", "subfolder", "file.py"), paths).as_posix()
329
+ 'folder/subfolder'
330
+
331
+ """
332
+ closest_ancestor = None
333
+ for ancestor in potential_ancestors :
334
+ if ancestor == path :
335
+ closest_ancestor = path
336
+ break
337
+ if ancestor in path .parents :
338
+ if closest_ancestor is None or (
339
+ len (path .relative_to (ancestor ).parts )
340
+ < len (path .relative_to (closest_ancestor ).parts )
341
+ ):
342
+ closest_ancestor = ancestor
343
+
344
+ return closest_ancestor
345
+
346
+
347
+ def shorten_node_name (node , paths : List [Path ]):
348
+ """Shorten the node name.
349
+
350
+ The whole name of the node - which includes the drive letter - can be very long
351
+ when using nested folder structures in bigger projects.
352
+
353
+ Thus, the part of the name which contains the path is replace by the relative
354
+ path from one path in ``session.config["paths"]`` to the node.
355
+
356
+ """
357
+ ancestor = _find_closest_ancestor (node .path , paths )
358
+ if ancestor is None :
359
+ raise ValueError ("A node must be defined in a child of 'paths'." )
360
+ elif isinstance (node , MetaTask ):
361
+ if ancestor == node .path :
362
+ name = _create_task_name (Path (node .path .name ), node .base_name )
363
+ else :
364
+ shortened_path = _relative_to (node .path , ancestor )
365
+ name = _create_task_name (shortened_path , node .base_name )
366
+ elif isinstance (node , MetaNode ):
367
+ name = _relative_to (node .path , ancestor ).as_posix ()
368
+ else :
369
+ raise ValueError (f"Unknown node { node } with type '{ type (node )} '." )
370
+
371
+ return name
0 commit comments