Merge branch 'master' of https://github.com/Pylons/colander into length_msg
Conflicts: CONTRIBUTORS.txt
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Wire up travis
|
# Wire up travis
|
||||||
language: python
|
language: python
|
||||||
|
sudo: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- TOXENV=py26
|
- TOXENV=py26
|
||||||
|
11
CHANGES.rst
11
CHANGES.rst
@@ -54,6 +54,15 @@ Bug Fixes
|
|||||||
- Updated translations: ``fr``, ``de``, ``ja``
|
- Updated translations: ``fr``, ``de``, ``ja``
|
||||||
|
|
||||||
|
|
||||||
|
Backwards Incompatibilities
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- ``SchemaNode.deserialize`` will now raise an
|
||||||
|
``UnboundDeferredError`` if the node has an unbound deferred
|
||||||
|
validator. Previously, deferred validators were silently ignored.
|
||||||
|
See https://github.com/Pylons/colander/issues/47
|
||||||
|
|
||||||
|
|
||||||
1.0b1 (2013-09-01)
|
1.0b1 (2013-09-01)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
@@ -109,6 +118,8 @@ Features
|
|||||||
- The ``typ`` of a ``SchemaNode`` can optionally be pased in as a keyword
|
- The ``typ`` of a ``SchemaNode`` can optionally be pased in as a keyword
|
||||||
argument. See https://github.com/Pylons/colander/issues/90
|
argument. See https://github.com/Pylons/colander/issues/90
|
||||||
|
|
||||||
|
- Allow interpolation of `missing_msg` with properties `title` and `name`
|
||||||
|
|
||||||
1.0a5 (2013-05-31)
|
1.0a5 (2013-05-31)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
@@ -117,7 +117,10 @@ Contributors
|
|||||||
- Veeti Paananen, 2013/08/20
|
- Veeti Paananen, 2013/08/20
|
||||||
- Michael Howitz, 2013/12/05
|
- Michael Howitz, 2013/12/05
|
||||||
- Alex Marandon, 2013/12/21
|
- Alex Marandon, 2013/12/21
|
||||||
|
- Jaseem Abid, 2014/06/16
|
||||||
- Cédric Messiant, 2014/06/27
|
- Cédric Messiant, 2014/06/27
|
||||||
- Gouji Ochiai, 2014/08/21
|
- Gouji Ochiai, 2014/08/21
|
||||||
- Tim Tisdall, 2014/09/10
|
- Tim Tisdall, 2014/09/10
|
||||||
- Nando Florestan, 2014-11-27
|
- Nando Florestan, 2014-11-27
|
||||||
|
- Amos Latteier, 2014/11/30
|
||||||
|
- Jimmy Thrasibule, 2014/12/11
|
||||||
|
@@ -57,6 +57,13 @@ def interpolate(msgs):
|
|||||||
else:
|
else:
|
||||||
yield s
|
yield s
|
||||||
|
|
||||||
|
class UnboundDeferredError(Exception):
|
||||||
|
"""
|
||||||
|
An exception raised by :meth:`SchemaNode.deserialize` when an attempt
|
||||||
|
is made to deserialize a node which has an unbound :class:`deferred`
|
||||||
|
validator.
|
||||||
|
"""
|
||||||
|
|
||||||
class Invalid(Exception):
|
class Invalid(Exception):
|
||||||
"""
|
"""
|
||||||
An exception raised by data types and validators indicating that
|
An exception raised by data types and validators indicating that
|
||||||
@@ -286,6 +293,9 @@ class Regex(object):
|
|||||||
error message to be used; otherwise, defaults to 'String does
|
error message to be used; otherwise, defaults to 'String does
|
||||||
not match expected pattern'.
|
not match expected pattern'.
|
||||||
|
|
||||||
|
The ``regex`` expression behaviour can be modified by specifying
|
||||||
|
any ``flags`` value taken by ``re.compile``.
|
||||||
|
|
||||||
The ``regex`` argument may also be a pattern object (the
|
The ``regex`` argument may also be a pattern object (the
|
||||||
result of ``re.compile``) instead of a string.
|
result of ``re.compile``) instead of a string.
|
||||||
|
|
||||||
@@ -293,9 +303,9 @@ class Regex(object):
|
|||||||
validation succeeds; otherwise, :exc:`colander.Invalid` is
|
validation succeeds; otherwise, :exc:`colander.Invalid` is
|
||||||
raised with the ``msg`` error message.
|
raised with the ``msg`` error message.
|
||||||
"""
|
"""
|
||||||
def __init__(self, regex, msg=None):
|
def __init__(self, regex, msg=None, flags=0):
|
||||||
if isinstance(regex, string_types):
|
if isinstance(regex, string_types):
|
||||||
self.match_object = re.compile(regex)
|
self.match_object = re.compile(regex, flags)
|
||||||
else:
|
else:
|
||||||
self.match_object = regex
|
self.match_object = regex
|
||||||
if msg is None:
|
if msg is None:
|
||||||
@@ -483,6 +493,11 @@ URL_REGEX = r"""(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9
|
|||||||
|
|
||||||
url = Regex(URL_REGEX, _('Must be a URL'))
|
url = Regex(URL_REGEX, _('Must be a URL'))
|
||||||
|
|
||||||
|
|
||||||
|
UUID_REGEX = r"""^(?:urn:uuid:)?\{?[a-f0-9]{8}(?:-?[a-f0-9]{4}){3}-?[a-f0-9]{12}\}?$"""
|
||||||
|
uuid = Regex(UUID_REGEX, _('Invalid UUID string'), re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
class SchemaType(object):
|
class SchemaType(object):
|
||||||
""" Base class for all schema types """
|
""" Base class for all schema types """
|
||||||
def flatten(self, node, appstruct, prefix='', listitem=False):
|
def flatten(self, node, appstruct, prefix='', listitem=False):
|
||||||
@@ -1837,7 +1852,7 @@ class _SchemaNode(object):
|
|||||||
validator = None
|
validator = None
|
||||||
default = null
|
default = null
|
||||||
missing = required
|
missing = required
|
||||||
missing_msg = _('Required')
|
missing_msg = 'Required'
|
||||||
name = ''
|
name = ''
|
||||||
raw_title = _marker
|
raw_title = _marker
|
||||||
title = _marker
|
title = _marker
|
||||||
@@ -1980,14 +1995,20 @@ class _SchemaNode(object):
|
|||||||
if appstruct is null:
|
if appstruct is null:
|
||||||
appstruct = self.missing
|
appstruct = self.missing
|
||||||
if appstruct is required:
|
if appstruct is required:
|
||||||
raise Invalid(self, self.missing_msg)
|
raise Invalid(self, _(self.missing_msg,
|
||||||
|
mapping={'title': self.title,
|
||||||
|
'name':self.name}))
|
||||||
|
|
||||||
if isinstance(appstruct, deferred): # unbound schema with deferreds
|
if isinstance(appstruct, deferred): # unbound schema with deferreds
|
||||||
raise Invalid(self, self.missing_msg)
|
raise Invalid(self, self.missing_msg)
|
||||||
# We never deserialize or validate the missing value
|
# We never deserialize or validate the missing value
|
||||||
return appstruct
|
return appstruct
|
||||||
|
|
||||||
if self.validator is not None:
|
if self.validator is not None:
|
||||||
if not isinstance(self.validator, deferred): # unbound
|
if isinstance(self.validator, deferred): # unbound
|
||||||
|
raise UnboundDeferredError(
|
||||||
|
"Schema node {node} has an unbound deferred validator"
|
||||||
|
.format(node=self))
|
||||||
self.validator(self, appstruct)
|
self.validator(self, appstruct)
|
||||||
return appstruct
|
return appstruct
|
||||||
|
|
||||||
|
@@ -550,6 +550,57 @@ class Test_url_validator(unittest.TestCase):
|
|||||||
from colander import Invalid
|
from colander import Invalid
|
||||||
self.assertRaises(Invalid, self._callFUT, val)
|
self.assertRaises(Invalid, self._callFUT, val)
|
||||||
|
|
||||||
|
class TestUUID(unittest.TestCase):
|
||||||
|
def _callFUT(self, val):
|
||||||
|
from colander import uuid
|
||||||
|
return uuid(None, val)
|
||||||
|
|
||||||
|
def test_success_hexadecimal(self):
|
||||||
|
val = '123e4567e89b12d3a456426655440000'
|
||||||
|
result = self._callFUT(val)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_success_with_dashes(self):
|
||||||
|
val = '123e4567-e89b-12d3-a456-426655440000'
|
||||||
|
result = self._callFUT(val)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_success_upper_case(self):
|
||||||
|
val = '123E4567-E89B-12D3-A456-426655440000'
|
||||||
|
result = self._callFUT(val)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_success_with_braces(self):
|
||||||
|
val = '{123e4567-e89b-12d3-a456-426655440000}'
|
||||||
|
result = self._callFUT(val)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_success_with_urn_ns(self):
|
||||||
|
val = 'urn:uuid:{123e4567-e89b-12d3-a456-426655440000}'
|
||||||
|
result = self._callFUT(val)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_failure_random_string(self):
|
||||||
|
val = 'not-a-uuid'
|
||||||
|
from colander import Invalid
|
||||||
|
self.assertRaises(Invalid, self._callFUT, val)
|
||||||
|
|
||||||
|
def test_failure_not_hexadecimal(self):
|
||||||
|
val = '123zzzzz-uuuu-zzzz-uuuu-42665544zzzz'
|
||||||
|
from colander import Invalid
|
||||||
|
self.assertRaises(Invalid, self._callFUT, val)
|
||||||
|
|
||||||
|
def test_failure_invalid_length(self):
|
||||||
|
# Correct UUID: 8-4-4-4-12
|
||||||
|
val = '88888888-333-4444-333-cccccccccccc'
|
||||||
|
from colander import Invalid
|
||||||
|
self.assertRaises(Invalid, self._callFUT, val)
|
||||||
|
|
||||||
|
def test_failure_with_invalid_urn_ns(self):
|
||||||
|
val = 'urn:abcd:{123e4567-e89b-12d3-a456-426655440000}'
|
||||||
|
from colander import Invalid
|
||||||
|
self.assertRaises(Invalid, self._callFUT, val)
|
||||||
|
|
||||||
class TestSchemaType(unittest.TestCase):
|
class TestSchemaType(unittest.TestCase):
|
||||||
def _makeOne(self, *arg, **kw):
|
def _makeOne(self, *arg, **kw):
|
||||||
from colander import SchemaType
|
from colander import SchemaType
|
||||||
@@ -1511,7 +1562,7 @@ class TestInteger(unittest.TestCase):
|
|||||||
result = typ.serialize(node, val)
|
result = typ.serialize(node, val)
|
||||||
self.assertEqual(result, colander.null)
|
self.assertEqual(result, colander.null)
|
||||||
|
|
||||||
def test_serialize_emptystring(self):
|
def test_deserialize_emptystring(self):
|
||||||
import colander
|
import colander
|
||||||
val = ''
|
val = ''
|
||||||
node = DummySchemaNode(None)
|
node = DummySchemaNode(None)
|
||||||
@@ -2474,6 +2525,19 @@ class TestSchemaNode(unittest.TestCase):
|
|||||||
e = invalid_exc(node.deserialize, 1)
|
e = invalid_exc(node.deserialize, 1)
|
||||||
self.assertEqual(e.msg, 'Wrong')
|
self.assertEqual(e.msg, 'Wrong')
|
||||||
|
|
||||||
|
def test_deserialize_with_unbound_validator(self):
|
||||||
|
from colander import Invalid
|
||||||
|
from colander import deferred
|
||||||
|
from colander import UnboundDeferredError
|
||||||
|
typ = DummyType()
|
||||||
|
def validator(node, kw):
|
||||||
|
def _validate(node, value):
|
||||||
|
node.raise_invalid('Invalid')
|
||||||
|
return _validate
|
||||||
|
node = self._makeOne(typ, validator=deferred(validator))
|
||||||
|
self.assertRaises(UnboundDeferredError, node.deserialize, None)
|
||||||
|
self.assertRaises(Invalid, node.bind(foo='foo').deserialize, None)
|
||||||
|
|
||||||
def test_deserialize_value_is_null_no_missing(self):
|
def test_deserialize_value_is_null_no_missing(self):
|
||||||
from colander import null
|
from colander import null
|
||||||
from colander import Invalid
|
from colander import Invalid
|
||||||
@@ -2495,6 +2559,14 @@ class TestSchemaNode(unittest.TestCase):
|
|||||||
e = invalid_exc(node.deserialize, null)
|
e = invalid_exc(node.deserialize, null)
|
||||||
self.assertEqual(e.msg, 'Missing')
|
self.assertEqual(e.msg, 'Missing')
|
||||||
|
|
||||||
|
def test_deserialize_value_with_interpolated_missing_msg(self):
|
||||||
|
from colander import null
|
||||||
|
typ = DummyType()
|
||||||
|
node = self._makeOne(typ, missing_msg='Missing attribute ${title}',
|
||||||
|
name='name_a')
|
||||||
|
e = invalid_exc(node.deserialize, null)
|
||||||
|
self.assertEqual(e.msg.interpolate(), 'Missing attribute Name A')
|
||||||
|
|
||||||
def test_deserialize_noargs_uses_default(self):
|
def test_deserialize_noargs_uses_default(self):
|
||||||
typ = DummyType()
|
typ = DummyType()
|
||||||
node = self._makeOne(typ)
|
node = self._makeOne(typ)
|
||||||
|
@@ -48,6 +48,9 @@ Exceptions
|
|||||||
from a widget as the value which should be redisplayed when an
|
from a widget as the value which should be redisplayed when an
|
||||||
error is shown.
|
error is shown.
|
||||||
|
|
||||||
|
.. autoclass:: UnboundDeferredError
|
||||||
|
|
||||||
|
|
||||||
Validators
|
Validators
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
@@ -75,6 +78,11 @@ Validators
|
|||||||
|
|
||||||
A validator which ensures the value is a URL (via regex).
|
A validator which ensures the value is a URL (via regex).
|
||||||
|
|
||||||
|
.. attribute:: uuid
|
||||||
|
|
||||||
|
A UUID hexadecimal string validator via regular expression
|
||||||
|
using :class:`colander.Regex`.
|
||||||
|
|
||||||
Types
|
Types
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
|
@@ -248,7 +248,8 @@ If you use a schema with deferred ``validator``, ``missing`` or
|
|||||||
``default`` attributes, but you use it to perform serialization and
|
``default`` attributes, but you use it to perform serialization and
|
||||||
deserialization without calling its ``bind`` method:
|
deserialization without calling its ``bind`` method:
|
||||||
|
|
||||||
- If ``validator`` is deferred, no validation will be performed.
|
- If ``validator`` is deferred, :meth:`~colander.SchemaNode.deserialize` will
|
||||||
|
raise an :exc:`~colander.UnboundDeferredError`.
|
||||||
|
|
||||||
- If ``missing`` is deferred, the field will be considered *required*.
|
- If ``missing`` is deferred, the field will be considered *required*.
|
||||||
|
|
||||||
|
@@ -1 +1 @@
|
|||||||
.. include:: ../CHANGES.txt
|
.. include:: ../CHANGES.rst
|
||||||
|
2
setup.py
2
setup.py
@@ -36,7 +36,7 @@ testing_extras = ['nose', 'coverage']
|
|||||||
docs_extras = ['Sphinx']
|
docs_extras = ['Sphinx']
|
||||||
|
|
||||||
setup(name='colander',
|
setup(name='colander',
|
||||||
version='1.1dev',
|
version='1.1.dev0',
|
||||||
description=('A simple schema-based serialization and deserialization '
|
description=('A simple schema-based serialization and deserialization '
|
||||||
'library'),
|
'library'),
|
||||||
long_description=README + '\n\n' + CHANGES,
|
long_description=README + '\n\n' + CHANGES,
|
||||||
|
Reference in New Issue
Block a user