diff --git a/.travis.yml b/.travis.yml index 832f535..d150541 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,14 @@ # limitations under the License. language: python -python: [ "2.7" ] +matrix: + include: + - python: "2.7" + env: REQUIREMENTS=requirements-dev.txt VENV=python2.7 + - python: "pypy" + env: REQUIREMENTS=requirements-dev.txt VENV=pypy + - python: "3.3" + env: REQUIREMENTS=requirements3-dev.txt VENV=python3.3 before_install: | # needed for the UML container @@ -37,5 +44,5 @@ before_install: | chmod +x tests.sh chmod +x run.sh -install: pip install -r requirements-dev.txt +install: pip install -r $REQUIREMENTS script: ./run.sh diff --git a/dockerpty/io.py b/dockerpty/io.py index dda53fb..06282b5 100644 --- a/dockerpty/io.py +++ b/dockerpty/io.py @@ -19,6 +19,7 @@ import errno import struct import select as builtin_select +import six def set_blocking(fd, blocking=True): @@ -31,7 +32,7 @@ def set_blocking(fd, blocking=True): old_flag = fcntl.fcntl(fd, fcntl.F_GETFL) if blocking: - new_flag = old_flag &~ os.O_NONBLOCK + new_flag = old_flag & ~ os.O_NONBLOCK else: new_flag = old_flag | os.O_NONBLOCK @@ -59,7 +60,8 @@ def select(read_streams, timeout=0): )[0] except builtin_select.error as e: # POSIX signals interrupt select() - if e[0] == errno.EINTR: + no = e.errno if six.PY3 else e[0] + if no == errno.EINTR: return [] else: raise e @@ -73,7 +75,6 @@ class Stream(object): add consistency to the reading of sockets and files alike. """ - """ Recoverable IO/OS Errors. """ @@ -83,7 +84,6 @@ class Stream(object): errno.EWOULDBLOCK, ] - def __init__(self, fd): """ Initialize the Stream for the file descriptor `fd`. @@ -92,7 +92,6 @@ def __init__(self, fd): """ self.fd = fd - def fileno(self): """ Return the fileno() of the file descriptor. @@ -100,7 +99,6 @@ def fileno(self): return self.fd.fileno() - def set_blocking(self, value): if hasattr(self.fd, 'setblocking'): self.fd.setblocking(value) @@ -108,7 +106,6 @@ def set_blocking(self, value): else: return set_blocking(self.fd, value) - def read(self, n=4096): """ Return `n` bytes of data from the Stream, or None at end of stream. @@ -122,7 +119,6 @@ def read(self, n=4096): if e.errno not in Stream.ERRNO_RECOVERABLE: raise e - def write(self, data): """ Write `data` to the Stream. @@ -170,7 +166,6 @@ def __init__(self, stream): self.stream = stream self.remain = 0 - def fileno(self): """ Returns the fileno() of the underlying Stream. @@ -180,11 +175,9 @@ def fileno(self): return self.stream.fileno() - def set_blocking(self, value): return self.stream.set_blocking(value) - def read(self, n=4096): """ Read up to `n` bytes of data from the Stream, after demuxing. @@ -201,16 +194,15 @@ def read(self, n=4096): if size <= 0: return else: - data = '' + data = six.binary_type() while len(data) < size: nxt = self.stream.read(size - len(data)) if not nxt: # the stream has closed, return what data we got return data - data = "{0}{1}".format(data, nxt) + data = data + nxt return data - def write(self, data): """ Delegates the the underlying Stream. @@ -218,7 +210,6 @@ def write(self, data): return self.stream.write(data) - def _next_packet_size(self, n=0): size = 0 @@ -226,13 +217,13 @@ def _next_packet_size(self, n=0): size = min(n, self.remain) self.remain -= size else: - data = '' + data = six.binary_type() while len(data) < 8: nxt = self.stream.read(8 - len(data)) if not nxt: # The stream has closed, there's nothing more to read return 0 - data = "{0}{1}".format(data, nxt) + data = data + nxt if data is None: return 0 @@ -270,7 +261,6 @@ def __init__(self, from_stream, to_stream): self.from_stream = from_stream self.to_stream = to_stream - def fileno(self): """ Returns the `fileno()` of the reader end of the Pump. @@ -280,11 +270,9 @@ def fileno(self): return self.from_stream.fileno() - def set_blocking(self, value): return self.from_stream.set_blocking(value) - def flush(self, n=4096): """ Flush `n` bytes of data from the reader Stream to the writer Stream. diff --git a/features/steps/step_definitions.py b/features/steps/step_definitions.py index b630af0..0c34ba8 100644 --- a/features/steps/step_definitions.py +++ b/features/steps/step_definitions.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from behave import * +from behave import then, given, when from expects import expect, equal, be_true, be_false import tests.util as util @@ -23,8 +23,8 @@ import sys import os import signal -import errno import time +import six @given('I am using a TTY') @@ -97,6 +97,7 @@ def step_impl(ctx): ) ctx.pid = pid util.wait(ctx.pty, timeout=5) + time.sleep(1) # give the terminal some time to print prompt @when('I resize the terminal to {rows} x {cols}') @@ -113,22 +114,21 @@ def step_impl(ctx, rows, cols): @when('I type "{text}"') def step_impl(ctx, text): - util.write(ctx.pty, text) - + util.write(ctx.pty, text.encode()) @when('I press {key}') def step_impl(ctx, key): mappings = { - "enter": "\x0a", - "up": "\x1b[A", - "down": "\x1b[B", - "right": "\x1b[C", - "left": "\x1b[D", - "esc": "\x1b", - "c-c": "\x03", - "c-d": "\x04", - "c-p": "\x10", - "c-q": "\x11", + "enter": b"\x0a", + "up": b"\x1b[A", + "down": b"\x1b[B", + "right": b"\x1b[C", + "left": b"\x1b[D", + "esc": b"\x1b", + "c-c": b"\x03", + "c-d": b"\x04", + "c-p": b"\x10", + "c-q": b"\x11", } util.write(ctx.pty, mappings[key.lower()]) diff --git a/requirements-dev.txt b/requirements-dev.txt index 78fe60c..c0ea3ed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ docker-py>=0.3.2 pytest>=2.5.2 behave>=1.2.4 expects>=0.4 +six>=1.3.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6913a59 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +six>=1.3.0 diff --git a/requirements3-dev.txt b/requirements3-dev.txt new file mode 100644 index 0000000..fdd3511 --- /dev/null +++ b/requirements3-dev.txt @@ -0,0 +1,5 @@ +docker-py>=0.7.1 +pytest>=2.5.2 +behave>=1.2.4 +expects>=0.4 +six>=1.3.0 diff --git a/run.sh b/run.sh index 6bb0255..ee1b892 100644 --- a/run.sh +++ b/run.sh @@ -6,7 +6,7 @@ echo 1 > /tmp/build.status # run the build inside UML kernel ./linux quiet mem=2G rootfstype=hostfs rw \ eth0=slirp,,/usr/bin/slirp-fullbolt \ - init=$(pwd)/tests.sh WORKDIR=$(pwd) HOME=$HOME + init=$(pwd)/tests.sh WORKDIR=$(pwd) VENV=$VENV HOME=$HOME # grab the build result and use it exit $(cat /tmp/build.status) diff --git a/setup.py b/setup.py index 2b7165d..03dcbd1 100644 --- a/setup.py +++ b/setup.py @@ -17,9 +17,11 @@ from setuptools import setup import os + def fopen(filename): return open(os.path.join(os.path.dirname(__file__), filename)) + def read(filename): return fopen(filename).read() @@ -31,6 +33,7 @@ def read(filename): url='https://github.com/d11wtq/dockerpty', author='Chris Corbyn', author_email='chris@w3style.co.uk', + install_requires=['six >= 1.3.0'], license='Apache 2.0', keywords='docker, tty, pty, terminal', packages=['dockerpty'], diff --git a/tests.sh b/tests.sh index cfd453a..5fe0c6e 100644 --- a/tests.sh +++ b/tests.sh @@ -60,7 +60,7 @@ mount --bind /run/resolvconf/resolv.conf /etc/resolv.conf sleep 5 # activate virtualenv -source $HOME/virtualenv/python2.7/bin/activate +source $HOME/virtualenv/$VENV/bin/activate # run the build py.test -q tests && behave -c diff --git a/tests/unit/test_io.py b/tests/unit/test_io.py index 8f1c791..658c2b0 100644 --- a/tests/unit/test_io.py +++ b/tests/unit/test_io.py @@ -15,7 +15,7 @@ # limitations under the License. from expects import expect, equal, be_none, be_true, be_false -from io import StringIO +from io import StringIO, BytesIO import dockerpty.io as io import sys @@ -23,6 +23,7 @@ import fcntl import socket import tempfile +import six def test_set_blocking_changes_fd_flags(): @@ -47,9 +48,9 @@ def test_set_blocking_returns_previous_state(): def test_select_returns_streams_for_reading(): a, b = socket.socketpair() - a.send('test') + a.send(b'test') expect(io.select([a, b], timeout=0)).to(equal([b])) - b.send('test') + b.send(b'test') expect(io.select([a, b], timeout=0)).to(equal([a, b])) b.recv(4) expect(io.select([a, b], timeout=0)).to(equal([a])) @@ -63,48 +64,41 @@ def test_fileno_delegates_to_file_descriptor(self): stream = io.Stream(sys.stdout) expect(stream.fileno()).to(equal(sys.stdout.fileno())) - def test_read_from_socket(self): a, b = socket.socketpair() - a.send('test') + a.send(b'test') stream = io.Stream(b) - expect(stream.read(32)).to(equal('test')) - + expect(stream.read(32)).to(equal(b'test')) def test_write_to_socket(self): a, b = socket.socketpair() stream = io.Stream(a) - stream.write('test') - expect(b.recv(32)).to(equal('test')) - + stream.write(b'test') + expect(b.recv(32)).to(equal(b'test')) def test_read_from_file(self): with tempfile.TemporaryFile() as f: stream = io.Stream(f) - f.write('test') + f.write(b'test') f.seek(0) - expect(stream.read(32)).to(equal('test')) - + expect(stream.read(32)).to(equal(b'test')) def test_read_returns_empty_string_at_eof(self): with tempfile.TemporaryFile() as f: stream = io.Stream(f) - expect(stream.read(32)).to(equal('')) - + expect(stream.read(32)).to(equal(b'')) def test_write_to_file(self): with tempfile.TemporaryFile() as f: stream = io.Stream(f) - stream.write('test') + stream.write(b'test') f.seek(0) - expect(f.read(32)).to(equal('test')) - + expect(f.read(32)).to(equal(b'test')) def test_write_returns_length_written(self): with tempfile.TemporaryFile() as f: stream = io.Stream(f) - expect(stream.write('test')).to(equal(4)) - + expect(stream.write(b'test')).to(equal(4)) def test_write_returns_none_when_no_data(self): stream = io.Stream(StringIO()) @@ -137,64 +131,56 @@ class TestDemuxer(object): def create_fixture(self): chunks = [ - "\x01\x00\x00\x00\x00\x00\x00\x03foo", - "\x01\x00\x00\x00\x00\x00\x00\x01d", + b"\x01\x00\x00\x00\x00\x00\x00\x03foo", + b"\x01\x00\x00\x00\x00\x00\x00\x01d", ] - return StringIO(u''.join(chunks)) - + return six.BytesIO(six.binary_type().join(chunks)) def test_fileno_delegates_to_stream(self): demuxer = io.Demuxer(sys.stdout) expect(demuxer.fileno()).to(equal(sys.stdout.fileno())) - def test_reading_single_chunk(self): demuxer = io.Demuxer(self.create_fixture()) - expect(demuxer.read(32)).to(equal('foo')) - + expect(demuxer.read(32)).to(equal(b'foo')) def test_reading_multiple_chunks(self): demuxer = io.Demuxer(self.create_fixture()) - expect(demuxer.read(32)).to(equal('foo')) - expect(demuxer.read(32)).to(equal('d')) - + expect(demuxer.read(32)).to(equal(b'foo')) + expect(demuxer.read(32)).to(equal(b'd')) def test_reading_data_from_slow_stream(self): slow_stream = SlowStream([ - "\x01\x00\x00\x00\x00\x00\x00\x03f", - "oo", - "\x01\x00\x00\x00\x00\x00\x00\x01d", + b"\x01\x00\x00\x00\x00\x00\x00\x03f", + b"oo", + b"\x01\x00\x00\x00\x00\x00\x00\x01d", ]) demuxer = io.Demuxer(slow_stream) - expect(demuxer.read(32)).to(equal('foo')) - expect(demuxer.read(32)).to(equal('d')) - + expect(demuxer.read(32)).to(equal(b'foo')) + expect(demuxer.read(32)).to(equal(b'd')) def test_reading_size_from_slow_stream(self): slow_stream = SlowStream([ - "\x01\x00\x00\x00", - "\x00\x00\x00\x03foo", - "\x01\x00", - "\x00\x00\x00\x00\x00\x01d", + b'\x01\x00\x00\x00', + b'\x00\x00\x00\x03foo', + b'\x01\x00', + b'\x00\x00\x00\x00\x00\x01d', ]) demuxer = io.Demuxer(slow_stream) - expect(demuxer.read(32)).to(equal('foo')) - expect(demuxer.read(32)).to(equal('d')) - + expect(demuxer.read(32)).to(equal(b'foo')) + expect(demuxer.read(32)).to(equal(b'd')) def test_reading_partial_chunk(self): demuxer = io.Demuxer(self.create_fixture()) - expect(demuxer.read(2)).to(equal('fo')) - + expect(demuxer.read(2)).to(equal(b'fo')) def test_reading_overlapping_chunks(self): demuxer = io.Demuxer(self.create_fixture()) - expect(demuxer.read(2)).to(equal('fo')) - expect(demuxer.read(2)).to(equal('o')) - expect(demuxer.read(2)).to(equal('d')) - + expect(demuxer.read(2)).to(equal(b'fo')) + expect(demuxer.read(2)).to(equal(b'o')) + expect(demuxer.read(2)).to(equal(b'd')) def test_write_delegates_to_stream(self): s = StringIO() @@ -214,7 +200,6 @@ def test_fileno_delegates_to_from_stream(self): pump = io.Pump(sys.stdout, sys.stderr) expect(pump.fileno()).to(equal(sys.stdout.fileno())) - def test_flush_pipes_data_between_streams(self): a = StringIO(u'food') b = StringIO() @@ -223,7 +208,6 @@ def test_flush_pipes_data_between_streams(self): expect(a.read(1)).to(equal('d')) expect(b.getvalue()).to(equal('foo')) - def test_flush_returns_length_written(self): a = StringIO(u'fo') b = StringIO() diff --git a/tests/util.py b/tests/util.py index c7356fc..6778e41 100644 --- a/tests/util.py +++ b/tests/util.py @@ -21,6 +21,7 @@ import os import re import time +import six def set_pty_size(fd, size): @@ -57,7 +58,6 @@ def write(fd, data): """ Write `data` to the PTY at `fd`. """ - os.write(fd, data) @@ -69,7 +69,7 @@ def readchar(fd): while True: ready = wait(fd) if len(ready) == 0: - return '' + return six.binary_type() else: for s in ready: return os.read(s, 1) @@ -82,15 +82,15 @@ def readline(fd): The line includes the line ending. """ - output = [] + output = six.binary_type() while True: char = readchar(fd) if char: - output.append(char) - if char == "\n": - return ''.join(output) + output = output + char + if char == b"\n": + return output else: - return ''.join(output) + return output def read(fd): @@ -98,13 +98,13 @@ def read(fd): Read all output from the PTY at `fd`, or nothing if no data to read. """ - output = [] + output = six.binary_type() while True: line = readline(fd) if line: - output.append(line) + output = output + line else: - return "".join(output) + return output.decode() def read_printable(fd): diff --git a/tox.ini b/tox.ini index 7147097..338182f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,11 @@ [tox] -envlist = py27, pypy +envlist = py27, pypy, py33 [testenv] deps = - -rrequirements-dev.txt + py27: -rrequirements-dev.txt + pypy: -rrequirements-dev.txt + py33: -rrequirements3-dev.txt commands = py.test -v tests/ behave