diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5be09d21..750e1c71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.1 + rev: v0.15.5 hooks: - id: ruff-check args: [ --fix, --show-fixes] diff --git a/tests/codegen/handlers/test_sanitize_attributes_default_value.py b/tests/codegen/handlers/test_sanitize_attributes_default_value.py index 934a5ddd..ece1d6c1 100644 --- a/tests/codegen/handlers/test_sanitize_attributes_default_value.py +++ b/tests/codegen/handlers/test_sanitize_attributes_default_value.py @@ -98,7 +98,7 @@ def test_process_attribute( attr = AttrFactory.extension() self.processor.process_attribute(target, attr) - self.assertEqual("", attr.default) + self.assertIsNone(attr.default) attr = AttrFactory.extension(default="abc") self.processor.process_attribute(target, attr) @@ -112,7 +112,7 @@ def test_process_attribute( types=[AttrTypeFactory.native(DataType.BASE64_BINARY)] ) self.processor.process_attribute(target, attr) - self.assertEqual("", attr.default) + self.assertIsNone(attr.default) def test_should_reset_required(self) -> None: attr = AttrFactory.create() diff --git a/tests/fixtures/artists/metadata.py b/tests/fixtures/artists/metadata.py index 954669c7..9a84f370 100644 --- a/tests/fixtures/artists/metadata.py +++ b/tests/fixtures/artists/metadata.py @@ -45,7 +45,7 @@ class Meta: "type": "Attribute", }, ) - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) @@ -83,7 +83,7 @@ class Meta: "type": "Attribute", } ) - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) diff --git a/tests/fixtures/dtd/models/complete_example.py b/tests/fixtures/dtd/models/complete_example.py index 768d42d5..cb7265d3 100644 --- a/tests/fixtures/dtd/models/complete_example.py +++ b/tests/fixtures/dtd/models/complete_example.py @@ -6,12 +6,12 @@ @dataclass(kw_only=True) class Body: - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) class Origin: - value: str = field(default="") + value: str = field() class PostStatus(Enum): @@ -21,17 +21,17 @@ class PostStatus(Enum): @dataclass(kw_only=True) class Source: - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) class Tag: - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) class Title: - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) diff --git a/tests/fixtures/primer/order.py b/tests/fixtures/primer/order.py index a0f245ba..d1326795 100644 --- a/tests/fixtures/primer/order.py +++ b/tests/fixtures/primer/order.py @@ -55,7 +55,7 @@ class Comment: class Meta: name = "comment" - value: str = field(default="") + value: str = field() @dataclass(kw_only=True) diff --git a/tests/fixtures/wrapper/models.py b/tests/fixtures/wrapper/models.py index 6c67cd7b..2593cea6 100644 --- a/tests/fixtures/wrapper/models.py +++ b/tests/fixtures/wrapper/models.py @@ -39,7 +39,7 @@ class Meta: name = "charlie" namespace = "xsdata" - value: str = field(default="") + value: str = field() lang: None | object = field( default=None, metadata={ diff --git a/tests/formats/dataclass/parsers/nodes/test_primitive.py b/tests/formats/dataclass/parsers/nodes/test_primitive.py index dd44a7aa..f3e39551 100644 --- a/tests/formats/dataclass/parsers/nodes/test_primitive.py +++ b/tests/formats/dataclass/parsers/nodes/test_primitive.py @@ -1,11 +1,10 @@ -from unittest import TestCase, mock +from unittest import TestCase from tests.fixtures.artists import Artist from xsdata.exceptions import XmlContextError from xsdata.formats.dataclass.models.elements import XmlType from xsdata.formats.dataclass.parsers.config import ParserConfig from xsdata.formats.dataclass.parsers.nodes import PrimitiveNode -from xsdata.formats.dataclass.parsers.utils import ParserUtils from xsdata.utils.testing import XmlMetaFactory, XmlVarFactory @@ -15,9 +14,7 @@ def setUp(self) -> None: self.meta = XmlMetaFactory.create(clazz=Artist) self.config = ParserConfig() - @mock.patch.object(ParserUtils, "parse_var") - def test_bind(self, mock_parse_var) -> None: - mock_parse_var.return_value = 13 + def test_bind(self) -> None: var = XmlVarFactory.create( xml_type=XmlType.TEXT, name="foo", types=(int,), format="Nope" ) @@ -28,10 +25,6 @@ def test_bind(self, mock_parse_var) -> None: self.assertTrue(node.bind("foo", "13", "Impossible", objects)) self.assertEqual(("foo", 13), objects[-1]) - mock_parse_var.assert_called_once_with( - meta=self.meta, var=var, config=self.config, value="13", ns_map=ns_map - ) - def test_bind_nillable_content(self) -> None: var = XmlVarFactory.create( xml_type=XmlType.TEXT, name="foo", types=(str,), nillable=False @@ -53,6 +46,7 @@ def test_bind_nillable_bytes_content(self) -> None: name="foo", types=(bytes,), nillable=False, + format="base64", ) ns_map = {"foo": "bar"} node = PrimitiveNode(self.meta, var, ns_map, self.config) diff --git a/tests/formats/dataclass/parsers/test_dict.py b/tests/formats/dataclass/parsers/test_dict.py index 0aa2d043..c11f45c2 100644 --- a/tests/formats/dataclass/parsers/test_dict.py +++ b/tests/formats/dataclass/parsers/test_dict.py @@ -134,6 +134,10 @@ def test_decode_wrapper(self) -> None: ) self.assertEqual(expected, actual) + def test_bind_dataclass_missing_text_field(self) -> None: + result = self.decoder.decode({"lang": "en"}, Charlie) + self.assertEqual(Charlie(value="", lang="en"), result) + def test_verify_type(self) -> None: invalid_cases = [ ( diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py index ad563854..94e8511d 100644 --- a/tests/formats/dataclass/parsers/test_utils.py +++ b/tests/formats/dataclass/parsers/test_utils.py @@ -120,6 +120,19 @@ def test_validate_fixed_value(self) -> None: var = XmlVarFactory.create("fixed", default=lambda: ProcessType.LAX) ParserUtils.validate_fixed_value(meta, var, "lax") + def test_parse_text_var(self) -> None: + meta = XmlMetaFactory.create(clazz=TypeA, qname="foo") + config = ParserConfig() + + var = XmlVarFactory.create("text", types=(str,)) + self.assertEqual( + "hello", ParserUtils.parse_text_var(meta, var, config, "hello") + ) + self.assertEqual("", ParserUtils.parse_text_var(meta, var, config, None)) + + var = XmlVarFactory.create("text", types=(bytes,), format="base64") + self.assertEqual(b"", ParserUtils.parse_text_var(meta, var, config, None)) + def test_parse_var_with_error(self) -> None: meta = XmlMetaFactory.create(clazz=TypeA, qname="foo") var = XmlVarFactory.create("fixed", default="a") diff --git a/xsdata/codegen/handlers/sanitize_attributes_default_value.py b/xsdata/codegen/handlers/sanitize_attributes_default_value.py index fdbf1958..ec279511 100644 --- a/xsdata/codegen/handlers/sanitize_attributes_default_value.py +++ b/xsdata/codegen/handlers/sanitize_attributes_default_value.py @@ -39,7 +39,6 @@ def process_attribute(self, target: Class, attr: Attr) -> None: - Reset min_occurs - Reset default value - Validate default value against types - - Set empty string as default value for string text nodes. Args: target: The target class instance @@ -54,12 +53,6 @@ def process_attribute(self, target: Class, attr: Attr) -> None: if attr.default is not None: self.process_types(target, attr) - elif attr.xml_type is None: - # Text nodes get an empty default value - if str in attr.native_types: - attr.default = "" - elif bytes in attr.native_types: - attr.default = "" # Will be converted to b"" during codegen def process_types(self, target: Class, attr: Attr) -> None: """Reset attr types if default value doesn't pass validation. diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py index 0ae7ce67..ed008cfe 100644 --- a/xsdata/formats/dataclass/parsers/dict.py +++ b/xsdata/formats/dataclass/parsers/dict.py @@ -120,6 +120,9 @@ def bind_dataclass(self, data: dict, clazz: type[T]) -> T: meta = self.context.build(clazz) xml_vars = meta.get_all_vars() + if meta.text and meta.text.local_name not in data: + data[meta.text.local_name] = "" + params = {} for key, value in data.items(): var = self.find_var(xml_vars, key, value) @@ -296,6 +299,9 @@ def bind_text(self, meta: XmlMeta, var: XmlVar, value: Any) -> Any: # field can support any object return the value as it is return value + if value is None: + return None + value = converter.serialize(value) # Convert value according to the field types diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py index 01de1383..e5b3666d 100644 --- a/xsdata/formats/dataclass/parsers/nodes/element.py +++ b/xsdata/formats/dataclass/parsers/nodes/element.py @@ -365,17 +365,17 @@ def bind_text(self, params: dict, text: str | None) -> bool: """ var = self.meta.text - if not var or (text is None and not self.xsi_nil): + if not var: return False - if self.xsi_nil and not text: + if self.xsi_nil and text is None: value = None else: - value = ParserUtils.parse_var( + value = ParserUtils.parse_text_var( meta=self.meta, var=var, config=self.config, - value=text, + text=text, ns_map=self.ns_map, ) diff --git a/xsdata/formats/dataclass/parsers/nodes/primitive.py b/xsdata/formats/dataclass/parsers/nodes/primitive.py index 1c4f5a18..44ddd3ff 100644 --- a/xsdata/formats/dataclass/parsers/nodes/primitive.py +++ b/xsdata/formats/dataclass/parsers/nodes/primitive.py @@ -46,16 +46,16 @@ def bind( Returns: Whether the binding process was successful or not. """ - obj = ParserUtils.parse_var( - meta=self.meta, - var=self.var, - config=self.config, - value=text, - ns_map=self.ns_map, - ) - - if obj is None and not self.var.nillable: - obj = b"" if bytes in self.var.types else "" + if text is None and self.var.nillable: + obj = None + else: + obj = ParserUtils.parse_text_var( + meta=self.meta, + var=self.var, + config=self.config, + text=text, + ns_map=self.ns_map, + ) objects.append((qname, obj)) diff --git a/xsdata/formats/dataclass/parsers/nodes/standard.py b/xsdata/formats/dataclass/parsers/nodes/standard.py index 630d7595..01354739 100644 --- a/xsdata/formats/dataclass/parsers/nodes/standard.py +++ b/xsdata/formats/dataclass/parsers/nodes/standard.py @@ -70,18 +70,19 @@ def bind( Always true, it's not possible to fail during parsing for this node. """ - obj = ParserUtils.parse_var( - meta=self.meta, - var=self.var, - config=self.config, - value=text, - types=[self.datatype.type], - ns_map=self.ns_map, - format=self.datatype.format, - ) - if obj is None and not self.nillable: - obj = "" + if text is None and self.nillable: + obj = None + else: + obj = ParserUtils.parse_text_var( + meta=self.meta, + var=self.var, + config=self.config, + text=text, + types=[self.datatype.type], + ns_map=self.ns_map, + format=self.datatype.format, + ) if self.datatype.wrapper: obj = self.datatype.wrapper(obj) diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py index 366054bd..67b7c279 100644 --- a/xsdata/formats/dataclass/parsers/utils.py +++ b/xsdata/formats/dataclass/parsers/utils.py @@ -89,9 +89,7 @@ def parse_var( config: ParserConfig, value: Any, ns_map: dict | None = None, - default: Any = None, types: Sequence[type] | None = None, - tokens_factory: Callable | None = None, format: str | None = None, ) -> Any: """Convert a value to a python primitive type. @@ -102,9 +100,7 @@ def parse_var( config: The parser config 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: @@ -114,9 +110,9 @@ def parse_var( value = cls.parse_value( value=value, types=types or var.types, - default=default or var.default, + default=var.default, ns_map=ns_map, - tokens_factory=tokens_factory or var.tokens_factory, + tokens_factory=var.tokens_factory, format=format or var.format, ) except ConverterError as ex: @@ -131,6 +127,41 @@ def parse_var( return value + @classmethod + def parse_text_var( + cls, + meta: XmlMeta, + var: XmlVar, + config: ParserConfig, + text: str | None, + ns_map: dict | None = None, + types: Sequence[type] | None = None, + format: str | None = None, + ) -> Any: + """Parse a text node value with empty-string fallback. + + Args: + meta: The xml meta instance + var: The xml var instance + config: The parser config instance + text: The element text content + ns_map: The element namespace prefix-URI map + types: Override the var types + format: Override the var format + + Returns: + The converted value or values. + """ + value = cls.parse_var( + meta, var, config, text, ns_map=ns_map, types=types, format=format + ) + if value is None: + value = cls.parse_var( + meta, var, config, "", ns_map=ns_map, types=types, format=format + ) + + return value + @classmethod def parse_value( cls,