88from pathlib import Path
99from typing import Any
1010from typing import Iterable
11+ from typing import List
1112from typing import Union
1213
1314import attr
@@ -45,24 +46,30 @@ def produces(objects: Union[Any, Iterable[Any]]) -> Union[Any, Iterable[Any]]:
4546 return objects
4647
4748
48- class MetaTask (metaclass = ABCMeta ):
49- """The base class for tasks ."""
49+ class MetaNode (metaclass = ABCMeta ):
50+ """Meta class for nodes ."""
5051
5152 @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 ."""
5455 pass
5556
57+
58+ class MetaTask (MetaNode ):
59+ """The base class for tasks."""
60+
5661 @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."""
5964 pass
6065
6166
6267@attr .s
6368class PythonFunctionTask (MetaTask ):
6469 """The class for tasks which are Python functions."""
6570
71+ base_name = attr .ib (type = str )
72+ """str: The base name of the task."""
6673 name = attr .ib (type = str )
6774 """str: The unique identifier for a task."""
6875 path = attr .ib (type = Path )
@@ -95,8 +102,9 @@ def from_path_name_function_session(cls, path, name, function, session):
95102 ]
96103
97104 return cls (
105+ base_name = name ,
106+ name = _create_task_name (path , name ),
98107 path = path ,
99- name = path .as_posix () + "::" + name ,
100108 function = function ,
101109 depends_on = dependencies ,
102110 produces = products ,
@@ -134,15 +142,6 @@ def add_report_section(self, when: str, key: str, content: str):
134142 self ._report_sections .append ((when , key , content ))
135143
136144
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-
146145@attr .s
147146class FilePathNode (MetaNode ):
148147 """The class for a node which is a path."""
@@ -153,6 +152,9 @@ class FilePathNode(MetaNode):
153152 value = attr .ib ()
154153 """Any: Value passed to the decorator which can be requested inside the function."""
155154
155+ path = attr .ib ()
156+ """pathlib.Path: Path to the FilePathNode."""
157+
156158 @classmethod
157159 @functools .lru_cache ()
158160 def from_path (cls , path : pathlib .Path ):
@@ -162,7 +164,7 @@ def from_path(cls, path: pathlib.Path):
162164
163165 """
164166 path = path .resolve ()
165- return cls (path .as_posix (), path )
167+ return cls (path .as_posix (), path , path )
166168
167169 def state (self ):
168170 """Return the last modified date for file path."""
@@ -280,3 +282,90 @@ def _convert_nodes_to_dictionary(list_of_tuples):
280282 nodes [node_name ] = tuple_ [0 ]
281283
282284 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