Skip to content
Commits on Source (6)
q2cli (2019.7.0-1) UNRELEASED; urgency=medium
* Team upload.
* New upstream version
* debhelper-compat 12
* Standards-Version: 4.4.0
TODO: Needs most probably qiime (>= 2019.7.0)
-- Andreas Tille <tille@debian.org> Fri, 02 Aug 2019 23:13:13 +0200
q2cli (2019.4.0-1) unstable; urgency=medium
* New upstream version
......
Source: q2cli
Section: science
Priority: optional
Maintainer: Debian Med Packaging Team <debian-med-packaging@lists.alioth.debian.org>
Uploaders: Liubov Chuprikova <chuprikovalv@gmail.com>
Build-Depends: debhelper (>= 12~),
Section: science
Priority: optional
Build-Depends: debhelper-compat (= 12),
dh-python,
python3,
python3-setuptools,
qiime,
python3-click,
python3-nose
Standards-Version: 4.3.0
Standards-Version: 4.4.0
Vcs-Browser: https://salsa.debian.org/med-team/q2cli
Vcs-Git: https://salsa.debian.org/med-team/q2cli.git
Homepage: https://qiime2.org/
......
......@@ -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
......
......@@ -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.")
@dev.command(name='import-theme',
short_help='Install new command line theme.',
help=import_theme_help,
cls=ToolCommand)
@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'))
@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)
@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()
@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)
@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.')
......@@ -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)
......@@ -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)
......@@ -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'
......
......@@ -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:
......
......@@ -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
......
......@@ -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):
......
# ----------------------------------------------------------------------------
# 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()
......@@ -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):
......
......@@ -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()
# ----------------------------------------------------------------------------
# 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)
......@@ -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,
......