Skip to content

Allow __tracebackhide__ to be a callable. #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
`Anaconda.org <https://anaconda.org/conda-forge/pytask>`_.


0.1.7 - 2022-01-28
------------------

- :gh:`153` adds support for Python 3.10 which requires pony >= 0.7.15.
- :gh:`192` deprecates Python 3.6.
- :gh:`209` cancels previous CI jobs when a new job is started.
- :gh:`210` allows ``__tracebackhide__`` to be a callable which accepts the current
exception as an input. Closes :gh:`145`.


0.1.6 - 2022-01-27
------------------

Expand Down
16 changes: 11 additions & 5 deletions src/_pytask/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,37 +57,43 @@ def remove_internal_traceback_frames_from_exc_info(
"""
if exc_info is not None:
if isinstance(exc_info[2], TracebackType):
filtered_traceback = _filter_internal_traceback_frames(exc_info[2])
filtered_traceback = _filter_internal_traceback_frames(exc_info)
exc_info = (*exc_info[:2], filtered_traceback)

return exc_info


def _is_internal_or_hidden_traceback_frame(frame: TracebackType) -> bool:
def _is_internal_or_hidden_traceback_frame(
frame: TracebackType, exc_info: ExceptionInfo
) -> bool:
"""Returns ``True`` if traceback frame belongs to internal packages or is hidden.

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:

if callable(is_hidden):
return is_hidden(exc_info)
elif 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])


def _filter_internal_traceback_frames(
frame: TracebackType,
exc_info: ExceptionInfo,
) -> TracebackType:
"""Filter internal traceback frames from traceback.

If the first external frame is visited, return the frame. Else return ``None``.

"""
frame = exc_info[2]
for frame in _yield_traceback_frames(frame):
if frame is None or not _is_internal_or_hidden_traceback_frame(frame):
if frame is None or not _is_internal_or_hidden_traceback_frame(frame, exc_info):
break
return frame

Expand Down
20 changes: 16 additions & 4 deletions tests/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@


@pytest.mark.end_to_end
@pytest.mark.parametrize("is_hidden", [True, False])
def test_hide_traceback_from_error_report(runner, tmp_path, is_hidden):
@pytest.mark.parametrize(
"value, exception, is_hidden",
[
("True", "Exception", True),
("False", "Exception", False),
("lambda exc_info: True", "Exception", True),
("lambda exc_info: False", "Exception", False),
("lambda exc_info: isinstance(exc_info[1], ValueError)", "ValueError", True),
("lambda exc_info: isinstance(exc_info[1], ValueError)", "TypeError", False),
],
)
def test_hide_traceback_from_error_report(
runner, tmp_path, value, exception, is_hidden
):
source = f"""
def task_main():
a = "This variable should not be shown."
__tracebackhide__ = {is_hidden}
__tracebackhide__ = {value}


helper()


def helper():
raise Exception
raise {exception}
"""
tmp_path.joinpath("task_main.py").write_text(textwrap.dedent(source))

Expand Down