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), get_config_path_from_options(project_dir, options, environment),
project_name=options.get('--project-name'), project_name=options.get('--project-name'),
verbose=options.get('--verbose'), verbose=options.get('--verbose'),
noansi=options.get('--no-ansi'),
host=host, host=host,
tls_config=tls_config_from_options(options), tls_config=tls_config_from_options(options),
environment=environment, 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, 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: if not environment:
environment = Environment.from_env_file(project_dir) environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment, override_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): 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): 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) -f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name (default: directory name) -p, --project-name NAME Specify an alternate project name (default: directory name)
--verbose Show more output --verbose Show more output
--no-ansi Do not print ANSI control characters
-v, --version Print version and exit -v, --version Print version and exit
-H, --host HOST Daemon socket to connect to -H, --host HOST Daemon socket to connect to

View File

@ -26,7 +26,7 @@ log = logging.getLogger(__name__)
STOP = object() 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 """Runs func on objects in parallel while ensuring that func is
ran on object only after it is ran on all its dependencies. 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) objects = list(objects)
stream = get_output_stream(sys.stderr) stream = get_output_stream(sys.stderr)
writer = ParallelStreamWriter(stream, msg) writer = ParallelStreamWriter(stream, msg, noansi)
for obj in objects: for obj in objects:
writer.add_object(get_name(obj)) writer.add_object(get_name(obj))
writer.write_initial() writer.write_initial()
@ -221,11 +221,12 @@ class ParallelStreamWriter(object):
to jump to the correct line, and write over the line. 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.stream = stream
self.msg = msg self.msg = msg
self.lines = [] self.lines = []
self.width = 0 self.width = 0
self.noansi = noansi
def add_object(self, obj_index): def add_object(self, obj_index):
self.lines.append(obj_index) self.lines.append(obj_index)
@ -239,9 +240,7 @@ class ParallelStreamWriter(object):
width=self.width)) width=self.width))
self.stream.flush() self.stream.flush()
def write(self, obj_index, status): def _write_ansi(self, obj_index, status):
if self.msg is None:
return
position = self.lines.index(obj_index) position = self.lines.index(obj_index)
diff = len(self.lines) - position diff = len(self.lines) - position
# move up # move up
@ -254,27 +253,41 @@ class ParallelStreamWriter(object):
self.stream.write("%c[%dB" % (27, diff)) self.stream.write("%c[%dB" % (27, diff))
self.stream.flush() 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( parallel_execute(
containers, containers,
operator.methodcaller(operation, **options), operator.methodcaller(operation, **options),
operator.attrgetter('name'), 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] 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): def parallel_pause(containers, options, noansi=False):
parallel_operation(containers, 'pause', options, 'Pausing') parallel_operation(containers, 'pause', options, 'Pausing', noansi=noansi)
def parallel_unpause(containers, options): def parallel_unpause(containers, options, noansi=False):
parallel_operation(containers, 'unpause', options, 'Unpausing') parallel_operation(containers, 'unpause', options, 'Unpausing', noansi=noansi)
def parallel_kill(containers, options): def parallel_kill(containers, options, noansi=False):
parallel_operation(containers, 'kill', options, 'Killing') parallel_operation(containers, 'kill', options, 'Killing', noansi=noansi)

View File

@ -60,13 +60,15 @@ class Project(object):
""" """
A collection of services. 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.name = name
self.services = services self.services = services
self.client = client self.client = client
self.volumes = volumes or ProjectVolumes({}) self.volumes = volumes or ProjectVolumes({})
self.networks = networks or ProjectNetworks({}, False) self.networks = networks or ProjectNetworks({}, False)
self.config_version = config_version self.config_version = config_version
self.noansi = noansi
def labels(self, one_off=OneOffFilter.exclude): def labels(self, one_off=OneOffFilter.exclude):
labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)] labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)]
@ -75,7 +77,7 @@ class Project(object):
return labels return labels
@classmethod @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. Construct a Project from a config.Config object.
""" """
@ -86,7 +88,7 @@ class Project(object):
networks, networks,
use_networking) use_networking)
volumes = ProjectVolumes.from_config(name, config_data, client) 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: for service_dict in config_data.services:
service_dict = dict(service_dict) service_dict = dict(service_dict)
@ -126,6 +128,7 @@ class Project(object):
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets, secrets=secrets,
pid_mode=pid_mode, pid_mode=pid_mode,
noansi=noansi,
**service_dict) **service_dict)
) )
@ -270,7 +273,8 @@ class Project(object):
start_service, start_service,
operator.attrgetter('name'), operator.attrgetter('name'),
'Starting', 'Starting',
get_deps) get_deps,
noansi=self.noansi)
return containers return containers
@ -288,25 +292,26 @@ class Project(object):
self.build_container_operation_with_timeout_func('stop', options), self.build_container_operation_with_timeout_func('stop', options),
operator.attrgetter('name'), operator.attrgetter('name'),
'Stopping', 'Stopping',
get_deps) get_deps,
noansi=self.noansi)
def pause(self, service_names=None, **options): def pause(self, service_names=None, **options):
containers = self.containers(service_names) containers = self.containers(service_names)
parallel.parallel_pause(reversed(containers), options) parallel.parallel_pause(reversed(containers), options, noansi=self.noansi)
return containers return containers
def unpause(self, service_names=None, **options): def unpause(self, service_names=None, **options):
containers = self.containers(service_names) containers = self.containers(service_names)
parallel.parallel_unpause(containers, options) parallel.parallel_unpause(containers, options, noansi=self.noansi)
return containers return containers
def kill(self, service_names=None, **options): 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): def remove_stopped(self, service_names=None, one_off=OneOffFilter.exclude, **options):
parallel.parallel_remove(self.containers( parallel.parallel_remove(self.containers(
service_names, stopped=True, one_off=one_off service_names, stopped=True, one_off=one_off
), options) ), options, noansi=self.noansi)
def down(self, remove_image_type, include_volumes, remove_orphans=False): def down(self, remove_image_type, include_volumes, remove_orphans=False):
self.stop(one_off=OneOffFilter.include) self.stop(one_off=OneOffFilter.include)
@ -331,7 +336,8 @@ class Project(object):
containers, containers,
self.build_container_operation_with_timeout_func('restart', options), self.build_container_operation_with_timeout_func('restart', options),
operator.attrgetter('name'), operator.attrgetter('name'),
'Restarting') 'Restarting',
noansi=self.noansi)
return containers return containers
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, build_args=None): 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, do,
operator.attrgetter('name'), operator.attrgetter('name'),
None, None,
get_deps get_deps,
noansi=self.noansi,
) )
if errors: if errors:
raise ProjectError( raise ProjectError(
@ -500,7 +507,8 @@ class Project(object):
pull_service, pull_service,
operator.attrgetter('name'), operator.attrgetter('name'),
'Pulling', 'Pulling',
limit=5) limit=5,
noansi=self.noansi)
else: else:
for service in services: for service in services:
service.pull(ignore_pull_failures, silent=silent) service.pull(ignore_pull_failures, silent=silent)

View File

@ -158,6 +158,7 @@ class Service(object):
secrets=None, secrets=None,
scale=None, scale=None,
pid_mode=None, pid_mode=None,
noansi=False,
**options **options
): ):
self.name = name self.name = name
@ -171,6 +172,7 @@ class Service(object):
self.networks = networks or {} self.networks = networks or {}
self.secrets = secrets or [] self.secrets = secrets or []
self.scale_num = scale or 1 self.scale_num = scale or 1
self.noansi = noansi
self.options = options self.options = options
def __repr__(self): def __repr__(self):
@ -393,6 +395,7 @@ class Service(object):
lambda n: create_and_start(self, n), lambda n: create_and_start(self, n),
lambda n: self.get_container_name(n), lambda n: self.get_container_name(n),
"Creating", "Creating",
noansi=self.noansi,
) )
for error in errors.values(): for error in errors.values():
raise OperationFailedError(error) raise OperationFailedError(error)
@ -414,6 +417,7 @@ class Service(object):
recreate, recreate,
lambda c: c.name, lambda c: c.name,
"Recreating", "Recreating",
noansi=self.noansi,
) )
for error in errors.values(): for error in errors.values():
raise OperationFailedError(error) raise OperationFailedError(error)
@ -434,6 +438,7 @@ class Service(object):
lambda c: self.start_container_if_stopped(c, attach_logs=not detached), lambda c: self.start_container_if_stopped(c, attach_logs=not detached),
lambda c: c.name, lambda c: c.name,
"Starting", "Starting",
noansi=self.noansi,
) )
for error in errors.values(): for error in errors.values():
@ -455,6 +460,7 @@ class Service(object):
stop_and_remove, stop_and_remove,
lambda c: c.name, lambda c: c.name,
"Stopping and removing", "Stopping and removing",
noansi=self.noansi,
) )
def execute_convergence_plan(self, plan, timeout=None, detached=False, 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() _, err = capsys.readouterr()
a, b = err.split('\n')[:2] a, b = err.split('\n')[:2]
assert a.index('...') == b.index('...') 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('...')