diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 04122fbdd0ae80..4a87e2bf7a1864 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -390,7 +390,10 @@ def __deepcopy__(self, memo): aliases = [ GenericAlias(list, T), GenericAlias(deque, T), - GenericAlias(X, T) + GenericAlias(X, T), + X[T], + list[T], + deque[T], ] + _UNPACKED_TUPLES for alias in aliases: with self.subTest(alias=alias): @@ -420,10 +423,26 @@ def test_union_generic(self): self.assertEqual(a.__parameters__, (T,)) def test_dir(self): - dir_of_gen_alias = set(dir(list[int])) + ga = list[int] + dir_of_gen_alias = set(dir(ga)) self.assertTrue(dir_of_gen_alias.issuperset(dir(list))) - for generic_alias_property in ("__origin__", "__args__", "__parameters__"): - self.assertIn(generic_alias_property, dir_of_gen_alias) + for generic_alias_property in ( + "__origin__", "__args__", "__parameters__", + "__unpacked__", + ): + with self.subTest(generic_alias_property=generic_alias_property): + self.assertIn(generic_alias_property, dir_of_gen_alias) + for blocked in ( + "__bases__", + "__copy__", + "__deepcopy__", + ): + with self.subTest(blocked=blocked): + self.assertNotIn(blocked, dir_of_gen_alias) + + for entry in dir_of_gen_alias: + with self.subTest(entry=entry): + getattr(ga, entry) # must not raise `AttributeError` def test_weakref(self): for t in self.generic_types: diff --git a/Misc/NEWS.d/next/Core and Builtins/2025-09-06-13-53-33.gh-issue-105487.a43YaY.rst b/Misc/NEWS.d/next/Core and Builtins/2025-09-06-13-53-33.gh-issue-105487.a43YaY.rst new file mode 100644 index 00000000000000..968e92c94c1ffc --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2025-09-06-13-53-33.gh-issue-105487.a43YaY.rst @@ -0,0 +1 @@ +Remove non-existent :meth:`~object.__copy__`, :meth:`~object.__deepcopy__`, and :attr:`~type.__bases__` from the :meth:`~object.__dir__` entries of :class:`types.GenericAlias`. diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index efe18adb4a8d10..bf1838061e9eb1 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -626,7 +626,6 @@ ga_vectorcall(PyObject *self, PyObject *const *args, static const char* const attr_exceptions[] = { "__class__", - "__bases__", "__origin__", "__args__", "__unpacked__", @@ -635,6 +634,11 @@ static const char* const attr_exceptions[] = { "__mro_entries__", "__reduce_ex__", // needed so we don't look up object.__reduce_ex__ "__reduce__", + NULL, +}; + +static const char* const attr_blocked[] = { + "__bases__", "__copy__", "__deepcopy__", NULL, @@ -645,15 +649,29 @@ ga_getattro(PyObject *self, PyObject *name) { gaobject *alias = (gaobject *)self; if (PyUnicode_Check(name)) { + // When we check blocked attrs, we don't allow to proxy them to `__origin__`. + // Otherwise, we can break existing code. + for (const char * const *p = attr_blocked; ; p++) { + if (*p == NULL) { + break; + } + if (_PyUnicode_EqualToASCIIString(name, *p)) { + goto generic_getattr; + } + } + + // When we see own attrs, it has a priority over `__origin__`'s attr. for (const char * const *p = attr_exceptions; ; p++) { if (*p == NULL) { return PyObject_GetAttr(alias->origin, name); } if (_PyUnicode_EqualToASCIIString(name, *p)) { - break; + goto generic_getattr; } } } + +generic_getattr: return PyObject_GenericGetAttr(self, name); }