Source code for schrodinger.application.inputconfig
"""
A modified version of the configobj module.
This module can be used to read and write simplified input file (SIF)
formats, such as those used by VSW, QPLD, Prime, Glide, MacroModel, etc.
The SIF format is related to the Windows INI (.ini) format that is read and
writen by configobj, but with following exceptions:
1. Spaces are used instead of equals signs to separate keywords from
values.
2. Keywords should be written in upper case. (The reading of keywords is
case-sensitive).
Example input file::
KEYWORD1 value
KEYWORD2 "value with $special$ characters"
KEYWORD3 item1, item2, item3
KEYWORD4 True
KEYWORD5 10.2
KEYWORD6 1.1, 2.2, 3.3, 4.4
[ SECTION1 ]
SUBKEYWORD1 True
SUBKEYWORD2 12345
SUBKEYWORD3 "some string"
#END
For more information on ConfigObj, see:
http://www.voidspace.org.uk/python/configobj.html
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: K. Shawn Watts, Matvey Adzhigirey
################################################################################
# Packages
################################################################################
import copy
import io
import re
import validate
from configobj import ConfigObj
from configobj import flatten_errors
from configobj import wspace_plus
wspace = ' \r\n\v\t'
[docs]def custom_is_list(value, min=None, max=None):
"""
This list validator turns single items without commas into 1-element
lists. The default list validator requires a trailing comma for these.
That is, with this function as the list validator and a list spec, an
input line of "indices = 1" will create a value of [1] for the key
'indices'.
"""
(min_len, max_len) = validate._is_num_param(('min', 'max'), (min, max))
# If checking one item and it's a string:
if type(value) != type([]) and type(value) != type(()):
# not a list (string, float, int)
value = [value]
try:
num_members = len(value)
except TypeError:
raise validate.VdtTypeError(value)
if min_len is not None and num_members < min_len:
raise validate.VdtValueTooShortError(value)
if max_len is not None and num_members > max_len:
raise validate.VdtValueTooLongError(value)
return value
[docs]def custom_is_string_list(value, min=None, max=None):
"""
Custom is_string_list() method which overrides the one in validate.py.
This method does not raise an exception if a string is passed, and
instead tries to break it into a list of strings.
"""
#print 'custom_is_string_list() input:', value
return [validate.is_string(mem) for mem in custom_is_list(value, min, max)]
validate.is_list = custom_is_list
validate.is_string_list = custom_is_string_list
[docs]class InputConfig(ConfigObj):
"""
Parse keyword-value input files and make the settings available in a
dictionary-like fashion.
Typical usage::
list_of_specs = ["NUM_RINGS = integer(min=1, max=100, default=1)"]
config = InputConfig(filename, list_of_specs)
if config['NUM_RINGS'] > 4:
do_something()
"""
[docs] def __init__(self, infile=None, specs=None):
"""
:type infile: string
:param infile: The name of the input file.
:type specs: list of strings
:param specs: A list of strings, each in the format
`<keywordname> = <validator>(<validatoroptions>)`. An example
string is `NUM_RINGS = integer(min=1, max=100, default=1)`.
For available validators, see:
http://www.voidspace.org.uk/python/validate.html.
"""
self._specs = specs
self._key_order = [] # List of supported keywords in preferred order
if isinstance(infile, str): # File path
# If file specified, re-Write keywords/values separated with "="
# to a StringIO handle, and pass it to ConfigObj:
tmpfh = io.StringIO()
item_re = re.compile(r'^\s*(\S+)\s+(.+)$')
# Replace white space after first word with "=":
with open(infile) as fh:
for iline in fh:
iline = iline.strip(
'\n') # Remove the trailing return character
# FIXME: Improve the matching mechanism
if not iline.strip().startswith('['): # not section iline
match = item_re.match(iline)
if match:
if len(match.groups()) != 2:
print('LENTH OF MATCH != 2!!! len:',
len(match.groups()))
iline = '='.join(match.groups())
#print 'OUTLINE', iline
#print ''
tmpfh.write(iline + '\n')
tmpfh.seek(0) # go to beginning of file
infile = tmpfh
elif isinstance(infile, dict):
# Keyword dict was specified
infile = copy.deepcopy(infile)
# ConfigObj seems to retain a references to values from the
# keyword dict, so when it's modified the input keywords
# dictionary's values (if they are lists/dicts/etc will also get
# modified. For this reason deep copy it first.
if specs:
specs_no_comments = []
for iline in specs:
# Use only everything before the first "#" (ignore comments):
iline = iline.split('#')[0]
specs_no_comments.append(iline)
s = iline.strip().split()
if s:
self._key_order.append(s[0])
try:
ConfigObj.__init__(self,
infile,
configspec=specs_no_comments,
raise_errors=True,
indent_type=" ")
except Exception as err:
raise RuntimeError(str(err))
elif infile:
try:
ConfigObj.__init__(self,
infile,
raise_errors=True,
indent_type=" ")
except Exception as err:
raise RuntimeError(str(err))
else:
try:
ConfigObj.__init__(self, raise_errors=True, indent_type=" ")
except Exception as err:
raise RuntimeError(str(err))
#print '**************'
#print 'OUTPUT:', self
#print '**************'
[docs] def getSpecsString(self):
"""
Return a string of specifications.
One keywords per line.
Raises ValueError if this class has no specifications.
"""
if self._specs:
outstr = ""
for spec in self._specs:
outstr += (spec + '\n')
return outstr
else:
raise ValueError("This class has no specification")
[docs] def printout(self):
"""
Print all keywords of this instance to stdout.
This method is meant for debugging purposes.
"""
output = io.StringIO()
self.write(output)
for iline in output.getvalue().split('\n'):
print(iline)
[docs] def writeInputFile(self,
filename,
ignore_none=False,
yesno=False,
smartsort=False):
"""
Write the configuration to a file in the InputConfig format.
:type filename: a file path or an open file handle
:param filename: The file to write the configuration to.
:type ignore_none: bool
:param ignore_none: If True, keywords with a value of None will not
be written to the input file.
:type yesno: bool
:param yesno: If True, boolean keywords will be written as "yes" and
"no", if False, as "True" and "False".
:type smartsort: bool
:param smartsort: If True, keywords that are identical except for the
numbers at the end will be sorted such that "2" will go before "10".
"""
lines = ConfigObj.write(self)
# Write keyword-value pairs to the input file:
if hasattr(filename, 'write'): # File handle passed
fh = filename
else:
fh = open(filename, 'w')
sections_present = False
kw_line_list = [] # list of tuples: (keyword, iline)
for iline in lines:
s = iline.split(None, 2)
if not s: # empty line
continue
keyword = s[0] # first word in line
value = s[-1] # last word in line
if ignore_none and value == 'None':
continue
# Ev:76198
if yesno:
if value == 'True':
iline = iline.replace('True', 'yes')
elif value == 'False':
iline = iline.replace('False', 'no')
if keyword.startswith('['):
sections_present = True
iline = iline.replace('=', ' ', 1) + "\n"
# NOTE: The Python file object will automatically convert "\n" to "\r\n" on Windows
kw_line_list.append((keyword, iline))
# Do not attempt to sort keywords if sections are present:
if sections_present:
for keyword, iline in kw_line_list:
# Add a blank line before new (root) sections:
if iline.startswith("[") and not iline.startswith("[["):
fh.write("\n")
# NOTE: The Python file object will automatically convert "\n" to "\r\n" on Windows
fh.write(iline)
else: # attempt to sort
kw_dict = {} # All keywords/lines as dict
max_key_size = 1 # Number of characters in the longest keyword
for key, iline in kw_line_list:
kw_dict[key] = iline
if len(key) > max_key_size:
max_key_size = len(key)
# Sort the keywords by the order that they appear in the specs:
out_kw_line_list = []
for key in self._key_order:
if key in kw_dict:
out_kw_line_list.append((key, kw_dict[key]))
# Smart-sorting was added as part of PYTHON-1815:
int_conv = lambda text: int(text) if text.isdigit() else text
alphanum_key = lambda key_value: [
int_conv(c) for c in re.split('([0-9]+)', key_value[0])
]
if smartsort:
sort_func = alphanum_key
else:
sort_func = None
# Write keywords that are not in the spec, sorted by the keyword name:
for key, iline in sorted(kw_line_list, key=sort_func):
if key not in self._key_order:
out_kw_line_list.append((key, iline))
# Write keyword-line pairs to the input file:
for key, iline in out_kw_line_list:
fh.write(iline)
if fh != filename:
fh.close()
[docs] def validateValues(self, preserve_errors=True, copy=True):
"""
Validate the values read in from the InputConfig file.
Provide values for keywords with validators that have default
values.
If a validator for a keyword is specified without a default and the
keyword is missing from the input file, a RuntimeError will be
raised.
:type preserve_errors: bool
:param preserve_errors: If set to False, this method returns True if
all tests passed, and False if there is a failure. If set to True,
then instead of getting False for failed checkes, the actual
detailed errors are printed for any validation errors encountered.
Even if preserve_errors is True, missing keys or sections will
still be represented by a False in the results dictionary.
:type copy: bool
:param copy: If False, default values (as specified in the 'specs'
strings in the constructor) will not be copied to object's
"defaults" list, which will cause them to not be written out
when writeInputFile() method is called.
If True, then all keywords with a default will be written
out to the file via the writeInputFile() method.
NOTE: Default is True, while in ConfigObj default is False.
"""
#for keyname in kw_dict:
# if not keyname in self._key_order:
# raise ValueError("Unsupported keyword: %s" % keyname)
#print '\nUNVALIDATED KEYWORDS:', kw_dict
#keywords = self.keys()
vdt = validate.Validator()
res = self.validate(vdt, preserve_errors=preserve_errors,
copy=copy) # Copy so that defaults are set
if preserve_errors:
errors = []
error_msg = ""
for entry in flatten_errors(self, res):
section_list, key, error = entry
section_list.insert(0, '[root]')
expected_type = None
if key is not None:
# Ev:99875 Save the expected type of the keyword:
# for now we'll only report expected types for keys in the root section
if len(section_list) == 1:
expected_type = self.configspec[key]
section_list.append(key)
else:
section_list.append('[missing]')
section_string = ', '.join(section_list)
errors.append((section_string, error, expected_type))
errors.sort()
for key, error, expected_type in errors:
if expected_type:
# Ev:99875 In addition to the error, print the expected type of the keyword:
error_msg += '%s : %s Expected type: %s\n' % (key, (
error or 'MISSING'), expected_type)
else:
error_msg += '%s : %s\n' % (key, (error or 'MISSING'))
if errors:
raise RuntimeError(error_msg)
#print '\nVALIDATED KEYWORDS:', self
#print ''
else: # not preserving errors. Returns True or False
return res
def _quote(self, value, multiline=True):
"""
Overwrite ConfigObj's quoting method to ensure that values with spaces
get quoted. (ConfigObj quotes only values that start and/or end with
spaces).
"""
# Only modify strings that contain white spaces:
modify = False
if isinstance(value, str):
for spacechar in wspace:
if spacechar in value:
modify = True
break
if modify:
# Do not modify values that are already properly quoted by ConfigObj:
# (values that start of end with a whilte space or a quote, and
# values that contain commas)
if value[0] in wspace_plus or value[
-1] in wspace_plus or ',' in value:
modify = False
if modify:
# Temporarily insert a space at position 0 to force ConfigObj to quote the value:
value = " " + value
value = ConfigObj._quote(self, value, multiline)
if modify:
# Remove the added space:
value = value[0] + value[2:]
return value
[docs]def determine_jobtype(inpath):
"""
Parse the specified file and determines its job type.
This is needed in order to avoid parsing of an input file if its
job type is invalid.
Return the job type as a string, or the empty string if no JOBTYPE
keyword is found.
"""
for iline in open(inpath):
s = iline.strip().split()
if len(s) >= 2 and s[0] in ['JOBTYPE', 'JOB_TYPE']:
return s[1]
return ''
# EOF