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