├── .coveragerc ├── .flake8 ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── css │ │ └── custom.css │ ├── conf.py │ ├── index.rst │ └── pages │ ├── api.rst │ ├── changelog.rst │ ├── contribute.rst │ ├── data-binding │ ├── aliases.rst │ ├── attributes.rst │ ├── elements.rst │ ├── generics.rst │ ├── heterogeneous.rst │ ├── homogeneous.rst │ ├── index.rst │ ├── mappings.rst │ ├── models.rst │ ├── raw.rst │ ├── text.rst │ ├── unions.rst │ └── wrapper.rst │ ├── installation.rst │ ├── misc.rst │ └── quickstart.rst ├── examples ├── computed-entities │ ├── doc.xml │ └── model.py ├── custom-encoder │ ├── doc.xml │ ├── file1.txt │ ├── file2.txt │ └── model.py ├── generic-model │ ├── doc.json │ ├── doc.xml │ └── model.py ├── quickstart │ ├── doc.json │ ├── doc.xml │ └── model.py ├── self-ref-model │ ├── doc.json │ ├── doc.xml │ └── model.py ├── snippets │ ├── aliases.py │ ├── attribute.py │ ├── attribute_namespace.py │ ├── attribute_namespace_inheritance.py │ ├── default_namespace.py │ ├── dynamic_model_creation.py │ ├── element_model.py │ ├── element_namespace.py │ ├── element_namespace_global.py │ ├── element_namespace_model.py │ ├── element_primitive.py │ ├── element_raw.py │ ├── exclude_none.py │ ├── exclude_unset.py │ ├── homogeneous_dicts.py │ ├── homogeneous_models.py │ ├── homogeneous_models_tuples.py │ ├── homogeneous_primitives.py │ ├── homogeneous_tuples.py │ ├── lxml │ │ └── model_mode_strict.py │ ├── mapping.py │ ├── mapping_element.py │ ├── mapping_typed.py │ ├── model_generic.py │ ├── model_mode_ordered.py │ ├── model_mode_unordered.py │ ├── model_namespace.py │ ├── model_root.py │ ├── model_root_collection.py │ ├── model_root_primitive.py │ ├── model_root_type.py │ ├── model_self_ref.py │ ├── model_template.py │ ├── py3.9 │ │ └── serialization.py │ ├── serialization_nillable.py │ ├── skip_empty.py │ ├── text_primitive.py │ ├── text_primitive_optional.py │ ├── union_discriminated.py │ ├── union_models.py │ ├── union_primitives.py │ ├── wrapper.py │ └── wrapper_nested.py ├── xml-serialization-annotation │ ├── doc.xml │ └── model.py └── xml-serialization-decorator │ ├── doc.xml │ └── model.py ├── pydantic_xml ├── __init__.py ├── config.py ├── element │ ├── __init__.py │ ├── element.py │ ├── native │ │ ├── __init__.py │ │ ├── lxml.py │ │ └── std.py │ └── utils.py ├── errors.py ├── model.py ├── mypy.py ├── py.typed ├── serializers │ ├── __init__.py │ ├── factories │ │ ├── __init__.py │ │ ├── call.py │ │ ├── heterogeneous.py │ │ ├── homogeneous.py │ │ ├── is_instance.py │ │ ├── mapping.py │ │ ├── model.py │ │ ├── named_tuple.py │ │ ├── primitive.py │ │ ├── raw.py │ │ ├── tagged_union.py │ │ ├── tuple.py │ │ ├── typed_mapping.py │ │ ├── union.py │ │ └── wrapper.py │ └── serializer.py ├── typedefs.py └── utils.py ├── pyproject.toml ├── pytest.ini └── tests ├── helpers.py ├── test_computed_fields.py ├── test_dynamic_model_creation.py ├── test_encoder.py ├── test_entity_naming.py ├── test_errors.py ├── test_examples.py ├── test_extra.py ├── test_forward_ref.py ├── test_generics.py ├── test_heterogeneous_collections.py ├── test_homogeneous_collections.py ├── test_mappings.py ├── test_misc.py ├── test_named_tuple.py ├── test_namespaces.py ├── test_preprocessors.py ├── test_primitives.py ├── test_raw.py ├── test_search_modes.py ├── test_submodels.py ├── test_unions.py └── test_wrapped.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | pydantic_xml/mypy.py 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = 4 | tests/** 5 | per-file-ignores = 6 | */__init__.py: F401 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.x' 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip poetry 20 | poetry install 21 | - name: Build and publish 22 | run: | 23 | poetry build 24 | poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - dev 7 | - master 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install poetry 28 | poetry install --no-root -E lxml 29 | - name: Run pre-commit hooks 30 | run: poetry run pre-commit run --hook-stage merge-commit --all-files 31 | - name: Run tests (lxml) 32 | run: PYTHONPATH="$(pwd):$PYTHONPATH" poetry run py.test tests 33 | - name: Run tests (std) 34 | run: PYTHONPATH="$(pwd):$PYTHONPATH" FORCE_STD_XML=true poetry run py.test --cov=pydantic_xml --cov-report=xml tests 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v1 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | files: ./coverage.xml 40 | flags: unittests 41 | fail_ci_if_error: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDEs 132 | .idea 133 | 134 | # poetry 135 | poetry.lock 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: 2 | - commit 3 | - merge-commit 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v5.0.0 8 | hooks: 9 | - id: check-yaml 10 | - id: check-toml 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | stages: 14 | - commit 15 | - id: mixed-line-ending 16 | name: fix line ending 17 | stages: 18 | - commit 19 | args: 20 | - --fix=lf 21 | - id: mixed-line-ending 22 | name: check line ending 23 | stages: 24 | - merge-commit 25 | args: 26 | - --fix=no 27 | - repo: https://github.com/asottile/add-trailing-comma 28 | rev: v3.1.0 29 | hooks: 30 | - id: add-trailing-comma 31 | stages: 32 | - commit 33 | - repo: https://github.com/hhatto/autopep8 34 | rev: v2.3.1 35 | hooks: 36 | - id: autopep8 37 | stages: 38 | - commit 39 | args: 40 | - --diff 41 | - repo: https://github.com/pycqa/flake8 42 | rev: 7.1.1 43 | hooks: 44 | - id: flake8 45 | - repo: https://github.com/pycqa/isort 46 | rev: 5.13.2 47 | hooks: 48 | - id: isort 49 | name: fix import order 50 | stages: 51 | - commit 52 | args: 53 | - --line-length=120 54 | - --multi-line=9 55 | - --project=pydantic_xml 56 | - id: isort 57 | name: check import order 58 | stages: 59 | - merge-commit 60 | args: 61 | - --check-only 62 | - --line-length=120 63 | - --multi-line=9 64 | - --project=pydantic_xml 65 | - repo: https://github.com/pre-commit/mirrors-mypy 66 | rev: v1.13.0 67 | hooks: 68 | - id: mypy 69 | stages: 70 | - commit 71 | name: mypy 72 | pass_filenames: false 73 | args: ["--package", "pydantic_xml"] 74 | additional_dependencies: 75 | - pydantic>=2.6.0 76 | - lxml-stubs>=0.4.0 77 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.11" 9 | 10 | sphinx: 11 | configuration: docs/source/conf.py 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - lxml 19 | - docs 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | pydantic-xml extension 3 | ====================== 4 | 5 | .. image:: https://static.pepy.tech/personalized-badge/pydantic-xml?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month 6 | :target: https://pepy.tech/project/pydantic-xml 7 | :alt: Downloads/month 8 | .. image:: https://github.com/dapper91/pydantic-xml/actions/workflows/test.yml/badge.svg?branch=master 9 | :target: https://github.com/dapper91/pydantic-xml/actions/workflows/test.yml 10 | :alt: Build status 11 | .. image:: https://img.shields.io/pypi/l/pydantic-xml.svg 12 | :target: https://pypi.org/project/pydantic-xml 13 | :alt: License 14 | .. image:: https://img.shields.io/pypi/pyversions/pydantic-xml.svg 15 | :target: https://pypi.org/project/pydantic-xml 16 | :alt: Supported Python versions 17 | .. image:: https://codecov.io/gh/dapper91/pydantic-xml/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/dapper91/pydantic-xml 19 | :alt: Code coverage 20 | .. image:: https://readthedocs.org/projects/pydantic-xml/badge/?version=stable&style=flat 21 | :alt: ReadTheDocs status 22 | :target: https://pydantic-xml.readthedocs.io 23 | 24 | 25 | ``pydantic-xml`` is a `pydantic `_ extension providing model fields xml binding 26 | and xml serialization / deserialization. 27 | It is closely integrated with ``pydantic`` which means it supports most of its features. 28 | 29 | 30 | Features 31 | -------- 32 | 33 | - pydantic v1 / v2 support 34 | - flexable attributes, elements and text binding 35 | - python collection types support (``Dict``, ``TypedDict``, ``List``, ``Set``, ``Tuple``, ...) 36 | - ``Union`` type support 37 | - pydantic `generic models `_ support 38 | - pydantic `computed fields `_ support 39 | - `lxml `_ xml parser support 40 | - ``xml.etree.ElementTree`` standard library xml parser support 41 | 42 | What is not supported? 43 | ______________________ 44 | 45 | - `dataclasses `_ 46 | - `callable discriminators `_ 47 | 48 | Getting started 49 | --------------- 50 | 51 | The following model fields binding: 52 | 53 | .. code-block:: python 54 | 55 | class Product(BaseXmlModel): 56 | status: Literal['running', 'development'] = attr() # extracted from the 'status' attribute 57 | launched: Optional[int] = attr(default=None) # extracted from the 'launched' attribute 58 | title: str # extracted from the element text 59 | 60 | 61 | class Company(BaseXmlModel): 62 | trade_name: str = attr(name='trade-name') # extracted from the 'trade-name' attribute 63 | website: HttpUrl = element() # extracted from the 'website' element text 64 | products: List[Product] = element(tag='product', default=[]) # extracted from the 'Company' element's children 65 | 66 | defines the XML document: 67 | 68 | .. code-block:: xml 69 | 70 | 71 | https://www.spacex.com 72 | Several launch vehicles 73 | Starlink 74 | Starship 75 | 76 | 77 | 78 | See `documentation `_ for more details. 79 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 55em; 3 | } 4 | 5 | .sidebar-drawer { 6 | width: 15em; 7 | } 8 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import sys 10 | from pathlib import Path 11 | 12 | import toml 13 | 14 | THIS_PATH = Path(__file__).parent 15 | ROOT_PATH = THIS_PATH.parent.parent 16 | sys.path.insert(0, str(ROOT_PATH)) 17 | 18 | PYPROJECT = toml.load(ROOT_PATH / 'pyproject.toml') 19 | PROJECT_INFO = PYPROJECT['tool']['poetry'] 20 | 21 | project = PROJECT_INFO['name'] 22 | copyright = f"2023, {PROJECT_INFO['name']}" 23 | author = PROJECT_INFO['authors'][0] 24 | release = PROJECT_INFO['version'] 25 | 26 | # -- General configuration --------------------------------------------------- 27 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 28 | 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.doctest', 32 | 'sphinx.ext.intersphinx', 33 | 'sphinx.ext.autosectionlabel', 34 | 'sphinx.ext.viewcode', 35 | 'sphinx_copybutton', 36 | 'sphinx_design', 37 | ] 38 | 39 | intersphinx_mapping = { 40 | 'python': ('https://docs.python.org/3', None), 41 | 'lxml': ('https://lxml.de/apidoc/', None), 42 | } 43 | 44 | autodoc_typehints = 'description' 45 | autodoc_typehints_format = 'short' 46 | autodoc_member_order = 'bysource' 47 | autodoc_default_options = { 48 | 'show-inheritance': True, 49 | } 50 | 51 | autosectionlabel_prefix_document = True 52 | 53 | html_theme_options = {} 54 | html_title = PROJECT_INFO['name'] 55 | 56 | templates_path = ['_templates'] 57 | exclude_patterns = [] 58 | 59 | # -- Options for HTML output ------------------------------------------------- 60 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 61 | 62 | html_theme = 'furo' 63 | html_static_path = ['_static'] 64 | html_css_files = ['css/custom.css'] 65 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pydantic-xml documentation master file, created by 2 | sphinx-quickstart on Sat Jan 28 18:00:17 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../../README.rst 7 | 8 | 9 | User Guide 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | pages/quickstart 16 | pages/installation 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | pages/data-binding/index 22 | pages/misc 23 | 24 | .. toctree:: 25 | :maxdepth: 1 26 | 27 | pages/changelog 28 | 29 | 30 | API Documentation 31 | ----------------- 32 | 33 | .. toctree:: 34 | :maxdepth: 1 35 | 36 | pages/api 37 | 38 | 39 | Contribute 40 | ---------- 41 | 42 | .. toctree:: 43 | :maxdepth: 1 44 | 45 | pages/contribute 46 | 47 | 48 | Links 49 | ----- 50 | 51 | - `Source code `_ 52 | - `Pydantic documentation `_ 53 | 54 | 55 | Indices and tables 56 | ================== 57 | 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | -------------------------------------------------------------------------------- /docs/source/pages/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | 4 | Developer Interface 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | .. automodule:: pydantic_xml 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/source/pages/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | .. include:: ../../../CHANGELOG.rst 4 | -------------------------------------------------------------------------------- /docs/source/pages/contribute.rst: -------------------------------------------------------------------------------- 1 | .. _contribute: 2 | 3 | 4 | Development 5 | ~~~~~~~~~~~ 6 | 7 | Initialize the development environment installing dev dependencies: 8 | 9 | .. code-block:: console 10 | 11 | $ poetry install --no-root 12 | 13 | 14 | Code style 15 | __________ 16 | 17 | After any code changes made make sure that the code style is followed. 18 | To control that automatically install pre-commit hooks: 19 | 20 | .. code-block:: console 21 | 22 | $ pre-commit install 23 | 24 | They will be checking your changes for the coding conventions used in the project before any commit. 25 | 26 | 27 | Pull Requests 28 | _____________ 29 | 30 | The library supports both pydantic versions: `v1 `_ (legacy) 31 | and `v2 `_ (latest). 32 | Since version 1 is outdated only bugfixes and security fixes will be accepted. 33 | New features should be targeted to version 2. 34 | 35 | Version 1 36 | ********* 37 | 38 | To make a PR to version 1 checkout branch ``v1`` and create a new branch implementing your changes. 39 | 40 | Version 2 41 | ********* 42 | 43 | To contribute to version 2 checkout branch ``dev``, create a feature branch and make a pull request setting 44 | ``dev`` as a target. 45 | 46 | 47 | Documentation 48 | _____________ 49 | 50 | If you've made any changes to the documentation, make sure it builds successfully. 51 | To build the documentation follow the instructions: 52 | 53 | - Install documentation generation dependencies: 54 | 55 | .. code-block:: console 56 | 57 | $ poetry install -E docs 58 | 59 | - Build the documentation: 60 | 61 | .. code-block:: console 62 | 63 | $ cd docs 64 | $ make html 65 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/aliases.rst: -------------------------------------------------------------------------------- 1 | .. _aliases: 2 | 3 | Pydantic aliases 4 | ________________ 5 | 6 | Aliased fields 7 | ************** 8 | 9 | ``pydantic`` library allows to set the alias for a field that is used during serialization/deserialization 10 | instead of the field name. ``pydantic-xml`` respects field aliases too: 11 | 12 | .. grid:: 2 13 | :gutter: 2 14 | 15 | .. grid-item-card:: Model 16 | 17 | .. literalinclude:: ../../../../examples/snippets/aliases.py 18 | :language: python 19 | :start-after: model-start 20 | :end-before: model-end 21 | 22 | .. grid-item-card:: Document 23 | 24 | .. tab-set:: 25 | 26 | .. tab-item:: XML 27 | 28 | .. literalinclude:: ../../../../examples/snippets/aliases.py 29 | :language: xml 30 | :lines: 2- 31 | :start-after: xml-start 32 | :end-before: xml-end 33 | 34 | .. tab-item:: JSON 35 | 36 | .. literalinclude:: ../../../../examples/snippets/aliases.py 37 | :language: json 38 | :lines: 2- 39 | :start-after: json-start 40 | :end-before: json-end 41 | 42 | 43 | Template models 44 | *************** 45 | 46 | ``pydantic`` aliases make it possible to declare so-called template models. 47 | The base model implements the data-validation and data-processing logic but 48 | the fields mapping is described in the inherited classes: 49 | 50 | .. grid:: 2 51 | :gutter: 2 52 | 53 | .. grid-item-card:: Model 54 | 55 | .. literalinclude:: ../../../../examples/snippets/model_template.py 56 | :language: python 57 | :start-after: model-start 58 | :end-before: model-end 59 | 60 | .. grid-item-card:: Document 61 | 62 | .. tab-set:: 63 | 64 | .. tab-item:: XML 65 | 66 | .. literalinclude:: ../../../../examples/snippets/model_template.py 67 | :language: xml 68 | :lines: 2- 69 | :start-after: xml-start 70 | :end-before: xml-end 71 | 72 | .. tab-item:: JSON 73 | 74 | .. literalinclude:: ../../../../examples/snippets/model_template.py 75 | :language: json 76 | :lines: 2- 77 | :start-after: json-start 78 | :end-before: json-end 79 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/attributes.rst: -------------------------------------------------------------------------------- 1 | .. _attributes: 2 | 3 | 4 | Attributes 5 | __________ 6 | 7 | Primitive types 8 | *************** 9 | 10 | A field of a primitive type marked as :py:func:`pydantic_xml.attr` is bound to a local element attribute. 11 | Parameter ``name`` is used to declare the attribute name from which the data is extracted. 12 | If it is omitted the field name is used (respecting ``pydantic`` field aliases). 13 | 14 | .. grid:: 2 15 | :gutter: 2 16 | 17 | .. grid-item-card:: Model 18 | 19 | .. literalinclude:: ../../../../examples/snippets/attribute.py 20 | :language: python 21 | :start-after: model-start 22 | :end-before: model-end 23 | 24 | .. grid-item-card:: Document 25 | 26 | .. tab-set:: 27 | 28 | .. tab-item:: XML 29 | 30 | .. literalinclude:: ../../../../examples/snippets/attribute.py 31 | :language: xml 32 | :lines: 2- 33 | :start-after: xml-start 34 | :end-before: xml-end 35 | 36 | .. tab-item:: JSON 37 | 38 | .. literalinclude:: ../../../../examples/snippets/attribute.py 39 | :language: json 40 | :lines: 2- 41 | :start-after: json-start 42 | :end-before: json-end 43 | 44 | 45 | Namespaces 46 | ********** 47 | 48 | The namespace can be defined for attributes as well. To bind a model field to a namespaced attribute 49 | pass parameter ``ns`` to a :py:func:`pydantic_xml.attr` and define a namespace map for the model. 50 | 51 | .. grid:: 2 52 | :gutter: 2 53 | 54 | .. grid-item-card:: Model 55 | 56 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace.py 57 | :language: python 58 | :start-after: model-start 59 | :end-before: model-end 60 | 61 | .. grid-item-card:: Document 62 | 63 | .. tab-set:: 64 | 65 | .. tab-item:: XML 66 | 67 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace.py 68 | :language: xml 69 | :lines: 2- 70 | :start-after: xml-start 71 | :end-before: xml-end 72 | 73 | .. tab-item:: JSON 74 | 75 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace.py 76 | :language: json 77 | :lines: 2- 78 | :start-after: json-start 79 | :end-before: json-end 80 | 81 | 82 | Namespace inheritance 83 | ********************* 84 | 85 | The attribute namespace can be inherited from the model. 86 | To make attributes inherit the model namespace define the model-level namespace and namespace map 87 | and set ``ns_attrs`` flag. 88 | 89 | .. grid:: 2 90 | :gutter: 2 91 | 92 | .. grid-item-card:: Model 93 | 94 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace_inheritance.py 95 | :language: python 96 | :start-after: model-start 97 | :end-before: model-end 98 | 99 | .. grid-item-card:: Document 100 | 101 | .. tab-set:: 102 | 103 | .. tab-item:: XML 104 | 105 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace_inheritance.py 106 | :language: xml 107 | :lines: 2- 108 | :start-after: xml-start 109 | :end-before: xml-end 110 | 111 | .. tab-item:: JSON 112 | 113 | .. literalinclude:: ../../../../examples/snippets/attribute_namespace_inheritance.py 114 | :language: json 115 | :lines: 2- 116 | :start-after: json-start 117 | :end-before: json-end 118 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/generics.rst: -------------------------------------------------------------------------------- 1 | .. _generics: 2 | 3 | 4 | Generic models 5 | ______________ 6 | 7 | ``pydantic`` library supports `generic-models `_. 8 | Generic xml model can be declared the same way: 9 | 10 | .. grid:: 2 11 | :gutter: 2 12 | 13 | .. grid-item-card:: Model 14 | 15 | .. literalinclude:: ../../../../examples/snippets/model_generic.py 16 | :language: python 17 | :start-after: model-start 18 | :end-before: model-end 19 | 20 | .. grid-item-card:: Document 21 | 22 | .. tab-set:: 23 | 24 | .. tab-item:: XML1 25 | 26 | .. literalinclude:: ../../../../examples/snippets/model_generic.py 27 | :language: xml 28 | :lines: 2- 29 | :start-after: xml-start 30 | :end-before: xml-end 31 | 32 | .. tab-item:: JSON1 33 | 34 | .. literalinclude:: ../../../../examples/snippets/model_generic.py 35 | :language: json 36 | :lines: 2- 37 | :start-after: json-start 38 | :end-before: json-end 39 | 40 | .. tab-item:: XML2 41 | 42 | .. literalinclude:: ../../../../examples/snippets/model_generic.py 43 | :language: xml 44 | :lines: 2- 45 | :start-after: xml-start-2 46 | :end-before: xml-end-2 47 | 48 | .. tab-item:: JSON2 49 | 50 | .. literalinclude:: ../../../../examples/snippets/model_generic.py 51 | :language: json 52 | :lines: 2- 53 | :start-after: json-start-2 54 | :end-before: json-end-2 55 | 56 | 57 | A generic model can be of one or more types and organized in a recursive structure. 58 | The following example illustrate how to describes a flexable SOAP request model: 59 | 60 | *model.py:* 61 | 62 | .. literalinclude:: ../../../../examples/generic-model/model.py 63 | :language: python 64 | 65 | *doc.xml:* 66 | 67 | .. literalinclude:: ../../../../examples/generic-model/doc.xml 68 | :language: xml 69 | 70 | *doc.json:* 71 | 72 | .. literalinclude:: ../../../../examples/generic-model/doc.json 73 | :language: json 74 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/heterogeneous.rst: -------------------------------------------------------------------------------- 1 | .. _heterogeneous: 2 | 3 | 4 | Heterogeneous collections 5 | _________________________ 6 | 7 | Heterogeneous collections are similar to :ref:`homogeneous collections ` 8 | except that the number of the elements is predefined which means they follow the same binding rules. 9 | The most common heterogeneous collection is :py:obj:`typing.Tuple` (like ``Tuple[int, int, int]``) 10 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/homogeneous.rst: -------------------------------------------------------------------------------- 1 | .. _homogeneous: 2 | 3 | 4 | Homogeneous collections 5 | _______________________ 6 | 7 | Homogeneous collection is a collection of same type elements. 8 | The most common homogeneous collections are :py:obj:`typing.List`, :py:obj:`typing.Set` and 9 | variable-length tuple :py:obj:`typing.Tuple` (like ``Tuple[int, ...]``) 10 | 11 | 12 | Primitive homogeneous collection 13 | ******************************** 14 | 15 | A field of a primitive homogeneous collection type marked as :py:func:`pydantic_xml.element` is bound 16 | to the sub-elements texts. 17 | 18 | .. grid:: 2 19 | :gutter: 2 20 | 21 | .. grid-item-card:: Model 22 | 23 | .. literalinclude:: ../../../../examples/snippets/homogeneous_primitives.py 24 | :language: python 25 | :start-after: model-start 26 | :end-before: model-end 27 | 28 | .. grid-item-card:: Document 29 | 30 | .. tab-set:: 31 | 32 | .. tab-item:: XML 33 | 34 | .. literalinclude:: ../../../../examples/snippets/homogeneous_primitives.py 35 | :language: xml 36 | :lines: 2- 37 | :start-after: xml-start 38 | :end-before: xml-end 39 | 40 | .. tab-item:: JSON 41 | 42 | .. literalinclude:: ../../../../examples/snippets/homogeneous_primitives.py 43 | :language: json 44 | :lines: 2- 45 | :start-after: json-start 46 | :end-before: json-end 47 | 48 | 49 | Model homogeneous collection 50 | **************************** 51 | 52 | A field of a model homogeneous collection type is bound to sub-elements. Then the sub-element is used 53 | as the root for that sub-model. For more information see :ref:`model data binding `. 54 | The ``tag`` parameter is used to declare sub-elements tag to which the sub-models are bound. 55 | If it is omitted the sub-model ``tag`` parameter is used. 56 | If it is omitted too field name is used (respecting ``pydantic`` field aliases). 57 | 58 | .. grid:: 2 59 | :gutter: 2 60 | 61 | .. grid-item-card:: Model 62 | 63 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models.py 64 | :language: python 65 | :start-after: model-start 66 | :end-before: model-end 67 | 68 | .. grid-item-card:: Document 69 | 70 | .. tab-set:: 71 | 72 | .. tab-item:: XML 73 | 74 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models.py 75 | :language: xml 76 | :lines: 2- 77 | :start-after: xml-start 78 | :end-before: xml-end 79 | 80 | .. tab-item:: JSON 81 | 82 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models.py 83 | :language: json 84 | :lines: 2- 85 | :start-after: json-start 86 | :end-before: json-end 87 | 88 | 89 | Dict homogeneous collection 90 | *************************** 91 | 92 | A field of a mapping homogeneous collection type is bound to sub-elements attributes: 93 | 94 | .. grid:: 2 95 | :gutter: 2 96 | 97 | .. grid-item-card:: Model 98 | 99 | .. literalinclude:: ../../../../examples/snippets/homogeneous_dicts.py 100 | :language: python 101 | :start-after: model-start 102 | :end-before: model-end 103 | 104 | .. grid-item-card:: Document 105 | 106 | .. tab-set:: 107 | 108 | .. tab-item:: XML 109 | 110 | .. literalinclude:: ../../../../examples/snippets/homogeneous_dicts.py 111 | :language: xml 112 | :lines: 2- 113 | :start-after: xml-start 114 | :end-before: xml-end 115 | 116 | .. tab-item:: JSON 117 | 118 | .. literalinclude:: ../../../../examples/snippets/homogeneous_dicts.py 119 | :language: json 120 | :lines: 2- 121 | :start-after: json-start 122 | :end-before: json-end 123 | 124 | 125 | Adjacent sub-elements 126 | ********************* 127 | 128 | Some xml documents contain a list of adjacent elements related to each other. 129 | To group such elements a homogeneous collection of heterogeneous ones may be used: 130 | 131 | .. grid:: 2 132 | :gutter: 2 133 | 134 | .. grid-item-card:: Model 135 | 136 | .. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py 137 | :language: python 138 | :start-after: model-start 139 | :end-before: model-end 140 | 141 | .. grid-item-card:: Document 142 | 143 | .. tab-set:: 144 | 145 | .. tab-item:: XML 146 | 147 | .. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py 148 | :language: xml 149 | :lines: 2- 150 | :start-after: xml-start 151 | :end-before: xml-end 152 | 153 | .. tab-item:: JSON 154 | 155 | .. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py 156 | :language: json 157 | :lines: 2- 158 | :start-after: json-start 159 | :end-before: json-end 160 | 161 | 162 | To group sub-elements with different tags it is necessary to declare a sub-model for each one: 163 | 164 | .. grid:: 2 165 | :gutter: 2 166 | 167 | .. grid-item-card:: Model 168 | 169 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py 170 | :language: python 171 | :start-after: model-start 172 | :end-before: model-end 173 | 174 | .. grid-item-card:: Document 175 | 176 | .. tab-set:: 177 | 178 | .. tab-item:: XML 179 | 180 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py 181 | :language: xml 182 | :lines: 2- 183 | :start-after: xml-start 184 | :end-before: xml-end 185 | 186 | .. tab-item:: JSON 187 | 188 | .. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py 189 | :language: json 190 | :lines: 2- 191 | :start-after: json-start 192 | :end-before: json-end 193 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/index.rst: -------------------------------------------------------------------------------- 1 | .. _data_binding: 2 | 3 | Data binding 4 | ~~~~~~~~~~~~ 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | text 10 | attributes 11 | elements 12 | models 13 | unions 14 | mappings 15 | homogeneous 16 | heterogeneous 17 | generics 18 | wrapper 19 | aliases 20 | raw 21 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/mappings.rst: -------------------------------------------------------------------------------- 1 | .. _mappings: 2 | 3 | 4 | Mappings 5 | ________ 6 | 7 | Local attributes 8 | **************** 9 | 10 | A field of a mapping type is bound to local element attributes. 11 | 12 | .. grid:: 2 13 | :gutter: 2 14 | 15 | .. grid-item-card:: Model 16 | 17 | .. literalinclude:: ../../../../examples/snippets/mapping.py 18 | :language: python 19 | :start-after: model-start 20 | :end-before: model-end 21 | 22 | .. grid-item-card:: Document 23 | 24 | .. tab-set:: 25 | 26 | .. tab-item:: XML 27 | 28 | .. literalinclude:: ../../../../examples/snippets/mapping.py 29 | :language: xml 30 | :lines: 2- 31 | :start-after: xml-start 32 | :end-before: xml-end 33 | 34 | .. tab-item:: JSON 35 | 36 | .. literalinclude:: ../../../../examples/snippets/mapping.py 37 | :language: json 38 | :lines: 2- 39 | :start-after: json-start 40 | :end-before: json-end 41 | 42 | 43 | Sub-element attributes 44 | ********************** 45 | 46 | A field of a mapping type marked as :py:func:`pydantic_xml.element` is bound to sub-element attributes. 47 | The ``tag`` parameter of :py:func:`pydantic_xml.element` is used as a sub-element tag to which attributes 48 | the field is bound. If it is omitted the field name is used (respecting ``pydantic`` field aliases). 49 | 50 | .. grid:: 2 51 | :gutter: 2 52 | 53 | .. grid-item-card:: Model 54 | 55 | .. literalinclude:: ../../../../examples/snippets/mapping_element.py 56 | :language: python 57 | :start-after: model-start 58 | :end-before: model-end 59 | 60 | .. grid-item-card:: Document 61 | 62 | .. tab-set:: 63 | 64 | .. tab-item:: XML 65 | 66 | .. literalinclude:: ../../../../examples/snippets/mapping_element.py 67 | :language: xml 68 | :lines: 2- 69 | :start-after: xml-start 70 | :end-before: xml-end 71 | 72 | .. tab-item:: JSON 73 | 74 | .. literalinclude:: ../../../../examples/snippets/mapping_element.py 75 | :language: json 76 | :lines: 2- 77 | :start-after: json-start 78 | :end-before: json-end 79 | 80 | 81 | Typed dict 82 | ********** 83 | 84 | Fields of :py:class:`typing.TypedDict` type are also supported: 85 | 86 | 87 | .. grid:: 2 88 | :gutter: 2 89 | 90 | .. grid-item-card:: Model 91 | 92 | .. literalinclude:: ../../../../examples/snippets/mapping_typed.py 93 | :language: python 94 | :start-after: model-start 95 | :end-before: model-end 96 | 97 | .. grid-item-card:: Document 98 | 99 | .. tab-set:: 100 | 101 | .. tab-item:: XML 102 | 103 | .. literalinclude:: ../../../../examples/snippets/mapping_typed.py 104 | :language: xml 105 | :lines: 2- 106 | :start-after: xml-start 107 | :end-before: xml-end 108 | 109 | .. tab-item:: JSON 110 | 111 | .. literalinclude:: ../../../../examples/snippets/mapping_typed.py 112 | :language: json 113 | :lines: 2- 114 | :start-after: json-start 115 | :end-before: json-end 116 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/raw.rst: -------------------------------------------------------------------------------- 1 | .. _raw_fields: 2 | 3 | 4 | Raw fields 5 | __________ 6 | 7 | Raw element typed fields 8 | ************************ 9 | 10 | The library supports raw xml elements. It is helpful when the element schema is unknown or its schema is too complex 11 | to define a model describing it. 12 | 13 | To declare a raw element field annotate it with :py:class:`xml.etree.ElementTree.Element` 14 | (or :py:class:`lxml.etree._Element` for ``lxml``). 15 | 16 | Since ``pydantic`` doesn't support arbitrary types by default it is necessary to allow them 17 | by setting ``arbitrary_types_allowed`` flag. 18 | See `documentation `_ for more details. 19 | 20 | 21 | .. grid:: 2 22 | :gutter: 2 23 | 24 | .. grid-item-card:: Model 25 | 26 | .. literalinclude:: ../../../../examples/snippets/element_raw.py 27 | :language: python 28 | :start-after: model-start 29 | :end-before: model-end 30 | 31 | .. grid-item-card:: Document 32 | 33 | .. tab-set:: 34 | 35 | .. tab-item:: input XML 36 | 37 | .. literalinclude:: ../../../../examples/snippets/element_raw.py 38 | :language: xml 39 | :lines: 2- 40 | :start-after: xml-start-1 41 | :end-before: xml-end-1 42 | 43 | .. tab-item:: output XML 44 | 45 | .. literalinclude:: ../../../../examples/snippets/element_raw.py 46 | :language: xml 47 | :lines: 2- 48 | :start-after: xml-start-2 49 | :end-before: xml-end-2 50 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/text.rst: -------------------------------------------------------------------------------- 1 | .. _text: 2 | 3 | Text 4 | ____ 5 | 6 | Primitive types 7 | *************** 8 | 9 | A field of a primitive type is bound to the local element text. 10 | 11 | .. grid:: 2 12 | :gutter: 2 13 | 14 | .. grid-item-card:: Model 15 | 16 | .. literalinclude:: ../../../../examples/snippets/text_primitive.py 17 | :language: python 18 | :start-after: model-start 19 | :end-before: model-end 20 | 21 | .. grid-item-card:: Document 22 | 23 | .. tab-set:: 24 | 25 | .. tab-item:: XML 26 | 27 | .. literalinclude:: ../../../../examples/snippets/text_primitive.py 28 | :language: xml 29 | :lines: 2- 30 | :start-after: xml-start 31 | :end-before: xml-end 32 | 33 | .. tab-item:: JSON 34 | 35 | .. literalinclude:: ../../../../examples/snippets/text_primitive.py 36 | :language: json 37 | :lines: 2- 38 | :start-after: json-start 39 | :end-before: json-end 40 | 41 | **Note**: the empty element text is deserialized as ``None`` not as an empty string: 42 | 43 | .. grid:: 2 44 | :gutter: 2 45 | 46 | .. grid-item-card:: Model 47 | 48 | .. literalinclude:: ../../../../examples/snippets/text_primitive_optional.py 49 | :language: python 50 | :start-after: model-start 51 | :end-before: model-end 52 | 53 | .. grid-item-card:: Document 54 | 55 | .. tab-set:: 56 | 57 | .. tab-item:: XML 58 | 59 | .. literalinclude:: ../../../../examples/snippets/text_primitive_optional.py 60 | :language: xml 61 | :lines: 2- 62 | :start-after: xml-start 63 | :end-before: xml-end 64 | 65 | .. tab-item:: JSON 66 | 67 | .. literalinclude:: ../../../../examples/snippets/text_primitive_optional.py 68 | :language: json 69 | :lines: 2- 70 | :start-after: json-start 71 | :end-before: json-end 72 | 73 | To have an empty string instead add ``""`` as a default value: 74 | 75 | .. code-block:: python 76 | 77 | class Company(BaseXmlModel): 78 | description: str = "" 79 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/unions.rst: -------------------------------------------------------------------------------- 1 | .. _unions: 2 | 3 | 4 | Union types 5 | ___________ 6 | 7 | To declare a field that can be of one type or anther :py:obj:`typing.Union` is used. 8 | It works for primitive types and models as well but not combined together. 9 | 10 | The type declaration order matters. 11 | Currently, ``pydantic`` has two validation modes: 12 | 13 | - ``left_to_right``, where the first successful validation is accepted, 14 | - ``smart`` (default), the first type that matches (without coercion) wins. 15 | 16 | You can read more about it in the 17 | `pydantic docs `_. 18 | 19 | Primitive types 20 | *************** 21 | 22 | Union can be applied to a text, attributes or elements. 23 | 24 | .. grid:: 2 25 | :gutter: 2 26 | 27 | .. grid-item-card:: Model 28 | 29 | .. literalinclude:: ../../../../examples/snippets/union_primitives.py 30 | :language: python 31 | :start-after: model-start 32 | :end-before: model-end 33 | 34 | .. grid-item-card:: Document 35 | 36 | .. tab-set:: 37 | 38 | .. tab-item:: XML 39 | 40 | .. literalinclude:: ../../../../examples/snippets/union_primitives.py 41 | :language: xml 42 | :lines: 2- 43 | :start-after: xml-start 44 | :end-before: xml-end 45 | 46 | .. tab-item:: JSON 47 | 48 | .. literalinclude:: ../../../../examples/snippets/union_primitives.py 49 | :language: json 50 | :lines: 2- 51 | :start-after: json-start 52 | :end-before: json-end 53 | 54 | 55 | Model types 56 | *********** 57 | 58 | Union can be applied to model types either. 59 | 60 | .. grid:: 2 61 | :gutter: 2 62 | 63 | .. grid-item-card:: Model 64 | 65 | .. literalinclude:: ../../../../examples/snippets/union_models.py 66 | :language: python 67 | :start-after: model-start 68 | :end-before: model-end 69 | 70 | .. grid-item-card:: Document 71 | 72 | .. tab-set:: 73 | 74 | .. tab-item:: XML 75 | 76 | .. literalinclude:: ../../../../examples/snippets/union_models.py 77 | :language: xml 78 | :lines: 2- 79 | :start-after: xml-start 80 | :end-before: xml-end 81 | 82 | .. tab-item:: JSON 83 | 84 | .. literalinclude:: ../../../../examples/snippets/union_models.py 85 | :language: json 86 | :lines: 2- 87 | :start-after: json-start 88 | :end-before: json-end 89 | 90 | 91 | Discriminated unions 92 | ******************** 93 | 94 | Pydantic supports so called 95 | `discriminated unions `_ - 96 | the unions where the sub-model type is selected based on its field value. 97 | 98 | ``pydantic-xml`` supports the similar mechanism to distinguish one sub-model from another by its xml attribute value: 99 | 100 | 101 | .. grid:: 2 102 | :gutter: 2 103 | 104 | .. grid-item-card:: Model 105 | 106 | .. literalinclude:: ../../../../examples/snippets/union_discriminated.py 107 | :language: python 108 | :start-after: model-start 109 | :end-before: model-end 110 | 111 | .. grid-item-card:: Document 112 | 113 | .. tab-set:: 114 | 115 | .. tab-item:: XML 116 | 117 | .. literalinclude:: ../../../../examples/snippets/union_discriminated.py 118 | :language: xml 119 | :lines: 2- 120 | :start-after: xml-start 121 | :end-before: xml-end 122 | 123 | .. tab-item:: JSON 124 | 125 | .. literalinclude:: ../../../../examples/snippets/union_discriminated.py 126 | :language: json 127 | :lines: 2- 128 | :start-after: json-start 129 | :end-before: json-end 130 | -------------------------------------------------------------------------------- /docs/source/pages/data-binding/wrapper.rst: -------------------------------------------------------------------------------- 1 | .. _wrapper: 2 | 3 | Wrapper 4 | _______ 5 | 6 | Some XML documents have deep element hierarchy which requires to declare a lot of "dumb" sub-models 7 | to extract the deepest element data. :py:func:`pydantic_xml.wrapped` helps to get rid of that boilerplate code. 8 | 9 | 10 | Wrapped entities 11 | **************** 12 | 13 | To declare a field bound to a sub-element text, attribute or element mark that field 14 | as :py:func:`pydantic_xml.wrapped` providing it with the sub-element path and the entity type. It can be applied to 15 | a primitive type, model, mapping or collection as well. 16 | 17 | .. grid:: 2 18 | :gutter: 2 19 | 20 | .. grid-item-card:: Model 21 | 22 | .. literalinclude:: ../../../../examples/snippets/wrapper.py 23 | :language: python 24 | :start-after: model-start 25 | :end-before: model-end 26 | 27 | .. grid-item-card:: Document 28 | 29 | .. tab-set:: 30 | 31 | .. tab-item:: XML 32 | 33 | .. literalinclude:: ../../../../examples/snippets/wrapper.py 34 | :language: xml 35 | :lines: 2- 36 | :start-after: xml-start 37 | :end-before: xml-end 38 | 39 | .. tab-item:: JSON 40 | 41 | .. literalinclude:: ../../../../examples/snippets/wrapper.py 42 | :language: json 43 | :lines: 2- 44 | :start-after: json-start 45 | :end-before: json-end 46 | 47 | 48 | Nested wrappers 49 | *************** 50 | 51 | Wrapper can be wrapped by another wrapper building up a nested structure. 52 | It helps to extract data from sub-elements from different namespaces: 53 | 54 | .. grid:: 2 55 | :gutter: 2 56 | 57 | .. grid-item-card:: Model 58 | 59 | .. literalinclude:: ../../../../examples/snippets/wrapper_nested.py 60 | :language: python 61 | :start-after: model-start 62 | :end-before: model-end 63 | 64 | .. grid-item-card:: Document 65 | 66 | .. tab-set:: 67 | 68 | .. tab-item:: XML 69 | 70 | .. literalinclude:: ../../../../examples/snippets/wrapper_nested.py 71 | :language: xml 72 | :lines: 2- 73 | :start-after: xml-start 74 | :end-before: xml-end 75 | 76 | .. tab-item:: JSON 77 | 78 | .. literalinclude:: ../../../../examples/snippets/wrapper_nested.py 79 | :language: json 80 | :lines: 2- 81 | :start-after: json-start 82 | :end-before: json-end 83 | -------------------------------------------------------------------------------- /docs/source/pages/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | 4 | Installation 5 | ~~~~~~~~~~~~ 6 | 7 | This part of the documentation covers the installation of ``pydantic-xml`` library. 8 | 9 | 10 | Installation using pip 11 | ______________________ 12 | 13 | To install ``pydantic-xml``, run: 14 | 15 | .. code-block:: console 16 | 17 | $ pip install pydantic-xml 18 | 19 | 20 | Optional dependencies 21 | _____________________ 22 | 23 | ``pydantic-xml`` library supports `lxml `_ as an xml serialization backend. 24 | If you wish to use ``lxml`` instead of standard :py:mod:`xml.etree.ElementTree` parser install ``lxml`` extra: 25 | 26 | .. code-block:: console 27 | 28 | $ pip install pydantic-xml[lxml] 29 | -------------------------------------------------------------------------------- /examples/computed-entities/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 150.172.238.178 3 | 150.172.230.21 4 | 5 | ********** 6 | 7 | -------------------------------------------------------------------------------- /examples/computed-entities/model.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from ipaddress import IPv4Address 3 | from typing import Dict, List 4 | from xml.etree.ElementTree import canonicalize 5 | 6 | from pydantic import Field, IPvAnyAddress, SecretStr, computed_field 7 | 8 | from pydantic_xml import BaseXmlModel, attr, computed_attr, computed_element 9 | 10 | 11 | class Auth(BaseXmlModel, tag='Authorization'): 12 | type: str = attr(name='Type') 13 | value: SecretStr 14 | 15 | 16 | class Request(BaseXmlModel, tag='Request'): 17 | raw_forwarded_for: str = Field(exclude=True) 18 | raw_cookies: str = Field(exclude=True) 19 | raw_auth: str = Field(exclude=True) 20 | 21 | @computed_attr(name='Client') 22 | def client(self) -> IPv4Address: 23 | client, *proxies = [IPvAnyAddress(addr) for addr in self.raw_forwarded_for.split(',')] 24 | return client 25 | 26 | @computed_element(tag='Proxy') 27 | def proxy(self) -> List[IPv4Address]: 28 | client, *proxies = [IPvAnyAddress(addr) for addr in self.raw_forwarded_for.split(',')] 29 | return proxies 30 | 31 | @computed_element(tag='Cookies') 32 | def cookies(self) -> Dict[str, str]: 33 | return dict( 34 | tuple(pair.split('=', maxsplit=1)) 35 | for cookie in self.raw_cookies.split(';') 36 | if (pair := cookie.strip()) 37 | ) 38 | 39 | @computed_field 40 | def auth(self) -> Auth: 41 | auth_type, auth_value = self.raw_auth.split(maxsplit=1) 42 | return Auth(type=auth_type, value=auth_value) 43 | 44 | 45 | request = Request( 46 | raw_forwarded_for="203.0.113.195,150.172.238.178,150.172.230.21", 47 | raw_cookies="PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43;", 48 | raw_auth="Basic YWxhZGRpbjpvcGVuc2VzYW1l", 49 | ) 50 | 51 | xml_doc = pathlib.Path('./doc.xml').read_text() 52 | assert canonicalize(request.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 53 | -------------------------------------------------------------------------------- /examples/custom-encoder/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | aGVsbG8gd29ybGQhISEK 4 | 5 | 6 | wqFIb2xhIE11bmRvIQo= 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/custom-encoder/file1.txt: -------------------------------------------------------------------------------- 1 | hello world!!! 2 | -------------------------------------------------------------------------------- /examples/custom-encoder/file2.txt: -------------------------------------------------------------------------------- 1 | ¡Hola Mundo! 2 | -------------------------------------------------------------------------------- /examples/custom-encoder/model.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import pathlib 3 | from typing import List, Optional, Union 4 | from xml.etree.ElementTree import canonicalize 5 | 6 | from pydantic import field_serializer, field_validator 7 | 8 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element 9 | 10 | 11 | class File(BaseXmlModel): 12 | name: str = attr() 13 | content: bytes = element() 14 | 15 | @field_serializer('content') 16 | def encode_content(self, value: bytes) -> str: 17 | return base64.b64encode(value).decode() 18 | 19 | @field_validator('content', mode='before') 20 | def decode_content(cls, value: Optional[Union[str, bytes]]) -> Optional[bytes]: 21 | if isinstance(value, str): 22 | return base64.b64decode(value) 23 | 24 | return value 25 | 26 | 27 | class Files(RootXmlModel, tag='files'): 28 | root: List[File] = element(tag='file', default=[]) 29 | 30 | 31 | files = Files() 32 | for filename in ['./file1.txt', './file2.txt']: 33 | with open(filename, 'rb') as f: 34 | content = f.read() 35 | 36 | files.root.append(File(name=filename, content=content)) 37 | 38 | expected_xml_doc = pathlib.Path('./doc.xml').read_bytes() 39 | 40 | assert canonicalize(files.to_xml(), strip_text=True) == canonicalize(expected_xml_doc, strip_text=True) 41 | -------------------------------------------------------------------------------- /examples/generic-model/doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "auth": { 4 | "user": "admin", 5 | "password": "secret" 6 | } 7 | }, 8 | "body": { 9 | "call": { 10 | "trade_name": "SpaceX", 11 | "founded": "2002-03-14", 12 | "website": "https://www.spacex.com" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/generic-model/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | admin 6 | secret 7 | 8 | 9 | 10 | 11 | SpaceX 12 | 2002-03-14 13 | https://www.spacex.com 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/generic-model/model.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pathlib 3 | from typing import Generic, TypeVar 4 | 5 | from pydantic import HttpUrl 6 | 7 | from pydantic_xml import BaseXmlModel, element 8 | 9 | AuthType = TypeVar('AuthType') 10 | 11 | 12 | class SoapHeader( 13 | BaseXmlModel, Generic[AuthType], 14 | tag='Header', 15 | ns='soap', 16 | ): 17 | auth: AuthType 18 | 19 | 20 | class SoapMethod(BaseXmlModel): 21 | pass 22 | 23 | 24 | MethodType = TypeVar('MethodType', bound=SoapMethod) 25 | 26 | 27 | class SoapBody( 28 | BaseXmlModel, Generic[MethodType], 29 | tag='Body', 30 | ns='soap', 31 | ): 32 | call: MethodType 33 | 34 | 35 | HeaderType = TypeVar('HeaderType', bound=SoapHeader) 36 | BodyType = TypeVar('BodyType', bound=SoapBody) 37 | 38 | 39 | class SoapEnvelope( 40 | BaseXmlModel, 41 | Generic[HeaderType, BodyType], 42 | tag='Envelope', 43 | ns='soap', 44 | nsmap={ 45 | 'soap': 'http://www.w3.org/2003/05/soap-envelope/', 46 | }, 47 | ): 48 | header: HeaderType 49 | body: BodyType 50 | 51 | 52 | class BasicAuth( 53 | BaseXmlModel, 54 | tag='BasicAuth', 55 | ns='auth', 56 | nsmap={ 57 | 'auth': 'http://www.company.com/auth', 58 | }, 59 | ): 60 | user: str = element(tag='Username') 61 | password: str = element(tag='Password') 62 | 63 | 64 | class CreateCompanyMethod( 65 | SoapMethod, 66 | tag='CreateCompany', 67 | ns='co', 68 | nsmap={ 69 | 'co': 'https://www.company.com/co', 70 | }, 71 | ): 72 | trade_name: str = element(tag='TradeName') 73 | founded: dt.date = element(tag='Founded') 74 | website: HttpUrl = element(tag='WebSite') 75 | 76 | 77 | CreateCompanyRequest = SoapEnvelope[ 78 | SoapHeader[ 79 | BasicAuth 80 | ], 81 | SoapBody[ 82 | CreateCompanyMethod 83 | ], 84 | ] 85 | 86 | xml_doc = pathlib.Path('./doc.xml').read_text() 87 | 88 | request = CreateCompanyRequest.from_xml(xml_doc) 89 | 90 | json_doc = pathlib.Path('./doc.json').read_text() 91 | assert request == CreateCompanyRequest.model_validate_json(json_doc) 92 | -------------------------------------------------------------------------------- /examples/quickstart/doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trade_name": "SpaceX", 3 | "type": "Private", 4 | "founder": { 5 | "name": "Elon", 6 | "surname": "Musk" 7 | }, 8 | "founded": "2002-03-14", 9 | "employees": 12000, 10 | "website": "https://www.spacex.com", 11 | "industries": [ 12 | "communications", 13 | "space" 14 | ], 15 | "key_people": [ 16 | { 17 | "name": "Elon Musk", 18 | "position": "CEO" 19 | }, 20 | { 21 | "name": "Elon Musk", 22 | "position": "CTO" 23 | }, 24 | { 25 | "name": "Gwynne Shotwell", 26 | "position": "COO" 27 | } 28 | ], 29 | "headquarters": { 30 | "country": "US", 31 | "state": "California", 32 | "city": "Hawthorne" 33 | }, 34 | "socials": [ 35 | { 36 | "type": "linkedin", 37 | "url": "https://www.linkedin.com/company/spacex" 38 | }, 39 | { 40 | "type": "twitter", 41 | "url": "https://twitter.com/spacex" 42 | }, 43 | { 44 | "type": "youtube", 45 | "url": "https://www.youtube.com/spacex" 46 | } 47 | ], 48 | "products": [ 49 | { 50 | "status": "running", 51 | "launched": 2013, 52 | "title": "Several launch vehicles" 53 | }, 54 | { 55 | "status": "running", 56 | "launched": 2019, 57 | "title": "Starlink" 58 | }, 59 | { 60 | "status": "development", 61 | "launched": null, 62 | "title": "Starship" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /examples/quickstart/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2002-03-14 4 | 12000 5 | https://www.spacex.com 6 | 7 | 8 | space 9 | communications 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | US 20 | California 21 | Hawthorne 22 | 23 | 24 | 25 | 26 | https://www.linkedin.com/company/spacex 27 | https://twitter.com/spacex 28 | https://www.youtube.com/spacex 29 | 30 | 31 | 32 | Several launch vehicles 33 | Starlink 34 | Starship 35 | 36 | -------------------------------------------------------------------------------- /examples/quickstart/model.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from datetime import date 3 | from enum import Enum 4 | from typing import Dict, List, Literal, Optional, Set, Tuple 5 | 6 | import pydantic as pd 7 | from pydantic import HttpUrl, conint 8 | 9 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, wrapped 10 | 11 | NSMAP = { 12 | 'co': 'http://www.company.com/contact', 13 | 'hq': 'http://www.company.com/hq', 14 | 'pd': 'http://www.company.com/prod', 15 | } 16 | 17 | 18 | class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP): 19 | country: str = element() 20 | state: str = element() 21 | city: str = element() 22 | 23 | @pd.field_validator('country') 24 | def validate_country(cls, value: str) -> str: 25 | if len(value) > 2: 26 | raise ValueError('country must be of 2 characters') 27 | return value 28 | 29 | 30 | class Industries(RootXmlModel): 31 | root: Set[str] = element(tag='Industry') 32 | 33 | 34 | class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP): 35 | type: str = attr() 36 | url: HttpUrl 37 | 38 | 39 | class Product(BaseXmlModel, ns_attrs=True, ns='pd', nsmap=NSMAP): 40 | status: Literal['running', 'development'] = attr() 41 | launched: Optional[int] = attr(default=None) 42 | title: str 43 | 44 | 45 | class Person(BaseXmlModel): 46 | name: str = attr() 47 | 48 | 49 | class CEO(Person): 50 | position: Literal['CEO'] = attr() 51 | 52 | 53 | class CTO(Person): 54 | position: Literal['CTO'] = attr() 55 | 56 | 57 | class COO(Person): 58 | position: Literal['COO'] = attr() 59 | 60 | 61 | class Company(BaseXmlModel, tag='Company', nsmap=NSMAP): 62 | class CompanyType(str, Enum): 63 | PRIVATE = 'Private' 64 | PUBLIC = 'Public' 65 | 66 | trade_name: str = attr(name='trade-name') 67 | type: CompanyType = attr() 68 | founder: Dict[str, str] = element(tag='Founder') 69 | founded: Optional[date] = element(tag='Founded') 70 | employees: conint(gt=0) = element(tag='Employees') 71 | website: HttpUrl = element(tag='WebSite') 72 | 73 | industries: Industries = element(tag='Industries') 74 | 75 | key_people: Tuple[CEO, CTO, COO] = wrapped('key-people', element(tag='person')) 76 | headquarters: Headquarters 77 | socials: List[Social] = wrapped( 78 | 'contacts/socials', 79 | element(tag='social', default_factory=list), 80 | ns='co', 81 | nsmap=NSMAP, 82 | ) 83 | 84 | products: Tuple[Product, ...] = element(tag='product', ns='pd') 85 | 86 | 87 | xml_doc = pathlib.Path('./doc.xml').read_text() 88 | 89 | company = Company.from_xml(xml_doc) 90 | 91 | json_doc = pathlib.Path('./doc.json').read_text() 92 | assert company == Company.model_validate_json(json_doc) 93 | -------------------------------------------------------------------------------- /examples/self-ref-model/doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "mode": "rwxr-xr-x", 4 | "dirs": [ 5 | { 6 | "name": "etc", 7 | "mode": "rwxr-xr-x", 8 | "dirs": [ 9 | { 10 | "name": "ssh", 11 | "mode": "rwxr-xr-x" 12 | } 13 | ], 14 | "files": [ 15 | { 16 | "name": "passwd", 17 | "mode": "-rw-r--r--" 18 | }, 19 | { 20 | "name": "hosts", 21 | "mode": "-rw-r--r--" 22 | } 23 | ] 24 | }, 25 | { 26 | "name": "bin", 27 | "mode": "rwxr-xr-x", 28 | "dirs": null, 29 | "files": [] 30 | }, 31 | { 32 | "name": "usr", 33 | "mode": "rwxr-xr-x", 34 | "dirs": [ 35 | { 36 | "name": "bin", 37 | "mode": "rwxr-xr-x", 38 | "dirs": null, 39 | "files": [] 40 | } 41 | ], 42 | "files": [] 43 | } 44 | ], 45 | "files": [] 46 | } 47 | -------------------------------------------------------------------------------- /examples/self-ref-model/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/self-ref-model/model.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import List, Optional 3 | 4 | from pydantic_xml import BaseXmlModel, attr, element 5 | 6 | 7 | class File(BaseXmlModel, tag="File"): 8 | name: str = attr(name='Name') 9 | mode: str = attr(name='Mode') 10 | 11 | 12 | class Directory(BaseXmlModel, tag="Directory"): 13 | name: str = attr(name='Name') 14 | mode: str = attr(name='Mode') 15 | dirs: Optional[List['Directory']] = element(tag='Directory', default=None) 16 | files: Optional[List[File]] = element(tag='File', default_factory=list) 17 | 18 | 19 | xml_doc = pathlib.Path('./doc.xml').read_text() 20 | 21 | directory = Directory.from_xml(xml_doc) 22 | 23 | json_doc = pathlib.Path('./doc.json').read_text() 24 | assert directory == Directory.model_validate_json(json_doc) 25 | -------------------------------------------------------------------------------- /examples/snippets/aliases.py: -------------------------------------------------------------------------------- 1 | from pydantic import HttpUrl 2 | 3 | from pydantic_xml import BaseXmlModel, attr, element 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel, tag='company'): 8 | title: str = attr(alias='trade-name') 9 | website: HttpUrl = element(alias='web-site') 10 | # [model-end] 11 | 12 | 13 | # [xml-start] 14 | xml_doc = ''' 15 | 16 | https://www.spacex.com 17 | 18 | ''' # [xml-end] 19 | 20 | # [json-start] 21 | json_doc = ''' 22 | { 23 | "trade-name": "SpaceX", 24 | "web-site": "https://www.spacex.com" 25 | } 26 | ''' # [json-end] 27 | 28 | company = Company.from_xml(xml_doc) 29 | assert company == Company.model_validate_json(json_doc) 30 | -------------------------------------------------------------------------------- /examples/snippets/attribute.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, attr 2 | 3 | 4 | # [model-start] 5 | class Company(BaseXmlModel): 6 | trade_name: str = attr(name='trade-name') 7 | type: str = attr() 8 | # [model-end] 9 | 10 | 11 | # [xml-start] 12 | xml_doc = ''' 13 | 14 | ''' # [xml-end] 15 | 16 | # [json-start] 17 | json_doc = ''' 18 | { 19 | "trade_name": "SpaceX", 20 | "type": "Private" 21 | } 22 | ''' # [json-end] 23 | 24 | company = Company.from_xml(xml_doc) 25 | assert company == Company.model_validate_json(json_doc) 26 | -------------------------------------------------------------------------------- /examples/snippets/attribute_namespace.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, attr 2 | 3 | 4 | # [model-start] 5 | class Company( 6 | BaseXmlModel, 7 | nsmap={'co': 'http://company.org/co'}, 8 | ): 9 | trade_name: str = attr(name='trade-name', ns='co') 10 | type: str = attr(ns='co') 11 | # [model-end] 12 | 13 | 14 | # [xml-start] 15 | xml_doc = ''' 16 | 19 | ''' # [xml-end] 20 | 21 | # [json-start] 22 | json_doc = ''' 23 | { 24 | "trade_name": "SpaceX", 25 | "type": "Private" 26 | } 27 | ''' # [json-end] 28 | 29 | company = Company.from_xml(xml_doc) 30 | assert company == Company.model_validate_json(json_doc) 31 | -------------------------------------------------------------------------------- /examples/snippets/attribute_namespace_inheritance.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, attr 2 | 3 | 4 | # [model-start] 5 | class Company( 6 | BaseXmlModel, 7 | ns_attrs=True, 8 | ns='co', 9 | nsmap={'co': 'http://company.org/co'}, 10 | ): 11 | trade_name: str = attr(name='trade-name') 12 | type: str = attr() 13 | # [model-end] 14 | 15 | 16 | # [xml-start] 17 | xml_doc = ''' 18 | 21 | ''' # [xml-end] 22 | 23 | # [json-start] 24 | json_doc = ''' 25 | { 26 | "trade_name": "SpaceX", 27 | "type": "Private" 28 | } 29 | ''' # [json-end] 30 | 31 | company = Company.from_xml(xml_doc) 32 | assert company == Company.model_validate_json(json_doc) 33 | -------------------------------------------------------------------------------- /examples/snippets/default_namespace.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic_xml import BaseXmlModel, element 4 | 5 | 6 | # [model-start] 7 | class Socials( 8 | BaseXmlModel, 9 | tag='socials', 10 | nsmap={'': 'http://www.company.com/soc'}, 11 | ): 12 | urls: List[str] = element(tag='social') 13 | 14 | 15 | class Contacts( 16 | BaseXmlModel, 17 | tag='contacts', 18 | nsmap={'': 'http://www.company.com/cnt'}, 19 | ): 20 | socials: Socials = element() 21 | 22 | 23 | class Company( 24 | BaseXmlModel, 25 | tag='company', 26 | nsmap={'': 'http://www.company.com/co'}, 27 | ): 28 | contacts: Contacts = element() 29 | # [model-end] 30 | 31 | 32 | # [xml-start] 33 | xml_doc = ''' 34 | 35 | 36 | 37 | https://www.linkedin.com/company/spacex 38 | https://twitter.com/spacex 39 | https://www.youtube.com/spacex 40 | 41 | 42 | 43 | ''' # [xml-end] 44 | 45 | # [json-start] 46 | json_doc = ''' 47 | { 48 | "contacts": { 49 | "socials": { 50 | "urls": [ 51 | "https://www.linkedin.com/company/spacex", 52 | "https://twitter.com/spacex", 53 | "https://www.youtube.com/spacex" 54 | ] 55 | } 56 | } 57 | } 58 | ''' # [json-end] 59 | 60 | company = Company.from_xml(xml_doc) 61 | assert company == Company.model_validate_json(json_doc) 62 | 63 | print(company.to_xml().decode()) 64 | -------------------------------------------------------------------------------- /examples/snippets/dynamic_model_creation.py: -------------------------------------------------------------------------------- 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 | 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) 28 | -------------------------------------------------------------------------------- /examples/snippets/element_model.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, element 2 | 3 | 4 | # [model-start] 5 | class Headquarters(BaseXmlModel, tag='headquarters'): 6 | country: str = element() 7 | state: str = element() 8 | city: str = element() 9 | 10 | 11 | class Company(BaseXmlModel, tag='company'): 12 | headquarters: Headquarters 13 | # [model-end] 14 | 15 | 16 | # [xml-start] 17 | xml_doc = ''' 18 | 19 | 20 | US 21 | California 22 | Hawthorne 23 | 24 | 25 | 26 | ''' # [xml-end] 27 | 28 | # [json-start] 29 | json_doc = ''' 30 | { 31 | "headquarters": { 32 | "country": "US", 33 | "state": "California", 34 | "city": "Hawthorne" 35 | } 36 | } 37 | ''' # [json-end] 38 | 39 | company = Company.from_xml(xml_doc) 40 | assert company == Company.model_validate_json(json_doc) 41 | -------------------------------------------------------------------------------- /examples/snippets/element_namespace.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from pydantic import HttpUrl 4 | 5 | from pydantic_xml import BaseXmlModel, element 6 | 7 | 8 | # [model-start] 9 | class Company(BaseXmlModel, tag='company'): 10 | founded: dt.date = element( 11 | ns='co', 12 | nsmap={'co': 'http://www.company.com/co'}, 13 | ) 14 | website: HttpUrl = element(tag='web-site') 15 | # [model-end] 16 | 17 | 18 | # [xml-start] 19 | xml_doc = ''' 20 | 21 | 2002-03-14 22 | https://www.spacex.com 23 | 24 | ''' # [xml-end] 25 | 26 | # [json-start] 27 | json_doc = ''' 28 | { 29 | "founded": "2002-03-14", 30 | "website": "https://www.spacex.com" 31 | } 32 | ''' # [json-end] 33 | 34 | company = Company.from_xml(xml_doc) 35 | assert company == Company.model_validate_json(json_doc) 36 | -------------------------------------------------------------------------------- /examples/snippets/element_namespace_global.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from pydantic import HttpUrl 4 | 5 | from pydantic_xml import BaseXmlModel, element 6 | 7 | 8 | # [model-start] 9 | class Company( 10 | BaseXmlModel, 11 | tag='company', 12 | ns='co', 13 | nsmap={'co': 'http://www.company.com/co'}, 14 | ): 15 | founded: dt.date = element() 16 | website: HttpUrl = element(tag='web-site', ns='co') 17 | # [model-end] 18 | 19 | 20 | # [xml-start] 21 | xml_doc = ''' 22 | 23 | 2002-03-14 24 | https://www.spacex.com 25 | 26 | ''' # [xml-end] 27 | 28 | # [json-start] 29 | json_doc = ''' 30 | { 31 | "founded": "2002-03-14", 32 | "website": "https://www.spacex.com" 33 | } 34 | ''' # [json-end] 35 | 36 | company = Company.from_xml(xml_doc) 37 | assert company == Company.model_validate_json(json_doc) 38 | -------------------------------------------------------------------------------- /examples/snippets/element_namespace_model.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, element 2 | 3 | 4 | # [model-start] 5 | class Headquarters( 6 | BaseXmlModel, 7 | tag='headquarters', 8 | ns='hq', 9 | nsmap={'hq': 'http://www.company.com/hq'}, 10 | ): 11 | country: str = element(ns='hq') 12 | state: str = element(ns='hq') 13 | city: str = element(ns='hq') 14 | 15 | 16 | class Company(BaseXmlModel, tag='company'): 17 | headquarters: Headquarters 18 | # [model-end] 19 | 20 | 21 | # [xml-start] 22 | xml_doc = ''' 23 | 24 | 25 | US 26 | California 27 | Hawthorne 28 | 29 | 30 | 31 | ''' # [xml-end] 32 | 33 | # [json-start] 34 | json_doc = ''' 35 | { 36 | "headquarters": { 37 | "country": "US", 38 | "state": "California", 39 | "city": "Hawthorne" 40 | } 41 | } 42 | ''' # [json-end] 43 | 44 | company = Company.from_xml(xml_doc) 45 | assert company == Company.model_validate_json(json_doc) 46 | -------------------------------------------------------------------------------- /examples/snippets/element_primitive.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from pydantic import HttpUrl 4 | 5 | from pydantic_xml import BaseXmlModel, element 6 | 7 | 8 | # [model-start] 9 | class Company(BaseXmlModel, tag='company'): 10 | founded: dt.date = element() 11 | website: HttpUrl = element(tag='web-site') 12 | # [model-end] 13 | 14 | 15 | # [xml-start] 16 | xml_doc = ''' 17 | 18 | 2002-03-14 19 | https://www.spacex.com 20 | 21 | ''' # [xml-end] 22 | 23 | # [json-start] 24 | json_doc = ''' 25 | { 26 | "founded": "2002-03-14", 27 | "website": "https://www.spacex.com" 28 | } 29 | ''' # [json-end] 30 | 31 | company = Company.from_xml(xml_doc) 32 | assert company == Company.model_validate_json(json_doc) 33 | -------------------------------------------------------------------------------- /examples/snippets/element_raw.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic import HttpUrl 5 | 6 | from pydantic_xml import BaseXmlModel, computed_element, element 7 | from pydantic_xml.element.native import ElementT as Element 8 | 9 | 10 | # [model-start] 11 | class Contact(BaseXmlModel, tag='contact'): 12 | url: HttpUrl 13 | 14 | 15 | class Contacts( 16 | BaseXmlModel, 17 | tag='contacts', 18 | arbitrary_types_allowed=True, 19 | ): 20 | contacts_raw: List[Element] = element(tag='contact', exclude=True) 21 | 22 | @computed_element 23 | def parse_raw_contacts(self) -> List[Contact]: 24 | contacts: List[Contact] = [] 25 | for contact_raw in self.contacts_raw: 26 | if url := contact_raw.attrib.get('url'): 27 | contact = Contact(url=url) 28 | elif (link := contact_raw.find('link')) is not None: 29 | contact = Contact(url=link.text) 30 | else: 31 | contact = Contact(url=contact_raw.text.strip()) 32 | 33 | contacts.append(contact) 34 | 35 | return contacts 36 | # [model-end] 37 | 38 | 39 | # [xml-start-1] 40 | src_doc = ''' 41 | 42 | 43 | 44 | https://twitter.com/spacex 45 | 46 | https://www.youtube.com/spacex 47 | 48 | ''' # [xml-end-1] 49 | 50 | 51 | contacts = Contacts.from_xml(src_doc) 52 | 53 | 54 | # [xml-start-2] 55 | dst_doc = ''' 56 | 57 | https://www.linkedin.com/company/spacex 58 | https://twitter.com/spacex 59 | https://www.youtube.com/spacex 60 | 61 | ''' # [xml-end-2] 62 | 63 | assert canonicalize(contacts.to_xml(), strip_text=True) == canonicalize(dst_doc, strip_text=True) 64 | -------------------------------------------------------------------------------- /examples/snippets/exclude_none.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic_xml import BaseXmlModel, element 5 | 6 | 7 | # [model-start] 8 | class Product(BaseXmlModel, tag='Product'): 9 | title: Optional[str] = element(tag='Title', default=None) 10 | status: Optional[Literal['running', 'development']] = element(tag='Status', default=None) 11 | launched: Optional[int] = element(tag='Launched', default=None) 12 | 13 | 14 | product = Product(title="Starlink", status=None) 15 | xml = product.to_xml(exclude_none=True) 16 | # [model-end] 17 | 18 | 19 | # [xml-start] 20 | xml_doc = ''' 21 | 22 | Starlink 23 | 24 | ''' # [xml-end] 25 | 26 | assert canonicalize(xml, strip_text=True) == canonicalize(xml_doc, strip_text=True) 27 | -------------------------------------------------------------------------------- /examples/snippets/exclude_unset.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic_xml import BaseXmlModel, element 5 | 6 | 7 | # [model-start] 8 | class Product(BaseXmlModel, tag='Product'): 9 | title: Optional[str] = element(tag='Title', default=None) 10 | status: Optional[Literal['running', 'development']] = element(tag='Status', default=None) 11 | launched: Optional[int] = element(tag='Launched', default=None) 12 | 13 | 14 | product = Product(title="Starlink", status=None) 15 | xml = product.to_xml(exclude_unset=True) 16 | # [model-end] 17 | 18 | 19 | # [xml-start] 20 | xml_doc = ''' 21 | 22 | Starlink 23 | 24 | 25 | ''' # [xml-end] 26 | 27 | assert canonicalize(xml, strip_text=True) == canonicalize(xml_doc, strip_text=True) 28 | -------------------------------------------------------------------------------- /examples/snippets/homogeneous_dicts.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from pydantic_xml import BaseXmlModel, element 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | products: List[Dict[str, str]] = element(tag='product') 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | 16 | 17 | 18 | 19 | ''' # [xml-end] 20 | 21 | # [json-start] 22 | json_doc = ''' 23 | { 24 | "products": [ 25 | { 26 | "status": "running", 27 | "launched": "2013" 28 | }, 29 | { 30 | "status": "running", 31 | "launched": "2019" 32 | }, 33 | { 34 | "status": "development" 35 | } 36 | ] 37 | } 38 | ''' # [json-end] 39 | 40 | company = Company.from_xml(xml_doc) 41 | assert company == Company.model_validate_json(json_doc) 42 | -------------------------------------------------------------------------------- /examples/snippets/homogeneous_models.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal, Optional, Tuple 2 | 3 | from pydantic_xml import BaseXmlModel, attr, element 4 | 5 | 6 | # [model-start] 7 | class Social(BaseXmlModel): 8 | type: str = attr() 9 | url: str 10 | 11 | 12 | class Product(BaseXmlModel): 13 | status: Literal['running', 'development'] = attr() 14 | launched: Optional[int] = attr(default=None) 15 | title: str 16 | 17 | 18 | class Company(BaseXmlModel): 19 | socials: Tuple[Social, ...] = element(tag='social') 20 | products: List[Product] = element(tag='product') 21 | # [model-end] 22 | 23 | 24 | # [xml-start] 25 | xml_doc = ''' 26 | 27 | https://www.linkedin.com/company/spacex 28 | https://twitter.com/spacex 29 | https://www.youtube.com/spacex 30 | 31 | Several launch vehicles 32 | Starlink 33 | Starship 34 | 35 | ''' # [xml-end] 36 | 37 | # [json-start] 38 | json_doc = ''' 39 | { 40 | "socials": [ 41 | { 42 | "type": "linkedin", 43 | "url": "https://www.linkedin.com/company/spacex" 44 | }, 45 | { 46 | "type": "twitter", 47 | "url": "https://twitter.com/spacex" 48 | }, 49 | { 50 | "type": "youtube", 51 | "url": "https://www.youtube.com/spacex" 52 | } 53 | ], 54 | "products": [ 55 | { 56 | "status": "running", 57 | "launched": 2013, 58 | "title": "Several launch vehicles" 59 | }, 60 | { 61 | "status": "running", 62 | "launched": 2019, 63 | "title": "Starlink" 64 | }, 65 | { 66 | "status": "development", 67 | "title": "Starship" 68 | } 69 | ] 70 | } 71 | ''' # [json-end] 72 | 73 | company = Company.from_xml(xml_doc) 74 | assert company == Company.model_validate_json(json_doc) 75 | -------------------------------------------------------------------------------- /examples/snippets/homogeneous_models_tuples.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr 4 | 5 | 6 | # [model-start] 7 | class Product(BaseXmlModel, tag='product'): 8 | status: str = attr() 9 | title: str 10 | 11 | 12 | class Launch(RootXmlModel[int], tag='launched'): 13 | pass 14 | 15 | 16 | class Products(RootXmlModel): 17 | root: List[Tuple[Product, Optional[Launch]]] 18 | # [model-end] 19 | 20 | 21 | # [xml-start] 22 | xml_doc = ''' 23 | 24 | Several launch vehicles 25 | 2013 26 | Starlink 27 | 2019 28 | Starship 29 | 30 | ''' # [xml-end] 31 | 32 | # [json-start] 33 | json_doc = ''' 34 | [ 35 | [ 36 | { 37 | "title": "Several launch vehicles", 38 | "status": "running" 39 | }, 40 | 2013 41 | ], 42 | [ 43 | { 44 | "title": "Starlink", 45 | "status": "running" 46 | }, 47 | 2019 48 | ], 49 | [ 50 | { 51 | "title": "Starship", 52 | "status": "development" 53 | }, 54 | null 55 | ] 56 | ] 57 | ''' # [json-end] 58 | 59 | products = Products.from_xml(xml_doc) 60 | assert products == Products.model_validate_json(json_doc) 61 | -------------------------------------------------------------------------------- /examples/snippets/homogeneous_primitives.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic_xml import BaseXmlModel, element 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | products: List[str] = element(tag='Product') 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | Several launch vehicles 16 | Starlink 17 | Starship 18 | 19 | ''' # [xml-end] 20 | 21 | # [json-start] 22 | json_doc = ''' 23 | { 24 | "products": [ 25 | "Several launch vehicles", 26 | "Starlink", 27 | "Starship" 28 | ] 29 | } 30 | ''' # [json-end] 31 | 32 | company = Company.from_xml(xml_doc) 33 | assert company == Company.model_validate_json(json_doc) 34 | -------------------------------------------------------------------------------- /examples/snippets/homogeneous_tuples.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | from pydantic_xml import RootXmlModel, element 4 | 5 | 6 | # [model-start] 7 | class Products(RootXmlModel): 8 | root: List[Tuple[str, Optional[int]]] = element(tag='info') 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | running 16 | 2013 17 | running 18 | 2019 19 | development 20 | 21 | 22 | ''' # [xml-end] 23 | 24 | # [json-start] 25 | json_doc = ''' 26 | [ 27 | [ 28 | "running", 29 | 2013 30 | ], 31 | [ 32 | "running", 33 | 2019 34 | ], 35 | [ 36 | "development", 37 | null 38 | ] 39 | ] 40 | ''' # [json-end] 41 | 42 | products = Products.from_xml(xml_doc) 43 | assert products == Products.model_validate_json(json_doc) 44 | -------------------------------------------------------------------------------- /examples/snippets/lxml/model_mode_strict.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY 2 | 3 | import pydantic 4 | 5 | from pydantic_xml import BaseXmlModel, element 6 | 7 | 8 | # [model-start] 9 | class Company( 10 | BaseXmlModel, 11 | tag='Company', 12 | search_mode='strict', 13 | ): 14 | founded: str = element(tag='Founded') 15 | website: str = element(tag='WebSite') 16 | # [model-end] 17 | 18 | 19 | # [xml-start] 20 | xml_doc = ''' 21 | 22 | https://www.spacex.com 23 | 2002-03-14 24 | 25 | ''' # [xml-end] 26 | 27 | # [json-start] 28 | json_doc = ''' 29 | {} 30 | ''' # [json-end] 31 | 32 | try: 33 | Company.from_xml(xml_doc) 34 | except pydantic.ValidationError as e: 35 | error = e.errors()[0] 36 | assert error == { 37 | 'loc': ('founded',), 38 | 'msg': '[line 2]: Field required', 39 | 'ctx': {'orig': 'Field required', 'sourceline': 2}, 40 | 'type': 'missing', 41 | 'input': ANY, 42 | } 43 | else: 44 | raise AssertionError('exception not raised') 45 | -------------------------------------------------------------------------------- /examples/snippets/mapping.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from pydantic_xml import BaseXmlModel 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | properties: Dict[str, str] 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | ''' # [xml-end] 16 | 17 | # [json-start] 18 | json_doc = ''' 19 | { 20 | "properties": { 21 | "trade-name": "SpaceX", 22 | "type": "Private" 23 | } 24 | } 25 | ''' # [json-end] 26 | 27 | company = Company.from_xml(xml_doc) 28 | assert company == Company.model_validate_json(json_doc) 29 | -------------------------------------------------------------------------------- /examples/snippets/mapping_element.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from pydantic_xml import BaseXmlModel, element 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | founder: Dict[str, str] = element(tag='Founder') 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | 16 | 17 | ''' # [xml-end] 18 | 19 | # [json-start] 20 | json_doc = ''' 21 | { 22 | "founder": { 23 | "name": "Elon", 24 | "surname": "Musk" 25 | } 26 | } 27 | ''' # [json-end] 28 | 29 | company = Company.from_xml(xml_doc) 30 | assert company == Company.model_validate_json(json_doc) 31 | -------------------------------------------------------------------------------- /examples/snippets/mapping_typed.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from pydantic import HttpUrl 4 | from typing_extensions import TypedDict 5 | 6 | from pydantic_xml import BaseXmlModel 7 | 8 | 9 | # [model-start] 10 | class Information(TypedDict): 11 | founded: dt.date 12 | employees: int 13 | website: HttpUrl 14 | 15 | 16 | class Company(BaseXmlModel): 17 | info: Information 18 | # [model-end] 19 | 20 | 21 | # [xml-start] 22 | xml_doc = ''' 23 | 26 | ''' # [xml-end] 27 | 28 | # [json-start] 29 | json_doc = ''' 30 | { 31 | "info": { 32 | "founded": "2002-03-14", 33 | "employees": 12000, 34 | "website": "https://www.spacex.com" 35 | } 36 | } 37 | ''' # [json-end] 38 | 39 | company = Company.from_xml(xml_doc) 40 | assert company == Company.model_validate_json(json_doc) 41 | -------------------------------------------------------------------------------- /examples/snippets/model_generic.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | from uuid import UUID 3 | 4 | from pydantic import SecretStr 5 | 6 | from pydantic_xml import BaseXmlModel, attr, element 7 | 8 | # [model-start] 9 | AuthType = TypeVar('AuthType') 10 | 11 | 12 | class Request(BaseXmlModel, Generic[AuthType], tag='request'): 13 | request_id: UUID = attr(name='id') 14 | timestamp: float = attr() 15 | auth: AuthType 16 | 17 | 18 | class BasicAuth(BaseXmlModel): 19 | user: str = attr() 20 | password: SecretStr = attr() 21 | 22 | 23 | class TokenAuth(BaseXmlModel): 24 | token: UUID = element() 25 | 26 | 27 | BasicRequest = Request[BasicAuth] 28 | TokenRequest = Request[TokenAuth] 29 | # [model-end] 30 | 31 | 32 | # [xml-start] 33 | xml_doc_1 = ''' 34 | 36 | 37 | 38 | ''' # [xml-end] 39 | 40 | # [json-start] 41 | json_doc_1 = ''' 42 | { 43 | "request_id": "27765d90-f3ef-426f-be9d-8da2b405b4a9", 44 | "timestamp": 1674976874.291046, 45 | "auth": { 46 | "user": "root", 47 | "password": "secret" 48 | } 49 | } 50 | ''' # [json-end] 51 | 52 | message = BasicRequest.from_xml(xml_doc_1) 53 | assert message == BasicRequest.model_validate_json(json_doc_1, strict=False) 54 | 55 | # [xml-start-2] 56 | xml_doc_2 = ''' 57 | 59 | 60 | 7de9e375-84c1-441f-a628-dbaf5017e94f 61 | 62 | 63 | ''' # [xml-end-2] 64 | 65 | # [json-start-2] 66 | json_doc_2 = ''' 67 | { 68 | "request_id": "27765d90-f3ef-426f-be9d-8da2b405b4a9", 69 | "timestamp": 1674976874.291046, 70 | "auth": { 71 | "token": "7de9e375-84c1-441f-a628-dbaf5017e94f" 72 | } 73 | } 74 | ''' # [json-end-2] 75 | 76 | message = TokenRequest.from_xml(xml_doc_2) 77 | assert message == TokenRequest.model_validate_json(json_doc_2) 78 | -------------------------------------------------------------------------------- /examples/snippets/model_mode_ordered.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, element 2 | 3 | 4 | # [model-start] 5 | class Company( 6 | BaseXmlModel, 7 | tag='Company', 8 | search_mode='ordered', 9 | ): 10 | founded: str = element(tag='Founded') 11 | website: str = element(tag='WebSite') 12 | # [model-end] 13 | 14 | 15 | # [xml-start] 16 | xml_doc = ''' 17 | 18 | 2002-03-14 19 | 20 | https://www.spacex.com 21 | 22 | ''' # [xml-end] 23 | 24 | # [json-start] 25 | json_doc = ''' 26 | { 27 | "founded": "2002-03-14", 28 | "website": "https://www.spacex.com" 29 | } 30 | ''' # [json-end] 31 | 32 | company = Company.from_xml(xml_doc) 33 | assert company == Company.model_validate_json(json_doc) 34 | -------------------------------------------------------------------------------- /examples/snippets/model_mode_unordered.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, element 2 | 3 | 4 | # [model-start] 5 | class Company( 6 | BaseXmlModel, 7 | tag='Company', 8 | search_mode='unordered', 9 | ): 10 | founded: str = element(tag='Founded') 11 | website: str = element(tag='WebSite') 12 | # [model-end] 13 | 14 | 15 | # [xml-start] 16 | xml_doc = ''' 17 | 18 | https://www.spacex.com 19 | 2002-03-14 20 | 21 | ''' # [xml-end] 22 | 23 | # [json-start] 24 | json_doc = ''' 25 | { 26 | "founded": "2002-03-14", 27 | "website": "https://www.spacex.com" 28 | } 29 | ''' # [json-end] 30 | 31 | company = Company.from_xml(xml_doc) 32 | assert company == Company.model_validate_json(json_doc) 33 | -------------------------------------------------------------------------------- /examples/snippets/model_namespace.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel 2 | 3 | 4 | # [model-start] 5 | class Company(BaseXmlModel, tag='company'): 6 | title: str 7 | # [model-end] 8 | 9 | 10 | # [xml-start] 11 | xml_doc = ''' 12 | SpaceX 13 | ''' # [xml-end] 14 | 15 | # [json-start] 16 | json_doc = ''' 17 | { 18 | "title": "SpaceX" 19 | } 20 | ''' # [json-end] 21 | 22 | company = Company.from_xml(xml_doc) 23 | assert company == Company.model_validate_json(json_doc) 24 | -------------------------------------------------------------------------------- /examples/snippets/model_root.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel 2 | 3 | 4 | # [model-start] 5 | class Company(BaseXmlModel, tag='company'): 6 | title: str 7 | # [model-end] 8 | 9 | 10 | # [xml-start] 11 | xml_doc = ''' 12 | SpaceX 13 | ''' # [xml-end] 14 | 15 | # [json-start] 16 | json_doc = ''' 17 | { 18 | "title": "SpaceX" 19 | } 20 | ''' # [json-end] 21 | 22 | company = Company.from_xml(xml_doc) 23 | assert company == Company.model_validate_json(json_doc) 24 | -------------------------------------------------------------------------------- /examples/snippets/model_root_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from pydantic_xml import RootXmlModel 4 | 5 | 6 | # [model-start] 7 | class Company(RootXmlModel, tag='company'): 8 | root: Dict[str, str] 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | ''' # [xml-end] 16 | 17 | # [json-start] 18 | json_doc = ''' 19 | { 20 | "trade-name": "SpaceX", 21 | "type":"Private" 22 | } 23 | ''' # [json-end] 24 | 25 | env = Company.from_xml(xml_doc) 26 | assert env == Company.model_validate_json(json_doc) 27 | -------------------------------------------------------------------------------- /examples/snippets/model_root_primitive.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, RootXmlModel 2 | 3 | 4 | # [model-start] 5 | class WebSite(RootXmlModel): 6 | root: str 7 | 8 | 9 | class Company(BaseXmlModel, tag='company'): 10 | website: WebSite 11 | # [model-end] 12 | 13 | 14 | # [xml-start] 15 | xml_doc = ''' 16 | 17 | https://www.spacex.com 18 | 19 | ''' # [xml-end] 20 | 21 | # [json-start] 22 | json_doc = ''' 23 | { 24 | "website": "https://www.spacex.com" 25 | } 26 | ''' # [json-end] 27 | 28 | env = Company.from_xml(xml_doc) 29 | assert env == Company.model_validate_json(json_doc) 30 | -------------------------------------------------------------------------------- /examples/snippets/model_root_type.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import HttpUrl 4 | 5 | from pydantic_xml import RootXmlModel, element 6 | 7 | 8 | # [model-start] 9 | class Socials(RootXmlModel, tag='socials'): 10 | root: List[HttpUrl] = element(tag='social') 11 | 12 | 13 | class Contacts(RootXmlModel[Socials], tag='contacts'): 14 | pass 15 | # [model-end] 16 | 17 | 18 | # [xml-start] 19 | xml_doc = ''' 20 | 21 | 22 | https://www.linkedin.com/company/spacex 23 | https://twitter.com/spacex 24 | https://www.youtube.com/spacex 25 | 26 | 27 | ''' # [xml-end] 28 | 29 | # [json-start] 30 | json_doc = ''' 31 | [ 32 | "https://www.linkedin.com/company/spacex", 33 | "https://twitter.com/spacex", 34 | "https://www.youtube.com/spacex" 35 | ] 36 | ''' # [json-end] 37 | 38 | contacts = Contacts.from_xml(xml_doc) 39 | 40 | assert contacts == Contacts.model_validate_json(json_doc) 41 | -------------------------------------------------------------------------------- /examples/snippets/model_self_ref.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic_xml import BaseXmlModel, attr, element 4 | 5 | 6 | # [model-start] 7 | class Directory(BaseXmlModel, tag="Directory"): 8 | name: str = attr(name='Name') 9 | dirs: Optional[List['Directory']] = element(tag='Directory', default=None) 10 | # [model-end] 11 | 12 | 13 | # [xml-start] 14 | xml_doc = ''' 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ''' # [xml-end] 23 | 24 | # [json-start] 25 | json_doc = ''' 26 | { 27 | "name": "root", 28 | "dirs": [ 29 | { 30 | "name": "etc", 31 | "dirs": [ 32 | { 33 | "name": "ssh" 34 | }, 35 | { 36 | "name": "init" 37 | } 38 | ] 39 | }, 40 | { 41 | "name": "bin" 42 | } 43 | ] 44 | } 45 | ''' # [json-end] 46 | 47 | directory = Directory.from_xml(xml_doc) 48 | 49 | assert directory == Directory.model_validate_json(json_doc) 50 | -------------------------------------------------------------------------------- /examples/snippets/model_template.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | import pydantic as pd 4 | 5 | from pydantic_xml import BaseXmlModel, attr, element 6 | 7 | 8 | # [model-start] 9 | class VehicleTemplate(BaseXmlModel): 10 | drivers: int = attr() 11 | title: str = attr() 12 | engine: str = element() 13 | 14 | 15 | class Car(VehicleTemplate, tag='car'): 16 | model_config = pd.ConfigDict( 17 | alias_generator=lambda field: {'title': 'make', 'engine': 'motor'}.get(field, field), 18 | ) 19 | 20 | 21 | class Airplane(VehicleTemplate, tag='airplane'): 22 | model_config = pd.ConfigDict( 23 | alias_generator=lambda field: {'drivers': 'pilots', 'title': 'model'}.get(field, field), 24 | ) 25 | 26 | 27 | class Vehicles(BaseXmlModel, tag='vehicles'): 28 | items: List[Union[Car, Airplane]] 29 | # [model-end] 30 | 31 | 32 | # [xml-start] 33 | xml_doc = ''' 34 | 35 | 36 | Coyote V8 37 | 38 | 39 | General Electric Passport 40 | 41 | 42 | V8 43 | 44 | 45 | ''' # [xml-end] 46 | 47 | # [json-start] 48 | json_doc = ''' 49 | { 50 | "items": [ 51 | { 52 | "make": "Ford Mustang", 53 | "drivers": 1, 54 | "motor": "Coyote V8" 55 | }, 56 | { 57 | "model": "Bombardier Global 7500", 58 | "pilots": 2, 59 | "engine": "General Electric Passport" 60 | }, 61 | { 62 | "make": "Audi A8", 63 | "drivers": 1, 64 | "motor": "V8" 65 | } 66 | ] 67 | } 68 | ''' # [json-end] 69 | 70 | vehicles = Vehicles.from_xml(xml_doc) 71 | assert vehicles == Vehicles.model_validate_json(json_doc) 72 | -------------------------------------------------------------------------------- /examples/snippets/py3.9/serialization.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Optional, TypeVar 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic import BeforeValidator, PlainSerializer 5 | 6 | from pydantic_xml import BaseXmlModel, element 7 | 8 | InnerType = TypeVar('InnerType') 9 | XmlOptional = Annotated[ 10 | Optional[InnerType], 11 | PlainSerializer(lambda val: val if val is not None else 'null'), 12 | BeforeValidator(lambda val: val if val != 'null' else None), 13 | ] 14 | 15 | 16 | class Company(BaseXmlModel): 17 | title: XmlOptional[str] = element(default=None) 18 | 19 | 20 | xml_doc = ''' 21 | 22 | null 23 | 24 | ''' 25 | 26 | company = Company.from_xml(xml_doc) 27 | 28 | assert company.title is None 29 | assert canonicalize(company.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 30 | -------------------------------------------------------------------------------- /examples/snippets/serialization_nillable.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic_xml import BaseXmlModel, element 5 | 6 | 7 | class Company(BaseXmlModel): 8 | title: Optional[str] = element(default=None, nillable=True) 9 | 10 | 11 | xml_doc = ''' 12 | 13 | 14 | </Company> 15 | ''' 16 | 17 | company = Company.from_xml(xml_doc) 18 | 19 | assert company.title is None 20 | assert canonicalize(company.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 21 | -------------------------------------------------------------------------------- /examples/snippets/skip_empty.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional, Tuple 2 | from xml.etree.ElementTree import canonicalize 3 | 4 | from pydantic_xml import BaseXmlModel, attr, element 5 | 6 | 7 | # [model-start] 8 | class Product(BaseXmlModel, tag='Product', skip_empty=True): 9 | status: Optional[Literal['running', 'development']] = attr(default=None) 10 | launched: Optional[int] = attr(default=None) 11 | title: Optional[str] = element(tag='Title', default=None) 12 | 13 | 14 | class Company(BaseXmlModel, tag='Company'): 15 | trade_name: str = attr(name='trade-name') 16 | website: str = element(tag='WebSite', default='') 17 | 18 | products: Tuple[Product, ...] = element() 19 | 20 | 21 | company = Company( 22 | trade_name="SpaceX", 23 | products=[ 24 | Product(status="running", launched=2013, title="Several launch vehicles"), 25 | Product(status="running", title="Starlink"), 26 | Product(status="development"), 27 | Product(), 28 | ], 29 | ) 30 | # [model-end] 31 | 32 | 33 | # [xml-start] 34 | xml_doc = ''' 35 | <Company trade-name="SpaceX"> 36 | <WebSite /><!--Company empty elements are not excluded--> 37 | 38 | <!--Product empty sub-elements and attributes are excluded--> 39 | <Product status="running" launched="2013"> 40 | <Title>Several launch vehicles 41 | 42 | 43 | Starlink 44 | 45 | 46 | 47 | 48 | ''' # [xml-end] 49 | 50 | assert canonicalize(company.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 51 | -------------------------------------------------------------------------------- /examples/snippets/text_primitive.py: -------------------------------------------------------------------------------- 1 | from pydantic import constr 2 | 3 | from pydantic_xml import BaseXmlModel 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | description: constr(strip_whitespace=True) 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | Space Exploration Technologies Corp. 16 | 17 | ''' # [xml-end] 18 | 19 | # [json-start] 20 | json_doc = ''' 21 | { 22 | "description": "Space Exploration Technologies Corp." 23 | } 24 | ''' # [json-end] 25 | 26 | company = Company.from_xml(xml_doc) 27 | assert company == Company.model_validate_json(json_doc) 28 | -------------------------------------------------------------------------------- /examples/snippets/text_primitive_optional.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic_xml import BaseXmlModel 4 | 5 | 6 | # [model-start] 7 | class Company(BaseXmlModel): 8 | description: Optional[str] = None 9 | # [model-end] 10 | 11 | 12 | # [xml-start] 13 | xml_doc = ''' 14 | 15 | ''' # [xml-end] 16 | 17 | # [json-start] 18 | json_doc = ''' 19 | { 20 | "description": null 21 | } 22 | ''' # [json-end] 23 | 24 | company = Company.from_xml(xml_doc) 25 | assert company == Company.model_validate_json(json_doc) 26 | -------------------------------------------------------------------------------- /examples/snippets/union_discriminated.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Union 2 | 3 | from pydantic import Field 4 | 5 | from pydantic_xml import BaseXmlModel, attr, element 6 | 7 | 8 | # [model-start] 9 | class Device(BaseXmlModel, tag='device'): 10 | type: str 11 | 12 | 13 | class CPU(Device): 14 | type: Literal['CPU'] = attr() 15 | cores: int = element() 16 | 17 | 18 | class GPU(Device): 19 | type: Literal['GPU'] = attr() 20 | cores: int = element() 21 | cuda: bool = attr(default=False) 22 | 23 | 24 | class Hardware(BaseXmlModel, tag='hardware'): 25 | accelerator: Union[CPU, GPU] = Field(..., discriminator='type') 26 | # [model-end] 27 | 28 | 29 | # [xml-start] 30 | xml_doc = ''' 31 | 32 | 33 | 4096 34 | 35 | 36 | ''' # [xml-end] 37 | 38 | # [json-start] 39 | json_doc = ''' 40 | { 41 | "accelerator": { 42 | "type": "GPU", 43 | "cores": 4096, 44 | "cuda": false 45 | } 46 | } 47 | ''' # [json-end] 48 | 49 | hardware = Hardware.from_xml(xml_doc) 50 | assert hardware == Hardware.model_validate_json(json_doc) 51 | -------------------------------------------------------------------------------- /examples/snippets/union_models.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Union 2 | 3 | from pydantic_xml import BaseXmlModel, attr, element 4 | 5 | 6 | # [model-start] 7 | class Event(BaseXmlModel): 8 | timestamp: float = attr() 9 | 10 | 11 | class KeyboardEvent(Event, tag='keyboard'): 12 | type: str = attr() 13 | key: str = element() 14 | 15 | 16 | class MouseEvent(Event, tag='mouse'): 17 | position: Dict[str, int] = element() 18 | 19 | 20 | class Log(BaseXmlModel, tag='log'): 21 | events: List[Union[KeyboardEvent, MouseEvent]] 22 | # [model-end] 23 | 24 | 25 | # [xml-start] 26 | xml_doc = ''' 27 | 28 | 29 | 30 | 31 | 33 | CTRL 34 | 35 | 37 | C 38 | 39 | 40 | 41 | 42 | 43 | ''' # [xml-end] 44 | 45 | # [json-start] 46 | json_doc = ''' 47 | { 48 | "events": [ 49 | { 50 | "timestamp": 1674999183.5486422, 51 | "position": {"x": 234, "y": 345} 52 | }, 53 | { 54 | "timestamp": 1674999184.227246, 55 | "type": "KEYDOWN", 56 | "key": "CTRL" 57 | }, 58 | { 59 | "timestamp": 1674999185.6342669, 60 | "type": "KEYDOWN", 61 | "key": "C" 62 | }, 63 | { 64 | "timestamp": 1674999186.270716, 65 | "position": {"x": 236, "y": 211} 66 | } 67 | ] 68 | } 69 | ''' # [json-end] 70 | 71 | log = Log.from_xml(xml_doc) 72 | assert log == Log.model_validate_json(json_doc) 73 | -------------------------------------------------------------------------------- /examples/snippets/union_primitives.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from typing import List, Optional, Union 3 | 4 | from pydantic_xml import BaseXmlModel, attr 5 | 6 | 7 | # [model-start] 8 | class Message(BaseXmlModel, tag='Message'): 9 | timestamp: Union[float, dt.datetime] = attr() 10 | text: Optional[str] = None 11 | 12 | 13 | class Messages(BaseXmlModel): 14 | messages: List[Message] 15 | # [model-end] 16 | 17 | 18 | # [xml-start] 19 | xml_doc_1 = ''' 20 | 21 | hello world 22 | 23 | 24 | ''' # [xml-end] 25 | 26 | # [json-start] 27 | json_doc_1 = ''' 28 | { 29 | "messages": [ 30 | { 31 | "timestamp": 1674995230.295639, 32 | "text": "hello world" 33 | }, 34 | { 35 | "timestamp": "2023-01-29T17:30:38.762166" 36 | } 37 | ] 38 | } 39 | ''' # [json-end] 40 | 41 | messages = Messages.from_xml(xml_doc_1) 42 | assert messages == Messages.model_validate_json(json_doc_1) 43 | -------------------------------------------------------------------------------- /examples/snippets/wrapper.py: -------------------------------------------------------------------------------- 1 | from pydantic_xml import BaseXmlModel, element, wrapped 2 | 3 | 4 | # [model-start] 5 | class Company(BaseXmlModel): 6 | city: str = wrapped( 7 | 'Info/Headquarters/Location', 8 | element(tag='City'), 9 | ) 10 | country: str = wrapped( 11 | 'Info/Headquarters/Location/Country', 12 | ) 13 | # [model-end] 14 | 15 | 16 | # [xml-start] 17 | xml_doc = ''' 18 | 19 | 20 | 21 | 22 | Hawthorne 23 | US 24 | 25 | 26 | 27 | 28 | ''' # [xml-end] 29 | 30 | # [json-start] 31 | json_doc = ''' 32 | { 33 | "country": "US", 34 | "city": "Hawthorne" 35 | } 36 | ''' # [json-end] 37 | 38 | company = Company.from_xml(xml_doc) 39 | assert company == Company.model_validate_json(json_doc) 40 | -------------------------------------------------------------------------------- /examples/snippets/wrapper_nested.py: -------------------------------------------------------------------------------- 1 | from pydantic import constr 2 | 3 | from pydantic_xml import BaseXmlModel, element, wrapped 4 | 5 | 6 | # [model-start] 7 | class Company( 8 | BaseXmlModel, 9 | ns='co', 10 | nsmap={'co': 'http://company.org/co'}, 11 | ): 12 | city: constr(strip_whitespace=True) = wrapped( 13 | 'Info', 14 | ns='co', 15 | entity=wrapped( 16 | 'Headquarters/Location', 17 | ns='hq', 18 | nsmap={'hq': 'http://company.org/hq'}, 19 | entity=element( 20 | tag='City', 21 | ns='loc', 22 | nsmap={'loc': 'http://company.org/loc'}, 23 | ), 24 | ), 25 | ) 26 | # [model-end] 27 | 28 | 29 | # [xml-start] 30 | xml_doc = ''' 31 | 32 | 33 | 34 | 35 | 36 | Hawthorne 37 | 38 | 39 | 40 | 41 | 42 | ''' # [xml-end] 43 | 44 | # [json-start] 45 | json_doc = ''' 46 | { 47 | "country": "US", 48 | "city": "Hawthorne" 49 | } 50 | ''' # [json-end] 51 | 52 | company = Company.from_xml(xml_doc) 53 | assert company == Company.model_validate_json(json_doc) 54 | -------------------------------------------------------------------------------- /examples/xml-serialization-annotation/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 0.0 1.0 2.0 3.0 4.0 5.0 3 | 0.0 3.2 5.4 4.1 2.0 -1.2 4 | 5 | -------------------------------------------------------------------------------- /examples/xml-serialization-annotation/model.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import Annotated, List, Type 3 | from xml.etree.ElementTree import canonicalize 4 | 5 | import pydantic_xml as pxml 6 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 7 | 8 | 9 | def validate_space_separated_list( 10 | cls: Type[pxml.BaseXmlModel], 11 | element: XmlElementReader, 12 | field_name: str, 13 | ) -> List[float]: 14 | if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__): 15 | return list(map(float, element.pop_text().split())) 16 | 17 | return [] 18 | 19 | 20 | def serialize_space_separated_list( 21 | model: pxml.BaseXmlModel, 22 | element: XmlElementWriter, 23 | value: List[float], 24 | field_name: str, 25 | ) -> None: 26 | sub_element = element.make_element(tag=field_name, nsmap=None) 27 | sub_element.set_text(' '.join(map(str, value))) 28 | 29 | element.append_element(sub_element) 30 | 31 | 32 | SpaceSeparatedValueList = Annotated[ 33 | List[float], 34 | pxml.XmlFieldValidator(validate_space_separated_list), 35 | pxml.XmlFieldSerializer(serialize_space_separated_list), 36 | ] 37 | 38 | 39 | class Plot(pxml.BaseXmlModel): 40 | x: SpaceSeparatedValueList = pxml.element() 41 | y: SpaceSeparatedValueList = pxml.element() 42 | 43 | 44 | xml_doc = pathlib.Path('./doc.xml').read_text() 45 | plot = Plot.from_xml(xml_doc) 46 | 47 | assert canonicalize(plot.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 48 | -------------------------------------------------------------------------------- /examples/xml-serialization-decorator/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 0.0 1.0 2.0 3.0 4.0 5.0 3 | 0.0 3.2 5.4 4.1 2.0 -1.2 4 | 5 | -------------------------------------------------------------------------------- /examples/xml-serialization-decorator/model.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import List 3 | from xml.etree.ElementTree import canonicalize 4 | 5 | from pydantic_xml import BaseXmlModel, element, xml_field_serializer, xml_field_validator 6 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 7 | 8 | 9 | class Plot(BaseXmlModel): 10 | x: List[float] = element() 11 | y: List[float] = element() 12 | 13 | @xml_field_validator('x', 'y') 14 | def validate_space_separated_list(cls, element: XmlElementReader, field_name: str) -> List[float]: 15 | if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__): 16 | return list(map(float, element.pop_text().split())) 17 | 18 | return [] 19 | 20 | @xml_field_serializer('x', 'y') 21 | def serialize_space_separated_list(self, element: XmlElementWriter, value: List[float], field_name: str) -> None: 22 | sub_element = element.make_element(tag=field_name, nsmap=None) 23 | sub_element.set_text(' '.join(map(str, value))) 24 | 25 | element.append_element(sub_element) 26 | 27 | 28 | xml_doc = pathlib.Path('./doc.xml').read_text() 29 | plot = Plot.from_xml(xml_doc) 30 | 31 | assert canonicalize(plot.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) 32 | -------------------------------------------------------------------------------- /pydantic_xml/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pydantic xml serialization/deserialization extension 3 | """ 4 | 5 | from . import config, errors, model 6 | from .errors import ModelError, ParsingError 7 | from .model import BaseXmlModel, RootXmlModel, XmlFieldSerializer, XmlFieldValidator, attr, computed_attr 8 | from .model import computed_element, create_model, element, wrapped, xml_field_serializer, xml_field_validator 9 | 10 | __all__ = ( 11 | 'BaseXmlModel', 12 | 'RootXmlModel', 13 | 'ModelError', 14 | 'ParsingError', 15 | 'attr', 16 | 'element', 17 | 'wrapped', 18 | 'computed_attr', 19 | 'computed_element', 20 | 'create_model', 21 | 'errors', 22 | 'model', 23 | 'xml_field_serializer', 24 | 'xml_field_validator', 25 | 'XmlFieldValidator', 26 | 'XmlFieldSerializer', 27 | ) 28 | -------------------------------------------------------------------------------- /pydantic_xml/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def strtobool(val: str) -> bool: 5 | """ 6 | Converts a string representation of boolean type to true or false. 7 | """ 8 | 9 | val = val.lower() 10 | if val in ('y', 'yes', 't', 'true', 'on', '1'): 11 | return True 12 | elif val in ('n', 'no', 'f', 'false', 'off', '0'): 13 | return False 14 | else: 15 | raise ValueError 16 | 17 | 18 | REGISTER_NS_PREFIXES = strtobool(os.environ.get('REGISTER_NS_PREFIXES', 'true')) 19 | FORCE_STD_XML = strtobool(os.environ.get('FORCE_STD_XML', 'false')) 20 | -------------------------------------------------------------------------------- /pydantic_xml/element/__init__.py: -------------------------------------------------------------------------------- 1 | from .element import SearchMode, XmlElement, XmlElementReader, XmlElementWriter 2 | from .utils import is_element_nill, make_element_nill 3 | -------------------------------------------------------------------------------- /pydantic_xml/element/native/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Type 2 | 3 | from pydantic_xml import config 4 | from pydantic_xml.element import XmlElement as BaseXmlElement 5 | 6 | XmlElement: Type[BaseXmlElement[Any]] 7 | ElementT: Type[Any] 8 | 9 | if config.FORCE_STD_XML: 10 | from .std import * # noqa: F403 11 | else: 12 | try: 13 | from .lxml import * # type: ignore[no-redef] # noqa: F403 14 | except ImportError: 15 | from .std import * # noqa: F403 16 | -------------------------------------------------------------------------------- /pydantic_xml/element/native/lxml.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Optional, Union 3 | 4 | from lxml import etree 5 | 6 | from pydantic_xml.element import XmlElement as BaseXmlElement 7 | from pydantic_xml.typedefs import NsMap 8 | 9 | __all__ = ( 10 | 'ElementT', 11 | 'XmlElement', 12 | 'etree', 13 | ) 14 | 15 | ElementT = etree._Element 16 | 17 | 18 | class XmlElement(BaseXmlElement[ElementT]): 19 | @classmethod 20 | def from_native(cls, element: ElementT) -> 'XmlElement': 21 | return cls( 22 | tag=element.tag, 23 | text=element.text, 24 | tail=element.tail, 25 | attributes={ 26 | force_str(name): force_str(value) # transformation is safe since lxml bytes values are ASCII compatible 27 | for name, value in element.attrib.items() 28 | }, 29 | elements=[ 30 | XmlElement.from_native(sub_element) 31 | for sub_element in element 32 | if not is_xml_comment(sub_element) 33 | ], 34 | sourceline=typing.cast(int, element.sourceline) if element.sourceline is not None else -1, 35 | ) 36 | 37 | def to_native(self) -> ElementT: 38 | element = etree.Element( 39 | self._tag, 40 | attrib=self._state.attrib, 41 | # https://github.com/lxml/lxml-stubs/issues/76 42 | nsmap={ns or None: uri for ns, uri in self._nsmap.items()} if self._nsmap else None, # type: ignore[misc] 43 | ) 44 | element.text = self._state.text 45 | element.tail = self._state.tail 46 | element.extend([element.to_native() for element in self._state.elements]) 47 | 48 | return element 49 | 50 | def make_element(self, tag: str, nsmap: Optional[NsMap]) -> 'XmlElement': 51 | return XmlElement(tag, nsmap=nsmap) 52 | 53 | def get_sourceline(self) -> int: 54 | return self._sourceline 55 | 56 | 57 | def force_str(val: Union[str, bytes]) -> str: 58 | if isinstance(val, bytes): 59 | return val.decode() 60 | else: 61 | return val 62 | 63 | 64 | def is_xml_comment(element: ElementT) -> bool: 65 | return isinstance(element, etree._Comment) 66 | -------------------------------------------------------------------------------- /pydantic_xml/element/native/std.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as etree 2 | from typing import Optional 3 | 4 | from pydantic_xml.element import XmlElement as BaseXmlElement 5 | from pydantic_xml.typedefs import NsMap 6 | 7 | __all__ = ( 8 | 'ElementT', 9 | 'XmlElement', 10 | 'etree', 11 | ) 12 | 13 | ElementT = etree.Element 14 | 15 | 16 | class XmlElement(BaseXmlElement[ElementT]): 17 | @classmethod 18 | def from_native(cls, element: ElementT) -> 'XmlElement': 19 | return cls( 20 | tag=element.tag, 21 | text=element.text, 22 | tail=element.tail, 23 | attributes=dict(element.attrib), 24 | elements=[ 25 | XmlElement.from_native(sub_element) 26 | for sub_element in element 27 | if not is_xml_comment(sub_element) 28 | ], 29 | ) 30 | 31 | def to_native(self) -> ElementT: 32 | element = etree.Element(self._tag, attrib=self._state.attrib or {}) 33 | element.text = self._state.text 34 | element.tail = self._state.tail 35 | element.extend([element.to_native() for element in self._state.elements]) 36 | 37 | return element 38 | 39 | def make_element(self, tag: str, nsmap: Optional[NsMap]) -> 'XmlElement': 40 | return XmlElement(tag) 41 | 42 | def get_sourceline(self) -> int: 43 | return -1 44 | 45 | 46 | def is_xml_comment(element: ElementT) -> bool: 47 | return element.tag is etree.Comment # type: ignore[comparison-overlap] 48 | -------------------------------------------------------------------------------- /pydantic_xml/element/utils.py: -------------------------------------------------------------------------------- 1 | from .element import XmlElementReader, XmlElementWriter 2 | 3 | XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance' 4 | 5 | 6 | def is_element_nill(element: XmlElementReader) -> bool: 7 | if (is_nil := element.pop_attrib('{%s}nil' % XSI_NS)) and is_nil == 'true': 8 | return True 9 | else: 10 | return False 11 | 12 | 13 | def make_element_nill(element: XmlElementWriter) -> None: 14 | element.set_attribute('{%s}nil' % XSI_NS, 'true') 15 | -------------------------------------------------------------------------------- /pydantic_xml/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class BaseError(Exception): 5 | """ 6 | Base package error. 7 | """ 8 | 9 | 10 | class ModelError(BaseError): 11 | """ 12 | Model definition error. 13 | """ 14 | 15 | 16 | class ModelFieldError(BaseError): 17 | """ 18 | Model field definition error. 19 | """ 20 | 21 | def __init__(self, model_name: str, field_name: Optional[str], message: str): 22 | self.model_name = model_name 23 | self.field_name = field_name = field_name or 'root' 24 | self.message = message 25 | 26 | super().__init__(f"{model_name}.{field_name} field type error: {message}") 27 | 28 | 29 | class ParsingError(BaseError): 30 | """ 31 | Xml parsing error. 32 | """ 33 | 34 | 35 | class SerializationError(BaseError): 36 | """ 37 | Model serialization error. 38 | """ 39 | -------------------------------------------------------------------------------- /pydantic_xml/mypy.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, Tuple, Union 2 | 3 | from mypy import nodes 4 | from mypy.plugin import ClassDefContext, Plugin 5 | from pydantic.mypy import PydanticModelTransformer, PydanticPlugin 6 | 7 | MODEL_METACLASS_FULLNAME = 'pydantic_xml.model.XmlModelMeta' 8 | ATTR_FULLNAME = 'pydantic_xml.model.attr' 9 | ELEMENT_FULLNAME = 'pydantic_xml.model.element' 10 | WRAPPED_FULLNAME = 'pydantic_xml.model.wrapped' 11 | ENTITIES_FULLNAME = (ATTR_FULLNAME, ELEMENT_FULLNAME, WRAPPED_FULLNAME) 12 | 13 | 14 | def plugin(version: str) -> type[Plugin]: 15 | return PydanticXmlPlugin 16 | 17 | 18 | class PydanticXmlPlugin(PydanticPlugin): 19 | def get_metaclass_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]: 20 | if fullname == MODEL_METACLASS_FULLNAME: 21 | return self._pydantic_model_metaclass_marker_callback 22 | return super().get_metaclass_hook(fullname) 23 | 24 | def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> None: 25 | transformer = PydanticXmlModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config) 26 | transformer.transform() 27 | 28 | 29 | class PydanticXmlModelTransformer(PydanticModelTransformer): 30 | @staticmethod 31 | def get_has_default(stmt: nodes.AssignmentStmt) -> bool: 32 | expr = stmt.rvalue 33 | if isinstance(expr, nodes.TempNode): 34 | return False 35 | 36 | if ( 37 | isinstance(expr, nodes.CallExpr) and 38 | isinstance(expr.callee, nodes.RefExpr) and 39 | expr.callee.fullname in ENTITIES_FULLNAME 40 | ): 41 | for arg, name in zip(expr.args, expr.arg_names): 42 | if name == 'default': 43 | return arg.__class__ is not nodes.EllipsisExpr 44 | if name == 'default_factory': 45 | return not (isinstance(arg, nodes.NameExpr) and arg.fullname == 'builtins.None') 46 | 47 | return False 48 | 49 | return PydanticModelTransformer.get_has_default(stmt) 50 | 51 | @staticmethod 52 | def get_alias_info(stmt: nodes.AssignmentStmt) -> Tuple[Union[str, None], bool]: 53 | expr = stmt.rvalue 54 | if isinstance(expr, nodes.TempNode): 55 | return None, False 56 | 57 | if ( 58 | isinstance(expr, nodes.CallExpr) and 59 | isinstance(expr.callee, nodes.RefExpr) and 60 | expr.callee.fullname in ENTITIES_FULLNAME 61 | ): 62 | for arg, arg_name in zip(expr.args, expr.arg_names): 63 | if arg_name != 'alias': 64 | continue 65 | if isinstance(arg, nodes.StrExpr): 66 | return arg.value, False 67 | else: 68 | return None, True 69 | 70 | return PydanticModelTransformer.get_alias_info(stmt) 71 | 72 | @staticmethod 73 | def get_strict(stmt: nodes.AssignmentStmt) -> Optional[bool]: 74 | expr = stmt.rvalue 75 | if ( 76 | isinstance(expr, nodes.CallExpr) and 77 | isinstance(expr.callee, nodes.RefExpr) and 78 | expr.callee.fullname in ENTITIES_FULLNAME 79 | ): 80 | for arg, name in zip(expr.args, expr.arg_names): 81 | if name != 'strict': 82 | continue 83 | if isinstance(arg, nodes.NameExpr): 84 | if arg.fullname == 'builtins.True': 85 | return True 86 | elif arg.fullname == 'builtins.False': 87 | return False 88 | return None 89 | 90 | return PydanticModelTransformer.get_strict(stmt) 91 | 92 | @staticmethod 93 | def is_field_frozen(stmt: nodes.AssignmentStmt) -> bool: 94 | expr = stmt.rvalue 95 | if isinstance(expr, nodes.TempNode): 96 | return False 97 | 98 | if not ( 99 | isinstance(expr, nodes.CallExpr) and 100 | isinstance(expr.callee, nodes.RefExpr) and 101 | expr.callee.fullname in ENTITIES_FULLNAME 102 | ): 103 | return False 104 | 105 | for i, arg_name in enumerate(expr.arg_names): 106 | if arg_name == 'frozen': 107 | arg = expr.args[i] 108 | return isinstance(arg, nodes.NameExpr) and arg.fullname == 'builtins.True' 109 | 110 | return PydanticModelTransformer.is_field_frozen(stmt) 111 | -------------------------------------------------------------------------------- /pydantic_xml/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dapper91/pydantic-xml/405b3bb41accaf7eb0ee4f591b58e81e33d210cf/pydantic_xml/py.typed -------------------------------------------------------------------------------- /pydantic_xml/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import factories, serializer 2 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/__init__.py: -------------------------------------------------------------------------------- 1 | from . import call, heterogeneous, homogeneous, is_instance, mapping, model, named_tuple, primitive, raw, tagged_union 2 | from . import tuple, typed_mapping, union, wrapper 3 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/call.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from pydantic_core import core_schema as pcs 4 | 5 | from pydantic_xml import errors 6 | from pydantic_xml.serializers.factories import named_tuple 7 | from pydantic_xml.serializers.serializer import Serializer 8 | 9 | 10 | def from_core_schema(schema: pcs.CallSchema, ctx: Serializer.Context) -> Serializer: 11 | func = schema['function'] 12 | 13 | if inspect.isclass(func) and issubclass(func, tuple): 14 | return named_tuple.from_core_schema(schema, ctx) 15 | else: 16 | raise errors.ModelError("type call is not supported") 17 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/heterogeneous.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | import pydantic as pd 4 | from pydantic_core import core_schema as pcs 5 | 6 | from pydantic_xml import errors, utils 7 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 8 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, Serializer 9 | from pydantic_xml.typedefs import EntityLocation, Location 10 | 11 | 12 | class ElementSerializer(Serializer): 13 | @classmethod 14 | def from_core_schema(cls, schema: pcs.TupleSchema, ctx: Serializer.Context) -> 'ElementSerializer': 15 | model_name = ctx.model_name 16 | computed = ctx.field_computed 17 | inner_serializers: List[Serializer] = [] 18 | for item_schema in schema['items_schema']: 19 | inner_serializers.append(Serializer.parse_core_schema(item_schema, ctx)) 20 | 21 | return cls(model_name, computed, tuple(inner_serializers)) 22 | 23 | def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Serializer, ...]): 24 | self._model_name = model_name 25 | self._computed = computed 26 | self._inner_serializers = inner_serializers 27 | 28 | def serialize( 29 | self, 30 | element: XmlElementWriter, 31 | value: List[Any], 32 | encoded: List[Any], 33 | *, 34 | skip_empty: bool = False, 35 | exclude_none: bool = False, 36 | exclude_unset: bool = False, 37 | ) -> Optional[XmlElementWriter]: 38 | if value is None: 39 | return element 40 | 41 | if skip_empty and len(value) == 0: 42 | return element 43 | 44 | if len(value) != len(self._inner_serializers): 45 | raise errors.SerializationError("value length is incorrect") 46 | 47 | for serializer, val, enc in zip(self._inner_serializers, value, encoded): 48 | serializer.serialize( 49 | element, val, enc, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 50 | ) 51 | 52 | return element 53 | 54 | def deserialize( 55 | self, 56 | element: Optional[XmlElementReader], 57 | *, 58 | context: Optional[Dict[str, Any]], 59 | sourcemap: Dict[Location, int], 60 | loc: Location, 61 | ) -> Optional[List[Any]]: 62 | if self._computed: 63 | return None 64 | 65 | if element is None: 66 | return None 67 | 68 | result: List[Any] = [] 69 | item_errors: Dict[Union[None, str, int], pd.ValidationError] = {} 70 | for idx, serializer in enumerate(self._inner_serializers): 71 | try: 72 | result.append(serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc + (idx,))) 73 | except pd.ValidationError as err: 74 | item_errors[idx] = err 75 | 76 | if item_errors: 77 | raise utils.into_validation_error(title=self._model_name, errors_map=item_errors) 78 | 79 | if all((value is None for value in result)): 80 | return None 81 | else: 82 | return result 83 | 84 | 85 | def from_core_schema(schema: pcs.TupleSchema, ctx: Serializer.Context) -> Serializer: 86 | for item_schema in schema['items_schema']: 87 | item_schema, ctx = Serializer.preprocess_schema(item_schema, ctx) 88 | 89 | items_type_family = TYPE_FAMILY.get(item_schema['type']) 90 | if items_type_family not in ( 91 | SchemaTypeFamily.PRIMITIVE, 92 | SchemaTypeFamily.MODEL, 93 | SchemaTypeFamily.MAPPING, 94 | SchemaTypeFamily.TYPED_MAPPING, 95 | SchemaTypeFamily.UNION, 96 | SchemaTypeFamily.TAGGED_UNION, 97 | SchemaTypeFamily.IS_INSTANCE, 98 | SchemaTypeFamily.CALL, 99 | ): 100 | raise errors.ModelFieldError( 101 | ctx.model_name, ctx.field_name, "collection item must be of primitive, model, mapping or union type", 102 | ) 103 | 104 | if items_type_family not in (SchemaTypeFamily.MODEL, SchemaTypeFamily.UNION) and ctx.entity_location is None: 105 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") 106 | 107 | if ctx.entity_location is EntityLocation.ELEMENT: 108 | return ElementSerializer.from_core_schema(schema, ctx) 109 | elif ctx.entity_location is None: 110 | return ElementSerializer.from_core_schema(schema, ctx) 111 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 112 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of collection types are not supported") 113 | else: 114 | raise AssertionError("unreachable") 115 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/homogeneous.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | from typing import Any, Dict, List, Optional, Union 3 | 4 | import pydantic as pd 5 | from pydantic_core import core_schema as pcs 6 | 7 | from pydantic_xml import errors, utils 8 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 9 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, Serializer 10 | from pydantic_xml.typedefs import EntityLocation, Location 11 | 12 | HomogeneousCollectionTypeSchema = Union[ 13 | pcs.TupleSchema, 14 | pcs.ListSchema, 15 | pcs.SetSchema, 16 | pcs.FrozenSetSchema, 17 | ] 18 | 19 | 20 | class ElementSerializer(Serializer): 21 | @classmethod 22 | def from_core_schema(cls, schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Context) -> 'ElementSerializer': 23 | model_name = ctx.model_name 24 | computed = ctx.field_computed 25 | 26 | items_schema = schema['items_schema'] 27 | if isinstance(items_schema, list): 28 | assert len(items_schema) == 1, "unexpected items schema type" 29 | items_schema = items_schema[0] 30 | 31 | inner_serializer = Serializer.parse_core_schema(items_schema, ctx) 32 | 33 | return cls(model_name, computed, inner_serializer) 34 | 35 | def __init__(self, model_name: str, computed: bool, inner_serializer: Serializer): 36 | self._model_name = model_name 37 | self._computed = computed 38 | self._inner_serializer = inner_serializer 39 | 40 | def serialize( 41 | self, 42 | element: XmlElementWriter, 43 | value: List[Any], 44 | encoded: List[Any], 45 | *, 46 | skip_empty: bool = False, 47 | exclude_none: bool = False, 48 | exclude_unset: bool = False, 49 | ) -> Optional[XmlElementWriter]: 50 | if value is None: 51 | return element 52 | 53 | if skip_empty and len(value) == 0: 54 | return element 55 | 56 | for val, enc in zip(value, encoded): 57 | if skip_empty and val is None: 58 | continue 59 | 60 | self._inner_serializer.serialize( 61 | element, val, enc, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 62 | ) 63 | 64 | return element 65 | 66 | def deserialize( 67 | self, 68 | element: Optional[XmlElementReader], 69 | *, 70 | context: Optional[Dict[str, Any]], 71 | sourcemap: Dict[Location, int], 72 | loc: Location, 73 | ) -> Optional[List[Any]]: 74 | if self._computed: 75 | return None 76 | 77 | if element is None: 78 | return None 79 | 80 | serializer = self._inner_serializer 81 | result: List[Any] = [] 82 | item_errors: Dict[Union[None, str, int], pd.ValidationError] = {} 83 | for idx in it.count(): 84 | try: 85 | value = serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc + (idx,)) 86 | if value is None: 87 | break 88 | except pd.ValidationError as err: 89 | item_errors[idx] = err 90 | else: 91 | result.append(value) 92 | 93 | if item_errors: 94 | raise utils.into_validation_error(title=self._model_name, errors_map=item_errors) 95 | 96 | return result or None 97 | 98 | 99 | def from_core_schema(schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Context) -> Serializer: 100 | items_schema = schema['items_schema'] 101 | if isinstance(items_schema, list): 102 | assert len(items_schema) == 1, "unexpected items schema type" 103 | items_schema = items_schema[0] 104 | 105 | items_schema, ctx = Serializer.preprocess_schema(items_schema, ctx) 106 | 107 | items_type_family = TYPE_FAMILY.get(items_schema['type']) 108 | if items_type_family not in ( 109 | SchemaTypeFamily.PRIMITIVE, 110 | SchemaTypeFamily.MODEL, 111 | SchemaTypeFamily.MAPPING, 112 | SchemaTypeFamily.TYPED_MAPPING, 113 | SchemaTypeFamily.UNION, 114 | SchemaTypeFamily.TAGGED_UNION, 115 | SchemaTypeFamily.IS_INSTANCE, 116 | SchemaTypeFamily.CALL, 117 | SchemaTypeFamily.TUPLE, 118 | ): 119 | raise errors.ModelFieldError( 120 | ctx.model_name, ctx.field_name, "collection item must be of primitive, model, mapping, union or tuple type", 121 | ) 122 | 123 | if items_type_family not in ( 124 | SchemaTypeFamily.MODEL, 125 | SchemaTypeFamily.UNION, 126 | SchemaTypeFamily.TAGGED_UNION, 127 | SchemaTypeFamily.TUPLE, 128 | SchemaTypeFamily.CALL, 129 | ) and ctx.entity_location is None: 130 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") 131 | 132 | if ctx.entity_location is EntityLocation.ELEMENT: 133 | return ElementSerializer.from_core_schema(schema, ctx) 134 | elif ctx.entity_location is None: 135 | return ElementSerializer.from_core_schema(schema, ctx) 136 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 137 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of collection types are not supported") 138 | else: 139 | raise AssertionError("unreachable") 140 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/is_instance.py: -------------------------------------------------------------------------------- 1 | from pydantic_core import core_schema as pcs 2 | 3 | from pydantic_xml.element import native 4 | from pydantic_xml.serializers.serializer import Serializer 5 | 6 | from . import primitive, raw 7 | 8 | 9 | def from_core_schema(schema: pcs.IsInstanceSchema, ctx: Serializer.Context) -> Serializer: 10 | field_cls = schema['cls'] 11 | 12 | if issubclass(field_cls, native.ElementT): 13 | return raw.from_core_schema(schema, ctx) 14 | else: 15 | return primitive.from_core_schema(schema, ctx) 16 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/mapping.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from pydantic_core import core_schema as pcs 4 | 5 | from pydantic_xml import errors 6 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 7 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, SearchMode, Serializer 8 | from pydantic_xml.typedefs import EntityLocation, Location, NsMap 9 | from pydantic_xml.utils import QName, merge_nsmaps, select_ns 10 | 11 | 12 | class AttributesSerializer(Serializer): 13 | @classmethod 14 | def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'AttributesSerializer': 15 | ns = select_ns(ctx.entity_ns, ctx.parent_ns) 16 | nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) 17 | namespaced_attrs = ctx.namespaced_attrs 18 | computed = ctx.field_computed 19 | 20 | return cls(ns, nsmap, namespaced_attrs, computed) 21 | 22 | def __init__(self, ns: Optional[str], nsmap: Optional[NsMap], namespaced_attrs: bool, computed: bool): 23 | self._ns = ns 24 | self._namespaced_attrs = namespaced_attrs 25 | self._nsmap = nsmap 26 | self._computed = computed 27 | 28 | def serialize( 29 | self, 30 | element: XmlElementWriter, 31 | value: Dict[str, Any], 32 | encoded: Dict[str, Any], 33 | *, 34 | skip_empty: bool = False, 35 | exclude_none: bool = False, 36 | exclude_unset: bool = False, 37 | ) -> Optional[XmlElementWriter]: 38 | if value is None: 39 | return element 40 | 41 | ns = self._nsmap.get(self._ns) if self._namespaced_attrs and self._ns and self._nsmap else None 42 | element.set_attributes({ 43 | QName(tag=attr, ns=ns).uri: str(enc) 44 | for attr, enc in encoded.items() 45 | }) 46 | 47 | return element 48 | 49 | def deserialize( 50 | self, 51 | element: Optional[XmlElementReader], 52 | *, 53 | context: Optional[Dict[str, Any]], 54 | sourcemap: Dict[Location, int], 55 | loc: Location, 56 | ) -> Optional[Dict[str, str]]: 57 | if self._computed: 58 | return None 59 | 60 | if element is None or (attributes := element.pop_attributes()) is None: 61 | return None 62 | 63 | return { 64 | QName.from_uri(attr).tag if self._namespaced_attrs else attr: val 65 | for attr, val in attributes.items() 66 | } 67 | 68 | 69 | class ElementSerializer(AttributesSerializer): 70 | @classmethod 71 | def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'ElementSerializer': 72 | name = ctx.entity_path or ctx.field_alias or ctx.field_name 73 | ns = select_ns(ctx.entity_ns, ctx.parent_ns) 74 | nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) 75 | namespaced_attrs = ctx.namespaced_attrs 76 | search_mode = ctx.search_mode 77 | computed = ctx.field_computed 78 | 79 | if name is None: 80 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") 81 | 82 | return cls(name, ns, nsmap, namespaced_attrs, search_mode, computed) 83 | 84 | def __init__( 85 | self, 86 | name: str, 87 | ns: Optional[str], 88 | nsmap: Optional[NsMap], 89 | namespaced_attrs: bool, 90 | search_mode: SearchMode, 91 | computed: bool, 92 | ): 93 | super().__init__(ns, nsmap, namespaced_attrs, computed) 94 | self._search_mode = search_mode 95 | self._name = name 96 | self._element_name = QName.from_alias(tag=self._name, ns=self._ns, nsmap=self._nsmap).uri 97 | 98 | def serialize( 99 | self, 100 | element: XmlElementWriter, 101 | value: Dict[str, Any], 102 | encoded: Dict[str, Any], 103 | *, 104 | skip_empty: bool = False, 105 | exclude_none: bool = False, 106 | exclude_unset: bool = False, 107 | ) -> Optional[XmlElementWriter]: 108 | if skip_empty and len(value) == 0: 109 | return element 110 | 111 | sub_element = element.make_element(self._element_name, nsmap=self._nsmap) 112 | super().serialize( 113 | sub_element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 114 | ) 115 | if skip_empty and sub_element.is_empty(): 116 | return None 117 | else: 118 | element.append_element(sub_element) 119 | return sub_element 120 | 121 | def deserialize( 122 | self, 123 | element: Optional[XmlElementReader], 124 | *, 125 | context: Optional[Dict[str, Any]], 126 | sourcemap: Dict[Location, int], 127 | loc: Location, 128 | ) -> Optional[Dict[str, str]]: 129 | if self._computed: 130 | return None 131 | 132 | if element and (sub_element := element.pop_element(self._element_name, self._search_mode)) is not None: 133 | sourcemap[loc] = sub_element.get_sourceline() 134 | return super().deserialize(sub_element, context=context, sourcemap=sourcemap, loc=loc) 135 | else: 136 | return None 137 | 138 | 139 | def from_core_schema(schema: pcs.DictSchema, ctx: Serializer.Context) -> Serializer: 140 | key_schema = schema['keys_schema'] 141 | key_schema, key_ctx = Serializer.preprocess_schema(key_schema, ctx) 142 | 143 | val_schema = schema['values_schema'] 144 | val_schema, val_ctx = Serializer.preprocess_schema(val_schema, ctx) 145 | 146 | if TYPE_FAMILY.get(key_schema['type']) is not SchemaTypeFamily.PRIMITIVE: 147 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "mapping key must be of a primitive type") 148 | if TYPE_FAMILY.get(val_schema['type']) is not SchemaTypeFamily.PRIMITIVE: 149 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "mapping value must be of a primitive type") 150 | 151 | if ctx.entity_location is EntityLocation.ELEMENT: 152 | return ElementSerializer.from_core_schema(schema, ctx) 153 | elif ctx.entity_location is None: 154 | return AttributesSerializer.from_core_schema(schema, ctx) 155 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 156 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of mapping type are not supported") 157 | else: 158 | raise AssertionError("unreachable") 159 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/named_tuple.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Any, Dict, List, Optional, Tuple 3 | 4 | from pydantic_core import core_schema as pcs 5 | 6 | from pydantic_xml import errors 7 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 8 | from pydantic_xml.serializers.factories import heterogeneous 9 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, Serializer 10 | from pydantic_xml.typedefs import EntityLocation, Location 11 | 12 | 13 | class ElementSerializer(Serializer): 14 | @classmethod 15 | def from_core_schema(cls, schema: pcs.ArgumentsSchema, ctx: Serializer.Context) -> 'ElementSerializer': 16 | model_name = ctx.model_name 17 | computed = ctx.field_computed 18 | inner_serializers: List[Serializer] = [] 19 | for argument_schema in schema['arguments_schema']: 20 | param_schema = argument_schema['schema'] 21 | inner_serializers.append(Serializer.parse_core_schema(param_schema, ctx)) 22 | 23 | return cls(model_name, computed, tuple(inner_serializers)) 24 | 25 | def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Serializer, ...]): 26 | self._inner_serializer = heterogeneous.ElementSerializer(model_name, computed, inner_serializers) 27 | 28 | def serialize( 29 | self, 30 | element: XmlElementWriter, 31 | value: List[Any], 32 | encoded: List[Any], 33 | *, 34 | skip_empty: bool = False, 35 | exclude_none: bool = False, 36 | exclude_unset: bool = False, 37 | ) -> Optional[XmlElementWriter]: 38 | return self._inner_serializer.serialize( 39 | element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 40 | ) 41 | 42 | def deserialize( 43 | self, 44 | element: Optional[XmlElementReader], 45 | *, 46 | context: Optional[Dict[str, Any]], 47 | sourcemap: Dict[Location, int], 48 | loc: Location, 49 | ) -> Optional[List[Any]]: 50 | return self._inner_serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc) 51 | 52 | 53 | def from_core_schema(schema: pcs.CallSchema, ctx: Serializer.Context) -> Serializer: 54 | arguments_schema = typing.cast(pcs.ArgumentsSchema, schema['arguments_schema']) 55 | for argument_schema in arguments_schema['arguments_schema']: 56 | param_schema = argument_schema['schema'] 57 | param_schema, ctx = Serializer.preprocess_schema(param_schema, ctx) 58 | 59 | param_type_family = TYPE_FAMILY.get(param_schema['type']) 60 | if param_type_family not in ( 61 | SchemaTypeFamily.PRIMITIVE, 62 | SchemaTypeFamily.MODEL, 63 | SchemaTypeFamily.MAPPING, 64 | SchemaTypeFamily.TYPED_MAPPING, 65 | SchemaTypeFamily.UNION, 66 | SchemaTypeFamily.TAGGED_UNION, 67 | SchemaTypeFamily.IS_INSTANCE, 68 | SchemaTypeFamily.CALL, 69 | ): 70 | raise errors.ModelFieldError( 71 | ctx.model_name, ctx.field_name, "tuple item must be of primitive, model, mapping or union type", 72 | ) 73 | 74 | if param_type_family not in (SchemaTypeFamily.MODEL, SchemaTypeFamily.UNION) and ctx.entity_location is None: 75 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") 76 | 77 | if ctx.entity_location is EntityLocation.ELEMENT: 78 | return ElementSerializer.from_core_schema(arguments_schema, ctx) 79 | elif ctx.entity_location is None: 80 | return ElementSerializer.from_core_schema(arguments_schema, ctx) 81 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 82 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of tuple types are not supported") 83 | else: 84 | raise AssertionError("unreachable") 85 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/raw.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from pydantic_core import core_schema as pcs 4 | 5 | from pydantic_xml import errors 6 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 7 | from pydantic_xml.serializers.serializer import SearchMode, Serializer 8 | from pydantic_xml.typedefs import EntityLocation, Location, NsMap 9 | from pydantic_xml.utils import QName, merge_nsmaps, select_ns 10 | 11 | 12 | class ElementSerializer(Serializer): 13 | @classmethod 14 | def from_core_schema(cls, schema: pcs.IsInstanceSchema, ctx: Serializer.Context) -> 'ElementSerializer': 15 | name = ctx.entity_path or ctx.field_alias or ctx.field_name 16 | ns = select_ns(ctx.entity_ns, ctx.parent_ns) 17 | nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) 18 | search_mode = ctx.search_mode 19 | computed = ctx.field_computed 20 | 21 | if name is None: 22 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") 23 | 24 | return cls(name, ns, nsmap, search_mode, computed) 25 | 26 | def __init__(self, name: str, ns: Optional[str], nsmap: Optional[NsMap], search_mode: SearchMode, computed: bool): 27 | self._computed = computed 28 | self._nsmap = nsmap 29 | self._search_mode = search_mode 30 | self._element_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap).uri 31 | 32 | def serialize( 33 | self, 34 | element: XmlElementWriter, 35 | value: Any, 36 | encoded: Any, 37 | *, 38 | skip_empty: bool = False, 39 | exclude_none: bool = False, 40 | exclude_unset: bool = False, 41 | ) -> Optional[XmlElementWriter]: 42 | if value is None: 43 | return element 44 | 45 | sub_element = element.from_native(value) 46 | if skip_empty and sub_element.is_empty(): 47 | return None 48 | else: 49 | element.append_element(sub_element) 50 | return sub_element 51 | 52 | def deserialize( 53 | self, 54 | element: Optional[XmlElementReader], 55 | *, 56 | context: Optional[Dict[str, Any]], 57 | sourcemap: Dict[Location, int], 58 | loc: Location, 59 | ) -> Optional[str]: 60 | if self._computed: 61 | return None 62 | 63 | if element is None: 64 | return None 65 | 66 | if (sub_element := element.pop_element(self._element_name, self._search_mode, remove=True)) is not None: 67 | sourcemap[loc] = sub_element.get_sourceline() 68 | return sub_element.to_native() 69 | else: 70 | return None 71 | 72 | 73 | def from_core_schema(schema: pcs.IsInstanceSchema, ctx: Serializer.Context) -> Serializer: 74 | if ctx.entity_location is EntityLocation.ELEMENT: 75 | return ElementSerializer.from_core_schema(schema, ctx) 76 | elif ctx.entity_location is None: 77 | return ElementSerializer.from_core_schema(schema, ctx) 78 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 79 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of raw types are not supported") 80 | else: 81 | raise AssertionError("unreachable") 82 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/tagged_union.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Any, Dict, Optional 3 | 4 | from pydantic_core import core_schema as pcs 5 | 6 | import pydantic_xml as pxml 7 | from pydantic_xml import errors 8 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 9 | from pydantic_xml.serializers import factories 10 | from pydantic_xml.serializers.factories.model import ModelProxySerializer 11 | from pydantic_xml.serializers.factories.primitive import AttributeSerializer 12 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, SearchMode, Serializer 13 | from pydantic_xml.typedefs import Location 14 | 15 | 16 | class ModelSerializer(Serializer): 17 | @classmethod 18 | def from_core_schema(cls, schema: pcs.TaggedUnionSchema, ctx: Serializer.Context) -> 'ModelSerializer': 19 | computed = ctx.field_computed 20 | search_mode = ctx.search_mode 21 | 22 | discriminator = schema['discriminator'] 23 | if not isinstance(discriminator, str): 24 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "only string discriminators are supported") 25 | 26 | discriminating_attr_name: Optional[str] = None 27 | inner_serializers: Dict[str, ModelProxySerializer] = {} 28 | for tag, choice_schema in schema['choices'].items(): 29 | if not isinstance(tag, str): 30 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "tagged union only supports string tags") 31 | 32 | serializer = Serializer.parse_core_schema(choice_schema, ctx) 33 | assert isinstance(serializer, ModelProxySerializer), "unexpected serializer type" 34 | inner_serializers[tag] = serializer 35 | 36 | model_serializer = serializer.model_serializer 37 | assert isinstance(model_serializer, factories.model.ModelSerializer), "unexpected model serializer type" 38 | 39 | discriminator_serializer = model_serializer.fields_serializers.get(discriminator) 40 | if not isinstance(discriminator_serializer, AttributeSerializer): 41 | raise errors.ModelFieldError( 42 | ctx.model_name, ctx.field_name, "discriminator field must be an xml attribute", 43 | ) 44 | 45 | if discriminating_attr_name is not None and discriminating_attr_name != discriminator_serializer.attr_name: 46 | raise errors.ModelFieldError( 47 | ctx.model_name, ctx.field_name, "sub-models discriminating attributes must have the same name", 48 | ) 49 | discriminating_attr_name = discriminator_serializer.attr_name 50 | 51 | assert discriminating_attr_name is not None, "schema choices are not provided" 52 | 53 | return cls(computed, discriminator, discriminating_attr_name, inner_serializers, search_mode) 54 | 55 | def __init__( 56 | self, 57 | computed: bool, 58 | discriminator: str, 59 | discriminating_attr_name: str, 60 | inner_serializers: Dict[str, ModelProxySerializer], 61 | search_mode: SearchMode, 62 | ): 63 | self._computed = computed 64 | self._discriminator = discriminator 65 | self._discriminating_attr_name = discriminating_attr_name 66 | self._inner_serializers = inner_serializers 67 | self._search_mode = search_mode 68 | 69 | def serialize( 70 | self, 71 | element: XmlElementWriter, 72 | value: 'pxml.BaseXmlModel', 73 | encoded: Dict[str, Any], 74 | *, 75 | skip_empty: bool = False, 76 | exclude_none: bool = False, 77 | exclude_unset: bool = False, 78 | ) -> Optional[XmlElementWriter]: 79 | if (tag := encoded.get(self._discriminator)) and (serializer := self._inner_serializers[tag]): 80 | return serializer.serialize( 81 | element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 82 | ) 83 | 84 | return None 85 | 86 | def deserialize( 87 | self, 88 | element: Optional[XmlElementReader], 89 | *, 90 | context: Optional[Dict[str, Any]], 91 | sourcemap: Dict[Location, int], 92 | loc: Location, 93 | ) -> Optional['pxml.BaseXmlModel']: 94 | if self._computed: 95 | return None 96 | 97 | if element is None: 98 | return None 99 | 100 | for tag, serializer in self._inner_serializers.items(): 101 | sub_element = element.find_element( 102 | serializer.element_name, 103 | self._search_mode, 104 | look_behind=False, 105 | step_forward=False, 106 | ) 107 | if sub_element is not None and sub_element.get_attrib(self._discriminating_attr_name) == tag: 108 | sourcemap[loc] = sub_element.get_sourceline() 109 | return serializer.deserialize(element, context=context, sourcemap=sourcemap, loc=loc) 110 | 111 | return None 112 | 113 | 114 | def from_core_schema(schema: pcs.TaggedUnionSchema, ctx: Serializer.Context) -> Serializer: 115 | for tag, choice_schema in schema['choices'].items(): 116 | choice_schema, ctx = Serializer.preprocess_schema(choice_schema, ctx) 117 | choice_type_family = TYPE_FAMILY.get(choice_schema['type']) 118 | 119 | if choice_type_family is not SchemaTypeFamily.MODEL: 120 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "tagged union must be of a model type") 121 | 122 | choice_schema = typing.cast(pcs.ModelSchema, choice_schema) 123 | if choice_schema['root_model']: 124 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "tagged union doesn't support root models") 125 | 126 | return ModelSerializer.from_core_schema(schema, ctx) 127 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/tuple.py: -------------------------------------------------------------------------------- 1 | from pydantic_core import core_schema as pcs 2 | 3 | from pydantic_xml import errors 4 | from pydantic_xml.serializers.serializer import Serializer 5 | 6 | from . import heterogeneous, homogeneous 7 | 8 | 9 | def from_core_schema(schema: pcs.TupleSchema, ctx: Serializer.Context) -> Serializer: 10 | # Starting from pydantic-core 2.15.0 `tuple-positional` and `tuple-variable` types 11 | # had been merged into a single `tuple` type to be able to handle variadic tuples (PEP-646). 12 | # Since that point is not possible to separate tuple into homogeneous and heterogeneous collections 13 | # by its type but only by presence of the `variadic_item_index` field in the schema. 14 | if (variadic_item_index := schema.get('variadic_item_index')) is not None: 15 | if variadic_item_index != 0: 16 | raise errors.ModelFieldError( 17 | ctx.model_name, ctx.field_name, "variadic tuples with prefixed items are not supported", 18 | ) 19 | return homogeneous.from_core_schema(schema, ctx) 20 | else: 21 | return heterogeneous.from_core_schema(schema, ctx) 22 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/typed_mapping.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic_core import core_schema as pcs 4 | 5 | from pydantic_xml import errors 6 | from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, Serializer 7 | from pydantic_xml.typedefs import EntityLocation 8 | 9 | from .mapping import AttributesSerializer, ElementSerializer 10 | 11 | 12 | def from_core_schema(schema: pcs.TypedDictSchema, ctx: Serializer.Context) -> Serializer: 13 | values_schemas: List[pcs.CoreSchema] = [] 14 | for field_name, field in schema['fields'].items(): 15 | values_schemas.append(field['schema']) 16 | 17 | for computed_field in schema['computed_fields']: 18 | values_schemas.append(computed_field['return_schema']) 19 | 20 | for val_schema in values_schemas: 21 | val_schema, val_ctx = Serializer.preprocess_schema(val_schema, ctx) 22 | if TYPE_FAMILY.get(val_schema['type']) is not SchemaTypeFamily.PRIMITIVE: 23 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "mapping values must be of a primitive type") 24 | 25 | if ctx.entity_location is EntityLocation.ELEMENT: 26 | return ElementSerializer.from_core_schema(schema, ctx) 27 | elif ctx.entity_location is None: 28 | return AttributesSerializer.from_core_schema(schema, ctx) 29 | elif ctx.entity_location is EntityLocation.ATTRIBUTE: 30 | raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of mapping type are not supported") 31 | else: 32 | raise AssertionError("unreachable") 33 | -------------------------------------------------------------------------------- /pydantic_xml/serializers/factories/wrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Sized 2 | 3 | from pydantic_core import core_schema as pcs 4 | 5 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 6 | from pydantic_xml.serializers.serializer import SearchMode, Serializer 7 | from pydantic_xml.typedefs import Location, NsMap 8 | from pydantic_xml.utils import QName, merge_nsmaps, select_ns 9 | 10 | 11 | class ElementPathSerializer(Serializer): 12 | @classmethod 13 | def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'ElementPathSerializer': 14 | path = ctx.entity_path 15 | ns = select_ns(ctx.entity_ns, ctx.parent_ns) 16 | nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) 17 | search_mode = ctx.search_mode 18 | computed = ctx.field_computed 19 | 20 | assert path is not None, "path is not provided" 21 | 22 | ctx = ctx.child(entity_info=ctx.entity_wrapped) 23 | inner_serializer = Serializer.parse_core_schema(schema, ctx) 24 | 25 | return cls(path, ns, nsmap, search_mode, computed, inner_serializer) 26 | 27 | def __init__( 28 | self, 29 | path: str, 30 | ns: Optional[str], 31 | nsmap: Optional[NsMap], 32 | search_mode: SearchMode, 33 | computed: bool, 34 | inner_serializer: Serializer, 35 | ): 36 | self._path = tuple(QName.from_alias(tag=part, ns=ns, nsmap=nsmap).uri for part in path.split('/')) 37 | self._nsmap = nsmap 38 | self._search_mode = search_mode 39 | self._computed = computed 40 | self._inner_serializer = inner_serializer 41 | 42 | def serialize( 43 | self, 44 | element: XmlElementWriter, 45 | value: Any, 46 | encoded: Any, 47 | *, 48 | skip_empty: bool = False, 49 | exclude_none: bool = False, 50 | exclude_unset: bool = False, 51 | ) -> Optional[XmlElementWriter]: 52 | if value is None: 53 | return element 54 | 55 | if skip_empty and isinstance(value, Sized) and len(value) == 0: 56 | return element 57 | 58 | for part in self._path: 59 | element = element.find_element_or_create(part, self._search_mode, nsmap=self._nsmap) 60 | 61 | self._inner_serializer.serialize( 62 | element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset, 63 | ) 64 | 65 | return element 66 | 67 | def deserialize( 68 | self, 69 | element: Optional[XmlElementReader], 70 | *, 71 | context: Optional[Dict[str, Any]], 72 | sourcemap: Dict[Location, int], 73 | loc: Location, 74 | ) -> Optional[Any]: 75 | if self._computed: 76 | return None 77 | 78 | if element is None: 79 | return None 80 | 81 | if sub_elements := element.find_sub_element(self._path, self._search_mode): 82 | sub_element = sub_elements[-1] 83 | if len(sub_elements) == len(self._path): 84 | sourcemap[loc] = sub_element.get_sourceline() 85 | return self._inner_serializer.deserialize(sub_element, context=context, sourcemap=sourcemap, loc=loc) 86 | else: 87 | return None 88 | else: 89 | return None 90 | 91 | 92 | def from_core_schema(schema: pcs.CoreSchema, ctx: Serializer.Context) -> Serializer: 93 | return ElementPathSerializer.from_core_schema(schema, ctx) 94 | -------------------------------------------------------------------------------- /pydantic_xml/typedefs.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import Dict, Tuple, Union 3 | 4 | Location = Tuple[Union[str, int], ...] 5 | NsMap = Dict[str, str] 6 | 7 | 8 | class EntityLocation(IntEnum): 9 | """ 10 | Field data location. 11 | """ 12 | 13 | ELEMENT = 1 # entity data is located at xml element 14 | ATTRIBUTE = 2 # entity data is located at xml attribute 15 | WRAPPED = 3 # entity data is wrapped by an element 16 | -------------------------------------------------------------------------------- /pydantic_xml/utils.py: -------------------------------------------------------------------------------- 1 | import dataclasses as dc 2 | import itertools as it 3 | import re 4 | from collections import ChainMap 5 | from typing import Dict, Iterable, List, Optional, Union, cast 6 | 7 | import pydantic as pd 8 | import pydantic_core as pdc 9 | 10 | from pydantic_xml import errors 11 | 12 | from .element.native import etree 13 | from .typedefs import Location, NsMap 14 | 15 | 16 | @dc.dataclass(frozen=True) 17 | class QName: 18 | """ 19 | XML entity qualified name. 20 | 21 | :param tag: entity tag 22 | :param ns: entity namespace 23 | """ 24 | 25 | tag: str 26 | ns: Optional[str] 27 | 28 | @classmethod 29 | def from_uri(cls, uri: str) -> 'QName': 30 | """ 31 | Creates `QName` from uri. 32 | 33 | :param uri: entity uri in format '{namespace}tag' 34 | :return: qualified name 35 | """ 36 | 37 | if (m := re.match(r'({(.*)})?(.*)', uri)) is None: 38 | raise ValueError 39 | 40 | return cls(tag=m[3], ns=m[2]) 41 | 42 | @classmethod 43 | def from_alias( 44 | cls, tag: str, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, is_attr: bool = False, 45 | ) -> 'QName': 46 | """ 47 | Creates `QName` from namespace alias. 48 | 49 | :param tag: entity tag 50 | :param ns: xml namespace alias 51 | :param nsmap: xml namespace mapping 52 | :param is_attr: is the tag of attribute type 53 | :return: qualified name 54 | """ 55 | 56 | if not is_attr or ns is not 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") 64 | 65 | return QName(tag=tag, ns=ns) 66 | 67 | @property 68 | def uri(self) -> str: 69 | return '{%s}%s' % (self.ns, self.tag) if self.ns else self.tag 70 | 71 | def __str__(self) -> str: 72 | return self.uri 73 | 74 | 75 | def merge_nsmaps(*maps: Optional[NsMap]) -> NsMap: 76 | """ 77 | Merges multiple namespace maps into s single one respecting provided order. 78 | 79 | :param maps: namespace maps 80 | :return: merged namespace 81 | """ 82 | 83 | return cast(NsMap, ChainMap(*(nsmap for nsmap in maps if nsmap))) 84 | 85 | 86 | def register_nsmap(nsmap: NsMap) -> None: 87 | """ 88 | Registers namespaces prefixes from the map. 89 | """ 90 | 91 | for prefix, uri in nsmap.items(): 92 | if prefix != '' and not re.match(r"ns\d+$", prefix): # skip default namespace and reserved ones 93 | etree.register_namespace(prefix, uri) 94 | 95 | 96 | def get_slots(o: object) -> Iterable[str]: 97 | return it.chain.from_iterable(getattr(cls, '__slots__', []) for cls in o.__class__.__mro__) 98 | 99 | 100 | def select_ns(*nss: Optional[str]) -> Optional[str]: 101 | for ns in nss: 102 | if ns is not None: 103 | return ns 104 | 105 | return None 106 | 107 | 108 | def into_validation_error( 109 | title: str, 110 | errors_map: Dict[Union[None, str, int], pd.ValidationError], 111 | ) -> pd.ValidationError: 112 | line_errors: List[pdc.InitErrorDetails] = [] 113 | for location in list(errors_map): 114 | validation_error = errors_map.pop(location) 115 | for error in validation_error.errors(): 116 | line_errors.append( 117 | pdc.InitErrorDetails( 118 | type=pdc.PydanticCustomError(error['type'], error['msg'], error.get('ctx')), 119 | loc=(location, *error['loc']) if location is not None else error['loc'], 120 | input=error['input'], 121 | ), 122 | ) 123 | 124 | return pd.ValidationError.from_exception_data( 125 | title=title, 126 | input_type='json', 127 | line_errors=line_errors, 128 | ) 129 | 130 | 131 | def set_validation_error_sourceline(err: pd.ValidationError, sourcemap: Dict[Location, int]) -> pd.ValidationError: 132 | line_errors: List[pdc.InitErrorDetails] = [] 133 | for error in err.errors(): 134 | loc, sourceline = error['loc'], -1 135 | while loc and (sourceline := sourcemap.get(loc, sourceline)) == -1: 136 | loc = tuple(loc[:-1]) 137 | 138 | line_errors.append( 139 | pdc.InitErrorDetails( 140 | type=pdc.PydanticCustomError( 141 | error['type'], 142 | "[line {sourceline}]: {orig}", 143 | {'sourceline': sourceline, 'orig': error['msg']}, 144 | ), 145 | loc=error['loc'], 146 | input=error['input'], 147 | ), 148 | ) 149 | 150 | return pd.ValidationError.from_exception_data( 151 | err.title, 152 | line_errors=line_errors, 153 | ) 154 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pydantic-xml" 3 | version = "2.17.0" 4 | description = "pydantic xml extension" 5 | authors = ["Dmitry Pershin "] 6 | license = "Unlicense" 7 | readme = "README.rst" 8 | homepage = "https://github.com/dapper91/pydantic-xml" 9 | repository = "https://github.com/dapper91/pydantic-xml" 10 | documentation = "https://pydantic-xml.readthedocs.io" 11 | keywords = [ 12 | 'pydantic', 'xml', 'serialization', 'deserialization', 'lxml', 13 | ] 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "Natural Language :: English", 18 | "License :: Public Domain", 19 | "Operating System :: OS Independent", 20 | "Topic :: Software Development :: Libraries", 21 | "Topic :: Text Processing :: Markup :: XML", 22 | 'Framework :: Pydantic', 23 | "Framework :: Pydantic :: 1", 24 | "Framework :: Pydantic :: 2", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Typing :: Typed", 33 | ] 34 | 35 | [tool.poetry.dependencies] 36 | python = ">=3.8" 37 | lxml = {version = ">=4.9.0", optional = true} 38 | pydantic = ">=2.6.0, !=2.10.0b1" 39 | pydantic-core = ">=2.15.0" 40 | 41 | furo = {version = "^2022.12.7", optional = true} 42 | Sphinx = {version = "^5.3.0", optional = true} 43 | sphinx-copybutton = {version = "^0.5.1", optional = true} 44 | sphinx_design = {version = "^0.3.0", optional = true} 45 | toml = {version = "^0.10.2", optional = true} 46 | 47 | [tool.poetry.extras] 48 | lxml = ['lxml'] 49 | docs = ['Sphinx', 'toml', 'sphinx_design', 'furo', 'sphinx-copybutton'] 50 | 51 | [tool.poetry.dev-dependencies] 52 | lxml-stubs = ">=0.4.0" 53 | mypy = "^1.4.1" 54 | pre-commit = "~3.2.0" 55 | pytest = "^7.4.0" 56 | pytest-cov = "^4.1.0" 57 | xmldiff = "2.5" 58 | 59 | [build-system] 60 | requires = ["poetry-core>=1.0.0"] 61 | build-backend = "poetry.core.masonry.api" 62 | 63 | [tool.mypy] 64 | allow_redefinition = true 65 | disallow_any_generics = true 66 | disallow_incomplete_defs = true 67 | disallow_untyped_decorators = false 68 | disallow_untyped_defs = true 69 | no_implicit_optional = true 70 | show_error_codes = true 71 | strict_equality = true 72 | warn_unused_ignores = true 73 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning 4 | default:::pydantic_xml 5 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | from typing import Union 3 | 4 | import xmldiff.actions 5 | import xmldiff.formatting 6 | import xmldiff.main 7 | from lxml import etree 8 | 9 | from pydantic_xml.element import native 10 | 11 | 12 | def assert_xml_equal( 13 | left: Union[str, bytes], 14 | right: Union[str, bytes], 15 | *, 16 | ignore_comments: bool = True, 17 | pretty: bool = True, 18 | **kwargs, 19 | ): 20 | diffs = xmldiff.main.diff_texts(left, right, **kwargs) 21 | 22 | if ignore_comments: 23 | diffs = list(filter(lambda diff: not isinstance(diff, xmldiff.actions.InsertComment), diffs)) 24 | 25 | if diffs: 26 | if pretty: 27 | parser = etree.XMLParser(remove_blank_text=True, remove_comments=ignore_comments) 28 | left = etree.tostring(etree.fromstring(left, parser=parser), pretty_print=True).decode() 29 | right = etree.tostring(etree.fromstring(right, parser=parser), pretty_print=True).decode() 30 | assert not diffs, '\n' + '\n'.join(difflib.Differ().compare(left.splitlines(), right.splitlines())) 31 | else: 32 | assert not diffs, '\n' + '\n'.join((str(diff) for diff in diffs)) 33 | 34 | 35 | def is_lxml_native() -> bool: 36 | try: 37 | import lxml.etree 38 | except ImportError: 39 | return False 40 | 41 | return native.etree is lxml.etree 42 | 43 | 44 | def fmt_sourceline(linenum: int) -> int: 45 | if is_lxml_native(): 46 | return linenum 47 | else: 48 | return -1 49 | -------------------------------------------------------------------------------- /tests/test_computed_fields.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from helpers import assert_xml_equal 4 | from pydantic import computed_field 5 | 6 | from pydantic_xml import BaseXmlModel, computed_attr, computed_element 7 | 8 | 9 | def test_computed_field(): 10 | class TestModel(BaseXmlModel, tag='model'): 11 | @computed_field 12 | def element1(self) -> str: 13 | return 'text' 14 | 15 | xml = ''' 16 | text 17 | ''' 18 | 19 | actual_obj = TestModel.from_xml(xml) 20 | 21 | actual_xml = actual_obj.to_xml() 22 | assert_xml_equal(actual_xml, xml) 23 | 24 | 25 | def test_computed_attributes(): 26 | class TestModel(BaseXmlModel, tag='model'): 27 | @computed_attr(name='attr1') 28 | def computed_attr1(self) -> str: 29 | return 'string1' 30 | 31 | @computed_attr 32 | def attr2(self) -> str: 33 | return 'string2' 34 | 35 | xml = ''' 36 | 37 | ''' 38 | 39 | actual_obj = TestModel.from_xml(xml) 40 | 41 | actual_xml = actual_obj.to_xml() 42 | assert_xml_equal(actual_xml, xml) 43 | 44 | 45 | def test_computed_elements(): 46 | class TestModel(BaseXmlModel, tag='model'): 47 | @computed_element(tag='element1') 48 | def computed_element1(self) -> str: 49 | return 'text1' 50 | 51 | @computed_element 52 | def element2(self) -> str: 53 | return 'text2' 54 | 55 | xml = ''' 56 | 57 | text1 58 | text2 59 | 60 | ''' 61 | 62 | actual_obj = TestModel.from_xml(xml) 63 | 64 | actual_xml = actual_obj.to_xml() 65 | assert_xml_equal(actual_xml, xml) 66 | 67 | 68 | def test_computed_nillable_elements(): 69 | class TestModel(BaseXmlModel, tag='model'): 70 | @computed_element(tag='element1', nillable=True) 71 | def computed_element1(self) -> Optional[int]: 72 | return None 73 | 74 | @computed_element(tag='element2', nillable=True) 75 | def computed_element2(self) -> Optional[int]: 76 | return 2 77 | 78 | xml = ''' 79 | 80 | 81 | 2 82 | 83 | ''' 84 | 85 | actual_obj = TestModel.from_xml(xml) 86 | 87 | actual_xml = actual_obj.to_xml() 88 | assert_xml_equal(actual_xml, xml) 89 | 90 | 91 | def test_computed_submodel(): 92 | class TestSumModel(BaseXmlModel): 93 | text: str 94 | 95 | class TestModel(BaseXmlModel, tag='model'): 96 | @computed_field(alias='submodel1') 97 | def computed_submodel1(self) -> TestSumModel: 98 | return TestSumModel(text='text1') 99 | 100 | @computed_element(tag='submodel2') 101 | def submodel2(self) -> TestSumModel: 102 | return TestSumModel(text='text2') 103 | 104 | xml = ''' 105 | 106 | text1 107 | text2 108 | 109 | ''' 110 | 111 | actual_obj = TestModel.from_xml(xml) 112 | 113 | actual_xml = actual_obj.to_xml() 114 | assert_xml_equal(actual_xml, xml) 115 | 116 | 117 | def test_computed_nillable_submodel(): 118 | class TestSumModel(BaseXmlModel): 119 | text: str 120 | 121 | class TestModel(BaseXmlModel, tag='model'): 122 | @computed_element(tag='submodel1', nillable=True) 123 | def submodel1(self) -> Optional[TestSumModel]: 124 | return None 125 | 126 | xml = ''' 127 | 128 | 129 | 130 | ''' 131 | 132 | actual_obj = TestModel.from_xml(xml) 133 | 134 | actual_xml = actual_obj.to_xml() 135 | assert_xml_equal(actual_xml, xml) 136 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import importlib.machinery 2 | import sys 3 | from pathlib import Path 4 | 5 | import pytest 6 | from helpers import is_lxml_native 7 | 8 | MODULE_PATH = Path(__file__).parent 9 | PROJECT_ROOT = MODULE_PATH.parent 10 | EXAMPLES_PATH = PROJECT_ROOT / 'examples' 11 | 12 | 13 | @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets').glob('*.py')), ids=lambda p: p.name) 14 | def test_snippets(snippet: Path): 15 | loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) 16 | loader.load_module('snippet') 17 | 18 | 19 | @pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above") 20 | @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets' / 'py3.9').glob('*.py')), ids=lambda p: p.name) 21 | def test_snippets_py39(snippet: Path): 22 | loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) 23 | loader.load_module('snippet') 24 | 25 | 26 | @pytest.mark.skipif(not is_lxml_native(), reason='not lxml used') 27 | @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets' / 'lxml').glob('*.py')), ids=lambda p: p.name) 28 | def test_snippets_py39(snippet: Path): 29 | loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) 30 | loader.load_module('snippet') 31 | 32 | 33 | @pytest.fixture( 34 | params=[ 35 | 'computed-entities', 36 | 'custom-encoder', 37 | 'generic-model', 38 | 'quickstart', 39 | 'self-ref-model', 40 | 'xml-serialization-decorator', 41 | pytest.param( 42 | 'xml-serialization-annotation', 43 | marks=pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above"), 44 | ), 45 | ], 46 | ) 47 | def example_dir(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch): 48 | example_dir = request.param 49 | 50 | with monkeypatch.context() as mp: 51 | path = EXAMPLES_PATH / example_dir 52 | mp.chdir(path) 53 | yield path 54 | 55 | 56 | def test_example(example_dir): 57 | loader = importlib.machinery.SourceFileLoader('example', str(example_dir / 'model.py')) 58 | loader.load_module('example') 59 | -------------------------------------------------------------------------------- /tests/test_generics.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List, TypeVar 2 | 3 | import pytest 4 | from helpers import assert_xml_equal 5 | 6 | from pydantic_xml import BaseXmlModel, attr, element 7 | 8 | 9 | def test_root_generic_model(): 10 | GenericType1 = TypeVar('GenericType1') 11 | GenericType2 = TypeVar('GenericType2') 12 | 13 | class GenericModel(BaseXmlModel, Generic[GenericType1, GenericType2], tag='model1'): 14 | attr1: GenericType1 = attr() 15 | attr2: GenericType2 = attr() 16 | 17 | xml1 = ''' 18 | 19 | ''' 20 | 21 | TestModel = GenericModel[int, float] 22 | actual_obj = TestModel.from_xml(xml1) 23 | expected_obj = TestModel(attr1=1, attr2=2.2) 24 | 25 | assert actual_obj == expected_obj 26 | 27 | actual_xml = actual_obj.to_xml() 28 | assert_xml_equal(actual_xml, xml1) 29 | 30 | xml2 = ''' 31 | 32 | ''' 33 | 34 | TestModel = GenericModel[bool, str] 35 | actual_obj = TestModel.from_xml(xml2) 36 | expected_obj = TestModel(attr1=True, attr2="string") 37 | 38 | assert actual_obj == expected_obj 39 | 40 | actual_xml = actual_obj.to_xml() 41 | assert_xml_equal(actual_xml, xml2) 42 | 43 | 44 | def test_generic_submodel(): 45 | GenericType = TypeVar('GenericType') 46 | 47 | class GenericSubModel(BaseXmlModel, Generic[GenericType]): 48 | attr1: GenericType = attr() 49 | 50 | class TestModel(BaseXmlModel, tag='model1'): 51 | model2: GenericSubModel[int] 52 | model3: GenericSubModel[float] 53 | 54 | xml = ''' 55 | 56 | 57 | 58 | 59 | ''' 60 | 61 | actual_obj = TestModel.from_xml(xml) 62 | expected_obj = TestModel( 63 | model2=GenericSubModel[int](attr1=1), 64 | model3=GenericSubModel[float](attr1=1.1), 65 | ) 66 | 67 | assert actual_obj == expected_obj 68 | 69 | actual_xml = actual_obj.to_xml() 70 | assert_xml_equal(actual_xml, xml) 71 | 72 | 73 | def test_generic_list(): 74 | GenericType = TypeVar('GenericType') 75 | 76 | class GenericModel(BaseXmlModel, Generic[GenericType], tag="model1"): 77 | elems: List[GenericType] = element(tag="elem") 78 | 79 | xml = ''' 80 | 81 | foo 82 | bar 83 | 84 | ''' 85 | 86 | actual_obj = GenericModel[str].from_xml(xml) 87 | expected_obj = GenericModel( 88 | elems = ["foo", "bar"], 89 | ) 90 | 91 | assert actual_obj == expected_obj 92 | 93 | actual_xml = actual_obj.to_xml() 94 | assert_xml_equal(actual_xml, xml) 95 | 96 | 97 | def test_generic_list_of_submodels(): 98 | GenericType = TypeVar('GenericType') 99 | 100 | class SubModel(BaseXmlModel, tag="model2"): 101 | attr1: str = attr() 102 | 103 | class GenericModel(BaseXmlModel, Generic[GenericType], tag="model1"): 104 | elems: List[GenericType] = element() 105 | 106 | xml = ''' 107 | 108 | 109 | 110 | 111 | ''' 112 | 113 | actual_obj = GenericModel[SubModel].from_xml(xml) 114 | expected_obj = GenericModel( 115 | elems=[ 116 | SubModel(attr1="foo"), 117 | SubModel(attr1="bar"), 118 | ], 119 | ) 120 | 121 | assert actual_obj == expected_obj 122 | 123 | actual_xml = actual_obj.to_xml() 124 | assert_xml_equal(actual_xml, xml) 125 | 126 | 127 | def test_generic_model_errors(): 128 | GenericType = TypeVar('GenericType') 129 | 130 | with pytest.raises(AssertionError): 131 | class GenericModel(BaseXmlModel, Generic[GenericType], tag='model1'): 132 | attr1: GenericType = attr() 133 | 134 | GenericModel.from_xml('') 135 | -------------------------------------------------------------------------------- /tests/test_heterogeneous_collections.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Tuple 2 | 3 | import pytest 4 | from helpers import assert_xml_equal 5 | 6 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors 7 | 8 | 9 | def test_set_of_primitives_extraction(): 10 | class TestModel(BaseXmlModel, tag='model1'): 11 | elements: Tuple[int, float, str, Optional[str]] = element(tag='element') 12 | 13 | xml = ''' 14 | 15 | 1 16 | 2.2 17 | string3 18 | 19 | ''' 20 | 21 | actual_obj = TestModel.from_xml(xml) 22 | expected_obj = TestModel(elements=(1, 2.2, "string3", None)) 23 | 24 | assert actual_obj == expected_obj 25 | 26 | actual_xml = actual_obj.to_xml(skip_empty=True) 27 | assert_xml_equal(actual_xml, xml) 28 | 29 | 30 | def test_tuple_of_nillable_primitives_extraction(): 31 | class TestModel(BaseXmlModel, tag='model1'): 32 | elements: Tuple[Optional[int], Optional[float], Optional[str]] = element(tag='element', nillable=True) 33 | 34 | xml = ''' 35 | 36 | 1 37 | 38 | string3 39 | 40 | ''' 41 | 42 | actual_obj = TestModel.from_xml(xml) 43 | expected_obj = TestModel(elements=(1, None, "string3")) 44 | 45 | assert actual_obj == expected_obj 46 | 47 | actual_xml = actual_obj.to_xml() 48 | assert_xml_equal(actual_xml, xml) 49 | 50 | 51 | def test_tuple_of_submodel_extraction(): 52 | class TestSubModel1(BaseXmlModel): 53 | attr1: int = attr() 54 | element1: float = element() 55 | 56 | class TestModel(BaseXmlModel, tag='model1'): 57 | submodels: Tuple[TestSubModel1, int] = element(tag='submodel') 58 | 59 | xml = ''' 60 | 61 | 62 | 2.2 63 | 64 | 1 65 | 66 | ''' 67 | 68 | actual_obj = TestModel.from_xml(xml) 69 | expected_obj = TestModel( 70 | submodels=[ 71 | TestSubModel1(attr1=1, element1=2.2), 72 | 1, 73 | ], 74 | ) 75 | 76 | assert actual_obj == expected_obj 77 | 78 | actual_xml = actual_obj.to_xml() 79 | assert_xml_equal(actual_xml, xml) 80 | 81 | 82 | def test_list_of_root_submodel_extraction(): 83 | class TestSubModel1(RootXmlModel): 84 | root: int 85 | 86 | class TestModel(BaseXmlModel, tag='model1'): 87 | elements: Tuple[float, TestSubModel1] = element(tag='element') 88 | 89 | xml = ''' 90 | 91 | 1.1 92 | 2 93 | 94 | ''' 95 | 96 | actual_obj = TestModel.from_xml(xml) 97 | expected_obj = TestModel( 98 | elements=[ 99 | 1.1, 100 | TestSubModel1(2), 101 | ], 102 | ) 103 | 104 | assert actual_obj == expected_obj 105 | 106 | actual_xml = actual_obj.to_xml() 107 | assert_xml_equal(actual_xml, xml) 108 | 109 | 110 | def test_list_of_dict_extraction(): 111 | class TestModel(BaseXmlModel, tag='model1'): 112 | elements: Tuple[int, Dict[str, int]] = element(tag='element') 113 | 114 | xml = ''' 115 | 116 | 1 117 | 118 | 119 | ''' 120 | 121 | actual_obj = TestModel.from_xml(xml) 122 | expected_obj = TestModel( 123 | elements=[ 124 | 1, 125 | {'attr1': 1, 'attr2': 2}, 126 | ], 127 | ) 128 | 129 | assert actual_obj == expected_obj 130 | 131 | actual_xml = actual_obj.to_xml() 132 | assert_xml_equal(actual_xml, xml) 133 | 134 | 135 | def test_root_tuple_of_submodels_extraction(): 136 | class TestSubModel(BaseXmlModel, tag='model2'): 137 | text: int 138 | 139 | class TestModel(RootXmlModel, tag='model1'): 140 | root: Tuple[TestSubModel, TestSubModel, TestSubModel] 141 | 142 | xml = ''' 143 | 144 | 1 145 | 2 146 | 3 147 | 148 | ''' 149 | 150 | actual_obj = TestModel.from_xml(xml) 151 | expected_obj = TestModel( 152 | [ 153 | TestSubModel(text=1), 154 | TestSubModel(text=2), 155 | TestSubModel(text=3), 156 | ], 157 | ) 158 | 159 | assert actual_obj == expected_obj 160 | 161 | actual_xml = actual_obj.to_xml() 162 | assert_xml_equal(actual_xml, xml) 163 | 164 | 165 | def test_heterogeneous_collection_definition_errors(): 166 | with pytest.raises(errors.ModelFieldError): 167 | class TestModel(BaseXmlModel): 168 | attr1: Tuple[int, int] = attr() 169 | 170 | with pytest.raises(errors.ModelFieldError): 171 | class TestModel(BaseXmlModel): 172 | attr1: Tuple[List[int], List[int]] 173 | 174 | with pytest.raises(errors.ModelFieldError): 175 | class TestModel(BaseXmlModel): 176 | attr1: Tuple[Tuple[int], Tuple[int]] 177 | 178 | with pytest.raises(errors.ModelFieldError): 179 | class TestModel(RootXmlModel): 180 | root: Tuple[List[int], int] 181 | 182 | with pytest.raises(errors.ModelFieldError): 183 | class TestSubModel(RootXmlModel): 184 | root: int 185 | 186 | class TestModel(RootXmlModel): 187 | root: Tuple[TestSubModel, int] 188 | -------------------------------------------------------------------------------- /tests/test_homogeneous_collections.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Set, Tuple 2 | 3 | import pytest 4 | from helpers import assert_xml_equal 5 | 6 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors 7 | 8 | 9 | def test_set_of_primitives_extraction(): 10 | class RootModel(BaseXmlModel, tag='model'): 11 | elements: Set[int] = element(tag='element') 12 | 13 | xml = ''' 14 | 15 | 1 16 | 2 17 | 3 18 | 19 | ''' 20 | 21 | actual_obj = RootModel.from_xml(xml) 22 | expected_obj = RootModel(elements={1, 2, 3}) 23 | 24 | assert actual_obj == expected_obj 25 | 26 | actual_xml = actual_obj.to_xml() 27 | assert_xml_equal(actual_xml, xml) 28 | 29 | 30 | def test_tuple_of_submodels_extraction(): 31 | class SubModel1(BaseXmlModel): 32 | attr1: int = attr() 33 | element1: float = element() 34 | 35 | class RootModel(BaseXmlModel, tag='model'): 36 | submodels: Tuple[SubModel1, ...] = element(tag='submodel') 37 | 38 | xml = ''' 39 | 40 | 41 | 2.2 42 | 43 | 44 | 4.4 45 | 46 | 47 | 6.6 48 | 49 | 50 | ''' 51 | 52 | actual_obj = RootModel.from_xml(xml) 53 | expected_obj = RootModel( 54 | submodels=[ 55 | SubModel1(attr1=1, element1=2.2), 56 | SubModel1(attr1=2, element1=4.4), 57 | SubModel1(attr1=3, element1=6.6), 58 | ], 59 | ) 60 | 61 | assert actual_obj == expected_obj 62 | 63 | actual_xml = actual_obj.to_xml() 64 | assert_xml_equal(actual_xml, xml) 65 | 66 | 67 | def test_list_of_root_submodels_extraction(): 68 | class SubModel(RootXmlModel): 69 | root: int 70 | 71 | class RootModel(BaseXmlModel, tag='model'): 72 | elements: List[SubModel] = element(tag='element') 73 | 74 | xml = ''' 75 | 76 | 1 77 | 2 78 | 3 79 | 80 | ''' 81 | 82 | actual_obj = RootModel.from_xml(xml) 83 | expected_obj = RootModel( 84 | elements=[ 85 | SubModel(1), 86 | SubModel(2), 87 | SubModel(3), 88 | ], 89 | ) 90 | 91 | assert actual_obj == expected_obj 92 | 93 | actual_xml = actual_obj.to_xml() 94 | assert_xml_equal(actual_xml, xml) 95 | 96 | 97 | def test_list_of_dicts_extraction(): 98 | class RootModel(BaseXmlModel, tag='model'): 99 | elements: List[Dict[str, int]] = element(tag='element') 100 | 101 | xml = ''' 102 | 103 | 104 | 105 | 106 | 107 | ''' 108 | 109 | actual_obj = RootModel.from_xml(xml) 110 | expected_obj = RootModel( 111 | elements=[ 112 | {'attr1': 1, 'attr2': 2}, 113 | {'attr3': 3}, 114 | {}, 115 | ], 116 | ) 117 | 118 | assert actual_obj == expected_obj 119 | 120 | actual_xml = actual_obj.to_xml() 121 | assert_xml_equal(actual_xml, xml) 122 | 123 | 124 | def test_list_of_tuples_extraction(): 125 | class RootModel(BaseXmlModel, tag='model'): 126 | elements: List[Tuple[str, Optional[int]]] = element(tag='element') 127 | 128 | xml = ''' 129 | 130 | text1 131 | 1 132 | text2 133 | 134 | text3 135 | 3 136 | 137 | ''' 138 | 139 | actual_obj = RootModel.from_xml(xml) 140 | expected_obj = RootModel( 141 | elements=[ 142 | ('text1', 1), 143 | ('text2', None), 144 | ('text3', 3), 145 | ], 146 | ) 147 | 148 | assert actual_obj == expected_obj 149 | 150 | actual_xml = actual_obj.to_xml() 151 | assert_xml_equal(actual_xml, xml) 152 | 153 | 154 | def test_list_of_tuples_of_models_extraction(): 155 | class SubModel1(RootXmlModel[str], tag='text'): 156 | pass 157 | 158 | class SubModel2(RootXmlModel[int], tag='number'): 159 | pass 160 | 161 | class RootModel(BaseXmlModel, tag='model'): 162 | elements: List[Tuple[SubModel1, Optional[SubModel2]]] 163 | 164 | xml = ''' 165 | 166 | text1 167 | 1 168 | text2 169 | text3 170 | 3 171 | 172 | ''' 173 | 174 | actual_obj = RootModel.from_xml(xml) 175 | expected_obj = RootModel( 176 | elements=[ 177 | (SubModel1('text1'), SubModel2(1)), 178 | (SubModel1('text2'), None), 179 | (SubModel1('text3'), SubModel2(3)), 180 | ], 181 | ) 182 | 183 | assert actual_obj == expected_obj 184 | 185 | actual_xml = actual_obj.to_xml() 186 | assert_xml_equal(actual_xml, xml) 187 | 188 | 189 | def test_root_list_of_submodels_extraction(): 190 | class TestSubModel(BaseXmlModel, tag='model2'): 191 | text: int 192 | 193 | class TestModel(RootXmlModel, tag='model1'): 194 | root: List[TestSubModel] = element() 195 | 196 | xml = ''' 197 | 198 | 1 199 | 2 200 | 3 201 | 202 | ''' 203 | 204 | actual_obj = TestModel.from_xml(xml) 205 | expected_obj = TestModel( 206 | [ 207 | TestSubModel(text=1), 208 | TestSubModel(text=2), 209 | TestSubModel(text=3), 210 | ], 211 | ) 212 | 213 | assert actual_obj == expected_obj 214 | 215 | actual_xml = actual_obj.to_xml() 216 | assert_xml_equal(actual_xml, xml) 217 | 218 | 219 | def test_homogeneous_collection_definition_errors(): 220 | with pytest.raises(errors.ModelFieldError): 221 | class TestModel(BaseXmlModel): 222 | attr1: List[int] = attr() 223 | 224 | with pytest.raises(errors.ModelFieldError): 225 | class TestModel(BaseXmlModel): 226 | attr1: List[Tuple[int, ...]] 227 | 228 | with pytest.raises(errors.ModelFieldError): 229 | class TestModel(BaseXmlModel): 230 | attr1: List[Tuple[int]] 231 | 232 | with pytest.raises(errors.ModelFieldError): 233 | class TestModel(RootXmlModel): 234 | root: List[List[int]] 235 | 236 | with pytest.raises(errors.ModelFieldError): 237 | class TestModel(RootXmlModel): 238 | root: List[Tuple[int, ...]] 239 | -------------------------------------------------------------------------------- /tests/test_mappings.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Mapping, Union 2 | 3 | import pytest 4 | from helpers import assert_xml_equal 5 | 6 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors 7 | 8 | 9 | def test_attrs_mapping_extraction(): 10 | class TestModel(BaseXmlModel, tag='model'): 11 | attributes: Mapping[str, int] 12 | 13 | xml = ''' 14 | 15 | ''' 16 | 17 | actual_obj = TestModel.from_xml(xml) 18 | expected_obj = TestModel(attributes={'attr1': 1, 'attr2': 2, 'attr3': 3}) 19 | 20 | assert actual_obj == expected_obj 21 | 22 | actual_xml = actual_obj.to_xml() 23 | assert_xml_equal(actual_xml, xml) 24 | 25 | 26 | def test_element_mapping_extraction(): 27 | class TestModel(BaseXmlModel, tag='model'): 28 | element1: Mapping[str, int] = element(tag='element1') 29 | 30 | xml = ''' 31 | 32 | 33 | 34 | ''' 35 | 36 | actual_obj = TestModel.from_xml(xml) 37 | expected_obj = TestModel(element1={'attr1': 1, 'attr2': 2, 'attr3': 3}) 38 | 39 | assert actual_obj == expected_obj 40 | 41 | actual_xml = actual_obj.to_xml() 42 | assert_xml_equal(actual_xml, xml) 43 | 44 | 45 | def test_root_model_attrs_mapping_extraction(): 46 | class TestModel(RootXmlModel, tag='model'): 47 | root: Dict[str, int] 48 | 49 | xml = ''' 50 | 51 | ''' 52 | 53 | actual_obj = TestModel.from_xml(xml) 54 | expected_obj = TestModel({'attr1': 1, 'attr2': 2, 'attr3': 3}) 55 | 56 | assert actual_obj == expected_obj 57 | 58 | actual_xml = actual_obj.to_xml() 59 | assert_xml_equal(actual_xml, xml) 60 | 61 | 62 | def test_root_model_element_mapping_extraction(): 63 | class TestModel(RootXmlModel, tag='model'): 64 | root: Dict[str, int] = element(tag='element1') 65 | 66 | xml = ''' 67 | 68 | 69 | 70 | ''' 71 | 72 | actual_obj = TestModel.from_xml(xml) 73 | expected_obj = TestModel({'attr1': 1, 'attr2': 2, 'attr3': 3}) 74 | 75 | assert actual_obj == expected_obj 76 | 77 | actual_xml = actual_obj.to_xml() 78 | assert_xml_equal(actual_xml, xml) 79 | 80 | 81 | def test_mapping_definition_errors(): 82 | with pytest.raises(errors.ModelFieldError): 83 | class TestModel(BaseXmlModel): 84 | attr1: Dict[str, int] = attr() 85 | 86 | with pytest.raises(errors.ModelFieldError): 87 | class TestModel(BaseXmlModel): 88 | element: Dict[str, List[int]] 89 | 90 | with pytest.raises(errors.ModelFieldError): 91 | class TestModel(BaseXmlModel): 92 | element: Dict[str, Dict[str, int]] 93 | 94 | with pytest.raises(errors.ModelFieldError): 95 | class TestModel(BaseXmlModel): 96 | element: Dict[str, Union[str, int]] 97 | 98 | with pytest.raises(errors.ModelFieldError): 99 | class TestSubModel(BaseXmlModel): 100 | attribute: int 101 | 102 | class TestModel(BaseXmlModel): 103 | element: Dict[str, TestSubModel] 104 | -------------------------------------------------------------------------------- /tests/test_named_tuple.py: -------------------------------------------------------------------------------- 1 | from typing import List, NamedTuple, Optional, Union 2 | 3 | from helpers import assert_xml_equal 4 | 5 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element 6 | 7 | 8 | def test_named_tuple_of_primitives_extraction(): 9 | class TestTuple(NamedTuple): 10 | field1: int 11 | field2: float 12 | field3: str 13 | field4: Optional[str] 14 | 15 | class TestModel(BaseXmlModel, tag='model1'): 16 | elements: TestTuple = element(tag='element') 17 | 18 | xml = ''' 19 | 20 | 1 21 | 2.2 22 | string3 23 | 24 | ''' 25 | 26 | actual_obj = TestModel.from_xml(xml) 27 | expected_obj = TestModel(elements=(1, 2.2, "string3", None)) 28 | 29 | assert actual_obj == expected_obj 30 | 31 | actual_xml = actual_obj.to_xml(skip_empty=True) 32 | assert_xml_equal(actual_xml, xml) 33 | 34 | 35 | def test_named_tuple_of_mixed_types_extraction(): 36 | class TestSubModel1(BaseXmlModel): 37 | attr1: int = attr() 38 | element1: float = element() 39 | 40 | class TestTuple(NamedTuple): 41 | field1: TestSubModel1 42 | field2: int 43 | 44 | class TestModel(BaseXmlModel, tag='model1'): 45 | submodels: TestTuple = element(tag='submodel') 46 | 47 | xml = ''' 48 | 49 | 50 | 2.2 51 | 52 | 1 53 | 54 | ''' 55 | 56 | actual_obj = TestModel.from_xml(xml) 57 | expected_obj = TestModel( 58 | submodels=[ 59 | TestSubModel1(attr1=1, element1=2.2), 60 | 1, 61 | ], 62 | ) 63 | 64 | assert actual_obj == expected_obj 65 | 66 | actual_xml = actual_obj.to_xml() 67 | assert_xml_equal(actual_xml, xml) 68 | 69 | 70 | def test_list_of_named_tuples_extraction(): 71 | class TestTuple(NamedTuple): 72 | field1: int 73 | field2: Optional[float] = None 74 | 75 | class RootModel(BaseXmlModel, tag='model'): 76 | elements: List[TestTuple] = element(tag='element') 77 | 78 | xml = ''' 79 | 80 | 1 81 | 1.1 82 | 2 83 | 84 | 3 85 | 3.3 86 | 87 | ''' 88 | 89 | actual_obj = RootModel.from_xml(xml) 90 | expected_obj = RootModel( 91 | elements=[ 92 | (1, 1.1), 93 | (2, None), 94 | (3, 3.3), 95 | ], 96 | ) 97 | 98 | assert actual_obj == expected_obj 99 | 100 | actual_xml = actual_obj.to_xml() 101 | assert_xml_equal(actual_xml, xml) 102 | 103 | 104 | def test_list_of_named_tuples_of_models_extraction(): 105 | class SubModel1(RootXmlModel[str], tag='text'): 106 | pass 107 | 108 | class SubModel2(RootXmlModel[int], tag='number'): 109 | pass 110 | 111 | class TestTuple(NamedTuple): 112 | field1: SubModel1 113 | field2: Optional[SubModel2] = None 114 | 115 | class RootModel(BaseXmlModel, tag='model'): 116 | elements: List[TestTuple] 117 | 118 | xml = ''' 119 | 120 | text1 121 | 1 122 | text2 123 | text3 124 | 3 125 | 126 | ''' 127 | 128 | actual_obj = RootModel.from_xml(xml) 129 | expected_obj = RootModel( 130 | elements=[ 131 | (SubModel1('text1'), SubModel2(1)), 132 | (SubModel1('text2'), None), 133 | (SubModel1('text3'), SubModel2(3)), 134 | ], 135 | ) 136 | 137 | assert actual_obj == expected_obj 138 | 139 | actual_xml = actual_obj.to_xml() 140 | assert_xml_equal(actual_xml, xml) 141 | 142 | 143 | def test_primitive_union_named_tuple(): 144 | class TestTuple(NamedTuple): 145 | field1: Union[int, float] 146 | field2: str 147 | field3: Union[int, float] 148 | 149 | class TestModel(BaseXmlModel, tag='model'): 150 | sublements: TestTuple = element(tag='model1') 151 | 152 | xml = ''' 153 | 154 | 1.1 155 | text 156 | 1 157 | 158 | ''' 159 | 160 | actual_obj = TestModel.from_xml(xml) 161 | expected_obj = TestModel( 162 | sublements=(float('1.1'), 'text', 1), 163 | ) 164 | 165 | assert actual_obj == expected_obj 166 | 167 | actual_xml = actual_obj.to_xml() 168 | assert_xml_equal(actual_xml, xml) 169 | -------------------------------------------------------------------------------- /tests/test_preprocessors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | import pydantic as pd 4 | import pytest 5 | from helpers import assert_xml_equal 6 | 7 | from pydantic_xml import BaseXmlModel, attr, element, xml_field_serializer, xml_field_validator 8 | from pydantic_xml.element import XmlElementReader, XmlElementWriter 9 | 10 | 11 | def test_xml_field_validator(): 12 | class TestModel(BaseXmlModel, tag='model1'): 13 | element1: List[int] = element() 14 | 15 | @xml_field_validator('element1') 16 | def validate_element(cls, element: XmlElementReader, field_name: str) -> List[int]: 17 | if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__): 18 | return list(map(int, element.pop_text().split())) 19 | 20 | return [] 21 | 22 | xml = ''' 23 | 24 | 1 2 3 4 5 25 | 26 | ''' 27 | 28 | actual_obj = TestModel.from_xml(xml) 29 | expected_obj = TestModel( 30 | element1=[1, 2, 3, 4, 5], 31 | ) 32 | 33 | assert actual_obj == expected_obj 34 | 35 | 36 | def test_xml_field_serializer(): 37 | class TestModel(BaseXmlModel, tag='model1'): 38 | element1: List[int] = element() 39 | 40 | @xml_field_serializer('element1') 41 | def serialize_element(self, element: XmlElementWriter, value: List[int], field_name: str) -> None: 42 | sub_element = element.make_element(tag=field_name, nsmap=None) 43 | sub_element.set_text(' '.join(map(str, value))) 44 | 45 | element.append_element(sub_element) 46 | 47 | expected_xml = ''' 48 | 49 | 1 2 3 4 5 50 | 51 | ''' 52 | 53 | obj = TestModel( 54 | element1=[1, 2, 3, 4, 5], 55 | ) 56 | 57 | actual_xml = obj.to_xml() 58 | assert_xml_equal(actual_xml, expected_xml) 59 | 60 | 61 | def test_pydantic_model_validator(): 62 | class TestModel(BaseXmlModel, tag='model1'): 63 | text: str 64 | attr1: str = attr() 65 | attr2: str = attr() 66 | 67 | @pd.model_validator(mode='before') 68 | def validate_before_attr1(cls, values: Dict[str, Any]) -> Dict[str, Any]: 69 | if values.get('attr1') != "expected attr value": 70 | raise ValueError('attr1') 71 | 72 | return values 73 | 74 | @pd.model_validator(mode='before') 75 | def validate_before_attr2(cls, values: Dict[str, Any]) -> Dict[str, Any]: 76 | if values.get('attr2') != "expected attr value": 77 | raise ValueError('attr2') 78 | 79 | return values 80 | 81 | @pd.model_validator(mode='after') 82 | def validate_model(self) -> 'TestModel': 83 | if self.text != "expected text value": 84 | raise ValueError('text') 85 | 86 | return self 87 | 88 | xml = 'expected text value' 89 | TestModel.from_xml(xml) 90 | 91 | xml = 'expected text value' 92 | with pytest.raises(ValueError) as err: 93 | TestModel.from_xml(xml) 94 | assert err.value.errors()[0]['ctx']['orig'] == 'Value error, attr1' 95 | 96 | xml = 'expected text value' 97 | with pytest.raises(ValueError) as err: 98 | TestModel.from_xml(xml) 99 | assert err.value.errors()[0]['ctx']['orig'] == 'Value error, attr2' 100 | 101 | xml = 'unexpected text value' 102 | with pytest.raises(ValueError) as err: 103 | TestModel.from_xml(xml) 104 | assert err.value.errors()[0]['ctx']['orig'] == 'Value error, text' 105 | -------------------------------------------------------------------------------- /tests/test_raw.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | from helpers import assert_xml_equal 4 | 5 | from pydantic_xml import BaseXmlModel, element, wrapped 6 | from pydantic_xml.element.native import ElementT, etree 7 | 8 | 9 | def test_raw_primitive_element_serialization(): 10 | class TestModel(BaseXmlModel, tag='model', arbitrary_types_allowed=True, extra='forbid'): 11 | element1: ElementT = element() 12 | element2: ElementT = element() 13 | 14 | xml = ''' 15 | 16 | text 17 | tail 18 | 19 | ''' 20 | 21 | actual_obj = TestModel.from_xml(xml) 22 | 23 | assert actual_obj.element1.tag == 'element1' 24 | assert actual_obj.element1.attrib == {'attr1': '1'} 25 | assert actual_obj.element1.text == 'text' 26 | 27 | sub_elements = list(actual_obj.element2) 28 | assert len(sub_elements) == 1 29 | assert sub_elements[0].tag == 'sub-element1' 30 | assert sub_elements[0].tail == 'tail' 31 | 32 | element1 = etree.Element('element1', attr1='1') 33 | element1.text = 'text' 34 | 35 | element2 = etree.Element('element2') 36 | sub_element = etree.Element('sub-element1') 37 | sub_element.tail = 'tail' 38 | element2.append(sub_element) 39 | 40 | actual_obj = TestModel(element1=element1, element2=element2) 41 | actual_xml = actual_obj.to_xml() 42 | assert_xml_equal(actual_xml, xml) 43 | 44 | 45 | def test_optional_raw_primitive_element_serialization(): 46 | class TestModel(BaseXmlModel, tag='model', arbitrary_types_allowed=True, extra='forbid'): 47 | element1: Optional[ElementT] = element(default=None) 48 | element2: ElementT = element() 49 | 50 | xml = ''' 51 | 52 | text 53 | 54 | ''' 55 | 56 | actual_obj = TestModel.from_xml(xml) 57 | 58 | assert actual_obj.element1 is None 59 | assert actual_obj.element2.text == 'text' 60 | 61 | element2 = etree.Element('element2') 62 | element2.text = 'text' 63 | actual_obj = TestModel(element1=None, element2=element2) 64 | actual_xml = actual_obj.to_xml() 65 | assert_xml_equal(actual_xml, xml) 66 | 67 | 68 | def test_raw_element_homogeneous_collection_serialization(): 69 | class TestModel(BaseXmlModel, tag='model', arbitrary_types_allowed=True, extra='forbid'): 70 | field1: List[ElementT] = element(tag="element1") 71 | 72 | xml = ''' 73 | 74 | text 1 75 | text 2 76 | 77 | ''' 78 | 79 | actual_obj = TestModel.from_xml(xml) 80 | 81 | assert len(actual_obj.field1) == 2 82 | assert actual_obj.field1[0].tag == 'element1' 83 | assert actual_obj.field1[0].attrib == {'attr1': '1'} 84 | assert actual_obj.field1[0].text == 'text 1' 85 | assert actual_obj.field1[1].tag == 'element1' 86 | assert actual_obj.field1[1].text == 'text 2' 87 | 88 | field10 = etree.Element('element1', attr1='1') 89 | field10.text = 'text 1' 90 | 91 | field11 = etree.Element('element1') 92 | field11.text = 'text 2' 93 | 94 | actual_obj = TestModel(field1=[field10, field11]) 95 | actual_xml = actual_obj.to_xml() 96 | assert_xml_equal(actual_xml, xml) 97 | 98 | 99 | def test_raw_element_heterogeneous_collection_serialization(): 100 | class TestModel(BaseXmlModel, tag='model', arbitrary_types_allowed=True, extra='forbid'): 101 | field1: Tuple[ElementT, ElementT] = element(tag="element1") 102 | 103 | xml = ''' 104 | 105 | text 1 106 | text 2 107 | 108 | ''' 109 | 110 | actual_obj = TestModel.from_xml(xml) 111 | 112 | assert len(actual_obj.field1) == 2 113 | assert actual_obj.field1[0].tag == 'element1' 114 | assert actual_obj.field1[0].attrib == {'attr1': '1'} 115 | assert actual_obj.field1[0].text == 'text 1' 116 | assert actual_obj.field1[1].tag == 'element1' 117 | assert actual_obj.field1[1].text == 'text 2' 118 | 119 | field10 = etree.Element('element1', attr1='1') 120 | field10.text = 'text 1' 121 | 122 | field11 = etree.Element('element1') 123 | field11.text = 'text 2' 124 | 125 | actual_obj = TestModel(field1=[field10, field11]) 126 | actual_xml = actual_obj.to_xml() 127 | assert_xml_equal(actual_xml, xml) 128 | 129 | 130 | def test_wrapped_raw_element_serialization(): 131 | class TestModel(BaseXmlModel, tag='model', arbitrary_types_allowed=True, extra='forbid'): 132 | field1: ElementT = wrapped('wrapper', element(tag="element1")) 133 | 134 | xml = ''' 135 | 136 | 137 | text 1 138 | 139 | 140 | ''' 141 | 142 | actual_obj = TestModel.from_xml(xml) 143 | 144 | assert actual_obj.field1.tag == 'element1' 145 | assert actual_obj.field1.attrib == {'attr1': '1'} 146 | assert actual_obj.field1.text == 'text 1' 147 | 148 | field1 = etree.Element('element1', attr1='1') 149 | field1.text = 'text 1' 150 | 151 | actual_obj = TestModel(field1=field1) 152 | actual_xml = actual_obj.to_xml() 153 | assert_xml_equal(actual_xml, xml) 154 | -------------------------------------------------------------------------------- /tests/test_submodels.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pytest 4 | from helpers import assert_xml_equal 5 | 6 | from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors 7 | 8 | 9 | def test_submodel_element_extraction(): 10 | class TestSubModel(BaseXmlModel, tag='model2'): 11 | attr1: int = attr() 12 | element1: float = element() 13 | 14 | class TestModel(BaseXmlModel, tag='model1'): 15 | model2: TestSubModel 16 | 17 | xml = ''' 18 | 19 | 20 | 2.2 21 | 22 | 23 | ''' 24 | 25 | actual_obj = TestModel.from_xml(xml) 26 | expected_obj = TestModel( 27 | model2=TestSubModel( 28 | attr1=1, 29 | element1=2.2, 30 | ), 31 | ) 32 | 33 | assert actual_obj == expected_obj 34 | 35 | actual_xml = actual_obj.to_xml() 36 | assert_xml_equal(actual_xml, xml) 37 | 38 | 39 | def test_optional_submodel_element_extraction(): 40 | class TestSubModel(BaseXmlModel, tag='model2'): 41 | element1: float = element() 42 | 43 | class TestModel(BaseXmlModel, tag='model1'): 44 | model2: Optional[TestSubModel] = None 45 | 46 | xml = '''''' 47 | 48 | actual_obj = TestModel.from_xml(xml) 49 | expected_obj = TestModel(model2=None) 50 | 51 | assert actual_obj == expected_obj 52 | 53 | actual_xml = actual_obj.to_xml() 54 | assert_xml_equal(actual_xml, xml) 55 | 56 | 57 | def test_nillable_submodel_element_extraction(): 58 | class TestSubModel(BaseXmlModel): 59 | text: int 60 | 61 | class TestModel(BaseXmlModel, tag='model1'): 62 | model2: Optional[TestSubModel] = element(default=None, nillable=True) 63 | model3: Optional[TestSubModel] = element(default=None, nillable=True) 64 | model4: Optional[TestSubModel] = element(default=None, nillable=True) 65 | 66 | xml = ''' 67 | 68 | 69 | 3 70 | 4 71 | 72 | ''' 73 | 74 | actual_obj = TestModel.from_xml(xml) 75 | expected_obj = TestModel( 76 | model2=None, 77 | model3=TestSubModel(text=3), 78 | model4=TestSubModel(text=4), 79 | ) 80 | 81 | assert actual_obj == expected_obj 82 | 83 | actual_xml = actual_obj.to_xml() 84 | expected_xml = ''' 85 | 86 | 87 | 3 88 | 4 89 | 90 | ''' 91 | 92 | assert_xml_equal(actual_xml, expected_xml) 93 | 94 | 95 | def test_root_submodel_element_extraction(): 96 | class TestSubModel(RootXmlModel, tag='model2'): 97 | root: int 98 | 99 | class TestModel(BaseXmlModel, tag='model1'): 100 | model2: TestSubModel = element() 101 | 102 | xml = ''' 103 | 104 | 1 105 | 106 | ''' 107 | 108 | actual_obj = TestModel.from_xml(xml) 109 | expected_obj = TestModel( 110 | model2=TestSubModel(1), 111 | ) 112 | 113 | assert actual_obj == expected_obj 114 | 115 | actual_xml = actual_obj.to_xml() 116 | assert_xml_equal(actual_xml, xml) 117 | 118 | 119 | def test_root_submodel_root_extraction(): 120 | class TestSubModel(RootXmlModel, tag='model2'): 121 | root: int 122 | 123 | class TestModel(RootXmlModel, tag='model1'): 124 | root: TestSubModel 125 | 126 | xml = ''' 127 | 128 | 1 129 | 130 | ''' 131 | 132 | actual_obj = TestModel.from_xml(xml) 133 | expected_obj = TestModel(TestSubModel(1)) 134 | 135 | assert actual_obj == expected_obj 136 | 137 | actual_xml = actual_obj.to_xml() 138 | assert_xml_equal(actual_xml, xml) 139 | 140 | 141 | def test_nested_root_submodel_element_extraction(): 142 | class TestSubModel2(RootXmlModel, tag='model2'): 143 | root: int 144 | 145 | class TestSubModel1(RootXmlModel): 146 | root: TestSubModel2 147 | 148 | class TestModel(BaseXmlModel, tag='model1'): 149 | element1: TestSubModel1 = element() 150 | 151 | xml = ''' 152 | 153 | 154 | 1 155 | 156 | 157 | ''' 158 | 159 | actual_obj = TestModel.from_xml(xml) 160 | expected_obj = TestModel( 161 | element1=TestSubModel1(TestSubModel2(1)), 162 | ) 163 | 164 | assert actual_obj == expected_obj 165 | 166 | actual_xml = actual_obj.to_xml() 167 | assert_xml_equal(actual_xml, xml) 168 | 169 | 170 | def test_submodel_definition_errors(): 171 | class TestSubModel(BaseXmlModel): 172 | attribute: int 173 | 174 | with pytest.raises(errors.ModelFieldError): 175 | 176 | class TestModel(BaseXmlModel): 177 | attr1: TestSubModel = attr() 178 | 179 | with pytest.raises(errors.ModelFieldError): 180 | class TestModel(RootXmlModel): 181 | root: TestSubModel = attr() 182 | --------------------------------------------------------------------------------