Skip to content
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__contains__)
STRUCT_FOR_ID(__copy__)
STRUCT_FOR_ID(__ctypes_from_outparam__)
STRUCT_FOR_ID(__default__)
STRUCT_FOR_ID(__del__)
STRUCT_FOR_ID(__delattr__)
STRUCT_FOR_ID(__delete__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,14 @@ def test_union_parameter_chaining(self):
self.assertEqual((list[T] | list[S])[int, T], list[int] | list[T])
self.assertEqual((list[T] | list[S])[int, int], list[int])

def test_union_parameter_default_ordering(self):
T = typing.TypeVar("T")
U = typing.TypeVar("U", default=int)

self.assertEqual((list[U] | list[T]).__parameters__, (U, T))
Copy link
Owner

Choose a reason for hiding this comment

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

Won't this raise TypeError as the next test asserts?

with self.assertRaises(TypeError):
list[U] | list[T]

def test_union_parameter_substitution(self):
def eq(actual, expected, typed=True):
self.assertEqual(actual, expected)
Expand Down Expand Up @@ -996,6 +1004,18 @@ def __eq__(self, other):
with self.assertRaises(TypeError):
issubclass(int, type_)

def test_generic_alias_subclass_with_defaults(self):
T = typing.TypeVar("T")
U = typing.TypeVar("U", default=int)
class MyGeneric:
__class_getitem__ = classmethod(types.GenericAlias)

class Fine(MyGeneric[T, U]):
...

with self.assertRaises(TypeError):
class NonDefaultFollows(MyGeneric[U, T]): ...

def test_or_type_operator_with_bad_module(self):
class BadMeta(type):
__qualname__ = 'TypeVar'
Expand Down
24 changes: 21 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,22 @@ def test_bound_errors(self):
with self.assertRaises(TypeError):
TypeVar('X', str, float, bound=Employee)

def test_default_error(self):
with self.assertRaises(TypeError):
TypeVar('X', default=Union)

def test_default_ordering(self):
T = TypeVar("T")
U = TypeVar("U", default=int)
V = TypeVar("V", default=float)

class Foo(Generic[T, U]): ...
with self.assertRaises(TypeError):
class Bar(Generic[U, T]): ...

class Baz(Foo[V]): ...


def test_missing__name__(self):
# See bpo-39942
code = ("import typing\n"
Expand Down Expand Up @@ -3971,22 +3987,24 @@ def test_immutability_by_copy_and_pickle(self):
TPB = TypeVar('TPB', bound=int)
TPV = TypeVar('TPV', bytes, str)
PP = ParamSpec('PP')
for X in [TP, TPB, TPV, PP,
TD = TypeVar('TD', default=int)
for X in [TP, TPB, TPV, PP, TD,
List, typing.Mapping, ClassVar, typing.Iterable,
Union, Any, Tuple, Callable]:
with self.subTest(thing=X):
self.assertIs(copy(X), X)
self.assertIs(deepcopy(X), X)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
self.assertIs(pickle.loads(pickle.dumps(X, proto)), X)
del TP, TPB, TPV, PP
del TP, TPB, TPV, PP, TD

# Check that local type variables are copyable.
TL = TypeVar('TL')
TLB = TypeVar('TLB', bound=int)
TLV = TypeVar('TLV', bytes, str)
PL = ParamSpec('PL')
for X in [TL, TLB, TLV, PL]:
TDL = TypeVar('TDL', default=int)
for X in [TL, TLB, TLV, PL, TDL]:
with self.subTest(thing=X):
self.assertIs(copy(X), X)
self.assertIs(deepcopy(X), X)
Expand Down
15 changes: 15 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def _collect_parameters(args):
_collect_parameters((T, Callable[P, T])) == (T, P)
"""
parameters = []
seen_default = False
for t in args:
if isinstance(t, type):
# We don't want __parameters__ descriptor of a bare Python class.
Expand All @@ -273,10 +274,24 @@ def _collect_parameters(args):
parameters.append(collected)
elif hasattr(t, '__typing_subst__'):
if t not in parameters:
if hasattr(t, "__default__") and t.__default__ is not None:
seen_default = True
elif seen_default:
raise TypeError(
f"non-default type parameter {t!r} follows default type parameter"
)

parameters.append(t)
else:
for x in getattr(t, '__parameters__', ()):
if x not in parameters:
if hasattr(x, "__default__") and x.__default__ is not None:
seen_default = True
elif seen_default:
raise TypeError(
f"non-default type parameter {x!r} follows default type parameter"
)

parameters.append(x)
return tuple(parameters)

Expand Down
38 changes: 38 additions & 0 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ _Py_make_parameters(PyObject *args)
if (parameters == NULL)
return NULL;
Py_ssize_t iparam = 0;
bool seen_default = false;
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *t = PyTuple_GET_ITEM(args, iarg);
PyObject *subst;
Expand All @@ -226,6 +227,25 @@ _Py_make_parameters(PyObject *args)
return NULL;
}
if (subst) {
PyObject *default_;
bool does_not_contain = tuple_index(parameters, nargs, t) == -1;
if (does_not_contain) {
if (_PyObject_LookupAttr(t, &_Py_ID(__default__), &default_) < 0) {
Py_DECREF(subst);
return NULL;
}
if (!Py_IsNone(default_)) {
seen_default = true;
} else if (seen_default) {
return PyErr_Format(
PyExc_TypeError,
"non-default type parameter %R follows default type parameter",
t
);
}
Py_DECREF(default_);
}

iparam += tuple_add(parameters, iparam, t);
Py_DECREF(subst);
}
Expand All @@ -248,7 +268,25 @@ _Py_make_parameters(PyObject *args)
}
}
for (Py_ssize_t j = 0; j < len2; j++) {
PyObject *default_;
PyObject *t2 = PyTuple_GET_ITEM(subparams, j);
bool does_not_contain = tuple_index(parameters, nargs, t2) == -1;
if (does_not_contain) {
if (_PyObject_LookupAttr(t2, &_Py_ID(__default__), &default_) < 0) {
Py_DECREF(subst);
return NULL;
}
if (default_ && !Py_IsNone(default_)) {
seen_default = true;
} else if (seen_default) {
return PyErr_Format(
PyExc_TypeError,
"non-default type parameter %R follows default type parameter",
t2
);
}
Py_DECREF(default_);
}
iparam += tuple_add(parameters, iparam, t2);
}
}
Expand Down