summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Quast <contact@jeffquast.com>2014-11-23 04:23:31 -0800
committerJeff Quast <contact@jeffquast.com>2014-11-23 04:23:31 -0800
commit01ea0ff440facd47894c076747488c28291a47cc (patch)
tree9a6a1f02e34f364ecfd9fed378a41ac6df8eb4eb
parenta88e60f8403b1d25db1cca857009f3612a8f1e19 (diff)
parent00c8aaed9605c446844bbe379582753492a3627b (diff)
downloadpexpect-01ea0ff440facd47894c076747488c28291a47cc.tar.gz
Merge remote-tracking branch 'origin/master' into issue-104-cannot-exec-setuids
Conflicts: doc/history.rst
-rw-r--r--.travis.yml2
-rw-r--r--LICENSE2
-rw-r--r--README.rst1
-rw-r--r--doc/history.rst12
-rwxr-xr-xexamples/cgishell.cgi4
-rw-r--r--pexpect/ANSI.py26
-rw-r--r--pexpect/__init__.py182
-rw-r--r--pexpect/async.py68
-rw-r--r--pexpect/bashrc.sh5
-rw-r--r--pexpect/expect.py105
-rw-r--r--pexpect/pxssh.py4
-rw-r--r--pexpect/replwrap.py7
-rw-r--r--pexpect/screen.py102
-rw-r--r--setup.cfg2
-rwxr-xr-xtests/test_ansi.py59
-rw-r--r--tests/test_async.py51
-rwxr-xr-xtests/test_constructor.py10
-rwxr-xr-xtests/test_expect.py37
-rw-r--r--tests/test_repr.py26
-rwxr-xr-xtests/test_run.py38
-rwxr-xr-xtests/test_screen.py124
21 files changed, 703 insertions, 164 deletions
diff --git a/.travis.yml b/.travis.yml
index 4b14d16..3a2f331 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,7 @@
language: python
python:
- - 2.6
- 2.7
- - 3.2
- 3.3
- 3.4
- pypy
diff --git a/LICENSE b/LICENSE
index 18ff9db..9e10acb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -3,7 +3,9 @@ PEXPECT LICENSE
This license is approved by the OSI and FSF as GPL-compatible.
http://opensource.org/licenses/isc-license.txt
+ Copyright (c) 2013-2014, Pexpect development team
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.
diff --git a/README.rst b/README.rst
index e0bbd84..dde7ade 100644
--- a/README.rst
+++ b/README.rst
@@ -36,6 +36,7 @@ PEXPECT LICENSE
http://opensource.org/licenses/isc-license.txt
+ Copyright (c) 2013-2014, Pexpect development team
Copyright (c) 2012, Noah Spurrier <noah@noah.org>
PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
diff --git a/doc/history.rst b/doc/history.rst
index 19c2973..c9d5640 100644
--- a/doc/history.rst
+++ b/doc/history.rst
@@ -4,12 +4,22 @@ History
Releases
--------
+Version 4.0
+```````````
+
+* Integration with :mod:`asyncio`: passing ``async=True`` to :meth:`~.expect`,
+ :meth:`~.expect_exact` or :meth:`~.expect_list` will make them return a
+ coroutine. You can get the result using ``yield from``, or wrap it in an
+ :class:`asyncio.Task`. This allows the event loop to do other things while
+ waiting for output that matches a pattern.
+
Version 3.4
```````````
* Fix regression that prevented executable, but unreadable files from
being found when not specified by absolute path -- such as
/usr/bin/sudo (:ghissue:`104`).
-
+* Fixed regression when executing pexpect with some prior releases of
+ the multiprocessing module where stdin has been closed (:ghissue:`86`).
Version 3.3
```````````
diff --git a/examples/cgishell.cgi b/examples/cgishell.cgi
index b807a8b..23bef5f 100755
--- a/examples/cgishell.cgi
+++ b/examples/cgishell.cgi
@@ -176,11 +176,11 @@ def daemonize (stdin=None, stdout=None, stderr=None, daemon_pid_filename=None):
if stderr is None: stderr = DEVNULL
try:
- pid = os.fork()
+ pid = os.fork() # fork first child
except OSError as e:
raise Exception("%s [%d]" % (e.strerror, e.errno))
- if pid != 0: # The first child.
+ if pid != 0:
os.waitpid(pid,0)
if daemon_pid_filename is not None:
daemon_pid = int(file(daemon_pid_filename,'r').read())
diff --git a/pexpect/ANSI.py b/pexpect/ANSI.py
index 3460114..1cd2e90 100644
--- a/pexpect/ANSI.py
+++ b/pexpect/ANSI.py
@@ -186,18 +186,18 @@ class term (screen.screen):
provides a common base class for other terminals
such as an ANSI terminal. '''
- def __init__ (self, r=24, c=80):
+ def __init__ (self, r=24, c=80, *args, **kwargs):
- screen.screen.__init__(self, r,c)
+ screen.screen.__init__(self, r,c,*args,**kwargs)
class ANSI (term):
'''This class implements an ANSI (VT100) terminal.
It is a stream filter that recognizes ANSI terminal
escape sequences and maintains the state of a screen object. '''
- def __init__ (self, r=24,c=80):
+ def __init__ (self, r=24,c=80,*args,**kwargs):
- term.__init__(self,r,c)
+ term.__init__(self,r,c,*args,**kwargs)
#self.screen = screen (24,80)
self.state = FSM.FSM ('INIT',[self])
@@ -279,7 +279,9 @@ class ANSI (term):
self.state.add_transition (';', 'NUMBER_X', None, 'SEMICOLON_X')
def process (self, c):
- """Process a single byte. Called by :meth:`write`."""
+ """Process a single character. Called by :meth:`write`."""
+ if isinstance(c, bytes):
+ c = self._decode(c)
self.state.process(c)
def process_list (self, l):
@@ -290,6 +292,8 @@ class ANSI (term):
"""Process text, writing it to the virtual screen while handling
ANSI escape codes.
"""
+ if isinstance(s, bytes):
+ s = self._decode(s)
for c in s:
self.process(c)
@@ -301,23 +305,21 @@ class ANSI (term):
position is moved forward with wrap-around, but no scrolling is done if
the cursor hits the lower-right corner of the screen. '''
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
#\r and \n both produce a call to cr() and lf(), respectively.
ch = ch[0]
- if ch == '\r':
+ if ch == u'\r':
self.cr()
return
- if ch == '\n':
+ if ch == u'\n':
self.crlf()
return
if ch == chr(screen.BS):
self.cursor_back()
return
- if ch not in string.printable:
- fout = open ('log', 'a')
- fout.write ('Nonprint: ' + str(ord(ch)) + '\n')
- fout.close()
- return
self.put_abs(self.cur_r, self.cur_c, ch)
old_r = self.cur_r
old_c = self.cur_c
diff --git a/pexpect/__init__.py b/pexpect/__init__.py
index 57d8d91..5583be7 100644
--- a/pexpect/__init__.py
+++ b/pexpect/__init__.py
@@ -88,6 +88,8 @@ except ImportError: # pragma: no cover
A critical module was not found. Probably this operating system does not
support it. Pexpect is intended for UNIX-like operating systems.''')
+from .expect import Expecter
+
__version__ = '3.3'
__revision__ = ''
__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'spawnu', 'run', 'runu',
@@ -113,7 +115,8 @@ class ExceptionPexpect(Exception):
is not included. '''
tblist = traceback.extract_tb(sys.exc_info()[2])
- tblist = [item for item in tblist if 'pexpect/__init__' not in item[0]]
+ tblist = [item for item in tblist if ('pexpect/__init__' not in item[0])
+ and ('pexpect/expect' not in item[0])]
tblist = traceback.format_list(tblist)
return ''.join(tblist)
@@ -195,18 +198,29 @@ def run(command, timeout=-1, withexitstatus=False, events=None,
run("mencoder dvd://1 -o video.avi -oac copy -ovc copy",
events={TIMEOUT:print_ticks}, timeout=5)
- The 'events' argument should be a dictionary of patterns and responses.
- Whenever one of the patterns is seen in the command out run() will send the
- associated response string. Note that you should put newlines in your
- string if Enter is necessary. The responses may also contain callback
- functions. Any callback is function that takes a dictionary as an argument.
+ The 'events' argument should be either a dictionary or a tuple list that
+ contains patterns and responses. Whenever one of the patterns is seen
+ in the command output, run() will send the associated response string.
+ So, run() in the above example can be also written as:
+
+ run("mencoder dvd://1 -o video.avi -oac copy -ovc copy",
+ events=[(TIMEOUT,print_ticks)], timeout=5)
+
+ Use a tuple list for events if the command output requires a delicate
+ control over what pattern should be matched, since the tuple list is passed
+ to pexpect() as its pattern list, with the order of patterns preserved.
+
+ Note that you should put newlines in your string if Enter is necessary.
+
+ Like the example above, the responses may also contain callback functions.
+ Any callback is a function that takes a dictionary as an argument.
The dictionary contains all the locals from the run() function, so you can
access the child spawn object or any other variable defined in run()
(event_count, child, and extra_args are the most useful). A callback may
- return True to stop the current run process otherwise run() continues until
- the next event. A callback may also return a string which will be sent to
- the child. 'extra_args' is not used by directly run(). It provides a way to
- pass data to a callback function through run() through the locals
+ return True to stop the current run process. Otherwise run() continues
+ until the next event. A callback may also return a string which will be
+ sent to the child. 'extra_args' is not used by directly run(). It provides
+ a way to pass data to a callback function through run() through the locals
dictionary passed to a callback.
'''
return _run(command, timeout=timeout, withexitstatus=withexitstatus,
@@ -232,7 +246,10 @@ def _run(command, timeout, withexitstatus, events, extra_args, logfile, cwd,
else:
child = _spawn(command, timeout=timeout, maxread=2000, logfile=logfile,
cwd=cwd, env=env, **kwargs)
- if events is not None:
+ if isinstance(events, list):
+ patterns= [x for x,y in events]
+ responses = [y for x,y in events]
+ elif isinstance(events, dict):
patterns = list(events.keys())
responses = list(events.values())
else:
@@ -498,12 +515,17 @@ class spawn(object):
# inherit EOF and INTR definitions from controlling process.
try:
from termios import VEOF, VINTR
- fd = sys.__stdin__.fileno()
+ try:
+ fd = sys.__stdin__.fileno()
+ except ValueError:
+ # ValueError: I/O operation on closed file
+ fd = sys.__stdout__.fileno()
self._INTR = ord(termios.tcgetattr(fd)[6][VINTR])
self._EOF = ord(termios.tcgetattr(fd)[6][VEOF])
- except (ImportError, OSError, IOError, termios.error):
+ except (ImportError, OSError, IOError, ValueError, termios.error):
# unless the controlling process is also not a terminal,
- # such as cron(1). Fall-back to using CEOF and CINTR.
+ # such as cron(1), or when stdin and stdout are both closed.
+ # Fall-back to using CEOF and CINTR. There
try:
from termios import CEOF, CINTR
(self._INTR, self._EOF) = (CINTR, CEOF)
@@ -561,8 +583,10 @@ class spawn(object):
s.append('command: ' + str(self.command))
s.append('args: %r' % (self.args,))
s.append('searcher: %r' % (self.searcher,))
- s.append('buffer (last 100 chars): %r' % (self.buffer)[-100:],)
- s.append('before (last 100 chars): %r' % (self.before)[-100:],)
+ s.append('buffer (last 100 chars): %r' % (
+ self.buffer[-100:] if self.buffer else self.buffer,))
+ s.append('before (last 100 chars): %r' % (
+ self.before[-100:] if self.before else self.before,))
s.append('after: %r' % (self.after,))
s.append('match: %r' % (self.match,))
s.append('match_index: ' + str(self.match_index))
@@ -1349,7 +1373,7 @@ class spawn(object):
cpl = self.compile_pattern_list(my_pattern)
while some_condition:
...
- i = self.expect_list(clp, timeout)
+ i = self.expect_list(cpl, timeout)
...
'''
@@ -1377,7 +1401,7 @@ class spawn(object):
self._pattern_type_err(p)
return compiled_pattern_list
- def expect(self, pattern, timeout=-1, searchwindowsize=-1):
+ def expect(self, pattern, timeout=-1, searchwindowsize=-1, async=False):
'''This seeks through the stream until a pattern is matched. The
pattern is overloaded and may take several types. The pattern can be a
@@ -1452,14 +1476,25 @@ class spawn(object):
print p.before
If you are trying to optimize for speed then see expect_list().
+
+ On Python 3.4, or Python 3.3 with asyncio installed, passing
+ ``async=True`` will make this return an :mod:`asyncio` coroutine,
+ which you can yield from to get the same result that this method would
+ normally give directly. So, inside a coroutine, you can replace this code::
+
+ index = p.expect(patterns)
+
+ With this non-blocking form::
+
+ index = yield from p.expect(patterns, async=True)
'''
compiled_pattern_list = self.compile_pattern_list(pattern)
return self.expect_list(compiled_pattern_list,
- timeout, searchwindowsize)
-
- def expect_list(self, pattern_list, timeout=-1, searchwindowsize=-1):
+ timeout, searchwindowsize, async)
+ def expect_list(self, pattern_list, timeout=-1, searchwindowsize=-1,
+ async=False):
'''This takes a list of compiled regular expressions and returns the
index into the pattern_list that matched the child output. The list may
also contain EOF or TIMEOUT(which are not compiled regular
@@ -1468,12 +1503,23 @@ class spawn(object):
may help if you are trying to optimize for speed, otherwise just use
the expect() method. This is called by expect(). If timeout==-1 then
the self.timeout value is used. If searchwindowsize==-1 then the
- self.searchwindowsize value is used. '''
+ self.searchwindowsize value is used.
- return self.expect_loop(searcher_re(pattern_list),
- timeout, searchwindowsize)
+ Like :meth:`expect`, passing ``async=True`` will make this return an
+ asyncio coroutine.
+ '''
+ if timeout == -1:
+ timeout = self.timeout
- def expect_exact(self, pattern_list, timeout=-1, searchwindowsize=-1):
+ exp = Expecter(self, searcher_re(pattern_list), searchwindowsize)
+ if async:
+ from .async import expect_async
+ return expect_async(exp, timeout)
+ else:
+ return exp.expect_loop(timeout)
+
+ def expect_exact(self, pattern_list, timeout=-1, searchwindowsize=-1,
+ async=False):
'''This is similar to expect(), but uses plain string matching instead
of compiled regular expressions in 'pattern_list'. The 'pattern_list'
@@ -1485,7 +1531,13 @@ class spawn(object):
search to just the end of the input buffer.
This method is also useful when you don't want to have to worry about
- escaping regular expression characters that you want to match.'''
+ escaping regular expression characters that you want to match.
+
+ Like :meth:`expect`, passing ``async=True`` will make this return an
+ asyncio coroutine.
+ '''
+ if timeout == -1:
+ timeout = self.timeout
if (isinstance(pattern_list, self.allowed_string_types) or
pattern_list in (TIMEOUT, EOF)):
@@ -1503,83 +1555,23 @@ class spawn(object):
except TypeError:
self._pattern_type_err(pattern_list)
pattern_list = [prepare_pattern(p) for p in pattern_list]
- return self.expect_loop(searcher_string(pattern_list),
- timeout, searchwindowsize)
- def expect_loop(self, searcher, timeout=-1, searchwindowsize=-1):
+ exp = Expecter(self, searcher_string(pattern_list), searchwindowsize)
+ if async:
+ from .async import expect_async
+ return expect_async(exp, timeout)
+ else:
+ return exp.expect_loop(timeout)
+ def expect_loop(self, searcher, timeout=-1, searchwindowsize=-1):
'''This is the common loop used inside expect. The 'searcher' should be
an instance of searcher_re or searcher_string, which describes how and
what to search for in the input.
See expect() for other arguments, return value and exceptions. '''
- self.searcher = searcher
-
- if timeout == -1:
- timeout = self.timeout
- if timeout is not None:
- end_time = time.time() + timeout
- if searchwindowsize == -1:
- searchwindowsize = self.searchwindowsize
-
- try:
- incoming = self.buffer
- freshlen = len(incoming)
- while True:
- # Keep reading until exception or return.
- index = searcher.search(incoming, freshlen, searchwindowsize)
- if index >= 0:
- self.buffer = incoming[searcher.end:]
- self.before = incoming[: searcher.start]
- self.after = incoming[searcher.start: searcher.end]
- self.match = searcher.match
- self.match_index = index
- return self.match_index
- # No match at this point
- if (timeout is not None) and (timeout < 0):
- raise TIMEOUT('Timeout exceeded in expect_any().')
- # Still have time left, so read more data
- c = self.read_nonblocking(self.maxread, timeout)
- freshlen = len(c)
- time.sleep(0.0001)
- incoming = incoming + c
- if timeout is not None:
- timeout = end_time - time.time()
- except EOF:
- err = sys.exc_info()[1]
- self.buffer = self.string_type()
- self.before = incoming
- self.after = EOF
- index = searcher.eof_index
- if index >= 0:
- self.match = EOF
- self.match_index = index
- return self.match_index
- else:
- self.match = None
- self.match_index = None
- raise EOF(str(err) + '\n' + str(self))
- except TIMEOUT:
- err = sys.exc_info()[1]
- self.buffer = incoming
- self.before = incoming
- self.after = TIMEOUT
- index = searcher.timeout_index
- if index >= 0:
- self.match = TIMEOUT
- self.match_index = index
- return self.match_index
- else:
- self.match = None
- self.match_index = None
- raise TIMEOUT(str(err) + '\n' + str(self))
- except:
- self.before = incoming
- self.after = None
- self.match = None
- self.match_index = None
- raise
+ exp = Expecter(self, searcher, searchwindowsize)
+ return exp.expect_loop(timeout)
def getwinsize(self):
diff --git a/pexpect/async.py b/pexpect/async.py
new file mode 100644
index 0000000..8ec9c3c
--- /dev/null
+++ b/pexpect/async.py
@@ -0,0 +1,68 @@
+import asyncio
+import errno
+
+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.new_data(expecter.spawn.buffer)
+ expecter.spawn.buffer = expecter.spawn.string_type()
+ if idx:
+ return idx
+
+ transport, pw = yield from asyncio.get_event_loop()\
+ .connect_read_pipe(lambda: PatternWaiter(expecter), expecter.spawn)
+
+ try:
+ return (yield from asyncio.wait_for(pw.fut, timeout))
+ except asyncio.TimeoutError as e:
+ transport.pause_reading()
+ return expecter.timeout(e)
+
+class PatternWaiter(asyncio.Protocol):
+ def __init__(self, expecter):
+ self.expecter = expecter
+ self.fut = asyncio.Future()
+
+ def found(self, result):
+ if not self.fut.done():
+ self.fut.set_result(result)
+
+ def error(self, exc):
+ if not self.fut.done():
+ self.fut.set_exception(exc)
+
+ def data_received(self, data):
+ spawn = self.expecter.spawn
+ s = spawn._coerce_read_string(data)
+ spawn._log(s, 'read')
+
+ if self.fut.done():
+ spawn.buffer += data
+ return
+
+ try:
+ index = self.expecter.new_data(data)
+ 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):
+ try:
+ 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) \ No newline at end of file
diff --git a/pexpect/bashrc.sh b/pexpect/bashrc.sh
new file mode 100644
index 0000000..99a3ac2
--- /dev/null
+++ b/pexpect/bashrc.sh
@@ -0,0 +1,5 @@
+source /etc/bash.bashrc
+source ~/.bashrc
+
+# Reset PS1 so pexpect can find it
+PS1="$"
diff --git a/pexpect/expect.py b/pexpect/expect.py
new file mode 100644
index 0000000..b8da406
--- /dev/null
+++ b/pexpect/expect.py
@@ -0,0 +1,105 @@
+import time
+
+class Expecter(object):
+ def __init__(self, spawn, searcher, searchwindowsize=-1):
+ self.spawn = spawn
+ self.searcher = searcher
+ if searchwindowsize == -1:
+ searchwindowsize = spawn.searchwindowsize
+ self.searchwindowsize = searchwindowsize
+
+ def new_data(self, data):
+ spawn = self.spawn
+ searcher = self.searcher
+
+ incoming = spawn.buffer + data
+ freshlen = len(data)
+ index = searcher.search(incoming, freshlen, self.searchwindowsize)
+ if index >= 0:
+ spawn.buffer = incoming[searcher.end:]
+ spawn.before = incoming[: searcher.start]
+ spawn.after = incoming[searcher.start: searcher.end]
+ spawn.match = searcher.match
+ spawn.match_index = index
+ # Found a match
+ return index
+
+ spawn.buffer = incoming
+
+ def eof(self, err=None):
+ spawn = self.spawn
+ from . import EOF
+
+ spawn.before = spawn.buffer
+ spawn.buffer = spawn.string_type()
+ spawn.after = EOF
+ index = self.searcher.eof_index
+ if index >= 0:
+ spawn.match = EOF
+ spawn.match_index = index
+ return index
+ else:
+ spawn.match = None
+ spawn.match_index = None
+ msg = str(spawn)
+ if err is not None:
+ msg = str(err) + '\n' + msg
+ raise EOF(msg)
+
+ def timeout(self, err=None):
+ spawn = self.spawn
+ from . import TIMEOUT
+
+ spawn.before = spawn.buffer
+ spawn.after = TIMEOUT
+ index = self.searcher.timeout_index
+ if index >= 0:
+ spawn.match = TIMEOUT
+ spawn.match_index = index
+ return index
+ else:
+ spawn.match = None
+ spawn.match_index = None
+ msg = str(spawn)
+ if err is not None:
+ msg = str(err) + '\n' + msg
+ raise TIMEOUT(msg)
+
+ def errored(self):
+ spawn = self.spawn
+ spawn.before = spawn.buffer
+ spawn.after = None
+ spawn.match = None
+ spawn.match_index = None
+
+ def expect_loop(self, timeout=-1):
+ """Blocking expect"""
+ spawn = self.spawn
+ from . import EOF, TIMEOUT
+
+ if timeout is not None:
+ end_time = time.time() + timeout
+
+ try:
+ incoming = spawn.buffer
+ spawn.buffer = spawn.string_type() # Treat buffer as new data
+ while True:
+ idx = self.new_data(incoming)
+ # Keep reading until exception or return.
+ if idx is not None:
+ return idx
+ # No match at this point
+ if (timeout is not None) and (timeout < 0):
+ return self.timeout()
+ # Still have time left, so read more data
+ incoming = spawn.read_nonblocking(spawn.maxread, timeout)
+ time.sleep(0.0001)
+ if timeout is not None:
+ timeout = end_time - time.time()
+ except EOF as e:
+ return self.eof(e)
+ except TIMEOUT as e:
+ return self.timeout(e)
+ except:
+ self.errored()
+ raise \ No newline at end of file
diff --git a/pexpect/pxssh.py b/pexpect/pxssh.py
index ec8c525..bd34f97 100644
--- a/pexpect/pxssh.py
+++ b/pexpect/pxssh.py
@@ -86,9 +86,9 @@ class pxssh (spawn):
'''
def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None,
- logfile=None, cwd=None, env=None):
+ logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True):
- spawn.__init__(self, None, timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, logfile=logfile, cwd=cwd, env=env)
+ spawn.__init__(self, None, timeout=timeout, maxread=maxread, searchwindowsize=searchwindowsize, logfile=logfile, cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo)
self.name = '<pxssh>'
diff --git a/pexpect/replwrap.py b/pexpect/replwrap.py
index 2e50286..0c879ff 100644
--- a/pexpect/replwrap.py
+++ b/pexpect/replwrap.py
@@ -1,5 +1,6 @@
"""Generic wrapper for read-eval-print-loops, a.k.a. interactive shells
"""
+import os.path
import signal
import sys
import re
@@ -104,7 +105,9 @@ def python(command="python"):
"""Start a Python shell and return a :class:`REPLWrapper` object."""
return REPLWrapper(command, u(">>> "), u("import sys; sys.ps1={0!r}; sys.ps2={1!r}"))
-def bash(command="bash", orig_prompt=re.compile('[$#]')):
+def bash(command="bash"):
"""Start a bash shell and return a :class:`REPLWrapper` object."""
- return REPLWrapper(command, orig_prompt, u("PS1='{0}' PS2='{1}' PROMPT_COMMAND=''"),
+ bashrc = os.path.join(os.path.dirname(__file__), 'bashrc.sh')
+ child = pexpect.spawnu(command, ['--rcfile', bashrc])
+ return REPLWrapper(child, u'\$', u("PS1='{0}' PS2='{1}' PROMPT_COMMAND=''"),
extra_init_cmd="export PAGER=cat")
diff --git a/pexpect/screen.py b/pexpect/screen.py
index 61d3b97..efe9ee5 100644
--- a/pexpect/screen.py
+++ b/pexpect/screen.py
@@ -23,7 +23,9 @@ PEXPECT LICENSE
'''
+import codecs
import copy
+import sys
NUL = 0 # Fill character; ignored on input.
ENQ = 5 # Transmit answerback message.
@@ -42,7 +44,11 @@ CAN = 24 # Cancel escape sequence.
SUB = 26 # Same as CAN.
ESC = 27 # Introduce a control sequence.
DEL = 127 # Fill character; ignored on input.
-SPACE = chr(32) # Space or blank character.
+SPACE = u' ' # Space or blank character.
+
+PY3 = (sys.version_info[0] >= 3)
+if PY3:
+ unicode = str
def constrain (n, min, max):
@@ -59,47 +65,89 @@ class screen:
rectangluar array. This maintains a virtual cursor position and handles
scrolling as characters are added. This supports most of the methods needed
by an ANSI text screen. Row and column indexes are 1-based (not zero-based,
- like arrays). '''
-
- def __init__ (self, r=24,c=80):
+ like arrays).
+
+ Characters are represented internally using unicode. Methods that accept
+ input characters, when passed 'bytes' (which in Python 2 is equivalent to
+ 'str'), convert them from the encoding specified in the 'encoding'
+ parameter to the constructor. Methods that return screen contents return
+ unicode strings, with the exception of __str__() under Python 2. Passing
+ ``encoding=None`` limits the API to only accept unicode input, so passing
+ bytes in will raise :exc:`TypeError`.
+ '''
+ def __init__(self, r=24, c=80, encoding='latin-1', encoding_errors='replace'):
'''This initializes a blank screen of the given dimensions.'''
self.rows = r
self.cols = c
+ self.encoding = encoding
+ self.encoding_errors = encoding_errors
+ if encoding is not None:
+ self.decoder = codecs.getincrementaldecoder(encoding)(encoding_errors)
+ else:
+ self.decoder = None
self.cur_r = 1
self.cur_c = 1
self.cur_saved_r = 1
self.cur_saved_c = 1
self.scroll_row_start = 1
self.scroll_row_end = self.rows
- self.w = [ [SPACE] * self.cols for c in range(self.rows)]
-
- def __str__ (self):
- '''This returns a printable representation of the screen. The end of
- each screen line is terminated by a newline. '''
-
- return '\n'.join ([ ''.join(c) for c in self.w ])
+ self.w = [ [SPACE] * self.cols for _ in range(self.rows)]
+
+ def _decode(self, s):
+ '''This converts from the external coding system (as passed to
+ the constructor) to the internal one (unicode). '''
+ if self.decoder is not None:
+ return self.decoder.decode(s)
+ else:
+ raise TypeError("This screen was constructed with encoding=None, "
+ "so it does not handle bytes.")
+
+ def _unicode(self):
+ '''This returns a printable representation of the screen as a unicode
+ string (which, under Python 3.x, is the same as 'str'). The end of each
+ screen line is terminated by a newline.'''
+
+ return u'\n'.join ([ u''.join(c) for c in self.w ])
+
+ if PY3:
+ __str__ = _unicode
+ else:
+ __unicode__ = _unicode
+
+ def __str__(self):
+ '''This returns a printable representation of the screen. The end of
+ each screen line is terminated by a newline. '''
+ encoding = self.encoding or 'ascii'
+ return self._unicode().encode(encoding, 'replace')
def dump (self):
- '''This returns a copy of the screen as a string. This is similar to
- __str__ except that lines are not terminated with line feeds. '''
+ '''This returns a copy of the screen as a unicode string. This is similar to
+ __str__/__unicode__ except that lines are not terminated with line
+ feeds.'''
- return ''.join ([ ''.join(c) for c in self.w ])
+ return u''.join ([ u''.join(c) for c in self.w ])
def pretty (self):
- '''This returns a copy of the screen as a string with an ASCII text box
- around the screen border. This is similar to __str__ except that it
- adds a box. '''
+ '''This returns a copy of the screen as a unicode string with an ASCII
+ text box around the screen border. This is similar to
+ __str__/__unicode__ except that it adds a box.'''
- top_bot = '+' + '-'*self.cols + '+\n'
- return top_bot + '\n'.join(['|'+line+'|' for line in str(self).split('\n')]) + '\n' + top_bot
+ top_bot = u'+' + u'-'*self.cols + u'+\n'
+ return top_bot + u'\n'.join([u'|'+line+u'|' for line in unicode(self).split(u'\n')]) + u'\n' + top_bot
def fill (self, ch=SPACE):
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
self.fill_region (1,1,self.rows,self.cols, ch)
def fill_region (self, rs,cs, re,ce, ch=SPACE):
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
rs = constrain (rs, 1, self.rows)
re = constrain (re, 1, self.rows)
cs = constrain (cs, 1, self.cols)
@@ -147,13 +195,19 @@ class screen:
r = constrain (r, 1, self.rows)
c = constrain (c, 1, self.cols)
- ch = str(ch)[0]
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)[0]
+ else:
+ ch = ch[0]
self.w[r-1][c-1] = ch
def put (self, ch):
'''This puts a characters at the current cursor position.
'''
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
self.put_abs (self.cur_r, self.cur_c, ch)
def insert_abs (self, r, c, ch):
@@ -162,6 +216,9 @@ class screen:
The last character of the line is lost.
'''
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
r = constrain (r, 1, self.rows)
c = constrain (c, 1, self.cols)
for ci in range (self.cols, c, -1):
@@ -170,6 +227,9 @@ class screen:
def insert (self, ch):
+ if isinstance(ch, bytes):
+ ch = self._decode(ch)
+
self.insert_abs (self.cur_r, self.cur_c, ch)
def get_abs (self, r, c):
@@ -196,7 +256,7 @@ class screen:
cs, ce = ce, cs
sc = []
for r in range (rs, re+1):
- line = ''
+ line = u''
for c in range (cs, ce + 1):
ch = self.get_abs (r,c)
line = line + ch
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ae62686
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[pytest]
+norecursedirs = .git
diff --git a/tests/test_ansi.py b/tests/test_ansi.py
index 516509c..a9d445e 100755
--- a/tests/test_ansi.py
+++ b/tests/test_ansi.py
@@ -21,6 +21,9 @@ PEXPECT LICENSE
from pexpect import ANSI
import unittest
from . import PexpectTestCase
+import sys
+
+PY3 = (sys.version_info[0] >= 3)
write_target = 'I\'ve got a ferret sticking up my nose. \n' +\
'(He\'s got a ferret sticking up his nose.) \n' +\
@@ -162,6 +165,62 @@ class ansiTestCase (PexpectTestCase.PexpectTestCase):
assert str(s) == ('test ')
assert s.state.memory == [s]
+ def test_utf8_bytes(self):
+ """Test that when bytes are passed in containing UTF-8 encoded
+ characters, where the encoding of each character consists of
+ multiple bytes, the characters are correctly decoded.
+ Incremental decoding is also tested."""
+ s = ANSI.ANSI(2, 10, encoding='utf-8')
+ # This is the UTF-8 encoding of the UCS character "HOURGLASS"
+ # followed by the UTF-8 encoding of the UCS character
+ # "KEYBOARD". These characters can't be encoded in cp437 or
+ # latin-1. The "KEYBOARD" character is split into two
+ # separate writes.
+ s.write(b'\xe2\x8c\x9b')
+ s.write(b'\xe2\x8c')
+ s.write(b'\xa8')
+ if PY3:
+ assert str(s) == u'\u231b\u2328 \n '
+ else:
+ assert unicode(s) == u'\u231b\u2328 \n '
+ assert str(s) == b'\xe2\x8c\x9b\xe2\x8c\xa8 \n '
+ assert s.dump() == u'\u231b\u2328 '
+ assert s.pretty() == u'+----------+\n|\u231b\u2328 |\n| |\n+----------+\n'
+ assert s.get_abs(1, 1) == u'\u231b'
+ assert s.get_region(1, 1, 1, 5) == [u'\u231b\u2328 ']
+
+ def test_unicode(self):
+ """Test passing in of a unicode string."""
+ s = ANSI.ANSI(2, 10, encoding="utf-8")
+ s.write(u'\u231b\u2328')
+ if PY3:
+ assert str(s) == u'\u231b\u2328 \n '
+ else:
+ assert unicode(s) == u'\u231b\u2328 \n '
+ assert str(s) == b'\xe2\x8c\x9b\xe2\x8c\xa8 \n '
+ assert s.dump() == u'\u231b\u2328 '
+ assert s.pretty() == u'+----------+\n|\u231b\u2328 |\n| |\n+----------+\n'
+ assert s.get_abs(1, 1) == u'\u231b'
+ assert s.get_region(1, 1, 1, 5) == [u'\u231b\u2328 ']
+
+ def test_decode_error(self):
+ """Test that default handling of decode errors replaces the
+ invalid characters."""
+ s = ANSI.ANSI(2, 10, encoding="ascii")
+ s.write(b'\xff') # a non-ASCII character
+ # In unicode, the non-ASCII character is replaced with
+ # REPLACEMENT CHARACTER.
+ if PY3:
+ assert str(s) == u'\ufffd \n '
+ else:
+ assert unicode(s) == u'\ufffd \n '
+ assert str(s) == b'? \n '
+ assert s.dump() == u'\ufffd '
+ assert s.pretty() == u'+----------+\n|\ufffd |\n| |\n+----------+\n'
+ assert s.get_abs(1, 1) == u'\ufffd'
+ assert s.get_region(1, 1, 1, 5) == [u'\ufffd ']
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_async.py b/tests/test_async.py
new file mode 100644
index 0000000..ce75572
--- /dev/null
+++ b/tests/test_async.py
@@ -0,0 +1,51 @@
+try:
+ import asyncio
+except ImportError:
+ asyncio = None
+
+import sys
+import unittest
+
+import pexpect
+from .PexpectTestCase import PexpectTestCase
+
+def run(coro):
+ 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)
+ assert run(coro) == 0
+ print('Done')
+
+ def test_timeout(self):
+ 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)
+ assert run(coro) == 1
+
+ def test_eof(self):
+ 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.sendeof()
+ coro = p.expect('Blah', async=True)
+ with self.assertRaises(pexpect.EOF):
+ run(coro)
+
+ def test_expect_exact(self):
+ p = pexpect.spawn('%s list100.py' % sys.executable)
+ 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
diff --git a/tests/test_constructor.py b/tests/test_constructor.py
index 60525a0..98c473a 100755
--- a/tests/test_constructor.py
+++ b/tests/test_constructor.py
@@ -28,11 +28,11 @@ class TestCaseConstructor(PexpectTestCase.PexpectTestCase):
the same results for different styles of invoking __init__().
This assumes that the root directory / is static during the test.
'''
- p1 = pexpect.spawn('/bin/ls -l /bin')
- p2 = pexpect.spawn('/bin/ls' ,['-l', '/bin'])
- p1.expect (pexpect.EOF)
- p2.expect (pexpect.EOF)
- assert (p1.before == p2.before)
+ p1 = pexpect.spawn('uname -m -n -p -r -s -v')
+ p2 = pexpect.spawn('uname', ['-m', '-n', '-p', '-r', '-s', '-v'])
+ p1.expect(pexpect.EOF)
+ p2.expect(pexpect.EOF)
+ assert p1.before == p2.before
def test_named_parameters (self):
'''This tests that named parameters work.
diff --git a/tests/test_expect.py b/tests/test_expect.py
index 8ccb9c5..3f4c9d8 100755
--- a/tests/test_expect.py
+++ b/tests/test_expect.py
@@ -18,11 +18,13 @@ PEXPECT LICENSE
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
'''
+import multiprocessing
import unittest
import subprocess
import time
import signal
import sys
+import os
import pexpect
from . import PexpectTestCase
@@ -542,7 +544,40 @@ class ExpectTestCase (PexpectTestCase.PexpectTestCase):
signal.alarm(1)
p1.expect('END')
+ def test_stdin_closed(self):
+ '''
+ Ensure pexpect continues to operate even when stdin is closed
+ '''
+ class Closed_stdin_proc(multiprocessing.Process):
+ def run(self):
+ sys.__stdin__.close()
+ cat = pexpect.spawn('cat')
+ cat.sendeof()
+ cat.expect(pexpect.EOF)
+
+ proc = Closed_stdin_proc()
+ proc.start()
+ proc.join()
+ assert proc.exitcode == 0
+
+ def test_stdin_stdout_closed(self):
+ '''
+ Ensure pexpect continues to operate even when stdin and stdout is closed
+ '''
+ class Closed_stdin_stdout_proc(multiprocessing.Process):
+ def run(self):
+ sys.__stdin__.close()
+ sys.__stdout__.close()
+ cat = pexpect.spawn('cat')
+ cat.sendeof()
+ cat.expect(pexpect.EOF)
+
+ proc = Closed_stdin_stdout_proc()
+ proc.start()
+ proc.join()
+ assert proc.exitcode == 0
+
if __name__ == '__main__':
unittest.main()
-suite = unittest.makeSuite(ExpectTestCase,'test')
+suite = unittest.makeSuite(ExpectTestCase, 'test')
diff --git a/tests/test_repr.py b/tests/test_repr.py
new file mode 100644
index 0000000..ce618d4
--- /dev/null
+++ b/tests/test_repr.py
@@ -0,0 +1,26 @@
+""" Test __str__ methods. """
+import pexpect
+
+from . import PexpectTestCase
+
+
+class TestCaseMisc(PexpectTestCase.PexpectTestCase):
+
+ def test_str_spawnu(self):
+ """ Exercise spawnu.__str__() """
+ # given,
+ p = pexpect.spawnu('cat')
+ # exercise,
+ value = str(p)
+ # verify
+ assert isinstance(value, str)
+
+ def test_str_spawn(self):
+ """ Exercise spawn.__str__() """
+ # given,
+ p = pexpect.spawn('cat')
+ # exercise,
+ value = str(p)
+ # verify
+ assert isinstance(value, str)
+
diff --git a/tests/test_run.py b/tests/test_run.py
index 814b70a..c018b4d 100755
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -22,14 +22,11 @@ PEXPECT LICENSE
import pexpect
import unittest
import subprocess
+import tempfile
import sys
+import os
from . import PexpectTestCase
-# TODO Many of these test cases blindly assume that sequential
-# TODO listing of the /bin directory will yield the same results.
-# TODO This may not always be true, but seems adequate for testing for now.
-# TODO I should fix this at some point.
-
unicode_type = str if pexpect.PY3 else unicode
def timeout_callback (d):
@@ -44,14 +41,24 @@ class RunFuncTestCase(PexpectTestCase.PexpectTestCase):
empty = b''
prep_subprocess_out = staticmethod(lambda x: x)
+ def setUp(self):
+ fd, self.rcfile = tempfile.mkstemp()
+ os.write(fd, b'PS1=GO: \n')
+ os.close(fd)
+ super(RunFuncTestCase, self).setUp()
+
+ def tearDown(self):
+ os.unlink(self.rcfile)
+ super(RunFuncTestCase, self).tearDown()
+
def test_run_exit (self):
(data, exitstatus) = self.runfunc('python exit1.py', withexitstatus=1)
assert exitstatus == 1, "Exit status of 'python exit1.py' should be 1."
def test_run (self):
- the_old_way = subprocess.Popen(args=['ls', '-l', '/bin'],
+ the_old_way = subprocess.Popen(args=['uname', '-m', '-n'],
stdout=subprocess.PIPE).communicate()[0].rstrip()
- (the_new_way, exitstatus) = self.runfunc('ls -l /bin', withexitstatus=1)
+ (the_new_way, exitstatus) = self.runfunc('uname -m -n', withexitstatus=1)
the_new_way = the_new_way.replace(self.cr, self.empty).rstrip()
self.assertEqual(self.prep_subprocess_out(the_old_way), the_new_way)
self.assertEqual(exitstatus, 0)
@@ -64,6 +71,23 @@ class RunFuncTestCase(PexpectTestCase.PexpectTestCase):
withexitstatus=1)
assert exitstatus != 0
+ def test_run_tuple_list (self):
+ events = [
+ # second match on 'abc', echo 'def'
+ ('abc\r\n.*GO:', 'echo "def"\n'),
+ # final match on 'def': exit
+ ('def\r\n.*GO:', 'exit\n'),
+ # first match on 'GO:' prompt, echo 'abc'
+ ('GO:', 'echo "abc"\n')
+ ]
+
+ (data, exitstatus) = pexpect.run(
+ 'bash --rcfile {0}'.format(self.rcfile),
+ withexitstatus=True,
+ events=events,
+ timeout=10)
+ assert exitstatus == 0
+
class RunUnicodeFuncTestCase(RunFuncTestCase):
runfunc = staticmethod(pexpect.runu)
cr = b'\r'.decode('ascii')
diff --git a/tests/test_screen.py b/tests/test_screen.py
index 3f0736b..2429e57 100755
--- a/tests/test_screen.py
+++ b/tests/test_screen.py
@@ -19,10 +19,14 @@ PEXPECT LICENSE
'''
+import sys
+
from pexpect import screen
import unittest
from . import PexpectTestCase
+PY3 = (sys.version_info[0] >= 3)
+
fill1_target='XXXXXXXXXX\n' + \
'XOOOOOOOOX\n' + \
'XO::::::OX\n' + \
@@ -76,6 +80,17 @@ insert_target = 'ZXZZZZZZXZ\n' +\
'ZZ/2.4.6ZZ'
get_region_target = ['......', '.\\/...', './\\...', '......']
+unicode_box_unicode_result = u'\u2554\u2557\n\u255A\u255D'
+unicode_box_pretty_result = u'''\
++--+
+|\u2554\u2557|
+|\u255A\u255D|
++--+
+'''
+unicode_box_ascii_bytes_result = b'??\n??'
+unicode_box_cp437_bytes_result = b'\xc9\xbb\n\xc8\xbc'
+unicode_box_utf8_bytes_result = b'\xe2\x95\x94\xe2\x95\x97\n\xe2\x95\x9a\xe2\x95\x9d'
+
class screenTestCase (PexpectTestCase.PexpectTestCase):
def make_screen_with_put (self):
s = screen.screen(10,10)
@@ -168,20 +183,101 @@ class screenTestCase (PexpectTestCase.PexpectTestCase):
s.insert_abs (10,9,'Z')
s.insert_abs (10,9,'Z')
assert str(s) == insert_target
- # def test_write (self):
- # s = screen.screen (6,65)
- # s.fill('.')
- # s.cursor_home()
- # for c in write_text:
- # s.write (c)
- # print str(s)
- # assert str(s) == write_target
- # def test_tetris (self):
- # s = screen.screen (24,80)
- # tetris_text = open ('tetris.data').read()
- # for c in tetris_text:
- # s.write (c)
- # assert str(s) == tetris_target
+
+ def make_screen_with_box_unicode(self, *args, **kwargs):
+ '''Creates a screen containing a box drawn using double-line
+ line drawing characters. The characters are fed in as
+ unicode. '''
+ s = screen.screen (2,2,*args,**kwargs)
+ s.put_abs (1,1,u'\u2554')
+ s.put_abs (1,2,u'\u2557')
+ s.put_abs (2,1,u'\u255A')
+ s.put_abs (2,2,u'\u255D')
+ return s
+
+ def make_screen_with_box_cp437(self, *args, **kwargs):
+ '''Creates a screen containing a box drawn using double-line
+ line drawing characters. The characters are fed in as
+ CP437. '''
+ s = screen.screen (2,2,*args,**kwargs)
+ s.put_abs (1,1,b'\xc9')
+ s.put_abs (1,2,b'\xbb')
+ s.put_abs (2,1,b'\xc8')
+ s.put_abs (2,2,b'\xbc')
+ return s
+
+ def make_screen_with_box_utf8(self, *args, **kwargs):
+ '''Creates a screen containing a box drawn using double-line
+ line drawing characters. The characters are fed in as
+ UTF-8. '''
+ s = screen.screen (2,2,*args,**kwargs)
+ s.put_abs (1,1,b'\xe2\x95\x94')
+ s.put_abs (1,2,b'\xe2\x95\x97')
+ s.put_abs (2,1,b'\xe2\x95\x9a')
+ s.put_abs (2,2,b'\xe2\x95\x9d')
+ return s
+
+ def test_unicode_ascii (self):
+ # With the default encoding set to ASCII, we should still be
+ # able to feed in unicode strings and get them back out:
+ s = self.make_screen_with_box_unicode('ascii')
+ if PY3:
+ assert str(s) == unicode_box_unicode_result
+ else:
+ assert unicode(s) == unicode_box_unicode_result
+ # And we should still get something for Python 2 str(), though
+ # it might not be very useful
+ str(s)
+
+ assert s.pretty() == unicode_box_pretty_result
+
+ def test_decoding_errors(self):
+ # With strict error handling, it should reject bytes it can't decode
+ with self.assertRaises(UnicodeDecodeError):
+ self.make_screen_with_box_cp437('ascii', 'strict')
+
+ # replace should turn them into unicode replacement characters, U+FFFD
+ s = self.make_screen_with_box_cp437('ascii', 'replace')
+ expected = u'\ufffd\ufffd\n\ufffd\ufffd'
+ if PY3:
+ assert str(s) == expected
+ else:
+ assert unicode(s) == expected
+
+ def test_unicode_cp437 (self):
+ # Verify decoding from and re-encoding to CP437.
+ s = self.make_screen_with_box_cp437('cp437','strict')
+ if PY3:
+ assert str(s) == unicode_box_unicode_result
+ else:
+ assert unicode(s) == unicode_box_unicode_result
+ assert str(s) == unicode_box_cp437_bytes_result
+ assert s.pretty() == unicode_box_pretty_result
+
+ def test_unicode_utf8 (self):
+ # Verify decoding from and re-encoding to UTF-8.
+ s = self.make_screen_with_box_utf8('utf-8','strict')
+ if PY3:
+ assert str(s) == unicode_box_unicode_result
+ else:
+ assert unicode(s) == unicode_box_unicode_result
+ assert str(s) == unicode_box_utf8_bytes_result
+ assert s.pretty() == unicode_box_pretty_result
+
+ def test_no_bytes(self):
+ s = screen.screen(2, 2, encoding=None)
+ s.put_abs(1, 1, u'A')
+ s.put_abs(2, 2, u'D')
+
+ with self.assertRaises(TypeError):
+ s.put_abs(1, 2, b'B')
+
+ if PY3:
+ assert str(s) == u'A \n D'
+ else:
+ assert unicode(s) == u'A \n D'
+ # This will still work if it's limited to ascii
+ assert str(s) == b'A \n D'
if __name__ == '__main__':
unittest.main()