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
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
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
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)
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
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')