Skip to content

Commit 96f0319

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 96f0319

File tree

2 files changed

+26
-6
lines changed

2 files changed

+26
-6
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/unittest.py

Lines changed: 25 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,15 @@ 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
246+
del saved_exc_info
233247
try:
234248
excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(
235249
rawexcinfo # type: ignore[arg-type]
@@ -394,31 +408,36 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
394408
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
395409
ut: Any = sys.modules["twisted.python.failure"]
396410
global classImplements_has_run
397-
Failure__init__ = ut.Failure.__init__
398411
if not classImplements_has_run:
399412
from twisted.trial.itrial import IReporter
400413
from zope.interface import classImplements
401414

402415
classImplements(TestCaseFunction, IReporter)
403416
classImplements_has_run = True
404417

405-
def excstore(
418+
# Monkeypatch `Failure.__init__` to store the raw exception info.
419+
Failure__init__ = ut.Failure.__init__
420+
421+
def store_raw_exception_info(
406422
self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
407423
):
408424
if exc_value is None:
409-
self._rawexcinfo = sys.exc_info()
425+
raw_exc_info = sys.exc_info()
410426
else:
411427
if exc_type is None:
412428
exc_type = type(exc_value)
413-
self._rawexcinfo = (exc_type, exc_value, exc_tb)
429+
if exc_tb is None:
430+
exc_tb = sys.exc_info()[2]
431+
raw_exc_info = (exc_type, exc_value, exc_tb)
432+
setattr(self, TWISTED_RAW_EXCINFO_ATTR, tuple(raw_exc_info))
414433
try:
415434
Failure__init__(
416435
self, exc_value, exc_type, exc_tb, captureVars=captureVars
417436
)
418437
except TypeError:
419438
Failure__init__(self, exc_value, exc_type, exc_tb)
420439

421-
ut.Failure.__init__ = excstore
440+
ut.Failure.__init__ = store_raw_exception_info
422441
try:
423442
res = yield
424443
finally:

0 commit comments

Comments
 (0)