From 6a73947f8499fa114532b19915a369b5d3db6358 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Sun, 12 Apr 2026 01:30:02 +0500 Subject: [PATCH 1/3] hide_input_in_errors model config flag support added. --- .../serializers/factories/heterogeneous.py | 17 ++++++-- .../serializers/factories/homogeneous.py | 11 +++-- pydantic_xml/serializers/factories/model.py | 42 +++++++++++++++---- .../serializers/factories/named_tuple.py | 14 +++++-- pydantic_xml/serializers/factories/union.py | 15 +++++-- pydantic_xml/serializers/serializer.py | 2 + pydantic_xml/utils.py | 9 +++- tests/test_misc.py | 29 +++++++++++++ 8 files changed, 117 insertions(+), 22 deletions(-) diff --git a/pydantic_xml/serializers/factories/heterogeneous.py b/pydantic_xml/serializers/factories/heterogeneous.py index 6c7241e..204dbd3 100644 --- a/pydantic_xml/serializers/factories/heterogeneous.py +++ b/pydantic_xml/serializers/factories/heterogeneous.py @@ -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, @@ -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 diff --git a/pydantic_xml/serializers/factories/homogeneous.py b/pydantic_xml/serializers/factories/homogeneous.py index 9855ef3..cba0efa 100644 --- a/pydantic_xml/serializers/factories/homogeneous.py +++ b/pydantic_xml/serializers/factories/homogeneous.py @@ -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, @@ -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 diff --git a/pydantic_xml/serializers/factories/model.py b/pydantic_xml/serializers/factories/model.py index 39fcf4a..dcb77df 100644 --- a/pydantic_xml/serializers/factories/model.py +++ b/pydantic_xml/serializers/factories/model.py @@ -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(): @@ -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): @@ -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] = {} @@ -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) @@ -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) @@ -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__( @@ -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 @@ -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']: @@ -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): @@ -229,10 +244,13 @@ 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) @@ -240,7 +258,7 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> ' 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, @@ -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']: @@ -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): diff --git a/pydantic_xml/serializers/factories/named_tuple.py b/pydantic_xml/serializers/factories/named_tuple.py index 27cbca0..ccf1562 100644 --- a/pydantic_xml/serializers/factories/named_tuple.py +++ b/pydantic_xml/serializers/factories/named_tuple.py @@ -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, diff --git a/pydantic_xml/serializers/factories/union.py b/pydantic_xml/serializers/factories/union.py index 248a9c0..41e3195 100644 --- a/pydantic_xml/serializers/factories/union.py +++ b/pydantic_xml/serializers/factories/union.py @@ -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, @@ -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 diff --git a/pydantic_xml/serializers/serializer.py b/pydantic_xml/serializers/serializer.py index 72a590d..ec00561 100644 --- a/pydantic_xml/serializers/serializer.py +++ b/pydantic_xml/serializers/serializer.py @@ -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 diff --git a/pydantic_xml/utils.py b/pydantic_xml/utils.py index f4ad9d4..bb6e2aa 100644 --- a/pydantic_xml/utils.py +++ b/pydantic_xml/utils.py @@ -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): @@ -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 @@ -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, ) diff --git a/tests/test_misc.py b/tests/test_misc.py index c97ad12..52b910c 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,3 +1,4 @@ +import re import sys from typing import Dict, List, Optional, Tuple, Union @@ -412,3 +413,31 @@ class TestModel(BaseXmlModel, tag="model"): hints = get_type_hints(TestModel) assert isinstance(hints, dict) + + +@pytest.mark.parametrize('model_hide_input, input_in_error_hidden', [(None, False), (False, False), (True, True)]) +def test_error_input_hiding(model_hide_input: bool, input_in_error_hidden: bool): + class TestSubModel(BaseXmlModel): + val: int + + class TestModel(BaseXmlModel, tag="model"): + if model_hide_input is not None: + model_config = pd.ConfigDict(hide_input_in_errors=model_hide_input) + + submodel: TestSubModel + + xml = ''' + + a + + ''' + + with pytest.raises(pd.ValidationError) as e: + TestModel.from_xml(xml) + + error_str = str(e.value) + error_value_input = re.search("input_value='a'", error_str) + if input_in_error_hidden: + assert not error_value_input, error_str + else: + assert error_value_input, error_str From 72b44b3e752d9611c0f86bed7ba20b6ca96e9bfd Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Sun, 12 Apr 2026 02:06:48 +0500 Subject: [PATCH 2/3] computed_attr and computed_element decorators typing fixed. --- pydantic_xml/fields.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pydantic_xml/fields.py b/pydantic_xml/fields.py index 8da86ae..3139662 100644 --- a/pydantic_xml/fields.py +++ b/pydantic_xml/fields.py @@ -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 @@ -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, *, @@ -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, *, From cd103e73fe0685a994c9f7c4242d89e199894c96 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Sun, 12 Apr 2026 02:18:04 +0500 Subject: [PATCH 3/3] bump version 2.20.0. --- CHANGELOG.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e053abf..a1111db 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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) ------------------- diff --git a/pyproject.toml b/pyproject.toml index bd889bb..f900a2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-xml" -version = "2.19.0" +version = "2.20.0" description = "pydantic xml extension" authors = ["Dmitry Pershin "] license = "Unlicense"