Skip to content

describe

trestle.core.commands.describe ¤

Trestle Describe Command.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

DescribeCmd ¤

Bases: CommandPlusDocs

Describe contents of a model file including optional element path.

Source code in trestle/core/commands/describe.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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
Attributes¤
name = 'describe' class-attribute instance-attribute ¤
Functions¤
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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@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

Functions¤

handler: python