Add a flag --no-ansi to remove control characters on parallel executions

Signed-off-by: Cecile Tonglet <cecile.tonglet@gmail.com>
This commit is contained in:
Cecile Tonglet 2017-06-23 15:17:38 +02:00 committed by Joffrey F
parent 7feb2685d2
commit 444d888720
6 changed files with 75 additions and 30 deletions

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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('...')