Skip to content

Commit

Permalink
Merge pull request #2 from dapper91/dev
Browse files Browse the repository at this point in the history
Feature improvements
  • Loading branch information
dapper91 committed Aug 25, 2019
2 parents 91114fd + 4b5ff2c commit e95e976
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 45 deletions.
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: flake8
args: [--max-line-length=120]
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
=========

0.2.0 (2019-08-25)
------------------

- documentation added.
- pre-commit hooks added.
- model serialization order implemented.
- private class attributes fixed.
- indexed fields serialization fixed.


0.1.0 (2019-08-13)
------------------

Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ name = "pypi"
attrs = "~=19.0"

[dev-packages]
pre-commit = "~=1.0"
pytest = "~=4.0"
xmldiff = "~=2.0"

Expand Down
20 changes: 13 additions & 7 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import sys
sys.path.insert(0, os.path.abspath('..'))

import paxb
import paxb # noqa


# -- Project information -----------------------------------------------------
Expand All @@ -34,11 +34,17 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
]

html_theme_options = {
'github_user': 'dapper91',
'github_repo': 'paxb',
'github_banner': True,
}

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

Expand All @@ -65,8 +71,8 @@
master_doc = 'index'

intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"attrs": ("https://www.attrs.org/en/stable", None),
'python': ('https://docs.python.org/3', None),
'attrs': ('https://www.attrs.org/en/stable', None),
}

autodoc_mock_imports = ["attrs"]
autodoc_mock_imports = ['attrs']
11 changes: 10 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,13 @@ The API Documentation
.. toctree::
:maxdepth: 2

paxb/api
paxb/api


Development
-----------

.. toctree::
:maxdepth: 2

paxb/development
2 changes: 1 addition & 1 deletion docs/paxb/attrs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,4 @@ First you need to describe models. Then deserialize the document to an object an
"employees": 7742
}
]
}
}
19 changes: 19 additions & 0 deletions docs/paxb/development.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.. development:
Development
===========

Install pre-commit hooks:

.. code-block:: console
$ pre-commit install
For more information see `pre-commit <https://pre-commit.com/>`_


You can run code check manually:

.. code-block:: console
$ pre-commit run --all-file
23 changes: 12 additions & 11 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pprint
import re
from datetime import date
from pprint import pprint

import attr
import paxb as pb
Expand All @@ -9,8 +9,8 @@
xml = '''<?xml version="1.0" encoding="utf-8"?>
<doc:envelope xmlns="http://www.test.org"
xmlns:doc="http://www.test1.org">
<doc:user name="Alexey" surname="Ivanov" age="26">
<doc:user name="Alex" surname="Ivanov" age="26">
<doc:birthdate year="1992" month="06" day="14"/>
<doc:contacts>
Expand Down Expand Up @@ -52,19 +52,19 @@ class User:
surname = pb.attr()
age = pb.attr(converter=int)

birth_year = pb.wrap('birthdate', pb.attr('year', converter=int))
birth_month = pb.wrap('birthdate', pb.attr('month', converter=int))
birth_day = pb.wrap('birthdate', pb.attr('day', converter=int))
_birth_year = pb.wrap('birthdate', pb.attr('year', converter=int))
_birth_month = pb.wrap('birthdate', pb.attr('month', converter=int))
_birth_day = pb.wrap('birthdate', pb.attr('day', converter=int))

@property
def birthdate(self):
return date(year=self.birth_year, month=self.birth_month, day=self.birth_day)
return date(year=self._birth_year, month=self._birth_month, day=self._birth_day)

@birthdate.setter
def birthdate(self, value):
self.birth_year = value.year
self.birth_month = value.month
self.birth_day = value.day
self._birth_year = value.year
self._birth_month = value.month
self._birth_day = value.day

phone = pb.wrap('contacts', pb.field())
emails = pb.wrap('contacts', pb.as_list(pb.field(name='email')))
Expand All @@ -87,7 +87,8 @@ def check(self, attribute, value):
try:
user = pb.from_xml(User, xml, envelope='doc:envelope', ns_map={'doc': 'http://www.test1.org'})
user.birthdate = user.birthdate.replace(year=1993)
pprint(attr.asdict(user))

pprint.pprint(attr.asdict(user))

except (pb.exc.DeserializationError, ValueError) as e:
print(f"deserialization error: {e}")
78 changes: 62 additions & 16 deletions paxb/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""

import abc
import collections
import operator as op
import xml.etree.ElementTree as et

import attr
Expand All @@ -14,9 +16,9 @@

def get_attrs(cls):
"""
Returns all paxb attributes of a class decorated with `@model` decorator.
Returns all paxb attributes of a class decorated with :py:func:`paxb.model` decorator.
:param cls: `@model` decorated class
:param cls: :py:func:`paxb.model` decorated class
:return: paxb class attributes
"""

Expand Down Expand Up @@ -52,7 +54,7 @@ def drop_nones(d):

def first(*args):
"""
Returns first not None argument.
Returns first not `None` argument.
"""

for item in args:
Expand All @@ -76,6 +78,27 @@ def merge_dicts(*dicts):
return result


def reorder(fields, order, key):
"""
Reorders `fields` list sorting its elements in order they appear in `order` list.
Elements that are not defined in `order` list keep the original order.
:param fields: elements to be reordered
:param order: iterable that defines a new order
:param key: a function of one argument that is used to extract a comparison key from each element in `fields`
:return: reordered elements list
"""

ordered = collections.OrderedDict()
for field in fields:
ordered[key(field)] = field

for ord in reversed(order or ()):
ordered.move_to_end(ord, last=False)

return ordered.values()


class Mapper(abc.ABC):
"""
Base mapper class. All mappers are inherited from it.
Expand All @@ -88,10 +111,11 @@ def xml(self, obj, root, name=None, ns=None, ns_map=None, idx=None, encoder=defa
:param obj: object to be serialized
:param root: root element the object will be added inside
:param name: element name
:param ns: element namespace
:param ns_map: mapping from namespace prefix to full name
:param idx: element index in the xml tree
:type root: :py:class:`xml.etree.ElementTree.Element`
:param str name: element name
:param str ns: element namespace
:param dict ns_map: mapping from namespace prefix to full name
:param int idx: element index in the xml tree
:param encoder: value encoder
:return: added xml tree node
"""
Expand All @@ -102,11 +126,12 @@ def obj(self, xml, name=None, ns=None, ns_map=None, idx=None, full_path=()):
Deserialization method.
:param xml: xml tree to deserialize the object from
:param name: element name
:param ns: element namespace
:param ns_map: mapping from namespace prefix to full name
:param idx: element index in the xml tree
:param full_path: full path to the current element
:type xml: :py:class:`xml.etree.ElementTree.Element`
:param str name: element name
:param str ns: element namespace
:param dict ns_map: mapping from namespace prefix to full name
:param int idx: element index in the xml tree
:param tuple full_path: full path to the current element
:return: deserialized object
"""

Expand Down Expand Up @@ -157,10 +182,18 @@ def __init__(self, name, ns=None, ns_map=None, idx=None, required=True):
self.idx = idx
self.required = required

def xml(self, obj, root, name=None, ns=None, ns_map=None, _=None, encoder=default_encoder):
def xml(self, obj, root, name=None, ns=None, ns_map=None, idx=None, encoder=default_encoder):
name = first(self.name, name)
ns = first(self.ns, ns)
ns_map = merge_dicts(self.ns_map, ns_map)
idx = first(idx, self.idx, 1)

existing_elements = root.findall(qname(ns=ns_map.get(ns), name=name), ns_map)
if idx > len(existing_elements) + 1:
raise exc.SerializationError(
"serialization can't be completed because {name}[{cur}] is going to be serialized, "
"but {name}[{prev}] is not serialized.".format(name=name, cur=idx, prev=idx-1)
)

if obj is None:
if self.required:
Expand Down Expand Up @@ -215,7 +248,8 @@ def xml(self, obj, root, name=None, ns=None, ns_map=None, idx=None, encoder=defa
existing_elements = root.findall(qname(ns=ns_map.get(ns), name=self.name), ns_map)
if idx > len(existing_elements) + 1:
raise exc.SerializationError(
"element {} at index {} is going to be serialized, but the previous one is omitted".format(name, idx)
"serialization can't be completed because {name}[{cur}] is going to be serialized, "
"but {name}[{prev}] is not serialized.".format(name=name, cur=idx, prev=idx - 1)
)
if idx == len(existing_elements) + 1:
element = et.Element(qname(ns=ns_map.get(ns), name=self.name))
Expand Down Expand Up @@ -295,10 +329,18 @@ def __init__(self, cls, name=None, ns=None, ns_map=None, idx=None, required=True
self.idx = idx
self.required = required

def xml(self, obj, root, name=None, ns=None, ns_map=None, _=None, encoder=default_encoder):
def xml(self, obj, root, name=None, ns=None, ns_map=None, idx=None, encoder=default_encoder):
name = first(self.name, name)
ns = first(self.ns, ns)
ns_map = merge_dicts(self.ns_map, ns_map)
idx = first(idx, self.idx, 1)

existing_elements = root.findall(qname(ns=ns_map.get(ns), name=name), ns_map)
if idx > len(existing_elements) + 1:
raise exc.SerializationError(
"serialization can't be completed because {name}[{cur}] is going to be serialized, "
"but {name}[{prev}] is not serialized.".format(name=name, cur=idx, prev=idx-1)
)

if obj is None:
if self.required:
Expand All @@ -309,7 +351,7 @@ def xml(self, obj, root, name=None, ns=None, ns_map=None, _=None, encoder=defaul
element = et.Element(qname(ns=ns_map.get(ns), name=name))

serialized_fields = []
for field in attr.fields(self.cls):
for field in reorder(attr.fields(self.cls), self.order, op.attrgetter('name')):
mapper = field.metadata.get('paxb.mapper')
if mapper:
serialized = mapper.xml(getattr(obj, field.name), element, field.name, ns, ns_map, encoder=encoder)
Expand Down Expand Up @@ -343,6 +385,10 @@ def obj(self, xml, name=None, ns=None, ns_map=None, idx=None, full_path=()):
if mapper:
cls_kwargs[attr_field.name] = mapper.obj(xml, attr_field.name, ns, ns_map, full_path=full_path + (tag,))

# Alter class initialization arguments that start with underscore (_). It is necessary because of
# `attrs` library implementation specific. See https://www.attrs.org/en/stable/init.html#private-attributes.
cls_kwargs = {k.lstrip('_'): w for k, w in cls_kwargs.items()}

cls_kwargs = drop_nones(cls_kwargs)

return self.cls(**cls_kwargs)
14 changes: 11 additions & 3 deletions paxb/paxb.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ class fields are serialized in in-class definition order. The order can be alter
:param str name: model name. If `None` class name will be used
:param str ns: element namespace. If `None` empty namespace will be used or if the model
is a nested one - namespace is inherited from the containing model
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model and it's elements and all nested models
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model
and it's elements and all nested models
:param tuple order: class fields serialization order. If `None` in-class definition order is used
:param kwargs: arguments that will be passed to :py:func:`attr.ib`
"""

def decorator(cls):
cls = attr.attrs(cls, **kwargs)
if order:
for element_name in order:
if not hasattr(getattr(cls, '__attrs_attrs__'), element_name):
raise AssertionError("order element '{}' not declared in model".format(element_name))

cls.__paxb_attrs__ = (name, ns, ns_map, order)

return cls
Expand Down Expand Up @@ -90,7 +96,8 @@ def nested(cls, name=None, ns=None, ns_map=None, idx=None, **kwargs):
:param cls: nested object class. `cls` must be an instance of :py:func:`paxb.model` decorated class
:param str name: element name. If `None` model decorator `name` attribute will be used
:param str ns: element namespace. If `None` model decorator ns attribute will be used
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model and it's elements and all nested models
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model
and it's elements and all nested models
:param int idx: element index in the xml document. If `None` 1 is used
:param kwargs: arguments that will be passed to :py:func:`attr.ib`
"""
Expand All @@ -113,7 +120,8 @@ def wrapper(path, wrapped, ns=None, ns_map=None, idx=None):
:param str path: full path to the `wrapped` element. Element names are separated by slashes
:param wrapped: a wrapped element
:param str ns: element namespace. If `None` the namespace is inherited from the containing model
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model and it's elements and all nested models
:param dict ns_map: mapping from a namespace prefix to a full name. It is applied to the current model
and it's elements and all nested models
:param int idx: element index in the xml document. If `None` 1 is used
"""

Expand Down

0 comments on commit e95e976

Please sign in to comment.