From 30fc0b2a411345b49d134c3557402baea591aa4d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 20 Jan 2025 11:19:00 +0200 Subject: [PATCH] [3.12] [3.13] gh-71339: Add additional assertion methods in test.support (GH-128707) (GH-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 c6a566e47b9903d48e6e1e78a1af20e6c6c535cf) Co-authored-by: Serhiy Storchaka (cherry picked from commit 06cad77a5b345adde88609be9c3c470c5cd9f417) --- Lib/test/support/testcase.py | 57 ++++++++++++++++++++++++++++++++++++ Lib/test/test_descr.py | 11 ++----- Lib/test/test_gdb/util.py | 8 ++--- Lib/test/test_pyclbr.py | 13 ++------ Lib/test/test_typing.py | 21 ++----------- Lib/test/test_venv.py | 7 ++--- 6 files changed, 68 insertions(+), 49 deletions(-) diff --git a/Lib/test/support/testcase.py b/Lib/test/support/testcase.py index fad1e4cb3499c0..fd32457d1467ca 100644 --- a/Lib/test/support/testcase.py +++ b/Lib/test/support/testcase.py @@ -1,6 +1,63 @@ from math import copysign, isnan +class ExtraAssertions: + + def assertIsSubclass(self, cls, superclass, msg=None): + if issubclass(cls, superclass): + return + standardMsg = f'{cls!r} is not a subclass of {superclass!r}' + self.fail(self._formatMessage(msg, standardMsg)) + + def assertNotIsSubclass(self, cls, superclass, msg=None): + if not issubclass(cls, superclass): + return + 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}' + elif isinstance(obj, type): + standardMsg = f'type object {obj.__name__!r} has no attribute {name!r}' + else: + standardMsg = f'{type(obj).__name__!r} object 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}' + elif isinstance(obj, type): + standardMsg = f'type object {obj.__name__!r} has unexpected attribute {name!r}' + else: + standardMsg = f'{type(obj).__name__!r} object has unexpected attribute {name!r}' + self.fail(self._formatMessage(msg, standardMsg)) + + def assertStartsWith(self, s, prefix, msg=None): + if s.startswith(prefix): + return + standardMsg = f"{s!r} doesn't start with {prefix!r}" + self.fail(self._formatMessage(msg, standardMsg)) + + def assertNotStartsWith(self, s, prefix, msg=None): + if not s.startswith(prefix): + return + self.fail(self._formatMessage(msg, f"{s!r} starts with {prefix!r}")) + + def assertEndsWith(self, s, suffix, msg=None): + if s.endswith(suffix): + return + standardMsg = f"{s!r} doesn't end with {suffix!r}" + self.fail(self._formatMessage(msg, standardMsg)) + + def assertNotEndsWith(self, s, suffix, msg=None): + if not s.endswith(suffix): + return + self.fail(self._formatMessage(msg, f"{s!r} ends with {suffix!r}")) + + class ExceptionIsLikeMixin: def assertExceptionIsLike(self, exc, template): """ diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 3a11435e3e2543..99388b53878f36 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -15,6 +15,7 @@ from copy import deepcopy from contextlib import redirect_stdout from test import support +from test.support.testcase import ExtraAssertions try: import _testcapi @@ -403,15 +404,7 @@ def test_wrap_lenfunc_bad_cast(self): self.assertEqual(range(sys.maxsize).__len__(), sys.maxsize) -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)) +class ClassPropertiesAndMethods(unittest.TestCase, ExtraAssertions): def test_python_dicts(self): # Testing Python subclass of dict... diff --git a/Lib/test/test_gdb/util.py b/Lib/test/test_gdb/util.py index 8fe9cfc543395e..54c6b2de7cc99d 100644 --- a/Lib/test/test_gdb/util.py +++ b/Lib/test/test_gdb/util.py @@ -7,6 +7,7 @@ import sysconfig import unittest from test import support +from test.support.testcase import ExtraAssertions GDB_PROGRAM = shutil.which('gdb') or 'gdb' @@ -152,7 +153,7 @@ def setup_module(): print() -class DebuggerTests(unittest.TestCase): +class DebuggerTests(unittest.TestCase, ExtraAssertions): """Test that the debugger can debug Python.""" @@ -280,11 +281,6 @@ def get_stack_trace(self, source=None, script=None, 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_pyclbr.py b/Lib/test/test_pyclbr.py index 5415fa08330325..a7580a4124defd 100644 --- a/Lib/test/test_pyclbr.py +++ b/Lib/test/test_pyclbr.py @@ -10,6 +10,7 @@ from unittest import TestCase, main as unittest_main from test.test_importlib import util as test_importlib_util import warnings +from test.support.testcase import ExtraAssertions StaticMethodType = type(staticmethod(lambda: None)) @@ -22,7 +23,7 @@ # is imperfect (as designed), testModule is called with a set of # members to ignore. -class PyclbrTest(TestCase): +class PyclbrTest(TestCase, ExtraAssertions): def assertListEq(self, l1, l2, ignore): ''' succeed iff {l1} - {ignore} == {l2} - {ignore} ''' @@ -31,14 +32,6 @@ def assertListEq(self, l1, l2, ignore): 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 +79,7 @@ def ismethod(oclass, obj, name): 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 c5f4d775f22fb6..5c862d7928c950 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -47,6 +47,7 @@ import types from test.support import captured_stderr, cpython_only +from test.support.testcase import ExtraAssertions from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper @@ -55,21 +56,7 @@ 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) +class BaseTestCase(TestCase, ExtraAssertions): def clear_caches(self): for f in typing._cleanups: @@ -1051,10 +1038,6 @@ class Gen[*Ts]: ... 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_venv.py b/Lib/test/test_venv.py index 8254a701e09a10..43c67ac751d585 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -25,6 +25,7 @@ requires_resource, copy_python_src_ignore) from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree, TESTFN, FakePath) +from test.support.testcase import ExtraAssertions import unittest import venv from unittest.mock import patch, Mock @@ -58,7 +59,7 @@ def check_output(cmd, encoding=None): p.returncode, cmd, out, err) return out, err -class BaseTest(unittest.TestCase): +class BaseTest(unittest.TestCase, ExtraAssertions): """Base class for venv tests.""" maxDiff = 80 * 50 @@ -98,10 +99,6 @@ def get_text_file_contents(self, *args, encoding='utf-8'): 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."""