Skip to content

Commit a9910a4

Browse files
Do not discover properties when iterating fixtures (#12781) (#12788)
Resolves #12446 (cherry picked from commit c6a5290) Co-authored-by: Anthony Sottile <asottile@umich.edu>
1 parent 0f10b6b commit a9910a4

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

changelog/12446.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid calling ``@property`` (and other instance descriptors) during fixture discovery -- by :user:`asottile`

src/_pytest/fixtures.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from _pytest.compat import NOTSET
5454
from _pytest.compat import NotSetType
5555
from _pytest.compat import safe_getattr
56+
from _pytest.compat import safe_isclass
5657
from _pytest.config import _PluggyPlugin
5758
from _pytest.config import Config
5859
from _pytest.config import ExitCode
@@ -1724,17 +1725,26 @@ def parsefactories(
17241725
if holderobj in self._holderobjseen:
17251726
return
17261727

1728+
# Avoid accessing `@property` (and other descriptors) when iterating fixtures.
1729+
if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType):
1730+
holderobj_tp: object = type(holderobj)
1731+
else:
1732+
holderobj_tp = holderobj
1733+
17271734
self._holderobjseen.add(holderobj)
17281735
for name in dir(holderobj):
17291736
# The attribute can be an arbitrary descriptor, so the attribute
1730-
# access below can raise. safe_getatt() ignores such exceptions.
1731-
obj = safe_getattr(holderobj, name, None)
1732-
marker = getfixturemarker(obj)
1737+
# access below can raise. safe_getattr() ignores such exceptions.
1738+
obj_ub = safe_getattr(holderobj_tp, name, None)
1739+
marker = getfixturemarker(obj_ub)
17331740
if not isinstance(marker, FixtureFunctionMarker):
17341741
# Magic globals with __getattr__ might have got us a wrong
17351742
# fixture attribute.
17361743
continue
17371744

1745+
# OK we know it is a fixture -- now safe to look up on the _instance_.
1746+
obj = getattr(holderobj, name)
1747+
17381748
if marker.name:
17391749
name = marker.name
17401750

testing/python/collect.py

+37
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,43 @@ def prop(self):
263263
result = pytester.runpytest()
264264
assert result.ret == ExitCode.NO_TESTS_COLLECTED
265265

266+
def test_does_not_discover_properties(self, pytester: Pytester) -> None:
267+
"""Regression test for #12446."""
268+
pytester.makepyfile(
269+
"""\
270+
class TestCase:
271+
@property
272+
def oops(self):
273+
raise SystemExit('do not call me!')
274+
"""
275+
)
276+
result = pytester.runpytest()
277+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
278+
279+
def test_does_not_discover_instance_descriptors(self, pytester: Pytester) -> None:
280+
"""Regression test for #12446."""
281+
pytester.makepyfile(
282+
"""\
283+
# not `@property`, but it acts like one
284+
# this should cover the case of things like `@cached_property` / etc.
285+
class MyProperty:
286+
def __init__(self, func):
287+
self._func = func
288+
def __get__(self, inst, owner):
289+
if inst is None:
290+
return self
291+
else:
292+
return self._func.__get__(inst, owner)()
293+
294+
class TestCase:
295+
@MyProperty
296+
def oops(self):
297+
raise SystemExit('do not call me!')
298+
"""
299+
)
300+
result = pytester.runpytest()
301+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
302+
266303
def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None:
267304
"""Regression test for #12275 (non-unittest version)."""
268305
pytester.makepyfile(

0 commit comments

Comments
 (0)