summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Kluyver <takowl@gmail.com>2015-10-03 11:57:35 +0100
committerThomas Kluyver <takowl@gmail.com>2015-10-03 11:57:35 +0100
commit26ff2f390a8d3179fef2bf1dba715fa0dcf886e8 (patch)
treeddacab825be4219d3edc71676336a42674baf19a
parent55d7aad50d9808be6eed989b5676b19b1a09ffe7 (diff)
parent84f3e245b27f96c559a984039099c74569f8ae64 (diff)
downloadpexpect-git-26ff2f390a8d3179fef2bf1dba715fa0dcf886e8.tar.gz
Merge pull request #146 from takluyver/popen
WIP: Add spawn class based on subprocess.Popen
-rw-r--r--pexpect/popen_spawn.py166
-rw-r--r--pexpect/pty_spawn.py2
-rw-r--r--tests/test_popen_spawn.py131
3 files changed, 298 insertions, 1 deletions
diff --git a/pexpect/popen_spawn.py b/pexpect/popen_spawn.py
new file mode 100644
index 0000000..ab51499
--- /dev/null
+++ b/pexpect/popen_spawn.py
@@ -0,0 +1,166 @@
+"""Spawn interface using subprocess.Popen
+"""
+import os
+import threading
+import subprocess
+import sys
+import time
+import signal
+import shlex
+
+try:
+ from queue import Queue, Empty # Python 3
+except ImportError:
+ from Queue import Queue, Empty # Python 2
+
+from .spawnbase import SpawnBase, PY3
+from .exceptions import EOF
+
+class PopenSpawn(SpawnBase):
+ if PY3:
+ crlf = '\n'.encode('ascii')
+ else:
+ crlf = '\n'
+
+ def __init__(self, cmd, timeout=30, maxread=2000, searchwindowsize=None,
+ logfile=None, cwd=None, env=None, encoding=None,
+ codec_errors='strict'):
+ super(PopenSpawn, self).__init__(timeout=timeout, maxread=maxread,
+ searchwindowsize=searchwindowsize, logfile=logfile,
+ encoding=encoding, codec_errors=codec_errors)
+
+ kwargs = dict(bufsize=0, stdin=subprocess.PIPE,
+ stderr=subprocess.STDOUT, stdout=subprocess.PIPE,
+ cwd=cwd, env=env)
+
+ if sys.platform == 'win32':
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ kwargs['startupinfo'] = startupinfo
+ kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
+
+ if not isinstance(cmd, (list, tuple)):
+ cmd = shlex.split(cmd)
+
+ self.proc = subprocess.Popen(cmd, **kwargs)
+ self.closed = False
+ self._buf = self.string_type()
+
+ self._read_queue = Queue()
+ self._read_thread = threading.Thread(target=self._read_incoming)
+ self._read_thread.setDaemon(True)
+ self._read_thread.start()
+
+ _read_reached_eof = False
+
+ def read_nonblocking(self, size, timeout):
+ buf = self._buf
+ if self._read_reached_eof:
+ # We have already finished reading. Use up any buffered data,
+ # then raise EOF
+ if buf:
+ self._buf = buf[size:]
+ return buf[:size]
+ else:
+ self.flag_eof = True
+ raise EOF('End Of File (EOF).')
+
+ if timeout == -1:
+ timeout = self.timeout
+ elif timeout is None:
+ timeout = 1e6
+
+ t0 = time.time()
+ while (time.time() - t0) < timeout and size and len(buf) < size:
+ try:
+ incoming = self._read_queue.get_nowait()
+ except Empty:
+ break
+ else:
+ if incoming is None:
+ self._read_reached_eof = True
+ break
+
+ buf += self._decoder.decode(incoming, final=False)
+
+ r, self._buf = buf[:size], buf[size:]
+
+ self._log(r, 'read')
+ return r
+
+ def _read_incoming(self):
+ """Run in a thread to move output from a pipe to a queue."""
+ fileno = self.proc.stdout.fileno()
+ while 1:
+ buf = b''
+ try:
+ buf = os.read(fileno, 1024)
+ except OSError as e:
+ self._log(e, 'read')
+
+ if not buf:
+ # This indicates we have reached EOF
+ self._read_queue.put(None)
+ return
+
+ self._read_queue.put(buf)
+
+ def write(self, s):
+ '''This is similar to send() except that there is no return value.
+ '''
+ self.send(s)
+
+ def writelines(self, sequence):
+ '''This calls write() for each element in the sequence.
+
+ The sequence can be any iterable object producing strings, typically a
+ list of strings. This does not add line separators. There is no return
+ value.
+ '''
+ for s in sequence:
+ self.send(s)
+
+ def send(self, s):
+ s = self._coerce_send_string(s)
+ self._log(s, 'send')
+
+ b = self._encoder.encode(s, final=False)
+ if PY3:
+ return self.proc.stdin.write(b)
+ else:
+ # On Python 2, .write() returns None, so we return the length of
+ # bytes written ourselves. This assumes they all got written.
+ self.proc.stdin.write(b)
+ return len(b)
+
+ def sendline(self, s=''):
+ '''Wraps send(), sending string ``s`` to child process, with os.linesep
+ automatically appended. Returns number of bytes written. '''
+
+ n = self.send(s)
+ return n + self.send(self.linesep)
+
+ def wait(self):
+ status = self.proc.wait()
+ if status >= 0:
+ self.exitstatus = status
+ self.signalstatus = None
+ else:
+ self.exitstatus = None
+ self.signalstatus = -status
+ self.terminated = True
+ return status
+
+ def kill(self, sig):
+ if sys.platform == 'win32':
+ if sig in [signal.SIGINT, signal.CTRL_C_EVENT]:
+ sig = signal.CTRL_C_EVENT
+ elif sig in [signal.SIGBREAK, signal.CTRL_BREAK_EVENT]:
+ sig = signal.CTRL_BREAK_EVENT
+ else:
+ sig = signal.SIGTERM
+
+ os.kill(self.proc.pid, sig)
+
+ def sendeof(self):
+ self.proc.stdin.close()
diff --git a/pexpect/pty_spawn.py b/pexpect/pty_spawn.py
index 7280a01..09c008b 100644
--- a/pexpect/pty_spawn.py
+++ b/pexpect/pty_spawn.py
@@ -407,7 +407,7 @@ class spawn(SpawnBase):
then this will raise a TIMEOUT exception.
The timeout refers only to the amount of time to read at least one
- character. This is not effected by the 'size' parameter, so if you call
+ character. This is not affected by the 'size' parameter, so if you call
read_nonblocking(size=100, timeout=30) and only one character is
available right away then one character will be returned immediately.
It will not wait for 30 seconds for another 99 characters to come in.
diff --git a/tests/test_popen_spawn.py b/tests/test_popen_spawn.py
new file mode 100644
index 0000000..98046ed
--- /dev/null
+++ b/tests/test_popen_spawn.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+'''
+PEXPECT LICENSE
+
+ This license is approved by the OSI and FSF as GPL-compatible.
+ http://opensource.org/licenses/isc-license.txt
+
+ Copyright (c) 2012, Noah Spurrier <noah@noah.org>
+ PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
+ PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
+ COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''
+import unittest
+import subprocess
+
+
+import pexpect
+from pexpect.popen_spawn import PopenSpawn
+from . import PexpectTestCase
+
+
+class ExpectTestCase (PexpectTestCase.PexpectTestCase):
+
+ def test_expect_basic(self):
+ p = PopenSpawn('cat', timeout=5)
+ p.sendline(b'Hello')
+ p.sendline(b'there')
+ p.sendline(b'Mr. Python')
+ p.expect(b'Hello')
+ p.expect(b'there')
+ p.expect(b'Mr. Python')
+ p.sendeof()
+ p.expect(pexpect.EOF)
+
+ def test_expect_exact_basic(self):
+ p = PopenSpawn('cat', timeout=5)
+ p.sendline(b'Hello')
+ p.sendline(b'there')
+ p.sendline(b'Mr. Python')
+ p.expect_exact(b'Hello')
+ p.expect_exact(b'there')
+ p.expect_exact(b'Mr. Python')
+ p.sendeof()
+ p.expect_exact(pexpect.EOF)
+
+ def test_expect(self):
+ the_old_way = subprocess.Popen(args=['ls', '-l', '/bin'],
+ stdout=subprocess.PIPE).communicate()[0].rstrip()
+ p = PopenSpawn('ls -l /bin')
+ the_new_way = b''
+ while 1:
+ i = p.expect([b'\n', pexpect.EOF])
+ the_new_way = the_new_way + p.before
+ if i == 1:
+ break
+ the_new_way += b'\n'
+ the_new_way = the_new_way.rstrip()
+ assert the_old_way == the_new_way, len(the_old_way) - len(the_new_way)
+
+ def test_expect_exact(self):
+ the_old_way = subprocess.Popen(args=['ls', '-l', '/bin'],
+ stdout=subprocess.PIPE).communicate()[0].rstrip()
+ p = PopenSpawn('ls -l /bin')
+ the_new_way = b''
+ while 1:
+ i = p.expect_exact([b'\n', pexpect.EOF])
+ the_new_way = the_new_way + p.before
+ if i == 1:
+ break
+ the_new_way += b'\n'
+ the_new_way = the_new_way.rstrip()
+
+ assert the_old_way == the_new_way, len(the_old_way) - len(the_new_way)
+ p = PopenSpawn('echo hello.?world')
+ i = p.expect_exact(b'.?')
+ self.assertEqual(p.before, b'hello')
+ self.assertEqual(p.after, b'.?')
+
+ def test_expect_eof(self):
+ the_old_way = subprocess.Popen(args=['ls', '-l', '/bin'],
+ stdout=subprocess.PIPE).communicate()[0].rstrip()
+ p = PopenSpawn('ls -l /bin')
+ # This basically tells it to read everything. Same as pexpect.run()
+ # function.
+ p.expect(pexpect.EOF)
+ the_new_way = p.before.rstrip()
+ assert the_old_way == the_new_way, len(the_old_way) - len(the_new_way)
+
+ def test_expect_timeout(self):
+ p = PopenSpawn('cat', timeout=5)
+ p.expect(pexpect.TIMEOUT) # This tells it to wait for timeout.
+ self.assertEqual(p.after, pexpect.TIMEOUT)
+
+ def test_unexpected_eof(self):
+ p = PopenSpawn('ls -l /bin')
+ try:
+ p.expect('_Z_XY_XZ') # Probably never see this in ls output.
+ except pexpect.EOF:
+ pass
+ else:
+ self.fail('Expected an EOF exception.')
+
+ def test_bad_arg(self):
+ p = PopenSpawn('cat')
+ with self.assertRaisesRegexp(TypeError, '.*must be one of'):
+ p.expect(1)
+ with self.assertRaisesRegexp(TypeError, '.*must be one of'):
+ p.expect([1, b'2'])
+ with self.assertRaisesRegexp(TypeError, '.*must be one of'):
+ p.expect_exact(1)
+ with self.assertRaisesRegexp(TypeError, '.*must be one of'):
+ p.expect_exact([1, b'2'])
+
+ def test_timeout_none(self):
+ p = PopenSpawn('echo abcdef', timeout=None)
+ p.expect('abc')
+ p.expect_exact('def')
+ p.expect(pexpect.EOF)
+
+if __name__ == '__main__':
+ unittest.main()
+
+suite = unittest.makeSuite(ExpectTestCase, 'test')