Skip to content

Commit b6ae5f3

Browse files
authored
Pretty format tasks with mixed priorities, simplify find_common_ancestor function and others. (#80)
1 parent 4936747 commit b6ae5f3

File tree

13 files changed

+132
-72
lines changed

13 files changed

+132
-72
lines changed

docs/_static/css/custom.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
/* Remove execution count for notebook cells. */
2-
div.prompt {
3-
display: none;
4-
}
5-
61
img.card-img-top {
72
height: 52px;
83
}

docs/changes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ chronological order. Releases follow `semantic versioning <https://semver.org/>`
66
all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
77
`Anaconda.org <https://anaconda.org/conda-forge/pytask>`_.
88

9+
10+
0.0.15 - 2021-xx-xx
11+
-------------------
12+
13+
- :gh:`80` replaces some remaining formatting using ``pprint`` with ``rich``.
14+
15+
916
0.0.14 - 2021-03-23
1017
-------------------
1118

docs/conf.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
# root, use os.path.abspath to make it absolute, like shown here.
88
import os
99
import sys
10+
from datetime import datetime
1011

11-
import pytask
1212
import sphinx
1313

1414

1515
sys.path.insert(0, os.path.abspath("../src"))
1616

1717

18+
import pytask # noqa: E402
19+
20+
1821
# -- Project information ---------------------------------------------------------------
1922

2023
project = "pytask"
21-
copyright = "2020, Tobias Raabe" # noqa: A001
24+
year = datetime.now().year
25+
copyright = f"2020-{year}, Tobias Raabe" # noqa: A001
2226
author = "Tobias Raabe"
2327

2428
# The full version, including alpha/beta/rc tags

docs/rtd_environment.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,3 @@ dependencies:
2424
- pony >=0.7.13
2525
- pexpect
2626
- rich
27-
28-
- pip:
29-
- -e ../

src/_pytask/console.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""This module contains the code to format output on the command line."""
22
import os
33
import sys
4+
from typing import List
45

56
from rich.console import Console
6-
7+
from rich.tree import Tree
78

89
_IS_WSL = "IS_WSL" in os.environ or "WSL_DISTRO_NAME" in os.environ
910
_IS_WINDOWS_TERMINAL = "WT_SESSION" in os.environ
@@ -26,3 +27,16 @@
2627

2728

2829
console = Console(color_system=_COLOR_SYSTEM)
30+
31+
32+
def format_strings_as_flat_tree(strings: List[str], title: str, icon: str) -> str:
33+
"""Format list of strings as flat tree."""
34+
tree = Tree(title)
35+
for name in strings:
36+
tree.add(icon + name)
37+
38+
text = "".join(
39+
[x.text for x in tree.__rich_console__(console, console.options)][:-1]
40+
)
41+
42+
return text

src/_pytask/dag.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
"""Implement some capabilities to deal with the DAG."""
22
import itertools
3-
import pprint
3+
from pathlib import Path
44
from typing import Dict
55
from typing import Generator
66
from typing import Iterable
77
from typing import List
88

99
import attr
1010
import networkx as nx
11+
from _pytask.console import format_strings_as_flat_tree
12+
from _pytask.console import TASK_ICON
1113
from _pytask.mark import get_specific_markers_from_task
1214
from _pytask.nodes import MetaTask
15+
from _pytask.nodes import reduce_node_name
1316

1417

1518
def descending_tasks(task_name: str, dag: nx.DiGraph) -> Generator[str, None, None]:
@@ -52,14 +55,17 @@ class TopologicalSorter:
5255
_nodes_out = attr.ib(factory=set)
5356

5457
@classmethod
55-
def from_dag(cls, dag: nx.DiGraph) -> "TopologicalSorter":
58+
def from_dag(cls, dag: nx.DiGraph, paths: List[Path] = None) -> "TopologicalSorter":
59+
if paths is None:
60+
paths = []
61+
5662
if not dag.is_directed():
5763
raise ValueError("Only directed graphs have a topological order.")
5864

5965
tasks = [
6066
dag.nodes[node]["task"] for node in dag.nodes if "task" in dag.nodes[node]
6167
]
62-
priorities = _extract_priorities_from_tasks(tasks)
68+
priorities = _extract_priorities_from_tasks(tasks, paths)
6369

6470
task_names = {task.name for task in tasks}
6571
task_dict = {name: nx.ancestors(dag, name) & task_names for name in task_names}
@@ -118,7 +124,9 @@ def static_order(self) -> Generator[str, None, None]:
118124
self.done(new_task)
119125

120126

121-
def _extract_priorities_from_tasks(tasks: List[MetaTask]) -> Dict[str, int]:
127+
def _extract_priorities_from_tasks(
128+
tasks: List[MetaTask], paths: List[Path]
129+
) -> Dict[str, int]:
122130
"""Extract priorities from tasks.
123131
124132
Priorities are set via the ``pytask.mark.try_first`` and ``pytask.mark.try_last``
@@ -138,10 +146,19 @@ def _extract_priorities_from_tasks(tasks: List[MetaTask]) -> Dict[str, int]:
138146
tasks_w_mixed_priorities = [
139147
name for name, p in priorities.items() if p["try_first"] and p["try_last"]
140148
]
149+
141150
if tasks_w_mixed_priorities:
151+
name_to_task = {task.name: task for task in tasks}
152+
reduced_names = [
153+
reduce_node_name(name_to_task[name], paths)
154+
for name in tasks_w_mixed_priorities
155+
]
156+
text = format_strings_as_flat_tree(
157+
reduced_names, "Tasks with mixed priorities", TASK_ICON
158+
)
142159
raise ValueError(
143160
"'try_first' and 'try_last' cannot be applied on the same task. See the "
144-
f"following tasks for errors:\n\n{pprint.pformat(tasks_w_mixed_priorities)}"
161+
f"following tasks for errors:\n\n{text}"
145162
)
146163

147164
# Recode to numeric values for sorting.

src/_pytask/nodes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,11 @@ def reduce_node_name(node, paths: List[Path]):
366366
raise ValueError(f"Unknown node {node} with type '{type(node)}'.")
367367

368368
return name
369+
370+
371+
def reduce_names_of_multiple_nodes(names, dag, paths):
372+
"""Reduce the names of multiple nodes in the DAG."""
373+
return [
374+
reduce_node_name(dag.nodes[n].get("node") or dag.nodes[n].get("task"), paths)
375+
for n in names
376+
]

src/_pytask/parametrize.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import copy
22
import functools
33
import itertools
4-
import pprint
54
import types
65
from typing import Any
76
from typing import Callable
@@ -12,7 +11,8 @@
1211
from typing import Union
1312

1413
from _pytask.config import hookimpl
15-
from _pytask.console import console
14+
from _pytask.console import format_strings_as_flat_tree
15+
from _pytask.console import TASK_ICON
1616
from _pytask.mark import Mark
1717
from _pytask.mark import MARK_GEN as mark # noqa: N811
1818
from _pytask.shared import find_duplicates
@@ -124,10 +124,12 @@ def pytask_parametrize_task(session, name, obj):
124124
names = [i[0] for i in names_and_functions]
125125
duplicates = find_duplicates(names)
126126
if duplicates:
127-
formatted = pprint.pformat(duplicates, width=console.width)
127+
text = format_strings_as_flat_tree(
128+
duplicates, "Duplicated task ids", TASK_ICON
129+
)
128130
raise ValueError(
129131
"The following ids are duplicated while parametrizing task "
130-
f"{obj.__name__}.\n\n{formatted}\n\nIt might be caused by "
132+
f"'{obj.__name__}'.\n\n{text}\n\nIt might be caused by "
131133
"parametrizing the task with the same combination of arguments "
132134
"multiple times. Change the arguments or change the ids generated by "
133135
"the parametrization."

src/_pytask/path.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""This module contains code to handle paths."""
2+
import os
23
from pathlib import Path
3-
from pathlib import PurePath
44
from typing import List
55
from typing import Union
66

@@ -83,19 +83,6 @@ def find_common_ancestor_of_nodes(*names: str) -> Path:
8383

8484
def find_common_ancestor(*paths: Union[str, Path]) -> Path:
8585
"""Find a common ancestor of many paths."""
86-
paths = [path if isinstance(path, PurePath) else Path(path) for path in paths]
87-
88-
for path in paths:
89-
if not path.is_absolute():
90-
raise ValueError(
91-
f"Cannot find common ancestor for relative paths. {path} is relative."
92-
)
93-
94-
common_parents = set.intersection(*[set(path.parents) for path in paths])
95-
96-
if len(common_parents) == 0:
97-
raise ValueError("Paths have no common ancestor.")
98-
else:
99-
longest_parent = sorted(common_parents, key=lambda x: len(x.parts))[-1]
100-
101-
return longest_parent
86+
path = os.path.commonpath(paths)
87+
path = Path(path)
88+
return path

src/_pytask/resolve_dependencies.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from _pytask.exceptions import NodeNotFoundError
1919
from _pytask.exceptions import ResolvingDependenciesError
2020
from _pytask.mark import Mark
21+
from _pytask.nodes import reduce_names_of_multiple_nodes
2122
from _pytask.nodes import reduce_node_name
2223
from _pytask.path import find_common_ancestor_of_nodes
2324
from _pytask.report import ResolvingDependenciesReport
@@ -180,7 +181,7 @@ def _check_if_root_nodes_are_available(dag):
180181
short_node_name = reduce_node_name(
181182
dag.nodes[node]["node"], [common_ancestor]
182183
)
183-
short_successors = _reduce_names_of_multiple_nodes(
184+
short_successors = reduce_names_of_multiple_nodes(
184185
dag.successors(node), dag, [common_ancestor]
185186
)
186187
dictionary[short_node_name] = short_successors
@@ -231,7 +232,7 @@ def _check_if_tasks_have_the_same_products(dag):
231232
short_node_name = reduce_node_name(
232233
dag.nodes[node]["node"], [common_ancestor]
233234
)
234-
short_predecessors = _reduce_names_of_multiple_nodes(
235+
short_predecessors = reduce_names_of_multiple_nodes(
235236
dag.predecessors(node), dag, [common_ancestor]
236237
)
237238
dictionary[short_node_name] = short_predecessors
@@ -257,11 +258,3 @@ def pytask_resolve_dependencies_log(report):
257258

258259
console.print()
259260
console.rule(style=ColorCode.FAILED)
260-
261-
262-
def _reduce_names_of_multiple_nodes(names, dag, paths):
263-
"""Reduce the names of multiple nodes in the DAG."""
264-
return [
265-
reduce_node_name(dag.nodes[n].get("node") or dag.nodes[n].get("task"), paths)
266-
for n in names
267-
]

0 commit comments

Comments
 (0)