Support python 3

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2014-08-19 17:36:46 -04:00
parent 17682c58db
commit 809443d6d0
23 changed files with 128 additions and 85 deletions

View File

@ -66,8 +66,8 @@ WORKDIR /code/
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD requirements-dev.txt /code/
RUN pip install -r requirements-dev.txt
ADD requirements-dev-py2.txt /code/
RUN pip install -r requirements-dev-py2.txt
RUN pip install tox==2.1.1

View File

@ -1,7 +1,7 @@
include Dockerfile
include LICENSE
include requirements.txt
include requirements-dev.txt
include requirements-dev*.txt
include tox.ini
include *.md
include compose/config/schema.json

View File

@ -4,6 +4,8 @@ from __future__ import unicode_literals
import sys
from itertools import cycle
import six
from . import colors
from .multiplexer import Multiplexer
from .utils import split_buffer
@ -20,6 +22,8 @@ class LogPrinter(object):
def run(self):
mux = Multiplexer(self.generators)
for line in mux.loop():
if isinstance(line, six.text_type) and not six.PY3:
line = line.encode('utf-8')
self.output.write(line)
def _calculate_prefix_width(self, containers):
@ -52,7 +56,7 @@ class LogPrinter(object):
return generators
def _make_log_generator(self, container, color_fn):
prefix = color_fn(self._generate_prefix(container)).encode('utf-8')
prefix = color_fn(self._generate_prefix(container))
# Attach to container before log printer starts running
line_generator = split_buffer(self._attach(container), '\n')

View File

@ -9,6 +9,7 @@ import ssl
import subprocess
from docker import version as docker_py_version
from six.moves import input
from .. import __version__
@ -23,7 +24,7 @@ def yesno(prompt, default=None):
Unrecognised input (anything other than "y", "n", "yes",
"no" or "") will return None.
"""
answer = raw_input(prompt).strip().lower()
answer = input(prompt).strip().lower()
if answer == "y" or answer == "yes":
return True

View File

@ -1,6 +1,7 @@
import codecs
import json
import os
import six
class StreamOutputError(Exception):
@ -8,8 +9,9 @@ class StreamOutputError(Exception):
def stream_output(output, stream):
is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno())
stream = codecs.getwriter('utf-8')(stream)
is_terminal = hasattr(stream, 'isatty') and stream.isatty()
if not six.PY3:
stream = codecs.getwriter('utf-8')(stream)
all_events = []
lines = {}
diff = 0
@ -55,7 +57,6 @@ def print_output_event(event, stream, is_terminal):
# erase current line
stream.write("%c[2K\r" % 27)
terminator = "\r"
pass
elif 'progressDetail' in event:
return

View File

@ -17,6 +17,7 @@ from .legacy import check_for_legacy_containers
from .service import Service
from .utils import parallel_execute
log = logging.getLogger(__name__)

View File

@ -724,7 +724,7 @@ class Service(object):
try:
all_events = stream_output(build_output, sys.stdout)
except StreamOutputError as e:
raise BuildError(self, unicode(e))
raise BuildError(self, six.text_type(e))
# Ensure the HTTP connection is not reused for another
# streaming command, as the Docker daemon can sometimes

2
requirements-dev-py3.txt Normal file
View File

@ -0,0 +1,2 @@
flake8
nose >= 1.3.0

View File

@ -48,8 +48,11 @@ tests_require = [
]
if sys.version_info < (2, 7):
if sys.version_info < (2, 6):
tests_require.append('unittest2')
if sys.version_info[:1] < (3,):
tests_require.append('pyinstaller')
tests_require.append('mock >= 1.0.1')
setup(

View File

@ -4,3 +4,8 @@ if sys.version_info >= (2, 7):
import unittest # NOQA
else:
import unittest2 as unittest # NOQA
try:
from unittest import mock
except ImportError:
import mock # NOQA

View File

@ -5,9 +5,9 @@ import shlex
import sys
from operator import attrgetter
from mock import patch
from six import StringIO
from .. import mock
from .testcases import DockerClientTestCase
from compose.cli.errors import UserError
from compose.cli.main import TopLevelCommand
@ -51,13 +51,13 @@ class CLITestCase(DockerClientTestCase):
self.command.base_dir = old_base_dir
# TODO: address the "Inappropriate ioctl for device" warnings in test output
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_ps(self, mock_stdout):
self.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('simplecomposefile_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_ps_default_composefile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
self.command.dispatch(['up', '-d'], None)
@ -68,7 +68,7 @@ class CLITestCase(DockerClientTestCase):
self.assertIn('multiplecomposefiles_another_1', output)
self.assertNotIn('multiplecomposefiles_yetanother_1', output)
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_ps_alternate_composefile(self, mock_stdout):
config_path = os.path.abspath(
'tests/fixtures/multiple-composefiles/compose2.yml')
@ -83,19 +83,19 @@ class CLITestCase(DockerClientTestCase):
self.assertNotIn('multiplecomposefiles_another_1', output)
self.assertIn('multiplecomposefiles_yetanother_1', output)
@patch('compose.service.log')
@mock.patch('compose.service.log')
def test_pull(self, mock_logging):
self.command.dispatch(['pull'], None)
mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
@patch('compose.service.log')
@mock.patch('compose.service.log')
def test_pull_with_digest(self, mock_logging):
self.command.dispatch(['-f', 'digest.yml', 'pull'], None)
mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
mock_logging.info.assert_any_call('Pulling digest (busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d)...')
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_build_no_cache(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/simple-dockerfile'
self.command.dispatch(['build', 'simple'], None)
@ -189,7 +189,7 @@ class CLITestCase(DockerClientTestCase):
self.assertFalse(config['AttachStdout'])
self.assertFalse(config['AttachStdin'])
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_without_links(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['run', 'console', '/bin/true'], None)
@ -202,7 +202,7 @@ class CLITestCase(DockerClientTestCase):
self.assertTrue(config['AttachStdout'])
self.assertTrue(config['AttachStdin'])
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_links(self, __):
self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['run', 'web', '/bin/true'], None)
@ -211,14 +211,14 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_with_no_deps(self, __):
self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
db = self.project.get_service('db')
self.assertEqual(len(db.containers()), 0)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_does_not_recreate_linked_containers(self, __):
self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['up', '-d', 'db'], None)
@ -234,7 +234,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(old_ids, new_ids)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_without_command(self, _):
self.command.base_dir = 'tests/fixtures/commands-composefile'
self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
@ -255,7 +255,7 @@ class CLITestCase(DockerClientTestCase):
[u'/bin/true'],
)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_entrypoint_overridden(self, _):
self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
name = 'service'
@ -270,7 +270,7 @@ class CLITestCase(DockerClientTestCase):
[u'/bin/echo', u'helloworld'],
)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_user_overridden(self, _):
self.command.base_dir = 'tests/fixtures/user-composefile'
name = 'service'
@ -281,7 +281,7 @@ class CLITestCase(DockerClientTestCase):
container = service.containers(stopped=True, one_off=True)[0]
self.assertEqual(user, container.get('Config.User'))
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_user_overridden_short_form(self, _):
self.command.base_dir = 'tests/fixtures/user-composefile'
name = 'service'
@ -292,7 +292,7 @@ class CLITestCase(DockerClientTestCase):
container = service.containers(stopped=True, one_off=True)[0]
self.assertEqual(user, container.get('Config.User'))
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_environement_overridden(self, _):
name = 'service'
self.command.base_dir = 'tests/fixtures/environment-composefile'
@ -312,7 +312,7 @@ class CLITestCase(DockerClientTestCase):
# make sure a value with a = don't crash out
self.assertEqual('moto=bobo', container.environment['allo'])
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_without_map_ports(self, __):
# create one off container
self.command.base_dir = 'tests/fixtures/ports-composefile'
@ -330,7 +330,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(port_random, None)
self.assertEqual(port_assigned, None)
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_map_ports(self, __):
# create one off container
@ -353,7 +353,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(port_range[0], "0.0.0.0:49153")
self.assertEqual(port_range[1], "0.0.0.0:49154")
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_explicitly_maped_ports(self, __):
# create one off container
@ -372,7 +372,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(port_short, "0.0.0.0:30000")
self.assertEqual(port_full, "0.0.0.0:30001")
@patch('dockerpty.start')
@mock.patch('dockerpty.start')
def test_run_service_with_explicitly_maped_ip_ports(self, __):
# create one off container
@ -508,7 +508,7 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['up', '-d'], None)
container = self.project.get_service('simple').get_container()
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def get_port(number, mock_stdout):
self.command.dispatch(['port', 'simple', str(number)], None)
return mock_stdout.getvalue().rstrip()
@ -525,7 +525,7 @@ class CLITestCase(DockerClientTestCase):
self.project.containers(service_names=['simple']),
key=attrgetter('name'))
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def get_port(number, mock_stdout, index=None):
if index is None:
self.command.dispatch(['port', 'simple', str(number)], None)
@ -547,7 +547,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(containers), 1)
self.assertIn("FOO=1", containers[0].get('Config.Env'))
@patch.dict(os.environ)
@mock.patch.dict(os.environ)
def test_home_and_env_var_in_volume_path(self):
os.environ['VOLUME_NAME'] = 'my-volume'
os.environ['HOME'] = '/tmp/home-dir'

View File

@ -7,10 +7,10 @@ import tempfile
from os import path
from docker.errors import APIError
from mock import patch
from six import StringIO
from six import text_type
from .. import mock
from .testcases import DockerClientTestCase
from compose import __version__
from compose.const import LABEL_CONTAINER_NUMBER
@ -460,7 +460,7 @@ class ServiceTest(DockerClientTestCase):
)
container = create_and_start_container(service)
container.wait()
self.assertIn('success', container.logs())
self.assertIn(b'success', container.logs())
self.assertEqual(len(self.client.images(name='composetest_test')), 1)
def test_start_container_uses_tagged_image_if_it_exists(self):
@ -473,7 +473,7 @@ class ServiceTest(DockerClientTestCase):
)
container = create_and_start_container(service)
container.wait()
self.assertIn('success', container.logs())
self.assertIn(b'success', container.logs())
def test_start_container_creates_ports(self):
service = self.create_service('web', ports=[8000])
@ -581,7 +581,7 @@ class ServiceTest(DockerClientTestCase):
service.scale(0)
self.assertEqual(len(service.containers()), 0)
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_scale_with_stopped_containers(self, mock_stdout):
"""
Given there are some stopped containers and scale is called with a
@ -608,7 +608,7 @@ class ServiceTest(DockerClientTestCase):
self.assertNotIn('Creating', captured_output)
self.assertIn('Starting', captured_output)
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_scale_with_stopped_containers_and_needing_creation(self, mock_stdout):
"""
Given there are some stopped containers and scale is called with a
@ -632,7 +632,7 @@ class ServiceTest(DockerClientTestCase):
self.assertIn('Creating', captured_output)
self.assertIn('Starting', captured_output)
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_scale_with_api_returns_errors(self, mock_stdout):
"""
Test that when scaling if the API returns an error, that error is handled
@ -642,7 +642,7 @@ class ServiceTest(DockerClientTestCase):
next_number = service._next_container_number()
service.create_container(number=next_number, quiet=True)
with patch(
with mock.patch(
'compose.container.Container.create',
side_effect=APIError(message="testing", response={}, explanation="Boom")):
@ -652,7 +652,7 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(service.containers()[0].is_running)
self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
@mock.patch('sys.stdout', new_callable=StringIO)
def test_scale_with_api_returns_unexpected_exception(self, mock_stdout):
"""
Test that when scaling if the API returns an error, that is not of type
@ -662,7 +662,7 @@ class ServiceTest(DockerClientTestCase):
next_number = service._next_container_number()
service.create_container(number=next_number, quiet=True)
with patch(
with mock.patch(
'compose.container.Container.create',
side_effect=ValueError("BOOM")):
with self.assertRaises(ValueError):
@ -671,7 +671,7 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual(len(service.containers()), 1)
self.assertTrue(service.containers()[0].is_running)
@patch('compose.service.log')
@mock.patch('compose.service.log')
def test_scale_with_desired_number_already_achieved(self, mock_log):
"""
Test that calling scale with a desired number that is equal to the
@ -694,7 +694,7 @@ class ServiceTest(DockerClientTestCase):
captured_output = mock_log.info.call_args[0]
self.assertIn('Desired container number already achieved', captured_output)
@patch('compose.service.log')
@mock.patch('compose.service.log')
def test_scale_with_custom_container_name_outputs_warning(self, mock_log):
"""
Test that calling scale on a service that has a custom container name
@ -815,7 +815,7 @@ class ServiceTest(DockerClientTestCase):
for k, v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.items():
self.assertEqual(env[k], v)
@patch.dict(os.environ)
@mock.patch.dict(os.environ)
def test_resolve_env(self):
os.environ['FILE_DEF'] = 'E1'
os.environ['FILE_DEF_EMPTY'] = 'E2'

View File

@ -3,9 +3,8 @@ from __future__ import unicode_literals
import os
import mock
from compose.cli import docker_client
from tests import mock
from tests import unittest

View File

@ -1,6 +1,8 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import six
from compose.cli import verbose_proxy
from tests import unittest
@ -8,7 +10,8 @@ from tests import unittest
class VerboseProxyTestCase(unittest.TestCase):
def test_format_call(self):
expected = "(u'arg1', True, key=u'value')"
prefix = '' if six.PY3 else 'u'
expected = "(%(p)s'arg1', True, key=%(p)s'value')" % dict(p=prefix)
actual = verbose_proxy.format_call(
("arg1", True),
{'key': 'value'})
@ -21,7 +24,7 @@ class VerboseProxyTestCase(unittest.TestCase):
self.assertEqual(expected, actual)
def test_format_return(self):
expected = "{u'Id': u'ok'}"
expected = repr({'Id': 'ok'})
actual = verbose_proxy.format_return({'Id': 'ok'}, 2)
self.assertEqual(expected, actual)

View File

@ -4,8 +4,8 @@ from __future__ import unicode_literals
import os
import docker
import mock
from .. import mock
from .. import unittest
from compose.cli.docopt_command import NoSuchCommand
from compose.cli.errors import UserError

View File

@ -1,9 +1,11 @@
from __future__ import print_function
import os
import shutil
import tempfile
from operator import itemgetter
import mock
from .. import mock
from .. import unittest
from compose.config import config
from compose.config.errors import ConfigurationError
@ -30,7 +32,7 @@ class ConfigTest(unittest.TestCase):
)
self.assertEqual(
sorted(service_dicts, key=lambda d: d['name']),
sorted(service_dicts, key=itemgetter('name')),
sorted([
{
'name': 'bar',
@ -41,7 +43,7 @@ class ConfigTest(unittest.TestCase):
'name': 'foo',
'image': 'busybox',
}
], key=lambda d: d['name'])
], key=itemgetter('name'))
)
def test_load_throws_error_when_not_dict(self):
@ -885,24 +887,24 @@ class ExtendsTest(unittest.TestCase):
other_config = {'web': {'links': ['db']}}
with mock.patch.object(config, 'load_yaml', return_value=other_config):
print load_config()
print(load_config())
with self.assertRaisesRegexp(ConfigurationError, 'volumes_from'):
other_config = {'web': {'volumes_from': ['db']}}
with mock.patch.object(config, 'load_yaml', return_value=other_config):
print load_config()
print(load_config())
with self.assertRaisesRegexp(ConfigurationError, 'net'):
other_config = {'web': {'net': 'container:db'}}
with mock.patch.object(config, 'load_yaml', return_value=other_config):
print load_config()
print(load_config())
other_config = {'web': {'net': 'host'}}
with mock.patch.object(config, 'load_yaml', return_value=other_config):
print load_config()
print(load_config())
def test_volume_path(self):
dicts = load_from_filename('tests/fixtures/volume-path/docker-compose.yml')

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals
import docker
import mock
from .. import mock
from .. import unittest
from compose.container import Container
from compose.container import get_container_name

View File

@ -3,6 +3,8 @@ from __future__ import unicode_literals
import os
import six
from .. import unittest
from compose.cli.log_printer import LogPrinter
@ -30,16 +32,17 @@ class LogPrinterTest(unittest.TestCase):
output = self.get_default_output()
self.assertIn('\033[', output)
@unittest.skipIf(six.PY3, "Only test unicode in python2")
def test_unicode(self):
glyph = u'\u2022'.encode('utf-8')
glyph = u'\u2022'
def reader(*args, **kwargs):
yield glyph + b'\n'
yield glyph + '\n'
container = MockContainer(reader)
output = run_log_printer([container])
self.assertIn(glyph, output)
self.assertIn(glyph, output.decode('utf-8'))
def run_log_printer(containers, monochrome=False):

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals
import docker
import mock
from .. import mock
from .. import unittest
from compose.const import LABEL_SERVICE
from compose.container import Container

View File

@ -2,9 +2,9 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import docker
import mock
from docker.utils import LogConfig
from .. import mock
from .. import unittest
from compose.const import LABEL_ONE_OFF
from compose.const import LABEL_PROJECT

View File

@ -8,38 +8,38 @@ from compose.cli.utils import split_buffer
class SplitBufferTest(unittest.TestCase):
def test_single_line_chunks(self):
def reader():
yield b'abc\n'
yield b'def\n'
yield b'ghi\n'
yield 'abc\n'
yield 'def\n'
yield 'ghi\n'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi\n'])
self.assert_produces(reader, ['abc\n', 'def\n', 'ghi\n'])
def test_no_end_separator(self):
def reader():
yield b'abc\n'
yield b'def\n'
yield b'ghi'
yield 'abc\n'
yield 'def\n'
yield 'ghi'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
self.assert_produces(reader, ['abc\n', 'def\n', 'ghi'])
def test_multiple_line_chunk(self):
def reader():
yield b'abc\ndef\nghi'
yield 'abc\ndef\nghi'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
self.assert_produces(reader, ['abc\n', 'def\n', 'ghi'])
def test_chunked_line(self):
def reader():
yield b'a'
yield b'b'
yield b'c'
yield b'\n'
yield b'd'
yield 'a'
yield 'b'
yield 'c'
yield '\n'
yield 'd'
self.assert_produces(reader, [b'abc\n', b'd'])
self.assert_produces(reader, ['abc\n', 'd'])
def test_preserves_unicode_sequences_within_lines(self):
string = u"a\u2022c\n".encode('utf-8')
string = u"a\u2022c\n"
def reader():
yield string
@ -47,7 +47,7 @@ class SplitBufferTest(unittest.TestCase):
self.assert_produces(reader, [string])
def assert_produces(self, reader, expectations):
split = split_buffer(reader(), b'\n')
split = split_buffer(reader(), '\n')
for (actual, expected) in zip(split, expectations):
self.assertEqual(type(actual), type(expected))

23
tox.ini
View File

@ -1,5 +1,5 @@
[tox]
envlist = py27,pre-commit
envlist = py27,py34,pre-commit
[testenv]
usedevelop=True
@ -7,7 +7,6 @@ passenv =
LD_LIBRARY_PATH
deps =
-rrequirements.txt
-rrequirements-dev.txt
commands =
nosetests -v {posargs}
flake8 compose tests setup.py
@ -20,6 +19,26 @@ commands =
pre-commit install
pre-commit run --all-files
[testenv:py26]
deps =
{[testenv]deps}
-rrequirements-dev-py2.txt
[testenv:py27]
deps = {[testenv:py26]deps}
[testenv:pypy]
deps = {[testenv:py26]deps}
[testenv:py33]
deps =
{[testenv]deps}
-rrequirements-dev-py3.txt
[testenv:py34]
deps = {[testenv:py33]deps}
[flake8]
# ignore line-length for now
ignore = E501,E203