Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modularizes cli.py #85

Merged
merged 7 commits into from
Sep 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def read(*names, **kwargs):
],
entry_points={
'console_scripts': [
'haddock3 = haddock.clis.cli:main',
'haddock3 = haddock.clis.cli:maincli',
]
},
# cmdclass={'build_ext': optional_build_ext},
Expand Down
170 changes: 119 additions & 51 deletions src/haddock/clis/cli.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,140 @@
#!/usr/bin/env python3

import argparse
import logging
import sys
from argparse import ArgumentTypeError
from functools import partial

from haddock.version import CURRENT_VERSION
from haddock.workflow import WorkflowManager
from haddock.gear.greetings import get_adieu, get_initial_greeting
from haddock.gear.prepare_run import setup_run
from haddock.error import HaddockError, ConfigurationError


def main(args=None):

def positive_int(n):
n = int(n)
if n < 0:
raise argparse.ArgumentTypeError("Minimum value is 0")
return n

# Command line interface parser
parser = argparse.ArgumentParser()
# Add logging to CLI parser
levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
parser.add_argument("--log-level", default="INFO", choices=levels)
# Restart option
parser.add_argument("--restart", type=positive_int, default=0,
help="Restart the recipe from this course")
# The recipe to be used
parser.add_argument("recipe", type=argparse.FileType("r"),
help="The input recipe file name")
# Version
parser.add_argument("-V", "-v", "--version", help="show version",
action="version",
version="%s %s" % (parser.prog, CURRENT_VERSION))
from haddock.libs.libutil import file_exists, non_negative_int


# Command line interface parser
ap = argparse.ArgumentParser()

_arg_file_exist = partial(
file_exists,
exception=ArgumentTypeError,
emsg="File {!r} does not exist or is not a file.")
ap.add_argument(
"recipe",
type=_arg_file_exist,
help="The input recipe file path",
)

_arg_pos_int = partial(
non_negative_int,
exception=ArgumentTypeError,
emsg="Minimum value is 0, {!r} given.",
)
ap.add_argument(
"--restart",
type=_arg_pos_int,
default=0,
help="Restart the recipe from this course",
)

ap.add_argument(
"--setup",
help="Only setup the run, do not execute",
action="store_true",
dest='setup_only',
)

_log_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
ap.add_argument(
"--log-level",
default="INFO",
choices=_log_levels,
)

ap.add_argument(
"-v",
"--version",
help="show version",
action="version",
version=f'{ap.prog} - {CURRENT_VERSION}',
)


def load_args(ap):
"""Load argument parser args."""
return ap.parse_args()

# Special case only using print instead of logging
options = parser.parse_args()
if not hasattr(options, "version"):
print(get_initial_greeting())

def cli(ap, main):
"""Command-line interface entry point."""
cmd = load_args(ap)
main(**vars(cmd))


def maincli():
"""Main client execution."""
cli(ap, main)


def main(
recipe,
restart=0,
setup_only=False,
log_level="INFO",
):
"""
Execute HADDOCK3 client logic.

Parameters
----------
recipe : str or pathlib.Path
The path to the recipe (config file).

restart : int
At which step to restart haddock3 run.

setup_only : bool
Whether to setup the run without running it.

log_level : str
The logging level: INFO, DEBUG, ERROR, WARNING, CRITICAL.
"""
# anti-pattern to speed up CLI initiation
from haddock.workflow import WorkflowManager
from haddock.gear.greetings import get_adieu, get_initial_greeting
from haddock.gear.prepare_run import setup_run
from haddock.error import HaddockError, ConfigurationError

# Configuring logging
logging.basicConfig(level=options.log_level,
format=("[%(asctime)s] %(name)s:L%(lineno)d"
" %(levelname)s - %(message)s"))
logging.basicConfig(
level=log_level,
format="[%(asctime)s] %(name)s:L%(lineno)d %(levelname)s - %(message)s",
)

# Special case only using print instead of logging
logging.info(get_initial_greeting())

try:
params, other_params = setup_run(options.recipe.name)
params, other_params = setup_run(recipe)

except ConfigurationError as se:
logging.error(se)
except ConfigurationError as err:
logging.error(err)
sys.exit()

try:
workflow = WorkflowManager(
workflow_params=params,
start=options.restart,
**other_params,
)
if not setup_only:
try:
workflow = WorkflowManager(
workflow_params=params,
start=restart,
**other_params,
)

# Main loop of execution
workflow.run()
# Main loop of execution
workflow.run()

except HaddockError as he:
logging.error(he)
except HaddockError as err:
logging.error(err)

# Finish
logging.info(get_adieu())


if __name__ == "__main__":
sys.exit(main())
sys.exit(maincli())
72 changes: 72 additions & 0 deletions src/haddock/libs/libutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import logging
import shutil
from copy import deepcopy
from operator import ge
from os import cpu_count
from pathlib import Path

from haddock.error import SetupError

Expand Down Expand Up @@ -124,3 +126,73 @@ def parse_ncores(n=None, njobs=None, max_cpus=None):
ncores = min(n, max_cpus)
logger.info(f"Selected {ncores} for a maximum of {max_cpus} CPUs")
return ncores


def non_negative_int(
n,
exception=ValueError,
emsg="`n` do not satisfies",
):
"""
Transform `n` in int and returns if `compare` evaluates to True.

Parameters
----------
n : int-convertable
Something that can be converted to int.

exception : Exception
The Exception to raise in case `n` is not a positive integer.

emsg : str
The error message to give to `exception`. May accept formatting
to pass `n`.

Raises
------
ValueError, TypeError
If `n` cannot be converted to `int`
"""
n1 = int(n)
if n1 >= 0:
return n1

# don't change to f-strings, .format has a purpose
raise exception(emsg.format(n))


def file_exists(
path,
exception=ValueError,
emsg="`path` is not a file or does not exist",
):
"""
Asserts file exist.

Parameters
----------
path : str or pathlib.Path
The file path.

exception : Exception
The Exception to raise in case `path` is not file or does not
exist.

emsg : str
The error message to give to `exception`. May accept formatting
to pass `path`.

Raises
------
Exception
Any exception that pathlib.Path can raise.
"""
p = Path(path)

valid = [p.exists, p.is_file]

if all(f() for f in valid):
return p

# don't change to f-strings, .format has a purpose
raise exception(emsg.format(str(path)))
Empty file added tests/recipe.toml
Empty file.
74 changes: 74 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from pathlib import Path

import pytest

from haddock.clis.cli import ap


recipe = Path(Path(__file__).resolve().parent, 'recipe.toml')


def test_ap_recipe_does_not_exist():
with pytest.raises(SystemExit) as exit:
ap.parse_args('does_not_exit.toml'.split())
assert exit.type == SystemExit
assert exit.value.code == 2


def test_ap_recipe_exists():
cmd = ap.parse_args(str(recipe).split())
with open(cmd.recipe) as fin:
fin.readlines()


def test_ap_setup_true():
cmd = ap.parse_args(f'{recipe} --setup'.split())
assert cmd.setup_only == True


def test_ap_setup_false():
cmd = ap.parse_args(str(recipe).split())
assert cmd.setup_only == False


@pytest.mark.parametrize(
'n',
(0, 1, 10, 1230, 50000),
)
def test_ap_restart(n):
cmd = ap.parse_args(f'{recipe} --restart {n}'.split())
assert cmd.restart == n


@pytest.mark.parametrize(
'n',
(-1, -10, -1230, -50000),
)
def test_ap_restart_error(n):
with pytest.raises(SystemExit) as exit:
cmd = ap.parse_args(f'{recipe} --restart {n}'.split())
assert exit.type == SystemExit
assert exit.value.code == 2


def test_ap_version():
with pytest.raises(SystemExit) as exit:
ap.parse_args('-v'.split())
assert exit.type == SystemExit
assert exit.value.code == 0


@pytest.mark.parametrize(
'level',
("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"),
)
def test_ap_log_level(level):
cmd = ap.parse_args(f'{recipe} --log-level {level}'.split())
assert cmd.log_level == level


def test_ap_log_level_error():
with pytest.raises(SystemExit) as exit:
ap.parse_args(f'{recipe} --log-level BAD'.split())
assert exit.type == SystemExit
assert exit.value.code == 2
Loading