Skip to content

Commit ae19226

Browse files
gh-119180: Add evaluate functions for type params and type aliases (#122212)
1 parent cbac8a3 commit ae19226

11 files changed

+385
-159
lines changed

Include/internal/pycore_global_objects.h

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ struct _Py_interp_cached_objects {
8181
PyTypeObject *paramspec_type;
8282
PyTypeObject *paramspecargs_type;
8383
PyTypeObject *paramspeckwargs_type;
84+
PyTypeObject *constevaluator_type;
8485
};
8586

8687
#define _Py_INTERP_STATIC_OBJECT(interp, NAME) \

Include/internal/pycore_typevarobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extern PyObject *_Py_subscript_generic(PyThreadState *, PyObject *);
1616
extern PyObject *_Py_set_typeparam_default(PyThreadState *, PyObject *, PyObject *);
1717
extern int _Py_initialize_generic(PyInterpreterState *);
1818
extern void _Py_clear_generic_types(PyInterpreterState *);
19+
extern int _Py_typing_type_repr(PyUnicodeWriter *, PyObject *);
1920

2021
extern PyTypeObject _PyTypeAlias_Type;
2122
extern PyTypeObject _PyNoDefault_Type;

Lib/annotationlib.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,16 @@ def __missing__(self, key):
413413
return fwdref
414414

415415

416-
def call_annotate_function(annotate, format, owner=None):
416+
def call_evaluate_function(evaluate, format, *, owner=None):
417+
"""Call an evaluate function. Evaluate functions are normally generated for
418+
the value of type aliases and the bounds, constraints, and defaults of
419+
type parameter objects.
420+
"""
421+
return call_annotate_function(evaluate, format, owner=owner, _is_evaluate=True)
422+
423+
424+
def call_annotate_function(annotate, format, *, owner=None,
425+
_is_evaluate=False):
417426
"""Call an __annotate__ function. __annotate__ functions are normally
418427
generated by the compiler to defer the evaluation of annotations. They
419428
can be called with any of the format arguments in the Format enum, but
@@ -459,8 +468,11 @@ def call_annotate_function(annotate, format, owner=None):
459468
closure = tuple(new_closure)
460469
else:
461470
closure = None
462-
func = types.FunctionType(annotate.__code__, globals, closure=closure)
471+
func = types.FunctionType(annotate.__code__, globals, closure=closure,
472+
argdefs=annotate.__defaults__, kwdefaults=annotate.__kwdefaults__)
463473
annos = func(Format.VALUE)
474+
if _is_evaluate:
475+
return annos if isinstance(annos, str) else repr(annos)
464476
return {
465477
key: val if isinstance(val, str) else repr(val)
466478
for key, val in annos.items()
@@ -511,7 +523,8 @@ def call_annotate_function(annotate, format, owner=None):
511523
closure = tuple(new_closure)
512524
else:
513525
closure = None
514-
func = types.FunctionType(annotate.__code__, globals, closure=closure)
526+
func = types.FunctionType(annotate.__code__, globals, closure=closure,
527+
argdefs=annotate.__defaults__, kwdefaults=annotate.__kwdefaults__)
515528
result = func(Format.VALUE)
516529
for obj in globals.stringifiers:
517530
obj.__class__ = ForwardRef

Lib/test/test_annotationlib.py

+19
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,25 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
773773
)
774774

775775

776+
class TestCallEvaluateFunction(unittest.TestCase):
777+
def test_evaluation(self):
778+
def evaluate(format, exc=NotImplementedError):
779+
if format != 1:
780+
raise exc
781+
return undefined
782+
783+
with self.assertRaises(NameError):
784+
annotationlib.call_evaluate_function(evaluate, annotationlib.Format.VALUE)
785+
self.assertEqual(
786+
annotationlib.call_evaluate_function(evaluate, annotationlib.Format.FORWARDREF),
787+
annotationlib.ForwardRef("undefined"),
788+
)
789+
self.assertEqual(
790+
annotationlib.call_evaluate_function(evaluate, annotationlib.Format.SOURCE),
791+
"undefined",
792+
)
793+
794+
776795
class MetaclassTests(unittest.TestCase):
777796
def test_annotated_meta(self):
778797
class Meta(type):

Lib/test/test_type_params.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import annotationlib
12
import asyncio
23
import textwrap
34
import types
@@ -6,7 +7,7 @@
67
import weakref
78
from test.support import requires_working_socket, check_syntax_error, run_code
89

9-
from typing import Generic, NoDefault, Sequence, TypeVar, TypeVarTuple, ParamSpec, get_args
10+
from typing import Generic, NoDefault, Sequence, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, get_args
1011

1112

1213
class TypeParamsInvalidTest(unittest.TestCase):
@@ -1394,3 +1395,43 @@ def test_symtable_key_regression_name(self):
13941395

13951396
self.assertEqual(ns["X1"].__type_params__[0].__default__, "A")
13961397
self.assertEqual(ns["X2"].__type_params__[0].__default__, "B")
1398+
1399+
1400+
class TestEvaluateFunctions(unittest.TestCase):
1401+
def test_general(self):
1402+
type Alias = int
1403+
Alias2 = TypeAliasType("Alias2", int)
1404+
def f[T: int = int, **P = int, *Ts = int](): pass
1405+
T, P, Ts = f.__type_params__
1406+
T2 = TypeVar("T2", bound=int, default=int)
1407+
P2 = ParamSpec("P2", default=int)
1408+
Ts2 = TypeVarTuple("Ts2", default=int)
1409+
cases = [
1410+
Alias.evaluate_value,
1411+
Alias2.evaluate_value,
1412+
T.evaluate_bound,
1413+
T.evaluate_default,
1414+
P.evaluate_default,
1415+
Ts.evaluate_default,
1416+
T2.evaluate_bound,
1417+
T2.evaluate_default,
1418+
P2.evaluate_default,
1419+
Ts2.evaluate_default,
1420+
]
1421+
for case in cases:
1422+
with self.subTest(case=case):
1423+
self.assertIs(case(1), int)
1424+
self.assertIs(annotationlib.call_evaluate_function(case, annotationlib.Format.VALUE), int)
1425+
self.assertIs(annotationlib.call_evaluate_function(case, annotationlib.Format.FORWARDREF), int)
1426+
self.assertEqual(annotationlib.call_evaluate_function(case, annotationlib.Format.SOURCE), 'int')
1427+
1428+
def test_constraints(self):
1429+
def f[T: (int, str)](): pass
1430+
T, = f.__type_params__
1431+
T2 = TypeVar("T2", int, str)
1432+
for case in [T, T2]:
1433+
with self.subTest(case=case):
1434+
self.assertEqual(case.evaluate_constraints(1), (int, str))
1435+
self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.VALUE), (int, str))
1436+
self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.FORWARDREF), (int, str))
1437+
self.assertEqual(annotationlib.call_evaluate_function(case.evaluate_constraints, annotationlib.Format.SOURCE), '(int, str)')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
As part of :pep:`749`, add the following attributes for customizing
2+
evaluation of annotation scopes:
3+
4+
* ``evaluate_value`` on :class:`typing.TypeAliasType`
5+
* ``evaluate_bound``, ``evaluate_constraints``, and ``evaluate_default`` on :class:`typing.TypeVar`
6+
* ``evaluate_default`` on :class:`typing.ParamSpec`
7+
* ``evaluate_default`` on :class:`typing.TypeVarTuple`

Objects/genericaliasobject.c

+4-66
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pycore_ceval.h" // _PyEval_GetBuiltin()
55
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
66
#include "pycore_object.h"
7+
#include "pycore_typevarobject.h" // _Py_typing_type_repr
78
#include "pycore_unionobject.h" // _Py_union_type_or, _PyGenericAlias_Check
89

910

@@ -50,69 +51,6 @@ ga_traverse(PyObject *self, visitproc visit, void *arg)
5051
return 0;
5152
}
5253

53-
static int
54-
ga_repr_item(PyUnicodeWriter *writer, PyObject *p)
55-
{
56-
PyObject *qualname = NULL;
57-
PyObject *module = NULL;
58-
int rc;
59-
60-
if (p == Py_Ellipsis) {
61-
// The Ellipsis object
62-
rc = PyUnicodeWriter_WriteUTF8(writer, "...", 3);
63-
goto done;
64-
}
65-
66-
if ((rc = PyObject_HasAttrWithError(p, &_Py_ID(__origin__))) > 0 &&
67-
(rc = PyObject_HasAttrWithError(p, &_Py_ID(__args__))) > 0)
68-
{
69-
// It looks like a GenericAlias
70-
goto use_repr;
71-
}
72-
if (rc < 0) {
73-
goto error;
74-
}
75-
76-
if (PyObject_GetOptionalAttr(p, &_Py_ID(__qualname__), &qualname) < 0) {
77-
goto error;
78-
}
79-
if (qualname == NULL) {
80-
goto use_repr;
81-
}
82-
if (PyObject_GetOptionalAttr(p, &_Py_ID(__module__), &module) < 0) {
83-
goto error;
84-
}
85-
if (module == NULL || module == Py_None) {
86-
goto use_repr;
87-
}
88-
89-
// Looks like a class
90-
if (PyUnicode_Check(module) &&
91-
_PyUnicode_EqualToASCIIString(module, "builtins"))
92-
{
93-
// builtins don't need a module name
94-
rc = PyUnicodeWriter_WriteStr(writer, qualname);
95-
goto done;
96-
}
97-
else {
98-
rc = PyUnicodeWriter_Format(writer, "%S.%S", module, qualname);
99-
goto done;
100-
}
101-
102-
error:
103-
rc = -1;
104-
goto done;
105-
106-
use_repr:
107-
rc = PyUnicodeWriter_WriteRepr(writer, p);
108-
goto done;
109-
110-
done:
111-
Py_XDECREF(qualname);
112-
Py_XDECREF(module);
113-
return rc;
114-
}
115-
11654
static int
11755
ga_repr_items_list(PyUnicodeWriter *writer, PyObject *p)
11856
{
@@ -131,7 +69,7 @@ ga_repr_items_list(PyUnicodeWriter *writer, PyObject *p)
13169
}
13270
}
13371
PyObject *item = PyList_GET_ITEM(p, i);
134-
if (ga_repr_item(writer, item) < 0) {
72+
if (_Py_typing_type_repr(writer, item) < 0) {
13573
return -1;
13674
}
13775
}
@@ -162,7 +100,7 @@ ga_repr(PyObject *self)
162100
goto error;
163101
}
164102
}
165-
if (ga_repr_item(writer, alias->origin) < 0) {
103+
if (_Py_typing_type_repr(writer, alias->origin) < 0) {
166104
goto error;
167105
}
168106
if (PyUnicodeWriter_WriteChar(writer, '[') < 0) {
@@ -181,7 +119,7 @@ ga_repr(PyObject *self)
181119
goto error;
182120
}
183121
}
184-
else if (ga_repr_item(writer, p) < 0) {
122+
else if (_Py_typing_type_repr(writer, p) < 0) {
185123
goto error;
186124
}
187125
}

0 commit comments

Comments
 (0)