Skip to content
Open
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/artists/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Meta:
"type": "Attribute",
},
)
value: str = field(default="")
value: str = field()


@dataclass(kw_only=True)
Expand Down Expand Up @@ -83,7 +83,7 @@ class Meta:
"type": "Attribute",
}
)
value: str = field(default="")
value: str = field()


@dataclass(kw_only=True)
Expand Down
10 changes: 5 additions & 5 deletions tests/fixtures/dtd/models/complete_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/primer/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Comment:
class Meta:
name = "comment"

value: str = field(default="")
value: str = field()


@dataclass(kw_only=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/wrapper/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Meta:
name = "charlie"
namespace = "xsdata"

value: str = field(default="")
value: str = field()
lang: None | object = field(
default=None,
metadata={
Expand Down
12 changes: 3 additions & 9 deletions tests/formats/dataclass/parsers/nodes/test_primitive.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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"
)
Expand All @@ -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
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions tests/formats/dataclass/parsers/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
(
Expand Down
13 changes: 13 additions & 0 deletions tests/formats/dataclass/parsers/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 0 additions & 7 deletions xsdata/codegen/handlers/sanitize_attributes_default_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions xsdata/formats/dataclass/parsers/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions xsdata/formats/dataclass/parsers/nodes/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
20 changes: 10 additions & 10 deletions xsdata/formats/dataclass/parsers/nodes/primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
23 changes: 12 additions & 11 deletions xsdata/formats/dataclass/parsers/nodes/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 37 additions & 6 deletions xsdata/formats/dataclass/parsers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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,
Expand Down
Loading