Issue #20400: Merge Tulip into Python: add the new asyncio.subprocess module

* Add a new asyncio.subprocess module
* Add new create_subprocess_exec() and create_subprocess_shell() functions
* The new asyncio.subprocess.SubprocessStreamProtocol creates stream readers
  for stdout and stderr and a stream writer for stdin.
* The new asyncio.subprocess.Process class offers an API close to the
  subprocess.Popen class:

  - pid, returncode, stdin, stdout and stderr attributes
  - communicate(), wait(), send_signal(), terminate() and kill() methods

* Remove STDIN (0), STDOUT (1) and STDERR (2) constants from base_subprocess
  and unix_events, to not be confused with the symbols with the same name of
  subprocess and asyncio.subprocess modules
* _ProactorBasePipeTransport.get_write_buffer_size() now counts also the size
  of the pending write
* _ProactorBaseWritePipeTransport._loop_writing() may now pause the protocol if
  the write buffer size is greater than the high water mark (64 KB by default)
This commit is contained in:
Victor Stinner 2014-02-01 22:49:59 +01:00
parent 153d97b24e
commit 915bcb0111
9 changed files with 434 additions and 50 deletions

View file

@ -116,18 +116,17 @@ class BaseEventLoopTests(unittest.TestCase):
self.loop.stop()
self.loop._process_events = unittest.mock.Mock()
delay = 0.1
when = self.loop.time() + delay
when = self.loop.time() + 0.1
self.loop.call_at(when, cb)
t0 = self.loop.time()
self.loop.run_forever()
dt = self.loop.time() - t0
self.assertGreaterEqual(dt, delay - self.loop._granularity, dt)
# tolerate a difference of +800 ms because some Python buildbots
# are really slow
self.assertLessEqual(dt, 0.9, dt)
self.assertTrue(0.09 <= dt <= 0.9,
# Issue #20452: add more info in case of failure,
# to try to investigate the bug
(dt,
self.loop._granularity,
time.get_clock_info('monotonic')))
def test_run_once_in_executor_handle(self):
def cb():

View file

@ -1179,14 +1179,6 @@ class EventLoopTestsMixin:
calls.append(self.loop._run_once_counter)
self.assertEqual(calls, [1, 3, 5, 6])
def test_granularity(self):
granularity = self.loop._granularity
self.assertGreater(granularity, 0.0)
# Worst expected granularity: 1 ms on Linux (limited by poll/epoll
# resolution), 15.6 ms on Windows (limited by time.monotonic
# resolution)
self.assertLess(granularity, 0.050)
class SubprocessTestsMixin:

View file

@ -0,0 +1,196 @@
from asyncio import subprocess
import asyncio
import signal
import sys
import unittest
from test import support
if sys.platform != 'win32':
from asyncio import unix_events
# Program exiting quickly
PROGRAM_EXIT_FAST = [sys.executable, '-c', 'pass']
# Program blocking
PROGRAM_BLOCKED = [sys.executable, '-c', 'import time; time.sleep(3600)']
# Program sleeping during 1 second
PROGRAM_SLEEP_1SEC = [sys.executable, '-c', 'import time; time.sleep(1)']
# Program copying input to output
PROGRAM_CAT = [
sys.executable, '-c',
';'.join(('import sys',
'data = sys.stdin.buffer.read()',
'sys.stdout.buffer.write(data)'))]
class SubprocessMixin:
def test_stdin_stdout(self):
args = PROGRAM_CAT
@asyncio.coroutine
def run(data):
proc = yield from asyncio.create_subprocess_exec(
*args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
loop=self.loop)
# feed data
proc.stdin.write(data)
yield from proc.stdin.drain()
proc.stdin.close()
# get output and exitcode
data = yield from proc.stdout.read()
exitcode = yield from proc.wait()
return (exitcode, data)
task = run(b'some data')
task = asyncio.wait_for(task, 10.0, loop=self.loop)
exitcode, stdout = self.loop.run_until_complete(task)
self.assertEqual(exitcode, 0)
self.assertEqual(stdout, b'some data')
def test_communicate(self):
args = PROGRAM_CAT
@asyncio.coroutine
def run(data):
proc = yield from asyncio.create_subprocess_exec(
*args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
loop=self.loop)
stdout, stderr = yield from proc.communicate(data)
return proc.returncode, stdout
task = run(b'some data')
task = asyncio.wait_for(task, 10.0, loop=self.loop)
exitcode, stdout = self.loop.run_until_complete(task)
self.assertEqual(exitcode, 0)
self.assertEqual(stdout, b'some data')
def test_shell(self):
create = asyncio.create_subprocess_shell('exit 7',
loop=self.loop)
proc = self.loop.run_until_complete(create)
exitcode = self.loop.run_until_complete(proc.wait())
self.assertEqual(exitcode, 7)
def test_start_new_session(self):
# start the new process in a new session
create = asyncio.create_subprocess_shell('exit 8',
start_new_session=True,
loop=self.loop)
proc = self.loop.run_until_complete(create)
exitcode = self.loop.run_until_complete(proc.wait())
self.assertEqual(exitcode, 8)
def test_kill(self):
args = PROGRAM_BLOCKED
create = asyncio.create_subprocess_exec(*args, loop=self.loop)
proc = self.loop.run_until_complete(create)
proc.kill()
returncode = self.loop.run_until_complete(proc.wait())
if sys.platform == 'win32':
self.assertIsInstance(returncode, int)
# expect 1 but sometimes get 0
else:
self.assertEqual(-signal.SIGKILL, returncode)
def test_terminate(self):
args = PROGRAM_BLOCKED
create = asyncio.create_subprocess_exec(*args, loop=self.loop)
proc = self.loop.run_until_complete(create)
proc.terminate()
returncode = self.loop.run_until_complete(proc.wait())
if sys.platform == 'win32':
self.assertIsInstance(returncode, int)
# expect 1 but sometimes get 0
else:
self.assertEqual(-signal.SIGTERM, returncode)
@unittest.skipIf(sys.platform == 'win32', "Don't have SIGHUP")
def test_send_signal(self):
args = PROGRAM_BLOCKED
create = asyncio.create_subprocess_exec(*args, loop=self.loop)
proc = self.loop.run_until_complete(create)
proc.send_signal(signal.SIGHUP)
returncode = self.loop.run_until_complete(proc.wait())
self.assertEqual(-signal.SIGHUP, returncode)
def test_get_subprocess(self):
args = PROGRAM_EXIT_FAST
@asyncio.coroutine
def run():
proc = yield from asyncio.create_subprocess_exec(*args,
loop=self.loop)
yield from proc.wait()
popen = proc.get_subprocess()
popen.wait()
return (proc, popen)
proc, popen = self.loop.run_until_complete(run())
self.assertEqual(popen.returncode, proc.returncode)
self.assertEqual(popen.pid, proc.pid)
def test_broken_pipe(self):
large_data = b'x' * support.PIPE_MAX_SIZE
create = asyncio.create_subprocess_exec(
*PROGRAM_SLEEP_1SEC,
stdin=subprocess.PIPE,
loop=self.loop)
proc = self.loop.run_until_complete(create)
with self.assertRaises(BrokenPipeError):
self.loop.run_until_complete(proc.communicate(large_data))
self.loop.run_until_complete(proc.wait())
if sys.platform != 'win32':
# Unix
class SubprocessWatcherMixin(SubprocessMixin):
Watcher = None
def setUp(self):
policy = asyncio.get_event_loop_policy()
self.loop = policy.new_event_loop()
# ensure that the event loop is passed explicitly in the code
policy.set_event_loop(None)
watcher = self.Watcher()
watcher.attach_loop(self.loop)
policy.set_child_watcher(watcher)
def tearDown(self):
policy = asyncio.get_event_loop_policy()
policy.set_child_watcher(None)
self.loop.close()
policy.set_event_loop(None)
class SubprocessSafeWatcherTests(SubprocessWatcherMixin, unittest.TestCase):
Watcher = unix_events.SafeChildWatcher
class SubprocessFastWatcherTests(SubprocessWatcherMixin, unittest.TestCase):
Watcher = unix_events.FastChildWatcher
else:
# Windows
class SubprocessProactorTests(SubprocessMixin, unittest.TestCase):
def setUp(self):
policy = asyncio.get_event_loop_policy()
self.loop = asyncio.ProactorEventLoop()
# ensure that the event loop is passed explicitly in the code
policy.set_event_loop(None)
def tearDown(self):
policy = asyncio.get_event_loop_policy()
self.loop.close()
policy.set_event_loop(None)
if __name__ == '__main__':
unittest.main()

View file

@ -105,7 +105,7 @@ class ProactorTests(unittest.TestCase):
self.loop.run_until_complete(f)
elapsed = self.loop.time() - start
self.assertFalse(f.result())
self.assertTrue(0.18 < elapsed < 0.9, elapsed)
self.assertTrue(0.18 < elapsed < 0.5, elapsed)
_overlapped.SetEvent(event)