diff --git a/docs/api/utils.rst b/docs/api/utils.rst
new file mode 100644
index 000000000..0d61f985e
--- /dev/null
+++ b/docs/api/utils.rst
@@ -0,0 +1,19 @@
+=========
+Utilities
+=========
+
+
+.. currentmodule:: xsdata.utils
+
+.. autosummary::
+ :toctree: reference
+ :nosignatures:
+
+ text.capitalize
+ text.pascal_case
+ text.camel_case
+ text.mixed_case
+ text.mixed_pascal_case
+ text.mixed_snake_case
+ text.snake_case
+ text.kebab_case
diff --git a/docs/models.rst b/docs/models.rst
index 7e499c9b9..97609c467 100644
--- a/docs/models.rst
+++ b/docs/models.rst
@@ -27,6 +27,9 @@ Basic Model
Class Meta
==========
+Through the Meta class you can control the model's behaviour during data binding
+procedures.
+
.. list-table::
:widths: 20 10 300
:header-rows: 1
@@ -36,13 +39,19 @@ Class Meta
- Description
* - name
- str
- - The real name of the element this class represents.
+ - The real/local name of the element this class represents.
* - nillable
- bool
- Specifies whether an explicit empty value can be assigned, default: False
* - namespace
- str
- The element xml namespace.
+ * - element_name_generator
+ - Callable
+ - Element name generator
+ * - attribute_name_generator
+ - Callable
+ - Attribute name generator
Field Typing
@@ -55,6 +64,9 @@ Simply follow the Python lib
Field Metadata
==============
+Through the metadata properties you can control the field's behaviour during data
+binding procedures.
+
.. list-table::
:widths: 20 10 250
:header-rows: 1
@@ -64,7 +76,7 @@ Field Metadata
- Description
* - name
- str
- - The real name of the element or attribute this field represents.
+ - The real/local name of the element or attribute this field represents.
* - type
- str
- The field xml type:
@@ -328,3 +340,142 @@ is directly assigned as text to elements.
.. code-block:: xml
2020
+
+
+Advance Topics
+==============
+
+Customize element and attribute names
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Through the model and field metadata you can explicitly specify the serialized
+names. You can also provide callables to set the real/local names per model or
+for the entire binding context.
+
+
+.. doctest::
+
+ >>> from dataclasses import dataclass, field
+ >>> from datetime import date
+ >>> from xsdata.formats.dataclass.context import XmlContext
+ >>> from xsdata.formats.dataclass.parsers import XmlParser
+ >>> from xsdata.formats.dataclass.serializers import XmlSerializer
+ >>> from xsdata.formats.dataclass.serializers.config import SerializerConfig
+ >>> from xsdata.utils import text
+ >>> config = SerializerConfig(pretty_print=True, xml_declaration=False)
+ >>> serializer = XmlSerializer(config=config)
+
+**Ordered by priority**
+
+.. tab:: Explicit names
+
+ Explicit model and field names is the most straight forward way to customize
+ the real/local names for elements and attributes. It can become tedious though
+ when you have to do this for models with a lot of fields.
+
+ .. doctest::
+
+ >>> @dataclass
+ ... class Person:
+ ...
+ ... class Meta:
+ ... name = "Person" # Explicit name
+ ...
+ ... first_name: str = field(metadata=dict(name="firstName"))
+ ... last_name: str = field(metadata=dict(name="lastName"))
+ ... birth_date: date = field(
+ ... metadata=dict(
+ ... type="Attribute",
+ ... format="%Y-%m-%d",
+ ... name="dob" # Explicit name
+ ... )
+ ... )
+ ...
+ >>> obj = Person(
+ ... first_name="Chris",
+ ... last_name="T",
+ ... birth_date=date(1986, 9, 25),
+ ... )
+ >>> print(serializer.render(obj))
+
+ Chris
+ T
+
+
+
+
+.. tab:: Model name generators
+
+ Through the Meta class you can provide callables to apply a naming scheme for all
+ the model fields. The :mod:`xsdata.utils.text` has various helpers that you can
+ reuse.
+
+ .. doctest::
+
+ >>> @dataclass
+ ... class person:
+ ...
+ ... class Meta:
+ ... element_name_generator = text.pascal_case
+ ... attribute_name_generator = text.camel_case
+ ...
+ ... first_name: str
+ ... last_name: str
+ ... birth_date: date = field(
+ ... metadata=dict(
+ ... type="Attribute",
+ ... format="%Y-%m-%d"
+ ... )
+ ... )
+ ...
+ >>> obj = person(
+ ... first_name="Chris",
+ ... last_name="T",
+ ... birth_date=date(1986, 9, 25),
+ ... )
+ >>> print(serializer.render(obj))
+
+ Chris
+ T
+
+
+
+
+.. tab:: Context name generators
+
+ Through the :class:`~xsdata.formats.dataclass.context.XmlContext` instance you can
+ provide callables to apply a naming scheme for all models and their fields. This way
+ you can avoid declaring them for every model but you have to use the same context
+ whenever you want to use a parser/serializer.
+
+ .. doctest::
+
+ >>> @dataclass
+ ... class Person:
+ ...
+ ... first_name: str
+ ... last_name: str
+ ... birth_date: date = field(
+ ... metadata=dict(
+ ... type="Attribute",
+ ... format="%Y-%m-%d"
+ ... )
+ ... )
+ ...
+ >>> obj = Person(
+ ... first_name="Chris",
+ ... last_name="T",
+ ... birth_date=date(1986, 9, 25),
+ ... )
+ ...
+ >>> context = XmlContext(
+ ... element_name_generator=text.camel_case,
+ ... attribute_name_generator=text.kebab_case
+ ... )
+ >>> serializer = XmlSerializer(context=context, config=config)
+ >>> print(serializer.render(obj))
+
+ Chris
+ T
+
+
diff --git a/tests/codegen/handlers/test_attribute_type.py b/tests/codegen/handlers/test_attribute_type.py
index 9eb9f65ca..c2af49e0c 100644
--- a/tests/codegen/handlers/test_attribute_type.py
+++ b/tests/codegen/handlers/test_attribute_type.py
@@ -10,12 +10,8 @@
from xsdata.codegen.models import Restrictions
from xsdata.codegen.models import Status
from xsdata.codegen.utils import ClassUtils
-from xsdata.exceptions import AnalyzerValueError
from xsdata.models.enums import DataType
from xsdata.models.enums import Tag
-from xsdata.models.xsd import ComplexType
-from xsdata.models.xsd import Element
-from xsdata.models.xsd import SimpleType
class AttributeTypeHandlerTests(FactoryTestCase):
diff --git a/tests/codegen/mappers/test_definitions.py b/tests/codegen/mappers/test_definitions.py
index 6baf42e95..7ecff8728 100644
--- a/tests/codegen/mappers/test_definitions.py
+++ b/tests/codegen/mappers/test_definitions.py
@@ -22,7 +22,6 @@
from xsdata.models.wsdl import PortTypeOperation
from xsdata.models.wsdl import Service
from xsdata.models.wsdl import ServicePort
-from xsdata.models.xsd import Element
from xsdata.utils.namespaces import build_qname
diff --git a/tests/codegen/models/test_attr.py b/tests/codegen/models/test_attr.py
index 3a01fac2c..dc17d45e5 100644
--- a/tests/codegen/models/test_attr.py
+++ b/tests/codegen/models/test_attr.py
@@ -1,11 +1,8 @@
import sys
-from unittest import mock
from tests.factories import AttrFactory
from tests.factories import FactoryTestCase
-from xsdata.codegen.models import Attr
from xsdata.codegen.models import Restrictions
-from xsdata.formats.dataclass.models.elements import XmlType
from xsdata.models.enums import Namespace
from xsdata.models.enums import Tag
diff --git a/tests/codegen/test_writer.py b/tests/codegen/test_writer.py
index 462d0f804..1f845ec69 100644
--- a/tests/codegen/test_writer.py
+++ b/tests/codegen/test_writer.py
@@ -52,9 +52,9 @@ def test_write(self, mock_designate, mock_render):
def test_print(self, mock_print, mock_designate, mock_render):
classes = ClassFactory.list(2)
mock_render.return_value = [
- GeneratorResult(Path(f"foo/a.py"), "file", "aAa"),
- GeneratorResult(Path(f"bar/b.py"), "file", "bBb"),
- GeneratorResult(Path(f"c.py"), "file", ""),
+ GeneratorResult(Path("foo/a.py"), "file", "aAa"),
+ GeneratorResult(Path("bar/b.py"), "file", "bBb"),
+ GeneratorResult(Path("c.py"), "file", ""),
]
self.writer.print(classes)
diff --git a/tests/conftest.py b/tests/conftest.py
index 5fff10611..c29f688e7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,3 @@
-import json
from pathlib import Path
from typing import Type
diff --git a/tests/formats/dataclass/test_context.py b/tests/formats/dataclass/test_context.py
index b598d52cb..9de172f01 100644
--- a/tests/formats/dataclass/test_context.py
+++ b/tests/formats/dataclass/test_context.py
@@ -29,6 +29,7 @@
from xsdata.models.datatype import XmlDate
from xsdata.models.enums import DataType
from xsdata.utils import text
+from xsdata.utils.constants import return_input
from xsdata.utils.constants import return_true
from xsdata.utils.namespaces import build_qname
@@ -130,7 +131,9 @@ def test_build_build_vars(self, mock_get_type_hints):
)
self.assertEqual(expected, result)
- mock_get_type_hints.assert_called_once_with(ItemsType, None)
+ mock_get_type_hints.assert_called_once_with(
+ ItemsType, None, return_input, return_input
+ )
@mock.patch.object(XmlContext, "get_type_hints", return_value={})
def test_build_with_meta_namespace(self, mock_get_type_hints):
@@ -139,18 +142,22 @@ def test_build_with_meta_namespace(self, mock_get_type_hints):
self.assertEqual(build_qname(namespace, "product"), result.qname)
self.assertEqual(build_qname(namespace, "product"), result.source_qname)
- mock_get_type_hints.assert_called_once_with(Product, namespace)
+ mock_get_type_hints.assert_called_once_with(
+ Product, namespace, return_input, return_input
+ )
@mock.patch.object(XmlContext, "get_type_hints", return_value={})
def test_build_with_parent_ns(self, mock_get_type_hints):
result = self.ctx.build(ProductType, "http://xsdata")
self.assertEqual(build_qname("http://xsdata", "ProductType"), str(result.qname))
- mock_get_type_hints.assert_called_once_with(ProductType, "http://xsdata")
+ mock_get_type_hints.assert_called_once_with(
+ ProductType, "http://xsdata", return_input, return_input
+ )
@mock.patch.object(XmlContext, "get_type_hints", return_value={})
def test_build_with_no_meta_name_and_name_generator(self, *args):
- inspect = XmlContext(element_name=lambda x: text.snake_case(x))
+ inspect = XmlContext(element_name_generator=lambda x: text.snake_case(x))
result = inspect.build(ItemsType)
self.assertEqual("items_type", result.qname)
@@ -184,21 +191,21 @@ def test_build_with_no_dataclass_raises_exception(self, *args):
self.assertEqual(f"Object {int} is not a dataclass.", str(cm.exception))
def test_get_type_hints(self):
- result = self.ctx.get_type_hints(BookForm, None)
+ result = self.ctx.get_type_hints(BookForm, None, text.pascal_case, str.upper)
self.assertIsInstance(result, Iterator)
expected = [
- XmlVar(element=True, name="author", qname="author", types=[str]),
- XmlVar(element=True, name="title", qname="title", types=[str]),
- XmlVar(element=True, name="genre", qname="genre", types=[str]),
- XmlVar(element=True, name="price", qname="price", types=[float]),
- XmlVar(element=True, name="pub_date", qname="pub_date", types=[XmlDate]),
- XmlVar(element=True, name="review", qname="review", types=[str]),
- XmlVar(attribute=True, name="id", qname="id", types=[str]),
+ XmlVar(element=True, name="author", qname="Author", types=[str]),
+ XmlVar(element=True, name="title", qname="Title", types=[str]),
+ XmlVar(element=True, name="genre", qname="Genre", types=[str]),
+ XmlVar(element=True, name="price", qname="Price", types=[float]),
+ XmlVar(element=True, name="pub_date", qname="PubDate", types=[XmlDate]),
+ XmlVar(element=True, name="review", qname="Review", types=[str]),
+ XmlVar(attribute=True, name="id", qname="ID", types=[str]),
XmlVar(
attribute=True,
name="lang",
- qname="lang",
+ qname="LANG",
types=[str],
init=False,
default="en",
@@ -218,7 +225,9 @@ class Example:
int: int
union: Union[int, bool]
- result = list(self.ctx.get_type_hints(Example, None))
+ result = list(
+ self.ctx.get_type_hints(Example, None, return_input, return_input)
+ )
expected = [
XmlVar(element=True, name="bool", qname="bool", types=[bool]),
XmlVar(element=True, name="int", qname="int", types=[int]),
@@ -231,7 +240,7 @@ class Example:
self.assertEqual(expected, result)
def test_get_type_hints_with_dataclass_list(self):
- result = list(self.ctx.get_type_hints(Books, None))
+ result = list(self.ctx.get_type_hints(Books, None, return_input, return_input))
expected = XmlVar(
element=True,
@@ -250,7 +259,7 @@ def test_get_type_hints_with_dataclass_list(self):
self.assertEqual(BookForm, result[0].clazz)
def test_get_type_hints_with_wildcard_element(self):
- result = list(self.ctx.get_type_hints(Umbrella, None))
+ actual = self.ctx.get_type_hints(Umbrella, None, return_input, return_input)
expected = XmlVar(
wildcard=True,
@@ -261,8 +270,7 @@ def test_get_type_hints_with_wildcard_element(self):
namespaces=["##any"],
)
- self.assertEqual(1, len(result))
- self.assertEqual(expected, result[0])
+ self.assertEqual([expected], list(actual))
def test_get_type_hints_with_undefined_types(self):
@dataclass
@@ -282,7 +290,8 @@ class Currencies:
XmlVar(element=True, name="iso_code", qname="CharCode", types=[str]),
XmlVar(element=True, name="nominal", qname="Nominal", types=[int]),
]
- self.assertEqual(expected, list(self.ctx.get_type_hints(Currency, None)))
+ actual = self.ctx.get_type_hints(Currency, None, return_input, return_input)
+ self.assertEqual(expected, list(actual))
expected = [
XmlVar(attribute=True, name="name", qname="name", types=[str]),
@@ -303,10 +312,12 @@ class Currencies:
types=[Currency],
),
]
- self.assertEqual(expected, list(self.ctx.get_type_hints(Currencies, None)))
+
+ actual = self.ctx.get_type_hints(Currencies, None, return_input, return_input)
+ self.assertEqual(expected, list(actual))
def test_get_type_hints_with_choices(self):
- actual = self.ctx.get_type_hints(Node, "bar")
+ actual = self.ctx.get_type_hints(Node, "bar", return_input, return_input)
self.assertIsInstance(actual, Generator)
expected = XmlVar(
elements=True,
diff --git a/tests/models/test_datatype.py b/tests/models/test_datatype.py
index 63b185b8b..a64923eeb 100644
--- a/tests/models/test_datatype.py
+++ b/tests/models/test_datatype.py
@@ -1,4 +1,3 @@
-from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
diff --git a/tests/models/test_mixins.py b/tests/models/test_mixins.py
index db1a44913..fd3d39dbd 100644
--- a/tests/models/test_mixins.py
+++ b/tests/models/test_mixins.py
@@ -1,10 +1,8 @@
-from dataclasses import dataclass
from typing import Generator
from typing import Iterator
from unittest import TestCase
from xsdata.exceptions import SchemaValueError
-from xsdata.models.enums import DataType
from xsdata.models.enums import FormType
from xsdata.models.enums import Namespace
from xsdata.models.mixins import ElementBase
diff --git a/tests/models/xsd/test_list.py b/tests/models/xsd/test_list.py
index ce16606dd..1f8981063 100644
--- a/tests/models/xsd/test_list.py
+++ b/tests/models/xsd/test_list.py
@@ -1,4 +1,3 @@
-import sys
from unittest import TestCase
from xsdata.models.xsd import List
diff --git a/tests/models/xsd/test_restriction.py b/tests/models/xsd/test_restriction.py
index 62b263b4c..d87d2ec54 100644
--- a/tests/models/xsd/test_restriction.py
+++ b/tests/models/xsd/test_restriction.py
@@ -1,8 +1,6 @@
from typing import Iterator
from unittest import TestCase
-from xsdata.models.enums import DataType
-from xsdata.models.enums import Namespace
from xsdata.models.xsd import Enumeration
from xsdata.models.xsd import FractionDigits
from xsdata.models.xsd import Length
diff --git a/tests/utils/test_collections.py b/tests/utils/test_collections.py
index 1df28f421..f7ab69bc9 100644
--- a/tests/utils/test_collections.py
+++ b/tests/utils/test_collections.py
@@ -1,6 +1,5 @@
from typing import Generator
from typing import Hashable
-from typing import Iterable
from unittest import TestCase
from xsdata.utils import collections
diff --git a/xsdata/formats/dataclass/context.py b/xsdata/formats/dataclass/context.py
index bcf81c6e4..c1dbeb9cd 100644
--- a/xsdata/formats/dataclass/context.py
+++ b/xsdata/formats/dataclass/context.py
@@ -26,6 +26,7 @@
from xsdata.models.enums import DataType
from xsdata.models.enums import NamespaceType
from xsdata.utils.constants import EMPTY_SEQUENCE
+from xsdata.utils.constants import return_input
from xsdata.utils.namespaces import build_qname
@@ -34,15 +35,15 @@ class XmlContext:
"""
The service provider for binding operations metadata.
- :param element_name: Default callable to convert field names to element tags
- :param attribute_name: Default callable to convert field names to attribute tags
+ :param element_name_generator: Default element name generator
+ :param attribute_name_generator: Default attribute name generator
:ivar cache: Cache models metadata
:ivar xsi_cache: Index models by xsi:type
:ivar sys_modules: Number of imported modules
"""
- element_name: Callable = field(default=lambda x: x)
- attribute_name: Callable = field(default=lambda x: x)
+ element_name_generator: Callable = field(default=return_input)
+ attribute_name_generator: Callable = field(default=return_input)
cache: Dict[Type, XmlMeta] = field(init=False, default_factory=dict)
xsi_cache: Dict[str, List[Type]] = field(
init=False, default_factory=lambda: defaultdict(list)
@@ -83,10 +84,14 @@ def build_xsi_cache(self):
for clazz in self.get_subclasses(object):
if is_dataclass(clazz):
meta = clazz.Meta if "Meta" in clazz.__dict__ else None
- name = getattr(meta, "name", None) or self.local_name(clazz.__name__)
+ local_name = getattr(meta, "name", None)
+ element_name_generator = getattr(
+ meta, "element_name_generator", self.element_name_generator
+ )
+ local_name = local_name or element_name_generator(clazz.__name__)
module = sys.modules[clazz.__module__]
source_namespace = getattr(module, "__NAMESPACE__", None)
- source_qname = build_qname(source_namespace, name)
+ source_qname = build_qname(source_namespace, local_name)
self.xsi_cache[source_qname].append(clazz)
self.sys_modules = len(sys.modules)
@@ -169,7 +174,14 @@ def build(self, clazz: Type, parent_ns: Optional[str] = None) -> XmlMeta:
# Fetch the dataclass meta settings and make sure we don't inherit
# the parent class meta.
meta = clazz.Meta if "Meta" in clazz.__dict__ else None
- name = getattr(meta, "name", None) or self.local_name(clazz.__name__)
+ element_name_generator = getattr(
+ meta, "element_name_generator", self.element_name_generator
+ )
+ attribute_name_generator = getattr(
+ meta, "attribute_name_generator", self.attribute_name_generator
+ )
+ local_name = getattr(meta, "name", None)
+ local_name = local_name or element_name_generator(clazz.__name__)
nillable = getattr(meta, "nillable", False)
namespace = getattr(meta, "namespace", parent_ns)
module = sys.modules[clazz.__module__]
@@ -177,14 +189,27 @@ def build(self, clazz: Type, parent_ns: Optional[str] = None) -> XmlMeta:
self.cache[clazz] = XmlMeta(
clazz=clazz,
- qname=build_qname(namespace, name),
- source_qname=build_qname(source_namespace, name),
+ qname=build_qname(namespace, local_name),
+ source_qname=build_qname(source_namespace, local_name),
nillable=nillable,
- vars=list(self.get_type_hints(clazz, namespace)),
+ vars=list(
+ self.get_type_hints(
+ clazz,
+ namespace,
+ element_name_generator,
+ attribute_name_generator,
+ )
+ ),
)
return self.cache[clazz]
- def get_type_hints(self, clazz: Type, parent_ns: Optional[str]) -> Iterator[XmlVar]:
+ def get_type_hints(
+ self,
+ clazz: Type,
+ parent_ns: Optional[str],
+ element_name_generator: Callable,
+ attribute_name_generator: Callable,
+ ) -> Iterator[XmlVar]:
"""
Build the model fields binding metadata.
@@ -211,7 +236,12 @@ def get_type_hints(self, clazz: Type, parent_ns: Optional[str]) -> Iterator[XmlV
element_list = self.is_element_list(type_hint, tokens)
is_class = any(is_dataclass(clazz) for clazz in types)
xml_type = xml_type or (XmlType.ELEMENT if is_class else default_xml_type)
- local_name = local_name or self.local_name(var.name, xml_type)
+
+ if not local_name:
+ if xml_type == XmlType.ATTRIBUTE:
+ local_name = attribute_name_generator(var.name)
+ else:
+ local_name = element_name_generator(var.name)
namespaces = self.resolve_namespaces(xml_type, namespace, parent_ns)
default_namespace = self.default_namespace(namespaces)
@@ -429,9 +459,3 @@ def get_subclasses(cls, clazz: Type):
yield subclass
except TypeError:
pass
-
- def local_name(self, name: str, xml_type: Optional[str] = None) -> str:
- if xml_type == "Attribute":
- return self.attribute_name(name)
-
- return self.element_name(name)
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index 799644737..350b27646 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -259,14 +259,20 @@ def create(cls) -> "GeneratorConfig":
@classmethod
def read(cls, path: Path) -> "GeneratorConfig":
- ctx = XmlContext(element_name=text.pascal_case, attribute_name=text.camel_case)
+ ctx = XmlContext(
+ element_name_generator=text.pascal_case,
+ attribute_name_generator=text.camel_case,
+ )
config = ParserConfig(fail_on_unknown_properties=False)
parser = XmlParser(context=ctx, config=config)
return parser.from_path(path, cls)
@classmethod
def write(cls, output: TextIO, obj: "GeneratorConfig"):
- ctx = XmlContext(element_name=text.pascal_case, attribute_name=text.camel_case)
+ ctx = XmlContext(
+ element_name_generator=text.pascal_case,
+ attribute_name_generator=text.camel_case,
+ )
config = SerializerConfig(pretty_print=True)
serializer = XmlSerializer(context=ctx, config=config, writer=XmlEventWriter)
serializer.write(output, obj, ns_map={None: "http://pypi.org/project/xsdata"})
diff --git a/xsdata/utils/constants.py b/xsdata/utils/constants.py
index 9313d084e..758484b25 100644
--- a/xsdata/utils/constants.py
+++ b/xsdata/utils/constants.py
@@ -11,6 +11,11 @@ def return_true(*_: Any) -> bool:
return True
+def return_input(obj: Any) -> Any:
+ """A dummy function that always returns input."""
+ return obj
+
+
class DateFormat:
DATE = "%Y-%m-%d%z"
TIME = "%H:%M:%S%z"
diff --git a/xsdata/utils/text.py b/xsdata/utils/text.py
index 07e680c9f..768c4619d 100644
--- a/xsdata/utils/text.py
+++ b/xsdata/utils/text.py
@@ -62,6 +62,11 @@ def snake_case(string: str, **kwargs: Any) -> str:
return "_".join(map(str.lower, split_words(string)))
+def kebab_case(string: str, **kwargs: Any) -> str:
+ """Convert the given string to kebab case."""
+ return "-".join(split_words(string))
+
+
def split_words(string: str) -> List[str]:
"""Split a string on new capital letters and not alphanumeric
characters."""