Skip to content
Merged

Dev #314

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

2.20.0 (2026-04-12)
-------------------

- feat: hide_input_in_errors model config flag support added.
- fix: computed_attr and computed_element decorators typing fixed.


2.19.0 (2026-02-14)
-------------------

Expand Down
34 changes: 33 additions & 1 deletion pydantic_xml/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
import dataclasses as dc
import typing
from typing import Any, Callable, Dict, Optional, Union
from typing import Any, Callable, Dict, Optional, Union, overload

import pydantic as pd
import pydantic_core as pdc
Expand Down Expand Up @@ -266,6 +266,21 @@ def decorator(prop: Any) -> Any:
return decorator(prop)


@overload
def computed_attr(
*,
name: Optional[str] = None,
ns: Optional[str] = None,
**kwargs: Any,
) -> Callable[[PropertyT], PropertyT]:
...


@overload
def computed_attr(prop: PropertyT) -> PropertyT:
...


def computed_attr(
prop: Optional[PropertyT] = None,
*,
Expand All @@ -285,6 +300,23 @@ def computed_attr(
return computed_entity(EntityLocation.ATTRIBUTE, prop, path=name, ns=ns, **kwargs)


@overload
def computed_element(
*,
tag: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
nillable: Optional[bool] = None,
**kwargs: Any,
) -> Callable[[PropertyT], PropertyT]:
...


@overload
def computed_element(prop: PropertyT) -> PropertyT:
...


def computed_element(
prop: Optional[PropertyT] = None,
*,
Expand Down
17 changes: 14 additions & 3 deletions pydantic_xml/serializers/factories/heterogeneous.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ def from_core_schema(cls, schema: pcs.TupleSchema, ctx: Serializer.Context) -> '
for item_schema in schema['items_schema']:
inner_serializers.append(Serializer.parse_core_schema(item_schema, ctx))

return cls(model_name, computed, tuple(inner_serializers))
return cls(model_name, computed, tuple(inner_serializers), ctx.hide_input_in_errors)

def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Serializer, ...]):
def __init__(
self,
model_name: str,
computed: bool,
inner_serializers: Tuple[Serializer, ...],
hide_input_in_errors: bool,
):
self._model_name = model_name
self._computed = computed
self._inner_serializers = inner_serializers
self._hide_input_in_errors = hide_input_in_errors

def serialize(
self,
Expand Down Expand Up @@ -74,7 +81,11 @@ def deserialize(
item_errors[idx] = err

if item_errors:
raise utils.into_validation_error(title=self._model_name, errors_map=item_errors)
raise utils.into_validation_error(
title=self._model_name,
errors_map=item_errors,
hide_input=self._hide_input_in_errors,
)

if all((value is None for value in result)):
return None
Expand Down
11 changes: 8 additions & 3 deletions pydantic_xml/serializers/factories/homogeneous.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ def from_core_schema(cls, schema: HomogeneousCollectionTypeSchema, ctx: Serializ

inner_serializer = Serializer.parse_core_schema(items_schema, ctx)

return cls(model_name, computed, inner_serializer)
return cls(model_name, computed, inner_serializer, ctx.hide_input_in_errors)

def __init__(self, model_name: str, computed: bool, inner_serializer: Serializer):
def __init__(self, model_name: str, computed: bool, inner_serializer: Serializer, hide_input_in_errors: bool):
self._model_name = model_name
self._computed = computed
self._inner_serializer = inner_serializer
self._hide_input_in_errors = hide_input_in_errors

def serialize(
self,
Expand Down Expand Up @@ -91,7 +92,11 @@ def deserialize(
result.append(value)

if item_errors:
raise utils.into_validation_error(title=self._model_name, errors_map=item_errors)
raise utils.into_validation_error(
title=self._model_name,
errors_map=item_errors,
hide_input=self._hide_input_in_errors,
)

return result or None

Expand Down
42 changes: 33 additions & 9 deletions pydantic_xml/serializers/factories/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def element_name(self) -> str: ...
def nsmap(self) -> Optional[NsMap]: ...

@classmethod
def _check_extra(cls, error_title: str, element: XmlElementReader) -> None:
def _check_extra(cls, error_title: str, element: XmlElementReader, hide_input_in_errors: bool) -> None:
line_errors: List[pdc.InitErrorDetails] = []

for path, attr, value in element.get_unbound():
Expand All @@ -49,7 +49,11 @@ def _check_extra(cls, error_title: str, element: XmlElementReader) -> None:
)

if line_errors:
raise pd.ValidationError.from_exception_data(title=error_title, line_errors=line_errors)
raise pd.ValidationError.from_exception_data(
title=error_title,
line_errors=line_errors,
hide_input=hide_input_in_errors,
)


class ModelSerializer(BaseModelSerializer):
Expand All @@ -65,6 +69,8 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
assert fields_schema['type'] == 'model-fields', f"unexpected schema type: {fields_schema['type']}"
fields_schema = typing.cast(pcs.ModelFieldsSchema, fields_schema)

hide_input_in_errors = model_cls.model_config.get('hide_input_in_errors', ctx.hide_input_in_errors)

entity_info: Optional[XmlEntityInfoP]
fields_serialization_exclude: Set[str] = set()
fields_validation_aliases: Dict[str, str] = {}
Expand All @@ -83,6 +89,7 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
field_name=field_name,
field_alias=field_alias,
entity_info=extract_field_xml_entity_info(field_info),
hide_input_in_errors=hide_input_in_errors,
)
fields_serializers[field_name] = Serializer.parse_core_schema(model_field['schema'], field_ctx)

Expand All @@ -101,6 +108,7 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
field_alias=field_alias,
field_computed=True,
entity_info=entity_info,
hide_input_in_errors=hide_input_in_errors,
)
fields_serializers[field_name] = Serializer.parse_core_schema(model_field['return_schema'], field_ctx)

Expand All @@ -111,6 +119,7 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
return cls(
model_cls, name, ns, nsmap,
fields_serializers, fields_validation_aliases, fields_serialization_exclude,
hide_input_in_errors,
)

def __init__(
Expand All @@ -122,6 +131,7 @@ def __init__(
field_serializers: Dict[str, Serializer],
fields_validation_aliases: Dict[str, str],
fields_serialization_exclude: Set[str],
hide_input_in_errors: bool,
):

self._model = model
Expand All @@ -130,6 +140,7 @@ def __init__(
self._nsmap = nsmap
self._fields_validation_aliases = fields_validation_aliases
self._fields_serialization_exclude = fields_serialization_exclude
self._hide_input_in_errors = hide_input_in_errors

@property
def model(self) -> Type['pxml.BaseXmlModel']:
Expand Down Expand Up @@ -210,15 +221,19 @@ def deserialize(
field_errors[field_name] = err

if field_errors:
raise utils.into_validation_error(title=self._model.__name__, errors_map=field_errors)
raise utils.into_validation_error(
title=self._model.__name__,
errors_map=field_errors,
hide_input=self._hide_input_in_errors,
)

if self._model.model_config.get('extra', 'ignore') == 'forbid':
self._check_extra(self._model.__name__, element)
self._check_extra(self._model.__name__, element, self._hide_input_in_errors)

try:
return self._model.model_validate(result, strict=False, context=context)
except pd.ValidationError as err:
raise utils.set_validation_error_sourceline(err, sourcemap)
raise utils.set_validation_error_sourceline(err, sourcemap, hide_input=self._hide_input_in_errors)


class RootModelSerializer(BaseModelSerializer):
Expand All @@ -229,18 +244,21 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '

assert issubclass(model_cls, pxml.BaseXmlModel), "model class must be a BaseXmlModel subclass"

hide_input_in_errors = model_cls.model_config.get('hide_input_in_errors', ctx.hide_input_in_errors)

field_info = model_cls.model_fields['root']
field_ctx = ctx.child(
field_name=None,
entity_info=extract_field_xml_entity_info(field_info),
hide_input_in_errors=hide_input_in_errors,
)
root_serializer = Serializer.parse_core_schema(root_schema, field_ctx)

name = model_cls.__xml_tag__ or model_cls.__name__
ns = model_cls.__xml_ns__
nsmap = model_cls.__xml_nsmap__

return cls(model_cls, name, ns, nsmap, root_serializer)
return cls(model_cls, name, ns, nsmap, root_serializer, hide_input_in_errors)

def __init__(
self,
Expand All @@ -249,12 +267,14 @@ def __init__(
ns: Optional[str],
nsmap: Optional[NsMap],
root_serializer: Serializer,
hide_input_in_errors: bool,
):

self._model = model
self._root_serializer = root_serializer
self._element_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap).uri
self._nsmap = nsmap
self._hide_input_in_errors = hide_input_in_errors

@property
def model(self) -> Type['pxml.BaseXmlModel']:
Expand Down Expand Up @@ -312,15 +332,19 @@ def deserialize(
if result is None:
result = pdc.PydanticUndefined
except pd.ValidationError as err:
raise utils.into_validation_error(title=self._model.__name__, errors_map={None: err})
raise utils.into_validation_error(
title=self._model.__name__,
errors_map={None: err},
hide_input=self._hide_input_in_errors,
)

if self._model.model_config.get('extra', 'ignore') == 'forbid':
self._check_extra(self._model.__name__, element)
self._check_extra(self._model.__name__, element, self._hide_input_in_errors)

try:
return self._model.model_validate(result, strict=False, context=context)
except pd.ValidationError as err:
raise utils.set_validation_error_sourceline(err, sourcemap)
raise utils.set_validation_error_sourceline(err, sourcemap, hide_input=self._hide_input_in_errors)


class ModelProxySerializer(BaseModelSerializer):
Expand Down
14 changes: 11 additions & 3 deletions pydantic_xml/serializers/factories/named_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ def from_core_schema(cls, schema: pcs.ArgumentsSchema, ctx: Serializer.Context)
param_schema = argument_schema['schema']
inner_serializers.append(Serializer.parse_core_schema(param_schema, ctx))

return cls(model_name, computed, tuple(inner_serializers))
return cls(model_name, computed, tuple(inner_serializers), ctx.hide_input_in_errors)

def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Serializer, ...]):
self._inner_serializer = heterogeneous.ElementSerializer(model_name, computed, inner_serializers)
def __init__(
self,
model_name: str,
computed: bool,
inner_serializers: Tuple[Serializer, ...],
hide_input_in_errors: bool,
):
self._inner_serializer = heterogeneous.ElementSerializer(
model_name, computed, inner_serializers, hide_input_in_errors,
)

def serialize(
self,
Expand Down
15 changes: 12 additions & 3 deletions pydantic_xml/serializers/factories/union.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,19 @@ def from_core_schema(cls, schema: pcs.UnionSchema, ctx: Serializer.Context) -> '

assert len(inner_serializers) > 0, "union choice is not provided"

return cls(model_name, computed, tuple(inner_serializers))
return cls(model_name, computed, tuple(inner_serializers), ctx.hide_input_in_errors)

def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[ModelProxySerializer, ...]):
def __init__(
self,
model_name: str,
computed: bool,
inner_serializers: Tuple[ModelProxySerializer, ...],
hide_input_in_errors: bool,
):
self._model_name = model_name
self._computed = computed
self._inner_serializers = inner_serializers
self._hide_input_in_errors = hide_input_in_errors

def serialize(
self,
Expand Down Expand Up @@ -136,7 +143,9 @@ def deserialize(

if union_errors:
element.step_forward()
raise utils.into_validation_error(title=self._model_name, errors_map=union_errors)
raise utils.into_validation_error(
title=self._model_name, errors_map=union_errors, hide_input=self._hide_input_in_errors,
)

return result

Expand Down
2 changes: 2 additions & 0 deletions pydantic_xml/serializers/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class Context:
has_default: bool = False
definitions: Dict[str, pcs.CoreSchema] = dc.field(default_factory=dict)

hide_input_in_errors: bool = False

parent_ctx: Optional['Serializer.Context'] = None

@property
Expand Down
9 changes: 8 additions & 1 deletion pydantic_xml/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def select_ns(*nss: Optional[str]) -> Optional[str]:
def into_validation_error(
title: str,
errors_map: Dict[Union[None, str, int], pd.ValidationError],
hide_input: bool,
) -> pd.ValidationError:
line_errors: List[pdc.InitErrorDetails] = []
for location in list(errors_map):
Expand All @@ -125,10 +126,15 @@ def into_validation_error(
title=title,
input_type='json',
line_errors=line_errors,
hide_input=hide_input,
)


def set_validation_error_sourceline(err: pd.ValidationError, sourcemap: Dict[Location, int]) -> pd.ValidationError:
def set_validation_error_sourceline(
err: pd.ValidationError,
sourcemap: Dict[Location, int],
hide_input: bool,
) -> pd.ValidationError:
line_errors: List[pdc.InitErrorDetails] = []
for error in err.errors():
loc, sourceline = error['loc'], -1
Expand All @@ -150,4 +156,5 @@ def set_validation_error_sourceline(err: pd.ValidationError, sourcemap: Dict[Loc
return pd.ValidationError.from_exception_data(
err.title,
line_errors=line_errors,
hide_input=hide_input,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-xml"
version = "2.19.0"
version = "2.20.0"
description = "pydantic xml extension"
authors = ["Dmitry Pershin <dapper1291@gmail.com>"]
license = "Unlicense"
Expand Down
Loading
Loading