Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Allow type parameters without default values to follow those with default values in some situations #392

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

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions 4 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
at runtime rather than `types.NoneType`.
- Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python
3.13.0b1 and newer.
- Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774),
allowing type parameters without default values to follow those with
default values in some type parameter lists. Patch by Alex Waygood,
backporting a CPython PR by Jelle Zijlstra.
- It is now disallowed to use a `TypeVar` with a default value after a
`TypeVarTuple` in a type parameter list. This matches the CPython
implementation of PEP 696 on Python 3.13+.
Expand Down
19 changes: 19 additions & 0 deletions 19 src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6457,6 +6457,25 @@ def test_pickle(self):
self.assertEqual(z.__bound__, typevar.__bound__)
self.assertEqual(z.__default__, typevar.__default__)

@skip_if_py313_beta_1
def test_allow_default_after_non_default_in_alias(self):
T_default = TypeVar('T_default', default=int)
T = TypeVar('T')
Ts = TypeVarTuple('Ts')

a1 = Callable[[T_default], T]
self.assertEqual(a1.__args__, (T_default, T))

if sys.version_info >= (3, 9):
a2 = dict[T_default, T]
self.assertEqual(a2.__args__, (T_default, T))

a3 = typing.Dict[T_default, T]
self.assertEqual(a3.__args__, (T_default, T))

a4 = Callable[[Unpack[Ts]], T]
self.assertEqual(a4.__args__, (Unpack[Ts], T))


class NoDefaultTests(BaseTestCase):
@skip_if_py313_beta_1
Expand Down
70 changes: 48 additions & 22 deletions 70 src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2857,6 +2857,18 @@ def _check_generic(cls, parameters, elen):
typing._check_generic = _check_generic


def _has_generic_or_protocol_as_origin() -> bool:
try:
frame = sys._getframe(2)
# not all platforms have sys._getframe()
except AttributeError:
return False # err on the side of leniency
else:
return frame.f_locals.get("origin") in {
typing.Generic, Protocol, typing.Protocol
}


_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)}


Expand All @@ -2882,23 +2894,29 @@ def _collect_type_vars(types, typevar_types=None):
if typevar_types is None:
typevar_types = typing.TypeVar
tvars = []
# required TypeVarLike cannot appear after TypeVarLike with default

# A required TypeVarLike cannot appear after a TypeVarLike with a default
# if it was a direct call to `Generic[]` or `Protocol[]`
enforce_default_ordering = _has_generic_or_protocol_as_origin()
default_encountered = False
# or after TypeVarTuple

# Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
type_var_tuple_encountered = False

for t in types:
if _is_unpacked_typevartuple(t):
type_var_tuple_encountered = True
elif isinstance(t, typevar_types) and t not in tvars:
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
if has_default:
if type_var_tuple_encountered:
raise TypeError('Type parameter with a default'
' follows TypeVarTuple')
default_encountered = True
elif default_encountered:
raise TypeError(f'Type parameter {t!r} without a default'
' follows type parameter with a default')
if enforce_default_ordering:
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
if has_default:
if type_var_tuple_encountered:
raise TypeError('Type parameter with a default'
' follows TypeVarTuple')
default_encountered = True
elif default_encountered:
raise TypeError(f'Type parameter {t!r} without a default'
' follows type parameter with a default')

tvars.append(t)
if _should_collect_from_parameters(t):
Expand All @@ -2916,10 +2934,15 @@ def _collect_parameters(args):
assert _collect_parameters((T, Callable[P, T])) == (T, P)
"""
parameters = []
# required TypeVarLike cannot appear after TypeVarLike with default

# A required TypeVarLike cannot appear after a TypeVarLike with default
# if it was a direct call to `Generic[]` or `Protocol[]`
enforce_default_ordering = _has_generic_or_protocol_as_origin()
default_encountered = False
# or after TypeVarTuple

# Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
type_var_tuple_encountered = False

for t in args:
if isinstance(t, type):
# We don't want __parameters__ descriptor of a bare Python class.
Expand All @@ -2933,17 +2956,20 @@ def _collect_parameters(args):
parameters.append(collected)
elif hasattr(t, '__typing_subst__'):
if t not in parameters:
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
if enforce_default_ordering:
has_default = (
getattr(t, '__default__', NoDefault) is not NoDefault
)

if type_var_tuple_encountered and has_default:
raise TypeError('Type parameter with a default'
' follows TypeVarTuple')
if type_var_tuple_encountered and has_default:
raise TypeError('Type parameter with a default'
' follows TypeVarTuple')

if has_default:
default_encountered = True
elif default_encountered:
raise TypeError(f'Type parameter {t!r} without a default'
' follows type parameter with a default')
if has_default:
default_encountered = True
elif default_encountered:
raise TypeError(f'Type parameter {t!r} without a default'
' follows type parameter with a default')

parameters.append(t)
else:
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.