dapper91 / pydantic-xml

python xml for humans
https://pydantic-xml.readthedocs.io
The Unlicense
141 stars 14 forks source link

Feature request: additional search mode #105

Closed bjones1 closed 9 months ago

bjones1 commented 10 months ago

Thanks for a great library! I'm really excited about and enjoying the v2 interface. The docs are excellent, which is very helpful.

I'd like to have an additional validation element search mode. I use your library to read in a user-written, XML-formatted config file, so I select the Unordered mode. However, I'd like to raise a validation error if an element isn't in the model, like strict mode, but without the ordering requirements imposed by strict mode.

Is this possible? If so, I'd certainly appreciate this feature.

Thanks!

dapper91 commented 10 months ago

Hi @bjones1

Thank you for the feedback.

I decided it is better not to add a new search_mode but respect extra='forbid' pydantic feature. Since v2.2.0 it raises an exception if extra='forbid' and the document contains an extra element/attribute/text. The following example illustrate that:

class Model(BaseXmlModel, tag='root', extra='forbid', search_mode='unordered'):
    attr1: str = attr()
    field1: str = element()

xml = '''
    <root attr1="attr value 1" attr2="attr value 2">
        <field1>field value 1</field1>
        <field2>field value 2</field2>
    </root>
'''

try:
    Model.from_xml(xml)
except pd.ValidationError as err:
    assert err.errors() == [
        {
            'type': 'extra_forbidden',
            'msg': 'Extra inputs are not permitted',
            'input': 'attr value 2',
            'loc': ('<attr> attr2',),
        },
        {
            'type': 'extra_forbidden',
            'msg': 'Extra inputs are not permitted',
            'input': 'field value 2',
            'loc': ('<element> field2',),
        },
    ]
bjones1 commented 10 months ago

Wow, this is fantastic! It's a much cleaner approach than adding an additional search_mode. I was already using extra="forbid" in my models, so this was a very simple change for me. Thank you!

However, it appears that validation errors for extra attributes don't get raised for wrapped items. Am I missing something?

from pydantic_xml import BaseXmlModel, attr, element, wrapped
import pydantic as pd

class Model(BaseXmlModel, tag="root", extra="forbid", search_mode="unordered"):
    attr1: str = attr()
    field1: str = wrapped("wrapper", element())

xml = """
    <root attr1="attr value 1" attr2="attr value 2">
        <wrapper>
            <field1>field value 1</field1>
            <field2>field value 2</field2>
        </wrapper>
    </root>
"""

try:
    Model.from_xml(xml)
except pd.ValidationError as err:
    assert err.errors() == [
        {
            "type": "extra_forbidden",
            "msg": "Extra inputs are not permitted",
            "input": "attr value 2",
            "loc": ("<attr> attr2",),
            "url": "https://errors.pydantic.dev/2.3/v/extra_forbidden",
        },
        {
            "type": "extra_forbidden",
            "msg": "Extra inputs are not permitted",
            "input": "field value 2",
            "loc": ("<element> field2",),
            "url": "https://errors.pydantic.dev/2.3/v/extra_forbidden",
        },
    ]

My output from running this (note the missing validation error for <field2>:

Traceback (most recent call last):
  File "C:\Users\bjones\documents\git\pretext-cli\test-fix.py", line 20, in <module>
    Model.from_xml(xml)
  File "C:\Users\bjones\documents\git\pretext-cli\.venv\Lib\site-packages\pydantic_xml\model.py", line 343, in from_xml
    return cls.from_xml_tree(etree.fromstring(source), context=context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\bjones\documents\git\pretext-cli\.venv\Lib\site-packages\pydantic_xml\model.py", line 326, in from_xml_tree
    obj = typing.cast(ModelT, cls.__xml_serializer__.deserialize(XmlElement.from_native(root), context=context))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\bjones\documents\git\pretext-cli\.venv\Lib\site-packages\pydantic_xml\serializers\factories\model.py", line 198, in deserialize
    self._check_extra(self._model.__name__, element)
  File "C:\Users\bjones\documents\git\pretext-cli\.venv\Lib\site-packages\pydantic_xml\serializers\factories\model.py", line 63, in _check_extra
    raise pd.ValidationError.from_exception_data(title=error_title, line_errors=line_errors)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Model
<attr> attr2
  Extra inputs are not permitted [type=extra_forbidden, input_value='attr value 2', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/extra_forbidden

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\bjones\documents\git\pretext-cli\test-fix.py", line 23, in <module>
    assert err.errors() == [
           ^^^^^^^^^^^^^^^^^
AssertionError
dapper91 commented 9 months ago

Forgot about wrapped items. FIxed it in 2.2.1:

from pydantic_xml import BaseXmlModel, attr, element, wrapped
import pydantic as pd

class Model(BaseXmlModel, tag="root", extra="forbid", search_mode="unordered"):
    attr1: str = attr()
    field1: str = wrapped("wrapper", element())

xml = """
    <root attr1="attr value 1" attr2="attr value 2">
        <wrapper>
            <field1>field value 1</field1>
            <field2>field value 2</field2>
        </wrapper>
    </root>
"""

try:
    Model.from_xml(xml)
except pd.ValidationError as err:
    assert err.errors() == [
        {
            "type": "extra_forbidden",
            "msg": "Extra inputs are not permitted",
            "input": "attr value 2",
            "loc": ("@attr2",),
            "url": "https://errors.pydantic.dev/2.3/v/extra_forbidden",
        },
        {
            "type": "extra_forbidden",
            "msg": "Extra inputs are not permitted",
            "input": "field value 2",
            "loc": ("wrapper", "field2",),
            "url": "https://errors.pydantic.dev/2.3/v/extra_forbidden",
        },
    ]
bjones1 commented 9 months ago

Fantastic! All my tests pass now -- thanks again!