Skip to content

Commit

Permalink
bpo-46642: Explicitly disallow subclassing of instaces of TypeVar, Pa…
Browse files Browse the repository at this point in the history
…ramSpec, etc (GH-31148)

The existing test covering this case passed only incidentally. We
explicitly disallow doing this and add a proper error message.

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
  • Loading branch information
GBeauregard and serhiy-storchaka authored Jun 25, 2022
1 parent 605e9c6 commit 81e91c9
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 39 deletions.
107 changes: 68 additions & 39 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -415,15 +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.assertRaises(TypeError):
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):
Expand Down Expand Up @@ -1016,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):
Expand Down Expand Up @@ -1411,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

Expand Down Expand Up @@ -3658,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

Expand Down Expand Up @@ -3700,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

Expand Down Expand Up @@ -6206,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

Expand Down Expand Up @@ -6252,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

Expand Down Expand Up @@ -6677,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

Expand Down Expand Up @@ -6879,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):
Expand Down Expand Up @@ -6945,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

Expand Down
12 changes: 12 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -1101,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.
Expand All @@ -1125,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.
Expand All @@ -1149,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.

0 comments on commit 81e91c9

Please sign in to comment.