Example #1
0
def test_get_inner_type() -> None:
    """Test retrievel of inner type of a model field representing a collection."""
    good_catalog = load_good_catalog()

    with pytest.raises(err.TrestleError):
        # Type of catalog is not a collection field type
        mutils.get_inner_type(type(good_catalog))

    with pytest.raises(err.TrestleError):
        # Type of field catalog is not a collection field type
        catalog_field = catalog.Model.alias_to_field_map()['catalog']
        mutils.get_inner_type(catalog_field.outer_type_)

    with pytest.raises(err.TrestleError):
        # Type of roles object is not a collection field type
        mutils.get_inner_type(type(good_catalog.metadata.roles))

    # Type of field roles is a collection field type
    roles_field = common.Metadata.alias_to_field_map()['roles']
    role_type = mutils.get_inner_type(roles_field.outer_type_)
    assert role_type == common.Role

    with pytest.raises(err.TrestleError):
        # Type of responsible_parties object is not a collection field type
        mutils.get_inner_type(type(good_catalog.metadata.responsible_parties))

    # Type of field responsible-parties is a collection field type
    responsible_parties_field = common.Metadata.alias_to_field_map(
    )['responsible-parties']
    responsible_party_type = mutils.get_inner_type(
        responsible_parties_field.outer_type_)
    assert responsible_party_type == common.ResponsibleParty
def test_get_obm_wrapped_type(element_path: str, collection: bool,
                              type_or_inner_type: Type[OscalBaseModel],
                              exception_expected: bool):
    """Test whether we can wrap a control properly."""
    if exception_expected:
        with pytest.raises(TrestleError):
            _ = ElementPath(element_path).get_obm_wrapped_type()
        return
    my_type = ElementPath(element_path).get_obm_wrapped_type()
    if collection:
        inner_type = utils.get_inner_type(my_type)
        assert type_or_inner_type == inner_type
    else:
        assert type_or_inner_type == my_type
Example #3
0
    def get_relative_model_type(
            relative_path: pathlib.Path) -> Tuple[Type[OscalBaseModel], str]:
        """
        Given the relative path of a file with respect to 'trestle_root' return the oscal model type.

        Args:
            relative_path: Relative path of the model with respect to the root directory of the trestle project.
        Returns:
            Type of Oscal Model for the provided model
            Alias of that oscal model.
        """
        if len(relative_path.parts) < 2:
            raise TrestleError(
                'Insufficient path length to be a valid relative path w.r.t Trestle project root directory.'
            )
        model_dir = relative_path.parts[0]
        model_relative_path = pathlib.Path(
            *relative_path.parts[2:])  # catalogs, profiles, etc

        if model_dir in const.MODEL_DIR_LIST:
            module_name = const.MODEL_DIR_TO_MODEL_MODULE[model_dir]
        else:
            raise TrestleError(
                f'No valid trestle model type directory (e.g. catalogs) found for {model_dir}.'
            )

        model_type, model_alias = ModelUtils.get_root_model(module_name)
        full_alias = model_alias

        for index, part in enumerate(model_relative_path.parts):
            alias = ModelUtils._extract_alias(part)
            if index > 0 or model_alias != alias:
                model_alias = alias
                full_alias = f'{full_alias}.{model_alias}'
                if utils.is_collection_field_type(model_type):
                    model_type = utils.get_inner_type(model_type)
                else:
                    model_type = model_type.alias_to_field_map(
                    )[alias].outer_type_

        return model_type, full_alias
Example #4
0
    def get_singular_alias(
            alias_path: str,
            relative_path: Optional[pathlib.Path] = None) -> str:
        """
        Get the alias in the singular form from a jsonpath.

        If contextual_mode is True and contextual_path is None, it assumes alias_path
        is relative to the directory the user is running trestle from.

        Args:
            alias_path: The current alias element path as a string
            relative_path: Optional relative path (w.r.t. trestle_root) to cater for relative element paths.
        Returns:
            Alias as a string
        """
        if len(alias_path.strip()) == 0:
            raise err.TrestleError(f'Invalid jsonpath {alias_path}')

        singular_alias: str = ''

        full_alias_path = alias_path
        if relative_path:
            logger.debug(f'get_singular_alias contextual mode: {str}')
            _, full_model_alias = ModelUtils.get_relative_model_type(
                relative_path)
            first_alias_a = full_model_alias.split('.')[-1]
            first_alias_b = alias_path.split('.')[0]
            if first_alias_a == first_alias_b:
                full_model_alias = '.'.join(full_model_alias.split('.')[:-1])
            full_alias_path = '.'.join([full_model_alias,
                                        alias_path]).strip('.')

        path_parts = full_alias_path.split(const.ALIAS_PATH_SEPARATOR)
        logger.debug(f'path parts: {path_parts}')

        model_types = []

        root_model_alias = path_parts[0]
        found = False
        for module_name in const.MODEL_TYPE_TO_MODEL_MODULE.values():
            model_type, model_alias = ModelUtils.get_root_model(module_name)
            if root_model_alias == model_alias:
                found = True
                model_types.append(model_type)
                break

        if not found:
            raise err.TrestleError(
                f'{root_model_alias} is an invalid root model alias.')

        if len(path_parts) == 1:
            return root_model_alias

        model_type = model_types[0]
        # go through path parts skipping first one
        for i in range(1, len(path_parts)):
            if utils.is_collection_field_type(model_type):
                # if it is a collection type and last part is * then break
                if i == len(path_parts) - 1 and path_parts[i] == '*':
                    break
                # otherwise get the inner type of items in the collection
                model_type = utils.get_inner_type(model_type)
                # and bump i
                i = i + 1
            else:
                path_part = path_parts[i]
                field_map = model_type.alias_to_field_map()
                if path_part not in field_map:
                    continue
                field = field_map[path_part]
                model_type = field.outer_type_
            model_types.append(model_type)

        last_alias = path_parts[-1]
        if last_alias == '*':
            last_alias = path_parts[-2]

        # generic model and not list, so return itself fixme doc
        if not utils.is_collection_field_type(model_type):
            return last_alias

        parent_model_type = model_types[-2]
        try:
            field_map = parent_model_type.alias_to_field_map()
            field = field_map[last_alias]
            outer_type = field.outer_type_
            inner_type = utils.get_inner_type(outer_type)
            inner_type_name = inner_type.__name__
            singular_alias = str_utils.classname_to_alias(
                inner_type_name, AliasMode.JSON)
        except Exception as e:
            raise err.TrestleError(f'Error in json path {alias_path}: {e}')

        return singular_alias
Example #5
0
def generate_sample_model(
    model: Union[Type[TG], List[TG], Dict[str, TG]], include_optional: bool = False, depth: int = -1
) -> TG:
    """Given a model class, generate an object of that class with sample values.

    Can generate optional variables with an enabled flag. Any array objects will have a single entry injected into it.

    Note: Trestle generate will not activate recursive loops irrespective of the depth flag.

    Args:
        model: The model type provided. Typically for a user as an OscalBaseModel Subclass.
        include_optional: Whether or not to generate optional fields.
        depth: Depth of the tree at which optional fields are generated. Negative values (default) removes the limit.

    Returns:
        The generated instance with a pro-forma values filled out as best as possible.
    """
    effective_optional = include_optional and not depth == 0

    model_type = model
    # This block normalizes model type down to
    if utils.is_collection_field_type(model):  # type: ignore
        model_type = utils.get_origin(model)  # type: ignore
        model = utils.get_inner_type(model)  # type: ignore
    model = cast(TG, model)

    model_dict = {}
    # this block is needed to avoid situations where an inbuilt is inside a list / dict.
    # the only time dict ever appears is with include_all, which is handled specially
    # the only type of collection possible after OSCAL 1.0.0 is list
    if safe_is_sub(model, OscalBaseModel):
        for field in model.__fields__:
            if field == 'include_all':
                if include_optional:
                    model_dict[field] = {}
                continue
            outer_type = model.__fields__[field].outer_type_
            # next appears to be needed for python 3.7
            if utils.get_origin(outer_type) == Union:
                outer_type = outer_type.__args__[0]
            if model.__fields__[field].required or effective_optional:
                # FIXME could be ForwardRef('SystemComponentStatus')
                if utils.is_collection_field_type(outer_type):
                    inner_type = utils.get_inner_type(outer_type)
                    if inner_type == model:
                        continue
                    model_dict[field] = generate_sample_model(
                        outer_type, include_optional=include_optional, depth=depth - 1
                    )
                elif safe_is_sub(outer_type, OscalBaseModel):
                    model_dict[field] = generate_sample_model(
                        outer_type, include_optional=include_optional, depth=depth - 1
                    )
                else:
                    # Hacking here:
                    # Root models should ideally not exist, however, sometimes we are stuck with them.
                    # If that is the case we need sufficient information on the type in order to generate a model.
                    # E.g. we need the type of the container.
                    if field == '__root__' and hasattr(model, '__name__'):
                        model_dict[field] = generate_sample_value_by_type(
                            outer_type, str_utils.classname_to_alias(model.__name__, AliasMode.FIELD)
                        )
                    else:
                        model_dict[field] = generate_sample_value_by_type(outer_type, field)
        # Note: this assumes list constrains in oscal are always 1 as a minimum size. if two this may still fail.
    else:
        if model_type is list:
            return [generate_sample_value_by_type(model, '')]
        if model_type is dict:
            return {'REPLACE_ME': generate_sample_value_by_type(model, '')}
        raise err.TrestleError('Unhandled collection type.')
    if model_type is list:
        return [model(**model_dict)]
    if model_type is dict:
        return {'REPLACE_ME': model(**model_dict)}
    return model(**model_dict)
Example #6
0
    def get_type(self,
                 root_model: Optional[Type[Any]] = None,
                 use_parent: bool = False) -> Type[Any]:
        """Get the type of an element.

        If possible the model type will be derived from one of the top level models,
        otherwise a 'root model' can be passed for situations where this is not possible.

        This type path should *NOT* have wild cards in it. It *may* have* indices.
        Valid Examples:
            catalog.metadata
            catalog.groups
            catalog.groups.group
            catalog
            catalog.groups.0

        Args:
            root_model: An OscalBaseModel Type from which to base the approach on.
            use_parent: Whether or not to normalise the full path across parent ElementPaths, default to not.

        Returns:
            The type of the model whether or not it is an OscalBaseModel or not.
        """
        effective_path: List[str]
        if use_parent:
            effective_path = self.get_full_path_parts()
        else:
            effective_path = self._path

        if not root_model:
            # lookup root model from top level oscal models or fail
            prev_model = self._top_level_type_lookup(effective_path[0])
        else:
            prev_model = root_model
        if len(effective_path) == 1:
            return prev_model
        # variables
        # for current_element_str in effective_path[1:]:
        for current_element_str in effective_path[1:]:
            # Determine if the parent model is a collection.
            if utils.is_collection_field_type(prev_model):
                inner_model = utils.get_inner_type(prev_model)
                inner_class_name = classname_to_alias(inner_model.__name__,
                                                      AliasMode.JSON)
                # Assert that the current name fits an expected form.
                # Valid choices here are *, integer (for arrays) and the inner model alias
                if (inner_class_name == current_element_str
                        or current_element_str == self.WILDCARD
                        or current_element_str.isnumeric()):
                    prev_model = inner_model

                else:
                    raise TrestleError(
                        'Unexpected key in element path when finding type.')

            else:
                # Indices, * are not allowed on non-collection types
                if current_element_str == self.WILDCARD:
                    raise TrestleError(
                        'Wild card in unexpected position when trying to find class type.'
                        +
                        ' Element path type lookup can only occur where a single type can be identified.'
                    )
                prev_model = prev_model.alias_to_field_map(
                )[current_element_str].outer_type_
        return prev_model
Example #7
0
def test_get_relative_model_type(tmp_path: pathlib.Path) -> None:
    """Test get model type and alias based on filesystem context."""
    import trestle.common.type_utils as cutils
    with pytest.raises(TrestleError):
        ModelUtils.get_relative_model_type(pathlib.Path('invalidpath'))

    with pytest.raises(TrestleError):
        ModelUtils.get_relative_model_type(pathlib.Path('./'))

    catalogs_dir = pathlib.Path('catalogs')
    mycatalog_dir = catalogs_dir / 'mycatalog'
    catalog_dir = mycatalog_dir / 'catalog'
    metadata_dir = catalog_dir / 'metadata'
    roles_dir = metadata_dir / 'roles'
    rps_dir = metadata_dir / 'responsible-parties'
    props_dir = metadata_dir / 'props'
    groups_dir = catalog_dir / 'groups'
    group_dir = groups_dir / f'00000{const.IDX_SEP}group'
    controls_dir = group_dir / 'controls'
    with pytest.raises(TrestleError):
        ModelUtils.get_relative_model_type(catalogs_dir)

    assert ModelUtils.get_relative_model_type(mycatalog_dir) == (
        catalog.Catalog, 'catalog')
    assert ModelUtils.get_relative_model_type(
        mycatalog_dir / 'catalog.json') == (catalog.Catalog, 'catalog')
    assert ModelUtils.get_relative_model_type(
        catalog_dir / 'back-matter.json') == (common.BackMatter,
                                              'catalog.back-matter')
    assert ModelUtils.get_relative_model_type(
        catalog_dir / 'metadata.yaml') == (common.Metadata, 'catalog.metadata')
    assert ModelUtils.get_relative_model_type(metadata_dir) == (
        common.Metadata, 'catalog.metadata')
    assert ModelUtils.get_relative_model_type(roles_dir) == (
        List[common.Role], 'catalog.metadata.roles')
    (type_, element) = ModelUtils.get_relative_model_type(roles_dir)
    assert cutils.get_origin(type_) == list
    assert element == 'catalog.metadata.roles'
    assert ModelUtils.get_relative_model_type(
        roles_dir / '00000__role.json') == (common.Role,
                                            'catalog.metadata.roles.role')
    model_type, full_alias = ModelUtils.get_relative_model_type(rps_dir)
    assert model_type == List[common.ResponsibleParty]
    assert full_alias == 'catalog.metadata.responsible-parties'
    assert ModelUtils.get_relative_model_type(
        rps_dir / 'creator__responsible-party.json') == (
            common.ResponsibleParty,
            'catalog.metadata.responsible-parties.responsible-party')
    (type_, element) = ModelUtils.get_relative_model_type(props_dir)
    assert cutils.get_origin(type_) == list
    assert cutils.get_inner_type(type_) == common.Property
    assert element == 'catalog.metadata.props'
    (expected_type, expected_json_path) = ModelUtils.get_relative_model_type(
        props_dir / f'00000{const.IDX_SEP}property.json')
    assert expected_type == common.Property
    assert expected_json_path == 'catalog.metadata.props.property'
    assert cutils.get_origin(type_) == list
    assert ModelUtils.get_relative_model_type(
        groups_dir /
        f'00000{const.IDX_SEP}group.json') == (catalog.Group,
                                               'catalog.groups.group')
    assert ModelUtils.get_relative_model_type(group_dir) == (
        catalog.Group, 'catalog.groups.group')
    assert ModelUtils.get_relative_model_type(
        controls_dir / f'00000{const.IDX_SEP}control.json') == (
            catalog.Control, 'catalog.groups.group.controls.control')