Skip to content

Commit

Permalink
config.POWConfigParser fixes and enhancements
Browse files Browse the repository at this point in the history
- raise new NoSectionError for missing section
- new warning NoOptionWarning
- run.get_mdp_files() now looks for full paths for MDP files and raises
  ValueError if file cannot be found
- getpath() of None returns None instead of TypeError
- getlist() and getarray() return empty containers instead of TypeError
- readfp() returns self instead of True so that one can initialize from
  file in one line
- findfile() reliably returns None if given None as input
- improve debugging with logging POWConfigParser.get() at DEBUG
- add tests
- update CHANGES (for 0.8.0)

more testing updates (SQUASHME)
  • Loading branch information
orbeckst committed Aug 18, 2021
1 parent abd80e2 commit 8c891be
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 147 deletions.
21 changes: 20 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ Add summary of changes for each release. Use ISO dates. Reference
GitHub issues numbers and PR numbers.

2021-??-?? 0.8.0
ALescoulie
ALescoulie, orbeckst

Changes

* dropped support for Python 2 and <3.6: only Python 3.7--3.9
are supported (#177)
* removed unused POWConfigParser.getintarray() (PR #187)

Enhancements

* new exception config.NoSectionError to indicate missing section
("protocol") in the YAML run input file (PR #187)
* new config.NoOptionWarning when undefined options are used (with
default None) and POWConfigParser.get() now logs when an option is used
at level DEBUG (PR #187)


Fixes

* run.get_mdp_files() finds the full path for an MDP file and raises ValueError
when MDP file is not found (PR #187)
* POWConfigParser.getpath() returns None for empty options instead of raising
TypeError (PR #187)
* POWConfigParser.getlist() and getarray() return empty list or array for
empty options instead of raising TypeError (PR #187)


2021-08-04 0.7.1
Expand Down
116 changes: 92 additions & 24 deletions mdpow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
.. autofunction:: _generate_template_dict
Exceptions
----------
.. autoexception:: NoSectionError
.. autoclass:: NoOptionWarning
"""
import os, errno
from pkg_resources import resource_filename, resource_listdir
Expand All @@ -96,6 +102,7 @@
import numpy as np
import gromacs.utilities

import warnings
import logging
logger = logging.getLogger("mdpow.config")

Expand All @@ -107,8 +114,22 @@
"runinput": resource_filename(__name__, "templates/runinput.yml"),
}

class NoSectionError(ValueError):
"""Section entry is missing.
.. versionadded:: 0.8.0
"""

# not used at the moment
# class NoOptionError(ValueError):
# """Option entry is missing from section"""

class NoOptionWarning(UserWarning):
"""Warning that an option is missing."""

def merge_dicts(user, default):
"""Merge two dictionaries recursively.
Uses recursive method to accurately
merge nested dictionaries
"""
Expand All @@ -122,7 +143,19 @@ def merge_dicts(user, default):


class POWConfigParser(object):
"""Parse YAML config file."""
"""Parse YAML config file.
Raises :exc:`NoSectionError` if a protocol section is missing.
Warns with :exc:`NoOptionWarning` if option under a protocol is missing
and returns ``None``.
.. versionchanged:: 0.8.0
:exc:`NoSectionError` is now raised to clearly distinguish
file structure problems (missing sections) from missing
entries (options).
The `getintarray()` method was removed.
"""

def __init__(self):
self.conf = None
Expand All @@ -131,9 +164,14 @@ def readfp(self, fn):
"""Read YAML from open stream ``fn``.
Overwrites everything.
Returns self.
.. versionchanged:: 0.8.0
Returns self instead of ``True``.
"""
self.conf = yaml.safe_load(fn)
return True
return self

def merge(self, fn):
"""Load YAML from open stream ``fn`` and merge into :attr:`conf`.
Expand All @@ -155,21 +193,35 @@ def get(self, section, option):
"""Return option, unless its "None" --> ``None``,
Conversion to basic python types str, float, int, boolean is
carried out automatically (unless it was None).
carried out automatically.
.. Note:: "none" remains a string, which is essential, see
`Issue 20 <https://github.com/Becksteinlab/MDPOW/issues/20>`_
Conversions are performed by the pyyaml parser.
.. versionchanged:: 0.6.0
Prior versions would convert case-insensitively (e.g. "NONE"
and "none")
"""

try:
value = self.conf[section][option]
return value if value != "None" else None
except TypeError:
return None
sec = self.conf[section]
except (KeyError, TypeError):
# TypeError when self.conf is None
raise NoSectionError(f"Config file has no section {section}")
try:
value = sec[option]
value = value if value != "None" else None # still needed??
except KeyError:
# Returning None has been standard behavior.
#raise NoOptionError(f"Config file section {section} contains "
# f"no option {option}.")
msg = (f"Config file section {section} contains "
f"no option {option}. Using 'None'.")
warnings.warn(msg, category=NoOptionWarning)
logger.warning(msg)
value = None
logger.debug("%s: %s = %r", section, option, value)
return value

# TODO:
# The YAML parser does automatic conversion: the following
Expand All @@ -181,40 +233,57 @@ def get(self, section, option):
getboolean = get

def getpath(self, section, option):
"""Return option as an expanded path."""
return os.path.expanduser(os.path.expandvars(self.get(section, option)))
"""Return option as an expanded path (or ``None``).
.. versionchanged:: 0.8.0
If the entry is ``None`` this method returns ``None``
instead of raising a :exc:`TypeError`.
"""
item = self.get(section, option)
return os.path.expanduser(
os.path.expandvars(item)) if item is not None else None

def findfile(self, section, option):
"""Return location of a file ``option``.
"""Return location of a file ``option`` or ``None``.
Uses :func:`mdpow.config.get_template`.
"""
return get_template(self.getpath(section, option))
pth = self.getpath(section, option)
return get_template(pth) if pth is not None else None

# TODO: Change input file format to use yaml lists and make this method superfluous
def getlist(self, section, option):
"""Return option as a list of strings.
"""Return option as a list of strings or ``[]``.
*option* must be comma-separated; leading/trailing whitespace
is stripped and quotes are treated verbatim.
.. versionchanged:: 0.8.0
If the entry is ``None``, an empty list is returned
instead of raising a :exc:`TypeError`.
"""
return [x.strip() for x in str(self.get(section, option)).split(",")]
item = self.get(section, option)
return [x.strip()
for x in str(item).split(",")] if item is not None else []

def getarray(self, section, option):
"""Return option as a numpy array of floats.
"""Return option as a numpy array of floats or ``np.array([])``.
*option* must be comma-separated; leading/trailing whitespace
is stripped and quotes are treated verbatim.
"""
return np.asarray(self.getlist(section, option), dtype=np.float)

def getintarray(self, section, option):
"""Return option as a numpy array of integers.
*option* must be comma-separated; leading/trailing whitespace
is stripped and quotes are treated verbatim.
.. versionchanged:: 0.8.0
If the entry is ``None``, an empty array is returned.
"""
return np.asarray(self.getlist(section, option), dtype=np.int)
return np.asarray(self.getlist(section, option), dtype=float)

# def getintarray(self, section, option):
# """Return option as a numpy array of integers.
#
# *option* must be comma-separated; leading/trailing whitespace
# is stripped and quotes are treated verbatim.
# """
# return np.asarray(self.getlist(section, option), dtype=int)

def get_configuration(filename=None):
"""Reads and parses a run input config file.
Expand Down Expand Up @@ -420,4 +489,3 @@ def asiterable(obj):
else:
logger.warning("Using user-supplied environment variable GMXLIB=%r to find force fields", os.environ['GMXLIB'])
logger.info("(You can use the MDPOW default by executing 'unset GMXLIB' in your shell before running MDPOW.)")

40 changes: 27 additions & 13 deletions mdpow/equil.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,19 +316,33 @@ def topology(self, itp='drug.itp', prm=None, **kwargs):
if prm is not None:
shutil.copy(prm, _prm)
gromacs.cbook.edit_txt(top_template,
[('#include +"oplsaa\.ff/forcefield\.itp"',
'oplsaa\.ff/', setting[0]),
('#include +"compound\.itp"', 'compound\.itp', _itp),
('#include +"oplsaa\.ff/tip4p\.itp"',
'oplsaa\.ff/tip4p\.itp', setting[0]+self.solvent.itp),
('#include +"oplsaa\.ff/ions_opls\.itp"',
'oplsaa\.ff/ions_opls\.itp', setting[1]),
('#include +"compound\.prm"',
'#include +"compound\.prm"', prm_kw),
('#include +"water\.itp"', 'water\.itp', setting[2]),
('Compound', 'solvent', self.solvent_type),
('Compound', 'DRUG', self.molecule),
('DRUG\s*1', 'DRUG', self.molecule),
[(r'#include +"oplsaa\.ff/forcefield\.itp"',
r'oplsaa\.ff/',
setting[0]),
(r'#include +"compound\.itp"',
r'compound\.itp',
_itp),
(r'#include +"oplsaa\.ff/tip4p\.itp"',
r'oplsaa\.ff/tip4p\.itp',
setting[0] + self.solvent.itp),
(r'#include +"oplsaa\.ff/ions_opls\.itp"',
r'oplsaa\.ff/ions_opls\.itp',
setting[1]),
(r'#include +"compound\.prm"',
r'#include +"compound\.prm"',
prm_kw),
(r'#include +"water\.itp"',
r'water\.itp',
setting[2]),
(r'Compound',
'solvent',
self.solvent_type),
(r'Compound',
'DRUG',
self.molecule),
(r'DRUG\s*1',
'DRUG',
self.molecule),
],
newname=topol)
logger.info('[%(dirname)s] Created topology %(topol)r that includes %(_itp)r', vars())
Expand Down
Loading

0 comments on commit 8c891be

Please sign in to comment.