Skip to content

describe

trestle.core.commands.describe ¤

Trestle Describe Command.

logger ¤

Classes¤

DescribeCmd (CommandPlusDocs) ¤

Describe contents of a model file including optional element path.

Source code in trestle/core/commands/describe.py
class DescribeCmd(CommandPlusDocs):
    """Describe contents of a model file including optional element path."""

    # The only output is via log lines.  No other results or side-effects.

    name = 'describe'

    def _init_arguments(self) -> None:
        logger.debug('Init arguments')
        self.add_argument('-f', '--file', help='OSCAL file to import.', type=str, required=True)

        self.add_argument(
            '-e', '--element', help='Optional name of element in file to describe.', type=str, required=False
        )

    def _run(self, args: argparse.Namespace) -> int:
        try:
            logger.debug('Entering trestle describe.')

            log.set_log_level_from_args(args)

            if args.file:
                model_file = pathlib.Path(args.file)

                element = '' if not args.element else args.element.strip("'")
                results = self.describe(model_file.resolve(), element, args.trestle_root)

                return CmdReturnCodes.SUCCESS.value if len(results) > 0 else CmdReturnCodes.COMMAND_ERROR.value
            else:
                raise TrestleIncorrectArgsError('No file specified for command describe.')

        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(e, logger, 'Error while describing contents of a model')

    @classmethod
    def _clean_type_string(cls, text: str) -> str:
        text = text.replace("<class '", '').replace("'>", '')
        text = text.replace('trestle.oscal.', '')
        text = text.replace('pydantic.main.', 'stripped.')
        return text

    @classmethod
    def _description_text(cls, sub_model: Optional[Union[OscalBaseModel, List[OscalBaseModel]]]) -> str:
        clip_string = 100
        if sub_model is None:
            return 'None'
        if type(sub_model) is list:
            n_items = len(sub_model)
            type_text = 'Unknown' if not n_items else f'{cls._clean_type_string(str(type(sub_model[0])))}'
            text = f'list of {n_items} items of type {type_text}'
            return text
        if type(sub_model) is str:
            return sub_model if len(sub_model
                                    ) < clip_string else sub_model[:clip_string] + '[truncated]'  # type: ignore
        if hasattr(sub_model, 'type_'):
            return cls._clean_type_string(str(sub_model.type_))
        return cls._clean_type_string(str(type(sub_model)))

    @classmethod
    def describe(cls, file_path: pathlib.Path, element_path_str: str, trestle_root: pathlib.Path) -> List[str]:
        """Describe the contents of the file.

        Args:
            file_path: pathlib.Path Path for model file to describe.
            element_path_str: Element path of element in model to describe.  Can be ''.

        Returns:
            The list of lines of text in the description, or an empty list on failure
        """
        # figure out the model type so we can read it
        try:
            model_type, _ = ModelUtils.get_stripped_model_type(file_path, trestle_root)
            model: OscalBaseModel = model_type.oscal_read(file_path)
        except TrestleError as e:
            logger.warning(f'Error loading model {file_path} to describe: {e}')
            return []

        sub_model = model

        # if an element path was provided, follow the path chain to the desired sub_model
        if element_path_str:
            if '*' in element_path_str or ',' in element_path_str:
                logger.warning('Wildcards and commas are not allowed in the element path for describe.')
                return []

            if '.' not in element_path_str:
                logger.warning('The element path for describe must either be omitted or contain at least 2 parts.')
                return []

            element_paths = utils.parse_element_arg(model, element_path_str)

            sub_model_element = Element(model)

            for element_path in element_paths:
                sub_model = sub_model_element.get_at(element_path, False)
                sub_model_element = Element(sub_model)

        # now that we have the desired sub_model we can describe it

        text_out: List[str] = []

        # create top level text depending on whether an element path was used
        element_text = '' if not element_path_str else f' at element path {element_path_str}'

        if type(sub_model) is list:
            text = f'Model file {file_path}{element_text} is a {cls._description_text(sub_model)}'
            text_out.append(text)
            logger.info(text)
        else:
            text = f'Model file {file_path}{element_text} is of type '
            text += f'{cls._clean_type_string(str(type(sub_model)))} and contains:'
            text_out.append(text)
            logger.info(text)
            for key in sub_model.__fields__.keys():
                value = getattr(sub_model, key, None)
                text = f'    {key}: {cls._description_text(value)}'
                text_out.append(text)
                logger.info(text)

        return text_out
name ¤
Methods¤
describe(file_path, element_path_str, trestle_root) classmethod ¤

Describe the contents of the file.

Parameters:

Name Type Description Default
file_path Path

pathlib.Path Path for model file to describe.

required
element_path_str str

Element path of element in model to describe. Can be ''.

required

Returns:

Type Description
List[str]

The list of lines of text in the description, or an empty list on failure

Source code in trestle/core/commands/describe.py
@classmethod
def describe(cls, file_path: pathlib.Path, element_path_str: str, trestle_root: pathlib.Path) -> List[str]:
    """Describe the contents of the file.

    Args:
        file_path: pathlib.Path Path for model file to describe.
        element_path_str: Element path of element in model to describe.  Can be ''.

    Returns:
        The list of lines of text in the description, or an empty list on failure
    """
    # figure out the model type so we can read it
    try:
        model_type, _ = ModelUtils.get_stripped_model_type(file_path, trestle_root)
        model: OscalBaseModel = model_type.oscal_read(file_path)
    except TrestleError as e:
        logger.warning(f'Error loading model {file_path} to describe: {e}')
        return []

    sub_model = model

    # if an element path was provided, follow the path chain to the desired sub_model
    if element_path_str:
        if '*' in element_path_str or ',' in element_path_str:
            logger.warning('Wildcards and commas are not allowed in the element path for describe.')
            return []

        if '.' not in element_path_str:
            logger.warning('The element path for describe must either be omitted or contain at least 2 parts.')
            return []

        element_paths = utils.parse_element_arg(model, element_path_str)

        sub_model_element = Element(model)

        for element_path in element_paths:
            sub_model = sub_model_element.get_at(element_path, False)
            sub_model_element = Element(sub_model)

    # now that we have the desired sub_model we can describe it

    text_out: List[str] = []

    # create top level text depending on whether an element path was used
    element_text = '' if not element_path_str else f' at element path {element_path_str}'

    if type(sub_model) is list:
        text = f'Model file {file_path}{element_text} is a {cls._description_text(sub_model)}'
        text_out.append(text)
        logger.info(text)
    else:
        text = f'Model file {file_path}{element_text} is of type '
        text += f'{cls._clean_type_string(str(type(sub_model)))} and contains:'
        text_out.append(text)
        logger.info(text)
        for key in sub_model.__fields__.keys():
            value = getattr(sub_model, key, None)
            text = f'    {key}: {cls._description_text(value)}'
            text_out.append(text)
            logger.info(text)

    return text_out

handler: python