From 491431690420c0251076b5a10e355a20d6181ba0 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 18 Jul 2021 22:43:56 +0200 Subject: [PATCH 1/6] Hide traceback frames with __tracebackhide__ = True. --- docs/changes.rst | 2 ++ src/_pytask/traceback.py | 13 +++++++++---- tests/test_traceback.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/test_traceback.py diff --git a/docs/changes.rst b/docs/changes.rst index 5ebf67a5..2c7b513e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,8 @@ all releases are available on `PyPI `_ and - :gh:`121` add skipped and persisted tasks to the execution footer. - :gh:`127` make the table during execution the default. Silence pytask with negative verbose mode integers and increase verbosity with positive ones. +- :gh:`129` allows to hide frames from the traceback by using ``__tracebackhide__ = + True``. 0.0.16 - 2021-06-25 diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 8d835f6f..e7a1ba64 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -27,12 +27,17 @@ def remove_internal_traceback_frames_from_exc_info(exc_info): return exc_info -def _is_internal_traceback_frame(frame): - """Returns ``True`` if traceback frame belongs to internal packages. +def _is_internal_or_hidden_traceback_frame(frame): + """Returns ``True`` if traceback frame belongs to internal packages or is hidden. - Internal packages are ``_pytask`` and ``pluggy``. + Internal packages are ``_pytask`` and ``pluggy``. A hidden frame is indicated by a + local variable called ``__tracebackhide__ = True``. """ + is_hidden = frame.tb_frame.f_locals.get("__tracebackhide__", False) + if is_hidden: + return True + path = Path(frame.tb_frame.f_code.co_filename) return any(root in path.parents for root in [_PLUGGY_DIRECTORY, _PYTASK_DIRECTORY]) @@ -44,7 +49,7 @@ def _filter_internal_traceback_frames(frame): """ for frame in _yield_traceback_frames(frame): - if frame is None or not _is_internal_traceback_frame(frame): + if frame is None or not _is_internal_or_hidden_traceback_frame(frame): break return frame diff --git a/tests/test_traceback.py b/tests/test_traceback.py new file mode 100644 index 00000000..38bfbd57 --- /dev/null +++ b/tests/test_traceback.py @@ -0,0 +1,24 @@ +import textwrap + +from pytask import cli + + +def test_hide_traceback_from_error_report(runner, tmp_path): + source = """ + def task_main(): + a = "This variable should not be shown." + __tracebackhide__ = True + + + helper() + + + def helper(): + raise Exception + """ + tmp_path.joinpath("task_main.py").write_text(textwrap.dedent(source)) + + result = runner.invoke(cli, [tmp_path.as_posix(), "--show-locals"]) + + assert result.exit_code == 1 + assert "This variable should not be shown." not in result.output From 8f67e04c50587ea6346c0c6077aeb79be1f4c24e Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 18 Jul 2021 22:47:17 +0200 Subject: [PATCH 2/6] add. --- src/_pytask/clean.py | 6 +++--- src/_pytask/collect.py | 6 ++---- src/_pytask/debugging.py | 4 ++-- src/_pytask/execute.py | 6 ++---- src/_pytask/profile.py | 4 ++-- src/_pytask/resolve_dependencies.py | 6 +++--- src/_pytask/traceback.py | 22 +++++++++++++++++++--- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/_pytask/clean.py b/src/_pytask/clean.py index 856d37d6..df6f6798 100644 --- a/src/_pytask/clean.py +++ b/src/_pytask/clean.py @@ -17,7 +17,7 @@ from _pytask.pluginmanager import get_plugin_manager from _pytask.session import Session from _pytask.shared import get_first_non_none_value -from rich.traceback import Traceback +from _pytask.traceback import render_exc_info _HELP_TEXT_MODE = ( @@ -84,7 +84,7 @@ def clean(**config_from_cli): except Exception: session = Session({}, None) session.exit_code = ExitCode.CONFIGURATION_FAILED - console.print(Traceback.from_exception(*sys.exc_info())) + console.print(render_exc_info(*sys.exc_info(), config["show_locals"])) else: try: @@ -136,7 +136,7 @@ def clean(**config_from_cli): console.rule(style=ColorCode.FAILED) except Exception: - console.print(Traceback.from_exception(*sys.exc_info())) + console.print(render_exc_info(*sys.exc_info(), config["show_locals"])) console.rule(style=ColorCode.FAILED) session.exit_code = ExitCode.FAILED diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index d78c5d75..b7bc3882 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -20,7 +20,7 @@ from _pytask.path import find_case_sensitive_path from _pytask.report import CollectionReport from _pytask.shared import reduce_node_name -from rich.traceback import Traceback +from _pytask.traceback import render_exc_info @hookimpl @@ -252,9 +252,7 @@ def pytask_collect_log(session, reports, tasks): console.print() console.print( - Traceback.from_exception( - *report.exc_info, show_locals=session.config["show_locals"] - ) + render_exc_info(*report.exc_info, session.config["show_locals"]) ) console.print() diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index a9abba1a..b6f999ce 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -10,7 +10,7 @@ from _pytask.shared import convert_truthy_or_falsy_to_bool from _pytask.shared import get_first_non_none_value from _pytask.traceback import remove_internal_traceback_frames_from_exc_info -from rich.traceback import Traceback +from _pytask.traceback import render_exc_info @hookimpl @@ -364,7 +364,7 @@ def wrapper(*args, **kwargs): console.print() console.rule("Traceback", characters=">", style=None) - console.print(Traceback.from_exception(*exc_info)) + console.print(render_exc_info(*exc_info, session.config["show_locals"])) post_mortem(exc_info[2]) diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 357df24d..08d2d735 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -17,7 +17,7 @@ from _pytask.report import ExecutionReport from _pytask.shared import reduce_node_name from _pytask.traceback import remove_traceback_from_exc_info -from rich.traceback import Traceback +from _pytask.traceback import render_exc_info @hookimpl @@ -198,9 +198,7 @@ def pytask_execute_log_end(session, reports): console.print() - console.print( - Traceback.from_exception(*report.exc_info, show_locals=show_locals) - ) + console.print(render_exc_info(*report.exc_info, show_locals)) console.print() show_capture = session.config["show_capture"] diff --git a/src/_pytask/profile.py b/src/_pytask/profile.py index 679b9440..4aebbb05 100644 --- a/src/_pytask/profile.py +++ b/src/_pytask/profile.py @@ -18,9 +18,9 @@ from _pytask.session import Session from _pytask.shared import get_first_non_none_value from _pytask.shared import reduce_node_name +from _pytask.traceback import render_exc_info from pony import orm from rich.table import Table -from rich.traceback import Traceback class Runtime(db.Entity): @@ -108,7 +108,7 @@ def profile(**config_from_cli): except (ConfigurationError, Exception): session = Session({}, None) session.exit_code = ExitCode.CONFIGURATION_FAILED - console.print(Traceback.from_exception(*sys.exc_info())) + console.print(render_exc_info(*sys.exc_info(), config["show_locals"])) else: try: diff --git a/src/_pytask/resolve_dependencies.py b/src/_pytask/resolve_dependencies.py index 4c9e9af5..d97f8245 100644 --- a/src/_pytask/resolve_dependencies.py +++ b/src/_pytask/resolve_dependencies.py @@ -24,8 +24,8 @@ from _pytask.report import ResolvingDependenciesReport from _pytask.shared import reduce_names_of_multiple_nodes from _pytask.shared import reduce_node_name +from _pytask.traceback import render_exc_info from pony import orm -from rich.traceback import Traceback from rich.tree import Tree @@ -297,7 +297,7 @@ def _check_if_tasks_have_the_same_products(dag): @hookimpl -def pytask_resolve_dependencies_log(report): +def pytask_resolve_dependencies_log(session, report): """Log errors which happened while resolving dependencies.""" console.print() console.rule( @@ -306,7 +306,7 @@ def pytask_resolve_dependencies_log(report): ) console.print() - console.print(Traceback.from_exception(*report.exc_info)) + console.print(render_exc_info(*report.exc_info, session.config["show_locals"])) console.print() console.rule(style=ColorCode.FAILED) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index e7a1ba64..a332a717 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -1,13 +1,26 @@ """Process tracebacks.""" from pathlib import Path +from types import TracebackType import _pytask import pluggy +from rich.traceback import Traceback _PLUGGY_DIRECTORY = Path(pluggy.__file__).parent _PYTASK_DIRECTORY = Path(_pytask.__file__).parent +def render_exc_info(exc_type, exc_value, traceback, show_locals=False): + if isinstance(traceback, str): + renderable = traceback + else: + renderable = Traceback.from_exception( + exc_type, exc_value, traceback, show_locals=show_locals + ) + + return renderable + + def remove_traceback_from_exc_info(exc_info): """Remove traceback from exception.""" return (*exc_info[:2], None) @@ -21,8 +34,9 @@ def remove_internal_traceback_frames_from_exc_info(exc_info): """ if exc_info is not None: - filtered_traceback = _filter_internal_traceback_frames(exc_info[2]) - exc_info = (*exc_info[:2], filtered_traceback) + if isinstance(exc_info[2], TracebackType): + filtered_traceback = _filter_internal_traceback_frames(exc_info[2]) + exc_info = (*exc_info[:2], filtered_traceback) return exc_info @@ -39,7 +53,9 @@ def _is_internal_or_hidden_traceback_frame(frame): return True path = Path(frame.tb_frame.f_code.co_filename) - return any(root in path.parents for root in [_PLUGGY_DIRECTORY, _PYTASK_DIRECTORY]) + return frame.tb_frame.f_locals.get("__tracebackhide__", False) or any( + root in path.parents for root in [_PLUGGY_DIRECTORY, _PYTASK_DIRECTORY] + ) def _filter_internal_traceback_frames(frame): From c820c6dc30c3875c43b0e0c394d25038a61106b3 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 18 Jul 2021 22:59:12 +0200 Subject: [PATCH 3/6] why not parametrize. --- tests/test_traceback.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_traceback.py b/tests/test_traceback.py index 38bfbd57..f37a37e1 100644 --- a/tests/test_traceback.py +++ b/tests/test_traceback.py @@ -1,13 +1,15 @@ import textwrap +import pytest from pytask import cli -def test_hide_traceback_from_error_report(runner, tmp_path): - source = """ +@pytest.mark.parametrize("is_hidden", [True, False]) +def test_hide_traceback_from_error_report(runner, tmp_path, is_hidden): + source = f""" def task_main(): a = "This variable should not be shown." - __tracebackhide__ = True + __tracebackhide__ = {is_hidden} helper() @@ -21,4 +23,4 @@ def helper(): result = runner.invoke(cli, [tmp_path.as_posix(), "--show-locals"]) assert result.exit_code == 1 - assert "This variable should not be shown." not in result.output + assert ("This variable should not be shown." in result.output) is not is_hidden From 9beb45bd9fae37e2f4fc1091177ad3dab4cb9d30 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 19 Jul 2021 00:02:53 +0200 Subject: [PATCH 4/6] simplify func. --- src/_pytask/traceback.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index a332a717..22321736 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -53,9 +53,7 @@ def _is_internal_or_hidden_traceback_frame(frame): return True path = Path(frame.tb_frame.f_code.co_filename) - return frame.tb_frame.f_locals.get("__tracebackhide__", False) or any( - root in path.parents for root in [_PLUGGY_DIRECTORY, _PYTASK_DIRECTORY] - ) + return any(root in path.parents for root in [_PLUGGY_DIRECTORY, _PYTASK_DIRECTORY]) def _filter_internal_traceback_frames(frame): From 5eea0d4679ce42cd6fb00f27be4cbab69740d72b Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 19 Jul 2021 23:19:30 +0200 Subject: [PATCH 5/6] to changes. --- docs/source/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 2c7b513e..7a498c6c 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -25,6 +25,7 @@ all releases are available on `PyPI `_ and verbose mode integers and increase verbosity with positive ones. - :gh:`129` allows to hide frames from the traceback by using ``__tracebackhide__ = True``. +- :gh:`130` enables rendering of tracebacks from subprocesses with rich. 0.0.16 - 2021-06-25 From 7571ac5fe8f68afb45d028288cf6af3d0d055942 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 20 Jul 2021 20:18:24 +0200 Subject: [PATCH 6/6] ready for 0.1.0. --- README.rst | 8 -------- docs/source/changes.rst | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/README.rst b/README.rst index c7043e7c..b01c5faa 100644 --- a/README.rst +++ b/README.rst @@ -79,14 +79,6 @@ projects. Its features include: .. end-features -New Features ------------- - -- Create a visualization of the DAG with ``pytask dag``. (`Tutorial - `_) -- Show a profile of all tasks (duration, size of products) with ``pytask profile``. - - Installation ------------ diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 7a498c6c..a2e05d85 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -7,7 +7,7 @@ all releases are available on `PyPI `_ and `Anaconda.org `_. -0.1.0 - 2021-xx-xx +0.1.0 - 2021-07-20 ------------------ - :gh:`106` implements a verbose mode for the execution which is available with ``pytask