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