Skip to content

trestle.common.type_utils

trestle.common.type_utils ¤

Utilities for dealing with models.

Attributes¤

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

Functions:¤

get_inner_type(collection_field_type) ¤

Get the inner model in a generic collection model such as a List or a Dict.

For a dict the return type is of the value and not the key.

Parameters:

Name Type Description Default
collection_field_type Union[Type[List[Any]], Type[Dict[str, Any]]]

Provided type annotation from a pydantic object

required

Returns:

Type Description
Type[Any]

The desired type.

Source code in trestle/common/type_utils.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def get_inner_type(collection_field_type: Union[Type[List[Any]], Type[Dict[str, Any]]]) -> Type[Any]:
    """Get the inner model in a generic collection model such as a List or a Dict.

    For a dict the return type is of the value and not the key.

    Args:
        collection_field_type: Provided type annotation from a pydantic object

    Returns:
        The desired type.
    """
    try:
        origin_type = get_origin(collection_field_type)
        if is_union_type(origin_type):
            union_args = [arg for arg in typing_extensions.get_args(collection_field_type) if arg is not type(None)]
            if len(union_args) == 1:
                return get_inner_type(union_args[0])

        # Pydantic RootModel special cases must only unwrap collection roots.
        _, root_type, singular_type = _get_model_field_info(collection_field_type)
        if root_type in ('List', 'Dict') and singular_type is not None:
            return get_inner_type(singular_type)

        # Get type arguments - try both typing_extensions and typing.get_args
        # In Python 3.9+, list[...] creates types.GenericAlias which needs typing.get_args
        args = typing_extensions.get_args(collection_field_type)
        if not args:
            # Try with standard typing.get_args for types.GenericAlias
            args = get_args(collection_field_type)

        # Handle bare list or dict types without type arguments (e.g., list instead of List[str])
        # But only if they come from type annotations, not runtime instances
        if not args:
            # Check if this is actually a type annotation (has __origin__ or is a typing construct)
            # vs a runtime instance type (which would just be 'list' or 'dict')
            if origin_type is list:
                return Any
            if origin_type is dict:
                return Any
            # If no origin_type and no args, this is likely a runtime instance type, not a type annotation
            raise err.TrestleError('Model type is not a Dict or List type annotation')

        return args[-1]
    except Exception as e:
        logger.debug(e)
        raise err.TrestleError('Model type is not a Dict or List') from e

get_origin(field_type) ¤

Generalized and robust get_origin function.

This function is derived from work by pydantic, however, avoids complications from various python versions.

Source code in trestle/common/type_utils.py
46
47
48
49
50
51
52
53
def get_origin(field_type: Type[Any]) -> Optional[Type[Any]]:
    """Generalized and robust get_origin function.

    This function is derived from work by pydantic, however, avoids complications
    from various python versions.
    """
    # This executes a fallback that allows a list to be generated from a constrained list.
    return typing_extensions.get_origin(field_type) or getattr(field_type, '__origin__', None)

is_collection_field_type(field_type) ¤

Check whether a type hint is a collection type as used by OSCAL.

Specifically this is whether the type is a list or not.

Parameters:

Name Type Description Default
field_type Type[Any]

A type or a type alias of a field typically as served via pydantic introspection

required

Returns:

Type Description
bool

True if it is a collection type list.

Source code in trestle/common/type_utils.py
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
def is_collection_field_type(field_type: Type[Any]) -> bool:
    """Check whether a type hint is a collection type as used by OSCAL.

    Specifically this is whether the type is a list or not.

    Args:
        field_type: A type or a type alias of a field typically as served via pydantic introspection

    Returns:
        True if it is a collection type list.
    """
    # first check if it is a pydantic root object (RootModel in v2)
    _, root_type, _ = _get_model_field_info(field_type)
    if root_type == 'List':
        return True

    origin_type = get_origin(field_type)
    if origin_type == list:
        return True

    # Optional[list[T]] / Union[list[T], None] in Pydantic v2 annotations
    if is_union_type(origin_type):
        union_args = [arg for arg in typing_extensions.get_args(field_type) if arg is not type(None)]
        return len(union_args) == 1 and is_collection_field_type(union_args[0])

    return False

is_union_type(origin) ¤

Return True if origin is any form of Union (typing.Union or Python 3.10+ X | Y).

Replaces the fragile string comparison str(origin) == "<class 'types.UnionType'>" with identity checks that are not tied to CPython string internals.

Background
  • get_origin(Union[A, B])typing.Union (the special form)
  • get_origin(A | B)types.UnionType (the class, Python 3.10+)
Source code in trestle/common/type_utils.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def is_union_type(origin: Any) -> bool:
    """Return True if *origin* is any form of Union (typing.Union or Python 3.10+ X | Y).

    Replaces the fragile string comparison ``str(origin) == "<class 'types.UnionType'>"``
    with identity checks that are not tied to CPython string internals.

    Background:
        - ``get_origin(Union[A, B])``  → ``typing.Union``  (the special form)
        - ``get_origin(A | B)``        → ``types.UnionType`` (the class, Python 3.10+)
    """
    if origin is Union:
        return True
    # types.UnionType is available on Python 3.10+; guard with hasattr for 3.9 compatibility.
    return origin is getattr(types, 'UnionType', None)

handler: python