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
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))
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))
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
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))
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)
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)
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))
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)
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_))
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}.'))
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)
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))
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
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')
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))
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)))