Beispiel #1
0
    def __init__(self, *args, **kwargs):
        import re
        import sys

        unicodes = ["\u2018", "\u2019", "\u201C", "\u201D", "\u2014", "\u2013"]
        category_regex = re.compile(r'--m-(\S+)-category')

        invalid_chars = []
        categories = []
        for command in sys.argv:
            if any(x in command for x in unicodes):
                invalid_chars.append(command)

            match = category_regex.fullmatch(command)
            if match is not None:
                param_name, = match.groups()
                # Maps old-style option name to new name.
                categories.append((command, '--m-%s-column' % param_name))

        if invalid_chars or categories:
            if invalid_chars:
                msg = ("Error: Detected invalid character in: %s\nVerify the "
                       "correct quotes or dashes (ASCII) are being used." %
                       ', '.join(invalid_chars))
                click.echo(CONFIG.cfg_style('error', msg), err=True)
            if categories:
                old_to_new_names = '\n'.join('Instead of %s, trying using %s' %
                                             (old, new)
                                             for old, new in categories)
                msg = ("Error: The following options no longer exist because "
                       "metadata *categories* are now called metadata "
                       "*columns* in QIIME 2.\n\n%s" % old_to_new_names)
                click.echo(CONFIG.cfg_style('error', msg), err=True)
            sys.exit(-1)

        super().__init__(*args, **kwargs)

        # Plugin state for current deployment that will be loaded from cache.
        # Used to construct the dynamic CLI.
        self._plugins = None
Beispiel #2
0
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)
    except (zipfile.BadZipFile, ValueError):
        raise click.BadParameter(
            '%s is not a valid QIIME 2 Result. Only QIIME 2 Artifacts and '
            'Visualizations can be extracted.' % input_path)
    else:
        success = 'Extracted %s to directory %s' % (input_path, extracted_dir)
        click.echo(CONFIG.cfg_style('success', success))
Beispiel #3
0
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):
            output_format = result.format.__name__
        else:
            output_format = 'Visualization'
        result.export_data(output_path)
    else:
        if isinstance(result, qiime2.sdk.Visualization):
            error = '--output-format cannot be used with visualizations'
            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))
            if os.path.isfile(str(source)):
                if os.path.isfile(output_path):
                    os.remove(output_path)
                elif os.path.dirname(output_path) == '':
                    # This allows the user to pass a filename as a path if they
                    # want their output in the current working directory
                    output_path = os.path.join('.', output_path)
                if os.path.dirname(output_path) != '':
                    # create directory (recursively) if it doesn't exist yet
                    os.makedirs(os.path.dirname(output_path), exist_ok=True)
                qiime2.util.duplicate(str(source), output_path)
            else:
                distutils.dir_util.copy_tree(str(source), output_path)

    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.echo(CONFIG.cfg_style('success', success))
Beispiel #4
0
    def _color_important(self, tokens, ctx):
        import re
        from q2cli.core.config import CONFIG

        for t in tokens:
            if '_' in t:
                names = self.get_option_names(ctx)
                if re.sub(r'[^\w]', '', t) in names:
                    m = re.search(r'(\w+)', t)
                    word = t[m.start():m.end()]
                    word = CONFIG.cfg_style('emphasis', word.replace('_', '-'))
                    token = t[:m.start()] + word + t[m.end():]
                    yield token
                    continue
            yield t
Beispiel #5
0
def assert_result_data(input_path, zip_data_path, expression):
    import re
    import q2cli.util
    import qiime2.sdk
    from q2cli.core.config import CONFIG

    q2cli.util.get_plugin_manager()

    try:
        result = qiime2.sdk.Result.load(input_path)
    except Exception as e:
        header = 'There was a problem loading %s as a QIIME 2 result:' % \
                input_path
        q2cli.util.exit_with_error(e, header=header)

    try:
        hits = sorted(result._archiver.data_dir.glob(zip_data_path))
        if len(hits) != 1:
            data_dir = result._archiver.data_dir
            all_fps = sorted(data_dir.glob('**/*'))
            all_fps = [x.relative_to(data_dir).name for x in all_fps]
            raise ValueError('Value provided for zip_data_path (%s) did not '
                             'produce exactly one match.\nMatches: %s\n'
                             'Paths observed: %s' %
                             (zip_data_path, hits, all_fps))
    except Exception as e:
        header = 'There was a problem locating the zip_data_path (%s)' % \
                zip_data_path
        q2cli.util.exit_with_error(e, header=header)

    try:
        target = hits[0].read_text()
        match = re.search(expression, target, flags=re.MULTILINE)
        if match is None:
            raise AssertionError('Expression %r not found in %s.' %
                                 (expression, hits[0]))
    except Exception as e:
        header = 'There was a problem finding the expression.'
        q2cli.util.exit_with_error(e, header=header)

    msg = '"%s" was found in %s' % (str(expression), str(zip_data_path))
    click.echo(CONFIG.cfg_style('success', msg))
Beispiel #6
0
    def get_command(self, ctx, name):
        try:
            action = self._action_lookup[name]
        except KeyError:
            from q2cli.util import get_close_matches

            possibilities = get_close_matches(name, self._action_lookup)
            if len(possibilities) == 1:
                hint = '  Did you mean %r?' % possibilities[0]
            elif possibilities:
                hint = '  (Possible commands: %s)' % ', '.join(possibilities)
            else:
                hint = ''

            click.echo(CONFIG.cfg_style(
                'error', "Error: QIIME 2 plugin %r has no "
                "action %r." % (self._plugin['name'], name) + hint),
                       err=True)
            ctx.exit(2)  # Match exit code of `return None`

        return ActionCommand(name, self._plugin, action)
Beispiel #7
0
    def callback(ctx, param, value):
        if not value or ctx.resilient_parsing:
            return

        records = get_citation_records()
        if records:
            import io
            import qiime2.sdk

            citations = qiime2.sdk.Citations([('key%d' % i, r)
                                              for i, r in enumerate(records)])
            with io.StringIO() as fh:
                fh.write('% use `qiime tools citations` on a QIIME 2 result'
                         ' for complete list\n\n')
                citations.save(fh)
                click.echo(fh.getvalue(), nl=False)
            ctx.exit()
        else:
            click.echo(CONFIG.cfg_style('problem', 'No citations found.'),
                       err=True)
            ctx.exit(1)
Beispiel #8
0
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)
    except qiime2.plugin.ValidationError as e:
        header = 'There was a problem importing %s:' % input_path
        q2cli.util.exit_with_error(e, header=header, traceback=None)
    except Exception as e:
        header = 'An unexpected error has occurred:'
        q2cli.util.exit_with_error(e, header=header)
    artifact.save(output_path)
    if input_format is None:
        input_format = artifact.format.__name__

    success = 'Imported %s as %s to %s' % (input_path,
                                           input_format,
                                           output_path)
    click.echo(CONFIG.cfg_style('success', success))
Beispiel #9
0
def citations(path):
    import qiime2.sdk
    import io
    from q2cli.core.config import CONFIG
    ctx = click.get_current_context()

    try:
        result = qiime2.sdk.Result.load(path)
    except Exception as e:
        header = 'There was a problem loading %s as a QIIME 2 result:' % path
        q2cli.util.exit_with_error(e, header=header)

    if result.citations:
        with io.StringIO() as fh:
            result.citations.save(fh)
            click.echo(fh.getvalue(), nl=False)
        ctx.exit(0)
    else:
        click.echo(CONFIG.cfg_style('problem', 'No citations found.'),
                   err=True)
        ctx.exit(1)
Beispiel #10
0
    def __init__(self, name, plugin, action):
        import q2cli.util
        import q2cli.click.type

        self.plugin = plugin
        self.action = action

        self._inputs, self._params, self._outputs = \
            self._build_generated_options()

        self._misc = [
            click.Option(['--output-dir'],
                         type=q2cli.click.type.OutDirType(),
                         help='Output unspecified results to a directory'),
            click.Option(['--verbose / --quiet'],
                         default=None,
                         required=False,
                         help='Display verbose output to stdout and/or stderr '
                         'during execution of this action. Or silence '
                         'output if execution is successful (silence is '
                         'golden).'),
            q2cli.util.example_data_option(self._get_plugin,
                                           self.action['id']),
            q2cli.util.citations_option(self._get_citation_records)
        ]

        options = [*self._inputs, *self._params, *self._outputs, *self._misc]
        help_ = [action['description']]
        if self.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='\n\n'.join(help_))
Beispiel #11
0
def validate(path, level):
    import qiime2.sdk
    from q2cli.core.config import CONFIG

    try:
        result = qiime2.sdk.Result.load(path)
    except Exception as e:
        header = 'There was a problem loading %s as a QIIME 2 Result:' % path
        q2cli.util.exit_with_error(e, header=header)

    try:
        result.validate(level)
    except qiime2.plugin.ValidationError as e:
        header = 'Result %s does not appear to be valid at level=%s:' % (
                path, level)
        q2cli.util.exit_with_error(e, header=header, traceback=None)
    except Exception as e:
        header = ('An unexpected error has occurred while attempting to '
                  'validate result %s:' % path)
        q2cli.util.exit_with_error(e, header=header)
    else:
        click.echo(CONFIG.cfg_style('success', f'Result {path} appears to be '
                                    f'valid at level={level}.'))
Beispiel #12
0
    def get_command(self, ctx, name):
        if name in self._builtin_commands:
            return self._builtin_commands[name]

        try:
            plugin = self._plugin_lookup[name]
        except KeyError:
            from q2cli.util import get_close_matches

            possibilities = get_close_matches(name, self._plugin_lookup)
            if len(possibilities) == 1:
                hint = '  Did you mean %r?' % possibilities[0]
            elif possibilities:
                hint = '  (Possible commands: %s)' % ', '.join(possibilities)
            else:
                hint = ''

            click.echo(CONFIG.cfg_style(
                'error', "Error: QIIME 2 has no "
                "plugin/command named %r." % name + hint),
                       err=True)
            ctx.exit(2)  # Match exit code of `return None`

        return PluginCommand(plugin, name)
Beispiel #13
0
def assert_result_type(input_path, qiime_type):
    import q2cli.util
    import qiime2.sdk
    from q2cli.core.config import CONFIG

    q2cli.util.get_plugin_manager()
    try:
        result = qiime2.sdk.Result.load(input_path)
    except Exception as e:
        header = 'There was a problem loading %s as a QIIME 2 Result:' % \
            input_path
        q2cli.util.exit_with_error(e, header=header)

    if str(result.type) != qiime_type:
        try:
            msg = 'Expected %s, observed %s' % (qiime_type, result.type)
            raise AssertionError(msg)
        except Exception as e:
            header = 'There was a problem asserting the type:'
            q2cli.util.exit_with_error(e, header=header)
    else:
        msg = 'The input file (%s) type and the expected type (%s)' \
              ' match' % (input_path, qiime_type)
        click.echo(CONFIG.cfg_style('success', msg))
Beispiel #14
0
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 '
            'environments. You can view Visualizations (and Artifacts) at '
            'https://view.qiime2.org, or move the Visualization to an '
            'environment with a display and view it with `qiime tools view`.')

    import zipfile
    import qiime2.sdk

    if index_extension.startswith('.'):
        index_extension = index_extension[1:]
    try:
        visualization = qiime2.sdk.Visualization.load(visualization_path)
    # TODO: currently a KeyError is raised if a zipped file that is not a
    # QIIME 2 result is passed. This should be handled better by the framework.
    except (zipfile.BadZipFile, KeyError, TypeError):
        raise click.BadParameter(
            '%s is not a QIIME 2 Visualization. Only QIIME 2 Visualizations '
            'can be viewed.' % visualization_path)

    index_paths = visualization.get_index_paths(relative=False)

    if index_extension not in index_paths:
        raise click.BadParameter(
            '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(CONFIG.cfg_style(
                'error', 'Viewing visualization '
                'failed while attempting to open '
                f'{index_path}'),
                       err=True)
        else:
            while True:
                click.echo(
                    "Press the 'q' key, Control-C, or Control-D to quit. This "
                    "view may no longer be accessible or work correctly after "
                    "quitting.",
                    nl=False)
                # There is currently a bug in click.getchar where translation
                # of Control-C and Control-D into KeyboardInterrupt and
                # EOFError (respectively) does not work on Python 3. The code
                # here should continue to work as expected when the bug is
                # fixed in Click.
                #
                # https://github.com/pallets/click/issues/583
                try:
                    char = click.getchar()
                    click.echo()
                    if char in {'q', '\x03', '\x04'}:
                        break
                except (KeyboardInterrupt, EOFError):
                    break
Beispiel #15
0
    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
        opt_text_secondary = None
        if type(opt_text) is tuple:
            opt_text, opt_text_secondary = opt_text
        help_text, requirements = self._clean_help(help_text)
        type_placement = None
        type_repr = None
        type_indent = 2 * indent_text

        if hasattr(opt.type, 'get_type_repr'):
            type_repr = opt.type.get_type_repr(opt)
            if type_repr is not None:
                if len(type_repr) <= border - len(type_indent):
                    type_placement = 'under'
                else:
                    type_placement = 'beside'

        if len(opt_text) > border:
            lines = simple_wrap(opt_text, full_width)
        else:
            lines = [opt_text.split(' ')]
        if opt_text_secondary is not None:
            lines.append(opt_text_secondary.split(' '))

        to_write = []
        for tokens in lines:
            dangling_edge = formatter.current_indent
            styled = []
            for token in tokens:
                dangling_edge += len(token) + 1
                if token.startswith('--'):
                    token = CONFIG.cfg_style('option',
                                             token,
                                             required=opt.required)
                styled.append(token)
            line = indent_text + ' '.join(styled)
            to_write.append(line)
        formatter.write('\n'.join(to_write))
        dangling_edge -= 1

        if type_placement == 'beside':
            lines = simple_wrap(type_repr,
                                formatter.width - len(type_indent),
                                start_col=dangling_edge - 1)
            to_write = []
            first_iter = True
            for tokens in lines:
                line = ' '.join(tokens)
                if first_iter:
                    dangling_edge += 1 + len(line)
                    line = " " + CONFIG.cfg_style('type', line)
                    first_iter = False
                else:
                    dangling_edge = len(type_indent) + len(line)
                    line = type_indent + CONFIG.cfg_style('type', line)
                to_write.append(line)
            formatter.write('\n'.join(to_write))

        if dangling_edge + 1 > border + COL_SPACING:
            formatter.write('\n')
            left_col = []
        else:
            padding = ' ' * (border + COL_SPACING - dangling_edge)
            formatter.write(padding)
            dangling_edge += len(padding)
            left_col = ['']  # jagged start

        if type_placement == 'under':
            padding = ' ' * (border + COL_SPACING - len(type_repr) -
                             len(type_indent))
            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:
            meta_help = simple_wrap(opt.meta_help,
                                    border - len(type_indent) - 1)
            for idx, line in enumerate([' '.join(t) for t in meta_help]):
                if idx == 0:
                    line = type_indent + '(' + line
                else:
                    line = type_indent + ' ' + line
                if idx == len(meta_help) - 1:
                    line += ')'
                line += ' ' * (border - len(line) + COL_SPACING)
                left_col.append(line)

        right_col = simple_wrap(help_text,
                                formatter.width - border - COL_SPACING)
        right_col = [
            ' '.join(self._color_important(tokens, ctx))
            for tokens in right_col
        ]

        to_write = []
        for left, right in itertools.zip_longest(left_col,
                                                 right_col,
                                                 fillvalue=' ' *
                                                 (border + COL_SPACING)):
            to_write.append(left)
            if right.strip():
                to_write[-1] += right

        formatter.write('\n'.join(to_write))

        if requirements is None:
            formatter.write('\n')
        else:
            if to_write:
                if len(to_write) > 1 or ((not left_col) or left_col[0] != ''):
                    dangling_edge = 0
                dangling_edge += click.formatting.term_len(to_write[-1])
            else:
                pass  # dangling_edge is still correct

            if dangling_edge + 1 + len(requirements) > formatter.width:
                formatter.write('\n')
                pad = formatter.width - len(requirements)
            else:
                pad = formatter.width - len(requirements) - dangling_edge

            formatter.write((' ' * pad) +
                            CONFIG.cfg_style('default_arg', requirements) +
                            '\n')
Beispiel #16
0
 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(CONFIG.cfg_style('command', ctx.command_path),
                           ' '.join(pieces))
Beispiel #17
0
    def __call__(self, **kwargs):
        """Called when user hits return, **kwargs are Dict[click_names, Obj]"""
        import os
        import qiime2.util

        output_dir = kwargs.pop('output_dir')
        verbose = kwargs.pop('verbose')
        if verbose is None:
            verbose = False
            quiet = False
        elif verbose:
            quiet = False
        else:
            quiet = True

        arguments = {}
        init_outputs = {}
        for key, value in kwargs.items():
            prefix, *parts = key.split('_')
            key = '_'.join(parts)

            if prefix == 'o':
                if value is None:
                    value = os.path.join(output_dir, key)
                init_outputs[key] = value
            elif prefix == 'm':
                arguments[key[:-len('_file')]] = value
            else:
                arguments[key] = value

        outputs = self._order_outputs(init_outputs)
        action = self._get_action()
        # `qiime2.util.redirected_stdio` defaults to stdout/stderr when
        # supplied `None`.
        log = None

        if not verbose:
            import tempfile
            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:
            with qiime2.util.redirected_stdio(stdout=log, stderr=log):
                results = action(**arguments)
        except Exception as e:
            header = ('Plugin error from %s:' %
                      q2cli.util.to_cli_name(self.plugin['name']))
            if verbose:
                # log is not a file
                log = 'stderr'
            q2cli.util.exit_with_error(e, header=header, traceback=log)
        else:
            cleanup_logfile = True
        finally:
            # OS X will reap temporary files that haven't been touched in
            # 36 hours, double check that the log is still on the filesystem
            # before trying to delete. Otherwise this will fail and the
            # output won't be written.
            if log and cleanup_logfile and os.path.exists(log.name):
                log.close()
                os.remove(log.name)

        if output_dir is not None:
            os.makedirs(output_dir)

        for result, output in zip(results, outputs):
            path = result.save(output)
            if not quiet:
                click.echo(
                    CONFIG.cfg_style('success',
                                     'Saved %s to: %s' % (result.type, path)))