Skip to content

Commit 7fb315b

Browse files
gh-71019: document which objects connect_read_pipe/connect_write_pipe accept and add tests (#153660)
1 parent 2fc3c4d commit 7fb315b

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
@@ -1263,7 +1263,9 @@ Working with pipes
12631263
*protocol_factory* must be a callable returning an
12641264
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12651265

1266-
*pipe* is a :term:`file-like object <file object>`.
1266+
*pipe* is a :term:`file-like object <file object>`. See
1267+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1268+
supported as *pipe*.
12671269

12681270
Return pair ``(transport, protocol)``, where *transport* supports
12691271
the :class:`ReadTransport` interface and *protocol* is an object
@@ -1280,7 +1282,9 @@ Working with pipes
12801282
*protocol_factory* must be a callable returning an
12811283
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12821284

1283-
*pipe* is :term:`file-like object <file object>`.
1285+
*pipe* is a :term:`file-like object <file object>`. See
1286+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1287+
supported as *pipe*.
12841288

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

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

12941325
: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
@@ -33,6 +33,7 @@
3333
from multiprocessing.util import _cleanup_tests as multiprocessing_cleanup_tests
3434
from test.test_asyncio import utils as test_utils
3535
from test import support
36+
from test.support import os_helper
3637
from test.support import socket_helper
3738
from test.support import threading_helper
3839
from test.support import ALWAYS_EQ, LARGEST, SMALLEST
@@ -3134,5 +3135,100 @@ def test_get_loop(self):
31343135
events.AbstractServer().get_loop()
31353136

31363137

3138+
@unittest.skipIf(sys.platform == 'win32', 'Unix pipe transport semantics')
3139+
class UnixPipeObjectSupportTests(unittest.TestCase):
3140+
def setUp(self):
3141+
super().setUp()
3142+
self.loop = asyncio.SelectorEventLoop()
3143+
self.addCleanup(self.loop.close)
3144+
3145+
def check_accepted(self, connect, pipeobj):
3146+
lost = self.loop.create_future()
3147+
3148+
class Proto(asyncio.Protocol):
3149+
def connection_lost(self, exc):
3150+
if not lost.done():
3151+
lost.set_result(exc)
3152+
3153+
async def run():
3154+
transport, protocol = await connect(Proto, pipeobj)
3155+
self.assertIsInstance(protocol, Proto)
3156+
self.assertFalse(pipeobj.closed)
3157+
transport.close()
3158+
self.assertIsNone(await lost)
3159+
3160+
self.loop.run_until_complete(run())
3161+
self.assertTrue(pipeobj.closed)
3162+
3163+
def check_rejected(self, connect, pipeobj):
3164+
self.addCleanup(pipeobj.close)
3165+
with self.assertRaisesRegex(ValueError, 'Pipe transport is'):
3166+
self.loop.run_until_complete(connect(asyncio.Protocol, pipeobj))
3167+
3168+
def test_read_pipe(self):
3169+
rfd, wfd = os.pipe()
3170+
self.addCleanup(os.close, wfd)
3171+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3172+
3173+
def test_write_pipe(self):
3174+
rfd, wfd = os.pipe()
3175+
self.addCleanup(os.close, rfd)
3176+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3177+
3178+
def test_read_fifo(self):
3179+
path = os_helper.TESTFN
3180+
os.mkfifo(path)
3181+
self.addCleanup(os_helper.unlink, path)
3182+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3183+
wfd = os.open(path, os.O_WRONLY)
3184+
self.addCleanup(os.close, wfd)
3185+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3186+
3187+
def test_write_fifo(self):
3188+
path = os_helper.TESTFN
3189+
os.mkfifo(path)
3190+
self.addCleanup(os_helper.unlink, path)
3191+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3192+
self.addCleanup(os.close, rfd)
3193+
wfd = os.open(path, os.O_WRONLY)
3194+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3195+
3196+
def test_read_socket(self):
3197+
rsock, wsock = socket.socketpair()
3198+
self.addCleanup(wsock.close)
3199+
self.check_accepted(self.loop.connect_read_pipe,
3200+
open(rsock.detach(), 'rb', 0))
3201+
3202+
def test_write_socket(self):
3203+
rsock, wsock = socket.socketpair()
3204+
self.addCleanup(rsock.close)
3205+
self.check_accepted(self.loop.connect_write_pipe,
3206+
open(wsock.detach(), 'wb', 0))
3207+
3208+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3209+
def test_read_character_device(self):
3210+
master, slave = os.openpty()
3211+
self.addCleanup(os.close, slave)
3212+
self.check_accepted(self.loop.connect_read_pipe, open(master, 'rb', 0))
3213+
3214+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3215+
def test_write_character_device(self):
3216+
master, slave = os.openpty()
3217+
self.addCleanup(os.close, master)
3218+
self.check_accepted(self.loop.connect_write_pipe, open(slave, 'wb', 0))
3219+
3220+
def test_read_regular_file(self):
3221+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3222+
with open(os_helper.TESTFN, 'wb') as f:
3223+
f.write(b'spam')
3224+
self.check_rejected(self.loop.connect_read_pipe,
3225+
open(os_helper.TESTFN, 'rb', 0))
3226+
3227+
def test_write_regular_file(self):
3228+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3229+
self.check_rejected(self.loop.connect_write_pipe,
3230+
open(os_helper.TESTFN, 'wb', 0))
3231+
3232+
31373233
if __name__ == '__main__':
31383234
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

@@ -324,5 +328,110 @@ def threadMain():
324328
thr.join()
325329

326330

331+
class ProactorPipeObjectSupportTests(unittest.TestCase):
332+
333+
def setUp(self):
334+
super().setUp()
335+
self.loop = asyncio.ProactorEventLoop()
336+
self.addCleanup(self.loop.close)
337+
self.errors = []
338+
self.loop.set_exception_handler(
339+
lambda loop, context: self.errors.append(context))
340+
341+
def check_read_rejected(self, pipe):
342+
self.addCleanup(pipe.close)
343+
lost = self.loop.create_future()
344+
345+
class Proto(asyncio.Protocol):
346+
def connection_lost(self, exc):
347+
if not lost.done():
348+
lost.set_result(exc)
349+
350+
async def run():
351+
transport, _ = await self.loop.connect_read_pipe(Proto, pipe)
352+
exc = await lost
353+
transport.close()
354+
return exc
355+
356+
exc = self.loop.run_until_complete(run())
357+
self.assertIsInstance(exc, OSError)
358+
359+
def check_write_rejected(self, pipe):
360+
self.addCleanup(pipe.close)
361+
with warnings.catch_warnings():
362+
warnings.simplefilter('ignore', ResourceWarning)
363+
with self.assertRaises(OSError):
364+
self.loop.run_until_complete(
365+
self.loop.connect_write_pipe(asyncio.BaseProtocol, pipe))
366+
support.gc_collect()
367+
368+
def check_accepted(self, rpipe, wpipe):
369+
self.addCleanup(rpipe.close)
370+
self.addCleanup(wpipe.close)
371+
372+
chunks = []
373+
lost = self.loop.create_future()
374+
375+
class ReadProto(asyncio.Protocol):
376+
def data_received(self, data):
377+
chunks.append(data)
378+
379+
def connection_lost(self, exc):
380+
if not lost.done():
381+
lost.set_result(exc)
382+
383+
async def run():
384+
rtransport, _ = await self.loop.connect_read_pipe(ReadProto, rpipe)
385+
wtransport, _ = await self.loop.connect_write_pipe(
386+
asyncio.BaseProtocol, wpipe)
387+
wtransport.write(b'spam')
388+
wtransport.close()
389+
await lost
390+
rtransport.close()
391+
392+
self.loop.run_until_complete(run())
393+
self.assertEqual(b''.join(chunks), b'spam')
394+
self.assertFalse(self.errors)
395+
396+
def test_overlapped_pipe(self):
397+
rhandle, whandle = windows_utils.pipe(duplex=True, overlapped=(True, True))
398+
self.check_accepted(windows_utils.PipeHandle(rhandle),
399+
windows_utils.PipeHandle(whandle))
400+
401+
def test_socketpair(self):
402+
rsock, wsock = socket.socketpair()
403+
self.check_accepted(rsock, wsock)
404+
405+
def test_read_non_overlapped_pipe(self):
406+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
407+
self.addCleanup(windows_utils.PipeHandle(whandle).close)
408+
self.check_read_rejected(windows_utils.PipeHandle(rhandle))
409+
410+
def test_write_non_overlapped_pipe(self):
411+
rhandle, whandle = windows_utils.pipe(overlapped=(False, False))
412+
self.addCleanup(windows_utils.PipeHandle(rhandle).close)
413+
self.check_write_rejected(windows_utils.PipeHandle(whandle))
414+
415+
def test_read_regular_file(self):
416+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
417+
with open(os_helper.TESTFN, 'wb') as f:
418+
f.write(b'spam')
419+
self.check_read_rejected(open(os_helper.TESTFN, 'rb', 0))
420+
421+
def test_write_regular_file(self):
422+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
423+
self.check_write_rejected(open(os_helper.TESTFN, 'wb', 0))
424+
425+
def test_read_os_pipe(self):
426+
rfd, wfd = os.pipe()
427+
self.addCleanup(os.close, wfd)
428+
self.check_read_rejected(open(rfd, 'rb', 0))
429+
430+
def test_write_os_pipe(self):
431+
rfd, wfd = os.pipe()
432+
self.addCleanup(os.close, rfd)
433+
self.check_write_rejected(open(wfd, 'wb', 0))
434+
435+
327436
if __name__ == '__main__':
328437
unittest.main()

0 commit comments

Comments
 (0)