Skip to content

Commit d717a29

Browse files
authored
Merge pull request #10 from dapper91/dev
- recursive (self-referencing) models support added. - inherit_ns flag dropped due to recursive models implementation details. - python 3.11 support added.
2 parents 37c3f10 + d7eddb2 commit d717a29

File tree

12 files changed

+276
-241
lines changed

12 files changed

+276
-241
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ['3.8', '3.9', '3.10']
17+
python-version: ['3.8', '3.9', '3.10', '3.11']
1818
steps:
1919
- uses: actions/checkout@v2
2020
- name: Set up Python ${{ matrix.python-version }}

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
0.3.0 (2022-11-10)
5+
------------------
6+
7+
- recursive (self-referencing) models support added.
8+
- inherit_ns flag dropped due to recursive models implementation details.
9+
10+
411
0.2.2 (2022-10-07)
512
------------------
613

README.md

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,14 @@ from pydantic import conint, HttpUrl
192192

193193
from pydantic_xml import BaseXmlModel, attr, element, wrapped
194194

195+
NSMAP = {
196+
'co': 'http://www.test.com/contact',
197+
'hq': 'http://www.test.com/hq',
198+
'pd': 'http://www.test.com/prod',
199+
}
200+
195201

196-
class Headquarters(BaseXmlModel, ns='hq', nsmap={'hq': 'http://www.test.com/hq'}):
202+
class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP):
197203
country: str = element()
198204
state: str = element()
199205
city: str = element()
@@ -209,12 +215,12 @@ class Industries(BaseXmlModel):
209215
__root__: Set[str] = element(tag='Industry')
210216

211217

212-
class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True):
218+
class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP):
213219
type: str = attr()
214220
url: str
215221

216222

217-
class Product(BaseXmlModel, ns_attrs=True, inherit_ns=True):
223+
class Product(BaseXmlModel, ns_attrs=True, ns='pd', nsmap=NSMAP):
218224
status: Literal['running', 'development'] = attr()
219225
launched: Optional[int] = attr()
220226
title: str
@@ -236,7 +242,7 @@ class COO(Person):
236242
position: Literal['COO'] = attr()
237243

238244

239-
class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}):
245+
class Company(BaseXmlModel, tag='Company', nsmap=NSMAP):
240246
class CompanyType(str, Enum):
241247
PRIVATE = 'Private'
242248
PUBLIC = 'Public'
@@ -254,9 +260,9 @@ class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/pro
254260
headquarters: Headquarters
255261
socials: List[Social] = wrapped(
256262
'contacts/socials',
257-
element(tag='social', default_factory=set),
263+
element(tag='social', default_factory=list),
258264
ns='co',
259-
nsmap={'co': 'http://www.test.com/contact'}
265+
nsmap=NSMAP,
260266
)
261267

262268
products: Tuple[Product, ...] = element(tag='product', ns='pd')
@@ -428,6 +434,108 @@ print(request.json(indent=4))
428434
```
429435

430436

437+
### Self-referencing models:
438+
439+
`pydantic` library supports [self-referencing models](https://pydantic-docs.helpmanual.io/usage/postponed_annotations/#self-referencing-models).
440+
`pydantic-xml` supports it either.
441+
442+
*request.xml:*
443+
444+
```xml
445+
<Directory Name="root" Mode="rwxr-xr-x">
446+
<Directory Name="etc" Mode="rwxr-xr-x">
447+
<File Name="passwd" Mode="-rw-r--r--"/>
448+
<File Name="hosts" Mode="-rw-r--r--"/>
449+
<Directory Name="ssh" Mode="rwxr-xr-x"/>
450+
</Directory>
451+
<Directory Name="bin" Mode="rwxr-xr-x"/>
452+
<Directory Name="usr" Mode="rwxr-xr-x">
453+
<Directory Name="bin" Mode="rwxr-xr-x"/>
454+
</Directory>
455+
</Directory>
456+
```
457+
458+
*main.py:*
459+
460+
```python
461+
from typing import List, Optional
462+
463+
import pydantic_xml as pxml
464+
465+
466+
class File(pxml.BaseXmlModel, tag="File"):
467+
name: str = pxml.attr(name='Name')
468+
mode: str = pxml.attr(name='Mode')
469+
470+
471+
class Directory(pxml.BaseXmlModel, tag="Directory"):
472+
name: str = pxml.attr(name='Name')
473+
mode: str = pxml.attr(name='Mode')
474+
dirs: Optional[List['Directory']] = pxml.element(tag='Directory')
475+
files: Optional[List[File]] = pxml.element(tag='File', default_factory=list)
476+
477+
478+
with open('request.xml') as file:
479+
xml = file.read()
480+
481+
root = Directory.from_xml(xml)
482+
print(root.json(indent=4))
483+
484+
```
485+
486+
*output:*
487+
488+
```json
489+
{
490+
"name": "root",
491+
"mode": "rwxr-xr-x",
492+
"dirs": [
493+
{
494+
"name": "etc",
495+
"mode": "rwxr-xr-x",
496+
"dirs": [
497+
{
498+
"name": "ssh",
499+
"mode": "rwxr-xr-x",
500+
"dirs": [],
501+
"files": []
502+
}
503+
],
504+
"files": [
505+
{
506+
"name": "passwd",
507+
"mode": "-rw-r--r--"
508+
},
509+
{
510+
"name": "hosts",
511+
"mode": "-rw-r--r--"
512+
}
513+
]
514+
},
515+
{
516+
"name": "bin",
517+
"mode": "rwxr-xr-x",
518+
"dirs": [],
519+
"files": []
520+
},
521+
{
522+
"name": "usr",
523+
"mode": "rwxr-xr-x",
524+
"dirs": [
525+
{
526+
"name": "bin",
527+
"mode": "rwxr-xr-x",
528+
"dirs": [],
529+
"files": []
530+
}
531+
],
532+
"files": []
533+
}
534+
],
535+
"files": []
536+
}
537+
```
538+
431539
### JSON
432540

433541
Since `pydantic` supports json serialization, `pydantic-xml` could be used as xml-to-json transcoder:

examples/quickstart.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,14 @@
4545
</Company>
4646
'''
4747

48+
NSMAP = {
49+
'co': 'http://www.test.com/contact',
50+
'hq': 'http://www.test.com/hq',
51+
'pd': 'http://www.test.com/prod',
52+
}
4853

49-
class Headquarters(BaseXmlModel, ns='hq', nsmap={'hq': 'http://www.test.com/hq'}):
54+
55+
class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP):
5056
country: str = element()
5157
state: str = element()
5258
city: str = element()
@@ -62,12 +68,12 @@ class Industries(BaseXmlModel):
6268
__root__: Set[str] = element(tag='Industry')
6369

6470

65-
class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True):
71+
class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP):
6672
type: str = attr()
6773
url: str
6874

6975

70-
class Product(BaseXmlModel, ns_attrs=True, inherit_ns=True):
76+
class Product(BaseXmlModel, ns_attrs=True, ns='pd', nsmap=NSMAP):
7177
status: Literal['running', 'development'] = attr()
7278
launched: Optional[int] = attr()
7379
title: str
@@ -89,7 +95,7 @@ class COO(Person):
8995
position: Literal['COO'] = attr()
9096

9197

92-
class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}):
98+
class Company(BaseXmlModel, tag='Company', nsmap=NSMAP):
9399
class CompanyType(str, Enum):
94100
PRIVATE = 'Private'
95101
PUBLIC = 'Public'
@@ -109,7 +115,7 @@ class CompanyType(str, Enum):
109115
'contacts/socials',
110116
element(tag='social', default_factory=list),
111117
ns='co',
112-
nsmap={'co': 'http://www.test.com/contact'},
118+
nsmap=NSMAP,
113119
)
114120

115121
products: Tuple[Product, ...] = element(tag='product', ns='pd')

examples/recursive.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import List, Optional
2+
3+
import pydantic_xml as pxml
4+
5+
xml = '''
6+
<Directory Name="root" Mode="rwxr-xr-x">
7+
<Directory Name="etc" Mode="rwxr-xr-x">
8+
<File Name="passwd" Mode="-rw-r--r--"/>
9+
<File Name="hosts" Mode="-rw-r--r--"/>
10+
<Directory Name="ssh" Mode="rwxr-xr-x"/>
11+
</Directory>
12+
<Directory Name="bin" Mode="rwxr-xr-x"/>
13+
<Directory Name="usr" Mode="rwxr-xr-x">
14+
<Directory Name="bin" Mode="rwxr-xr-x"/>
15+
</Directory>
16+
</Directory>
17+
'''
18+
19+
20+
class File(pxml.BaseXmlModel, tag="File"):
21+
name: str = pxml.attr(name='Name')
22+
mode: str = pxml.attr(name='Mode')
23+
24+
25+
class Directory(pxml.BaseXmlModel, tag="Directory"):
26+
name: str = pxml.attr(name='Name')
27+
mode: str = pxml.attr(name='Mode')
28+
dirs: Optional[List['Directory']] = pxml.element(tag='Directory')
29+
files: Optional[List[File]] = pxml.element(tag='File', default_factory=list)
30+
31+
32+
root = Directory.from_xml(xml)
33+
print(root.json(indent=4))

pydantic_xml/model.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ class BaseXmlModel(pd.BaseModel, metaclass=XmlModelMeta):
172172
__xml_tag__: ClassVar[Optional[str]]
173173
__xml_ns__: ClassVar[Optional[str]]
174174
__xml_nsmap__: ClassVar[Optional[NsMap]]
175-
__xml_inherit_ns__: ClassVar[bool]
176175
__xml_ns_attrs__: ClassVar[bool]
177176
__xml_serializer__: ClassVar[Optional[serializers.ModelSerializerFactory.RootSerializer]]
178177

@@ -182,7 +181,6 @@ def __init_subclass__(
182181
tag: Optional[str] = None,
183182
ns: Optional[str] = None,
184183
nsmap: Optional[NsMap] = None,
185-
inherit_ns: bool = False,
186184
ns_attrs: bool = False,
187185
**kwargs: Any,
188186
):
@@ -192,7 +190,6 @@ def __init_subclass__(
192190
:param tag: element tag
193191
:param ns: element namespace
194192
:param nsmap: element namespace map
195-
:param inherit_ns: if `True` and ns argument is not provided - inherits namespace from the outer model
196193
:param ns_attrs: use namespaced attributes
197194
"""
198195

@@ -201,7 +198,6 @@ def __init_subclass__(
201198
cls.__xml_tag__ = tag
202199
cls.__xml_ns__ = ns
203200
cls.__xml_nsmap__ = nsmap
204-
cls.__xml_inherit_ns__ = inherit_ns
205201
cls.__xml_ns_attrs__ = ns_attrs
206202

207203
@classmethod
@@ -220,7 +216,7 @@ def from_xml_tree(cls, root: etree.Element) -> 'BaseXmlModel':
220216
:return: deserialized object
221217
"""
222218

223-
assert cls.__xml_serializer__ is not None
219+
assert cls.__xml_serializer__ is not None, "model is partially initialized"
224220
obj = cls.__xml_serializer__.deserialize(root)
225221

226222
return cls.parse_obj(obj)
@@ -254,6 +250,7 @@ def to_xml_tree(
254250

255251
assert self.__xml_serializer__ is not None
256252
root = self.__xml_serializer__.serialize(None, self, encoder=encoder, skip_empty=skip_empty)
253+
assert root is not None
257254

258255
if self.__xml_nsmap__ and (default_ns := self.__xml_nsmap__.get('')):
259256
root.set('xmlns', default_ns)
@@ -289,7 +286,6 @@ def __class_getitem__(cls, params: Union[Type[Any], Tuple[Type[Any], ...]]) -> T
289286
model.__xml_tag__ = cls.__xml_tag__
290287
model.__xml_ns__ = cls.__xml_ns__
291288
model.__xml_nsmap__ = cls.__xml_nsmap__
292-
model.__xml_inherit_ns__ = cls.__xml_inherit_ns__
293289
model.__xml_ns_attrs__ = cls.__xml_ns_attrs__
294290
model.__init_serializer__()
295291

0 commit comments

Comments
 (0)