Skip to content

PydanticLogicalTypeFactory: typing.Literal fields unsupported as pipeline columns (Unsupported annotation) #187

Description

@brian-arnold

orcapod: typing.Literal fields in a pydantic/dataclass model aren't supported as pipeline columns

Repo / commit: nauticalab/orcapod-python @ main (fef6add24d82f66257461f36f3c416fe89a80509) — i.e. with PR #185 / ITL-432 (the fix for #184).
Environment: Python 3.12.10 · polars 1.41.2 · pyarrow 24.0.0 · pydantic 2.13.4 · starfix 0.3.1

Summary

PR #185 made pydantic models flow through pipelines as Arrow extension-type columns — thank you, that works. But registering a model whose fields use typing.Literal[...] fails:

ValueError: Unsupported annotation: typing.Literal['a', 'b']

Literal is one of the most common pydantic config patterns (enumerated string/int choices), so this blocks real configs. Our SpikeSortingConfig has 11 Literal fields (e.g. method_chosen: Literal["dredge_ap", "iterative_template", "medicine"]), and it can't be used as a broadcast config column until Literal is handled.

Minimal reproduction

import pydantic
from typing import Literal
from orcapod.contexts import get_default_context


class LiteralModel(pydantic.BaseModel):
    model_config = pydantic.ConfigDict(extra="forbid", frozen=True)
    method: Literal["a", "b"]          # <-- the only thing that matters


# Fails. (A model with plain `method: str` registers fine; so do int/float/bool,
# list[int], list[str]. Only Literal is rejected.)
get_default_context().type_converter.register_python_class(LiteralModel)

The same failure occurs via the intended path — building a source with such a model as a column:

import orcapod as op
from orcapod.types import Schema
op.sources.DictSource(
    [{"config": LiteralModel(method="a")}],
    tag_columns=[],
    data_schema=Schema({"config": LiteralModel}),
)

Error / traceback (key frames)

File ".../orcapod/extension_types/pydantic_logical_type_factory.py", line 271, in create_for_python_type
    arrow_type = converter.register_python_class(annotation)
File ".../orcapod/semantic_types/universal_converter.py", line 266, in register_python_class
    return self._register_python_class_impl(annotation, in_progress)
File ".../orcapod/semantic_types/universal_converter.py", line 403, in _register_python_class_impl
    raise ValueError(f"Unsupported annotation: {annotation!r}")
ValueError: Unsupported annotation: typing.Literal['a', 'b']

Root cause

UniversalTypeConverter._register_python_class_impl (orcapod/semantic_types/universal_converter.py) walks a pydantic/dataclass model's field annotations to build the Arrow struct. It has branches for the scalar type_map, Optional/Union, list[T], set[T], dict[K,V], and the factory-backed (dataclass/BaseModel) cases — but no branch for typing.Literal — so a Literal[...] field falls through to the terminal raise ValueError("Unsupported annotation: ...").

Suggested fix

Add a Literal branch that maps to the Arrow type of the literal values' type. Literal values are restricted to hashable constants (str/int/bool/bytes/enum/None), so:

import typing
...
origin = get_origin(annotation)
args = get_args(annotation)
...
if origin is typing.Literal:
    value_types = {type(a) for a in args if a is not None}
    if len(value_types) == 1:
        return self.register_python_class(next(iter(value_types)))   # e.g. Literal[str,...] -> large_string
    raise ValueError(
        f"Mixed-type Literal is not supported: {annotation!r}. "
        f"All members must share one type (e.g. Literal['a','b'])."
    )

(Literal[str, ...]large_string, Literal[int, ...] → int64, etc.) On the read path the value is just the underlying scalar, so no special deserialization is needed. Optionally preserve the allowed-value set as metadata, but mapping to the value type is enough to unblock.

Context

This is a follow-on to #184 / PR #185 (ITL-432). The generic pydantic-column support works; this is specifically the Literal field-type gap, which is common enough in real pydantic configs that it's worth supporting. Until then we stay pinned to the older arnoldb/pydantic commit c23fe264 (its struct-based PydanticModelConverter handled arbitrary BaseModel subclasses generically).

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