|
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