summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2025-01-14 08:02:38 (GMT)
committerGitHub <noreply@github.com>2025-01-14 08:02:38 (GMT)
commit06cad77a5b345adde88609be9c3c470c5cd9f417 (patch)
tree3c66069a2a22b4bffe221c3db5da638faad45ee9
parent41f73501eca2ff8b42fa4811d918a81c052a758b (diff)
downloadcpython-06cad77a5b345adde88609be9c3c470c5cd9f417.zip
cpython-06cad77a5b345adde88609be9c3c470c5cd9f417.tar.gz
cpython-06cad77a5b345adde88609be9c3c470c5cd9f417.tar.bz2
gh-71339: Add additional assertion methods for unittest (GH-128707)
Add the following methods: * assertHasAttr() and assertNotHasAttr() * assertIsSubclass() and assertNotIsSubclass() * assertStartsWith() and assertNotStartsWith() * assertEndsWith() and assertNotEndsWith() Also improve error messages for assertIsInstance() and assertNotIsInstance().
-rw-r--r--Doc/library/unittest.rst61
-rw-r--r--Doc/whatsnew/3.14.rst17
-rw-r--r--Lib/test/test_descr.py8
-rw-r--r--Lib/test/test_gdb/util.py5
-rw-r--r--Lib/test/test_importlib/resources/test_functional.py6
-rw-r--r--Lib/test/test_pyclbr.py10
-rw-r--r--Lib/test/test_typing.py18
-rw-r--r--Lib/test/test_unittest/test_case.py307
-rw-r--r--Lib/test/test_unittest/test_loader.py2
-rw-r--r--Lib/test/test_unittest/test_program.py10
-rw-r--r--Lib/test/test_unittest/test_result.py2
-rw-r--r--Lib/test/test_unittest/testmock/testasync.py14
-rw-r--r--Lib/test/test_unittest/testmock/testcallable.py14
-rw-r--r--Lib/test/test_unittest/testmock/testhelpers.py2
-rw-r--r--Lib/test/test_unittest/testmock/testmagicmethods.py12
-rw-r--r--Lib/test/test_unittest/testmock/testmock.py16
-rw-r--r--Lib/test/test_unittest/testmock/testpatch.py6
-rw-r--r--Lib/test/test_venv.py4
-rw-r--r--Lib/unittest/case.py132
-rw-r--r--Misc/NEWS.d/next/Library/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst9
20 files changed, 555 insertions, 100 deletions
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 7f8b710..0eead59 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -883,6 +883,12 @@ Test cases
| :meth:`assertNotIsInstance(a, b) | ``not isinstance(a, b)`` | 3.2 |
| <TestCase.assertNotIsInstance>` | | |
+-----------------------------------------+-----------------------------+---------------+
+ | :meth:`assertIsSubclass(a, b) | ``issubclass(a, b)`` | 3.14 |
+ | <TestCase.assertIsSubclass>` | | |
+ +-----------------------------------------+-----------------------------+---------------+
+ | :meth:`assertNotIsSubclass(a, b) | ``not issubclass(a, b)`` | 3.14 |
+ | <TestCase.assertNotIsSubclass>` | | |
+ +-----------------------------------------+-----------------------------+---------------+
All the assert methods accept a *msg* argument that, if specified, is used
as the error message on failure (see also :data:`longMessage`).
@@ -961,6 +967,15 @@ Test cases
.. versionadded:: 3.2
+ .. method:: assertIsSubclass(cls, superclass, msg=None)
+ assertNotIsSubclass(cls, superclass, msg=None)
+
+ Test that *cls* is (or is not) a subclass of *superclass* (which can be a
+ class or a tuple of classes, as supported by :func:`issubclass`).
+ To check for the exact type, use :func:`assertIs(cls, superclass) <assertIs>`.
+
+ .. versionadded:: next
+
It is also possible to check the production of exceptions, warnings, and
log messages using the following methods:
@@ -1210,6 +1225,24 @@ Test cases
| <TestCase.assertCountEqual>` | elements in the same number, | |
| | regardless of their order. | |
+---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertStartsWith(a, b) | ``a.startswith(b)`` | 3.14 |
+ | <TestCase.assertStartsWith>` | | |
+ +---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertNotStartsWith(a, b) | ``not a.startswith(b)`` | 3.14 |
+ | <TestCase.assertNotStartsWith>` | | |
+ +---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertEndsWith(a, b) | ``a.endswith(b)`` | 3.14 |
+ | <TestCase.assertEndsWith>` | | |
+ +---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertNotEndsWith(a, b) | ``not a.endswith(b)`` | 3.14 |
+ | <TestCase.assertNotEndsWith>` | | |
+ +---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertHasAttr(a, b) | ``hastattr(a, b)`` | 3.14 |
+ | <TestCase.assertHasAttr>` | | |
+ +---------------------------------------+--------------------------------+--------------+
+ | :meth:`assertNotHasAttr(a, b) | ``not hastattr(a, b)`` | 3.14 |
+ | <TestCase.assertNotHasAttr>` | | |
+ +---------------------------------------+--------------------------------+--------------+
.. method:: assertAlmostEqual(first, second, places=7, msg=None, delta=None)
@@ -1279,6 +1312,34 @@ Test cases
.. versionadded:: 3.2
+ .. method:: assertStartsWith(s, prefix, msg=None)
+ .. method:: assertNotStartsWith(s, prefix, msg=None)
+
+ Test that the Unicode or byte string *s* starts (or does not start)
+ with a *prefix*.
+ *prefix* can also be a tuple of strings to try.
+
+ .. versionadded:: next
+
+
+ .. method:: assertEndsWith(s, suffix, msg=None)
+ .. method:: assertNotEndsWith(s, suffix, msg=None)
+
+ Test that the Unicode or byte string *s* ends (or does not end)
+ with a *suffix*.
+ *suffix* can also be a tuple of strings to try.
+
+ .. versionadded:: next
+
+
+ .. method:: assertHasAttr(obj, name, msg=None)
+ .. method:: assertNotHasAttr(obj, name, msg=None)
+
+ Test that the object *obj* has (or has not) an attribute *name*.
+
+ .. versionadded:: next
+
+
.. _type-specific-methods:
The :meth:`assertEqual` method dispatches the equality check for objects of
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 1dbd871..eedcc62 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -670,6 +670,23 @@ unittest
directory again. It was removed in Python 3.11.
(Contributed by Jacob Walls in :gh:`80958`.)
+* A number of new methods were added in the :class:`~unittest.TestCase` class
+ that provide more specialized tests.
+
+ - :meth:`~unittest.TestCase.assertHasAttr` and
+ :meth:`~unittest.TestCase.assertNotHasAttr` check whether the object
+ has a particular attribute.
+ - :meth:`~unittest.TestCase.assertIsSubclass` and
+ :meth:`~unittest.TestCase.assertNotIsSubclass` check whether the object
+ is a subclass of a particular class, or of one of a tuple of classes.
+ - :meth:`~unittest.TestCase.assertStartsWith`,
+ :meth:`~unittest.TestCase.assertNotStartsWith`,
+ :meth:`~unittest.TestCase.assertEndsWith` and
+ :meth:`~unittest.TestCase.assertNotEndsWith` check whether the Unicode
+ or byte string starts or ends with particular string(s).
+
+ (Contributed by Serhiy Storchaka in :gh:`71339`.)
+
urllib
------
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 168b78a..51f97bb 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -405,14 +405,6 @@ class OperatorsTest(unittest.TestCase):
class ClassPropertiesAndMethods(unittest.TestCase):
- def assertHasAttr(self, obj, name):
- self.assertTrue(hasattr(obj, name),
- '%r has no attribute %r' % (obj, name))
-
- def assertNotHasAttr(self, obj, name):
- self.assertFalse(hasattr(obj, name),
- '%r has unexpected attribute %r' % (obj, name))
-
def test_python_dicts(self):
# Testing Python subclass of dict...
self.assertTrue(issubclass(dict, dict))
diff --git a/Lib/test/test_gdb/util.py b/Lib/test/test_gdb/util.py
index 8fe9cfc..8097fd5 100644
--- a/Lib/test/test_gdb/util.py
+++ b/Lib/test/test_gdb/util.py
@@ -280,11 +280,6 @@ class DebuggerTests(unittest.TestCase):
return out
- def assertEndsWith(self, actual, exp_end):
- '''Ensure that the given "actual" string ends with "exp_end"'''
- self.assertTrue(actual.endswith(exp_end),
- msg='%r did not end with %r' % (actual, exp_end))
-
def assertMultilineMatches(self, actual, pattern):
m = re.match(pattern, actual, re.DOTALL)
if not m:
diff --git a/Lib/test/test_importlib/resources/test_functional.py b/Lib/test/test_importlib/resources/test_functional.py
index 4317abf..e8d25fa 100644
--- a/Lib/test/test_importlib/resources/test_functional.py
+++ b/Lib/test/test_importlib/resources/test_functional.py
@@ -43,12 +43,6 @@ class FunctionalAPIBase(util.DiskSetup):
with self.subTest(path_parts=path_parts):
yield path_parts
- def assertEndsWith(self, string, suffix):
- """Assert that `string` ends with `suffix`.
-
- Used to ignore an architecture-specific UTF-16 byte-order mark."""
- self.assertEqual(string[-len(suffix) :], suffix)
-
def test_read_text(self):
self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'),
diff --git a/Lib/test/test_pyclbr.py b/Lib/test/test_pyclbr.py
index 4bf0576..25b313f 100644
--- a/Lib/test/test_pyclbr.py
+++ b/Lib/test/test_pyclbr.py
@@ -31,14 +31,6 @@ class PyclbrTest(TestCase):
print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
self.fail("%r missing" % missing.pop())
- def assertHasattr(self, obj, attr, ignore):
- ''' succeed iff hasattr(obj,attr) or attr in ignore. '''
- if attr in ignore: return
- if not hasattr(obj, attr): print("???", attr)
- self.assertTrue(hasattr(obj, attr),
- 'expected hasattr(%r, %r)' % (obj, attr))
-
-
def assertHaskey(self, obj, key, ignore):
''' succeed iff key in obj or key in ignore. '''
if key in ignore: return
@@ -86,7 +78,7 @@ class PyclbrTest(TestCase):
for name, value in dict.items():
if name in ignore:
continue
- self.assertHasattr(module, name, ignore)
+ self.assertHasAttr(module, name, ignore)
py_item = getattr(module, name)
if isinstance(value, pyclbr.Function):
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index c51ee76..c98e6f8 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -59,20 +59,6 @@ CANNOT_SUBCLASS_INSTANCE = 'Cannot subclass an instance of %s'
class BaseTestCase(TestCase):
- def assertIsSubclass(self, cls, class_or_tuple, msg=None):
- if not issubclass(cls, class_or_tuple):
- message = '%r is not a subclass of %r' % (cls, class_or_tuple)
- if msg is not None:
- message += ' : %s' % msg
- raise self.failureException(message)
-
- def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
- if issubclass(cls, class_or_tuple):
- message = '%r is a subclass of %r' % (cls, class_or_tuple)
- if msg is not None:
- message += ' : %s' % msg
- raise self.failureException(message)
-
def clear_caches(self):
for f in typing._cleanups:
f()
@@ -1252,10 +1238,6 @@ class UnpackTests(BaseTestCase):
class TypeVarTupleTests(BaseTestCase):
- def assertEndsWith(self, string, tail):
- if not string.endswith(tail):
- self.fail(f"String {string!r} does not end with {tail!r}")
-
def test_name(self):
Ts = TypeVarTuple('Ts')
self.assertEqual(Ts.__name__, 'Ts')
diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py
index 621f826..cd36649 100644
--- a/Lib/test/test_unittest/test_case.py
+++ b/Lib/test/test_unittest/test_case.py
@@ -10,6 +10,7 @@ import weakref
import inspect
import types
+from collections import UserString
from copy import deepcopy
from test import support
@@ -54,6 +55,10 @@ class Test(object):
self.events.append('tearDown')
+class List(list):
+ pass
+
+
class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
### Set up attributes used by inherited tests
@@ -85,7 +90,7 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
def runTest(self): raise MyException()
def test(self): pass
- self.assertEqual(Test().id()[-13:], '.Test.runTest')
+ self.assertEndsWith(Test().id(), '.Test.runTest')
# test that TestCase can be instantiated with no args
# primarily for use at the interactive interpreter
@@ -106,7 +111,7 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
def runTest(self): raise MyException()
def test(self): pass
- self.assertEqual(Test('test').id()[-10:], '.Test.test')
+ self.assertEndsWith(Test('test').id(), '.Test.test')
# "class TestCase([methodName])"
# ...
@@ -700,16 +705,120 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
self.assertRaises(self.failureException, self.assertIsNot, thing, thing)
def testAssertIsInstance(self):
- thing = []
+ thing = List()
self.assertIsInstance(thing, list)
- self.assertRaises(self.failureException, self.assertIsInstance,
- thing, dict)
+ self.assertIsInstance(thing, (int, list))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsInstance(thing, int)
+ self.assertEqual(str(cm.exception),
+ "[] is not an instance of <class 'int'>")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsInstance(thing, (int, float))
+ self.assertEqual(str(cm.exception),
+ "[] is not an instance of any of (<class 'int'>, <class 'float'>)")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsInstance(thing, int, 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsInstance(thing, int, msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotIsInstance(self):
- thing = []
- self.assertNotIsInstance(thing, dict)
- self.assertRaises(self.failureException, self.assertNotIsInstance,
- thing, list)
+ thing = List()
+ self.assertNotIsInstance(thing, int)
+ self.assertNotIsInstance(thing, (int, float))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsInstance(thing, list)
+ self.assertEqual(str(cm.exception),
+ "[] is an instance of <class 'list'>")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsInstance(thing, (int, list))
+ self.assertEqual(str(cm.exception),
+ "[] is an instance of <class 'list'>")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsInstance(thing, list, 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsInstance(thing, list, msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertIsSubclass(self):
+ self.assertIsSubclass(List, list)
+ self.assertIsSubclass(List, (int, list))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsSubclass(List, int)
+ self.assertEqual(str(cm.exception),
+ f"{List!r} is not a subclass of <class 'int'>")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsSubclass(List, (int, float))
+ self.assertEqual(str(cm.exception),
+ f"{List!r} is not a subclass of any of (<class 'int'>, <class 'float'>)")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsSubclass(1, int)
+ self.assertEqual(str(cm.exception), "1 is not a class")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsSubclass(List, int, 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertIsSubclass(List, int, msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertNotIsSubclass(self):
+ self.assertNotIsSubclass(List, int)
+ self.assertNotIsSubclass(List, (int, float))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsSubclass(List, list)
+ self.assertEqual(str(cm.exception),
+ f"{List!r} is a subclass of <class 'list'>")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsSubclass(List, (int, list))
+ self.assertEqual(str(cm.exception),
+ f"{List!r} is a subclass of <class 'list'>")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsSubclass(1, int)
+ self.assertEqual(str(cm.exception), "1 is not a class")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsSubclass(List, list, 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotIsSubclass(List, list, msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertHasAttr(self):
+ a = List()
+ a.x = 1
+ self.assertHasAttr(a, 'x')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertHasAttr(a, 'y')
+ self.assertEqual(str(cm.exception),
+ "List instance has no attribute 'y'")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertHasAttr(a, 'y', 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertHasAttr(a, 'y', msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertNotHasAttr(self):
+ a = List()
+ a.x = 1
+ self.assertNotHasAttr(a, 'y')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotHasAttr(a, 'x')
+ self.assertEqual(str(cm.exception),
+ "List instance has unexpected attribute 'x'")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotHasAttr(a, 'x', 'ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotHasAttr(a, 'x', msg='ababahalamaha')
+ self.assertIn('ababahalamaha', str(cm.exception))
def testAssertIn(self):
animals = {'monkey': 'banana', 'cow': 'grass', 'seal': 'fish'}
@@ -1864,6 +1973,186 @@ test case
pass
self.assertIsNone(value)
+ def testAssertStartswith(self):
+ self.assertStartsWith('ababahalamaha', 'ababa')
+ self.assertStartsWith('ababahalamaha', ('x', 'ababa', 'y'))
+ self.assertStartsWith(UserString('ababahalamaha'), 'ababa')
+ self.assertStartsWith(UserString('ababahalamaha'), ('x', 'ababa', 'y'))
+ self.assertStartsWith(bytearray(b'ababahalamaha'), b'ababa')
+ self.assertStartsWith(bytearray(b'ababahalamaha'), (b'x', b'ababa', b'y'))
+ self.assertStartsWith(b'ababahalamaha', bytearray(b'ababa'))
+ self.assertStartsWith(b'ababahalamaha',
+ (bytearray(b'x'), bytearray(b'ababa'), bytearray(b'y')))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', 'amaha')
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' doesn't start with 'amaha'")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', ('x', 'y'))
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' doesn't start with any of ('x', 'y')")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith(b'ababahalamaha', 'ababa')
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith(b'ababahalamaha', ('amaha', 'ababa'))
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith([], 'ababa')
+ self.assertEqual(str(cm.exception), 'Expected str, not list')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', b'ababa')
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', (b'amaha', b'ababa'))
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(TypeError):
+ self.assertStartsWith('ababahalamaha', ord('a'))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', 'amaha', 'abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertStartsWith('ababahalamaha', 'amaha', msg='abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertNotStartswith(self):
+ self.assertNotStartsWith('ababahalamaha', 'amaha')
+ self.assertNotStartsWith('ababahalamaha', ('x', 'amaha', 'y'))
+ self.assertNotStartsWith(UserString('ababahalamaha'), 'amaha')
+ self.assertNotStartsWith(UserString('ababahalamaha'), ('x', 'amaha', 'y'))
+ self.assertNotStartsWith(bytearray(b'ababahalamaha'), b'amaha')
+ self.assertNotStartsWith(bytearray(b'ababahalamaha'), (b'x', b'amaha', b'y'))
+ self.assertNotStartsWith(b'ababahalamaha', bytearray(b'amaha'))
+ self.assertNotStartsWith(b'ababahalamaha',
+ (bytearray(b'x'), bytearray(b'amaha'), bytearray(b'y')))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', 'ababa')
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' starts with 'ababa'")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', ('x', 'ababa', 'y'))
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' starts with 'ababa'")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith(b'ababahalamaha', 'ababa')
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith(b'ababahalamaha', ('amaha', 'ababa'))
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith([], 'ababa')
+ self.assertEqual(str(cm.exception), 'Expected str, not list')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', b'ababa')
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', (b'amaha', b'ababa'))
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(TypeError):
+ self.assertNotStartsWith('ababahalamaha', ord('a'))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', 'ababa', 'abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotStartsWith('ababahalamaha', 'ababa', msg='abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertEndswith(self):
+ self.assertEndsWith('ababahalamaha', 'amaha')
+ self.assertEndsWith('ababahalamaha', ('x', 'amaha', 'y'))
+ self.assertEndsWith(UserString('ababahalamaha'), 'amaha')
+ self.assertEndsWith(UserString('ababahalamaha'), ('x', 'amaha', 'y'))
+ self.assertEndsWith(bytearray(b'ababahalamaha'), b'amaha')
+ self.assertEndsWith(bytearray(b'ababahalamaha'), (b'x', b'amaha', b'y'))
+ self.assertEndsWith(b'ababahalamaha', bytearray(b'amaha'))
+ self.assertEndsWith(b'ababahalamaha',
+ (bytearray(b'x'), bytearray(b'amaha'), bytearray(b'y')))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', 'ababa')
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' doesn't end with 'ababa'")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', ('x', 'y'))
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' doesn't end with any of ('x', 'y')")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith(b'ababahalamaha', 'amaha')
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith(b'ababahalamaha', ('ababa', 'amaha'))
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith([], 'amaha')
+ self.assertEqual(str(cm.exception), 'Expected str, not list')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', b'amaha')
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', (b'ababa', b'amaha'))
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(TypeError):
+ self.assertEndsWith('ababahalamaha', ord('a'))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', 'ababa', 'abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertEndsWith('ababahalamaha', 'ababa', msg='abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
+ def testAssertNotEndswith(self):
+ self.assertNotEndsWith('ababahalamaha', 'ababa')
+ self.assertNotEndsWith('ababahalamaha', ('x', 'ababa', 'y'))
+ self.assertNotEndsWith(UserString('ababahalamaha'), 'ababa')
+ self.assertNotEndsWith(UserString('ababahalamaha'), ('x', 'ababa', 'y'))
+ self.assertNotEndsWith(bytearray(b'ababahalamaha'), b'ababa')
+ self.assertNotEndsWith(bytearray(b'ababahalamaha'), (b'x', b'ababa', b'y'))
+ self.assertNotEndsWith(b'ababahalamaha', bytearray(b'ababa'))
+ self.assertNotEndsWith(b'ababahalamaha',
+ (bytearray(b'x'), bytearray(b'ababa'), bytearray(b'y')))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', 'amaha')
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' ends with 'amaha'")
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', ('x', 'amaha', 'y'))
+ self.assertEqual(str(cm.exception),
+ "'ababahalamaha' ends with 'amaha'")
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith(b'ababahalamaha', 'amaha')
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith(b'ababahalamaha', ('ababa', 'amaha'))
+ self.assertEqual(str(cm.exception), 'Expected str, not bytes')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith([], 'amaha')
+ self.assertEqual(str(cm.exception), 'Expected str, not list')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', b'amaha')
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', (b'ababa', b'amaha'))
+ self.assertEqual(str(cm.exception), 'Expected bytes, not str')
+ with self.assertRaises(TypeError):
+ self.assertNotEndsWith('ababahalamaha', ord('a'))
+
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', 'amaha', 'abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+ with self.assertRaises(self.failureException) as cm:
+ self.assertNotEndsWith('ababahalamaha', 'amaha', msg='abracadabra')
+ self.assertIn('ababahalamaha', str(cm.exception))
+
def testDeprecatedFailMethods(self):
"""Test that the deprecated fail* methods get removed in 3.12"""
deprecated_names = [
diff --git a/Lib/test/test_unittest/test_loader.py b/Lib/test/test_unittest/test_loader.py
index 83dd25c..cdff6d1 100644
--- a/Lib/test/test_unittest/test_loader.py
+++ b/Lib/test/test_unittest/test_loader.py
@@ -76,7 +76,7 @@ class Test_TestLoader(unittest.TestCase):
loader = unittest.TestLoader()
# This has to be false for the test to succeed
- self.assertFalse('runTest'.startswith(loader.testMethodPrefix))
+ self.assertNotStartsWith('runTest', loader.testMethodPrefix)
suite = loader.loadTestsFromTestCase(Foo)
self.assertIsInstance(suite, loader.suiteClass)
diff --git a/Lib/test/test_unittest/test_program.py b/Lib/test/test_unittest/test_program.py
index 0b46f33..58d0cef 100644
--- a/Lib/test/test_unittest/test_program.py
+++ b/Lib/test/test_unittest/test_program.py
@@ -128,14 +128,14 @@ class Test_TestProgram(unittest.TestCase):
argv=["foobar"],
testRunner=unittest.TextTestRunner(stream=stream),
testLoader=self.TestLoader(self.FooBar))
- self.assertTrue(hasattr(program, 'result'))
+ self.assertHasAttr(program, 'result')
out = stream.getvalue()
self.assertIn('\nFAIL: testFail ', out)
self.assertIn('\nERROR: testError ', out)
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n')
- self.assertTrue(out.endswith(expected))
+ self.assertEndsWith(out, expected)
@force_not_colorized
def test_Exit(self):
@@ -153,7 +153,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n')
- self.assertTrue(out.endswith(expected))
+ self.assertEndsWith(out, expected)
@force_not_colorized
def test_ExitAsDefault(self):
@@ -169,7 +169,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n')
- self.assertTrue(out.endswith(expected))
+ self.assertEndsWith(out, expected)
@force_not_colorized
def test_ExitSkippedSuite(self):
@@ -182,7 +182,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertEqual(cm.exception.code, 0)
out = stream.getvalue()
expected = '\n\nOK (skipped=1)\n'
- self.assertTrue(out.endswith(expected))
+ self.assertEndsWith(out, expected)
@force_not_colorized
def test_ExitEmptySuite(self):
diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py
index ad6f52d..327b246 100644
--- a/Lib/test/test_unittest/test_result.py
+++ b/Lib/test/test_unittest/test_result.py
@@ -462,7 +462,7 @@ class Test_TestResult(unittest.TestCase):
self.assertTrue(result.failfast)
result = runner.run(test)
stream.flush()
- self.assertTrue(stream.getvalue().endswith('\n\nOK\n'))
+ self.assertEndsWith(stream.getvalue(), '\n\nOK\n')
class Test_TextTestResult(unittest.TestCase):
diff --git a/Lib/test/test_unittest/testmock/testasync.py b/Lib/test/test_unittest/testmock/testasync.py
index afc9d1f..0791675 100644
--- a/Lib/test/test_unittest/testmock/testasync.py
+++ b/Lib/test/test_unittest/testmock/testasync.py
@@ -586,16 +586,16 @@ class AsyncMagicMethods(unittest.TestCase):
def test_magicmock_has_async_magic_methods(self):
m_mock = MagicMock()
- self.assertTrue(hasattr(m_mock, "__aenter__"))
- self.assertTrue(hasattr(m_mock, "__aexit__"))
- self.assertTrue(hasattr(m_mock, "__anext__"))
+ self.assertHasAttr(m_mock, "__aenter__")
+ self.assertHasAttr(m_mock, "__aexit__")
+ self.assertHasAttr(m_mock, "__anext__")
def test_asyncmock_has_sync_magic_methods(self):
a_mock = AsyncMock()
- self.assertTrue(hasattr(a_mock, "__enter__"))
- self.assertTrue(hasattr(a_mock, "__exit__"))
- self.assertTrue(hasattr(a_mock, "__next__"))
- self.assertTrue(hasattr(a_mock, "__len__"))
+ self.assertHasAttr(a_mock, "__enter__")
+ self.assertHasAttr(a_mock, "__exit__")
+ self.assertHasAttr(a_mock, "__next__")
+ self.assertHasAttr(a_mock, "__len__")
def test_magic_methods_are_async_functions(self):
m_mock = MagicMock()
diff --git a/Lib/test/test_unittest/testmock/testcallable.py b/Lib/test/test_unittest/testmock/testcallable.py
index ca88511..03cb983 100644
--- a/Lib/test/test_unittest/testmock/testcallable.py
+++ b/Lib/test/test_unittest/testmock/testcallable.py
@@ -23,21 +23,21 @@ class TestCallable(unittest.TestCase):
def test_non_callable(self):
for mock in NonCallableMagicMock(), NonCallableMock():
self.assertRaises(TypeError, mock)
- self.assertFalse(hasattr(mock, '__call__'))
+ self.assertNotHasAttr(mock, '__call__')
self.assertIn(mock.__class__.__name__, repr(mock))
def test_hierarchy(self):
- self.assertTrue(issubclass(MagicMock, Mock))
- self.assertTrue(issubclass(NonCallableMagicMock, NonCallableMock))
+ self.assertIsSubclass(MagicMock, Mock)
+ self.assertIsSubclass(NonCallableMagicMock, NonCallableMock)
def test_attributes(self):
one = NonCallableMock()
- self.assertTrue(issubclass(type(one.one), Mock))
+ self.assertIsSubclass(type(one.one), Mock)
two = NonCallableMagicMock()
- self.assertTrue(issubclass(type(two.two), MagicMock))
+ self.assertIsSubclass(type(two.two), MagicMock)
def test_subclasses(self):
@@ -45,13 +45,13 @@ class TestCallable(unittest.TestCase):
pass
one = MockSub()
- self.assertTrue(issubclass(type(one.one), MockSub))
+ self.assertIsSubclass(type(one.one), MockSub)
class MagicSub(MagicMock):
pass
two = MagicSub()
- self.assertTrue(issubclass(type(two.two), MagicSub))
+ self.assertIsSubclass(type(two.two), MagicSub)
def test_patch_spec(self):
diff --git a/Lib/test/test_unittest/testmock/testhelpers.py b/Lib/test/test_unittest/testmock/testhelpers.py
index f260769..8d0f3eb 100644
--- a/Lib/test/test_unittest/testmock/testhelpers.py
+++ b/Lib/test/test_unittest/testmock/testhelpers.py
@@ -951,7 +951,7 @@ class SpecSignatureTest(unittest.TestCase):
proxy = Foo()
autospec = create_autospec(proxy)
- self.assertFalse(hasattr(autospec, '__name__'))
+ self.assertNotHasAttr(autospec, '__name__')
def test_autospec_signature_staticmethod(self):
diff --git a/Lib/test/test_unittest/testmock/testmagicmethods.py b/Lib/test/test_unittest/testmock/testmagicmethods.py
index 2a8aa11..acdbd69 100644
--- a/Lib/test/test_unittest/testmock/testmagicmethods.py
+++ b/Lib/test/test_unittest/testmock/testmagicmethods.py
@@ -10,13 +10,13 @@ class TestMockingMagicMethods(unittest.TestCase):
def test_deleting_magic_methods(self):
mock = Mock()
- self.assertFalse(hasattr(mock, '__getitem__'))
+ self.assertNotHasAttr(mock, '__getitem__')
mock.__getitem__ = Mock()
- self.assertTrue(hasattr(mock, '__getitem__'))
+ self.assertHasAttr(mock, '__getitem__')
del mock.__getitem__
- self.assertFalse(hasattr(mock, '__getitem__'))
+ self.assertNotHasAttr(mock, '__getitem__')
def test_magicmock_del(self):
@@ -252,12 +252,12 @@ class TestMockingMagicMethods(unittest.TestCase):
self.assertEqual(list(mock), [1, 2, 3])
getattr(mock, '__bool__').return_value = False
- self.assertFalse(hasattr(mock, '__nonzero__'))
+ self.assertNotHasAttr(mock, '__nonzero__')
self.assertFalse(bool(mock))
for entry in _magics:
- self.assertTrue(hasattr(mock, entry))
- self.assertFalse(hasattr(mock, '__imaginary__'))
+ self.assertHasAttr(mock, entry)
+ self.assertNotHasAttr(mock, '__imaginary__')
def test_magic_mock_equality(self):
diff --git a/Lib/test/test_unittest/testmock/testmock.py b/Lib/test/test_unittest/testmock/testmock.py
index e1b108f..5d1bf42 100644
--- a/Lib/test/test_unittest/testmock/testmock.py
+++ b/Lib/test/test_unittest/testmock/testmock.py
@@ -2215,13 +2215,13 @@ class MockTest(unittest.TestCase):
def test_attribute_deletion(self):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()):
- self.assertTrue(hasattr(mock, 'm'))
+ self.assertHasAttr(mock, 'm')
del mock.m
- self.assertFalse(hasattr(mock, 'm'))
+ self.assertNotHasAttr(mock, 'm')
del mock.f
- self.assertFalse(hasattr(mock, 'f'))
+ self.assertNotHasAttr(mock, 'f')
self.assertRaises(AttributeError, getattr, mock, 'f')
@@ -2230,18 +2230,18 @@ class MockTest(unittest.TestCase):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()):
mock.foo = 3
- self.assertTrue(hasattr(mock, 'foo'))
+ self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 3)
del mock.foo
- self.assertFalse(hasattr(mock, 'foo'))
+ self.assertNotHasAttr(mock, 'foo')
mock.foo = 4
- self.assertTrue(hasattr(mock, 'foo'))
+ self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 4)
del mock.foo
- self.assertFalse(hasattr(mock, 'foo'))
+ self.assertNotHasAttr(mock, 'foo')
def test_mock_raises_when_deleting_nonexistent_attribute(self):
@@ -2259,7 +2259,7 @@ class MockTest(unittest.TestCase):
mock.child = True
del mock.child
mock.reset_mock()
- self.assertFalse(hasattr(mock, 'child'))
+ self.assertNotHasAttr(mock, 'child')
def test_class_assignable(self):
diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py
index 037c021..7c5fc3d 100644
--- a/Lib/test/test_unittest/testmock/testpatch.py
+++ b/Lib/test/test_unittest/testmock/testpatch.py
@@ -366,7 +366,7 @@ class PatchTest(unittest.TestCase):
self.assertEqual(SomeClass.frooble, sentinel.Frooble)
test()
- self.assertFalse(hasattr(SomeClass, 'frooble'))
+ self.assertNotHasAttr(SomeClass, 'frooble')
def test_patch_wont_create_by_default(self):
@@ -383,7 +383,7 @@ class PatchTest(unittest.TestCase):
@patch.object(SomeClass, 'ord', sentinel.Frooble)
def test(): pass
test()
- self.assertFalse(hasattr(SomeClass, 'ord'))
+ self.assertNotHasAttr(SomeClass, 'ord')
def test_patch_builtins_without_create(self):
@@ -1477,7 +1477,7 @@ class PatchTest(unittest.TestCase):
finally:
patcher.stop()
- self.assertFalse(hasattr(Foo, 'blam'))
+ self.assertNotHasAttr(Foo, 'blam')
def test_patch_multiple_spec_set(self):
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index 0b09010..6e23097 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -111,10 +111,6 @@ class BaseTest(unittest.TestCase):
result = f.read()
return result
- def assertEndsWith(self, string, tail):
- if not string.endswith(tail):
- self.fail(f"String {string!r} does not end with {tail!r}")
-
class BasicTest(BaseTest):
"""Test venv module functionality."""
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index 55c79d3..e9ef551 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -1321,13 +1321,67 @@ class TestCase(object):
"""Same as self.assertTrue(isinstance(obj, cls)), with a nicer
default message."""
if not isinstance(obj, cls):
- standardMsg = '%s is not an instance of %r' % (safe_repr(obj), cls)
+ if isinstance(cls, tuple):
+ standardMsg = f'{safe_repr(obj)} is not an instance of any of {cls!r}'
+ else:
+ standardMsg = f'{safe_repr(obj)} is not an instance of {cls!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertNotIsInstance(self, obj, cls, msg=None):
"""Included for symmetry with assertIsInstance."""
if isinstance(obj, cls):
- standardMsg = '%s is an instance of %r' % (safe_repr(obj), cls)
+ if isinstance(cls, tuple):
+ for x in cls:
+ if isinstance(obj, x):
+ cls = x
+ break
+ standardMsg = f'{safe_repr(obj)} is an instance of {cls!r}'
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertIsSubclass(self, cls, superclass, msg=None):
+ try:
+ if issubclass(cls, superclass):
+ return
+ except TypeError:
+ if not isinstance(cls, type):
+ self.fail(self._formatMessage(msg, f'{cls!r} is not a class'))
+ raise
+ if isinstance(superclass, tuple):
+ standardMsg = f'{cls!r} is not a subclass of any of {superclass!r}'
+ else:
+ standardMsg = f'{cls!r} is not a subclass of {superclass!r}'
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertNotIsSubclass(self, cls, superclass, msg=None):
+ try:
+ if not issubclass(cls, superclass):
+ return
+ except TypeError:
+ if not isinstance(cls, type):
+ self.fail(self._formatMessage(msg, f'{cls!r} is not a class'))
+ raise
+ if isinstance(superclass, tuple):
+ for x in superclass:
+ if issubclass(cls, x):
+ superclass = x
+ break
+ standardMsg = f'{cls!r} is a subclass of {superclass!r}'
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertHasAttr(self, obj, name, msg=None):
+ if not hasattr(obj, name):
+ if isinstance(obj, types.ModuleType):
+ standardMsg = f'module {obj.__name__!r} has no attribute {name!r}'
+ else:
+ standardMsg = f'{type(obj).__name__} instance has no attribute {name!r}'
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertNotHasAttr(self, obj, name, msg=None):
+ if hasattr(obj, name):
+ if isinstance(obj, types.ModuleType):
+ standardMsg = f'module {obj.__name__!r} has unexpected attribute {name!r}'
+ else:
+ standardMsg = f'{type(obj).__name__} instance has unexpected attribute {name!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertRaisesRegex(self, expected_exception, expected_regex,
@@ -1391,6 +1445,80 @@ class TestCase(object):
msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg)
+ def _tail_type_check(self, s, tails, msg):
+ if not isinstance(tails, tuple):
+ tails = (tails,)
+ for tail in tails:
+ if isinstance(tail, str):
+ if not isinstance(s, str):
+ self.fail(self._formatMessage(msg,
+ f'Expected str, not {type(s).__name__}'))
+ elif isinstance(tail, (bytes, bytearray)):
+ if not isinstance(s, (bytes, bytearray)):
+ self.fail(self._formatMessage(msg,
+ f'Expected bytes, not {type(s).__name__}'))
+
+ def assertStartsWith(self, s, prefix, msg=None):
+ try:
+ if s.startswith(prefix):
+ return
+ except (AttributeError, TypeError):
+ self._tail_type_check(s, prefix, msg)
+ raise
+ a = safe_repr(s, short=True)
+ b = safe_repr(prefix)
+ if isinstance(prefix, tuple):
+ standardMsg = f"{a} doesn't start with any of {b}"
+ else:
+ standardMsg = f"{a} doesn't start with {b}"
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertNotStartsWith(self, s, prefix, msg=None):
+ try:
+ if not s.startswith(prefix):
+ return
+ except (AttributeError, TypeError):
+ self._tail_type_check(s, prefix, msg)
+ raise
+ if isinstance(prefix, tuple):
+ for x in prefix:
+ if s.startswith(x):
+ prefix = x
+ break
+ a = safe_repr(s, short=True)
+ b = safe_repr(prefix)
+ self.fail(self._formatMessage(msg, f"{a} starts with {b}"))
+
+ def assertEndsWith(self, s, suffix, msg=None):
+ try:
+ if s.endswith(suffix):
+ return
+ except (AttributeError, TypeError):
+ self._tail_type_check(s, suffix, msg)
+ raise
+ a = safe_repr(s, short=True)
+ b = safe_repr(suffix)
+ if isinstance(suffix, tuple):
+ standardMsg = f"{a} doesn't end with any of {b}"
+ else:
+ standardMsg = f"{a} doesn't end with {b}"
+ self.fail(self._formatMessage(msg, standardMsg))
+
+ def assertNotEndsWith(self, s, suffix, msg=None):
+ try:
+ if not s.endswith(suffix):
+ return
+ except (AttributeError, TypeError):
+ self._tail_type_check(s, suffix, msg)
+ raise
+ if isinstance(suffix, tuple):
+ for x in suffix:
+ if s.endswith(x):
+ suffix = x
+ break
+ a = safe_repr(s, short=True)
+ b = safe_repr(suffix)
+ self.fail(self._formatMessage(msg, f"{a} ends with {b}"))
class FunctionTestCase(TestCase):
diff --git a/Misc/NEWS.d/next/Library/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst b/Misc/NEWS.d/next/Library/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst
new file mode 100644
index 0000000..5f33a30
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst
@@ -0,0 +1,9 @@
+Add new assertion methods for :mod:`unittest`:
+:meth:`~unittest.TestCase.assertHasAttr`,
+:meth:`~unittest.TestCase.assertNotHasAttr`,
+:meth:`~unittest.TestCase.assertIsSubclass`,
+:meth:`~unittest.TestCase.assertNotIsSubclass`
+:meth:`~unittest.TestCase.assertStartsWith`,
+:meth:`~unittest.TestCase.assertNotStartsWith`,
+:meth:`~unittest.TestCase.assertEndsWith` and
+:meth:`~unittest.TestCase.assertNotEndsWith`.