]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-71339: Add additional assertion methods for unittest (GH-128707)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 14 Jan 2025 08:02:38 +0000 (10:02 +0200)
committerGitHub <noreply@github.com>
Tue, 14 Jan 2025 08:02:38 +0000 (10:02 +0200)
Add the following methods:

* assertHasAttr() and assertNotHasAttr()
* assertIsSubclass() and assertNotIsSubclass()
* assertStartsWith() and assertNotStartsWith()
* assertEndsWith() and assertNotEndsWith()

Also improve error messages for assertIsInstance() and
assertNotIsInstance().

20 files changed:
Doc/library/unittest.rst
Doc/whatsnew/3.14.rst
Lib/test/test_descr.py
Lib/test/test_gdb/util.py
Lib/test/test_importlib/resources/test_functional.py
Lib/test/test_pyclbr.py
Lib/test/test_typing.py
Lib/test/test_unittest/test_case.py
Lib/test/test_unittest/test_loader.py
Lib/test/test_unittest/test_program.py
Lib/test/test_unittest/test_result.py
Lib/test/test_unittest/testmock/testasync.py
Lib/test/test_unittest/testmock/testcallable.py
Lib/test/test_unittest/testmock/testhelpers.py
Lib/test/test_unittest/testmock/testmagicmethods.py
Lib/test/test_unittest/testmock/testmock.py
Lib/test/test_unittest/testmock/testpatch.py
Lib/test/test_venv.py
Lib/unittest/case.py
Misc/NEWS.d/next/Library/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst [new file with mode: 0644]

index 7f8b710f611002fff9b2f0738e172701a4f9a771..0eead59a337ef96309e08f228976d82dbffc3851 100644 (file)
@@ -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
index 1dbd871f3e3e5e33d7f543f05da729094c356100..eedcc621c3c688283f93daa0191ed4e796eab79a 100644 (file)
@@ -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
 ------
index 168b78a477ee9c52bf8675d1b386b23d62b873e2..51f97bb51f7bd24aa4860b6026f3d7865e914012 100644 (file)
@@ -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))
index 8fe9cfc543395e16e2aa8c5cffb048f82a4ff697..8097fd52ababe6cf6483fc0b64dd1c7557cb5e88 100644 (file)
@@ -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:
index 4317abf3162c5290e4de969d0f4b1ea3356a3e72..e8d25fa4d9faf033587cf2d93e1d6cb574ac89c1 100644 (file)
@@ -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'),
index 4bf0576586cca50fbb763bb6f1109fa1b9d9ced6..25b313f6c25a4eabd0156727eb09e23d304abcbd 100644 (file)
@@ -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))
index c51ee763890af25d50434f158ff2a1651f61a999..c98e6f820e8cf76855b9da6938e207ed81fc5740 100644 (file)
@@ -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')
index 621f8269a177ce2eb1152b961ad11590bb0e164f..cd366496eedca307ed01976224978cb95b5b18ad 100644 (file)
@@ -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 = [
index 83dd25ca54623f8383991ab48ae59a486c2a46b7..cdff6d1a20c8dfda384178ab48403074e66409bd 100644 (file)
@@ -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)
index 0b46f338ac77e10851195a565bc3e7d65896afdf..58d0cef9708c95bbdb4e9611fb5c3478f1b30af6 100644 (file)
@@ -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):
index ad6f52d7e0260e6f54bb87b0923341b1f3cc00d5..327b246452bedff58cb1397e2979f50accecd0ab 100644 (file)
@@ -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):
index afc9d1f11da1e26e43c0b91284c507de0eb607a0..0791675b5401ca1de268d8cf7a3f735162b61e1d 100644 (file)
@@ -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()
index ca88511f63959d02c43f87d8b0334d434cce6bea..03cb983e447c70c6a45d7106086b6c19271bb386 100644 (file)
@@ -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):
index f260769eb8c35e9747b6d5876d012ffb15d6cd53..8d0f3ebc5cba884010c1f8fadd7a3f9d9a15dd61 100644 (file)
@@ -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):
index 2a8aa11b3284f6c141e6646b0ac14e6a338b7869..acdbd699d181344fe34088d849cafcdf4974a9e5 100644 (file)
@@ -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):
index e1b108f81e513c12245f7f2593ff9175875e7449..5d1bf4258afacd5baabb381b6865faabcac1fe47 100644 (file)
@@ -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):
index 037c021e6eafcfa12249361037484d82365f5238..7c5fc3deed2ca2dc5812f73aec0fa38eed438666 100644 (file)
@@ -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):
index 0b09010c69d4eac2565c06694811d9b4a076ef99..6e23097deaf221e8339be20bf6fb52d89eeb7f17 100644 (file)
@@ -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."""
 
index 55c79d353539ca90176c2a43c2d4e1a960c013e6..e9ef551d0b3dedcbc2bb4eeaa7dd2613fe23ae73 100644 (file)
@@ -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 (file)
index 0000000..5f33a30
--- /dev/null
@@ -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`.