Skip to content

Commit

Permalink
Merge pull request #190 from dapper91/dev
Browse files Browse the repository at this point in the history
- dynamic model creation support added.
  • Loading branch information
dapper91 authored May 8, 2024
2 parents d92402e + 0e2c209 commit b8348a9
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

2.10.0 (2024-05-09)
------------------

- dynamic model creation support added. See https://pydantic-xml.readthedocs.io/en/latest/pages/misc.html#dynamic-model-creation


2.9.2 (2024-04-19)
------------------

Expand Down
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ Features
What is not supported?
______________________

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

Getting started
Expand Down
15 changes: 15 additions & 0 deletions docs/source/pages/misc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,21 @@ Standard library serializer also supports customizations.
For more information see :py:func:`xml.etree.ElementTree.tostring`,


Dynamic model creation
~~~~~~~~~~~~~~~~~~~~~~

There are some cases when it is necessary to create a model using runtime information to describe model fields.
For this ``pydantic-xml`` provides the :py:func:`pydantic_xml.create_model` function to create a model on the fly:

.. literalinclude:: ../../../examples/snippets/dynamic_model_creation.py
:language: python
:start-after: model-start
:end-before: model-end

Field specification syntax is similar to ``pydantic`` one. For more information
see the `documentation <https://docs.pydantic.dev/latest/concepts/models/#dynamic-model-creation>`_.


Mypy
~~~~

Expand Down
27 changes: 27 additions & 0 deletions examples/snippets/dynamic_model_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pydantic_xml import attr, create_model

# [model-start]
Company = create_model(
'Company',
trade_name=(str, attr(name='trade-name')),
type=(str, attr()),
)

# [model-end]


# [xml-start]
xml_doc = '''
<Company trade-name="SpaceX" type="Private"/>
''' # [xml-end]

# [json-start]
json_doc = '''
{
"trade_name": "SpaceX",
"type": "Private"
}
''' # [json-end]

company = Company.from_xml(xml_doc)
assert company == Company.model_validate_json(json_doc)
3 changes: 2 additions & 1 deletion pydantic_xml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from . import config, errors, model
from .errors import ModelError, ParsingError
from .model import BaseXmlModel, RootXmlModel, attr, computed_attr, computed_element, element, wrapped
from .model import BaseXmlModel, RootXmlModel, attr, computed_attr, computed_element, create_model, element, wrapped

__all__ = (
'BaseXmlModel',
Expand All @@ -16,6 +16,7 @@
'wrapped',
'computed_attr',
'computed_element',
'create_model',
'errors',
'model',
)
66 changes: 66 additions & 0 deletions pydantic_xml/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

__all__ = (
'attr',
'create_model',
'element',
'wrapped',
'computed_attr',
Expand Down Expand Up @@ -250,6 +251,70 @@ def wrapped(
)


Model = TypeVar('Model', bound='BaseXmlModel')


def create_model(
__model_name: str,
*,
__tag__: Optional[str] = None,
__ns__: Optional[str] = None,
__nsmap__: Optional[NsMap] = None,
__ns_attrs__: Optional[bool] = None,
__skip_empty__: Optional[bool] = None,
__search_mode__: Optional[SearchMode] = None,
__base__: Union[Type[Model], Tuple[Type[Model], ...], None] = None,
__module__: Optional[str] = None,
**kwargs: Any,
) -> Type[Model]:
"""
Dynamically creates a new pydantic-xml model.
:param __model_name: model name
:param __tag__: element tag
:param __ns__: element namespace
:param __nsmap__: element namespace map
:param __ns_attrs__: use namespaced attributes
:param __skip_empty__: skip empty elements (elements without sub-elements, attributes and text)
:param __search_mode__: element search mode
:param __base__: model base class
:param __module__: module name that the model belongs to
:param kwargs: pydantic model creation arguments.
See https://docs.pydantic.dev/latest/api/base_model/#pydantic.create_model.
:return: created model
"""

cls_kwargs = kwargs.setdefault('__cls_kwargs__', {})
cls_kwargs['metaclass'] = XmlModelMeta

cls_kwargs['tag'] = __tag__
cls_kwargs['ns'] = __ns__
cls_kwargs['nsmap'] = __nsmap__
cls_kwargs['ns_attrs'] = __ns_attrs__
cls_kwargs['skip_empty'] = __skip_empty__
cls_kwargs['search_mode'] = __search_mode__

model_base: Union[Type[BaseModel], Tuple[Type[BaseModel], ...]] = __base__ or BaseXmlModel

if model_config := kwargs.pop('__config__', None):
# since pydantic create_model function forbids __base__ and __config__ arguments together,
# we create base pydantic class with __config__ and inherit from it
BaseWithConfig = pd.create_model(
f'{__model_name}Base',
__module__=__module__, # type: ignore[arg-type]
__config__=model_config,
)
if not isinstance(model_base, tuple):
model_base = (model_base, BaseWithConfig)
else:
model_base = (*model_base, BaseWithConfig)

model = pd.create_model(__model_name, __base__=model_base, **kwargs)

return typing.cast(Type[Model], model)


@te.dataclass_transform(kw_only_default=True, field_specifiers=(attr, element, wrapped, pd.Field))
class XmlModelMeta(ModelMetaclass):
"""
Expand Down Expand Up @@ -305,6 +370,7 @@ def __init_subclass__(
:param ns: element namespace
:param nsmap: element namespace map
:param ns_attrs: use namespaced attributes
:param skip_empty: skip empty elements (elements without sub-elements, attributes and text)
:param search_mode: element search mode
"""

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.9.2"
version = "2.10.0"
description = "pydantic xml extension"
authors = ["Dmitry Pershin <[email protected]>"]
license = "Unlicense"
Expand Down
Loading

0 comments on commit b8348a9

Please sign in to comment.