Skip to content

Commit c17135e

Browse files
Merge branch 'main' into fix/129640
2 parents ee93a71 + 052cb71 commit c17135e

File tree

22 files changed

+301
-90
lines changed

22 files changed

+301
-90
lines changed

Doc/library/io.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,55 @@ Text I/O
11471147
It inherits from :class:`codecs.IncrementalDecoder`.
11481148

11491149

1150+
Static Typing
1151+
-------------
1152+
1153+
The following protocols can be used for annotating function and method
1154+
arguments for simple stream reading or writing operations. They are decorated
1155+
with :deco:`typing.runtime_checkable`.
1156+
1157+
.. class:: Reader[T]
1158+
1159+
Generic protocol for reading from a file or other input stream. ``T`` will
1160+
usually be :class:`str` or :class:`bytes`, but can be any type that is
1161+
read from the stream.
1162+
1163+
.. versionadded:: next
1164+
1165+
.. method:: read()
1166+
read(size, /)
1167+
1168+
Read data from the input stream and return it. If *size* is
1169+
specified, it should be an integer, and at most *size* items
1170+
(bytes/characters) will be read.
1171+
1172+
For example::
1173+
1174+
def read_it(reader: Reader[str]):
1175+
data = reader.read(11)
1176+
assert isinstance(data, str)
1177+
1178+
.. class:: Writer[T]
1179+
1180+
Generic protocol for writing to a file or other output stream. ``T`` will
1181+
usually be :class:`str` or :class:`bytes`, but can be any type that can be
1182+
written to the stream.
1183+
1184+
.. versionadded:: next
1185+
1186+
.. method:: write(data, /)
1187+
1188+
Write *data* to the output stream and return the number of items
1189+
(bytes/characters) written.
1190+
1191+
For example::
1192+
1193+
def write_binary(writer: Writer[bytes]):
1194+
writer.write(b"Hello world!\n")
1195+
1196+
See :ref:`typing-io` for other I/O related protocols and classes that can be
1197+
used for static type checking.
1198+
11501199
Performance
11511200
-----------
11521201

Doc/library/typing.rst

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,17 +2834,35 @@ with :func:`@runtime_checkable <runtime_checkable>`.
28342834
An ABC with one abstract method ``__round__``
28352835
that is covariant in its return type.
28362836

2837-
ABCs for working with IO
2838-
------------------------
2837+
.. _typing-io:
2838+
2839+
ABCs and Protocols for working with I/O
2840+
---------------------------------------
28392841

2840-
.. class:: IO
2841-
TextIO
2842-
BinaryIO
2842+
.. class:: IO[AnyStr]
2843+
TextIO[AnyStr]
2844+
BinaryIO[AnyStr]
28432845

2844-
Generic type ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])``
2846+
Generic class ``IO[AnyStr]`` and its subclasses ``TextIO(IO[str])``
28452847
and ``BinaryIO(IO[bytes])``
28462848
represent the types of I/O streams such as returned by
2847-
:func:`open`.
2849+
:func:`open`. Please note that these classes are not protocols, and
2850+
their interface is fairly broad.
2851+
2852+
The protocols :class:`io.Reader` and :class:`io.Writer` offer a simpler
2853+
alternative for argument types, when only the ``read()`` or ``write()``
2854+
methods are accessed, respectively::
2855+
2856+
def read_and_write(reader: Reader[str], writer: Writer[bytes]):
2857+
data = reader.read()
2858+
writer.write(data.encode())
2859+
2860+
Also consider using :class:`collections.abc.Iterable` for iterating over
2861+
the lines of an input stream::
2862+
2863+
def read_config(stream: Iterable[str]):
2864+
for line in stream:
2865+
...
28482866

28492867
Functions and decorators
28502868
------------------------

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,11 @@ io
619619
:exc:`BlockingIOError` if the operation cannot immediately return bytes.
620620
(Contributed by Giovanni Siragusa in :gh:`109523`.)
621621

622+
* Add protocols :class:`io.Reader` and :class:`io.Writer` as a simpler
623+
alternatives to the pseudo-protocols :class:`typing.IO`,
624+
:class:`typing.TextIO`, and :class:`typing.BinaryIO`.
625+
(Contributed by Sebastian Rittau in :gh:`127648`.)
626+
622627

623628
json
624629
----

Include/cpython/pystate.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,14 @@ struct _ts {
8383
unsigned int bound_gilstate:1;
8484
/* Currently in use (maybe holds the GIL). */
8585
unsigned int active:1;
86-
/* Currently holds the GIL. */
87-
unsigned int holds_gil:1;
8886

8987
/* various stages of finalization */
9088
unsigned int finalizing:1;
9189
unsigned int cleared:1;
9290
unsigned int finalized:1;
9391

9492
/* padding to align to 4 bytes */
95-
unsigned int :23;
93+
unsigned int :24;
9694
} _status;
9795
#ifdef Py_BUILD_CORE
9896
# define _PyThreadState_WHENCE_NOTSET -1
@@ -103,6 +101,10 @@ struct _ts {
103101
# define _PyThreadState_WHENCE_GILSTATE 4
104102
# define _PyThreadState_WHENCE_EXEC 5
105103
#endif
104+
105+
/* Currently holds the GIL. Must be its own field to avoid data races */
106+
int holds_gil;
107+
106108
int _whence;
107109

108110
/* Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED).

Include/internal/pycore_pystate.h

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ extern "C" {
2727
// "suspended" state. Only the thread performing a stop-the-world pause may
2828
// transition a thread from the "suspended" state back to the "detached" state.
2929
//
30+
// The "shutting down" state is used when the interpreter is being finalized.
31+
// Threads in this state can't do anything other than block the OS thread.
32+
// (See _PyThreadState_HangThread).
33+
//
3034
// State transition diagram:
3135
//
3236
// (bound thread) (stop-the-world thread)
@@ -37,9 +41,10 @@ extern "C" {
3741
//
3842
// The (bound thread) and (stop-the-world thread) labels indicate which thread
3943
// is allowed to perform the transition.
40-
#define _Py_THREAD_DETACHED 0
41-
#define _Py_THREAD_ATTACHED 1
42-
#define _Py_THREAD_SUSPENDED 2
44+
#define _Py_THREAD_DETACHED 0
45+
#define _Py_THREAD_ATTACHED 1
46+
#define _Py_THREAD_SUSPENDED 2
47+
#define _Py_THREAD_SHUTTING_DOWN 3
4348

4449

4550
/* Check if the current thread is the main thread.
@@ -118,7 +123,8 @@ extern _Py_thread_local PyThreadState *_Py_tss_tstate;
118123
extern int _PyThreadState_CheckConsistency(PyThreadState *tstate);
119124
#endif
120125

121-
int _PyThreadState_MustExit(PyThreadState *tstate);
126+
extern int _PyThreadState_MustExit(PyThreadState *tstate);
127+
extern void _PyThreadState_HangThread(PyThreadState *tstate);
122128

123129
// Export for most shared extensions, used via _PyThreadState_GET() static
124130
// inline function.
@@ -169,6 +175,11 @@ extern void _PyThreadState_Detach(PyThreadState *tstate);
169175
// to the "detached" state.
170176
extern void _PyThreadState_Suspend(PyThreadState *tstate);
171177

178+
// Mark the thread state as "shutting down". This is used during interpreter
179+
// and runtime finalization. The thread may no longer attach to the
180+
// interpreter and will instead block via _PyThreadState_HangThread().
181+
extern void _PyThreadState_SetShuttingDown(PyThreadState *tstate);
182+
172183
// Perform a stop-the-world pause for all threads in the all interpreters.
173184
//
174185
// Threads in the "attached" state are paused and transitioned to the "GC"
@@ -238,7 +249,7 @@ PyAPI_FUNC(PyThreadState *) _PyThreadState_NewBound(
238249
PyInterpreterState *interp,
239250
int whence);
240251
extern PyThreadState * _PyThreadState_RemoveExcept(PyThreadState *tstate);
241-
extern void _PyThreadState_DeleteList(PyThreadState *list);
252+
extern void _PyThreadState_DeleteList(PyThreadState *list, int is_after_fork);
242253
extern void _PyThreadState_ClearMimallocHeaps(PyThreadState *tstate);
243254

244255
// Export for '_testinternalcapi' shared extension

Include/internal/pycore_runtime_init.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ extern PyTypeObject _PyExc_MemoryError;
171171
#define _PyThreadStateImpl_INIT \
172172
{ \
173173
.base = _PyThreadState_INIT, \
174+
/* The thread and the interpreter's linked list hold a reference */ \
175+
.refcount = 2, \
174176
}
175177

176178
#define _PyThreadState_INIT \

Include/internal/pycore_tstate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ typedef struct _PyThreadStateImpl {
2121
// semi-public fields are in PyThreadState.
2222
PyThreadState base;
2323

24+
// The reference count field is used to synchronize deallocation of the
25+
// thread state during runtime finalization.
26+
Py_ssize_t refcount;
27+
2428
// These are addresses, but we need to convert to ints to avoid UB.
2529
uintptr_t c_stack_top;
2630
uintptr_t c_stack_soft_limit;

Lib/_pyio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
_setmode = None
1717

1818
import io
19-
from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END) # noqa: F401
19+
from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END, Reader, Writer) # noqa: F401
2020

2121
valid_seek_flags = {0, 1, 2} # Hardwired values
2222
if hasattr(os, 'SEEK_HOLE') :

Lib/io.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@
4646
"BufferedReader", "BufferedWriter", "BufferedRWPair",
4747
"BufferedRandom", "TextIOBase", "TextIOWrapper",
4848
"UnsupportedOperation", "SEEK_SET", "SEEK_CUR", "SEEK_END",
49-
"DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder"]
49+
"DEFAULT_BUFFER_SIZE", "text_encoding", "IncrementalNewlineDecoder",
50+
"Reader", "Writer"]
5051

5152

5253
import _io
5354
import abc
5455

56+
from _collections_abc import _check_methods
5557
from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation,
5658
open, open_code, FileIO, BytesIO, StringIO, BufferedReader,
5759
BufferedWriter, BufferedRWPair, BufferedRandom,
@@ -97,3 +99,55 @@ class TextIOBase(_io._TextIOBase, IOBase):
9799
pass
98100
else:
99101
RawIOBase.register(_WindowsConsoleIO)
102+
103+
#
104+
# Static Typing Support
105+
#
106+
107+
GenericAlias = type(list[int])
108+
109+
110+
class Reader(metaclass=abc.ABCMeta):
111+
"""Protocol for simple I/O reader instances.
112+
113+
This protocol only supports blocking I/O.
114+
"""
115+
116+
__slots__ = ()
117+
118+
@abc.abstractmethod
119+
def read(self, size=..., /):
120+
"""Read data from the input stream and return it.
121+
122+
If *size* is specified, at most *size* items (bytes/characters) will be
123+
read.
124+
"""
125+
126+
@classmethod
127+
def __subclasshook__(cls, C):
128+
if cls is Reader:
129+
return _check_methods(C, "read")
130+
return NotImplemented
131+
132+
__class_getitem__ = classmethod(GenericAlias)
133+
134+
135+
class Writer(metaclass=abc.ABCMeta):
136+
"""Protocol for simple I/O writer instances.
137+
138+
This protocol only supports blocking I/O.
139+
"""
140+
141+
__slots__ = ()
142+
143+
@abc.abstractmethod
144+
def write(self, data, /):
145+
"""Write *data* to the output stream and return the number of items written."""
146+
147+
@classmethod
148+
def __subclasshook__(cls, C):
149+
if cls is Writer:
150+
return _check_methods(C, "write")
151+
return NotImplemented
152+
153+
__class_getitem__ = classmethod(GenericAlias)

Lib/test/test_io.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4916,6 +4916,24 @@ class PySignalsTest(SignalsTest):
49164916
test_reentrant_write_text = None
49174917

49184918

4919+
class ProtocolsTest(unittest.TestCase):
4920+
class MyReader:
4921+
def read(self, sz=-1):
4922+
return b""
4923+
4924+
class MyWriter:
4925+
def write(self, b: bytes):
4926+
pass
4927+
4928+
def test_reader_subclass(self):
4929+
self.assertIsSubclass(MyReader, io.Reader[bytes])
4930+
self.assertNotIsSubclass(str, io.Reader[bytes])
4931+
4932+
def test_writer_subclass(self):
4933+
self.assertIsSubclass(MyWriter, io.Writer[bytes])
4934+
self.assertNotIsSubclass(str, io.Writer[bytes])
4935+
4936+
49194937
def load_tests(loader, tests, pattern):
49204938
tests = (CIOTest, PyIOTest, APIMismatchTest,
49214939
CBufferedReaderTest, PyBufferedReaderTest,

0 commit comments

Comments
 (0)