From ccb87e5cd6b36e6463d78c9e9027c3b2d5dadcc7 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Mon, 14 Apr 2014 15:04:41 -0400 Subject: [PATCH 01/16] Make deserialize with an unbound validator raise an exception. This should help avoid programming mistakes where you forget to bind a schema. --- colander/__init__.py | 13 ++++++++++++- colander/tests/test_colander.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/colander/__init__.py b/colander/__init__.py index 6f4185b..462f36b 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -57,6 +57,13 @@ def interpolate(msgs): else: 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): """ An exception raised by data types and validators indicating that @@ -1961,7 +1968,11 @@ class _SchemaNode(object): return appstruct 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)) + else: self.validator(self, appstruct) return appstruct diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 487eca1..5ea17d1 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -2462,6 +2462,19 @@ class TestSchemaNode(unittest.TestCase): e = invalid_exc(node.deserialize, 1) 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): from colander import null from colander import Invalid From 8383eb894c101ea89b02aa90ee257199369f0a6e Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Mon, 14 Apr 2014 15:28:49 -0400 Subject: [PATCH 02/16] Add documentation. --- CHANGES.txt | 9 +++++++++ docs/api.rst | 3 +++ docs/binding.rst | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 02b4949..1908d9e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -26,6 +26,15 @@ Features - Allow localization of error messages returned by ``colander.Invalid.asdict`` by adding an optional ``translate`` callable argument. +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) ------------------ diff --git a/docs/api.rst b/docs/api.rst index 8f972bd..51b6d75 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,6 +48,9 @@ Exceptions from a widget as the value which should be redisplayed when an error is shown. + .. autoclass:: UnboundDeferredError + + Validators ~~~~~~~~~~ diff --git a/docs/binding.rst b/docs/binding.rst index 61e5efa..94340c2 100644 --- a/docs/binding.rst +++ b/docs/binding.rst @@ -248,7 +248,8 @@ If you use a schema with deferred ``validator``, ``missing`` or ``default`` attributes, but you use it to perform serialization and 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*. From 98e9bc775af469377942cf19af5186a8f9f31b2d Mon Sep 17 00:00:00 2001 From: Jaseem Abid Date: Mon, 16 Jun 2014 10:34:20 +0530 Subject: [PATCH 03/16] Allow interpolation of `missing_msg` string The missing message can be interpolated with the properties `title` as well as `name` --- colander/__init__.py | 7 +++++-- colander/tests/test_colander.py | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/colander/__init__.py b/colander/__init__.py index 82eb96b..02e64c9 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1812,7 +1812,7 @@ class _SchemaNode(object): validator = None default = null missing = required - missing_msg = _('Required') + missing_msg = 'Required' name = '' raw_title = _marker title = _marker @@ -1955,7 +1955,10 @@ class _SchemaNode(object): if appstruct is null: appstruct = self.missing 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 raise Invalid(self, self.missing_msg) # We never deserialize or validate the missing value diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 4ff4939..567312c 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -2483,6 +2483,14 @@ class TestSchemaNode(unittest.TestCase): e = invalid_exc(node.deserialize, null) self.assertEqual(e.msg, 'Missing') + def test_deserialize_value_is_null_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): typ = DummyType() node = self._makeOne(typ) From eb7626e1e0d3f09231703f044c0afaac78a31ec0 Mon Sep 17 00:00:00 2001 From: Jaseem Abid Date: Thu, 27 Nov 2014 20:10:27 +0530 Subject: [PATCH 04/16] Fix test name --- colander/tests/test_colander.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 567312c..0a45ed9 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -2483,7 +2483,7 @@ class TestSchemaNode(unittest.TestCase): e = invalid_exc(node.deserialize, null) self.assertEqual(e.msg, 'Missing') - def test_deserialize_value_is_null_with_interpolated_missing_msg(self): + def test_deserialize_value_with_interpolated_missing_msg(self): from colander import null typ = DummyType() node = self._makeOne(typ, missing_msg='Missing attribute ${title}', From 2b38a4d39ad6de2b8ea0eb5aa296798c80bb4ca3 Mon Sep 17 00:00:00 2001 From: Jaseem Abid Date: Thu, 27 Nov 2014 20:17:35 +0530 Subject: [PATCH 05/16] Update changes and contributors files --- CHANGES.rst | 2 ++ CONTRIBUTORS.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 05afe56..ec55409 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -105,6 +105,8 @@ Features - The ``typ`` of a ``SchemaNode`` can optionally be pased in as a keyword argument. See https://github.com/Pylons/colander/issues/90 +- Allow interpolation of `missing_msg` with properties `title` and `name` + 1.0a5 (2013-05-31) ------------------ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 466fc7b..7a31d8d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -117,6 +117,7 @@ Contributors - Veeti Paananen, 2013/08/20 - Michael Howitz, 2013/12/05 - Alex Marandon, 2013/12/21 +- Jaseem Abid, 2014/06/16 - Cédric Messiant, 2014/06/27 - Gouji Ochiai, 2014/08/21 - Tim Tisdall, 2014/09/10 From 186fde756806594ca7426ad73bd5a3e31386d5cf Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Sun, 30 Nov 2014 10:21:47 -0500 Subject: [PATCH 06/16] Remove unneeded else. Update contributors. --- CONTRIBUTORS.txt | 1 + colander/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 466fc7b..4233f59 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -120,3 +120,4 @@ Contributors - Cédric Messiant, 2014/06/27 - Gouji Ochiai, 2014/08/21 - Tim Tisdall, 2014/09/10 +- Amos Latteier, 2014/11/30 diff --git a/colander/__init__.py b/colander/__init__.py index 8a9201d..84e6107 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1973,8 +1973,7 @@ class _SchemaNode(object): raise UnboundDeferredError( "Schema node {node} has an unbound deferred validator" .format(node=self)) - else: - self.validator(self, appstruct) + self.validator(self, appstruct) return appstruct def add(self, node): From 048b76c143d9888d0e7fae45f0f685f53e58c73b Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Thu, 11 Dec 2014 10:58:21 +0100 Subject: [PATCH 07/16] Add a UUID string validator The `uuid` validator uses a regex validator to match on all UUID strings allowed by the `uuid.UUID` class. --- colander/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/colander/__init__.py b/colander/__init__.py index be973bb..235503a 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -300,9 +300,9 @@ class Regex(object): validation succeeds; otherwise, :exc:`colander.Invalid` is 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): - self.match_object = re.compile(regex) + self.match_object = re.compile(regex, flags) else: self.match_object = regex if msg is None: @@ -465,6 +465,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')) + +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'), 2) # 2 = re.IGNORECASE + + class SchemaType(object): """ Base class for all schema types """ def flatten(self, node, appstruct, prefix='', listitem=False): From 369e9dc2ef273def6a742ea35fddceedd3d29bb8 Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Thu, 11 Dec 2014 11:01:58 +0100 Subject: [PATCH 08/16] Sign the contributors agreement Signed-off-by: Jimmy Thrasibule --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a2709d0..d208a1d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -122,3 +122,4 @@ Contributors - Gouji Ochiai, 2014/08/21 - Tim Tisdall, 2014/09/10 - Amos Latteier, 2014/11/30 +- Jimmy Thrasibule, 2014/12/11 From 442014665227c736bffa8b68e40e1caadf1c4c3d Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 12 Dec 2014 22:09:14 +0100 Subject: [PATCH 09/16] Fix UUID validator implementation - Document the new `flags` parameter now accepted by the Regex validator. - Make use of the correct regex flag. --- colander/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/colander/__init__.py b/colander/__init__.py index 235503a..d0cd3fa 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -293,6 +293,9 @@ class Regex(object): error message to be used; otherwise, defaults to 'String does 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 result of ``re.compile``) instead of a string. @@ -467,7 +470,7 @@ 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'), 2) # 2 = re.IGNORECASE +uuid = Regex(UUID_REGEX, _('Invalid UUID string'), re.IGNORECASE) class SchemaType(object): From 3741e6296093b9ca43e6230b9a88883c5603fded Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 12 Dec 2014 22:50:04 +0100 Subject: [PATCH 10/16] Add unit tests for UUID validator --- colander/tests/test_colander.py | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 961ff5d..cd2976f 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -538,6 +538,57 @@ class Test_url_validator(unittest.TestCase): from colander import Invalid 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.assertIsNone(result) + + def test_success_with_dashes(self): + val = '123e4567-e89b-12d3-a456-426655440000' + result = self._callFUT(val) + self.assertIsNone(result) + + def test_success_upper_case(self): + val = '123E4567-E89B-12D3-A456-426655440000' + result = self._callFUT(val) + self.assertIsNone(result) + + def test_success_with_braces(self): + val = '{123e4567-e89b-12d3-a456-426655440000}' + result = self._callFUT(val) + self.assertIsNone(result) + + def test_success_with_urn_ns(self): + val = 'urn:uuid:{123e4567-e89b-12d3-a456-426655440000}' + result = self._callFUT(val) + self.assertIsNone(result) + + 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): def _makeOne(self, *arg, **kw): from colander import SchemaType From 6c873ba762e94c82cd8ee389e357f8d4df508633 Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 12 Dec 2014 23:17:24 +0100 Subject: [PATCH 11/16] Add the `uuid` validator to the API documentation --- docs/api.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 51b6d75..15439f5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -78,6 +78,11 @@ Validators 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 ~~~~~ From 412d2a79bd88cc41437d9fb216a1eafa3e2c9af5 Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 12 Dec 2014 23:25:13 +0100 Subject: [PATCH 12/16] Include the right CHANGES file in documentation --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index abb1f10..d9e113e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1 +1 @@ -.. include:: ../CHANGES.txt +.. include:: ../CHANGES.rst From b706ea144cab2068be2f047593cc12fb47b9d683 Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 12 Dec 2014 23:49:01 +0100 Subject: [PATCH 13/16] Fix test unit errors Replace assertIsNone by assertEqual. --- colander/tests/test_colander.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index cd2976f..7918e45 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -546,27 +546,27 @@ class TestUUID(unittest.TestCase): def test_success_hexadecimal(self): val = '123e4567e89b12d3a456426655440000' result = self._callFUT(val) - self.assertIsNone(result) + self.assertEqual(result, None) def test_success_with_dashes(self): val = '123e4567-e89b-12d3-a456-426655440000' result = self._callFUT(val) - self.assertIsNone(result) + self.assertEqual(result, None) def test_success_upper_case(self): val = '123E4567-E89B-12D3-A456-426655440000' result = self._callFUT(val) - self.assertIsNone(result) + self.assertEqual(result, None) def test_success_with_braces(self): val = '{123e4567-e89b-12d3-a456-426655440000}' result = self._callFUT(val) - self.assertIsNone(result) + self.assertEqual(result, None) def test_success_with_urn_ns(self): val = 'urn:uuid:{123e4567-e89b-12d3-a456-426655440000}' result = self._callFUT(val) - self.assertIsNone(result) + self.assertEqual(result, None) def test_failure_random_string(self): val = 'not-a-uuid' From e07092050c04ec72a3a8aed33462a3c4b5ac2ced Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 18 Dec 2014 21:23:36 -0500 Subject: [PATCH 14/16] Speed up Travis start via 'sudo: false'. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5a205b2..cb98fdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ # Wire up travis language: python +sudo: false env: - TOXENV=py26 From c1b4f71a07e9b53d7f74afecd7a1d0fd9b4fcd2d Mon Sep 17 00:00:00 2001 From: Tim Tisdall Date: Wed, 14 Jan 2015 22:52:26 +0200 Subject: [PATCH 15/16] make version PEP440 compliant --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 130f79f..81a6314 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ testing_extras = ['nose', 'coverage'] docs_extras = ['Sphinx'] setup(name='colander', - version='1.1dev', + version='1.1.dev0', description=('A simple schema-based serialization and deserialization ' 'library'), long_description=README + '\n\n' + CHANGES, From afd3c0f6a9ccfc34720a0e71a69786c6f503191c Mon Sep 17 00:00:00 2001 From: Tim Tisdall Date: Thu, 29 Jan 2015 22:08:53 +0200 Subject: [PATCH 16/16] fix badly named test The test says it's serializing and then calls deserialize() --- colander/tests/test_colander.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 7918e45..64e1cf9 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -1550,7 +1550,7 @@ class TestInteger(unittest.TestCase): result = typ.serialize(node, val) self.assertEqual(result, colander.null) - def test_serialize_emptystring(self): + def test_deserialize_emptystring(self): import colander val = '' node = DummySchemaNode(None)