Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subprocess support: run_process() #872

Merged
merged 18 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 72 additions & 106 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -651,16 +651,47 @@ Spawning subprocesses

Trio provides support for spawning other programs as subprocesses,
communicating with them via pipes, sending them signals, and waiting
for them to exit. Currently this interface consists of the
:class:`trio.Process` class, which is modelled after :class:`subprocess.Popen`
in the standard library.
for them to exit. The interface for doing so consists of two layers:

* :func:`trio.run_process` runs a process from start to
finish and returns a :class:`~subprocess.CompletedProcess` object describing
its outputs and return value. This is what you should reach for if you
want to run a process to completion before continuing, while possibly
sending it some input or capturing its output. It is modelled after
the standard :func:`subprocess.run` with some additional features
and safer defaults.

* :class:`trio.Process` starts a process in the background and optionally
provides Trio streams for interacting with it (sending input,
receiving output and errors). Using it requires a bit more code
than :func:`~trio.run_process`, but exposes additional capabilities:
back-and-forth communication, processing output as soon as it is generated,
and so forth. It is modelled after the standard :class:`subprocess.Popen`.


.. _subprocess-options:

Options for starting subprocesses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

<<<<<<< HEAD
oremanj marked this conversation as resolved.
Show resolved Hide resolved
All of Trio's subprocess APIs accept the numerous keyword arguments used
by the standard :mod:`subprocess` module to control the environment in
which a process starts and the mechanisms used for communicating with
it. These may be passed wherever you see ``**options`` in the
documentation below. See the `full list
<https://docs.python.org/3/library/subprocess.html#popen-constructor>`__
or just the `frequently used ones
<https://docs.python.org/3/library/subprocess.html#frequently-used-arguments>`__
in the :mod:`subprocess` documentation. (You may need to ``import
subprocess`` in order to access constants such as ``PIPE`` or
``DEVNULL``.)

Currently, Trio always uses unbuffered byte streams for communicating with a
process, so it does not support the ``encoding``, ``errors``,
``universal_newlines`` (alias ``text`` in 3.7+), and ``bufsize``
options.
=======
oremanj marked this conversation as resolved.
Show resolved Hide resolved
The standard :mod:`subprocess` module supports a dizzying array
of `options <https://docs.python.org/3/library/subprocess.html#popen-constructor>`__
for controlling the environment in which a process starts and the
Expand All @@ -680,80 +711,61 @@ Trio always uses unbuffered byte streams for communicating with a
process, so these options don't make sense. Text I/O should use a
layer on top of the raw byte streams, just as it does with sockets.
[This layer does not yet exist, but is in the works.]
>>>>>>> origin/master
oremanj marked this conversation as resolved.
Show resolved Hide resolved


Running a process and waiting for it to finish
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We're `working on <https://github.com/python-trio/trio/pull/791>`__
figuring out the best API for common higher-level subprocess operations.
In the meantime, you can implement something like the standard library
:func:`subprocess.run` in terms of :class:`trio.Process`
as follows::

async def run(
command, *, input=None, capture_output=False, **options
):
if input is not None:
options['stdin'] = subprocess.PIPE
if capture_output:
options['stdout'] = options['stderr'] = subprocess.PIPE

stdout_chunks = []
stderr_chunks = []

async with trio.Process(command, **options) as proc:

async def feed_input():
async with proc.stdin:
if input:
try:
await proc.stdin.send_all(input)
except trio.BrokenResourceError:
pass

async def read_output(stream, chunks):
async with stream:
while True:
chunk = await stream.receive_some(32768)
if not chunk:
break
chunks.append(chunk)

async with trio.open_nursery() as nursery:
if proc.stdin is not None:
nursery.start_soon(feed_input)
if proc.stdout is not None:
nursery.start_soon(read_output, proc.stdout, stdout_chunks)
if proc.stderr is not None:
nursery.start_soon(read_output, proc.stderr, stderr_chunks)
await proc.wait()

stdout = b"".join(stdout_chunks) if proc.stdout is not None else None
stderr = b"".join(stderr_chunks) if proc.stderr is not None else None

if proc.returncode:
raise subprocess.CalledProcessError(
proc.returncode, proc.args, output=stdout, stderr=stderr
)
else:
return subprocess.CompletedProcess(
proc.args, proc.returncode, stdout, stderr
)
The basic interface for running a subprocess start-to-finish is
:func:`trio.run_process`. It always waits for the subprocess to exit
before returning, so there's no need to worry about leaving a process
running by mistake after you've gone on to do other things.
:func:`~trio.run_process` is similar to the standard library
:func:`subprocess.run` function, but tries to have safer defaults:
with no options, the subprocess's input is provided and its outputs
are captured by the parent Trio process rather than connecting them
to the user's terminal, and a failure in the subprocess will be propagated
as a :exc:`subprocess.CalledProcessError` exception. Of course, these
defaults can be changed where necessary.

.. autofunction:: trio.run_process


Interacting with a process as it runs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can spawn a subprocess by creating an instance of
If you want more control than :func:`~trio.run_process` affords,
you can spawn a subprocess by creating an instance of
:class:`trio.Process` and then interact with it using its
:attr:`~trio.Process.stdin`,
:attr:`~trio.Process.stdout`, and/or
:attr:`~trio.Process.stderr` streams.

.. autoclass:: trio.Process
:members:

.. autoattribute:: returncode

.. automethod:: aclose

.. automethod:: wait

.. automethod:: poll

.. automethod:: kill

.. automethod:: terminate

.. automethod:: send_signal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does .. autoclass: :members: not pick these up for some reason...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be able to control the order, and I couldn't get them to go in non-alphabetical order using :members:. (This despite our docs/source/conf.py having autodoc_member_order = "bysource", and also trying an explicit :member-order: bysource.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, that's python-trio/sphinxcontrib-trio#13

This was already fixed back in August, we just never released it... whoops. I just released that, so hopefully bysource works now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can drop these now; "bysource" should work now. (And if it doesn't I want to know ;-))


.. note:: :meth:`~subprocess.Popen.communicate` is not provided as a
method on :class:`~trio.Process` objects; use :func:`~trio.run_process`
instead, or write the loop yourself if you have unusual
needs. :meth:`~subprocess.Popen.communicate` has quite unusual
cancellation behavior in the standard library (on some platforms it
spawns a background thread which continues to read from the child
process even after the timeout has expired) and we wanted to
provide an interface with fewer surprises.

.. _subprocess-quoting:

Expand Down Expand Up @@ -849,52 +861,6 @@ Further reading:
* https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts


Differences from :class:`subprocess.Popen`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* All arguments to the constructor of
:class:`~trio.Process`, except the command to run, must be
passed using keywords.

* :meth:`~subprocess.Popen.communicate` is not provided as a method on
:class:`~trio.Process` objects; use a higher-level
function instead, or write the loop yourself if
you have unusual needs. :meth:`~subprocess.Popen.communicate` has
quite unusual cancellation behavior in the standard library (on some
platforms it spawns a background thread which continues to read from
the child process even after the timeout has expired) and we wanted
to provide an interface with fewer surprises.

* :meth:`~trio.Process.wait` is an async function that does
not take a ``timeout`` argument; combine it with
:func:`~trio.fail_after` if you want a timeout.

* Text I/O is not supported: you may not use the
:class:`~trio.Process` constructor arguments
``universal_newlines`` (or its 3.7+ alias ``text``), ``encoding``,
or ``errors``.

* :attr:`~trio.Process.stdin` is a :class:`~trio.abc.SendStream` and
:attr:`~trio.Process.stdout` and :attr:`~trio.Process.stderr`
are :class:`~trio.abc.ReceiveStream`\s, rather than file objects. The
:class:`~trio.Process` constructor argument ``bufsize`` is
not supported since there would be no file object to pass it to.

* :meth:`~trio.Process.aclose` (and thus also
``__aexit__``) behave like the standard :class:`~subprocess.Popen`
context manager exit (close pipes to the process, then wait for it
to exit), but add additional behavior if cancelled: kill the process
and wait for it to finish terminating. This is useful for scoping
the lifetime of a simple subprocess that doesn't spawn any children
of its own. (For subprocesses that do in turn spawn their own
subprocesses, there is not currently any way to clean up the whole
tree; moreover, using the :class:`Process` context manager in such
cases is likely to be counterproductive as killing the top-level
subprocess leaves it no chance to do any cleanup of its children
that might be desired. You'll probably want to write your own
supervision logic in that case.)


Signals
-------

Expand Down
2 changes: 2 additions & 0 deletions newsfragments/822.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`trio.run_process` as a high-level helper for running a process
and waiting for it to finish, like the standard :func:`subprocess.run` does.
2 changes: 1 addition & 1 deletion trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

from ._path import Path

from ._subprocess import Process
from ._subprocess import Process, run_process

from ._ssl import SSLStream, SSLListener, NeedHandshakeError

Expand Down
Loading