def __fix(self, current_data: dict, users_data: CommentedMap, key_path='') -> dict: if not isinstance(users_data, dict): self.__has_changes = True return current_data else: result = users_data.copy() divider = ' -> ' if len(key_path) > 0 else '' for key in current_data.keys(): current_key_path = key_path + divider + key if key in users_data: # if key presents in user's data if isinstance(current_data[key], dict): # dive deeper result[key] = self.__fix(current_data[key], users_data[key], current_key_path) else: # use the value in user's data result[key] = users_data[key] else: # missing config key result[key] = current_data[key] self.__has_changes = True self.logger.warning( 'Option "{}" missing, use default value "{}"'.format( current_key_path, current_data[key])) return result
def __split_off_extra_attributes(self, mapping: CommentedMap, known_attrs: List[str]) -> CommentedMap: """Separates the extra attributes in mapping into _yatiml_extra. This returns a mapping containing all key-value pairs from mapping whose key is in known_attrs, and an additional key _yatiml_extra which maps to a dict containing the remaining key-value pairs. Args: mapping: The mapping to split known_attrs: Attributes that should be kept in the main map, and not moved to _yatiml_extra. Returns: A map with attributes reorganised as described above. """ attr_names = list(mapping.keys()) main_attrs = mapping.copy() # type: CommentedMap extra_attrs = OrderedDict(mapping.items()) for name in attr_names: if name not in known_attrs or name == '_yatiml_extra': del (main_attrs[name]) else: del (extra_attrs[name]) main_attrs['_yatiml_extra'] = extra_attrs return main_attrs
def __call__(self, loader: 'Loader', node: yaml.Node) -> Generator[Any, None, None]: """Construct an object from a yaml node. This constructs an object of the user-defined type that this is the constructor for. It is registered with the yaml library, and called by it. Recursion is handled by calling the yaml library, so we only need to construct an object using the keys and values of the given MappingNode, and those values have been converted recursively for us. Since Python does not do type checks, we do a type check manually, to ensure that the class's constructor gets the types it expects. This avoids confusing errors, but moreover is a security feature that ensures that regardless of the content of the YAML file, we produce the objects that the programmer defined and expects. Note that this yields rather than returns, in a somewhat odd way. That's directly from the PyYAML/ruamel.yaml documentation. Args: loader: The yatiml.loader that is creating this object. node: The node to construct from. Yields: The incomplete constructed object. """ logger.debug('Constructing an object of type {}'.format( self.class_.__name__)) if not isinstance(node, yaml.MappingNode): raise RecognitionError( ('{}{}Expected a MappingNode. There' ' is probably something wrong with your _yatiml_savorize()' ' function.').format(node.start_mark, os.linesep)) self.__loader = loader # figure out which keys are extra and strip them of tags # to prevent constructing objects we haven't type checked argspec = inspect.getfullargspec(self.class_.__init__) self.__strip_extra_attributes(node, argspec.args) # create object and let yaml lib construct subobjects new_obj = self.class_.__new__(self.class_) # type: ignore yield new_obj mapping = CommentedMap() loader.construct_mapping(node, mapping, deep=True) # Convert ruamel.yaml's round-trip types to list and OrderedDict, # recursively for each attribute value in our mapping. Note that # mapping itself is still a CommentedMap. for key, value in mapping.copy().items(): if (isinstance(value, CommentedMap) or isinstance(value, CommentedSeq)): mapping[key] = self.__to_plain_containers(value) # do type check self.__check_no_missing_attributes(node, mapping) self.__type_check_attributes(node, mapping, argspec) # construct object, this should work now try: logger.debug('Calling __init__') if '_yatiml_extra' in argspec.args: attrs = self.__split_off_extra_attributes( mapping, argspec.args) new_obj.__init__(**attrs) else: new_obj.__init__(**mapping) except Exception as e: tb = traceback.format_exc() raise RecognitionError(( '{}{}Could not construct object of class {}: {}\n{}' ).format( node.start_mark, os.linesep, self.class_.__name__, e, tb)) logger.debug('Done constructing {}'.format(self.class_.__name__))
class YAMLRoundtripConfig(MutableConfigFile, MutableAbstractItemAccessMixin, MutableAbstractDictFunctionsMixin): """ Class for YAML-based (roundtrip) configurations """ def __init__(self, owner: Any, manager: "m.StorageManager", path: str, *args: List[Any], **kwargs: Dict[Any, Any]): self.data = CommentedMap() super().__init__(owner, manager, path, *args, **kwargs) def load(self): with open(self.path, "r") as fh: self.data = yaml.round_trip_load(fh, version=(1, 2)) def reload(self): self.unload() self.load() def unload(self): self.data.clear() def save(self): if not self.mutable: raise RuntimeError("You may not modify a defaults file at runtime - check the mutable attribute!") with open(self.path, "w") as fh: yaml.round_trip_dump(self.data, fh) # region: CommentedMap functions def insert(self, pos, key, value, *, comment=None): """ Insert a `key: value` pair at the given position, attaching a comment if provided Wrapper for `CommentedMap.insert()` """ return self.data.insert(pos, key, value, comment) def add_eol_comment(self, comment, *, key=NoComment, column=30): """ Add an end-of-line comment for a key at a particular column (30 by default) Wrapper for `CommentedMap.yaml_add_eol_comment()` """ # Setting the column to None as the API actually defaults to will raise an exception, so we have to # specify one unfortunately return self.data.yaml_add_eol_comment(comment, key=key, column=column) def set_comment_before_key(self, key, comment, *, indent=0): """ Set a comment before a given key Wrapper for `CommentedMap.yaml_set_comment_before_after_key()` """ return self.data.yaml_set_comment_before_after_key( key, before=comment, indent=indent, after=None, after_indent=None ) def set_start_comment(self, comment, indent=0): """ Set the starting comment Wrapper for `CommentedMap.yaml_set_start_comment()` """ return self.data.yaml_set_start_comment(comment, indent=indent) # endregion # region: Dict functions def clear(self): return self.data.clear() def copy(self): return self.data.copy() def get(self, key, default=None): return self.data.get(key, default) def items(self): return self.data.items() def keys(self): return self.data.keys() def pop(self, key, default=None): return self.data.pop(key, default) def popitem(self): return self.data.popitem() def setdefault(self, key, default=None): if key not in self.data: self.data[key] = default return default return self.data[key] def update(self, other): return self.data.update(other) def values(self): return self.data.values() # endregion # Item access functions def __contains__(self, key): """ Wrapper for `dict.__contains__()` """ return self.data.__contains__(key) def __delitem__(self, key): """ Wrapper for `dict.__delitem__()` """ del self.data[key] def __getitem__(self, key): """ Wrapper for `dict.__getitem__()` """ return self.data.__getitem__(key) def __iter__(self): """ Wrapper for `dict.__iter__()` """ return self.data.__iter__() def __len__(self): """ Wrapper for `dict.__len__()` """ return self.data.__len__() def __setitem__(self, key, value): """ Wrapper for `dict.__getitem__()` """ return self.data.__setitem__(key, value)
def test_CommentedMap(self): cm = CommentedMap() # check bug in ruamel.yaml is fixed: raises TypeError: source has undefined order self.assertEqual(cm, cm.copy())