Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serializer option to ignore optional default attributes #575

Merged
merged 1 commit into from
Aug 2, 2021
Merged
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
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