Skip to content

Commit 9465c84

Browse files
[3.15] gh-71019: document which objects connect_read_pipe/connect_write_pipe accept and add tests (GH-153660) (#153685)
gh-71019: document which objects connect_read_pipe/connect_write_pipe accept and add tests (GH-153660) (cherry picked from commit 7fb315b)
1 parent 3e99c70 commit 9465c84

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
@@ -1274,7 +1274,9 @@ Working with pipes
12741274
*protocol_factory* must be a callable returning an
12751275
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12761276

1277-
*pipe* is a :term:`file-like object <file object>`.
1277+
*pipe* is a :term:`file-like object <file object>`. See
1278+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1279+
supported as *pipe*.
12781280

12791281
Return pair ``(transport, protocol)``, where *transport* supports
12801282
the :class:`ReadTransport` interface and *protocol* is an object
@@ -1291,7 +1293,9 @@ Working with pipes
12911293
*protocol_factory* must be a callable returning an
12921294
:ref:`asyncio protocol <asyncio-protocol>` implementation.
12931295

1294-
*pipe* is :term:`file-like object <file object>`.
1296+
*pipe* is a :term:`file-like object <file object>`. See
1297+
:ref:`Supported pipe objects <asyncio-pipe-objects>` for the objects
1298+
supported as *pipe*.
12951299

12961300
Return pair ``(transport, protocol)``, where *transport* supports
12971301
:class:`WriteTransport` interface and *protocol* is an object
@@ -1300,6 +1304,33 @@ Working with pipes
13001304
With :class:`SelectorEventLoop` event loop, the *pipe* is set to
13011305
non-blocking mode.
13021306

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

13051336
: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
@@ -3172,5 +3173,100 @@ def test_get_loop(self):
31723173
events.AbstractServer().get_loop()
31733174

31743175

3176+
@unittest.skipIf(sys.platform == 'win32', 'Unix pipe transport semantics')
3177+
class UnixPipeObjectSupportTests(unittest.TestCase):
3178+
def setUp(self):
3179+
super().setUp()
3180+
self.loop = asyncio.SelectorEventLoop()
3181+
self.addCleanup(self.loop.close)
3182+
3183+
def check_accepted(self, connect, pipeobj):
3184+
lost = self.loop.create_future()
3185+
3186+
class Proto(asyncio.Protocol):
3187+
def connection_lost(self, exc):
3188+
if not lost.done():
3189+
lost.set_result(exc)
3190+
3191+
async def run():
3192+
transport, protocol = await connect(Proto, pipeobj)
3193+
self.assertIsInstance(protocol, Proto)
3194+
self.assertFalse(pipeobj.closed)
3195+
transport.close()
3196+
self.assertIsNone(await lost)
3197+
3198+
self.loop.run_until_complete(run())
3199+
self.assertTrue(pipeobj.closed)
3200+
3201+
def check_rejected(self, connect, pipeobj):
3202+
self.addCleanup(pipeobj.close)
3203+
with self.assertRaisesRegex(ValueError, 'Pipe transport is'):
3204+
self.loop.run_until_complete(connect(asyncio.Protocol, pipeobj))
3205+
3206+
def test_read_pipe(self):
3207+
rfd, wfd = os.pipe()
3208+
self.addCleanup(os.close, wfd)
3209+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3210+
3211+
def test_write_pipe(self):
3212+
rfd, wfd = os.pipe()
3213+
self.addCleanup(os.close, rfd)
3214+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3215+
3216+
def test_read_fifo(self):
3217+
path = os_helper.TESTFN
3218+
os.mkfifo(path)
3219+
self.addCleanup(os_helper.unlink, path)
3220+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3221+
wfd = os.open(path, os.O_WRONLY)
3222+
self.addCleanup(os.close, wfd)
3223+
self.check_accepted(self.loop.connect_read_pipe, open(rfd, 'rb', 0))
3224+
3225+
def test_write_fifo(self):
3226+
path = os_helper.TESTFN
3227+
os.mkfifo(path)
3228+
self.addCleanup(os_helper.unlink, path)
3229+
rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
3230+
self.addCleanup(os.close, rfd)
3231+
wfd = os.open(path, os.O_WRONLY)
3232+
self.check_accepted(self.loop.connect_write_pipe, open(wfd, 'wb', 0))
3233+
3234+
def test_read_socket(self):
3235+
rsock, wsock = socket.socketpair()
3236+
self.addCleanup(wsock.close)
3237+
self.check_accepted(self.loop.connect_read_pipe,
3238+
open(rsock.detach(), 'rb', 0))
3239+
3240+
def test_write_socket(self):
3241+
rsock, wsock = socket.socketpair()
3242+
self.addCleanup(rsock.close)
3243+
self.check_accepted(self.loop.connect_write_pipe,
3244+
open(wsock.detach(), 'wb', 0))
3245+
3246+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3247+
def test_read_character_device(self):
3248+
master, slave = os.openpty()
3249+
self.addCleanup(os.close, slave)
3250+
self.check_accepted(self.loop.connect_read_pipe, open(master, 'rb', 0))
3251+
3252+
@unittest.skipUnless(hasattr(os, 'openpty'), 'need os.openpty()')
3253+
def test_write_character_device(self):
3254+
master, slave = os.openpty()
3255+
self.addCleanup(os.close, master)
3256+
self.check_accepted(self.loop.connect_write_pipe, open(slave, 'wb', 0))
3257+
3258+
def test_read_regular_file(self):
3259+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3260+
with open(os_helper.TESTFN, 'wb') as f:
3261+
f.write(b'spam')
3262+
self.check_rejected(self.loop.connect_read_pipe,
3263+
open(os_helper.TESTFN, 'rb', 0))
3264+
3265+
def test_write_regular_file(self):
3266+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
3267+
self.check_rejected(self.loop.connect_write_pipe,
3268+
open(os_helper.TESTFN, 'wb', 0))
3269+
3270+
31753271
if __name__ == '__main__':
31763272
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

@@ -359,5 +363,110 @@ async def main():
359363
asyncio.events._set_event_loop_policy(old_policy)
360364

361365

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

0 commit comments

Comments
 (0)