diff --git a/.gitignore b/.gitignore index e8e56fc..54a4a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,8 @@ *.swp .coverage .tox/ -nosetests.xml +coverage-*.xml +nosetests*.xml env26/ env25/ env24/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5171704 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +# Wire up travis +language: python +sudo: false + +matrix: + include: + - python: 2.6 + env: TOXENV=py26 + - python: 2.7 + env: TOXENV=py27 + - python: 3.2 + env: TOXENV=py32 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: pypy + env: TOXENV=pypy + - python: pypy3 + env: TOXENV=pypy3 + - python: 3.5 + env: TOXENV=py2-cover,py3-cover,coverage + - python: 3.5 + env: TOXENV=docs + +install: + - travis_retry pip install tox + +script: + - travis_retry tox + +notifications: + email: + - pyramid-checkins@lists.repoze.org + irc: + channels: + - "chat.freenode.net#pyramid" diff --git a/CHANGES.txt b/CHANGES.rst similarity index 91% rename from CHANGES.txt rename to CHANGES.rst index e27ee34..43b91b5 100644 --- a/CHANGES.txt +++ b/CHANGES.rst @@ -1,34 +1,103 @@ -Unreleased ----------- +unreleased +========== -Bug Fixes -~~~~~~~~~ - -- Un-break wrapping of callable instances as ``colander.deferred``. - See https://github.com/Pylons/colander/issues/141. -- Set the max length TLD to 22 in ``Email`` validator based on the - current list of valid TLDs. - See https://github.com/Pylons/colander/issues/159 -- Fix an issue where ``drop`` was not recognized as a default and was - returning the ``drop`` instance instead of omitting the value. - https://github.com/Pylons/colander/issues/139 - We implicitly assign the schema type to MappingSchema, SequenceSchema, and TupleSchema with types of Mapping, Sequence, and Tuple explicitly. This is a minor backwards incompatibility, because we no longer allow any of those - schema variants to accept the implied type as a first argument. + schema variants to accept the implied type as a first argument. i.e. MappingSchema(Mapping()) will no longer work. +1.1 (2016-01-15) +================ + +Platform +-------- + +- Add explicit support for Python 3.4, Python 3.5 and PyPy3. + Features -~~~~~~~~ +-------- -- Add `Any` validator which succeeds if at least one of its subvalidators - succeeded. +- Add ``min_err`` and ``max_err`` arguments to ``Length``, allowing + customization of its error messages. -1.0b1 (2013-09-01) ------------------- +- Add ``colander.Any`` validator: succeeds if at least one of its + subvalidators succeeded. + +- Allow localization of error messages returned by ``colander.Invalid.asdict`` + by adding an optional ``translate`` callable argument. + +- Add a ``missing_msg`` argument to ``SchemaNode``, allowing customization + of the error message used when the node is required and missing. + +- Add `NoneOf` validator wich succeeds if the value is none of the choices. + +- Add ``normalize`` option to ``Decimal``, stripping the rightmost + trailing zeros. Bug Fixes -~~~~~~~~~ +--------- + +- Update translations: ``de``, ``ja``, ``fr``. + +- Fix an issue where the ``flatten()`` method produces an invalid name + (ex: "answer.0.") for the type ``Sequence``. See + https://github.com/Pylons/colander/issues/179 + +- Fixed issue with ``String`` not being properly encoded when non-string + values were passed into ``serialize()`` + See `#235 `_ + +- ``title`` was being overwritten when made a child through defining a schema + as a class. See `#239 `_ + + +1.0 (2014-11-26) +================ + +Bug Fixes +--------- + +- Removed forked ``iso8601`` and change to dependency on PyPI ``iso8601`` + (due to float rounding bug on microsecond portion when parsing + iso8601 datetime string). Left an ``iso8601.py`` stub for backwards + compatibility. + +- Time of "00:00" no longer gives ``colander.Invalid``. + +- Un-break wrapping of callable instances as ``colander.deferred``. + See https://github.com/Pylons/colander/issues/141. + +- Set the max length TLD to 22 in ``Email`` validator based on the + current list of valid TLDs. + See https://github.com/Pylons/colander/issues/159 + +- Fix an issue where ``drop`` was not recognized as a default and was + returning the ``drop`` instance instead of omitting the value. + https://github.com/Pylons/colander/issues/139 + +- Fix an issue where the ``SchemaNode.title`` was clobbered by the ``name`` + when defined as a class attribute. + See https://github.com/Pylons/colander/pull/183 and + https://github.com/Pylons/colander/pull/185 + +- 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) +================== + +Bug Fixes +--------- - In 1.0a1, there was a change merged from https://github.com/Pylons/colander/pull/73 which made it possible to supply @@ -58,7 +127,7 @@ Bug Fixes See https://github.com/Pylons/colander/issues/100 Features -~~~~~~~~ +-------- - Add ``colander.List`` type, modeled on ``deform.List``: this type preserves ordering, and allows duplicates. @@ -79,18 +148,17 @@ Features - The ``typ`` of a ``SchemaNode`` can optionally be pased in as a keyword argument. See https://github.com/Pylons/colander/issues/90 -- Add a ``missing_msg`` argument to ``SchemaNode`` that specifies the error - message to be used when the node is required and missing +- Allow interpolation of `missing_msg` with properties `title` and `name` 1.0a5 (2013-05-31) ------------------- +================== - Fix bug introduced by supporting spec-mandated truncations of ISO-8601 timezones. A TypeError would be raised instead of Invalid. See https://github.com/Pylons/colander/issues/111. 1.0a4 (2013-05-21) ------------------- +================== - Loosen Email validator regex (permit apostrophes, bang, etc in localpart). @@ -99,10 +167,10 @@ Features https://github.com/Pylons/colander/pull/108. 1.0a3 (2013-05-16) ------------------- +================== Features -~~~~~~~~ +-------- - Support spec-mandated truncations of ISO-8601 timezones. @@ -111,7 +179,7 @@ Features - Allow specifying custom representations of values for boolean fields. Bug Fixes -~~~~~~~~~ +--------- - Ensure that ``colander.iso8601.FixedOffset`` instances can be unpickled. @@ -122,10 +190,10 @@ Bug Fixes 1.0a2 (2013-01-30) ------------------- +================== Features -~~~~~~~~ +-------- - Add ``colander.ContainsOnly`` and ``colander.url`` validators. @@ -133,10 +201,10 @@ Features mappings and sequences more succinctly. 1.0a1 (2013-01-10) ------------------- +================== Bug Fixes -~~~~~~~~~ +--------- - Work around a regression in Python 3.3 for ``colander.Decimal`` when it's used with a ``quant`` argument but without a ``rounding`` argument. @@ -152,7 +220,7 @@ Bug Fixes https://github.com/Pylons/colander/pull/96). Features -~~~~~~~~ +-------- - Add ``colander.Set`` type, ported from ``deform.Set`` @@ -391,7 +459,7 @@ Features id='a2', ) c = colander.SchemaNode( - colander.String(), + colander.String(), id='c2', ) e = colander.SchemaNode( @@ -447,7 +515,7 @@ Features id='a2', ) c = colander.SchemaNode( - colander.String(), + colander.String(), id='c2', ) e = colander.SchemaNode( @@ -476,17 +544,17 @@ Features MRO deepest-first ordering (``One``, then ``Two``, then ``Three``). Backwards Incompatibilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- - Passing non-SchemaNode derivative instances as ``*children`` into a SchemaNode constructor is no longer supported. Symptom: ``AttributeError: name`` when constructing a SchemaNode. 0.9.9 (2012-09-24) ------------------- +================== Features -~~~~~~~~ +-------- - Allow the use of ``missing=None`` for Number. See https://github.com/Pylons/colander/pull/59 . @@ -525,7 +593,7 @@ Features custom type). Backwards Incompatibilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- - The inheritance changes required a minor backwards incompatibility: calling ``__setitem__`` on a SchemaNode will no longer raise ``KeyError`` when @@ -534,13 +602,13 @@ Backwards Incompatibilities the child list. Documentation -~~~~~~~~~~~~~ +------------- - A "Schema Inheritance" section was added to the Basics chapter documentation. 0.9.8 (2012-04-27) ------------------- +================== - False evaluating values are now serialized to colander.null for String, Date, and Time. This resolves the issue where a None value @@ -560,7 +628,7 @@ Documentation - Add ``dev`` and ``docs`` setup.py aliases (e.g. ``python setup.py dev``). 0.9.7 (2012-03-20) ------------------- +================== - Using ``schema.flatten(...)`` against a mapping schema node without a name produced incorrectly dot-prefixed keys. See @@ -587,7 +655,7 @@ Documentation error message. See https://github.com/Pylons/colander/pull/41 0.9.6 (2012-02-14) ------------------- +================== - No longer runs on Python 2.4 or 2.5. Python 2.6+ is now required. @@ -600,7 +668,7 @@ Documentation LICENSE.txt. 0.9.5 (2012-01-13) ------------------- +================== - Added Czech translation. @@ -611,7 +679,7 @@ Documentation - Documentation added about flatten and unflatten. 0.9.4 (2011-10-14) ------------------- +================== - ``flatten`` now only includes leaf nodes in the flattened dict. @@ -629,7 +697,7 @@ Documentation - Add Swedish, French, Chinese translations. 0.9.3 (2011-06-23) ------------------- +================== - Add ``Time`` type. @@ -650,7 +718,7 @@ Documentation documentation. 0.9.2 (2011-03-28) ------------------- +================== - Added Polish translation, thanks to Jedrzej Nowak. @@ -674,7 +742,7 @@ Documentation cstruct during ``deserialize``. 0.9.1 (2010-12-02) ------------------- +================== - When ``colander.null`` was unpickled, the reference created during unpickling was *not* a reference to the singleton but rather a new instance @@ -684,7 +752,7 @@ Documentation pickled is unpickled as the singleton itself. 0.9 (2010-11-28) ------------------ +================= - SchemaNode constructor now accepts arbitrary keyword arguments. It sets any unknown values within the ``**kw`` sequence as attributes @@ -708,7 +776,7 @@ Documentation ``colander.SchemaType`` to get a default implementation. 0.8 (2010/09/08) ------------------ +================= - Docstring fixes to ``colander.SchemaNode`` (``missing`` is not the ``null`` value when required, it's a special marker value). @@ -724,7 +792,7 @@ Documentation been properly documented. 0.7.3 (2010/09/02) ------------------- +================== - The title of a schema node now defaults to a titleization of the ``name``. Underscores in the ``name`` are replaced with empty @@ -739,7 +807,7 @@ Documentation single-element list containing the ``msg`` value is returned. 0.7.2 (2010/08/30) ------------------- +================== - Add an ``colander.SchemaNode.__iter__`` method, which iterates over the children nodes of a schema node. @@ -749,7 +817,7 @@ Documentation internally). 0.7.1 (2010/06/12) ------------------- +================== - Make it possible to use ``colander.null`` as a ``missing`` argument to ``colander.SchemaNode`` for roundtripping purposes. @@ -757,13 +825,13 @@ Documentation - Make it possible to pickle ``colander.null``. 0.7.0 ------ +===== A release centered around normalizing the treatment of default and missing values. Bug Fixes -~~~~~~~~~ +--------- - Allow ``colander.Regex`` validator to accept a pattern object instead of just a string. @@ -779,7 +847,7 @@ Bug Fixes ``colander.SchemaNode``. Backwards Incompatiblities / New Features -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------------- - ``missing`` constructor arg to SchemaNode: signifies *deserialization* default, disambiguated from ``default`` which acted @@ -845,7 +913,7 @@ Backwards Incompatiblities / New Features no longer a keyword argument that has a default. 0.6.2 (2010-05-08) ------------------- +================== - The default ``encoding`` parameter value to the ``colander.String`` type is still ``None``, however its meaning has changed. An @@ -864,13 +932,13 @@ Backwards Incompatiblities / New Features a bool value when a default value was found for the schema node. 0.6.1 (2010-05-04) ------------------- +================== - Add a Decimal type (number type which uses ``decimal.Decimal`` as a deserialization target). 0.6.0 (2010-05-02) ------------------- +================== - (Hopefully) fix intermittent datetime-granularity-related test failures. @@ -893,7 +961,7 @@ Backwards Incompatiblities / New Features to its interface within the API documentation. 0.5.2 (2010-04-09) ------------------- +================== - Add Email and Regex validators (courtesy Steve Howe). @@ -911,7 +979,7 @@ Backwards Incompatiblities / New Features add something like it back later if we need it. 0.5.1 (2010-04-02) ------------------- +================== - The constructor arguments to a the ``colander.Schema`` class are now sent to the constructed SchemaNode rather than to the type it represents. @@ -935,20 +1003,20 @@ Backwards Incompatiblities / New Features the type). 0.5 (2010-03-31) ----------------- +================ - 0.4 was mispackaged (CHANGES.txt missing); no code changes from 0.4 however. 0.4 (2010-03-30) ----------------- +================ - Add ``colander.DateTime`` and ``colander.Date`` data types. - Depend on the ``iso8601`` package for date support. 0.3 (2010-03-29) ----------------- +================ - Subnodes of a schema node are now kept in the ``children`` attribute rather than the ``nodes`` attribute. @@ -965,7 +1033,7 @@ Backwards Incompatiblities / New Features - Add ``colander.Length`` validator class. 0.2 (2010-03-23) ----------------- +================ - Make nodetype overrideable. @@ -995,6 +1063,6 @@ Backwards Incompatiblities / New Features 0.1 (2010-03-14) ----------------- +================ - Initial release. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 279da11..c3fdf73 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -101,7 +101,8 @@ and agrees to the terms above in the section within this document entitled Contributors ------------ -- Chris McDonough, 2011/02/16 +- Chris McDonough, 2010/03/11 +- Tres Seaver, 2010/04/05 - Chris Withers, 2011/05/45 - Mathieu Le Marec - Pasquet (kiorky), 2011/07/11 - Atsushi Odagiri, 2012/02/04 @@ -115,4 +116,14 @@ Contributors - Peter Lamut, 2013/08/16 - Veeti Paananen, 2013/08/20 - Michael Howitz, 2013/12/05 +- Alex Marandon, 2013/12/21 - Joe Dallago, 2014/2/10 +- Jaseem Abid, 2014/06/16 +- Cédric Messiant, 2014/06/27 +- Gouji Ochiai, 2014/08/21 +- Tim Tisdall, 2014/09/10 +- Romain Commandé, 2014/10/11 +- Nando Florestan, 2014/11/27 +- Amos Latteier, 2014/11/30 +- Jimmy Thrasibule, 2014/12/11 +- Tinne Cahy, 2015/12/22 diff --git a/LICENSE.txt b/LICENSE.txt index c4a6ed2..5ced96e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -39,6 +39,3 @@ License THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -This package uses code from the "pyiso8601" package by Michael Twomey, -licensed under the MIT license. See the source file named "iso8601.py" for -copyright information and license text. diff --git a/README.txt b/README.rst similarity index 51% rename from README.txt rename to README.rst index 215ba5d..82e26d6 100644 --- a/README.txt +++ b/README.rst @@ -1,6 +1,15 @@ Colander ======== + +.. image:: https://travis-ci.org/Pylons/colander.svg?branch=master + :target: https://travis-ci.org/Pylons/colander + +.. image:: https://readthedocs.org/projects/colander/badge/?version=master + :target: http://docs.pylonsproject.org/projects/colander/en/master/ + :alt: Documentation Status + + An extensible package which can be used to: - deserialize and validate a data structure composed of strings, @@ -9,7 +18,8 @@ An extensible package which can be used to: - serialize an arbitrary data structure to a data structure composed of strings, mappings, and lists. -It runs on Python 2.6, 2.7, 3.2, and 3.3. +It runs on Python 2.6, 2.7, 3.2, 3.3, 3.4, and 3.5, and on current PyPy +and PyPy3 versions. Please see http://docs.pylonsproject.org/projects/colander/en/latest/ for further documentation. diff --git a/colander/__init__.py b/colander/__init__.py index 870b0d0..3858b00 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -22,7 +22,12 @@ from . import iso8601 _ = translationstring.TranslationStringFactory('colander') -required = object() +class _required(object): + """ Represents a required value in colander-related operations. """ + def __repr__(self): + return '' + +required = _required() _marker = required # bw compat class _null(object): @@ -42,11 +47,12 @@ class _null(object): null = _null() class _drop(object): + """ Represents a value that will be dropped from the schema if it + is missing during *deserialization*. Passed as a value to the + `missing` keyword argument of :class:`SchemaNode`. """ - Represents a value that should be dropped if it is missing during - deserialization. - """ - pass + def __repr__(self): + return '' drop = _drop() @@ -57,6 +63,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 @@ -165,9 +178,14 @@ class Invalid(Exception): return str(self.pos) return str(self.node.name) - def asdict(self): + def asdict(self, translate=None): """ Return a dictionary containing a basic - (non-language-translated) error report for this exception""" + (non-language-translated) error report for this exception. + + If ``translate`` is supplied, it must be a callable taking a + translation string as its sole argument and returning a localized, + interpolated string. + """ paths = self.paths() errors = {} for path in paths: @@ -177,6 +195,8 @@ class Invalid(Exception): exc.msg and msgs.extend(exc.messages()) keyname = exc._keyname() keyname and keyparts.append(keyname) + if translate: + msgs = [translate(msg) for msg in msgs] errors['.'.join(keyparts)] = '; '.join(interpolate(msgs)) return errors @@ -279,6 +299,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. @@ -286,9 +309,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: @@ -307,7 +330,7 @@ class Email(Regex): the error message to be used when raising :exc:`colander.Invalid`; otherwise, defaults to 'Invalid email address'. """ - + def __init__(self, msg=None): email_regex = text_(EMAIL_RE) if msg is None: @@ -339,49 +362,74 @@ class Range(object): provided, it defaults to ``'${val} is greater than maximum value ${max}'``. """ - min_err = _('${val} is less than minimum value ${min}') - max_err = _('${val} is greater than maximum value ${max}') + _MIN_ERR = _('${val} is less than minimum value ${min}') + _MAX_ERR = _('${val} is greater than maximum value ${max}') - def __init__(self, min=None, max=None, min_err=None, max_err=None): + def __init__(self, min=None, max=None, min_err=_MIN_ERR, max_err=_MAX_ERR): self.min = min self.max = max - if min_err is not None: - self.min_err = min_err - if max_err is not None: - self.max_err = max_err + self.min_err = min_err + self.max_err = max_err def __call__(self, node, value): if self.min is not None: if value < self.min: - min_err = _(self.min_err, mapping={'val':value, 'min':self.min}) + min_err = _( + self.min_err, mapping={'val':value, 'min':self.min}) raise Invalid(node, min_err) if self.max is not None: if value > self.max: - max_err = _(self.max_err, mapping={'val':value, 'max':self.max}) + max_err = _( + self.max_err, mapping={'val':value, 'max':self.max}) raise Invalid(node, max_err) + class Length(object): - """ Validator which succeeds if the value passed to it has a - length between a minimum and maximum. The value is most often a - string.""" - def __init__(self, min=None, max=None): + """Validator which succeeds if the value passed to it has a + length between a minimum and maximum, expressed in the + optional ``min`` and ``max`` arguments. + The value can be any sequence, most often a string. + + If ``min`` is not specified, or is specified as ``None``, + no lower bound exists. If ``max`` is not specified, or + is specified as ``None``, no upper bound exists. + + The default error messages are "Shorter than minimum length ${min}" + and "Longer than maximum length ${max}". These can be customized: + + ``min_err`` is used to form the ``msg`` of the + :exc:`colander.Invalid` error when reporting a validation failure + caused by a value not meeting the minimum length. If ``min_err`` is + specified, it must be a string. The string may contain the + replacement target ``${min}``. + + ``max_err`` is used to form the ``msg`` of the + :exc:`colander.Invalid` error when reporting a validation failure + caused by a value exceeding the maximum length. If ``max_err`` is + specified, it must be a string. The string may contain the + replacement target ``${max}``. + """ + _MIN_ERR = _('Shorter than minimum length ${min}') + _MAX_ERR = _('Longer than maximum length ${max}') + + def __init__(self, min=None, max=None, min_err=_MIN_ERR, max_err=_MAX_ERR): self.min = min self.max = max + self.min_err = min_err + self.max_err = max_err def __call__(self, node, value): if self.min is not None: if len(value) < self.min: - min_err = _('Shorter than minimum length ${min}', - mapping={'min':self.min}) + min_err = _(self.min_err, mapping={'min': self.min}) raise Invalid(node, min_err) - if self.max is not None: if len(value) > self.max: - max_err = _('Longer than maximum length ${max}', - mapping={'max':self.max}) + max_err = _(self.max_err, mapping={'max': self.max}) raise Invalid(node, max_err) + class OneOf(object): """ Validator which succeeds if the value passed to it is one of a fixed set of values """ @@ -395,6 +443,33 @@ class OneOf(object): mapping={'val':value, 'choices':choices}) raise Invalid(node, err) + +class NoneOf(object): + """ Validator which succeeds if the value passed to it is none of a + fixed set of values. + + ``msg_err`` is used to form the ``msg`` of the :exc:`colander.Invalid` + error when reporting a validation failure. If ``msg_err`` is specified, + it must be a string. The string may contain the replacement targets + ``${choices}`` and ``${val}``, representing the set of forbidden values + and the provided value respectively. + """ + _MSG_ERR = _('"${val}" must not be one of ${choices}') + + def __init__(self, choices, msg_err=_MSG_ERR): + self.forbidden = choices + self.msg_err = msg_err + + def __call__(self, node, value): + if value not in self.forbidden: + return + + choices = ', '.join(['%s' % x for x in self.forbidden]) + err = _(self.msg_err, mapping={'val': value, 'choices': choices}) + + raise Invalid(node, err) + + class ContainsOnly(object): """ Validator which succeeds if the value passed to is a sequence and each element in the sequence is also in the sequence passed as ``choices``. @@ -451,6 +526,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'), re.IGNORECASE) + + class SchemaType(object): """ Base class for all schema types """ def flatten(self, node, appstruct, prefix='', listitem=False): @@ -459,7 +539,7 @@ class SchemaType(object): selfname = prefix else: selfname = '%s%s' % (prefix, node.name) - result[selfname] = appstruct + result[selfname.rstrip('.')] = appstruct return result def unflatten(self, node, paths, fstruct): @@ -910,7 +990,7 @@ class Sequence(Positional, SchemaType): def _validate(self, node, value, accept_scalar): if (hasattr(value, '__iter__') and - not hasattr(value, 'get') and + not hasattr(value, 'get') and not isinstance(value, string_types)): return list(value) if accept_scalar: @@ -1095,7 +1175,7 @@ class String(SchemaType): - A non-Unicode input value to ``serialize`` is converted to a Unicode using the encoding (``unicode(value, encoding)``); - subsequently the Unicode object is reeencoded to a ``str`` + subsequently the Unicode object is re-encoded to a ``str`` object using the encoding and returned. - A Unicode input value to ``deserialize`` is returned @@ -1132,6 +1212,8 @@ class String(SchemaType): result = text_type(appstruct) else: result = text_type(appstruct) + if self.encoding: + result = result.encode(self.encoding) return result except Exception as e: raise Invalid(node, @@ -1223,24 +1305,27 @@ class Decimal(Number): method of this class, the :attr:`colander.null` value will be returned. - The Decimal constructor takes two optional arguments, ``quant`` and - ``rounding``. If supplied, ``quant`` should be a string, + The Decimal constructor takes three optional arguments, ``quant``, + ``rounding`` and ``normalize``. If supplied, ``quant`` should be a string, (e.g. ``1.00``). If supplied, ``rounding`` should be one of the Python ``decimal`` module rounding options (e.g. ``decimal.ROUND_UP``, ``decimal.ROUND_DOWN``, etc). The serialized and deserialized result will be quantized and rounded via ``result.quantize(decimal.Decimal(quant), rounding)``. ``rounding`` is - ignored if ``quant`` is not supplied. + ignored if ``quant`` is not supplied. If ``normalize`` is ``True``, + the serialized and deserialized result will be normalized by stripping + the rightmost trailing zeros. The subnodes of the :class:`colander.SchemaNode` that wraps this type are ignored. """ - def __init__(self, quant=None, rounding=None): + def __init__(self, quant=None, rounding=None, normalize=False): if quant is None: self.quant = None else: self.quant = decimal.Decimal(quant) self.rounding = rounding + self.normalize = normalize def num(self, val): result = decimal.Decimal(str(val)) @@ -1249,6 +1334,8 @@ class Decimal(Number): result = result.quantize(self.quant) else: result = result.quantize(self.quant, self.rounding) + if self.normalize: + result = result.normalize() return result class Money(Decimal): @@ -1264,8 +1351,7 @@ class Money(Decimal): this type are ignored. """ def __init__(self): - self.quant = decimal.Decimal('.01') - self.rounding = decimal.ROUND_UP + super(Money, self).__init__(decimal.Decimal('.01'), decimal.ROUND_UP) class Boolean(SchemaType): """ A type representing a boolean object. @@ -1293,7 +1379,7 @@ class Boolean(SchemaType): are considered ``True``, and an Invalid exception would be raised for values outside of both :attr:`false_choices` and :attr:`true_choices`. - Serialization will produce :attr:`true_val` or :attr:`false_val` + Serialization will produce :attr:`true_val` or :attr:`false_val` based on the value. If the :attr:`colander.null` value is passed to the serialize @@ -1340,8 +1426,8 @@ class Boolean(SchemaType): else: raise Invalid(node, _('"${val}" is neither in (${false_choices}) ' - 'nor in (${true_choices})', - mapping={'val':cstruct, + 'nor in (${true_choices})', + mapping={'val':cstruct, 'false_choices': self.false_reprs, 'true_choices': self.true_reprs }) ) @@ -1524,9 +1610,7 @@ class DateTime(SchemaType): """ err_template = _('Invalid date') - def __init__(self, default_tzinfo=_marker): - if default_tzinfo is _marker: - default_tzinfo = iso8601.Utc() + def __init__(self, default_tzinfo=iso8601.UTC): self.default_tzinfo = default_tzinfo def serialize(self, node, appstruct): @@ -1635,7 +1719,7 @@ class Time(SchemaType): This type serializes python ``datetime.time`` objects to a `ISO8601 `_ string format. - The format includes the date only. + The format includes the time only. The constructor accepts no arguments. @@ -1673,13 +1757,12 @@ class Time(SchemaType): err_template = _('Invalid time') def serialize(self, node, appstruct): - if not appstruct: - return null - if isinstance(appstruct, datetime.datetime): appstruct = appstruct.time() if not isinstance(appstruct, datetime.time): + if not appstruct: + return null raise Invalid(node, _('"${val}" is not a time object', mapping={'val':appstruct}) @@ -1735,7 +1818,9 @@ class _SchemaNode(object): - ``typ``: The 'type' for this node. It should be an instance of a class that implements the :class:`colander.interfaces.Type` interface. If ``typ`` is not passed, - it defaults to ``colander.Mapping()``. + a call to the ``schema_type()`` method on this class is made to + get a default type. (When subclassing, ``schema_type()`` should + be overridden to provide a reasonable default type). - ``*children``: a sequence of subnodes. If the subnodes of this node are not known at construction time, they can later be added @@ -1806,10 +1891,10 @@ class _SchemaNode(object): validator = None default = null missing = required - missing_msg = _('Required') + missing_msg = 'Required' name = '' - raw_title = _marker - title = '' + raw_title = _marker # only changes if title is explicitly set + title = _marker description = '' widget = None after_bind = None @@ -1834,7 +1919,7 @@ class _SchemaNode(object): self.typ = self.schema_type() # bw compat forces us to manufacture a title if one is not supplied - title = kw.get('title', _marker) + title = kw.get('title', self.title) if title is _marker: name = kw.get('name', self.name) kw['title'] = name.replace('_', ' ').title() @@ -1846,7 +1931,7 @@ class _SchemaNode(object): @staticmethod def schema_type(): raise NotImplementedError( - 'Schema node construction without a typ argument or ' + 'Schema node construction without a `typ` argument or ' 'a schema_type() callable present on the node class ' ) @@ -1948,15 +2033,21 @@ 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 return appstruct if self.validator is not None: - if not isinstance(self.validator, deferred): # unbound - self.validator(self, appstruct) + if isinstance(self.validator, deferred): # unbound + raise UnboundDeferredError( + "Schema node {node} has an unbound deferred validator" + .format(node=self)) + self.validator(self, appstruct) return appstruct def add(self, node): @@ -2214,9 +2305,9 @@ class instantiate(object): 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/compat.py b/colander/compat.py index 5e6014b..4e7e598 100644 --- a/colander/compat.py +++ b/colander/compat.py @@ -1,13 +1,14 @@ import sys +PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 -if PY3: # pragma: no cover - string_types = str, - text_type = str -else: # pragma: no cover +if PY2: string_types = basestring, text_type = unicode +else: + string_types = str, + text_type = str def text_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``bytes``, return ``s.decode(encoding, @@ -16,14 +17,14 @@ def text_(s, encoding='latin-1', errors='strict'): return s.decode(encoding, errors) return s # pragma: no cover -if PY3: # pragma: no cover +if PY2: + def is_nonstr_iter(v): + return hasattr(v, '__iter__') +else: def is_nonstr_iter(v): if isinstance(v, str): return False return hasattr(v, '__iter__') -else: # pragma: no cover - def is_nonstr_iter(v): - return hasattr(v, '__iter__') try: xrange = xrange diff --git a/colander/iso8601.py b/colander/iso8601.py index 0dcd9a6..f608c11 100644 --- a/colander/iso8601.py +++ b/colander/iso8601.py @@ -1,152 +1,5 @@ -""" -Copyright (c) 2007 Michael Twomey - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -ISO 8601 date time string parsing - -Basic usage: ->>> import iso8601 ->>> iso8601.parse_date("2007-01-25T12:00:00Z") -datetime.datetime(2007, 1, 25, 12, 0, tzinfo=) ->>> -""" - - -from datetime import datetime, timedelta, tzinfo -import re - -from .compat import string_types +from __future__ import absolute_import +import iso8601 +from iso8601.iso8601 import (parse_date, ParseError, Utc, FixedOffset, UTC, ZERO, ISO8601_REGEX) __all__ = ["parse_date", "ParseError", "Utc", "FixedOffset"] - -# Adapted from http://delete.me.uk/2005/03/iso8601.html -ISO8601_REGEX = re.compile( - r"(?P[0-9]{4})(-(?P[0-9]{1,2})(-(?P[0-9]{1,2})" - r"((?P.)(?P[0-9]{2})(:(?P[0-9]{2})(:(?P[0-9]{2})(\.(?P[0-9]+))?)?)?" - r"(?PZ|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?" -) -TIMEZONE_REGEX = re.compile( - "(?P[+-])(?P[0-9]{2})(:?(?P[0-9]{2}))?") - -class ParseError(Exception): - """Raised when there is a problem parsing a date string""" - -# Yoinked from python docs -ZERO = timedelta(0) -class Utc(tzinfo): - """UTC - - """ - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO -UTC = Utc() - -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) - self.__name = name - - def __getinitargs__(self): - # tzinfo.__reduce__ returns the type as the factory: supply - # defaults here, rather than in __init__. - return 0, 0, 'unknown' - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return ZERO - - def __repr__(self): - return "" % self.__name - -def parse_timezone(tzstring, default_timezone=UTC): - """Parses ISO 8601 time zone specs into tzinfo offsets - - """ - if tzstring == "Z": - return UTC - # This isn't strictly correct, but it's common to encounter dates without - # timezones so I'll assume the default (which defaults to UTC). - # Addresses issue 4. - if tzstring is None: - return default_timezone - m = TIMEZONE_REGEX.match(tzstring) - prefix = m.group('prefix') - hours = int(m.group('hours')) - minutes = m.group('minutes') - if minutes is None: - minutes = 0 - else: - minutes = int(minutes) - if prefix == "-": - hours = -hours - minutes = -minutes - return FixedOffset(hours, minutes, tzstring) - -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 - default. - """ - if not isinstance(datestring, string_types): - raise ParseError("Expecting a string %r" % datestring) - m = ISO8601_REGEX.match(datestring) - if not m: - raise ParseError("Unable to parse date string %r" % datestring) - groups = m.groupdict() - tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) - if (groups['year'] is None or - groups['month'] is None or - groups['day'] is None): - raise ParseError('Unable to parse date string %r' % datestring) - if groups["hour"] is None: - groups["hour"] = 0 - if groups["minute"] is None: - groups["minute"] = 0 - if groups["second"] is None: - groups["second"] = 0 - if groups["fraction"] is None: - groups["fraction"] = 0 - else: - groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) - try: - return datetime( - int(groups["year"]), int(groups["month"]), int(groups["day"]), - int(groups["hour"]), int(groups["minute"]), int(groups["second"]), - int(groups["fraction"]), tz) - except ValueError as e: - raise ParseError(*e.args) diff --git a/colander/locale/de/LC_MESSAGES/colander.po b/colander/locale/de/LC_MESSAGES/colander.po index b58eeca..b7663d1 100644 --- a/colander/locale/de/LC_MESSAGES/colander.po +++ b/colander/locale/de/LC_MESSAGES/colander.po @@ -31,11 +31,11 @@ msgstr "Ungültige E-Mail-Adresse" #: colander/__init__.py:315 msgid "${val} is less than minimum value ${min}" -msgstr "${val} ist kleiner als der erlaubter Mindestwert (${min})" +msgstr "${val} ist kleiner als der erlaubte Mindestwert (${min})" #: colander/__init__.py:316 msgid "${val} is greater than maximum value ${max}" -msgstr "${val} ist größer als der erlaubter Höchstwert (${max})" +msgstr "${val} ist größer als der erlaubte Höchstwert (${max})" #: colander/__init__.py:348 msgid "Shorter than minimum length ${min}" @@ -51,11 +51,11 @@ msgstr "\"${val}\" ist nicht in der Auswahl ${choices}" #: colander/__init__.py:377 msgid "One or more of the choices you made was not acceptable" -msgstr "" +msgstr "Mindestens eine Auswahl war nicht akzeptabel" #: colander/__init__.py:423 msgid "Must be a URL" -msgstr "" +msgstr "Muss eine URL sein" #: colander/__init__.py:519 msgid "\"${val}\" is not a mapping type: ${err}" @@ -99,7 +99,7 @@ msgstr "${val} ist keine Zeichenkette" #: colander/__init__.py:1279 msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})" -msgstr "" +msgstr "\"${val}\" ist weder in (${false_choices}) noch in (${true_choices}) enthalten" #: colander/__init__.py:1339 colander/__init__.py:1356 #: colander/__init__.py:1366 diff --git a/colander/locale/el/LC_MESSAGES/colander.mo b/colander/locale/el/LC_MESSAGES/colander.mo new file mode 100644 index 0000000..44a781f Binary files /dev/null and b/colander/locale/el/LC_MESSAGES/colander.mo differ diff --git a/colander/locale/el/LC_MESSAGES/colander.po b/colander/locale/el/LC_MESSAGES/colander.po new file mode 100644 index 0000000..f5d9782 --- /dev/null +++ b/colander/locale/el/LC_MESSAGES/colander.po @@ -0,0 +1,156 @@ +# Greek translations for colander. +# Copyright (C) 2013 ORGANIZATION +# This file is distributed under the same license as the colander project. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: colander 1.0b1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-11-16 14:23+0900\n" +"PO-Revision-Date: 2015-03-04 10:03+0200\n" +"Last-Translator: Daniel Dourvaris \n" +"Language-Team: el \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: colander/__init__.py:240 +msgid "Invalid value" +msgstr "Λάθος στοιχείο" + +#: colander/__init__.py:283 +msgid "String does not match expected pattern" +msgstr "Το κείμενο δέν αντιστοιχεί στην μακέτα" + +#: colander/__init__.py:302 +msgid "Invalid email address" +msgstr "Λάθος διεύθηνση email" + +#: colander/__init__.py:330 +msgid "${val} is less than minimum value ${min}" +msgstr "${val} είναι λιγότερο απο το ελάχιστο ${min}" + +#: colander/__init__.py:331 +msgid "${val} is greater than maximum value ${max}" +msgstr "${val} είναι μεγαλύτερο απο το μέγιστο ${max}" + +#: colander/__init__.py:363 +msgid "Shorter than minimum length ${min}" +msgstr "Λιγότερο απο το ελάχιστο ${min} στοιχεία" + +#: colander/__init__.py:369 +msgid "Longer than maximum length ${max}" +msgstr "Μεγαλύτερο απο το μέγιστο ${max} χαρακτήρες" + +#: colander/__init__.py:382 +msgid "\"${val}\" is not one of ${choices}" +msgstr "\"${val}\" δέν είναι ενα απο ${choices}" + +#: colander/__init__.py:392 +msgid "One or more of the choices you made was not acceptable" +msgstr "Ενα η παραπάνω απο τις επιλογές δέν ήταν αποδεχτό" + +#: colander/__init__.py:414 colander/__init__.py:419 +msgid "\"${val}\" is not a valid credit card number" +msgstr "\"${val}'" δέν είναι έγκυρη πιστοτική κάρτα" + +#: colander/__init__.py:440 +msgid "Must be a URL" +msgstr "Πρέπει να είναι URL" + +#: colander/__init__.py:536 +msgid "\"${val}\" is not a mapping type: ${err}" +msgstr "\"${val}\" δέν είναι τύπου mapping: ${err}" + +#: colander/__init__.py:578 +msgid "Unrecognized keys in mapping: \"${val}\"" +msgstr "Αγνωστα keys στο mapping: \"${val}\"" + +#: colander/__init__.py:674 colander/__init__.py:905 +msgid "\"${val}\" is not iterable" +msgstr "\"${val}\" δέν είναι λίστα" + +#: colander/__init__.py:682 +msgid "" +"\"${val}\" has an incorrect number of elements (expected ${exp}, was " +"${was})" +msgstr "" +"\"${val}\" έχει λάθος νούμερο στοιχέιων (ήθελε ${exp}, έχει " +"${was})" + +#: colander/__init__.py:821 colander/__init__.py:852 +msgid "${cstruct} is not iterable" +msgstr "${cstruct} δέν είναι λίστα" + +#: colander/__init__.py:1124 +msgid "${val} cannot be serialized: ${err}" +msgstr "${val} δέν μπορεί να γίνει serialize:${err}" + +#: colander/__init__.py:1142 +msgid "${val} is not a string: ${err}" +msgstr "${val} δέν είναι κείμενο: ${err}" + +#: colander/__init__.py:1162 colander/__init__.py:1173 +msgid "\"${val}\" is not a number" +msgstr "\"${val}\" δέν είναι νούμερο" + +#: colander/__init__.py:1317 +msgid "${val} is not a string" +msgstr "${val} δέν είναι κείμενο" + +#: colander/__init__.py:1328 +msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})" +msgstr "\"${val}\" ούτε στο (${false_choices}) είναι, ούτε στο (${true_choices})" + +#: colander/__init__.py:1388 colander/__init__.py:1405 +#: colander/__init__.py:1415 +msgid "relative name \"${val}\" irresolveable without package" +msgstr "" + +#: colander/__init__.py:1445 +msgid "\"${val}\" has no __name__" +msgstr "\"${val}\" δέν έχει __name__" + +#: colander/__init__.py:1454 +msgid "\"${val}\" is not a string" +msgstr "\"${val}\" δέν είναι κείμενο" + +#: colander/__init__.py:1463 +msgid "The dotted name \"${name}\" cannot be imported" +msgstr "" + +#: colander/__init__.py:1511 colander/__init__.py:1587 +msgid "Invalid date" +msgstr "Λάθος ημερομηνία" + +#: colander/__init__.py:1527 +msgid "\"${val}\" is not a datetime object" +msgstr "\"${val}\" δέν είναι τύπου datetime" + +#: colander/__init__.py:1598 +msgid "\"${val}\" is not a date object" +msgstr "\"${val}\" δέν είναι τύπου date" + +#: colander/__init__.py:1659 +msgid "Invalid time" +msgstr "Λάθος ώρα" + +#: colander/__init__.py:1670 +msgid "\"${val}\" is not a time object" +msgstr "\"${val}\" δέν είναι τύπου time" + +#: colander/__init__.py:1795 +msgid "Required" +msgstr "Απαραίτητο" + +#: colander/tests/test_colander.py:295 colander/tests/test_colander.py:302 +msgid "fail ${val}" +msgstr "λάθος ${val}" + +#: colander/tests/test_colander.py:469 +msgid "${val}: ${choices}" +msgstr "${val}: ${choices}" + diff --git a/colander/locale/fr/LC_MESSAGES/colander.mo b/colander/locale/fr/LC_MESSAGES/colander.mo index 164708b..5f0ee9f 100644 Binary files a/colander/locale/fr/LC_MESSAGES/colander.mo and b/colander/locale/fr/LC_MESSAGES/colander.mo differ diff --git a/colander/locale/fr/LC_MESSAGES/colander.po b/colander/locale/fr/LC_MESSAGES/colander.po index e30f1c0..3cba97b 100644 --- a/colander/locale/fr/LC_MESSAGES/colander.po +++ b/colander/locale/fr/LC_MESSAGES/colander.po @@ -8,14 +8,15 @@ msgstr "" "Project-Id-Version: colander 0.8\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2013-05-19 13:15+0200\n" -"PO-Revision-Date: 2011-10-02 21:38+0100\n" -"Last-Translator: Bard Stéphane \n" +"PO-Revision-Date: 2014-06-27 11:46+0100\n" +"Last-Translator: Cédric Messiant \n" "Language-Team: fr \n" -"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 1.3\n" +"X-Generator: Poedit 1.5.4\n" #: colander/__init__.py:233 msgid "Invalid value" @@ -23,11 +24,11 @@ msgstr "Valeur incorrecte" #: colander/__init__.py:270 msgid "String does not match expected pattern" -msgstr "La chaîne de caractères n'a pas vérifié le modèle attendu" +msgstr "La chaîne de caractères ne correspond pas au modèle attendu" #: colander/__init__.py:287 msgid "Invalid email address" -msgstr "Invalide adresse email" +msgstr "Adresse email invalide" #: colander/__init__.py:315 msgid "${val} is less than minimum value ${min}" @@ -39,7 +40,7 @@ msgstr "${val} est plus grand que la valeur maximum autorisée (${max})" #: colander/__init__.py:348 msgid "Shorter than minimum length ${min}" -msgstr "La longueur est inférieur à la taille minimum autorisée (${min})" +msgstr "La longueur est inférieure à la taille minimum autorisée (${min})" #: colander/__init__.py:354 msgid "Longer than maximum length ${max}" @@ -73,11 +74,10 @@ msgstr "\"${val}\" n'est pas itérable" #: colander/__init__.py:664 msgid "" -"\"${val}\" has an incorrect number of elements (expected ${exp}, was " -"${was})" +"\"${val}\" has an incorrect number of elements (expected ${exp}, was ${was})" msgstr "" -"\"${val}\" possède un nombre incorrecte d'éléments (attendu ${exp}, " -"trouvé ${was})" +"\"${val}\" possède un nombre incorrect d'éléments (attendu ${exp}, trouvé " +"${was})" #: colander/__init__.py:803 msgid "${cstruct} is not iterable" @@ -97,11 +97,12 @@ msgstr "\"${val}\" n'est pas un nombre" #: colander/__init__.py:1268 msgid "${val} is not a string" -msgstr "${val} n'est pas une chaîne de carctères" +msgstr "${val} n'est pas une chaîne de caractères" #: colander/__init__.py:1279 msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})" -msgstr "\"${val}\" ne fait partie ni de (${false_choices}) ni de (${true_choices})" +msgstr "" +"\"${val}\" ne fait partie ni de (${false_choices}) ni de (${true_choices})" #: colander/__init__.py:1339 colander/__init__.py:1356 #: colander/__init__.py:1366 @@ -133,16 +134,14 @@ msgid "\"${val}\" is not a date object" msgstr "\"${val}\" n'est pas un objet date" #: colander/__init__.py:1610 -#, fuzzy #| msgid "Invalid date" msgid "Invalid time" -msgstr "Date invalide" +msgstr "Heure invalide" #: colander/__init__.py:1621 -#, fuzzy #| msgid "\"${val}\" is not a datetime object" msgid "\"${val}\" is not a time object" -msgstr "\"${val}\" n'est pas un objet datetime" +msgstr "\"${val}\" n'est pas un objet time" #: colander/__init__.py:1878 colander/__init__.py:1880 msgid "Required" diff --git a/colander/locale/nl/LC_MESSAGES/colander.mo b/colander/locale/nl/LC_MESSAGES/colander.mo index caf830b..7d90605 100644 Binary files a/colander/locale/nl/LC_MESSAGES/colander.mo and b/colander/locale/nl/LC_MESSAGES/colander.mo differ diff --git a/colander/locale/nl/LC_MESSAGES/colander.po b/colander/locale/nl/LC_MESSAGES/colander.po index dc218ca..0152f5c 100644 --- a/colander/locale/nl/LC_MESSAGES/colander.po +++ b/colander/locale/nl/LC_MESSAGES/colander.po @@ -7,141 +7,152 @@ msgid "" msgstr "" "Project-Id-Version: colander 0.8\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2013-05-19 13:15+0200\n" +"POT-Creation-Date: 2013-11-16 14:23+0900\n" "PO-Revision-Date: 2011-06-03 00:22+0200\n" -"Last-Translator: Wichert Akkerman \n" +"Last-Translator: Tinne Cahy \n" "Language-Team: nl \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Generated-By: Babel 1.3\n" -#: colander/__init__.py:233 +#: colander/__init__.py:240 msgid "Invalid value" msgstr "Ongeldige waarde" -#: colander/__init__.py:270 +#: colander/__init__.py:283 msgid "String does not match expected pattern" msgstr "Tekst is niet in het juiste formaat" -#: colander/__init__.py:287 +#: colander/__init__.py:302 msgid "Invalid email address" msgstr "Ongeldig e-mail adres" -#: colander/__init__.py:315 +#: colander/__init__.py:330 msgid "${val} is less than minimum value ${min}" msgstr "${val} is minder dan de minimum waarde ${min}" -#: colander/__init__.py:316 +#: colander/__init__.py:331 msgid "${val} is greater than maximum value ${max}" -msgstr "${val} is groter dan de maximale toegestaane waarde ${max}" +msgstr "${val} is groter dan de maximale toegestane waarde ${max}" -#: colander/__init__.py:348 +#: colander/__init__.py:363 msgid "Shorter than minimum length ${min}" msgstr "De minimale lengte is ${min}" -#: colander/__init__.py:354 +#: colander/__init__.py:369 msgid "Longer than maximum length ${max}" msgstr "De maximale lengte is ${max}" -#: colander/__init__.py:367 +#: colander/__init__.py:382 msgid "\"${val}\" is not one of ${choices}" -msgstr "\"${val}\" is geen toegestaande waarde. Kies één van ${choices}." +msgstr "\"${val}\" is geen toegestane waarde. Kies één van ${choices}." -#: colander/__init__.py:377 +#: colander/__init__.py:392 msgid "One or more of the choices you made was not acceptable" -msgstr "" +msgstr "Één of meer van de keuzes die je maakte zijn niet aanvaardbaar" -#: colander/__init__.py:423 +#: colander/__init__.py:414 colander/__init__.py:419 +#, fuzzy +msgid "\"${val}\" is not a valid credit card number" +msgstr "\"${val}\" is geen geldige kredietkaartnummer" + +#: colander/__init__.py:440 msgid "Must be a URL" -msgstr "" +msgstr "Moet een URL zijn" -#: colander/__init__.py:519 +#: colander/__init__.py:536 msgid "\"${val}\" is not a mapping type: ${err}" -msgstr "\"${val}\" is geen map type: ${err}" +msgstr "\"${val}\" is geen mapping type: ${err}" -#: colander/__init__.py:560 +#: colander/__init__.py:578 msgid "Unrecognized keys in mapping: \"${val}\"" -msgstr "Onbekende waardes in map: \"${val}\"" +msgstr "Onbekende waardes in mapping: \"${val}\"" -#: colander/__init__.py:656 colander/__init__.py:856 +#: colander/__init__.py:674 colander/__init__.py:905 msgid "\"${val}\" is not iterable" msgstr "\"${val}\" is niet itereerbaar" -#: colander/__init__.py:664 +#: colander/__init__.py:682 msgid "" -"\"${val}\" has an incorrect number of elements (expected ${exp}, was " -"${was})" +"\"${val}\" has an incorrect number of elements (expected ${exp}, was ${was})" msgstr "" -"\"${val}\" bevat niet het juiste aantal element (${was} in plaats van " -"${exp})" +"\"${val}\" bevat niet het juiste aantal elementen (${was} in plaats van ${exp})" -#: colander/__init__.py:803 +#: colander/__init__.py:821 colander/__init__.py:852 msgid "${cstruct} is not iterable" msgstr "${cstruct} is niet itereerbaar" -#: colander/__init__.py:1075 +#: colander/__init__.py:1124 msgid "${val} cannot be serialized: ${err}" msgstr "${val} kan niet worden opgeslagen: ${err}" -#: colander/__init__.py:1093 +#: colander/__init__.py:1142 msgid "${val} is not a string: ${err}" msgstr "${val} is geen tekst: ${err}" -#: colander/__init__.py:1113 colander/__init__.py:1124 +#: colander/__init__.py:1162 colander/__init__.py:1173 msgid "\"${val}\" is not a number" msgstr "\"${val}\" is geen getal" -#: colander/__init__.py:1268 +#: colander/__init__.py:1317 msgid "${val} is not a string" msgstr "${val} is geen tekst" -#: colander/__init__.py:1279 +#: colander/__init__.py:1328 msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})" -msgstr "" +msgstr "\"${val}\" is niet aanwezig in (${false_choices}) noch aanwezig in (${true_choices})" -#: colander/__init__.py:1339 colander/__init__.py:1356 -#: colander/__init__.py:1366 +#: colander/__init__.py:1388 colander/__init__.py:1405 +#: colander/__init__.py:1415 msgid "relative name \"${val}\" irresolveable without package" msgstr "relatieve aanduiding \"${val}\" kan niet worden opgezocht zonder package" -#: colander/__init__.py:1396 +#: colander/__init__.py:1445 msgid "\"${val}\" has no __name__" msgstr "\"${val}\" heeft geen __name__" -#: colander/__init__.py:1405 +#: colander/__init__.py:1454 msgid "\"${val}\" is not a string" msgstr "\"${val}\" is geen tekst" -#: colander/__init__.py:1414 +#: colander/__init__.py:1463 msgid "The dotted name \"${name}\" cannot be imported" msgstr "Kan \"${name}\" niet importeren" -#: colander/__init__.py:1462 colander/__init__.py:1538 +#: colander/__init__.py:1511 colander/__init__.py:1587 msgid "Invalid date" msgstr "Geen geldige datum" -#: colander/__init__.py:1478 +#: colander/__init__.py:1527 msgid "\"${val}\" is not a datetime object" msgstr "\"${val}\" is geen datetime object" -#: colander/__init__.py:1549 +#: colander/__init__.py:1598 msgid "\"${val}\" is not a date object" msgstr "\"${val}\" is geen date object" -#: colander/__init__.py:1610 +#: colander/__init__.py:1659 #, fuzzy -#| msgid "Invalid date" msgid "Invalid time" msgstr "Geen geldige datum" -#: colander/__init__.py:1621 +#: colander/__init__.py:1670 #, fuzzy -#| msgid "\"${val}\" is not a datetime object" msgid "\"${val}\" is not a time object" msgstr "\"${val}\" is geen datetime object" -#: colander/__init__.py:1878 colander/__init__.py:1880 +#: colander/__init__.py:1795 msgid "Required" msgstr "Verplicht" + +#: colander/tests/test_colander.py:295 colander/tests/test_colander.py:302 +msgid "fail ${val}" +msgstr "faal ${val}" + +#: colander/tests/test_colander.py:469 +#, fuzzy +msgid "${val}: ${choices}" +msgstr "\"${val}\" is geen toegestane waarde. Kies één van ${choices}." diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 99e4159..6c8cb5a 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -110,9 +110,10 @@ class TestInvalid(unittest.TestCase): exc1.add(exc2, 2) exc2.add(exc3, 3) d = exc1.asdict() - self.assertEqual(d, - {'node1.node2.3': 'exc1; exc2; validator1; validator2', - 'node1.node3': 'exc1; message1'}) + self.assertEqual( + d, + {'node1.node2.3': 'exc1; exc2; validator1; validator2', + 'node1.node3': 'exc1; message1'}) def test_asdict_with_all_validator_functional(self): # see https://github.com/Pylons/colander/issues/2 @@ -153,7 +154,8 @@ class TestInvalid(unittest.TestCase): result = str(exc1) self.assertEqual( result, - "{'node1.node2.3': 'exc1; exc2; exc3', 'node1.node4': 'exc1; exc4'}" + "{'node1.node2.3': 'exc1; exc2; exc3', " + "'node1.node4': 'exc1; exc4'}" ) def test___setitem__fails(self): @@ -425,15 +427,17 @@ class TestEmail(unittest.TestCase): validator = self._makeOne() from colander import Invalid self.assertRaises(Invalid, validator, None, 'me@here.') - self.assertRaises(Invalid, validator, None, 'name@here.tldiswaytoolooooooooong') + self.assertRaises(Invalid, + validator, None, 'name@here.tldiswaytoolooooooooong') self.assertRaises(Invalid, validator, None, '@here.us') self.assertRaises(Invalid, validator, None, 'me@here..com') self.assertRaises(Invalid, validator, None, 'me@we-here-.com') + class TestLength(unittest.TestCase): - def _makeOne(self, min=None, max=None): + def _makeOne(self, **kw): from colander import Length - return Length(min=min, max=max) + return Length(**kw) def test_success_no_bounds(self): validator = self._makeOne() @@ -461,6 +465,17 @@ class TestLength(unittest.TestCase): e = invalid_exc(validator, None, 'ab') self.assertEqual(e.msg.interpolate(), 'Longer than maximum length 1') + def test_min_failure_msg_override(self): + validator = self._makeOne(min=1, min_err='Need at least ${min}, mate') + e = invalid_exc(validator, None, []) + self.assertEqual(e.msg.interpolate(), 'Need at least 1, mate') + + def test_max_failure_msg_override(self): + validator = self._makeOne(max=1, max_err='No more than ${max}, mate') + e = invalid_exc(validator, None, [1, 2]) + self.assertEqual(e.msg.interpolate(), 'No more than 1, mate') + + class TestOneOf(unittest.TestCase): def _makeOne(self, values): from colander import OneOf @@ -475,6 +490,22 @@ class TestOneOf(unittest.TestCase): e = invalid_exc(validator, None, None) self.assertEqual(e.msg.interpolate(), '"None" is not one of 1, 2') + +class TestNoneOf(unittest.TestCase): + def _makeOne(self, values): + from colander import NoneOf + return NoneOf(values) + + def test_success(self): + validator = self._makeOne([1, 2]) + self.assertEqual(validator(None, 3), None) + + def test_failure(self): + validator = self._makeOne([1, 2]) + e = invalid_exc(validator, None, 2) + self.assertEqual(e.msg.interpolate(), '"2" must not be one of 1, 2') + + class TestContainsOnly(unittest.TestCase): def _makeOne(self, values): from colander import ContainsOnly @@ -538,6 +569,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.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): def _makeOne(self, *arg, **kw): from colander import SchemaType @@ -725,7 +807,7 @@ class TestMapping(unittest.TestCase): typ = self._makeOne() result = typ.serialize(node, {'a':drop}) self.assertEqual(result, {}) - + def test_flatten(self): node = DummySchemaNode(None, name='node') int1 = DummyType() @@ -1309,6 +1391,16 @@ class TestSequence(unittest.TestCase): result = typ.flatten(node, [1, 2]) self.assertEqual(result, {'node.0': 1, 'node.1': 2}) + def test_flatten_with_integer(self): + from colander import Integer + node = DummySchemaNode(None, name='node') + node.children = [ + DummySchemaNode(Integer(), name='foo'), + ] + typ = self._makeOne() + result = typ.flatten(node, [1, 2]) + self.assertEqual(result, {'node.0': 1, 'node.1': 2}) + def test_flatten_listitem(self): node = DummySchemaNode(None, name='node') node.children = [ @@ -1481,6 +1573,13 @@ class TestString(unittest.TestCase): e = invalid_exc(typ.serialize, node, not_utf8) self.assertTrue('cannot be serialized' in e.msg) + def test_serialize_encoding_with_non_string_type(self): + utf8 = text_type('123').encode('utf-8') + node = DummySchemaNode(None) + typ = self._makeOne('utf-8') + result = typ.serialize(node, 123) + self.assertEqual(result, utf8) + class TestInteger(unittest.TestCase): def _makeOne(self): from colander import Integer @@ -1499,7 +1598,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) @@ -1592,9 +1691,9 @@ class TestFloat(unittest.TestCase): self.assertEqual(result, '1.0') class TestDecimal(unittest.TestCase): - def _makeOne(self, quant=None, rounding=None): + def _makeOne(self, quant=None, rounding=None, normalize=False): from colander import Decimal - return Decimal(quant, rounding) + return Decimal(quant, rounding, normalize) def test_serialize_null(self): import colander @@ -1626,6 +1725,14 @@ class TestDecimal(unittest.TestCase): result = typ.serialize(node, val) self.assertEqual(result, '0.01') + def test_serialize_normalize(self): + from decimal import Decimal + val = Decimal('1.00') + node = DummySchemaNode(None) + typ = self._makeOne(normalize=True) + result = typ.serialize(node, val) + self.assertEqual(result, '1') + def test_deserialize_fails(self): val = 'P' node = DummySchemaNode(None) @@ -1649,6 +1756,15 @@ class TestDecimal(unittest.TestCase): result = typ.deserialize(node, val) self.assertEqual(result, decimal.Decimal('1.01')) + def test_deserialize_with_normalize(self): + from decimal import Decimal + val = '1.00' + node = DummySchemaNode(None) + typ = self._makeOne(normalize=True) + result = typ.deserialize(node, val) + self.assertEqual(result, Decimal('1')) + self.assertEqual(str(result), '1') + def test_serialize_fails(self): val = 'P' node = DummySchemaNode(None) @@ -2257,6 +2373,15 @@ class TestTime(unittest.TestCase): expected = time.isoformat().split('.')[0] self.assertEqual(result, expected) + def test_serialize_with_zero_time(self): + import datetime + typ = self._makeOne() + time = datetime.time(0) + node = DummySchemaNode(None) + result = typ.serialize(node, time) + expected = time.isoformat().split('.')[0] + self.assertEqual(result, expected) + def test_serialize_with_datetime(self): typ = self._makeOne() dt = self._dt() @@ -2333,8 +2458,9 @@ class TestSchemaNode(unittest.TestCase): def test_ctor_no_title(self): child = DummySchemaNode(None, name='fred') - node = self._makeOne(None, child, validator=1, default=2, name='name_a', - missing='missing') + node = self._makeOne( + None, child, validator=1, default=2, + name='name_a', missing='missing') self.assertEqual(node.typ, None) self.assertEqual(node.children, [child]) self.assertEqual(node.validator, 1) @@ -2453,6 +2579,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 @@ -2474,6 +2613,14 @@ class TestSchemaNode(unittest.TestCase): e = invalid_exc(node.deserialize, null) 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): typ = DummyType() node = self._makeOne(typ) @@ -2717,6 +2864,32 @@ class TestSchemaNodeSubclassing(unittest.TestCase): result = node.deserialize(colander.null) self.assertEqual(result, 10) + def test_subclass_uses_title(self): + import colander + class MyNode(colander.SchemaNode): + schema_type = colander.Int + title = 'some title' + node = MyNode(name='my') + self.assertEqual(node.title, 'some title') + + def test_subclass_title_overwritten_by_constructor(self): + import colander + class MyNode(colander.SchemaNode): + schema_type = colander.Int + title = 'some title' + node = MyNode(name='my', title='other title') + self.assertEqual(node.title, 'other title') + + def test_subelement_title_not_overwritten(self): + import colander + class SampleNode(colander.SchemaNode): + schema_type = colander.String + title = 'Some Title' + class SampleSchema(colander.Schema): + node = SampleNode() + schema = SampleSchema() + self.assertEqual('Some Title', schema.children[0].title) + def test_subclass_value_overridden_by_constructor(self): import colander class MyNode(colander.SchemaNode): @@ -3436,7 +3609,8 @@ class TestFunctional(object): def test_invalid_asdict(self): expected = { 'schema.int': '20 is greater than maximum value 10', - 'schema.ob': 'The dotted name "no.way.this.exists" cannot be imported', + 'schema.ob': 'The dotted name "no.way.this.exists" ' + 'cannot be imported', 'schema.seq.0.0': '"q" is not a number', 'schema.seq.1.0': '"w" is not a number', 'schema.seq.2.0': '"e" is not a number', @@ -3458,6 +3632,39 @@ class TestFunctional(object): errors = e.asdict() self.assertEqual(errors, expected) + def test_invalid_asdict_translation_callback(self): + from translationstring import TranslationString + + expected = { + 'schema.int': 'translated', + 'schema.ob': 'translated', + 'schema.seq.0.0': 'translated', + 'schema.seq.1.0': 'translated', + 'schema.seq.2.0': 'translated', + 'schema.seq.3.0': 'translated', + 'schema.seq2.0.key': 'translated', + 'schema.seq2.0.key2': 'translated', + 'schema.seq2.1.key': 'translated', + 'schema.seq2.1.key2': 'translated', + 'schema.tup.0': 'translated', + } + data = { + 'int': '20', + 'ob': 'no.way.this.exists', + 'seq': [('q', 's'), ('w', 's'), ('e', 's'), ('r', 's')], + 'seq2': [{'key': 't', 'key2': 'y'}, {'key':'u', 'key2':'i'}], + 'tup': ('s', 's'), + } + schema = self._makeSchema() + e = invalid_exc(schema.deserialize, data) + + def translation_function(string): + return TranslationString('translated') + + errors = e.asdict(translate=translation_function) + self.assertEqual(errors, expected) + + class TestImperative(unittest.TestCase, TestFunctional): def _makeSchema(self, name='schema'): @@ -3598,7 +3805,7 @@ class TestUltraDeclarative(unittest.TestCase, TestFunctional): return schema class TestDeclarativeWithInstantiate(unittest.TestCase, TestFunctional): - + def _makeSchema(self, name='schema'): import colander @@ -3612,20 +3819,20 @@ class TestDeclarativeWithInstantiate(unittest.TestCase, TestFunctional): 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()) @@ -3647,6 +3854,16 @@ class Test_null(unittest.TestCase): import pickle self.assertTrue(pickle.loads(pickle.dumps(null)) is null) +class Test_required(unittest.TestCase): + def test___repr__(self): + from colander import required + self.assertEqual(repr(required), '') + +class Test_drop(unittest.TestCase): + def test___repr__(self): + from colander import drop + self.assertEqual(repr(drop), '') + class Dummy(object): pass diff --git a/colander/tests/test_interfaces.py b/colander/tests/test_interfaces.py new file mode 100644 index 0000000..2cca492 --- /dev/null +++ b/colander/tests/test_interfaces.py @@ -0,0 +1,2 @@ +def test_interfaces(): + from colander import interfaces diff --git a/colander/tests/test_iso8601.py b/colander/tests/test_iso8601.py index c579eee..bd73ae2 100644 --- a/colander/tests/test_iso8601.py +++ b/colander/tests/test_iso8601.py @@ -68,12 +68,17 @@ class Test_FixedOffset(unittest.TestCase): def test___repr__(self): inst = self._makeOne() result = inst.__repr__() - self.assertEqual(result, "") + self.assertEqual(result, "") class Test_parse_timezone(unittest.TestCase): def _callFUT(self, tzstring, **kw): - from ..iso8601 import parse_timezone - return parse_timezone(tzstring, **kw) + # mimic old parse_timezone() by returning a FixedOffset + from datetime import tzinfo + from ..iso8601 import (parse_date, FixedOffset) + if tzstring is None: + tzstring = '' + dt = parse_date("2006-10-11T00:14:33{0}".format(tzstring), **kw) + return dt.tzinfo def test_default_Z(self): from ..iso8601 import UTC diff --git a/docs/.static/logo_hi.gif b/docs/.static/logo_hi.gif deleted file mode 100644 index 178e49f..0000000 Binary files a/docs/.static/logo_hi.gif and /dev/null differ diff --git a/docs/.static/repoze.css b/docs/.static/repoze.css deleted file mode 100644 index 1fbae93..0000000 --- a/docs/.static/repoze.css +++ /dev/null @@ -1,33 +0,0 @@ -@import url('default.css'); -body { - background-color: #006339; -} - -div.document { - background-color: #dad3bd; -} - -div.sphinxsidebar h3, h4, h5, a { - color: #127c56 !important; -} - -div.related { - color: #dad3bd !important; - background-color: #00744a; -} - -div.related a { - color: #dad3bd !important; -} - -/* override the justify text align of the default */ - -div.body p { - text-align: left !important; -} - -/* fix google chrome
 tag renderings */
-
-pre {
-   line-height: normal !important;
-}
diff --git a/docs/api.rst b/docs/api.rst
index 8f972bd..3ecc9cc 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
 ~~~~~~~~~~
 
@@ -61,6 +64,8 @@ Validators
 
   .. autoclass:: OneOf
 
+  .. autoclass:: NoneOf
+
   .. autoclass:: ContainsOnly
 
   .. autoclass:: Function
@@ -75,6 +80,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
 ~~~~~
 
@@ -139,16 +149,12 @@ Schema-Related
 
   .. autoclass:: instantiate
 
-  .. attribute:: null
+  .. autodata:: null
+     :annotation:
 
-     Represents a null value in colander-related operations.
+  .. autodata:: required
+     :annotation:
 
-  .. attribute:: required
+  .. autodata:: drop
+     :annotation:
 
-     Represents a required value in colander-related operations.
-
-  .. attribute:: drop
-
-     Represents a value that will be dropped from the schema if it is missing
-     during *deserialization*.  Passed as a value to the `missing` keyword
-     argument of :class:`SchemaNode`.
diff --git a/docs/basics.rst b/docs/basics.rst
index 83c862c..4e8bbcf 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -171,7 +171,7 @@ The imperative style that looks like this still works, of course:
 .. code-block:: python
 
      ranged_int = colander.SchemaNode(
-         typ=colander.Int(),
+         schema_type=colander.Int,
          validator=colander.Range(0, 10),
          default=10,
          title='Ranged Int'
@@ -182,7 +182,7 @@ But in 1.0a1+, you can alternately now do something like this:
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          validator = colander.Range(0, 10)
          default = 10
          title = 'Ranged Int'
@@ -195,7 +195,7 @@ the schemanode subclass instead of plain attributes:
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          default = 10
          title = 'Ranged Int'
 
@@ -220,7 +220,7 @@ example this will *not* work:
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          default = 10
          title = 'Ranged Int'
 
@@ -247,7 +247,7 @@ indeed work):
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          default = 10
          title = 'Ranged Int'
 
@@ -271,7 +271,7 @@ the bind parameters within values that are plain old methods:
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          default = 10
          title = 'Ranged Int'
 
@@ -292,7 +292,7 @@ attributes of the schemanode that rely on binding variables:
 .. code-block:: python
 
      class UserIdSchemaNode(colander.SchemaNode):
-         typ = colander.String()
+         schema_type = colander.String
          title = 'User Id'
 
          def after_bind(self, node, kw):
@@ -304,7 +304,7 @@ constructor:
 .. code-block:: python
 
      class RangedInt(colander.SchemaNode):
-         typ = colander.Int()
+         schema_type = colander.Int
          default = 10
          title = 'Ranged Int'
          validator = colander.Range(0, 10)
@@ -395,7 +395,7 @@ Earlier we defined a schema:
 Let's now use this schema to try to deserialize some concrete data
 structures.
 
-Each of thse concrete data structures is called a :term:`cstruct`.
+Each of these concrete data structures is called a :term:`cstruct`.
 "cstruct" is an abbreviation of "colander structure": you can think of
 a cstruct as a serialized representation of some application data.  A
 "cstruct" is usually generated by the
@@ -999,7 +999,7 @@ schema, then the schema definition can be made more succinct using the
 
            @colander.instantiate()
            class friend(colander.TupleSchema):
-               rank = colander.SchemaNode(colander.Int(), 
+               rank = colander.SchemaNode(colander.Int(),
                                           validator=colander.Range(0, 9999))
                name = colander.SchemaNode(colander.String())
 
@@ -1008,7 +1008,7 @@ schema, then the schema definition can be made more succinct using the
 
            @colander.instantiate()
            class phone(colander.MappingSchema):
-               location = colander.SchemaNode(colander.String(), 
+               location = colander.SchemaNode(colander.String(),
                                               validator=colander.OneOf(['home', 'work']))
                number = colander.SchemaNode(colander.String())
 
@@ -1076,22 +1076,22 @@ We can imperatively construct a completely equivalent schema like so:
 
    import colander
 
-   friend = colander.SchemaNode(Tuple())
+   friend = colander.SchemaNode(colander.Tuple())
    friend.add(colander.SchemaNode(colander.Int(),
                                  validator=colander.Range(0, 9999),
               name='rank'))
-   friend.add(colander.SchemaNode(colander.String(), name='name')
+   friend.add(colander.SchemaNode(colander.String(), name='name'))
 
-   phone = colander.SchemaNode(Mapping())
+   phone = colander.SchemaNode(colander.Mapping())
    phone.add(colander.SchemaNode(colander.String(),
                                 validator=colander.OneOf(['home', 'work']),
                                 name='location'))
    phone.add(colander.SchemaNode(colander.String(), name='number'))
 
-   schema = colander.SchemaNode(Mapping())
+   schema = colander.SchemaNode(colander.Mapping())
    schema.add(colander.SchemaNode(colander.String(), name='name'))
-   schema.add(colander.SchemaNode(colander.Int(), name='age'),
-                                 validator=colander.Range(0, 200))
+   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'))
 
@@ -1129,7 +1129,7 @@ For example, in a Python module, you might have code that looks like this:
 
 .. code-block:: python
 
-   from colander import MappingSchema
+   from colander import SchemaNode, MappingSchema
    from colander import Int
 
    class MySchema1(MappingSchema):
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*.
 
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
diff --git a/docs/conf.py b/docs/conf.py
index d53d0a1..8b1991e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,7 +12,9 @@
 # All configuration values have a default value; values that are commented
 # out serve to show the default value.
 
-import sys, os
+import sys, os, datetime
+import pkg_resources
+import pylons_sphinx_themes
 
 # General configuration
 # ---------------------
@@ -32,13 +34,14 @@ master_doc = 'index'
 
 # General substitutions.
 project = 'colander'
-copyright = '2012, Agendaless Consulting '
+thisyear = datetime.datetime.now().year
+copyright = '2012-%s, Agendaless Consulting ' % thisyear
 
 # The default replacements for |version| and |release|, also used in various
 # other places throughout the built documents.
 #
 # The short X.Y version.
-version = '1.0b1'
+version = pkg_resources.get_distribution('colander').version
 # The full version, including alpha/beta/rc tags.
 release = version
 
@@ -49,7 +52,7 @@ release = version
 today_fmt = '%B %d, %Y'
 
 # List of documents that shouldn't be included in the build.
-unused_docs = ['_themes/README']
+#unused_docs = ['_themes/README']
 
 # List of directories, relative to source directories, that shouldn't be
 # searched for source files.
@@ -73,43 +76,11 @@ unused_docs = ['_themes/README']
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = 'sphinx'
 
-# Add and use Pylons theme
-if 'sphinx-build' in ' '.join(sys.argv): # protect against dumb importers
-    from subprocess import call, Popen, PIPE
-
-    p = Popen('which git', shell=True, stdout=PIPE)
-    git = p.stdout.read().strip()
-    cwd = os.getcwd()
-    _themes = os.path.join(cwd, '_themes')
-
-    if not os.path.isdir(_themes):
-        call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git',
-                '_themes'])
-    else:
-        os.chdir(_themes)
-        call([git, 'checkout', 'master'])
-        call([git, 'pull'])
-        os.chdir(cwd)
-
-    sys.path.append(os.path.abspath('_themes'))
-
-    parent = os.path.dirname(os.path.dirname(__file__))
-    sys.path.append(os.path.abspath(parent))
-    wd = os.getcwd()
-    os.chdir(parent)
-    os.system('%s setup.py test -q' % sys.executable)
-    os.chdir(wd)
-
-    for item in os.listdir(parent):
-        if item.endswith('.egg'):
-            sys.path.append(os.path.join(parent, item))
-
 # Options for HTML output
 # -----------------------
-
-sys.path.append(os.path.abspath('_themes'))
-html_theme_path = ['_themes']
-html_theme = 'pylons'
+# sys.path.append(os.path.abspath('_themes'))
+html_theme = 'pyramid'
+html_theme_path = pylons_sphinx_themes.get_html_themes_path()
 html_theme_options = dict(github_url='https://github.com/Pylons/colander')
 
 # The style sheet to use for HTML and HTML Help pages. A file of that name
@@ -146,7 +117,7 @@ html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+html_use_smartypants = False
 
 # Custom sidebar templates, maps document names to template names.
 #html_sidebars = {}
@@ -177,7 +148,7 @@ html_last_updated_fmt = '%b %d, %Y'
 #html_file_suffix = ''
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'atemplatedoc'
+htmlhelp_basename = 'colanderdoc'
 
 
 # Options for LaTeX output
@@ -193,13 +164,13 @@ htmlhelp_basename = 'atemplatedoc'
 # (source start file, target name, title,
 #  author, document class [howto/manual]).
 latex_documents = [
-  ('index', 'atemplate.tex', 'colander Documentation',
+  ('index', 'colander.tex', 'colander Documentation',
    'Pylons Developers', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the
 # top of the title page.
-latex_logo = '.static/logo_hi.gif'
+#latex_logo = '.static/logo_hi.gif'
 
 # For "manual" documents, if this is true, then toplevel headings are
 # parts, not chapters.
diff --git a/docs/index.rst b/docs/index.rst
index 1ac8051..d899227 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -6,7 +6,8 @@ Colander
 
 Colander is useful as a system for validating and deserializing data obtained
 via XML, JSON, an HTML form post or any other equally simple data
-serialization.  It runs on Python 2.6, 2.7 and 3.2.  Colander can be used to:
+serialization.  It runs on Python 2.6, 2.7, 3.2, 3.3, 3.4, pypy and pypy3.
+Colander can be used to:
 
 - Define a data schema.
 
diff --git a/rtd.txt b/rtd.txt
new file mode 100644
index 0000000..142b6ca
--- /dev/null
+++ b/rtd.txt
@@ -0,0 +1 @@
+-e .[docs]
diff --git a/setup.cfg b/setup.cfg
index 50ce659..1a97358 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,6 @@
+[wheel]
+universal = 1
+
 [easy_install]
 zip_ok = false
 
diff --git a/setup.py b/setup.py
index 9a6c66c..0e19fe4 100644
--- a/setup.py
+++ b/setup.py
@@ -24,31 +24,37 @@ def read(fname):
         return fp.read()
 
 try:
-    README = read(os.path.join(here, 'README.txt'))
-    CHANGES = read(os.path.join(here, 'CHANGES.txt'))
+    README = read(os.path.join(here, 'README.rst'))
+    CHANGES = read(os.path.join(here, 'CHANGES.rst'))
 except:
     README = ''
     CHANGES = ''
 
-requires = ['translationstring']
+requires = ['translationstring', 'iso8601']
 
 testing_extras = ['nose', 'coverage']
-docs_extras = ['Sphinx']
+docs_extras = [
+    'Sphinx >= 1.3.1',
+    'docutils',
+    'pylons-sphinx-themes',
+]
 
 setup(name='colander',
-      version='1.0b1',
+      version='1.1',
       description=('A simple schema-based serialization and deserialization '
                    'library'),
       long_description=README + '\n\n' +  CHANGES,
       classifiers=[
         "Intended Audience :: Developers",
         "Programming Language :: Python",
-        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
         "Programming Language :: Python :: 2.6",
         "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.2",
         "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
         ],
diff --git a/tox.ini b/tox.ini
index 69295a2..8508bb7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,25 +1,62 @@
 [tox]
 envlist =
-    py26,py27,py32,py33,pypy,cover,docs
+    py26,py27,py32,py33,py34,py35,pypy,pypy3,
+    docs,
+    {py2,py3}-cover,coverage
 
 [testenv]
-commands =
-    python setup.py dev
-    python -Wd setup.py test -q
-
-[testenv:cover]
+# Most of these are defaults but if you specify any you can't fall back
+# to defaults for others.
 basepython =
-    python2.6
+    py26: python2.6
+    py27: python2.7
+    py32: python3.2
+    py33: python3.3
+    py34: python3.4
+    py35: python3.5
+    pypy: pypy
+    pypy3: pypy3
+    py2: python2.7
+    py3: python3.5
+
 commands =
-    python setup.py nosetests --with-xunit --with-xcoverage
-deps =
-    nosexcover
+    pip install colander[testing]
+    nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
 
 [testenv:docs]
-basepython =
-    python2.6
-commands = 
-    sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
-#    sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
+basepython = python3.5
+whitelist_externals = make
+commands =
+    pip install colander[docs]
+    make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E"
+
+# we separate coverage into its own testenv because a) "last run wins" wrt
+# cobertura jenkins reporting and b) pypy and jython can't handle any
+# combination of versions of coverage and nosexcover that i can find.
+[testenv:py2-cover]
+commands =
+    pip install colander[testing]
+    coverage run --source=colander {envbindir}/nosetests
+    coverage xml -o coverage-py2.xml
+setenv =
+    COVERAGE_FILE=.coverage.py2
+
+[testenv:py3-cover]
+commands =
+    pip install colander[testing]
+    coverage run --source=colander {envbindir}/nosetests
+    coverage xml -o coverage-py3.xml
+setenv =
+    COVERAGE_FILE=.coverage.py3
+
+[testenv:coverage]
+basepython = python3.5
+commands =
+    coverage erase
+    coverage combine
+    coverage xml
+    coverage report --show-missing --fail-under=100
 deps =
-    Sphinx
+    coverage
+setenv =
+    COVERAGE_FILE=.coverage