From b89196ee85d16602fcfb28c04e05774345757509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Wei=C3=9Fenborn?= Date: Fri, 13 Aug 2021 16:27:42 +0200 Subject: [PATCH] Fix/cli0.5 (#765) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Various fixes and improvements to the glotaran command line interface. * Changed CLI save plugin to folder * Added outputformat option to CLI * Added basic test for CLI * Rename CLI entrypoint to main and add more CLI tests * 👌 CLI use same default for non_negative_least_squares as scheme * 👌 CLI dedent pluginlist output * 🩹 CLI fixed result outformat accepting none supported formats Co-authored-by: Joris Snellenburg Co-authored-by: s-weigand --- glotaran/cli/__init__.py | 1 + glotaran/cli/commands/optimize.py | 27 +++++++------- glotaran/cli/commands/pluginlist.py | 16 +++++---- glotaran/cli/commands/test/test_util.py | 10 ++++++ glotaran/cli/commands/util.py | 47 ++++++++++++++++++++++--- glotaran/cli/main.py | 29 +++++++-------- glotaran/cli/test/test_cli.py | 43 ++++++++++++++++++++++ glotaran/project/scheme.py | 2 +- setup.cfg | 2 +- 9 files changed, 138 insertions(+), 39 deletions(-) create mode 100644 glotaran/cli/commands/test/test_util.py create mode 100644 glotaran/cli/test/test_cli.py diff --git a/glotaran/cli/__init__.py b/glotaran/cli/__init__.py index e69de29bb..125ef8fd8 100644 --- a/glotaran/cli/__init__.py +++ b/glotaran/cli/__init__.py @@ -0,0 +1 @@ +from glotaran.cli.main import main diff --git a/glotaran/cli/commands/optimize.py b/glotaran/cli/commands/optimize.py index 643f0d066..6236e0be0 100644 --- a/glotaran/cli/commands/optimize.py +++ b/glotaran/cli/commands/optimize.py @@ -5,8 +5,8 @@ from glotaran.analysis.optimize import optimize from glotaran.cli.commands import util -from glotaran.io import save_result from glotaran.plugin_system.data_io_registration import known_data_formats +from glotaran.plugin_system.project_io_registration import save_result from glotaran.project.scheme import Scheme @@ -32,6 +32,14 @@ help="Path to an output directory.", show_default=True, ) +@click.option( + "--outformat", + "-ofmt", + default="folder", + type=click.Choice(util.project_io_list_supporting_plugins("save_result", ("yml_str"))), + help="The format of the output.", + show_default=True, +) @click.option( "--nfev", "-n", @@ -40,13 +48,14 @@ help="Maximum number of function evaluations.", show_default=True, ) -@click.option("--nnls", is_flag=True, help="Use non-negative least squares.") +@click.option("--nnls", is_flag=True, default=False, help="Use non-negative least squares.") @click.option("--yes", "-y", is_flag=True, help="Don't ask for confirmation.") @util.signature_analysis def optimize_cmd( dataformat: str, data: typing.List[str], out: str, + outformat: str, nfev: int, nnls: bool, yes: bool, @@ -62,7 +71,9 @@ def optimize_cmd( if scheme_file is not None: scheme = util.load_scheme_file(scheme_file, verbose=True) if nfev is not None: - scheme.nfev = nfev + scheme.maximum_number_function_evaluations = nfev + + scheme.non_negative_least_squares = nnls else: if model_file is None: click.echo("Error: Neither scheme nor model specified", err=True) @@ -100,14 +111,6 @@ def optimize_cmd( click.echo(f"Saving directory: is '{out if out is not None else 'None'}'") if yes or click.confirm("Do you want to start optimization?", abort=True, default=True): - # try: - # click.echo('Preparing optimization...', nl=False) - # optimizer = gta.analysis.optimizer.Optimizer(scheme) - # click.echo(' Success') - # except Exception as e: - # click.echo(" Error") - # click.echo(e, err=True) - # sys.exit(1) try: click.echo("Optimizing...") result = optimize(scheme) @@ -123,7 +126,7 @@ def optimize_cmd( try: click.echo(f"Saving directory is '{out}'") if yes or click.confirm("Do you want to save the data?", default=True): - save_result(result_path=out, format_name="yml", result=result) + save_result(result_path=out, format_name=outformat, result=result) click.echo("File saving successful.") except Exception as e: click.echo(f"An error occurred during saving: \n\n{e}", err=True) diff --git a/glotaran/cli/commands/pluginlist.py b/glotaran/cli/commands/pluginlist.py index 58a9e4607..e970de91b 100644 --- a/glotaran/cli/commands/pluginlist.py +++ b/glotaran/cli/commands/pluginlist.py @@ -1,21 +1,25 @@ +from textwrap import dedent + import click -from glotaran.model import known_model_names from glotaran.plugin_system.data_io_registration import known_data_formats +from glotaran.plugin_system.megacomplex_registration import known_megacomplex_names from glotaran.plugin_system.project_io_registration import known_project_formats def plugin_list_cmd(): """Prints a list of installed plugins.""" - output = """ - Installed Glotaran Plugins: + output = dedent( + """ + Installed Glotaran Plugins: - Models: - """ + Megacomplex Models: + """ + ) output += "\n" - for name in known_model_names(): + for name in known_megacomplex_names(): output += f" * {name}\n" output += "\nData file Formats\n\n" diff --git a/glotaran/cli/commands/test/test_util.py b/glotaran/cli/commands/test/test_util.py new file mode 100644 index 000000000..6a9cce9da --- /dev/null +++ b/glotaran/cli/commands/test/test_util.py @@ -0,0 +1,10 @@ +from glotaran.cli.commands.util import project_io_list_supporting_plugins + + +def test_project_io_list_supporting_plugins_save_result(): + """Same as used in ``--outformat`` CLI option.""" + result = project_io_list_supporting_plugins("save_result", ("yml_str")) + + assert "csv" not in result + assert "yml_str" not in result + assert "folder" in result diff --git a/glotaran/cli/commands/util.py b/glotaran/cli/commands/util.py index 27c5c3551..ef83b42ec 100644 --- a/glotaran/cli/commands/util.py +++ b/glotaran/cli/commands/util.py @@ -1,10 +1,20 @@ +from __future__ import annotations + import sys +from typing import Iterable import click from click import echo from click import prompt -import glotaran as gta +from glotaran.io import ProjectIoInterface +from glotaran.io import load_dataset +from glotaran.io import load_model +from glotaran.io import load_parameters +from glotaran.io import load_scheme +from glotaran.plugin_system.base_registry import methods_differ_from_baseclass_table +from glotaran.plugin_system.project_io_registration import get_project_io +from glotaran.plugin_system.project_io_registration import known_project_formats def signature_analysis(cmd): @@ -46,23 +56,25 @@ def _load_file(filename, loader, name, verbose): def load_scheme_file(filename, verbose=False): - return _load_file(filename, gta.analysis.scheme.Scheme.from_yaml_file, "scheme", verbose) + return _load_file( + filename, lambda file: load_scheme(file, format_name="yml"), "scheme", verbose + ) def load_model_file(filename, verbose=False): - return _load_file(filename, gta.read_model_from_yaml_file, "model", verbose) + return _load_file(filename, lambda file: load_model(file, format_name="yml"), "model", verbose) def load_parameter_file(filename, fmt=None, verbose=False): def loader(filename): - return gta.parameter.ParameterGroup.from_file(filename, fmt=fmt) + return load_parameters(filename, format_name=fmt) return _load_file(filename, loader, "parameter", verbose) def load_dataset_file(filename, fmt=None, verbose=False): def loader(filename): - return gta.io.read_data_file(filename, fmt=fmt) + return load_dataset(filename, format_name=fmt) return _load_file(filename, loader, "parameter", verbose) @@ -116,6 +128,31 @@ def write_data(data, out): df.to_csv(out) +def project_io_list_supporting_plugins( + method_name: str, block_list: Iterable[str] | None = None +) -> Iterable[str]: + """List all project-io plugin that implement ``method_name``. + + Parameters + ---------- + method_name: str + Name of the method which should be supported. + block_list: Iterable[str] + Iterable of plugin names which should be omitted. + """ + if block_list is None: + block_list = [] + support_table = methods_differ_from_baseclass_table( + method_names=method_name, + plugin_registry_keys=known_project_formats(full_names=False), + get_plugin_function=get_project_io, + base_class=ProjectIoInterface, + ) + support_table = filter(lambda entry: entry[1], support_table) + supporting_list: Iterable[str] = (entry[0].replace("`", "") for entry in support_table) + return list(filter(lambda entry: entry not in block_list, supporting_list)) + + class ValOrRangeOrList(click.ParamType): name = "number or range or list" diff --git a/glotaran/cli/main.py b/glotaran/cli/main.py index 34124ffa2..cf516e727 100644 --- a/glotaran/cli/main.py +++ b/glotaran/cli/main.py @@ -1,6 +1,6 @@ import click -import glotaran as gta +from glotaran import __version__ as VERSION from glotaran.cli.commands.optimize import optimize_cmd from glotaran.cli.commands.pluginlist import plugin_list_cmd from glotaran.cli.commands.print import print_cmd @@ -8,6 +8,8 @@ class Cli(click.Group): + """The glotaran CLI implementation of :class:`click.group`""" + def __init__(self, *args, **kwargs): self.help_priorities = {} super().__init__(*args, **kwargs) @@ -42,32 +44,31 @@ def decorator(f): @click.group(cls=Cli) -@click.version_option(version=gta.__version__) -def glotaran(): +@click.version_option(version=VERSION) +def main(prog_name="glotaran"): + """The glotaran CLI main function.""" pass -glotaran.add_command( - glotaran.command( +main.add_command( + main.command( name="pluginlist", short_help="Prints a list of installed plugins.", help_priority=4 )(plugin_list_cmd) ) -glotaran.add_command( - glotaran.command(name="print", short_help="Prints a model as markdown.", help_priority=3)( +main.add_command( + main.command(name="print", short_help="Prints a model as markdown.", help_priority=3)( print_cmd ) ) -glotaran.add_command( - glotaran.command(name="validate", short_help="Validates a model file.", help_priority=2)( +main.add_command( + main.command(name="validate", short_help="Validates a model file.", help_priority=2)( validate_cmd ) ) -glotaran.add_command( - glotaran.command(name="optimize", short_help="Optimizes a model.", help_priority=1)( - optimize_cmd - ) +main.add_command( + main.command(name="optimize", short_help="Optimizes a model.", help_priority=1)(optimize_cmd) ) if __name__ == "__main__": - glotaran() + raise SystemExit(main(prog_name="glotaran")) diff --git a/glotaran/cli/test/test_cli.py b/glotaran/cli/test/test_cli.py new file mode 100644 index 000000000..76922ef05 --- /dev/null +++ b/glotaran/cli/test/test_cli.py @@ -0,0 +1,43 @@ +from pathlib import Path + +from click.testing import CliRunner + +from glotaran.cli import main + + +def test_cli_help(): + """Test the CLI help options.""" + runner = CliRunner() + result = runner.invoke(main) + assert result.exit_code == 0 + help_result = runner.invoke(main, ["--help"], prog_name="glotaran") + assert help_result.exit_code == 0 + assert "Usage: glotaran [OPTIONS] COMMAND [ARGS]..." in help_result.output + + +def test_cli_pluginlist(): + """Test the CLI pluginlist option.""" + runner = CliRunner() + result = runner.invoke(main, ["pluginlist"], prog_name="glotaran") + assert result.exit_code == 0 + assert "Installed Glotaran Plugins" in result.output + + +def test_cli_validate_parameters_file(tmp_path: Path): + """Test the CLI pluginlist option.""" + empty_file = tmp_path.joinpath("empty_file.yml") + empty_file.touch() + runner = CliRunner() + result_ok = runner.invoke( + main, ["validate", "--parameters_file", str(empty_file)], prog_name="glotaran" + ) + assert result_ok.exit_code == 0 + assert "Type 'glotaran validate --help' for more info." in result_ok.output + non_existing_file = tmp_path.joinpath("_does_not_exist_.yml") + result_file_not_exist = runner.invoke( + main, ["validate", "--parameters_file", str(non_existing_file)], prog_name="glotaran" + ) + assert result_file_not_exist.exit_code == 2 + assert all( + substring in result_file_not_exist.output for substring in ("Error", "does not exist") + ) diff --git a/glotaran/project/scheme.py b/glotaran/project/scheme.py index b1c73c5f5..3376a5dc5 100644 --- a/glotaran/project/scheme.py +++ b/glotaran/project/scheme.py @@ -37,7 +37,7 @@ class Scheme: group: bool | None = None group_tolerance: float = 0.0 non_negative_least_squares: bool = False - maximum_number_function_evaluations: int = None + maximum_number_function_evaluations: int | None = None add_svd: bool = True ftol: float = 1e-8 gtol: float = 1e-8 diff --git a/setup.cfg b/setup.cfg index 3175b7a64..9b3a44fbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,7 @@ zip_safe = True [options.entry_points] console_scripts = - glotaran=glotaran.cli.main:glotaran + glotaran=glotaran.cli.main:main glotaran.plugins.data_io = ascii = glotaran.builtin.io.ascii.wavelength_time_explicit_file sdt = glotaran.builtin.io.sdt.sdt_file_reader