Skip to content

python,unittest: don't collect abstract classes #12318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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

Check warning on line 372 in src/_pytest/python.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/python.py#L372

Added line #L372 was not covered by tests
if inspect.isabstract(obj):
return False

Check warning on line 374 in src/_pytest/python.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/python.py#L374

Added line #L374 was not covered by tests
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?
try:
# Has unittest been imported?
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

Check warning on line 65 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L65

Added line #L65 was not covered by tests
# 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)
Loading