From cb69d1fec85dbd4c31a908537971f06a7aaab1cb Mon Sep 17 00:00:00 2001 From: Christodoulos Tsoulloftas Date: Fri, 10 May 2024 11:17:02 +0300 Subject: [PATCH] feat: Improve valdiation warnings --- .../dataclass/parsers/nodes/test_element.py | 2 +- .../dataclass/parsers/nodes/test_primitive.py | 36 ++++++------ .../dataclass/parsers/nodes/test_standard.py | 26 ++++++--- .../dataclass/parsers/nodes/test_union.py | 17 ++++-- tests/formats/dataclass/parsers/test_dict.py | 2 +- tests/formats/dataclass/parsers/test_node.py | 2 +- tests/formats/dataclass/parsers/test_utils.py | 16 ++++++ tests/formats/dataclass/parsers/test_xml.py | 6 +- tests/formats/test_converter.py | 20 +++---- xsdata/formats/converter.py | 36 ++++++------ xsdata/formats/dataclass/parsers/dict.py | 8 +-- xsdata/formats/dataclass/parsers/mixins.py | 2 + .../dataclass/parsers/nodes/element.py | 21 ++++--- .../dataclass/parsers/nodes/primitive.py | 21 +++---- .../dataclass/parsers/nodes/standard.py | 11 +++- .../formats/dataclass/parsers/nodes/union.py | 13 ++++- xsdata/formats/dataclass/parsers/utils.py | 56 ++++++++++++++++++- 17 files changed, 192 insertions(+), 103 deletions(-) diff --git a/tests/formats/dataclass/parsers/nodes/test_element.py b/tests/formats/dataclass/parsers/nodes/test_element.py index a1e5369ed..dde50d590 100644 --- a/tests/formats/dataclass/parsers/nodes/test_element.py +++ b/tests/formats/dataclass/parsers/nodes/test_element.py @@ -538,5 +538,5 @@ def test_build_node_with_primitive_var(self): self.assertIsInstance(actual, PrimitiveNode) self.assertEqual(ns_map, actual.ns_map) + self.assertEqual(self.meta, actual.meta) self.assertEqual(var, actual.var) - self.assertEqual(self.node.meta.mixed_content, actual.mixed) diff --git a/tests/formats/dataclass/parsers/nodes/test_primitive.py b/tests/formats/dataclass/parsers/nodes/test_primitive.py index 5e8f76d08..ffddb3336 100644 --- a/tests/formats/dataclass/parsers/nodes/test_primitive.py +++ b/tests/formats/dataclass/parsers/nodes/test_primitive.py @@ -1,33 +1,33 @@ from unittest import TestCase, mock +from tests.fixtures.artists import Artist from xsdata.exceptions import XmlContextError from xsdata.formats.dataclass.models.elements import XmlType from xsdata.formats.dataclass.parsers.nodes import PrimitiveNode from xsdata.formats.dataclass.parsers.utils import ParserUtils -from xsdata.utils.testing import XmlVarFactory +from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory class PrimitiveNodeTests(TestCase): - @mock.patch.object(ParserUtils, "parse_value") - def test_bind(self, mock_parse_value): - mock_parse_value.return_value = 13 + def setUp(self): + super().setUp() + self.meta = XmlMetaFactory.create(clazz=Artist) + + @mock.patch.object(ParserUtils, "parse_var") + def test_bind(self, mock_parse_var): + mock_parse_var.return_value = 13 var = XmlVarFactory.create( xml_type=XmlType.TEXT, name="foo", types=(int,), format="Nope" ) ns_map = {"foo": "bar"} - node = PrimitiveNode(var, ns_map, False) + node = PrimitiveNode(self.meta, var, ns_map) objects = [] self.assertTrue(node.bind("foo", "13", "Impossible", objects)) self.assertEqual(("foo", 13), objects[-1]) - mock_parse_value.assert_called_once_with( - value="13", - types=var.types, - default=var.default, - ns_map=ns_map, - tokens_factory=var.tokens_factory, - format=var.format, + mock_parse_var.assert_called_once_with( + meta=self.meta, var=var, value="13", ns_map=ns_map ) def test_bind_nillable_content(self): @@ -35,7 +35,7 @@ def test_bind_nillable_content(self): xml_type=XmlType.TEXT, name="foo", types=(str,), nillable=False ) ns_map = {"foo": "bar"} - node = PrimitiveNode(var, ns_map, False) + node = PrimitiveNode(self.meta, var, ns_map) objects = [] self.assertTrue(node.bind("foo", None, None, objects)) @@ -53,7 +53,7 @@ def test_bind_nillable_bytes_content(self): nillable=False, ) ns_map = {"foo": "bar"} - node = PrimitiveNode(var, ns_map, False) + node = PrimitiveNode(self.meta, var, ns_map) objects = [] self.assertTrue(node.bind("foo", None, None, objects)) @@ -64,8 +64,9 @@ def test_bind_nillable_bytes_content(self): self.assertIsNone(objects[-1][1]) def test_bind_mixed_with_tail_content(self): + self.meta.mixed_content = True var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(int,)) - node = PrimitiveNode(var, {}, True) + node = PrimitiveNode(self.meta, var, {}) objects = [] self.assertTrue(node.bind("foo", "13", "tail", objects)) @@ -73,8 +74,9 @@ def test_bind_mixed_with_tail_content(self): self.assertEqual(13, objects[-2][1]) def test_bind_mixed_without_tail_content(self): + self.meta.mixed_content = True var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(int,)) - node = PrimitiveNode(var, {}, True) + node = PrimitiveNode(self.meta, var, {}) objects = [] self.assertTrue(node.bind("foo", "13", "", objects)) @@ -82,7 +84,7 @@ def test_bind_mixed_without_tail_content(self): def test_child(self): var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo") - node = PrimitiveNode(var, {}, False) + node = PrimitiveNode(self.meta, var, {}) with self.assertRaises(XmlContextError): node.child("foo", {}, {}, 0) diff --git a/tests/formats/dataclass/parsers/nodes/test_standard.py b/tests/formats/dataclass/parsers/nodes/test_standard.py index 19a5e719c..4f00d3b90 100644 --- a/tests/formats/dataclass/parsers/nodes/test_standard.py +++ b/tests/formats/dataclass/parsers/nodes/test_standard.py @@ -1,39 +1,46 @@ from unittest import TestCase +from tests.fixtures.artists import Artist from xsdata.exceptions import XmlContextError from xsdata.formats.dataclass.models.generics import DerivedElement from xsdata.formats.dataclass.parsers.nodes import StandardNode from xsdata.models.enums import DataType +from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory class StandardNodeTests(TestCase): + def setUp(self): + super().setUp() + self.meta = XmlMetaFactory.create(clazz=Artist) + self.var = XmlVarFactory.create() + def test_bind_simple(self): - var = DataType.INT - node = StandardNode(var, {}, False, False) + datatype = DataType.INT + node = StandardNode(self.meta, self.var, datatype, {}, False, False) objects = [] self.assertTrue(node.bind("a", "13", None, objects)) self.assertEqual(("a", 13), objects[-1]) def test_bind_derived(self): - var = DataType.INT - node = StandardNode(var, {}, False, DerivedElement) + datatype = DataType.INT + node = StandardNode(self.meta, self.var, datatype, {}, False, DerivedElement) objects = [] self.assertTrue(node.bind("a", "13", None, objects)) self.assertEqual(("a", DerivedElement("a", 13)), objects[-1]) def test_bind_wrapper_type(self): - var = DataType.HEX_BINARY - node = StandardNode(var, {}, False, DerivedElement) + datatype = DataType.HEX_BINARY + node = StandardNode(self.meta, self.var, datatype, {}, False, DerivedElement) objects = [] self.assertTrue(node.bind("a", "13", None, objects)) self.assertEqual(("a", DerivedElement(qname="a", value=b"\x13")), objects[-1]) def test_bind_nillable(self): - var = DataType.STRING - node = StandardNode(var, {}, True, None) + datatype = DataType.STRING + node = StandardNode(self.meta, self.var, datatype, {}, True, None) objects = [] self.assertTrue(node.bind("a", None, None, objects)) @@ -44,7 +51,8 @@ def test_bind_nillable(self): self.assertEqual(("a", ""), objects[-1]) def test_child(self): - node = StandardNode(DataType.STRING, {}, False, False) + datatype = DataType.STRING + node = StandardNode(self.meta, self.var, datatype, {}, False, False) with self.assertRaises(XmlContextError): node.child("foo", {}, {}, 0) diff --git a/tests/formats/dataclass/parsers/nodes/test_union.py b/tests/formats/dataclass/parsers/nodes/test_union.py index 2bb8c2dfb..15a44b51b 100644 --- a/tests/formats/dataclass/parsers/nodes/test_union.py +++ b/tests/formats/dataclass/parsers/nodes/test_union.py @@ -2,6 +2,7 @@ from typing import Union from unittest import TestCase +from tests.fixtures.artists import Artist from tests.fixtures.models import UnionType from xsdata.exceptions import ParserError from xsdata.formats.dataclass.context import XmlContext @@ -9,7 +10,7 @@ from xsdata.formats.dataclass.parsers.config import ParserConfig from xsdata.formats.dataclass.parsers.nodes import UnionNode from xsdata.models.mixins import attribute -from xsdata.utils.testing import XmlVarFactory +from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory class UnionNodeTests(TestCase): @@ -22,10 +23,12 @@ def setUp(self) -> None: def test_child(self): attrs = {"id": "1"} ns_map = {"ns0": "xsdata"} + meta = XmlMetaFactory.create(clazz=Artist) var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo") node = UnionNode( - position=0, + meta=meta, var=var, + position=0, config=self.config, context=self.context, attrs={}, @@ -38,10 +41,12 @@ def test_child(self): self.assertIsNot(attrs, node.events[0][2]) def test_bind_appends_end_event_when_level_not_zero(self): + meta = XmlMetaFactory.create(clazz=Artist) var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo") node = UnionNode( - position=0, + meta=meta, var=var, + position=0, config=self.config, context=self.context, attrs={}, @@ -67,8 +72,9 @@ def test_bind_returns_best_matching_object(self): attrs = {"a": "1", "b": 2} ns_map = {} node = UnionNode( - position=0, + meta=meta, var=var, + position=0, config=self.config, context=self.context, attrs=attrs, @@ -102,8 +108,9 @@ def test_bind_raises_parser_error_on_failure(self): var = next(meta.find_children("element")) node = UnionNode( - position=0, + meta=meta, var=var, + position=0, config=self.config, context=self.context, attrs={}, diff --git a/tests/formats/dataclass/parsers/test_dict.py b/tests/formats/dataclass/parsers/test_dict.py index 598424775..e9b7c783e 100644 --- a/tests/formats/dataclass/parsers/test_dict.py +++ b/tests/formats/dataclass/parsers/test_dict.py @@ -102,7 +102,7 @@ def test_decode_with_fail_on_converter_warnings(self): self.decoder.decode(json_str, TypeA) self.assertEqual( - "Failed to convert value `foo` to one of (,)", + "Failed to convert value for `TypeA.x`\n `foo` is not a valid `int`", str(cm.exception), ) diff --git a/tests/formats/dataclass/parsers/test_node.py b/tests/formats/dataclass/parsers/test_node.py index 0b7452e1a..4497a70c7 100644 --- a/tests/formats/dataclass/parsers/test_node.py +++ b/tests/formats/dataclass/parsers/test_node.py @@ -47,7 +47,7 @@ def test_parse_with_fail_on_converter_warnings(self): parser.from_string(xml, TypeA) self.assertEqual( - "Failed to convert value `foo` to one of (,)", + "Failed to convert value for `TypeA.x`\n `foo` is not a valid `int`", str(cm.exception), ) diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py index 78abdf94d..81871bdb5 100644 --- a/tests/formats/dataclass/parsers/test_utils.py +++ b/tests/formats/dataclass/parsers/test_utils.py @@ -1,3 +1,4 @@ +import warnings from unittest import mock from tests.fixtures.models import TypeA @@ -114,3 +115,18 @@ def test_validate_fixed_value(self): var = XmlVarFactory.create("fixed", default=lambda: float("nan")) ParserUtils.validate_fixed_value(meta, var, float("nan")) + + def test_parse_var_with_warnings(self): + meta = XmlMetaFactory.create(clazz=TypeA, qname="foo") + var = XmlVarFactory.create("fixed", default="a") + + with warnings.catch_warnings(record=True) as w: + result = ParserUtils.parse_var(meta, var, "a", types=[int, float]) + + expected = ( + "Failed to convert value for `TypeA.fixed`\n" + " `a` is not a valid `int | float`" + ) + self.assertEqual("a", result) + + self.assertEqual(expected, str(w[-1].message)) diff --git a/tests/formats/dataclass/parsers/test_xml.py b/tests/formats/dataclass/parsers/test_xml.py index fa5af97b5..6667569e4 100644 --- a/tests/formats/dataclass/parsers/test_xml.py +++ b/tests/formats/dataclass/parsers/test_xml.py @@ -1,11 +1,12 @@ from unittest import mock +from tests.fixtures.artists import Artist from tests.fixtures.books import Books from xsdata.formats.dataclass.models.elements import XmlType from xsdata.formats.dataclass.parsers.nodes import PrimitiveNode, SkipNode from xsdata.formats.dataclass.parsers.xml import UserXmlParser from xsdata.models.enums import EventType -from xsdata.utils.testing import FactoryTestCase, XmlVarFactory +from xsdata.utils.testing import FactoryTestCase, XmlMetaFactory, XmlVarFactory class UserXmlParserTests(FactoryTestCase): @@ -30,8 +31,9 @@ def test_start(self, mock_emit_event): def test_end(self, mock_emit_event): objects = [] queue = [] + meta = XmlMetaFactory.create(clazz=Artist) var = XmlVarFactory.create(xml_type=XmlType.TEXT, name="foo", types=(bool,)) - queue.append(PrimitiveNode(var, {}, False)) + queue.append(PrimitiveNode(meta, var, {})) result = self.parser.end(queue, objects, "enabled", "true", None) self.assertTrue(result) diff --git a/tests/formats/test_converter.py b/tests/formats/test_converter.py index 493b18fce..a1d0d6feb 100644 --- a/tests/formats/test_converter.py +++ b/tests/formats/test_converter.py @@ -1,5 +1,4 @@ import sys -import warnings from datetime import date, datetime, time from decimal import Decimal from enum import Enum @@ -18,12 +17,10 @@ class ConverterFactoryTests(TestCase): def test_deserialize(self): - with warnings.catch_warnings(record=True) as w: - self.assertEqual("a", converter.deserialize("a", [int])) + with self.assertRaises(ConverterError) as cm: + converter.deserialize("a", [int]) - self.assertEqual( - "Failed to convert value `a` to one of []", str(w[-1].message) - ) + self.assertEqual("`a` is not a valid `int`", str(cm.exception)) self.assertFalse(converter.deserialize("false", [int, bool])) self.assertEqual(1, converter.deserialize("1", [int, bool])) @@ -73,13 +70,12 @@ def test_unknown_converter(self): class A: pass - class B(A): - pass - - with warnings.catch_warnings(record=True) as w: - converter.serialize(B()) + with self.assertRaises(ConverterError) as cm: + converter.serialize(A()) - self.assertEqual(f"No converter registered for `{B}`", str(w[-1].message)) + self.assertEqual( + f"No converter registered for `{A.__qualname__}`", str(cm.exception) + ) def test_register_converter(self): class MinusOneInt(int): diff --git a/xsdata/formats/converter.py b/xsdata/formats/converter.py index 25864a4ec..9da4a854f 100644 --- a/xsdata/formats/converter.py +++ b/xsdata/formats/converter.py @@ -2,7 +2,6 @@ import base64 import binascii import math -import warnings from datetime import date, datetime, time from decimal import Decimal, InvalidOperation from enum import Enum, EnumMeta @@ -20,7 +19,7 @@ ) from xml.etree.ElementTree import QName -from xsdata.exceptions import ConverterError, ConverterWarning +from xsdata.exceptions import ConverterError from xsdata.models.datatype import ( XmlBase64Binary, XmlDate, @@ -87,16 +86,16 @@ def __init__(self): def deserialize(self, value: Any, types: Sequence[Type], **kwargs: Any) -> Any: """Attempt to convert any value to one of the given types. - If all attempts fail return the value input value and emit a - warning. - Args: value: The input value types: The target candidate types **kwargs: Additional keyword arguments needed per converter + Raises: + ConverterError: if the value can't be converted to any of the given types. + Returns: - The converted value or the input value. + The converted value """ for data_type in types: try: @@ -105,10 +104,8 @@ def deserialize(self, value: Any, types: Sequence[Type], **kwargs: Any) -> Any: except ConverterError: pass - warnings.warn( - f"Failed to convert value `{value}` to one of {types}", ConverterWarning - ) - return value + type_names = " | ".join(tp.__name__ for tp in types) + raise ConverterError(f"`{value}` is not a valid `{type_names}`") def serialize(self, value: Any, **kwargs: Any) -> Any: """Convert the given value to string. @@ -153,10 +150,9 @@ def test( if not isinstance(value, str): return False - with warnings.catch_warnings(record=True) as w: + try: decoded = self.deserialize(value, types, **kwargs) - - if w and w[-1].category is ConverterWarning: + except ConverterError: return False if strict and isinstance(decoded, (float, int, Decimal, XmlPeriod)): @@ -197,12 +193,14 @@ def type_converter(self, data_type: Type) -> Converter: """Find a suitable converter for given data type. Iterate over all but last mro items and check for registered - converters, fall back to str and issue a warning if there are - no matches. + converters. Args: data_type: The data type + Raises: + ConverterError: if the data type is not registered. + Returns: A converter instance """ @@ -217,8 +215,7 @@ def type_converter(self, data_type: Type) -> Converter: if mro in self.registry: return self.registry[mro] - warnings.warn(f"No converter registered for `{data_type}`", ConverterWarning) - return self.registry[str] + raise ConverterError(f"No converter registered for `{data_type.__qualname__}`") def value_converter(self, value: Any) -> Converter: """Get a suitable converter for the given value.""" @@ -698,9 +695,10 @@ def _match_list(cls, raw: Sequence, real: Sequence, **kwargs: Any) -> bool: @classmethod def _match_atomic(cls, raw: Any, real: Any, **kwargs: Any) -> bool: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + try: cmp = converter.deserialize(raw, [type(real)], **kwargs) + except ConverterError: + cmp = raw if isinstance(real, float): return cmp == real or repr(cmp) == repr(real) diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py index 9e469cdb6..d225c90fa 100644 --- a/xsdata/formats/dataclass/parsers/dict.py +++ b/xsdata/formats/dataclass/parsers/dict.py @@ -325,13 +325,11 @@ def bind_text(self, meta: XmlMeta, var: XmlVar, value: Any) -> Any: value = converter.serialize(value) # Convert value according to the field types - return ParserUtils.parse_value( + return ParserUtils.parse_var( + meta=meta, + var=var, value=value, - types=var.types, - default=var.default, ns_map=EMPTY_MAP, - tokens_factory=var.tokens_factory, - format=var.format, ) def bind_complex_type(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any: diff --git a/xsdata/formats/dataclass/parsers/mixins.py b/xsdata/formats/dataclass/parsers/mixins.py index 9599aa738..3c7399578 100644 --- a/xsdata/formats/dataclass/parsers/mixins.py +++ b/xsdata/formats/dataclass/parsers/mixins.py @@ -174,6 +174,8 @@ class XmlNode(abc.ABC): and a list of all the intermediate objects. """ + __slots__ = () + @abc.abstractmethod def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> "XmlNode": """Initialize the next child node to be queued, when an element starts. diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py index 4b9a1f05d..5cf7489e3 100644 --- a/xsdata/formats/dataclass/parsers/nodes/element.py +++ b/xsdata/formats/dataclass/parsers/nodes/element.py @@ -192,13 +192,11 @@ def bind_attr(self, params: Dict, var: XmlVar, value: Any): var: The xml var instance value: The attribute value """ - value = ParserUtils.parse_value( + value = ParserUtils.parse_var( + meta=self.meta, + var=var, value=value, - types=var.types, - default=var.default, ns_map=self.ns_map, - tokens_factory=var.tokens_factory, - format=var.format, ) if var.init: @@ -372,13 +370,11 @@ def bind_text(self, params: Dict, text: Optional[str]) -> bool: if self.xsi_nil and not text: value = None else: - value = ParserUtils.parse_value( + value = ParserUtils.parse_var( + meta=self.meta, + var=var, value=text, - types=var.types, - default=var.default, ns_map=self.ns_map, - tokens_factory=var.tokens_factory, - format=var.format, ) if var.init: @@ -496,6 +492,7 @@ def build_node( """ if var.is_clazz_union: return nodes.UnionNode( + meta=self.meta, var=var, attrs=attrs, ns_map=ns_map, @@ -522,12 +519,14 @@ def build_node( ) if not var.any_type and not var.is_wildcard: - return nodes.PrimitiveNode(var, ns_map, self.meta.mixed_content) + return nodes.PrimitiveNode(self.meta, var, ns_map) datatype = DataType.from_qname(xsi_type) if xsi_type else None derived = var.is_wildcard if datatype: return nodes.StandardNode( + self.meta, + var, datatype, ns_map, var.nillable, diff --git a/xsdata/formats/dataclass/parsers/nodes/primitive.py b/xsdata/formats/dataclass/parsers/nodes/primitive.py index 8cbcaef8c..8537eb77a 100644 --- a/xsdata/formats/dataclass/parsers/nodes/primitive.py +++ b/xsdata/formats/dataclass/parsers/nodes/primitive.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional from xsdata.exceptions import XmlContextError -from xsdata.formats.dataclass.models.elements import XmlVar +from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar from xsdata.formats.dataclass.parsers.mixins import XmlNode from xsdata.formats.dataclass.parsers.utils import ParserUtils @@ -10,17 +10,17 @@ class PrimitiveNode(XmlNode): """XmlNode for text elements with simple type values. Args: + meta: The parent xml meta instance var: The xml var instance ns_map: The element namespace prefix-URI map - mixed: Specifies if this node supports mixed content """ - __slots__ = "var", "ns_map" + __slots__ = "meta", "var", "ns_map" - def __init__(self, var: XmlVar, ns_map: Dict, mixed: bool): + def __init__(self, meta: XmlMeta, var: XmlVar, ns_map: Dict): + self.meta = meta self.var = var self.ns_map = ns_map - self.mixed = mixed def bind( self, @@ -44,13 +44,8 @@ def bind( Returns: Whether the binding process was successful or not. """ - obj = ParserUtils.parse_value( - value=text, - types=self.var.types, - default=self.var.default, - ns_map=self.ns_map, - tokens_factory=self.var.tokens_factory, - format=self.var.format, + obj = ParserUtils.parse_var( + meta=self.meta, var=self.var, value=text, ns_map=self.ns_map ) if obj is None and not self.var.nillable: @@ -58,7 +53,7 @@ def bind( objects.append((qname, obj)) - if self.mixed: + if self.meta.mixed_content: tail = ParserUtils.normalize_content(tail) if tail: objects.append((None, tail)) diff --git a/xsdata/formats/dataclass/parsers/nodes/standard.py b/xsdata/formats/dataclass/parsers/nodes/standard.py index 362d1dcde..297444ab3 100644 --- a/xsdata/formats/dataclass/parsers/nodes/standard.py +++ b/xsdata/formats/dataclass/parsers/nodes/standard.py @@ -1,6 +1,7 @@ from typing import Dict, List, Optional, Type from xsdata.exceptions import XmlContextError +from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar from xsdata.formats.dataclass.parsers.mixins import XmlNode from xsdata.formats.dataclass.parsers.utils import ParserUtils from xsdata.models.enums import DataType @@ -16,15 +17,19 @@ class StandardNode(XmlNode): derived_factory: The derived element factory """ - __slots__ = "datatype", "ns_map", "nillable", "derived_factory" + __slots__ = "meta", "var", "datatype", "ns_map", "nillable", "derived_factory" def __init__( self, + meta: XmlMeta, + var: XmlVar, datatype: DataType, ns_map: Dict, nillable: bool, derived_factory: Optional[Type], ): + self.meta = meta + self.var = var self.datatype = datatype self.ns_map = ns_map self.nillable = nillable @@ -52,7 +57,9 @@ def bind( Always true, it's not possible to fail during parsing for this node. """ - obj = ParserUtils.parse_value( + obj = ParserUtils.parse_var( + meta=self.meta, + var=self.var, value=text, types=[self.datatype.type], ns_map=self.ns_map, diff --git a/xsdata/formats/dataclass/parsers/nodes/union.py b/xsdata/formats/dataclass/parsers/nodes/union.py index 5d42b4537..fa842df9e 100644 --- a/xsdata/formats/dataclass/parsers/nodes/union.py +++ b/xsdata/formats/dataclass/parsers/nodes/union.py @@ -4,7 +4,7 @@ from xsdata.exceptions import ConverterWarning, ParserError from xsdata.formats.dataclass.context import XmlContext -from xsdata.formats.dataclass.models.elements import XmlVar +from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar from xsdata.formats.dataclass.parsers.bases import NodeParser from xsdata.formats.dataclass.parsers.config import ParserConfig from xsdata.formats.dataclass.parsers.mixins import EventsHandler, XmlNode @@ -31,6 +31,7 @@ class UnionNode(XmlNode): """ __slots__ = ( + "meta", "var", "attrs", "ns_map", @@ -43,6 +44,7 @@ class UnionNode(XmlNode): def __init__( self, + meta: XmlMeta, var: XmlVar, attrs: Dict, ns_map: Dict, @@ -50,6 +52,7 @@ def __init__( config: ParserConfig, context: XmlContext, ): + self.meta = meta self.var = var self.attrs = attrs self.ns_map = ns_map @@ -169,8 +172,12 @@ def parse_value(self, value: Any, types: List[Type]) -> Any: try: with warnings.catch_warnings(): warnings.filterwarnings("error", category=ConverterWarning) - return ParserUtils.parse_value( - value=value, types=types, ns_map=self.ns_map + return ParserUtils.parse_var( + meta=self.meta, + var=self.var, + value=value, + types=types, + ns_map=self.ns_map, ) except Exception: return None diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py index 305e49a6f..bac892c91 100644 --- a/xsdata/formats/dataclass/parsers/utils.py +++ b/xsdata/formats/dataclass/parsers/utils.py @@ -1,8 +1,9 @@ import math +import warnings from collections import UserList -from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Type +from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Type, Union -from xsdata.exceptions import ParserError +from xsdata.exceptions import ConverterError, ConverterWarning, ParserError from xsdata.formats.converter import QNameConverter, converter from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar from xsdata.models.enums import QNames @@ -10,6 +11,12 @@ from xsdata.utils.namespaces import build_qname +class _MissingType: ... + + +MISSING = _MissingType() + + class PendingCollection(UserList): """An iterable implementation of parsed values. @@ -71,6 +78,51 @@ def xsi_nil(cls, attrs: Dict) -> Optional[bool]: xsi_nil = attrs.get(QNames.XSI_NIL) return xsi_nil == constants.XML_TRUE if xsi_nil else None + @classmethod + def parse_var( + cls, + meta: XmlMeta, + var: XmlVar, + value: Any, + ns_map: Optional[Dict] = None, + default: Any = None, + types: Optional[Sequence[Type]] = None, + tokens_factory: Optional[Callable] = None, + format: Optional[str] = None, + ) -> Any: + """Convert a value to a python primitive type. + + Args: + meta: The xml meta instance + var: The xml var instance + value: A primitive value or a list of primitive values + ns_map: The element namespace prefix-URI map + default: Override the var default value + types: Override the var types + tokens_factory: Override the var tokens factory + format: Override the var format + + Returns: + The converted value or values. + """ + try: + value = cls.parse_value( + value=value, + types=types or var.types, + default=default or var.default, + ns_map=ns_map, + tokens_factory=tokens_factory or var.tokens_factory, + format=format or var.format, + ) + except ConverterError as ex: + message = f" {str(ex)}" + warnings.warn( + f"Failed to convert value for `{meta.clazz.__qualname__}.{var.name}`\n{message}", + ConverterWarning, + ) + + return value + @classmethod def parse_value( cls,