Skip to content

Commit c9e9a59

Browse files
authored
Merge pull request #8241 from bluetech/skip-improvements
Minor code improvements in nose, unittest, skipping
2 parents 42d5545 + 7f98920 commit c9e9a59

File tree

6 files changed

+46
-53
lines changed

6 files changed

+46
-53
lines changed

src/_pytest/fixtures.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -543,10 +543,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
543543
self._addfinalizer(finalizer, scope=self.scope)
544544

545545
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
546-
colitem = self._getscopeitem(scope)
547-
self._pyfuncitem.session._setupstate.addfinalizer(
548-
finalizer=finalizer, colitem=colitem
549-
)
546+
item = self._getscopeitem(scope)
547+
item.addfinalizer(finalizer)
550548

551549
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
552550
"""Apply a marker to a single test function invocation.
@@ -694,9 +692,7 @@ def _schedule_finalizers(
694692
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
695693
) -> None:
696694
# If fixture function failed it might have registered finalizers.
697-
self.session._setupstate.addfinalizer(
698-
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
699-
)
695+
subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest))
700696

701697
def _check_scope(
702698
self,

src/_pytest/nose.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
"""Run testsuites written for nose."""
2-
from _pytest import python
3-
from _pytest import unittest
42
from _pytest.config import hookimpl
53
from _pytest.fixtures import getfixturemarker
64
from _pytest.nodes import Item
5+
from _pytest.python import Function
6+
from _pytest.unittest import TestCaseFunction
77

88

99
@hookimpl(trylast=True)
10-
def pytest_runtest_setup(item) -> None:
11-
if is_potential_nosetest(item):
12-
if not call_optional(item.obj, "setup"):
13-
# Call module level setup if there is no object level one.
14-
call_optional(item.parent.obj, "setup")
15-
# XXX This implies we only call teardown when setup worked.
16-
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
17-
18-
19-
def teardown_nose(item) -> None:
20-
if is_potential_nosetest(item):
21-
if not call_optional(item.obj, "teardown"):
22-
call_optional(item.parent.obj, "teardown")
23-
24-
25-
def is_potential_nosetest(item: Item) -> bool:
26-
# Extra check needed since we do not do nose style setup/teardown
27-
# on direct unittest style classes.
28-
return isinstance(item, python.Function) and not isinstance(
29-
item, unittest.TestCaseFunction
30-
)
10+
def pytest_runtest_setup(item: Item) -> None:
11+
if not isinstance(item, Function):
12+
return
13+
# Don't do nose style setup/teardown on direct unittest style classes.
14+
if isinstance(item, TestCaseFunction):
15+
return
16+
17+
# Capture the narrowed type of item for the teardown closure,
18+
# see https://github.com/python/mypy/issues/2608
19+
func = item
20+
21+
if not call_optional(func.obj, "setup"):
22+
# Call module level setup if there is no object level one.
23+
assert func.parent is not None
24+
call_optional(func.parent.obj, "setup") # type: ignore[attr-defined]
25+
26+
def teardown_nose() -> None:
27+
if not call_optional(func.obj, "teardown"):
28+
assert func.parent is not None
29+
call_optional(func.parent.obj, "teardown") # type: ignore[attr-defined]
30+
31+
# XXX This implies we only call teardown when setup worked.
32+
func.addfinalizer(teardown_nose)
3133

3234

3335
def call_optional(obj: object, name: str) -> bool:

src/_pytest/outcomes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ def __init__(
5858
msg: Optional[str] = None,
5959
pytrace: bool = True,
6060
allow_module_level: bool = False,
61+
*,
62+
_use_item_location: bool = False,
6163
) -> None:
6264
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
6365
self.allow_module_level = allow_module_level
66+
# If true, the skip location is reported as the item's location,
67+
# instead of the place that raises the exception/calls skip().
68+
self._use_item_location = _use_item_location
6469

6570

6671
class Failed(OutcomeException):

src/_pytest/reports.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,12 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
324324
elif isinstance(excinfo.value, skip.Exception):
325325
outcome = "skipped"
326326
r = excinfo._getreprcrash()
327-
longrepr = (str(r.path), r.lineno, r.message)
327+
if excinfo.value._use_item_location:
328+
filename, line = item.reportinfo()[:2]
329+
assert line is not None
330+
longrepr = str(filename), line + 1, r.message
331+
else:
332+
longrepr = (str(r.path), r.lineno, r.message)
328333
else:
329334
outcome = "failed"
330335
if call.when == "call":

src/_pytest/skipping.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -230,18 +230,15 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
230230
return None
231231

232232

233-
# Whether skipped due to skip or skipif marks.
234-
skipped_by_mark_key = StoreKey[bool]()
235233
# Saves the xfail mark evaluation. Can be refreshed during call if None.
236234
xfailed_key = StoreKey[Optional[Xfail]]()
237235

238236

239237
@hookimpl(tryfirst=True)
240238
def pytest_runtest_setup(item: Item) -> None:
241239
skipped = evaluate_skip_marks(item)
242-
item._store[skipped_by_mark_key] = skipped is not None
243240
if skipped:
244-
skip(skipped.reason)
241+
raise skip.Exception(skipped.reason, _use_item_location=True)
245242

246243
item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item)
247244
if xfailed and not item.config.option.runxfail and not xfailed.run:
@@ -292,19 +289,6 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
292289
rep.outcome = "passed"
293290
rep.wasxfail = xfailed.reason
294291

295-
if (
296-
item._store.get(skipped_by_mark_key, True)
297-
and rep.skipped
298-
and type(rep.longrepr) is tuple
299-
):
300-
# Skipped by mark.skipif; change the location of the failure
301-
# to point to the item definition, otherwise it will display
302-
# the location of where the skip exception was raised within pytest.
303-
_, _, reason = rep.longrepr
304-
filename, line = item.reportinfo()[:2]
305-
assert line is not None
306-
rep.longrepr = str(filename), line + 1, reason
307-
308292

309293
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
310294
if hasattr(report, "wasxfail"):

src/_pytest/unittest.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from _pytest.python import Function
3030
from _pytest.python import PyCollector
3131
from _pytest.runner import CallInfo
32-
from _pytest.skipping import skipped_by_mark_key
3332

3433
if TYPE_CHECKING:
3534
import unittest
@@ -150,7 +149,7 @@ def cleanup(*args):
150149
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
151150
if _is_skipped(self):
152151
reason = self.__unittest_skip_why__
153-
pytest.skip(reason)
152+
raise pytest.skip.Exception(reason, _use_item_location=True)
154153
if setup is not None:
155154
try:
156155
if pass_self:
@@ -256,9 +255,8 @@ def addFailure(
256255

257256
def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
258257
try:
259-
skip(reason)
258+
raise pytest.skip.Exception(reason, _use_item_location=True)
260259
except skip.Exception:
261-
self._store[skipped_by_mark_key] = True
262260
self._addexcinfo(sys.exc_info())
263261

264262
def addExpectedFailure(
@@ -343,14 +341,17 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
343341
except AttributeError:
344342
pass
345343

344+
# Convert unittest.SkipTest to pytest.skip.
345+
# This is actually only needed for nose, which reuses unittest.SkipTest for
346+
# its own nose.SkipTest. For unittest TestCases, SkipTest is already
347+
# handled internally, and doesn't reach here.
346348
unittest = sys.modules.get("unittest")
347349
if (
348350
unittest
349351
and call.excinfo
350352
and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
351353
):
352354
excinfo = call.excinfo
353-
# Let's substitute the excinfo with a pytest.skip one.
354355
call2 = CallInfo[None].from_call(
355356
lambda: pytest.skip(str(excinfo.value)), call.when
356357
)

0 commit comments

Comments
 (0)