From 444d88872059ab87b41e5416682b7793e8845c8e Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Fri, 23 Jun 2017 15:17:38 +0200 Subject: [PATCH] Add a flag --no-ansi to remove control characters on parallel executions Signed-off-by: Cecile Tonglet --- compose/cli/command.py | 5 +++-- compose/cli/main.py | 1 + compose/parallel.py | 45 ++++++++++++++++++++++++------------- compose/project.py | 32 ++++++++++++++++---------- compose/service.py | 6 +++++ tests/unit/parallel_test.py | 16 +++++++++++++ 6 files changed, 75 insertions(+), 30 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index e1ae690c0..f5330d1c2 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -31,6 +31,7 @@ def project_from_options(project_dir, options): get_config_path_from_options(project_dir, options, environment), project_name=options.get('--project-name'), verbose=options.get('--verbose'), + noansi=options.get('--no-ansi'), host=host, tls_config=tls_config_from_options(options), environment=environment, @@ -81,7 +82,7 @@ def get_client(environment, verbose=False, version=None, tls_config=None, host=N def get_project(project_dir, config_path=None, project_name=None, verbose=False, - host=None, tls_config=None, environment=None, override_dir=None): + noansi=False, host=None, tls_config=None, environment=None, override_dir=None): if not environment: environment = Environment.from_env_file(project_dir) config_details = config.find(project_dir, config_path, environment, override_dir) @@ -100,7 +101,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, ) with errors.handle_connection_errors(client): - return Project.from_config(project_name, config_data, client) + return Project.from_config(project_name, config_data, client, noansi=noansi) def get_project_name(working_dir, project_name=None, environment=None): diff --git a/compose/cli/main.py b/compose/cli/main.py index 20f3b55b4..c0cf8747f 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -159,6 +159,7 @@ class TopLevelCommand(object): -f, --file FILE Specify an alternate compose file (default: docker-compose.yml) -p, --project-name NAME Specify an alternate project name (default: directory name) --verbose Show more output + --no-ansi Do not print ANSI control characters -v, --version Print version and exit -H, --host HOST Daemon socket to connect to diff --git a/compose/parallel.py b/compose/parallel.py index a611fd6e0..89d074e35 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) STOP = object() -def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None): +def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None, noansi=False): """Runs func on objects in parallel while ensuring that func is ran on object only after it is ran on all its dependencies. @@ -36,7 +36,7 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None): objects = list(objects) stream = get_output_stream(sys.stderr) - writer = ParallelStreamWriter(stream, msg) + writer = ParallelStreamWriter(stream, msg, noansi) for obj in objects: writer.add_object(get_name(obj)) writer.write_initial() @@ -221,11 +221,12 @@ class ParallelStreamWriter(object): to jump to the correct line, and write over the line. """ - def __init__(self, stream, msg): + def __init__(self, stream, msg, noansi): self.stream = stream self.msg = msg self.lines = [] self.width = 0 + self.noansi = noansi def add_object(self, obj_index): self.lines.append(obj_index) @@ -239,9 +240,7 @@ class ParallelStreamWriter(object): width=self.width)) self.stream.flush() - def write(self, obj_index, status): - if self.msg is None: - return + def _write_ansi(self, obj_index, status): position = self.lines.index(obj_index) diff = len(self.lines) - position # move up @@ -254,27 +253,41 @@ class ParallelStreamWriter(object): self.stream.write("%c[%dB" % (27, diff)) self.stream.flush() + def _write_noansi(self, obj_index, status): + self.stream.write("{} {:<{width}} ... {}\r\n".format(self.msg, obj_index, + status, width=self.width)) + self.stream.flush() -def parallel_operation(containers, operation, options, message): + def write(self, obj_index, status): + if self.msg is None: + return + if self.noansi: + self._write_noansi(obj_index, status) + else: + self._write_ansi(obj_index, status) + + +def parallel_operation(containers, operation, options, message, noansi=False): parallel_execute( containers, operator.methodcaller(operation, **options), operator.attrgetter('name'), - message) + message, + noansi=noansi) -def parallel_remove(containers, options): +def parallel_remove(containers, options, noansi=False): stopped_containers = [c for c in containers if not c.is_running] - parallel_operation(stopped_containers, 'remove', options, 'Removing') + parallel_operation(stopped_containers, 'remove', options, 'Removing', noansi=noansi) -def parallel_pause(containers, options): - parallel_operation(containers, 'pause', options, 'Pausing') +def parallel_pause(containers, options, noansi=False): + parallel_operation(containers, 'pause', options, 'Pausing', noansi=noansi) -def parallel_unpause(containers, options): - parallel_operation(containers, 'unpause', options, 'Unpausing') +def parallel_unpause(containers, options, noansi=False): + parallel_operation(containers, 'unpause', options, 'Unpausing', noansi=noansi) -def parallel_kill(containers, options): - parallel_operation(containers, 'kill', options, 'Killing') +def parallel_kill(containers, options, noansi=False): + parallel_operation(containers, 'kill', options, 'Killing', noansi=noansi) diff --git a/compose/project.py b/compose/project.py index 28af45c71..9ea6ff6bb 100644 --- a/compose/project.py +++ b/compose/project.py @@ -60,13 +60,15 @@ class Project(object): """ A collection of services. """ - def __init__(self, name, services, client, networks=None, volumes=None, config_version=None): + def __init__(self, name, services, client, networks=None, volumes=None, config_version=None, + noansi=False): self.name = name self.services = services self.client = client self.volumes = volumes or ProjectVolumes({}) self.networks = networks or ProjectNetworks({}, False) self.config_version = config_version + self.noansi = noansi def labels(self, one_off=OneOffFilter.exclude): labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)] @@ -75,7 +77,7 @@ class Project(object): return labels @classmethod - def from_config(cls, name, config_data, client): + def from_config(cls, name, config_data, client, noansi=False): """ Construct a Project from a config.Config object. """ @@ -86,7 +88,7 @@ class Project(object): networks, use_networking) volumes = ProjectVolumes.from_config(name, config_data, client) - project = cls(name, [], client, project_networks, volumes, config_data.version) + project = cls(name, [], client, project_networks, volumes, config_data.version, noansi=noansi) for service_dict in config_data.services: service_dict = dict(service_dict) @@ -126,6 +128,7 @@ class Project(object): volumes_from=volumes_from, secrets=secrets, pid_mode=pid_mode, + noansi=noansi, **service_dict) ) @@ -270,7 +273,8 @@ class Project(object): start_service, operator.attrgetter('name'), 'Starting', - get_deps) + get_deps, + noansi=self.noansi) return containers @@ -288,25 +292,26 @@ class Project(object): self.build_container_operation_with_timeout_func('stop', options), operator.attrgetter('name'), 'Stopping', - get_deps) + get_deps, + noansi=self.noansi) def pause(self, service_names=None, **options): containers = self.containers(service_names) - parallel.parallel_pause(reversed(containers), options) + parallel.parallel_pause(reversed(containers), options, noansi=self.noansi) return containers def unpause(self, service_names=None, **options): containers = self.containers(service_names) - parallel.parallel_unpause(containers, options) + parallel.parallel_unpause(containers, options, noansi=self.noansi) return containers def kill(self, service_names=None, **options): - parallel.parallel_kill(self.containers(service_names), options) + parallel.parallel_kill(self.containers(service_names), options, noansi=self.noansi) def remove_stopped(self, service_names=None, one_off=OneOffFilter.exclude, **options): parallel.parallel_remove(self.containers( service_names, stopped=True, one_off=one_off - ), options) + ), options, noansi=self.noansi) def down(self, remove_image_type, include_volumes, remove_orphans=False): self.stop(one_off=OneOffFilter.include) @@ -331,7 +336,8 @@ class Project(object): containers, self.build_container_operation_with_timeout_func('restart', options), operator.attrgetter('name'), - 'Restarting') + 'Restarting', + noansi=self.noansi) return containers def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, build_args=None): @@ -447,7 +453,8 @@ class Project(object): do, operator.attrgetter('name'), None, - get_deps + get_deps, + noansi=self.noansi, ) if errors: raise ProjectError( @@ -500,7 +507,8 @@ class Project(object): pull_service, operator.attrgetter('name'), 'Pulling', - limit=5) + limit=5, + noansi=self.noansi) else: for service in services: service.pull(ignore_pull_failures, silent=silent) diff --git a/compose/service.py b/compose/service.py index c43f635b2..22aae08b7 100644 --- a/compose/service.py +++ b/compose/service.py @@ -158,6 +158,7 @@ class Service(object): secrets=None, scale=None, pid_mode=None, + noansi=False, **options ): self.name = name @@ -171,6 +172,7 @@ class Service(object): self.networks = networks or {} self.secrets = secrets or [] self.scale_num = scale or 1 + self.noansi = noansi self.options = options def __repr__(self): @@ -393,6 +395,7 @@ class Service(object): lambda n: create_and_start(self, n), lambda n: self.get_container_name(n), "Creating", + noansi=self.noansi, ) for error in errors.values(): raise OperationFailedError(error) @@ -414,6 +417,7 @@ class Service(object): recreate, lambda c: c.name, "Recreating", + noansi=self.noansi, ) for error in errors.values(): raise OperationFailedError(error) @@ -434,6 +438,7 @@ class Service(object): lambda c: self.start_container_if_stopped(c, attach_logs=not detached), lambda c: c.name, "Starting", + noansi=self.noansi, ) for error in errors.values(): @@ -455,6 +460,7 @@ class Service(object): stop_and_remove, lambda c: c.name, "Stopping and removing", + noansi=self.noansi, ) def execute_convergence_plan(self, plan, timeout=None, detached=False, diff --git a/tests/unit/parallel_test.py b/tests/unit/parallel_test.py index 73728fdfd..519c66669 100644 --- a/tests/unit/parallel_test.py +++ b/tests/unit/parallel_test.py @@ -130,3 +130,19 @@ def test_parallel_execute_alignment(capsys): _, err = capsys.readouterr() a, b = err.split('\n')[:2] assert a.index('...') == b.index('...') + + +def test_parallel_execute_alignment_noansi(capsys): + results, errors = parallel_execute( + objects=["short", "a very long name"], + func=lambda x: x, + get_name=six.text_type, + msg="Aligning", + noansi=True, + ) + + assert errors == {} + + _, err = capsys.readouterr() + a, b, c, d = err.split('\n')[:4] + assert a.index('...') == b.index('...') == c.index('...') == d.index('...')