Skip to content

Commit 8d92470

Browse files
committed
fix: multiple field annotations bug fixed.
1 parent 5dd3864 commit 8d92470

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

pydantic_xml/fields.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,41 @@ class XmlEntityInfoP(typing.Protocol):
4040
wrapped: Optional['XmlEntityInfoP']
4141

4242

43-
class XmlEntityInfo(pd.fields.FieldInfo):
43+
class XmlEntityInfo(pd.fields.FieldInfo, XmlEntityInfoP):
4444
"""
4545
Field xml meta-information.
4646
"""
4747

4848
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
4949

50+
@staticmethod
51+
def merge_field_infos(*field_infos: pd.fields.FieldInfo, **overrides: Any) -> pd.fields.FieldInfo:
52+
location, path, ns, nsmap, nillable, wrapped = None, None, None, None, None, None
53+
54+
for field_info in field_infos:
55+
if isinstance(field_info, XmlEntityInfo):
56+
location = field_info.location if field_info.location is not None else location
57+
path = field_info.path if field_info.path is not None else path
58+
ns = field_info.ns if field_info.ns is not None else ns
59+
nsmap = field_info.nsmap if field_info.nsmap is not None else nsmap
60+
nillable = field_info.nillable if field_info.nillable is not None else nillable
61+
wrapped = field_info.wrapped if field_info.wrapped is not None else wrapped
62+
63+
field_info = pd.fields.FieldInfo.merge_field_infos(*field_infos, **overrides)
64+
65+
xml_entity_info = XmlEntityInfo(
66+
location,
67+
path=path,
68+
ns=ns,
69+
nsmap=nsmap,
70+
nillable=nillable,
71+
wrapped=wrapped if isinstance(wrapped, XmlEntityInfo) else None,
72+
**field_info._attributes_set,
73+
)
74+
xml_entity_info.metadata = field_info.metadata
75+
76+
return xml_entity_info
77+
5078
def __init__(
5179
self,
5280
location: Optional[EntityLocation],
@@ -58,10 +86,13 @@ def __init__(
5886
wrapped: Optional[pd.fields.FieldInfo] = None,
5987
**kwargs: Any,
6088
):
89+
wrapped_metadata: list[Any] = []
6190
if wrapped is not None:
6291
# copy arguments from the wrapped entity to let pydantic know how to process the field
6392
for entity_field_name in utils.get_slots(wrapped):
64-
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
93+
if entity_field_name in pd.fields._FIELD_ARG_NAMES:
94+
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
95+
wrapped_metadata = wrapped.metadata
6596

6697
if kwargs.get('serialization_alias') is None:
6798
kwargs['serialization_alias'] = kwargs.get('alias')
@@ -70,6 +101,8 @@ def __init__(
70101
kwargs['validation_alias'] = kwargs.get('alias')
71102

72103
super().__init__(**kwargs)
104+
self.metadata.extend(wrapped_metadata)
105+
73106
self.location = location
74107
self.path = path
75108
self.ns = ns
@@ -168,7 +201,7 @@ def wrapped(
168201

169202

170203
@dc.dataclass
171-
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo):
204+
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo, XmlEntityInfoP):
172205
"""
173206
Computed field xml meta-information.
174207
"""

tests/test_misc.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import sys
12
from typing import Dict, List, Optional, Tuple, Union
23

34
import pydantic as pd
45
import pytest
56
from helpers import assert_xml_equal
67

78
from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors, wrapped
9+
from pydantic_xml.fields import XmlEntityInfo
810

911

1012
def test_xml_declaration():
@@ -377,3 +379,30 @@ def validate_field(cls, v: str, info: pd.FieldValidationInfo):
377379
'''
378380

379381
TestModel.from_xml(xml, validation_context)
382+
383+
384+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above")
385+
def test_field_info_merge():
386+
from typing import Annotated
387+
388+
from annotated_types import Ge, Lt
389+
390+
class TestModel(BaseXmlModel, tag='root'):
391+
element1: Annotated[
392+
int,
393+
pd.Field(ge=0),
394+
pd.Field(default=0, lt=100),
395+
element(nillable=True),
396+
] = element(tag='elm', lt=10)
397+
398+
field_info = TestModel.model_fields['element1']
399+
assert isinstance(field_info, XmlEntityInfo)
400+
assert field_info.metadata == [Ge(ge=0), Lt(lt=10)]
401+
assert field_info.default == 0
402+
assert field_info.nillable == True
403+
assert field_info.path == 'elm'
404+
405+
TestModel.from_xml("<root><elm>0</elm></root>")
406+
407+
with pytest.raises(pd.ValidationError):
408+
TestModel.from_xml("<root><elm>-1</elm></root>")

tests/test_wrapped.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Dict, List, Optional
22

3+
import pydantic as pd
4+
import pytest
35
from helpers import assert_xml_equal
46

57
from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, wrapped
@@ -28,6 +30,30 @@ class TestModel(BaseXmlModel, tag='model1'):
2830
assert_xml_equal(actual_xml, xml)
2931

3032

33+
@pytest.mark.parametrize(
34+
'value, field_gt, wrapped_gt, should_fail',
35+
[
36+
(1, 1, 0, True),
37+
(1, 1, None, True),
38+
(1, None, 1, True),
39+
(1, 0, 1, False),
40+
],
41+
)
42+
def test_wrapped_field(value: int, field_gt: Optional[int], wrapped_gt: Optional[int], should_fail: bool):
43+
class TestModel(BaseXmlModel, tag='model1'):
44+
data: int = wrapped('model2', pd.Field(gt=field_gt), gt=wrapped_gt)
45+
46+
xml = f'''
47+
<model1><model2>{value}</model2></model1>
48+
'''
49+
50+
if should_fail:
51+
with pytest.raises(pd.ValidationError):
52+
TestModel.from_xml(xml)
53+
else:
54+
TestModel.from_xml(xml)
55+
56+
3157
def test_wrapped_path_merge():
3258
class TestModel(BaseXmlModel, tag='model1'):
3359
element0: int = element(tag='element0')

0 commit comments

Comments
 (0)