lodum API documentation

# lodum

View Source

     1# SPDX-FileCopyrightText: 2025-present Michael R. Bernstein <zopemaven@gmail.com>
     2#
     3# SPDX-License-Identifier: Apache-2.0
     4__version__ = "0.2.0"
     5
     6from .core import lodum
     7from .field import field
     8from .internal import generate_schema as schema
     9from . import json, yaml, pickle, toml, msgpack, cbor, bson
    10from typing import Any, Type, TypeVar
    11
    12T = TypeVar("T")
    13
    14
    15def asdict(obj: Any) -> Any:
    16    """
    17    Recursively converts a lodum-enabled object into plain Python primitives (dict, list, etc.).
    18    This handles renaming, skipping fields, and converting enums/datetimes to values.
    19    """
    20    from .internal import dump
    21    from .core import BaseDumper
    22
    23    return dump(obj, BaseDumper())
    24
    25
    26def fromdict(cls: Type[T], data: Any) -> T:
    27    """
    28    Hydrates a lodum-enabled class from a dictionary or other plain Python primitives.
    29    This performs full type validation and nested object instantiation.
    30    """
    31    from .internal import load
    32    from .core import BaseLoader
    33
    34    return load(cls, BaseLoader(data))
    35
    36
    37# Register extensions if available
    38try:
    39    from .extensions import numpy as ext_numpy
    40
    41    ext_numpy.register()
    42except ImportError:
    43    pass
    44
    45try:
    46    from .extensions import pandas as ext_pandas
    47
    48    ext_pandas.register()
    49except ImportError:
    50    pass
    51
    52try:
    53    from .extensions import polars as ext_polars
    54
    55    ext_polars.register()
    56except ImportError:
    57    pass
    58
    59__all__ = [
    60    "lodum",
    61    "field",
    62    "schema",
    63    "asdict",
    64    "fromdict",
    65    "json",
    66    "yaml",
    67    "pickle",
    68    "toml",
    69    "msgpack",
    70    "cbor",
    71    "bson",
    72]

def lodum( cls: Optional[~T] = None, tag: Optional[str] = None, tag_value: Optional[str] = None) -> Any: View Source

     84def lodum(
     85    cls: Optional[T] = None,
     86    tag: Optional[str] = None,
     87    tag_value: Optional[str] = None,
     88) -> Any:
     89    """
     90    A class decorator that marks a class as lodum-enabled and processes field metadata.
     91
     92    Args:
     93        cls: The class to decorate.
     94        tag: An optional field name to use as a tag for identifying the class in a Union.
     95        tag_value: An optional value for the tag field. Defaults to the class name.
     96    """
     97
     98    def decorator(c: T) -> T:
     99        setattr(c, "_lodum_enabled", True)
    100        setattr(c, "_lodum_tag", tag)
    101        setattr(c, "_lodum_tag_value", tag_value or c.__name__)
    102
    103        original_init = c.__init__
    104        init_sig = inspect.signature(original_init)
    105        fields: Dict[str, Field] = {}
    106
    107        for param in init_sig.parameters.values():
    108            if param.name == "self":
    109                continue
    110
    111            is_field_spec = isinstance(param.default, Field)
    112
    113            if is_field_spec:
    114                field_info = param.default
    115            else:
    116                # Create a default Field for params without one, preserving its default value
    117                default = (
    118                    param.default if param.default is not param.empty else _MISSING
    119                )
    120                field_info = Field(default=default)
    121
    122            field_info.name = param.name
    123            field_info.type = param.annotation
    124            fields[param.name] = field_info
    125
    126        setattr(c, "_lodum_fields", fields)
    127
    128        @functools.wraps(original_init)
    129        def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
    130            bound_args = init_sig.bind(self, *args, **kwargs)
    131            bound_args.apply_defaults()
    132
    133            resolved_args = {}
    134            for name, value in bound_args.arguments.items():
    135                if name == "self":
    136                    continue
    137
    138                if isinstance(value, Field):
    139                    if value.has_default:
    140                        resolved_args[name] = value.get_default()
    141                else:
    142                    resolved_args[name] = value
    143
    144            original_init(self, **resolved_args)
    145
    146        c.__init__ = new_init  # type: ignore[method-assign]
    147        register_type(c)
    148        return c
    149
    150    if cls is None:
    151        return decorator
    152    return decorator(cls)

A class decorator that marks a class as lodum-enabled and processes field metadata.

Args: cls: The class to decorate. tag: An optional field name to use as a tag for identifying the class in a Union. tag_value: An optional value for the tag field. Defaults to the class name.

def field( *, rename: Optional[str] = None, skip_serializing: bool = False, default: Any = <lodum.field._MISSING_TYPE object>, default_factory: Optional[Callable[[], Any]] = None, serializer: Optional[Callable[[Any], Any]] = None, deserializer: Optional[Callable[[Any], Any]] = None, validate: Union[Callable[[Any], NoneType], List[Callable[[Any], NoneType]], NoneType] = None) -> Any: View Source

    108def field(
    109    *,
    110    rename: Optional[str] = None,
    111    skip_serializing: bool = False,
    112    default: Any = _MISSING,
    113    default_factory: Optional[Callable[[], Any]] = None,
    114    serializer: Optional[Callable[[Any], Any]] = None,
    115    deserializer: Optional[Callable[[Any], Any]] = None,
    116    validate: Optional[
    117        Union[Callable[[Any], None], List[Callable[[Any], None]]]
    118    ] = None,
    119) -> Any:
    120    """
    121    Provides metadata to the `@lodum` decorator for a single field.
    122
    123    Args:
    124        rename: The name to use for the field in the output.
    125        skip_serializing: If `True`, the field will not be included in the
    126            output.
    127        default: A default value to use for the field during decoding
    128            if it is missing from the input data.
    129        default_factory: A zero-argument function that will be called to
    130            create a default value for a missing field.
    131        serializer: A function to call to encode the field's value.
    132        deserializer: A function to call to decode the field's value.
    133        validate: A callable or list of callables to validate the field's value during decoding.
    134    """
    135    return Field(
    136        rename=rename,
    137        skip_serializing=skip_serializing,
    138        default=default,
    139        default_factory=default_factory,
    140        serializer=serializer,
    141        deserializer=deserializer,
    142        validate=validate,
    143    )

Provides metadata to the `@lodum` decorator for a single field.

Args: rename: The name to use for the field in the output. skip_serializing: If `True`, the field will not be included in the output. default: A default value to use for the field during decoding if it is missing from the input data. default_factory: A zero-argument function that will be called to create a default value for a missing field. serializer: A function to call to encode the field's value. deserializer: A function to call to decode the field's value. validate: A callable or list of callables to validate the field's value during decoding.

def schema( t: Type[Any], depth: int = 0, visited: Optional[set] = None) -> Dict[str, Any]: View Source

    27def generate_schema(
    28    t: Type[Any], depth: int = 0, visited: Optional[set] = None
    29) -> Dict[str, Any]:
    30    """Generates a JSON Schema for a given type."""
    31    if depth > DEFAULT_MAX_DEPTH:
    32        raise ValueError(
    33            f"Max recursion depth ({DEFAULT_MAX_DEPTH}) exceeded during schema generation"
    34        )
    35
    36    if visited is None:
    37        visited = set()
    38
    39    ctx = get_context()
    40
    41    # Direct registry lookup
    42    if t in ctx.registry._handlers:
    43        return ctx.registry._handlers[t].schema_fn(t, depth, visited)
    44
    45    origin = get_origin(t) or t
    46
    47    # Generic lookup (exact match)
    48    if origin in ctx.registry._handlers:
    49        return ctx.registry._handlers[origin].schema_fn(t, depth, visited)
    50
    51    # Inheritance lookup
    52    for super_t, h_obj in ctx.registry._handlers.items():
    53        try:
    54            if inspect.isclass(origin) and issubclass(origin, super_t):
    55                return h_obj.schema_fn(t, depth, visited)
    56        except TypeError:
    57            continue
    58
    59    if inspect.isclass(t) and getattr(t, "_lodum_enabled", False):
    60        if t in visited:
    61            # Recursive reference
    62            return {"$ref": f"#/definitions/{_sanitize_name(t.__name__)}"}
    63
    64        visited.add(t)
    65        fields: Dict[str, Field] = getattr(t, "_lodum_fields", {})
    66        properties = {}
    67        required = []
    68        for field_name, field_info in fields.items():
    69            key = field_info.rename if field_info.rename else field_info.name
    70            properties[key] = generate_schema(field_info.type, depth + 1, visited)
    71            if not field_info.has_default:
    72                required.append(key)
    73
    74        schema = {"type": "object", "properties": properties}
    75
    76        tag_name = getattr(t, "_lodum_tag", None)
    77        if tag_name:
    78            tag_value = getattr(t, "_lodum_tag_value", t.__name__)
    79            properties[tag_name] = {"const": tag_value}
    80            if tag_name not in required:
    81                required.append(tag_name)
    82
    83        if required:
    84            schema["required"] = required
    85
    86        visited.remove(t)
    87        return schema
    88
    89    return {}

Generates a JSON Schema for a given type.

def asdict(obj: Any) -> Any: View Source

    16def asdict(obj: Any) -> Any:
    17    """
    18    Recursively converts a lodum-enabled object into plain Python primitives (dict, list, etc.).
    19    This handles renaming, skipping fields, and converting enums/datetimes to values.
    20    """
    21    from .internal import dump
    22    from .core import BaseDumper
    23
    24    return dump(obj, BaseDumper())

Recursively converts a lodum-enabled object into plain Python primitives (dict, list, etc.). This handles renaming, skipping fields, and converting enums/datetimes to values.

def fromdict(cls: Type[~T], data: Any) -> ~T: View Source

    27def fromdict(cls: Type[T], data: Any) -> T:
    28    """
    29    Hydrates a lodum-enabled class from a dictionary or other plain Python primitives.
    30    This performs full type validation and nested object instantiation.
    31    """
    32    from .internal import load
    33    from .core import BaseLoader
    34
    35    return load(cls, BaseLoader(data))

Hydrates a lodum-enabled class from a dictionary or other plain Python primitives. This performs full type validation and nested object instantiation.