[med-svn] [Git][med-team/q2cli][upstream] New upstream version 2019.7.0

Andreas Tille gitlab at salsa.debian.org
Fri Aug 2 22:31:47 BST 2019



Andreas Tille pushed to branch upstream at Debian Med / q2cli


Commits:
7b94726b by Andreas Tille at 2019-08-02T21:13:12Z
New upstream version 2019.7.0
- - - - -


13 changed files:

- q2cli/_version.py
- q2cli/builtin/dev.py
- q2cli/builtin/tools.py
- q2cli/click/command.py
- q2cli/click/type.py
- q2cli/commands.py
- q2cli/core/cache.py
- q2cli/core/completion.py
- + q2cli/core/config.py
- q2cli/tests/test_cli.py
- q2cli/tests/test_core.py
- + q2cli/tests/test_dev.py
- q2cli/util.py


Changes:

=====================================
q2cli/_version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
     # setup.py/versioneer.py will grep for the variable names, so they must
     # each be defined on a line of their own. _version.py will just call
     # get_keywords().
-    git_refnames = " (tag: 2019.4.0)"
-    git_full = "dc80fad32777035091692ce1083088380a6ac509"
-    git_date = "2019-05-03 04:14:45 +0000"
+    git_refnames = " (tag: 2019.7.0)"
+    git_full = "06b978c96c8efce8be0c8213e744cb4b389f2bc6"
+    git_date = "2019-07-30 18:15:53 +0000"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 


=====================================
q2cli/builtin/dev.py
=====================================
@@ -7,6 +7,7 @@
 # ----------------------------------------------------------------------------
 
 import click
+
 from q2cli.click.command import ToolCommand, ToolGroupCommand
 
 
@@ -29,3 +30,112 @@ def dev():
 def refresh_cache():
     import q2cli.core.cache
     q2cli.core.cache.CACHE.refresh()
+
+
+import_theme_help = \
+    ("Allows for customization of q2cli's command line styling based on an "
+     "imported .theme (INI formatted) file. If you are unfamiliar with .ini "
+     "formatted files look here https://en.wikipedia.org/wiki/INI_file."
+     "\n"
+     "\n"
+     "The .theme file allows you to customize text on the basis of what that "
+     "text represents with the following supported text types: command, "
+     "option, type, default_arg, required, emphasis, problem, warning, error, "
+     "and success. These will be your headers in the '[]' brackets. "
+     "\n"
+     "\n"
+     "`command` refers to the name of the command you issued. `option` refers "
+     "to the arguments you give to the command when running it. `type` refers "
+     "to the QIIME 2 semantic typing of these arguments (where applicable). "
+     "`default_arg` refers to the label next to the argument indicating its "
+     "default value (where applicable), and if it is required (where "
+     "applicable). `required` refers to any arguments that must be passed to "
+     "the command for it to work and gives them special formatting on top of "
+     "your normal `option` formatting. `emphasis` refers to any emphasized "
+     "pieces of text within help text. `problem` refers to the text informing "
+     "you there were issues with the command. `warning` refers to the text "
+     "for non-fatal issues while `error` refers to the text for fatal issues."
+     "`success` refers to text indicating a process completed as expected."
+     "\n"
+     "\n"
+     "Depending on what your terminal supports, some or all of the following "
+     "pieces of the text's formatting may be customized: bold, dim (if true "
+     "the text's brightness is reduced), underline, blink, reverse (if true "
+     "foreground and background colors are reversed), and finally fg "
+     "(foreground color) and bg (background color). The first five may each "
+     "be either true or false, while the colors may be set to any of the "
+     "following: black, red, green, yellow, blue, magenta, cyan, white, "
+     "bright_black, bright_red, bright_green, bright_yellow, bright_blue, "
+     "bright_magenta, bright_cyan, or bright_white.")
+
+
+ at dev.command(name='import-theme',
+             short_help='Install new command line theme.',
+             help=import_theme_help,
+             cls=ToolCommand)
+ at click.option('--theme', required=True,
+              type=click.Path(exists=True, file_okay=True,
+                              dir_okay=False, readable=True),
+              help='Path to file containing new theme info')
+def import_theme(theme):
+    import os
+    import shutil
+    from configparser import Error
+
+    import q2cli.util
+    from q2cli.core.config import CONFIG
+
+    try:
+        CONFIG.parse_file(theme)
+    except Error as e:
+        # If they tried to change [error] in a valid manner before we hit our
+        # parsing error, we don't want to use their imported error settings
+        CONFIG.styles = CONFIG.get_default_styles()
+        header = 'Something went wrong while parsing your theme: '
+        q2cli.util.exit_with_error(e, header=header, traceback=None)
+    shutil.copy(theme, os.path.join(q2cli.util.get_app_dir(),
+                'cli-colors.theme'))
+
+
+ at dev.command(name='export-default-theme',
+             short_help='Export the default settings.',
+             help='Create a .theme (INI formatted) file from the default '
+             'settings at the specified filepath.',
+             cls=ToolCommand)
+ at click.option('--output-path', required=True,
+              type=click.Path(exists=False, file_okay=True,
+                              dir_okay=False, readable=True),
+              help='Path to output the config to')
+def export_default_theme(output_path):
+    import configparser
+    from q2cli.core.config import CONFIG
+
+    parser = configparser.ConfigParser()
+    parser.read_dict(CONFIG.get_default_styles())
+    with open(output_path, 'w') as fh:
+        parser.write(fh)
+
+
+def abort_if_false(ctx, param, value):
+    if not value:
+        ctx.abort()
+
+
+ at dev.command(name='reset-theme',
+             short_help='Reset command line theme to default.',
+             help="Reset command line theme to default. Requres the '--yes' "
+             "parameter to be passed asserting you do want to reset.",
+             cls=ToolCommand)
+ at click.option('--yes', is_flag=True, callback=abort_if_false,
+              expose_value=False,
+              prompt='Are you sure you want to reset your theme?')
+def reset_theme():
+    import os
+    import q2cli.util
+
+    path = os.path.join(q2cli.util.get_app_dir(), 'cli-colors.theme')
+    if os.path.exists(path):
+        os.unlink(path)
+        click.echo('Theme reset.')
+    else:
+        click.echo('Theme was already default.')


=====================================
q2cli/builtin/tools.py
=====================================
@@ -46,6 +46,7 @@ def export_data(input_path, output_path, output_format):
     import qiime2.util
     import qiime2.sdk
     import distutils
+    from q2cli.core.config import CONFIG
     result = qiime2.sdk.Result.load(input_path)
     if output_format is None:
         if isinstance(result, qiime2.sdk.Artifact):
@@ -56,7 +57,7 @@ def export_data(input_path, output_path, output_format):
     else:
         if isinstance(result, qiime2.sdk.Visualization):
             error = '--output-format cannot be used with visualizations'
-            click.secho(error, fg='red', bold=True, err=True)
+            click.echo(CONFIG.cfg_style('error', error), err=True)
             click.get_current_context().exit(1)
         else:
             source = result.view(qiime2.sdk.parse_format(output_format))
@@ -73,7 +74,7 @@ def export_data(input_path, output_path, output_format):
     output_type = 'file' if os.path.isfile(output_path) else 'directory'
     success = 'Exported %s as %s to %s %s' % (input_path, output_format,
                                               output_type, output_path)
-    click.secho(success, fg='green')
+    click.echo(CONFIG.cfg_style('success', success))
 
 
 def show_importable_types(ctx, param, value):
@@ -147,6 +148,7 @@ def show_importable_formats(ctx, param, value):
 def import_data(type, input_path, output_path, input_format):
     import qiime2.sdk
     import qiime2.plugin
+    from q2cli.core.config import CONFIG
     try:
         artifact = qiime2.sdk.Artifact.import_data(type, input_path,
                                                    view_type=input_format)
@@ -163,7 +165,7 @@ def import_data(type, input_path, output_path, input_format):
     success = 'Imported %s as %s to %s' % (input_path,
                                            input_format,
                                            output_path)
-    click.secho(success, fg='green')
+    click.echo(CONFIG.cfg_style('success', success))
 
 
 @tools.command(short_help='Take a peek at a QIIME 2 Artifact or '
@@ -176,16 +178,17 @@ def import_data(type, input_path, output_path, input_format):
                 metavar=_COMBO_METAVAR)
 def peek(path):
     import qiime2.sdk
+    from q2cli.core.config import CONFIG
 
     metadata = qiime2.sdk.Result.peek(path)
 
-    click.secho("UUID:        ", fg="green", nl=False)
-    click.secho(metadata.uuid)
-    click.secho("Type:        ", fg="green", nl=False)
-    click.secho(metadata.type)
+    click.echo(CONFIG.cfg_style('type', "UUID")+":        ", nl=False)
+    click.echo(metadata.uuid)
+    click.echo(CONFIG.cfg_style('type', "Type")+":        ", nl=False)
+    click.echo(metadata.type)
     if metadata.format is not None:
-        click.secho("Data format: ", fg="green", nl=False)
-        click.secho(metadata.format)
+        click.echo(CONFIG.cfg_style('type', "Data format")+": ", nl=False)
+        click.echo(metadata.format)
 
 
 @tools.command('inspect-metadata',
@@ -274,7 +277,7 @@ def _load_metadata(path):
 @tools.command(short_help='View a QIIME 2 Visualization.',
                help="Displays a QIIME 2 Visualization until the command "
                     "exits. To open a QIIME 2 Visualization so it can be "
-                    "used after the command exits, use 'qiime extract'.",
+                    "used after the command exits, use 'qiime tools extract'.",
                cls=ToolCommand)
 @click.argument('visualization-path', metavar='VISUALIZATION',
                 type=click.Path(exists=True, file_okay=True, dir_okay=False,
@@ -285,6 +288,7 @@ def _load_metadata(path):
 def view(visualization_path, index_extension):
     # Guard headless envs from having to import anything large
     import sys
+    from q2cli.core.config import CONFIG
     if not os.getenv("DISPLAY") and sys.platform != "darwin":
         raise click.UsageError(
             'Visualization viewing is currently not supported in headless '
@@ -310,15 +314,16 @@ def view(visualization_path, index_extension):
 
     if index_extension not in index_paths:
         raise click.BadParameter(
-            'No index %s file with is present in the archive. Available index '
+            'No index %s file is present in the archive. Available index '
             'extensions are: %s' % (index_extension,
                                     ', '.join(index_paths.keys())))
     else:
         index_path = index_paths[index_extension]
         launch_status = click.launch(index_path)
         if launch_status != 0:
-            click.echo('Viewing visualization failed while attempting to '
-                       'open %s' % index_path, err=True)
+            click.echo(CONFIG.cfg_style('error', 'Viewing visualization '
+                                        'failed while attempting to open '
+                                        f'{index_path}'), err=True)
         else:
             while True:
                 click.echo(
@@ -362,6 +367,7 @@ def view(visualization_path, index_extension):
 def extract(input_path, output_path):
     import zipfile
     import qiime2.sdk
+    from q2cli.core.config import CONFIG
 
     try:
         extracted_dir = qiime2.sdk.Result.extract(input_path, output_path)
@@ -371,7 +377,7 @@ def extract(input_path, output_path):
             'Visualizations can be extracted.' % input_path)
     else:
         success = 'Extracted %s to directory %s' % (input_path, extracted_dir)
-        click.secho(success, fg='green')
+        click.echo(CONFIG.cfg_style('success', success))
 
 
 @tools.command(short_help='Validate data in a QIIME 2 Artifact.',
@@ -393,6 +399,7 @@ def extract(input_path, output_path):
               default='max', show_default=True)
 def validate(path, level):
     import qiime2.sdk
+    from q2cli.core.config import CONFIG
 
     try:
         result = qiime2.sdk.Result.load(path)
@@ -411,8 +418,8 @@ def validate(path, level):
                   'validate result %s:' % path)
         q2cli.util.exit_with_error(e, header=header)
     else:
-        click.secho('Result %s appears to be valid at level=%s.'
-                    % (path, level), fg="green")
+        click.echo(CONFIG.cfg_style('success', f'Result {path} appears to be '
+                                    f'valid at level={level}.'))
 
 
 @tools.command(short_help='Print citations for a QIIME 2 result.',
@@ -425,6 +432,7 @@ def validate(path, level):
 def citations(path):
     import qiime2.sdk
     import io
+    from q2cli.core.config import CONFIG
     ctx = click.get_current_context()
 
     try:
@@ -439,5 +447,6 @@ def citations(path):
             click.echo(fh.getvalue(), nl=False)
         ctx.exit(0)
     else:
-        click.secho('No citations found.', fg='yellow', err=True)
+        click.echo(CONFIG.cfg_style('problem', 'No citations found.'),
+                   err=True)
         ctx.exit(1)


=====================================
q2cli/click/command.py
=====================================
@@ -38,6 +38,7 @@ class BaseCommandMixin:
     #   c6042bf2607c5be22b1efef2e42a94ffd281434c/click/core.py#L934 >
     # Copyright (c) 2014 by the Pallets team.
     def parse_args(self, ctx, args):
+        from q2cli.core.config import CONFIG
         if isinstance(self, click.MultiCommand):
             return super().parse_args(ctx, args)
 
@@ -71,14 +72,15 @@ class BaseCommandMixin:
                 problems = 'There were some problems with the command:'
             else:
                 problems = 'There was a problem with the command:'
-            click.secho(problems.center(78, ' '), fg='yellow', err=True)
+            click.echo(CONFIG.cfg_style('problem',
+                       problems.center(78, ' ')), err=True)
             for idx, e in enumerate(errors, 1):
                 msg = click.formatting.wrap_text(
                     e.format_message(),
                     initial_indent=' (%d/%d%s) ' % (idx, len(errors),
                                                     '?' if skip_rest else ''),
                     subsequent_indent='  ')
-                click.secho(msg, err=True, fg='red', bold=True)
+                click.secho(CONFIG.cfg_style('error', msg), err=True)
             ctx.exit(1)
 
         ctx.args = args
@@ -113,12 +115,14 @@ class BaseCommandMixin:
     #   /c6042bf2607c5be22b1efef2e42a94ffd281434c/click/core.py#L830 >
     # Copyright (c) 2014 by the Pallets team.
     def format_usage(self, ctx, formatter):
+        from q2cli.core.config import CONFIG
         """Writes the usage line into the formatter."""
         pieces = self.collect_usage_pieces(ctx)
-        formatter.write_usage(_style_command(ctx.command_path),
+        formatter.write_usage(CONFIG.cfg_style('command', ctx.command_path),
                               ' '.join(pieces))
 
     def format_options(self, ctx, formatter, COL_MAX=23, COL_MIN=10):
+        from q2cli.core.config import CONFIG
         # write options
         opt_groups = {}
         records = []
@@ -167,7 +171,7 @@ class BaseCommandMixin:
             rows = []
             for subcommand, cmd in commands:
                 help = cmd.get_short_help_str(limit)
-                rows.append((_style_command(subcommand), help))
+                rows.append((CONFIG.cfg_style('command', subcommand), help))
 
             if rows:
                 with formatter.section(click.style('Commands', bold=True)):
@@ -175,6 +179,7 @@ class BaseCommandMixin:
 
     def write_option(self, ctx, formatter, opt, record, border, COL_SPACING=2):
         import itertools
+        from q2cli.core.config import CONFIG
         full_width = formatter.width - formatter.current_indent
         indent_text = ' ' * formatter.current_indent
         opt_text, help_text = record
@@ -208,7 +213,8 @@ class BaseCommandMixin:
             for token in tokens:
                 dangling_edge += len(token) + 1
                 if token.startswith('--'):
-                    token = _style_option(token, required=opt.required)
+                    token = CONFIG.cfg_style('option', token,
+                                             required=opt.required)
                 styled.append(token)
             line = indent_text + ' '.join(styled)
             to_write.append(line)
@@ -224,11 +230,11 @@ class BaseCommandMixin:
                 line = ' '.join(tokens)
                 if first_iter:
                     dangling_edge += 1 + len(line)
-                    line = " " + _style_type(line)
+                    line = " " + CONFIG.cfg_style('type', line)
                     first_iter = False
                 else:
                     dangling_edge = len(type_indent) + len(line)
-                    line = type_indent + _style_type(line)
+                    line = type_indent + CONFIG.cfg_style('type', line)
                 to_write.append(line)
             formatter.write('\n'.join(to_write))
 
@@ -244,7 +250,8 @@ class BaseCommandMixin:
         if type_placement == 'under':
             padding = ' ' * (border + COL_SPACING
                              - len(type_repr) - len(type_indent))
-            line = ''.join([type_indent, _style_type(type_repr), padding])
+            line = ''.join(
+                [type_indent, CONFIG.cfg_style('type', type_repr), padding])
             left_col.append(line)
 
         if hasattr(opt, 'meta_help') and opt.meta_help is not None:
@@ -290,10 +297,13 @@ class BaseCommandMixin:
             else:
                 pad = formatter.width - len(requirements) - dangling_edge
 
-            formatter.write((' ' * pad) + _style_reqs(requirements) + '\n')
+            formatter.write(
+                (' ' * pad) + CONFIG.cfg_style(
+                    'default_arg', requirements) + '\n')
 
     def _color_important(self, tokens, ctx):
         import re
+        from q2cli.core.config import CONFIG
 
         for t in tokens:
             if '_' in t:
@@ -301,7 +311,7 @@ class BaseCommandMixin:
                 if re.sub(r'[^\w]', '', t) in names:
                     m = re.search(r'(\w+)', t)
                     word = t[m.start():m.end()]
-                    word = _style_emphasis(word.replace('_', '-'))
+                    word = CONFIG.cfg_style('emphasis', word.replace('_', '-'))
                     token = t[:m.start()] + word + t[m.end():]
                     yield token
                     continue
@@ -353,23 +363,3 @@ def simple_wrap(text, target, start_col=0):
             current_width += 1 + token_len
 
     return result
-
-
-def _style_option(text, required=False):
-    return click.style(text, fg='blue', underline=required)
-
-
-def _style_type(text):
-    return click.style(text, fg='green')
-
-
-def _style_reqs(text):
-    return click.style(text, fg='magenta')
-
-
-def _style_command(text):
-    return _style_option(text)
-
-
-def _style_emphasis(text):
-    return click.style(text, underline=True)


=====================================
q2cli/click/type.py
=====================================
@@ -46,6 +46,10 @@ class OutDirType(click.Path):
         return value
 
 
+class ControlFlowException(Exception):
+    pass
+
+
 class QIIME2Type(click.ParamType):
     def __init__(self, type_ast, type_repr, is_output=False):
         self.type_repr = type_repr
@@ -94,14 +98,33 @@ class QIIME2Type(click.ParamType):
 
     def _convert_input(self, value, param, ctx):
         import os
+        import tempfile
         import qiime2.sdk
         import qiime2.sdk.util
 
         try:
-            result = qiime2.sdk.Result.load(value)
-        except Exception:
-            self.fail('%r is not a QIIME 2 Artifact (.qza)' % value,
-                      param, ctx)
+            try:
+                result = qiime2.sdk.Result.load(value)
+            except OSError as e:
+                if e.errno == 28:
+                    temp = tempfile.tempdir
+                    self.fail(f'There was not enough space left on {temp!r} '
+                              f'to extract the artifact {value!r}. '
+                              '(Try setting $TMPDIR to a directory with '
+                              'more space, or increasing the size of '
+                              f'{temp!r})', param, ctx)
+                else:
+                    raise ControlFlowException
+            except ValueError as e:
+                if 'does not exist' in str(e):
+                    self.fail(f'{value!r} is not a valid filepath', param, ctx)
+                else:
+                    raise ControlFlowException
+            except Exception:
+                raise ControlFlowException
+        except ControlFlowException:
+            self.fail('%r is not a QIIME 2 Artifact (.qza)' % value, param,
+                      ctx)
 
         if isinstance(result, qiime2.sdk.Visualization):
             maybe = value[:-1] + 'a'


=====================================
q2cli/commands.py
=====================================
@@ -12,6 +12,8 @@ import q2cli.builtin.dev
 import q2cli.builtin.info
 import q2cli.builtin.tools
 
+from q2cli.core.config import CONFIG
+
 from q2cli.click.command import BaseCommandMixin
 
 
@@ -220,8 +222,13 @@ class ActionCommand(BaseCommandMixin, click.Command):
         ]
 
         options = [*self._inputs, *self._params, *self._outputs, *self._misc]
+        help_ = [action['description']]
+        if self._get_action().deprecated:
+            help_.append(CONFIG.cfg_style(
+                'warning', 'WARNING:\n\nThis command is deprecated and will '
+                           'be removed in a future version of this plugin.'))
         super().__init__(name, params=options, callback=self,
-                         short_help=action['name'], help=action['description'])
+                         short_help=action['name'], help='\n\n'.join(help_))
 
     def _build_generated_options(self):
         import q2cli.click.option
@@ -304,6 +311,15 @@ class ActionCommand(BaseCommandMixin, click.Command):
             log = tempfile.NamedTemporaryFile(prefix='qiime2-q2cli-err-',
                                               suffix='.log',
                                               delete=False, mode='w')
+        if action.deprecated:
+            # We don't need to worry about redirecting this, since it should a)
+            # always be shown to the user and b) the framework-originated
+            # FutureWarning will wind up in the log file in quiet mode.
+
+            msg = ('Plugin warning from %s:\n\n%s is deprecated and '
+                   'will be removed in a future version of this plugin.' %
+                   (q2cli.util.to_cli_name(self.plugin['name']), self.name))
+            click.echo(CONFIG.cfg_style('warning', msg))
 
         cleanup_logfile = False
         try:


=====================================
q2cli/core/cache.py
=====================================
@@ -341,7 +341,8 @@ class DeploymentCache:
         type_repr = repr(type)
         style = qiime2.sdk.util.interrogate_collection_type(type)
 
-        if not qiime2.sdk.util.is_semantic_type(type):
+        if not qiime2.sdk.util.is_semantic_type(type) and \
+                not qiime2.sdk.util.is_union(type):
             if style.style is None:
                 if style.expr.predicate is not None:
                     type_repr = repr(style.expr.predicate)
@@ -380,6 +381,8 @@ class DeploymentCache:
             metavar = 'METADATA'
         elif style.style is not None and style.style != 'simple':
             metavar = 'VALUE'
+        elif qiime2.sdk.util.is_union(type):
+            metavar = 'VALUE'
         else:
             metavar = name_to_var[inner_type.name]
         if (metavar == 'NUMBER' and inner_type is not None


=====================================
q2cli/core/completion.py
=====================================
@@ -51,7 +51,8 @@ def write_bash_completion_script(plugins, path):
     # Make bash completion script executable:
     #   http://stackoverflow.com/a/12792002/3776794
     st = os.stat(path)
-    os.chmod(path, st.st_mode | stat.S_IEXEC)
+    # Set executable bit for user,group,other for root/sudo installs
+    os.chmod(path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
 
 
 def _generate_command_reply(cmd):


=====================================
q2cli/core/config.py
=====================================
@@ -0,0 +1,118 @@
+# ----------------------------------------------------------------------------
+# Copyright (c) 2016-2019, QIIME 2 development team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the file LICENSE, distributed with this software.
+# ----------------------------------------------------------------------------
+
+import os
+import configparser
+
+import click
+
+import q2cli.util
+
+
+class CLIConfig():
+    path = os.path.join(q2cli.util.get_app_dir(), 'cli-colors.theme')
+    VALID_SELECTORS = frozenset(
+        ['option', 'type', 'default_arg', 'command', 'emphasis', 'problem',
+         'warning', 'error', 'required', 'success'])
+    VALID_STYLINGS = frozenset(
+        ['fg', 'bg', 'bold', 'dim', 'underline', 'blink', 'reverse'])
+    VALID_COLORS = frozenset(
+        ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
+         'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
+         'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white'])
+    VALID_BOOLEANS = {'true': True,
+                      'false': False,
+                      't': True,
+                      'f': False}
+
+    def __init__(self):
+        if os.path.exists(self.path):
+            self.styles = self.get_editable_styles()
+            try:
+                self.parse_file(self.path)
+            except Exception as e:
+                click.secho(
+                    "We encountered the following error when parsing your "
+                    f"theme:\n\n{str(e)}\n\nIf you want to use a custom "
+                    "theme, please either import a new theme, or reset your "
+                    "current theme. If you encountered this message while "
+                    "importing a new theme or resetting your current theme, "
+                    "ignore it.",
+                    fg='yellow')
+                self.styles = self.get_default_styles()
+        else:
+            self.styles = self.get_default_styles()
+
+    def get_default_styles(self):
+        return {'option': {'fg': 'blue'},
+                'type': {'fg': 'green'},
+                'default_arg': {'fg': 'magenta'},
+                'command': {'fg': 'blue'},
+                'emphasis': {'underline': True},
+                'problem': {'fg': 'yellow'},
+                'warning': {'fg': 'yellow', 'bold': True},
+                'error': {'fg': 'red', 'bold': True},
+                'required': {'underline': True},
+                'success': {'fg': 'green'}}
+
+    # This maintains the default colors while getting rid of all the default
+    # styling modifiers so what the user puts in their file is all they'll see
+    def get_editable_styles(self):
+        return {'option': {},
+                'type': {},
+                'default_arg': {},
+                'command': {},
+                'emphasis': {},
+                'problem': {},
+                'warning': {},
+                'error': {},
+                'required': {},
+                'success': {}}
+
+    def _build_error(self, current, valid_list, valid_string):
+        valids = ', '.join(valid_list)
+        raise configparser.Error(f'{current!r} is not a {valid_string}. The '
+                                 f'{valid_string}s are:\n{valids}')
+
+    def parse_file(self, fp):
+        if os.path.exists(fp):
+            parser = configparser.ConfigParser()
+            parser.read(fp)
+            for selector_user in parser.sections():
+                selector = selector_user.lower()
+                if selector not in self.VALID_SELECTORS:
+                    self._build_error(selector_user, self.VALID_SELECTORS,
+                                      'valid selector')
+                for styling_user in parser[selector]:
+                    styling = styling_user.lower()
+                    if styling not in self.VALID_STYLINGS:
+                        self._build_error(styling_user, self.VALID_STYLINGS,
+                                          'valid styling')
+                    val_user = parser[selector][styling]
+                    val = val_user.lower()
+                    if styling == 'fg' or styling == 'bg':
+                        if val not in self.VALID_COLORS:
+                            self._build_error(val_user, self.VALID_COLORS,
+                                              'valid color')
+                    else:
+                        if val not in self.VALID_BOOLEANS:
+                            self._build_error(val_user, self.VALID_BOOLEANS,
+                                              'valid boolean')
+                        val = self.VALID_BOOLEANS[val]
+                    self.styles[selector][styling] = val
+        else:
+            raise configparser.Error(f'{fp!r} is not a valid filepath.')
+
+    def cfg_style(self, selector, text, required=False):
+        kwargs = self.styles[selector]
+        if required:
+            kwargs = {**self.styles[selector], **self.styles['required']}
+        return click.style(text, **kwargs)
+
+
+CONFIG = CLIConfig()


=====================================
q2cli/tests/test_cli.py
=====================================
@@ -8,8 +8,11 @@
 
 import os.path
 import unittest
+import unittest.mock
 import tempfile
 import shutil
+import click
+import errno
 
 from click.testing import CliRunner
 from qiime2 import Artifact, Visualization
@@ -19,6 +22,7 @@ from qiime2.core.testing.util import get_dummy_plugin
 from q2cli.builtin.info import info
 from q2cli.builtin.tools import tools
 from q2cli.commands import RootCommand
+from q2cli.click.type import QIIME2Type
 
 
 class CliTests(unittest.TestCase):
@@ -282,6 +286,60 @@ class CliTests(unittest.TestCase):
         self.assertEqual(result.exit_code, 1)
         self.assertIn('Traceback (most recent call last)', result.output)
 
+    def test_input_conversion(self):
+        obj = QIIME2Type(IntSequence1.to_ast(), repr(IntSequence1))
+
+        with self.assertRaisesRegex(click.exceptions.BadParameter,
+                                    f'{self.tempdir!r} is not a QIIME 2 '
+                                    'Artifact'):
+            obj._convert_input(self.tempdir, None, None)
+
+        with self.assertRaisesRegex(click.exceptions.BadParameter,
+                                    "'x' is not a valid filepath"):
+            obj._convert_input('x', None, None)
+
+        # This is to ensure the temp in the regex matches the temp used in the
+        # method under test in type.py
+        temp = tempfile.tempdir
+        with unittest.mock.patch('qiime2.sdk.Result.load',
+                                 side_effect=OSError(errno.ENOSPC,
+                                                     'No space left on '
+                                                     'device')):
+            with self.assertRaisesRegex(click.exceptions.BadParameter,
+                                        f'{temp!r}.*'
+                                        f'{self.artifact1_path!r}.*'
+                                        f'{temp!r}'):
+                obj._convert_input(self.artifact1_path, None, None)
+
+    def test_deprecated_help_text(self):
+        qiime_cli = RootCommand()
+        command = qiime_cli.get_command(ctx=None, name='dummy-plugin')
+
+        result = self.runner.invoke(command, ['deprecated-method', '--help'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertTrue('WARNING' in result.output)
+        self.assertTrue('deprecated' in result.output)
+
+    def test_run_deprecated_gets_warning_msg(self):
+        qiime_cli = RootCommand()
+        command = qiime_cli.get_command(ctx=None, name='dummy-plugin')
+        output_path = os.path.join(self.tempdir, 'output.qza')
+
+        result = self.runner.invoke(
+            command,
+            ['deprecated-method', '--o-out', output_path, '--verbose'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertTrue(os.path.exists(output_path))
+
+        artifact = Artifact.load(output_path)
+
+        # Just make sure that the command ran as expected
+        self.assertEqual(artifact.view(dict), {'foo': '43'})
+
+        self.assertTrue('deprecated' in result.output)
+
 
 class TestOptionalArtifactSupport(unittest.TestCase):
     def setUp(self):


=====================================
q2cli/tests/test_core.py
=====================================
@@ -10,6 +10,7 @@ import os.path
 import shutil
 import tempfile
 import unittest
+import configparser
 
 from click.testing import CliRunner
 from qiime2 import Artifact
@@ -17,9 +18,11 @@ from qiime2.core.testing.type import IntSequence1
 from qiime2.core.testing.util import get_dummy_plugin
 
 import q2cli
+import q2cli.util
 import q2cli.builtin.info
 import q2cli.builtin.tools
 from q2cli.commands import RootCommand
+from q2cli.core.config import CLIConfig
 
 
 class TestOption(unittest.TestCase):
@@ -28,6 +31,9 @@ class TestOption(unittest.TestCase):
         self.runner = CliRunner()
         self.tempdir = tempfile.mkdtemp(prefix='qiime2-q2cli-test-temp-')
 
+        self.parser = configparser.ConfigParser()
+        self.path = os.path.join(q2cli.util.get_app_dir(), 'cli-colors.theme')
+
     def tearDown(self):
         shutil.rmtree(self.tempdir)
 
@@ -114,6 +120,64 @@ class TestOption(unittest.TestCase):
         self.assertTrue(os.path.exists(output_path))
         self.assertEqual(Artifact.load(output_path).view(list), [0, 42, 43])
 
+    def test_config_expected(self):
+        self.parser['type'] = {'underline': 't'}
+        with open(self.path, 'w') as fh:
+            self.parser.write(fh)
+
+        config = CLIConfig()
+        config.parse_file(self.path)
+
+        self.assertEqual(
+            config.styles['type'], {'underline': True})
+
+    def test_config_bad_selector(self):
+        self.parser['tye'] = {'underline': 't'}
+        with open(self.path, 'w') as fh:
+            self.parser.write(fh)
+
+        config = CLIConfig()
+        with self.assertRaisesRegex(
+                configparser.Error, 'tye.*valid selector.*valid selectors'):
+            config.parse_file(self.path)
+
+    def test_config_bad_styling(self):
+        self.parser['type'] = {'underlined': 't'}
+        with open(self.path, 'w') as fh:
+            self.parser.write(fh)
+
+        config = CLIConfig()
+        with self.assertRaisesRegex(
+                configparser.Error, 'underlined.*valid styling.*valid '
+                'stylings'):
+            config.parse_file(self.path)
+
+    def test_config_bad_color(self):
+        self.parser['type'] = {'fg': 'purple'}
+        with open(self.path, 'w') as fh:
+            self.parser.write(fh)
+
+        config = CLIConfig()
+        with self.assertRaisesRegex(
+                configparser.Error, 'purple.*valid color.*valid colors'):
+            config.parse_file(self.path)
+
+    def test_config_bad_boolean(self):
+        self.parser['type'] = {'underline': 'g'}
+        with open(self.path, 'w') as fh:
+            self.parser.write(fh)
+
+        config = CLIConfig()
+        with self.assertRaisesRegex(
+                configparser.Error, 'g.*valid boolean.*valid booleans'):
+            config.parse_file(self.path)
+
+    def test_no_file(self):
+        config = CLIConfig()
+        with self.assertRaisesRegex(
+                configparser.Error, "'Path' is not a valid filepath."):
+            config.parse_file('Path')
+
 
 if __name__ == "__main__":
     unittest.main()


=====================================
q2cli/tests/test_dev.py
=====================================
@@ -0,0 +1,62 @@
+# ----------------------------------------------------------------------------
+# Copyright (c) 2016-2019, QIIME 2 development team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the file LICENSE, distributed with this software.
+# ----------------------------------------------------------------------------
+
+import os
+import unittest
+import tempfile
+import configparser
+
+from click.testing import CliRunner
+
+import q2cli.util
+from q2cli.builtin.dev import dev
+
+
+class TestDev(unittest.TestCase):
+    path = os.path.join(q2cli.util.get_app_dir(), 'cli-colors.theme')
+    old_settings = None
+    if os.path.exists(path):
+        old_settings = configparser.ConfigParser()
+        old_settings.read(path)
+
+    def setUp(self):
+        self.parser = configparser.ConfigParser()
+        self.runner = CliRunner()
+        self.tempdir = tempfile.mkdtemp(prefix='qiime2-q2cli-test-temp-')
+        self.generated_config = os.path.join(self.tempdir, 'generated-theme')
+
+        self.config = os.path.join(self.tempdir, 'good-config.ini')
+        self.parser['type'] = {'underline': 't'}
+        with open(self.config, 'w') as fh:
+            self.parser.write(fh)
+
+    def tearDown(self):
+        if self.old_settings is not None:
+            with open(self.path, 'w') as fh:
+                self.old_settings.write(fh)
+
+    def test_import_theme(self):
+        result = self.runner.invoke(
+            dev, ['import-theme', '--theme', self.config])
+        self.assertEqual(result.exit_code, 0)
+
+    def test_export_default_theme(self):
+        result = self.runner.invoke(
+            dev, ['export-default-theme', '--output-path',
+                  self.generated_config])
+        self.assertEqual(result.exit_code, 0)
+
+    def test_reset_theme(self):
+        result = self.runner.invoke(
+            dev, ['reset-theme', '--yes'])
+        self.assertEqual(result.exit_code, 0)
+
+    def test_reset_theme_no_yes(self):
+        result = self.runner.invoke(
+            dev, ['reset-theme'])
+        self.assertNotEqual(result.exit_code, 0)


=====================================
q2cli/util.py
=====================================
@@ -42,6 +42,7 @@ def exit_with_error(e, header='An error has been encountered:',
     import traceback as tb
     import textwrap
     import click
+    from q2cli.core.config import CONFIG
 
     footer = []  # footer only exists if traceback is set
     tb_file = None
@@ -60,7 +61,7 @@ def exit_with_error(e, header='An error has been encountered:',
 
         tb_file.write('\n')
 
-    click.secho('\n\n'.join(segments), fg='red', bold=True, err=True)
+    click.echo(CONFIG.cfg_style('error', '\n\n'.join(segments)), err=True)
 
     if not footer:
         click.echo(err=True)  # extra newline to look normal
@@ -150,6 +151,7 @@ def convert_primitive(ast):
 
 def citations_option(get_citation_records):
     import click
+    from q2cli.core.config import CONFIG
 
     def callback(ctx, param, value):
         if not value or ctx.resilient_parsing:
@@ -169,7 +171,8 @@ def citations_option(get_citation_records):
                 click.echo(fh.getvalue(), nl=False)
             ctx.exit()
         else:
-            click.secho('No citations found.', fg='yellow', err=True)
+            click.secho(
+                CONFIG.cfg_style('problem', 'No citations found.'), err=True)
             ctx.exit(1)
 
     return click.Option(['--citations'], is_flag=True, expose_value=False,



View it on GitLab: https://salsa.debian.org/med-team/q2cli/commit/7b94726b8b7ea6b86eb410497b3ca308f532703f

-- 
View it on GitLab: https://salsa.debian.org/med-team/q2cli/commit/7b94726b8b7ea6b86eb410497b3ca308f532703f
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20190802/68fc235d/attachment-0001.html>


More information about the debian-med-commit mailing list