Skip to content

Implement a strategy to resolve natively Sequence[X] | Y #743

@jhominal

Description

@jhominal

Hello,

I wonder whether you would be interested in providing in cattrs a strategy to allow the structuring of Sequence[X] | Y, under the conditions that:

  • both X and Y can be structured;
  • Y is not a list, tuple, MutableSequence or Sequence;

I have already written the implementation for my work (my main worry is that my colleagues may not be able to maintain that hook factory long-term if I am gone):

def configure_union_with_single_sequence_dispatch(converter: cattrs.BaseConverter):
    def is_union_containing_single_sequence(target_type: Any) -> bool:
        # If target_type is a new-style alias type, we need to resolve on the __value__ field
        if isinstance(target_type, TypeAliasType):
            target_type = target_type.__value__

        if get_origin(target_type) not in (UnionType, Union):
            return False

        type_args = set(target_type.__args__)
        if len(type_args) < 2 or (len(type_args) == 2 and type(None) in type_args):
            return False

        # Design choice: support the case where tuple[X, Y], list[X], MutableSequence[X] or Sequence[X] appears in the Union
        # We could have an issue in cases where one or more elements are behind new-style type aliases
        sequence_types = (tuple, list, MutableSequence, Sequence)
        sequence_type_args = [t for t in type_args if t in sequence_types or get_origin(t) in sequence_types]
        return len(sequence_type_args) == 1

    def make_structure_union_containing_single_sequence(target_type: Any, /) -> Callable[[Any, Any], Any]:
        # If target_type is a new-style alias type, we need to resolve on the __value__ field
        if isinstance(target_type, TypeAliasType):
            target_type = target_type.__value__

        type_args = set(target_type.__args__)
        sequence_types = (tuple, list, MutableSequence, Sequence)
        sequence_type_arg = next(t for t in type_args if t in sequence_types or get_origin(t) in sequence_types)

        other_type_args = [t for t in type_args if t != sequence_type_arg]
        spillover_type: Any = Union[tuple(other_type_args)] if len(other_type_args) > 1 else other_type_args[0]

        def structure_union_containing_single_sequence(val: Any, _: Any) -> Any:
            if isinstance(val, (tuple, list)):
                return converter.structure(val, sequence_type_arg)
            return converter.structure(val, spillover_type)

        return structure_union_containing_single_sequence

    converter.register_structure_hook_factory(is_union_containing_single_sequence, make_structure_union_containing_single_sequence)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions