Skip to content

Commit

Permalink
Merge pull request #85
Browse files Browse the repository at this point in the history
Modularizes cli.py
  • Loading branch information
joaomcteixeira committed Sep 6, 2021
2 parents 6aa0b69 + 19ac131 commit 0f1cb4c
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 53 deletions.
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
164 changes: 112 additions & 52 deletions src/haddock/clis/cli.py
Original file line number Diff line number Diff line change
@@ -1,80 +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")

parser.add_argument(
"--setup",
help="Only setup the run, do not execute",
action="store_true",
)
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()


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

# Version
parser.add_argument("-V", "-v", "--version", help="show version",
action="version",
version="%s %s" % (parser.prog, CURRENT_VERSION))
# Configuring logging
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
options = parser.parse_args()
if not hasattr(options, "version"):
print(get_initial_greeting())

# Configuring logging
logging.basicConfig(level=options.log_level,
format=("[%(asctime)s] %(name)s:L%(lineno)d"
" %(levelname)s - %(message)s"))
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()

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

# 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

0 comments on commit 0f1cb4c

Please sign in to comment.