Source code for biryani.baseconv

# -*- coding: utf-8 -*-


# Biryani -- A conversion and validation toolbox
# By: Emmanuel Raviart <emmanuel@raviart.com>
#
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Emmanuel Raviart
# http://packages.python.org/Biryani/
#
# This file is part of Biryani.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Base Conversion Functions

.. note:: Most converters do only one operation and can fail when given wrong data. To ensure that they don't fail, they
   must be combined with other converters.

.. note:: Most converters work on unicode strings. To use them you must first convert your strings to unicode. By using
   converter :func:`decode_str`, for example.
"""


import re

from . import states, strings


__all__ = [
    'anything_to_bool',
    'anything_to_float',
    'anything_to_int',
    'anything_to_str',
    'bool_to_str',
    'catch_error',
    'check',
    'cleanup_line',
    'cleanup_text',
    'condition',
    'decode_str',
    'default',
    'empty_to_none',
    'encode_str',
    'extract_when_singleton',
    'fail',
    'first_match',
    'function',
    'get',
    'guess_bool',
    'input_to_bool',
    'input_to_email',
    'input_to_float',
    'input_to_int',
    'input_to_slug',
    'input_to_url_name',
    'input_to_url_path_and_query',
    'item_or_sequence',
    'make_item_to_singleton',
    'make_input_to_normal_form',
    'make_input_to_slug',
    'make_input_to_url',
    'make_input_to_url_name',
    'make_str_to_url',
    'merge',
    'new_mapping',
    'new_sequence',
    'new_struct',
    'noop',
    'not_none',
    'ok',
    'pipe',
    'rename_item',
    'set_value',
    'str_to_bool',
    'str_to_email',
    'str_to_url_path_and_query',
    'struct',
    'structured_mapping',
    'structured_sequence',
    'submapping',
    'switch',
    'test',
    'test_between',
    'test_conv',
    'test_equals',
    'test_greater_or_equal',
    'test_in',
    'test_is',
    'test_isinstance',
    'test_issubclass',
    'test_less_or_equal',
    'test_none',
    'test_not_in',
    'test_not_none',
    'translate',
    'uniform_mapping',
    'uniform_sequence',
    ]

domain_re = re.compile(r'''
    (?:[a-z0-9][a-z0-9\-]{0,62}\.)+ # (sub)domain - alpha followed by 62max chars (63 total)
    [a-z]{2,}$                      # TLD
    ''', re.I | re.VERBOSE)
N_ = lambda message: message
username_re = re.compile(r"[^ \t\n\r@<>()]+$", re.I)


# Level-1 Converters


[docs]def anything_to_float(value, state = None): """Convert any python data to a float. .. warning:: Like most converters, a ``None`` value is not converted. >>> anything_to_float(42) (42.0, None) >>> anything_to_float('42') (42.0, None) >>> anything_to_float(u'42') (42.0, None) >>> anything_to_float(42.75) (42.75, None) >>> anything_to_float(None) (None, None) """ if value is None: return value, None if state is None: state = states.default_state try: return float(value), None except ValueError: return value, state._(u'Value must be a float')
[docs]def anything_to_int(value, state = None): """Convert any python data to an integer. .. warning:: Like most converters, a ``None`` value is not converted. >>> anything_to_int(42) (42, None) >>> anything_to_int('42') (42, None) >>> anything_to_int(u'42') (42, None) >>> anything_to_int(42.75) (42, None) >>> anything_to_int(u'42.75') (42, None) >>> anything_to_int(u'42,75') (u'42,75', u'Value must be an integer') >>> anything_to_int(None) (None, None) """ if value is None: return value, None if state is None: state = states.default_state try: return int(value), None except ValueError: try: return int(float(value)), None except ValueError: return value, state._(u'Value must be an integer')
[docs]def anything_to_str(value, state = None): """Convert any Python data to unicode. .. warning:: Like most converters, a ``None`` value is not converted. >>> anything_to_str(42) (u'42', None) >>> anything_to_str('42') (u'42', None) >>> anything_to_str(None) (None, None) """ if value is None: return value, None if state is None: state = states.default_state if isinstance(value, str): return value.decode('utf-8'), None try: return unicode(value), None except UnicodeDecodeError: return str(value).decode('utf-8'), None
[docs]def bool_to_str(value, state = None): """Convert a boolean to a "0" or "1" string. .. warning:: Like most converters, a ``None`` value is not converted. When you want ``None`` to be converted to ``"0"``, use:: pipe(bool_to_str, default(u'0')) >>> bool_to_str(False) (u'0', None) >>> bool_to_str(True) (u'1', None) >>> bool_to_str(0) (u'0', None) >>> bool_to_str('') (u'0', None) >>> bool_to_str('any non-empty string') (u'1', None) >>> bool_to_str('0') (u'1', None) >>> bool_to_str(None) (None, None) >>> pipe(default(False), bool_to_str)(None) (u'0', None) """ if value is None: return value, None return unicode(int(bool(value))), None
[docs]def catch_error(converter, error_value = None): """Make a converter that calls another converter and ignore its errors. >>> catch_error(noop)(42) (42, None) >>> catch_error(fail())(42) (None, None) >>> catch_error(noop, error_value = 0)(42) (42, None) >>> catch_error(fail(), error_value = 0)(42) (0, None) """ def catch_error_converter(value, state = None): if state is None: state = states.default_state result, error = converter(value, state = state) if error is not None: return error_value, None return result, None return catch_error_converter
[docs]def condition(test_converter, ok_converter, error_converter = None): """When *test_converter* succeeds (ie no error), then apply *ok_converter*, otherwise apply *error_converter*. .. note:: See also :func:`first_match`. >>> detect_unknown_values = condition( ... test_in(['?', 'x']), ... set_value(False), ... set_value(True), ... ) >>> detect_unknown_values(u'Hello world!') (True, None) >>> detect_unknown_values(u'?') (False, None) """ def condition_converter(value, state = None): if state is None: state = states.default_state test, error = test_converter(value, state = state) if error is None: return ok_converter(value, state = state) elif error_converter is None: return value, None else: return error_converter(value, state = state) return condition_converter
[docs]def decode_str(encoding = 'utf-8'): """Return a string to unicode converter that uses given *encoding*. >>> decode_str()(' Hello world! ') (u' Hello world! ', None) >>> decode_str()(u' Hello world! ') (u' Hello world! ', None) >>> decode_str()(42) (42, None) >>> decode_str()(None) (None, None) """ return function(lambda value: value.decode(encoding) if isinstance(value, str) else value)
[docs]def default(constant): """Return a converter that replace a ``None`` value by given one. .. note:: See converter :func:`set_value` to replace a non-``None`` value. >>> default(42)(None) (42, None) >>> default(42)(u'1234') (u'1234', None) >>> pipe(input_to_int, default(42))(u'1234') (1234, None) >>> pipe(input_to_int, default(42))(u' ') (42, None) >>> pipe(input_to_int, default(42))(None) (42, None) """ return lambda value, state = None: (constant, None) if value is None else (value, None)
[docs]def empty_to_none(value, state = None): """When value is comparable to False (ie None, 0 , '', etc) replace it with None else keep it as is. >>> empty_to_none(0) (None, None) >>> empty_to_none('') (None, None) >>> empty_to_none([]) (None, None) >>> empty_to_none([42, 43]) ([42, 43], None) >>> empty_to_none({}) (None, None) >>> empty_to_none({'answer': 42}) ({'answer': 42}, None) >>> empty_to_none(u'hello world') (u'hello world', None) >>> empty_to_none(u' hello world ') (u' hello world ', None) """ return value if value else None, None
[docs]def encode_str(encoding = 'utf-8'): """Return a unicode to string converter that uses given *encoding*. >>> encode_str()(u' Hello world! ') (' Hello world! ', None) >>> encode_str()(' Hello world! ') (' Hello world! ', None) >>> encode_str()(42) (42, None) >>> encode_str()(None) (None, None) """ return function(lambda value: value.encode(encoding) if isinstance(value, unicode) else value)
[docs]def fail(error = N_(u'An error occured')): """Return a converter that always returns an error. >>> fail(u'Wrong answer')(42) (42, u'Wrong answer') >>> fail()(42) (42, u'An error occured') >>> fail()(None) (None, u'An error occured') """ def fail_converter(value, state = None): if state is None: state = states.default_state return value, state._(error) if isinstance(error, basestring) else error return fail_converter
[docs]def first_match(*converters): """Try each converter successively until one succeeds. When every converter fail, return the result of the last one. >>> first_match(test_equals(u'NaN'), input_to_int)(u'NaN') (u'NaN', None) >>> first_match(test_equals(u'NaN'), input_to_int)(u'42') (42, None) >>> first_match(test_equals(u'NaN'), input_to_int)(u'abc') (u'abc', u'Value must be an integer') >>> first_match(test_equals(u'NaN'), input_to_int, set_value(0))(u'Hello world!') (0, None) >>> first_match()(u'Hello world!') (u'Hello world!', None) """ def first_match_converter(value, state = None): if state is None: state = states.default_state converted_value = value error = None for converter in converters: converted_value, error = converter(value, state = state) if error is None: return converted_value, error return converted_value, error return first_match_converter
[docs]def function(function, handle_none_value = False, handle_state = False): """Return a converter that applies a function to value and returns a new value. .. note:: Like most converters, by default a ``None`` value is not converted (ie function is not called). Set ``handle_none_value`` to ``True`` to call function when value is ``None``. .. note:: When your function doesn't modify value but may generate an error, use a :func:`test` instead. .. note:: When your function modifies value and may generate an error, write a full converter instead of a function. See :doc:`how-to-create-converter` for more information. >>> function(int)('42') (42, None) >>> function(sorted)([3, 2, 1]) ([1, 2, 3], None) >>> function(lambda value: value + 1)(42) (43, None) >>> function(lambda value: value + 1)(None) (None, None) >>> function(lambda value: value + 1)(u'hello world') Traceback (most recent call last): TypeError: >>> function(lambda value: value + 1, handle_none_value = True)(None) Traceback (most recent call last): TypeError: """ def function_converter(value, state = None): if value is None and not handle_none_value or function is None: return value, None if state is None: state = states.default_state if handle_state: return function(value, state = state), None return function(value), None return function_converter
[docs]def get(key, default = UnboundLocalError, error = None): """Return a converter that returns an item of a collection. Collection can either be a mapping (ie dict, etc) or a sequence (ie list, tuple, string, etc). Usage with a mapping: >>> get('a')(dict(a = 1, b = 2)) (1, None) >>> get('c')(dict(a = 1, b = 2)) (None, u'Unknown key: c') >>> get('c', default = None)(dict(a = 1, b = 2)) (None, None) >>> get('c', error = u'Key Error')(dict(a = 1, b = 2)) (None, u'Key Error') >>> get(u'a')(None) (None, None) Usage with a sequence: >>> get(0)(u'ab') (u'a', None) >>> get(-2)([u'a', u'b']) (u'a', None) >>> get(-3)(u'ab') (None, u'Index out of range: -3') >>> get(2, error = u"Index Error')([u'a', u'b']) (None, u"Index Error') >>> get(-3, default = None)(u'ab') (None, None) >>> get(0)(None) (None, None) """ def get_converter(value, state = None): import collections if value is None: return value, None if state is None: state = states.default_state if isinstance(value, collections.Mapping): converted_value = value.get(key, default) if converted_value is UnboundLocalError: return None, state._(u'Unknown key: {0}').format(key) \ if error is None \ else state._(error) if isinstance(error, basestring) else error return converted_value, None assert isinstance(value, collections.Sequence), \ 'Value must be a mapping or a sequence. Got {0} instead.'.format(type(value)) if 0 <= key < len(value): return value[key], None if default is UnboundLocalError: return None, state._(u'Index out of range: {0}').format(key) \ if error is None \ else state._(error) if isinstance(error, basestring) else error return default, None return get_converter
[docs]def guess_bool(value, state = None): """Convert the content of a string (or a number) to a boolean. Do nothing when input value is already a boolean. This converter accepts usual values for ``True`` and ``False``: "0", "f", "false", "n", etc. .. warning:: Like most converters, a ``None`` value is not converted. When you want ``None`` to be converted to ``False``, use:: pipe(guess_bool, default(False)) >>> guess_bool(u'0') (False, None) >>> guess_bool(u'f') (False, None) >>> guess_bool(u'FALSE') (False, None) >>> guess_bool(u'false') (False, None) >>> guess_bool(u'n') (False, None) >>> guess_bool(u'no') (False, None) >>> guess_bool(u'off') (False, None) >>> guess_bool(u' 0 ') (False, None) >>> guess_bool(u' f ') (False, None) >>> guess_bool(False) (False, None) >>> guess_bool(0) (False, None) >>> guess_bool(u'1') (True, None) >>> guess_bool(u'on') (True, None) >>> guess_bool(u't') (True, None) >>> guess_bool(u'TRUE') (True, None) >>> guess_bool(u'true') (True, None) >>> guess_bool(u'y') (True, None) >>> guess_bool(u'yes') (True, None) >>> guess_bool(u' 1 ') (True, None) >>> guess_bool(u' tRuE ') (True, None) >>> guess_bool(True) (True, None) >>> guess_bool(1) (True, None) >>> guess_bool(2) (True, None) >>> guess_bool(-1) (True, None) >>> guess_bool(u'') (None, None) >>> guess_bool(u' ') (None, None) >>> guess_bool(None) (None, None) >>> guess_bool(u'vrai') (u'vrai', u'Value must be a boolean') """ if value is None: return value, None if state is None: state = states.default_state try: return bool(int(value)), None except ValueError: lower_value = value.strip().lower() if not lower_value: return None, None if lower_value in (u'f', u'false', u'n', u'no', u'off'): return False, None if lower_value in (u'on', u't', u'true', u'y', u'yes'): return True, None return value, state._(u'Value must be a boolean')
[docs]def item_or_sequence(converter, constructor = list, drop_none_items = False): """Return a converter that accepts either an item or a sequence of items and applies a converter to them. >>> item_or_sequence(input_to_int)(u'42') (42, None) >>> item_or_sequence(input_to_int)([u'42']) (42, None) >>> item_or_sequence(input_to_int)([u'42', u'43']) ([42, 43], None) >>> item_or_sequence(input_to_int)([u'42', u'43', u'Hello world!']) ([42, 43, u'Hello world!'], {2: u'Value must be an integer'}) >>> item_or_sequence(input_to_int)([None, None]) ([None, None], None) >>> item_or_sequence(input_to_int, drop_none_items = True)([None, None]) ([], None) >>> item_or_sequence(input_to_int)([u'42', None, u'43']) ([42, None, 43], None) >>> item_or_sequence(input_to_int, drop_none_items = True)([u'42', None, u'43']) ([42, 43], None) >>> item_or_sequence(input_to_int)([u'42', u'43', u'Hello world!']) ([42, 43, u'Hello world!'], {2: u'Value must be an integer'}) >>> item_or_sequence(input_to_int, constructor = set)(set([u'42', u'43'])) (set([42, 43]), None) """ return condition( test_isinstance(constructor), pipe( uniform_sequence(converter, constructor = constructor, drop_none_items = drop_none_items), extract_when_singleton, ), converter, )
[docs]def make_str_to_url(add_prefix = None, error_if_fragment = False, error_if_path = False, error_if_query = False, full = False, remove_fragment = False, remove_path = False, remove_query = False, schemes = (u'http', u'https')): """Return a converter that converts a clean string to an URL. .. note:: For a converter that doesn't require a clean string, see :func:`make_input_to_url`. >>> make_str_to_url()(u'http://packages.python.org/Biryani/') (u'http://packages.python.org/Biryani/', None) >>> make_str_to_url()(u'packages.python.org/Biryani/') (u'packages.python.org/Biryani/', None) >>> make_str_to_url(full = True)(u'packages.python.org/Biryani/') (u'packages.python.org/Biryani/', u'URL must be complete') >>> make_str_to_url(add_prefix = u'http://', full = True)(u'packages.python.org/Biryani/') (u'http://packages.python.org/Biryani/', None) >>> make_str_to_url()(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html#tutorial', None) >>> make_str_to_url(full = True)(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html#tutorial', u'URL must be complete') >>> make_str_to_url(remove_path = True)(u'http://packages.python.org/Biryani/presentation.html') (u'http://packages.python.org/', None) >>> make_str_to_url(error_if_path = True)(u'http://packages.python.org/Biryani/presentation.html') (u'http://packages.python.org/Biryani/presentation.html', u'URL must not contain a path') >>> make_str_to_url(remove_query = True)(u'http://packages.python.org/Biryani/presentation.html?tuto=1') (u'http://packages.python.org/Biryani/presentation.html', None) >>> make_str_to_url(error_if_query = True)(u'http://packages.python.org/Biryani/presentation.html?tuto=1') (u'http://packages.python.org/Biryani/presentation.html?tuto=1', u'URL must not contain a query') >>> make_str_to_url(remove_fragment = True)(u'http://packages.python.org/Biryani/presentation.html#tutorial') (u'http://packages.python.org/Biryani/presentation.html', None) >>> make_str_to_url(error_if_fragment = True)(u'http://packages.python.org/Biryani/presentation.html#tuto') (u'http://packages.python.org/Biryani/presentation.html#tuto', u'URL must not contain a fragment') >>> make_str_to_url(full = True)(u'[www.nordnet.fr/grandmix/]') (u'[www.nordnet.fr/grandmix/]', u'URL must be complete') >>> make_str_to_url(full = True)(u'http://[www.nordnet.fr/grandmix/]') (u'http://[www.nordnet.fr/grandmix/]', u'Invalid URL') """ def str_to_url(value, state = None): if value is None: return value, None import urlparse if state is None: state = states.default_state try: split_url = list(urlparse.urlsplit(value)) except ValueError: return value, state._(u'Invalid URL') if full and add_prefix and not split_url[0] and not split_url[1] and split_url[2] \ and not split_url[2].startswith(u'/'): try: split_url = list(urlparse.urlsplit(unicode(add_prefix) + value)) except ValueError: return value, state._(u'Invalid URL') scheme = split_url[0] if scheme != scheme.lower(): split_url[0] = scheme = scheme.lower() if full and not scheme: return value, state._(u'URL must be complete') if scheme and schemes is not None and scheme not in schemes: return value, state._(u'Scheme must belong to {0}').format(sorted(schemes)) network_location = split_url[1] if network_location != network_location.lower(): split_url[1] = network_location = network_location.lower() if split_url[2] and split_url[2] != u'/': if error_if_path: return value, state._(u'URL must not contain a path') if remove_path: split_url[2] = u'/' if scheme in (u'http', u'https') and not split_url[2]: # By convention a full HTTP URL must always have at least a "/" in its path. split_url[2] = u'/' if split_url[3]: if error_if_query: return value, state._(u'URL must not contain a query') if remove_query: split_url[3] = u'' if split_url[4]: if error_if_fragment: return value, state._(u'URL must not contain a fragment') if remove_fragment: split_url[4] = u'' return unicode(urlparse.urlunsplit(split_url)), None return str_to_url
[docs]def make_item_to_singleton(constructor = list): """Convert an item to a singleton, but keep a sequence of items unchanged. >>> make_item_to_singleton()(u'Hello world!') ([u'Hello world!'], None) >>> make_item_to_singleton()([u'Hello world!']) ([u'Hello world!'], None) >>> make_item_to_singleton()([42, u'Hello world!']) ([42, u'Hello world!'], None) >>> make_item_to_singleton()([]) ([], None) >>> make_item_to_singleton()(None) (None, None) >>> make_item_to_singleton(constructor = set)(u'Hello world!') (set([u'Hello world!']), None) >>> make_item_to_singleton(constructor = set)(set([u'Hello world!'])) (set([u'Hello world!']), None) >>> make_item_to_singleton(constructor = set)(set([42, u'Hello world!'])) (set([u'Hello world!', 42]), None) >>> make_item_to_singleton(constructor = set)([42, u'Hello world!']) Traceback (most recent call last): TypeError: >>> make_item_to_singleton(constructor = set)(set()) (set([]), None) >>> make_item_to_singleton(constructor = set)(None) (None, None) """ return condition( test_isinstance(constructor), noop, new_struct([noop], constructor = constructor), )
[docs]def make_input_to_normal_form(encoding = 'utf-8', separator = u' ', transform = strings.lower): """Return a convert that simplifies a string to normal form using compatibility decomposition and removing combining characters. .. note:: For a converter that is dedicated to a name in an URL path, see :func:`input_to_url_name`. .. note:: For a converter that keep only letters, digits and separator, see :func:`make_input_to_slug` or :func:`input_to_slug`. >>> make_input_to_normal_form()(u'Hello world!') (u'hello world!', None) >>> make_input_to_normal_form()('Hello world!') (u'hello world!', None) >>> make_input_to_normal_form()(u' Hello world! ') (u'hello world!', None) >>> make_input_to_normal_form(encoding = u'iso-8859-1')(u'Hello world!') (u'hello world!', None) >>> make_input_to_normal_form(separator = u'_')(u'Hello world!') (u'hello_world!', None) >>> from biryani import strings >>> make_input_to_normal_form(separator = u' ', transform = strings.upper)(u'Hello world!') (u'HELLO WORLD!', None) >>> make_input_to_normal_form()(u'') (None, None) >>> make_input_to_normal_form()(u' ') (None, None) """ def input_to_normal_form(value, state = None): if value is None: return value, None value = strings.normalize(value, encoding = encoding, separator = separator, transform = transform) return value or None, None return input_to_normal_form
[docs]def make_input_to_slug(encoding = 'utf-8', separator = u'-', transform = strings.lower): """Return a convert that simplifies a string to a slug. .. note:: For a converter that uses default parameters, see :func:`input_to_slug`. >>> make_input_to_slug()(u'Hello world!') (u'hello-world', None) >>> make_input_to_slug()('Hello world!') (u'hello-world', None) >>> make_input_to_slug()(u' Hello world! ') (u'hello-world', None) >>> make_input_to_slug(encoding = u'iso-8859-1')(u'Hello world!') (u'hello-world', None) >>> make_input_to_slug(separator = u' ')(u'Hello world!') (u'hello world', None) >>> from biryani import strings >>> make_input_to_slug(separator = u' ', transform = strings.upper)(u'Hello world!') (u'HELLO WORLD', None) >>> make_input_to_slug()(u'') (None, None) >>> make_input_to_slug()(u' ') (None, None) """ def input_to_slug(value, state = None): if value is None: return value, None value = strings.slugify(value, encoding = encoding, separator = separator, transform = transform) return unicode(value) if value else None, None return input_to_slug
[docs]def make_input_to_url(add_prefix = None, error_if_fragment = False, error_if_path = False, error_if_query = False, full = False, remove_fragment = False, remove_path = False, remove_query = False, schemes = (u'http', u'https')): """Return a converter that converts an string to an URL. >>> make_input_to_url()(u'http://packages.python.org/Biryani/') (u'http://packages.python.org/Biryani/', None) >>> make_input_to_url()(u'packages.python.org/Biryani/') (u'packages.python.org/Biryani/', None) >>> make_input_to_url(full = True)(u'packages.python.org/Biryani/') (u'packages.python.org/Biryani/', u'URL must be complete') >>> make_input_to_url(add_prefix = u'http://', full = True)(u'packages.python.org/Biryani/') (u'http://packages.python.org/Biryani/', None) >>> make_input_to_url()(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html#tutorial', None) >>> make_input_to_url(full = True)(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html#tutorial', u'URL must be complete') >>> make_input_to_url(remove_path = True)(u'http://packages.python.org/Biryani/presentation.html') (u'http://packages.python.org/', None) >>> make_input_to_url(error_if_path = True)(u'http://packages.python.org/Biryani/presentation.html') (u'http://packages.python.org/Biryani/presentation.html', u'URL must not contain a path') >>> make_input_to_url(remove_query = True)(u'http://packages.python.org/Biryani/presentation.html?tuto=1') (u'http://packages.python.org/Biryani/presentation.html', None) >>> make_input_to_url(error_if_query = True)(u'http://packages.python.org/Biryani/presentation.html?tuto=1') (u'http://packages.python.org/Biryani/presentation.html?tuto=1', u'URL must not contain a query') >>> make_input_to_url(remove_fragment = True)(u'http://packages.python.org/Biryani/presentation.html#tutorial') (u'http://packages.python.org/Biryani/presentation.html', None) >>> make_input_to_url(error_if_fragment = True)(u'http://packages.python.org/Biryani/presentation.html#tutorial') (u'http://packages.python.org/Biryani/presentation.html#tutorial', u'URL must not contain a fragment') >>> make_input_to_url()(u' http://packages.python.org/Biryani/ ') (u'http://packages.python.org/Biryani/', None) """ return pipe( cleanup_line, make_str_to_url(add_prefix = add_prefix, error_if_fragment = error_if_fragment, error_if_path = error_if_path, error_if_query = error_if_query, full = full, remove_fragment = remove_fragment, remove_path = remove_path, remove_query = remove_query, schemes = schemes), )
[docs]def make_input_to_url_name(encoding = 'utf-8', separator = u'_', transform = strings.lower): """Return a converts that normalizes a string to allow its use in an URL (or file system) path or a query parameter. .. note:: For a converter that keep only letters, digits and separator, see :func:`make_input_to_slug` or :func:`input_to_slug`. >>> make_input_to_url_name()(u' Hello world! ') (u'hello_world!', None) >>> make_input_to_url_name()(u' ') (None, None) >>> make_input_to_url_name()(u'') (None, None) """ def input_to_url_name(value, state = None): if value is None: return value, None if isinstance(value, str): value = value.decode(encoding) # Replace unsafe characters (for URLs and file-systems). value = value.translate({ ord(u'\n'): separator, ord(u'\r'): separator, ord(u'\\'): separator, ord(u'/'): separator, ord(u';'): separator, ord(u':'): separator, ord(u'"'): separator, ord(u'#'): separator, ord(u'*'): separator, ord(u'?'): separator, ord(u'&'): separator, ord(u'<'): separator, ord(u'>'): separator, ord(u'|'): separator, ord(u'.'): separator, }) value = strings.normalize(value, encoding = encoding, separator = separator, transform = transform) two_separators = separator * 2 while two_separators in value: value = value.replace(two_separators, separator) value = value.strip(separator) return value or None, None return input_to_url_name
[docs]def merge(*converters): """Return a converter that merge the resulsts of several :func:`structured_mapping` converters. >>> ab_converter = struct( ... dict( ... a = input_to_int, ... b = input_to_float, ... ), ... default = 'drop', ... ) >>> c_converter = struct( ... dict( ... c = input_to_email, ... ), ... default = 'drop', ... ) >>> abc_converter = merge(ab_converter, c_converter) >>> abc_converter(dict(a = u'1', b = u'2', c = u'john@doe.name')) ({'a': 1, 'c': u'john@doe.name', 'b': 2.0}, None) >>> abc_converter(dict(a = u'1')) ({'a': 1, 'c': None, 'b': None}, None) >>> abc_converter(dict(c = u'john@doe.name')) ({'a': None, 'c': u'john@doe.name', 'b': None}, None) >>> abc_converter(dict(a = u'a', b = u'b', c = u'1')) ({'a': u'a', 'c': u'1', 'b': u'b'}, {'a': u'Value must be an integer', \ 'c': u'An email must contain exactly one "@"', 'b': u'Value must be a float'}) >>> abc_converter({}) ({'a': None, 'c': None, 'b': None}, None) >>> abc_converter(None) (None, None) """ def merge_converter(values, state = None): if values is None: return values, None if state is None: state = states.default_state merged_values = None merged_errors = None for converter in converters: converter_values, converter_errors = converter(values, state = state) if converter_errors is not None: if not isinstance(converter_errors, dict): return converter_values, converter_errors if merged_errors is None: merged_errors = {} merged_errors.update(converter_errors) if converter_values is not None: if not isinstance(converter_values, dict): return converter_values, converter_errors if merged_values is None: merged_values = {} merged_values.update(converter_values) return merged_values, merged_errors return merge_converter
[docs]def new_mapping(converters, constructor = None, drop_none_values = False, handle_none_value = False): """Return a converter that constructs a mapping (ie dict, etc) from any kind of value. .. note:: This converter should not be used directly. Use :func:`new_struct` instead. .. note:: When input value has the same structure, converter :func:`struct` should be used instead. >>> def convert_list_to_dict(constructor = None, drop_none_values = False, handle_none_value = False): ... return new_mapping( ... dict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... constructor = constructor, ... drop_none_values = drop_none_values, ... handle_none_value = handle_none_value, ... ) >>> convert_list_to_dict()([u'John Doe', u'72', u'john@doe.name']) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> convert_list_to_dict()([u'John Doe', u'72']) ({'age': 72, 'email': None, 'name': u'John Doe'}, {'email': u'Index out of range: 2'}) >>> convert_list_to_dict()([u'John Doe', u'72', None]) ({'age': 72, 'email': None, 'name': u'John Doe'}, None) >>> convert_list_to_dict()([None, u' ', None]) ({'age': None, 'email': None, 'name': None}, None) >>> convert_list_to_dict(drop_none_values = True)([None, u' ', None]) ({}, None) >>> convert_list_to_dict()(None) (None, None) >>> convert_list_to_dict(handle_none_value = True)(None) ({'age': None, 'email': None, 'name': None}, None) >>> convert_list_to_dict(drop_none_values = True, handle_none_value = True)(None) ({}, None) >>> import collections >>> new_mapping( ... dict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... constructor = collections.OrderedDict, ... )([u'John Doe', u'72', u'john@doe.name']) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> new_mapping( ... collections.OrderedDict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... )([u'John Doe', u'72', u'john@doe.name']) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) """ if constructor is None: constructor = type(converters) converters = dict( (name, converter) for name, converter in (converters or {}).iteritems() if converter is not None ) def new_mapping_converter(value, state = None): if value is None and not handle_none_value: return value, None if state is None: state = states.default_state errors = {} converted_values = constructor() for name, converter in converters.iteritems(): converted_value, error = converter(value, state = state) if converted_value is not None or not drop_none_values: converted_values[name] = converted_value if error is not None: errors[name] = error return converted_values, errors or None return new_mapping_converter
[docs]def new_sequence(converters, constructor = None, handle_none_value = False): """Return a converter that constructs a sequence (ie list, tuple, etc) from any kind of value. .. note:: This converter should not be used directly. Use :func:`new_struct` instead. .. note:: When input value has the same structure, converter :func:`struct` should be used instead. >>> def convert_dict_to_list(constructor = None, handle_none_value = False): ... return new_sequence( ... [ ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ], ... constructor = constructor, ... handle_none_value = handle_none_value, ... ) >>> convert_dict_to_list()({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ([u'John Doe', 72, u'john@doe.name'], None) >>> convert_dict_to_list(constructor = tuple)({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) >>> convert_dict_to_list()({'email': u'john@doe.name', 'name': u'John Doe'}) ([u'John Doe', None, u'john@doe.name'], None) >>> convert_dict_to_list()({}) ([None, None, None], None) >>> convert_dict_to_list()(None) (None, None) >>> convert_dict_to_list(handle_none_value = True)(None) ([None, None, None], None) >>> import collections >>> new_sequence( ... [ ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ], ... constructor = tuple, ... )({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) >>> new_sequence( ... ( ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ), ... )({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) """ if constructor is None: constructor = type(converters) converters = [ converter for converter in converters or [] if converter is not None ] def new_sequence_converter(value, state = None): if value is None and not handle_none_value: return value, None if state is None: state = states.default_state errors = {} converted_values = [] for i, converter in enumerate(converters): converted_value, error = converter(value, state = state) converted_values.append(converted_value) if error is not None: errors[i] = error return constructor(converted_values), errors or None return new_sequence_converter
[docs]def new_struct(converters, constructor = None, drop_none_values = False, handle_none_value = False): """Return a converter that constructs a collection (ie dict, list, set, etc) from any kind of value. .. note:: When input value has the same structure, converter :func:`struct` should be used instead. .. note:: Parameter ``drop_none_values`` is not used for sequences. Usage to create a mapping (ie dict, etc): >>> def convert_list_to_dict(constructor = None, drop_none_values = False, handle_none_value = False): ... return new_struct( ... dict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... constructor = constructor, ... drop_none_values = drop_none_values, ... handle_none_value = handle_none_value, ... ) >>> convert_list_to_dict()([u'John Doe', u'72', u'john@doe.name']) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> convert_list_to_dict()([u'John Doe', u'72']) ({'age': 72, 'email': None, 'name': u'John Doe'}, {'email': u'Index out of range: 2'}) >>> convert_list_to_dict()([u'John Doe', u'72', None]) ({'age': 72, 'email': None, 'name': u'John Doe'}, None) >>> convert_list_to_dict()([None, u' ', None]) ({'age': None, 'email': None, 'name': None}, None) >>> convert_list_to_dict(drop_none_values = True)([None, u' ', None]) ({}, None) >>> convert_list_to_dict()(None) (None, None) >>> convert_list_to_dict(handle_none_value = True)(None) ({'age': None, 'email': None, 'name': None}, None) >>> convert_list_to_dict(drop_none_values = True, handle_none_value = True)(None) ({}, None) >>> import collections >>> new_struct( ... dict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... constructor = collections.OrderedDict, ... )([u'John Doe', u'72', u'john@doe.name']) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> new_struct( ... collections.OrderedDict( ... name = get(0), ... age = pipe(get(1), input_to_int), ... email = pipe(get(2), input_to_email), ... ), ... )([u'John Doe', u'72', u'john@doe.name']) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) Usage to create a sequence (ie list, tuple, etc) or a set: >>> def convert_dict_to_list(constructor = None, handle_none_value = False): ... return new_struct( ... [ ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ], ... constructor = constructor, ... handle_none_value = handle_none_value, ... ) >>> convert_dict_to_list()({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ([u'John Doe', 72, u'john@doe.name'], None) >>> convert_dict_to_list(constructor = tuple)({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) >>> convert_dict_to_list()({'email': u'john@doe.name', 'name': u'John Doe'}) ([u'John Doe', None, u'john@doe.name'], None) >>> convert_dict_to_list()({}) ([None, None, None], None) >>> convert_dict_to_list()(None) (None, None) >>> convert_dict_to_list(handle_none_value = True)(None) ([None, None, None], None) >>> import collections >>> new_struct( ... [ ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ], ... constructor = tuple, ... )({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) >>> new_struct( ... ( ... get('name', default = None), ... pipe(get('age', default = None), input_to_int), ... pipe(get('email', default = None), input_to_email), ... ), ... )({'age': u'72', 'email': u'john@doe.name', 'name': u'John Doe'}) ((u'John Doe', 72, u'john@doe.name'), None) """ import collections if isinstance(converters, collections.Mapping): return new_mapping(converters, constructor = constructor, drop_none_values = drop_none_values, handle_none_value = handle_none_value) assert isinstance(converters, collections.Sequence), \ 'Converters must be a mapping or a sequence. Got {0} instead.'.format(type(converters)) return new_sequence(converters, constructor = constructor, handle_none_value = handle_none_value)
[docs]def noop(value, state = None): """Return value as is. >>> noop(42) (42, None) >>> noop(None) (None, None) """ return value, None
[docs]def ok(converter_or_value_and_error): """Run a conversion and return ``True`` when it doesn't generate an error or ``False`` otherwise. This function can be called with either a converter or the result of a conversion. Usage with a converter: >>> ok(input_to_int)(u'42') True >>> ok(input_to_int)(u'hello world') False Usage with a conversion result : >>> ok(input_to_int(u'42')) True >>> ok(input_to_int(u'hello world')) False """ import collections if isinstance(converter_or_value_and_error, collections.Sequence): value, error = converter_or_value_and_error return error is None else: # converter_or_value_and_error is a converter. def ok_converter(*args, **kwargs): value, error = converter_or_value_and_error(*args, **kwargs) return error is None return ok_converter
[docs]def pipe(*converters): """Return a compound converter that applies each of its converters till the end or an error occurs. >>> input_to_bool(42) Traceback (most recent call last): AttributeError: >>> pipe(input_to_bool)(42) Traceback (most recent call last): AttributeError: >>> pipe(test_isinstance(unicode), input_to_bool)(42) (42, u"Value is not an instance of <type 'unicode'>") >>> pipe(anything_to_str, test_isinstance(unicode), input_to_bool)(42) (True, None) >>> pipe()(42) (42, None) """ def pipe_converter(*args, **kwargs): if not converters: return noop(*args, **kwargs) state = kwargs.get('state', UnboundLocalError) for converter in converters: if converter is None: continue value, error = converter(*args, **kwargs) if error is not None: return value, error args = [value] kwargs = {} if state is not UnboundLocalError: kwargs['state'] = state return value, None return pipe_converter
[docs]def rename_item(old_key, new_key): """Return a converter that renames a key in a mapping. >>> rename_item('a', 'c')(dict(a = 1, b = 2)) ({'c': 1, 'b': 2}, None) >>> rename_item('c', 'd')(dict(a = 1, b = 2)) ({'a': 1, 'b': 2}, None) >>> rename_item('c', 'd')(None) (None, None) """ def rename_item_converter(value, state = None): if value is None: return value, None if old_key in value: value = value.copy() # Don't modify existing mapping. value[new_key] = value.pop(old_key) return value, None return rename_item_converter
[docs]def set_value(constant, handle_none_value = False): """Return a converter that replaces any value by given one. >>> set_value(42)(u'Answer to the Ultimate Question of Life, the Universe, and Everything') (42, None) >>> set_value(42)(None) (None, None) >>> set_value(42, handle_none_value = True)(None) (42, None) """ return lambda value, state = None: (constant, None) \ if value is not None or handle_none_value \ else (None, None)
[docs]def str_to_bool(value, state = None): """Convert a clean string to a boolean. .. note:: For a converter that doesn't require a clean string, see :func:`input_to_bool`. .. note:: For a converter that accepts special strings like "f", "off", "no", etc, see :func:`guess_bool`. .. warning:: Like most converters, a ``None`` value is not converted. When you want ``None`` to be converted to ``False``, use:: pipe(str_to_bool, default(False)) >>> str_to_bool(u'0') (False, None) >>> str_to_bool(u'1') (True, None) >>> str_to_bool(None) (None, None) >>> str_to_bool(u'vrai') (u'vrai', u'Value must be a boolean') >>> str_to_bool(u'on') (u'on', u'Value must be a boolean') """ if value is None: return value, None if state is None: state = states.default_state try: return bool(int(value)), None except ValueError: return value, state._(u'Value must be a boolean')
[docs]def str_to_email(value, state = None): """Convert a clean string to an email address. .. note:: For a converter that doesn't require a clean string, see :func:`input_to_email`. >>> str_to_email(u'john@doe.name') (u'john@doe.name', None) >>> str_to_email(u'mailto:john@doe.name') (u'john@doe.name', None) >>> str_to_email(u'root@localhost') (u'root@localhost', None) >>> str_to_email(u'root@127.0.0.1') (u'root@127.0.0.1', u'Invalid domain name') >>> str_to_email(u'root') (u'root', u'An email must contain exactly one "@"') """ if value is None: return value, None if state is None: state = states.default_state value = value.lower() if value.startswith(u'mailto:'): value = value.replace(u'mailto:', u'') try: username, domain = value.split('@', 1) except ValueError: return value, state._(u'An email must contain exactly one "@"') if not username_re.match(username): return value, state._(u'Invalid username') if not domain_re.match(domain) and domain != 'localhost': return value, state._(u'Invalid domain name') return value, None
[docs]def str_to_url_path_and_query(value, state = None): """Convert a clean string to the path and query of an URL. .. note:: For a converter that doesn't require a clean string, see :func:`input_to_url_path_and_query`. >>> str_to_url_path_and_query(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html', None) >>> str_to_url_path_and_query(u'/Biryani/search.html?q=pipe') (u'/Biryani/search.html?q=pipe', None) >>> str_to_url_path_and_query(u'http://packages.python.org/Biryani/search.html?q=pipe') (u'http://packages.python.org/Biryani/search.html?q=pipe', u'URL must not be complete') >>> str_to_url_path_and_query(u'[www.nordnet.fr/grandmix/]') (u'[www.nordnet.fr/grandmix/]', None) >>> print str_to_url_path_and_query(None) (None, None) >>> import urlparse >>> pipe( ... make_str_to_url(), ... function(lambda value: urlparse.urlunsplit([u'', u''] + list(urlparse.urlsplit(value))[2:])), ... str_to_url_path_and_query, ... )(u'http://packages.python.org/Biryani/search.html?q=pipe') (u'/Biryani/search.html?q=pipe', None) """ if value is None: return value, None import urlparse if state is None: state = states.default_state try: split_url = list(urlparse.urlsplit(value)) except ValueError: return value, state._(u'Invalid URL') if split_url[0] or split_url[1]: return value, state._(u'URL must not be complete') if split_url[4]: split_url[4] = '' return unicode(urlparse.urlunsplit(split_url)), None
[docs]def struct(converters, constructor = None, default = None, drop_none_values = False, keep_value_order = False, skip_missing_items = False): """Return a converter that maps a collection of converters to a collection (ie dict, list, set, etc) of values. .. note:: Parameters ``drop_none_values``, ``keep_value_order`` & ``skip_missing_items`` are not used for sequences. Usage to convert a mapping (ie dict, etc): >>> strict_converter = struct(dict( ... name = pipe(cleanup_line, not_none), ... age = input_to_int, ... email = input_to_email, ... )) ... >>> strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', age = None, email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', age = u'72', phone = u' +33 9 12 34 56 78 ')) ({'phone': u' +33 9 12 34 56 78 ', 'age': 72, 'email': None, 'name': u'John Doe'}, {'phone': u'Unexpected item'}) >>> non_strict_converter = struct( ... dict( ... name = pipe(cleanup_line, not_none), ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... ) ... >>> non_strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> non_strict_converter(dict(name = u'John Doe', email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> non_strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name', ... phone = u' +33 9 12 34 56 78 ')) ({'phone': u'+33 9 12 34 56 78', 'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... )(dict(name = u' ', email = None)) ({'age': None, 'email': None, 'name': None}, None) >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... drop_none_values = True, ... )(dict(name = u' ', email = None)) ({}, None) >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... drop_none_values = 'missing', ... )(dict(name = u' ', email = None)) ({'email': None, 'name': None}, None) >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... skip_missing_items = True, ... )(dict(name = u' ', email = None)) ({'email': None, 'name': None}, None) >>> import collections >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... constructor = collections.OrderedDict, ... )(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> struct( ... collections.OrderedDict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... )(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> struct( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... )(collections.OrderedDict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) Usage to convert a sequence (ie list, tuple, etc) or a set: >>> strict_converter = struct([ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ]) ... >>> strict_converter([u'John Doe', u'72', u'john@doe.name']) ([u'John Doe', 72, u'john@doe.name'], None) >>> strict_converter([u'John Doe', u'john@doe.name']) ([u'John Doe', u'john@doe.name', None], {1: u'Value must be an integer'}) >>> strict_converter([u'John Doe', None, u'john@doe.name']) ([u'John Doe', None, u'john@doe.name'], None) >>> strict_converter([u'John Doe', u'72', u'john@doe.name', u' +33 9 12 34 56 78 ']) ([u'John Doe', 72, u'john@doe.name', u' +33 9 12 34 56 78 '], {3: u'Unexpected item'}) >>> non_strict_converter = struct( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... default = cleanup_line, ... ) ... >>> non_strict_converter([u'John Doe', u'72', u'john@doe.name']) ([u'John Doe', 72, u'john@doe.name'], None) >>> non_strict_converter([u'John Doe', u'john@doe.name']) ([u'John Doe', u'john@doe.name', None], {1: u'Value must be an integer'}) >>> non_strict_converter([u'John Doe', None, u'john@doe.name']) ([u'John Doe', None, u'john@doe.name'], None) >>> non_strict_converter([u'John Doe', u'72', u'john@doe.name', u' +33 9 12 34 56 78 ']) ([u'John Doe', 72, u'john@doe.name', u'+33 9 12 34 56 78'], None) >>> import collections >>> struct( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... constructor = tuple, ... )([u'John Doe', u'72', u'john@doe.name']) ((u'John Doe', 72, u'john@doe.name'), None) >>> struct( ... ( ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ), ... )([u'John Doe', u'72', u'john@doe.name']) ((u'John Doe', 72, u'john@doe.name'), None) >>> struct( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... )((u'John Doe', u'72', u'john@doe.name')) ([u'John Doe', 72, u'john@doe.name'], None) """ import collections if isinstance(converters, collections.Mapping): return structured_mapping(converters, constructor = constructor, default = default, drop_none_values = drop_none_values, keep_value_order = keep_value_order, skip_missing_items = skip_missing_items) assert isinstance(converters, collections.Sequence), \ 'Converters must be a mapping or a sequence. Got {0} instead.'.format(type(converters)) return structured_sequence(converters, constructor = constructor, default = default)
[docs]def structured_mapping(converters, constructor = None, default = None, drop_none_values = False, keep_value_order = False, skip_missing_items = False): """Return a converter that maps a mapping of converters to a mapping (ie dict, etc) of values. .. note:: This converter should not be used directly. Use :func:`struct` instead. >>> strict_converter = structured_mapping(dict( ... name = pipe(cleanup_line, not_none), ... age = input_to_int, ... email = input_to_email, ... )) ... >>> strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', age = None, email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> strict_converter(dict(name = u'John Doe', age = u'72', phone = u' +33 9 12 34 56 78 ')) ({'phone': u' +33 9 12 34 56 78 ', 'age': 72, 'email': None, 'name': u'John Doe'}, {'phone': u'Unexpected item'}) >>> non_strict_converter = structured_mapping( ... dict( ... name = pipe(cleanup_line, not_none), ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... ) ... >>> non_strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> non_strict_converter(dict(name = u'John Doe', email = u'john@doe.name')) ({'age': None, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> non_strict_converter(dict(name = u'John Doe', age = u'72', email = u'john@doe.name', ... phone = u' +33 9 12 34 56 78 ')) ({'phone': u'+33 9 12 34 56 78', 'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... )(dict(name = u' ', email = None)) ({'age': None, 'email': None, 'name': None}, None) >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... drop_none_values = True, ... )(dict(name = u' ', email = None)) ({}, None) >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... drop_none_values = 'missing', ... )(dict(name = u' ', email = None)) ({'email': None, 'name': None}, None) >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... default = cleanup_line, ... skip_missing_items = True, ... )(dict(name = u' ', email = None)) ({'email': None, 'name': None}, None) >>> import collections >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... constructor = collections.OrderedDict, ... )(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> structured_mapping( ... collections.OrderedDict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... )(dict(name = u'John Doe', age = u'72', email = u'john@doe.name')) (OrderedDict([('age', 72), ('email', u'john@doe.name'), ('name', u'John Doe')]), None) >>> structured_mapping( ... dict( ... name = cleanup_line, ... age = input_to_int, ... email = input_to_email, ... ), ... )(collections.OrderedDict(name = u'John Doe', age = u'72', email = u'john@doe.name')) ({'age': 72, 'email': u'john@doe.name', 'name': u'John Doe'}, None) """ if constructor is None: constructor = type(converters) converters = constructor( (name, converter) for name, converter in (converters or {}).iteritems() if converter is not None ) def structured_mapping_converter(values, state = None): if values is None: return values, None if state is None: state = states.default_state if keep_value_order: values_converter = constructor() for name in values: if name in converters: values_converter[name] = converters[name] elif default != 'drop': values_converter[name] = default \ if default is not None \ else fail(error = N_(u'Unexpected item')) for name, value_converter in converters.iteritems(): if name not in values_converter: values_converter[name] = value_converter elif default == 'drop': values_converter = converters else: values_converter = converters.copy() for name in values: if name not in values_converter: values_converter[name] = default if default is not None else fail(error = N_(u'Unexpected item')) errors = constructor() converted_values = constructor() for name, converter in values_converter.iteritems(): if skip_missing_items and name not in values: continue value, error = converter(values.get(name), state = state) if value is not None or not drop_none_values or drop_none_values == 'missing' and name in values: converted_values[name] = value if error is not None: errors[name] = error return converted_values, errors or None return structured_mapping_converter
[docs]def structured_sequence(converters, constructor = None, default = None): """Return a converter that map a sequence of converters to a sequence of values. .. note:: This converter should not be used directly. Use :func:`struct` instead. >>> strict_converter = structured_sequence([ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ]) ... >>> strict_converter([u'John Doe', u'72', u'john@doe.name']) ([u'John Doe', 72, u'john@doe.name'], None) >>> strict_converter([u'John Doe', u'john@doe.name']) ([u'John Doe', u'john@doe.name', None], {1: u'Value must be an integer'}) >>> strict_converter([u'John Doe', None, u'john@doe.name']) ([u'John Doe', None, u'john@doe.name'], None) >>> strict_converter([u'John Doe', u'72', u'john@doe.name', u' +33 9 12 34 56 78 ']) ([u'John Doe', 72, u'john@doe.name', u' +33 9 12 34 56 78 '], {3: u'Unexpected item'}) >>> non_strict_converter = structured_sequence( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... default = cleanup_line, ... ) ... >>> non_strict_converter([u'John Doe', u'72', u'john@doe.name']) ([u'John Doe', 72, u'john@doe.name'], None) >>> non_strict_converter([u'John Doe', u'john@doe.name']) ([u'John Doe', u'john@doe.name', None], {1: u'Value must be an integer'}) >>> non_strict_converter([u'John Doe', None, u'john@doe.name']) ([u'John Doe', None, u'john@doe.name'], None) >>> non_strict_converter([u'John Doe', u'72', u'john@doe.name', u' +33 9 12 34 56 78 ']) ([u'John Doe', 72, u'john@doe.name', u'+33 9 12 34 56 78'], None) >>> import collections >>> structured_sequence( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... constructor = tuple, ... )([u'John Doe', u'72', u'john@doe.name']) ((u'John Doe', 72, u'john@doe.name'), None) >>> structured_sequence( ... ( ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ), ... )([u'John Doe', u'72', u'john@doe.name']) ((u'John Doe', 72, u'john@doe.name'), None) >>> structured_sequence( ... [ ... pipe(cleanup_line, not_none), ... input_to_int, ... input_to_email, ... ], ... )((u'John Doe', u'72', u'john@doe.name')) ([u'John Doe', 72, u'john@doe.name'], None) """ if constructor is None: constructor = type(converters) converters = [ converter for converter in converters or [] if converter is not None ] def structured_sequence_converter(values, state = None): if values is None: return values, None if state is None: state = states.default_state if default == 'drop': values_converter = converters else: values_converter = converters[:] while len(values) > len(values_converter): values_converter.append(default if default is not None else fail(error = N_(u'Unexpected item'))) import itertools errors = {} converted_values = [] for i, (converter, value) in enumerate(itertools.izip_longest( values_converter, itertools.islice(values, len(values_converter)))): value, error = converter(value, state = state) converted_values.append(value) if error is not None: errors[i] = error return constructor(converted_values), errors or None return structured_sequence_converter
[docs]def submapping(keys, converter, remaining_converter = None, constructor = None): """Return a converter that splits a mapping into 2 mappings, converts them separately and merge both results. >>> submapping( ... ['x', 'y'], ... function(lambda subdict: dict(a = subdict['x'], b = subdict['y'])), ... )(dict(x = 1, y = 2, z = 3, t = 4)) ({'a': 1, 'b': 2, 'z': 3, 't': 4}, None) >>> submapping( ... ['x', 'y'], ... function(lambda subdict: dict(a = subdict['x'], b = subdict['y'])), ... uniform_mapping(noop, anything_to_str), ... )(dict(x = 1, y = 2, z = 3, t = 4)) ({'a': 1, 'b': 2, 'z': u'3', 't': u'4'}, None) >>> submapping( ... ['x', 'y'], ... uniform_mapping(noop, test_equals(1)), ... uniform_mapping(noop, anything_to_str), ... )(dict(x = 1, y = 2, z = 3, t = 4)) ({'y': 2, 'x': 1, 'z': u'3', 't': u'4'}, {'y': u'Value must be equal to 1'}) >>> submapping( ... ['x', 'y'], ... uniform_mapping(noop, test_equals(1)), ... uniform_mapping(noop, test_equals(3)), ... )(dict(x = 1, y = 2, z = 3, t = 4)) ({'y': 2, 'x': 1, 'z': 3, 't': 4}, {'y': u'Value must be equal to 1', 't': u'Value must be equal to 3'}) >>> submapping( ... ['x', 'y'], ... test_equals(1), ... uniform_mapping(noop, test_equals(3)), ... )(dict(x = 1, y = 2, z = 3, t = 4)) ({'y': 2, 'x': 1, 'z': 3, 't': 4}, u'Value must be equal to 1') >>> submapping( ... ['x', 'y'], ... uniform_mapping(noop, test_equals(1)), ... uniform_mapping(noop, test_equals(3)), ... )({}) ({}, None) >>> submapping( ... ['x', 'y'], ... uniform_mapping(noop, test_equals(1)), ... uniform_mapping(noop, test_equals(3)), ... )(None) (None, None) """ def submapping_converter(value, state = None): if value is None: return value, None if state is None: state = states.default_state mapping_constructor = type(value) if constructor is None else constructor submapping = mapping_constructor() remaining = mapping_constructor() for item_key, item_value in value.iteritems(): if item_key in keys: submapping[item_key] = item_value else: remaining[item_key] = item_value submapping_value, submapping_error = converter(submapping, state = state) remaining_value, remaining_error = (remaining_converter or noop)(remaining, state = state) merged_value = mapping_constructor() if submapping_value is not None: merged_value.update(submapping_value) if remaining_value is not None: merged_value.update(remaining_value) if submapping_error is None: merged_error = remaining_error elif remaining_error is None: merged_error = submapping_error elif isinstance(submapping_error, dict) and isinstance(remaining_error, dict): merged_error = submapping_error.copy() merged_error.update(remaining_error) else: # The errors are not compatible (at least one of them is not a dict) => Return only the first one. merged_error = submapping_error return merged_value, merged_error return submapping_converter
[docs]def switch(key_converter, converters, default = None, handle_none_value = False): """Return a converter that extracts a key from value and then converts value using the converter matching the key. >>> simple_type_switcher = switch( ... function(lambda value: type(value)), ... { ... bool: set_value(u'boolean'), ... int: anything_to_str, ... str: set_value(u'encoded string'), ... }, ... handle_none_value = True, ... ) >>> simple_type_switcher(True) (u'boolean', None) >>> simple_type_switcher(42) (u'42', None) >>> simple_type_switcher('Hello world!') (u'encoded string', None) >>> simple_type_switcher(u'Hello world!') (u'Hello world!', u'Expression "<type \\'unicode\\'>" doesn\\'t match any key') >>> simple_type_switcher(None) (None, u'Expression "None" doesn\\'t match any key') >>> type_switcher = switch( ... function(lambda value: type(value)), ... { ... list: uniform_sequence(simple_type_switcher), ... }, ... default = anything_to_str, ... ) >>> type_switcher([False, 42]) ([u'boolean', u'42'], None) >>> type_switcher(u'Hello world!') (u'Hello world!', None) >>> type_switcher(None) (None, None) >>> type_switcher([None]) ([None], {0: u'Expression "None" doesn\\'t match any key'}) """ def switch_converter(value, state = None): if value is None and not handle_none_value: return None, None if state is None: state = states.default_state key, error = key_converter(value, state = state) if error is not None: return value, error if key not in converters: if default is None: return value, state._(u'''Expression "{0}" doesn't match any key''').format(key) return default(value, state = state) return converters[key](value, state = state) return switch_converter
[docs]def test(function, error = N_(u'Test failed'), handle_none_value = False, handle_state = False): """Return a converter that applies a test function to a value and returns an error when test fails. ``test`` always returns the initial value, even when test fails. See :doc:`how-to-create-converter` for more information. >>> test(lambda value: isinstance(value, basestring))('hello') ('hello', None) >>> test(lambda value: isinstance(value, basestring))(1) (1, u'Test failed') >>> test(lambda value: isinstance(value, basestring), error = u'Value is not a string')(1) (1, u'Value is not a string') """ def test_converter(value, state = None): if value is None and not handle_none_value or function is None: return value, None if state is None: state = states.default_state ok = function(value, state = state) if handle_state else function(value) if ok: return value, None return value, state._(error) if isinstance(error, basestring) else error return test_converter
[docs]def test_between(min_value, max_value, error = None): """Return a converter that accepts only values between the two given bounds (included). .. warning:: Like most converters, a ``None`` value is not compared. >>> test_between(0, 9)(5) (5, None) >>> test_between(0, 9)(0) (0, None) >>> test_between(0, 9)(9) (9, None) >>> test_between(0, 9)(10) (10, u'Value must be between 0 and 9') >>> test_between(0, 9, error = u'Number must be a digit')(10) (10, u'Number must be a digit') >>> test_between(0, 9)(None) (None, None) """ return test(lambda value: min_value <= value <= max_value, error = error or N_(u'Value must be between {0} and {1}').format(min_value, max_value))
[docs]def test_conv(converter): """Return a converter that applies a converter to test a value without modifying it. ``test_conv`` always returns the initial value, even when test fails. >>> test_conv(input_to_int)(u'42') (u'42', None) >>> test_conv(input_to_int)(u'Hello world!') (u'Hello world!', u'Value must be an integer') """ def test_conv_converter(value, state = None): if state is None: state = states.default_state converted_value, error = converter(value, state = state) return value, error return test_conv_converter
[docs]def test_equals(constant, error = None): """Return a converter that accepts only values equals to given constant. .. warning:: Like most converters, a ``None`` value is not compared. Furthermore, when *constant* is ``None``, value is never compared. >>> test_equals(42)(42) (42, None) >>> test_equals(dict(a = 1, b = 2))(dict(a = 1, b = 2)) ({'a': 1, 'b': 2}, None) >>> test_equals(41)(42) (42, u'Value must be equal to 41') >>> test_equals(41, error = u'Value is not the answer')(42) (42, u'Value is not the answer') >>> test_equals(None)(42) (42, None) >>> test_equals(42)(None) (None, None) """ return test(lambda value: value == constant if constant is not None else True, error = error or N_(u'Value must be equal to {0}').format(constant))
[docs]def test_greater_or_equal(constant, error = None): """Return a converter that accepts only values greater than or equal to given constant. .. warning:: Like most converters, a ``None`` value is not compared. >>> test_greater_or_equal(0)(5) (5, None) >>> test_greater_or_equal(9)(5) (5, u'Value must be greater than or equal to 9') >>> test_greater_or_equal(9, error = u'Value must be a positive two-digits number')(5) (5, u'Value must be a positive two-digits number') >>> test_greater_or_equal(9)(None) (None, None) >>> test_greater_or_equal(None)(5) (5, None) """ return test(lambda value: (value >= constant) if constant is not None else True, error = error or N_(u'Value must be greater than or equal to {0}').format(constant))
[docs]def test_in(values, error = None): """Return a converter that accepts only values belonging to a given set (or list or...). .. warning:: Like most converters, a ``None`` value is not compared. Furthermore, when *values* is ``None``, value is never compared. >>> test_in('abcd')('a') ('a', None) >>> test_in(['a', 'b', 'c', 'd'])('a') ('a', None) >>> test_in(['a', 'b', 'c', 'd'])('z') ('z', u"Value must belong to ['a', 'b', 'c', 'd']") >>> test_in(['a', 'b', 'c', 'd'], error = u'Value must be a letter less than "e"')('z') ('z', u'Value must be a letter less than "e"') >>> test_in([])('z') ('z', u'Value must belong to []') >>> test_in(None)('z') ('z', None) >>> test_in(['a', 'b', 'c', 'd'])(None) (None, None) """ return test(lambda value: value in values if values is not None else True, error = error or N_(u'Value must belong to {0}').format(values if values is None or len(values) <= 5 else sorted(values)[:5] + [N_(u'...')]))
[docs]def test_is(constant, error = None): """Return a converter that accepts only values that are strictly equal to given constant. .. warning:: Like most converters, a ``None`` value is not compared. Furthermore, when *constant* is ``None``, value is never compared. >>> test_is(42)(42) (42, None) >>> test_is(dict(a = 1, b = 2))(dict(a = 1, b = 2)) ({'a': 1, 'b': 2}, u"Value must be {'a': 1, 'b': 2}") >>> test_is(41)(42) (42, u'Value must be 41') >>> test_is(41, error = u'Value is not the answer')(42) (42, u'Value is not the answer') >>> test_is(None)(42) (42, None) >>> test_is(42)(None) (None, None) """ return test(lambda value: value is constant if constant is not None else True, error = error or N_(u'Value must be {0}').format(constant))
[docs]def test_isinstance(class_or_classes, error = None): """Return a converter that accepts only an instance of given class (or tuple of classes). >>> test_isinstance(basestring)('This is a string') ('This is a string', None) >>> test_isinstance(basestring)(42) (42, u"Value is not an instance of <type 'basestring'>") >>> test_isinstance(basestring, error = u'Value is not a string')(42) (42, u'Value is not a string') >>> test_isinstance((float, int))(42) (42, None) """ return test(lambda value: isinstance(value, class_or_classes), error = error or N_(u'Value is not an instance of {0}').format(class_or_classes))
[docs]def test_issubclass(class_or_classes, error = None): """Return a converter that accepts only a class (or tuple of classes). >>> test_issubclass(basestring)(int) (<type 'int'>, u"Value is not a subclass of <type 'basestring'>") >>> test_issubclass(basestring)('This is a string') ('This is a string', u"Value is not a subclass of <type 'basestring'>") >>> test_issubclass(basestring)(str) (<type 'str'>, None) >>> test_issubclass(basestring, error = u'Value is not a string')(int) (<type 'int'>, u'Value is not a string') >>> test_issubclass((float, int, basestring))(unicode) (<type 'unicode'>, None) """ def test_issubclass_converter(value, state = None): if value is None: return value, None try: result = issubclass(value, class_or_classes) except TypeError: result = False if not result: return value, error or N_(u'Value is not a subclass of {0}'.format(class_or_classes)) return value, None return test_issubclass_converter
[docs]def test_less_or_equal(constant, error = None): """Return a converter that accepts only values less than or equal to given constant. .. warning:: Like most converters, a ``None`` value is not compared. >>> test_less_or_equal(9)(5) (5, None) >>> test_less_or_equal(0)(5) (5, u'Value must be less than or equal to 0') >>> test_less_or_equal(0, error = u'Value must be negative')(5) (5, u'Value must be negative') >>> test_less_or_equal(9)(None) (None, None) >>> test_less_or_equal(None)(5) (5, None) """ return test(lambda value: (value <= constant) if constant is not None else True, error = error or N_(u'Value must be less than or equal to {0}').format(constant))
[docs]def test_none(error = N_(u'Unexpected value')): """Return a converters that signals an error when value is not ``None``. >>> test_none()(42) (42, u'Unexpected value') >>> test_none(error = u'No value allowed')(42) (42, u'No value allowed') >>> test_none()(u'') (u'', u'Unexpected value') >>> test_none()(None) (None, None) """ def none(value, state = None): if value is None: return value, None if state is None: state = states.default_state return value, state._(error) if isinstance(error, basestring) else error return none
[docs]def test_not_in(values, error = None): """Return a converter that rejects only values belonging to a given set (or list or...). .. warning:: Like most converters, a ``None`` value is not compared. Furthermore, when *values* is ``None``, value is never compared. >>> test_not_in('abcd')('e') ('e', None) >>> test_not_in(['a', 'b', 'c', 'd'])('e') ('e', None) >>> test_not_in('abcd')('a') ('a', u'Value must not belong to abcd') >>> test_not_in(['a', 'b', 'c', 'd'])('a') ('a', u"Value must not belong to ['a', 'b', 'c', 'd']") >>> test_not_in(['a', 'b', 'c', 'd'], error = u'Value must not be a letter less than "e"')('a') ('a', u'Value must not be a letter less than "e"') >>> test_not_in([])('z') ('z', None) >>> test_not_in(None)('z') ('z', None) >>> test_not_in(['a', 'b', 'c', 'd'])(None) (None, None) """ return test(lambda value: value not in (values or []), error = error or N_(u'Value must not belong to {0}').format(values))
[docs]def test_not_none(error = N_(u'Missing value')): """Return a converters that signals an error when value is ``None``. .. note:: When error message "Missing value" can be kept, use :func:`exists` instead. >>> test_not_none()(42) (42, None) >>> test_not_none()(u'') (u'', None) >>> test_not_none()(None) (None, u'Missing value') >>> test_not_none(error = u'Required value')(None) (None, u'Required value') """ def not_none(value, state = None): if state is None: state = states.default_state if value is None: return value, state._(error) if isinstance(error, basestring) else error return value, None return not_none
[docs]def translate(conversions): """Return a converter that converts values found in given dictionary and keep others as is. .. warning:: Unlike most converters, a ``None`` value is handled => It can be translated. >>> translate({0: u'bad', 1: u'OK'})(0) (u'bad', None) >>> translate({0: u'bad', 1: u'OK'})(1) (u'OK', None) >>> translate({0: u'bad', 1: u'OK'})(2) (2, None) >>> translate({0: u'bad', 1: u'OK'})(u'three') (u'three', None) >>> translate({None: u'no problem', 0: u'bad', 1: u'OK'})(None) (u'no problem', None) """ return function( lambda value: value if conversions is None or value not in conversions else conversions[value], handle_none_value = True)
[docs]def uniform_mapping(key_converter, value_converter, constructor = None, drop_none_keys = False, drop_none_values = False): """Return a converter that applies a unique converter to each key and another unique converter to each value of a mapping. >>> uniform_mapping(cleanup_line, input_to_int)({u'a': u'1', u'b': u'2'}) ({u'a': 1, u'b': 2}, None) >>> uniform_mapping(cleanup_line, input_to_int)({u' answer ': u'42'}) ({u'answer': 42}, None) >>> uniform_mapping(cleanup_line, pipe(test_isinstance(basestring), input_to_int))({u'a': u'1', u'b': u'2', 'c': 3}) ({u'a': 1, 'c': 3, u'b': 2}, {'c': u"Value is not an instance of <type 'basestring'>"}) >>> uniform_mapping(cleanup_line, input_to_int)({}) ({}, None) >>> uniform_mapping(cleanup_line, input_to_int)({None: u'42'}) ({None: 42}, None) >>> uniform_mapping(cleanup_line, input_to_int, drop_none_keys = True)({None: u'42'}) ({}, None) >>> uniform_mapping(cleanup_line, input_to_int)(None) (None, None) """ def uniform_mapping_converter(values, state = None): if values is None: return values, None if state is None: state = states.default_state custom_constructor = type(values) if constructor is None else constructor errors = {} converted_values = custom_constructor() for key, value in values.iteritems(): key, error = key_converter(key, state = state) if error is not None: errors[key] = error if key is None and drop_none_keys: continue value, error = value_converter(value, state = state) if value is not None or not drop_none_values: converted_values[key] = value if error is not None: errors[key] = error return converted_values, errors or None return uniform_mapping_converter
[docs]def uniform_sequence(converter, constructor = list, drop_none_items = False): """Return a converter that applies the same converter to each value of a list. >>> uniform_sequence(input_to_int)([u'42']) ([42], None) >>> uniform_sequence(input_to_int)([u'42', u'43']) ([42, 43], None) >>> uniform_sequence(input_to_int)([u'42', u'43', u'Hello world!']) ([42, 43, u'Hello world!'], {2: u'Value must be an integer'}) >>> uniform_sequence(input_to_int)([None, None]) ([None, None], None) >>> uniform_sequence(input_to_int, drop_none_items = True)([None, None]) ([], None) >>> uniform_sequence(input_to_int)([u'42', None, u'43']) ([42, None, 43], None) >>> uniform_sequence(input_to_int, drop_none_items = True)([u'42', None, u'43']) ([42, 43], None) >>> uniform_sequence(input_to_int)([u'42', u'43', u'Hello world!']) ([42, 43, u'Hello world!'], {2: u'Value must be an integer'}) >>> uniform_sequence(input_to_int, constructor = set)(set([u'42', u'43'])) (set([42, 43]), None) """ def uniform_sequence_converter(values, state = None): if values is None: return values, None if state is None: state = states.default_state custom_constructor = type(values) if constructor is None else constructor errors = {} converted_values = [] for i, value in enumerate(values): value, error = converter(value, state = state) if not drop_none_items or value is not None: converted_values.append(value) if error is not None: errors[i] = error return custom_constructor(converted_values), errors or None return uniform_sequence_converter # Level-2 Converters
cleanup_line = pipe( function(lambda value: value.strip()), empty_to_none, ) """Strip spaces from a string and remove it when empty. >>> cleanup_line(u' Hello world! ') (u'Hello world!', None) >>> cleanup_line(' ') (None, None) >>> cleanup_line(None) (None, None) """ cleanup_text = pipe( function(lambda value: value.replace(u'\r\n', u'\n').replace(u'\r', u'\n')), cleanup_line, ) """Replaces CR + LF or CR to LF in a string, then strip spaces and remove it when empty. >>> cleanup_text(u' Hello\\r\\n world!\\r ') (u'Hello\\n world!', None) >>> cleanup_text(' ') (None, None) >>> cleanup_text(None) (None, None) """ extract_when_singleton = condition( test(lambda value: len(value) == 1 and not isinstance(value[0], (list, set, tuple))), function(lambda value: list(value)[0]), ) """Extract first item of sequence when it is a singleton and it is not itself a sequence, otherwise keep it unchanged. >>> extract_when_singleton([42]) (42, None) >>> extract_when_singleton([42, 43]) ([42, 43], None) >>> extract_when_singleton([]) ([], None) >>> extract_when_singleton(None) (None, None) >>> extract_when_singleton([[42]]) ([[42]], None) """ not_none = test_not_none() """Return an error when value is ``None``. .. note:: To change error message "Missing value", use :func:`test_not_none` instead. >>> not_none(42) (42, None) >>> not_none(u'') (u'', None) >>> not_none(None) (None, u'Missing value') """ # Level-3 Converters anything_to_bool = function(lambda value: bool(value)) """Convert any Python data to a boolean. .. warning:: Like most converters, a ``None`` value is not converted. When you want ``None`` to be converted to ``False``, use:: pipe(anything_to_bool, default(False)) >>> anything_to_bool(0) (False, None) >>> anything_to_bool(-1) (True, None) >>> anything_to_bool(u'0') (True, None) >>> anything_to_bool(u'1') (True, None) >>> anything_to_bool(u'true') (True, None) >>> anything_to_bool(u'false') (True, None) >>> anything_to_bool(u' 0 ') (True, None) >>> anything_to_bool(u' ') (True, None) >>> anything_to_bool(None) (None, None) """ input_to_bool = pipe(cleanup_line, str_to_bool) """Convert a string to a boolean. .. warning:: Like most converters, a ``None`` value is not converted. When you want ``None`` to be converted to ``False``, use:: pipe(input_to_bool, default(False)) >>> input_to_bool(u'0') (False, None) >>> input_to_bool(u' 0 ') (False, None) >>> input_to_bool(u'1') (True, None) >>> input_to_bool(u' 1 ') (True, None) >>> input_to_bool(None) (None, None) >>> input_to_bool(u'vrai') (u'vrai', u'Value must be a boolean') >>> input_to_bool(u'on') (u'on', u'Value must be a boolean') """ input_to_email = pipe(cleanup_line, str_to_email) """Convert a string to an email address. >>> input_to_email(u'john@doe.name') (u'john@doe.name', None) >>> input_to_email(u'mailto:john@doe.name') (u'john@doe.name', None) >>> input_to_email(u'root@localhost') (u'root@localhost', None) >>> input_to_email('root@127.0.0.1') ('root@127.0.0.1', u'Invalid domain name') >>> input_to_email(u'root') (u'root', u'An email must contain exactly one "@"') >>> input_to_email(u' john@doe.name ') (u'john@doe.name', None) >>> input_to_email(None) (None, None) >>> input_to_email(u' ') (None, None) """ input_to_float = pipe(cleanup_line, anything_to_float) """Convert a string to float. >>> input_to_float('42') (42.0, None) >>> input_to_float(u' 42.25 ') (42.25, None) >>> input_to_float(u'hello world') (u'hello world', u'Value must be a float') >>> input_to_float(None) (None, None) """ input_to_int = pipe(cleanup_line, anything_to_int) """Convert a string to an integer. >>> input_to_int('42') (42, None) >>> input_to_int(u' 42 ') (42, None) >>> input_to_int(u'42.75') (42, None) >>> input_to_int(u'42,75') (u'42,75', u'Value must be an integer') >>> input_to_int(None) (None, None) """ input_to_slug = make_input_to_slug() """Convert a string to a slug. .. note:: For a configurable converter, see :func:`make_input_to_slug`. .. note:: For a converter that doesn't use "-" as word separators or doesn't convert characters to lower case, see :func:`input_to_normal_form`. >>> input_to_slug(u' Hello world! ') (u'hello-world', None) >>> input_to_slug(' Hello world! ') (u'hello-world', None) >>> input_to_slug(u'') (None, None) >>> input_to_slug(u' ') (None, None) """ input_to_url_name = make_input_to_url_name() """Normalize a string to allow its use in an URL (or file system) path or a query parameter. .. note:: For a configurable converter, see :func:`make_input_to_url_name`. .. note:: For a converter that keep only letters, digits and separator, see :func:`make_input_to_slug` or :func:`input_to_slug`. .. note:: For a converter that doesn't use "_" as word separators or doesn't convert characters to lower case, see :func:`input_to_normal_form`. >>> input_to_url_name(u' Hello world! ') (u'hello_world!', None) >>> input_to_url_name(u' ') (None, None) >>> input_to_url_name(u'') (None, None) """ input_to_url_path_and_query = pipe(cleanup_line, str_to_url_path_and_query) """Convert a string to the path and query of an URL. >>> input_to_url_path_and_query(u'/Biryani/presentation.html#tutorial') (u'/Biryani/presentation.html', None) >>> input_to_url_path_and_query(u'/Biryani/search.html?q=pipe') (u'/Biryani/search.html?q=pipe', None) >>> input_to_url_path_and_query(u' /Biryani/search.html?q=pipe ') (u'/Biryani/search.html?q=pipe', None) >>> input_to_url_path_and_query(u'http://packages.python.org/Biryani/search.html?q=pipe') (u'http://packages.python.org/Biryani/search.html?q=pipe', u'URL must not be complete') >>> print input_to_url_path_and_query(None) (None, None) """ # Utility Functions
[docs]def check(converter_or_value_and_error, clear_on_error = False): """Check a conversion and either return its value or raise a *ValueError* exception. This function can be called with either a converter or the result of a conversion. Usage with a converter: >>> check(input_to_int)(u'42') 42 >>> check(input_to_int)(u'hello world') Traceback (most recent call last): ValueError: >>> check(pipe(anything_to_str, test_isinstance(unicode), input_to_bool))(42) True >>> check(input_to_int, clear_on_error = True)(u'42') 42 >>> print check(input_to_int, clear_on_error = True)(u'hello world') None Usage with a conversion result : >>> check(input_to_int(u'42')) 42 >>> check(input_to_int(u'hello world')) Traceback (most recent call last): ValueError: >>> check(pipe(anything_to_str, test_isinstance(unicode), input_to_bool)(42)) True >>> check(input_to_int(u'42'), clear_on_error = True) 42 >>> print check(input_to_int(u'hello world'), clear_on_error = True) None """ import collections if isinstance(converter_or_value_and_error, collections.Sequence): value, error = converter_or_value_and_error if error is not None: if clear_on_error: return None raise ValueError(u'{0} for: {1}'.format(error, value).encode('utf-8')) return value else: # converter_or_value_and_error is a converter. def check_converter(*args, **kwargs): value, error = converter_or_value_and_error(*args, **kwargs) if error is not None: if clear_on_error: return None raise ValueError(u'{0} for: {1}'.format(error, value).encode('utf-8')) return value return check_converter