Skip to content

Commit b4bb61d

Browse files
authored
Show rich tracebacks for errors in subprocesses. (#27)
1 parent f5624bd commit b4bb61d

File tree

6 files changed

+86
-16
lines changed

6 files changed

+86
-16
lines changed

Diff for: CHANGES.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ all releases are available on `PyPI <https://pypi.org/project/pytask-parallel>`_
77
`Anaconda.org <https://anaconda.org/conda-forge/pytask-parallel>`_.
88

99

10-
0.0.9 - 2021-xx-xx
10+
0.1.0 - 2021-07-20
1111
------------------
1212

1313
- :gh:`19` adds ``conda-forge`` to the ``README.rst``.
1414
- :gh:`22` add note that the debugger cannot be used together with pytask-parallel.
1515
- :gh:`24` replaces versioneer with setuptools-scm.
1616
- :gh:`25` aborts build and prints reports on ``KeyboardInterrupt``.
17+
- :gh:`27` enables rich tracebacks from subprocesses.
1718

1819

1920
0.0.8 - 2021-03-05

Diff for: environment.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ channels:
55
- nodefaults
66

77
dependencies:
8-
- python=3.6
8+
- python
99
- pip
1010
- setuptools_scm
1111
- toml
@@ -16,7 +16,7 @@ dependencies:
1616
- conda-verify
1717

1818
# Package dependencies
19-
- pytask >= 0.0.11
19+
- pytask >= 0.1.0
2020
- cloudpickle
2121
- loky
2222

Diff for: setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ install_requires =
3131
click
3232
cloudpickle
3333
loky
34-
pytask>=0.0.11
34+
pytask>=0.1.0
3535
python_requires = >=3.6
3636
include_package_data = True
3737
package_dir = =src

Diff for: src/pytask_parallel/execute.py

+49-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
"""Contains code relevant to the execution."""
22
import sys
33
import time
4+
from typing import Any
5+
from typing import Tuple
46

57
import cloudpickle
68
from _pytask.config import hookimpl
9+
from _pytask.console import console
710
from _pytask.report import ExecutionReport
11+
from _pytask.traceback import remove_internal_traceback_frames_from_exc_info
812
from pytask_parallel.backends import PARALLEL_BACKENDS
13+
from rich.console import ConsoleOptions
14+
from rich.traceback import Traceback
915

1016

1117
@hookimpl
1218
def pytask_post_parse(config):
1319
"""Register the parallel backend."""
14-
if config["parallel_backend"] == "processes":
20+
if config["parallel_backend"] in ["loky", "processes"]:
1521
config["pm"].register(ProcessesNameSpace)
16-
elif config["parallel_backend"] in ["threads", "loky"]:
22+
elif config["parallel_backend"] in ["threads"]:
1723
config["pm"].register(DefaultBackendNameSpace)
1824

1925

@@ -72,13 +78,23 @@ def pytask_execute_build(session):
7278

7379
for task_name in list(running_tasks):
7480
future = running_tasks[task_name]
75-
if future.done() and future.exception() is not None:
81+
if future.done() and (
82+
future.exception() is not None
83+
or future.result() is not None
84+
):
7685
task = session.dag.nodes[task_name]["task"]
77-
exception = future.exception()
78-
newly_collected_reports.append(
79-
ExecutionReport.from_task_and_exception(
80-
task, (type(exception), exception, None)
86+
if future.exception() is not None:
87+
exception = future.exception()
88+
exc_info = (
89+
type(exception),
90+
exception,
91+
exception.__traceback__,
8192
)
93+
else:
94+
exc_info = future.result()
95+
96+
newly_collected_reports.append(
97+
ExecutionReport.from_task_and_exception(task, exc_info)
8298
)
8399
running_tasks.pop(task_name)
84100
session.scheduler.done(task_name)
@@ -132,18 +148,41 @@ def pytask_execute_task(session, task): # noqa: N805
132148
"""
133149
if session.config["n_workers"] > 1:
134150
bytes_ = cloudpickle.dumps(task)
135-
return session.executor.submit(unserialize_and_execute_task, bytes_)
151+
return session.executor.submit(
152+
_unserialize_and_execute_task,
153+
bytes_=bytes_,
154+
show_locals=session.config["show_locals"],
155+
console_options=console.options,
156+
)
136157

137158

138-
def unserialize_and_execute_task(bytes_):
159+
def _unserialize_and_execute_task(bytes_, show_locals, console_options):
139160
"""Unserialize and execute task.
140161
141162
This function receives bytes and unpickles them to a task which is them execute
142163
in a spawned process or thread.
143164
144165
"""
166+
__tracebackhide__ = True
167+
145168
task = cloudpickle.loads(bytes_)
146-
task.execute()
169+
170+
try:
171+
task.execute()
172+
except Exception:
173+
exc_info = sys.exc_info()
174+
processed_exc_info = _process_exception(exc_info, show_locals, console_options)
175+
return processed_exc_info
176+
177+
178+
def _process_exception(
179+
exc_info: Tuple[Any], show_locals: bool, console_options: ConsoleOptions
180+
) -> Tuple[Any]:
181+
exc_info = remove_internal_traceback_frames_from_exc_info(exc_info)
182+
traceback = Traceback.from_exception(*exc_info, show_locals=show_locals)
183+
segments = console.render(traceback, options=console_options)
184+
text = "".join(segment.text for segment in segments)
185+
return (*exc_info[:2], text)
147186

148187

149188
class DefaultBackendNameSpace:

Diff for: tests/test_execute.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ def myfunc():
119119
task = DummyTask(myfunc)
120120

121121
session = Session()
122-
session.config = {"n_workers": 2, "parallel_backend": parallel_backend}
122+
session.config = {
123+
"n_workers": 2,
124+
"parallel_backend": parallel_backend,
125+
"show_locals": False,
126+
}
123127

124128
with PARALLEL_BACKENDS[parallel_backend](
125129
max_workers=session.config["n_workers"]
@@ -235,3 +239,29 @@ def task_5():
235239
assert first_task_name.endswith("task_0") or first_task_name.endswith("task_3")
236240
last_task_name = session.execution_reports[-1].task.name
237241
assert last_task_name.endswith("task_2") or last_task_name.endswith("task_5")
242+
243+
244+
@pytest.mark.end_to_end
245+
@pytest.mark.parametrize("parallel_backend", PARALLEL_BACKENDS)
246+
@pytest.mark.parametrize("show_locals", [True, False])
247+
def test_rendering_of_tracebacks_with_rich(
248+
runner, tmp_path, parallel_backend, show_locals
249+
):
250+
source = """
251+
import pytask
252+
253+
def task_raising_error():
254+
a = list(range(5))
255+
raise Exception
256+
"""
257+
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
258+
259+
args = [tmp_path.as_posix(), "-n", "2", "--parallel-backend", parallel_backend]
260+
if show_locals:
261+
args.append("--show-locals")
262+
result = runner.invoke(cli, args)
263+
264+
assert result.exit_code == 1
265+
assert "───── Traceback" in result.output
266+
assert ("───── locals" in result.output) is show_locals
267+
assert ("[0, 1, 2, 3, 4]" in result.output) is show_locals

Diff for: tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ basepython = python
1010
conda_deps =
1111
cloudpickle
1212
loky
13-
pytask >=0.0.11
13+
pytask >=0.1.0
1414
pytest
1515
pytest-cov
1616
pytest-xdist

0 commit comments

Comments
 (0)