|
10 | 10 | import sys
|
11 | 11 | import traceback
|
12 | 12 | import types
|
13 |
| -from typing import Any |
14 | 13 | from typing import TYPE_CHECKING
|
15 | 14 | from typing import Union
|
16 | 15 |
|
@@ -197,12 +196,6 @@ def unittest_setup_method_fixture(
|
197 | 196 | )
|
198 | 197 |
|
199 | 198 |
|
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 |
| - |
206 | 199 | class TestCaseFunction(Function):
|
207 | 200 | nofuncargs = True
|
208 | 201 | _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None
|
@@ -234,16 +227,7 @@ def startTest(self, testcase: unittest.TestCase) -> None:
|
234 | 227 | pass
|
235 | 228 |
|
236 | 229 | def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
|
237 |
| - # Unwrap potential exception info (see twisted trial support below). |
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 |
| 230 | + rawexcinfo = _handle_twisted_exc_info(rawexcinfo) |
247 | 231 | try:
|
248 | 232 | excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(
|
249 | 233 | rawexcinfo # type: ignore[arg-type]
|
@@ -399,54 +383,42 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
399 | 383 | call.excinfo = call2.excinfo
|
400 | 384 |
|
401 | 385 |
|
402 |
| -# Twisted trial support. |
403 |
| -classImplements_has_run = False |
| 386 | +def pytest_configure() -> None: |
| 387 | + """Register the TestCaseFunction class as an IReporter if twisted.trial is available.""" |
| 388 | + if _is_twisted_trial_available(): |
| 389 | + from twisted.trial.itrial import IReporter |
| 390 | + from zope.interface import classImplements |
404 | 391 |
|
| 392 | + classImplements(TestCaseFunction, IReporter) |
405 | 393 |
|
406 |
| -@hookimpl(wrapper=True) |
407 |
| -def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: |
408 |
| - if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: |
409 |
| - ut: Any = sys.modules["twisted.python.failure"] |
410 |
| - global classImplements_has_run |
411 |
| - if not classImplements_has_run: |
412 |
| - from twisted.trial.itrial import IReporter |
413 |
| - from zope.interface import classImplements |
414 | 394 |
|
415 |
| - classImplements(TestCaseFunction, IReporter) |
416 |
| - classImplements_has_run = True |
| 395 | +def _is_skipped(obj) -> bool: |
| 396 | + """Return True if the given object has been marked with @unittest.skip.""" |
| 397 | + return bool(getattr(obj, "__unittest_skip__", False)) |
417 | 398 |
|
418 |
| - # Monkeypatch `Failure.__init__` to store the raw exception info. |
419 |
| - Failure__init__ = ut.Failure.__init__ |
420 | 399 |
|
421 |
| - def store_raw_exception_info( |
422 |
| - self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None |
423 |
| - ): |
424 |
| - if exc_value is None: |
425 |
| - raw_exc_info = sys.exc_info() |
426 |
| - else: |
427 |
| - if exc_type is None: |
428 |
| - exc_type = type(exc_value) |
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)) |
433 |
| - try: |
434 |
| - Failure__init__( |
435 |
| - self, exc_value, exc_type, exc_tb, captureVars=captureVars |
436 |
| - ) |
437 |
| - except TypeError: |
438 |
| - Failure__init__(self, exc_value, exc_type, exc_tb) |
| 400 | +def _is_twisted_trial_available() -> bool: |
| 401 | + return "twisted.trial.unittest" in sys.modules |
439 | 402 |
|
440 |
| - ut.Failure.__init__ = store_raw_exception_info |
441 |
| - try: |
442 |
| - res = yield |
443 |
| - finally: |
444 |
| - ut.Failure.__init__ = Failure__init__ |
445 |
| - else: |
446 |
| - res = yield |
447 |
| - return res |
448 | 403 |
|
| 404 | +def _handle_twisted_exc_info( |
| 405 | + rawexcinfo: _SysExcInfoType | BaseException, |
| 406 | +) -> _SysExcInfoType: |
| 407 | + """ |
| 408 | + Twisted passes a custom Failure instance to `addError()` instead of using `sys.exc_info()`. |
| 409 | + Therefore, if `rawexcinfo` is a `Failure` instance, convert it into the equivalent `sys.exc_info()` tuple |
| 410 | + as expected by pytest. |
| 411 | + """ |
| 412 | + if isinstance(rawexcinfo, BaseException) and _is_twisted_trial_available(): |
| 413 | + import twisted.python.failure |
449 | 414 |
|
450 |
| -def _is_skipped(obj) -> bool: |
451 |
| - """Return True if the given object has been marked with @unittest.skip.""" |
452 |
| - return bool(getattr(obj, "__unittest_skip__", False)) |
| 415 | + if isinstance(rawexcinfo, twisted.python.failure.Failure): |
| 416 | + tb = rawexcinfo.__traceback__ |
| 417 | + if tb is None: |
| 418 | + tb = sys.exc_info()[2] |
| 419 | + return type(rawexcinfo.value), rawexcinfo.value, tb |
| 420 | + |
| 421 | + # Unfortunately, because we cannot import `twisted.python.failure` at the top of the file |
| 422 | + # and use it in the signature, we need to use `type:ignore` here because we cannot narrow |
| 423 | + # the type properly in the `if` statement above. |
| 424 | + return rawexcinfo # type:ignore[return-value] |
0 commit comments