diff options
author | Filippo Carra <karrukola@users.noreply.github.com> | 2022-11-20 16:42:55 +0100 |
---|---|---|
committer | Filippo Carra <karrukola@users.noreply.github.com> | 2022-11-23 17:23:22 +0100 |
commit | 70d735584d9420caf3f7bc50847dc80a28a1d575 (patch) | |
tree | c3630d3c95bb6aafa96fea73729384065b80d782 | |
parent | 2e003effe885a54a320ba0a6eb91751714bc9ad3 (diff) | |
download | pexpect-70d735584d9420caf3f7bc50847dc80a28a1d575.tar.gz |
refactor(_async.py): introduce modern coroutine syntax
Python 3.5 introduced the await and async def keywords. This change imports the coroutines from two different places, depending on the running Python version.
-rw-r--r-- | pexpect/_async.py | 131 | ||||
-rw-r--r-- | pexpect/_async_pre_await.py | 111 | ||||
-rw-r--r-- | pexpect/_async_w_await.py | 118 | ||||
-rw-r--r-- | tests/test_async.py | 71 |
4 files changed, 296 insertions, 135 deletions
diff --git a/pexpect/_async.py b/pexpect/_async.py index dfbfeef..261720c 100644 --- a/pexpect/_async.py +++ b/pexpect/_async.py @@ -1,103 +1,28 @@ -import asyncio -import errno -import signal - -from pexpect import EOF - -@asyncio.coroutine -def expect_async(expecter, timeout=None): - # First process data that was previously read - if it maches, we don't need - # async stuff. - idx = expecter.existing_data() - if idx is not None: - return idx - if not expecter.spawn.async_pw_transport: - pw = PatternWaiter() - pw.set_expecter(expecter) - transport, pw = yield from asyncio.get_event_loop()\ - .connect_read_pipe(lambda: pw, expecter.spawn) - expecter.spawn.async_pw_transport = pw, transport - else: - pw, transport = expecter.spawn.async_pw_transport - pw.set_expecter(expecter) - transport.resume_reading() - try: - return (yield from asyncio.wait_for(pw.fut, timeout)) - except asyncio.TimeoutError as e: - transport.pause_reading() - return expecter.timeout(e) - -@asyncio.coroutine -def repl_run_command_async(repl, cmdlines, timeout=-1): - res = [] - repl.child.sendline(cmdlines[0]) - for line in cmdlines[1:]: - yield from repl._expect_prompt(timeout=timeout, async_=True) - res.append(repl.child.before) - repl.child.sendline(line) - - # Command was fully submitted, now wait for the next prompt - prompt_idx = yield from repl._expect_prompt(timeout=timeout, async_=True) - if prompt_idx == 1: - # We got the continuation prompt - command was incomplete - repl.child.kill(signal.SIGINT) - yield from repl._expect_prompt(timeout=1, async_=True) - raise ValueError("Continuation prompt found - input was incomplete:") - return u''.join(res + [repl.child.before]) - -class PatternWaiter(asyncio.Protocol): - transport = None - - def set_expecter(self, expecter): - self.expecter = expecter - self.fut = asyncio.Future() - - def found(self, result): - if not self.fut.done(): - self.fut.set_result(result) - self.transport.pause_reading() - - def error(self, exc): - if not self.fut.done(): - self.fut.set_exception(exc) - self.transport.pause_reading() - - def connection_made(self, transport): - self.transport = transport - - def data_received(self, data): - spawn = self.expecter.spawn - s = spawn._decoder.decode(data) - spawn._log(s, 'read') - - if self.fut.done(): - spawn._before.write(s) - spawn._buffer.write(s) - return - - try: - index = self.expecter.new_data(s) - if index is not None: - # Found a match - self.found(index) - except Exception as e: - self.expecter.errored() - self.error(e) - - def eof_received(self): - # N.B. If this gets called, async will close the pipe (the spawn object) - # for us - try: - self.expecter.spawn.flag_eof = True - index = self.expecter.eof() - except EOF as e: - self.error(e) - else: - self.found(index) - - def connection_lost(self, exc): - if isinstance(exc, OSError) and exc.errno == errno.EIO: - # We may get here without eof_received being called, e.g on Linux - self.eof_received() - elif exc is not None: - self.error(exc) +"""Facade that provides coroutines implementation pertinent to running Py version. + +Python 3.5 introduced the async def/await syntax keyword. +With later versions coroutines and methods to get the running asyncio loop are +being deprecated, not supported anymore. + +For Python versions later than 3.6, coroutines and objects that are defined via +``async def``/``await`` keywords are imported. + +Here the code is just imported, to provide the same interface to older code. +""" +# pylint: disable=unused-import +# flake8: noqa: F401 +from sys import version_info as py_version_info + +# this assumes async def/await are more stable +if py_version_info >= (3, 6): + from pexpect._async_w_await import ( + PatternWaiter, + expect_async, + repl_run_command_async, + ) +else: + from pexpect._async_pre_await import ( + PatternWaiter, + expect_async, + repl_run_command_async, + ) diff --git a/pexpect/_async_pre_await.py b/pexpect/_async_pre_await.py new file mode 100644 index 0000000..81ece1b --- /dev/null +++ b/pexpect/_async_pre_await.py @@ -0,0 +1,111 @@ +"""Implementation of coroutines without using ``async def``/``await`` keywords. + +``@asyncio.coroutine`` and ``yield from`` are used here instead. +""" +import asyncio +import errno +import signal + +from pexpect import EOF + + +@asyncio.coroutine +def expect_async(expecter, timeout=None): + # First process data that was previously read - if it maches, we don't need + # async stuff. + idx = expecter.existing_data() + if idx is not None: + return idx + if not expecter.spawn.async_pw_transport: + pw = PatternWaiter() + pw.set_expecter(expecter) + transport, pw = yield from asyncio.get_event_loop().connect_read_pipe( + lambda: pw, expecter.spawn + ) + expecter.spawn.async_pw_transport = pw, transport + else: + pw, transport = expecter.spawn.async_pw_transport + pw.set_expecter(expecter) + transport.resume_reading() + try: + return (yield from asyncio.wait_for(pw.fut, timeout)) + except asyncio.TimeoutError as e: + transport.pause_reading() + return expecter.timeout(e) + + +@asyncio.coroutine +def repl_run_command_async(repl, cmdlines, timeout=-1): + res = [] + repl.child.sendline(cmdlines[0]) + for line in cmdlines[1:]: + yield from repl._expect_prompt(timeout=timeout, async_=True) + res.append(repl.child.before) + repl.child.sendline(line) + + # Command was fully submitted, now wait for the next prompt + prompt_idx = yield from repl._expect_prompt(timeout=timeout, async_=True) + if prompt_idx == 1: + # We got the continuation prompt - command was incomplete + repl.child.kill(signal.SIGINT) + yield from repl._expect_prompt(timeout=1, async_=True) + raise ValueError("Continuation prompt found - input was incomplete:") + return "".join(res + [repl.child.before]) + + +class PatternWaiter(asyncio.Protocol): + transport = None + + def set_expecter(self, expecter): + self.expecter = expecter + self.fut = asyncio.Future() + + def found(self, result): + if not self.fut.done(): + self.fut.set_result(result) + self.transport.pause_reading() + + def error(self, exc): + if not self.fut.done(): + self.fut.set_exception(exc) + self.transport.pause_reading() + + def connection_made(self, transport): + self.transport = transport + + def data_received(self, data): + spawn = self.expecter.spawn + s = spawn._decoder.decode(data) + spawn._log(s, "read") + + if self.fut.done(): + spawn._before.write(s) + spawn._buffer.write(s) + return + + try: + index = self.expecter.new_data(s) + if index is not None: + # Found a match + self.found(index) + except Exception as e: + self.expecter.errored() + self.error(e) + + def eof_received(self): + # N.B. If this gets called, async will close the pipe (the spawn object) + # for us + try: + self.expecter.spawn.flag_eof = True + index = self.expecter.eof() + except EOF as e: + self.error(e) + else: + self.found(index) + + def connection_lost(self, exc): + if isinstance(exc, OSError) and exc.errno == errno.EIO: + # We may get here without eof_received being called, e.g on Linux + self.eof_received() + elif exc is not None: + self.error(exc) diff --git a/pexpect/_async_w_await.py b/pexpect/_async_w_await.py new file mode 100644 index 0000000..59cb1ef --- /dev/null +++ b/pexpect/_async_w_await.py @@ -0,0 +1,118 @@ +"""Implementation of coroutines using ``async def``/``await`` keywords. + +These keywords replaced ``@asyncio.coroutine`` and ``yield from`` from +Python 3.5 onwards. +""" +import asyncio +import errno +import signal +from sys import version_info as py_version_info + +from pexpect import EOF + +if py_version_info >= (3, 7): + # get_running_loop, new in 3.7, is preferred to get_event_loop + _loop_getter = asyncio.get_running_loop +else: + # Deprecation warning since 3.10 + _loop_getter = asyncio.get_event_loop + + +async def expect_async(expecter, timeout=None): + # First process data that was previously read - if it maches, we don't need + # async stuff. + idx = expecter.existing_data() + if idx is not None: + return idx + if not expecter.spawn.async_pw_transport: + pattern_waiter = PatternWaiter() + pattern_waiter.set_expecter(expecter) + transport, pattern_waiter = await _loop_getter().connect_read_pipe( + lambda: pattern_waiter, expecter.spawn + ) + expecter.spawn.async_pw_transport = pattern_waiter, transport + else: + pattern_waiter, transport = expecter.spawn.async_pw_transport + pattern_waiter.set_expecter(expecter) + transport.resume_reading() + try: + return await asyncio.wait_for(pattern_waiter.fut, timeout) + except asyncio.TimeoutError as exc: + transport.pause_reading() + return expecter.timeout(exc) + + +async def repl_run_command_async(repl, cmdlines, timeout=-1): + res = [] + repl.child.sendline(cmdlines[0]) + for line in cmdlines[1:]: + await repl._expect_prompt(timeout=timeout, async_=True) + res.append(repl.child.before) + repl.child.sendline(line) + + # Command was fully submitted, now wait for the next prompt + prompt_idx = await repl._expect_prompt(timeout=timeout, async_=True) + if prompt_idx == 1: + # We got the continuation prompt - command was incomplete + repl.child.kill(signal.SIGINT) + await repl._expect_prompt(timeout=1, async_=True) + raise ValueError("Continuation prompt found - input was incomplete:") + return "".join(res + [repl.child.before]) + + +class PatternWaiter(asyncio.Protocol): + transport = None + + def set_expecter(self, expecter): + self.expecter = expecter + self.fut = asyncio.Future() + + def found(self, result): + if not self.fut.done(): + self.fut.set_result(result) + self.transport.pause_reading() + + def error(self, exc): + if not self.fut.done(): + self.fut.set_exception(exc) + self.transport.pause_reading() + + def connection_made(self, transport): + self.transport = transport + + def data_received(self, data): + spawn = self.expecter.spawn + s = spawn._decoder.decode(data) + spawn._log(s, "read") + + if self.fut.done(): + spawn._before.write(s) + spawn._buffer.write(s) + return + + try: + index = self.expecter.new_data(s) + if index is not None: + # Found a match + self.found(index) + except Exception as exc: + self.expecter.errored() + self.error(exc) + + def eof_received(self): + # N.B. If this gets called, async will close the pipe (the spawn object) + # for us + try: + self.expecter.spawn.flag_eof = True + index = self.expecter.eof() + except EOF as exc: + self.error(exc) + else: + self.found(index) + + def connection_lost(self, exc): + if isinstance(exc, OSError) and exc.errno == errno.EIO: + # We may get here without eof_received being called, e.g on Linux + self.eof_received() + elif exc is not None: + self.error(exc) diff --git a/tests/test_async.py b/tests/test_async.py index 466d56f..dd9179a 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -4,83 +4,90 @@ except ImportError: asyncio = None import gc -import sys import unittest +from sys import version_info as py_version_info import pexpect from pexpect import replwrap + from .PexpectTestCase import PexpectTestCase + def run(coro): - return asyncio.get_event_loop().run_until_complete(coro) + if py_version_info >= (3, 7): + # get_running_loop, new in 3.7, is preferred to get_event_loop + return asyncio.get_running_loop().run_until_complete(coro) + else: + return asyncio.get_event_loop().run_until_complete(coro) + @unittest.skipIf(asyncio is None, "Requires asyncio") class AsyncTests(PexpectTestCase): def test_simple_expect(self): - p = pexpect.spawn('cat') - p.sendline('Hello asyncio') - coro = p.expect(['Hello', pexpect.EOF] , async_=True) + p = pexpect.spawn("cat") + p.sendline("Hello asyncio") + coro = p.expect(["Hello", pexpect.EOF], async_=True) assert run(coro) == 0 - print('Done') + print("Done") def test_timeout(self): - p = pexpect.spawn('cat') - coro = p.expect('foo', timeout=1, async_=True) + p = pexpect.spawn("cat") + coro = p.expect("foo", timeout=1, async_=True) with self.assertRaises(pexpect.TIMEOUT): run(coro) - p = pexpect.spawn('cat') - coro = p.expect(['foo', pexpect.TIMEOUT], timeout=1, async_=True) + p = pexpect.spawn("cat") + coro = p.expect(["foo", pexpect.TIMEOUT], timeout=1, async_=True) assert run(coro) == 1 def test_eof(self): - p = pexpect.spawn('cat') - p.sendline('Hi') + p = pexpect.spawn("cat") + p.sendline("Hi") coro = p.expect(pexpect.EOF, async_=True) p.sendeof() assert run(coro) == 0 - p = pexpect.spawn('cat') + p = pexpect.spawn("cat") p.sendeof() - coro = p.expect('Blah', async_=True) + coro = p.expect("Blah", async_=True) with self.assertRaises(pexpect.EOF): run(coro) def test_expect_exact(self): - p = pexpect.spawn('%s list100.py' % self.PYTHONBIN) - assert run(p.expect_exact(b'5', async_=True)) == 0 - assert run(p.expect_exact(['wpeok', b'11'], async_=True)) == 1 - assert run(p.expect_exact([b'foo', pexpect.EOF], async_=True)) == 1 + p = pexpect.spawn("%s list100.py" % self.PYTHONBIN) + assert run(p.expect_exact(b"5", async_=True)) == 0 + assert run(p.expect_exact(["wpeok", b"11"], async_=True)) == 1 + assert run(p.expect_exact([b"foo", pexpect.EOF], async_=True)) == 1 def test_async_utf8(self): - p = pexpect.spawn('%s list100.py' % self.PYTHONBIN, encoding='utf8') - assert run(p.expect_exact(u'5', async_=True)) == 0 - assert run(p.expect_exact([u'wpeok', u'11'], async_=True)) == 1 - assert run(p.expect_exact([u'foo', pexpect.EOF], async_=True)) == 1 + p = pexpect.spawn("%s list100.py" % self.PYTHONBIN, encoding="utf8") + assert run(p.expect_exact("5", async_=True)) == 0 + assert run(p.expect_exact(["wpeok", "11"], async_=True)) == 1 + assert run(p.expect_exact(["foo", pexpect.EOF], async_=True)) == 1 def test_async_and_gc(self): - p = pexpect.spawn('%s sleep_for.py 1' % self.PYTHONBIN, encoding='utf8') - assert run(p.expect_exact(u'READY', async_=True)) == 0 + p = pexpect.spawn("%s sleep_for.py 1" % self.PYTHONBIN, encoding="utf8") + assert run(p.expect_exact("READY", async_=True)) == 0 gc.collect() - assert run(p.expect_exact(u'END', async_=True)) == 0 + assert run(p.expect_exact("END", async_=True)) == 0 def test_async_and_sync(self): - p = pexpect.spawn('echo 1234', encoding='utf8', maxread=1) - assert run(p.expect_exact(u'1', async_=True)) == 0 - assert p.expect_exact(u'2') == 0 - assert run(p.expect_exact(u'3', async_=True)) == 0 + p = pexpect.spawn("echo 1234", encoding="utf8", maxread=1) + assert run(p.expect_exact("1", async_=True)) == 0 + assert p.expect_exact("2") == 0 + assert run(p.expect_exact("3", async_=True)) == 0 def test_async_replwrap(self): bash = replwrap.bash() coro = bash.run_command("time", async_=True) res = run(coro) - assert 'real' in res, res + assert "real" in res, res def test_async_replwrap_multiline(self): bash = replwrap.bash() coro = bash.run_command("echo '1 2\n3 4'", async_=True) res = run(coro) - self.assertEqual(res.strip().splitlines(), ['1 2', '3 4']) + self.assertEqual(res.strip().splitlines(), ["1 2", "3 4"]) # Should raise ValueError if input is incomplete coro = bash.run_command("echo '5 6", async_=True) @@ -94,4 +101,4 @@ class AsyncTests(PexpectTestCase): # Check that the REPL was reset (SIGINT) after the incomplete input coro = bash.run_command("echo '1 2\n3 4'", async_=True) res = run(coro) - self.assertEqual(res.strip().splitlines(), ['1 2', '3 4']) + self.assertEqual(res.strip().splitlines(), ["1 2", "3 4"]) |