Skip to content

Commit f530a76

Browse files
authored
Merge pull request #10816 from pytest-dev/backport-10772-to-7.2.x
[7.2.x] Correctly handle tracebackhide for chained exceptions
2 parents c27350e + 894338e commit f530a76

28 files changed

+85
-62
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ default_language_version:
22
python: "3.10"
33
repos:
44
- repo: https://github.com/psf/black
5-
rev: 22.12.0
5+
rev: 23.1.0
66
hooks:
77
- id: black
88
args: [--safe, --quiet]
99
- repo: https://github.com/asottile/blacken-docs
10-
rev: v1.12.1
10+
rev: 1.13.0
1111
hooks:
1212
- id: blacken-docs
13-
additional_dependencies: [black==20.8b1]
13+
additional_dependencies: [black==23.1.0]
1414
- repo: https://github.com/pre-commit/pre-commit-hooks
15-
rev: v4.3.0
15+
rev: v4.4.0
1616
hooks:
1717
- id: trailing-whitespace
1818
- id: end-of-file-fixer
@@ -23,23 +23,23 @@ repos:
2323
exclude: _pytest/(debugging|hookspec).py
2424
language_version: python3
2525
- repo: https://github.com/PyCQA/autoflake
26-
rev: v1.7.6
26+
rev: v2.0.2
2727
hooks:
2828
- id: autoflake
2929
name: autoflake
3030
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
3131
language: python
3232
files: \.py$
3333
- repo: https://github.com/PyCQA/flake8
34-
rev: 5.0.4
34+
rev: 6.0.0
3535
hooks:
3636
- id: flake8
3737
language_version: python3
3838
additional_dependencies:
3939
- flake8-typing-imports==1.12.0
4040
- flake8-docstrings==1.5.0
4141
- repo: https://github.com/asottile/reorder_python_imports
42-
rev: v3.8.5
42+
rev: v3.9.0
4343
hooks:
4444
- id: reorder-python-imports
4545
args: ['--application-directories=.:src', --py37-plus]
@@ -49,16 +49,16 @@ repos:
4949
- id: pyupgrade
5050
args: [--py37-plus]
5151
- repo: https://github.com/asottile/setup-cfg-fmt
52-
rev: v2.1.0
52+
rev: v2.2.0
5353
hooks:
5454
- id: setup-cfg-fmt
5555
args: ["--max-py-version=3.11", "--include-version-classifiers"]
5656
- repo: https://github.com/pre-commit/pygrep-hooks
57-
rev: v1.9.0
57+
rev: v1.10.0
5858
hooks:
5959
- id: python-use-type-annotations
6060
- repo: https://github.com/pre-commit/mirrors-mypy
61-
rev: v0.982
61+
rev: v1.1.1
6262
hooks:
6363
- id: mypy
6464
files: ^(src/|testing/)

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Erik M. Bray
126126
Evan Kepner
127127
Fabien Zarifian
128128
Fabio Zadrozny
129+
Felix Hofstätter
129130
Felix Nieuwenhuizen
130131
Feng Ma
131132
Florian Bruhin

changelog/1904.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correctly handle ``__tracebackhide__`` for chained exceptions.

doc/en/deprecations.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
10521052
.. code-block:: python
10531053
10541054
def check(x, y):
1055-
assert x ** x == y
1055+
assert x**x == y
10561056
10571057
10581058
def test_squared():
@@ -1067,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s
10671067
10681068
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
10691069
def test_squared(x, y):
1070-
assert x ** x == y
1070+
assert x**x == y
10711071
10721072
.. _internal classes accessed through node deprecated:
10731073

doc/en/how-to/fixtures.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
12371237
12381238
@pytest.fixture
12391239
def make_customer_record():
1240-
12411240
created_records = []
12421241
12431242
def _make_customer_record(name):

doc/en/how-to/monkeypatch.rst

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,17 @@ This can be done in our test file by defining a class to represent ``r``.
135135
# this is the previous code block example
136136
import app
137137
138+
138139
# custom class to be the mock return value
139140
# will override the requests.Response returned from requests.get
140141
class MockResponse:
141-
142142
# mock json() method always returns a specific testing dictionary
143143
@staticmethod
144144
def json():
145145
return {"mock_key": "mock_response"}
146146
147147
148148
def test_get_json(monkeypatch):
149-
150149
# Any arguments may be passed and mock_get() will always return our
151150
# mocked object, which only has the .json() method.
152151
def mock_get(*args, **kwargs):
@@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
181180
# app.py that includes the get_json() function
182181
import app
183182
183+
184184
# custom class to be the mock return value of requests.get()
185185
class MockResponse:
186186
@staticmethod
@@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
358358
359359
360360
def test_connection(monkeypatch):
361-
362361
# Patch the values of DEFAULT_CONFIG to specific
363362
# testing values only for this test.
364363
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
383382
384383
385384
def test_missing_user(monkeypatch):
386-
387385
# patch the DEFAULT_CONFIG t be missing the 'user' key
388386
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
389387
@@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
404402
# app.py with the connection string function
405403
import app
406404
405+
407406
# all of the mocks are moved into separated fixtures
408407
@pytest.fixture
409408
def mock_test_user(monkeypatch):
@@ -425,15 +424,13 @@ separate fixtures for each potential mock and reference them in the needed tests
425424
426425
# tests reference only the fixture mocks that are needed
427426
def test_connection(mock_test_user, mock_test_database):
428-
429427
expected = "User Id=test_user; Location=test_db;"
430428
431429
result = app.create_connection_string()
432430
assert result == expected
433431
434432
435433
def test_missing_user(mock_missing_default_user):
436-
437434
with pytest.raises(KeyError):
438435
_ = app.create_connection_string()
439436

doc/en/how-to/writing_hook_functions.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ and use pytest_addoption as follows:
249249
250250
# contents of hooks.py
251251
252+
252253
# Use firstresult=True because we only want one plugin to define this
253254
# default value
254255
@hookspec(firstresult=True)

src/_pytest/_code/code.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,13 @@ def filter(
411411
"""
412412
return Traceback(filter(fn, self), self._excinfo)
413413

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

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

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

611613
def getrepr(
612614
self,
@@ -942,18 +944,23 @@ def repr_excinfo(
942944
)
943945
else:
944946
reprtraceback = self.repr_traceback(excinfo_)
945-
reprcrash: Optional[ReprFileLocation] = (
946-
excinfo_._getreprcrash() if self.style != "value" else None
947-
)
947+
948+
# will be None if all traceback entries are hidden
949+
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
950+
if reprcrash:
951+
if self.style == "value":
952+
repr_chain += [(reprtraceback, None, descr)]
953+
else:
954+
repr_chain += [(reprtraceback, reprcrash, descr)]
948955
else:
949956
# Fallback to native repr if the exception doesn't have a traceback:
950957
# ExceptionInfo objects require a full traceback to work.
951958
reprtraceback = ReprTracebackNative(
952959
traceback.format_exception(type(e), e, None)
953960
)
954961
reprcrash = None
962+
repr_chain += [(reprtraceback, reprcrash, descr)]
955963

956-
repr_chain += [(reprtraceback, reprcrash, descr)]
957964
if e.__cause__ is not None and self.chain:
958965
e = e.__cause__
959966
excinfo_ = (
@@ -1037,7 +1044,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10371044
@attr.s(eq=False, auto_attribs=True)
10381045
class ReprExceptionInfo(ExceptionRepr):
10391046
reprtraceback: "ReprTraceback"
1040-
reprcrash: "ReprFileLocation"
1047+
reprcrash: Optional["ReprFileLocation"]
10411048

10421049
def toterminal(self, tw: TerminalWriter) -> None:
10431050
self.reprtraceback.toterminal(tw)

src/_pytest/_py/path.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from stat import S_ISREG
2525
from typing import Any
2626
from typing import Callable
27+
from typing import cast
2728
from typing import overload
2829
from typing import TYPE_CHECKING
2930

@@ -146,7 +147,7 @@ def __init__(self, fil, rec, ignore, bf, sort):
146147
self.fil = fil
147148
self.ignore = ignore
148149
self.breadthfirst = bf
149-
self.optsort = sort and sorted or (lambda x: x)
150+
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
150151

151152
def gen(self, path):
152153
try:
@@ -224,7 +225,7 @@ def owner(self):
224225
raise NotImplementedError("XXX win32")
225226
import pwd
226227

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

230231
@property
@@ -234,7 +235,7 @@ def group(self):
234235
raise NotImplementedError("XXX win32")
235236
import grp
236237

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

240241
def isdir(self):
@@ -252,15 +253,15 @@ def getuserid(user):
252253
import pwd
253254

254255
if not isinstance(user, int):
255-
user = pwd.getpwnam(user)[2]
256+
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
256257
return user
257258

258259

259260
def getgroupid(group):
260261
import grp
261262

262263
if not isinstance(group, int):
263-
group = grp.getgrnam(group)[2]
264+
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
264265
return group
265266

266267

src/_pytest/assertion/rewrite.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,6 @@ def get_data(self, pathname: Union[str, bytes]) -> bytes:
278278
return f.read()
279279

280280
if sys.version_info >= (3, 10):
281-
282281
if sys.version_info >= (3, 12):
283282
from importlib.resources.abc import TraversableResources
284283
else:

0 commit comments

Comments
 (0)