Skip to content

Commit

Permalink
Add serializer option to ignore optional default attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed Aug 2, 2021
1 parent 7234cf3 commit 8025fbe
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 42 deletions.
3 changes: 2 additions & 1 deletion docs/xml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,13 @@ Serializer Config
... encoding="UTF-8",
... xml_version="1.1",
... xml_declaration=False,
... ignore_default_attributes=True,
... schema_location="urn books.xsd",
... no_namespace_schema_location=None,
... ))
>>> print(serializer.render(books))
<ns0:books xmlns:ns0="urn:books" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn books.xsd">
<book id="bk001" lang="en">
<book id="bk001">
<author>Hightower, Kim</author>
<title>The First Book</title>
<genre>Fiction</genre>
Expand Down
6 changes: 6 additions & 0 deletions tests/formats/dataclass/models/test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,41 +144,47 @@ def test_build_vars(self):
name="author",
qname="Author",
types=(str,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
index=2,
name="title",
qname="Title",
types=(str,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
index=3,
name="genre",
qname="Genre",
types=(str,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
index=4,
name="price",
qname="Price",
types=(float,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
index=5,
name="pub_date",
qname="PubDate",
types=(XmlDate,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
index=6,
name="review",
qname="Review",
types=(str,),
required=True,
),
XmlVarFactory.create(
xml_type=XmlType.ATTRIBUTE, index=7, name="id", qname="ID", types=(str,)
Expand Down
22 changes: 22 additions & 0 deletions tests/formats/dataclass/serializers/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,25 @@ def test_indent_deprecation(self):
"JsonSerializer indent property is deprecated, use SerializerConfig",
str(w[-1].message),
)

def test_next_value(self):
book = self.books.book[0]
serializer = JsonSerializer()

actual = [name for name, value in serializer.next_value(book)]
expected = [
"author",
"title",
"genre",
"price",
"pub_date",
"review",
"id",
"lang",
]
self.assertEqual(expected, actual)

serializer.config.ignore_default_attributes = True
expected = expected[:-1]
actual = [name for name, value in serializer.next_value(book)]
self.assertEqual(expected, actual)
17 changes: 14 additions & 3 deletions tests/formats/dataclass/serializers/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ def test_write_value_with_list_value(self):
self.assertEqual(expected, list(result))

def test_next_value(self):
obj = SequentialType(x0=1, x1=[2, 3, 4], x2=[6, 7], x3=[9])
obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9])
meta = self.serializer.context.build(SequentialType)
x0 = meta.text
x1 = next(meta.find_children("x1"))
Expand All @@ -530,7 +530,7 @@ def test_next_attribute(self):
obj = SequentialType(a0="foo", a1={"b": "c", "d": "e"})
meta = self.serializer.context.build(SequentialType)

actual = self.serializer.next_attribute(obj, meta, False, None)
actual = self.serializer.next_attribute(obj, meta, False, None, False)
expected = [
("a0", "foo"),
("b", "c"),
Expand All @@ -540,7 +540,7 @@ def test_next_attribute(self):
self.assertIsInstance(actual, Generator)
self.assertEqual(expected, list(actual))

actual = self.serializer.next_attribute(obj, meta, True, "xs:bool")
actual = self.serializer.next_attribute(obj, meta, True, "xs:bool", False)
expected.extend(
[
(QNames.XSI_TYPE, "xs:bool"),
Expand All @@ -549,6 +549,17 @@ def test_next_attribute(self):
)
self.assertEqual(expected, list(actual))

meta.attributes["a0"].required = False
meta.attributes["a0"].default = "foo"
actual = self.serializer.next_attribute(obj, meta, False, None, True)
expected = [
("b", "c"),
("d", "e"),
]

self.assertIsInstance(actual, Generator)
self.assertEqual(expected, list(actual))

def test_render_mixed_content(self):

obj = Paragraph()
Expand Down
12 changes: 12 additions & 0 deletions tests/formats/dataclass/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def test_find_value_choice(self):
self.assertEqual(var.elements["a"], var.find_value_choice(TypeA(1), True))
self.assertEqual(var.elements["b"], var.find_value_choice(TypeB(1, "b"), True))

def test_is_optional(self):
var = XmlVarFactory.create(xml_type=XmlType.ATTRIBUTE, name="att")

self.assertTrue(var.is_optional(None))
self.assertFalse(var.is_optional("foo"))

var.default = lambda: [1, 2, 3]
self.assertTrue(var.is_optional([1, 2, 3]))

var.required = True
self.assertFalse(var.is_optional([1, 2, 3]))

def test_match_namespace(self):
var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, name="foo")
self.assertTrue(var.match_namespace("a"))
Expand Down
2 changes: 2 additions & 0 deletions xsdata/formats/dataclass/models/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def build(
namespace = metadata.get("namespace")
choices = metadata.get("choices", EMPTY_SEQUENCE)
mixed = metadata.get("mixed", False)
required = metadata.get("required", False)
nillable = metadata.get("nillable", False)
format_str = metadata.get("format", None)
sequential = metadata.get("sequential", False)
Expand Down Expand Up @@ -288,6 +289,7 @@ def build(
format=format_str,
clazz=clazz,
any_type=any_type,
required=required,
nillable=nillable,
sequential=sequential,
factory=origin,
Expand Down
14 changes: 14 additions & 0 deletions xsdata/formats/dataclass/models/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class XmlVar(MetaMixin):
:param format: Value format information
:param derived: Wrap parsed values with a generic type
:param any_type: Field supports dynamic value types
:param required: Field is mandatory
:param nillable: Field supports nillable content
:param sequential: Render values in sequential mode
:param list_element: Field is a list of elements
Expand All @@ -88,6 +89,7 @@ class XmlVar(MetaMixin):
"format",
"derived",
"any_type",
"required",
"nillable",
"sequential",
"default",
Expand Down Expand Up @@ -122,6 +124,7 @@ def __init__(
format: Optional[str],
derived: bool,
any_type: bool,
required: bool,
nillable: bool,
sequential: bool,
default: Any,
Expand All @@ -142,6 +145,7 @@ def __init__(
self.format = format
self.derived = derived
self.any_type = any_type
self.required = required
self.nillable = nillable
self.sequential = sequential
self.list_element = factory in (list, tuple)
Expand Down Expand Up @@ -219,6 +223,16 @@ def find_type_choice(

return None

def is_optional(self, value: Any) -> bool:
"""Return whether this var instance is not required and the given value
matches the default one."""
if self.required:
return False

if callable(self.default):
return self.default() == value
return self.default == value

@classmethod
def match_type(
cls, value: Any, tp: Type, types: Sequence[Type], check_subclass: bool
Expand Down
31 changes: 22 additions & 9 deletions xsdata/formats/dataclass/parsers/config.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
from dataclasses import dataclass
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Type
from typing import TypeVar

T = TypeVar("T")
from xsdata.formats.bindings import T


def default_class_factory(cls: Type[T], params: Dict) -> T:
return cls(**params) # type: ignore


@dataclass
class ParserConfig:
"""
Parsing configuration options.
Expand All @@ -27,8 +24,24 @@ class ParserConfig:
exceptions
"""

base_url: Optional[str] = None
process_xinclude: bool = False
class_factory: Callable[[Type[T], Dict], T] = default_class_factory
fail_on_unknown_properties: bool = True
fail_on_converter_warnings: bool = False
__slots__ = (
"base_url",
"process_xinclude",
"class_factory",
"fail_on_unknown_properties",
"fail_on_converter_warnings",
)

def __init__(
self,
base_url: Optional[str] = None,
process_xinclude: bool = False,
class_factory: Callable[[Type[T], Dict], T] = default_class_factory,
fail_on_unknown_properties: bool = True,
fail_on_converter_warnings: bool = False,
):
self.base_url = base_url
self.process_xinclude = process_xinclude
self.class_factory = class_factory
self.fail_on_unknown_properties = fail_on_unknown_properties
self.fail_on_converter_warnings = fail_on_converter_warnings
3 changes: 1 addition & 2 deletions xsdata/formats/dataclass/parsers/nodes/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,12 @@ def bind(
self, qname: str, text: Optional[str], tail: Optional[str], objects: List
) -> bool:

obj: Any = None
if not self.xsi_nil or self.meta.nillable:
params: Dict = {}
self.bind_attrs(params)
self.bind_content(params, text, tail, objects)
obj = self.config.class_factory(self.meta.clazz, params)
else:
obj = None

if self.derived_factory:
obj = self.derived_factory(qname=qname, value=obj, type=self.xsi_type)
Expand Down
1 change: 1 addition & 0 deletions xsdata/formats/dataclass/parsers/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def start(
format=None,
derived=False,
any_type=False,
required=False,
nillable=False,
sequential=False,
default=None,
Expand Down
46 changes: 34 additions & 12 deletions xsdata/formats/dataclass/serializers/config.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,47 @@
from dataclasses import dataclass
from dataclasses import field
from typing import Optional


@dataclass
class SerializerConfig:
"""
Serializing configuration options.
Serializer configuration options.
Some options are not applicable for both xml or json documents.
:param encoding: Text encoding
:param xml_version: XML Version number (1.0|1.1)
:param xml_declaration: Generate XML declaration
:param pretty_print: Enable pretty output
:param schema_location: Specify the xsi:schemaLocation attribute value
:param no_namespace_schema_location: Specify the xsi:noNamespaceSchemaLocation
:param ignore_default_attributes: Ignore optional attributes with
default values
:param schema_location: xsi:schemaLocation attribute value
:param no_namespace_schema_location: xsi:noNamespaceSchemaLocation
attribute value
"""

encoding: str = field(default="UTF-8")
xml_version: str = field(default="1.0")
xml_declaration: bool = field(default=True)
pretty_print: bool = field(default=False)
schema_location: Optional[str] = field(default=None)
no_namespace_schema_location: Optional[str] = field(default=None)
__slots__ = (
"encoding",
"xml_version",
"xml_declaration",
"pretty_print",
"ignore_default_attributes",
"schema_location",
"no_namespace_schema_location",
)

def __init__(
self,
encoding: str = "UTF-8",
xml_version: str = "1.0",
xml_declaration: bool = True,
pretty_print: bool = False,
ignore_default_attributes: bool = False,
schema_location: Optional[str] = None,
no_namespace_schema_location: Optional[str] = None,
):
self.encoding = encoding
self.xml_version = xml_version
self.xml_declaration = xml_declaration
self.pretty_print = pretty_print
self.ignore_default_attributes = ignore_default_attributes
self.schema_location = schema_location
self.no_namespace_schema_location = no_namespace_schema_location
8 changes: 7 additions & 1 deletion xsdata/formats/dataclass/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,11 @@ def convert(self, obj: Any, var: Optional[XmlVar] = None) -> Any:
return converter.serialize(obj, format=var.format)

def next_value(self, obj: Any) -> Iterator[Tuple[str, Any]]:
ignore_optionals = self.config.ignore_default_attributes

for var in self.context.build(obj.__class__).get_all_vars():
yield var.local_name, self.convert(getattr(obj, var.name), var)
value = getattr(obj, var.name)
if var.is_attribute and ignore_optionals and var.is_optional(value):
continue

yield var.local_name, self.convert(value, var)
Loading

0 comments on commit 8025fbe

Please sign in to comment.