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

Base implementation of PEP 696 #2

Draft
wants to merge 12 commits into
base: pep696
Choose a base branch
from
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