Skip to content

gh-104600: Make function.__type_params__ writable #104601

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 3 commits into from
May 18, 2023
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
2 changes: 1 addition & 1 deletion Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# wrapper functions that can handle naive introspection

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
'__annotations__', '__type_params__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_funcattrs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import textwrap
import types
import typing
import unittest


Expand Down Expand Up @@ -190,6 +191,20 @@ def test___qualname__(self):
# __qualname__ must be a string
self.cannot_set_attr(self.b, '__qualname__', 7, TypeError)

def test___type_params__(self):
def generic[T](): pass
def not_generic(): pass
T, = generic.__type_params__
self.assertIsInstance(T, typing.TypeVar)
self.assertEqual(generic.__type_params__, (T,))
self.assertEqual(not_generic.__type_params__, ())
with self.assertRaises(TypeError):
del not_generic.__type_params__
with self.assertRaises(TypeError):
not_generic.__type_params__ = 42
not_generic.__type_params__ = (T,)
self.assertEqual(not_generic.__type_params__, (T,))

def test___code__(self):
num_one, num_two = 7, 8
def a(): pass
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ def check_wrapper(self, wrapper, wrapped,


def _default_update(self):
def f(a:'This is a new annotation'):
def f[T](a:'This is a new annotation'):
"""This is a test"""
pass
f.attr = 'This is also a test'
Expand All @@ -630,12 +630,14 @@ def wrapper(b:'This is the prior annotation'):
def test_default_update(self):
wrapper, f = self._default_update()
self.check_wrapper(wrapper, f)
T, = f.__type_params__
self.assertIs(wrapper.__wrapped__, f)
self.assertEqual(wrapper.__name__, 'f')
self.assertEqual(wrapper.__qualname__, f.__qualname__)
self.assertEqual(wrapper.attr, 'This is also a test')
self.assertEqual(wrapper.__annotations__['a'], 'This is a new annotation')
self.assertNotIn('b', wrapper.__annotations__)
self.assertEqual(wrapper.__type_params__, (T,))

@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_type_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,5 +843,5 @@ def func[A]():
func.__type_params__ = ()
"""

with self.assertRaisesRegex(AttributeError, "attribute '__type_params__' of 'function' objects is not writable"):
run_code(code)
ns = run_code(code)
self.assertEqual(ns["func"].__type_params__, ())
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`functools.update_wrapper` now sets the ``__type_params__`` attribute
(added by :pep:`695`).
17 changes: 16 additions & 1 deletion Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,20 @@ func_get_type_params(PyFunctionObject *op, void *Py_UNUSED(ignored))
return Py_NewRef(op->func_typeparams);
}

static int
func_set_type_params(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
{
/* Not legal to del f.__type_params__ or to set it to anything
* other than a tuple object. */
if (value == NULL || !PyTuple_Check(value)) {
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to use PyTuple_CheckExact instead of PyTuple_Check? With PyTuple_Check we can pass to __type_params__ subclass of tuple. May it break something?

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't really use the __type_params__ in the interpreter, so there's low risk. The other attributes also allow subclasses (e.g. check func_set_defaults and func_set_name).

PyErr_SetString(PyExc_TypeError,
"__type_params__ must be set to a tuple");
return -1;
}
Py_XSETREF(op->func_typeparams, Py_NewRef(value));
return 0;
}

PyObject *
_Py_set_function_type_params(PyThreadState *Py_UNUSED(ignored), PyObject *func,
PyObject *type_params)
Expand All @@ -687,7 +701,8 @@ static PyGetSetDef func_getsetlist[] = {
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
{"__name__", (getter)func_get_name, (setter)func_set_name},
{"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname},
{"__type_params__", (getter)func_get_type_params, NULL},
{"__type_params__", (getter)func_get_type_params,
(setter)func_set_type_params},
{NULL} /* Sentinel */
};

Expand Down