Skip to content

Commit a53512e

Browse files
authored
Shorten and colorize task ids. (#178)
1 parent 9c55a0d commit a53512e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+579
-296
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ repos:
3030
- id: rst-inline-touching-normal
3131
- id: text-unicode-replacement-char
3232
- repo: https://github.com/asottile/pyupgrade
33-
rev: v2.29.1
33+
rev: v2.31.0
3434
hooks:
3535
- id: pyupgrade
3636
args: [--py36-plus]
@@ -90,10 +90,9 @@ repos:
9090
hooks:
9191
- id: check-manifest
9292
- repo: https://github.com/guilatrova/tryceratops
93-
rev: v1.0.0
93+
rev: v1.0.1
9494
hooks:
9595
- id: tryceratops
96-
exclude: (console\.py|test_mark_expression\.py)
9796
- repo: https://github.com/pre-commit/mirrors-mypy
9897
rev: 'v0.930'
9998
hooks:

MANIFEST.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ exclude *.yaml
55
exclude *.yml
66
exclude tox.ini
77

8-
prune .conda
98
prune docs
109
prune tests

docs/source/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
2222
- :gh:`174` restructures loosely defined outcomes to clear ``enum.Enum``.
2323
- :gh:`176` and :gh:`177` implement a summary panel which holds aggregate information
2424
about the number of successes, fails and other status.
25+
- :gh:`178` makes some stylistic changes like reducing tasks ids even more and dims the
26+
path part.
2527

2628

2729
0.1.3 - 2021-11-30

docs/source/tutorials/how_to_collect_tasks.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ For example, let us take the following task
88

99
.. code-block:: python
1010
11-
# Content of task_dummy.py
11+
# Content of task_module.py
1212
1313
import pytask
1414
@@ -29,7 +29,7 @@ Now, running :program:`pytask collect` will produce the following output.
2929
Root: xxx
3030
Collected 1 task(s).
3131
32-
<Module /.../task_dummy.py>
32+
<Module /.../task_module.py>
3333
<Function task_write_file>
3434
3535
========================================================================
@@ -45,7 +45,7 @@ append the ``--nodes`` flag.
4545
Root: xxx
4646
Collected 1 task(s).
4747
48-
<Module /.../task_dummy.py>
48+
<Module /.../task_module.py>
4949
<Function task_write_file>
5050
<Dependency /.../in.txt>
5151
<Product /.../out.txt>

src/_pytask/collect.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from _pytask.mark_utils import has_marker
2222
from _pytask.nodes import create_task_name
2323
from _pytask.nodes import FilePathNode
24+
from _pytask.nodes import find_duplicates
25+
from _pytask.nodes import MetaTask
2426
from _pytask.nodes import PythonFunctionTask
2527
from _pytask.outcomes import CollectionOutcome
2628
from _pytask.outcomes import count_outcomes
@@ -82,6 +84,7 @@ def pytask_ignore_collect(path: Path, config: Dict[str, Any]) -> bool:
8284
def pytask_collect_file_protocol(
8385
session: Session, path: Path, reports: List[CollectionReport]
8486
) -> List[CollectionReport]:
87+
"""Wrap the collection of tasks from a file to collect reports."""
8588
try:
8689
reports = session.hook.pytask_collect_file(
8790
session=session, path=path, reports=reports
@@ -268,6 +271,52 @@ def _not_ignored_paths(
268271
yield path
269272

270273

274+
@hookimpl(trylast=True)
275+
def pytask_collect_modify_tasks(tasks: List[MetaTask]) -> None:
276+
"""Given all tasks, assign a short uniquely identifiable name to each task.
277+
278+
The shorter ids are necessary to display
279+
280+
"""
281+
id_to_short_id = _find_shortest_uniquely_identifiable_name_for_tasks(tasks)
282+
for task in tasks:
283+
short_id = id_to_short_id[task.name]
284+
task.short_name = short_id
285+
286+
287+
def _find_shortest_uniquely_identifiable_name_for_tasks(
288+
tasks: List[MetaTask],
289+
) -> Dict[str, str]:
290+
"""Find the shortest uniquely identifiable name for tasks.
291+
292+
The shortest unique id consists of the module name plus the base name (e.g. function
293+
name) of the task. If this does not make the id unique, append more and more parent
294+
folders until the id is unique.
295+
296+
"""
297+
id_to_short_id = {}
298+
299+
# Make attempt to add up to twenty parts of the path to ensure uniqueness.
300+
id_to_task = {task.name: task for task in tasks}
301+
for n_parts in range(1, 20):
302+
dupl_id_to_short_id = {
303+
id_: "/".join(task.path.parts[-n_parts:]) + "::" + task.base_name
304+
for id_, task in id_to_task.items()
305+
}
306+
duplicates = find_duplicates(dupl_id_to_short_id.values())
307+
308+
for id_, short_id in dupl_id_to_short_id.items():
309+
if short_id not in duplicates:
310+
id_to_short_id[id_] = short_id
311+
id_to_task.pop(id_)
312+
313+
# If there are still non-unique task ids, just use the full id as the short id.
314+
for id_, task in id_to_task.items():
315+
id_to_short_id[id_] = task.name
316+
317+
return id_to_short_id
318+
319+
271320
@hookimpl
272321
def pytask_collect_log(
273322
session: Session, reports: List[CollectionReport], tasks: List[PythonFunctionTask]

src/_pytask/collect_command.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from _pytask.config import hookimpl
1212
from _pytask.console import console
1313
from _pytask.console import create_url_style_for_path
14-
from _pytask.console import create_url_style_for_task
1514
from _pytask.console import FILE_ICON
15+
from _pytask.console import format_task_id
1616
from _pytask.console import PYTHON_ICON
1717
from _pytask.console import TASK_ICON
1818
from _pytask.enums import ExitCode
@@ -25,7 +25,6 @@
2525
from _pytask.path import relative_to
2626
from _pytask.pluginmanager import get_plugin_manager
2727
from _pytask.session import Session
28-
from _pytask.shared import reduce_node_name
2928
from rich.text import Text
3029
from rich.tree import Tree
3130

@@ -194,15 +193,11 @@ def _print_collected_tasks(
194193
)
195194

196195
for task in tasks:
197-
reduced_task_name = reduce_node_name(task, [common_ancestor])
198-
url_style = create_url_style_for_task(task, editor_url_scheme)
196+
reduced_task_name = format_task_id(
197+
task, editor_url_scheme=editor_url_scheme, relative_to=common_ancestor
198+
)
199199
task_branch = module_branch.add(
200-
Text.assemble(
201-
TASK_ICON,
202-
"<Function ",
203-
Text(reduced_task_name, style=url_style),
204-
">",
205-
),
200+
Text.assemble(TASK_ICON, "<Function ", reduced_task_name, ">"),
206201
)
207202

208203
if show_nodes:

src/_pytask/console.py

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
from typing import Callable
1010
from typing import Dict
1111
from typing import Iterable
12+
from typing import Optional
1213
from typing import Type
1314
from typing import TYPE_CHECKING
1415
from typing import Union
1516

17+
import rich
18+
from _pytask.path import relative_to as relative_to_
1619
from rich.console import Console
1720
from rich.padding import Padding
1821
from rich.panel import Panel
22+
from rich.segment import Segment
1923
from rich.style import Style
2024
from rich.table import Table
25+
from rich.text import Text
2126
from rich.theme import Theme
2227
from rich.tree import Tree
2328

@@ -76,43 +81,75 @@
7681
console = Console(theme=theme, color_system=_COLOR_SYSTEM)
7782

7883

84+
def render_to_string(text: Union[str, Text], console: Optional[Console] = None) -> str:
85+
"""Render text with rich to string including ANSI codes, etc..
86+
87+
This function allows to render text with is not automatically printed with rich. For
88+
example, render warnings with colors or text in exceptions.
89+
90+
"""
91+
if console is None:
92+
console = rich.get_console()
93+
94+
segments = console.render(text)
95+
96+
output = []
97+
if console.no_color and console._color_system:
98+
segments = Segment.remove_color(segments)
99+
100+
for segment in segments:
101+
if segment.style:
102+
output.append(
103+
segment.style.render(
104+
segment.text,
105+
color_system=console._color_system,
106+
legacy_windows=console.legacy_windows,
107+
)
108+
)
109+
else:
110+
output.append(segment.text)
111+
112+
rendered = "".join(output)
113+
return rendered
114+
115+
116+
def format_task_id(
117+
task: "MetaTask",
118+
editor_url_scheme: str,
119+
short_name: bool = False,
120+
relative_to: Optional[Path] = None,
121+
) -> Text:
122+
"""Format a task id."""
123+
if short_name:
124+
path, task_name = task.short_name.split("::")
125+
elif relative_to:
126+
path = relative_to_(task.path, relative_to).as_posix()
127+
task_name = task.base_name
128+
else:
129+
path, task_name = task.name.split("::")
130+
url_style = create_url_style_for_task(task, editor_url_scheme)
131+
task_id = Text.assemble(
132+
Text(path + "::", style="dim"), Text(task_name, style=url_style)
133+
)
134+
return task_id
135+
136+
79137
def format_strings_as_flat_tree(strings: Iterable[str], title: str, icon: str) -> str:
80138
"""Format list of strings as flat tree."""
81139
tree = Tree(title)
82140
for name in strings:
83141
tree.add(icon + name)
84-
85-
text = "".join(
86-
[x.text for x in tree.__rich_console__(console, console.options)][:-1]
87-
)
88-
142+
text = render_to_string(tree, console)
89143
return text
90144

91145

92-
def escape_squared_brackets(string: str) -> str:
93-
"""Escape squared brackets which would be accidentally parsed by rich.
94-
95-
An example are the ids of parametrized tasks which are suffixed with squared
96-
brackets surrounding string representations of the parametrized arguments.
97-
98-
Example
99-
-------
100-
>>> escape_squared_brackets("Hello!")
101-
'Hello!'
102-
>>> escape_squared_brackets("task_dummy[arg1-arg2]")
103-
'task_dummy\\\\[arg1-arg2]'
104-
105-
"""
106-
return string.replace("[", "\\[")
107-
108-
109146
def create_url_style_for_task(task: "MetaTask", edtior_url_scheme: str) -> Style:
110147
"""Create the style to add a link to a task id."""
111148
url_scheme = _EDITOR_URL_SCHEMES.get(edtior_url_scheme, edtior_url_scheme)
112149

113150
info = {
114151
"path": _get_file(task.function),
115-
"line_number": inspect.getsourcelines(task.function)[1],
152+
"line_number": _get_source_lines(task.function),
116153
}
117154

118155
return Style() if not url_scheme else Style(link=url_scheme.format(**info))
@@ -136,6 +173,14 @@ def _get_file(function: Callable[..., Any]) -> Path:
136173
return Path(inspect.getfile(function))
137174

138175

176+
def _get_source_lines(function: Callable[..., Any]) -> int:
177+
"""Get the source line number of the function."""
178+
if isinstance(function, functools.partial):
179+
return _get_source_lines(function.func)
180+
else:
181+
return inspect.getsourcelines(function)[1]
182+
183+
139184
def unify_styles(*styles: Union[str, Style]) -> Style:
140185
"""Unify styles."""
141186
parsed_styles = []

src/_pytask/dag.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Implement some capabilities to deal with the DAG."""
22
import itertools
3-
from pathlib import Path
43
from typing import Dict
54
from typing import Generator
65
from typing import Iterable
@@ -13,7 +12,6 @@
1312
from _pytask.console import TASK_ICON
1413
from _pytask.mark_utils import get_specific_markers_from_task
1514
from _pytask.nodes import MetaTask
16-
from _pytask.shared import reduce_node_name
1715

1816

1917
def descending_tasks(task_name: str, dag: nx.DiGraph) -> Generator[str, None, None]:
@@ -71,17 +69,15 @@ class TopologicalSorter:
7169
_nodes_out = attr.ib(factory=set, type=Set[str])
7270

7371
@classmethod
74-
def from_dag(cls, dag: nx.DiGraph, paths: List[Path] = None) -> "TopologicalSorter":
75-
if paths is None:
76-
paths = []
77-
72+
def from_dag(cls, dag: nx.DiGraph) -> "TopologicalSorter":
73+
"""Instantiate from a DAG."""
7874
if not dag.is_directed():
7975
raise ValueError("Only directed graphs have a topological order.")
8076

8177
tasks = [
8278
dag.nodes[node]["task"] for node in dag.nodes if "task" in dag.nodes[node]
8379
]
84-
priorities = _extract_priorities_from_tasks(tasks, paths)
80+
priorities = _extract_priorities_from_tasks(tasks)
8581

8682
task_names = {task.name for task in tasks}
8783
task_dict = {name: nx.ancestors(dag, name) & task_names for name in task_names}
@@ -140,9 +136,7 @@ def static_order(self) -> Generator[str, None, None]:
140136
self.done(new_task)
141137

142138

143-
def _extract_priorities_from_tasks(
144-
tasks: List[MetaTask], paths: List[Path]
145-
) -> Dict[str, int]:
139+
def _extract_priorities_from_tasks(tasks: List[MetaTask]) -> Dict[str, int]:
146140
"""Extract priorities from tasks.
147141
148142
Priorities are set via the ``pytask.mark.try_first`` and ``pytask.mark.try_last``
@@ -166,8 +160,7 @@ def _extract_priorities_from_tasks(
166160
if tasks_w_mixed_priorities:
167161
name_to_task = {task.name: task for task in tasks}
168162
reduced_names = [
169-
reduce_node_name(name_to_task[name], paths)
170-
for name in tasks_w_mixed_priorities
163+
name_to_task[name].short_name for name in tasks_w_mixed_priorities
171164
]
172165
text = format_strings_as_flat_tree(
173166
reduced_names, "Tasks with mixed priorities", TASK_ICON

src/_pytask/database.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def pytask_parse_config(
134134

135135
@hookimpl
136136
def pytask_post_parse(config: Dict[str, Any]) -> None:
137+
"""Post-parse the configuration."""
137138
create_database(**config["database"])
138139

139140

src/_pytask/debugging.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ def wrapper(*args: Any, **kwargs: Any) -> None:
455455

456456

457457
def post_mortem(t: TracebackType) -> None:
458+
"""Start post-mortem debugging."""
458459
p = PytaskPDB._init_pdb("post_mortem")
459460
p.reset()
460461
p.interaction(None, t)

0 commit comments

Comments
 (0)