Skip to content

Commit

Permalink
Fix/cli0.5 (#765)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: s-weigand <[email protected]>
  • Loading branch information
3 people committed Aug 14, 2021

Verified

This commit was signed with the committer’s verified signature. The key has expired.
jsnel Joris Snellenburg
1 parent 5d24823 commit b89196e
Showing 9 changed files with 138 additions and 39 deletions.
1 change: 1 addition & 0 deletions glotaran/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from glotaran.cli.main import main
27 changes: 15 additions & 12 deletions glotaran/cli/commands/optimize.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 10 additions & 6 deletions glotaran/cli/commands/pluginlist.py
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions glotaran/cli/commands/test/test_util.py
Original file line number Diff line number Diff line change
@@ -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
47 changes: 42 additions & 5 deletions glotaran/cli/commands/util.py
Original file line number Diff line number Diff line change
@@ -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"

29 changes: 15 additions & 14 deletions glotaran/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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
from glotaran.cli.commands.validate import validate_cmd


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"))
43 changes: 43 additions & 0 deletions glotaran/cli/test/test_cli.py
Original file line number Diff line number Diff line change
@@ -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")
)
2 changes: 1 addition & 1 deletion glotaran/project/scheme.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b89196e

Please sign in to comment.