Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Made it possible to have multiple preparers. #63

Merged
merged 7 commits into from

3 participants

@jayd3e
Collaborator

Still need to write tests/docs for this feature. Just curious if there was an api reason for this feature not getting implemented yet. All tests pass. With this feature, you are now able to provide the preparer kwarg with a list of preparers, as opposed to a single one, like so:

class SolutionSchema(MappingSchema):
      post_id = SchemaNode(Int())
      body = SchemaNode(String(),
                        validator=Length(15, 3000),
                        preparer=[strip_whitespace,
                                  remove_multiple_spaces,
                                  remove_multiple_newlines])
@jayd3e
Collaborator

Looks like I removed some whitespace too XD.

@mcdonc
Owner

When you say "yet", do you mean it was suggested before? The feature seems fine (although small nit, always a little hinky to test for "is list" or so.. usually better to check for iter, although it's tricky on py3, there's a is_nonstr_iter func in compat).

@jayd3e
Collaborator

I couldn't find an issue where it was being suggested, yah dunno what I really meant by that in hindsight. Check out that latest commit, is that what you meant? Err and I just saw your note about is_nonstr_iter. Should I use that instead?

@jayd3e
Collaborator

Yah nvm, need to use that.

@jayd3e
Collaborator

@mcdonc This should be ready to go.

@mcdonc mcdonc merged commit 371071b into from
@mcdonc
Owner

Thanks Joe! (next time try to go a little lighter on the whitespace changes, though eh? ;-) )

@mcdonc
Owner

Hey Joe when you get a chance could you submit a different pull request and sign colander's CONTRIBUTORS.txt?

@jaseemabid

@jayd3e This should be if callable(self.preparer):?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
2  CHANGES.txt
@@ -28,7 +28,7 @@ Features
constructor. This is a string naming a node in a superclass. A node
with an ``insert_before`` will be placed before the named node in a
parent mapping schema.
-
+
Backwards Incompatibilities
~~~~~~~~~~~~~~~~~~~~~~~~~~~
View
16 colander/__init__.py
@@ -362,11 +362,11 @@ def luhnok(node, value):
try:
sum = _luhnok(value)
except:
- raise Invalid(node,
+ raise Invalid(node,
'%r is not a valid credit card number' % value)
if not (sum % 10) == 0:
- raise Invalid(node,
+ raise Invalid(node,
'%r is not a valid credit card number' % value)
def _luhnok(value):
@@ -1055,7 +1055,7 @@ def __init__(self, quant=None, rounding=None):
else:
self.quant = decimal.Decimal(quant)
self.rounding = rounding
-
+
def num(self, val):
result = decimal.Decimal(str(val))
if self.quant is not None:
@@ -1667,7 +1667,13 @@ def deserialize(self, cstruct=null):
appstruct = self.typ.deserialize(self, cstruct)
if self.preparer is not None:
- appstruct = self.preparer(appstruct)
+ # if the preparer is a function, call a single preparer
+ if hasattr(self.preparer, '__call__'):
+ appstruct = self.preparer(appstruct)
+ # if the preparer is a list, call each separate preparer
+ elif is_nonstr_iter(self.preparer):
+ for preparer in self.preparer:
+ appstruct = preparer(appstruct)
if appstruct is null:
appstruct = self.missing
@@ -1825,7 +1831,7 @@ def _Schema__new__(cls, *args, **kw):
node.add_before(insert_before, n)
return node
-Schema = _SchemaMeta('Schema', (object,),
+Schema = _SchemaMeta('Schema', (object,),
dict(schema_type=Mapping,
node_type=SchemaNode,
__new__=_Schema__new__))
View
8 colander/interfaces.py
@@ -2,7 +2,7 @@ def Preparer(value):
"""
A preparer is called after deserialization of a value but before
that value is validated.
-
+
Any modifications to ``value`` required should be made by
returning the modified value rather than modifying in-place.
@@ -10,11 +10,11 @@ def Preparer(value):
as-is.
"""
-
+
def Validator(node, value):
"""
A validator is called after preparation of the deserialized value.
-
+
If ``value`` is not valid, raise a :class:`colander.Invalid`
instance as an exception after.
@@ -57,4 +57,4 @@ def deserialize(self, node, cstruct):
If the object cannot be deserialized for any reason, a
:exc:`colander.Invalid` exception should be raised.
"""
-
+
View
10 colander/iso8601.py
@@ -53,7 +53,7 @@ class ParseError(Exception):
ZERO = timedelta(0)
class Utc(tzinfo):
"""UTC
-
+
"""
def utcoffset(self, dt):
return ZERO
@@ -67,7 +67,7 @@ def dst(self, dt):
class FixedOffset(tzinfo):
"""Fixed offset in hours and minutes from UTC
-
+
"""
def __init__(self, offset_hours, offset_minutes, name):
self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
@@ -81,13 +81,13 @@ def tzname(self, dt):
def dst(self, dt):
return ZERO
-
+
def __repr__(self):
return "<FixedOffset %r>" % self.__name
def parse_timezone(tzstring, default_timezone=UTC):
"""Parses ISO 8601 time zone specs into tzinfo offsets
-
+
"""
if tzstring == "Z":
return default_timezone
@@ -106,7 +106,7 @@ def parse_timezone(tzstring, default_timezone=UTC):
def parse_date(datestring, default_timezone=UTC):
"""Parses ISO 8601 dates into datetime objects
-
+
The timezone is parsed from the date string. However it is quite common to
have dates without a timezone (not strictly correct). In this case the
default timezone specified in default_timezone is used. This is UTC by
View
42 colander/tests/test_colander.py
@@ -110,7 +110,7 @@ def test_asdict_with_all_validator(self):
exc1.add(exc2, 2)
exc2.add(exc3, 3)
d = exc1.asdict()
- self.assertEqual(d,
+ self.assertEqual(d,
{'node1.node2.3': 'exc1; exc2; validator1; validator2',
'node1.node3': 'exc1; message1'})
@@ -187,7 +187,7 @@ def test_messages_msg_None(self):
node = DummySchemaNode(None)
exc = self._makeOne(node, None)
self.assertEqual(exc.messages(), [])
-
+
class TestAll(unittest.TestCase):
def _makeOne(self, validators):
from colander import All
@@ -218,7 +218,7 @@ def test_Invalid_children(self):
validator = self._makeOne([validator1, validator2])
exc = invalid_exc(validator, node, None)
self.assertEqual(exc.children, [exc1, exc2])
-
+
class TestFunction(unittest.TestCase):
def _makeOne(self, *arg, **kw):
from colander import Function
@@ -1433,7 +1433,7 @@ def test__zope_dottedname_style_resolve_relative(self):
typ = self._makeOne(package=colander)
node = DummySchemaNode(None)
result = typ._zope_dottedname_style(
- node,
+ node,
'.tests.test_colander.TestGlobalObject')
self.assertEqual(result, self.__class__)
@@ -1442,7 +1442,7 @@ def test__zope_dottedname_style_resolve_relative_leading_dots(self):
typ = self._makeOne(package=colander.tests)
node = DummySchemaNode(None)
result = typ._zope_dottedname_style(
- node,
+ node,
'..tests.test_colander.TestGlobalObject')
self.assertEqual(result, self.__class__)
@@ -1513,7 +1513,7 @@ def test__pkg_resources_style_resolve_relative_startswith_dot(self):
import colander
typ = self._makeOne(package=colander)
result = typ._pkg_resources_style(
- None,
+ None,
'.tests.test_colander:TestGlobalObject')
self.assertEqual(result, self.__class__)
@@ -1559,7 +1559,7 @@ def test_deserialize_using_pkgresources_style(self):
typ = self._makeOne()
node = DummySchemaNode(None)
result = typ.deserialize(
- node,
+ node,
'colander.tests.test_colander:TestGlobalObject')
self.assertEqual(result, self.__class__)
@@ -1567,7 +1567,7 @@ def test_deserialize_using_zope_dottedname_style(self):
typ = self._makeOne()
node = DummySchemaNode(None)
result = typ.deserialize(
- node,
+ node,
'colander.tests.test_colander.TestGlobalObject')
self.assertEqual(result, self.__class__)
@@ -2035,6 +2035,22 @@ def validator(node, value):
self.assertEqual(node.deserialize('value'),
'prepared_value')
+ def test_deserialize_with_multiple_preparers(self):
+ from colander import Invalid
+ typ = DummyType()
+ def preparer1(value):
+ return 'prepared1_'+value
+ def preparer2(value):
+ return 'prepared2_'+value
+ def validator(node, value):
+ if not value.startswith('prepared2_prepared1'):
+ raise Invalid(node, 'not prepared') # pragma: no cover
+ node = self._makeOne(typ,
+ preparer=[preparer1, preparer2],
+ validator=validator)
+ self.assertEqual(node.deserialize('value'),
+ 'prepared2_prepared1_value')
+
def test_deserialize_preparer_before_missing_check(self):
from colander import null
typ = DummyType()
@@ -2258,8 +2274,8 @@ def test_declarative_name_reassignment(self):
import colander
class FnordSchema(colander.Schema):
fnord = colander.SchemaNode(
- colander.Sequence(),
- colander.SchemaNode(colander.Integer(), name=''),
+ colander.Sequence(),
+ colander.SchemaNode(colander.Integer(), name=''),
name="fnord[]"
)
schema = FnordSchema()
@@ -2291,7 +2307,7 @@ def test_single_inheritance(self):
colander.Bool(),
insert_before='name',
)
-
+
class Friend(colander.Schema):
rank = rank_node
name = name_node
@@ -2558,7 +2574,7 @@ def test_flatten_mapping_has_no_name(self):
self.assertEqual(result[k], v)
for k, v in result.items():
self.assertEqual(expected[k], v)
-
+
def test_unflatten_ok(self):
import colander
fstruct = {
@@ -2630,7 +2646,7 @@ def test_unflatten_mapping_no_name(self):
self.assertEqual(result[k], v)
for k, v in result.items():
self.assertEqual(expected[k], v)
-
+
def test_flatten_unflatten_roundtrip(self):
import colander
appstruct = {
View
18 colander/tests/test_iso8601.py
@@ -16,13 +16,13 @@ def test_tzname(self):
inst = self._makeOne()
result = inst.tzname(None)
self.assertEqual(result, "UTC")
-
+
def test_dst(self):
from ..iso8601 import ZERO
inst = self._makeOne()
result = inst.dst(None)
self.assertEqual(result, ZERO)
-
+
class Test_FixedOffset(unittest.TestCase):
def _makeOne(self):
from ..iso8601 import FixedOffset
@@ -67,15 +67,15 @@ def test_default_None(self):
def test_positive(self):
tzstring = "+01:00"
result = self._callFUT(tzstring)
- self.assertEqual(result.utcoffset(None),
+ self.assertEqual(result.utcoffset(None),
datetime.timedelta(hours=1, minutes=0))
def test_negative(self):
tzstring = "-01:00"
result = self._callFUT(tzstring)
- self.assertEqual(result.utcoffset(None),
+ self.assertEqual(result.utcoffset(None),
datetime.timedelta(hours=-1, minutes=0))
-
+
class Test_parse_date(unittest.TestCase):
def _callFUT(self, datestring):
from ..iso8601 import parse_date
@@ -92,13 +92,13 @@ def test_cantparse(self):
def test_normal(self):
from ..iso8601 import UTC
result = self._callFUT("2007-01-25T12:00:00Z")
- self.assertEqual(result,
+ self.assertEqual(result,
datetime.datetime(2007, 1, 25, 12, 0, tzinfo=UTC))
-
+
def test_fraction(self):
from ..iso8601 import UTC
result = self._callFUT("2007-01-25T12:00:00.123Z")
- self.assertEqual(result,
+ self.assertEqual(result,
datetime.datetime(2007, 1, 25, 12, 0, 0, 123000,
tzinfo=UTC))
-
+
View
4 docs/.static/repoze.css
@@ -2,7 +2,7 @@
body {
background-color: #006339;
}
-
+
div.document {
background-color: #dad3bd;
}
@@ -15,7 +15,7 @@ div.related {
color: #dad3bd !important;
background-color: #00744a;
}
-
+
div.related a {
color: #dad3bd !important;
}
View
49 docs/basics.rst
@@ -42,12 +42,12 @@ different types.
import colander
class Friend(colander.TupleSchema):
- rank = colander.SchemaNode(colander.Int(),
+ rank = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 9999))
name = colander.SchemaNode(colander.String())
class Phone(colander.MappingSchema):
- location = colander.SchemaNode(colander.String(),
+ location = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['home', 'work']))
number = colander.SchemaNode(colander.String())
@@ -63,7 +63,7 @@ different types.
validator=colander.Range(0, 200))
friends = Friends()
phones = Phones()
-
+
For ease of reading, we've actually defined *five* schemas above, but
we coalesce them all into a single ``Person`` schema. As the result
of our definitions, a ``Person`` represents:
@@ -105,7 +105,7 @@ deserialization but before validation; it prepares a deserialized
value for validation. Examples would be to prepend schemes that may be
missing on url values or to filter html provided by a rich text
editor. A preparer is not called during serialization, only during
-deserialization.
+deserialization. You can also pass a schema node a list of preparers.
The *validator* of a schema node is called after deserialization and
preparation ; it makes sure the value matches a constraint. An example of
@@ -144,7 +144,7 @@ attached to the node unmolested (e.g. when ``foo=1`` is passed, the
resulting schema node will have an attribute named ``foo`` with the
value ``1``).
-.. note::
+.. note::
You may see some higher-level systems (such as Deform) pass a ``widget``
argument to a SchemaNode constructor. Such systems make use of the fact
@@ -167,7 +167,7 @@ its class attribute name. For example:
import colander
class Phone(colander.MappingSchema):
- location = colander.SchemaNode(colander.String(),
+ location = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['home', 'work']))
number = colander.SchemaNode(colander.String())
@@ -181,7 +181,7 @@ Schema Objects
In the examples above, if you've been paying attention, you'll have
noticed that we're defining classes which subclass from
:class:`colander.MappingSchema`, :class:`colander.TupleSchema` and
-:class:`colander.SequenceSchema`.
+:class:`colander.SequenceSchema`.
It's turtles all the way down: the result of creating an instance of
any of :class:`colander.MappingSchema`, :class:`colander.TupleSchema`
@@ -208,12 +208,12 @@ Earlier we defined a schema:
import colander
class Friend(colander.TupleSchema):
- rank = colander.SchemaNode(colander.Int(),
+ rank = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 9999))
name = colander.SchemaNode(colander.String())
class Phone(colander.MappingSchema):
- location = colander.SchemaNode(colander.String(),
+ location = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['home', 'work']))
number = colander.SchemaNode(colander.String())
@@ -370,13 +370,13 @@ error reporting in a different way. In particular, such a system may
need to present the errors next to a field in a form. It may need to
translate error messages to another language. To do these things
effectively, it will almost certainly need to walk and introspect the
-exception graph manually.
+exception graph manually.
The :exc:`colander.Invalid` exceptions raised by Colander validation
are very rich. They contain detailed information about the
circumstances of an error. If you write a system based on Colander
that needs to display and format Colander exceptions specially, you
-will need to get comfy with the Invalid exception API.
+will need to get comfy with the Invalid exception API.
When a validation-related error occurs during deserialization, each
node in the schema that had an error (and any of its parents) will be
@@ -396,7 +396,7 @@ attribute with the value ``None``. Each exception instance will also
have an attribute named ``node``, representing the schema node to
which the exception is related.
-.. note::
+.. note::
Translation strings are objects which behave like Unicode objects but have
extra metadata associated with them for use in translation systems. See
@@ -421,7 +421,7 @@ value before validating it.
For example, a :class:`~colander.String` node may be required to
contain content, but that content may come from a rich text
editor. Such an editor may return ``<b></b>`` which may appear to be
-valid but doesn't contain content, or
+valid but doesn't contain content, or
``<a href="javascript:alert('evil'')">good</a>`` which is valid, but
only after some processing.
@@ -443,6 +443,23 @@ __ http://pypi.python.org/pypi/htmllaundry/
preparer=htmllaundry.sanitize,
validator=colander.Length(1))
+You can even specify multiple preparers to be run in order, by passing
+a list of functions to the `preparer` kwarg, like so:
+
+.. code-block:: python
+ :linenos:
+
+ import colander
+ # removes whitespace, newlines, and tabs from the beginning/end of a string
+ strip_whitespace = lambda v: v.strip(' \t\n\r') if v is not None else v
+ # replaces multiple spaces with a single space
+ remove_multiple_spaces = lambda v: re.sub(' +', ' ', v)
+
+ class Page(colander.MappingSchema):
+ title = colander.SchemaNode(colander.String())
+ content = colander.SchemaNode(colander.String(),
+ preparer=[strip_whitespace, remove_multiple_spaces],
+ validator=colander.Length(1))
Serialization
-------------
@@ -653,12 +670,12 @@ schema configuration. Here's our previous declarative schema:
import colander
class Friend(colander.TupleSchema):
- rank = colander.SchemaNode(colander.Int(),
+ rank = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 9999))
name = colander.SchemaNode(colander.String())
class Phone(colander.MappingSchema):
- location = colander.SchemaNode(colander.String(),
+ location = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['home', 'work']))
number = colander.SchemaNode(colander.String())
@@ -696,7 +713,7 @@ We can imperatively construct a completely equivalent schema like so:
schema = colander.SchemaNode(Mapping())
schema.add(colander.SchemaNode(colander.String(), name='name'))
- schema.add(colander.SchemaNode(colander.Int(), name='age'),
+ schema.add(colander.SchemaNode(colander.Int(), name='age'),
validator=colander.Range(0, 200))
schema.add(colander.SchemaNode(colander.Sequence(), friend, name='friends'))
schema.add(colander.SchemaNode(colander.Sequence(), phone, name='phones'))
View
6 docs/binding.rst
@@ -1,7 +1,7 @@
Schema Binding
==============
-.. note::
+.. note::
Schema binding is new in colander 0.8.
@@ -150,7 +150,7 @@ Let's take a look at an example:
validator = deferred_category_validator,
widget = deferred_category_widget,
)
-
+
schema = BlogPostSchema().bind(
max_date = datetime.date.max,
max_bodylen = 5000,
@@ -164,7 +164,7 @@ decorator to a function that takes two arguments. For a schema node
value to be considered deferred, it must be an instance of
``colander.deferred`` and using that class as a decorator is the
easiest way to ensure that this happens.
-
+
To perform binding, the ``bind`` method of a schema node must be
called. ``bind`` returns a *clone* of the schema node (and its
children, recursively), with all ``colander.deferred`` values
View
6 docs/extending.rst
@@ -111,7 +111,7 @@ within its ``deserialize`` method.
Type Constructors
~~~~~~~~~~~~~~~~~
-
+
A type class does not need to implement a constructor (``__init__``),
but it isn't prevented from doing so if it needs to accept arguments;
Colander itself doesn't construct any types, only users of Colander
@@ -157,9 +157,9 @@ card number.
sum = sum + digit
if not (sum % 10) == 0:
- raise Invalid(node,
+ raise Invalid(node,
'%r is not a valid credit card number' % value)
-
+
Here's how the resulting ``luhnok`` validator might be used in a
schema:
View
14 docs/manipulation.rst
@@ -4,9 +4,9 @@ Manipulating Data Structures
============================
Colander schemas have some utility functions which can be used to manipulate
-an :term:`appstruct` or a :term:`cstruct`. Nested data structures can be
-flattened into a single dictionary or a single flattened dictionary can be used
-to produce a nested data structure. Values of particular nodes can also be set
+an :term:`appstruct` or a :term:`cstruct`. Nested data structures can be
+flattened into a single dictionary or a single flattened dictionary can be used
+to produce a nested data structure. Values of particular nodes can also be set
or retrieved based on a flattened path spec.
Flattening a Data Structure
@@ -24,12 +24,12 @@ Consider the following schema:
import colander
class Friend(colander.TupleSchema):
- rank = colander.SchemaNode(colander.Int(),
+ rank = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 9999))
name = colander.SchemaNode(colander.String())
class Phone(colander.MappingSchema):
- location = colander.SchemaNode(colander.String(),
+ location = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['home', 'work']))
number = colander.SchemaNode(colander.String())
@@ -63,7 +63,7 @@ This data can be flattened:
.. code-block:: python
:linenos:
-
+
schema = Person()
fstruct = schema.flatten(appstruct)
@@ -103,7 +103,7 @@ Accessing and Mutating Nodes in a Data Structure
------------------------------------------------
:attr:`colander.SchemaNode.get_value` and :attr:`colander.SchemaNode.set_value`
-can be used to access and mutate nodes in an :term:`appstruct` or
+can be used to access and mutate nodes in an :term:`appstruct` or
:term:`cstruct`. Using the example from above:
.. code-block:: python
View
8 docs/null.rst
@@ -70,7 +70,7 @@ for serialization which had the :attr:`colander.null` sentinel as the
import colander
schema = Person()
- serialized = schema.serialize({'name':'Fred', 'age':20,
+ serialized = schema.serialize({'name':'Fred', 'age':20,
'hair_color':colander.null})
When the above is run, the value of ``serialized`` will be
@@ -188,7 +188,7 @@ colander.null <missing> null serialized
colander.null value null serialized
===================== ===================== ===========================
-.. note::
+.. note::
``<missing>`` in the above table represents the circumstance in which a
key present in a :class:`colander.MappingSchema` is not present in a
@@ -230,7 +230,7 @@ a schema, the node will take the following steps:
with an explicit ``missing`` value), a :exc:`colander.Invalid` exception
will be raised with a message indicating that the field is required.
-.. note::
+.. note::
There are differences between serialization and deserialization involving
the :attr:`colander.null` value. During serialization, if an
@@ -310,7 +310,7 @@ value colander.null value used
value_a value_b value_a used
===================== ===================== ===========================
-.. note::
+.. note::
``<missing>`` in the above table represents the circumstance in which a
key present in a :class:`colander.MappingSchema` is not present in a
View
8 tox.ini
@@ -1,17 +1,17 @@
[tox]
-envlist =
+envlist =
py26,py27,py32,pypy,cover
[testenv]
-commands =
+commands =
python setup.py test -q
[testenv:cover]
basepython =
python2.6
-commands =
+commands =
python setup.py nosetests --with-xunit --with-xcoverage
-deps =
+deps =
nose
coverage==3.4
nosexcover
Something went wrong with that request. Please try again.