Skip to content

Commit b8348a9

Browse files
authored
Merge pull request #190 from dapper91/dev
- dynamic model creation support added.
2 parents d92402e + 0e2c209 commit b8348a9

File tree

8 files changed

+371
-3
lines changed

8 files changed

+371
-3
lines changed

CHANGELOG.rst

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

4+
2.10.0 (2024-05-09)
5+
------------------
6+
7+
- dynamic model creation support added. See https://pydantic-xml.readthedocs.io/en/latest/pages/misc.html#dynamic-model-creation
8+
9+
410
2.9.2 (2024-04-19)
511
------------------
612

README.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ Features
4242
What is not supported?
4343
______________________
4444

45-
- `dynamic model creation <https://docs.pydantic.dev/usage/models/#dynamic-model-creation>`_
4645
- `dataclasses <https://docs.pydantic.dev/usage/dataclasses/>`_
4746

4847
Getting started

docs/source/pages/misc.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ Standard library serializer also supports customizations.
215215
For more information see :py:func:`xml.etree.ElementTree.tostring`,
216216

217217

218+
Dynamic model creation
219+
~~~~~~~~~~~~~~~~~~~~~~
220+
221+
There are some cases when it is necessary to create a model using runtime information to describe model fields.
222+
For this ``pydantic-xml`` provides the :py:func:`pydantic_xml.create_model` function to create a model on the fly:
223+
224+
.. literalinclude:: ../../../examples/snippets/dynamic_model_creation.py
225+
:language: python
226+
:start-after: model-start
227+
:end-before: model-end
228+
229+
Field specification syntax is similar to ``pydantic`` one. For more information
230+
see the `documentation <https://docs.pydantic.dev/latest/concepts/models/#dynamic-model-creation>`_.
231+
232+
218233
Mypy
219234
~~~~
220235

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pydantic_xml import attr, create_model
2+
3+
# [model-start]
4+
Company = create_model(
5+
'Company',
6+
trade_name=(str, attr(name='trade-name')),
7+
type=(str, attr()),
8+
)
9+
10+
# [model-end]
11+
12+
13+
# [xml-start]
14+
xml_doc = '''
15+
<Company trade-name="SpaceX" type="Private"/>
16+
''' # [xml-end]
17+
18+
# [json-start]
19+
json_doc = '''
20+
{
21+
"trade_name": "SpaceX",
22+
"type": "Private"
23+
}
24+
''' # [json-end]
25+
26+
company = Company.from_xml(xml_doc)
27+
assert company == Company.model_validate_json(json_doc)

pydantic_xml/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from . import config, errors, model
66
from .errors import ModelError, ParsingError
7-
from .model import BaseXmlModel, RootXmlModel, attr, computed_attr, computed_element, element, wrapped
7+
from .model import BaseXmlModel, RootXmlModel, attr, computed_attr, computed_element, create_model, element, wrapped
88

99
__all__ = (
1010
'BaseXmlModel',
@@ -16,6 +16,7 @@
1616
'wrapped',
1717
'computed_attr',
1818
'computed_element',
19+
'create_model',
1920
'errors',
2021
'model',
2122
)

pydantic_xml/model.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
__all__ = (
2121
'attr',
22+
'create_model',
2223
'element',
2324
'wrapped',
2425
'computed_attr',
@@ -250,6 +251,70 @@ def wrapped(
250251
)
251252

252253

254+
Model = TypeVar('Model', bound='BaseXmlModel')
255+
256+
257+
def create_model(
258+
__model_name: str,
259+
*,
260+
__tag__: Optional[str] = None,
261+
__ns__: Optional[str] = None,
262+
__nsmap__: Optional[NsMap] = None,
263+
__ns_attrs__: Optional[bool] = None,
264+
__skip_empty__: Optional[bool] = None,
265+
__search_mode__: Optional[SearchMode] = None,
266+
__base__: Union[Type[Model], Tuple[Type[Model], ...], None] = None,
267+
__module__: Optional[str] = None,
268+
**kwargs: Any,
269+
) -> Type[Model]:
270+
"""
271+
Dynamically creates a new pydantic-xml model.
272+
273+
:param __model_name: model name
274+
:param __tag__: element tag
275+
:param __ns__: element namespace
276+
:param __nsmap__: element namespace map
277+
:param __ns_attrs__: use namespaced attributes
278+
:param __skip_empty__: skip empty elements (elements without sub-elements, attributes and text)
279+
:param __search_mode__: element search mode
280+
:param __base__: model base class
281+
:param __module__: module name that the model belongs to
282+
:param kwargs: pydantic model creation arguments.
283+
See https://docs.pydantic.dev/latest/api/base_model/#pydantic.create_model.
284+
285+
:return: created model
286+
"""
287+
288+
cls_kwargs = kwargs.setdefault('__cls_kwargs__', {})
289+
cls_kwargs['metaclass'] = XmlModelMeta
290+
291+
cls_kwargs['tag'] = __tag__
292+
cls_kwargs['ns'] = __ns__
293+
cls_kwargs['nsmap'] = __nsmap__
294+
cls_kwargs['ns_attrs'] = __ns_attrs__
295+
cls_kwargs['skip_empty'] = __skip_empty__
296+
cls_kwargs['search_mode'] = __search_mode__
297+
298+
model_base: Union[Type[BaseModel], Tuple[Type[BaseModel], ...]] = __base__ or BaseXmlModel
299+
300+
if model_config := kwargs.pop('__config__', None):
301+
# since pydantic create_model function forbids __base__ and __config__ arguments together,
302+
# we create base pydantic class with __config__ and inherit from it
303+
BaseWithConfig = pd.create_model(
304+
f'{__model_name}Base',
305+
__module__=__module__, # type: ignore[arg-type]
306+
__config__=model_config,
307+
)
308+
if not isinstance(model_base, tuple):
309+
model_base = (model_base, BaseWithConfig)
310+
else:
311+
model_base = (*model_base, BaseWithConfig)
312+
313+
model = pd.create_model(__model_name, __base__=model_base, **kwargs)
314+
315+
return typing.cast(Type[Model], model)
316+
317+
253318
@te.dataclass_transform(kw_only_default=True, field_specifiers=(attr, element, wrapped, pd.Field))
254319
class XmlModelMeta(ModelMetaclass):
255320
"""
@@ -305,6 +370,7 @@ def __init_subclass__(
305370
:param ns: element namespace
306371
:param nsmap: element namespace map
307372
:param ns_attrs: use namespaced attributes
373+
:param skip_empty: skip empty elements (elements without sub-elements, attributes and text)
308374
:param search_mode: element search mode
309375
"""
310376

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.9.2"
3+
version = "2.10.0"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <dapper1291@gmail.com>"]
66
license = "Unlicense"

0 commit comments

Comments
 (0)