Skip to content
Merged

Dev #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

0.3.0 (2022-11-10)
------------------

- recursive (self-referencing) models support added.
- inherit_ns flag dropped due to recursive models implementation details.


0.2.2 (2022-10-07)
------------------

Expand Down
120 changes: 114 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,14 @@ from pydantic import conint, HttpUrl

from pydantic_xml import BaseXmlModel, attr, element, wrapped

NSMAP = {
'co': 'http://www.test.com/contact',
'hq': 'http://www.test.com/hq',
'pd': 'http://www.test.com/prod',
}


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


class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True):
class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP):
type: str = attr()
url: str


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


class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}):
class Company(BaseXmlModel, tag='Company', nsmap=NSMAP):
class CompanyType(str, Enum):
PRIVATE = 'Private'
PUBLIC = 'Public'
Expand All @@ -254,9 +260,9 @@ class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/pro
headquarters: Headquarters
socials: List[Social] = wrapped(
'contacts/socials',
element(tag='social', default_factory=set),
element(tag='social', default_factory=list),
ns='co',
nsmap={'co': 'http://www.test.com/contact'}
nsmap=NSMAP,
)

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


### Self-referencing models:

`pydantic` library supports [self-referencing models](https://pydantic-docs.helpmanual.io/usage/postponed_annotations/#self-referencing-models).
`pydantic-xml` supports it either.

*request.xml:*

```xml
<Directory Name="root" Mode="rwxr-xr-x">
<Directory Name="etc" Mode="rwxr-xr-x">
<File Name="passwd" Mode="-rw-r--r--"/>
<File Name="hosts" Mode="-rw-r--r--"/>
<Directory Name="ssh" Mode="rwxr-xr-x"/>
</Directory>
<Directory Name="bin" Mode="rwxr-xr-x"/>
<Directory Name="usr" Mode="rwxr-xr-x">
<Directory Name="bin" Mode="rwxr-xr-x"/>
</Directory>
</Directory>
```

*main.py:*

```python
from typing import List, Optional

import pydantic_xml as pxml


class File(pxml.BaseXmlModel, tag="File"):
name: str = pxml.attr(name='Name')
mode: str = pxml.attr(name='Mode')


class Directory(pxml.BaseXmlModel, tag="Directory"):
name: str = pxml.attr(name='Name')
mode: str = pxml.attr(name='Mode')
dirs: Optional[List['Directory']] = pxml.element(tag='Directory')
files: Optional[List[File]] = pxml.element(tag='File', default_factory=list)


with open('request.xml') as file:
xml = file.read()

root = Directory.from_xml(xml)
print(root.json(indent=4))

```

*output:*

```json
{
"name": "root",
"mode": "rwxr-xr-x",
"dirs": [
{
"name": "etc",
"mode": "rwxr-xr-x",
"dirs": [
{
"name": "ssh",
"mode": "rwxr-xr-x",
"dirs": [],
"files": []
}
],
"files": [
{
"name": "passwd",
"mode": "-rw-r--r--"
},
{
"name": "hosts",
"mode": "-rw-r--r--"
}
]
},
{
"name": "bin",
"mode": "rwxr-xr-x",
"dirs": [],
"files": []
},
{
"name": "usr",
"mode": "rwxr-xr-x",
"dirs": [
{
"name": "bin",
"mode": "rwxr-xr-x",
"dirs": [],
"files": []
}
],
"files": []
}
],
"files": []
}
```

### JSON

Since `pydantic` supports json serialization, `pydantic-xml` could be used as xml-to-json transcoder:
Expand Down
16 changes: 11 additions & 5 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@
</Company>
'''

NSMAP = {
'co': 'http://www.test.com/contact',
'hq': 'http://www.test.com/hq',
'pd': 'http://www.test.com/prod',
}

class Headquarters(BaseXmlModel, ns='hq', nsmap={'hq': 'http://www.test.com/hq'}):

class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP):
country: str = element()
state: str = element()
city: str = element()
Expand All @@ -62,12 +68,12 @@ class Industries(BaseXmlModel):
__root__: Set[str] = element(tag='Industry')


class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True):
class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP):
type: str = attr()
url: str


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


class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}):
class Company(BaseXmlModel, tag='Company', nsmap=NSMAP):
class CompanyType(str, Enum):
PRIVATE = 'Private'
PUBLIC = 'Public'
Expand All @@ -109,7 +115,7 @@ class CompanyType(str, Enum):
'contacts/socials',
element(tag='social', default_factory=list),
ns='co',
nsmap={'co': 'http://www.test.com/contact'},
nsmap=NSMAP,
)

products: Tuple[Product, ...] = element(tag='product', ns='pd')
Expand Down
33 changes: 33 additions & 0 deletions examples/recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import List, Optional

import pydantic_xml as pxml

xml = '''
<Directory Name="root" Mode="rwxr-xr-x">
<Directory Name="etc" Mode="rwxr-xr-x">
<File Name="passwd" Mode="-rw-r--r--"/>
<File Name="hosts" Mode="-rw-r--r--"/>
<Directory Name="ssh" Mode="rwxr-xr-x"/>
</Directory>
<Directory Name="bin" Mode="rwxr-xr-x"/>
<Directory Name="usr" Mode="rwxr-xr-x">
<Directory Name="bin" Mode="rwxr-xr-x"/>
</Directory>
</Directory>
'''


class File(pxml.BaseXmlModel, tag="File"):
name: str = pxml.attr(name='Name')
mode: str = pxml.attr(name='Mode')


class Directory(pxml.BaseXmlModel, tag="Directory"):
name: str = pxml.attr(name='Name')
mode: str = pxml.attr(name='Mode')
dirs: Optional[List['Directory']] = pxml.element(tag='Directory')
files: Optional[List[File]] = pxml.element(tag='File', default_factory=list)


root = Directory.from_xml(xml)
print(root.json(indent=4))
8 changes: 2 additions & 6 deletions pydantic_xml/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ class BaseXmlModel(pd.BaseModel, metaclass=XmlModelMeta):
__xml_tag__: ClassVar[Optional[str]]
__xml_ns__: ClassVar[Optional[str]]
__xml_nsmap__: ClassVar[Optional[NsMap]]
__xml_inherit_ns__: ClassVar[bool]
__xml_ns_attrs__: ClassVar[bool]
__xml_serializer__: ClassVar[Optional[serializers.ModelSerializerFactory.RootSerializer]]

Expand All @@ -182,7 +181,6 @@ def __init_subclass__(
tag: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
inherit_ns: bool = False,
ns_attrs: bool = False,
**kwargs: Any,
):
Expand All @@ -192,7 +190,6 @@ def __init_subclass__(
:param tag: element tag
:param ns: element namespace
:param nsmap: element namespace map
:param inherit_ns: if `True` and ns argument is not provided - inherits namespace from the outer model
:param ns_attrs: use namespaced attributes
"""

Expand All @@ -201,7 +198,6 @@ def __init_subclass__(
cls.__xml_tag__ = tag
cls.__xml_ns__ = ns
cls.__xml_nsmap__ = nsmap
cls.__xml_inherit_ns__ = inherit_ns
cls.__xml_ns_attrs__ = ns_attrs

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

assert cls.__xml_serializer__ is not None
assert cls.__xml_serializer__ is not None, "model is partially initialized"
obj = cls.__xml_serializer__.deserialize(root)

return cls.parse_obj(obj)
Expand Down Expand Up @@ -254,6 +250,7 @@ def to_xml_tree(

assert self.__xml_serializer__ is not None
root = self.__xml_serializer__.serialize(None, self, encoder=encoder, skip_empty=skip_empty)
assert root is not None

if self.__xml_nsmap__ and (default_ns := self.__xml_nsmap__.get('')):
root.set('xmlns', default_ns)
Expand Down Expand Up @@ -289,7 +286,6 @@ def __class_getitem__(cls, params: Union[Type[Any], Tuple[Type[Any], ...]]) -> T
model.__xml_tag__ = cls.__xml_tag__
model.__xml_ns__ = cls.__xml_ns__
model.__xml_nsmap__ = cls.__xml_nsmap__
model.__xml_inherit_ns__ = cls.__xml_inherit_ns__
model.__xml_ns_attrs__ = cls.__xml_ns_attrs__
model.__init_serializer__()

Expand Down
Loading