Skip to content

Commit

Permalink
Merge pull request #26 from maximkulkin/more-docs
Browse files Browse the repository at this point in the history
More docs
  • Loading branch information
maximkulkin authored Aug 11, 2016
2 parents 4e08adc + 464b525 commit 25a8f94
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 8 deletions.
3 changes: 3 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dl.class, dl.data {
margin-bottom: 2em;
}
Binary file added docs/_static/lollipop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ API Reference

.. module:: lollipop

Data
====

.. autodata:: lollipop.types.MISSING

Types
=====

.. automodule:: lollipop.types
:members:
:exclude-members: MISSING

Validators
==========
Expand Down
16 changes: 10 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,13 @@
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
html_theme_options = {
'page_width': '1024px',
'github_user': 'maximkulkin',
'github_repo': 'lollipop',
'github_banner': True,
'github_type': 'star',
}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
Expand All @@ -151,7 +157,7 @@
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
html_logo = 'lollipop.png'

# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
Expand Down Expand Up @@ -204,11 +210,11 @@

# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
html_show_sourcelink = False

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
html_show_sphinx = False

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
Expand Down Expand Up @@ -345,5 +351,3 @@
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

issues_github_path = 'maximkulkin/lollipop'
30 changes: 30 additions & 0 deletions docs/custom_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,33 @@
Custom Types
============

To build a custom type object you can inherit from :class:`~lollipop.types.Type` and
implement functions `load(data, **kwargs)` and `dump(value, **kwargs)`: ::

from lollipop.types import MISSING, String
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin

class URL(String):
def _load(self, data, *args, **kwargs):
loaded = super(URL, self)._load(data, *args, **kwargs)
return urlparse(loaded)

def _dump(self, value, *args, **kwargs):
dumped = urljoin(value)
return super(URL, self)._dump(dumped, *args, **kwargs)


Other variant is to take existing type and extend it with some validations while
allowing users to add more validations: ::

from lollipop import types, validators

EMAIL_REGEXP = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"

class Email(types.String):
def __init__(self, *args, **kwargs):
super(Email, self).__init__(*args, **kwargs)
self._validators.insert(0, validators.Regexp(EMAIL_REGEXP))
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Guide

install
quickstart
objects
validation
custom_types

API Reference
Expand Down
207 changes: 207 additions & 0 deletions docs/objects.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
.. _objects:

Object schemas
==============

Declaration
-----------

Object schemas are defined with :class:`~lollipop.types.Object` class by passing
it a dictionary mapping field names to :class:`~lollipop.types.Type` instances.

So given an object ::

class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age

You can define it's type like this: ::

from lollipop.types import Object, String, Integer

PersonType = Object({
'name': String(),
'age': Integer(),
})

It will allow serializing Person types to Python's basic types (that you can use to
serialize to JSON) or validate that basic Python data: ::

PersonType.dump(Person('John', 38))
# => {"name": "John", "age": 38}

PersonType.validate({"name": "John"})
# => {"age": "Value is required"}

PersonType.load({"name": "John", "age": 38})
# => {"name": "John", "age": 38}

Yet it loads to same basic type dict instead of real object. To fix that, you need
to provide a data constructor to type object: ::

PersonType = Object({
'name': String(),
'age': Integer(),
}, constructor=Person)

PersonType.load({"name": "John", "age": 38})
# => Person(name="John", age=38)

Constructor function should take field values as keyword arguments and return
constructed object.

Value extraction
----------------

When you serialize (dump) objects, field values are expected to be object attributes.
But library actually allows controlling that. This is done with
:class:`~lollipop.types.Field` class instances. When you define your object and
pass types for it's fields, what really happens is those types are wrapped with
a :class:`~lollipop.types.Field` subclass objects. The actual object fields are
defined like this: ::

PersonType = Object({
'name': AttributeField(String()),
'age': AttributeField(Integer()),
})

Passing just a :class:`~lollipop.types.Type` instances for field types is just a
shortcut to wrap them all with a default field type which is
:class:`~lollipop.types.AttributeField`. You can change default field type with
`Object.default_field_type` argument: ::

PersonType = Object({
'name': String(),
'age': Integer(),
}, default_field_type=AttributeField)

And you can actually mix fields defined with just :class:`~lollipop.types.Type`
with fields defined with :class:`~lollipop.types.Field`. The first ones will be
wrapped with default field type while the later ones will be used as is.

:class:`~lollipop.types.AttributeField` is probably the one that would be used most
of the time. It extracts value for serialization from object attribute with the same
name as the field name. You can change the name of attribute to extract value from:
::

Person = namedtuple('Person', ['full_name'])

PersonType = Object({'name': AttributeField(String(), attribute='full_name')})

PersonType.dump(Person('John Doe')) # => {'name': 'John Doe'}

Other useful instances are :class:`~lollipop.types.MethodField` which calls given
method on the object to get value instead of getting attribute,
:class:`~lollipop.types.FunctionField` which uses given function on a serialized
object to get value, :class:`~lollipop.types.ConstantField` which always serializes
to given constant value. For last one there is another shortcut: if you provide a
value for a field which is not :class:`~lollipop.types.Type` and not
:class:`~lollipop.types.Field` then it will be wrapped with a
:class:`~lollipop.types.ConstantField`.

::

# Following lines are equivalent
Object({'answer': ConstantField(Any(), 42)}).dump(object()) # => {'answer': 42}
Object({'answer': 42}).dump(object()) # => {'answer': 42}


Object Schema Inheritance
-------------------------

To be able to allow reusing parts of schema, you can supply a base
:class:`~lollipop.type.Object`: ::

BaseType = Object({'base': String()})
InheritedType = Object(BaseType, {'foo': Integer()})

# is the same as
InheritedType = Object({'base': String(), 'foo': Integer()})

You can actually supply multple base types which allows using them as mixins: ::

TimeStamped = Object({'created_at': DateTime(), 'updated_at': DateTime()})

BaseType = Object({'base': String()})
InheritedType = Object([BaseType, TimeStamped], {'foo': Integer()})


Polymorphic types
-----------------

Sometimes you need a way to serialize and deserialize values of different types put
in the same list. Or maybe you value can be of either one of given types. E.g. you
have a graphical application which operates with objects of different shapes: ::

class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y

class Shape(object):
pass

class Circle(Shape):
def __init__(self, center, radius):
self.center = center
self.radius = radius

class Rectangle(Shape):
def __init__(self, left_top, right_bottom):
self.left_top = left_top
self.right_bottom = right_bottom

PointType = Object({'x': Integer(), 'y': Integer()}, constructor=Point)

CircleType = Object({
'center': PointType,
'radius': Integer
}, constructor=Circle)

RectangleType = Object({
'left_top': PointType,
'right_bottom': PointType,
}, constructor=Rectangle)


To support that library provides a special type - :class:`~lollipop.types.OneOf`: ::

def with_type_annotation(subject_type, type_name):
return Object(subject_type, {'type': type_name},
constructor=subject_type.constructor)

AnyShapeType = OneOf(
{
'circle': with_type_annotation(CircleType, 'circle'),
'rectangle': with_type_annotation(RectangleType, 'rectangle'),
},
dump_hint=lambda obj: obj.__class__.__name__.lower(),
load_hint=dict_value_hint('type'),
)

dumped = List(AnyShapeType).dump([
Circle(Point(5, 8), 4), Rectangle(Point(1, 10), Point(10, 1))
])
# => [
# {'type': 'circle',
# 'center': {'x': 5, 'y': 8},
# 'radius': 4},
# {'type': 'rectangle',
# 'left_top': {'x': 1, 'y': 10},
# 'right_bottom': {'x': 10, 'y': 1}}]

List(AnyShapeType).load(dumped)
# => [Circle(Point(5, 8), 4), Rectangle(Point(1, 10), Point(10, 1))]

:class:`~lollipop.types.OneOf` uses user supplied functions to determine which
particular type to use during serialization/deserialization. It helps returning
proper error messages. If you're not interested in providing detailed error message,
you can just supply all types as a list. :class:`~lollipop.types.OneOf` will try
to use each of them in given order returning first successfull result. If all types
return errors it will provide generic error message. Here is example of library's
error messages schema: ::

ErrorMessagesType = OneOf([
String(), List(String()), Dict('ErrorMessages')
], name='ErrorMessages')
62 changes: 62 additions & 0 deletions docs/validation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.. _validation:

Validation
==========

Validation allows to check that data is consistent. It is run on raw data before
it is deserialized. E.g. :class:`~lollipop.types.DateTime` deserializes string to
:class:`datetime.datetime` so validations are run on a string before it is parsed.
In :class:`~lollipop.types.Object` validations are run on a dictionary of fields
but after fields themselves were already deserialized. So if you had a field of type
:class:`~lollipop.types.DateTime` your validator will get a dictionary with
:class:`~datetime.datetime` object.

Validators are just callable objects that take one or two arguments (first is
the data to be validated, second (optional) is the operation context) and raise
:class:`~lollipop.errors.ValidationError` in case of errors. Return value of
validator is always ignored.

To add validator or validators to a type, you pass them to type contructor's
`validate` argument: ::

def is_odd(data):
if data % 2 == 0:
raise ValidationError('Value should be odd')

MyNumber = Integer(validate=is_odd)
MyNumber.load(1) # => returns 1
MyNumber.load(2) # => raises ValidationError('Value should be odd')

In simple cases you can create a :class:`~lollipop.validators.Predicate` validator
for which you need to specify a boolean function and error message: ::

is_odd = Predicate(lambda x: x % 2 != 0, 'Value should be odd')

MyNumber = Integer(validate=is_odd)

In more complex cases where you need to parametrize validator with some data
it is more convenient to create a validator class: ::

from lollipop.validators import Validator

class GreaterThan(Validator):
default_error_messages = {
'greater': 'Value should be greater than {value}'
}

def __init__(self, value, **kwargs):
super(GreaterThan, self).__init__(**kwargs)
self.value = value

def __call__(self, data):
if data <= self.value:
self._fail('greater', data=data, value=self.value)

The last example demonstrates how you can support customizing error messages in
your validators: there is a default error message keyed with string 'greater' and
users can override it when creating validator with supplying new set of error
messages in validator constructor: ::

message = 'Should be greater than answer to the Ultimate Question of Life, the Universe, and Everything'
Integer(validate=GreaterThan(42, error_messages={'greater': message}))
Loading

0 comments on commit 25a8f94

Please sign in to comment.