lodum

 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:
 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:
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]:
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:
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:
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.