From c51f48ee1a964c9e225b8d95db45336a91b439b6 Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Sat, 5 Feb 2022 10:32:17 -0800 Subject: [PATCH 1/5] explicitly disallow typing.TypeVar subclassing The existing test covering this case passed only incidentally. We explicitly disallow doing this and add a proper error message. --- Lib/typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 0cf9755022e9b8..8c0b14804c95e8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -818,6 +818,9 @@ def __init__(self, name, *constraints, bound=None, if def_mod != 'typing': self.__module__ = def_mod + def __mro_entries__(self, bases): + raise TypeError("Cannot subclass an instance of TypeVar.") + class ParamSpecArgs(_Final, _Immutable, _root=True): """The args for a ParamSpec object. From 36597721cf7f60c3890bcc670d2bcb40e8051869 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 5 Feb 2022 18:46:55 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst diff --git a/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst b/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst new file mode 100644 index 00000000000000..6d7a66dc7dcf72 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst @@ -0,0 +1 @@ +Improve error message when trying to subclass an instance of :data:`typing.TypeVar`. Patch by Gregory Beauregard. \ No newline at end of file From a3c54d57f8cf2c868550b99c4b4119b8891b98da Mon Sep 17 00:00:00 2001 From: GBeauregard Date: Sat, 5 Feb 2022 18:23:48 -0800 Subject: [PATCH 3/5] make test match regex --- Lib/test/test_typing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 85f74064458f2d..cbb180eff99bf7 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -225,8 +225,10 @@ def test_no_redefinition(self): self.assertNotEqual(TypeVar('T', int, str), TypeVar('T', int, str)) def test_cannot_subclass_vars(self): - with self.assertRaises(TypeError): - class V(TypeVar('T')): + with self.assertRaisesRegex( + TypeError, r"Cannot subclass an instance of TypeVar\." + ): + class V(TypeVar("T")): pass def test_cannot_subclass_var_itself(self): From bf233d12f3e0aa269e9458b3f5c98c2ed97582f9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jun 2022 20:34:58 +0300 Subject: [PATCH 4/5] More explicit errors and more tests. --- Lib/test/test_typing.py | 109 +++++++++++++++++++++++++--------------- Lib/typing.py | 15 ++++-- 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 978cf5f25ae0c6..3894ab43b5d657 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -51,6 +51,10 @@ c_typing = import_helper.import_fresh_module('typing', fresh=['_typing']) +CANNOT_SUBCLASS_TYPE = 'Cannot subclass special typing classes' +CANNOT_SUBCLASS_INSTANCE = 'Cannot subclass an instance of %s' + + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): @@ -170,10 +174,11 @@ def test_not_generic(self): self.bottom_type[int] def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + 'Cannot subclass ' + re.escape(str(self.bottom_type))): class A(self.bottom_type): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class A(type(self.bottom_type)): pass @@ -266,10 +271,11 @@ def test_cannot_subscript(self): Self[int] def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Self)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.Self'): class C(Self): pass @@ -322,10 +328,11 @@ def test_cannot_subscript(self): LiteralString[int] def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(LiteralString)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.LiteralString'): class C(LiteralString): pass @@ -415,17 +422,13 @@ def test_no_redefinition(self): self.assertNotEqual(TypeVar('T'), TypeVar('T')) self.assertNotEqual(TypeVar('T', int, str), TypeVar('T', int, str)) - def test_cannot_subclass_vars(self): - with self.assertRaisesRegex( - TypeError, r"Cannot subclass an instance of TypeVar\." - ): - class V(TypeVar("T")): - pass - - def test_cannot_subclass_var_itself(self): - with self.assertRaises(TypeError): - class V(TypeVar): - pass + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class V(TypeVar): pass + T = TypeVar("T") + with self.assertRaisesRegex(TypeError, + CANNOT_SUBCLASS_INSTANCE % 'TypeVar'): + class V(T): pass def test_cannot_instantiate_vars(self): with self.assertRaises(TypeError): @@ -1018,15 +1021,14 @@ class A(Generic[Unpack[Ts]]): pass self.assertEndsWith(repr(F[float]), 'A[float, *tuple[str, ...]]') self.assertEndsWith(repr(F[float, str]), 'A[float, str, *tuple[str, ...]]') - def test_cannot_subclass_class(self): - with self.assertRaises(TypeError): + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(TypeVarTuple): pass - - def test_cannot_subclass_instance(self): Ts = TypeVarTuple('Ts') - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + CANNOT_SUBCLASS_INSTANCE % 'TypeVarTuple'): class C(Ts): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, r'Cannot subclass \*Ts'): class C(Unpack[Ts]): pass def test_variadic_class_args_are_correct(self): @@ -1413,13 +1415,15 @@ def test_repr(self): self.assertEqual(repr(u), 'typing.Optional[str]') def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.Union'): class C(Union): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Union)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.Union\[int, str\]'): class C(Union[int, str]): pass @@ -3660,10 +3664,10 @@ def test_repr(self): self.assertEqual(repr(cv), 'typing.ClassVar[%s.Employee]' % __name__) def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(ClassVar)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(ClassVar[int])): pass @@ -3702,10 +3706,10 @@ def test_repr(self): self.assertEqual(repr(cv), 'typing.Final[tuple[int]]') def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Final)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Final[int])): pass @@ -6208,16 +6212,18 @@ def test_repr(self): self.assertEqual(repr(cv), f'typing.Required[{__name__}.Employee]') def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Required)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(Required[int])): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.Required'): class C(Required): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.Required\[int\]'): class C(Required[int]): pass @@ -6254,16 +6260,18 @@ def test_repr(self): self.assertEqual(repr(cv), f'typing.NotRequired[{__name__}.Employee]') def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(NotRequired)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(NotRequired[int])): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.NotRequired'): class C(NotRequired): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.NotRequired\[int\]'): class C(NotRequired[int]): pass @@ -6679,7 +6687,8 @@ def test_no_issubclass(self): issubclass(TypeAlias, Employee) def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeAlias'): class C(TypeAlias): pass @@ -6881,6 +6890,24 @@ def test_paramspec_gets_copied(self): self.assertEqual(C2[Concatenate[str, P2]].__parameters__, (P2,)) self.assertEqual(C2[Concatenate[T, P2]].__parameters__, (T, P2)) + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(ParamSpec): pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(ParamSpecArgs): pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(ParamSpecKwargs): pass + P = ParamSpec('P') + with self.assertRaisesRegex(TypeError, + CANNOT_SUBCLASS_INSTANCE % 'ParamSpec'): + class C(P): pass + with self.assertRaisesRegex(TypeError, + CANNOT_SUBCLASS_INSTANCE % 'ParamSpecArgs'): + class C(P.args): pass + with self.assertRaisesRegex(TypeError, + CANNOT_SUBCLASS_INSTANCE % 'ParamSpecKwargs'): + class C(P.kwargs): pass + class ConcatenateTests(BaseTestCase): def test_basics(self): @@ -6947,10 +6974,10 @@ def test_repr(self): self.assertEqual(repr(cv), 'typing.TypeGuard[tuple[int]]') def test_cannot_subclass(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(TypeGuard)): pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): class C(type(TypeGuard[int])): pass diff --git a/Lib/typing.py b/Lib/typing.py index 4e7de984d7d360..25ae19f71fe94a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -954,6 +954,9 @@ def __repr__(self): prefix = '~' return prefix + self.__name__ + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass an instance of {type(self).__name__}") + class TypeVar(_Final, _Immutable, _BoundVarianceMixin, _PickleUsingNameMixin, _root=True): @@ -1022,9 +1025,6 @@ def __typing_subst__(self, arg): raise TypeError(f"{arg} is not valid as type argument") return arg - def __mro_entries__(self, bases): - raise TypeError("Cannot subclass an instance of TypeVar.") - class TypeVarTuple(_Final, _Immutable, _PickleUsingNameMixin, _root=True): """Type variable tuple. @@ -1104,6 +1104,9 @@ def __typing_prepare_subst__(self, alias, args): *args[alen - right:], ) + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass an instance of {type(self).__name__}") + class ParamSpecArgs(_Final, _Immutable, _root=True): """The args for a ParamSpec object. @@ -1128,6 +1131,9 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass an instance of {type(self).__name__}") + class ParamSpecKwargs(_Final, _Immutable, _root=True): """The kwargs for a ParamSpec object. @@ -1152,6 +1158,9 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass an instance of {type(self).__name__}") + class ParamSpec(_Final, _Immutable, _BoundVarianceMixin, _PickleUsingNameMixin, _root=True): From a854d32e7f0eeef4a842bab88f0f2d861107ee95 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jun 2022 20:40:45 +0300 Subject: [PATCH 5/5] Update a NEWS entry. --- .../next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst b/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst index 6d7a66dc7dcf72..2d2815c1e4b00d 100644 --- a/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst +++ b/Misc/NEWS.d/next/Library/2022-02-05-18-46-54.bpo-46642.YI6nHQ.rst @@ -1 +1 @@ -Improve error message when trying to subclass an instance of :data:`typing.TypeVar`. Patch by Gregory Beauregard. \ No newline at end of file +Improve error message when trying to subclass an instance of :data:`typing.TypeVar`, :data:`typing.ParamSpec`, :data:`typing.TypeVarTuple`, etc. Based on patch by Gregory Beauregard.