Exemple #1
0
    def require_scalar(self, *args: Type) -> None:
        """Require the node to be a scalar.

        If additional arguments are passed, these are taken as a list \
        of valid types; if the node matches one of these, then it is \
        accepted.

        Example:
            # Match either an int or a string
            node.require_scalar(int, str)

        Arguments:
            args: One or more types to match one of.
        """
        node = Node(self.yaml_node)
        if len(args) == 0:
            if not node.is_scalar():
                raise RecognitionError(('{}{}A scalar is required').format(
                    self.yaml_node.start_mark, os.linesep))
        else:
            for typ in args:
                if node.is_scalar(typ):
                    return
            raise RecognitionError(
                ('{}{}A scalar of type {} is required').format(
                    self.yaml_node.start_mark, os.linesep, args))
Exemple #2
0
    def __check_no_missing_attributes(self, node: yaml.Node,
                                      mapping: CommentedMap) -> None:
        """Checks that all required attributes are present.

        Also checks that they're of the correct type.

        Args:
            mapping: The mapping with subobjects of this object.

        Raises:
            RecognitionError: if an attribute is missing or the type
                is incorrect.
        """
        logger.debug('Checking presence of required attributes')
        for name, type_, required in class_subobjects(self.class_):
            if required and name not in mapping:
                raise RecognitionError(('{}{}Missing attribute "{}" needed for'
                                        ' constructing a {}').format(
                                            node.start_mark, os.linesep, name,
                                            self.class_.__name__))
            if name in mapping and not self.__type_matches(
                    mapping[name], type_):
                raise RecognitionError(('{}{}Attribute "{}" has incorrect type'
                                        ' {}, expecting a {}').format(
                                            node.start_mark, os.linesep, name,
                                            type(mapping[name]), type_))
Exemple #3
0
    def require_attribute(self,
                          attribute: str,
                          typ: Union[None, Type] = _Any) -> None:
        """Require an attribute on the node to exist.

        This implies that the node must be a mapping.

        If ``typ`` is given, the attribute must have this type.

        Args:
            attribute: The name of the attribute / mapping key.
            typ: The type the attribute must have.
        """
        self.require_mapping()
        attr_nodes = [
            value_node for key_node, value_node in self.yaml_node.value
            if key_node.value == attribute
        ]
        if len(attr_nodes) == 0:
            raise RecognitionError(
                ('{}{}Missing required attribute "{}"').format(
                    self.yaml_node.start_mark, os.linesep, attribute))
        attr_node = attr_nodes[0]

        if typ != _Any:
            recognized_types, message = self.__recognizer.recognize(
                attr_node, cast(Type, typ))
            if len(recognized_types) == 0:
                raise RecognitionError(message)
Exemple #4
0
    def __call__(
            self, loader: 'Loader', node: yaml.Node
            ) -> Generator[Any, None, None]:
        """Construct a Path object from a yaml node.

        This expects the node to contain a string with the path.

        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 pathlib.Path')

        if not isinstance(node, yaml.ScalarNode) or not isinstance(
                node.value, str):
            raise RecognitionError(
                    ('{}{}Expected a string containing a Path.').format(
                        node.start_mark, os.linesep))

        # ruamel.yaml expects us to yield an incomplete object, but Paths are
        # special, so we'll have to make the whole thing right away.
        new_obj = pathlib.Path(node.value)
        yield new_obj
Exemple #5
0
    def __call__(self, loader: 'Loader',
                 node: yaml.Node) -> Generator[Any, None, None]:
        """Construct an user string value from a yaml node.

        This constructs an object of the user-defined string class that
        this is the constructor for. It is registered with the yaml
        library, and called by it.

        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.ScalarNode) or not isinstance(
                node.value, str):
            raise RecognitionError(
                ('{}{}Expected a string matching a {}.').format(
                    node.start_mark, os.linesep, self.class_.__name__))

        # ruamel.yaml expects us to yield an incomplete object, but strings are
        # immutable, so we'll have to make the whole thing right away.
        new_obj = self.class_(node.value)
        yield new_obj
Exemple #6
0
    def __strip_extra_attributes(self, node: yaml.Node,
                                 known_attrs: List[str]) -> None:
        """Strips tags from extra attributes.

        This prevents nodes under attributes that are not part of our
        data model from being converted to objects. They'll be plain
        CommentedMaps instead, which then get converted to OrderedDicts
        for the user.

        Args:
            node: The node to process
            known_attrs: The attributes to not strip
        """
        known_keys = list(known_attrs)
        if 'self' not in known_keys:
            raise RuntimeError('The __init__ method of {} does not have a'
                               ' "self" attribute! Please add one, this is'
                               ' not a valid constructor.'.format(
                                   self.class_.__name__))
        known_keys.remove('self')
        if '_yatiml_extra' in known_keys:
            known_keys.remove('_yatiml_extra')

        for key_node, value_node in node.value:
            if (not isinstance(key_node, yaml.ScalarNode)
                    or key_node.tag != 'tag:yaml.org,2002:str'):
                raise RecognitionError(
                    ('{}{}Mapping keys that are not of type'
                     ' string are not supported by YAtiML.').format(
                         node.start_mark, os.linesep))
            if key_node.value not in known_keys:
                strip_tags(self.__loader, value_node)
Exemple #7
0
    def __strip_extra_attributes(self, node: yaml.Node,
                                 known_attrs: List[str]) -> None:
        """Strips tags from extra attributes.

        This prevents nodes under attributes that are not part of our \
        data model from being converted to objects. They'll be plain \
        CommentedMaps instead, which then get converted to OrderedDicts \
        for the user.

        Args:
            node: The node to process
            known_attrs: The attributes to not strip
        """
        known_keys = list(known_attrs)
        known_keys.remove('self')
        if 'yatiml_extra' in known_keys:
            known_keys.remove('yatiml_extra')

        for key_node, value_node in node.value:
            if (not isinstance(key_node, yaml.ScalarNode)
                    or key_node.tag != 'tag:yaml.org,2002:str'):
                raise RecognitionError(
                    ('{}{}Mapping keys that are not of type'
                     ' string are not supported by YAtiML.').format(
                         node.start_mark, os.linesep))
            if key_node.value not in known_keys:
                self.__strip_tags(value_node)
Exemple #8
0
    def require_attribute_value(
            self, attribute: str, value: Union[int, str, float, bool,
                                               None]) -> None:
        """Require an attribute on the node to have a particular value.

        This requires the attribute to exist, and to have the given value \
        and corresponding type. Handy for in your yatiml_recognize() \
        function.

        Args:
            attribute: The name of the attribute / mapping key.
            value: The value the attribute must have to recognize an \
                    object of this type.

        Raises:
            RecognitionError: If the attribute does not exist, or does \
                    not have the required value.
        """
        found = False
        for key_node, value_node in self.yaml_node.value:
            if (key_node.tag == 'tag:yaml.org,2002:str'
                    and key_node.value == attribute):
                found = True
                node = Node(value_node)
                if not node.is_scalar(type(value)):
                    raise RecognitionError(
                        ('{}{}Incorrect attribute type where value {}'
                         ' of type {} was required').format(
                             self.yaml_node.start_mark, os.linesep, value,
                             type(value)))
                if node.get_value() != value:
                    raise RecognitionError(
                        ('{}{}Incorrect attribute value'
                         ' {} where {} was required').format(
                             self.yaml_node.start_mark, os.linesep,
                             value_node.value, value))

        if not found:
            raise RecognitionError(
                ('{}{}Required attribute {} not found').format(
                    self.yaml_node.start_mark, os.linesep, attribute))
Exemple #9
0
    def __type_check_attributes(self, node: yaml.Node, mapping: CommentedMap,
                                argspec: inspect.FullArgSpec) -> None:
        """Ensure all attributes have a matching constructor argument.

        This checks that there is a constructor argument with a
        matching type for each existing attribute.

        If the class has a _yatiml_extra attribute, then extra
        attributes are okay and no error will be raised if they exist.

        Args:
            node: The node we're processing
            mapping: The mapping with constructed subobjects
            constructor_attrs: The attributes of the constructor,
                    including self and _yatiml_extra, if applicable
        """
        logger.debug('Checking for extraneous attributes')
        logger.debug('Constructor arguments: {}, mapping: {}'.format(
            argspec.args, list(mapping.keys())))
        for key, value in mapping.items():
            if not isinstance(key, str):
                raise RecognitionError(('{}{}YAtiML only supports strings'
                                        ' for mapping keys').format(
                                            node.start_mark, os.linesep))
            if key not in argspec.args and '_yatiml_extra' not in argspec.args:
                raise RecognitionError(
                    ('{}{}Found additional attributes ("{}")'
                     ' and {} does not support those').format(
                         node.start_mark, os.linesep, key,
                         self.class_.__name__))

            if key in argspec.args and key in argspec.annotations:
                if not self.__type_matches(value, argspec.annotations[key]):
                    raise RecognitionError(
                            ('{}{}Expected attribute "{}" to be of type {}'
                             ' but it is a(n) {}').format(
                                    node.start_mark, os.linesep, key,
                                    argspec.annotations[key], type(value)))
Exemple #10
0
    def recognize(self, node: yaml.Node, expected_type: Type) -> RecResult:
        """Figure out how to interpret this node.

        This is not quite a type check. This function makes a list of
        all types that match the expected type and also the node, and
        returns that list. The goal here is not to test validity, but
        to determine how to process this node further.

        That said, it will recognize built-in types only in case of
        an exact match.

        Args:
            node: The YAML node to recognize.
            expected_type: The type we expect this node to be, based
                    on the context provided by our type definitions.

        Returns:
            A list of matching types.
        """
        logger.debug('Recognizing {} as a {}'.format(node, expected_type))
        recognized_types = None  # type: Any
        if expected_type in (str, int, float, bool, bool_union_fix, date, None,
                             type(None)):
            recognized_types, message = self.__recognize_scalar(
                node, expected_type)
        elif expected_type in self.__additional_classes:
            recognized_types, message = self.__recognize_additional(
                node, expected_type)
        elif is_generic_union(expected_type):
            recognized_types, message = self.__recognize_union(
                node, expected_type)
        elif is_generic_sequence(expected_type):
            recognized_types, message = self.__recognize_list(
                node, expected_type)
        elif is_generic_mapping(expected_type):
            recognized_types, message = self.__recognize_dict(
                node, expected_type)
        elif expected_type in self.__registered_classes.values():
            recognized_types, message = self.__recognize_user_classes(
                node, expected_type)
        elif expected_type in (Any, ):
            recognized_types, message = [Any], ''

        if recognized_types is None:
            raise RecognitionError(
                ('Could not recognize for type {},'
                 ' is it registered?').format(expected_type.__name__))
        logger.debug('Recognized types {} matching {}'.format(
            recognized_types, expected_type))
        return recognized_types, message
Exemple #11
0
    def require_attribute_value_not(
            self, attribute: str, value: Union[int, str, float, bool,
                                               None]) -> None:
        """Require an attribute on the node to not have a given value.

        This requires the attribute to exist, and to not have the given
        value.

        Args:
            attribute: The name of the attribute / mapping key.
            value: The value the attribute must not have to recognize an
                    object of this type.

        Raises:
            yatiml.RecognitionError: If the attribute does not exist,
                    or has the required value.
        """
        found = False
        for key_node, value_node in self.yaml_node.value:
            if (key_node.tag == 'tag:yaml.org,2002:str'
                    and key_node.value == attribute):
                found = True
                node = Node(value_node)
                if not node.is_scalar(type(value)):
                    return
                if node.get_value() == value:
                    raise RecognitionError(
                        ('{}{}Incorrect attribute value'
                         ' {} where {} was not allowed').format(
                             self.yaml_node.start_mark, os.linesep,
                             value_node.value, value))

        if not found:
            raise RecognitionError(
                ('{}{}Required attribute "{}" not found').format(
                    self.yaml_node.start_mark, os.linesep, attribute))
Exemple #12
0
 def require_sequence(self) -> None:
     """Require the node to be a sequence."""
     if not isinstance(self.yaml_node, yaml.SequenceNode):
         raise RecognitionError(('{}{}A sequence is required here').format(
             self.yaml_node.start_mark, os.linesep))
Exemple #13
0
 def require_mapping(self) -> None:
     """Require the node to be a mapping."""
     if not isinstance(self.yaml_node, yaml.MappingNode):
         raise RecognitionError(('{}{}A mapping is required here').format(
             self.yaml_node.start_mark, os.linesep))
Exemple #14
0
    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__))
Exemple #15
0
    def __process_node(self, node: yaml.Node,
                       expected_type: Type) -> yaml.Node:
        """Processes a node.

        This is the main function that implements yatiml's \
        functionality. It figures out how to interpret this node \
        (recognition), then applies syntactic sugar, and finally \
        recurses to the subnodes, if any.

        Args:
            node: The node to process.
            expected_type: The type we expect this node to be.

        Returns:
            The transformed node, or a transformed copy.
        """
        logger.info('Processing node {} expecting type {}'.format(
            node, expected_type))

        # figure out how to interpret this node
        recognized_types, message = self.__recognizer.recognize(
            node, expected_type)

        if len(recognized_types) != 1:
            raise RecognitionError(message)

        recognized_type = recognized_types[0]

        # remove syntactic sugar
        logger.debug('Savorizing node {}'.format(node))
        if recognized_type in self._registered_classes.values():
            node = self.__savorize(node, recognized_type)
        logger.debug('Savorized, now {}'.format(node))

        # process subnodes
        logger.debug('Recursing into subnodes')
        if is_generic_list(recognized_type):
            if node.tag != 'tag:yaml.org,2002:seq':
                raise RecognitionError('{}{}Expected a {} here'.format(
                    node.start_mark, os.linesep, type_to_desc(expected_type)))
            for item in node.value:
                self.__process_node(item,
                                    generic_type_args(recognized_type)[0])
        elif is_generic_dict(recognized_type):
            if node.tag != 'tag:yaml.org,2002:map':
                raise RecognitionError('{}{}Expected a {} here'.format(
                    node.start_mark, os.linesep, type_to_desc(expected_type)))
            for _, value_node in node.value:
                self.__process_node(value_node,
                                    generic_type_args(recognized_type)[1])

        elif recognized_type in self._registered_classes.values():
            if (not issubclass(recognized_type, enum.Enum)
                    and not issubclass(recognized_type, str)
                    and not issubclass(recognized_type, UserString)):
                for attr_name, type_, _ in class_subobjects(recognized_type):
                    cnode = Node(node)
                    if cnode.has_attribute(attr_name):
                        subnode = cnode.get_attribute(attr_name)
                        new_subnode = self.__process_node(
                            subnode.yaml_node, type_)
                        cnode.set_attribute(attr_name, new_subnode)
        else:
            logger.debug('Not a generic class or a user-defined class, not'
                         ' recursing')

        node.tag = self.__type_to_tag(recognized_type)
        logger.debug('Finished processing node {}'.format(node))
        return node