diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 7838f1e487707d..6a448207ed2d12 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -610,6 +610,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__contains__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__copy__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__ctypes_from_outparam__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__default__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__del__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__delattr__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__delete__)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 1e0ec2b7c0e915..54f087544a26cd 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -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__) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 7f9f49734c1691..1d63ffb744ad3b 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -604,6 +604,7 @@ extern "C" { INIT_ID(__contains__), \ INIT_ID(__copy__), \ INIT_ID(__ctypes_from_outparam__), \ + INIT_ID(__default__), \ INIT_ID(__del__), \ INIT_ID(__delattr__), \ INIT_ID(__delete__), \ diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 89548100da62d7..a936526b740516 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -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)) + with self.assertRaises(TypeError): + list[U] | list[T] + def test_union_parameter_substitution(self): def eq(actual, expected, typed=True): self.assertEqual(actual, expected) @@ -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' diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 881be2477f0413..40d48e224de35d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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" @@ -3971,7 +3987,8 @@ 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): @@ -3979,14 +3996,15 @@ def test_immutability_by_copy_and_pickle(self): 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) diff --git a/Lib/typing.py b/Lib/typing.py index 02a194f3959bb6..ee15d7a595cafc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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. @@ -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) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 888cb16edd1b46..81ca783d77db4e 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -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; @@ -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); } @@ -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); } }