Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for TypeVarLike default parameter (PEP 696) #77

Merged
merged 6 commits into from Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@
- Add `typing_extensions.Any` a backport of python 3.11's Any class which is
subclassable at runtime. (backport from python/cpython#31841, by Shantanu
and Jelle Zijlstra). Patch by James Hilton-Balfe (@Gobot1234).
- Add initial support for TypeVarLike `default` parameter, PEP 696.
Patch by Marc Mueller (@cdce8p).

# Release 4.3.0 (July 1, 2022)

Expand Down
26 changes: 26 additions & 0 deletions src/test_typing_extensions.py
Expand Up @@ -3067,8 +3067,11 @@ def test_all_names_in___all__(self):
def test_typing_extensions_defers_when_possible(self):
exclude = {
'overload',
'ParamSpec',
'Text',
'TypedDict',
'TypeVar',
'TypeVarTuple',
'TYPE_CHECKING',
'Final',
'get_type_hints',
Expand Down Expand Up @@ -3395,5 +3398,28 @@ def test_same_as_typing_NamedTuple_38_minus(self):
)


class TypeVarLikeDefaultsTests(BaseTestCase):
def test_typevar(self):
T = typing_extensions.TypeVar("T", default=int)
cdce8p marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(T.__default__, int)

class A(Generic[T]): ...
Alias = Optional[T]

def test_paramspec(self):
P = ParamSpec("P", default=(str, int))
self.assertEqual(P.__default__, (str, int))

class A(Generic[P]): ...
Alias = typing.Callable[P, None]

def test_typevartuple(self):
Ts = TypeVarTuple("Ts", default=Unpack[Tuple[str, int]])
self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])

class A(Generic[Unpack[Ts]]): ...
Alias = Optional[Unpack[Ts]]


if __name__ == '__main__':
main()
88 changes: 82 additions & 6 deletions src/typing_extensions.py
Expand Up @@ -21,6 +21,7 @@
'ParamSpecKwargs',
'Self',
'Type',
'TypeVar',
'TypeVarTuple',
'Unpack',

Expand Down Expand Up @@ -1147,6 +1148,42 @@ def __repr__(self):
above.""")


class _DefaultMixin:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class should define empty slots to make TypeVar not have dict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Although I'm not sure it will work. For 3.10 _TypeVarLike and for 3.11 _BoundVarianceMixin don't add __slots__, so the __dict__ for TypeVar is created anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're interested, we can fix that for 3.12. (I'd be hesitant for older versions since it may break some users.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like __slots__ was removed on purpose since it didn't have any use. I've updated the PR to remove it from TypeVar as well. Ref: python/cpython#30444

"""Mixin for TypeVarLike defaults."""

def __init__(self, default):
if isinstance(default, tuple):
cdce8p marked this conversation as resolved.
Show resolved Hide resolved
self.__default__ = tuple((typing._type_check(d, "Default must be a type")
for d in default))
elif default:
self.__default__ = typing._type_check(default, "Default must be a type")
else:
self.__default__ = None


# Add default Parameter - PEP 696
class TypeVar(typing.TypeVar, _DefaultMixin, _root=True):
"""Type variable."""

__slots__ = ('__default__',)
__module__ = "typing"

def __init__(self, name, *constraints, bound=None,
covariant=False, contravariant=False,
default=None):
super().__init__(name, *constraints, bound=bound, covariant=covariant,
contravariant=contravariant)
_DefaultMixin.__init__(self, default)

# for pickling:
try:
def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
def_mod = None
if def_mod != 'typing_extensions':
self.__module__ = def_mod


# Python 3.10+ has PEP 612
if hasattr(typing, 'ParamSpecArgs'):
ParamSpecArgs = typing.ParamSpecArgs
Expand Down Expand Up @@ -1211,12 +1248,32 @@ def __eq__(self, other):

# 3.10+
if hasattr(typing, 'ParamSpec'):
cdce8p marked this conversation as resolved.
Show resolved Hide resolved
ParamSpec = typing.ParamSpec

# Add default Parameter - PEP 696
class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True):
"""Parameter specification variable."""

__module__ = "typing"

def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
default=None):
super().__init__(name, bound=bound, covariant=covariant,
contravariant=contravariant)
_DefaultMixin.__init__(self, default)

# for pickling:
try:
def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
def_mod = None
if def_mod != 'typing_extensions':
self.__module__ = def_mod

# 3.7-3.9
else:

# Inherits from list as a workaround for Callable checks in Python < 3.9.2.
class ParamSpec(list):
class ParamSpec(list, _DefaultMixin):
"""Parameter specification variable.

Usage::
Expand Down Expand Up @@ -1274,7 +1331,8 @@ def args(self):
def kwargs(self):
return ParamSpecKwargs(self)

def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
default=None):
super().__init__([self])
self.__name__ = name
self.__covariant__ = bool(covariant)
Expand All @@ -1283,6 +1341,7 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
self.__bound__ = typing._type_check(bound, 'Bound must be a type.')
else:
self.__bound__ = None
_DefaultMixin.__init__(self, default)

# for pickling:
try:
Expand Down Expand Up @@ -1784,9 +1843,25 @@ def _is_unpack(obj):


if hasattr(typing, "TypeVarTuple"): # 3.11+
TypeVarTuple = typing.TypeVarTuple

# Add default Parameter - PEP 696
class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True):
"""Type variable tuple."""

def __init__(self, name, *, default=None):
super().__init__(name)
_DefaultMixin.__init__(self, default)

# for pickling:
try:
def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
def_mod = None
if def_mod != 'typing_extensions':
self.__module__ = def_mod

else:
class TypeVarTuple:
class TypeVarTuple(_DefaultMixin):
"""Type variable tuple.

Usage::
Expand Down Expand Up @@ -1836,8 +1911,9 @@ def get_shape(self) -> Tuple[*Ts]:
def __iter__(self):
yield self.__unpacked__

def __init__(self, name):
def __init__(self, name, *, default=None):
self.__name__ = name
_DefaultMixin.__init__(self, default)

# for pickling:
try:
Expand Down