pydantic-marshmallow 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_marshmallow/bridge.py +204 -76
- pydantic_marshmallow/field_conversion.py +28 -7
- pydantic_marshmallow/type_mapping.py +37 -28
- {pydantic_marshmallow-1.0.1.dist-info → pydantic_marshmallow-1.1.0.dist-info}/METADATA +26 -14
- pydantic_marshmallow-1.1.0.dist-info/RECORD +12 -0
- pydantic_marshmallow-1.0.1.dist-info/RECORD +0 -12
- {pydantic_marshmallow-1.0.1.dist-info → pydantic_marshmallow-1.1.0.dist-info}/WHEEL +0 -0
- {pydantic_marshmallow-1.0.1.dist-info → pydantic_marshmallow-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {pydantic_marshmallow-1.0.1.dist-info → pydantic_marshmallow-1.1.0.dist-info}/top_level.txt +0 -0
pydantic_marshmallow/bridge.py
CHANGED
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import threading
|
|
11
11
|
from collections.abc import Callable, Sequence, Set as AbstractSet
|
|
12
12
|
from functools import lru_cache
|
|
13
|
+
from importlib.metadata import version as get_version
|
|
13
14
|
from typing import Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin
|
|
14
15
|
|
|
15
16
|
from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields as ma_fields
|
|
@@ -17,13 +18,29 @@ from marshmallow.decorators import VALIDATES, VALIDATES_SCHEMA
|
|
|
17
18
|
from marshmallow.error_store import ErrorStore
|
|
18
19
|
from marshmallow.exceptions import ValidationError as MarshmallowValidationError
|
|
19
20
|
from marshmallow.schema import SchemaMeta
|
|
20
|
-
from marshmallow.utils import missing as ma_missing
|
|
21
|
+
from marshmallow.utils import missing as ma_missing # type: ignore[attr-defined,unused-ignore]
|
|
21
22
|
from pydantic import BaseModel, ConfigDict, ValidationError as PydanticValidationError
|
|
22
23
|
|
|
23
24
|
from .errors import BridgeValidationError, convert_pydantic_errors, format_pydantic_error
|
|
24
|
-
from .field_conversion import convert_model_fields, convert_pydantic_field
|
|
25
|
+
from .field_conversion import _get_computed_fields, convert_model_fields, convert_pydantic_field
|
|
25
26
|
from .validators import cache_validators
|
|
26
27
|
|
|
28
|
+
|
|
29
|
+
# Marshmallow version detection for context parameter compatibility
|
|
30
|
+
# MA 4.x removes the `context` parameter from Schema.__init__
|
|
31
|
+
def _parse_version(version_str: str) -> tuple[int, int]:
|
|
32
|
+
"""Parse version string, handling pre-release versions like '4.0.0rc1'."""
|
|
33
|
+
import re
|
|
34
|
+
match = re.match(r'(\d+)\.(\d+)', version_str)
|
|
35
|
+
if match:
|
|
36
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
37
|
+
raise ValueError(f"Cannot parse marshmallow version: {version_str}") # pragma: no cover
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_MARSHMALLOW_VERSION = _parse_version(get_version("marshmallow"))
|
|
41
|
+
_MARSHMALLOW_4_PLUS = _MARSHMALLOW_VERSION >= (4, 0)
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
M = TypeVar("M", bound=BaseModel)
|
|
28
45
|
|
|
29
46
|
# =============================================================================
|
|
@@ -170,10 +187,11 @@ class PydanticSchemaMeta(SchemaMeta):
|
|
|
170
187
|
attrs[field_name] = convert_pydantic_field(field_name, field_info)
|
|
171
188
|
|
|
172
189
|
# Add computed fields as dump_only
|
|
173
|
-
|
|
190
|
+
computed_fields = _get_computed_fields(model_class)
|
|
191
|
+
if computed_fields:
|
|
174
192
|
from .field_conversion import convert_computed_field
|
|
175
193
|
|
|
176
|
-
for field_name, computed_info in
|
|
194
|
+
for field_name, computed_info in computed_fields.items():
|
|
177
195
|
if field_name in attrs:
|
|
178
196
|
continue
|
|
179
197
|
# If attrs has pre-filtered fields, don't add computed fields
|
|
@@ -189,7 +207,7 @@ class PydanticSchemaMeta(SchemaMeta):
|
|
|
189
207
|
return cast(PydanticSchemaMeta, super().__new__(mcs, name, bases, attrs))
|
|
190
208
|
|
|
191
209
|
|
|
192
|
-
class
|
|
210
|
+
class _PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
193
211
|
"""
|
|
194
212
|
A Marshmallow schema backed by a Pydantic model.
|
|
195
213
|
|
|
@@ -268,20 +286,27 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
268
286
|
self._dump_only_fields: set[str] = set(dump_only) if dump_only else set()
|
|
269
287
|
self._partial: bool | Sequence[str] | AbstractSet[str] | None = partial
|
|
270
288
|
self._unknown_override: str | None = unknown
|
|
271
|
-
self._context = context or {}
|
|
272
289
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
many
|
|
279
|
-
load_only
|
|
280
|
-
dump_only
|
|
281
|
-
partial
|
|
282
|
-
unknown
|
|
290
|
+
# Build kwargs for super().__init__()
|
|
291
|
+
# MA 4.x removes context parameter - only pass it for MA 3.x
|
|
292
|
+
parent_kwargs: dict[str, Any] = {
|
|
293
|
+
"only": only,
|
|
294
|
+
"exclude": exclude,
|
|
295
|
+
"many": many,
|
|
296
|
+
"load_only": load_only,
|
|
297
|
+
"dump_only": dump_only,
|
|
298
|
+
"partial": partial,
|
|
299
|
+
"unknown": unknown,
|
|
283
300
|
**kwargs,
|
|
284
|
-
|
|
301
|
+
}
|
|
302
|
+
if not _MARSHMALLOW_4_PLUS:
|
|
303
|
+
parent_kwargs["context"] = context
|
|
304
|
+
|
|
305
|
+
super().__init__(**parent_kwargs)
|
|
306
|
+
|
|
307
|
+
# For MA 4.x, store context manually if provided
|
|
308
|
+
if _MARSHMALLOW_4_PLUS and context is not None: # pragma: no cover
|
|
309
|
+
self.context = context
|
|
285
310
|
self._model_class = self._get_model_class()
|
|
286
311
|
if self._model_class:
|
|
287
312
|
self._setup_fields_from_model()
|
|
@@ -290,7 +315,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
290
315
|
for field_name, field_obj in self.fields.items():
|
|
291
316
|
self.on_bind_field(field_name, field_obj)
|
|
292
317
|
|
|
293
|
-
def on_bind_field(self, field_name: str, field_obj:
|
|
318
|
+
def on_bind_field(self, field_name: str, field_obj: Any) -> None:
|
|
294
319
|
"""
|
|
295
320
|
Hook called when a field is bound to the schema.
|
|
296
321
|
|
|
@@ -340,16 +365,6 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
340
365
|
"""
|
|
341
366
|
raise error
|
|
342
367
|
|
|
343
|
-
@property
|
|
344
|
-
def context(self) -> dict[str, Any]:
|
|
345
|
-
"""Get the validation context."""
|
|
346
|
-
return self._context
|
|
347
|
-
|
|
348
|
-
@context.setter
|
|
349
|
-
def context(self, value: dict[str, Any]) -> None:
|
|
350
|
-
"""Set the validation context."""
|
|
351
|
-
self._context = value
|
|
352
|
-
|
|
353
368
|
def _get_model_class(self) -> type[BaseModel] | None:
|
|
354
369
|
"""Get the Pydantic model class from Meta or generic parameter."""
|
|
355
370
|
# Try Meta.model first
|
|
@@ -616,7 +631,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
616
631
|
if many:
|
|
617
632
|
if not isinstance(data, list):
|
|
618
633
|
raise MarshmallowValidationError({"_schema": ["Expected a list."]})
|
|
619
|
-
|
|
634
|
+
|
|
635
|
+
# Process each item individually (validators run per-item with pass_many=False)
|
|
636
|
+
results = [
|
|
620
637
|
self._do_load(
|
|
621
638
|
item,
|
|
622
639
|
many=False,
|
|
@@ -628,18 +645,47 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
628
645
|
for item in data
|
|
629
646
|
]
|
|
630
647
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
648
|
+
# After all items processed, run @validates_schema(pass_many=True) validators
|
|
649
|
+
# These should see the FULL collection, not individual items
|
|
650
|
+
coll_error_store_cls: Callable[[], Any] = cast(Callable[[], Any], ErrorStore)
|
|
651
|
+
coll_error_store: Any = coll_error_store_cls()
|
|
652
|
+
|
|
653
|
+
# Build kwargs for collection-level validation
|
|
654
|
+
coll_pass_param = "pass_collection" if _MARSHMALLOW_4_PLUS else "pass_many"
|
|
655
|
+
coll_validator_kwargs: dict[str, Any] = {
|
|
656
|
+
"error_store": coll_error_store,
|
|
657
|
+
"data": results,
|
|
658
|
+
"original_data": data,
|
|
659
|
+
"many": True,
|
|
660
|
+
"partial": partial,
|
|
661
|
+
"field_errors": False, # Field errors already handled per-item
|
|
662
|
+
}
|
|
663
|
+
# MA 4.x requires 'unknown' parameter
|
|
664
|
+
if _MARSHMALLOW_4_PLUS: # pragma: no cover
|
|
665
|
+
coll_validator_kwargs["unknown"] = unknown_setting
|
|
666
|
+
# Only run pass_many=True validators (collection-level)
|
|
667
|
+
self._invoke_schema_validators(**{coll_pass_param: True, **coll_validator_kwargs})
|
|
668
|
+
|
|
669
|
+
if coll_error_store.errors:
|
|
670
|
+
error = MarshmallowValidationError(dict(coll_error_store.errors))
|
|
671
|
+
self.handle_error(error, data, many=True)
|
|
672
|
+
|
|
673
|
+
return results
|
|
674
|
+
|
|
675
|
+
# Step 1: Run pre_load hooks
|
|
676
|
+
# MA 4.x requires 'unknown' parameter, MA 3.x doesn't accept it
|
|
677
|
+
pre_load_kwargs: dict[str, Any] = {
|
|
678
|
+
"many": False,
|
|
679
|
+
"original_data": data,
|
|
680
|
+
"partial": partial,
|
|
681
|
+
}
|
|
682
|
+
if _MARSHMALLOW_4_PLUS: # pragma: no cover
|
|
683
|
+
pre_load_kwargs["unknown"] = unknown_setting
|
|
684
|
+
processed_data_raw = self._invoke_load_processors(
|
|
685
|
+
"pre_load",
|
|
686
|
+
data,
|
|
687
|
+
**pre_load_kwargs,
|
|
688
|
+
)
|
|
643
689
|
|
|
644
690
|
# Type narrowing: at this point (many=False path), data is always a dict
|
|
645
691
|
processed_data: dict[str, Any] = cast(dict[str, Any], processed_data_raw)
|
|
@@ -669,9 +715,25 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
669
715
|
|
|
670
716
|
# OPTIMIZATION: Determine if we need model_dump() for validators/result
|
|
671
717
|
# Skip expensive model_dump() when not needed
|
|
718
|
+
# Note: MA 3.x uses tuple keys ('validates', field) and ('validates_schema', pass_many)
|
|
719
|
+
# MA 4.x uses string keys ('validates', 'validates_schema')
|
|
720
|
+
# Check for any hook that starts with the validator type
|
|
721
|
+
def _has_hook(hook_type: str) -> bool:
|
|
722
|
+
"""Check if any hook matches the type (handles both MA 3.x and 4.x key formats)."""
|
|
723
|
+
# Check string key (MA 4.x) - safe to access defaultdict
|
|
724
|
+
if hooks.get(hook_type):
|
|
725
|
+
return True
|
|
726
|
+
# Check tuple keys (MA 3.x) - cast to Any to avoid mypy narrowing issues
|
|
727
|
+
# across different Marshmallow versions with different key types
|
|
728
|
+
hooks_any: dict[Any, Any] = cast(dict[Any, Any], hooks)
|
|
729
|
+
for key in hooks_any:
|
|
730
|
+
if isinstance(key, tuple) and len(key) >= 1 and key[0] == hook_type and hooks_any[key]:
|
|
731
|
+
return True
|
|
732
|
+
return False
|
|
733
|
+
|
|
672
734
|
has_validators = bool(
|
|
673
|
-
|
|
674
|
-
or
|
|
735
|
+
_has_hook(VALIDATES)
|
|
736
|
+
or _has_hook(VALIDATES_SCHEMA)
|
|
675
737
|
or self._field_validators_cache
|
|
676
738
|
or self._schema_validators_cache
|
|
677
739
|
)
|
|
@@ -709,12 +771,11 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
709
771
|
error_store: Any = error_store_cls()
|
|
710
772
|
|
|
711
773
|
# 4a: Run Marshmallow's native @validates decorators (from hooks)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
)
|
|
774
|
+
self._invoke_field_validators(
|
|
775
|
+
error_store=error_store,
|
|
776
|
+
data=validated_data,
|
|
777
|
+
many=False,
|
|
778
|
+
)
|
|
718
779
|
|
|
719
780
|
# 4b: Run our custom @validates decorators (backwards compatibility)
|
|
720
781
|
try:
|
|
@@ -729,25 +790,25 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
729
790
|
|
|
730
791
|
# Step 5: Run schema validators (BOTH Marshmallow native AND our custom)
|
|
731
792
|
# 5a: Run Marshmallow's native @validates_schema decorators
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
793
|
+
# MA 4.x renamed pass_many -> pass_collection
|
|
794
|
+
# Build common kwargs
|
|
795
|
+
validator_kwargs: dict[str, Any] = {
|
|
796
|
+
"error_store": error_store,
|
|
797
|
+
"data": validated_data,
|
|
798
|
+
"original_data": data,
|
|
799
|
+
"many": False,
|
|
800
|
+
"partial": partial,
|
|
801
|
+
"field_errors": has_field_errors,
|
|
802
|
+
}
|
|
803
|
+
# MA 4.x requires 'unknown' parameter
|
|
804
|
+
if _MARSHMALLOW_4_PLUS: # pragma: no cover
|
|
805
|
+
validator_kwargs["unknown"] = unknown_setting
|
|
806
|
+
# Pass the correctly-named parameter for the marshmallow version
|
|
807
|
+
pass_param = "pass_collection" if _MARSHMALLOW_4_PLUS else "pass_many"
|
|
808
|
+
|
|
809
|
+
# Only run pass_many=False validators in per-item mode
|
|
810
|
+
# pass_many=True validators are handled at the collection level (in many=True block above)
|
|
811
|
+
self._invoke_schema_validators(**{pass_param: False, **validator_kwargs})
|
|
751
812
|
|
|
752
813
|
# 5b: Run our custom @validates_schema decorators (backwards compatibility)
|
|
753
814
|
try:
|
|
@@ -797,14 +858,20 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
797
858
|
# Return dict instead of instance
|
|
798
859
|
result = validated_data
|
|
799
860
|
|
|
800
|
-
# Step 7: Run post_load hooks
|
|
801
|
-
if postprocess
|
|
861
|
+
# Step 7: Run post_load hooks
|
|
862
|
+
if postprocess:
|
|
863
|
+
# MA 4.x requires 'unknown' parameter, MA 3.x doesn't accept it
|
|
864
|
+
post_load_kwargs: dict[str, Any] = {
|
|
865
|
+
"many": False,
|
|
866
|
+
"original_data": data,
|
|
867
|
+
"partial": partial,
|
|
868
|
+
}
|
|
869
|
+
if _MARSHMALLOW_4_PLUS: # pragma: no cover
|
|
870
|
+
post_load_kwargs["unknown"] = unknown_setting
|
|
802
871
|
result = self._invoke_load_processors(
|
|
803
872
|
"post_load",
|
|
804
|
-
result,
|
|
805
|
-
|
|
806
|
-
original_data=data,
|
|
807
|
-
partial=partial,
|
|
873
|
+
cast(dict[str, Any], result),
|
|
874
|
+
**post_load_kwargs,
|
|
808
875
|
)
|
|
809
876
|
|
|
810
877
|
return result
|
|
@@ -1048,8 +1115,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
1048
1115
|
fields_to_exclude.add(field_name)
|
|
1049
1116
|
|
|
1050
1117
|
# Extract computed field values BEFORE converting to dict
|
|
1051
|
-
|
|
1052
|
-
|
|
1118
|
+
computed_fields = _get_computed_fields(model_class)
|
|
1119
|
+
if include_computed and computed_fields:
|
|
1120
|
+
for field_name in computed_fields:
|
|
1053
1121
|
value = getattr(obj, field_name)
|
|
1054
1122
|
# Apply exclusion rules to computed fields too
|
|
1055
1123
|
if exclude_none and value is None:
|
|
@@ -1159,6 +1227,66 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
1159
1227
|
return schema_cls
|
|
1160
1228
|
|
|
1161
1229
|
|
|
1230
|
+
if _MARSHMALLOW_4_PLUS: # pragma: no cover
|
|
1231
|
+
class PydanticSchema(_PydanticSchema[M], Generic[M]):
|
|
1232
|
+
def __init__(
|
|
1233
|
+
self,
|
|
1234
|
+
*,
|
|
1235
|
+
only: Sequence[str] | None = None,
|
|
1236
|
+
exclude: Sequence[str] = (),
|
|
1237
|
+
load_only: Sequence[str] = (),
|
|
1238
|
+
dump_only: Sequence[str] = (),
|
|
1239
|
+
partial: bool | Sequence[str] | AbstractSet[str] | None = None,
|
|
1240
|
+
unknown: str | None = None,
|
|
1241
|
+
many: bool | None = None,
|
|
1242
|
+
**kwargs: Any,
|
|
1243
|
+
) -> None:
|
|
1244
|
+
# MA 4.x removed context parameter - reject it to match native behavior
|
|
1245
|
+
if "context" in kwargs:
|
|
1246
|
+
raise TypeError(
|
|
1247
|
+
"PydanticSchema.__init__() got an unexpected keyword argument 'context'. "
|
|
1248
|
+
"The context parameter was removed in Marshmallow 4.x. "
|
|
1249
|
+
"Use contextvars.ContextVar instead."
|
|
1250
|
+
)
|
|
1251
|
+
super().__init__(
|
|
1252
|
+
only=only,
|
|
1253
|
+
exclude=exclude,
|
|
1254
|
+
load_only=load_only,
|
|
1255
|
+
dump_only=dump_only,
|
|
1256
|
+
partial=partial,
|
|
1257
|
+
unknown=unknown,
|
|
1258
|
+
many=many,
|
|
1259
|
+
**kwargs,
|
|
1260
|
+
)
|
|
1261
|
+
else:
|
|
1262
|
+
class PydanticSchema(_PydanticSchema[M], Generic[M]): # type: ignore[no-redef]
|
|
1263
|
+
def __init__(
|
|
1264
|
+
self,
|
|
1265
|
+
*,
|
|
1266
|
+
only: Sequence[str] | None = None,
|
|
1267
|
+
exclude: Sequence[str] = (),
|
|
1268
|
+
context: dict[str, Any] | None = None,
|
|
1269
|
+
load_only: Sequence[str] = (),
|
|
1270
|
+
dump_only: Sequence[str] = (),
|
|
1271
|
+
partial: bool | Sequence[str] | AbstractSet[str] | None = None,
|
|
1272
|
+
unknown: str | None = None,
|
|
1273
|
+
many: bool | None = None,
|
|
1274
|
+
**kwargs: Any,
|
|
1275
|
+
) -> None:
|
|
1276
|
+
# Pass context to super so Marshmallow sets self.context correctly
|
|
1277
|
+
super().__init__(
|
|
1278
|
+
only=only,
|
|
1279
|
+
exclude=exclude,
|
|
1280
|
+
context=context,
|
|
1281
|
+
load_only=load_only,
|
|
1282
|
+
dump_only=dump_only,
|
|
1283
|
+
partial=partial,
|
|
1284
|
+
unknown=unknown,
|
|
1285
|
+
many=many,
|
|
1286
|
+
**kwargs,
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
|
|
1162
1290
|
def schema_for(model: type[M], **meta_options: Any) -> type[PydanticSchema[M]]:
|
|
1163
1291
|
"""
|
|
1164
1292
|
Shortcut to create a Marshmallow schema from a Pydantic model.
|
|
@@ -10,17 +10,37 @@ from __future__ import annotations
|
|
|
10
10
|
from types import UnionType
|
|
11
11
|
from typing import Any, Union, get_args, get_origin
|
|
12
12
|
|
|
13
|
-
from marshmallow import fields as ma_fields
|
|
14
13
|
from pydantic import BaseModel
|
|
15
14
|
from pydantic_core import PydanticUndefined
|
|
16
15
|
|
|
17
16
|
from .type_mapping import type_to_marshmallow_field
|
|
18
17
|
|
|
19
18
|
|
|
19
|
+
def _get_computed_fields(model_class: type[BaseModel]) -> dict[str, Any]:
|
|
20
|
+
"""Get computed fields dict from a Pydantic model class.
|
|
21
|
+
|
|
22
|
+
Handles differences between Pydantic versions:
|
|
23
|
+
- Pydantic 2.0.x: model_computed_fields is a property (only works on instances)
|
|
24
|
+
- Pydantic 2.4+: model_computed_fields works directly on classes
|
|
25
|
+
"""
|
|
26
|
+
# Try direct access first (works in newer Pydantic)
|
|
27
|
+
result = getattr(model_class, 'model_computed_fields', None)
|
|
28
|
+
if isinstance(result, dict):
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
# For Pydantic 2.0.x: model_computed_fields is a property, call its getter
|
|
32
|
+
if isinstance(result, property) and result.fget:
|
|
33
|
+
prop_result = result.fget(model_class)
|
|
34
|
+
if isinstance(prop_result, dict):
|
|
35
|
+
return prop_result
|
|
36
|
+
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
20
40
|
def convert_pydantic_field(
|
|
21
41
|
field_name: str,
|
|
22
42
|
field_info: Any,
|
|
23
|
-
) ->
|
|
43
|
+
) -> Any:
|
|
24
44
|
"""
|
|
25
45
|
Convert a single Pydantic FieldInfo to a Marshmallow Field.
|
|
26
46
|
|
|
@@ -70,7 +90,7 @@ def convert_pydantic_field(
|
|
|
70
90
|
def convert_computed_field(
|
|
71
91
|
field_name: str,
|
|
72
92
|
computed_info: Any,
|
|
73
|
-
) ->
|
|
93
|
+
) -> Any:
|
|
74
94
|
"""
|
|
75
95
|
Convert a Pydantic computed_field to a dump-only Marshmallow Field.
|
|
76
96
|
|
|
@@ -93,7 +113,7 @@ def convert_model_fields(
|
|
|
93
113
|
include: set[str] | None = None,
|
|
94
114
|
exclude: set[str] | None = None,
|
|
95
115
|
include_computed: bool = True,
|
|
96
|
-
) -> dict[str,
|
|
116
|
+
) -> dict[str, Any]:
|
|
97
117
|
"""
|
|
98
118
|
Convert all fields from a Pydantic model to Marshmallow fields.
|
|
99
119
|
|
|
@@ -111,7 +131,7 @@ def convert_model_fields(
|
|
|
111
131
|
Returns:
|
|
112
132
|
Dict mapping field names to Marshmallow field instances
|
|
113
133
|
"""
|
|
114
|
-
fields: dict[str,
|
|
134
|
+
fields: dict[str, Any] = {}
|
|
115
135
|
exclude_set = exclude or set()
|
|
116
136
|
|
|
117
137
|
# Convert regular model fields
|
|
@@ -125,8 +145,9 @@ def convert_model_fields(
|
|
|
125
145
|
fields[field_name] = convert_pydantic_field(field_name, field_info)
|
|
126
146
|
|
|
127
147
|
# Convert computed fields (dump-only)
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
computed_fields = _get_computed_fields(model)
|
|
149
|
+
if include_computed and computed_fields:
|
|
150
|
+
for field_name, computed_info in computed_fields.items():
|
|
130
151
|
if field_name in exclude_set:
|
|
131
152
|
continue
|
|
132
153
|
if include is not None and field_name not in include:
|
|
@@ -4,6 +4,9 @@ Type mapping utilities for converting Python/Pydantic types to Marshmallow field
|
|
|
4
4
|
This module provides type-to-field conversions, leveraging Marshmallow's native
|
|
5
5
|
TYPE_MAPPING for basic types and adding support for generic collections.
|
|
6
6
|
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
7
10
|
from enum import Enum as PyEnum
|
|
8
11
|
from functools import lru_cache
|
|
9
12
|
from types import UnionType
|
|
@@ -20,7 +23,7 @@ _processing_models: set[type[Any]] = set()
|
|
|
20
23
|
# These are the most common types and benefit most from caching.
|
|
21
24
|
# maxsize=512 handles large codebases with ~80KB memory overhead.
|
|
22
25
|
@lru_cache(maxsize=512)
|
|
23
|
-
def _get_simple_field_class(type_hint: type) -> type[
|
|
26
|
+
def _get_simple_field_class(type_hint: type) -> type[Any]:
|
|
24
27
|
"""
|
|
25
28
|
Cached lookup for simple, hashable types in Marshmallow's TYPE_MAPPING.
|
|
26
29
|
|
|
@@ -30,7 +33,7 @@ def _get_simple_field_class(type_hint: type) -> type[ma_fields.Field]:
|
|
|
30
33
|
return Schema.TYPE_MAPPING.get(type_hint, ma_fields.Raw)
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
def type_to_marshmallow_field(type_hint: Any) ->
|
|
36
|
+
def type_to_marshmallow_field(type_hint: Any) -> Any:
|
|
34
37
|
"""
|
|
35
38
|
Map a Python type to a Marshmallow field instance.
|
|
36
39
|
|
|
@@ -88,40 +91,47 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
|
88
91
|
return ma_fields.Raw(allow_none=True)
|
|
89
92
|
|
|
90
93
|
# Handle Enum types
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
# Use try-except because some type hints pass isinstance(x, type) but fail issubclass()
|
|
95
|
+
try:
|
|
96
|
+
if isinstance(type_hint, type) and issubclass(type_hint, PyEnum):
|
|
97
|
+
return ma_fields.Enum(type_hint)
|
|
98
|
+
except TypeError:
|
|
99
|
+
pass # Not a valid class for issubclass check
|
|
93
100
|
|
|
94
101
|
# Handle nested Pydantic models - use Nested with a dynamically created schema
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
try:
|
|
103
|
+
if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
|
|
104
|
+
# Import here to avoid circular imports
|
|
105
|
+
from pydantic_marshmallow.bridge import PydanticSchema
|
|
106
|
+
|
|
107
|
+
# Check if we're already processing this model (recursion detection)
|
|
108
|
+
# Use a module-level set to track models being processed
|
|
109
|
+
if type_hint in _processing_models:
|
|
110
|
+
# Self-referential model - use Raw to avoid infinite recursion
|
|
111
|
+
# Pydantic will still handle the validation correctly
|
|
112
|
+
return ma_fields.Raw()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
_processing_models.add(type_hint)
|
|
116
|
+
# Create a nested schema class for this model
|
|
117
|
+
nested_schema = PydanticSchema.from_model(type_hint)
|
|
118
|
+
return ma_fields.Nested(nested_schema)
|
|
119
|
+
finally:
|
|
120
|
+
_processing_models.discard(type_hint)
|
|
121
|
+
except TypeError:
|
|
122
|
+
pass # Not a valid class for issubclass check
|
|
113
123
|
|
|
114
124
|
# Handle list[T]
|
|
115
125
|
if origin is list:
|
|
116
|
-
inner
|
|
126
|
+
inner = ma_fields.Raw()
|
|
117
127
|
if args:
|
|
118
128
|
inner = type_to_marshmallow_field(args[0])
|
|
119
129
|
return ma_fields.List(inner)
|
|
120
130
|
|
|
121
131
|
# Handle dict[K, V]
|
|
122
132
|
if origin is dict:
|
|
123
|
-
key_field
|
|
124
|
-
value_field
|
|
133
|
+
key_field = ma_fields.String()
|
|
134
|
+
value_field = ma_fields.Raw()
|
|
125
135
|
if args and len(args) >= 2:
|
|
126
136
|
key_field = type_to_marshmallow_field(args[0])
|
|
127
137
|
value_field = type_to_marshmallow_field(args[1])
|
|
@@ -129,7 +139,7 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
|
129
139
|
|
|
130
140
|
# Handle set[T] and frozenset[T] - convert to List in Marshmallow
|
|
131
141
|
if origin in (set, frozenset):
|
|
132
|
-
inner_set
|
|
142
|
+
inner_set = ma_fields.Raw()
|
|
133
143
|
if args:
|
|
134
144
|
inner_set = type_to_marshmallow_field(args[0])
|
|
135
145
|
return ma_fields.List(inner_set)
|
|
@@ -155,8 +165,7 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
|
155
165
|
if any(ut in type_name for ut in url_types):
|
|
156
166
|
return ma_fields.URL()
|
|
157
167
|
if 'IP' in type_name:
|
|
158
|
-
|
|
159
|
-
return ma_fields.IP() # type: ignore[no-untyped-call]
|
|
168
|
+
return ma_fields.IP() # type: ignore[no-untyped-call,unused-ignore]
|
|
160
169
|
|
|
161
170
|
# Use Marshmallow's native TYPE_MAPPING for basic types
|
|
162
171
|
# This ensures we stay in sync with Marshmallow's type handling
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-marshmallow
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Bring Pydantic's power to Marshmallow: type annotations, validators, and automatic schema generation
|
|
5
|
-
Author
|
|
5
|
+
Author: Michael Thomas
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/mockodin/pydantic-marshmallow
|
|
8
8
|
Project-URL: Documentation, https://mockodin.github.io/pydantic-marshmallow
|
|
@@ -32,10 +32,9 @@ Provides-Extra: dev
|
|
|
32
32
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
33
33
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
34
34
|
Requires-Dist: email-validator>=2.1.0; extra == "dev"
|
|
35
|
-
Requires-Dist: mypy
|
|
36
|
-
Requires-Dist: ruff
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist: flake8-pyproject>=1.2.4; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy==1.19.1; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff==0.15.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pre-commit>=4.2.0; extra == "dev"
|
|
39
38
|
Requires-Dist: sqlalchemy>=2.0.30; extra == "dev"
|
|
40
39
|
Requires-Dist: marshmallow-sqlalchemy>=1.1.0; extra == "dev"
|
|
41
40
|
Requires-Dist: flask>=3.0.0; extra == "dev"
|
|
@@ -73,16 +72,27 @@ Get the best of both worlds: **Pydantic's speed** with **Marshmallow's ecosystem
|
|
|
73
72
|
|
|
74
73
|
### Performance
|
|
75
74
|
|
|
76
|
-
pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the hood, delivering significant performance improvements over native Marshmallow—especially for nested data structures
|
|
75
|
+
pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the hood, delivering significant performance improvements over native Marshmallow—especially for nested data structures.
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|-----------|---------------------|-------------|---------|
|
|
80
|
-
| Simple load | 3.0 µs | 5.3 µs | **1.8x faster** |
|
|
81
|
-
| Nested model | 3.5 µs | 11.6 µs | **3.3x faster** |
|
|
82
|
-
| Deep nested (4 levels) | 5.6 µs | 32.3 µs | **5.8x faster** |
|
|
83
|
-
| Batch (100 items) | 255 µs | 474 µs | **1.9x faster** |
|
|
77
|
+
#### Performance Comparison
|
|
84
78
|
|
|
85
|
-
|
|
79
|
+
| Operation | MA 3.x | MA 4.x | Bridge (MA 3.x) | Bridge (MA 4.x) |
|
|
80
|
+
|-----------|--------|--------|-----------------|-----------------|
|
|
81
|
+
| Simple load | 5.3 µs | 4.8 µs | 2.9 µs | 2.9 µs |
|
|
82
|
+
| Nested model | 10.9 µs | 10.5 µs | 3.4 µs | 3.2 µs |
|
|
83
|
+
| Deep nested (4 levels) | 31.2 µs | 29.0 µs | 5.3 µs | 5.2 µs |
|
|
84
|
+
| Batch (100 items) | 454 µs | 424 µs | 260 µs | 259 µs |
|
|
85
|
+
|
|
86
|
+
#### What the Numbers Show
|
|
87
|
+
|
|
88
|
+
1. **MA 3.x → MA 4.x**: Marshmallow 4.x improved ~10% over 3.x (5.3 → 4.8 µs for simple loads)
|
|
89
|
+
2. **MA 3.x → Bridge**: Adding Pydantic delivers **1.8x–5.9x speedup** over pure MA 3.x
|
|
90
|
+
3. **MA 4.x → Bridge**: Still **1.6x–5.6x faster** than native MA 4.x
|
|
91
|
+
4. **Bridge consistency**: ~2.9 µs for simple loads regardless of Marshmallow version
|
|
92
|
+
|
|
93
|
+
The bridge delegates validation to Pydantic's Rust-powered core, bypassing Marshmallow's field processing. This means bridge performance is **independent of Marshmallow version**—you get consistent speed whether you're on MA 3.x or 4.x.
|
|
94
|
+
|
|
95
|
+
*Benchmarks: Python 3.11, median of 3 runs with IQR outlier removal. Run `python -m benchmarks.run_benchmarks` to reproduce.*
|
|
86
96
|
|
|
87
97
|
### Why it matters
|
|
88
98
|
|
|
@@ -107,6 +117,8 @@ pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the ho
|
|
|
107
117
|
pip install pydantic-marshmallow
|
|
108
118
|
```
|
|
109
119
|
|
|
120
|
+
**Requirements:** Python 3.10+, Pydantic 2.0+, Marshmallow 3.18+ (including 4.x)
|
|
121
|
+
|
|
110
122
|
## Quick Start
|
|
111
123
|
|
|
112
124
|
### Basic Usage
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pydantic_marshmallow/__init__.py,sha256=EQON4HpiOFDtC33MrrE6WFW68GBtDlx1IqbDWxbqghU,3100
|
|
2
|
+
pydantic_marshmallow/bridge.py,sha256=fBk9vv6xZd-r-rhHufl-fbWSt784SEQ-IYrlm3duzpY,58026
|
|
3
|
+
pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
|
|
4
|
+
pydantic_marshmallow/field_conversion.py,sha256=WaWhvpB8rLL05I_D8b9q7BCymf24L7rxOpjMJY-TtYo,4978
|
|
5
|
+
pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
|
|
6
|
+
pydantic_marshmallow/type_mapping.py,sha256=XlgOH3kksrv2kratQ1B1J1AJZj2spu7izoIF58hZeJs,6751
|
|
7
|
+
pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
|
|
8
|
+
pydantic_marshmallow-1.1.0.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
|
|
9
|
+
pydantic_marshmallow-1.1.0.dist-info/METADATA,sha256=gwysXAIX0iAhRfpAe1aCzQPQEX_u5DC-YYNcMu1zghg,15021
|
|
10
|
+
pydantic_marshmallow-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
pydantic_marshmallow-1.1.0.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
|
|
12
|
+
pydantic_marshmallow-1.1.0.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pydantic_marshmallow/__init__.py,sha256=EQON4HpiOFDtC33MrrE6WFW68GBtDlx1IqbDWxbqghU,3100
|
|
2
|
-
pydantic_marshmallow/bridge.py,sha256=MOBwr0casMgaPjP5GAcYjnG8bGgNpmpEa-zagjkClEY,52147
|
|
3
|
-
pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
|
|
4
|
-
pydantic_marshmallow/field_conversion.py,sha256=xFQtR63DwTDq-hGA_8Xe8sO6lFW6BCZNmnk5oxz5B6w,4262
|
|
5
|
-
pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
|
|
6
|
-
pydantic_marshmallow/type_mapping.py,sha256=NYTJdmTVL-EjNW93ysb2KN9ASAj0mQu8rQpa_vOgqbU,6529
|
|
7
|
-
pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
|
|
8
|
-
pydantic_marshmallow-1.0.1.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
|
|
9
|
-
pydantic_marshmallow-1.0.1.dist-info/METADATA,sha256=yNbwhXPJSg0WZAc8tfFo4U6fy57gX0eVNRf4JPLHO4g,14302
|
|
10
|
-
pydantic_marshmallow-1.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
-
pydantic_marshmallow-1.0.1.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
|
|
12
|
-
pydantic_marshmallow-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
{pydantic_marshmallow-1.0.1.dist-info → pydantic_marshmallow-1.1.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|