Skip to content

Commit 26a4383

Browse files
authored
Implement --show-errors-immediately. (#156)
1 parent f3bc48e commit 26a4383

File tree

6 files changed

+101
-26
lines changed

6 files changed

+101
-26
lines changed

docs/source/changes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
2222
days, hours, minutes and seconds.
2323
- :gh:`155` implements functions to check for optional packages and programs and raises
2424
errors for requirements to draw the DAG earlier.
25+
- :gh:`156` adds an option to print/show errors as soon as they occur.
2526

2627

2728
0.1.1 - 2021-08-25

docs/source/reference_guides/configuration.rst

+14
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ The options
147147
pdb = True
148148
149149
150+
.. confval:: show_errors_immediately
151+
152+
If you want to print the exception and tracebacks of errors as soon as they occur,
153+
set this value to true.
154+
155+
.. code-block:: console
156+
157+
pytask build --show-errors-immediately
158+
159+
.. code-block:: ini
160+
161+
show_errors_immediately = True
162+
163+
150164
.. confval:: show_locals
151165

152166
If you want to print local variables of each stack frame in the tracebacks, set this

src/_pytask/build.py

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ def main(config_from_cli):
9494
help="Stop after the first failure.",
9595
)
9696
@click.option("--max-failures", default=None, help="Stop after some failures.")
97+
@click.option(
98+
"--show-errors-immediately",
99+
is_flag=True,
100+
default=None,
101+
help="Print errors with tracebacks as soon as the task fails.",
102+
)
97103
def build(**config_from_cli):
98104
"""Collect and execute tasks and report the results.
99105

src/_pytask/execute.py

+49-22
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,30 @@
1616
from _pytask.outcomes import Persisted
1717
from _pytask.outcomes import Skipped
1818
from _pytask.report import ExecutionReport
19+
from _pytask.shared import get_first_non_none_value
1920
from _pytask.shared import reduce_node_name
2021
from _pytask.traceback import format_exception_without_traceback
2122
from _pytask.traceback import remove_traceback_from_exc_info
2223
from _pytask.traceback import render_exc_info
2324

2425

26+
@hookimpl
27+
def pytask_post_parse(config):
28+
if config["show_errors_immediately"]:
29+
config["pm"].register(ShowErrorsImmediatelyPlugin)
30+
31+
32+
@hookimpl
33+
def pytask_parse_config(config, config_from_cli, config_from_file):
34+
config["show_errors_immediately"] = get_first_non_none_value(
35+
config_from_cli,
36+
config_from_file,
37+
key="show_errors_immediately",
38+
default=False,
39+
callback=lambda x: x if x is None else bool(x),
40+
)
41+
42+
2543
@hookimpl
2644
def pytask_execute(session):
2745
"""Execute tasks."""
@@ -172,10 +190,17 @@ def pytask_execute_task_log_end(report):
172190
console.print(report.symbol, style=report.color, end="")
173191

174192

193+
class ShowErrorsImmediatelyPlugin:
194+
@staticmethod
195+
@hookimpl(tryfirst=True)
196+
def pytask_execute_task_log_end(session, report):
197+
if not report.success:
198+
_print_errored_task_report(session, report)
199+
200+
175201
@hookimpl
176202
def pytask_execute_log_end(session, reports):
177203
session.execution_end = time.time()
178-
show_locals = session.config["show_locals"]
179204

180205
n_failed = len(reports) - sum(report.success for report in reports)
181206
n_skipped = sum(
@@ -195,27 +220,7 @@ def pytask_execute_log_end(session, reports):
195220

196221
for report in reports:
197222
if not report.success:
198-
199-
task_name = reduce_node_name(report.task, session.config["paths"])
200-
if len(task_name) > console.width - 15:
201-
task_name = report.task.base_name
202-
console.rule(
203-
f"[{ColorCode.FAILED}]Task {task_name} failed", style=ColorCode.FAILED
204-
)
205-
206-
console.print()
207-
208-
if report.exc_info and isinstance(report.exc_info[1], Exit):
209-
console.print(format_exception_without_traceback(report.exc_info))
210-
else:
211-
console.print(render_exc_info(*report.exc_info, show_locals))
212-
213-
console.print()
214-
show_capture = session.config["show_capture"]
215-
for when, key, content in report.sections:
216-
if key in ("stdout", "stderr") and show_capture in (key, "all"):
217-
console.rule(f"Captured {key} during {when}", style=None)
218-
console.print(content)
223+
_print_errored_task_report(session, report)
219224

220225
session.hook.pytask_log_session_footer(
221226
session=session,
@@ -235,6 +240,28 @@ def pytask_execute_log_end(session, reports):
235240
return True
236241

237242

243+
def _print_errored_task_report(session, report):
244+
"""Print the traceback and the exception of an errored report."""
245+
task_name = reduce_node_name(report.task, session.config["paths"])
246+
if len(task_name) > console.width - 15:
247+
task_name = report.task.base_name
248+
console.rule(f"[{ColorCode.FAILED}]Task {task_name} failed", style=ColorCode.FAILED)
249+
250+
console.print()
251+
252+
if report.exc_info and isinstance(report.exc_info[1], Exit):
253+
console.print(format_exception_without_traceback(report.exc_info))
254+
else:
255+
console.print(render_exc_info(*report.exc_info, session.config["show_locals"]))
256+
257+
console.print()
258+
show_capture = session.config["show_capture"]
259+
for when, key, content in report.sections:
260+
if key in ("stdout", "stderr") and show_capture in (key, "all"):
261+
console.rule(f"Captured {key} during {when}", style=None)
262+
console.print(content)
263+
264+
238265
def _update_states_in_database(dag, task_name):
239266
"""Update the state for each node of a task in the database."""
240267
for name in node_and_neighbors(dag, task_name):

src/_pytask/live.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def pytask_execute_task_log_start(self, task):
132132
self.update_running_tasks(task)
133133
return True
134134

135-
@hookimpl(tryfirst=True)
135+
@hookimpl
136136
def pytask_execute_task_log_end(self, report):
137137
self.update_reports(report)
138138
return True

tests/test_execute.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import subprocess
23
import textwrap
34

@@ -265,7 +266,7 @@ def task_y(): pass
265266

266267

267268
@pytest.mark.end_to_end
268-
def test_scheduling_w_mixed_priorities(tmp_path):
269+
def test_scheduling_w_mixed_priorities(runner, tmp_path):
269270
source = """
270271
import pytask
271272
@@ -275,6 +276,32 @@ def task_mixed(): pass
275276
"""
276277
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
277278

278-
session = main({"paths": tmp_path})
279+
result = runner.invoke(cli, [tmp_path.as_posix()])
280+
281+
assert result.exit_code == 4
282+
assert "Failures during resolving dependencies" in result.output
283+
assert "'try_first' and 'try_last' cannot be applied" in result.output
284+
285+
286+
@pytest.mark.end_to_end
287+
@pytest.mark.parametrize("show_errors_immediately", [True, False])
288+
def test_show_errors_immediately(runner, tmp_path, show_errors_immediately):
289+
source = """
290+
def task_succeed(): pass
291+
def task_error(): raise ValueError
292+
"""
293+
tmp_path.joinpath("task_error.py").write_text(textwrap.dedent(source))
294+
295+
args = [tmp_path.as_posix()]
296+
if show_errors_immediately:
297+
args.append("--show-errors-immediately")
298+
result = runner.invoke(cli, args)
299+
300+
assert result.exit_code == 1
301+
assert "::task_succeed │ ." in result.output
279302

280-
assert session.exit_code == 4
303+
matches_traceback = re.findall("Traceback", result.output)
304+
if show_errors_immediately:
305+
assert len(matches_traceback) == 2
306+
else:
307+
assert len(matches_traceback) == 1

0 commit comments

Comments
 (0)