Răsfoiți Sursa

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

Signed-off-by: Cecile Tonglet <[email protected]>
Cecile Tonglet 8 ani în urmă
părinte
comite
b0b671dbf2
6 a modificat fișierele cu 78 adăugiri și 33 ștergeri
  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. 9 3
      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),
         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):

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

+ 29 - 16
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 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(
         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)

+ 20 - 12
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)

+ 9 - 3
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):
@@ -392,7 +394,8 @@ class Service(object):
                 range(i, i + scale),
                 lambda n: create_and_start(self, n),
                 lambda n: self.get_container_name(n),
-                "Creating"
+                "Creating",
+                noansi=self.noansi,
             )
             for error in errors.values():
                 raise OperationFailedError(error)
@@ -413,7 +416,8 @@ class Service(object):
                 containers,
                 recreate,
                 lambda c: c.name,
-                "Recreating"
+                "Recreating",
+                noansi=self.noansi,
             )
             for error in errors.values():
                 raise OperationFailedError(error)
@@ -433,7 +437,8 @@ class Service(object):
                     containers,
                     lambda c: self.start_container_if_stopped(c, attach_logs=not detached),
                     lambda c: c.name,
-                    "Starting"
+                    "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,

+ 16 - 0
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('...')