Skip to content

Commit c3dd1db

Browse files
authored
Rework reports and tracebacks. (#474)
1 parent 1c76085 commit c3dd1db

28 files changed

+278
-299
lines changed

docs/source/reference_guides/api.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,8 @@ outcome.
359359

360360
## Reports
361361

362-
There are some classes to handle different kinds of reports.
362+
Reports are classes that handle successes and errors during the collection, dag
363+
resolution and execution.
363364

364365
```{eval-rst}
365366
.. autoclass:: pytask.CollectionReport
@@ -393,10 +394,8 @@ There are some classes to handle different kinds of reports.
393394
## Tracebacks
394395

395396
```{eval-rst}
396-
.. autofunction:: pytask.format_exception_without_traceback
397397
.. autofunction:: pytask.remove_internal_traceback_frames_from_exc_info
398-
.. autofunction:: pytask.remove_traceback_from_exc_info
399-
.. autofunction:: pytask.render_exc_info
398+
.. autoclass:: pytask.Traceback
400399
```
401400

402401
## Warnings

environment.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ dependencies:
3232
- pygraphviz
3333
- pytest
3434
- pytest-cov
35-
- pytest-env
3635
- pytest-xdist
3736
- ruff
3837
- syrupy

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ filterwarnings = [
160160
"ignore:'@pytask.mark.*. is deprecated:FutureWarning",
161161
"ignore:The --rsyncdir command line argument:DeprecationWarning",
162162
]
163-
env = ["PYDEVD_DISABLE_FILE_VALIDATION=1"]
164163

165164
[tool.mypy]
166165
files = ["src", "tests"]

src/_pytask/clean.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from _pytask.pluginmanager import get_plugin_manager
3030
from _pytask.session import Session
3131
from _pytask.shared import to_list
32-
from _pytask.traceback import render_exc_info
32+
from _pytask.traceback import Traceback
3333
from _pytask.tree_util import tree_leaves
3434
from attrs import define
3535

@@ -108,7 +108,7 @@ def clean(**raw_config: Any) -> NoReturn: # noqa: C901, PLR0912
108108

109109
except Exception: # noqa: BLE001
110110
session = Session(exit_code=ExitCode.CONFIGURATION_FAILED)
111-
console.print(render_exc_info(*sys.exc_info()))
111+
console.print(Traceback(sys.exc_info()))
112112

113113
else:
114114
try:
@@ -161,8 +161,7 @@ def clean(**raw_config: Any) -> NoReturn: # noqa: C901, PLR0912
161161
console.rule(style="failed")
162162

163163
except Exception: # noqa: BLE001
164-
exc_info = sys.exc_info()
165-
console.print(render_exc_info(*exc_info, show_locals=config["show_locals"]))
164+
console.print(Traceback(sys.exc_info()))
166165
console.rule(style="failed")
167166
session.exit_code = ExitCode.FAILED
168167

src/_pytask/collect.py

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
from _pytask.config import IS_FILE_SYSTEM_CASE_SENSITIVE
2020
from _pytask.console import console
2121
from _pytask.console import create_summary_panel
22-
from _pytask.console import format_node_name
23-
from _pytask.console import format_task_name
2422
from _pytask.console import get_file
2523
from _pytask.console import is_jupyter
2624
from _pytask.exceptions import CollectionError
@@ -38,10 +36,9 @@
3836
from _pytask.path import find_case_sensitive_path
3937
from _pytask.path import import_path
4038
from _pytask.path import shorten_path
41-
from _pytask.report import CollectionReport
39+
from _pytask.reports import CollectionReport
4240
from _pytask.shared import find_duplicates
4341
from _pytask.task_utils import task as task_decorator
44-
from _pytask.traceback import render_exc_info
4542
from _pytask.typing import is_task_function
4643
from rich.text import Text
4744

@@ -464,41 +461,7 @@ def pytask_collect_log(
464461
)
465462

466463
for report in failed_reports:
467-
if report.node is None:
468-
header = "Error"
469-
else:
470-
if isinstance(report.node, PTask):
471-
short_name = format_task_name(
472-
report.node, editor_url_scheme="no_link"
473-
).plain
474-
elif isinstance(report.node, PNode):
475-
short_name = format_node_name(
476-
report.node, session.config["paths"]
477-
).plain
478-
else:
479-
msg = (
480-
"Requires a 'PTask' or a 'PNode' and not "
481-
f"{type(report.node)!r}."
482-
)
483-
raise TypeError(msg)
484-
485-
header = f"Could not collect {short_name}"
486-
487-
console.rule(
488-
Text(header, style=CollectionOutcome.FAIL.style),
489-
style=CollectionOutcome.FAIL.style,
490-
)
491-
492-
console.print()
493-
494-
assert report.exc_info
495-
console.print(
496-
render_exc_info(
497-
*report.exc_info, show_locals=session.config["show_locals"]
498-
)
499-
)
500-
501-
console.print()
464+
console.print(report)
502465

503466
panel = create_summary_panel(
504467
counts, CollectionOutcome, "Collected errors and tasks"

src/_pytask/console.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from typing import Sequence
1515
from typing import TYPE_CHECKING
1616

17-
import rich
1817
from _pytask.node_protocols import PNode
1918
from _pytask.node_protocols import PPathNode
2019
from _pytask.node_protocols import PTaskWithPath
@@ -109,9 +108,9 @@
109108

110109

111110
def render_to_string(
112-
text: RenderableType,
111+
renderable: RenderableType,
112+
console: Console,
113113
*,
114-
console: Console | None = None,
115114
strip_styles: bool = False,
116115
) -> str:
117116
"""Render text with rich to string including ANSI codes, etc..
@@ -120,31 +119,10 @@ def render_to_string(
120119
example, render warnings with colors or text in exceptions.
121120
122121
"""
123-
if console is None:
124-
console = rich.get_console()
125-
126-
segments = console.render(text)
127-
128-
output = []
129-
if console.no_color and console._color_system:
130-
segments = Segment.remove_color(segments)
131-
122+
buffer = console.render(renderable)
132123
if strip_styles:
133-
segments = Segment.strip_styles(segments)
134-
135-
for segment in segments:
136-
if segment.style:
137-
output.append(
138-
segment.style.render(
139-
segment.text,
140-
color_system=console._color_system,
141-
legacy_windows=console.legacy_windows,
142-
)
143-
)
144-
else:
145-
output.append(segment.text)
146-
147-
return "".join(output)
124+
buffer = Segment.strip_styles(buffer)
125+
return console._render_buffer(buffer)
148126

149127

150128
def format_task_name(task: PTask, editor_url_scheme: str) -> Text:

src/_pytask/dag.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@
2828
from _pytask.node_protocols import PNode
2929
from _pytask.node_protocols import PTask
3030
from _pytask.nodes import PythonNode
31-
from _pytask.report import DagReport
31+
from _pytask.reports import DagReport
3232
from _pytask.shared import reduce_names_of_multiple_nodes
33-
from _pytask.traceback import render_exc_info
3433
from _pytask.tree_util import tree_map
3534
from rich.text import Text
3635
from rich.tree import Tree
@@ -43,14 +42,7 @@
4342

4443
@hookimpl
4544
def pytask_dag(session: Session) -> bool | None:
46-
"""Create a directed acyclic graph (DAG) capturing dependencies between functions.
47-
48-
Parameters
49-
----------
50-
session : _pytask.session.Session
51-
Dictionary containing tasks.
52-
53-
"""
45+
"""Create a directed acyclic graph (DAG) for the workflow."""
5446
try:
5547
session.dag = session.hook.pytask_dag_create_dag(
5648
session=session, tasks=session.tasks
@@ -337,18 +329,13 @@ def _check_if_tasks_have_the_same_products(dag: nx.DiGraph, paths: list[Path]) -
337329

338330

339331
@hookimpl
340-
def pytask_dag_log(session: Session, report: DagReport) -> None:
332+
def pytask_dag_log(report: DagReport) -> None:
341333
"""Log errors which happened while resolving dependencies."""
342334
console.print()
343335
console.rule(
344-
Text("Failures during resolving dependencies", style="failed"),
345-
style="failed",
336+
Text("Failures during resolving dependencies", style="failed"), style="failed"
346337
)
347-
348338
console.print()
349-
console.print(
350-
render_exc_info(*report.exc_info, show_locals=session.config["show_locals"])
351-
)
352-
339+
console.print(report)
353340
console.print()
354341
console.rule(style="failed")

src/_pytask/debugging.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
from _pytask.console import console
1515
from _pytask.node_protocols import PTask
1616
from _pytask.outcomes import Exit
17-
from _pytask.traceback import remove_internal_traceback_frames_from_exc_info
18-
from _pytask.traceback import render_exc_info
17+
from _pytask.traceback import Traceback
1918

2019

2120
if TYPE_CHECKING:
@@ -351,13 +350,11 @@ def wrapper(*args: Any, **kwargs: Any) -> None:
351350
console.rule("Captured stderr", style="default")
352351
console.print(err)
353352

354-
exc_info = remove_internal_traceback_frames_from_exc_info(sys.exc_info())
353+
exc_info = sys.exc_info()
355354

356355
console.print()
357356
console.rule("Traceback", characters=">", style="default")
358-
console.print(
359-
render_exc_info(*exc_info, show_locals=session.config["show_locals"])
360-
)
357+
console.print(Traceback(exc_info))
361358

362359
post_mortem(exc_info[2]) # type: ignore[arg-type]
363360

src/_pytask/execute.py

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@
1313
from _pytask.console import create_url_style_for_task
1414
from _pytask.console import format_node_name
1515
from _pytask.console import format_strings_as_flat_tree
16-
from _pytask.console import format_task_name
1716
from _pytask.console import unify_styles
1817
from _pytask.dag_utils import descending_tasks
1918
from _pytask.dag_utils import TopologicalSorter
2019
from _pytask.database_utils import update_states_in_database
21-
from _pytask.enums import ShowCapture
2220
from _pytask.exceptions import ExecutionError
2321
from _pytask.exceptions import NodeLoadError
2422
from _pytask.exceptions import NodeNotFoundError
@@ -31,10 +29,8 @@
3129
from _pytask.outcomes import Exit
3230
from _pytask.outcomes import TaskOutcome
3331
from _pytask.outcomes import WouldBeExecuted
34-
from _pytask.report import ExecutionReport
35-
from _pytask.traceback import format_exception_without_traceback
32+
from _pytask.reports import ExecutionReport
3633
from _pytask.traceback import remove_traceback_from_exc_info
37-
from _pytask.traceback import render_exc_info
3834
from _pytask.tree_util import tree_leaves
3935
from _pytask.tree_util import tree_map
4036
from _pytask.tree_util import tree_structure
@@ -274,10 +270,10 @@ class ShowErrorsImmediatelyPlugin:
274270

275271
@staticmethod
276272
@hookimpl(tryfirst=True)
277-
def pytask_execute_task_log_end(session: Session, report: ExecutionReport) -> None:
273+
def pytask_execute_task_log_end(report: ExecutionReport) -> None:
278274
"""Print the error report of a task."""
279275
if report.outcome == TaskOutcome.FAIL:
280-
_print_errored_task_report(session, report)
276+
console.print(report)
281277

282278

283279
@hookimpl
@@ -301,7 +297,7 @@ def pytask_execute_log_end(session: Session, reports: list[ExecutionReport]) ->
301297
report.outcome == TaskOutcome.SKIP_PREVIOUS_FAILED
302298
and session.config["verbose"] >= 2 # noqa: PLR2004
303299
):
304-
_print_errored_task_report(session, report)
300+
console.print(report)
305301

306302
console.rule(style="dim")
307303

@@ -319,32 +315,3 @@ def pytask_execute_log_end(session: Session, reports: list[ExecutionReport]) ->
319315
raise ExecutionError
320316

321317
return True
322-
323-
324-
def _print_errored_task_report(session: Session, report: ExecutionReport) -> None:
325-
"""Print the traceback and the exception of an errored report."""
326-
task_name = format_task_name(
327-
task=report.task, editor_url_scheme=session.config["editor_url_scheme"]
328-
)
329-
text = Text.assemble("Task ", task_name, " failed", style="failed")
330-
console.rule(text, style=report.outcome.style)
331-
332-
console.print()
333-
334-
if report.exc_info and isinstance(report.exc_info[1], Exit):
335-
console.print(format_exception_without_traceback(report.exc_info))
336-
else:
337-
assert report.exc_info
338-
console.print(
339-
render_exc_info(*report.exc_info, show_locals=session.config["show_locals"])
340-
)
341-
342-
console.print()
343-
show_capture = session.config["show_capture"]
344-
for when, key, content in report.sections:
345-
if key in ("stdout", "stderr") and show_capture in (
346-
ShowCapture(key),
347-
ShowCapture.ALL,
348-
):
349-
console.rule(f"Captured {key} during {when}", style="default")
350-
console.print(content)

src/_pytask/hookspecs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
from _pytask.session import Session
2424
from _pytask.outcomes import CollectionOutcome
2525
from _pytask.outcomes import TaskOutcome
26-
from _pytask.report import CollectionReport
27-
from _pytask.report import ExecutionReport
28-
from _pytask.report import DagReport
26+
from _pytask.reports import CollectionReport
27+
from _pytask.reports import ExecutionReport
28+
from _pytask.reports import DagReport
2929

3030

3131
hookspec = pluggy.HookspecMarker("pytask")

0 commit comments

Comments
 (0)