diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 4b5515900cad41..333cbc65b64096 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -761,6 +761,14 @@ Other language changes ASCII :class:`bytes` and :term:`bytes-like objects `. (Contributed by Daniel Pope in :gh:`129349`.) +* :func:`str`, :func:`repr` and :func:`ascii` now always return an instance + of :class:`str`, even if the special methods ``__str__()`` or ``__repr__()`` + return an instance of a subclass of :class:`!str`. + :func:`bytes` now always returns an instance of :class:`bytes`, even + if the special method ``__bytes__()`` returns an instance of a subclass of + :class:`!bytes`. + (Contributed by Serhiy Storchaka in :gh:`104231`.) + * Support ``\z`` as a synonym for ``\Z`` in :mod:`regular expressions `. It is interpreted unambiguously in many other regular expression engines, unlike ``\Z``, which has subtly different behavior. @@ -2442,6 +2450,15 @@ Limited C API changes C API. Keep :c:func:`PySequence_Fast` in the limited C API. (Contributed by Victor Stinner in :gh:`91417`.) +* :c:func:`PyObject_Str`, :c:func:`PyObject_Repr` and :c:func:`PyObject_ASCII` + now always return an instance of :class:`str`, even if the special methods + ``__str__()`` or ``__repr__()`` return an instance of a subclass of + :class:`!str`. + :c:func:`PyObject_Bytes` now always returns an instance of :class:`bytes`, + even if the special method ``__bytes__()`` returns an instance of a subclass + of :class:`!bytes`. + (Contributed by Serhiy Storchaka in :gh:`104231`.) + Porting to Python 3.14 ---------------------- diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 82d9916e38d341..bb3ab0fc08af45 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1127,7 +1127,7 @@ def __bytes__(self): self.assertEqual(BytesSubclass(StrWithBytes(OtherBytesSubclass(b'abc'))), BytesSubclass(b'abc')) # Issue #24731 - self.assertTypedEqual(bytes(WithBytes(BytesSubclass(b'abc'))), BytesSubclass(b'abc')) + self.assertTypedEqual(bytes(WithBytes(BytesSubclass(b'abc'))), b'abc') self.assertTypedEqual(BytesSubclass(WithBytes(BytesSubclass(b'abc'))), BytesSubclass(b'abc')) self.assertTypedEqual(BytesSubclass(WithBytes(OtherBytesSubclass(b'abc'))), @@ -1143,8 +1143,7 @@ def __bytes__(self): self.assertTypedEqual(bytes(BytesWithBytes(b'abc')), b'abc') self.assertTypedEqual(BytesSubclass(BytesWithBytes(b'abc')), BytesSubclass(b'abc')) - self.assertTypedEqual(bytes(BytesWithBytes(BytesSubclass(b'abc'))), - BytesSubclass(b'abc')) + self.assertTypedEqual(bytes(BytesWithBytes(BytesSubclass(b'abc'))), b'abc') self.assertTypedEqual(BytesSubclass(BytesWithBytes(BytesSubclass(b'abc'))), BytesSubclass(b'abc')) self.assertTypedEqual(BytesSubclass(BytesWithBytes(OtherBytesSubclass(b'abc'))), diff --git a/Lib/test/test_capi/test_abstract.py b/Lib/test/test_capi/test_abstract.py index 7d548ae87c0fa6..ffdcf5b56e9622 100644 --- a/Lib/test/test_capi/test_abstract.py +++ b/Lib/test/test_capi/test_abstract.py @@ -81,9 +81,9 @@ def test_object_str(self): self.assertTypedEqual(object_str('\U0001f40d'), '\U0001f40d') self.assertTypedEqual(object_str(StrSubclass('abc')), 'abc') self.assertTypedEqual(object_str(WithStr('abc')), 'abc') - self.assertTypedEqual(object_str(WithStr(StrSubclass('abc'))), StrSubclass('abc')) + self.assertTypedEqual(object_str(WithStr(StrSubclass('abc'))), 'abc') self.assertTypedEqual(object_str(WithRepr('')), '') - self.assertTypedEqual(object_str(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(object_str(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(object_str(NULL), '') def test_object_repr(self): @@ -94,9 +94,9 @@ def test_object_repr(self): self.assertTypedEqual(object_repr('\U0001f40d'), "'\U0001f40d'") self.assertTypedEqual(object_repr(StrSubclass('abc')), "'abc'") self.assertTypedEqual(object_repr(WithRepr('')), '') - self.assertTypedEqual(object_repr(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(object_repr(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(object_repr(WithRepr('<\U0001f40d>')), '<\U0001f40d>') - self.assertTypedEqual(object_repr(WithRepr(StrSubclass('<\U0001f40d>'))), StrSubclass('<\U0001f40d>')) + self.assertTypedEqual(object_repr(WithRepr(StrSubclass('<\U0001f40d>'))), '<\U0001f40d>') self.assertTypedEqual(object_repr(NULL), '') def test_object_ascii(self): @@ -107,7 +107,7 @@ def test_object_ascii(self): self.assertTypedEqual(object_ascii('\U0001f40d'), r"'\U0001f40d'") self.assertTypedEqual(object_ascii(StrSubclass('abc')), "'abc'") self.assertTypedEqual(object_ascii(WithRepr('')), '') - self.assertTypedEqual(object_ascii(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(object_ascii(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(object_ascii(WithRepr('<\U0001f40d>')), r'<\U0001f40d>') self.assertTypedEqual(object_ascii(WithRepr(StrSubclass('<\U0001f40d>'))), r'<\U0001f40d>') self.assertTypedEqual(object_ascii(NULL), '') @@ -119,7 +119,7 @@ def test_object_bytes(self): self.assertTypedEqual(object_bytes(b'abc'), b'abc') self.assertTypedEqual(object_bytes(BytesSubclass(b'abc')), b'abc') self.assertTypedEqual(object_bytes(WithBytes(b'abc')), b'abc') - self.assertTypedEqual(object_bytes(WithBytes(BytesSubclass(b'abc'))), BytesSubclass(b'abc')) + self.assertTypedEqual(object_bytes(WithBytes(BytesSubclass(b'abc'))), b'abc') self.assertTypedEqual(object_bytes(bytearray(b'abc')), b'abc') self.assertTypedEqual(object_bytes(memoryview(b'abc')), b'abc') self.assertTypedEqual(object_bytes([97, 98, 99]), b'abc') diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index d6a7bd0da59910..9e0b044d08e4ee 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -150,7 +150,7 @@ def test_ascii(self): self.assertTypedEqual(ascii('\U0001f40d'), r"'\U0001f40d'") self.assertTypedEqual(ascii(StrSubclass('abc')), "'abc'") self.assertTypedEqual(ascii(WithRepr('')), '') - self.assertTypedEqual(ascii(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(ascii(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(ascii(WithRepr('<\U0001f40d>')), r'<\U0001f40d>') self.assertTypedEqual(ascii(WithRepr(StrSubclass('<\U0001f40d>'))), r'<\U0001f40d>') self.assertRaises(TypeError, ascii, WithRepr(b'byte-repr')) @@ -194,9 +194,9 @@ def test_repr(self): self.assertTypedEqual(repr('\U0001f40d'), "'\U0001f40d'") self.assertTypedEqual(repr(StrSubclass('abc')), "'abc'") self.assertTypedEqual(repr(WithRepr('')), '') - self.assertTypedEqual(repr(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(repr(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(repr(WithRepr('<\U0001f40d>')), '<\U0001f40d>') - self.assertTypedEqual(repr(WithRepr(StrSubclass('<\U0001f40d>'))), StrSubclass('<\U0001f40d>')) + self.assertTypedEqual(repr(WithRepr(StrSubclass('<\U0001f40d>'))), '<\U0001f40d>') self.assertRaises(TypeError, repr, WithRepr(b'byte-repr')) def test_iterators(self): @@ -2415,7 +2415,7 @@ def __str__(self): return self.value self.assertTypedEqual(str(WithStr('abc')), 'abc') - self.assertTypedEqual(str(WithStr(StrSubclass('abc'))), StrSubclass('abc')) + self.assertTypedEqual(str(WithStr(StrSubclass('abc'))), 'abc') self.assertTypedEqual(StrSubclass(WithStr('abc')), StrSubclass('abc')) self.assertTypedEqual(StrSubclass(WithStr(StrSubclass('abc'))), StrSubclass('abc')) @@ -2423,7 +2423,7 @@ def __str__(self): StrSubclass('abc')) self.assertTypedEqual(str(StrWithStr('abc')), 'abc') - self.assertTypedEqual(str(StrWithStr(StrSubclass('abc'))), StrSubclass('abc')) + self.assertTypedEqual(str(StrWithStr(StrSubclass('abc'))), 'abc') self.assertTypedEqual(StrSubclass(StrWithStr('abc')), StrSubclass('abc')) self.assertTypedEqual(StrSubclass(StrWithStr(StrSubclass('abc'))), StrSubclass('abc')) @@ -2431,7 +2431,7 @@ def __str__(self): StrSubclass('abc')) self.assertTypedEqual(str(WithRepr('')), '') - self.assertTypedEqual(str(WithRepr(StrSubclass(''))), StrSubclass('')) + self.assertTypedEqual(str(WithRepr(StrSubclass(''))), '') self.assertTypedEqual(StrSubclass(WithRepr('')), StrSubclass('')) self.assertTypedEqual(StrSubclass(WithRepr(StrSubclass(''))), StrSubclass('')) diff --git a/Misc/NEWS.d/next/C_API/2023-12-01-11-27-41.gh-issue-104231.yXgoYF.rst b/Misc/NEWS.d/next/C_API/2023-12-01-11-27-41.gh-issue-104231.yXgoYF.rst new file mode 100644 index 00000000000000..bc5410fcc0d80a --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2023-12-01-11-27-41.gh-issue-104231.yXgoYF.rst @@ -0,0 +1,7 @@ +:c:func:`PyObject_Str`, :c:func:`PyObject_Repr` and :c:func:`PyObject_ASCII` +now always return an instance of :class:`str`, even if the special methods +``__str__()`` or ``__repr__()`` return an instance of a subclass of +:class:`!str`. +:c:func:`PyObject_Bytes` now always returns an instance of :class:`bytes`, even +if the special method ``__bytes__()`` returns an instance of a subclass of +:class:`!bytes`. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2023-12-01-11-25-17.gh-issue-104231.nGL6Xk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2023-12-01-11-25-17.gh-issue-104231.nGL6Xk.rst new file mode 100644 index 00000000000000..7affd488ef84d4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2023-12-01-11-25-17.gh-issue-104231.nGL6Xk.rst @@ -0,0 +1,6 @@ +:func:`str`, :func:`repr` and :func:`ascii` now always return an instance +of :class:`str`, even if the special methods ``__str__()`` or ``__repr__()`` +return an instance of a subclass of :class:`!str`. +:func:`bytes` now always returns an instance of :class:`bytes`, even +if the special method ``__bytes__()`` returns an instance of a subclass of +:class:`!bytes`. diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index fc407ec6bf99d6..1fe285aa7f4c80 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -2786,12 +2786,19 @@ bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding, Py_DECREF(func); if (bytes == NULL) return NULL; - if (!PyBytes_Check(bytes)) { - PyErr_Format(PyExc_TypeError, - "__bytes__ returned non-bytes (type %.200s)", - Py_TYPE(bytes)->tp_name); - Py_DECREF(bytes); - return NULL; + if (!PyBytes_CheckExact(bytes)) { + if (!PyBytes_Check(bytes)) { + PyErr_Format(PyExc_TypeError, + "__bytes__ returned non-bytes (type %.200s)", + Py_TYPE(bytes)->tp_name); + Py_DECREF(bytes); + return NULL; + } + Py_SETREF(bytes, PyBytes_FromStringAndSize(PyBytes_AS_STRING(bytes), + PyBytes_GET_SIZE(bytes))); + if (bytes == NULL) { + return NULL; + } } } else if (PyErr_Occurred()) diff --git a/Objects/object.c b/Objects/object.c index 0974a231ec101a..f29f5d39b18df3 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -779,8 +779,8 @@ PyObject_Repr(PyObject *v) res = (*Py_TYPE(v)->tp_repr)(v); _Py_LeaveRecursiveCallTstate(tstate); - if (res == NULL) { - return NULL; + if (res == NULL || PyUnicode_CheckExact(res)) { + return res; } if (!PyUnicode_Check(res)) { _PyErr_Format(tstate, PyExc_TypeError, @@ -789,6 +789,7 @@ PyObject_Repr(PyObject *v) Py_DECREF(res); return NULL; } + Py_SETREF(res, _PyUnicode_Copy(res)); return res; } @@ -825,6 +826,10 @@ PyObject_Str(PyObject *v) if (res == NULL) { return NULL; } + if (PyUnicode_CheckExact(res)) { + assert(_PyUnicode_CheckConsistency(res, 1)); + return res; + } if (!PyUnicode_Check(res)) { _PyErr_Format(tstate, PyExc_TypeError, "__str__ returned non-string (type %.200s)", @@ -833,6 +838,7 @@ PyObject_Str(PyObject *v) return NULL; } assert(_PyUnicode_CheckConsistency(res, 1)); + Py_SETREF(res, _PyUnicode_Copy(res)); return res; } @@ -881,6 +887,9 @@ PyObject_Bytes(PyObject *v) Py_DECREF(func); if (result == NULL) return NULL; + if (PyBytes_CheckExact(result)) { + return result; + } if (!PyBytes_Check(result)) { PyErr_Format(PyExc_TypeError, "__bytes__ returned non-bytes (type %.200s)", @@ -888,6 +897,8 @@ PyObject_Bytes(PyObject *v) Py_DECREF(result); return NULL; } + Py_SETREF(result, PyBytes_FromStringAndSize(PyBytes_AS_STRING(result), + PyBytes_GET_SIZE(result))); return result; } else if (PyErr_Occurred())