def test_optional(): '''Test that optional values are validated if present, but no error is raised if not.''' d1 = {'age': 28, 'city': 'utrecht'} d2 = {'city': 'utrecht'} dval = DictValue({'age': Value(int, kind=Value.optional), 'city': str}) assert dval.validate(d1) == d1 assert dval.validate(d2) == d2
def test_NamedValue(value): '''Test the parsing of dictionary elements with NamedValue''' d = {value: 5} assert NamedValue(value, int).validate(d) == d # simple constructor assert NamedValue(value, Value(int, val_max=6)).validate(d) == d # constructor from Value d_dval = {value: {value: 5}} # constructor from DictValue assert NamedValue(value, DictValue({value: int})).validate(d_dval) == d_dval d2 = {value: value} assert NamedValue(value, type(value)).validate(d2) == d2 assert NamedValue(value, Value(type(value))).validate(d2) == d2 # the key must match exactly with pytest.raises(SettingsValueError) as excinfo: NamedValue(value, int).validate({'wrong_key': 5}) assert excinfo.match(r"Setting ") assert excinfo.match(r" not in dictionary") assert excinfo.type == SettingsValueError # the value must have the right type with pytest.raises(SettingsValueError) as excinfo: NamedValue(value, int).validate({value: 'wrong_value'}) assert excinfo.match('does not have the right type') assert excinfo.type == SettingsValueError # the value to validate must be a dictionary with pytest.raises(SettingsValueError) as excinfo: NamedValue(value, int).validate(value) assert excinfo.match('is not a dictionary') assert excinfo.type == SettingsValueError
def __init__(self, values_dict: Dict) -> None: # pylint: disable=W0231 super(Settings, self).__init__({}) val_copy = copy.deepcopy(values_dict) self._dict_value = DictValue(val_copy) namedvalue_list = self._dict_value.values_list self._needed_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.mandatory) self._optional_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.optional) self._exclusive_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.exclusive) self._optional_values = self._optional_values | self._exclusive_values self._config_file = None # type: str
def test_DictValue_in_Dict(): '''Test the combination of DictValue and Dict.''' d = { 'key1': { 'age': 28, 'city': 'utrecht' }, 'key2': { 'age': 52, 'city': 'london' }, 'key3': { 'age': 24, 'city': 'rome' } } assert Value(Dict[str, DictValue({ 'age': int, 'city': str })]).validate(d) == d # wrong key type with pytest.raises(SettingsValueError) as excinfo: assert Value(Dict[int, DictValue({ 'age': int, 'city': str })]).validate(d) == d assert excinfo.match("Setting") assert excinfo.match("does not have the right type") assert excinfo.type == SettingsValueError # wrong key type with pytest.raises(SettingsValueError) as excinfo: assert Value(Dict[int, DictValue({ 'age': int, 'city': str })]).validate(45) == d assert excinfo.match("Setting") assert excinfo.match("does not have the right type") assert excinfo.match("This type can only validate dictionaries") assert excinfo.type == SettingsValueError
def test_nested_DictValue(): '''Test DictValues with other DictValues as types.''' d = { 'subsection1': { 'subsubsection1': 'asd', 'subsubsection2': 5 }, 'subsection2': [1, 2, 3] } assert DictValue({ 'subsection1': DictValue({ 'subsubsection1': str, 'subsubsection2': int }), 'subsection2': Value(List[int]) }).validate(d) == d # avoid repetition of DictValue in nested types assert DictValue({ 'subsection1': { 'subsubsection1': str, 'subsubsection2': int }, 'subsection2': Value(List[int]) }).validate(d) == d d2 = { 'subsection1': { 'subsubsection1': 'asd', 'subsubsection2': 5 }, 'subsection2': 1 } with pytest.raises(SettingsValueError) as excinfo: DictValue({ 'subsection1': { 'subsubsection1': str, 'subsubsection2': int }, 'subsection2': Value(List[int]) }).validate(d2) assert excinfo.match('Setting "subsection2"') assert excinfo.match("does not have the right type") assert excinfo.type == SettingsValueError # with optional sub-DictValues d3 = {'subsection2': [1, 2, 3]} assert DictValue({ 'subsection1': DictValue({ 'subsubsection1': str, 'subsubsection2': int }, kind=Kind.optional), 'subsection2': Value(List[int]) }).validate(d3) == d3
def test_exclusive(): '''Test exclusive values.''' d1 = {'age': 28} dval = DictValue({'age': Value(int, kind=Value.exclusive)}) assert dval.validate(d1) == d1 dval2 = DictValue({ 'age': Value(int, kind=Value.exclusive), 'city': Value(str, kind=Value.exclusive) }) # only one of them is present, ok assert dval2.validate(d1) == d1 d2 = {'age': 28, 'city': 'utrecht'} with pytest.raises(SettingsValueError) as excinfo: dval2.validate(d2) assert excinfo.match("Only one of the values") assert excinfo.match("can be present at the same time") assert excinfo.type == SettingsValueError
class Settings(dict): '''Contains the user settings and a method to validate settings files.''' def __init__(self, values_dict: Dict) -> None: # pylint: disable=W0231 super(Settings, self).__init__({}) val_copy = copy.deepcopy(values_dict) self._dict_value = DictValue(val_copy) namedvalue_list = self._dict_value.values_list self._needed_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.mandatory) self._optional_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.optional) self._exclusive_values = set(namedvalue.key for namedvalue in namedvalue_list if namedvalue.kind is Value.exclusive) self._optional_values = self._optional_values | self._exclusive_values self._config_file = None # type: str def __repr__(self) -> str: '''Representation of a settings instance.''' if self._config_file is None: # not validated _dict_value = repr(self._dict_value).replace('DictValue(', '', 1) _dict_value = _dict_value[:-1] return '{}({})'.format(self.__class__.__name__, _dict_value) else: return pprint.pformat(self.settings) def __eq__(self, other: object) -> bool: if not isinstance(other, Dict): return NotImplemented for key in self: if not key in other or self[key] != other[key]: return False return True def __ne__(self, other: object) -> bool: if not isinstance(other, Dict): return NotImplemented return not self.__eq__(other) # def __getstate__(self): # '''For pickle''' # d = self.__dict__.copy() # d['val_type'] = repr(d['val_type']) # return d # # def __setstate__(self, d): # '''For pickle''' # print(d) # d['val_type'] = eval(d['val_type']) # self.__dict__ = d @staticmethod def _get_property(key: str) -> Callable: '''Returns functions to get dictionary items. Used to create properties.''' def _get_prop(self: 'Settings') -> Any: '''Property to get values''' return self[key] return _get_prop @staticmethod def _set_property(key: str) -> Callable: '''Returns functions to set dictionary items. Used to create properties.''' def _set_prop(self: 'Settings', value: Any) -> None: '''Property to set values''' self[key] = value return _set_prop @staticmethod def _del_property(key: str) -> Callable: '''Returns functions to delete dictionary items. Used to create properties.''' def _del_prop(self: 'Settings') -> None: '''Property to delete values: delete from dictionary and the property.''' del self[key] delattr(self.__class__, key) return _del_prop def __setitem__(self, key: str, value: Any) -> None: '''All items added to the dictionary are accesible via dot notation''' dict.__setitem__(self, key, value) setattr( self.__class__, str(key), property(fget=self._get_property(key), fset=self._set_property(key), fdel=self._del_property(key), doc=str(key))) def __setattr__(self, key: str, value: Any) -> None: '''Set attributes as items in the dictionary, accesible with dot notation''' # this test allows attributes to be set in the __init__ method if not '_config_file' in self.__dict__: return dict.__setattr__(self, key, value) # known attributes set at __init__ are handled normally elif key in self.__dict__: dict.__setattr__(self, key, value) # own dictionary key (it's a property already) elif key in self: getattr(self.__class__, key).fset(self, value) # add unkown attributes to the dictionary else: self.__setitem__(key, value) @staticmethod def load_from_dict(d: Dict) -> 'Settings': '''Load the dictionary d as settings.''' settings = Settings({}) for key, value in d.items(): settings[key] = value settings._config_file = '' return settings @property def settings(self) -> Dict: '''Returns a dictionary with the settings''' return {key: value for key, value in self.items()} @log_exceptions_warnings def _validate_all_values(self, file_dict: Dict) -> Dict: '''Validates the settings in the config_dict using the settings list.''' # pprint.pprint(file_cte) present_values = set(file_dict.keys()) # if present values don't include all needed values if not present_values.issuperset(self._needed_values): raise SettingsFileError( 'Sections that are needed but not present in the file: ' + str(self._needed_values - present_values) + '. Those sections must be present!') set_extra = present_values - self._needed_values # if there are extra values and they aren't optional if set_extra and not set_extra.issubset(self._optional_values): warnings.warn( 'WARNING! The following values are not recognized:: ' + str(set_extra - self._optional_values) + '. Those values or sections should not be present', SettingsFileWarning) parsed_dict = dict(self._dict_value.validate(file_dict)) # pprint.pprint(parsed_dict) return parsed_dict @log_exceptions_warnings def validate(self, filename: str) -> None: ''' Load filename and extract the settings for the simulations If mandatory values are missing, errors are logged and exceptions are raised Warnings are logged if extra settings are found ''' logger = logging.getLogger(__name__) logger.info('Reading settings file (%s)...', filename) # load file into config_cte dictionary. # the function checks that the file exists and that there are no errors file_cte = Loader().load_settings_file(filename) # store original configuration file with open(filename, 'rt') as file: self._config_file = file.read() # validate all values in the configuration file settings_dict = self._validate_all_values(file_cte) # access settings directly as Setting.setting for key, value in settings_dict.items(): self[key] = value # log read and validated settings # use pretty print logger.debug('Settings dump:') logger.debug('File dict (config_cte):') logger.debug(pprint.pformat(file_cte)) logger.debug('Validated dict (cte):') logger.debug(repr(self)) logger.info('Settings loaded!')
f_float.__name__ = 'float(Fraction())' # smallest float number min_float = sys.float_info.min Vector = Tuple[f_float] settings = [DictValue('lattice', [Value('name', str), Value('spacegroup', Union[int, str]), Value('S_conc', float, val_min=0, val_max=100), Value('A_conc', float, val_min=0, val_max=100), Value('a', float, val_min=0), Value('b', float, val_min=0), Value('c', float, val_min=0), Value('alpha', float, val_min=0, val_max=360), Value('beta', float, val_min=0, val_max=360), Value('gamma', float, val_min=0, val_max=360), Value('sites_pos', Union[Vector, List[Vector]], val_min=0, val_max=1, len_min=[1, 3], len_max=[None, 3]), Value('sites_occ', Union[f_float, List[f_float]], val_min=0, val_max=1, len_min=1), Value('d_max', float, val_min=0, kind=Value.optional), Value('d_max_coop', float, val_min=0, kind=Value.optional), Value('N_uc', int, val_min=1, kind=Value.exclusive), Value('radius', float, val_min=min_float, kind=Value.exclusive)]), DictValue('states', [Value('sensitizer_ion_label', str), Value('activator_ion_label', str), Value('sensitizer_states_labels', List[str], len_min=1), Value('activator_states_labels', List[str], len_min=1)]),
def test_DictValue(): '''Test DictValues''' d = {'age': 28, 'city': 'utrecht'} # simple type list dval1 = DictValue({'age': int, 'city': str}) assert dval1.validate(d) == d # Value list dval2 = DictValue({'age': Value(int, val_min=0), 'city': Value(str)}) assert dval2.validate(d) == d # mixed dval3 = DictValue({'age': Value(int, val_min=0), 'city': str}) assert dval3.validate(d) == d # both methods create the same DictValues, their reprs are equal assert repr(dval1) == repr(dval2) assert repr(dval1) == repr(dval3) d = {'age': 28} # list of names and types is not a valid constructor with pytest.raises(SettingsValueError) as excinfo: DictValue(['age', str]).validate(d) assert excinfo.match("The first argument must be a dictionary") assert excinfo.type == SettingsValueError d = 'not a dictionary' # DictValues only validate dictionaries with pytest.raises(SettingsValueError) as excinfo: DictValue({'age': int}).validate(d) assert excinfo.match("does not have the right type") assert excinfo.type == SettingsValueError # extra key:value pairs are ok, but they give off a warning and are ignored d = {'age': 28, 'city': 'utrecht'} d2 = {'age': 28, 'city': 'utrecht', 'extra': True} with pytest.warns(SettingsExtraValueWarning) as warnings: assert DictValue({'age': int, 'city': str}).validate(d2) == d assert len(warnings) == 1 # one warning warning = warnings.pop(SettingsExtraValueWarning) assert warning.category == SettingsExtraValueWarning assert 'Some values or sections should not be present in the file' in str( warning.message) d = {'age': 28} # the dict doesn't contain all keys with pytest.raises(SettingsValueError) as excinfo: DictValue({'age': int, 'city': str}).validate(d) assert excinfo.match("Setting ") assert excinfo.match("not in dictionary") assert excinfo.type == SettingsValueError d = {'ag': 28, 'city': 'utrecht'} # wrong key name with pytest.raises(SettingsValueError) as excinfo: DictValue({'age': int, 'city': str}).validate(d) assert excinfo.match("Setting ") assert excinfo.match("not in dictionary") assert excinfo.type == SettingsValueError d = {'age': 'asd', 'city': 'utrecht'} # wrong value type with pytest.raises(SettingsValueError) as excinfo: DictValue({'age': int, 'city': str}).validate(d) assert excinfo.match("Setting ") assert excinfo.match("does not have the right type") assert excinfo.type == SettingsValueError # DictValue in Value d = {'age': 28, 'city': 'utrecht'} assert Value(DictValue({'age': int, 'city': str})).validate(d) == d