From e9253ebf74433de5ae6d7f1bce693a3a1173b3b1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 4 May 2025 17:06:49 +0300 Subject: gh-122559: Synchronize C and Python implementation of the io module about pickling (GH-122628) In the C implementation, remove __reduce__ and __reduce_ex__ methods that always raise TypeError and restore __getstate__ methods that always raise TypeErrori. This restores fine details of the pre-3.12 behavior and unifies both implementations. --- Lib/test/test_io.py | 44 ++++++++++++++++++++++ .../2024-08-02-20-01-36.gh-issue-122559.2JlJr3.rst | 6 +++ Modules/_io/bufferedio.c | 9 ++--- Modules/_io/fileio.c | 3 +- Modules/_io/textio.c | 3 +- 5 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-08-02-20-01-36.gh-issue-122559.2JlJr3.rst diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index 545643a..5a8f194 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -1373,6 +1373,28 @@ class CommonBufferedTests: with self.assertRaises(AttributeError): buf.raw = x + def test_pickling_subclass(self): + global MyBufferedIO + class MyBufferedIO(self.tp): + def __init__(self, raw, tag): + super().__init__(raw) + self.tag = tag + def __getstate__(self): + return self.tag, self.raw.getvalue() + def __setstate__(slf, state): + tag, value = state + slf.__init__(self.BytesIO(value), tag) + + raw = self.BytesIO(b'data') + buf = MyBufferedIO(raw, tag='ham') + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=proto): + pickled = pickle.dumps(buf, proto) + newbuf = pickle.loads(pickled) + self.assertEqual(newbuf.raw.getvalue(), b'data') + self.assertEqual(newbuf.tag, 'ham') + del MyBufferedIO + class SizeofTest: @@ -3950,6 +3972,28 @@ class TextIOWrapperTest(unittest.TestCase): f.write(res) self.assertEqual(res + f.readline(), 'foo\nbar\n') + def test_pickling_subclass(self): + global MyTextIO + class MyTextIO(self.TextIOWrapper): + def __init__(self, raw, tag): + super().__init__(raw) + self.tag = tag + def __getstate__(self): + return self.tag, self.buffer.getvalue() + def __setstate__(slf, state): + tag, value = state + slf.__init__(self.BytesIO(value), tag) + + raw = self.BytesIO(b'data') + txt = MyTextIO(raw, 'ham') + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=proto): + pickled = pickle.dumps(txt, proto) + newtxt = pickle.loads(pickled) + self.assertEqual(newtxt.buffer.getvalue(), b'data') + self.assertEqual(newtxt.tag, 'ham') + del MyTextIO + @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") def test_read_non_blocking(self): import os diff --git a/Misc/NEWS.d/next/Library/2024-08-02-20-01-36.gh-issue-122559.2JlJr3.rst b/Misc/NEWS.d/next/Library/2024-08-02-20-01-36.gh-issue-122559.2JlJr3.rst new file mode 100644 index 0000000..4ef9daa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-02-20-01-36.gh-issue-122559.2JlJr3.rst @@ -0,0 +1,6 @@ +Remove :meth:`!__reduce__` and :meth:`!__reduce_ex__` methods that always +raise :exc:`TypeError` in the C implementation of :class:`io.FileIO`, +:class:`io.BufferedReader`, :class:`io.BufferedWriter` and +:class:`io.BufferedRandom` and replace them with default +:meth:`!__getstate__` methods that raise :exc:`!TypeError`. +This restores fine details of behavior of Python 3.11 and older versions. diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 2189b1f..4724e97 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -2555,8 +2555,7 @@ static PyMethodDef bufferedreader_methods[] = { _IO__BUFFERED_TRUNCATE_METHODDEF _IO__BUFFERED___SIZEOF___METHODDEF - {"__reduce__", _PyIOBase_cannot_pickle, METH_NOARGS}, - {"__reduce_ex__", _PyIOBase_cannot_pickle, METH_O}, + {"__getstate__", _PyIOBase_cannot_pickle, METH_NOARGS}, {NULL, NULL} }; @@ -2615,8 +2614,7 @@ static PyMethodDef bufferedwriter_methods[] = { _IO__BUFFERED_TELL_METHODDEF _IO__BUFFERED___SIZEOF___METHODDEF - {"__reduce__", _PyIOBase_cannot_pickle, METH_NOARGS}, - {"__reduce_ex__", _PyIOBase_cannot_pickle, METH_O}, + {"__getstate__", _PyIOBase_cannot_pickle, METH_NOARGS}, {NULL, NULL} }; @@ -2733,8 +2731,7 @@ static PyMethodDef bufferedrandom_methods[] = { _IO_BUFFEREDWRITER_WRITE_METHODDEF _IO__BUFFERED___SIZEOF___METHODDEF - {"__reduce__", _PyIOBase_cannot_pickle, METH_NOARGS}, - {"__reduce_ex__", _PyIOBase_cannot_pickle, METH_O}, + {"__getstate__", _PyIOBase_cannot_pickle, METH_NOARGS}, {NULL, NULL} }; diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 0c54249..8fcb270 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -1262,8 +1262,7 @@ static PyMethodDef fileio_methods[] = { _IO_FILEIO_ISATTY_METHODDEF {"_isatty_open_only", _io_FileIO_isatty_open_only, METH_NOARGS}, {"_dealloc_warn", fileio_dealloc_warn, METH_O, NULL}, - {"__reduce__", _PyIOBase_cannot_pickle, METH_NOARGS}, - {"__reduce_ex__", _PyIOBase_cannot_pickle, METH_O}, + {"__getstate__", _PyIOBase_cannot_pickle, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index a5b2ca7..86328e4 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -3366,8 +3366,7 @@ static PyMethodDef textiowrapper_methods[] = { _IO_TEXTIOWRAPPER_TELL_METHODDEF _IO_TEXTIOWRAPPER_TRUNCATE_METHODDEF - {"__reduce__", _PyIOBase_cannot_pickle, METH_NOARGS}, - {"__reduce_ex__", _PyIOBase_cannot_pickle, METH_O}, + {"__getstate__", _PyIOBase_cannot_pickle, METH_NOARGS}, {NULL, NULL} }; -- cgit v0.12