Skip to content

Remove parse_none_according_to_model after upgrade to pydantic v2 #268

@github-actions

Description

@github-actions

## TODO: Remove parse_none_according_to_model after upgrade to pydantic v2

    def __repr_args__(self):
        return [(None, self.content)]


def is_model_instance(__obj: object) -> 'TypeIs[Model]':
    return lenient_isinstance(__obj, Model) \
        and not is_none_type(__obj)  # Consequence of _ModelMetaclass hack


@functools.cache
def is_model_subclass(__cls: TypeForm) -> 'TypeIs[type[Model]]':
    return lenient_issubclass(__cls, Model) \
        and not is_none_type(__cls)  # Consequence of _ModelMetaclass hack


def is_non_omnipy_pydantic_model(obj: object):
    mro = type(obj).__mro__
    return mro[0] != pyd.BaseModel \
        and (pyd.BaseModel in mro or pyd.GenericModel in mro) \
        and Model not in mro \
        and Dataset not in mro


## TODO: Remove parse_none_according_to_model after upgrade to pydantic v2

_RootT = TypeVar('_RootT')


# Partial workaround of https://github.com/pydantic/pydantic/issues/3836 and similar bugs,
# together with hacks setting allow_none=True (_ModelMetaclass and _recursively_set_allow_none).
# See series of relevant tests in test_model.py starting with  test_list_of_none_variants().
def parse_none_according_to_model(value, root_model):  # IsModel
    outer_type = root_model.outer_type(with_args=True)
    plain_outer_type = root_model.outer_type(with_args=False)
    outer_args = get_args(outer_type)

    if is_model_subclass(outer_type):
        return _parse_none_in_model(outer_type, value)

    if root_model.is_nested_type():
        inner_val_type = root_model.inner_type(with_args=True)

        # Mutable sequences or variable tuples
        if _outer_type_and_value_are_of_types(plain_outer_type, value, (MutableSequence, tuple)):
            return _parse_none_in_mutable_sequence_or_tuple(plain_outer_type, inner_val_type, value)

        # Mappings
        if _outer_type_and_value_are_of_types(plain_outer_type, value, Mapping) and outer_args:
            return _parse_none_in_mapping(plain_outer_type, outer_args, inner_val_type, value)

        if lenient_isinstance(outer_type, TypeVar):
            return _parse_none_in_typevar(inner_val_type)

        if value is None:
            raise OmnipyNoneIsNotAllowedError()

    else:
        union_variants = _split_outer_type_to_union_variants(outer_args)
        flattened_union_variants = _flatten_two_level_tuple(union_variants)

        if any(is_model_subclass(tp_) or _supports_none(tp_) for tp_ in flattened_union_variants):

            # Fixed tuples
            if _outer_type_and_value_are_of_types(plain_outer_type, value, tuple) and outer_args:
                return _parse_none_in_fixed_tuple(plain_outer_type, union_variants, value)

            # Unions
            if is_union(plain_outer_type):
                return _parse_none_in_union(flattened_union_variants, value)

    return value


def _parse_none_in_model(outer_type, value):
    # Not exactly sure which is the best solution. Both seem to work. The latter option should be
    # more general, but potentially slower
    #
    # return outer_type(value) if value is None else value

    return outer_type(value) if type(value) is not outer_type else value


def _split_outer_type_to_union_variants(outer_type_args):
    return tuple(split_to_union_variants(_) for _ in outer_type_args)


def _flatten_two_level_tuple(two_level_tuple):
    return tuple(el for first_level_tuple in two_level_tuple for el in first_level_tuple)


def _supports_none(type_: TypeForm) -> bool:
    return is_none_type(type_) or is_optional(type_) or type_ in (object, Any)


def _outer_type_and_value_are_of_types(plain_outer_type, value, *types):
    return any(
        lenient_issubclass(plain_outer_type, type_) and lenient_isinstance(value, type_)
        for type_ in types)


def _parse_none_in_mutable_sequence_or_tuple(plain_outer_type, inner_val_type, value):
    inner_val_union_types = split_to_union_variants(inner_val_type)

    if any(is_model_subclass(_) or _supports_none(_) for _ in inner_val_union_types):
        return plain_outer_type(
            _parse_none_in_types(inner_val_union_types) if val is None else val for val in value)
    return value


def _parse_none_in_mapping(plain_outer_type, outer_type_args, inner_val_type, value):
    inner_val_union_types = split_to_union_variants(inner_val_type)

    inner_key_type = outer_type_args[0] if lenient_issubclass(
        plain_outer_type, Mapping) and outer_type_args else Undefined
    inner_key_union_types = split_to_union_variants(inner_key_type)

    if any(
            is_model_subclass(_) or _supports_none(_)
            for _ in chain(inner_key_union_types, inner_val_union_types)):
        return plain_outer_type({
            _parse_none_in_types(inner_key_union_types) if key is None else key:
                _parse_none_in_types(inner_val_union_types) if val is None else val
            for key, val in value.items()
        })
    return value


def _parse_none_in_typevar(inner_val_type):
    inner_val_union_types = split_to_union_variants(inner_val_type)

    return _parse_none_in_types(inner_val_union_types)


def _parse_none_in_fixed_tuple(plain_outer_type, tuple_of_union_variant_types, value):
    return plain_outer_type(
        _parse_none_in_types(tuple_of_union_variant_types[i]) if val is None else val
        for i, val in enumerate(value))


def _parse_none_in_union(flattened_union_variant_types, value):
    if value is None:
        return _parse_none_in_types(flattened_union_variant_types)
    else:
        return value


def _parse_none_in_types(inner_union_types: tuple[TypeForm]) -> object:
    for type_ in inner_union_types:
        if is_model_subclass(type_):
            model = type_
            return model(parse_none_according_to_model(None, model))
        elif _supports_none(type_):
            return None
    raise OmnipyNoneIsNotAllowedError()


ClassOrTupleT = TypeVar('ClassOrTupleT')


def obj_or_model_content_isinstance(
    __obj: object,
    __class_or_tuple: type[ClassOrTupleT] | tuple[type[ClassOrTupleT], ...],
) -> TypeIs[ClassOrTupleT]:
    return isinstance(__obj.content if is_model_instance(__obj) else __obj, __class_or_tuple)


def is_pure_pydantic_model(obj: object):
    return type(obj).__bases__ == (pyd.BaseModel,)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions