From f282b93d282a65b6404b7f1300aba91942007895 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 3 Nov 2023 15:51:25 +0100 Subject: [PATCH 1/2] gh-111696: type(str) returns the fully qualified name * Update modules: * enum * functools * optparse * pdb * xmlrcp.server * Update tests: * test_dataclasses * test_descrtut * test_cmd_line_script --- Doc/whatsnew/3.13.rst | 5 ++ Lib/dataclasses.py | 2 +- Lib/enum.py | 2 +- Lib/functools.py | 2 +- Lib/optparse.py | 2 +- Lib/pdb.py | 2 +- Lib/test/test_cmd_line_script.py | 4 +- Lib/test/test_dataclasses/__init__.py | 4 +- Lib/test/test_descrtut.py | 10 ++-- Lib/xmlrpc/server.py | 6 +- Objects/typeobject.c | 84 ++++++++++++++++++++------- 11 files changed, 84 insertions(+), 39 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index b64cfc51f75701..66a7265db377a3 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -125,6 +125,11 @@ Other Language Changes equivalent of the :option:`-X frozen_modules <-X>` command-line option. (Contributed by Yilei Yang in :gh:`111374`.) +* Formatting a type as a string now formats the type fully qualified name, + instead of formating its representation (``). + (Contributed by Victor Stinner in :gh:`111696`.) + + New Modules =========== diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 2fba32b5ffbc1e..3249da22b6cfec 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -849,7 +849,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # indicator for mutability. Read the __hash__ attribute from the class, # not the instance. if f._field_type is _FIELD and f.default.__class__.__hash__ is None: - raise ValueError(f'mutable default {type(f.default)} for field ' + raise ValueError(f'mutable default {type(f.default)!r} for field ' f'{f.name} is not allowed: use default_factory') return f diff --git a/Lib/enum.py b/Lib/enum.py index 4e76e96d2c916e..db497f91c17759 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -729,7 +729,7 @@ def __call__(cls, value, names=None, *values, module=None, qualname=None, type=N if names is None and type is None: # no body? no data-type? possibly wrong usage raise TypeError( - f"{cls} has no members; specify `names=()` if you meant to create a new, empty, enum" + f"{cls!r} has no members; specify `names=()` if you meant to create a new, empty, enum" ) return cls._create_( class_name=value, diff --git a/Lib/functools.py b/Lib/functools.py index 55990e742bf23f..8892461d70e3d7 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -793,7 +793,7 @@ def _find_impl(cls, registry): and match not in cls.__mro__ and not issubclass(match, t)): raise RuntimeError("Ambiguous dispatch: {} or {}".format( - match, t)) + repr(match), repr(t))) break if t in registry: match = t diff --git a/Lib/optparse.py b/Lib/optparse.py index 1c450c6fcbe3b6..bd9d189d189489 100644 --- a/Lib/optparse.py +++ b/Lib/optparse.py @@ -667,7 +667,7 @@ def _check_choice(self): elif not isinstance(self.choices, (tuple, list)): raise OptionError( "choices must be a list of strings ('%s' supplied)" - % str(type(self.choices)).split("'")[1], self) + % repr(type(self.choices)).split("'")[1], self) elif self.choices is not None: raise OptionError( "must not supply choices for type %r" % self.type, self) diff --git a/Lib/pdb.py b/Lib/pdb.py index ed78d749a47fa8..231141c0ef7ae5 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1729,7 +1729,7 @@ def do_whatis(self, arg): self.message('Class %s.%s' % (value.__module__, value.__qualname__)) return # None of the above... - self.message(type(value)) + self.message(repr(type(value))) complete_whatis = _complete_expression diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 48754d5a63da3b..ce59f8138ffc11 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -153,7 +153,7 @@ def _check_import_error(self, script_exec_args, expected_msg, self.assertIn(expected_msg.encode('utf-8'), err) def test_dash_c_loader(self): - rc, out, err = assert_python_ok("-c", "print(__loader__)") + rc, out, err = assert_python_ok("-c", "print(repr(__loader__))") expected = repr(importlib.machinery.BuiltinImporter).encode("utf-8") self.assertIn(expected, out) @@ -163,7 +163,7 @@ def test_stdin_loader(self): # stdin is an interactive tty. p = spawn_python() try: - p.stdin.write(b"print(__loader__)\n") + p.stdin.write(b"print(repr(__loader__))\n") p.stdin.flush() finally: out = kill_python(p) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 272d427875ae40..94167076aa395e 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -738,7 +738,7 @@ def test_disallowed_mutable_defaults(self): with self.subTest(typ=typ): # Can't use a zero-length value. with self.assertRaisesRegex(ValueError, - f'mutable default {typ} for field ' + f'mutable default {typ!r} for field ' 'x is not allowed'): @dataclass class Point: @@ -747,7 +747,7 @@ class Point: # Nor a non-zero-length value with self.assertRaisesRegex(ValueError, - f'mutable default {typ} for field ' + f'mutable default {typ!r} for field ' 'y is not allowed'): @dataclass class Point: diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index 13e3ea41bdb76c..7d67e5004d87c3 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -38,16 +38,16 @@ def merge(self, other): Here's the new type at work: - >>> print(defaultdict) # show our type + >>> print(repr(defaultdict)) # show our type - >>> print(type(defaultdict)) # its metatype + >>> print(repr(type(defaultdict))) # its metatype >>> a = defaultdict(default=0.0) # create an instance >>> print(a) # show the instance {} - >>> print(type(a)) # show its type + >>> print(repr(type(a))) # show its type - >>> print(a.__class__) # show its class + >>> print(repr(a.__class__)) # show its class >>> print(type(a) is a.__class__) # its type is its class True @@ -261,7 +261,7 @@ def merge(self, other): >>> class C: ... @classmethod ... def foo(cls, y): - ... print("classmethod", cls, y) + ... print("classmethod", repr(cls), y) >>> C.foo(1) classmethod 1 diff --git a/Lib/xmlrpc/server.py b/Lib/xmlrpc/server.py index 4dddb1d10e08bd..0b0c9b64897726 100644 --- a/Lib/xmlrpc/server.py +++ b/Lib/xmlrpc/server.py @@ -270,7 +270,7 @@ def _marshaled_dispatch(self, data, dispatch_method = None, path = None): encoding=self.encoding) except BaseException as exc: response = dumps( - Fault(1, "%s:%s" % (type(exc), exc)), + Fault(1, "%s:%s" % (repr(type(exc)), exc)), encoding=self.encoding, allow_none=self.allow_none, ) @@ -365,7 +365,7 @@ def system_multicall(self, call_list): except BaseException as exc: results.append( {'faultCode' : 1, - 'faultString' : "%s:%s" % (type(exc), exc)} + 'faultString' : "%s:%s" % (repr(type(exc)), exc)} ) return results @@ -628,7 +628,7 @@ def _marshaled_dispatch(self, data, dispatch_method = None, path = None): # (each dispatcher should have handled their own # exceptions) response = dumps( - Fault(1, "%s:%s" % (type(exc), exc)), + Fault(1, "%s:%s" % (repr(type(exc)), exc)), encoding=self.encoding, allow_none=self.allow_none) response = response.encode(self.encoding, 'xmlcharrefreplace') return response diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 4464b5af8cd15b..e1b3f94b3386e9 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1596,37 +1596,75 @@ static PyGetSetDef type_getsets[] = { {0} }; -static PyObject * -type_repr(PyTypeObject *type) +static PyObject* +type_fullyqualname(PyTypeObject *type, int ignore_module_error) { + // type is a static type and PyType_Ready() was not called on it yet? if (type->tp_name == NULL) { - // type_repr() called before the type is fully initialized - // by PyType_Ready(). - return PyUnicode_FromFormat("", type); + PyErr_SetString(PyExc_TypeError, "static type not initialized"); + return NULL; } - PyObject *mod, *name, *rtn; + if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + // Static type + return PyUnicode_FromString(type->tp_name); + } - mod = type_module(type, NULL); - if (mod == NULL) - PyErr_Clear(); - else if (!PyUnicode_Check(mod)) { - Py_SETREF(mod, NULL); + PyObject *qualname = type_qualname(type, NULL); + if (qualname == NULL) { + return NULL; } - name = type_qualname(type, NULL); - if (name == NULL) { - Py_XDECREF(mod); + + PyObject *module = type_module(type, NULL); + if (module == NULL) { + if (ignore_module_error) { + // type_repr() ignores type_module() errors + PyErr_Clear(); + return qualname; + } + + Py_DECREF(qualname); return NULL; } - if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins))) - rtn = PyUnicode_FromFormat("", mod, name); - else - rtn = PyUnicode_FromFormat("", type->tp_name); + PyObject *result; + if (PyUnicode_Check(module) + && !_PyUnicode_Equal(module, &_Py_ID(builtins))) + { + result = PyUnicode_FromFormat("%U.%U", module, qualname); + } + else { + result = Py_NewRef(qualname); + } + Py_DECREF(module); + Py_DECREF(qualname); + return result; +} - Py_XDECREF(mod); - Py_DECREF(name); - return rtn; + +static PyObject * +type_str(PyTypeObject *type) +{ + return type_fullyqualname(type, 0); +} + + +static PyObject * +type_repr(PyTypeObject *type) +{ + if (type->tp_name == NULL) { + // If type_repr() is called before the type is fully initialized + // by PyType_Ready(), just format the type memory address. + return PyUnicode_FromFormat("", type); + } + + PyObject *fullqualname = type_fullyqualname(type, 1); + if (fullqualname == NULL) { + return NULL; + } + PyObject *result = PyUnicode_FromFormat("", fullqualname); + Py_DECREF(fullqualname); + return result; } static PyObject * @@ -4540,6 +4578,7 @@ PyType_GetQualName(PyTypeObject *type) return type_qualname(type, NULL); } + void * PyType_GetSlot(PyTypeObject *type, int slot) { @@ -5230,6 +5269,7 @@ type___sizeof___impl(PyTypeObject *self) return PyLong_FromSize_t(size); } + static PyMethodDef type_methods[] = { TYPE_MRO_METHODDEF TYPE___SUBCLASSES___METHODDEF @@ -5354,7 +5394,7 @@ PyTypeObject PyType_Type = { 0, /* tp_as_mapping */ 0, /* tp_hash */ (ternaryfunc)type_call, /* tp_call */ - 0, /* tp_str */ + (reprfunc)type_str, /* tp_str */ (getattrofunc)_Py_type_getattro, /* tp_getattro */ (setattrofunc)type_setattro, /* tp_setattro */ 0, /* tp_as_buffer */ From 365ca3dc4ec9d6dbb62e9e35a38d5c18963b1469 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 16 Nov 2023 00:04:43 +0100 Subject: [PATCH 2/2] Update test_idle --- Lib/idlelib/idle_test/test_searchbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py index 8c9c410ebaf47c..36549496f94e37 100644 --- a/Lib/idlelib/idle_test/test_searchbase.py +++ b/Lib/idlelib/idle_test/test_searchbase.py @@ -100,7 +100,7 @@ def test_make_frame(self): self.dialog.frame = Frame(self.root) frame, label = self.dialog.make_frame() self.assertEqual(label, '') - self.assertEqual(str(type(frame)), "") + self.assertEqual(repr(type(frame)), "") # self.assertIsInstance(frame, Frame) fails when test is run by # test_idle not run from IDLE editor. See issue 33987 PR.