Skip to content

Commit f0d4c64

Browse files
sbstpmiss-islington
authored andcommitted
bpo-36686: Improve the documentation of the std* params in loop.subprocess_exec (GH-13586)
https://bugs.python.org/issue36686
1 parent a356841 commit f0d4c64

File tree

4 files changed

+158
-25
lines changed

4 files changed

+158
-25
lines changed

Doc/library/asyncio-eventloop.rst

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,32 +1217,52 @@ async/await code consider using the high-level
12171217

12181218
Other parameters:
12191219

1220-
* *stdin*: either a file-like object representing a pipe to be
1221-
connected to the subprocess's standard input stream using
1222-
:meth:`~loop.connect_write_pipe`, or the
1223-
:const:`subprocess.PIPE` constant (default). By default a new
1224-
pipe will be created and connected.
1225-
1226-
* *stdout*: either a file-like object representing the pipe to be
1227-
connected to the subprocess's standard output stream using
1228-
:meth:`~loop.connect_read_pipe`, or the
1229-
:const:`subprocess.PIPE` constant (default). By default a new pipe
1230-
will be created and connected.
1231-
1232-
* *stderr*: either a file-like object representing the pipe to be
1233-
connected to the subprocess's standard error stream using
1234-
:meth:`~loop.connect_read_pipe`, or one of
1235-
:const:`subprocess.PIPE` (default) or :const:`subprocess.STDOUT`
1236-
constants.
1237-
1238-
By default a new pipe will be created and connected. When
1239-
:const:`subprocess.STDOUT` is specified, the subprocess' standard
1240-
error stream will be connected to the same pipe as the standard
1241-
output stream.
1220+
* *stdin* can be any of these:
1221+
1222+
* a file-like object representing a pipe to be connected to the
1223+
subprocess's standard input stream using
1224+
:meth:`~loop.connect_write_pipe`
1225+
* the :const:`subprocess.PIPE` constant (default) which will create a new
1226+
pipe and connect it,
1227+
* the value ``None`` which will make the subprocess inherit the file
1228+
descriptor from this process
1229+
* the :const:`subprocess.DEVNULL` constant which indicates that the
1230+
special :data:`os.devnull` file will be used
1231+
1232+
* *stdout* can be any of these:
1233+
1234+
* a file-like object representing a pipe to be connected to the
1235+
subprocess's standard output stream using
1236+
:meth:`~loop.connect_write_pipe`
1237+
* the :const:`subprocess.PIPE` constant (default) which will create a new
1238+
pipe and connect it,
1239+
* the value ``None`` which will make the subprocess inherit the file
1240+
descriptor from this process
1241+
* the :const:`subprocess.DEVNULL` constant which indicates that the
1242+
special :data:`os.devnull` file will be used
1243+
1244+
* *stderr* can be any of these:
1245+
1246+
* a file-like object representing a pipe to be connected to the
1247+
subprocess's standard error stream using
1248+
:meth:`~loop.connect_write_pipe`
1249+
* the :const:`subprocess.PIPE` constant (default) which will create a new
1250+
pipe and connect it,
1251+
* the value ``None`` which will make the subprocess inherit the file
1252+
descriptor from this process
1253+
* the :const:`subprocess.DEVNULL` constant which indicates that the
1254+
special :data:`os.devnull` file will be used
1255+
* the :const:`subprocess.STDOUT` constant which will connect the standard
1256+
error stream to the process' standard output stream
12421257

12431258
* All other keyword arguments are passed to :class:`subprocess.Popen`
1244-
without interpretation, except for *bufsize*, *universal_newlines*
1245-
and *shell*, which should not be specified at all.
1259+
without interpretation, except for *bufsize*, *universal_newlines*,
1260+
*shell*, *text*, *encoding* and *errors*, which should not be specified
1261+
at all.
1262+
1263+
The ``asyncio`` subprocess API does not support decoding the streams
1264+
as text. :func:`bytes.decode` can be used to convert the bytes returned
1265+
from the stream to text.
12461266

12471267
See the constructor of the :class:`subprocess.Popen` class
12481268
for documentation on other arguments.

Lib/asyncio/base_events.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,7 @@ async def subprocess_shell(self, protocol_factory, cmd, *,
15551555
stderr=subprocess.PIPE,
15561556
universal_newlines=False,
15571557
shell=True, bufsize=0,
1558+
encoding=None, errors=None, text=None,
15581559
**kwargs):
15591560
if not isinstance(cmd, (bytes, str)):
15601561
raise ValueError("cmd must be a string")
@@ -1564,6 +1565,13 @@ async def subprocess_shell(self, protocol_factory, cmd, *,
15641565
raise ValueError("shell must be True")
15651566
if bufsize != 0:
15661567
raise ValueError("bufsize must be 0")
1568+
if text:
1569+
raise ValueError("text must be False")
1570+
if encoding is not None:
1571+
raise ValueError("encoding must be None")
1572+
if errors is not None:
1573+
raise ValueError("errors must be None")
1574+
15671575
protocol = protocol_factory()
15681576
debug_log = None
15691577
if self._debug:
@@ -1580,13 +1588,22 @@ async def subprocess_shell(self, protocol_factory, cmd, *,
15801588
async def subprocess_exec(self, protocol_factory, program, *args,
15811589
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
15821590
stderr=subprocess.PIPE, universal_newlines=False,
1583-
shell=False, bufsize=0, **kwargs):
1591+
shell=False, bufsize=0,
1592+
encoding=None, errors=None, text=None,
1593+
**kwargs):
15841594
if universal_newlines:
15851595
raise ValueError("universal_newlines must be False")
15861596
if shell:
15871597
raise ValueError("shell must be False")
15881598
if bufsize != 0:
15891599
raise ValueError("bufsize must be 0")
1600+
if text:
1601+
raise ValueError("text must be False")
1602+
if encoding is not None:
1603+
raise ValueError("encoding must be None")
1604+
if errors is not None:
1605+
raise ValueError("errors must be None")
1606+
15901607
popen_args = (program,) + args
15911608
for arg in popen_args:
15921609
if not isinstance(arg, (str, bytes)):

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,63 @@ async def empty_input():
335335
self.assertEqual(output.rstrip(), b'0')
336336
self.assertEqual(exitcode, 0)
337337

338+
def test_devnull_input(self):
339+
340+
async def empty_input():
341+
code = 'import sys; data = sys.stdin.read(); print(len(data))'
342+
proc = await asyncio.create_subprocess_exec(
343+
sys.executable, '-c', code,
344+
stdin=asyncio.subprocess.DEVNULL,
345+
stdout=asyncio.subprocess.PIPE,
346+
stderr=asyncio.subprocess.PIPE,
347+
close_fds=False,
348+
loop=self.loop)
349+
stdout, stderr = await proc.communicate()
350+
exitcode = await proc.wait()
351+
return (stdout, exitcode)
352+
353+
output, exitcode = self.loop.run_until_complete(empty_input())
354+
self.assertEqual(output.rstrip(), b'0')
355+
self.assertEqual(exitcode, 0)
356+
357+
def test_devnull_output(self):
358+
359+
async def empty_output():
360+
code = 'import sys; data = sys.stdin.read(); print(len(data))'
361+
proc = await asyncio.create_subprocess_exec(
362+
sys.executable, '-c', code,
363+
stdin=asyncio.subprocess.PIPE,
364+
stdout=asyncio.subprocess.DEVNULL,
365+
stderr=asyncio.subprocess.PIPE,
366+
close_fds=False,
367+
loop=self.loop)
368+
stdout, stderr = await proc.communicate(b"abc")
369+
exitcode = await proc.wait()
370+
return (stdout, exitcode)
371+
372+
output, exitcode = self.loop.run_until_complete(empty_output())
373+
self.assertEqual(output, None)
374+
self.assertEqual(exitcode, 0)
375+
376+
def test_devnull_error(self):
377+
378+
async def empty_error():
379+
code = 'import sys; data = sys.stdin.read(); print(len(data))'
380+
proc = await asyncio.create_subprocess_exec(
381+
sys.executable, '-c', code,
382+
stdin=asyncio.subprocess.PIPE,
383+
stdout=asyncio.subprocess.PIPE,
384+
stderr=asyncio.subprocess.DEVNULL,
385+
close_fds=False,
386+
loop=self.loop)
387+
stdout, stderr = await proc.communicate(b"abc")
388+
exitcode = await proc.wait()
389+
return (stderr, exitcode)
390+
391+
output, exitcode = self.loop.run_until_complete(empty_error())
392+
self.assertEqual(output, None)
393+
self.assertEqual(exitcode, 0)
394+
338395
def test_cancel_process_wait(self):
339396
# Issue #23140: cancel Process.wait()
340397

@@ -531,6 +588,39 @@ def test_process_create_warning(self):
531588
with self.assertWarns(DeprecationWarning):
532589
subprocess.Process(transp, proto, loop=self.loop)
533590

591+
def test_create_subprocess_exec_text_mode_fails(self):
592+
async def execute():
593+
with self.assertRaises(ValueError):
594+
await subprocess.create_subprocess_exec(sys.executable,
595+
text=True)
596+
597+
with self.assertRaises(ValueError):
598+
await subprocess.create_subprocess_exec(sys.executable,
599+
encoding="utf-8")
600+
601+
with self.assertRaises(ValueError):
602+
await subprocess.create_subprocess_exec(sys.executable,
603+
errors="strict")
604+
605+
self.loop.run_until_complete(execute())
606+
607+
def test_create_subprocess_shell_text_mode_fails(self):
608+
609+
async def execute():
610+
with self.assertRaises(ValueError):
611+
await subprocess.create_subprocess_shell(sys.executable,
612+
text=True)
613+
614+
with self.assertRaises(ValueError):
615+
await subprocess.create_subprocess_shell(sys.executable,
616+
encoding="utf-8")
617+
618+
with self.assertRaises(ValueError):
619+
await subprocess.create_subprocess_shell(sys.executable,
620+
errors="strict")
621+
622+
self.loop.run_until_complete(execute())
623+
534624

535625
if sys.platform != 'win32':
536626
# Unix
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Improve documentation of the stdin, stdout, and stderr arguments of of the
2+
``asyncio.subprocess_exec`` function to specficy which values are supported.
3+
Also mention that decoding as text is not supported.
4+
5+
Add a few tests to verify that the various values passed to the std*
6+
arguments actually work.

0 commit comments

Comments
 (0)