Skip to content

Commit ed885d3

Browse files
authored
Merge pull request #257 from dapper91/dev
- exception is raised if namespace alias is not found in the namespace map. - child model inherits parent namespace map. - documentation enhanced.
2 parents bd579ce + d528114 commit ed885d3

File tree

7 files changed

+120
-4
lines changed

7 files changed

+120
-4
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
2.16.0 (2025-04-20)
5+
-------------------
6+
7+
- exception is raised if namespace alias is not found in the namespace map.
8+
- child model inherits parent namespace map.
9+
- documentation enhanced.
10+
11+
412
2.15.0 (2025-03-29)
513
-------------------
614

docs/source/pages/data-binding/elements.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,39 @@ The namespace and namespace mapping can be declared for a model. In that case al
153153
:start-after: json-start
154154
:end-before: json-end
155155

156+
.. note::
157+
**Pay attention** to the namespace inheritance rule: namespace and namespace mapping
158+
are only inherited by primitive types not sub-models. If your sub-model share
159+
the namespace with the parent model you must define it explicitly:
160+
161+
.. code-block:: python
162+
163+
from pydantic_xml import BaseXmlModel, element
164+
165+
NSMAP = {
166+
'co': 'http://www.company.com/co',
167+
}
168+
169+
class SubModel(BaseXmlModel, ns='co', nsmap=NSMAP): # define ns and nsmap explicitly
170+
field2: str = element(tag='element1')
171+
172+
class Model(BaseXmlModel, ns='co', nsmap=NSMAP):
173+
field1: str = element(tag='element1') # ns "co" is inherited by the element
174+
sub: SubModel # ns and nsmap are not inherited by the SubModel
175+
176+
model = Model(field1="value1", sub=SubModel(field2="value2"))
177+
print(model.to_xml(pretty_print=True).decode())
178+
179+
180+
.. code-block:: xml
181+
182+
<co:Model xmlns:co="http://www.company.com/co">
183+
<co:element1>value1</co:element1>
184+
<co:sub>
185+
<co:element1>value2</co:element1>
186+
</co:sub>
187+
</co:Model>
188+
156189
157190
The namespace and namespace mapping can be also applied to model types passing ``ns`` and ``nsmap``
158191
to :py:func:`pydantic_xml.element`. If they are omitted the model namespace and namespace mapping is used:

docs/source/pages/misc.rst

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,68 @@ Field specification syntax is similar to ``pydantic`` one. For more information
277277
see the `documentation <https://docs.pydantic.dev/latest/concepts/models/#dynamic-model-creation>`_.
278278

279279

280+
Document type declaration
281+
~~~~~~~~~~~~~~~~~~~~~~~~~
282+
283+
A document type declaration is an instruction that associates a particular XML document
284+
with a document type definition (DTD).
285+
286+
DTD is supported by ``lxml`` backend only so the library doesn't provide an api for that natively,
287+
but it can be easily implemented by your hand:
288+
289+
.. code-block:: python
290+
291+
from typing import Any, ClassVar, Union
292+
293+
import pydantic_xml as pxml
294+
import lxml.etree
295+
296+
297+
class DTDXmlModel(pxml.BaseXmlModel):
298+
DOC_PUBLIC_ID: ClassVar[str]
299+
DOC_SYSTEM_URL: ClassVar[str]
300+
301+
def to_xml(
302+
self,
303+
*,
304+
skip_empty: bool = False,
305+
exclude_none: bool = False,
306+
exclude_unset: bool = False,
307+
**kwargs: Any,
308+
) -> Union[str, bytes]:
309+
root = self.to_xml_tree(skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset)
310+
tree = lxml.etree.ElementTree(root)
311+
tree.docinfo.public_id = self.DOC_PUBLIC_ID
312+
tree.docinfo.system_url = self.DOC_SYSTEM_URL
313+
314+
return lxml.etree.tostring(tree, **kwargs)
315+
316+
317+
class Html(DTDXmlModel, tag='html'):
318+
DOC_PUBLIC_ID: ClassVar[str] = '-//W3C//DTD HTML 4.01//EN'
319+
DOC_SYSTEM_URL: ClassVar[str] = 'http://www.w3.org/TR/html4/strict.dtd'
320+
321+
title: str = pxml.wrapped('head', pxml.element())
322+
body: str = pxml.element()
323+
324+
325+
html_doc = Html(title="This is a title", body="Hello world!")
326+
xml = html_doc.to_xml(pretty_print=True)
327+
328+
print(xml.decode())
329+
330+
331+
.. code-block:: xml
332+
333+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
334+
<html>
335+
<head>
336+
<title>This is a title</title>
337+
</head>
338+
<body>Hello world!</body>
339+
</html>
340+
341+
280342
Mypy
281343
~~~~
282344

pydantic_xml/model.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,12 +421,17 @@ def __init_subclass__(
421421

422422
cls.__xml_tag__ = tag if tag is not None else getattr(cls, '__xml_tag__', None)
423423
cls.__xml_ns__ = ns if ns is not None else getattr(cls, '__xml_ns__', None)
424-
cls.__xml_nsmap__ = nsmap if nsmap is not None else getattr(cls, '__xml_nsmap__', None)
425424
cls.__xml_ns_attrs__ = ns_attrs if ns_attrs is not None else getattr(cls, '__xml_ns_attrs__', False)
426425
cls.__xml_skip_empty__ = skip_empty if skip_empty is not None else getattr(cls, '__xml_skip_empty__', None)
427426
cls.__xml_search_mode__ = search_mode if search_mode is not None \
428427
else getattr(cls, '__xml_search_mode__', SearchMode.STRICT)
429428

429+
if parent_nsmap := getattr(cls, '__xml_nsmap__', None):
430+
parent_nsmap.update(nsmap or {})
431+
cls.__xml_nsmap__ = parent_nsmap
432+
else:
433+
cls.__xml_nsmap__ = nsmap
434+
430435
cls.__xml_field_serializers__ = {}
431436
cls.__xml_field_validators__ = {}
432437

pydantic_xml/utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import pydantic as pd
88
import pydantic_core as pdc
99

10+
from pydantic_xml import errors
11+
1012
from .element.native import etree
1113
from .typedefs import Location, NsMap
1214

@@ -52,7 +54,13 @@ def from_alias(
5254
"""
5355

5456
if not is_attr or ns is not None:
55-
ns = nsmap.get(ns or '') if nsmap else None
57+
if ns is None:
58+
ns = nsmap.get('') if nsmap else None
59+
else:
60+
try:
61+
ns = nsmap[ns] if nsmap else None
62+
except KeyError:
63+
raise errors.ModelError(f"namespace alias {ns} not declared in nsmap")
5664

5765
return QName(tag=tag, ns=ns)
5866

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydantic-xml"
3-
version = "2.15.0"
3+
version = "2.16.0"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <[email protected]>"]
66
license = "Unlicense"

tests/test_misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ class BaseModel(
320320
BaseXmlModel,
321321
tag='TestTag',
322322
ns='TestNamespace',
323-
nsmap={'test': 'value'},
323+
nsmap={'TestNamespace': 'value'},
324324
ns_attrs=True,
325325
search_mode='ordered',
326326
):

0 commit comments

Comments
 (0)