Browse Source

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

Signed-off-by: Cecile Tonglet <[email protected]>
Cecile Tonglet 8 years ago
parent
commit
444d888720
6 changed files with 75 additions and 30 deletions
  1. 3 2
      compose/cli/command.py
  2. 1 0
      compose/cli/main.py
  3. 29 16
      compose/parallel.py
  4. 20 12
      compose/project.py
  5. 6 0
      compose/service.py
  6. 16 0
      tests/unit/parallel_test.py

+ 3 - 2
compose/cli/command.py

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

+ 1 - 0
compose/cli/main.py

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

+ 29 - 16
compose/parallel.py

@@ -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):
-        if self.msg is None:
-            return
+    def _write_ansi(self, obj_index, status):
         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 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):
+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):
-    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)

+ 20 - 12
compose/project.py

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

+ 6 - 0
compose/service.py

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

+ 16 - 0
tests/unit/parallel_test.py

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