Edit on GitHub

serde.core

pyserde core module.

   1"""
   2pyserde core module.
   3"""
   4
   5from __future__ import annotations
   6import dataclasses
   7import enum
   8import functools
   9import logging
  10import re
  11import casefy
  12from dataclasses import dataclass
  13
  14from beartype.door import is_bearable
  15from collections import deque, Counter
  16from collections.abc import Mapping, Sequence, MutableSequence, Set, Callable, Hashable
  17from typing import (
  18    overload,
  19    TypeVar,
  20    Generic,
  21    Any,
  22    Protocol,
  23    get_type_hints,
  24    cast,
  25)
  26
  27from .compat import (
  28    T,
  29    SerdeError,
  30    dataclass_fields,
  31    get_origin,
  32    is_bare_counter,
  33    is_bare_deque,
  34    is_bare_dict,
  35    is_bare_list,
  36    is_bare_set,
  37    is_bare_tuple,
  38    is_class_var,
  39    is_counter,
  40    is_deque,
  41    is_dict,
  42    is_flatten_dict,
  43    is_generic,
  44    is_list,
  45    is_literal,
  46    is_new_type_primitive,
  47    is_any,
  48    is_opt,
  49    is_opt_dataclass,
  50    is_set,
  51    is_tuple,
  52    is_union,
  53    is_variable_tuple,
  54    type_args,
  55    typename,
  56    _WithTagging,
  57)
  58
  59__all__ = [
  60    "Scope",
  61    "gen",
  62    "add_func",
  63    "Func",
  64    "Field",
  65    "fields",
  66    "FlattenOpts",
  67    "conv",
  68    "union_func_name",
  69]
  70
  71logger = logging.getLogger("serde")
  72
  73
  74# name of the serde context key
  75SERDE_SCOPE = "__serde__"
  76
  77# main function keys
  78FROM_ITER = "from_iter"
  79FROM_DICT = "from_dict"
  80TO_ITER = "to_iter"
  81TO_DICT = "to_dict"
  82TYPE_CHECK = "typecheck"
  83
  84# prefixes used to distinguish the direction of a union function
  85UNION_SE_PREFIX = "union_se"
  86UNION_DE_PREFIX = "union_de"
  87
  88LITERAL_DE_PREFIX = "literal_de"
  89
  90SETTINGS = {"debug": False}
  91
  92
  93@dataclass(frozen=True)
  94class UnionCacheKey:
  95    union: Hashable
  96    tagging: Tagging
  97
  98
  99def init(debug: bool = False) -> None:
 100    SETTINGS["debug"] = debug
 101
 102
 103@dataclass
 104class Cache:
 105    """
 106    Cache the generated code for non-dataclass classes.
 107
 108    for example, a type not bound in a dataclass is passed in from_json
 109
 110    ```
 111    from_json(Union[Foo, Bar], ...)
 112    ```
 113
 114    It creates the following wrapper dataclass on the fly,
 115
 116    ```
 117    @serde
 118    @dataclass
 119    class Union_Foo_bar:
 120        v: Union[Foo, Bar]
 121    ```
 122
 123    Then store this class in this cache. Whenever the same type is passed,
 124    the class is retrieved from this cache. So the overhead of the codegen
 125    should be only once.
 126    """
 127
 128    classes: dict[Hashable, type[Any]] = dataclasses.field(default_factory=dict)
 129
 130    def _get_class(self, cls: type[Any]) -> type[Any]:
 131        """
 132        Get a wrapper class from the the cache. If not found, it will generate
 133        the class and store it in the cache.
 134        """
 135        wrapper = self.classes.get(cls)  # type: ignore[call-overload] # mypy doesn't recognize type[Any] as Hashable
 136        return wrapper or self._generate_class(cls)
 137
 138    def _generate_class(self, cls: type[Any]) -> type[Any]:
 139        """
 140        Generate a wrapper dataclass then make the it (de)serializable using
 141        @serde decorator.
 142        """
 143        from . import serde
 144
 145        class_name = f"Wrapper{typename(cls)}"
 146        logger.debug(f"Generating a wrapper class code for {class_name}")
 147
 148        wrapper = dataclasses.make_dataclass(class_name, [("v", cls)])
 149
 150        serde(wrapper)
 151        self.classes[cls] = wrapper  # type: ignore[index] # mypy doesn't recognize type[Any] as Hashable
 152
 153        logger.debug(f"(de)serializing code for {class_name} was generated")
 154        return wrapper
 155
 156    def serialize(self, cls: type[Any], obj: Any, **kwargs: Any) -> Any:
 157        """
 158        Serialize the specified type of object into dict or tuple.
 159        """
 160        wrapper = self._get_class(cls)
 161        scope: Scope = getattr(wrapper, SERDE_SCOPE)
 162        data = scope.funcs[TO_DICT](wrapper(obj), **kwargs)
 163
 164        logging.debug(f"Intermediate value: {data}")
 165
 166        return data["v"]
 167
 168    def deserialize(self, cls: type[T], obj: Any) -> T:
 169        """
 170        Deserialize from dict or tuple into the specified type.
 171        """
 172        wrapper = self._get_class(cls)
 173        scope: Scope = getattr(wrapper, SERDE_SCOPE)
 174        return scope.funcs[FROM_DICT](data={"v": obj}).v  # type: ignore
 175
 176    def _get_union_class(self, cls: type[Any]) -> type[Any] | None:
 177        """
 178        Get a wrapper class from the the cache. If not found, it will generate
 179        the class and store it in the cache.
 180        """
 181        union_cls, tagging = _extract_from_with_tagging(cls)
 182        cache_key = UnionCacheKey(union=union_cls, tagging=tagging)
 183        wrapper = self.classes.get(cache_key)
 184        return wrapper or self._generate_union_class(cls)
 185
 186    def _generate_union_class(self, cls: type[Any]) -> type[Any]:
 187        """
 188        Generate a wrapper dataclass then make the it (de)serializable using
 189        @serde decorator.
 190        """
 191        import serde
 192
 193        union_cls, tagging = _extract_from_with_tagging(cls)
 194        cache_key = UnionCacheKey(union=union_cls, tagging=tagging)
 195        class_name = union_func_name(
 196            f"{tagging.produce_unique_class_name()}Union", list(type_args(union_cls))
 197        )
 198        wrapper = dataclasses.make_dataclass(class_name, [("v", union_cls)])
 199        serde.serde(wrapper, tagging=tagging)
 200        self.classes[cache_key] = wrapper
 201        return wrapper
 202
 203    def serialize_union(self, cls: type[Any], obj: Any) -> Any:
 204        """
 205        Serialize the specified Union into dict or tuple.
 206        """
 207        union_cls, _ = _extract_from_with_tagging(cls)
 208        wrapper = self._get_union_class(cls)
 209        scope: Scope = getattr(wrapper, SERDE_SCOPE)
 210        func_name = union_func_name(UNION_SE_PREFIX, list(type_args(union_cls)))
 211        return scope.funcs[func_name](obj, False, False)
 212
 213    def deserialize_union(
 214        self,
 215        cls: type[T],
 216        data: Any,
 217        deserialize_numbers: Callable[[str | int], float] | None = None,
 218    ) -> T:
 219        """
 220        Deserialize from dict or tuple into the specified Union.
 221        """
 222        union_cls, _ = _extract_from_with_tagging(cls)
 223        wrapper = self._get_union_class(cls)
 224        scope: Scope = getattr(wrapper, SERDE_SCOPE)
 225        func_name = union_func_name(UNION_DE_PREFIX, list(type_args(union_cls)))
 226        return cast(
 227            T,
 228            scope.funcs[func_name](
 229                cls=union_cls, data=data, deserialize_numbers=deserialize_numbers
 230            ),
 231        )
 232
 233
 234def _extract_from_with_tagging(maybe_with_tagging: Any) -> tuple[Any, Tagging]:
 235    if isinstance(maybe_with_tagging, _WithTagging):
 236        return maybe_with_tagging.inner, maybe_with_tagging.tagging
 237    else:
 238        return maybe_with_tagging, ExternalTagging
 239
 240
 241CACHE = Cache()
 242""" Global cache variable for non-dataclass classes """
 243
 244
 245@dataclass
 246class Scope:
 247    """
 248    Container to store types and functions used in code generation context.
 249    """
 250
 251    cls: type[Any]
 252    """ The exact class this scope is for
 253    (needed to distinguish scopes between inherited classes) """
 254
 255    funcs: dict[str, Callable[..., Any]] = dataclasses.field(default_factory=dict)
 256    """ Generated serialize and deserialize functions """
 257
 258    defaults: dict[str, Callable[..., Any] | Any] = dataclasses.field(default_factory=dict)
 259    """ Default values of the dataclass fields (factories & normal values) """
 260
 261    code: dict[str, str] = dataclasses.field(default_factory=dict)
 262    """ Generated source code (only filled when debug is True) """
 263
 264    union_se_args: dict[str, list[type[Any]]] = dataclasses.field(default_factory=dict)
 265    """ The union serializing functions need references to their types """
 266
 267    reuse_instances_default: bool = True
 268    """ Default values for to_dict & from_dict arguments """
 269
 270    convert_sets_default: bool = False
 271
 272    transparent: bool = False
 273    """If True, serialize/deserialize as the single inner field (serde-rs `transparent`)."""
 274
 275    def __repr__(self) -> str:
 276        res: list[str] = []
 277
 278        res.append("==================================================")
 279        res.append(self._justify(self.cls.__name__))
 280        res.append("==================================================")
 281        res.append("")
 282
 283        if self.code:
 284            res.append("--------------------------------------------------")
 285            res.append(self._justify("Functions generated by pyserde"))
 286            res.append("--------------------------------------------------")
 287            res.extend(list(self.code.values()))
 288            res.append("")
 289
 290        if self.funcs:
 291            res.append("--------------------------------------------------")
 292            res.append(self._justify("Function references in scope"))
 293            res.append("--------------------------------------------------")
 294            for k, v in self.funcs.items():
 295                res.append(f"{k}: {v}")
 296            res.append("")
 297
 298        if self.defaults:
 299            res.append("--------------------------------------------------")
 300            res.append(self._justify("Default values for the dataclass fields"))
 301            res.append("--------------------------------------------------")
 302            for k, v in self.defaults.items():
 303                res.append(f"{k}: {v}")
 304            res.append("")
 305
 306        if self.union_se_args:
 307            res.append("--------------------------------------------------")
 308            res.append(self._justify("Type list by used for union serialize functions"))
 309            res.append("--------------------------------------------------")
 310            for k, lst in self.union_se_args.items():
 311                res.append(f"{k}: {list(lst)}")
 312            res.append("")
 313
 314        return "\n".join(res)
 315
 316    def _justify(self, s: str, length: int = 50) -> str:
 317        white_spaces = int((50 - len(s)) / 2)
 318        return " " * (white_spaces if white_spaces > 0 else 0) + s
 319
 320
 321def raise_unsupported_type(obj: Any) -> None:
 322    # needed because we can not render a raise statement everywhere, e.g. as argument
 323    raise SerdeError(f"Unsupported type: {typename(type(obj))}")
 324
 325
 326def gen(
 327    code: str, globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None
 328) -> str:
 329    """
 330    A wrapper of builtin `exec` function.
 331    """
 332    if SETTINGS["debug"]:
 333        # black formatting is only important when debugging
 334        try:
 335            from black import FileMode, format_str
 336
 337            code = format_str(code, mode=FileMode(line_length=100))
 338        except Exception:
 339            pass
 340    exec(code, globals, locals)
 341    return code
 342
 343
 344def add_func(serde_scope: Scope, func_name: str, func_code: str, globals: dict[str, Any]) -> None:
 345    """
 346    Generate a function and add it to a Scope's `funcs` dictionary.
 347
 348    * `serde_scope`: the Scope instance to modify
 349    * `func_name`: the name of the function
 350    * `func_code`: the source code of the function
 351    * `globals`: global variables that should be accessible to the generated function
 352    """
 353
 354    code = gen(func_code, globals)
 355    serde_scope.funcs[func_name] = globals[func_name]
 356
 357    if SETTINGS["debug"]:
 358        serde_scope.code[func_name] = code
 359
 360
 361def is_instance(obj: Any, typ: Any) -> bool:
 362    """
 363    pyserde's own `isinstance` helper. It accepts subscripted generics e.g. `list[int]` and
 364    deeply check object against declared type.
 365    """
 366    if dataclasses.is_dataclass(typ):
 367        if not isinstance(typ, type):
 368            raise SerdeError("expect dataclass class but dataclass instance received")
 369        return isinstance(obj, typ)
 370    elif is_opt(typ):
 371        return is_opt_instance(obj, typ)
 372    elif is_union(typ):
 373        return is_union_instance(obj, typ)
 374    elif is_list(typ):
 375        return is_list_instance(obj, typ)
 376    elif is_set(typ):
 377        return is_set_instance(obj, typ)
 378    elif is_tuple(typ):
 379        return is_tuple_instance(obj, typ)
 380    elif is_counter(typ):
 381        # Counter must be checked before dict since Counter is a subclass of dict
 382        return is_counter_instance(obj, typ)
 383    elif is_dict(typ):
 384        return is_dict_instance(obj, typ)
 385    elif is_deque(typ):
 386        return is_deque_instance(obj, typ)
 387    elif is_generic(typ):
 388        return is_generic_instance(obj, typ)
 389    elif is_literal(typ):
 390        return True
 391    elif is_new_type_primitive(typ):
 392        inner = getattr(typ, "__supertype__", None)
 393        if type(inner) is type:
 394            return isinstance(obj, inner)
 395        else:
 396            return False
 397    elif is_any(typ):
 398        return True
 399    elif typ is Ellipsis:
 400        return True
 401    else:
 402        return is_bearable(obj, typ)
 403
 404
 405def is_opt_instance(obj: Any, typ: type[Any]) -> bool:
 406    if obj is None:
 407        return True
 408    opt_arg = type_args(typ)[0]
 409    return is_instance(obj, opt_arg)
 410
 411
 412def is_union_instance(obj: Any, typ: type[Any]) -> bool:
 413    for arg in type_args(typ):
 414        if is_instance(obj, arg):
 415            return True
 416    return False
 417
 418
 419def is_list_instance(obj: Any, typ: type[Any]) -> bool:
 420    origin = get_origin(typ) or typ
 421    if origin is list:
 422        if not isinstance(obj, list):
 423            return False
 424    elif origin is MutableSequence:
 425        if isinstance(obj, (str, bytes, bytearray)):
 426            return False
 427        if not isinstance(obj, MutableSequence):
 428            return False
 429    else:
 430        if isinstance(obj, (str, bytes, bytearray)):
 431            return False
 432        if not isinstance(obj, Sequence):
 433            return False
 434    if len(obj) == 0 or is_bare_list(typ):
 435        return True
 436    list_arg = type_args(typ)[0]
 437    # for speed reasons we just check the type of the 1st element
 438    return is_instance(obj[0], list_arg)
 439
 440
 441def is_set_instance(obj: Any, typ: type[Any]) -> bool:
 442    if not isinstance(obj, Set):
 443        return False
 444    if len(obj) == 0 or is_bare_set(typ):
 445        return True
 446    set_arg = type_args(typ)[0]
 447    # for speed reasons we just check the type of the 1st element
 448    return is_instance(next(iter(obj)), set_arg)
 449
 450
 451def is_tuple_instance(obj: Any, typ: type[Any]) -> bool:
 452    args = type_args(typ)
 453
 454    if not isinstance(obj, tuple):
 455        return False
 456
 457    # empty tuple
 458    if len(args) == 0 and len(obj) == 0:
 459        return True
 460
 461    # In the form of tuple[T, ...]
 462    elif is_variable_tuple(typ):
 463        # Get the first type arg. Since tuple[T, ...] is homogeneous tuple,
 464        # all the elements should be of this type.
 465        arg = type_args(typ)[0]
 466        for v in obj:
 467            if not is_instance(v, arg):
 468                return False
 469        return True
 470
 471    # bare tuple "tuple" is equivalent to tuple[Any, ...]
 472    if is_bare_tuple(typ) and isinstance(obj, tuple):
 473        return True
 474
 475    # All the other tuples e.g. tuple[int, str]
 476    if len(obj) == len(args):
 477        for element, arg in zip(obj, args, strict=False):
 478            if not is_instance(element, arg):
 479                return False
 480    else:
 481        return False
 482
 483    return True
 484
 485
 486def is_dict_instance(obj: Any, typ: type[Any]) -> bool:
 487    if not isinstance(obj, Mapping):
 488        return False
 489    if len(obj) == 0 or is_bare_dict(typ):
 490        return True
 491    ktyp = type_args(typ)[0]
 492    vtyp = type_args(typ)[1]
 493    for k, v in obj.items():
 494        # for speed reasons we just check the type of the 1st element
 495        return is_instance(k, ktyp) and is_instance(v, vtyp)
 496    return False
 497
 498
 499def is_deque_instance(obj: Any, typ: type[Any]) -> bool:
 500    if not isinstance(obj, deque):
 501        return False
 502    if len(obj) == 0 or is_bare_deque(typ):
 503        return True
 504    deque_arg = type_args(typ)[0]
 505    # for speed reasons we just check the type of the 1st element
 506    return is_instance(obj[0], deque_arg)
 507
 508
 509def is_counter_instance(obj: Any, typ: type[Any]) -> bool:
 510    if not isinstance(obj, Counter):
 511        return False
 512    if len(obj) == 0 or is_bare_counter(typ):
 513        return True
 514    counter_arg = type_args(typ)[0]
 515    # for speed reasons we just check the type of the 1st key
 516    return is_instance(next(iter(obj.keys())), counter_arg)
 517
 518
 519def is_generic_instance(obj: Any, typ: type[Any]) -> bool:
 520    return is_instance(obj, get_origin(typ))
 521
 522
 523@dataclass
 524class Func:
 525    """
 526    Function wrapper that provides `mangled` optional field.
 527
 528    pyserde copies every function reference into global scope
 529    for code generation. Mangling function names is needed in
 530    order to avoid name conflict in the global scope when
 531    multiple fields receives `skip_if` attribute.
 532    """
 533
 534    inner: Callable[..., Any]
 535    """ Function to wrap in """
 536
 537    mangeld: str = ""
 538    """ Mangled function name """
 539
 540    def __call__(self, v: Any) -> None:
 541        return self.inner(v)  # type: ignore
 542
 543    @property
 544    def name(self) -> str:
 545        """
 546        Mangled function name
 547        """
 548        return self.mangeld
 549
 550
 551def skip_if_false(v: Any) -> Any:
 552    return not bool(v)
 553
 554
 555def skip_if_default(v: Any, default: Any | None = None) -> Any:
 556    return v == default  # Why return type is deduced to be Any?
 557
 558
 559@dataclass
 560class FlattenOpts:
 561    """
 562    Flatten options. Currently not used.
 563    """
 564
 565
 566def field(
 567    *args: Any,
 568    rename: str | None = None,
 569    alias: list[str] | None = None,
 570    skip: bool | None = None,
 571    skip_if: Callable[[Any], Any] | None = None,
 572    skip_if_false: bool | None = None,
 573    skip_if_default: bool | None = None,
 574    serializer: Callable[..., Any] | None = None,
 575    deserializer: Callable[..., Any] | None = None,
 576    flatten: FlattenOpts | bool | None = None,
 577    metadata: dict[str, Any] | None = None,
 578    **kwargs: Any,
 579) -> Any:
 580    """
 581    Declare a field with parameters.
 582    """
 583    if not metadata:
 584        metadata = {}
 585
 586    if rename is not None:
 587        metadata["serde_rename"] = rename
 588    if alias is not None:
 589        metadata["serde_alias"] = alias
 590    if skip is not None:
 591        metadata["serde_skip"] = skip
 592    if skip_if is not None:
 593        metadata["serde_skip_if"] = skip_if
 594    if skip_if_false is not None:
 595        metadata["serde_skip_if_false"] = skip_if_false
 596    if skip_if_default is not None:
 597        metadata["serde_skip_if_default"] = skip_if_default
 598    if serializer:
 599        metadata["serde_serializer"] = serializer
 600    if deserializer:
 601        metadata["serde_deserializer"] = deserializer
 602    if flatten is True:
 603        metadata["serde_flatten"] = FlattenOpts()
 604    elif flatten:
 605        metadata["serde_flatten"] = flatten
 606
 607    return dataclasses.field(*args, metadata=metadata, **kwargs)
 608
 609
 610@dataclass
 611class Field(Generic[T]):
 612    """
 613    Field class is similar to `dataclasses.Field`. It provides pyserde specific options.
 614
 615    `type`, `name`, `default` and `default_factory` are the same members as `dataclasses.Field`.
 616    """
 617
 618    type: type[T]
 619    """ Type of Field """
 620    name: str | None
 621    """ Name of Field """
 622    default: Any = field(default_factory=dataclasses._MISSING_TYPE)
 623    """ Default value of Field """
 624    default_factory: Any = field(default_factory=dataclasses._MISSING_TYPE)
 625    """ Default factory method of Field """
 626    init: bool = field(default_factory=dataclasses._MISSING_TYPE)
 627    repr: Any = field(default_factory=dataclasses._MISSING_TYPE)
 628    hash: Any = field(default_factory=dataclasses._MISSING_TYPE)
 629    compare: Any = field(default_factory=dataclasses._MISSING_TYPE)
 630    metadata: Mapping[str, Any] = field(default_factory=dict)
 631    kw_only: bool = False
 632    case: str | None = None
 633    alias: list[str] = field(default_factory=list)
 634    rename: str | None = None
 635    skip: bool | None = None
 636    skip_if: Func | None = None
 637    skip_if_false: bool | None = None
 638    skip_if_default: bool | None = None
 639    serializer: Func | None = None  # Custom field serializer.
 640    deserializer: Func | None = None  # Custom field deserializer.
 641    flatten: FlattenOpts | None = None
 642    parent: Any | None = None
 643    type_args: list[str] | None = None
 644
 645    @classmethod
 646    def from_dataclass(cls, f: dataclasses.Field[T], parent: Any | None = None) -> Field[T]:
 647        """
 648        Create `Field` object from `dataclasses.Field`.
 649        """
 650        skip_if_false_func: Func | None = None
 651        if f.metadata.get("serde_skip_if_false"):
 652            skip_if_false_func = Func(skip_if_false, cls.mangle(f, "skip_if_false"))
 653
 654        skip_if_default_func: Func | None = None
 655        if f.metadata.get("serde_skip_if_default"):
 656            skip_if_def = functools.partial(skip_if_default, default=f.default)
 657            skip_if_default_func = Func(skip_if_def, cls.mangle(f, "skip_if_default"))
 658
 659        skip_if: Func | None = None
 660        if f.metadata.get("serde_skip_if"):
 661            func = f.metadata.get("serde_skip_if")
 662            if callable(func):
 663                skip_if = Func(func, cls.mangle(f, "skip_if"))
 664
 665        serializer: Func | None = None
 666        func = f.metadata.get("serde_serializer")
 667        if func:
 668            serializer = Func(func, cls.mangle(f, "serializer"))
 669
 670        deserializer: Func | None = None
 671        func = f.metadata.get("serde_deserializer")
 672        if func:
 673            deserializer = Func(func, cls.mangle(f, "deserializer"))
 674
 675        flatten = f.metadata.get("serde_flatten")
 676        if flatten is True:
 677            flatten = FlattenOpts()
 678        if flatten:
 679            is_valid_flatten_type = (
 680                dataclasses.is_dataclass(f.type)
 681                or is_opt_dataclass(f.type)
 682                or is_flatten_dict(f.type)
 683            )
 684            if not is_valid_flatten_type:
 685                raise SerdeError(
 686                    f"pyserde does not support flatten attribute for {typename(f.type)}. "
 687                    "Supported types: dataclass, Optional[dataclass], dict[str, Any], dict"
 688                )
 689
 690        kw_only = bool(f.kw_only)
 691
 692        return cls(
 693            f.type,  # type: ignore
 694            f.name,
 695            default=f.default,
 696            default_factory=f.default_factory,
 697            init=f.init,
 698            repr=f.repr,
 699            hash=f.hash,
 700            compare=f.compare,
 701            metadata=f.metadata,
 702            rename=f.metadata.get("serde_rename"),
 703            alias=f.metadata.get("serde_alias", []),
 704            skip=f.metadata.get("serde_skip"),
 705            skip_if=skip_if or skip_if_false_func or skip_if_default_func,
 706            serializer=serializer,
 707            deserializer=deserializer,
 708            flatten=flatten,
 709            parent=parent,
 710            kw_only=kw_only,
 711        )
 712
 713    def to_dataclass(self) -> dataclasses.Field[T]:
 714        base_kwargs = {
 715            "init": self.init,
 716            "repr": self.repr,
 717            "hash": self.hash,
 718            "compare": self.compare,
 719            "metadata": self.metadata,
 720            "kw_only": self.kw_only,
 721            "default": self.default,
 722            "default_factory": self.default_factory,
 723        }
 724
 725        f = cast(dataclasses.Field[T], dataclasses.field(**base_kwargs))
 726        assert self.name
 727        f.name = self.name
 728        f.type = self.type
 729        return f
 730
 731    def is_self_referencing(self) -> bool:
 732        if self.type is None:
 733            return False
 734        if self.parent is None:
 735            return False
 736        return self.type == self.parent  # type: ignore
 737
 738    @staticmethod
 739    def mangle(field: dataclasses.Field[Any], name: str) -> str:
 740        """
 741        Get mangled name based on field name.
 742        """
 743        return f"{field.name}_{name}"
 744
 745    def conv_name(self, case: str | None = None) -> str:
 746        """
 747        Get an actual field name which `rename` and `rename_all` conversions
 748        are made. Use `name` property to get a field name before conversion.
 749        """
 750        return conv(self, case or self.case)
 751
 752    def supports_default(self) -> bool:
 753        return not getattr(self, "iterbased", False) and (
 754            has_default(self) or has_default_factory(self)
 755        )
 756
 757
 758F = TypeVar("F", bound=Field[Any])
 759
 760
 761def fields(field_cls: type[F], cls: type[Any], serialize_class_var: bool = False) -> list[F]:
 762    """
 763    Iterate fields of the dataclass and returns `serde.core.Field`.
 764    """
 765    fields = [field_cls.from_dataclass(f, parent=cls) for f in dataclass_fields(cls)]
 766
 767    if serialize_class_var:
 768        for name, typ in get_type_hints(cls).items():
 769            if is_class_var(typ):
 770                fields.append(field_cls(typ, name, default=getattr(cls, name)))
 771
 772    return fields  # type: ignore
 773
 774
 775def get_transparent_field(cls: type[Any]) -> Field[Any]:
 776    """
 777    Return the single "transparent" field for `cls`.
 778
 779    A transparent class must have exactly one init=True field that is not skipped.
 780    Any other dataclass fields must be both init=False and skipped.
 781    """
 782    all_fields: list[Field[Any]] = fields(Field, cls)
 783
 784    candidates = [f for f in all_fields if f.init and not f.skip]
 785    if len(candidates) != 1:
 786        raise SerdeError(
 787            f"{typename(cls)} with `transparent=True` must have exactly one init=True, "
 788            "non-skipped field"
 789        )
 790
 791    chosen = candidates[0]
 792    for f in all_fields:
 793        if f.name == chosen.name:
 794            continue
 795        if f.init:
 796            raise SerdeError(
 797                f"{typename(cls)} with `transparent=True` can not have additional init=True fields "
 798                f"(found {f.name!r})"
 799            )
 800        if not f.skip:
 801            raise SerdeError(
 802                f"{typename(cls)} with `transparent=True` requires non-transparent fields to be "
 803                f"skipped (set `serde.field(skip=True)`) (found {f.name!r})"
 804            )
 805
 806    return chosen
 807
 808
 809def conv(f: Field[Any], case: str | None = None) -> str:
 810    """
 811    Convert field name.
 812    """
 813    name = f.name
 814    if case:
 815        casef = getattr(casefy, case, None)
 816        if not casef:
 817            raise SerdeError(
 818                f"Unkown case type: {f.case}. Pass the name of case supported by 'casefy' package."
 819            )
 820        name = casef(name)
 821    if f.rename:
 822        name = f.rename
 823    if name is None:
 824        raise SerdeError("Field name is None.")
 825    return name
 826
 827
 828def union_func_name(prefix: str, union_args: Sequence[Any]) -> str:
 829    """
 830    Generate a function name that contains all union types
 831
 832    * `prefix` prefix to distinguish between serializing and deserializing
 833    * `union_args`: type arguments of a Union
 834
 835    >>> from ipaddress import IPv4Address
 836    >>> union_func_name("union_se", [int, list[str], IPv4Address])
 837    'union_se_int_list_str__IPv4Address'
 838    """
 839    return re.sub(r"[^A-Za-z0-9]", "_", f"{prefix}_{'_'.join([typename(e) for e in union_args])}")
 840
 841
 842def literal_func_name(literal_args: Sequence[Any]) -> str:
 843    """
 844    Generate a function name with all literals and corresponding types specified with Literal[...]
 845
 846
 847    * `literal_args`: arguments of a Literal
 848
 849    >>> literal_func_name(["r", "w", "a", "x", "r+", "w+", "a+", "x+"])
 850    'literal_de_r_str_w_str_a_str_x_str_r__str_w__str_a__str_x__str'
 851    """
 852    return re.sub(
 853        r"[^A-Za-z0-9]",
 854        "_",
 855        f"{LITERAL_DE_PREFIX}_{'_'.join(f'{a}_{typename(type(a))}' for a in literal_args)}",
 856    )
 857
 858
 859@dataclass(frozen=True)
 860class Tagging:
 861    """
 862    Controls how union is (de)serialized. This is the same concept as in
 863    https://serde.rs/enum-representations.html
 864    """
 865
 866    class Kind(enum.Enum):
 867        External = enum.auto()
 868        Internal = enum.auto()
 869        Adjacent = enum.auto()
 870        Untagged = enum.auto()
 871
 872    tag: str | None = None
 873    content: str | None = None
 874    kind: Kind = Kind.External
 875
 876    def is_external(self) -> bool:
 877        return self.kind == self.Kind.External
 878
 879    def is_internal(self) -> bool:
 880        return self.kind == self.Kind.Internal
 881
 882    def is_adjacent(self) -> bool:
 883        return self.kind == self.Kind.Adjacent
 884
 885    def is_untagged(self) -> bool:
 886        return self.kind == self.Kind.Untagged
 887
 888    @classmethod
 889    def is_taggable(cls, typ: type[Any]) -> bool:
 890        return dataclasses.is_dataclass(typ)
 891
 892    def check(self) -> None:
 893        if self.is_internal() and self.tag is None:
 894            raise SerdeError('"tag" must be specified in InternalTagging')
 895        if self.is_adjacent() and (self.tag is None or self.content is None):
 896            raise SerdeError('"tag" and "content" must be specified in AdjacentTagging')
 897
 898    def produce_unique_class_name(self) -> str:
 899        """
 900        Produce a unique class name for this tagging. The name is used for generated
 901        wrapper dataclass and stored in `Cache`.
 902        """
 903        if self.is_internal():
 904            tag = casefy.pascalcase(self.tag)  # type: ignore
 905            if not tag:
 906                raise SerdeError('"tag" must be specified in InternalTagging')
 907            return f"Internal{tag}"
 908        elif self.is_adjacent():
 909            tag = casefy.pascalcase(self.tag)  # type: ignore
 910            content = casefy.pascalcase(self.content)  # type: ignore
 911            if not tag:
 912                raise SerdeError('"tag" must be specified in AdjacentTagging')
 913            if not content:
 914                raise SerdeError('"content" must be specified in AdjacentTagging')
 915            return f"Adjacent{tag}{content}"
 916        else:
 917            return self.kind.name
 918
 919    def __call__(self, cls: T) -> _WithTagging[T]:
 920        return _WithTagging(cls, self)
 921
 922
 923@overload
 924def InternalTagging(tag: str) -> Tagging: ...
 925
 926
 927@overload
 928def InternalTagging(tag: str, cls: T) -> _WithTagging[T]: ...
 929
 930
 931def InternalTagging(tag: str, cls: T | None = None) -> Tagging | _WithTagging[T]:
 932    tagging = Tagging(tag, kind=Tagging.Kind.Internal)
 933    if cls:
 934        return tagging(cls)
 935    else:
 936        return tagging
 937
 938
 939@overload
 940def AdjacentTagging(tag: str, content: str) -> Tagging: ...
 941
 942
 943@overload
 944def AdjacentTagging(tag: str, content: str, cls: T) -> _WithTagging[T]: ...
 945
 946
 947def AdjacentTagging(tag: str, content: str, cls: T | None = None) -> Tagging | _WithTagging[T]:
 948    tagging = Tagging(tag, content, kind=Tagging.Kind.Adjacent)
 949    if cls:
 950        return tagging(cls)
 951    else:
 952        return tagging
 953
 954
 955ExternalTagging = Tagging()
 956
 957Untagged = Tagging(kind=Tagging.Kind.Untagged)
 958
 959
 960DefaultTagging = ExternalTagging
 961
 962
 963def ensure(expr: Any, description: str) -> None:
 964    if not expr:
 965        raise Exception(description)
 966
 967
 968def should_impl_dataclass(cls: type[Any]) -> bool:
 969    """
 970    Test if class doesn't have @dataclass.
 971
 972    `dataclasses.is_dataclass` returns True even Derived class doesn't actually @dataclass.
 973    >>> @dataclasses.dataclass
 974    ... class Base:
 975    ...     a: int
 976    >>> class Derived(Base):
 977    ...     b: int
 978    >>> dataclasses.is_dataclass(Derived)
 979    True
 980
 981    This function tells the class actually have @dataclass or not.
 982    >>> should_impl_dataclass(Base)
 983    False
 984    >>> should_impl_dataclass(Derived)
 985    True
 986    """
 987    if not dataclasses.is_dataclass(cls):
 988        return True
 989
 990    # Checking is_dataclass is not enough in such a case that the class is inherited
 991    # from another dataclass. To do it correctly, check if all fields in __annotations__
 992    # are present as dataclass fields.
 993    annotations = getattr(cls, "__annotations__", {})
 994    if not annotations:
 995        return False
 996
 997    field_names = [field.name for field in dataclass_fields(cls)]
 998    for field_name, annotation in annotations.items():
 999        # Omit InitVar field because it doesn't appear in dataclass fields.
1000        if is_instance(annotation, dataclasses.InitVar):
1001            continue
1002        # This field in __annotations__ is not a part of dataclass fields.
1003        # This means this class does not implement dataclass directly.
1004        if field_name not in field_names:
1005            return True
1006
1007    # If all of the fields in __annotation__ are present as dataclass fields,
1008    # the class already implemented dataclass, thus returns False.
1009    return False
1010
1011
1012@dataclass
1013class TypeCheck:
1014    """
1015    Specify type check flavors.
1016    """
1017
1018    class Kind(enum.Enum):
1019        Disabled = enum.auto()
1020        """ No check performed """
1021
1022        Coerce = enum.auto()
1023        """ Value is coerced into the declared type """
1024        Strict = enum.auto()
1025        """ Value are strictly checked against the declared type """
1026
1027    kind: Kind
1028
1029    def is_strict(self) -> bool:
1030        return self.kind == self.Kind.Strict
1031
1032    def is_coerce(self) -> bool:
1033        return self.kind == self.Kind.Coerce
1034
1035    def __call__(self, **kwargs: Any) -> TypeCheck:
1036        # TODO
1037        return self
1038
1039
1040disabled = TypeCheck(kind=TypeCheck.Kind.Disabled)
1041
1042coerce = TypeCheck(kind=TypeCheck.Kind.Coerce)
1043
1044strict = TypeCheck(kind=TypeCheck.Kind.Strict)
1045
1046
1047def coerce_object(cls: str, field: str, typ: type[Any], obj: Any) -> Any:
1048    try:
1049        return typ(obj) if is_coercible(typ, obj) else obj
1050    except Exception as e:
1051        raise SerdeError(
1052            f"failed to coerce the field {cls}.{field} value {obj} into {typename(typ)}: {e}"
1053        )
1054
1055
1056def is_coercible(typ: type[Any], obj: Any) -> bool:
1057    if obj is None:
1058        return False
1059    return True
1060
1061
1062def has_default(field: Field[Any]) -> bool:
1063    """
1064    Test if the field has default value.
1065
1066    >>> @dataclasses.dataclass
1067    ... class C:
1068    ...     a: int
1069    ...     d: int = 10
1070    >>> has_default(dataclasses.fields(C)[0])
1071    False
1072    >>> has_default(dataclasses.fields(C)[1])
1073    True
1074    """
1075    return not isinstance(field.default, dataclasses._MISSING_TYPE)
1076
1077
1078def has_default_factory(field: Field[Any]) -> bool:
1079    """
1080    Test if the field has default factory.
1081
1082    >>> @dataclasses.dataclass
1083    ... class C:
1084    ...     a: int
1085    ...     d: dict = dataclasses.field(default_factory=dict)
1086    >>> has_default_factory(dataclasses.fields(C)[0])
1087    False
1088    >>> has_default_factory(dataclasses.fields(C)[1])
1089    True
1090    """
1091    return not isinstance(field.default_factory, dataclasses._MISSING_TYPE)
1092
1093
1094class ClassSerializer(Protocol):
1095    """
1096    Interface for custom class serializer.
1097
1098    This protocol is intended to be used for custom class serializer.
1099
1100    >>> from datetime import datetime
1101    >>> from serde import serde
1102    >>> from plum import dispatch
1103    >>> class MySerializer(ClassSerializer):
1104    ...     @dispatch
1105    ...     def serialize(self, value: datetime) -> str:
1106    ...         return value.strftime("%d/%m/%y")
1107    """
1108
1109    def serialize(self, value: Any) -> Any:
1110        pass
1111
1112
1113class ClassDeserializer(Protocol):
1114    """
1115    Interface for custom class deserializer.
1116
1117    This protocol is intended to be used for custom class deserializer.
1118
1119    >>> from datetime import datetime
1120    >>> from serde import serde
1121    >>> from plum import dispatch
1122    >>> class MyDeserializer(ClassDeserializer):
1123    ...     @dispatch
1124    ...     def deserialize(self, cls: type[datetime], value: Any) -> datetime:
1125    ...         return datetime.strptime(value, "%d/%m/%y")
1126    """
1127
1128    def deserialize(self, cls: Any, value: Any) -> Any:
1129        pass
1130
1131
1132GLOBAL_CLASS_SERIALIZER: list[ClassSerializer] = []
1133
1134GLOBAL_CLASS_DESERIALIZER: list[ClassDeserializer] = []
1135
1136
1137def add_serializer(serializer: ClassSerializer) -> None:
1138    """
1139    Register custom global serializer.
1140    """
1141    GLOBAL_CLASS_SERIALIZER.append(serializer)
1142
1143
1144def add_deserializer(deserializer: ClassDeserializer) -> None:
1145    """
1146    Register custom global deserializer.
1147    """
1148    GLOBAL_CLASS_DESERIALIZER.append(deserializer)
@dataclass
class Scope:
246@dataclass
247class Scope:
248    """
249    Container to store types and functions used in code generation context.
250    """
251
252    cls: type[Any]
253    """ The exact class this scope is for
254    (needed to distinguish scopes between inherited classes) """
255
256    funcs: dict[str, Callable[..., Any]] = dataclasses.field(default_factory=dict)
257    """ Generated serialize and deserialize functions """
258
259    defaults: dict[str, Callable[..., Any] | Any] = dataclasses.field(default_factory=dict)
260    """ Default values of the dataclass fields (factories & normal values) """
261
262    code: dict[str, str] = dataclasses.field(default_factory=dict)
263    """ Generated source code (only filled when debug is True) """
264
265    union_se_args: dict[str, list[type[Any]]] = dataclasses.field(default_factory=dict)
266    """ The union serializing functions need references to their types """
267
268    reuse_instances_default: bool = True
269    """ Default values for to_dict & from_dict arguments """
270
271    convert_sets_default: bool = False
272
273    transparent: bool = False
274    """If True, serialize/deserialize as the single inner field (serde-rs `transparent`)."""
275
276    def __repr__(self) -> str:
277        res: list[str] = []
278
279        res.append("==================================================")
280        res.append(self._justify(self.cls.__name__))
281        res.append("==================================================")
282        res.append("")
283
284        if self.code:
285            res.append("--------------------------------------------------")
286            res.append(self._justify("Functions generated by pyserde"))
287            res.append("--------------------------------------------------")
288            res.extend(list(self.code.values()))
289            res.append("")
290
291        if self.funcs:
292            res.append("--------------------------------------------------")
293            res.append(self._justify("Function references in scope"))
294            res.append("--------------------------------------------------")
295            for k, v in self.funcs.items():
296                res.append(f"{k}: {v}")
297            res.append("")
298
299        if self.defaults:
300            res.append("--------------------------------------------------")
301            res.append(self._justify("Default values for the dataclass fields"))
302            res.append("--------------------------------------------------")
303            for k, v in self.defaults.items():
304                res.append(f"{k}: {v}")
305            res.append("")
306
307        if self.union_se_args:
308            res.append("--------------------------------------------------")
309            res.append(self._justify("Type list by used for union serialize functions"))
310            res.append("--------------------------------------------------")
311            for k, lst in self.union_se_args.items():
312                res.append(f"{k}: {list(lst)}")
313            res.append("")
314
315        return "\n".join(res)
316
317    def _justify(self, s: str, length: int = 50) -> str:
318        white_spaces = int((50 - len(s)) / 2)
319        return " " * (white_spaces if white_spaces > 0 else 0) + s

Container to store types and functions used in code generation context.

Scope( cls: type[typing.Any], funcs: dict[str, Callable[..., typing.Any]] = <factory>, defaults: dict[str, Callable[..., typing.Any] | typing.Any] = <factory>, code: dict[str, str] = <factory>, union_se_args: dict[str, list[type[typing.Any]]] = <factory>, reuse_instances_default: bool = True, convert_sets_default: bool = False, transparent: bool = False)
cls: type[typing.Any]

The exact class this scope is for (needed to distinguish scopes between inherited classes)

funcs: dict[str, Callable[..., typing.Any]]

Generated serialize and deserialize functions

defaults: dict[str, Callable[..., typing.Any] | typing.Any]

Default values of the dataclass fields (factories & normal values)

code: dict[str, str]

Generated source code (only filled when debug is True)

union_se_args: dict[str, list[type[typing.Any]]]

The union serializing functions need references to their types

reuse_instances_default: bool = True

Default values for to_dict & from_dict arguments

convert_sets_default: bool = False
transparent: bool = False

If True, serialize/deserialize as the single inner field (serde-rs transparent).

def gen( code: str, globals: dict[str, typing.Any] | None = None, locals: dict[str, typing.Any] | None = None) -> str:
327def gen(
328    code: str, globals: dict[str, Any] | None = None, locals: dict[str, Any] | None = None
329) -> str:
330    """
331    A wrapper of builtin `exec` function.
332    """
333    if SETTINGS["debug"]:
334        # black formatting is only important when debugging
335        try:
336            from black import FileMode, format_str
337
338            code = format_str(code, mode=FileMode(line_length=100))
339        except Exception:
340            pass
341    exec(code, globals, locals)
342    return code

A wrapper of builtin exec function.

def add_func( serde_scope: Scope, func_name: str, func_code: str, globals: dict[str, typing.Any]) -> None:
345def add_func(serde_scope: Scope, func_name: str, func_code: str, globals: dict[str, Any]) -> None:
346    """
347    Generate a function and add it to a Scope's `funcs` dictionary.
348
349    * `serde_scope`: the Scope instance to modify
350    * `func_name`: the name of the function
351    * `func_code`: the source code of the function
352    * `globals`: global variables that should be accessible to the generated function
353    """
354
355    code = gen(func_code, globals)
356    serde_scope.funcs[func_name] = globals[func_name]
357
358    if SETTINGS["debug"]:
359        serde_scope.code[func_name] = code

Generate a function and add it to a Scope's funcs dictionary.

  • serde_scope: the Scope instance to modify
  • func_name: the name of the function
  • func_code: the source code of the function
  • globals: global variables that should be accessible to the generated function
@dataclass
class Func:
524@dataclass
525class Func:
526    """
527    Function wrapper that provides `mangled` optional field.
528
529    pyserde copies every function reference into global scope
530    for code generation. Mangling function names is needed in
531    order to avoid name conflict in the global scope when
532    multiple fields receives `skip_if` attribute.
533    """
534
535    inner: Callable[..., Any]
536    """ Function to wrap in """
537
538    mangeld: str = ""
539    """ Mangled function name """
540
541    def __call__(self, v: Any) -> None:
542        return self.inner(v)  # type: ignore
543
544    @property
545    def name(self) -> str:
546        """
547        Mangled function name
548        """
549        return self.mangeld

Function wrapper that provides mangled optional field.

pyserde copies every function reference into global scope for code generation. Mangling function names is needed in order to avoid name conflict in the global scope when multiple fields receives skip_if attribute.

Func(inner: Callable[..., typing.Any], mangeld: str = '')
inner: Callable[..., typing.Any]

Function to wrap in

mangeld: str = ''

Mangled function name

name: str
544    @property
545    def name(self) -> str:
546        """
547        Mangled function name
548        """
549        return self.mangeld

Mangled function name

@dataclass
class Field(typing.Generic[~T]):
611@dataclass
612class Field(Generic[T]):
613    """
614    Field class is similar to `dataclasses.Field`. It provides pyserde specific options.
615
616    `type`, `name`, `default` and `default_factory` are the same members as `dataclasses.Field`.
617    """
618
619    type: type[T]
620    """ Type of Field """
621    name: str | None
622    """ Name of Field """
623    default: Any = field(default_factory=dataclasses._MISSING_TYPE)
624    """ Default value of Field """
625    default_factory: Any = field(default_factory=dataclasses._MISSING_TYPE)
626    """ Default factory method of Field """
627    init: bool = field(default_factory=dataclasses._MISSING_TYPE)
628    repr: Any = field(default_factory=dataclasses._MISSING_TYPE)
629    hash: Any = field(default_factory=dataclasses._MISSING_TYPE)
630    compare: Any = field(default_factory=dataclasses._MISSING_TYPE)
631    metadata: Mapping[str, Any] = field(default_factory=dict)
632    kw_only: bool = False
633    case: str | None = None
634    alias: list[str] = field(default_factory=list)
635    rename: str | None = None
636    skip: bool | None = None
637    skip_if: Func | None = None
638    skip_if_false: bool | None = None
639    skip_if_default: bool | None = None
640    serializer: Func | None = None  # Custom field serializer.
641    deserializer: Func | None = None  # Custom field deserializer.
642    flatten: FlattenOpts | None = None
643    parent: Any | None = None
644    type_args: list[str] | None = None
645
646    @classmethod
647    def from_dataclass(cls, f: dataclasses.Field[T], parent: Any | None = None) -> Field[T]:
648        """
649        Create `Field` object from `dataclasses.Field`.
650        """
651        skip_if_false_func: Func | None = None
652        if f.metadata.get("serde_skip_if_false"):
653            skip_if_false_func = Func(skip_if_false, cls.mangle(f, "skip_if_false"))
654
655        skip_if_default_func: Func | None = None
656        if f.metadata.get("serde_skip_if_default"):
657            skip_if_def = functools.partial(skip_if_default, default=f.default)
658            skip_if_default_func = Func(skip_if_def, cls.mangle(f, "skip_if_default"))
659
660        skip_if: Func | None = None
661        if f.metadata.get("serde_skip_if"):
662            func = f.metadata.get("serde_skip_if")
663            if callable(func):
664                skip_if = Func(func, cls.mangle(f, "skip_if"))
665
666        serializer: Func | None = None
667        func = f.metadata.get("serde_serializer")
668        if func:
669            serializer = Func(func, cls.mangle(f, "serializer"))
670
671        deserializer: Func | None = None
672        func = f.metadata.get("serde_deserializer")
673        if func:
674            deserializer = Func(func, cls.mangle(f, "deserializer"))
675
676        flatten = f.metadata.get("serde_flatten")
677        if flatten is True:
678            flatten = FlattenOpts()
679        if flatten:
680            is_valid_flatten_type = (
681                dataclasses.is_dataclass(f.type)
682                or is_opt_dataclass(f.type)
683                or is_flatten_dict(f.type)
684            )
685            if not is_valid_flatten_type:
686                raise SerdeError(
687                    f"pyserde does not support flatten attribute for {typename(f.type)}. "
688                    "Supported types: dataclass, Optional[dataclass], dict[str, Any], dict"
689                )
690
691        kw_only = bool(f.kw_only)
692
693        return cls(
694            f.type,  # type: ignore
695            f.name,
696            default=f.default,
697            default_factory=f.default_factory,
698            init=f.init,
699            repr=f.repr,
700            hash=f.hash,
701            compare=f.compare,
702            metadata=f.metadata,
703            rename=f.metadata.get("serde_rename"),
704            alias=f.metadata.get("serde_alias", []),
705            skip=f.metadata.get("serde_skip"),
706            skip_if=skip_if or skip_if_false_func or skip_if_default_func,
707            serializer=serializer,
708            deserializer=deserializer,
709            flatten=flatten,
710            parent=parent,
711            kw_only=kw_only,
712        )
713
714    def to_dataclass(self) -> dataclasses.Field[T]:
715        base_kwargs = {
716            "init": self.init,
717            "repr": self.repr,
718            "hash": self.hash,
719            "compare": self.compare,
720            "metadata": self.metadata,
721            "kw_only": self.kw_only,
722            "default": self.default,
723            "default_factory": self.default_factory,
724        }
725
726        f = cast(dataclasses.Field[T], dataclasses.field(**base_kwargs))
727        assert self.name
728        f.name = self.name
729        f.type = self.type
730        return f
731
732    def is_self_referencing(self) -> bool:
733        if self.type is None:
734            return False
735        if self.parent is None:
736            return False
737        return self.type == self.parent  # type: ignore
738
739    @staticmethod
740    def mangle(field: dataclasses.Field[Any], name: str) -> str:
741        """
742        Get mangled name based on field name.
743        """
744        return f"{field.name}_{name}"
745
746    def conv_name(self, case: str | None = None) -> str:
747        """
748        Get an actual field name which `rename` and `rename_all` conversions
749        are made. Use `name` property to get a field name before conversion.
750        """
751        return conv(self, case or self.case)
752
753    def supports_default(self) -> bool:
754        return not getattr(self, "iterbased", False) and (
755            has_default(self) or has_default_factory(self)
756        )

Field class is similar to dataclasses.Field. It provides pyserde specific options.

type, name, default and default_factory are the same members as dataclasses.Field.

Field( type: type[~T], name: str | None, default: Any = <factory>, default_factory: Any = <factory>, init: bool = <factory>, repr: Any = <factory>, hash: Any = <factory>, compare: Any = <factory>, metadata: Mapping[str, typing.Any] = <factory>, kw_only: bool = False, case: str | None = None, alias: list[str] = <factory>, rename: str | None = None, skip: bool | None = None, skip_if: Func | None = None, skip_if_false: bool | None = None, skip_if_default: bool | None = None, serializer: Func | None = None, deserializer: Func | None = None, flatten: FlattenOpts | None = None, parent: typing.Any | None = None, type_args: list[str] | None = None)
type: type[~T]

Type of Field

name: str | None

Name of Field

default: Any

Default value of Field

default_factory: Any

Default factory method of Field

init: bool
repr: Any
hash: Any
compare: Any
metadata: Mapping[str, typing.Any]
kw_only: bool = False
case: str | None = None
alias: list[str]
rename: str | None = None
skip: bool | None = None
skip_if: Func | None = None
skip_if_false: bool | None = None
skip_if_default: bool | None = None
serializer: Func | None = None
deserializer: Func | None = None
flatten: FlattenOpts | None = None
parent: typing.Any | None = None
type_args: list[str] | None = None
@classmethod
def from_dataclass( cls, f: dataclasses.Field[~T], parent: typing.Any | None = None) -> Field[~T]:
646    @classmethod
647    def from_dataclass(cls, f: dataclasses.Field[T], parent: Any | None = None) -> Field[T]:
648        """
649        Create `Field` object from `dataclasses.Field`.
650        """
651        skip_if_false_func: Func | None = None
652        if f.metadata.get("serde_skip_if_false"):
653            skip_if_false_func = Func(skip_if_false, cls.mangle(f, "skip_if_false"))
654
655        skip_if_default_func: Func | None = None
656        if f.metadata.get("serde_skip_if_default"):
657            skip_if_def = functools.partial(skip_if_default, default=f.default)
658            skip_if_default_func = Func(skip_if_def, cls.mangle(f, "skip_if_default"))
659
660        skip_if: Func | None = None
661        if f.metadata.get("serde_skip_if"):
662            func = f.metadata.get("serde_skip_if")
663            if callable(func):
664                skip_if = Func(func, cls.mangle(f, "skip_if"))
665
666        serializer: Func | None = None
667        func = f.metadata.get("serde_serializer")
668        if func:
669            serializer = Func(func, cls.mangle(f, "serializer"))
670
671        deserializer: Func | None = None
672        func = f.metadata.get("serde_deserializer")
673        if func:
674            deserializer = Func(func, cls.mangle(f, "deserializer"))
675
676        flatten = f.metadata.get("serde_flatten")
677        if flatten is True:
678            flatten = FlattenOpts()
679        if flatten:
680            is_valid_flatten_type = (
681                dataclasses.is_dataclass(f.type)
682                or is_opt_dataclass(f.type)
683                or is_flatten_dict(f.type)
684            )
685            if not is_valid_flatten_type:
686                raise SerdeError(
687                    f"pyserde does not support flatten attribute for {typename(f.type)}. "
688                    "Supported types: dataclass, Optional[dataclass], dict[str, Any], dict"
689                )
690
691        kw_only = bool(f.kw_only)
692
693        return cls(
694            f.type,  # type: ignore
695            f.name,
696            default=f.default,
697            default_factory=f.default_factory,
698            init=f.init,
699            repr=f.repr,
700            hash=f.hash,
701            compare=f.compare,
702            metadata=f.metadata,
703            rename=f.metadata.get("serde_rename"),
704            alias=f.metadata.get("serde_alias", []),
705            skip=f.metadata.get("serde_skip"),
706            skip_if=skip_if or skip_if_false_func or skip_if_default_func,
707            serializer=serializer,
708            deserializer=deserializer,
709            flatten=flatten,
710            parent=parent,
711            kw_only=kw_only,
712        )

Create Field object from dataclasses.Field.

def to_dataclass(self) -> dataclasses.Field[~T]:
714    def to_dataclass(self) -> dataclasses.Field[T]:
715        base_kwargs = {
716            "init": self.init,
717            "repr": self.repr,
718            "hash": self.hash,
719            "compare": self.compare,
720            "metadata": self.metadata,
721            "kw_only": self.kw_only,
722            "default": self.default,
723            "default_factory": self.default_factory,
724        }
725
726        f = cast(dataclasses.Field[T], dataclasses.field(**base_kwargs))
727        assert self.name
728        f.name = self.name
729        f.type = self.type
730        return f
def is_self_referencing(self) -> bool:
732    def is_self_referencing(self) -> bool:
733        if self.type is None:
734            return False
735        if self.parent is None:
736            return False
737        return self.type == self.parent  # type: ignore
@staticmethod
def mangle(field: dataclasses.Field[typing.Any], name: str) -> str:
739    @staticmethod
740    def mangle(field: dataclasses.Field[Any], name: str) -> str:
741        """
742        Get mangled name based on field name.
743        """
744        return f"{field.name}_{name}"

Get mangled name based on field name.

def conv_name(self, case: str | None = None) -> str:
746    def conv_name(self, case: str | None = None) -> str:
747        """
748        Get an actual field name which `rename` and `rename_all` conversions
749        are made. Use `name` property to get a field name before conversion.
750        """
751        return conv(self, case or self.case)

Get an actual field name which rename and rename_all conversions are made. Use name property to get a field name before conversion.

def supports_default(self) -> bool:
753    def supports_default(self) -> bool:
754        return not getattr(self, "iterbased", False) and (
755            has_default(self) or has_default_factory(self)
756        )
def fields( field_cls: type[~F], cls: type[typing.Any], serialize_class_var: bool = False) -> list[~F]:
762def fields(field_cls: type[F], cls: type[Any], serialize_class_var: bool = False) -> list[F]:
763    """
764    Iterate fields of the dataclass and returns `serde.core.Field`.
765    """
766    fields = [field_cls.from_dataclass(f, parent=cls) for f in dataclass_fields(cls)]
767
768    if serialize_class_var:
769        for name, typ in get_type_hints(cls).items():
770            if is_class_var(typ):
771                fields.append(field_cls(typ, name, default=getattr(cls, name)))
772
773    return fields  # type: ignore

Iterate fields of the dataclass and returns serde.core.Field.

@dataclass
class FlattenOpts:
560@dataclass
561class FlattenOpts:
562    """
563    Flatten options. Currently not used.
564    """

Flatten options. Currently not used.

def conv(f: Field[typing.Any], case: str | None = None) -> str:
810def conv(f: Field[Any], case: str | None = None) -> str:
811    """
812    Convert field name.
813    """
814    name = f.name
815    if case:
816        casef = getattr(casefy, case, None)
817        if not casef:
818            raise SerdeError(
819                f"Unkown case type: {f.case}. Pass the name of case supported by 'casefy' package."
820            )
821        name = casef(name)
822    if f.rename:
823        name = f.rename
824    if name is None:
825        raise SerdeError("Field name is None.")
826    return name

Convert field name.

def union_func_name(prefix: str, union_args: Sequence[typing.Any]) -> str:
829def union_func_name(prefix: str, union_args: Sequence[Any]) -> str:
830    """
831    Generate a function name that contains all union types
832
833    * `prefix` prefix to distinguish between serializing and deserializing
834    * `union_args`: type arguments of a Union
835
836    >>> from ipaddress import IPv4Address
837    >>> union_func_name("union_se", [int, list[str], IPv4Address])
838    'union_se_int_list_str__IPv4Address'
839    """
840    return re.sub(r"[^A-Za-z0-9]", "_", f"{prefix}_{'_'.join([typename(e) for e in union_args])}")

Generate a function name that contains all union types

  • prefix prefix to distinguish between serializing and deserializing
  • union_args: type arguments of a Union
>>> from ipaddress import IPv4Address
>>> union_func_name("union_se", [int, list[str], IPv4Address])
'union_se_int_list_str__IPv4Address'