diff --git a/CHANGES.txt b/CHANGES.txt index 8513b07..6f448cb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,9 @@ Features - Add ``colander.ContainsOnly`` and ``colander.url`` validators. +- Add ``colander.instantiate`` to help define schemas containing + mappings and sequences more succinctly. + 1.0a1 (2013-01-10) ------------------ diff --git a/colander/__init__.py b/colander/__init__.py index c433d3e..73e300d 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -2089,3 +2089,18 @@ def _unflatten_mapping(node, paths, fstruct, appstruct[curname] = subnode.typ.unflatten( subnode, subpaths, subfstruct) return appstruct + +class instantiate(object): + """ + A decorator which can be used to instantiate :class:`SchemaNode` + elements inline within a class definition. + + All parameters passed to the decorator and passed along to the + :class:`SchemaNode` during instantiation. + """ + + def __init__(self,*args,**kw): + self.args,self.kw = args,kw + + def __call__(self,class_): + return class_(*self.args,**self.kw) diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 1f22f64..7a02564 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -3337,6 +3337,42 @@ class TestUltraDeclarative(unittest.TestCase, TestFunctional): schema = MainSchema() return schema +class TestDeclarativeWithInstantiate(unittest.TestCase, TestFunctional): + + def _makeSchema(self, name='schema'): + + import colander + + # an unlikely usage, but goos to test passing + # parameters to instantiation works + @colander.instantiate(name=name) + class schema(colander.MappingSchema): + int = colander.SchemaNode(colander.Int(), + validator=colander.Range(0, 10)) + ob = colander.SchemaNode(colander.GlobalObject(package=colander)) + @colander.instantiate() + class seq(colander.SequenceSchema): + + @colander.instantiate() + class tup(colander.TupleSchema): + tupint = colander.SchemaNode(colander.Int()) + tupstring = colander.SchemaNode(colander.String()) + + @colander.instantiate() + class tup(colander.TupleSchema): + tupint = colander.SchemaNode(colander.Int()) + tupstring = colander.SchemaNode(colander.String()) + + @colander.instantiate() + class seq2(colander.SequenceSchema): + + @colander.instantiate() + class mapping(colander.MappingSchema): + key = colander.SchemaNode(colander.Int()) + key2 = colander.SchemaNode(colander.Int()) + + return schema + class Test_null(unittest.TestCase): def test___nonzero__(self): from colander import null diff --git a/docs/api.rst b/docs/api.rst index 323d5e6..7be6ac7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -132,6 +132,8 @@ Schema-Related .. autoclass:: deferred + .. autoclass:: instantiate + .. attribute:: null Represents a null value in colander-related operations. diff --git a/docs/basics.rst b/docs/basics.rst index 4bdd9e3..eb7b13d 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -982,6 +982,63 @@ indeed be present in the child list of the ``schema`` instance ``title`` attribute will be ``Some Schema`` (``schema.title`` will return ``Some Schema``). +Defining A Schema Declaratively +------------------------------- + +Previously, we defined the schema in such a way that the individual +sequences and mappings within the schema could be re-used in different +schemas. If all nodes within a schema are only likely to be used in that +schema, then the schema definition can be made more succinct using the +:class:`~colander.instantiate` class decorator as shown below: + +.. code-block:: python + :linenos: + + import colander + + class Person(colander.MappingSchema): + name = colander.SchemaNode(colander.String()) + age = colander.SchemaNode(colander.Int(), + validator=colander.Range(0, 200)) + + @colander.instantiate() + class friends(colander.SequenceSchema): + + @colander.instantiate() + class friend(colander.TupleSchema): + rank = colander.SchemaNode(colander.Int(), + validator=colander.Range(0, 9999)) + name = colander.SchemaNode(colander.String()) + + @colander.instantiate() + class phones(colander.SequenceSchema): + + @colander.instantiate() + class phone(colander.MappingSchema): + location = colander.SchemaNode(colander.String(), + validator=colander.OneOf(['home', 'work'])) + number = colander.SchemaNode(colander.String()) + +If you need to pass parameters when using this style of schema +definition, such as a ``missing`` value to a :class:`SchemaNode` +during instantiation, you can pass these as parameters to +:class:`~colander.instantiate`. +For example, if we wanted to limit the number of friends a person can +have, and cater for people who have no friends, we could adjust the +schema as shown below: + +.. code-block:: python + :linenos: + + class Person(colander.MappingSchema): + + @colander.instantiate(missing=(), + validator=colander.Length(max=5)) + class friends(colander.SequenceSchema): + + @colander.instantiate() + class friend(colander.TupleSchema): + name = colander.SchemaNode(colander.String()) Defining A Schema Imperatively ------------------------------