Skip to content

[7.2.x] Correctly handle tracebackhide for chained exceptions #10816

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 4 commits into from
Mar 15, 2023
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
20 changes: 10 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ default_language_version:
python: "3.10"
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.1.0
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: 1.13.0
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -23,23 +23,23 @@ repos:
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v1.7.6
rev: v2.0.2
hooks:
- id: autoflake
name: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 6.0.0
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.8.5
rev: v3.9.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus]
Expand All @@ -49,16 +49,16 @@ repos:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.1.0
rev: v2.2.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982
rev: v1.1.1
hooks:
- id: mypy
files: ^(src/|testing/)
Expand Down
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Erik M. Bray
Evan Kepner
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
Expand Down
1 change: 1 addition & 0 deletions changelog/1904.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correctly handle ``__tracebackhide__`` for chained exceptions.
4 changes: 2 additions & 2 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python

def check(x, y):
assert x ** x == y
assert x**x == y


def test_squared():
Expand All @@ -1067,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s

@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x ** x == y
assert x**x == y

.. _internal classes accessed through node deprecated:

Expand Down
1 change: 0 additions & 1 deletion doc/en/how-to/fixtures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care

@pytest.fixture
def make_customer_record():

created_records = []

def _make_customer_record(name):
Expand Down
9 changes: 3 additions & 6 deletions doc/en/how-to/monkeypatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,17 @@ This can be done in our test file by defining a class to represent ``r``.
# this is the previous code block example
import app


# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:

# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
return {"mock_key": "mock_response"}


def test_get_json(monkeypatch):

# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
Expand Down Expand Up @@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
# app.py that includes the get_json() function
import app


# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
Expand Down Expand Up @@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific


def test_connection(monkeypatch):

# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
Expand All @@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v


def test_missing_user(monkeypatch):

# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)

Expand All @@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
# app.py with the connection string function
import app


# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
Expand All @@ -425,15 +424,13 @@ separate fixtures for each potential mock and reference them in the needed tests

# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):

expected = "User Id=test_user; Location=test_db;"

result = app.create_connection_string()
assert result == expected


def test_missing_user(mock_missing_default_user):

with pytest.raises(KeyError):
_ = app.create_connection_string()

Expand Down
1 change: 1 addition & 0 deletions doc/en/how-to/writing_hook_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ and use pytest_addoption as follows:

# contents of hooks.py


# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
Expand Down
27 changes: 17 additions & 10 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,13 +411,13 @@ def filter(
"""
return Traceback(filter(fn, self), self._excinfo)

def getcrashentry(self) -> TracebackEntry:
def getcrashentry(self) -> Optional[TracebackEntry]:
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return self[-1]
return None

def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
Expand Down Expand Up @@ -602,11 +602,13 @@ def errisinstance(
"""
return isinstance(self.value, exc)

def _getreprcrash(self) -> "ReprFileLocation":
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
if entry:
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
return None

def getrepr(
self,
Expand Down Expand Up @@ -942,18 +944,23 @@ def repr_excinfo(
)
else:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
)

# will be None if all traceback entries are hidden
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
if reprcrash:
if self.style == "value":
repr_chain += [(reprtraceback, None, descr)]
else:
repr_chain += [(reprtraceback, reprcrash, descr)]
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
reprtraceback = ReprTracebackNative(
traceback.format_exception(type(e), e, None)
)
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]

repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
Expand Down Expand Up @@ -1037,7 +1044,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
@attr.s(eq=False, auto_attribs=True)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation"
reprcrash: Optional["ReprFileLocation"]

def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
Expand Down
11 changes: 6 additions & 5 deletions src/_pytest/_py/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from stat import S_ISREG
from typing import Any
from typing import Callable
from typing import cast
from typing import overload
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -146,7 +147,7 @@ def __init__(self, fil, rec, ignore, bf, sort):
self.fil = fil
self.ignore = ignore
self.breadthfirst = bf
self.optsort = sort and sorted or (lambda x: x)
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)

def gen(self, path):
try:
Expand Down Expand Up @@ -224,7 +225,7 @@ def owner(self):
raise NotImplementedError("XXX win32")
import pwd

entry = error.checked_call(pwd.getpwuid, self.uid)
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
return entry[0]

@property
Expand All @@ -234,7 +235,7 @@ def group(self):
raise NotImplementedError("XXX win32")
import grp

entry = error.checked_call(grp.getgrgid, self.gid)
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
return entry[0]

def isdir(self):
Expand All @@ -252,15 +253,15 @@ def getuserid(user):
import pwd

if not isinstance(user, int):
user = pwd.getpwnam(user)[2]
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
return user


def getgroupid(group):
import grp

if not isinstance(group, int):
group = grp.getgrnam(group)[2]
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
return group


Expand Down
1 change: 0 additions & 1 deletion src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ def get_data(self, pathname: Union[str, bytes]) -> bytes:
return f.read()

if sys.version_info >= (3, 10):

if sys.version_info >= (3, 12):
from importlib.resources.abc import TraversableResources
else:
Expand Down
1 change: 0 additions & 1 deletion src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ class NoCapture:


class SysCaptureBinary:

EMPTY_BUFFER = b""

def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
Expand Down
2 changes: 0 additions & 2 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
from _pytest.warning_types import warn_explicit_for

if TYPE_CHECKING:

from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
Expand Down Expand Up @@ -1059,7 +1058,6 @@ def pytest_cmdline_parse(
try:
self.parse(args)
except UsageError:

# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
Expand Down
1 change: 0 additions & 1 deletion src/_pytest/config/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def __getattr__(self, key, _wraps=functools.wraps):

@_wraps(hook)
def fixed_hook(**kw):

path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None:
Expand Down
1 change: 0 additions & 1 deletion src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,6 @@ def _find(
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():

# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
Expand Down
10 changes: 6 additions & 4 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
# -------------------------------------------------------------------------


def pytest_report_header(
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
Expand Down Expand Up @@ -767,7 +767,7 @@ def pytest_report_header(
"""


def pytest_report_collectionfinish(
def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config",
start_path: Path,
startdir: "LEGACY_PATH",
Expand Down Expand Up @@ -800,7 +800,7 @@ def pytest_report_collectionfinish(


@hookspec(firstresult=True)
def pytest_report_teststatus(
def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config"
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
"""Return result-category, shortletter and verbose word for status
Expand Down Expand Up @@ -880,7 +880,9 @@ def pytest_warning_recorded(
# -------------------------------------------------------------------------


def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
def pytest_markeval_namespace( # type:ignore[empty-body]
config: "Config",
) -> Dict[str, Any]:
"""Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers.

Expand Down
1 change: 0 additions & 1 deletion src/_pytest/outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ def _resolve_msg_to_reason(
"""
__tracebackhide__ = True
if msg is not None:

if reason:
from pytest import UsageError

Expand Down
Loading