Skip to content

Commit 08c6d68

Browse files
committed
python,unittest: don't collect abstract classes
Fix pytest-dev#12275.
1 parent eea04c2 commit 08c6d68

File tree

5 files changed

+67
-2
lines changed

5 files changed

+67
-2
lines changed

changelog/12275.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix collection error upon encountering an :mod:`abstract <abc>` class, including abstract `unittest.TestCase` subclasses.

src/_pytest/python.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,11 @@ def istestfunction(self, obj: object, name: str) -> bool:
368368
return False
369369

370370
def istestclass(self, obj: object, name: str) -> bool:
371-
return self.classnamefilter(name) or self.isnosetest(obj)
371+
if not (self.classnamefilter(name) or self.isnosetest(obj)):
372+
return False
373+
if inspect.isabstract(obj):
374+
return False
375+
return True
372376

373377
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
374378
"""Check if the given name matches the prefix or glob-pattern defined

src/_pytest/unittest.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# mypy: allow-untyped-defs
22
"""Discover and run std-library "unittest" style tests."""
33

4+
import inspect
45
import sys
56
import traceback
67
import types
@@ -49,14 +50,19 @@
4950
def pytest_pycollect_makeitem(
5051
collector: Union[Module, Class], name: str, obj: object
5152
) -> Optional["UnitTestCase"]:
52-
# Has unittest been imported and is obj a subclass of its TestCase?
53+
# Has unittest been imported?
5354
try:
5455
ut = sys.modules["unittest"]
56+
# Is obj a subclass of unittest.TestCase?
5557
# Type ignored because `ut` is an opaque module.
5658
if not issubclass(obj, ut.TestCase): # type: ignore
5759
return None
5860
except Exception:
5961
return None
62+
# Is obj a concrete class?
63+
# Abstract classes can't be instantiated so no point collecting them.
64+
if inspect.isabstract(obj):
65+
return None
6066
# Yes, so let's collect it.
6167
return UnitTestCase.from_parent(collector, name=name, obj=obj)
6268

testing/python/collect.py

+26
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,32 @@ def prop(self):
262262
result = pytester.runpytest()
263263
assert result.ret == ExitCode.NO_TESTS_COLLECTED
264264

265+
def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None:
266+
"""Regression test for #12275 (non-unittest version)."""
267+
pytester.makepyfile(
268+
"""
269+
import abc
270+
271+
class TestBase(abc.ABC):
272+
@abc.abstractmethod
273+
def abstract1(self): pass
274+
275+
@abc.abstractmethod
276+
def abstract2(self): pass
277+
278+
def test_it(self): pass
279+
280+
class TestPartial(TestBase):
281+
def abstract1(self): pass
282+
283+
class TestConcrete(TestPartial):
284+
def abstract2(self): pass
285+
"""
286+
)
287+
result = pytester.runpytest()
288+
assert result.ret == ExitCode.OK
289+
result.assert_outcomes(passed=1)
290+
265291

266292
class TestFunction:
267293
def test_getmodulecollector(self, pytester: Pytester) -> None:

testing/test_unittest.py

+28
Original file line numberDiff line numberDiff line change
@@ -1640,3 +1640,31 @@ def test_it2(self): pass
16401640
assert skipped == 1
16411641
assert failed == 0
16421642
assert reprec.ret == ExitCode.NO_TESTS_COLLECTED
1643+
1644+
1645+
def test_abstract_testcase_is_not_collected(pytester: Pytester) -> None:
1646+
"""Regression test for #12275."""
1647+
pytester.makepyfile(
1648+
"""
1649+
import abc
1650+
import unittest
1651+
1652+
class TestBase(unittest.TestCase, abc.ABC):
1653+
@abc.abstractmethod
1654+
def abstract1(self): pass
1655+
1656+
@abc.abstractmethod
1657+
def abstract2(self): pass
1658+
1659+
def test_it(self): pass
1660+
1661+
class TestPartial(TestBase):
1662+
def abstract1(self): pass
1663+
1664+
class TestConcrete(TestPartial):
1665+
def abstract2(self): pass
1666+
"""
1667+
)
1668+
result = pytester.runpytest()
1669+
assert result.ret == ExitCode.OK
1670+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)