Skip to content

Commit c6a566e

Browse files
[3.13] pythongh-71339: Add additional assertion methods in test.support (pythonGH-128707) (pythonGH-128815)
Add a mix-in class ExtraAssertions containing the following methods: * assertHasAttr() and assertNotHasAttr() * assertIsSubclass() and assertNotIsSubclass() * assertStartsWith() and assertNotStartsWith() * assertEndsWith() and assertNotEndsWith() (cherry picked from commit 06cad77)
1 parent 59b919b commit c6a566e

File tree

7 files changed

+70
-56
lines changed

7 files changed

+70
-56
lines changed

Lib/test/support/testcase.py

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,63 @@
11
from math import copysign, isnan
22

33

4+
class ExtraAssertions:
5+
6+
def assertIsSubclass(self, cls, superclass, msg=None):
7+
if issubclass(cls, superclass):
8+
return
9+
standardMsg = f'{cls!r} is not a subclass of {superclass!r}'
10+
self.fail(self._formatMessage(msg, standardMsg))
11+
12+
def assertNotIsSubclass(self, cls, superclass, msg=None):
13+
if not issubclass(cls, superclass):
14+
return
15+
standardMsg = f'{cls!r} is a subclass of {superclass!r}'
16+
self.fail(self._formatMessage(msg, standardMsg))
17+
18+
def assertHasAttr(self, obj, name, msg=None):
19+
if not hasattr(obj, name):
20+
if isinstance(obj, types.ModuleType):
21+
standardMsg = f'module {obj.__name__!r} has no attribute {name!r}'
22+
elif isinstance(obj, type):
23+
standardMsg = f'type object {obj.__name__!r} has no attribute {name!r}'
24+
else:
25+
standardMsg = f'{type(obj).__name__!r} object has no attribute {name!r}'
26+
self.fail(self._formatMessage(msg, standardMsg))
27+
28+
def assertNotHasAttr(self, obj, name, msg=None):
29+
if hasattr(obj, name):
30+
if isinstance(obj, types.ModuleType):
31+
standardMsg = f'module {obj.__name__!r} has unexpected attribute {name!r}'
32+
elif isinstance(obj, type):
33+
standardMsg = f'type object {obj.__name__!r} has unexpected attribute {name!r}'
34+
else:
35+
standardMsg = f'{type(obj).__name__!r} object has unexpected attribute {name!r}'
36+
self.fail(self._formatMessage(msg, standardMsg))
37+
38+
def assertStartsWith(self, s, prefix, msg=None):
39+
if s.startswith(prefix):
40+
return
41+
standardMsg = f"{s!r} doesn't start with {prefix!r}"
42+
self.fail(self._formatMessage(msg, standardMsg))
43+
44+
def assertNotStartsWith(self, s, prefix, msg=None):
45+
if not s.startswith(prefix):
46+
return
47+
self.fail(self._formatMessage(msg, f"{s!r} starts with {prefix!r}"))
48+
49+
def assertEndsWith(self, s, suffix, msg=None):
50+
if s.endswith(suffix):
51+
return
52+
standardMsg = f"{s!r} doesn't end with {suffix!r}"
53+
self.fail(self._formatMessage(msg, standardMsg))
54+
55+
def assertNotEndsWith(self, s, suffix, msg=None):
56+
if not s.endswith(suffix):
57+
return
58+
self.fail(self._formatMessage(msg, f"{s!r} ends with {suffix!r}"))
59+
60+
461
class ExceptionIsLikeMixin:
562
def assertExceptionIsLike(self, exc, template):
663
"""

Lib/test/test_descr.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from copy import deepcopy
1616
from contextlib import redirect_stdout
1717
from test import support
18+
from test.support.testcase import ExtraAssertions
1819

1920
try:
2021
import _testcapi
@@ -403,15 +404,7 @@ def test_wrap_lenfunc_bad_cast(self):
403404
self.assertEqual(range(sys.maxsize).__len__(), sys.maxsize)
404405

405406

406-
class ClassPropertiesAndMethods(unittest.TestCase):
407-
408-
def assertHasAttr(self, obj, name):
409-
self.assertTrue(hasattr(obj, name),
410-
'%r has no attribute %r' % (obj, name))
411-
412-
def assertNotHasAttr(self, obj, name):
413-
self.assertFalse(hasattr(obj, name),
414-
'%r has unexpected attribute %r' % (obj, name))
407+
class ClassPropertiesAndMethods(unittest.TestCase, ExtraAssertions):
415408

416409
def test_python_dicts(self):
417410
# Testing Python subclass of dict...

Lib/test/test_gdb/util.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sysconfig
88
import unittest
99
from test import support
10+
from test.support.testcase import ExtraAssertions
1011

1112

1213
GDB_PROGRAM = shutil.which('gdb') or 'gdb'
@@ -152,7 +153,7 @@ def setup_module():
152153
print()
153154

154155

155-
class DebuggerTests(unittest.TestCase):
156+
class DebuggerTests(unittest.TestCase, ExtraAssertions):
156157

157158
"""Test that the debugger can debug Python."""
158159

@@ -280,11 +281,6 @@ def get_stack_trace(self, source=None, script=None,
280281

281282
return out
282283

283-
def assertEndsWith(self, actual, exp_end):
284-
'''Ensure that the given "actual" string ends with "exp_end"'''
285-
self.assertTrue(actual.endswith(exp_end),
286-
msg='%r did not end with %r' % (actual, exp_end))
287-
288284
def assertMultilineMatches(self, actual, pattern):
289285
m = re.match(pattern, actual, re.DOTALL)
290286
if not m:

Lib/test/test_importlib/resources/test_functional.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import importlib
44

55
from test.support import warnings_helper
6+
from test.support.testcase import ExtraAssertions
67

78
from importlib import resources
89

@@ -28,7 +29,7 @@ def anchor02(self):
2829
return importlib.import_module('data02')
2930

3031

31-
class FunctionalAPIBase(util.DiskSetup):
32+
class FunctionalAPIBase(util.DiskSetup, ExtraAssertions):
3233
def setUp(self):
3334
super().setUp()
3435
self.load_fixture('data02')
@@ -43,12 +44,6 @@ def _gen_resourcetxt_path_parts(self):
4344
with self.subTest(path_parts=path_parts):
4445
yield path_parts
4546

46-
def assertEndsWith(self, string, suffix):
47-
"""Assert that `string` ends with `suffix`.
48-
49-
Used to ignore an architecture-specific UTF-16 byte-order mark."""
50-
self.assertEqual(string[-len(suffix) :], suffix)
51-
5247
def test_read_text(self):
5348
self.assertEqual(
5449
resources.read_text(self.anchor01, 'utf-8.file'),

Lib/test/test_pyclbr.py

+3-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from unittest import TestCase, main as unittest_main
1111
from test.test_importlib import util as test_importlib_util
1212
import warnings
13+
from test.support.testcase import ExtraAssertions
1314

1415

1516
StaticMethodType = type(staticmethod(lambda: None))
@@ -22,7 +23,7 @@
2223
# is imperfect (as designed), testModule is called with a set of
2324
# members to ignore.
2425

25-
class PyclbrTest(TestCase):
26+
class PyclbrTest(TestCase, ExtraAssertions):
2627

2728
def assertListEq(self, l1, l2, ignore):
2829
''' succeed iff {l1} - {ignore} == {l2} - {ignore} '''
@@ -31,14 +32,6 @@ def assertListEq(self, l1, l2, ignore):
3132
print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
3233
self.fail("%r missing" % missing.pop())
3334

34-
def assertHasattr(self, obj, attr, ignore):
35-
''' succeed iff hasattr(obj,attr) or attr in ignore. '''
36-
if attr in ignore: return
37-
if not hasattr(obj, attr): print("???", attr)
38-
self.assertTrue(hasattr(obj, attr),
39-
'expected hasattr(%r, %r)' % (obj, attr))
40-
41-
4235
def assertHaskey(self, obj, key, ignore):
4336
''' succeed iff key in obj or key in ignore. '''
4437
if key in ignore: return
@@ -86,7 +79,7 @@ def ismethod(oclass, obj, name):
8679
for name, value in dict.items():
8780
if name in ignore:
8881
continue
89-
self.assertHasattr(module, name, ignore)
82+
self.assertHasAttr(module, name, ignore)
9083
py_item = getattr(module, name)
9184
if isinstance(value, pyclbr.Function):
9285
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))

Lib/test/test_typing.py

+2-19
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import types
4747

4848
from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper
49+
from test.support.testcase import ExtraAssertions
4950
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper
5051

5152

@@ -54,21 +55,7 @@
5455
CANNOT_SUBCLASS_INSTANCE = 'Cannot subclass an instance of %s'
5556

5657

57-
class BaseTestCase(TestCase):
58-
59-
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
60-
if not issubclass(cls, class_or_tuple):
61-
message = '%r is not a subclass of %r' % (cls, class_or_tuple)
62-
if msg is not None:
63-
message += ' : %s' % msg
64-
raise self.failureException(message)
65-
66-
def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
67-
if issubclass(cls, class_or_tuple):
68-
message = '%r is a subclass of %r' % (cls, class_or_tuple)
69-
if msg is not None:
70-
message += ' : %s' % msg
71-
raise self.failureException(message)
58+
class BaseTestCase(TestCase, ExtraAssertions):
7259

7360
def clear_caches(self):
7461
for f in typing._cleanups:
@@ -1249,10 +1236,6 @@ class Gen[*Ts]: ...
12491236

12501237
class TypeVarTupleTests(BaseTestCase):
12511238

1252-
def assertEndsWith(self, string, tail):
1253-
if not string.endswith(tail):
1254-
self.fail(f"String {string!r} does not end with {tail!r}")
1255-
12561239
def test_name(self):
12571240
Ts = TypeVarTuple('Ts')
12581241
self.assertEqual(Ts.__name__, 'Ts')

Lib/test/test_venv.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
requires_resource, copy_python_src_ignore)
2727
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
2828
TESTFN, FakePath)
29+
from test.support.testcase import ExtraAssertions
2930
import unittest
3031
import venv
3132
from unittest.mock import patch, Mock
@@ -64,7 +65,7 @@ def check_output(cmd, encoding=None):
6465
)
6566
return out, err
6667

67-
class BaseTest(unittest.TestCase):
68+
class BaseTest(unittest.TestCase, ExtraAssertions):
6869
"""Base class for venv tests."""
6970
maxDiff = 80 * 50
7071

@@ -111,10 +112,6 @@ def get_text_file_contents(self, *args, encoding='utf-8'):
111112
result = f.read()
112113
return result
113114

114-
def assertEndsWith(self, string, tail):
115-
if not string.endswith(tail):
116-
self.fail(f"String {string!r} does not end with {tail!r}")
117-
118115
class BasicTest(BaseTest):
119116
"""Test venv module functionality."""
120117

0 commit comments

Comments
 (0)