Skip to content

Commit 32ba4e1

Browse files
[3.13] gh-71019: document which objects connect_read_pipe/connect_write_pipe accept and add tests (GH-153660) (#153687)
gh-71019: document which objects connect_read_pipe/connect_write_pipe accept and add tests (GH-153660) (cherry picked from commit 7fb315b)
1 parent 895fe0b commit 32ba4e1

5 files changed

Lines changed: 248 additions & 7 deletions

File tree

Doc/library/asyncio-eventloop.rst

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,7 +1258,9 @@ Working with pipes
12581258
*protocol_factory* must be a callable returning an
12591259
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12601260

1261-
*pipe* is a :term:`file-like object <file object>`.
1261+
*pipe* is a :term:`file-like object <file object>`. See
1262+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1263+
supported as *pipe*.
12621264

12631265
Return pair ``(transport, protocol)``, where *transport* supports
12641266
the :class:`ReadTransport` interface and *protocol* is an object
@@ -1275,7 +1277,9 @@ Working with pipes
12751277
*protocol_factory* must be a callable returning an
12761278
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12771279

1278-
*pipe* is :term:`file-like object <file object>`.
1280+
*pipe* is a :term:`file-like object <file object>`. See
1281+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1282+
supported as *pipe*.
12791283

12801284
Return pair ``(transport, protocol)``, where *transport* supports
12811285
:class:`WriteTransport` interface and *protocol* is an object
@@ -1284,6 +1288,33 @@ Working with pipes
12841288
With :class:`SelectorEventLoop` event loop, the *pipe* is set to
12851289
non-blocking mode.
12861290

1291+
.. _asyncio-pipe-objects:
1292+
1293+
.. rubric:: Supported pipe objects
1294+
1295+
These methods only work with objects the operating system can poll for
1296+
readiness or perform overlapped I/O on. Regular files on disk are **not**
1297+
supported on any platform. There is no asynchronous file I/O in asyncio;
1298+
use :meth:`loop.run_in_executor` to read and write regular files without
1299+
blocking the event loop.
1300+
1301+
On Unix, with :class:`SelectorEventLoop`, *pipe* must wrap one of the
1302+
following:
1303+
1304+
* a pipe, such as an end of an :func:`os.pipe` pair or a FIFO created with
1305+
:func:`os.mkfifo`;
1306+
* a socket;
1307+
* a character device, such as a terminal.
1308+
1309+
On Windows, where only :class:`ProactorEventLoop` implements these methods,
1310+
*pipe* must wrap a handle opened for overlapped I/O (that is, created with the
1311+
``FILE_FLAG_OVERLAPPED`` flag), since the handle has to be associated with an
1312+
I/O completion port. Handles that were not opened for overlapped I/O are
1313+
rejected. In particular, the standard streams (:data:`sys.stdin`,
1314+
:data:`sys.stdout` and :data:`sys.stderr`), console handles, and the pipes
1315+
created by :func:`os.pipe` are **not** opened for overlapped I/O and therefore
1316+
cannot be used with these methods.
1317+
12871318
.. note::
12881319

12891320
:class:`SelectorEventLoop` does not support the above methods on

Doc/library/asyncio-platforms.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ All Platforms
1919
* :meth:`loop.add_reader` and :meth:`loop.add_writer`
2020
cannot be used to monitor file I/O.
2121

22+
* :meth:`loop.connect_read_pipe` and :meth:`loop.connect_write_pipe`
23+
cannot be used with regular files. See :ref:`Supported pipe objects
24+
<asyncio-pipe-objects>` for the objects that are accepted on each
25+
platform.
26+
2227

2328
Windows
2429
=======
@@ -62,6 +67,10 @@ All event loops on Windows do not support the following methods:
6267
* The :meth:`loop.add_reader` and :meth:`loop.add_writer`
6368
methods are not supported.
6469

70+
* :meth:`loop.connect_read_pipe` and :meth:`loop.connect_write_pipe` only
71+
accept a handle opened for overlapped I/O.
72+
See :ref:`Supported pipe objects <asyncio-pipe-objects>` for which objects are supported.
73+
6574
The resolution of the monotonic clock on Windows is usually around 15.6
6675
milliseconds. The best resolution is 0.5 milliseconds. The resolution depends on the
6776
hardware (availability of `HPET

Lib/asyncio/proactor_events.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, loop, sock, protocol, waiter=None,
6262
self._closing = False # Set when close() called.
6363
self._called_connection_lost = False
6464
self._eof_written = False
65+
self._empty_waiter = None
6566
if self._server is not None:
6667
self._server._attach(self)
6768
self._loop.call_soon(self._protocol.connection_made, self)
@@ -331,10 +332,6 @@ class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport,
331332

332333
_start_tls_compatible = True
333334

334-
def __init__(self, *args, **kw):
335-
super().__init__(*args, **kw)
336-
self._empty_waiter = None
337-
338335
def write(self, data):
339336
if not isinstance(data, (bytes, bytearray, memoryview)):
340337
raise TypeError(
@@ -465,7 +462,6 @@ class _ProactorDatagramTransport(_ProactorBasePipeTransport,
465462
def __init__(self, loop, sock, protocol, address=None,
466463
waiter=None, extra=None):
467464
self._address = address
468-
self._empty_waiter = None
469465
self._buffer_size = 0
470466
# We don't need to call _protocol.connection_made() since our base
471467
# constructor does it for us.

Lib/test/test_asyncio/test_events.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from multiprocessing.util import _cleanup_tests as multiprocessing_cleanup_tests
3535
from test.test_asyncio import utils as test_utils
3636
from test import support
37+
from test.support import os_helper
3738
from test.support import socket_helper
3839
from test.support import threading_helper
3940
from test.support import ALWAYS_EQ, LARGEST, SMALLEST
@@ -3049,5 +3050,100 @@ def test_get_loop(self):
30493050
events.AbstractServer().get_loop()
30503051

30513052

3053+
@unittest.skipIf(sys.platform == 'win32', 'Unix pipe transport semantics')
3054+
class UnixPipeObjectSupportTests(unittest.TestCase):
3055+
def setUp(self):
3056+
super().setUp()
3057+
self.loop = asyncio.SelectorEventLoop()
3058+
self.addCleanup(self.loop.close)
3059+
3060+
def check_accepted(self, connect, pipeobj):
3061+
lost = self.loop.create_future()
3062+
3063+
class Proto(asyncio.Protocol):
3064+
def connection_lost(self, exc):
3065+
if not lost.done():
3066+
lost.set_result(exc)
3067+
3068+
async def run():
3069+
transport, protocol = await connect(Proto, pipeobj)
3070+
self.assertIsInstance(protocol, Proto)
3071+
self.assertFalse(pipeobj.closed)
3072+
transport.close()
3073+
self.assertIsNone(await lost)
3074+
3075+
self.loop.run_until_complete(run())
3076+
self.assertTrue(pipeobj.closed)
3077+
3078+
def check_rejected(self, connect, pipeobj):
3079+
self.addCleanup(pipeobj.close)
3080+
with self.assertRaisesRegex(ValueError, 'Pipe transport is'):
3081+
self.loop.run_until_complete(connect(asyncio.Protocol, pipeobj))
3082+
3083+
def test_read_pipe(self):
3084+
rfd, wfd = os.pipe()
3085+
self.addCleanup(os.close, wfd)
3086+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3087+
3088+
def test_write_pipe(self):
3089+
rfd, wfd = os.pipe()
3090+
self.addCleanup(os.close, rfd)
3091+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3092+
3093+
def test_read_fifo(self):
3094+
path = os_helper.TESTFN
3095+
os.mkfifo(path)
3096+
self.addCleanup(os_helper.unlink, path)
3097+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3098+
wfd = os.open(path, os.O_WRONLY)
3099+
self.addCleanup(os.close, wfd)
3100+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3101+
3102+
def test_write_fifo(self):
3103+
path = os_helper.TESTFN
3104+
os.mkfifo(path)
3105+
self.addCleanup(os_helper.unlink, path)
3106+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3107+
self.addCleanup(os.close, rfd)
3108+
wfd = os.open(path, os.O_WRONLY)
3109+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3110+
3111+
def test_read_socket(self):
3112+
rsock, wsock = socket.socketpair()
3113+
self.addCleanup(wsock.close)
3114+
self.check_accepted(self.loop.connect_read_pipe,
3115+
open(rsock.detach(), 'rb', 0))
3116+
3117+
def test_write_socket(self):
3118+
rsock, wsock = socket.socketpair()
3119+
self.addCleanup(rsock.close)
3120+
self.check_accepted(self.loop.connect_write_pipe,
3121+
open(wsock.detach(), 'wb', 0))
3122+
3123+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3124+
def test_read_character_device(self):
3125+
master, slave = os.openpty()
3126+
self.addCleanup(os.close, slave)
3127+
self.check_accepted(self.loop.connect_read_pipe, open(master, 'rb', 0))
3128+
3129+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3130+
def test_write_character_device(self):
3131+
master, slave = os.openpty()
3132+
self.addCleanup(os.close, master)
3133+
self.check_accepted(self.loop.connect_write_pipe, open(slave, 'wb', 0))
3134+
3135+
def test_read_regular_file(self):
3136+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3137+
with open(os_helper.TESTFN, 'wb') as f:
3138+
f.write(b'spam')
3139+
self.check_rejected(self.loop.connect_read_pipe,
3140+
open(os_helper.TESTFN, 'rb', 0))
3141+
3142+
def test_write_regular_file(self):
3143+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3144+
self.check_rejected(self.loop.connect_write_pipe,
3145+
open(os_helper.TESTFN, 'wb', 0))
3146+
3147+
30523148
if __name__ == '__main__':
30533149
unittest.main()

Lib/test/test_asyncio/test_windows_events.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import threading
77
import unittest
8+
import warnings
89
from unittest import mock
910

1011
if sys.platform != 'win32':
@@ -15,6 +16,9 @@
1516

1617
import asyncio
1718
from asyncio import windows_events
19+
from asyncio import windows_utils
20+
from test import support
21+
from test.support import os_helper
1822
from test.test_asyncio import utils as test_utils
1923

2024

@@ -355,5 +359,110 @@ async def main():
355359
asyncio.set_event_loop_policy(old_policy)
356360

357361

362+
class ProactorPipeObjectSupportTests(unittest.TestCase):
363+
364+
def setUp(self):
365+
super().setUp()
366+
self.loop = asyncio.ProactorEventLoop()
367+
self.addCleanup(self.loop.close)
368+
self.errors = []
369+
self.loop.set_exception_handler(
370+
lambda loop, context: self.errors.append(context))
371+
372+
def check_read_rejected(self, pipe):
373+
self.addCleanup(pipe.close)
374+
lost = self.loop.create_future()
375+
376+
class Proto(asyncio.Protocol):
377+
def connection_lost(self, exc):
378+
if not lost.done():
379+
lost.set_result(exc)
380+
381+
async def run():
382+
transport, _ = await self.loop.connect_read_pipe(Proto, pipe)
383+
exc = await lost
384+
transport.close()
385+
return exc
386+
387+
exc = self.loop.run_until_complete(run())
388+
self.assertIsInstance(exc, OSError)
389+
390+
def check_write_rejected(self, pipe):
391+
self.addCleanup(pipe.close)
392+
with warnings.catch_warnings():
393+
warnings.simplefilter('ignore', ResourceWarning)
394+
with self.assertRaises(OSError):
395+
self.loop.run_until_complete(
396+
self.loop.connect_write_pipe(asyncio.BaseProtocol, pipe))
397+
support.gc_collect()
398+
399+
def check_accepted(self, rpipe, wpipe):
400+
self.addCleanup(rpipe.close)
401+
self.addCleanup(wpipe.close)
402+
403+
chunks = []
404+
lost = self.loop.create_future()
405+
406+
class ReadProto(asyncio.Protocol):
407+
def data_received(self, data):
408+
chunks.append(data)
409+
410+
def connection_lost(self, exc):
411+
if not lost.done():
412+
lost.set_result(exc)
413+
414+
async def run():
415+
rtransport, _ = await self.loop.connect_read_pipe(ReadProto, rpipe)
416+
wtransport, _ = await self.loop.connect_write_pipe(
417+
asyncio.BaseProtocol, wpipe)
418+
wtransport.write(b'spam')
419+
wtransport.close()
420+
await lost
421+
rtransport.close()
422+
423+
self.loop.run_until_complete(run())
424+
self.assertEqual(b''.join(chunks), b'spam')
425+
self.assertFalse(self.errors)
426+
427+
def test_overlapped_pipe(self):
428+
rhandle, whandle = windows_utils.pipe(duplex=True, overlapped=(True, True))
429+
self.check_accepted(windows_utils.PipeHandle(rhandle),
430+
windows_utils.PipeHandle(whandle))
431+
432+
def test_socketpair(self):
433+
rsock, wsock = socket.socketpair()
434+
self.check_accepted(rsock, wsock)
435+
436+
def test_read_non_overlapped_pipe(self):
437+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
438+
self.addCleanup(windows_utils.PipeHandle(whandle).close)
439+
self.check_read_rejected(windows_utils.PipeHandle(rhandle))
440+
441+
def test_write_non_overlapped_pipe(self):
442+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
443+
self.addCleanup(windows_utils.PipeHandle(rhandle).close)
444+
self.check_write_rejected(windows_utils.PipeHandle(whandle))
445+
446+
def test_read_regular_file(self):
447+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
448+
with open(os_helper.TESTFN, 'wb') as f:
449+
f.write(b'spam')
450+
self.check_read_rejected(open(os_helper.TESTFN, 'rb', 0))
451+
452+
def test_write_regular_file(self):
453+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
454+
self.check_write_rejected(open(os_helper.TESTFN, 'wb', 0))
455+
456+
def test_read_os_pipe(self):
457+
rfd, wfd = os.pipe()
458+
self.addCleanup(os.close, wfd)
459+
self.check_read_rejected(open(rfd, 'rb', 0))
460+
461+
def test_write_os_pipe(self):
462+
rfd, wfd = os.pipe()
463+
self.addCleanup(os.close, rfd)
464+
self.check_write_rejected(open(wfd, 'wb', 0))
465+
466+
358467
if __name__ == '__main__':
359468
unittest.main()

0 commit comments

Comments
 (0)