Skip to content

Commit d0ee227

Browse files
committed
Fix compatibility with Twisted 25
The issue arises because `Failure.__init__` in Twisted 25 can receive a non-None `exc_value` while `exc_tb` is `None`. In such cases, `ExceptionInfo[BaseException].from_exc_info` fails, as it expects a traceback when `sys.exc_info()` returns a tuple. This leads to the error message `'NoneType' object is not iterable`. Adjusted the `Failure.__init__` wrapper's logic to ensure we always have a valid traceback. Tested with `twisted 24.11.0` and `25.5.0`. Fixes #13497
1 parent 9e9633d commit d0ee227

File tree

3 files changed

+26
-7
lines changed

3 files changed

+26
-7
lines changed

changelog/13497.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed compatibility with ``Twisted 25``.

src/_pytest/_code/code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ def typename(self) -> str:
623623
assert self._excinfo is not None, (
624624
".typename can only be used after the context manager exits"
625625
)
626-
return self.type.__name__
626+
return getattr(self.type, "__name__", f"<unknown type name: {self.type!r}>")
627627

628628
@property
629629
def traceback(self) -> Traceback:

src/_pytest/unittest.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ def unittest_setup_method_fixture(
197197
)
198198

199199

200+
# Name of the attribute in `twisted.python.Failure` instances that stores
201+
# the `sys.exc_info()` tuple.
202+
# See twisted.trial support in `pytest_runtest_protocol`.
203+
TWISTED_RAW_EXCINFO_ATTR = "_twisted_raw_excinfo"
204+
205+
200206
class TestCaseFunction(Function):
201207
nofuncargs = True
202208
_excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None
@@ -229,7 +235,14 @@ def startTest(self, testcase: unittest.TestCase) -> None:
229235

230236
def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
231237
# Unwrap potential exception info (see twisted trial support below).
232-
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
238+
# Twisted calls addError() passing its own classes (like `twisted.python.Failure`), which violates
239+
# the `addError()` signature, so we extract the original `sys.exc_info()` tuple which is stored
240+
# in the object.
241+
if hasattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR):
242+
saved_exc_info = getattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
243+
# Delete the attribute from the original object to avoid leaks.
244+
delattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
245+
rawexcinfo = saved_exc_info
233246
try:
234247
excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(
235248
rawexcinfo # type: ignore[arg-type]
@@ -394,31 +407,36 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
394407
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
395408
ut: Any = sys.modules["twisted.python.failure"]
396409
global classImplements_has_run
397-
Failure__init__ = ut.Failure.__init__
398410
if not classImplements_has_run:
399411
from twisted.trial.itrial import IReporter
400412
from zope.interface import classImplements
401413

402414
classImplements(TestCaseFunction, IReporter)
403415
classImplements_has_run = True
404416

405-
def excstore(
417+
# Monkeypatch `Failure.__init__` to store the raw exception info.
418+
Failure__init__ = ut.Failure.__init__
419+
420+
def store_raw_exception_info(
406421
self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
407422
):
408423
if exc_value is None:
409-
self._rawexcinfo = sys.exc_info()
424+
raw_exc_info = sys.exc_info()
410425
else:
411426
if exc_type is None:
412427
exc_type = type(exc_value)
413-
self._rawexcinfo = (exc_type, exc_value, exc_tb)
428+
if exc_tb is None:
429+
exc_tb = sys.exc_info()[2]
430+
raw_exc_info = (exc_type, exc_value, exc_tb)
431+
setattr(self, TWISTED_RAW_EXCINFO_ATTR, tuple(raw_exc_info))
414432
try:
415433
Failure__init__(
416434
self, exc_value, exc_type, exc_tb, captureVars=captureVars
417435
)
418436
except TypeError:
419437
Failure__init__(self, exc_value, exc_type, exc_tb)
420438

421-
ut.Failure.__init__ = excstore
439+
ut.Failure.__init__ = store_raw_exception_info
422440
try:
423441
res = yield
424442
finally:

0 commit comments

Comments
 (0)