Skip to content

Commit 485cf62

Browse files
committed
refactor: model field logic moved to a separate module.
1 parent 32e205c commit 485cf62

File tree

5 files changed

+325
-298
lines changed

5 files changed

+325
-298
lines changed

pydantic_xml/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
from . import config, errors, model
66
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
7+
from .fields import XmlFieldSerializer, XmlFieldValidator, attr, computed_attr, computed_element, element, wrapped
8+
from .fields import xml_field_serializer, xml_field_validator
9+
from .model import BaseXmlModel, RootXmlModel, create_model
910

1011
__all__ = (
1112
'BaseXmlModel',

pydantic_xml/fields.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import dataclasses as dc
2+
import typing
3+
from typing import Any, Callable, Optional, Type, TypeVar, Union
4+
5+
import pydantic as pd
6+
import pydantic_core as pdc
7+
from pydantic._internal._model_construction import ModelMetaclass # noqa
8+
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
9+
10+
from . import config, model, utils
11+
from .element import XmlElementReader, XmlElementWriter
12+
from .typedefs import EntityLocation
13+
from .utils import NsMap
14+
15+
__all__ = (
16+
'attr',
17+
'computed_attr',
18+
'computed_element',
19+
'computed_entity',
20+
'element',
21+
'wrapped',
22+
'xml_field_serializer',
23+
'xml_field_validator',
24+
'ComputedXmlEntityInfo',
25+
'SerializerFunc',
26+
'ValidatorFunc',
27+
'XmlEntityInfo',
28+
'XmlEntityInfoP',
29+
'XmlFieldSerializer',
30+
'XmlFieldValidator',
31+
)
32+
33+
34+
class XmlEntityInfoP(typing.Protocol):
35+
location: Optional[EntityLocation]
36+
path: Optional[str]
37+
ns: Optional[str]
38+
nsmap: Optional[NsMap]
39+
nillable: bool
40+
wrapped: Optional['XmlEntityInfoP']
41+
42+
43+
class XmlEntityInfo(pd.fields.FieldInfo):
44+
"""
45+
Field xml meta-information.
46+
"""
47+
48+
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
49+
50+
def __init__(
51+
self,
52+
location: Optional[EntityLocation],
53+
/,
54+
path: Optional[str] = None,
55+
ns: Optional[str] = None,
56+
nsmap: Optional[NsMap] = None,
57+
nillable: bool = False,
58+
wrapped: Optional[pd.fields.FieldInfo] = None,
59+
**kwargs: Any,
60+
):
61+
if wrapped is not None:
62+
# copy arguments from the wrapped entity to let pydantic know how to process the field
63+
for entity_field_name in utils.get_slots(wrapped):
64+
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
65+
66+
if kwargs.get('serialization_alias') is None:
67+
kwargs['serialization_alias'] = kwargs.get('alias')
68+
69+
if kwargs.get('validation_alias') is None:
70+
kwargs['validation_alias'] = kwargs.get('alias')
71+
72+
super().__init__(**kwargs)
73+
self.location = location
74+
self.path = path
75+
self.ns = ns
76+
self.nsmap = nsmap
77+
self.nillable = nillable
78+
self.wrapped: Optional[XmlEntityInfoP] = wrapped if isinstance(wrapped, XmlEntityInfo) else None
79+
80+
if config.REGISTER_NS_PREFIXES and nsmap:
81+
utils.register_nsmap(nsmap)
82+
83+
84+
_Unset: Any = pdc.PydanticUndefined
85+
86+
87+
def attr(
88+
name: Optional[str] = None,
89+
ns: Optional[str] = None,
90+
*,
91+
default: Any = pdc.PydanticUndefined,
92+
default_factory: Optional[Callable[[], Any]] = _Unset,
93+
**kwargs: Any,
94+
) -> Any:
95+
"""
96+
Marks a pydantic field as an xml attribute.
97+
98+
:param name: attribute name
99+
:param ns: attribute xml namespace
100+
:param default: the default value of the field.
101+
:param default_factory: the factory function used to construct the default for the field.
102+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
103+
"""
104+
105+
return XmlEntityInfo(
106+
EntityLocation.ATTRIBUTE,
107+
path=name, ns=ns, default=default, default_factory=default_factory,
108+
**kwargs,
109+
)
110+
111+
112+
def element(
113+
tag: Optional[str] = None,
114+
ns: Optional[str] = None,
115+
nsmap: Optional[NsMap] = None,
116+
nillable: bool = False,
117+
*,
118+
default: Any = pdc.PydanticUndefined,
119+
default_factory: Optional[Callable[[], Any]] = _Unset,
120+
**kwargs: Any,
121+
) -> Any:
122+
"""
123+
Marks a pydantic field as an xml element.
124+
125+
:param tag: element tag
126+
:param ns: element xml namespace
127+
:param nsmap: element xml namespace map
128+
:param nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
129+
:param default: the default value of the field.
130+
:param default_factory: the factory function used to construct the default for the field.
131+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
132+
"""
133+
134+
return XmlEntityInfo(
135+
EntityLocation.ELEMENT,
136+
path=tag, ns=ns, nsmap=nsmap, nillable=nillable, default=default, default_factory=default_factory,
137+
**kwargs,
138+
)
139+
140+
141+
def wrapped(
142+
path: str,
143+
entity: Optional[pd.fields.FieldInfo] = None,
144+
ns: Optional[str] = None,
145+
nsmap: Optional[NsMap] = None,
146+
*,
147+
default: Any = pdc.PydanticUndefined,
148+
default_factory: Optional[Callable[[], Any]] = _Unset,
149+
**kwargs: Any,
150+
) -> Any:
151+
"""
152+
Marks a pydantic field as a wrapped xml entity.
153+
154+
:param entity: wrapped entity
155+
:param path: entity path
156+
:param ns: element xml namespace
157+
:param nsmap: element xml namespace map
158+
:param default: the default value of the field.
159+
:param default_factory: the factory function used to construct the default for the field.
160+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
161+
"""
162+
163+
return XmlEntityInfo(
164+
EntityLocation.WRAPPED,
165+
path=path, ns=ns, nsmap=nsmap, wrapped=entity, default=default, default_factory=default_factory,
166+
**kwargs,
167+
)
168+
169+
170+
@dc.dataclass
171+
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo):
172+
"""
173+
Computed field xml meta-information.
174+
"""
175+
176+
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
177+
178+
location: Optional[EntityLocation]
179+
path: Optional[str]
180+
ns: Optional[str]
181+
nsmap: Optional[NsMap]
182+
nillable: bool
183+
wrapped: Optional[XmlEntityInfoP] # to be compliant with XmlEntityInfoP protocol
184+
185+
def __post_init__(self) -> None:
186+
if config.REGISTER_NS_PREFIXES and self.nsmap:
187+
utils.register_nsmap(self.nsmap)
188+
189+
190+
PropertyT = typing.TypeVar('PropertyT')
191+
192+
193+
def computed_entity(
194+
location: EntityLocation,
195+
prop: Optional[PropertyT] = None,
196+
**kwargs: Any,
197+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
198+
def decorator(prop: Any) -> Any:
199+
path = kwargs.pop('path', None)
200+
ns = kwargs.pop('ns', None)
201+
nsmap = kwargs.pop('nsmap', None)
202+
nillable = kwargs.pop('nillable', False)
203+
204+
descriptor_proxy = pd.computed_field(**kwargs)(prop)
205+
descriptor_proxy.decorator_info = ComputedXmlEntityInfo(
206+
location=location,
207+
path=path,
208+
ns=ns,
209+
nsmap=nsmap,
210+
nillable=nillable,
211+
wrapped=None,
212+
**dc.asdict(descriptor_proxy.decorator_info),
213+
)
214+
215+
return descriptor_proxy
216+
217+
if prop is None:
218+
return decorator
219+
else:
220+
return decorator(prop)
221+
222+
223+
def computed_attr(
224+
prop: Optional[PropertyT] = None,
225+
*,
226+
name: Optional[str] = None,
227+
ns: Optional[str] = None,
228+
**kwargs: Any,
229+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
230+
"""
231+
Marks a property as an xml attribute.
232+
233+
:param prop: decorated property
234+
:param name: attribute name
235+
:param ns: attribute xml namespace
236+
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
237+
"""
238+
239+
return computed_entity(EntityLocation.ATTRIBUTE, prop, path=name, ns=ns, **kwargs)
240+
241+
242+
def computed_element(
243+
prop: Optional[PropertyT] = None,
244+
*,
245+
tag: Optional[str] = None,
246+
ns: Optional[str] = None,
247+
nsmap: Optional[NsMap] = None,
248+
nillable: bool = False,
249+
**kwargs: Any,
250+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
251+
"""
252+
Marks a property as an xml element.
253+
254+
:param prop: decorated property
255+
:param tag: element tag
256+
:param ns: element xml namespace
257+
:param nsmap: element xml namespace map
258+
:param nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
259+
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
260+
"""
261+
262+
return computed_entity(EntityLocation.ELEMENT, prop, path=tag, ns=ns, nsmap=nsmap, nillable=nillable, **kwargs)
263+
264+
265+
ValidatorFunc = Callable[[Type['model.BaseXmlModel'], XmlElementReader, str], Any]
266+
ValidatorFuncT = TypeVar('ValidatorFuncT', bound=ValidatorFunc)
267+
268+
269+
def xml_field_validator(field: str, /, *fields: str) -> Callable[[ValidatorFuncT], ValidatorFuncT]:
270+
"""
271+
Marks the method as a field xml validator.
272+
273+
:param field: field to be validated
274+
:param fields: fields to be validated
275+
"""
276+
277+
def wrapper(func: ValidatorFuncT) -> ValidatorFuncT:
278+
setattr(func, '__xml_field_validator__', (field, *fields))
279+
return func
280+
281+
return wrapper
282+
283+
284+
SerializerFunc = Callable[['model.BaseXmlModel', XmlElementWriter, Any, str], Any]
285+
SerializerFuncT = TypeVar('SerializerFuncT', bound=SerializerFunc)
286+
287+
288+
def xml_field_serializer(field: str, /, *fields: str) -> Callable[[SerializerFuncT], SerializerFuncT]:
289+
"""
290+
Marks the method as a field xml serializer.
291+
292+
:param field: field to be serialized
293+
:param fields: fields to be serialized
294+
"""
295+
296+
def wrapper(func: SerializerFuncT) -> SerializerFuncT:
297+
setattr(func, '__xml_field_serializer__', (field, *fields))
298+
return func
299+
300+
return wrapper
301+
302+
303+
@dc.dataclass(frozen=True)
304+
class XmlFieldValidator:
305+
func: ValidatorFunc
306+
307+
308+
@dc.dataclass(frozen=True)
309+
class XmlFieldSerializer:
310+
func: SerializerFunc

0 commit comments

Comments
 (0)