pydantic-marshmallow 1.0.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.
@@ -0,0 +1,154 @@
1
+ """
2
+ Error handling utilities for the Marshmallow-Pydantic bridge.
3
+
4
+ This module provides:
5
+ - BridgeValidationError: Exception with valid_data tracking for partial success
6
+ - Error path building and formatting utilities for Pydantic→Marshmallow conversion
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from marshmallow.exceptions import ValidationError as MarshmallowValidationError
14
+ from pydantic import BaseModel, ValidationError as PydanticValidationError
15
+ from pydantic_core import ErrorDetails
16
+
17
+
18
+ class BridgeValidationError(MarshmallowValidationError):
19
+ """
20
+ Validation error that bridges Marshmallow and Pydantic error formats.
21
+
22
+ Extends Marshmallow's ValidationError to track partially valid data,
23
+ enabling graceful handling of multi-field validation failures.
24
+
25
+ Attributes:
26
+ messages: Dict of field -> error messages (Marshmallow format)
27
+ valid_data: Dict of successfully validated fields
28
+ data: Original input data
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ message: str | list[Any] | dict[str, Any],
34
+ field_name: str = "_schema",
35
+ data: Any | None = None,
36
+ valid_data: dict[str, Any] | None = None,
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ self.data = data
40
+ self.valid_data = valid_data or {}
41
+ super().__init__(message, field_name, data, valid_data=valid_data or {}, **kwargs)
42
+
43
+
44
+ def build_error_path(loc: tuple[Any, ...]) -> str:
45
+ """
46
+ Build a dotted error path from Pydantic's location tuple.
47
+
48
+ Converts Pydantic's error location format to Marshmallow's dotted path format.
49
+ Handles collection indices like ("items", 0, "name") -> "items.0.name".
50
+
51
+ Args:
52
+ loc: Pydantic error location tuple (e.g., ('addresses', 0, 'zip_code'))
53
+
54
+ Returns:
55
+ Dotted path string (e.g., 'addresses.0.zip_code')
56
+ """
57
+ if len(loc) == 1:
58
+ return str(loc[0])
59
+ return ".".join(str(part) for part in loc)
60
+
61
+
62
+ def format_pydantic_error(
63
+ error: ErrorDetails,
64
+ model_class: type[BaseModel] | None = None,
65
+ ) -> str:
66
+ """
67
+ Format a Pydantic error dict into a user-friendly message.
68
+
69
+ Checks for custom error messages in field metadata when model_class is provided.
70
+
71
+ Args:
72
+ error: Pydantic ErrorDetails with 'msg', 'type', 'loc' keys
73
+ model_class: Optional Pydantic model for custom message lookup
74
+
75
+ Returns:
76
+ Formatted error message string
77
+ """
78
+ msg: str = error.get("msg", "Validation error")
79
+ error_type: str = error.get("type", "")
80
+ loc = error.get("loc", ())
81
+
82
+ # Check for custom error message in model field metadata
83
+ if model_class and loc:
84
+ field_name = str(loc[0])
85
+ field_info = model_class.model_fields.get(field_name)
86
+ if field_info and field_info.json_schema_extra:
87
+ extra = field_info.json_schema_extra
88
+ if isinstance(extra, dict):
89
+ custom_messages = extra.get("error_messages")
90
+ if isinstance(custom_messages, dict):
91
+ if error_type in custom_messages:
92
+ custom_msg = custom_messages[error_type]
93
+ if isinstance(custom_msg, str):
94
+ return custom_msg
95
+ if "default" in custom_messages:
96
+ default_msg = custom_messages["default"]
97
+ if isinstance(default_msg, str):
98
+ return default_msg
99
+
100
+ return msg
101
+
102
+
103
+ def convert_pydantic_errors(
104
+ pydantic_error: PydanticValidationError,
105
+ model_class: type[BaseModel] | None = None,
106
+ original_data: dict[str, Any] | None = None,
107
+ ) -> BridgeValidationError:
108
+ """
109
+ Convert a Pydantic ValidationError to BridgeValidationError.
110
+
111
+ Transforms Pydantic's error format to Marshmallow-compatible format,
112
+ tracking which fields failed and which succeeded.
113
+
114
+ Args:
115
+ pydantic_error: The Pydantic ValidationError to convert
116
+ model_class: The Pydantic model class (for custom messages)
117
+ original_data: The original input data
118
+
119
+ Returns:
120
+ BridgeValidationError with Marshmallow-formatted messages and valid_data
121
+ """
122
+ errors: dict[str, Any] = {}
123
+ failed_fields: set[str] = set()
124
+
125
+ for error in pydantic_error.errors():
126
+ loc = error.get("loc", ())
127
+ msg = format_pydantic_error(error, model_class)
128
+
129
+ if loc:
130
+ field_path = build_error_path(loc)
131
+ failed_fields.add(str(loc[0])) # Track top-level field
132
+
133
+ if field_path in errors:
134
+ if isinstance(errors[field_path], list):
135
+ errors[field_path].append(msg)
136
+ else:
137
+ errors[field_path] = [errors[field_path], msg]
138
+ else:
139
+ errors[field_path] = [msg]
140
+ else:
141
+ errors["_schema"] = [*errors.get("_schema", []), msg]
142
+
143
+ # Build valid_data: fields that weren't in the error list
144
+ valid_data: dict[str, Any] = {}
145
+ if original_data and model_class:
146
+ for field_name in original_data:
147
+ if field_name not in failed_fields and field_name in model_class.model_fields:
148
+ valid_data[field_name] = original_data[field_name]
149
+
150
+ return BridgeValidationError(
151
+ errors,
152
+ data=original_data,
153
+ valid_data=valid_data,
154
+ )
@@ -0,0 +1,137 @@
1
+ """
2
+ Field conversion utilities for Pydantic to Marshmallow field mapping.
3
+
4
+ This module centralizes the conversion of Pydantic model fields to Marshmallow fields,
5
+ eliminating duplication between the metaclass, instance setup, and factory methods.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from types import UnionType
11
+ from typing import Any, Union, get_args, get_origin
12
+
13
+ from marshmallow import fields as ma_fields
14
+ from pydantic import BaseModel
15
+ from pydantic_core import PydanticUndefined
16
+
17
+ from .type_mapping import type_to_marshmallow_field
18
+
19
+
20
+ def convert_pydantic_field(
21
+ field_name: str,
22
+ field_info: Any,
23
+ ) -> ma_fields.Field:
24
+ """
25
+ Convert a single Pydantic FieldInfo to a Marshmallow Field.
26
+
27
+ Handles:
28
+ - Type conversion via type_mapping
29
+ - Required status (fields without defaults are required)
30
+ - Default values (static and factory)
31
+ - Field aliases (data_key)
32
+ - Optional/None handling (allow_none)
33
+
34
+ Args:
35
+ field_name: Name of the field
36
+ field_info: Pydantic FieldInfo object
37
+
38
+ Returns:
39
+ Configured Marshmallow field instance
40
+ """
41
+ annotation = field_info.annotation
42
+ ma_field = type_to_marshmallow_field(annotation)
43
+
44
+ # Apply default values and set required status
45
+ if field_info.default is not PydanticUndefined:
46
+ ma_field.load_default = field_info.default
47
+ ma_field.dump_default = field_info.default
48
+ ma_field.required = False
49
+ elif field_info.default_factory is not None:
50
+ ma_field.load_default = field_info.default_factory
51
+ ma_field.required = False
52
+ else:
53
+ # No default - field is required
54
+ ma_field.required = True
55
+
56
+ # Handle alias → data_key
57
+ if field_info.alias:
58
+ ma_field.data_key = field_info.alias
59
+
60
+ # Handle Optional types (X | None or Optional[X]) → allow_none
61
+ origin = get_origin(annotation)
62
+ args = get_args(annotation)
63
+ is_union = origin is Union or origin is UnionType
64
+ if origin is type(None) or (is_union and type(None) in args):
65
+ ma_field.allow_none = True
66
+
67
+ return ma_field
68
+
69
+
70
+ def convert_computed_field(
71
+ field_name: str,
72
+ computed_info: Any,
73
+ ) -> ma_fields.Field:
74
+ """
75
+ Convert a Pydantic computed_field to a dump-only Marshmallow Field.
76
+
77
+ Args:
78
+ field_name: Name of the computed field
79
+ computed_info: Pydantic ComputedFieldInfo object
80
+
81
+ Returns:
82
+ Dump-only Marshmallow field instance
83
+ """
84
+ return_type = getattr(computed_info, 'return_type', Any)
85
+ ma_field = type_to_marshmallow_field(return_type)
86
+ ma_field.dump_only = True
87
+ return ma_field
88
+
89
+
90
+ def convert_model_fields(
91
+ model: type[BaseModel],
92
+ *,
93
+ include: set[str] | None = None,
94
+ exclude: set[str] | None = None,
95
+ include_computed: bool = True,
96
+ ) -> dict[str, ma_fields.Field]:
97
+ """
98
+ Convert all fields from a Pydantic model to Marshmallow fields.
99
+
100
+ This is the main entry point for field conversion, used by:
101
+ - PydanticSchemaMeta.__new__()
102
+ - PydanticSchema._setup_fields_from_model()
103
+ - PydanticSchema.from_model()
104
+
105
+ Args:
106
+ model: Pydantic model class
107
+ include: Optional whitelist of field names to include
108
+ exclude: Optional blacklist of field names to exclude
109
+ include_computed: Whether to include @computed_field properties
110
+
111
+ Returns:
112
+ Dict mapping field names to Marshmallow field instances
113
+ """
114
+ fields: dict[str, ma_fields.Field] = {}
115
+ exclude_set = exclude or set()
116
+
117
+ # Convert regular model fields
118
+ for field_name, field_info in model.model_fields.items():
119
+ # Apply filtering
120
+ if field_name in exclude_set:
121
+ continue
122
+ if include is not None and field_name not in include:
123
+ continue
124
+
125
+ fields[field_name] = convert_pydantic_field(field_name, field_info)
126
+
127
+ # Convert computed fields (dump-only)
128
+ if include_computed and hasattr(model, 'model_computed_fields'):
129
+ for field_name, computed_info in model.model_computed_fields.items():
130
+ if field_name in exclude_set:
131
+ continue
132
+ if include is not None and field_name not in include:
133
+ continue
134
+
135
+ fields[field_name] = convert_computed_field(field_name, computed_info)
136
+
137
+ return fields
@@ -0,0 +1,2 @@
1
+ # Marker file for PEP 561.
2
+ # The marshmallow-pydantic package uses inline type hints.
@@ -0,0 +1,138 @@
1
+ """
2
+ Type mapping utilities for converting Python/Pydantic types to Marshmallow fields.
3
+
4
+ This module provides type-to-field conversions, leveraging Marshmallow's native
5
+ TYPE_MAPPING for basic types and adding support for generic collections.
6
+ """
7
+ from enum import Enum as PyEnum
8
+ from types import UnionType
9
+ from typing import Any, Literal, Union, get_args, get_origin
10
+
11
+ from marshmallow import Schema, fields as ma_fields
12
+ from pydantic import BaseModel
13
+
14
+ # Track models being processed to detect recursion
15
+ _processing_models: set[type[Any]] = set()
16
+
17
+
18
+ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
19
+ """
20
+ Map a Python type to a Marshmallow field instance.
21
+
22
+ Uses Marshmallow's native TYPE_MAPPING for basic types (str, int, datetime, etc.)
23
+ and adds support for:
24
+ - Generic collections (list[T], dict[K, V], set[T], frozenset[T])
25
+ - Optional types (T | None)
26
+ - Union types (X | Y)
27
+ - Tuple types
28
+ - Nested Pydantic models
29
+ - Enums
30
+ - Literal types
31
+ - Pydantic special types (EmailStr, AnyUrl, etc.)
32
+
33
+ Args:
34
+ type_hint: A Python type annotation
35
+
36
+ Returns:
37
+ An appropriate Marshmallow field instance
38
+ """
39
+ origin = get_origin(type_hint)
40
+ args = get_args(type_hint)
41
+
42
+ # Handle NoneType
43
+ if type_hint is type(None):
44
+ return ma_fields.Raw(allow_none=True)
45
+
46
+ # Handle Literal types
47
+ if origin is Literal:
48
+ # For single-value Literal, could be a constant
49
+ if args and len(args) == 1:
50
+ # Use Raw with validation - Pydantic will enforce the literal
51
+ return ma_fields.Raw()
52
+ return ma_fields.Raw()
53
+
54
+ # Handle Union (including Optional) - supports both Union[X, Y] and X | Y syntax
55
+ if origin is Union or origin is UnionType:
56
+ non_none_args = [a for a in args if a is not type(None)]
57
+ if len(non_none_args) == 1:
58
+ field = type_to_marshmallow_field(non_none_args[0])
59
+ field.allow_none = True
60
+ return field
61
+ return ma_fields.Raw(allow_none=True)
62
+
63
+ # Handle Enum types
64
+ if isinstance(type_hint, type) and issubclass(type_hint, PyEnum):
65
+ return ma_fields.Enum(type_hint)
66
+
67
+ # Handle nested Pydantic models - use Nested with a dynamically created schema
68
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
69
+ # Import here to avoid circular imports
70
+ from pydantic_marshmallow.bridge import PydanticSchema
71
+
72
+ # Check if we're already processing this model (recursion detection)
73
+ # Use a module-level set to track models being processed
74
+ if type_hint in _processing_models:
75
+ # Self-referential model - use Raw to avoid infinite recursion
76
+ # Pydantic will still handle the validation correctly
77
+ return ma_fields.Raw()
78
+
79
+ try:
80
+ _processing_models.add(type_hint)
81
+ # Create a nested schema class for this model
82
+ nested_schema = PydanticSchema.from_model(type_hint)
83
+ return ma_fields.Nested(nested_schema)
84
+ finally:
85
+ _processing_models.discard(type_hint)
86
+
87
+ # Handle list[T]
88
+ if origin is list:
89
+ inner: ma_fields.Field = ma_fields.Raw()
90
+ if args:
91
+ inner = type_to_marshmallow_field(args[0])
92
+ return ma_fields.List(inner)
93
+
94
+ # Handle dict[K, V]
95
+ if origin is dict:
96
+ key_field: ma_fields.Field = ma_fields.String()
97
+ value_field: ma_fields.Field = ma_fields.Raw()
98
+ if args and len(args) >= 2:
99
+ key_field = type_to_marshmallow_field(args[0])
100
+ value_field = type_to_marshmallow_field(args[1])
101
+ return ma_fields.Dict(keys=key_field, values=value_field)
102
+
103
+ # Handle set[T] and frozenset[T] - convert to List in Marshmallow
104
+ if origin in (set, frozenset):
105
+ inner_set: ma_fields.Field = ma_fields.Raw()
106
+ if args:
107
+ inner_set = type_to_marshmallow_field(args[0])
108
+ return ma_fields.List(inner_set)
109
+
110
+ # Handle Tuple - use Marshmallow's Tuple field if available
111
+ if origin is tuple:
112
+ if args:
113
+ # Fixed-length tuple with specific types
114
+ tuple_fields = [type_to_marshmallow_field(arg) for arg in args if arg is not ...]
115
+ if tuple_fields:
116
+ return ma_fields.Tuple(tuple_fields=tuple(tuple_fields))
117
+ return ma_fields.List(ma_fields.Raw())
118
+
119
+ # Check for Pydantic special types by module
120
+ type_module = getattr(type_hint, '__module__', '')
121
+ type_name = getattr(type_hint, '__name__', str(type_hint))
122
+
123
+ # Handle Pydantic networking types
124
+ if 'pydantic' in type_module:
125
+ if 'EmailStr' in type_name or type_name == 'EmailStr':
126
+ return ma_fields.Email()
127
+ url_types = ('Url', 'URL', 'HttpUrl', 'AnyUrl')
128
+ if any(ut in type_name for ut in url_types):
129
+ return ma_fields.URL()
130
+ if 'IP' in type_name:
131
+ # IP field exists in marshmallow but may not have type stubs
132
+ return ma_fields.IP() # type: ignore[no-untyped-call]
133
+
134
+ # Use Marshmallow's native TYPE_MAPPING for basic types
135
+ # This ensures we stay in sync with Marshmallow's type handling
136
+ resolved = origin if origin else type_hint
137
+ field_cls = Schema.TYPE_MAPPING.get(resolved, ma_fields.Raw)
138
+ return field_cls()
@@ -0,0 +1,207 @@
1
+ """
2
+ Custom validator decorators for PydanticSchema.
3
+
4
+ These provide backwards-compatible validators that work alongside Marshmallow's
5
+ native @validates and @validates_schema decorators. In most cases, users should
6
+ prefer Marshmallow's native decorators (from marshmallow import validates).
7
+
8
+ Note: These custom decorators are provided for scenarios where additional
9
+ functionality beyond Marshmallow's native validators is needed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Protocol, TypeVar, cast, overload
15
+
16
+ F = TypeVar("F", bound="ValidatorFunc")
17
+
18
+
19
+ class ValidatorFunc(Protocol):
20
+ """Protocol for validator functions with metadata attributes."""
21
+
22
+ _validates_field: str
23
+ _validates_schema: bool
24
+ _pass_many: bool
25
+ _skip_on_field_errors: bool
26
+
27
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
28
+ """Call the validator function."""
29
+ ...
30
+
31
+
32
+ class FieldValidatorFunc(Protocol):
33
+ """Protocol for field validator functions."""
34
+
35
+ _validates_field: str
36
+
37
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
38
+ """Call the field validator."""
39
+ ...
40
+
41
+
42
+ class SchemaValidatorFunc(Protocol):
43
+ """Protocol for schema validator functions."""
44
+
45
+ _validates_schema: bool
46
+ _pass_many: bool
47
+ _skip_on_field_errors: bool
48
+
49
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
50
+ """Call the schema validator."""
51
+ ...
52
+
53
+
54
+ # Registry for custom validators (used for backwards compatibility)
55
+ _field_validators: dict[tuple[type[Any], str], list[FieldValidatorFunc]] = {}
56
+ _schema_validators: dict[type[Any], list[SchemaValidatorFunc]] = {}
57
+
58
+
59
+ def validates(field_name: str) -> ValidatesDecorator:
60
+ """
61
+ Decorator to register a field validator on a PydanticSchema.
62
+
63
+ The decorated method receives the field value and should raise
64
+ ValidationError if validation fails. Compatible with Marshmallow's
65
+ @validates decorator.
66
+
67
+ Note: In most cases, prefer `from marshmallow import validates` which
68
+ works natively with PydanticSchema via Marshmallow's hook system.
69
+
70
+ Example:
71
+ class UserSchema(PydanticSchema[User]):
72
+ class Meta:
73
+ model = User
74
+
75
+ @validates("name")
76
+ def validate_name(self, value):
77
+ if value.lower() == "admin":
78
+ raise ValidationError("Cannot use 'admin' as name")
79
+ """
80
+ return ValidatesDecorator(field_name)
81
+
82
+
83
+ class ValidatesDecorator:
84
+ """Decorator class for @validates that preserves function types."""
85
+
86
+ def __init__(self, field_name: str) -> None:
87
+ self.field_name = field_name
88
+
89
+ def __call__(self, fn: F) -> F:
90
+ """Decorate a function as a field validator."""
91
+ # Use object.__setattr__ to set attributes on the function
92
+ object.__setattr__(fn, "_validates_field", self.field_name)
93
+ return fn
94
+
95
+
96
+ @overload
97
+ def validates_schema(fn: F) -> F:
98
+ ...
99
+
100
+
101
+ @overload
102
+ def validates_schema(
103
+ fn: None = None,
104
+ *,
105
+ pass_many: bool = False,
106
+ skip_on_field_errors: bool = True,
107
+ ) -> ValidatesSchemaDecorator:
108
+ ...
109
+
110
+
111
+ def validates_schema(
112
+ fn: F | None = None,
113
+ *,
114
+ pass_many: bool = False,
115
+ skip_on_field_errors: bool = True,
116
+ ) -> F | ValidatesSchemaDecorator:
117
+ """
118
+ Decorator to register a schema-level validator on a PydanticSchema.
119
+
120
+ The decorated method receives the full data dict and should raise
121
+ ValidationError if validation fails. Compatible with Marshmallow's
122
+ @validates_schema decorator.
123
+
124
+ Note: In most cases, prefer `from marshmallow import validates_schema`
125
+ which works natively with PydanticSchema via Marshmallow's hook system.
126
+
127
+ Args:
128
+ fn: The validator function (when used without parentheses)
129
+ pass_many: Whether to pass the full collection when many=True
130
+ skip_on_field_errors: Skip this validator if field errors exist
131
+
132
+ Example:
133
+ class UserSchema(PydanticSchema[User]):
134
+ class Meta:
135
+ model = User
136
+
137
+ @validates_schema
138
+ def validate_user(self, data, **kwargs):
139
+ if data.get("password") != data.get("confirm_password"):
140
+ raise ValidationError("Passwords must match", "_schema")
141
+ """
142
+ decorator = ValidatesSchemaDecorator(pass_many, skip_on_field_errors)
143
+ if fn is not None:
144
+ return decorator(fn)
145
+ return decorator
146
+
147
+
148
+ class ValidatesSchemaDecorator:
149
+ """Decorator class for @validates_schema that preserves function types."""
150
+
151
+ def __init__(self, pass_many: bool, skip_on_field_errors: bool) -> None:
152
+ self.pass_many = pass_many
153
+ self.skip_on_field_errors = skip_on_field_errors
154
+
155
+ def __call__(self, func: F) -> F:
156
+ """Decorate a function as a schema validator."""
157
+ object.__setattr__(func, "_validates_schema", True)
158
+ object.__setattr__(func, "_pass_many", self.pass_many)
159
+ object.__setattr__(func, "_skip_on_field_errors", self.skip_on_field_errors)
160
+ return func
161
+
162
+
163
+ class SchemaWithValidatorCache(Protocol):
164
+ """Protocol for schema classes that have validator caches."""
165
+
166
+ _field_validators_cache: dict[str, list[str]]
167
+ _schema_validators_cache: list[str]
168
+
169
+
170
+ def cache_validators(cls: type[Any]) -> None:
171
+ """
172
+ Cache custom validators at class creation time.
173
+
174
+ Scans the class for methods decorated with @validates and @validates_schema
175
+ and caches them for efficient lookup during validation.
176
+
177
+ Args:
178
+ cls: The schema class to cache validators for (must have validator cache attrs)
179
+ """
180
+ # Set cache attributes on the class
181
+ field_cache: dict[str, list[str]] = {}
182
+ schema_cache: list[str] = []
183
+
184
+ for attr_name in dir(cls):
185
+ if attr_name.startswith('_'):
186
+ continue
187
+ try:
188
+ attr = getattr(cls, attr_name, None)
189
+ except AttributeError:
190
+ continue
191
+
192
+ if callable(attr):
193
+ if hasattr(attr, "_validates_field"):
194
+ # Cast to protocol type since hasattr confirmed the attribute exists
195
+ field_validator = cast(FieldValidatorFunc, attr)
196
+ field: str = field_validator._validates_field
197
+ if field not in field_cache:
198
+ field_cache[field] = []
199
+ field_cache[field].append(attr_name)
200
+ elif hasattr(attr, "_validates_schema"):
201
+ schema_cache.append(attr_name)
202
+
203
+ # Assign to class - cast to type that has the cache attributes
204
+ # PydanticSchema defines these as ClassVar, so they exist at runtime
205
+ schema_cls = cast(type[SchemaWithValidatorCache], cls)
206
+ schema_cls._field_validators_cache = field_cache
207
+ schema_cls._schema_validators_cache = schema_cache