From 95abc7f815e9c9579e3e58cc6b80bc7925613200 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2020 21:27:28 +0000 Subject: [PATCH 01/27] Bump pytest from 5.4.3 to 6.0.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.3 to 6.0.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.4.3...6.0.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b0be27d3b..1695e98e2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,6 @@ ddt==1.4.1 flake8==3.8.3 gitpython==3.1.7 mock==3.0.5 -pytest==5.4.3; python_version >= '3.5' +pytest==6.0.1; python_version >= '3.5' pytest==4.6.5; python_version < '3.5' pytest-cov==2.10.0 From e5edc787397c169ec7d0798595e56992d4723d8d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2020 21:27:50 +0000 Subject: [PATCH 02/27] Bump cryptography from 2.9.2 to 3.0 Bumps [cryptography](https://github.com/pyca/cryptography) from 2.9.2 to 3.0. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/2.9.2...3.0) Signed-off-by: dependabot-preview[bot] --- requirements-indirect.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-indirect.txt b/requirements-indirect.txt index 917ef6817..b417a22d9 100644 --- a/requirements-indirect.txt +++ b/requirements-indirect.txt @@ -3,7 +3,7 @@ appdirs==1.4.4 attrs==19.3.0 bcrypt==3.1.7 cffi==1.14.0 -cryptography==2.9.2 +cryptography==3.0 distlib==0.3.1 entrypoints==0.3 filelock==3.0.12 From 095e297dcbd30640b9afbcf4a57f12cb1e4bdde5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2020 21:28:34 +0000 Subject: [PATCH 03/27] Bump urllib3 from 1.25.9 to 1.25.10 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.9 to 1.25.10. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.9...1.25.10) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a0e90370..09fa58ece 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ python-dotenv==0.14.0 PyYAML==5.3.1 requests==2.24.0 texttable==1.6.2 -urllib3==1.25.9; python_version == '3.3' +urllib3==1.25.10; python_version == '3.3' websocket-client==0.57.0 From 4d3d9f64b965af66662a934fc07af430e83ce524 Mon Sep 17 00:00:00 2001 From: alexrecuenco Date: Mon, 24 Feb 2020 13:24:57 +0100 Subject: [PATCH 04/27] Removed Python2 support Closes: #6890 Some remarks, - `# coding ... utf-8` statements are not needed - isdigit on strings instead of a try-catch. - Default opening mode is read, so we can do `open()` without the `'r'` everywhere - Removed inheritinng from `object` class, it isn't necessary in python3. - `super(ClassName, self)` can now be replaced with `super()` - Use of itertools and `chain` on a couple places dealing with sets. - Used the operator module instead of lambdas when warranted `itemgetter(0)` instead of `lambda x: x[0]` `attrgetter('name')` instead of `lambda x: x.name` - `sorted` returns a list, so no need to use `list(sorted(...))` - Removed `dict()` using dictionary comprehensions whenever possible - Attempted to remove python3.2 support Signed-off-by: alexrecuenco --- .pre-commit-config.yaml | 6 ++ compose/__init__.py | 1 - compose/cli/colors.py | 8 +-- compose/cli/command.py | 13 ++-- compose/cli/docopt_command.py | 4 +- compose/cli/errors.py | 4 +- compose/cli/formatter.py | 17 ++--- compose/cli/log_printer.py | 11 +-- compose/cli/main.py | 13 ++-- compose/cli/utils.py | 13 +--- compose/cli/verbose_proxy.py | 8 +-- compose/config/config.py | 51 +++++++------ compose/config/environment.py | 16 ++--- compose/config/errors.py | 6 +- compose/config/interpolation.py | 26 +++---- compose/config/serialize.py | 2 +- compose/config/sort_services.py | 2 +- compose/config/types.py | 25 +++---- compose/config/validation.py | 8 +-- compose/container.py | 14 ++-- compose/errors.py | 6 +- compose/network.py | 21 +++--- compose/parallel.py | 8 +-- compose/progress_stream.py | 10 +-- compose/project.py | 34 ++++----- compose/service.py | 72 +++++++++---------- compose/timeparse.py | 19 +++-- compose/utils.py | 6 +- compose/volume.py | 21 +++--- .../migrate-compose-file-v1-to-v2.py | 2 +- docker-compose_darwin.spec | 2 +- requirements.txt | 2 - script/release/utils.py | 4 +- setup.py | 4 +- tests/acceptance/cli_test.py | 23 +++--- tests/acceptance/context_test.py | 1 - tests/helpers.py | 2 +- tests/integration/environment_test.py | 4 +- tests/integration/project_test.py | 60 ++++++++-------- tests/integration/resilience_test.py | 2 +- tests/integration/service_test.py | 28 ++++---- tests/integration/state_test.py | 46 ++++++------ tests/integration/volume_test.py | 2 +- tests/unit/cli/command_test.py | 3 +- tests/unit/cli/docker_client_test.py | 16 ++--- tests/unit/cli/errors_test.py | 6 +- tests/unit/cli/formatter_test.py | 4 +- tests/unit/cli/log_printer_test.py | 10 +-- tests/unit/cli/main_test.py | 10 +-- tests/unit/cli_test.py | 1 - tests/unit/config/config_test.py | 13 ++-- tests/unit/config/environment_test.py | 1 - tests/unit/config/interpolation_test.py | 3 +- tests/unit/config/sort_services_test.py | 2 +- tests/unit/config/types_test.py | 6 +- tests/unit/progress_stream_test.py | 4 +- tests/unit/project_test.py | 3 +- tests/unit/service_test.py | 6 +- tests/unit/split_buffer_test.py | 2 +- tests/unit/utils_test.py | 13 ++-- tests/unit/volume_test.py | 2 +- 61 files changed, 350 insertions(+), 382 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2aeb014a..05cd52026 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,9 @@ language_version: 'python3.7' args: - --py3-plus +- repo: https://github.com/asottile/pyupgrade + rev: v2.1.0 + hooks: + - id: pyupgrade + args: + - --py3-plus diff --git a/compose/__init__.py b/compose/__init__.py index 00afb07c0..76e27d25f 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,2 +1 @@ - __version__ = '1.27.0dev' diff --git a/compose/cli/colors.py b/compose/cli/colors.py index c6a869bf5..a4983a9f5 100644 --- a/compose/cli/colors.py +++ b/compose/cli/colors.py @@ -14,16 +14,16 @@ NAMES = [ def get_pairs(): for i, name in enumerate(NAMES): - yield(name, str(30 + i)) - yield('intense_' + name, str(30 + i) + ';1') + yield (name, str(30 + i)) + yield ('intense_' + name, str(30 + i) + ';1') def ansi(code): - return '\033[{0}m'.format(code) + return '\033[{}m'.format(code) def ansi_color(code, s): - return '{0}{1}{2}'.format(ansi(code), s, ansi(0)) + return '{}{}{}'.format(ansi(code), s, ansi(0)) def make_color_fn(code): diff --git a/compose/cli/command.py b/compose/cli/command.py index e907a05cc..8882727ba 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -147,15 +147,17 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, def execution_context_labels(config_details, environment_file): extra_labels = [ - '{0}={1}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir)) + '{}={}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir)) ] if not use_config_from_stdin(config_details): - extra_labels.append('{0}={1}'.format(LABEL_CONFIG_FILES, config_files_label(config_details))) + extra_labels.append('{}={}'.format(LABEL_CONFIG_FILES, config_files_label(config_details))) if environment_file is not None: - extra_labels.append('{0}={1}'.format(LABEL_ENVIRONMENT_FILE, - os.path.normpath(environment_file))) + extra_labels.append('{}={}'.format( + LABEL_ENVIRONMENT_FILE, + os.path.normpath(environment_file)) + ) return extra_labels @@ -168,7 +170,8 @@ def use_config_from_stdin(config_details): def config_files_label(config_details): return ",".join( - map(str, (os.path.normpath(c.filename) for c in config_details.config_files))) + os.path.normpath(c.filename) for c in config_details.config_files + ) def get_project_name(working_dir, project_name=None, environment=None): diff --git a/compose/cli/docopt_command.py b/compose/cli/docopt_command.py index 856c92348..d0ba7f670 100644 --- a/compose/cli/docopt_command.py +++ b/compose/cli/docopt_command.py @@ -11,7 +11,7 @@ def docopt_full_help(docstring, *args, **kwargs): raise SystemExit(docstring) -class DocoptDispatcher(object): +class DocoptDispatcher: def __init__(self, command_class, options): self.command_class = command_class @@ -50,7 +50,7 @@ def get_handler(command_class, command): class NoSuchCommand(Exception): def __init__(self, command, supercommand): - super(NoSuchCommand, self).__init__("No such command: %s" % command) + super().__init__("No such command: %s" % command) self.command = command self.supercommand = supercommand diff --git a/compose/cli/errors.py b/compose/cli/errors.py index d1a47f078..a807c7d1c 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -26,11 +26,9 @@ class UserError(Exception): def __init__(self, msg): self.msg = dedent(msg).strip() - def __unicode__(self): + def __str__(self): return self.msg - __str__ = __unicode__ - class ConnectionError(Exception): pass diff --git a/compose/cli/formatter.py b/compose/cli/formatter.py index a59f0742c..ff81ee651 100644 --- a/compose/cli/formatter.py +++ b/compose/cli/formatter.py @@ -1,15 +1,10 @@ import logging -import shutil +from shutil import get_terminal_size import texttable from compose.cli import colors -if hasattr(shutil, "get_terminal_size"): - from shutil import get_terminal_size -else: - from backports.shutil_get_terminal_size import get_terminal_size - def get_tty_width(): try: @@ -45,15 +40,15 @@ class ConsoleWarningFormatter(logging.Formatter): def get_level_message(self, record): separator = ': ' - if record.levelno == logging.WARNING: - return colors.yellow(record.levelname) + separator - if record.levelno == logging.ERROR: + if record.levelno >= logging.ERROR: return colors.red(record.levelname) + separator + if record.levelno >= logging.WARNING: + return colors.yellow(record.levelname) + separator return '' def format(self, record): if isinstance(record.msg, bytes): record.msg = record.msg.decode('utf-8') - message = super(ConsoleWarningFormatter, self).format(record) - return '{0}{1}'.format(self.get_level_message(record), message) + message = super().format(record) + return '{}{}'.format(self.get_level_message(record), message) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index 100f3a825..cd9f73c2c 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -2,6 +2,7 @@ import _thread as thread import sys from collections import namedtuple from itertools import cycle +from operator import attrgetter from queue import Empty from queue import Queue from threading import Thread @@ -13,7 +14,7 @@ from compose.cli.signals import ShutdownException from compose.utils import split_buffer -class LogPresenter(object): +class LogPresenter: def __init__(self, prefix_width, color_func): self.prefix_width = prefix_width @@ -50,7 +51,7 @@ def max_name_width(service_names, max_index_width=3): return max(len(name) for name in service_names) + max_index_width -class LogPrinter(object): +class LogPrinter: """Print logs from many containers to a single output stream.""" def __init__(self, @@ -133,7 +134,7 @@ def build_thread_map(initial_containers, presenters, thread_args): # Container order is unspecified, so they are sorted by name in order to make # container:presenter (log color) assignment deterministic when given a list of containers # with the same names. - for container in sorted(initial_containers, key=lambda c: c.name) + for container in sorted(initial_containers, key=attrgetter('name')) } @@ -194,9 +195,9 @@ def build_log_generator(container, log_args): def wait_on_exit(container): try: exit_code = container.wait() - return "%s exited with code %s\n" % (container.name, exit_code) + return "{} exited with code {}\n".format(container.name, exit_code) except APIError as e: - return "Unexpected API error for %s (HTTP code %s)\nResponse body:\n%s\n" % ( + return "Unexpected API error for {} (HTTP code {})\nResponse body:\n{}\n".format( container.name, e.response.status_code, e.response.text or '[empty]' ) diff --git a/compose/cli/main.py b/compose/cli/main.py index 1de8a774f..8809318f7 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -73,7 +73,7 @@ def main(): log.error(e.msg) sys.exit(1) except BuildError as e: - log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason)) + log.error("Service '{}' failed to build: {}".format(e.service.name, e.reason)) sys.exit(1) except StreamOutputError as e: log.error(e) @@ -175,7 +175,7 @@ def parse_doc_section(name, source): return [s.strip() for s in pattern.findall(source)] -class TopLevelCommand(object): +class TopLevelCommand: """Define and run multi-container applications with Docker. Usage: @@ -546,7 +546,7 @@ class TopLevelCommand(object): key=attrgetter('name')) if options['--quiet']: - for image in set(c.image for c in containers): + for image in {c.image for c in containers}: print(image.split(':')[1]) return @@ -1130,7 +1130,7 @@ def compute_service_exit_code(exit_value_from, attached_containers): attached_containers)) if not candidates: log.error( - 'No containers matching the spec "{0}" ' + 'No containers matching the spec "{}" ' 'were run.'.format(exit_value_from) ) return 2 @@ -1453,10 +1453,7 @@ def call_docker(args, dockeropts, environment): args = [executable_path] + tls_options + args log.debug(" ".join(map(pipes.quote, args))) - filtered_env = {} - for k, v in environment.items(): - if v is not None: - filtered_env[k] = environment[k] + filtered_env = {k: v for k, v in environment.items() if v is not None} return subprocess.call(args, env=filtered_env) diff --git a/compose/cli/utils.py b/compose/cli/utils.py index b91ab6a13..6a4615a96 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -11,13 +11,6 @@ import docker import compose from ..const import IS_WINDOWS_PLATFORM -# WindowsError is not defined on non-win32 platforms. Avoid runtime errors by -# defining it as OSError (its parent class) if missing. -try: - WindowsError -except NameError: - WindowsError = OSError - def yesno(prompt, default=None): """ @@ -58,7 +51,7 @@ def call_silently(*args, **kwargs): with open(os.devnull, 'w') as shutup: try: return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs) - except WindowsError: + except OSError: # On Windows, subprocess.call() can still raise exceptions. Normalize # to POSIXy behaviour by returning a nonzero exit code. return 1 @@ -120,7 +113,7 @@ def generate_user_agent(): try: p_system = platform.system() p_release = platform.release() - except IOError: + except OSError: pass else: parts.append("{}/{}".format(p_system, p_release)) @@ -133,7 +126,7 @@ def human_readable_file_size(size): if order >= len(suffixes): order = len(suffixes) - 1 - return '{0:.4g} {1}'.format( + return '{:.4g} {}'.format( size / pow(10, order * 3), suffixes[order] ) diff --git a/compose/cli/verbose_proxy.py b/compose/cli/verbose_proxy.py index 1d2f28b5c..c9340c4e0 100644 --- a/compose/cli/verbose_proxy.py +++ b/compose/cli/verbose_proxy.py @@ -6,13 +6,13 @@ from itertools import chain def format_call(args, kwargs): args = (repr(a) for a in args) - kwargs = ("{0!s}={1!r}".format(*item) for item in kwargs.items()) - return "({0})".format(", ".join(chain(args, kwargs))) + kwargs = ("{!s}={!r}".format(*item) for item in kwargs.items()) + return "({})".format(", ".join(chain(args, kwargs))) def format_return(result, max_lines): if isinstance(result, (list, tuple, set)): - return "({0} with {1} items)".format(type(result).__name__, len(result)) + return "({} with {} items)".format(type(result).__name__, len(result)) if result: lines = pprint.pformat(result).split('\n') @@ -22,7 +22,7 @@ def format_return(result, max_lines): return result -class VerboseProxy(object): +class VerboseProxy: """Proxy all function calls to another class and log method name, arguments and return values for each call. """ diff --git a/compose/config/config.py b/compose/config/config.py index c7cc72404..8f5790215 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1,12 +1,13 @@ import functools -import io import logging import os import re import string import sys from collections import namedtuple +from itertools import chain from operator import attrgetter +from operator import itemgetter import yaml from cached_property import cached_property @@ -166,7 +167,7 @@ class ConfigDetails(namedtuple('_ConfigDetails', 'working_dir config_files envir def __new__(cls, working_dir, config_files, environment=None): if environment is None: environment = Environment.from_env_file(working_dir) - return super(ConfigDetails, cls).__new__( + return super().__new__( cls, working_dir, config_files, environment ) @@ -315,8 +316,8 @@ def validate_config_version(config_files): if main_file.version != next_file.version: raise ConfigurationError( - "Version mismatch: file {0} specifies version {1} but " - "extension file {2} uses version {3}".format( + "Version mismatch: file {} specifies version {} but " + "extension file {} uses version {}".format( main_file.filename, main_file.version, next_file.filename, @@ -595,7 +596,7 @@ def process_config_file(config_file, environment, service_name=None, interpolate return config_file -class ServiceExtendsResolver(object): +class ServiceExtendsResolver: def __init__(self, service_config, config_file, environment, already_seen=None): self.service_config = service_config self.working_dir = service_config.working_dir @@ -703,7 +704,7 @@ def resolve_build_args(buildargs, environment): def validate_extended_service_dict(service_dict, filename, service): - error_prefix = "Cannot extend service '%s' in %s:" % (service, filename) + error_prefix = "Cannot extend service '{}' in {}:".format(service, filename) if 'links' in service_dict: raise ConfigurationError( @@ -826,9 +827,9 @@ def process_ports(service_dict): def process_depends_on(service_dict): if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict): - service_dict['depends_on'] = dict([ - (svc, {'condition': 'service_started'}) for svc in service_dict['depends_on'] - ]) + service_dict['depends_on'] = { + svc: {'condition': 'service_started'} for svc in service_dict['depends_on'] + } return service_dict @@ -1071,9 +1072,9 @@ def merge_service_dicts(base, override, version): def merge_unique_items_lists(base, override): - override = [str(o) for o in override] - base = [str(b) for b in base] - return sorted(set().union(base, override)) + override = (str(o) for o in override) + base = (str(b) for b in base) + return sorted(set(chain(base, override))) def merge_healthchecks(base, override): @@ -1086,9 +1087,7 @@ def merge_healthchecks(base, override): def merge_ports(md, base, override): def parse_sequence_func(seq): - acc = [] - for item in seq: - acc.extend(ServicePort.parse(item)) + acc = [s for item in seq for s in ServicePort.parse(item)] return to_mapping(acc, 'merge_field') field = 'ports' @@ -1098,7 +1097,7 @@ def merge_ports(md, base, override): merged = parse_sequence_func(md.base.get(field, [])) merged.update(parse_sequence_func(md.override.get(field, []))) - md[field] = [item for item in sorted(merged.values(), key=lambda x: x.target)] + md[field] = [item for item in sorted(merged.values(), key=attrgetter("target"))] def merge_build(output, base, override): @@ -1170,8 +1169,8 @@ def merge_reservations(base, override): def merge_unique_objects_lists(base, override): - result = dict((json_hash(i), i) for i in base + override) - return [i[1] for i in sorted([(k, v) for k, v in result.items()], key=lambda x: x[0])] + result = {json_hash(i): i for i in base + override} + return [i[1] for i in sorted(((k, v) for k, v in result.items()), key=itemgetter(0))] def merge_blkio_config(base, override): @@ -1179,11 +1178,11 @@ def merge_blkio_config(base, override): md.merge_scalar('weight') def merge_blkio_limits(base, override): - index = dict((b['path'], b) for b in base) - for o in override: - index[o['path']] = o + get_path = itemgetter('path') + index = {get_path(b): b for b in base} + index.update((get_path(o), o) for o in override) - return sorted(list(index.values()), key=lambda x: x['path']) + return sorted(index.values(), key=get_path) for field in [ "device_read_bps", "device_read_iops", "device_write_bps", @@ -1304,7 +1303,7 @@ def resolve_volume_path(working_dir, volume): if host_path.startswith('.'): host_path = expand_path(working_dir, host_path) host_path = os.path.expanduser(host_path) - return u"{}:{}{}".format(host_path, container_path, (':' + mode if mode else '')) + return "{}:{}{}".format(host_path, container_path, (':' + mode if mode else '')) return container_path @@ -1447,13 +1446,13 @@ def has_uppercase(name): def load_yaml(filename, encoding=None, binary=True): try: - with io.open(filename, 'rb' if binary else 'r', encoding=encoding) as fh: + with open(filename, 'rb' if binary else 'r', encoding=encoding) as fh: return yaml.safe_load(fh) - except (IOError, yaml.YAMLError, UnicodeDecodeError) as e: + except (OSError, yaml.YAMLError, UnicodeDecodeError) as e: if encoding is None: # Sometimes the user's locale sets an encoding that doesn't match # the YAML files. Im such cases, retry once with the "default" # UTF-8 encoding return load_yaml(filename, encoding='utf-8-sig', binary=False) error_name = getattr(e, '__module__', '') + '.' + e.__class__.__name__ - raise ConfigurationError(u"{}: {}".format(error_name, e)) + raise ConfigurationError("{}: {}".format(error_name, e)) diff --git a/compose/config/environment.py b/compose/config/environment.py index 4526d0b3e..1780851fd 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -43,7 +43,7 @@ def env_vars_from_file(filename, interpolate=True): class Environment(dict): def __init__(self, *args, **kwargs): - super(Environment, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.missing_keys = [] self.silent = False @@ -81,11 +81,11 @@ class Environment(dict): def __getitem__(self, key): try: - return super(Environment, self).__getitem__(key) + return super().__getitem__(key) except KeyError: if IS_WINDOWS_PLATFORM: try: - return super(Environment, self).__getitem__(key.upper()) + return super().__getitem__(key.upper()) except KeyError: pass if not self.silent and key not in self.missing_keys: @@ -98,20 +98,20 @@ class Environment(dict): return "" def __contains__(self, key): - result = super(Environment, self).__contains__(key) + result = super().__contains__(key) if IS_WINDOWS_PLATFORM: return ( - result or super(Environment, self).__contains__(key.upper()) + result or super().__contains__(key.upper()) ) return result def get(self, key, *args, **kwargs): if IS_WINDOWS_PLATFORM: - return super(Environment, self).get( + return super().get( key, - super(Environment, self).get(key.upper(), *args, **kwargs) + super().get(key.upper(), *args, **kwargs) ) - return super(Environment, self).get(key, *args, **kwargs) + return super().get(key, *args, **kwargs) def get_boolean(self, key): # Convert a value to a boolean using "common sense" rules. diff --git a/compose/config/errors.py b/compose/config/errors.py index 7db079e9c..b66433a79 100644 --- a/compose/config/errors.py +++ b/compose/config/errors.py @@ -1,5 +1,3 @@ - - VERSION_EXPLANATION = ( 'You might be seeing this error because you\'re using the wrong Compose file version. ' 'Either specify a supported version (e.g "2.2" or "3.3") and place ' @@ -40,7 +38,7 @@ class CircularReference(ConfigurationError): class ComposeFileNotFound(ConfigurationError): def __init__(self, supported_filenames): - super(ComposeFileNotFound, self).__init__(""" + super().__init__(""" Can't find a suitable configuration file in this directory or any parent. Are you in the right directory? @@ -51,7 +49,7 @@ class ComposeFileNotFound(ConfigurationError): class DuplicateOverrideFileFound(ConfigurationError): def __init__(self, override_filenames): self.override_filenames = override_filenames - super(DuplicateOverrideFileFound, self).__init__( + super().__init__( "Multiple override files found: {}. You may only use a single " "override file.".format(", ".join(override_filenames)) ) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index bfa6a56c9..71e78bbaa 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -11,7 +11,7 @@ from compose.utils import parse_nanoseconds_int log = logging.getLogger(__name__) -class Interpolator(object): +class Interpolator: def __init__(self, templater, mapping): self.templater = templater @@ -31,15 +31,15 @@ def interpolate_environment_variables(version, config, section, environment): interpolator = Interpolator(TemplateWithDefaults, environment) def process_item(name, config_dict): - return dict( - (key, interpolate_value(name, key, val, section, interpolator)) + return { + key: interpolate_value(name, key, val, section, interpolator) for key, val in (config_dict or {}).items() - ) + } - return dict( - (name, process_item(name, config_dict or {})) + return { + name: process_item(name, config_dict or {}) for name, config_dict in config.items() - ) + } def get_config_path(config_key, section, name): @@ -75,10 +75,10 @@ def recursive_interpolate(obj, interpolator, config_path): if isinstance(obj, str): return converter.convert(config_path, interpolator.interpolate(obj)) if isinstance(obj, dict): - return dict( - (key, recursive_interpolate(val, interpolator, append(config_path, key))) - for (key, val) in obj.items() - ) + return { + key: recursive_interpolate(val, interpolator, append(config_path, key)) + for key, val in obj.items() + } if isinstance(obj, list): return [recursive_interpolate(val, interpolator, config_path) for val in obj] return converter.convert(config_path, obj) @@ -135,7 +135,7 @@ class TemplateWithDefaults(Template): val = mapping[named] if isinstance(val, bytes): val = val.decode('utf-8') - return '%s' % (val,) + return '{}'.format(val) if mo.group('escaped') is not None: return self.delimiter if mo.group('invalid') is not None: @@ -224,7 +224,7 @@ def to_microseconds(v): return int(parse_nanoseconds_int(v) / 1000) -class ConversionMap(object): +class ConversionMap: map = { service_path('blkio_config', 'weight'): to_int, service_path('blkio_config', 'weight_device', 'weight'): to_int, diff --git a/compose/config/serialize.py b/compose/config/serialize.py index e41e1ba4f..2dd2c47f1 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -104,7 +104,7 @@ def serialize_ns_time_value(value): result = (int(value), stage[1]) else: break - return '{0}{1}'.format(*result) + return '{}{}'.format(*result) def denormalize_service_dict(service_dict, version, image_digest=None): diff --git a/compose/config/sort_services.py b/compose/config/sort_services.py index 65953891f..0a7eb2b4f 100644 --- a/compose/config/sort_services.py +++ b/compose/config/sort_services.py @@ -21,7 +21,7 @@ def get_source_name_from_network_mode(network_mode, source_type): def get_service_names(links): - return [link.split(':')[0] for link in links] + return [link.split(':', 1)[0] for link in links] def get_service_names_from_volumes_from(volumes_from): diff --git a/compose/config/types.py b/compose/config/types.py index 0c654fa6f..f52b56541 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -146,7 +146,7 @@ def normpath(path, win_host=False): return path -class MountSpec(object): +class MountSpec: options_map = { 'volume': { 'nocopy': 'no_copy' @@ -338,9 +338,9 @@ class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid return self.source def repr(self): - return dict( - [(k, v) for k, v in zip(self._fields, self) if v is not None] - ) + return { + k: v for k, v in zip(self._fields, self) if v is not None + } class ServiceSecret(ServiceConfigBase): @@ -362,10 +362,7 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext if published: if isinstance(published, str) and '-' in published: # "x-y:z" format a, b = published.split('-', 1) - try: - int(a) - int(b) - except ValueError: + if not a.isdigit() or not b.isdigit(): raise ConfigurationError('Invalid published port: {}'.format(published)) else: try: @@ -373,7 +370,7 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext except ValueError: raise ConfigurationError('Invalid published port: {}'.format(published)) - return super(ServicePort, cls).__new__( + return super().__new__( cls, target, published, *args, **kwargs ) @@ -422,9 +419,9 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext return (self.target, self.published, self.external_ip, self.protocol) def repr(self): - return dict( - [(k, v) for k, v in zip(self._fields, self) if v is not None] - ) + return { + k: v for k, v in zip(self._fields, self) if v is not None + } def legacy_repr(self): return normalize_port_dict(self.repr()) @@ -484,9 +481,9 @@ class SecurityOpt(namedtuple('_SecurityOpt', 'value src_file')): if con[0] == 'seccomp' and con[1] != 'unconfined': try: - with open(unquote_path(con[1]), 'r') as f: + with open(unquote_path(con[1])) as f: seccomp_data = json.load(f) - except (IOError, ValueError) as e: + except (OSError, ValueError) as e: raise ConfigurationError('Error reading seccomp profile: {}'.format(e)) return cls( 'seccomp={}'.format(json.dumps(seccomp_data)), con[1] diff --git a/compose/config/validation.py b/compose/config/validation.py index 61a3370db..2b46cafe4 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -100,7 +100,7 @@ def match_named_volumes(service_dict, project_volumes): for volume_spec in service_volumes: if volume_spec.is_named_volume and volume_spec.external not in project_volumes: raise ConfigurationError( - 'Named volume "{0}" is used in service "{1}" but no' + 'Named volume "{}" is used in service "{}" but no' ' declaration was found in the volumes section.'.format( volume_spec.repr(), service_dict.get('name') ) @@ -508,13 +508,13 @@ def load_jsonschema(version): filename = os.path.join( get_schema_path(), - "config_schema_{0}.json".format(suffix)) + "config_schema_{}.json".format(suffix)) if not os.path.exists(filename): raise ConfigurationError( 'Version in "{}" is unsupported. {}' .format(filename, VERSION_EXPLANATION)) - with open(filename, "r") as fh: + with open(filename) as fh: return json.load(fh) @@ -534,7 +534,7 @@ def handle_errors(errors, format_error_func, filename): gone wrong. Process each error and pull out relevant information and re-write helpful error messages that are relevant. """ - errors = list(sorted(errors, key=str)) + errors = sorted(errors, key=str) if not errors: return diff --git a/compose/container.py b/compose/container.py index 7160a982e..00626b619 100644 --- a/compose/container.py +++ b/compose/container.py @@ -12,7 +12,7 @@ from .utils import truncate_id from .version import ComposeVersion -class Container(object): +class Container: """ Represents a Docker container, constructed from the output of GET /containers/:id:/json. @@ -78,8 +78,8 @@ class Container(object): @property def name_without_project(self): - if self.name.startswith('{0}_{1}'.format(self.project, self.service)): - return '{0}_{1}'.format(self.service, self.number if self.number is not None else self.slug) + if self.name.startswith('{}_{}'.format(self.project, self.service)): + return '{}_{}'.format(self.service, self.number if self.number is not None else self.slug) else: return self.name @@ -91,7 +91,7 @@ class Container(object): number = self.labels.get(LABEL_CONTAINER_NUMBER) if not number: - raise ValueError("Container {0} does not have a {1} label".format( + raise ValueError("Container {} does not have a {} label".format( self.short_id, LABEL_CONTAINER_NUMBER)) return int(number) @@ -224,7 +224,7 @@ class Container(object): return reduce(get_value, key.split('.'), self.dictionary) def get_local_port(self, port, protocol='tcp'): - port = self.ports.get("%s/%s" % (port, protocol)) + port = self.ports.get("{}/{}".format(port, protocol)) return "{HostIp}:{HostPort}".format(**port[0]) if port else None def get_mount(self, mount_dest): @@ -266,7 +266,7 @@ class Container(object): """ if not self.name.startswith(self.short_id): self.client.rename( - self.id, '{0}_{1}'.format(self.short_id, self.name) + self.id, '{}_{}'.format(self.short_id, self.name) ) def inspect_if_not_inspected(self): @@ -309,7 +309,7 @@ class Container(object): ) def __repr__(self): - return '' % (self.name, self.id[:6]) + return ''.format(self.name, self.id[:6]) def __eq__(self, other): if type(self) != type(other): diff --git a/compose/errors.py b/compose/errors.py index 530656173..d4fead251 100644 --- a/compose/errors.py +++ b/compose/errors.py @@ -1,5 +1,3 @@ - - class OperationFailedError(Exception): def __init__(self, reason): self.msg = reason @@ -17,14 +15,14 @@ class HealthCheckException(Exception): class HealthCheckFailed(HealthCheckException): def __init__(self, container_id): - super(HealthCheckFailed, self).__init__( + super().__init__( 'Container "{}" is unhealthy.'.format(container_id) ) class NoHealthCheckConfigured(HealthCheckException): def __init__(self, service_name): - super(NoHealthCheckConfigured, self).__init__( + super().__init__( 'Service "{}" is missing a healthcheck configuration'.format( service_name ) diff --git a/compose/network.py b/compose/network.py index bc3ade168..a67c703c0 100644 --- a/compose/network.py +++ b/compose/network.py @@ -1,6 +1,7 @@ import logging import re from collections import OrderedDict +from operator import itemgetter from docker.errors import NotFound from docker.types import IPAMConfig @@ -24,7 +25,7 @@ OPTS_EXCEPTIONS = [ ] -class Network(object): +class Network: def __init__(self, client, project, name, driver=None, driver_opts=None, ipam=None, external=False, internal=False, enable_ipv6=False, labels=None, custom_name=False): @@ -51,7 +52,7 @@ class Network(object): try: self.inspect() log.debug( - 'Network {0} declared as external. No new ' + 'Network {} declared as external. No new ' 'network will be created.'.format(self.name) ) except NotFound: @@ -107,7 +108,7 @@ class Network(object): def legacy_full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format( + return '{}_{}'.format( re.sub(r'[_-]', '', self.project), self.name ) @@ -115,7 +116,7 @@ class Network(object): def full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format(self.project, self.name) + return '{}_{}'.format(self.project, self.name) @property def true_name(self): @@ -167,7 +168,7 @@ def create_ipam_config_from_dict(ipam_dict): class NetworkConfigChangedError(ConfigurationError): def __init__(self, net_name, property_name): - super(NetworkConfigChangedError, self).__init__( + super().__init__( 'Network "{}" needs to be recreated - {} has changed'.format( net_name, property_name ) @@ -258,7 +259,7 @@ def build_networks(name, config_data, client): return networks -class ProjectNetworks(object): +class ProjectNetworks: def __init__(self, networks, use_networking): self.networks = networks or {} @@ -299,10 +300,10 @@ def get_network_defs_for_service(service_dict): if 'network_mode' in service_dict: return {} networks = service_dict.get('networks', {'default': None}) - return dict( - (net, (config or {})) + return { + net: (config or {}) for net, config in networks.items() - ) + } def get_network_names_for_service(service_dict): @@ -328,4 +329,4 @@ def get_networks(service_dict, network_definitions): else: # Ensure Compose will pick a consistent primary network if no # priority is set - return OrderedDict(sorted(networks.items(), key=lambda t: t[0])) + return OrderedDict(sorted(networks.items(), key=itemgetter(0))) diff --git a/compose/parallel.py b/compose/parallel.py index 15c3ad572..acf9e4a84 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -25,7 +25,7 @@ log = logging.getLogger(__name__) STOP = object() -class GlobalLimit(object): +class GlobalLimit: """Simple class to hold a global semaphore limiter for a project. This class should be treated as a singleton that is instantiated when the project is. """ @@ -114,7 +114,7 @@ def _no_deps(x): return [] -class State(object): +class State: """ Holds the state of a partially-complete parallel operation. @@ -136,7 +136,7 @@ class State(object): return set(self.objects) - self.started - self.finished - self.failed -class NoLimit(object): +class NoLimit: def __enter__(self): pass @@ -252,7 +252,7 @@ class UpstreamError(Exception): pass -class ParallelStreamWriter(object): +class ParallelStreamWriter: """Write out messages for operations happening in parallel. Each operation has its own line, and ANSI code characters are used diff --git a/compose/progress_stream.py b/compose/progress_stream.py index 8792ff287..3c03cc4b5 100644 --- a/compose/progress_stream.py +++ b/compose/progress_stream.py @@ -79,19 +79,19 @@ def print_output_event(event, stream, is_terminal): status = event.get('status', '') if 'progress' in event: - write_to_stream("%s %s%s" % (status, event['progress'], terminator), stream) + write_to_stream("{} {}{}".format(status, event['progress'], terminator), stream) elif 'progressDetail' in event: detail = event['progressDetail'] total = detail.get('total') if 'current' in detail and total: percentage = float(detail['current']) / float(total) * 100 - write_to_stream('%s (%.1f%%)%s' % (status, percentage, terminator), stream) + write_to_stream('{} ({:.1f}%){}'.format(status, percentage, terminator), stream) else: - write_to_stream('%s%s' % (status, terminator), stream) + write_to_stream('{}{}'.format(status, terminator), stream) elif 'stream' in event: - write_to_stream("%s%s" % (event['stream'], terminator), stream) + write_to_stream("{}{}".format(event['stream'], terminator), stream) else: - write_to_stream("%s%s\n" % (status, terminator), stream) + write_to_stream("{}{}\n".format(status, terminator), stream) def get_digest_from_pull(events): diff --git a/compose/project.py b/compose/project.py index 5a03b30ca..0ae5721bc 100644 --- a/compose/project.py +++ b/compose/project.py @@ -55,16 +55,16 @@ class OneOffFilter(enum.Enum): @classmethod def update_labels(cls, value, labels): if value == cls.only: - labels.append('{0}={1}'.format(LABEL_ONE_OFF, "True")) + labels.append('{}={}'.format(LABEL_ONE_OFF, "True")) elif value == cls.exclude: - labels.append('{0}={1}'.format(LABEL_ONE_OFF, "False")) + labels.append('{}={}'.format(LABEL_ONE_OFF, "False")) elif value == cls.include: pass else: raise ValueError("Invalid value for one_off: {}".format(repr(value))) -class Project(object): +class Project: """ A collection of services. """ @@ -80,7 +80,7 @@ class Project(object): name = self.name if legacy: name = re.sub(r'[_-]', '', name) - labels = ['{0}={1}'.format(LABEL_PROJECT, name)] + labels = ['{}={}'.format(LABEL_PROJECT, name)] OneOffFilter.update_labels(one_off, labels) return labels @@ -549,10 +549,10 @@ class Project(object): 'action': event['status'], 'id': event['Actor']['ID'], 'service': container_attrs.get(LABEL_SERVICE), - 'attributes': dict([ - (k, v) for k, v in container_attrs.items() + 'attributes': { + k: v for k, v in container_attrs.items() if not k.startswith('com.docker.compose.') - ]), + }, 'container': container, } @@ -812,7 +812,7 @@ class Project(object): return if remove_orphans: for ctnr in orphans: - log.info('Removing orphan container "{0}"'.format(ctnr.name)) + log.info('Removing orphan container "{}"'.format(ctnr.name)) try: ctnr.kill() except APIError: @@ -820,7 +820,7 @@ class Project(object): ctnr.remove(force=True) else: log.warning( - 'Found orphan containers ({0}) for this project. If ' + 'Found orphan containers ({}) for this project. If ' 'you removed or renamed this service in your compose ' 'file, you can run this command with the ' '--remove-orphans flag to clean it up.'.format( @@ -966,16 +966,16 @@ def get_secrets(service, service_secrets, secret_defs): .format(service=service, secret=secret.source)) if secret_def.get('external'): - log.warning("Service \"{service}\" uses secret \"{secret}\" which is external. " - "External secrets are not available to containers created by " - "docker-compose.".format(service=service, secret=secret.source)) + log.warning('Service "{service}" uses secret "{secret}" which is external. ' + 'External secrets are not available to containers created by ' + 'docker-compose.'.format(service=service, secret=secret.source)) continue if secret.uid or secret.gid or secret.mode: log.warning( - "Service \"{service}\" uses secret \"{secret}\" with uid, " - "gid, or mode. These fields are not supported by this " - "implementation of the Compose file".format( + 'Service "{service}" uses secret "{secret}" with uid, ' + 'gid, or mode. These fields are not supported by this ' + 'implementation of the Compose file'.format( service=service, secret=secret.source ) ) @@ -983,8 +983,8 @@ def get_secrets(service, service_secrets, secret_defs): secret_file = secret_def.get('file') if not path.isfile(str(secret_file)): log.warning( - "Service \"{service}\" uses an undefined secret file \"{secret_file}\", " - "the following file should be created \"{secret_file}\"".format( + 'Service "{service}" uses an undefined secret file "{secret_file}", ' + 'the following file should be created "{secret_file}"'.format( service=service, secret_file=secret_file ) ) diff --git a/compose/service.py b/compose/service.py index f52bd6ffe..5980310b3 100644 --- a/compose/service.py +++ b/compose/service.py @@ -163,7 +163,7 @@ class BuildAction(enum.Enum): skip = 2 -class Service(object): +class Service: def __init__( self, name, @@ -230,10 +230,10 @@ class Service(object): """Return a :class:`compose.container.Container` for this service. The container must be active, and match `number`. """ - for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]): + for container in self.containers(labels=['{}={}'.format(LABEL_CONTAINER_NUMBER, number)]): return container - raise ValueError("No container found for %s_%s" % (self.name, number)) + raise ValueError("No container found for {}_{}".format(self.name, number)) def start(self, **options): containers = self.containers(stopped=True) @@ -642,7 +642,7 @@ class Service(object): expl = binarystr_to_unicode(ex.explanation) if "driver failed programming external connectivity" in expl: log.warn("Host is already in use by another container") - raise OperationFailedError("Cannot start service %s: %s" % (self.name, expl)) + raise OperationFailedError("Cannot start service {}: {}".format(self.name, expl)) return container @property @@ -736,12 +736,12 @@ class Service(object): pid_namespace = self.pid_mode.service_name ipc_namespace = self.ipc_mode.service_name - configs = dict( - [(name, None) for name in self.get_linked_service_names()] + configs = { + name: None for name in self.get_linked_service_names() + } + configs.update( + (name, None) for name in self.get_volumes_from_names() ) - configs.update(dict( - [(name, None) for name in self.get_volumes_from_names()] - )) configs.update({net_name: None} if net_name else {}) configs.update({pid_namespace: None} if pid_namespace else {}) configs.update({ipc_namespace: None} if ipc_namespace else {}) @@ -863,9 +863,9 @@ class Service(object): add_config_hash = (not one_off and not override_options) slug = generate_random_id() if one_off else None - container_options = dict( - (k, self.options[k]) - for k in DOCKER_CONFIG_KEYS if k in self.options) + container_options = { + k: self.options[k] + for k in DOCKER_CONFIG_KEYS if k in self.options} override_volumes = override_options.pop('volumes', []) container_options.update(override_options) @@ -957,7 +957,7 @@ class Service(object): ) container_options['environment'].update(affinity) - container_options['volumes'] = dict((v.internal, {}) for v in container_volumes or {}) + container_options['volumes'] = {v.internal: {} for v in container_volumes or {}} if version_gte(self.client.api_version, '1.30'): override_options['mounts'] = [build_mount(v) for v in container_mounts] or None else: @@ -1159,9 +1159,9 @@ class Service(object): def labels(self, one_off=False, legacy=False): proj_name = self.project if not legacy else re.sub(r'[_-]', '', self.project) return [ - '{0}={1}'.format(LABEL_PROJECT, proj_name), - '{0}={1}'.format(LABEL_SERVICE, self.name), - '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"), + '{}={}'.format(LABEL_PROJECT, proj_name), + '{}={}'.format(LABEL_SERVICE, self.name), + '{}={}'.format(LABEL_ONE_OFF, "True" if one_off else "False"), ] @property @@ -1178,7 +1178,7 @@ class Service(object): ext_links_origins = [link.split(':')[0] for link in self.options.get('external_links', [])] if container_name in ext_links_origins: raise DependencyError( - 'Service {0} has a self-referential external link: {1}'.format( + 'Service {} has a self-referential external link: {}'.format( self.name, container_name ) ) @@ -1233,11 +1233,9 @@ class Service(object): output = self.client.pull(repo, **pull_kwargs) if silent: with open(os.devnull, 'w') as devnull: - for event in stream_output(output, devnull): - yield event + yield from stream_output(output, devnull) else: - for event in stream_output(output, sys.stdout): - yield event + yield from stream_output(output, sys.stdout) except (StreamOutputError, NotFound) as e: if not ignore_pull_failures: raise @@ -1255,7 +1253,7 @@ class Service(object): 'platform': self.platform, } if not silent: - log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) + log.info('Pulling {} ({}{}{})...'.format(self.name, repo, separator, tag)) if kwargs['platform'] and version_lt(self.client.api_version, '1.35'): raise OperationFailedError( @@ -1273,7 +1271,7 @@ class Service(object): repo, tag, separator = parse_repository_tag(self.options['image']) tag = tag or 'latest' - log.info('Pushing %s (%s%s%s)...' % (self.name, repo, separator, tag)) + log.info('Pushing {} ({}{}{})...'.format(self.name, repo, separator, tag)) output = self.client.push(repo, tag=tag, stream=True) try: @@ -1335,7 +1333,7 @@ def short_id_alias_exists(container, network): return container.short_id in aliases -class IpcMode(object): +class IpcMode: def __init__(self, mode): self._mode = mode @@ -1375,7 +1373,7 @@ class ContainerIpcMode(IpcMode): self._mode = 'container:{}'.format(container.id) -class PidMode(object): +class PidMode: def __init__(self, mode): self._mode = mode @@ -1415,7 +1413,7 @@ class ContainerPidMode(PidMode): self._mode = 'container:{}'.format(container.id) -class NetworkMode(object): +class NetworkMode: """A `standard` network mode (ex: host, bridge)""" service_name = None @@ -1430,7 +1428,7 @@ class NetworkMode(object): mode = id -class ContainerNetworkMode(object): +class ContainerNetworkMode: """A network mode that uses a container's network stack.""" service_name = None @@ -1447,7 +1445,7 @@ class ContainerNetworkMode(object): return 'container:' + self.container.id -class ServiceNetworkMode(object): +class ServiceNetworkMode: """A network mode that uses a service's network stack.""" def __init__(self, service): @@ -1552,10 +1550,10 @@ def get_container_data_volumes(container, volumes_option, tmpfs_option, mounts_o volumes = [] volumes_option = volumes_option or [] - container_mounts = dict( - (mount['Destination'], mount) + container_mounts = { + mount['Destination']: mount for mount in container.get('Mounts') or {} - ) + } image_volumes = [ VolumeSpec.parse(volume) @@ -1607,9 +1605,9 @@ def get_container_data_volumes(container, volumes_option, tmpfs_option, mounts_o def warn_on_masked_volume(volumes_option, container_volumes, service): - container_volumes = dict( - (volume.internal, volume.external) - for volume in container_volumes) + container_volumes = { + volume.internal: volume.external + for volume in container_volumes} for volume in volumes_option: if ( @@ -1759,7 +1757,7 @@ def convert_blkio_config(blkio_config): continue arr = [] for item in blkio_config[field]: - arr.append(dict([(k.capitalize(), v) for k, v in item.items()])) + arr.append({k.capitalize(): v for k, v in item.items()}) result[field] = arr return result @@ -1771,7 +1769,7 @@ def rewrite_build_path(path): return path -class _CLIBuilder(object): +class _CLIBuilder: def __init__(self, progress): self._progress = progress @@ -1879,7 +1877,7 @@ class _CLIBuilder(object): yield json.dumps({"stream": "{}{}\n".format(magic_word, image_id)}) -class _CommandBuilder(object): +class _CommandBuilder: def __init__(self): self._args = ["docker", "build"] diff --git a/compose/timeparse.py b/compose/timeparse.py index bdd9f611f..477445625 100644 --- a/compose/timeparse.py +++ b/compose/timeparse.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- ''' timeparse.py (c) Will Roberts 1 February, 2014 @@ -54,14 +53,14 @@ TIMEFORMAT = r'{HOURS}{MINS}{SECS}{MILLI}{MICRO}{NANO}'.format( NANO=opt(NANO), ) -MULTIPLIERS = dict([ - ('hours', 60 * 60), - ('mins', 60), - ('secs', 1), - ('milli', 1.0 / 1000), - ('micro', 1.0 / 1000.0 / 1000), - ('nano', 1.0 / 1000.0 / 1000.0 / 1000.0), -]) +MULTIPLIERS = { + 'hours': 60 * 60, + 'mins': 60, + 'secs': 1, + 'milli': 1.0 / 1000, + 'micro': 1.0 / 1000.0 / 1000, + 'nano': 1.0 / 1000.0 / 1000.0 / 1000.0, +} def timeparse(sval): @@ -90,4 +89,4 @@ def timeparse(sval): def cast(value): - return int(value, 10) if value.isdigit() else float(value) + return int(value) if value.isdigit() else float(value) diff --git a/compose/utils.py b/compose/utils.py index 8b5ab38d9..060ba50cc 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -29,7 +29,7 @@ def stream_as_text(stream): yield data -def line_splitter(buffer, separator=u'\n'): +def line_splitter(buffer, separator='\n'): index = buffer.find(str(separator)) if index == -1: return None @@ -45,7 +45,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): of the input. """ splitter = splitter or line_splitter - buffered = str('') + buffered = '' for data in stream_as_text(stream): buffered += data @@ -116,7 +116,7 @@ def parse_nanoseconds_int(value): def build_string_dict(source_dict): - return dict((k, str(v if v is not None else '')) for k, v in source_dict.items()) + return {k: str(v if v is not None else '') for k, v in source_dict.items()} def splitdrive(path): diff --git a/compose/volume.py b/compose/volume.py index d31417a55..5f36e432b 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -1,5 +1,6 @@ import logging import re +from itertools import chain from docker.errors import NotFound from docker.utils import version_lt @@ -15,7 +16,7 @@ from .const import LABEL_VOLUME log = logging.getLogger(__name__) -class Volume(object): +class Volume: def __init__(self, client, project, name, driver=None, driver_opts=None, external=False, labels=None, custom_name=False): self.client = client @@ -57,13 +58,13 @@ class Volume(object): def full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format(self.project.lstrip('-_'), self.name) + return '{}_{}'.format(self.project.lstrip('-_'), self.name) @property def legacy_full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format( + return '{}_{}'.format( re.sub(r'[_-]', '', self.project), self.name ) @@ -96,7 +97,7 @@ class Volume(object): self.legacy = False -class ProjectVolumes(object): +class ProjectVolumes: def __init__(self, volumes): self.volumes = volumes @@ -132,7 +133,7 @@ class ProjectVolumes(object): volume_exists = volume.exists() if volume.external: log.debug( - 'Volume {0} declared as external. No new ' + 'Volume {} declared as external. No new ' 'volume will be created.'.format(volume.name) ) if not volume_exists: @@ -148,7 +149,7 @@ class ProjectVolumes(object): if not volume_exists: log.info( - 'Creating volume "{0}" with {1} driver'.format( + 'Creating volume "{}" with {} driver'.format( volume.full_name, volume.driver or 'default' ) ) @@ -157,7 +158,7 @@ class ProjectVolumes(object): check_remote_volume_config(volume.inspect(legacy=volume.legacy), volume) except NotFound: raise ConfigurationError( - 'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver) + 'Volume {} specifies nonexistent driver {}'.format(volume.name, volume.driver) ) def namespace_spec(self, volume_spec): @@ -174,7 +175,7 @@ class ProjectVolumes(object): class VolumeConfigChangedError(ConfigurationError): def __init__(self, local, property_name, local_value, remote_value): - super(VolumeConfigChangedError, self).__init__( + super().__init__( 'Configuration for volume {vol_name} specifies {property_name} ' '{local_value}, but a volume with the same name uses a different ' '{property_name} ({remote_value}). If you wish to use the new ' @@ -192,7 +193,7 @@ def check_remote_volume_config(remote, local): raise VolumeConfigChangedError(local, 'driver', local.driver, remote.get('Driver')) local_opts = local.driver_opts or {} remote_opts = remote.get('Options') or {} - for k in set.union(set(remote_opts.keys()), set(local_opts.keys())): + for k in set(chain(remote_opts, local_opts)): if k.startswith('com.docker.'): # These options are set internally continue if remote_opts.get(k) != local_opts.get(k): @@ -202,7 +203,7 @@ def check_remote_volume_config(remote, local): local_labels = local.labels or {} remote_labels = remote.get('Labels') or {} - for k in set.union(set(remote_labels.keys()), set(local_labels.keys())): + for k in set(chain(remote_labels, local_labels)): if k.startswith('com.docker.'): # We are only interested in user-specified labels continue if remote_labels.get(k) != local_labels.get(k): diff --git a/contrib/migration/migrate-compose-file-v1-to-v2.py b/contrib/migration/migrate-compose-file-v1-to-v2.py index e217b7072..26511206c 100755 --- a/contrib/migration/migrate-compose-file-v1-to-v2.py +++ b/contrib/migration/migrate-compose-file-v1-to-v2.py @@ -156,7 +156,7 @@ def main(args): opts = parse_opts(args) - with open(opts.filename, 'r') as fh: + with open(opts.filename) as fh: new_format = migrate(fh.read()) if opts.in_place: diff --git a/docker-compose_darwin.spec b/docker-compose_darwin.spec index 6f96b29a4..3228884f5 100644 --- a/docker-compose_darwin.spec +++ b/docker-compose_darwin.spec @@ -1,4 +1,4 @@ -# -*- mode: python ; coding: utf-8 -*- +# -*- mode: python -*- block_cipher = None diff --git a/requirements.txt b/requirements.txt index d0a386642..59317bdfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,6 @@ docker==4.3.0 docker-pycreds==0.4.0 dockerpty==0.4.1 docopt==0.6.2 -enum34==1.1.6; python_version < '3.4' -functools32==3.2.3.post2; python_version < '3.2' idna==2.10 ipaddress==1.0.23 jsonschema==3.2.0 diff --git a/script/release/utils.py b/script/release/utils.py index 25b39ca74..5ed53ec85 100644 --- a/script/release/utils.py +++ b/script/release/utils.py @@ -6,7 +6,7 @@ from const import REPO_ROOT def update_init_py_version(version): path = os.path.join(REPO_ROOT, 'compose', '__init__.py') - with open(path, 'r') as f: + with open(path) as f: contents = f.read() contents = re.sub(r"__version__ = '[0-9a-z.-]+'", "__version__ = '{}'".format(version), contents) with open(path, 'w') as f: @@ -15,7 +15,7 @@ def update_init_py_version(version): def update_run_sh_version(version): path = os.path.join(REPO_ROOT, 'script', 'run', 'run.sh') - with open(path, 'r') as f: + with open(path) as f: contents = f.read() contents = re.sub(r'VERSION="[0-9a-z.-]+"', 'VERSION="{}"'.format(version), contents) with open(path, 'w') as f: diff --git a/setup.py b/setup.py index a2e946b33..6041fce15 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import codecs import os import re @@ -50,7 +49,6 @@ if sys.version_info[:2] < (3, 4): tests_require.append('mock >= 1.0.1, < 4') extras_require = { - ':python_version < "3.2"': ['subprocess32 >= 3.5.4, < 4'], ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'], ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5, < 4'], ':python_version < "3.3"': ['backports.shutil_get_terminal_size == 1.0.0', @@ -94,7 +92,7 @@ setup( install_requires=install_requires, extras_require=extras_require, tests_require=tests_require, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', entry_points={ 'console_scripts': ['docker-compose=compose.cli.main:main'], }, diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 92168e93f..4dd935210 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import datetime import json import os.path @@ -99,7 +98,7 @@ def kill_service(service): container.kill() -class ContainerCountCondition(object): +class ContainerCountCondition: def __init__(self, project, expected): self.project = project @@ -112,7 +111,7 @@ class ContainerCountCondition(object): return "waiting for counter count == %s" % self.expected -class ContainerStateCondition(object): +class ContainerStateCondition: def __init__(self, client, name, status): self.client = client @@ -140,7 +139,7 @@ class ContainerStateCondition(object): class CLITestCase(DockerClientTestCase): def setUp(self): - super(CLITestCase, self).setUp() + super().setUp() self.base_dir = 'tests/fixtures/simple-composefile' self.override_dir = None @@ -162,7 +161,7 @@ class CLITestCase(DockerClientTestCase): if hasattr(self, '_project'): del self._project - super(CLITestCase, self).tearDown() + super().tearDown() @property def project(self): @@ -206,14 +205,14 @@ class CLITestCase(DockerClientTestCase): def test_shorthand_host_opt(self): self.dispatch( - ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), + ['-H={}'.format(os.environ.get('DOCKER_HOST', 'unix://')), 'up', '-d'], returncode=0 ) def test_shorthand_host_opt_interactive(self): self.dispatch( - ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), + ['-H={}'.format(os.environ.get('DOCKER_HOST', 'unix://')), 'run', 'another', 'ls'], returncode=0 ) @@ -1453,7 +1452,7 @@ services: if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] - assert set([v['Name'].split('/')[-1] for v in volumes]) == {volume_with_label} + assert {v['Name'].split('/')[-1] for v in volumes} == {volume_with_label} assert 'label_key' in volumes[0]['Labels'] assert volumes[0]['Labels']['label_key'] == 'label_val' @@ -1866,12 +1865,12 @@ services: self.dispatch(['run', 'implicit']) service = self.project.get_service('implicit') containers = service.containers(stopped=True, one_off=OneOffFilter.only) - assert [c.human_readable_command for c in containers] == [u'/bin/sh -c echo "success"'] + assert [c.human_readable_command for c in containers] == ['/bin/sh -c echo "success"'] self.dispatch(['run', 'explicit']) service = self.project.get_service('explicit') containers = service.containers(stopped=True, one_off=OneOffFilter.only) - assert [c.human_readable_command for c in containers] == [u'/bin/true'] + assert [c.human_readable_command for c in containers] == ['/bin/true'] @pytest.mark.skipif(SWARM_SKIP_RM_VOLUMES, reason='Swarm DELETE /containers/ bug') def test_run_rm(self): @@ -2701,7 +2700,7 @@ services: str_iso_date, str_iso_time, container_info = string.split(' ', 2) try: return isinstance(datetime.datetime.strptime( - '%s %s' % (str_iso_date, str_iso_time), + '{} {}'.format(str_iso_date, str_iso_time), '%Y-%m-%d %H:%M:%S.%f'), datetime.datetime) except ValueError: @@ -2790,7 +2789,7 @@ services: self.base_dir = 'tests/fixtures/extends' self.dispatch(['up', '-d'], None) - assert set([s.name for s in self.project.services]) == {'mydb', 'myweb'} + assert {s.name for s in self.project.services} == {'mydb', 'myweb'} # Sort by name so we get [db, web] containers = sorted( diff --git a/tests/acceptance/context_test.py b/tests/acceptance/context_test.py index e17e71717..a5d0c1473 100644 --- a/tests/acceptance/context_test.py +++ b/tests/acceptance/context_test.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import shutil import unittest diff --git a/tests/helpers.py b/tests/helpers.py index d17868485..3642e6ebc 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -49,7 +49,7 @@ def create_custom_host_file(client, filename, content): def create_host_file(client, filename): - with open(filename, 'r') as fh: + with open(filename) as fh: content = fh.read() return create_custom_host_file(client, filename, content) diff --git a/tests/integration/environment_test.py b/tests/integration/environment_test.py index 43df2c52b..12a969c94 100644 --- a/tests/integration/environment_test.py +++ b/tests/integration/environment_test.py @@ -15,7 +15,7 @@ from tests.integration.testcases import DockerClientTestCase class EnvironmentTest(DockerClientTestCase): @classmethod def setUpClass(cls): - super(EnvironmentTest, cls).setUpClass() + super().setUpClass() cls.compose_file = tempfile.NamedTemporaryFile(mode='w+b') cls.compose_file.write(bytes("""version: '3.2' services: @@ -27,7 +27,7 @@ services: @classmethod def tearDownClass(cls): - super(EnvironmentTest, cls).tearDownClass() + super().tearDownClass() cls.compose_file.close() @data('events', diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 4848c53ee..879701076 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -289,19 +289,19 @@ class ProjectTest(DockerClientTestCase): db_container = db.create_container() project.start(service_names=['web']) - assert set(c.name for c in project.containers() if c.is_running) == { + assert {c.name for c in project.containers() if c.is_running} == { web_container_1.name, web_container_2.name} project.start() - assert set(c.name for c in project.containers() if c.is_running) == { + assert {c.name for c in project.containers() if c.is_running} == { web_container_1.name, web_container_2.name, db_container.name} project.pause(service_names=['web']) - assert set([c.name for c in project.containers() if c.is_paused]) == { + assert {c.name for c in project.containers() if c.is_paused} == { web_container_1.name, web_container_2.name} project.pause() - assert set([c.name for c in project.containers() if c.is_paused]) == { + assert {c.name for c in project.containers() if c.is_paused} == { web_container_1.name, web_container_2.name, db_container.name} project.unpause(service_names=['db']) @@ -311,7 +311,7 @@ class ProjectTest(DockerClientTestCase): assert len([c.name for c in project.containers() if c.is_paused]) == 0 project.stop(service_names=['web'], timeout=1) - assert set(c.name for c in project.containers() if c.is_running) == {db_container.name} + assert {c.name for c in project.containers() if c.is_running} == {db_container.name} project.kill(service_names=['db']) assert len([c for c in project.containers() if c.is_running]) == 0 @@ -1177,8 +1177,8 @@ class ProjectTest(DockerClientTestCase): assert networks[0]['Labels']['label_key'] == 'label_val' def test_project_up_volumes(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( services=[{ 'name': 'web', @@ -1232,9 +1232,9 @@ class ProjectTest(DockerClientTestCase): if v['Name'].split('/')[-1].startswith('composetest_') ] - assert set([v['Name'].split('/')[-1] for v in volumes]) == set( - ['composetest_{}'.format(volume_name)] - ) + assert {v['Name'].split('/')[-1] for v in volumes} == { + 'composetest_{}'.format(volume_name) + } assert 'label_key' in volumes[0]['Labels'] assert volumes[0]['Labels']['label_key'] == 'label_val' @@ -1348,8 +1348,8 @@ class ProjectTest(DockerClientTestCase): assert len(project.containers()) == 3 def test_initialize_volumes(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( services=[{ 'name': 'web', @@ -1370,8 +1370,8 @@ class ProjectTest(DockerClientTestCase): assert volume_data['Driver'] == 'local' def test_project_up_implicit_volume_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( services=[{ 'name': 'web', @@ -1479,7 +1479,7 @@ class ProjectTest(DockerClientTestCase): assert output == b"This is the secret\n" def test_initialize_volumes_invalid_volume_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) + vol_name = '{:x}'.format(random.getrandbits(32)) config_data = build_config( version=VERSION, @@ -1500,8 +1500,8 @@ class ProjectTest(DockerClientTestCase): @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_updated_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( services=[{ @@ -1531,14 +1531,14 @@ class ProjectTest(DockerClientTestCase): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Configuration for volume {0} specifies driver smb'.format( + assert 'Configuration for volume {} specifies driver smb'.format( vol_name ) in str(e.value) @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_updated_driver_opts(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) tmpdir = tempfile.mkdtemp(prefix='compose_test_') self.addCleanup(shutil.rmtree, tmpdir) driver_opts = {'o': 'bind', 'device': tmpdir, 'type': 'none'} @@ -1575,13 +1575,13 @@ class ProjectTest(DockerClientTestCase): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Configuration for volume {0} specifies "device" driver_opt {1}'.format( + assert 'Configuration for volume {} specifies "device" driver_opt {}'.format( vol_name, driver_opts['device'] ) in str(e.value) def test_initialize_volumes_updated_blank_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( services=[{ @@ -1617,8 +1617,8 @@ class ProjectTest(DockerClientTestCase): @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_external_volumes(self): # Use composetest_ prefix so it gets garbage-collected in tearDown() - vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = 'composetest_{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) self.client.create_volume(vol_name) config_data = build_config( services=[{ @@ -1640,7 +1640,7 @@ class ProjectTest(DockerClientTestCase): self.client.inspect_volume(full_vol_name) def test_initialize_volumes_inexistent_external_volume(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) + vol_name = '{:x}'.format(random.getrandbits(32)) config_data = build_config( services=[{ @@ -1658,13 +1658,13 @@ class ProjectTest(DockerClientTestCase): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Volume {0} declared as external'.format( + assert 'Volume {} declared as external'.format( vol_name ) in str(e.value) def test_project_up_named_volumes_in_binds(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) base_file = config.ConfigFile( 'base.yml', @@ -1673,7 +1673,7 @@ class ProjectTest(DockerClientTestCase): 'simple': { 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', - 'volumes': ['{0}:/data'.format(vol_name)] + 'volumes': ['{}:/data'.format(vol_name)] }, }, 'volumes': { diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py index 81cb2382f..2fbaafb28 100644 --- a/tests/integration/resilience_test.py +++ b/tests/integration/resilience_test.py @@ -22,7 +22,7 @@ class ResilienceTest(DockerClientTestCase): def tearDown(self): del self.project del self.db - super(ResilienceTest, self).tearDown() + super().tearDown() def test_successful_recreate(self): self.project.up(strategy=ConvergenceStrategy.always) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 76efba440..985c4d77a 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -248,7 +248,7 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('db', security_opt=security_opt) container = service.create_container() service.start_container(container) - assert set(container.get('HostConfig.SecurityOpt')) == set([o.repr() for o in security_opt]) + assert set(container.get('HostConfig.SecurityOpt')) == {o.repr() for o in security_opt} @pytest.mark.xfail(True, reason='Not supported on most drivers') def test_create_container_with_storage_opt(self): @@ -290,7 +290,7 @@ class ServiceTest(DockerClientTestCase): actual_host_path = container.get_mount(container_path)['Source'] assert path.basename(actual_host_path) == path.basename(host_path), ( - "Last component differs: %s, %s" % (actual_host_path, host_path) + "Last component differs: {}, {}".format(actual_host_path, host_path) ) def test_create_container_with_host_mount(self): @@ -844,11 +844,11 @@ class ServiceTest(DockerClientTestCase): db2 = create_and_start_container(db) create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'db' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_container_creates_links_with_names(self): @@ -859,11 +859,11 @@ class ServiceTest(DockerClientTestCase): db2 = create_and_start_container(db) create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'custom_link_name' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_container_with_external_links(self): @@ -879,11 +879,11 @@ class ServiceTest(DockerClientTestCase): create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db_ctnrs[0].name, db_ctnrs[1].name, 'db_3' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_normal_container_does_not_create_links_to_its_own_service(self): @@ -893,7 +893,7 @@ class ServiceTest(DockerClientTestCase): create_and_start_container(db) c = create_and_start_container(db) - assert set(get_links(c)) == set([]) + assert set(get_links(c)) == set() @no_cluster('No legacy links support in Swarm') def test_start_one_off_container_creates_links_to_its_own_service(self): @@ -904,11 +904,11 @@ class ServiceTest(DockerClientTestCase): c = create_and_start_container(db, one_off=OneOffFilter.only) - assert set(get_links(c)) == set([ + assert set(get_links(c)) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'db' - ]) + } def test_start_container_builds_images(self): service = Service( @@ -1719,14 +1719,14 @@ class ServiceTest(DockerClientTestCase): options = service._get_container_create_options({}, service._next_container_number()) original = Container.create(service.client, **options) - assert set(service.containers(stopped=True)) == set([original]) + assert set(service.containers(stopped=True)) == {original} assert set(service.duplicate_containers()) == set() options['name'] = 'temporary_container_name' duplicate = Container.create(service.client, **options) - assert set(service.containers(stopped=True)) == set([original, duplicate]) - assert set(service.duplicate_containers()) == set([duplicate]) + assert set(service.containers(stopped=True)) == {original, duplicate} + assert set(service.duplicate_containers()) == {duplicate} def converge(service, strategy=ConvergenceStrategy.changed): diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index 611d0cc94..5258e310c 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -39,7 +39,7 @@ class ProjectTestCase(DockerClientTestCase): class BasicProjectTest(ProjectTestCase): def setUp(self): - super(BasicProjectTest, self).setUp() + super().setUp() self.cfg = { 'db': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, @@ -95,7 +95,7 @@ class BasicProjectTest(ProjectTestCase): class ProjectWithDependenciesTest(ProjectTestCase): def setUp(self): - super(ProjectWithDependenciesTest, self).setUp() + super().setUp() self.cfg = { 'db': { @@ -116,7 +116,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): def test_up(self): containers = self.run_up(self.cfg) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} def test_change_leaf(self): old_containers = self.run_up(self.cfg) @@ -124,7 +124,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['nginx']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['nginx']) + assert {c.service for c in new_containers - old_containers} == {'nginx'} def test_change_middle(self): old_containers = self.run_up(self.cfg) @@ -132,7 +132,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['web']) + assert {c.service for c in new_containers - old_containers} == {'web'} def test_change_middle_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -140,7 +140,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == {'web', 'nginx'} + assert {c.service for c in new_containers - old_containers} == {'web', 'nginx'} def test_change_root(self): old_containers = self.run_up(self.cfg) @@ -148,7 +148,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['db']) + assert {c.service for c in new_containers - old_containers} == {'db'} def test_change_root_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -156,7 +156,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == { + assert {c.service for c in new_containers - old_containers} == { 'db', 'web', 'nginx' } @@ -213,7 +213,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): class ProjectWithDependsOnDependenciesTest(ProjectTestCase): def setUp(self): - super(ProjectWithDependsOnDependenciesTest, self).setUp() + super().setUp() self.cfg = { 'version': '2', @@ -238,7 +238,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): def test_up(self): local_cfg = copy.deepcopy(self.cfg) containers = self.run_up(local_cfg) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} def test_change_leaf(self): local_cfg = copy.deepcopy(self.cfg) @@ -247,7 +247,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): local_cfg['services']['nginx']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(local_cfg) - assert set(c.service for c in new_containers - old_containers) == set(['nginx']) + assert {c.service for c in new_containers - old_containers} == {'nginx'} def test_change_middle(self): local_cfg = copy.deepcopy(self.cfg) @@ -256,7 +256,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(local_cfg) - assert set(c.service for c in new_containers - old_containers) == set(['web']) + assert {c.service for c in new_containers - old_containers} == {'web'} def test_change_middle_always_recreate_deps(self): local_cfg = copy.deepcopy(self.cfg) @@ -265,7 +265,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(local_cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == set(['web', 'nginx']) + assert {c.service for c in new_containers - old_containers} == {'web', 'nginx'} def test_change_root(self): local_cfg = copy.deepcopy(self.cfg) @@ -274,7 +274,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(local_cfg) - assert set(c.service for c in new_containers - old_containers) == set(['db']) + assert {c.service for c in new_containers - old_containers} == {'db'} def test_change_root_always_recreate_deps(self): local_cfg = copy.deepcopy(self.cfg) @@ -283,7 +283,7 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(local_cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in new_containers - old_containers} == {'db', 'web', 'nginx'} def test_change_root_no_recreate(self): local_cfg = copy.deepcopy(self.cfg) @@ -303,24 +303,24 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): del next_cfg['services']['web']['depends_on'] containers = self.run_up(local_cfg) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} project = self.make_project(local_cfg) project.stop(timeout=1) next_containers = self.run_up(next_cfg) - assert set(c.service for c in next_containers) == set(['web', 'nginx']) + assert {c.service for c in next_containers} == {'web', 'nginx'} def test_service_removed_while_up(self): local_cfg = copy.deepcopy(self.cfg) containers = self.run_up(local_cfg) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} del local_cfg['services']['db'] del local_cfg['services']['web']['depends_on'] containers = self.run_up(local_cfg) - assert set(c.service for c in containers) == set(['web', 'nginx']) + assert {c.service for c in containers} == {'web', 'nginx'} def test_dependency_removed(self): local_cfg = copy.deepcopy(self.cfg) @@ -328,24 +328,24 @@ class ProjectWithDependsOnDependenciesTest(ProjectTestCase): del next_cfg['services']['nginx']['depends_on'] containers = self.run_up(local_cfg, service_names=['nginx']) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} project = self.make_project(local_cfg) project.stop(timeout=1) next_containers = self.run_up(next_cfg, service_names=['nginx']) - assert set(c.service for c in next_containers if c.is_running) == set(['nginx']) + assert {c.service for c in next_containers if c.is_running} == {'nginx'} def test_dependency_added(self): local_cfg = copy.deepcopy(self.cfg) del local_cfg['services']['nginx']['depends_on'] containers = self.run_up(local_cfg, service_names=['nginx']) - assert set(c.service for c in containers) == set(['nginx']) + assert {c.service for c in containers} == {'nginx'} local_cfg['services']['nginx']['depends_on'] = ['db'] containers = self.run_up(local_cfg, service_names=['nginx']) - assert set(c.service for c in containers) == set(['nginx', 'db']) + assert {c.service for c in containers} == {'nginx', 'db'} class ServiceStateTest(DockerClientTestCase): diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 2ede7bf28..0e7c78bc2 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -18,7 +18,7 @@ class VolumeTest(DockerClientTestCase): except DockerException: pass del self.tmp_volumes - super(VolumeTest, self).tearDown() + super().tearDown() def create_volume(self, name, driver=None, opts=None, external=None, custom_name=False): if external: diff --git a/tests/unit/cli/command_test.py b/tests/unit/cli/command_test.py index 20702d975..9d4db5b59 100644 --- a/tests/unit/cli/command_test.py +++ b/tests/unit/cli/command_test.py @@ -1,4 +1,3 @@ -# ~*~ encoding: utf-8 ~*~ import os import pytest @@ -9,7 +8,7 @@ from compose.const import IS_WINDOWS_PLATFORM from tests import mock -class TestGetConfigPathFromOptions(object): +class TestGetConfigPathFromOptions: def test_path_from_options(self): paths = ['one.yml', 'two.yml'] diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 873c1ff3d..941aed4f4 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -55,7 +55,7 @@ class DockerClientTestCase(unittest.TestCase): def test_user_agent(self): client = docker_client(os.environ) - expected = "docker-compose/{0} docker-py/{1} {2}/{3}".format( + expected = "docker-compose/{} docker-py/{} {}/{}".format( compose.__version__, docker.__version__, platform.system(), @@ -151,9 +151,9 @@ class TLSConfigTestCase(unittest.TestCase): def test_tls_client_and_ca_quoted_paths(self): options = { - '--tlscacert': '"{0}"'.format(self.ca_cert), - '--tlscert': '"{0}"'.format(self.client_cert), - '--tlskey': '"{0}"'.format(self.key), + '--tlscacert': '"{}"'.format(self.ca_cert), + '--tlscert': '"{}"'.format(self.client_cert), + '--tlskey': '"{}"'.format(self.key), '--tlsverify': True } result = tls_config_from_options(options) @@ -185,9 +185,9 @@ class TLSConfigTestCase(unittest.TestCase): 'DOCKER_TLS_VERIFY': 'false' }) options = { - '--tlscacert': '"{0}"'.format(self.ca_cert), - '--tlscert': '"{0}"'.format(self.client_cert), - '--tlskey': '"{0}"'.format(self.key), + '--tlscacert': '"{}"'.format(self.ca_cert), + '--tlscert': '"{}"'.format(self.client_cert), + '--tlskey': '"{}"'.format(self.key), '--tlsverify': True } @@ -230,7 +230,7 @@ class TLSConfigTestCase(unittest.TestCase): assert result.cert == (self.client_cert, self.key) -class TestGetTlsVersion(object): +class TestGetTlsVersion: def test_get_tls_version_default(self): environment = {} assert get_tls_version(environment) is None diff --git a/tests/unit/cli/errors_test.py b/tests/unit/cli/errors_test.py index f7359a48b..3b70ffe7b 100644 --- a/tests/unit/cli/errors_test.py +++ b/tests/unit/cli/errors_test.py @@ -21,7 +21,7 @@ def patch_find_executable(side_effect): side_effect=side_effect) -class TestHandleConnectionErrors(object): +class TestHandleConnectionErrors: def test_generic_connection_error(self, mock_logging): with pytest.raises(errors.ConnectionError): @@ -43,7 +43,7 @@ class TestHandleConnectionErrors(object): def test_api_error_version_mismatch_unicode_explanation(self, mock_logging): with pytest.raises(errors.ConnectionError): with handle_connection_errors(mock.Mock(api_version='1.38')): - raise APIError(None, None, u"client is newer than server") + raise APIError(None, None, "client is newer than server") _, args, _ = mock_logging.error.mock_calls[0] assert "Docker Engine of version 18.06.0 or greater" in args[0] @@ -57,7 +57,7 @@ class TestHandleConnectionErrors(object): mock_logging.error.assert_called_once_with(msg.decode('utf-8')) def test_api_error_version_other_unicode_explanation(self, mock_logging): - msg = u"Something broke!" + msg = "Something broke!" with pytest.raises(errors.ConnectionError): with handle_connection_errors(mock.Mock(api_version='1.22')): raise APIError(None, None, msg) diff --git a/tests/unit/cli/formatter_test.py b/tests/unit/cli/formatter_test.py index 07f5a8f50..08752a622 100644 --- a/tests/unit/cli/formatter_test.py +++ b/tests/unit/cli/formatter_test.py @@ -40,10 +40,10 @@ class ConsoleWarningFormatterTestCase(unittest.TestCase): message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' output = self.formatter.format(make_log_record(logging.WARN, message)) expected = colors.yellow('WARNING') + ': ' - assert output == '{0}{1}'.format(expected, message.decode('utf-8')) + assert output == '{}{}'.format(expected, message.decode('utf-8')) def test_format_unicode_error(self): message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' output = self.formatter.format(make_log_record(logging.ERROR, message)) expected = colors.red('ERROR') + ': ' - assert output == '{0}{1}'.format(expected, message.decode('utf-8')) + assert output == '{}{}'.format(expected, message.decode('utf-8')) diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index 38dd56c71..aeeed31f3 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -29,7 +29,7 @@ def mock_container(): return mock.Mock(spec=Container, name_without_project='web_1') -class TestLogPresenter(object): +class TestLogPresenter: def test_monochrome(self, mock_container): presenters = build_log_presenters(['foo', 'bar'], True) @@ -83,7 +83,7 @@ def test_build_no_log_generator(mock_container): assert "exited with code" not in output -class TestBuildLogGenerator(object): +class TestBuildLogGenerator: def test_no_log_stream(self, mock_container): mock_container.log_stream = None @@ -108,7 +108,7 @@ class TestBuildLogGenerator(object): assert next(generator) == "world" def test_unicode(self, output_stream): - glyph = u'\u2022\n' + glyph = '\u2022\n' mock_container.log_stream = iter([glyph.encode('utf-8')]) generator = build_log_generator(mock_container, {}) @@ -125,7 +125,7 @@ def mock_presenters(): return itertools.cycle([mock.Mock()]) -class TestWatchEvents(object): +class TestWatchEvents: def test_stop_event(self, thread_map, mock_presenters): event_stream = [{'action': 'stop', 'id': 'cid'}] @@ -167,7 +167,7 @@ class TestWatchEvents(object): assert container_id not in thread_map -class TestConsumeQueue(object): +class TestConsumeQueue: def test_item_is_an_exception(self): diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index ac3df2920..d75b6bd4c 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -22,7 +22,7 @@ def mock_container(service, number): container.Container, service=service, number=number, - name_without_project='{0}_{1}'.format(service, number)) + name_without_project='{}_{}'.format(service, number)) @pytest.fixture @@ -32,7 +32,7 @@ def logging_handler(): return logging.StreamHandler(stream=stream) -class TestCLIMainTestCase(object): +class TestCLIMainTestCase: def test_filter_attached_containers(self): containers = [ @@ -135,7 +135,7 @@ class TestCLIMainTestCase(object): assert expected_docker_start_call == docker_start_call -class TestSetupConsoleHandlerTestCase(object): +class TestSetupConsoleHandlerTestCase: def test_with_tty_verbose(self, logging_handler): setup_console_handler(logging_handler, True) @@ -155,7 +155,7 @@ class TestSetupConsoleHandlerTestCase(object): assert type(logging_handler.formatter) == logging.Formatter -class TestConvergeStrategyFromOptsTestCase(object): +class TestConvergeStrategyFromOptsTestCase: def test_invalid_opts(self): options = {'--force-recreate': True, '--no-recreate': True} @@ -189,7 +189,7 @@ def mock_find_executable(exe): @mock.patch('compose.cli.main.find_executable', mock_find_executable) -class TestCallDocker(object): +class TestCallDocker: def test_simple_no_options(self): with mock.patch('subprocess.call') as fake_call: call_docker(['ps'], {}, {}) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 2c53e00a8..f1a3e270d 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 import os import shutil import tempfile diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index f562141dc..03e95f77a 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 import codecs import os import shutil @@ -3885,12 +3884,12 @@ class VolumeConfigTest(unittest.TestCase): assert d['volumes'] == ['~:/data'] def test_volume_path_with_non_ascii_directory(self): - volume = u'/Füü/data:/data' + volume = '/Füü/data:/data' container_path = config.resolve_volume_path(".", volume) assert container_path == volume -class MergePathMappingTest(object): +class MergePathMappingTest: config_name = "" def test_empty(self): @@ -3963,7 +3962,7 @@ class BuildOrImageMergeTest(unittest.TestCase): assert config.merge_service_dicts({'image': 'redis'}, {'build': '.'}, V1) == {'build': '.'} -class MergeListsTest(object): +class MergeListsTest: config_name = "" base_config = [] override_config = [] @@ -4396,7 +4395,7 @@ class EnvTest(unittest.TestCase): {'env_file': ['tests/fixtures/env/resolve.env']}, Environment.from_env_file(None) ) == { - 'FILE_DEF': u'bär', + 'FILE_DEF': 'bär', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': None @@ -5042,14 +5041,14 @@ class VolumePathTest(unittest.TestCase): container_path = 'c:\\scarletdevil\\data' expected_mapping = (container_path, (host_path, None)) - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) + mapping = config.split_path_mapping('{}:{}'.format(host_path, container_path)) assert mapping == expected_mapping def test_split_path_mapping_with_root_mount(self): host_path = '/' container_path = '/var/hostroot' expected_mapping = (container_path, (host_path, None)) - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) + mapping = config.split_path_mapping('{}:{}'.format(host_path, container_path)) assert mapping == expected_mapping diff --git a/tests/unit/config/environment_test.py b/tests/unit/config/environment_test.py index 507385468..6a80ff122 100644 --- a/tests/unit/config/environment_test.py +++ b/tests/unit/config/environment_test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 import codecs import os import shutil diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index f8ff8c662..6f3533d0b 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 import pytest from compose.config.environment import Environment @@ -439,7 +438,7 @@ def test_unbraced_separators(defaults_interpolator): def test_interpolate_unicode_values(): variable_mapping = { - 'FOO': '十六夜 咲夜'.encode('utf-8'), + 'FOO': '十六夜 咲夜'.encode(), 'BAR': '十六夜 咲夜' } interpol = Interpolator(TemplateWithDefaults, variable_mapping).interpolate diff --git a/tests/unit/config/sort_services_test.py b/tests/unit/config/sort_services_test.py index 430fed6a6..508c4bba1 100644 --- a/tests/unit/config/sort_services_test.py +++ b/tests/unit/config/sort_services_test.py @@ -5,7 +5,7 @@ from compose.config.sort_services import sort_service_dicts from compose.config.types import VolumeFromSpec -class TestSortService(object): +class TestSortService: def test_sort_service_dicts_1(self): services = [ { diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index 23b9b6767..e5fcde1a6 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -39,7 +39,7 @@ def test_parse_extra_hosts_dict(): } -class TestServicePort(object): +class TestServicePort: def test_parse_dict(self): data = { 'target': 8000, @@ -129,7 +129,7 @@ class TestServicePort(object): ServicePort.parse(port_def) -class TestVolumeSpec(object): +class TestVolumeSpec: def test_parse_volume_spec_only_one_path(self): spec = VolumeSpec.parse('/the/volume') @@ -216,7 +216,7 @@ class TestVolumeSpec(object): ) -class TestVolumesFromSpec(object): +class TestVolumesFromSpec: services = ['servicea', 'serviceb'] diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index b885b239b..288c9b6e4 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -1,5 +1,3 @@ -# ~*~ encoding: utf-8 ~*~ -import io import os import random import shutil @@ -75,7 +73,7 @@ class ProgressStreamTestCase(unittest.TestCase): def mktempfile(encoding): fname = os.path.join(tmpdir, hex(random.getrandbits(128))[2:-1]) - return io.open(fname, mode='w+', encoding=encoding) + return open(fname, mode='w+', encoding=encoding) text = '就吃饭' with mktempfile(encoding='utf-8') as tf: diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index b889e5e2c..01f0b11ef 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 import datetime import os import tempfile @@ -739,7 +738,7 @@ class ProjectTest(unittest.TestCase): assert fake_log.warn.call_count == 0 def test_no_such_service_unicode(self): - assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜' + assert NoSuchService('十六夜 咲夜'.encode()).msg == 'No such service: 十六夜 咲夜' assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜' def test_project_platform_value(self): diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index d4e7f3c5d..72cb1d7f9 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -63,9 +63,9 @@ class ServiceTest(unittest.TestCase): assert [c.id for c in service.containers()] == list(range(3)) expected_labels = [ - '{0}=myproject'.format(LABEL_PROJECT), - '{0}=db'.format(LABEL_SERVICE), - '{0}=False'.format(LABEL_ONE_OFF), + '{}=myproject'.format(LABEL_PROJECT), + '{}=db'.format(LABEL_SERVICE), + '{}=False'.format(LABEL_ONE_OFF), ] self.mock_client.containers.assert_called_once_with( diff --git a/tests/unit/split_buffer_test.py b/tests/unit/split_buffer_test.py index f1974c831..d6b5b884c 100644 --- a/tests/unit/split_buffer_test.py +++ b/tests/unit/split_buffer_test.py @@ -36,7 +36,7 @@ class SplitBufferTest(unittest.TestCase): self.assert_produces(reader, ['abc\n', 'd']) def test_preserves_unicode_sequences_within_lines(self): - string = u"a\u2022c\n" + string = "a\u2022c\n" def reader(): yield string.encode('utf-8') diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index f1febc13e..3052e4d86 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -1,8 +1,7 @@ -# encoding: utf-8 from compose import utils -class TestJsonSplitter(object): +class TestJsonSplitter: def test_json_splitter_no_object(self): data = '{"foo": "bar' @@ -17,7 +16,7 @@ class TestJsonSplitter(object): assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') -class TestStreamAsText(object): +class TestStreamAsText: def test_stream_with_non_utf_unicode_character(self): stream = [b'\xed\xf3\xf3'] @@ -25,12 +24,12 @@ class TestStreamAsText(object): assert output == '���' def test_stream_with_utf_character(self): - stream = ['ěĝ'.encode('utf-8')] + stream = ['ěĝ'.encode()] output, = utils.stream_as_text(stream) assert output == 'ěĝ' -class TestJsonStream(object): +class TestJsonStream: def test_with_falsy_entries(self): stream = [ @@ -59,7 +58,7 @@ class TestJsonStream(object): ] -class TestParseBytes(object): +class TestParseBytes: def test_parse_bytes(self): assert utils.parse_bytes('123kb') == 123 * 1024 assert utils.parse_bytes(123) == 123 @@ -67,7 +66,7 @@ class TestParseBytes(object): assert utils.parse_bytes('123') == 123 -class TestMoreItertools(object): +class TestMoreItertools: def test_unique_everseen(self): unique = utils.unique_everseen assert list(unique([2, 1, 2, 1])) == [2, 1] diff --git a/tests/unit/volume_test.py b/tests/unit/volume_test.py index 8b2f6cfee..0dfbfcd40 100644 --- a/tests/unit/volume_test.py +++ b/tests/unit/volume_test.py @@ -10,7 +10,7 @@ def mock_client(): return mock.create_autospec(docker.APIClient) -class TestVolume(object): +class TestVolume: def test_remove_local_volume(self, mock_client): vol = volume.Volume(mock_client, 'foo', 'project') From 46624cee75c76ef58b648ecf43727e1f3ce23719 Mon Sep 17 00:00:00 2001 From: alexrecuenco Date: Tue, 11 Aug 2020 17:38:22 +0700 Subject: [PATCH 05/27] Suggestions by @ulyssessouza Removed unused versions, (we only support python3.4 onwards) Signed-off-by: alexrecuenco --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 6041fce15..590e0ebdc 100644 --- a/setup.py +++ b/setup.py @@ -49,10 +49,7 @@ if sys.version_info[:2] < (3, 4): tests_require.append('mock >= 1.0.1, < 4') extras_require = { - ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'], ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5, < 4'], - ':python_version < "3.3"': ['backports.shutil_get_terminal_size == 1.0.0', - 'ipaddress >= 1.0.16, < 2'], ':sys_platform == "win32"': ['colorama >= 0.4, < 1'], 'socks': ['PySocks >= 1.5.6, != 1.5.7, < 2'], 'tests': tests_require, @@ -92,7 +89,7 @@ setup( install_requires=install_requires, extras_require=extras_require, tests_require=tests_require, - python_requires='>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.4', entry_points={ 'console_scripts': ['docker-compose=compose.cli.main:main'], }, From f2a43d755e351a415fe426ad92c9ee75387ef51d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2020 15:04:11 +0200 Subject: [PATCH 06/27] Bump virtualenv from 20.0.29 to 20.0.30 (#7657) * Bump virtualenv from 20.0.29 to 20.0.30 Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.0.29 to 20.0.30. - [Release notes](https://github.com/pypa/virtualenv/releases) - [Changelog](https://github.com/pypa/virtualenv/blob/master/docs/changelog.rst) - [Commits](https://github.com/pypa/virtualenv/compare/20.0.29...20.0.30) Signed-off-by: dependabot-preview[bot] * Bump virtualenv version in all files Signed-off-by: aiordache Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> Co-authored-by: aiordache Co-authored-by: Anca Iordache --- Dockerfile | 2 +- requirements-indirect.txt | 2 +- script/build/windows.ps1 | 2 +- script/setup/osx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index fb6b41487..7c14bc1d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ ENTRYPOINT ["sh", "/usr/local/bin/docker-compose-entrypoint.sh"] COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker WORKDIR /code/ # FIXME(chris-crone): virtualenv 16.3.0 breaks build, force 16.2.0 until fixed -RUN pip install virtualenv==20.0.29 +RUN pip install virtualenv==20.0.30 RUN pip install tox==3.19.0 COPY requirements-indirect.txt . diff --git a/requirements-indirect.txt b/requirements-indirect.txt index 1f028ca3e..b25f3c811 100644 --- a/requirements-indirect.txt +++ b/requirements-indirect.txt @@ -24,5 +24,5 @@ smmap==3.0.4 smmap2==3.0.1 toml==0.10.1 tox==3.19.0 -virtualenv==20.0.29 +virtualenv==20.0.30 wcwidth==0.2.5 diff --git a/script/build/windows.ps1 b/script/build/windows.ps1 index b01feb750..2778cc884 100644 --- a/script/build/windows.ps1 +++ b/script/build/windows.ps1 @@ -16,7 +16,7 @@ # # 4. In Powershell, run the following commands: # -# $ pip install 'virtualenv==20.0.29' +# $ pip install 'virtualenv==20.0.30' # $ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned # # 5. Clone the repository: diff --git a/script/setup/osx b/script/setup/osx index 25b13ded2..44ec4adcc 100755 --- a/script/setup/osx +++ b/script/setup/osx @@ -36,7 +36,7 @@ if ! [ -x "$(command -v python3)" ]; then brew install python3 fi if ! [ -x "$(command -v virtualenv)" ]; then - pip3 install virtualenv==20.0.29 + pip3 install virtualenv==20.0.30 fi # From 454f5125d93d4c1928ba660aae04431319315703 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2020 13:08:20 +0000 Subject: [PATCH 07/27] Bump cffi from 1.14.0 to 1.14.1 Bumps [cffi](https://github.com/python-cffi/release-doc) from 1.14.0 to 1.14.1. - [Release notes](https://github.com/python-cffi/release-doc/releases) - [Commits](https://github.com/python-cffi/release-doc/commits) Signed-off-by: dependabot-preview[bot] --- requirements-indirect.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-indirect.txt b/requirements-indirect.txt index 490701bcf..ef302dcdf 100644 --- a/requirements-indirect.txt +++ b/requirements-indirect.txt @@ -2,7 +2,7 @@ altgraph==0.17 appdirs==1.4.4 attrs==19.3.0 bcrypt==3.1.7 -cffi==1.14.0 +cffi==1.14.1 cryptography==3.0 distlib==0.3.1 entrypoints==0.3 From 7009370bf2054d67aafa4bd124014b0cae7e2cbf Mon Sep 17 00:00:00 2001 From: ulyssessouza Date: Tue, 11 Aug 2020 16:46:27 +0200 Subject: [PATCH 08/27] Recover ./script/release/release.py Signed-off-by: ulyssessouza --- script/release/release.py | 126 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 script/release/release.py diff --git a/script/release/release.py b/script/release/release.py new file mode 100644 index 000000000..f53d1f3c1 --- /dev/null +++ b/script/release/release.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +from __future__ import absolute_import +from __future__ import unicode_literals + +import re + +import click +from git import Repo +from utils import update_init_py_version +from utils import update_run_sh_version +from utils import yesno + +VALID_VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(-rc\d+)?$") + + +class Version(str): + def matching_groups(self): + match = VALID_VERSION_PATTERN.match(self) + if not match: + return False + + return match.groups() + + def is_ga_version(self): + groups = self.matching_groups() + if not groups: + return False + + rc_suffix = groups[1] + return not rc_suffix + + def validate(self): + return len(self.matching_groups()) > 0 + + def branch_name(self): + if not self.validate(): + return None + + rc_part = self.matching_groups()[0] + ver = self + if rc_part: + ver = ver[:-len(rc_part)] + + tokens = ver.split(".") + tokens[-1] = 'x' + + return ".".join(tokens) + + +def create_bump_commit(repository, version): + print('Creating bump commit...') + repository.commit('-a', '-s', '-m "Bump {}"'.format(version), '--no-verify') + + +def validate_environment(version, repository): + if not version.validate(): + print('Version "{}" has an invalid format. This should follow D+.D+.D+(-rcD+). ' + 'Like: 1.26.0 or 1.26.0-rc1'.format(version)) + return False + + expected_branch = version.branch_name() + if str(repository.active_branch) != expected_branch: + print('Cannot tag in this branch with version "{}". ' + 'Please checkout "{}" to tag'.format(version, version.branch_name())) + return False + return True + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.argument('version') +def tag(version): + """ + Updates the version related files and tag + """ + repo = Repo(".") + version = Version(version) + if not validate_environment(version, repo): + return + + update_init_py_version(version) + update_run_sh_version(version) + + input('Please add the release notes to the CHANGELOG.md file, then press Enter to continue.') + proceed = False + while not proceed: + print(repo.git.diff()) + proceed = yesno('Are these changes ok? y/N ', default=False) + + if repo.git.diff(): + create_bump_commit(repo.git, version) + else: + print('No changes to commit. Exiting...') + return + + repo.create_tag(version) + + print('Please, check the changes. If everything is OK, you just need to push with:\n' + '$ git push --tags upstream {}'.format(version.branch_name())) + + +@cli.command() +@click.argument('version') +def push_latest(version): + """ + TODO Pushes the latest tag pointing to a certain GA version + """ + raise NotImplementedError + + +@cli.command() +@click.argument('version') +def ghtemplate(version): + """ + TODO Generates the github release page content + """ + version = Version(version) + raise NotImplementedError + + +if __name__ == '__main__': + cli() From c90ba119f59e2c87b09f1938abba42d68b8501dc Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 11 Aug 2020 17:22:06 +0200 Subject: [PATCH 09/27] Fix tox failures Signed-off-by: aiordache --- compose/config/interpolation.py | 18 +++++++++--------- script/release/release.py | 3 --- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 71e78bbaa..72cb484b1 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -86,17 +86,17 @@ def recursive_interpolate(obj, interpolator, config_path): class TemplateWithDefaults(Template): pattern = r""" - %(delim)s(?: - (?P%(delim)s) | - (?P%(id)s) | - {(?P%(bid)s)} | + {delim}(?: + (?P{delim}) | + (?P{id}) | + {{(?P{bid})}} | (?P) ) - """ % { - 'delim': re.escape('$'), - 'id': r'[_a-z][_a-z0-9]*', - 'bid': r'[_a-z][_a-z0-9]*(?:(?P:?[-?])[^}]*)?', - } + """.format( + delim=re.escape('$'), + id=r'[_a-z][_a-z0-9]*', + bid=r'[_a-z][_a-z0-9]*(?:(?P:?[-?])[^}]*)?', + ) @staticmethod def process_braced_group(braced, sep, mapping): diff --git a/script/release/release.py b/script/release/release.py index f53d1f3c1..c8e5e7f76 100644 --- a/script/release/release.py +++ b/script/release/release.py @@ -1,7 +1,4 @@ #!/usr/bin/env python3 -from __future__ import absolute_import -from __future__ import unicode_literals - import re import click From f3928aac7e9c2c580c13aef1fdb5a995d8d9ff4b Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 11 Aug 2020 19:12:15 +0200 Subject: [PATCH 10/27] Set agent in Release.Jenkinsfile Signed-off-by: aiordache --- Release.Jenkinsfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Release.Jenkinsfile b/Release.Jenkinsfile index be3d935d9..52574b4c7 100644 --- a/Release.Jenkinsfile +++ b/Release.Jenkinsfile @@ -37,6 +37,9 @@ pipeline { } } stage('Test') { + agent { + label 'linux && docker && ubuntu-2004' + } steps { // TODO use declarative 1.5.0 `matrix` once available on CI script { From 096d938bacbc5797a6ca053f43122e1ad3c2e2b5 Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 11 Aug 2020 20:48:37 +0200 Subject: [PATCH 11/27] Update jenkins node filter Signed-off-by: aiordache --- Release.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Release.Jenkinsfile b/Release.Jenkinsfile index 52574b4c7..1594d2f27 100644 --- a/Release.Jenkinsfile +++ b/Release.Jenkinsfile @@ -244,7 +244,7 @@ def buildImage(baseImage) { def runTests(dockerVersion, pythonVersion, baseImage) { return { stage("python=${pythonVersion} docker=${dockerVersion} ${baseImage}") { - node("linux") { + node("linux && docker && ubuntu-2004") { def scmvar = checkout(scm) def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}" def storageDriver = sh(script: "docker info -f \'{{.Driver}}\'", returnStdout: true).trim() From 35f1334cbd34074c271d0e3b1932a9a3e860ac66 Mon Sep 17 00:00:00 2001 From: Ryosuke TOKUAMI Date: Sun, 9 Aug 2020 20:03:24 +0900 Subject: [PATCH 12/27] Use docker cli on run when the envvar passed. Make docker-compose run pass the the cli option to project.up to build images using docker cli considering COMPOSE_DOCKER_CLI_BUILD environment variable. Signed-off-by: Ryosuke TOKUAMI --- compose/cli/main.py | 2 ++ tests/unit/cli_test.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/compose/cli/main.py b/compose/cli/main.py index 8809318f7..b07b2b728 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -1298,6 +1298,7 @@ def build_one_off_container_options(options, detach, command): def run_one_off_container(container_options, project, service, options, toplevel_options, toplevel_environment): + native_builder = toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD') detach = options.get('--detach') use_network_aliases = options.get('--use-aliases') containers = project.up( @@ -1306,6 +1307,7 @@ def run_one_off_container(container_options, project, service, options, toplevel strategy=ConvergenceStrategy.never, detached=detach, rescale=False, + cli=native_builder, one_off=True, override_options=container_options, ) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index f1a3e270d..bcd8123d4 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -220,6 +220,55 @@ class CLITestCase(unittest.TestCase): assert not mock_client.create_host_config.call_args[1].get('restart_policy') + @mock.patch('compose.project.Project.up') + @mock.patch.dict(os.environ) + def test_run_up_with_docker_cli_build(self, mock_project_up): + os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '1' + mock_client = mock.create_autospec(docker.APIClient) + mock_client.api_version = DEFAULT_DOCKER_API_VERSION + mock_client._general_configs = {} + container = Container(mock_client, { + 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8', + 'Name': 'composetest_service_37b35', + 'Config': { + 'Labels': { + LABEL_SERVICE: 'service', + } + }, + }, has_been_inspected=True) + mock_project_up.return_value = [container] + + project = Project.from_config( + name='composetest', + config_data=build_config({ + 'service': {'image': 'busybox'} + }), + client=mock_client, + ) + + command = TopLevelCommand(project) + command.run({ + 'SERVICE': 'service', + 'COMMAND': None, + '-e': [], + '--label': [], + '--user': None, + '--no-deps': None, + '--detach': True, + '-T': None, + '--entrypoint': None, + '--service-ports': None, + '--use-aliases': None, + '--publish': [], + '--volume': [], + '--rm': None, + '--name': None, + '--workdir': None, + }) + + _, _, call_kwargs = mock_project_up.mock_calls[0] + assert call_kwargs.get('cli') + def test_command_manual_and_service_ports_together(self): project = Project.from_config( name='composetest', From d773719060967929e1300fb440c9c236e2a15060 Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 17 Aug 2020 11:59:23 +0200 Subject: [PATCH 13/27] set scale default to 1 on deploy Signed-off-by: aiordache --- compose/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/project.py b/compose/project.py index 0ae5721bc..a5bb39b93 100644 --- a/compose/project.py +++ b/compose/project.py @@ -318,6 +318,8 @@ class Project: ) if replicas: scale = replicas + if scale is None: + return 1 # deploy may contain placement constraints introduced in v3.8 max_replicas = deploy_dict.get('placement', {}).get( 'max_replicas_per_node', From 2b4d409ac3da2096c43284e262f2b0c1b66c488b Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 17 Aug 2020 18:58:12 +0200 Subject: [PATCH 14/27] Update schema and fix memory limit parsing Signed-off-by: aiordache --- compose/config/config_schema_compose_spec.json | 7 ++++--- compose/config/interpolation.py | 1 + requirements.txt | 3 ++- tests/acceptance/cli_test.py | 4 ++-- tests/fixtures/v3-full/docker-compose.yml | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/compose/config/config_schema_compose_spec.json b/compose/config/config_schema_compose_spec.json index 3b26ba825..8af7faa63 100644 --- a/compose/config/config_schema_compose_spec.json +++ b/compose/config/config_schema_compose_spec.json @@ -258,7 +258,7 @@ "patternProperties": {"^x-": {}} }, "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, + "mem_limit": {"type": "string"}, "mem_reservation": {"type": ["string", "integer"]}, "mem_swappiness": {"type": "integer"}, "memswap_limit": {"type": ["number", "string"]}, @@ -503,7 +503,7 @@ "limits": { "type": "object", "properties": { - "cpus": {"type": "string"}, + "cpus": {"type": "number", "minimum": 0}, "memory": {"type": "string"} }, "additionalProperties": false, @@ -512,7 +512,7 @@ "reservations": { "type": "object", "properties": { - "cpus": {"type": "string"}, + "cpus": {"type": "number", "minimum": 0}, "memory": {"type": "string"}, "generic_resources": {"$ref": "#/definitions/generic_resources"} }, @@ -633,6 +633,7 @@ "patternProperties": {"^x-": {}} }, "internal": {"type": "boolean"}, + "enable_ipv6": {"type": "boolean"}, "attachable": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 72cb484b1..832344e2c 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -241,6 +241,7 @@ class ConversionMap: service_path('healthcheck', 'disable'): to_boolean, service_path('deploy', 'labels', PATH_JOKER): to_str, service_path('deploy', 'replicas'): to_int, + service_path('deploy', 'resources', 'limits', "cpus"): to_float, service_path('deploy', 'update_config', 'parallelism'): to_int, service_path('deploy', 'update_config', 'max_failure_ratio'): to_float, service_path('deploy', 'rollback_config', 'parallelism'): to_int, diff --git a/requirements.txt b/requirements.txt index b99542861..28ecf8f6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,11 @@ certifi==2020.6.20 chardet==3.0.4 colorama==0.4.3; sys_platform == 'win32' distro==1.5.0 -docker==4.3.0 docker-pycreds==0.4.0 dockerpty==0.4.1 docopt==0.6.2 +# temporary fix for the mem_limit float parsing +git+git://github.com/docker/docker-py@2c522fb362247a692c0493f0b47a33988eb2f3e3#egg=docker idna==2.10 ipaddress==1.0.23 jsonschema==3.2.0 diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 4dd935210..ced0a2733 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -525,11 +525,11 @@ services: }, 'resources': { 'limits': { - 'cpus': '0.05', + 'cpus': 0.05, 'memory': '50M', }, 'reservations': { - 'cpus': '0.01', + 'cpus': 0.01, 'memory': '20M', }, }, diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml index 3a7ac25c9..0a5156582 100644 --- a/tests/fixtures/v3-full/docker-compose.yml +++ b/tests/fixtures/v3-full/docker-compose.yml @@ -14,10 +14,10 @@ services: max_failure_ratio: 0.3 resources: limits: - cpus: '0.05' + cpus: 0.05 memory: 50M reservations: - cpus: '0.01' + cpus: 0.01 memory: 20M restart_policy: condition: on-failure From 6cebde77fbd7e7779f5d5d72e9e9f06382020772 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Wed, 19 Aug 2020 22:32:25 +0200 Subject: [PATCH 15/27] Parse network-mode on CLI build Signed-off-by: Ulysses Souza --- compose/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/service.py b/compose/service.py index 5980310b3..471f9e199 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1845,6 +1845,7 @@ class _CLIBuilder: command_builder.add_flag("--force-rm", forcerm) command_builder.add_params("--label", labels) command_builder.add_arg("--memory", container_limits.get("memory")) + command_builder.add_arg("--network", network_mode) command_builder.add_flag("--no-cache", nocache) command_builder.add_arg("--progress", self._progress) command_builder.add_flag("--pull", pull) From 81ce72c95244cebd3711d9b81899e600803d6816 Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 19 Aug 2020 20:11:42 +0200 Subject: [PATCH 16/27] Use docker-py's default api version for engine queries Bump docker-py version to 4.3.1 Signed-off-by: aiordache --- compose/cli/command.py | 5 +---- compose/config/config.py | 23 ----------------------- requirements.txt | 3 +-- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index 8882727ba..d471e78df 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -6,7 +6,6 @@ from . import errors from .. import config from .. import parallel from ..config.environment import Environment -from ..const import API_VERSIONS from ..const import LABEL_CONFIG_FILES from ..const import LABEL_ENVIRONMENT_FILE from ..const import LABEL_WORKING_DIR @@ -127,9 +126,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, ) config_data = config.load(config_details, interpolate) - api_version = environment.get( - 'COMPOSE_API_VERSION', - API_VERSIONS[config_data.version]) + api_version = environment.get('COMPOSE_API_VERSION') client = get_client( verbose=verbose, version=api_version, context=context, environment=environment diff --git a/compose/config/config.py b/compose/config/config.py index 8f5790215..881f5d683 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -367,27 +367,6 @@ def find_candidates_in_parent_dirs(filenames, path): return (candidates, path) -def check_swarm_only_config(service_dicts): - warning_template = ( - "Some services ({services}) use the '{key}' key, which will be ignored. " - "Compose does not support '{key}' configuration - use " - "`docker stack deploy` to deploy to a swarm." - ) - - def check_swarm_only_key(service_dicts, key): - services = [s for s in service_dicts if s.get(key)] - if services: - log.warning( - warning_template.format( - services=", ".join(sorted(s['name'] for s in services)), - key=key - ) - ) - - check_swarm_only_key(service_dicts, 'deploy') - check_swarm_only_key(service_dicts, 'configs') - - def load(config_details, interpolate=True): """Load the configuration from a working directory and a list of configuration files. Files are loaded in order, and merged on top @@ -424,8 +403,6 @@ def load(config_details, interpolate=True): for service_dict in service_dicts: match_named_volumes(service_dict, volumes) - check_swarm_only_config(service_dicts) - version = main_file.version return Config(version, service_dicts, volumes, networks, secrets, configs) diff --git a/requirements.txt b/requirements.txt index 28ecf8f6e..7de88eefc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,10 @@ certifi==2020.6.20 chardet==3.0.4 colorama==0.4.3; sys_platform == 'win32' distro==1.5.0 +docker==4.3.1 docker-pycreds==0.4.0 dockerpty==0.4.1 docopt==0.6.2 -# temporary fix for the mem_limit float parsing -git+git://github.com/docker/docker-py@2c522fb362247a692c0493f0b47a33988eb2f3e3#egg=docker idna==2.10 ipaddress==1.0.23 jsonschema==3.2.0 From 5bbd67016651704fc3d1597a7c2e358f2f4e59c2 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Fri, 21 Aug 2020 16:42:00 +0200 Subject: [PATCH 17/27] Update tests to use the version on docker client Signed-off-by: Ulysses Souza --- tests/unit/cli/docker_client_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 941aed4f4..74e0f9c8f 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -6,6 +6,7 @@ import docker import pytest import compose +from compose import const from compose.cli import errors from compose.cli.docker_client import docker_client from compose.cli.docker_client import get_tls_version @@ -23,18 +24,18 @@ class DockerClientTestCase(unittest.TestCase): del os.environ['HOME'] except KeyError: pass - docker_client(os.environ) + docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) @mock.patch.dict(os.environ) def test_docker_client_with_custom_timeout(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ) + client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) assert client.timeout == 123 @mock.patch.dict(os.environ) def test_custom_timeout_error(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ) + client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) with mock.patch('compose.cli.errors.log') as fake_log: with pytest.raises(errors.ConnectionError): @@ -54,7 +55,7 @@ class DockerClientTestCase(unittest.TestCase): assert '123' in fake_log.error.call_args[0][0] def test_user_agent(self): - client = docker_client(os.environ) + client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) expected = "docker-compose/{} docker-py/{} {}/{}".format( compose.__version__, docker.__version__, From c447ff8c2ce00c3d5b84a5c99877aab879a846a6 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Fri, 21 Aug 2020 18:11:27 +0200 Subject: [PATCH 18/27] Update API version for docker client Signed-off-by: Ulysses Souza --- tests/unit/cli/docker_client_test.py | 9 +++++---- tests/unit/cli_test.py | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 74e0f9c8f..5413effda 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -4,6 +4,7 @@ import ssl import docker import pytest +from docker.constants import DEFAULT_DOCKER_API_VERSION import compose from compose import const @@ -24,18 +25,18 @@ class DockerClientTestCase(unittest.TestCase): del os.environ['HOME'] except KeyError: pass - docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) + docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) @mock.patch.dict(os.environ) def test_docker_client_with_custom_timeout(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) assert client.timeout == 123 @mock.patch.dict(os.environ) def test_custom_timeout_error(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) with mock.patch('compose.cli.errors.log') as fake_log: with pytest.raises(errors.ConnectionError): @@ -55,7 +56,7 @@ class DockerClientTestCase(unittest.TestCase): assert '123' in fake_log.error.call_args[0][0] def test_user_agent(self): - client = docker_client(os.environ, version=const.API_VERSIONS[const.COMPOSE_SPEC]) + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) expected = "docker-compose/{} docker-py/{} {}/{}".format( compose.__version__, docker.__version__, diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index bcd8123d4..03522d5fa 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -20,6 +20,7 @@ from compose.const import IS_WINDOWS_PLATFORM from compose.const import LABEL_SERVICE from compose.container import Container from compose.project import Project +from compose.config.environment import Environment class CLITestCase(unittest.TestCase): @@ -77,7 +78,9 @@ class CLITestCase(unittest.TestCase): def test_get_project(self): base_dir = 'tests/fixtures/longer-filename-composefile' - project = get_project(base_dir) + env = Environment.from_env_file(base_dir) + env['COMPOSE_API_VERSION'] = DEFAULT_DOCKER_API_VERSION + project = get_project(base_dir, environment=env) assert project.name == 'longer-filename-composefile' assert project.client assert project.services From dff3ce28f49e2d0a572da93d5e092d157d8cf0f0 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Fri, 21 Aug 2020 19:24:12 +0200 Subject: [PATCH 19/27] Fix flake8 Signed-off-by: Ulysses Souza --- tests/unit/cli/docker_client_test.py | 1 - tests/unit/cli_test.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 5413effda..307e47f1b 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -7,7 +7,6 @@ import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION import compose -from compose import const from compose.cli import errors from compose.cli.docker_client import docker_client from compose.cli.docker_client import get_tls_version diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 03522d5fa..fa6e76747 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -16,11 +16,11 @@ from compose.cli.command import get_project_name from compose.cli.docopt_command import NoSuchCommand from compose.cli.errors import UserError from compose.cli.main import TopLevelCommand +from compose.config.environment import Environment from compose.const import IS_WINDOWS_PLATFORM from compose.const import LABEL_SERVICE from compose.container import Container from compose.project import Project -from compose.config.environment import Environment class CLITestCase(unittest.TestCase): From 76c92b01d455b56324f5241acc81d3471178c10f Mon Sep 17 00:00:00 2001 From: Erfan Gholamian Date: Tue, 18 Aug 2020 19:59:37 +0430 Subject: [PATCH 20/27] Check stderr when building with docker cli. Signed-off-by: Erfan Gholamian --- compose/service.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compose/service.py b/compose/service.py index 471f9e199..9d96a4abe 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1856,7 +1856,9 @@ class _CLIBuilder: magic_word = "Successfully built " appear = False - with subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) as p: + with subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) as p: while True: line = p.stdout.readline() if not line: @@ -1865,6 +1867,10 @@ class _CLIBuilder: appear = True yield json.dumps({"stream": line}) + err = p.stderr.readline().strip() + if err: + raise StreamOutputError(err) + with open(iidfile) as f: line = f.readline() image_id = line.split(":")[1].strip() From 76963e44add9810c1d906b5fbd052dc3fb480479 Mon Sep 17 00:00:00 2001 From: Erfan Gholamian Date: Fri, 21 Aug 2020 19:32:37 +0430 Subject: [PATCH 21/27] Added integration test for build error when building with docker cli Signed-off-by: Erfan Gholamian --- tests/integration/service_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 985c4d77a..e11616491 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -36,6 +36,7 @@ from compose.parallel import ParallelStreamWriter from compose.project import OneOffFilter from compose.project import Project from compose.service import BuildAction +from compose.service import BuildError from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import IpcMode @@ -987,6 +988,26 @@ class ServiceTest(DockerClientTestCase): image = self.client.inspect_image('composetest_web') assert image['Config']['Labels']['com.docker.compose.test'] + def test_build_cli_with_build_error(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('\n'.join([ + "FROM busybox", + "RUN exit 2", + ])) + service = self.create_service('web', + build={ + 'context': base_dir, + 'labels': {'com.docker.compose.test': 'true'}}, + ) + with pytest.raises(BuildError) as excinfo: + service.build(cli=True) + + reason = excinfo.value.reason + assert "The command '/bin/sh -c exit 2' returned a non-zero code: 2" == reason + def test_up_build_cli(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) From da0e18305256e73e1406b8767491173705b6340d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 21:23:43 +0000 Subject: [PATCH 22/27] Bump pytest-cov from 2.10.0 to 2.10.1 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.0 to 2.10.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.10.0...v2.10.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1695e98e2..8655e5c0a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,4 @@ gitpython==3.1.7 mock==3.0.5 pytest==6.0.1; python_version >= '3.5' pytest==4.6.5; python_version < '3.5' -pytest-cov==2.10.0 +pytest-cov==2.10.1 From e9b93d706fd612dbd262cccaed40e9a9370ea36d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 21:23:25 +0000 Subject: [PATCH 23/27] Bump attrs from 19.3.0 to 20.1.0 Bumps [attrs](https://github.com/python-attrs/attrs) from 19.3.0 to 20.1.0. - [Release notes](https://github.com/python-attrs/attrs/releases) - [Changelog](https://github.com/python-attrs/attrs/blob/master/CHANGELOG.rst) - [Commits](https://github.com/python-attrs/attrs/compare/19.3.0...20.1.0) Signed-off-by: dependabot-preview[bot] --- requirements-indirect.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-indirect.txt b/requirements-indirect.txt index ef302dcdf..9d9ae3684 100644 --- a/requirements-indirect.txt +++ b/requirements-indirect.txt @@ -1,6 +1,6 @@ altgraph==0.17 appdirs==1.4.4 -attrs==19.3.0 +attrs==20.1.0 bcrypt==3.1.7 cffi==1.14.1 cryptography==3.0 From 9ee6b17d9c3ed7f507873c7c88b9d6c0b21ad485 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 31 Aug 2020 21:40:11 +0200 Subject: [PATCH 24/27] Add Anca to Maintainers Signed-off-by: Ulysses Souza --- .github/CODEOWNERS | 2 +- MAINTAINERS | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 13fb9bac0..85ab9015f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,4 @@ # # KEEP THIS FILE SORTED. Order is important. Last match takes precedence. -* @ndeloof @rumpl @ulyssessouza +* @aiordache @ndeloof @rumpl @ulyssessouza diff --git a/MAINTAINERS b/MAINTAINERS index 273f724ec..7e178147e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11,6 +11,7 @@ [Org] [Org."Core maintainers"] people = [ + "aiordache", "ndeloof", "rumpl", "ulyssessouza", @@ -53,6 +54,11 @@ Email = "aanand.prasad@gmail.com" GitHub = "aanand" + [people.aiordache] + Name = "Anca Iordache" + Email = "anca.iordache@docker.com" + GitHub = "aiordache" + [people.bfirsh] Name = "Ben Firshman" Email = "ben@firshman.co.uk" From 827c68fe6f9c86657952832d83f03704531468e4 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 31 Aug 2020 15:17:56 +0200 Subject: [PATCH 25/27] Fix bump of docker-py on `setup.py` Signed-off-by: Ulysses Souza --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 590e0ebdc..e0d4340e5 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires = [ 'texttable >= 0.9.0, < 2', 'websocket-client >= 0.32.0, < 1', 'distro >= 1.5.0, < 2', - 'docker[ssh] >= 4.2.2, < 5', + 'docker[ssh] >= 4.3.1, < 5', 'dockerpty >= 0.4.1, < 1', 'jsonschema >= 2.5.1, < 4', 'python-dotenv >= 0.13.0, < 1', From 3b17b3c2c09e468a1d6bf4db9a6e15cde21c48a0 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 31 Aug 2020 21:16:18 +0200 Subject: [PATCH 26/27] Fix stderr on returncode is different of 0 Signed-off-by: Ulysses Souza --- compose/cli/main.py | 7 +++++-- compose/service.py | 7 +++---- tests/integration/service_test.py | 5 +---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index b07b2b728..84a5ab088 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -73,13 +73,16 @@ def main(): log.error(e.msg) sys.exit(1) except BuildError as e: - log.error("Service '{}' failed to build: {}".format(e.service.name, e.reason)) + reason = "" + if e.reason: + reason = " : " + e.reason + log.error("Service '{}' failed to build{}".format(e.service.name, reason)) sys.exit(1) except StreamOutputError as e: log.error(e) sys.exit(1) except NeedsBuildError as e: - log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name) + log.error("Service '{}' needs to be built, but --no-build was passed.".format(e.service.name)) sys.exit(1) except NoSuchCommand as e: commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand))) diff --git a/compose/service.py b/compose/service.py index 9d96a4abe..70939cac7 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1857,7 +1857,6 @@ class _CLIBuilder: magic_word = "Successfully built " appear = False with subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) as p: while True: line = p.stdout.readline() @@ -1867,9 +1866,9 @@ class _CLIBuilder: appear = True yield json.dumps({"stream": line}) - err = p.stderr.readline().strip() - if err: - raise StreamOutputError(err) + p.communicate() + if p.returncode != 0: + raise StreamOutputError() with open(iidfile) as f: line = f.readline() diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index e11616491..efb1fd5fa 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -1002,12 +1002,9 @@ class ServiceTest(DockerClientTestCase): 'context': base_dir, 'labels': {'com.docker.compose.test': 'true'}}, ) - with pytest.raises(BuildError) as excinfo: + with pytest.raises(BuildError): service.build(cli=True) - reason = excinfo.value.reason - assert "The command '/bin/sh -c exit 2' returned a non-zero code: 2" == reason - def test_up_build_cli(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) From 134319c73504633b60a917606e3430a738010906 Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 1 Sep 2020 14:31:18 +0200 Subject: [PATCH 27/27] Pass context to docker cli Signed-off-by: aiordache --- compose/cli/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compose/cli/main.py b/compose/cli/main.py index 84a5ab088..acac12246 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -1439,6 +1439,7 @@ def call_docker(args, dockeropts, environment): key = dockeropts.get('--tlskey') verify = dockeropts.get('--tlsverify') host = dockeropts.get('--host') + context = dockeropts.get('--context') tls_options = [] if tls: tls_options.append('--tls') @@ -1454,6 +1455,10 @@ def call_docker(args, dockeropts, environment): tls_options.extend( ['--host', re.sub(r'^https?://', 'tcp://', host.lstrip('='))] ) + if context: + tls_options.extend( + ['--context', context] + ) args = [executable_path] + tls_options + args log.debug(" ".join(map(pipes.quote, args)))