Skip to content

Commit

Permalink
pythongh-111696: Add %T format to PyUnicode_FromFormat()
Browse files Browse the repository at this point in the history
* Add "%T" and "%#T" formats to PyUnicode_FromFormat().
* Add type.__fullyqualname__ read-only attribute.
  • Loading branch information
vstinner committed Nov 4, 2023
1 parent f62c7cc commit 50923bf
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 22 deletions.
16 changes: 16 additions & 0 deletions Doc/c-api/unicode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ APIs:
| ``-`` | The converted value is left adjusted (overrides the ``0`` |
| | flag if both are given). |
+-------+-------------------------------------------------------------+
| ``#`` | Alternate form |
+-------+-------------------------------------------------------------+
The length modifiers for following integer conversions (``d``, ``i``,
``o``, ``u``, ``x``, or ``X``) specify the type of the argument
Expand Down Expand Up @@ -518,6 +520,17 @@ APIs:
- :c:expr:`PyObject*`
- The result of calling :c:func:`PyObject_Repr`.
* - ``T``
- :c:expr:`PyObject*`
- Get the name of an object type (``type.__name__``): the result of calling
``PyType_GetName(Py_TYPE(obj))``.
* - ``#T``
- :c:expr:`PyObject*`
- Get the fully qualified name of an object type
(:attr:`class.__fullyqualname__`): the result of calling
``PyObject_GetAttrString(Py_TYPE(obj), "__fullyqualname__")``.
.. note::
The width formatter unit is number of characters rather than bytes.
The precision formatter unit is number of bytes or :c:type:`wchar_t`
Expand Down Expand Up @@ -553,6 +566,9 @@ APIs:
In previous versions it caused all the rest of the format string to be
copied as-is to the result string, and any extra arguments discarded.
.. versionchanged:: 3.13
Support for ``"%T"`` and ``"%#T"`` added.
.. c:function:: PyObject* PyUnicode_FromFormatV(const char *format, va_list vargs)
Expand Down
10 changes: 10 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5493,6 +5493,16 @@ types, where they are relevant. Some of these are not reported by the
.. versionadded:: 3.3


.. attribute:: class.__fullyqualname__

The fully qualified name of the class instance:
``f"{class.__module__}.{class.__qualname__}"``, or
``f"{class.__qualname__}"`` if ``class.__module__`` is equal to
``"builtins"``.

.. versionadded:: 3.13


.. attribute:: definition.__type_params__

The :ref:`type parameters <type-params>` of generic classes, functions,
Expand Down
14 changes: 14 additions & 0 deletions Doc/library/string.rst
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,20 @@ The available presentation types for :class:`float` and
| | as altered by the other format modifiers. |
+---------+----------------------------------------------------------+

The available presentation types for :class:`type` values are:

+----------+----------------------------------------------------------+
| Type | Meaning |
+==========+==========================================================+
| ``'T'`` | Format the type name (``type.__name__``). |
+----------+----------------------------------------------------------+
| ``'#T'`` | Format the type fully qualified name |
| | (:attr:`class.__fullyqualname__`). |
+----------+----------------------------------------------------------+

.. versionchanged:: 3.13
Add ``T`` and ``T#`` formats for :class:`type` values.


.. _formatexamples:

Expand Down
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.)

* Add :attr:`__fullyqualname__ <class.__fullyqualname__>` read-only attribute
to types: the fully qualified type name.
(Contributed by Victor Stinner in :gh:`111696`.)


New Modules
===========

Expand Down Expand Up @@ -1127,6 +1132,12 @@ New Features
* Add :c:func:`PyUnicode_AsUTF8` function to the limited C API.
(Contributed by Victor Stinner in :gh:`111089`.)

* Add support for ``"%T"`` and ``"%#T"`` formats to
:c:func:`PyUnicode_FromFormat`: ``"%T"`` formats the name of an object type
(``type.__name__``) and ``"%#T"`` formats the fully qualifed name of an
object type (:attr:`class.__fullyqualname__`).
(Contributed by Victor Stinner in :gh:`111696`.)


Porting to Python 3.13
----------------------
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ extern PyTypeObject _PyBufferWrapper_Type;
extern PyObject* _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj,
PyObject *name, int *meth_found);

extern PyObject* _PyType_GetFullyQualName(
PyTypeObject *type,
int ignore_module_error);

#ifdef __cplusplus
}
#endif
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,7 @@ def test_new_type(self):
self.assertEqual(A.__name__, 'A')
self.assertEqual(A.__qualname__, 'A')
self.assertEqual(A.__module__, __name__)
self.assertEqual(A.__fullyqualname__, f'{__name__}.A')
self.assertEqual(A.__bases__, (object,))
self.assertIs(A.__base__, object)
x = A()
Expand All @@ -2443,6 +2444,7 @@ def ham(self):
self.assertEqual(C.__name__, 'C')
self.assertEqual(C.__qualname__, 'C')
self.assertEqual(C.__module__, __name__)
self.assertEqual(C.__fullyqualname__, f'{__name__}.C')
self.assertEqual(C.__bases__, (B, int))
self.assertIs(C.__base__, int)
self.assertIn('spam', C.__dict__)
Expand All @@ -2468,6 +2470,7 @@ def test_type_name(self):
self.assertEqual(A.__name__, name)
self.assertEqual(A.__qualname__, name)
self.assertEqual(A.__module__, __name__)
self.assertEqual(A.__fullyqualname__, f'{__name__}.{name}')
with self.assertRaises(ValueError):
type('A\x00B', (), {})
with self.assertRaises(UnicodeEncodeError):
Expand All @@ -2482,6 +2485,7 @@ def test_type_name(self):
self.assertEqual(C.__name__, name)
self.assertEqual(C.__qualname__, 'C')
self.assertEqual(C.__module__, __name__)
self.assertEqual(C.__fullyqualname__, f'{__name__}.C')

A = type('C', (), {})
with self.assertRaises(ValueError):
Expand All @@ -2499,13 +2503,15 @@ def test_type_qualname(self):
self.assertEqual(A.__name__, 'A')
self.assertEqual(A.__qualname__, 'B.C')
self.assertEqual(A.__module__, __name__)
self.assertEqual(A.__fullyqualname__, f'{__name__}.B.C')
with self.assertRaises(TypeError):
type('A', (), {'__qualname__': b'B'})
self.assertEqual(A.__qualname__, 'B.C')

A.__qualname__ = 'D.E'
self.assertEqual(A.__name__, 'A')
self.assertEqual(A.__qualname__, 'D.E')
self.assertEqual(A.__fullyqualname__, f'{__name__}.D.E')
with self.assertRaises(TypeError):
A.__qualname__ = b'B'
self.assertEqual(A.__qualname__, 'D.E')
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_capi/test_unicode.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,24 @@ def check_format(expected, format, *args):
check_format('xyz',
b'%V', None, b'xyz')

# test %T and %#T
check_format('type: str',
b'type: %T', 'abc')
check_format('type: str',
b'type: %#T', 'abc')
class LocalType:
pass
obj = LocalType()
name = 'LocalType'
check_format(f'type: {name}',
b'type: %T', py_object(obj))
check_format(f'type: {name[:3]}',
b'type: %.3T', py_object(obj))
check_format(f'type: {name.rjust(20)}',
b'type: %20T', py_object(obj))
check_format(f'type: {LocalType.__module__}.{LocalType.__qualname__}',
b'type: %#T', py_object(obj))

# test %ls
check_format('abc', b'%ls', c_wchar_p('abc'))
check_format('\u4eba\u6c11', b'%ls', c_wchar_p('\u4eba\u6c11'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add support for ``"%T"`` and ``"%#T"`` formats to
:c:func:`PyUnicode_FromFormat`: ``"%T"`` formats the name of an object type
(``type.__name__``) and ``"%#T"`` formats the fully qualifed name of an object
type (:attr:`class.__fullyqualname__`). Patch by Victor Stinner.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :attr:`__fullyqualname__ <class.__fullyqualname__>` read-only attribute
to types: the fully qualified type name. Patch by Victor Stinner.
84 changes: 62 additions & 22 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,12 @@ type_qualname(PyTypeObject *type, void *context)
}
}

static PyObject *
type_fullyqualname(PyTypeObject *type, void *context)
{
return _PyType_GetFullyQualName(type, 0);
}

static int
type_set_name(PyTypeObject *type, PyObject *value, void *context)
{
Expand Down Expand Up @@ -1598,6 +1604,7 @@ type___subclasscheck___impl(PyTypeObject *self, PyObject *subclass)
static PyGetSetDef type_getsets[] = {
{"__name__", (getter)type_name, (setter)type_set_name, NULL},
{"__qualname__", (getter)type_qualname, (setter)type_set_qualname, NULL},
{"__fullyqualname__", (getter)type_fullyqualname, NULL, NULL},
{"__bases__", (getter)type_get_bases, (setter)type_set_bases, NULL},
{"__mro__", (getter)type_get_mro, NULL, NULL},
{"__module__", (getter)type_module, (setter)type_set_module, NULL},
Expand All @@ -1615,33 +1622,18 @@ static PyObject *
type_repr(PyTypeObject *type)
{
if (type->tp_name == NULL) {
// type_repr() called before the type is fully initialized
// by PyType_Ready().
// If type_repr() is called before the type is fully initialized
// by PyType_Ready(), just format the type memory address.
return PyUnicode_FromFormat("<class at %p>", type);
}

PyObject *mod, *name, *rtn;

mod = type_module(type, NULL);
if (mod == NULL)
PyErr_Clear();
else if (!PyUnicode_Check(mod)) {
Py_SETREF(mod, NULL);
}
name = type_qualname(type, NULL);
if (name == NULL) {
Py_XDECREF(mod);
PyObject *fullqualname = _PyType_GetFullyQualName(type, 1);
if (fullqualname == NULL) {
return NULL;
}

if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins)))
rtn = PyUnicode_FromFormat("<class '%U.%U'>", mod, name);
else
rtn = PyUnicode_FromFormat("<class '%s'>", type->tp_name);

Py_XDECREF(mod);
Py_DECREF(name);
return rtn;
PyObject *result = PyUnicode_FromFormat("<class '%U'>", fullqualname);
Py_DECREF(fullqualname);
return result;
}

static PyObject *
Expand Down Expand Up @@ -4560,6 +4552,53 @@ PyType_GetQualName(PyTypeObject *type)
return type_qualname(type, NULL);
}


PyObject*
_PyType_GetFullyQualName(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) {
PyErr_SetString(PyExc_TypeError, "static type not initialized");
return NULL;
}

if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
// Static type
return PyUnicode_FromString(type->tp_name);
}

PyObject *qualname = type_qualname(type, NULL);
if (qualname == NULL) {
return NULL;
}

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;
}

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;
}


void *
PyType_GetSlot(PyTypeObject *type, int slot)
{
Expand Down Expand Up @@ -5250,6 +5289,7 @@ type___sizeof___impl(PyTypeObject *self)
return PyLong_FromSize_t(size);
}


static PyMethodDef type_methods[] = {
TYPE_MRO_METHODDEF
TYPE___SUBCLASSES___METHODDEF
Expand Down
24 changes: 24 additions & 0 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2464,6 +2464,7 @@ unicode_fromformat_arg(_PyUnicodeWriter *writer,
switch (*f++) {
case '-': flags |= F_LJUST; continue;
case '0': flags |= F_ZERO; continue;
case '#': flags |= F_ALT; continue;
}
f--;
break;
Expand Down Expand Up @@ -2787,6 +2788,29 @@ unicode_fromformat_arg(_PyUnicodeWriter *writer,
break;
}

case 'T':
{
PyObject *obj = va_arg(*vargs, PyObject *);
assert(obj);
PyObject *type_name;
if (flags & F_ALT) {
type_name = _PyType_GetFullyQualName(Py_TYPE(obj), 0);
}
else {
type_name = PyType_GetName(Py_TYPE(obj));
}
if (!type_name) {
return NULL;
}
if (unicode_fromformat_write_str(writer, type_name,
width, precision, flags) == -1) {
Py_DECREF(type_name);
return NULL;
}
Py_DECREF(type_name);
break;
}

default:
invalid_format:
PyErr_Format(PyExc_SystemError, "invalid format string: %s", p);
Expand Down

0 comments on commit 50923bf

Please sign in to comment.