Skip to content
Draft
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
8 changes: 8 additions & 0 deletions Doc/c-api/unicode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,11 @@ APIs:
- :c:expr:`PyObject*`
- The result of calling :c:func:`PyObject_Repr`.

* - ``#R``
- :c:expr:`PyObject*`
- Similar to ``R`` format, but don't call :meth:`~object.__repr__`
method on :class:`str` subclasses.

* - ``T``
- :c:expr:`PyObject*`
- Get the fully qualified name of an object type;
Expand Down Expand Up @@ -630,6 +635,9 @@ APIs:
.. versionchanged:: 3.13
Support for ``%T``, ``%#T``, ``%N`` and ``%#N`` formats added.

.. versionchanged:: next
Support for ``%#R`` format added.


.. c:function:: PyObject* PyUnicode_FromFormatV(const char *format, va_list vargs)

Expand Down
6 changes: 5 additions & 1 deletion Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,11 @@ C API changes
New features
------------

* TODO
* Add ``%#R`` format to :c:func:`PyUnicode_FromFormat`: similar to ``%R``
format, but don't call :meth:`~object.__str__` method on :class:`str`
subclasses.
(Contributed by Serhiy Storchaka in :gh:`154610`.)


Porting to Python 3.16
----------------------
Expand Down
5 changes: 3 additions & 2 deletions Include/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ PyAPI_FUNC(void) PyEval_ReleaseThread(PyThreadState *tstate);
#define FVC_STR 0x1
#define FVC_REPR 0x2
#define FVC_ASCII 0x3
#define FVS_MASK 0x4
#define FVS_HAVE_SPEC 0x4
#define FVC_ALT_REPR 0x4
#define FVS_MASK 0x8
#define FVS_HAVE_SPEC 0x8

#ifndef Py_LIMITED_API
# define Py_CPYTHON_CEVAL_H
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(all_threads)
STRUCT_FOR_ID(allow_code)
STRUCT_FOR_ID(alphabet)
STRUCT_FOR_ID(alt)
STRUCT_FOR_ID(any)
STRUCT_FOR_ID(append)
STRUCT_FOR_ID(arg)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ static inline Py_ALWAYS_INLINE void _Py_INCREF_MORTAL(PyObject *op)
* references. */
PyAPI_FUNC(int) _PyObject_VisitType(PyObject *op, visitproc visit, void *arg);

extern PyObject* _PyObject_AltRepr(PyObject *);

#ifdef __cplusplus
}
#endif
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Lib/test/test_capi/test_unicode.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ def check_format(expected, format, *args):
check_format(' abc[\ufffd]',
b'%10.7s', b'abc[\xe2\x82]')

# Test %A and %R
check_format("'\\u20acABC'",
b'%A', '\u20acABC')
check_format("'\\u20",
Expand All @@ -455,6 +456,16 @@ def check_format(expected, format, *args):
check_format('\u20acAB',
b'%.3U', '\u20acABCDEF')

# Test %#R: do not call __str__() of str subclasses
check_format("'abc'",
b'%#R', 'abc')
class StrSubclass(str):
def __repr__(self):
return 'StrSubclass'
check_format("'abc'",
b'%#R', StrSubclass('abc'))
self.assertEqual(repr(StrSubclass()), 'StrSubclass')

check_format('\u20acAB',
b'%.3V', '\u20acABCDEF', None)
check_format('abc[',
Expand Down
12 changes: 10 additions & 2 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,14 @@
self.assertEqual(f'{"a"!r}', "'a'")
self.assertEqual(f'{"a"!a}', "'a'")

# Test !R conversion
class OverrideRepr(str):
def __repr__(self):
return 'CUSTOM REPR'
abc = OverrideRepr('abc')
self.assertEqual(f'{abc!r}', 'CUSTOM REPR')
self.assertEqual(f'{abc!R}', "'abc'")

Check failure on line 1374 in Lib/test/test_fstring.py

View workflow job for this annotation

GitHub Actions / lint

ruff (invalid-syntax)

Lib/test/test_fstring.py:1374:33: invalid-syntax: f-string: invalid conversion character

# Conversions can have trailing whitespace after them since it
# does not provide any significance
self.assertEqual(f"{3!s }", "3")
Expand All @@ -1390,7 +1398,7 @@
for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ':
self.assertAllRaise(SyntaxError,
"f-string: invalid conversion character %r: "
"expected 's', 'r', or 'a'" % conv_identifier,
"expected 's', 'r', 'R', or 'a'" % conv_identifier,
["f'{3!" + conv_identifier + "}'"])

for conv_non_identifier in '3', '!':
Expand All @@ -1406,7 +1414,7 @@

self.assertAllRaise(SyntaxError,
"f-string: invalid conversion character 'ss': "
"expected 's', 'r', or 'a'",
"expected 's', 'r', 'R', or 'a'",
["f'{3!ss}'",
"f'{3!ss:}'",
"f'{3!ss:s}'",
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,14 @@ def __repr__(self):
self.assertEqual('{0!a}'.format(F('Hello')), 'F(Hello)')
self.assertEqual('{0!a}'.format(F('\u0374')), 'F(\\u0374)')

# !R conversion
class OverrideRepr(str):
def __repr__(self):
return 'CUSTOM REPR'
abc = OverrideRepr('abc')
self.assertEqual('{0!r}'.format(abc), 'CUSTOM REPR')
self.assertEqual('{0!R}'.format(abc), "'abc'")

# test fallback to object.__format__
self.assertEqual('{0}'.format({}), '{}')
self.assertEqual('{0}'.format([]), '[]')
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_string/_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def convert(value, conversion):
return ascii(value)
elif conversion == "r":
return repr(value)
elif conversion == "R":
return repr(value, alt=True)
elif conversion == "s":
return str(value)
return value
Expand Down
15 changes: 12 additions & 3 deletions Lib/test/test_tstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@
self.assertTStringEqual(t, ("ASCII: ", ""), [(text, "text", "a")])
self.assertEqual(fstring(t), f"ASCII: {ascii(text)}")

# Test !R conversion (repr)
class StrSubclass(str):
def __repr__(self):
return '<custom repr>'
obj = StrSubclass('abc')
t = t"Data: {obj!R}"

Check failure on line 114 in Lib/test/test_tstring.py

View workflow job for this annotation

GitHub Actions / lint

ruff (invalid-syntax)

Lib/test/test_tstring.py:114:26: invalid-syntax: t-string: invalid conversion character
self.assertTStringEqual(t, ("Data: ", ""), [(obj, "obj", "R")])
self.assertEqual(fstring(t), f"Data: 'abc'")

# Test !z conversion (error)
num = 1
with self.assertRaises(SyntaxError):
Expand Down Expand Up @@ -305,11 +314,11 @@
("t'{x!}'", "t-string: missing conversion character"),
("t'{x=!}'", "t-string: missing conversion character"),
("t'{x!z}'", "t-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
"expected 's', 'r', 'R', or 'a'"),
("f\"{t'{x!z}'}\"", "t-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
"expected 's', 'r', 'R', or 'a'"),
("t'{f\"{x!z}\"}'", "f-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
"expected 's', 'r', 'R', or 'a'"),
("t'{lambda:1}'", "t-string: lambda expressions are not allowed "
"without parentheses"),
("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add ``%#R`` format to :c:func:`PyUnicode_FromFormat`: similar to ``%R``
format, but don't call :meth:`~object.__str__` method on :class:`str`
subclasses. Patch by Victor Stinner.
2 changes: 1 addition & 1 deletion Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Objects/interpolationobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ _PyInterpolation_Build(PyObject *value, PyObject *str, int conversion, PyObject
case FVC_REPR:
interpolation->conversion = _Py_LATIN1_CHR('r');
break;
case FVC_ALT_REPR:
interpolation->conversion = _Py_LATIN1_CHR('R');
break;
case FVC_STR:
interpolation->conversion = _Py_LATIN1_CHR('s');
break;
Expand Down
13 changes: 13 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,19 @@ PyObject_Repr(PyObject *v)
return res;
}

PyObject *
_PyObject_AltRepr(PyObject *v)
{
if (v != NULL && PyUnicode_Check(v)) {
if (PyErr_CheckSignals()) {
return NULL;
}
reprfunc repr_func = PyUnicode_Type.tp_repr;
return repr_func(v);
}
return PyObject_Repr(v);
}

PyObject *
PyObject_Str(PyObject *v)
{
Expand Down
2 changes: 2 additions & 0 deletions Objects/stringlib/unicode_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,8 @@ do_conversion(PyObject *obj, Py_UCS4 conversion)
switch (conversion) {
case 'r':
return PyObject_Repr(obj);
case 'R':
return _PyObject_AltRepr(obj);
case 's':
return PyObject_Str(obj);
case 'a':
Expand Down
9 changes: 7 additions & 2 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2971,9 +2971,14 @@ unicode_fromformat_arg(_PyUnicodeWriter *writer,
case 'R':
{
PyObject *obj = va_arg(*vargs, PyObject *);
PyObject *repr;
assert(obj);
repr = PyObject_Repr(obj);
PyObject *repr;
if (flags & F_ALT) {
repr = _PyObject_AltRepr(obj);
}
else {
repr = PyObject_Repr(obj);
}
if (!repr)
return NULL;
if (unicode_fromformat_write_str(writer, repr, width, precision, flags) == -1) {
Expand Down
4 changes: 2 additions & 2 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,9 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)

Py_UCS4 first = PyUnicode_READ_CHAR(conv->v.Name.id, 0);
if (PyUnicode_GET_LENGTH(conv->v.Name.id) > 1 ||
!(first == 's' || first == 'r' || first == 'a')) {
!(first == 's' || first == 'r' || first == 'R' || first == 'a')) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv,
"%c-string: invalid conversion character %R: expected 's', 'r', or 'a'",
"%c-string: invalid conversion character %R: expected 's', 'r', 'R', or 'a'",
formatted_string_prefix(p),
conv->v.Name.id);
return NULL;
Expand Down
13 changes: 10 additions & 3 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2639,17 +2639,24 @@ repr as builtin_repr

obj: object
/
*
alt: bool = False

Return the canonical string representation of the object.

For many object types, including most builtins, eval(repr(obj)) == obj.
[clinic start generated code]*/

static PyObject *
builtin_repr(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=7ed3778c44fd0194 input=1c9e6d66d3e3be04]*/
builtin_repr_impl(PyObject *module, PyObject *obj, int alt)
/*[clinic end generated code: output=fc5168bb1b4c8846 input=0b1b2969c594dc9a]*/
{
return PyObject_Repr(obj);
if (!alt) {
return PyObject_Repr(obj);
}
else {
return _PyObject_AltRepr(obj);
}
}


Expand Down
2 changes: 1 addition & 1 deletion Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -5868,7 +5868,7 @@ dummy_func(

inst(CONVERT_VALUE, (value -- result)) {
conversion_func conv_fn;
assert(oparg >= FVC_STR && oparg <= FVC_ASCII);
assert(oparg >= FVC_STR && oparg <= FVC_ALT_REPR);
conv_fn = _PyEval_ConversionFuncs[oparg];
PyObject *result_o = conv_fn(PyStackRef_AsPyObjectBorrow(value));
PyStackRef_CLOSE(value);
Expand Down
5 changes: 3 additions & 2 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,11 @@ const binaryfunc _PyEval_BinaryOps[] = {
[NB_SUBSCR] = PyObject_GetItem,
};

const conversion_func _PyEval_ConversionFuncs[4] = {
const conversion_func _PyEval_ConversionFuncs[5] = {
[FVC_STR] = PyObject_Str,
[FVC_REPR] = PyObject_Repr,
[FVC_ASCII] = PyObject_ASCII
[FVC_ASCII] = PyObject_ASCII,
[FVC_ALT_REPR] = _PyObject_AltRepr,
};

const _Py_SpecialMethod _Py_SpecialMethods[] = {
Expand Down
Loading
Loading