Skip to content
Merged

Dev #257

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
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
=========

2.16.0 (2025-04-20)
-------------------

- exception is raised if namespace alias is not found in the namespace map.
- child model inherits parent namespace map.
- documentation enhanced.


2.15.0 (2025-03-29)
-------------------

Expand Down
33 changes: 33 additions & 0 deletions docs/source/pages/data-binding/elements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,39 @@ The namespace and namespace mapping can be declared for a model. In that case al
:start-after: json-start
:end-before: json-end

.. note::
**Pay attention** to the namespace inheritance rule: namespace and namespace mapping
are only inherited by primitive types not sub-models. If your sub-model share
the namespace with the parent model you must define it explicitly:

.. code-block:: python

from pydantic_xml import BaseXmlModel, element

NSMAP = {
'co': 'http://www.company.com/co',
}

class SubModel(BaseXmlModel, ns='co', nsmap=NSMAP): # define ns and nsmap explicitly
field2: str = element(tag='element1')

class Model(BaseXmlModel, ns='co', nsmap=NSMAP):
field1: str = element(tag='element1') # ns "co" is inherited by the element
sub: SubModel # ns and nsmap are not inherited by the SubModel

model = Model(field1="value1", sub=SubModel(field2="value2"))
print(model.to_xml(pretty_print=True).decode())


.. code-block:: xml

<co:Model xmlns:co="http://www.company.com/co">
<co:element1>value1</co:element1>
<co:sub>
<co:element1>value2</co:element1>
</co:sub>
</co:Model>


The namespace and namespace mapping can be also applied to model types passing ``ns`` and ``nsmap``
to :py:func:`pydantic_xml.element`. If they are omitted the model namespace and namespace mapping is used:
Expand Down
62 changes: 62 additions & 0 deletions docs/source/pages/misc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,68 @@ Field specification syntax is similar to ``pydantic`` one. For more information
see the `documentation <https://docs.pydantic.dev/latest/concepts/models/#dynamic-model-creation>`_.


Document type declaration
~~~~~~~~~~~~~~~~~~~~~~~~~

A document type declaration is an instruction that associates a particular XML document
with a document type definition (DTD).

DTD is supported by ``lxml`` backend only so the library doesn't provide an api for that natively,
but it can be easily implemented by your hand:

.. code-block:: python

from typing import Any, ClassVar, Union

import pydantic_xml as pxml
import lxml.etree


class DTDXmlModel(pxml.BaseXmlModel):
DOC_PUBLIC_ID: ClassVar[str]
DOC_SYSTEM_URL: ClassVar[str]

def to_xml(
self,
*,
skip_empty: bool = False,
exclude_none: bool = False,
exclude_unset: bool = False,
**kwargs: Any,
) -> Union[str, bytes]:
root = self.to_xml_tree(skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset)
tree = lxml.etree.ElementTree(root)
tree.docinfo.public_id = self.DOC_PUBLIC_ID
tree.docinfo.system_url = self.DOC_SYSTEM_URL

return lxml.etree.tostring(tree, **kwargs)


class Html(DTDXmlModel, tag='html'):
DOC_PUBLIC_ID: ClassVar[str] = '-//W3C//DTD HTML 4.01//EN'
DOC_SYSTEM_URL: ClassVar[str] = 'http://www.w3.org/TR/html4/strict.dtd'

title: str = pxml.wrapped('head', pxml.element())
body: str = pxml.element()


html_doc = Html(title="This is a title", body="Hello world!")
xml = html_doc.to_xml(pretty_print=True)

print(xml.decode())


.. code-block:: xml

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>This is a title</title>
</head>
<body>Hello world!</body>
</html>


Mypy
~~~~

Expand Down
7 changes: 6 additions & 1 deletion pydantic_xml/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,17 @@ def __init_subclass__(

cls.__xml_tag__ = tag if tag is not None else getattr(cls, '__xml_tag__', None)
cls.__xml_ns__ = ns if ns is not None else getattr(cls, '__xml_ns__', None)
cls.__xml_nsmap__ = nsmap if nsmap is not None else getattr(cls, '__xml_nsmap__', None)
cls.__xml_ns_attrs__ = ns_attrs if ns_attrs is not None else getattr(cls, '__xml_ns_attrs__', False)
cls.__xml_skip_empty__ = skip_empty if skip_empty is not None else getattr(cls, '__xml_skip_empty__', None)
cls.__xml_search_mode__ = search_mode if search_mode is not None \
else getattr(cls, '__xml_search_mode__', SearchMode.STRICT)

if parent_nsmap := getattr(cls, '__xml_nsmap__', None):
parent_nsmap.update(nsmap or {})
cls.__xml_nsmap__ = parent_nsmap
else:
cls.__xml_nsmap__ = nsmap

cls.__xml_field_serializers__ = {}
cls.__xml_field_validators__ = {}

Expand Down
10 changes: 9 additions & 1 deletion pydantic_xml/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import pydantic as pd
import pydantic_core as pdc

from pydantic_xml import errors

from .element.native import etree
from .typedefs import Location, NsMap

Expand Down Expand Up @@ -52,7 +54,13 @@ def from_alias(
"""

if not is_attr or ns is not None:
ns = nsmap.get(ns or '') if nsmap else None
if ns is None:
ns = nsmap.get('') if nsmap else None
else:
try:
ns = nsmap[ns] if nsmap else None
except KeyError:
raise errors.ModelError(f"namespace alias {ns} not declared in nsmap")

return QName(tag=tag, ns=ns)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-xml"
version = "2.15.0"
version = "2.16.0"
description = "pydantic xml extension"
authors = ["Dmitry Pershin <[email protected]>"]
license = "Unlicense"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ class BaseModel(
BaseXmlModel,
tag='TestTag',
ns='TestNamespace',
nsmap={'test': 'value'},
nsmap={'TestNamespace': 'value'},
ns_attrs=True,
search_mode='ordered',
):
Expand Down