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().
This commit is contained in:
Serhiy Storchaka 2025-01-14 10:02:38 +02:00 committed by GitHub
parent 41f73501ec
commit 06cad77a5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 555 additions and 100 deletions

View 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):