Skip to content

Commit

Permalink
Merge pull request #66 from dwhswenson/pylint-wizard
Browse files Browse the repository at this point in the history
pylint/flake8: `paths_cli.wizard`
  • Loading branch information
dwhswenson authored Nov 5, 2021
2 parents 59bc1b9 + 3fb1571 commit 539c8b0
Show file tree
Hide file tree
Showing 20 changed files with 368 additions and 110 deletions.
8 changes: 1 addition & 7 deletions paths_cli/tests/wizard/test_load_from_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from paths_cli.tests.wizard.mock_wizard import mock_wizard

from paths_cli.wizard.load_from_ops import (
named_objs_helper, _get_ops_storage, _get_ops_object, load_from_ops
_get_ops_storage, _get_ops_object, load_from_ops
)

# for some reason I couldn't get these to work with MagicMock
Expand Down Expand Up @@ -44,12 +44,6 @@ def ops_file_fixture():
storage = FakeStorage(foo)
return storage

def test_named_objs_helper(ops_file_fixture):
helper_func = named_objs_helper(ops_file_fixture, 'foo')
result = helper_func('any')
assert "what I found" in result
assert "bar" in result
assert "baz" in result

@pytest.mark.parametrize('with_failure', [False, True])
def test_get_ops_storage(tmpdir, with_failure):
Expand Down
58 changes: 50 additions & 8 deletions paths_cli/wizard/core.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import random
from paths_cli.wizard.tools import a_an

from collections import namedtuple
def interpret_req(req):
"""Create user-facing string representation of the input requirement.
WIZARD_STORE_NAMES = ['engines', 'cvs', 'states', 'networks', 'schemes']
WizardSay = namedtuple("WizardSay", ['msg', 'mode'])
Parameters
----------
req : Tuple[..., int, int]
req[1] is the minimum number of objects to create; req[2] is the
maximum number of objects to create
def interpret_req(req):
Returns
-------
str :
human-reading string for how many objects to create
"""
_, min_, max_ = req
string = ""
if min_ == max_:
Expand All @@ -23,7 +28,32 @@ def interpret_req(req):
return string


# TODO: REFACTOR: It looks like get_missing_object may be redundant with
# other code for obtaining prerequisites for a function
def get_missing_object(wizard, obj_dict, display_name, fallback_func):
"""Get a prerequisite object.
The ``obj_dict`` here is typically a mapping of objects known by the
Wizard. If it is empty, the ``fallback_func`` is used to create a new
object. If it has exactly 1 entry, that is used implicitly. If it has
more than 1 entry, the user must select which one to use.
Parameters
----------
wizard : :class:`.Wizard`
the wizard for user interaction
obj_dict : Dict[str, Any]
mapping of object name to object
display_name: str
the user-facing name of this type of object
fallback_func: Callable[:class:`.Wizard`] -> Any
method to create a new object of this type
Returns
-------
Any :
the prerequisite object
"""
if len(obj_dict) == 0:
obj = fallback_func(wizard)
elif len(obj_dict) == 1:
Expand All @@ -37,10 +67,22 @@ def get_missing_object(wizard, obj_dict, display_name, fallback_func):


def get_object(func):
"""Decorator to wrap methods for obtaining objects from user input.
This decorator implements the user interaction loop when dealing with a
single user input. The wrapped function is intended to create some
object. If the user's input cannot create a valid object, the wrapped
function should return None.
Parameters
----------
func : Callable
object creation method to wrap; should return None on failure
"""
# TODO: use functools.wraps?
def inner(*args, **kwargs):
obj = None
while obj is None:
obj = func(*args, **kwargs)
return obj
return inner

60 changes: 50 additions & 10 deletions paths_cli/wizard/cvs.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from functools import partial
from collections import namedtuple
import numpy as np

from paths_cli.compiling.tools import mdtraj_parse_atomlist
from paths_cli.wizard.plugin_classes import (
LoadFromOPS, WizardObjectPlugin, WrapCategory
)
from paths_cli.wizard.core import get_object
import paths_cli.wizard.engines

from functools import partial
from collections import namedtuple
import numpy as np

from paths_cli.wizard.parameters import (
FromWizardPrerequisite
)
Expand All @@ -27,6 +27,10 @@
"You should specify atom indices enclosed in double brackets, e.g. "
"[{list_range_natoms}]"
)
_MDTrajParams = namedtuple("_MDTrajParams", ['period', 'n_atoms',
'kwarg_name', 'cv_user_str'])
_MDTRAJ_INTRO = "We'll make a CV that measures the {user_str}."


# TODO: implement so the following can be the help string:
# _ATOM_INDICES_HELP_STR = (
Expand Down Expand Up @@ -56,6 +60,25 @@

@get_object
def _get_atom_indices(wizard, topology, n_atoms, cv_user_str):
"""Parameter loader for atom_indices parameters in MDTraj.
Parameters
----------
wizard : :class:`.Wizard`
wizard for user interaction
topology :
topology (reserved for future use)
n_atoms : int
number of atoms to define this CV (i.e., 2 for a distance; 3 for an
angle; 4 for a dihedral)
cv_user_str : str
user-facing name for the CV being created
Returns
-------
:class`np.ndarray` :
array of indices for the MDTraj function
"""
helper = Helper(_ATOM_INDICES_HELP_STR.format(
list_range_natoms=list(range(n_atoms))
))
Expand All @@ -67,14 +90,14 @@ def _get_atom_indices(wizard, topology, n_atoms, cv_user_str):
except Exception as e:
wizard.exception(f"Sorry, I didn't understand '{atoms_str}'.", e)
helper("?")
return
return None

return arr

_MDTrajParams = namedtuple("_MDTrajParams", ['period', 'n_atoms',
'kwarg_name', 'cv_user_str'])

def _mdtraj_cv_builder(wizard, prereqs, func_name):
"""General function to handle building MDTraj CVs.
"""
from openpathsampling.experimental.storage.collective_variables import \
MDTrajFunctionCV
dct = TOPOLOGY_CV_PREREQ(wizard)
Expand Down Expand Up @@ -108,9 +131,9 @@ def _mdtraj_cv_builder(wizard, prereqs, func_name):
return MDTrajFunctionCV(func, topology, period_min=period_min,
period_max=period_max, **kwargs)

_MDTRAJ_INTRO = "We'll make a CV that measures the {user_str}."

def _mdtraj_summary(wizard, context, result):
"""Standard summary of MDTraj CVs: function, atom, topology"""
cv = result
func = cv.func
topology = cv.topology
Expand All @@ -121,6 +144,7 @@ def _mdtraj_summary(wizard, context, result):
f" Topology: {repr(topology.mdtraj)}")
return [summary]


if HAS_MDTRAJ:
MDTRAJ_DISTANCE = WizardObjectPlugin(
name='Distance',
Expand Down Expand Up @@ -152,10 +176,24 @@ def _mdtraj_summary(wizard, context, result):
"four atoms"),
summary=_mdtraj_summary,
)

# TODO: add RMSD -- need to figure out how to select a frame


def coordinate(wizard, prereqs=None):
"""Builder for coordinate CV.
Parameters
----------
wizard : :class:`.Wizard`
wizard for user interaction
prereqs :
prerequisites (unused in this method)
Return
------
CoordinateFunctionCV :
the OpenPathSampling CV for this selecting this coordinate
"""
# TODO: atom_index should be from wizard.ask_custom_eval
from openpathsampling.experimental.storage.collective_variables import \
CoordinateFunctionCV
Expand All @@ -174,12 +212,13 @@ def coordinate(wizard, prereqs=None):
f"atom {atom_index}?")
try:
coord = {'x': 0, 'y': 1, 'z': 2}[xyz]
except KeyError as e:
except KeyError:
wizard.bad_input("Please select one of 'x', 'y', or 'z'")

cv = CoordinateFunctionCV(lambda snap: snap.xyz[atom_index][coord])
return cv


COORDINATE_CV = WizardObjectPlugin(
name="Coordinate",
category="cv",
Expand All @@ -202,6 +241,7 @@ def coordinate(wizard, prereqs=None):
"you can also create your own and load it from a file.")
)


if __name__ == "__main__": # no-cov
from paths_cli.wizard.run_module import run_category
run_category('cv')
3 changes: 0 additions & 3 deletions paths_cli/wizard/engines.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import paths_cli.wizard.openmm as openmm
from paths_cli.wizard.plugin_classes import LoadFromOPS, WrapCategory
from functools import partial

_ENGINE_HELP = "An engine describes how you'll do the actual dynamics."
ENGINE_PLUGIN = WrapCategory(
Expand All @@ -17,4 +15,3 @@
if __name__ == "__main__": # no-cov
from paths_cli.wizard.run_module import run_category
run_category('engine')

27 changes: 23 additions & 4 deletions paths_cli/wizard/errors.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
class ImpossibleError(Exception):
"""Error to throw for sections that should be unreachable code"""
def __init__(self, msg=None):
if msg is None:
msg = "Something went really wrong. You should never see this."
super().__init__(msg)


class RestartObjectException(BaseException):
"""Exception to indicate the restart of an object.
Raised when the user issues a command to cause an object restart.
"""
pass


def not_installed(wizard, package, obj_type):
"""Behavior when an integration is not installed.
In actual practice, this calling code should ensure this doesn't get
used. However, we keep it around as a defensive practice.
Parameters
----------
package : str
name of the missing package
obj_type : str
name of the object category that would have been created
"""
retry = wizard.ask(f"Hey, it looks like you don't have {package} "
"installed. Do you want to try a different "
f"{obj_type}, or do you want to quit?",
options=["[R]etry", "[Q]uit"])
options=["[r]etry", "[q]uit"])
if retry == 'r':
raise RestartObjectException()
elif retry == 'q':
if retry == 'q':
# TODO: maybe raise QuitWizard instead?
exit()
else: # no-cov
raise ImpossibleError()
raise ImpossibleError() # -no-cov-


FILE_LOADING_ERROR_MSG = ("Sorry, something went wrong when loading that "
Expand Down
11 changes: 9 additions & 2 deletions paths_cli/wizard/helper.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import sys
from .errors import RestartObjectException


class QuitWizard(BaseException):
pass
"""Exception raised when user expresses desire to quit the wizard"""


# the following command functions take cmd and ctx -- future commands might
# use the full command text or the context internally.

def raise_quit(cmd, ctx):
"""Command function to quit the wizard (with option to save).
"""
raise QuitWizard()


def raise_restart(cmd, ctx):
"""Command function to restart the current object.
"""
raise RestartObjectException()


def force_exit(cmd, ctx):
"""Command function to force immediate exit.
"""
print("Exiting...")
exit()
sys.exit()


HELPER_COMMANDS = {
Expand Down
7 changes: 7 additions & 0 deletions paths_cli/wizard/joke.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,38 @@
"It would also be a good name for a death metal band.",
]


def _joke1(name, obj_type): # no-cov
return (f"I probably would have named it something like "
f"'{random.choice(_NAMES)}'.")


def _joke2(name, obj_type): # no-cov
thing = random.choice(_THINGS)
joke = (f"I had {a_an(thing)} {thing} named '{name}' "
f"when I was young.")
return joke


def _joke3(name, obj_type): # no-cov
return (f"I wanted to name my {random.choice(_SPAWN)} '{name}', but my "
f"wife wouldn't let me.")


def _joke4(name, obj_type): # no-cov
a_an_thing = a_an(obj_type) + f" {obj_type}"
return random.choice(_MISC).format(name=name, obj_type=obj_type,
a_an_thing=a_an_thing)


def name_joke(name, obj_type): # no-cov
"""Make a joke about the naming process."""
jokes = [_joke1, _joke2, _joke3, _joke4]
weights = [5, 5, 3, 7]
joke = random.choices(jokes, weights=weights)[0]
return joke(name, obj_type)


if __name__ == "__main__": # no-cov
for _ in range(5):
print()
Expand Down
Loading

0 comments on commit 539c8b0

Please sign in to comment.