Skip to content

gh-87390: Fix starred tuple equality and pickling #92249

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

Closed
wants to merge 5 commits into from
Closed
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
27 changes: 24 additions & 3 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ def test_equality(self):
self.assertEqual(list[int], list[int])
self.assertEqual(dict[str, int], dict[str, int])
self.assertEqual((*tuple[int],)[0], (*tuple[int],)[0])
self.assertNotEqual(tuple[int], (*tuple[int],)[0])
self.assertNotEqual(tuple[int], Unpack[tuple[int]])
self.assertEqual(
tuple[
tuple( # Effectively the same as starring; TODO
Expand Down Expand Up @@ -394,6 +396,9 @@ def test_pickle(self):
self.assertEqual(loaded.__origin__, alias.__origin__)
self.assertEqual(loaded.__args__, alias.__args__)
self.assertEqual(loaded.__parameters__, alias.__parameters__)
if isinstance(alias, GenericAlias):
self.assertEqual(loaded.__unpacked__, alias.__unpacked__)


def test_copy(self):
class X(list):
Expand All @@ -418,12 +423,28 @@ def __deepcopy__(self, memo):
self.assertEqual(copied.__args__, alias.__args__)
self.assertEqual(copied.__parameters__, alias.__parameters__)

def test_non_bool_to_third_constructor_argument_raises_typeerror(self):
with self.assertRaises(TypeError):
GenericAlias(tuple, int, 0)
with self.assertRaises(TypeError):
GenericAlias(tuple, int, 'foo')
with self.assertRaises(TypeError):
GenericAlias(tuple, int, list)

def test_unpack(self):
alias = tuple[str, ...]
self.assertIs(alias.__unpacked__, False)
unpacked = (*alias,)[0]
alias1 = tuple[str, ...]
self.assertIs(alias1.__unpacked__, False)
unpacked = (*alias1,)[0]
self.assertIs(unpacked.__unpacked__, True)

# The (optional) third positional argument should control unpackedness.
alias2 = GenericAlias(tuple, int)
self.assertIs(alias2.__unpacked__, False)
alias3 = GenericAlias(tuple, int, False)
self.assertIs(alias3.__unpacked__, False)
alias4 = GenericAlias(tuple, int, True)
self.assertIs(alias4.__unpacked__, True)

def test_union(self):
a = typing.Union[list[int], list[str]]
self.assertEqual(a.__args__, (list[int], list[str]))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Give ``types.GenericAlias`` constructor an optional third ``bool`` argument that controls whether it represents an unpacked type (e.g. ``*tuple[int, str]``).
38 changes: 32 additions & 6 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ ga_richcompare(PyObject *a, PyObject *b, int op)

gaobject *aa = (gaobject *)a;
gaobject *bb = (gaobject *)b;
if (aa->starred != bb->starred) {
Py_RETURN_FALSE;
}
int eq = PyObject_RichCompareBool(aa->origin, bb->origin, Py_EQ);
if (eq < 0) {
return NULL;
Expand Down Expand Up @@ -604,8 +607,13 @@ static PyObject *
ga_reduce(PyObject *self, PyObject *Py_UNUSED(ignored))
{
gaobject *alias = (gaobject *)self;
return Py_BuildValue("O(OO)", Py_TYPE(alias),
alias->origin, alias->args);
PyObject *starred = PyBool_FromLong(alias->starred);
PyObject *value = Py_BuildValue("O(OOO)", Py_TYPE(alias),
alias->origin, alias->args, starred);
// Avoid double increment of reference count on Py_True/Py_False - once from
// PyBool_FromLong, and once from Py_BuildValue.
Py_CLEAR(starred);
return value;
Comment on lines +610 to +616
Copy link
Member

Choose a reason for hiding this comment

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

How about:

Suggested change
PyObject *starred = PyBool_FromLong(alias->starred);
PyObject *value = Py_BuildValue("O(OOO)", Py_TYPE(alias),
alias->origin, alias->args, starred);
// Avoid double increment of reference count on Py_True/Py_False - once from
// PyBool_FromLong, and once from Py_BuildValue.
Py_CLEAR(starred);
return value;
PyObject *starred = alias->starred ? Py_True : Py_False;
return Py_BuildValue("O(OOO)", Py_TYPE(alias),
alias->origin, alias->args, starred);

BTW, if you don't want to use the code above, you can avoid the double incref by using the format string O(OON). N doesn't incref. https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue

}

static PyObject *
Expand Down Expand Up @@ -685,7 +693,7 @@ static PyGetSetDef ga_properties[] = {
* Returns 1 on success, 0 on failure.
*/
static inline int
setup_ga(gaobject *alias, PyObject *origin, PyObject *args) {
setup_ga(gaobject *alias, PyObject *origin, PyObject *args, bool starred) {
if (!PyTuple_Check(args)) {
args = PyTuple_Pack(1, args);
if (args == NULL) {
Expand All @@ -700,6 +708,7 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) {
alias->origin = origin;
alias->args = args;
alias->parameters = NULL;
alias->starred = starred;
alias->weakreflist = NULL;

if (PyVectorcall_Function(origin) != NULL) {
Expand All @@ -718,16 +727,33 @@ ga_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
if (!_PyArg_NoKeywords("GenericAlias", kwds)) {
return NULL;
}
if (!_PyArg_CheckPositional("GenericAlias", PyTuple_GET_SIZE(args), 2, 2)) {
if (!_PyArg_CheckPositional("GenericAlias", PyTuple_GET_SIZE(args), 2, 3)) {
Copy link
Member

Choose a reason for hiding this comment

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

Please update the creation signature in the docs https://docs.python.org/3/library/types.html#types.GenericAlias.

Also, this doesn't break backwards compatibility right? IIUC, the two-argument form will still work.

return NULL;
}

PyObject *origin = PyTuple_GET_ITEM(args, 0);
PyObject *arguments = PyTuple_GET_ITEM(args, 1);

Comment on lines 734 to +736
Copy link
Member

Choose a reason for hiding this comment

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

This code has gotten complex enough that I recommend using https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTuple or https://docs.python.org/3/c-api/arg.html#c.PyArg_UnpackTuple instead to parse our arguments for us.

The format value list is on that page. But off the top of my head, it should be OO|O:GenericAlias. Then you keep your bool checking/error raising code below.

Copy link
Contributor

Choose a reason for hiding this comment

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

Argument clinic can also be used here.

bool starred;
if (PyTuple_GET_SIZE(args) < 3) {
starred = false;
} else {
PyObject *py_starred = PyTuple_GET_ITEM(args, 2);
if (!PyBool_Check(py_starred)) {
PyErr_SetString(PyExc_TypeError,
"Third argument to constructor of GenericAlias "
"must be a bool (since it's a flag controlling "
"whether the alias is unpacked)");
Comment on lines +745 to +746
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"must be a bool (since it's a flag controlling "
"whether the alias is unpacked)");
"must be a bool");

We don't need the explanation.

return NULL;
}
starred = PyLong_AsLong(py_starred);
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised you don't need to cast to (bool) here (or maybe it's implicit, I don't remember how C treats this).

}

gaobject *self = (gaobject *)type->tp_alloc(type, 0);
if (self == NULL) {
return NULL;
}
if (!setup_ga(self, origin, arguments)) {
if (!setup_ga(self, origin, arguments, starred)) {
Py_DECREF(self);
return NULL;
}
Expand Down Expand Up @@ -837,7 +863,7 @@ Py_GenericAlias(PyObject *origin, PyObject *args)
if (alias == NULL) {
return NULL;
}
if (!setup_ga(alias, origin, args)) {
if (!setup_ga(alias, origin, args, false)) {
Py_DECREF(alias);
return NULL;
}
Expand Down