https://github.com/python/cpython/commit/5ad3c6dfbfe60a7f232e9604866c77ced24c4bfe
commit: 5ad3c6dfbfe60a7f232e9604866c77ced24c4bfe
branch: main
author: blhsing <[email protected]>
committer: nedbat <[email protected]>
date: 2026-06-17T07:48:09-04:00
summary:
gh-120665: make unittest loaders avoid loading test cases that are abstract
base classes (#120666)
files:
A Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst
M Lib/test/test_unittest/test_loader.py
M Lib/unittest/loader.py
diff --git a/Lib/test/test_unittest/test_loader.py
b/Lib/test/test_unittest/test_loader.py
index cdff6d1a20c8dfd..f4e50a3dccb2bc2 100644
--- a/Lib/test/test_unittest/test_loader.py
+++ b/Lib/test/test_unittest/test_loader.py
@@ -1,3 +1,4 @@
+import abc
import functools
import sys
import types
@@ -98,6 +99,22 @@ def test_loadTestsFromTestCase__from_FunctionTestCase(self):
self.assertIsInstance(suite, loader.suiteClass)
self.assertEqual(list(suite), [])
+ # "Do not load any tests from a TestCase-derived class that is an abstract
+ # base class."
+ def test_loadTestsFromTestCase__from_abc_TestCase(self):
+ class FooBase(unittest.TestCase, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test(self): ...
+ class Foo(FooBase):
+ def test(self): pass
+
+ empty_suite = unittest.TestSuite()
+
+ loader = unittest.TestLoader()
+ suite = loader.loadTestsFromTestCase(Foo)
+ self.assertEqual(loader.loadTestsFromTestCase(FooBase), empty_suite)
+ self.assertEqual(list(suite), [Foo('test')])
+
################################################################
### /Tests for TestLoader.loadTestsFromTestCase
@@ -252,6 +269,24 @@ def load_tests(loader, tests, pattern):
self.assertRaisesRegex(TypeError, "some failure", test.m)
+ # Check that loadTestsFromModule skips abstract base classes derived from
+ # TestCase, which can't be instantiated.
+ def test_loadTestsFromModule__skip_abc_TestCase(self):
+ m = types.ModuleType('m')
+ class MyTestCaseBase(unittest.TestCase, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test(self):
+ ...
+ class MyTestCase(MyTestCaseBase):
+ def test(self):
+ pass
+ m.testcase_1 = MyTestCaseBase
+ m.testcase_2 = MyTestCase
+ loader = unittest.TestLoader()
+ suite = loader.loadTestsFromModule(m)
+ expected = [loader.suiteClass([MyTestCase('test')])]
+ self.assertEqual(list(suite), expected)
+
################################################################
### /Tests for TestLoader.loadTestsFromModule()
@@ -1052,6 +1087,22 @@ def test_loadTestsFromNames__module_not_loaded(self):
if module_name in sys.modules:
del sys.modules[module_name]
+ # "The specifier should not refer to a test method in a TestCase-derived
+ # subclass that is an abstract base class"
+ def test_loadTestsFromNames__testmethod_in_abc_TestCase(self):
+ m = types.ModuleType('m')
+ class Foo(unittest.TestCase, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test_1(self): ...
+ def test_2(self): pass
+ m.Foo = Foo
+
+ loader = unittest.TestLoader()
+ for name in 'Foo.test_1', 'Foo.test_2':
+ with self.subTest(name=name), self.assertRaisesRegex(TypeError,
+ "Cannot instantiate abstract test case Foo"):
+ loader.loadTestsFromNames([name], m)
+
################################################################
### /Tests for TestLoader.loadTestsFromNames()
diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py
index a52950dad224ee7..697520246f0e3c6 100644
--- a/Lib/unittest/loader.py
+++ b/Lib/unittest/loader.py
@@ -1,5 +1,6 @@
"""Loading unittests."""
+import inspect
import os
import re
import sys
@@ -84,8 +85,10 @@ def loadTestsFromTestCase(self, testCaseClass):
raise TypeError("Test cases should not be derived from "
"TestSuite. Maybe you meant to derive from "
"TestCase?")
- if testCaseClass in (case.TestCase, case.FunctionTestCase):
- # We don't load any tests from base types that should not be
loaded.
+ if (testCaseClass in (case.TestCase, case.FunctionTestCase) or
+ inspect.isabstract(testCaseClass)):
+ # We don't load any tests from base types that should not be
loaded,
+ # and abstract base classes that can't be instantiated
testCaseNames = []
else:
testCaseNames = self.getTestCaseNames(testCaseClass)
@@ -103,6 +106,7 @@ def loadTestsFromModule(self, module, *, pattern=None):
isinstance(obj, type)
and issubclass(obj, case.TestCase)
and obj not in (case.TestCase, case.FunctionTestCase)
+ and not inspect.isabstract(obj)
):
tests.append(self.loadTestsFromTestCase(obj))
@@ -181,6 +185,9 @@ def loadTestsFromName(self, name, module=None):
elif (isinstance(obj, types.FunctionType) and
isinstance(parent, type) and
issubclass(parent, case.TestCase)):
+ if inspect.isabstract(parent):
+ raise TypeError(
+ "Cannot instantiate abstract test case %s" %
parent.__name__)
name = parts[-1]
inst = parent(name)
# static methods follow a different path
diff --git
a/Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst
b/Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst
new file mode 100644
index 000000000000000..27e93988ed11efb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-18-04-08-37.gh-issue-120665.x7T1hV.rst
@@ -0,0 +1 @@
+Fixed an issue where ``unittest`` loaders would load and instantiate
:class:`unittest.TestCase`-derived subclasses that are also abstract base
classes, which can't be instantiated.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]