From 876da464de96dcf1ac4b8db34a1e2b47b4b7c176 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 24 May 2011 17:32:58 +0100 Subject: [PATCH 1/5] documentation --- colander/interfaces.py | 15 ++++++++++++- docs/basics.rst | 51 +++++++++++++++++++++++++++++++++++++----- docs/interfaces.rst | 2 ++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/colander/interfaces.py b/colander/interfaces.py index 82ee077..1048cbe 100644 --- a/colander/interfaces.py +++ b/colander/interfaces.py @@ -1,6 +1,19 @@ +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. + + If no modification is required, then ``value`` should be returned + as-is. + """ + + def Validator(node, value): """ - A validator is called after deserialization of a 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. diff --git a/docs/basics.rst b/docs/basics.rst index ad15ba0..3a0d6bc 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -89,15 +89,24 @@ Schema Node Objects A schema is composed of one or more *schema node* objects, each typically of the class :class:`colander.SchemaNode`, usually in a nested arrangement. -Each schema node object has a required *type*, an optional deserialization -*validator*, an optional *default*, an optional *missing*, an optional -*title*, an optional *description*, and a slightly less optional *name*. It -also accepts *arbitrary* keyword arguments, which are attached directly as -attributes to the node instance. +Each schema node object has a required *type*, an optional *preparer* +for adjusting data after deserialization, an optional +*validator* for deserialized prepared data, an optional *default*, an +optional *missing*, an optional *title*, an optional *description*, +and a slightly less optional *name*. It also accepts *arbitrary* +keyword arguments, which are attached directly as attributes to the +node instance. The *type* of a schema node indicates its data type (such as :class:`colander.Int` or :class:`colander.String`). +The *preparer* of a schema node is called after +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. + The *validator* of a schema node is called after deserialization; it makes sure the deserialized value matches a constraint. An example of such a validator is provided in the schema above: @@ -392,6 +401,38 @@ which the exception is related. See the :class:`colander.Invalid` API documentation for more information. +Preparing deserialized data for validation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In certain circumstances, it is necessary to modify the deserialized +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 ```` which may appear to be +valid but doesn't contain content, or +``good`` which is valid, but +only after some processing. + +The following schema uses `htmllaundry`__ and a +:class:`~colander.interfaces.Preparer` to do the correct thing in both +cases: + +__ http://pypi.python.org/pypi/htmllaundry/ + +.. code-block:: python + :linenos: + + import colander + import htmlaundry + + class Page(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + content = colander.SchemaNode(colander.String(), + preparer=htmllaundry.sanitize, + validator=colander.Length(1)) + + Serialization ------------- diff --git a/docs/interfaces.rst b/docs/interfaces.rst index 633070e..563eae9 100644 --- a/docs/interfaces.rst +++ b/docs/interfaces.rst @@ -3,6 +3,8 @@ Interfaces .. automodule:: colander.interfaces + .. autofunction:: Preparer + .. autofunction:: Validator .. autoclass:: Type From cb92da9dc694c61a99672597443939f57964005e Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 24 May 2011 18:01:40 +0100 Subject: [PATCH 2/5] code --- colander/__init__.py | 19 ++++++++++++++----- colander/tests.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/colander/__init__.py b/colander/__init__.py index a10bac6..c579c89 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1231,6 +1231,10 @@ class SchemaNode(object): 'required'. When ``missing`` is :attr:`colander.required`, the ``required`` computed attribute will be ``True``. + - ``preparer``: Optional preparer for this node. It should be + an object that implements the + :class:`colander.interfaces.Preparer` interface. + - ``validator``: Optional validator for this node. It should be an object that implements the :class:`colander.interfaces.Validator` interface. @@ -1275,6 +1279,7 @@ class SchemaNode(object): def __init__(self, typ, *children, **kw): self.typ = typ + self.preparer = kw.pop('preparer', None) self.validator = kw.pop('validator', None) self.default = kw.pop('default', null) self.missing = kw.pop('missing', required) @@ -1338,13 +1343,14 @@ class SchemaNode(object): def deserialize(self, cstruct=null): """ Deserialize the :term:`cstruct` into an :term:`appstruct` based - on the schema, and return the deserialized, then validate the - resulting appstruct. The ``cstruct`` value is deserialized into an + on the schema, run this :term:`appstruct` through the + preparer, if one is present, then validate the + prepared appstruct. The ``cstruct`` value is deserialized into an ``appstruct`` unconditionally. - If ``appstruct`` returned by type deserialization is the value - :attr:`colander.null`, do something special before attempting - validation: + If ``appstruct`` returned by type deserialization and + preparation is the value :attr:`colander.null`, do something + special before attempting validation: - If the ``missing`` attribute of this node has been set explicitly, return its value. No validation of this value is performed; it is @@ -1361,6 +1367,9 @@ class SchemaNode(object): """ appstruct = self.typ.deserialize(self, cstruct) + if self.preparer is not None: + appstruct = self.preparer(appstruct) + if appstruct is null: appstruct = self.missing if appstruct is required: diff --git a/colander/tests.py b/colander/tests.py index f747190..f3c457e 100644 --- a/colander/tests.py +++ b/colander/tests.py @@ -1463,6 +1463,14 @@ class TestSchemaNode(unittest.TestCase): node = self._makeOne(None, 0, widget='abc') self.assertEqual(node.widget, 'abc') + def test_ctor_with_preparer(self): + node = self._makeOne(None, 0, preparer='abc') + self.assertEqual(node.preparer, 'abc') + + def test_ctor_without_preparer(self): + node = self._makeOne(None, 0) + self.assertEqual(node.preparer, None) + def test_ctor_with_unknown_kwarg(self): node = self._makeOne(None, 0, foo=1) self.assertEqual(node.foo, 1) @@ -1486,6 +1494,29 @@ class TestSchemaNode(unittest.TestCase): result = node.deserialize(1) self.assertEqual(result, 1) + def test_deserialize_with_preparer(self): + from colander import Invalid + typ = DummyType() + def preparer(value): + return 'prepared_'+value + def validator(node, value): + if not value.startswith('prepared'): + raise Invalid(node, 'not prepared') + node = self._makeOne(typ, + preparer=preparer, + validator=validator) + self.assertEqual(node.deserialize('value'), + 'prepared_value') + + def test_deserialize_preparer_before_missing_check(self): + from colander import null + typ = DummyType() + def preparer(value): + return null + node = self._makeOne(typ,preparer=preparer) + e = invalid_exc(node.deserialize, 1) + self.assertEqual(e.msg, 'Required') + def test_deserialize_with_validator(self): typ = DummyType() validator = DummyValidator(msg='Wrong') From e35d13efadbb9dfe320791121ca93aba2afad099 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 24 May 2011 18:02:06 +0100 Subject: [PATCH 3/5] sign contributor agreement --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 97eb658..d1b530f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -102,4 +102,4 @@ Contributors ------------ - Chris McDonough, 2011/02/16 - +- Chris Withers, 2011/05/45 From e96597db44901f8240980be4c3bbe95cdc06f8a0 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 24 May 2011 18:08:53 +0100 Subject: [PATCH 4/5] add changelog entry --- CHANGES.txt | 3 +++ docs/basics.rst | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index f1e59a2..b0ae133 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,6 +14,9 @@ Next release ``colander.DateTime``. See https://github.com/Pylons/colander/issues#issue/6. +- Add the ability to insert a :class:`~colander.interfaces.Preparer` + between deserialization and validation. See the :ref:`preparing`. + 0.9.2 (2011-03-28) ------------------ diff --git a/docs/basics.rst b/docs/basics.rst index 3a0d6bc..0d2fc01 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -401,6 +401,8 @@ which the exception is related. See the :class:`colander.Invalid` API documentation for more information. +.. _preparing: + Preparing deserialized data for validation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 7c206f1bf9101a6a72a9f714a631d887e1800274 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 24 May 2011 18:17:40 +0100 Subject: [PATCH 5/5] doc tweak to make it clear validation happens on the prepared value --- docs/basics.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basics.rst b/docs/basics.rst index 0d2fc01..54d04cd 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -107,11 +107,11 @@ missing on url values or to filter html provided by a rich text editor. A preparer is not called during serialization, only during deserialization. -The *validator* of a schema node is called after deserialization; it -makes sure the deserialized value matches a constraint. An example of +The *validator* of a schema node is called after deserialization and +preparation ; it makes sure the value matches a constraint. An example of such a validator is provided in the schema above: ``validator=colander.Range(0, 200)``. A validator is not called after -serialization, only after deserialization. +schema node serialization, only after node deserialization. The *default* of a schema node indicates the value to be serialized if a value for the schema node is not found in the input data during