Skip to content

Commit

Permalink
python,unittest: don't collect abstract classes
Browse files Browse the repository at this point in the history
  • Loading branch information
bluetech committed May 13, 2024
1 parent eea04c2 commit 08c6d68
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog/12275.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix collection error upon encountering an :mod:`abstract <abc>` class, including abstract `unittest.TestCase` subclasses.
6 changes: 5 additions & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,11 @@ def istestfunction(self, obj: object, name: str) -> bool:
return False

def istestclass(self, obj: object, name: str) -> bool:
return self.classnamefilter(name) or self.isnosetest(obj)
if not (self.classnamefilter(name) or self.isnosetest(obj)):
return False
if inspect.isabstract(obj):
return False
return True

def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
"""Check if the given name matches the prefix or glob-pattern defined
Expand Down
8 changes: 7 additions & 1 deletion src/_pytest/unittest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
"""Discover and run std-library "unittest" style tests."""

import inspect
import sys
import traceback
import types
Expand Down Expand Up @@ -49,14 +50,19 @@
def pytest_pycollect_makeitem(
collector: Union[Module, Class], name: str, obj: object
) -> Optional["UnitTestCase"]:
# Has unittest been imported and is obj a subclass of its TestCase?
# Has unittest been imported?
try:
ut = sys.modules["unittest"]
# Is obj a subclass of unittest.TestCase?
# Type ignored because `ut` is an opaque module.
if not issubclass(obj, ut.TestCase): # type: ignore
return None
except Exception:
return None
# Is obj a concrete class?
# Abstract classes can't be instantiated so no point collecting them.
if inspect.isabstract(obj):
return None
# Yes, so let's collect it.
return UnitTestCase.from_parent(collector, name=name, obj=obj)

Expand Down
26 changes: 26 additions & 0 deletions testing/python/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,32 @@ def prop(self):
result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED

def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None:
"""Regression test for #12275 (non-unittest version)."""
pytester.makepyfile(
"""
import abc
class TestBase(abc.ABC):
@abc.abstractmethod
def abstract1(self): pass
@abc.abstractmethod
def abstract2(self): pass
def test_it(self): pass
class TestPartial(TestBase):
def abstract1(self): pass
class TestConcrete(TestPartial):
def abstract2(self): pass
"""
)
result = pytester.runpytest()
assert result.ret == ExitCode.OK
result.assert_outcomes(passed=1)


class TestFunction:
def test_getmodulecollector(self, pytester: Pytester) -> None:
Expand Down
28 changes: 28 additions & 0 deletions testing/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,3 +1640,31 @@ def test_it2(self): pass
assert skipped == 1
assert failed == 0
assert reprec.ret == ExitCode.NO_TESTS_COLLECTED


def test_abstract_testcase_is_not_collected(pytester: Pytester) -> None:
"""Regression test for #12275."""
pytester.makepyfile(
"""
import abc
import unittest
class TestBase(unittest.TestCase, abc.ABC):
@abc.abstractmethod
def abstract1(self): pass
@abc.abstractmethod
def abstract2(self): pass
def test_it(self): pass
class TestPartial(TestBase):
def abstract1(self): pass
class TestConcrete(TestPartial):
def abstract2(self): pass
"""
)
result = pytester.runpytest()
assert result.ret == ExitCode.OK
result.assert_outcomes(passed=1)

0 comments on commit 08c6d68

Please sign in to comment.