+++ /dev/null
-## FormEncode, a Form processor
-## Copyright (C) 2003, Ian Bicking <ianb@colorstudy.com>
-
-"""
-Validator/Converters for use with FormEncode.
-"""
-
-import cgi
-import locale
-import re
-import warnings
-from encodings import idna
-
-try: # import dnspython
- import dns.resolver
- import dns.exception
-except (IOError, ImportError):
- have_dns = False
-else:
- have_dns = True
-
-
-# These are only imported when needed
-httplib = None
-random = None
-sha1 = None
-socket = None
-urlparse = None
-
-from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator,
- deprecation_warning, is_empty)
-
-assert Identity and Invalid and NoDefault # silence unused import warnings
-
-# Dummy i18n translation function, nothing is translated here.
-# Instead this is actually done in api.message.
-# The surrounding _('string') of the strings is only for extracting
-# the strings automatically.
-# If you run pygettext with this source comment this function out temporarily.
-_ = lambda s: s
-
-
-############################################################
-## Utility methods
-############################################################
-
-# These all deal with accepting both datetime and mxDateTime modules and types
-datetime_module = None
-mxDateTime_module = None
-
-
-def import_datetime(module_type):
- global datetime_module, mxDateTime_module
- module_type = module_type.lower() if module_type else 'datetime'
- if module_type == 'datetime':
- if datetime_module is None:
- import datetime as datetime_module
- return datetime_module
- elif module_type == 'mxdatetime':
- if mxDateTime_module is None:
- from mx import DateTime as mxDateTime_module
- return mxDateTime_module
- else:
- raise ImportError('Invalid datetime module %r' % module_type)
-
-
-def datetime_now(module):
- if module.__name__ == 'datetime':
- return module.datetime.now()
- else:
- return module.now()
-
-
-def datetime_makedate(module, year, month, day):
- if module.__name__ == 'datetime':
- return module.date(year, month, day)
- else:
- try:
- return module.DateTime(year, month, day)
- except module.RangeError as e:
- raise ValueError(str(e))
-
-
-def datetime_time(module):
- if module.__name__ == 'datetime':
- return module.time
- else:
- return module.Time
-
-
-def datetime_isotime(module):
- if module.__name__ == 'datetime':
- return module.time.isoformat
- else:
- return module.ISO.Time
-
-
-############################################################
-## Wrapper Validators
-############################################################
-
-class ConfirmType(FancyValidator):
- """
- Confirms that the input/output is of the proper type.
-
- Uses the parameters:
-
- subclass:
- The class or a tuple of classes; the item must be an instance
- of the class or a subclass.
- type:
- A type or tuple of types (or classes); the item must be of
- the exact class or type. Subclasses are not allowed.
-
- Examples::
-
- >>> cint = ConfirmType(subclass=int)
- >>> cint.to_python(True)
- True
- >>> cint.to_python('1')
- Traceback (most recent call last):
- ...
- Invalid: '1' is not a subclass of <type 'int'>
- >>> cintfloat = ConfirmType(subclass=(float, int))
- >>> cintfloat.to_python(1.0), cintfloat.from_python(1.0)
- (1.0, 1.0)
- >>> cintfloat.to_python(1), cintfloat.from_python(1)
- (1, 1)
- >>> cintfloat.to_python(None)
- Traceback (most recent call last):
- ...
- Invalid: None is not a subclass of one of the types <type 'float'>, <type 'int'>
- >>> cint2 = ConfirmType(type=int)
- >>> cint2(accept_python=False).from_python(True)
- Traceback (most recent call last):
- ...
- Invalid: True must be of the type <type 'int'>
- """
-
- accept_iterator = True
-
- subclass = None
- type = None
-
- messages = dict(
- subclass=_('%(object)r is not a subclass of %(subclass)s'),
- inSubclass=_('%(object)r is not a subclass of one of the types %(subclassList)s'),
- inType=_('%(object)r must be one of the types %(typeList)s'),
- type=_('%(object)r must be of the type %(type)s'))
-
- def __init__(self, *args, **kw):
- FancyValidator.__init__(self, *args, **kw)
- if self.subclass:
- if isinstance(self.subclass, list):
- self.subclass = tuple(self.subclass)
- elif not isinstance(self.subclass, tuple):
- self.subclass = (self.subclass,)
- self._validate_python = self.confirm_subclass
- if self.type:
- if isinstance(self.type, list):
- self.type = tuple(self.type)
- elif not isinstance(self.type, tuple):
- self.type = (self.type,)
- self._validate_python = self.confirm_type
-
- def confirm_subclass(self, value, state):
- if not isinstance(value, self.subclass):
- if len(self.subclass) == 1:
- msg = self.message('subclass', state, object=value,
- subclass=self.subclass[0])
- else:
- subclass_list = ', '.join(map(str, self.subclass))
- msg = self.message('inSubclass', state, object=value,
- subclassList=subclass_list)
- raise Invalid(msg, value, state)
-
- def confirm_type(self, value, state):
- for t in self.type:
- if type(value) is t:
- break
- else:
- if len(self.type) == 1:
- msg = self.message('type', state, object=value,
- type=self.type[0])
- else:
- msg = self.message('inType', state, object=value,
- typeList=', '.join(map(str, self.type)))
- raise Invalid(msg, value, state)
- return value
-
- def is_empty(self, value):
- return False
-
-
-class Wrapper(FancyValidator):
- """
- Used to convert functions to validator/converters.
-
- You can give a simple function for `_convert_to_python`,
- `_convert_from_python`, `_validate_python` or `_validate_other`.
- If that function raises an exception, the value is considered invalid.
- Whatever value the function returns is considered the converted value.
-
- Unlike validators, the `state` argument is not used. Functions
- like `int` can be used here, that take a single argument.
-
- Note that as Wrapper will generate a FancyValidator, empty
- values (those who pass ``FancyValidator.is_empty)`` will return ``None``.
- To override this behavior you can use ``Wrapper(empty_value=callable)``.
- For example passing ``Wrapper(empty_value=lambda val: val)`` will return
- the value itself when is considered empty.
-
- Examples::
-
- >>> def downcase(v):
- ... return v.lower()
- >>> wrap = Wrapper(convert_to_python=downcase)
- >>> wrap.to_python('This')
- 'this'
- >>> wrap.from_python('This')
- 'This'
- >>> wrap.to_python('') is None
- True
- >>> wrap2 = Wrapper(
- ... convert_from_python=downcase, empty_value=lambda value: value)
- >>> wrap2.from_python('This')
- 'this'
- >>> wrap2.to_python('')
- ''
- >>> wrap2.from_python(1)
- Traceback (most recent call last):
- ...
- Invalid: 'int' object has no attribute 'lower'
- >>> wrap3 = Wrapper(validate_python=int)
- >>> wrap3.to_python('1')
- '1'
- >>> wrap3.to_python('a') # doctest: +ELLIPSIS
- Traceback (most recent call last):
- ...
- Invalid: invalid literal for int()...
- """
-
- func_convert_to_python = None
- func_convert_from_python = None
- func_validate_python = None
- func_validate_other = None
-
- _deprecated_methods = (
- ('func_to_python', 'func_convert_to_python'),
- ('func_from_python', 'func_convert_from_python'))
-
- def __init__(self, *args, **kw):
- # allow old method names as parameters
- if 'to_python' in kw and 'convert_to_python' not in kw:
- kw['convert_to_python'] = kw.pop('to_python')
- if 'from_python' in kw and 'convert_from_python' not in kw:
- kw['convert_from_python'] = kw.pop('from_python')
- for n in ('convert_to_python', 'convert_from_python',
- 'validate_python', 'validate_other'):
- if n in kw:
- kw['func_%s' % n] = kw.pop(n)
- FancyValidator.__init__(self, *args, **kw)
- self._convert_to_python = self.wrap(self.func_convert_to_python)
- self._convert_from_python = self.wrap(self.func_convert_from_python)
- self._validate_python = self.wrap(self.func_validate_python)
- self._validate_other = self.wrap(self.func_validate_other)
-
- def wrap(self, func):
- if not func:
- return None
-
- def result(value, state, func=func):
- try:
- return func(value)
- except Exception as e:
- raise Invalid(str(e), value, state)
-
- return result
-
-
-class Constant(FancyValidator):
- """
- This converter converts everything to the same thing.
-
- I.e., you pass in the constant value when initializing, then all
- values get converted to that constant value.
-
- This is only really useful for funny situations, like::
-
- # Any evaluates sub validators in reverse order for to_python
- fromEmailValidator = Any(
- Constant('unknown@localhost'),
- Email())
-
- In this case, the if the email is not valid
- ``'unknown@localhost'`` will be used instead. Of course, you
- could use ``if_invalid`` instead.
-
- Examples::
-
- >>> Constant('X').to_python('y')
- 'X'
- """
-
- __unpackargs__ = ('value',)
-
- def _convert_to_python(self, value, state):
- return self.value
-
- _convert_from_python = _convert_to_python
-
-
-############################################################
-## Normal validators
-############################################################
-
-class MaxLength(FancyValidator):
- """
- Invalid if the value is longer than `maxLength`. Uses len(),
- so it can work for strings, lists, or anything with length.
-
- Examples::
-
- >>> max5 = MaxLength(5)
- >>> max5.to_python('12345')
- '12345'
- >>> max5.from_python('12345')
- '12345'
- >>> max5.to_python('123456')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value less than 5 characters long
- >>> max5(accept_python=False).from_python('123456')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value less than 5 characters long
- >>> max5.to_python([1, 2, 3])
- [1, 2, 3]
- >>> max5.to_python([1, 2, 3, 4, 5, 6])
- Traceback (most recent call last):
- ...
- Invalid: Enter a value less than 5 characters long
- >>> max5.to_python(5)
- Traceback (most recent call last):
- ...
- Invalid: Invalid value (value with length expected)
- """
-
- __unpackargs__ = ('maxLength',)
-
- messages = dict(
- tooLong=_('Enter a value less than %(maxLength)i characters long'),
- invalid=_('Invalid value (value with length expected)'))
-
- def _validate_python(self, value, state):
- try:
- if value and len(value) > self.maxLength:
- raise Invalid(
- self.message('tooLong', state,
- maxLength=self.maxLength), value, state)
- else:
- return None
- except TypeError:
- raise Invalid(
- self.message('invalid', state), value, state)
-
-
-class MinLength(FancyValidator):
- """
- Invalid if the value is shorter than `minlength`. Uses len(), so
- it can work for strings, lists, or anything with length. Note
- that you **must** use ``not_empty=True`` if you don't want to
- accept empty values -- empty values are not tested for length.
-
- Examples::
-
- >>> min5 = MinLength(5)
- >>> min5.to_python('12345')
- '12345'
- >>> min5.from_python('12345')
- '12345'
- >>> min5.to_python('1234')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value at least 5 characters long
- >>> min5(accept_python=False).from_python('1234')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value at least 5 characters long
- >>> min5.to_python([1, 2, 3, 4, 5])
- [1, 2, 3, 4, 5]
- >>> min5.to_python([1, 2, 3])
- Traceback (most recent call last):
- ...
- Invalid: Enter a value at least 5 characters long
- >>> min5.to_python(5)
- Traceback (most recent call last):
- ...
- Invalid: Invalid value (value with length expected)
-
- """
-
- __unpackargs__ = ('minLength',)
-
- messages = dict(
- tooShort=_('Enter a value at least %(minLength)i characters long'),
- invalid=_('Invalid value (value with length expected)'))
-
- def _validate_python(self, value, state):
- try:
- if len(value) < self.minLength:
- raise Invalid(
- self.message('tooShort', state,
- minLength=self.minLength), value, state)
- except TypeError:
- raise Invalid(
- self.message('invalid', state), value, state)
-
-
-class NotEmpty(FancyValidator):
- """
- Invalid if value is empty (empty string, empty list, etc).
-
- Generally for objects that Python considers false, except zero
- which is not considered invalid.
-
- Examples::
-
- >>> ne = NotEmpty(messages=dict(empty='enter something'))
- >>> ne.to_python('')
- Traceback (most recent call last):
- ...
- Invalid: enter something
- >>> ne.to_python(0)
- 0
- """
- not_empty = True
-
- messages = dict(
- empty=_('Please enter a value'))
-
- def _validate_python(self, value, state):
- if value == 0:
- # This isn't "empty" for this definition.
- return value
- if not value:
- raise Invalid(self.message('empty', state), value, state)
-
-
-class Empty(FancyValidator):
- """
- Invalid unless the value is empty. Use cleverly, if at all.
-
- Examples::
-
- >>> Empty.to_python(0)
- Traceback (most recent call last):
- ...
- Invalid: You cannot enter a value here
- """
-
- messages = dict(
- notEmpty=_('You cannot enter a value here'))
-
- def _validate_python(self, value, state):
- if value or value == 0:
- raise Invalid(self.message('notEmpty', state), value, state)
-
-
-class Regex(FancyValidator):
- """
- Invalid if the value doesn't match the regular expression `regex`.
-
- The regular expression can be a compiled re object, or a string
- which will be compiled for you.
-
- Use strip=True if you want to strip the value before validation,
- and as a form of conversion (often useful).
-
- Examples::
-
- >>> cap = Regex(r'^[A-Z]+$')
- >>> cap.to_python('ABC')
- 'ABC'
-
- Note that ``.from_python()`` calls (in general) do not validate
- the input::
-
- >>> cap.from_python('abc')
- 'abc'
- >>> cap(accept_python=False).from_python('abc')
- Traceback (most recent call last):
- ...
- Invalid: The input is not valid
- >>> cap.to_python(1)
- Traceback (most recent call last):
- ...
- Invalid: The input must be a string (not a <type 'int'>: 1)
- >>> Regex(r'^[A-Z]+$', strip=True).to_python(' ABC ')
- 'ABC'
- >>> Regex(r'this', regexOps=('I',)).to_python('THIS')
- 'THIS'
- """
-
- regexOps = ()
- strip = False
- regex = None
-
- __unpackargs__ = ('regex',)
-
- messages = dict(
- invalid=_('The input is not valid'))
-
- def __init__(self, *args, **kw):
- FancyValidator.__init__(self, *args, **kw)
- if isinstance(self.regex, str):
- ops = 0
- assert not isinstance(self.regexOps, str), (
- "regexOps should be a list of options from the re module "
- "(names, or actual values)")
- for op in self.regexOps:
- if isinstance(op, str):
- ops |= getattr(re, op)
- else:
- ops |= op
- self.regex = re.compile(self.regex, ops)
-
- def _validate_python(self, value, state):
- self.assert_string(value, state)
- if self.strip and isinstance(value, str):
- value = value.strip()
- if not self.regex.search(value):
- raise Invalid(self.message('invalid', state), value, state)
-
- def _convert_to_python(self, value, state):
- if self.strip and isinstance(value, str):
- return value.strip()
- return value
-
-
-class PlainText(Regex):
- """
- Test that the field contains only letters, numbers, underscore,
- and the hyphen. Subclasses Regex.
-
- Examples::
-
- >>> PlainText.to_python('_this9_')
- '_this9_'
- >>> PlainText.from_python(' this ')
- ' this '
- >>> PlainText(accept_python=False).from_python(' this ')
- Traceback (most recent call last):
- ...
- Invalid: Enter only letters, numbers, or _ (underscore)
- >>> PlainText(strip=True).to_python(' this ')
- 'this'
- >>> PlainText(strip=True).from_python(' this ')
- 'this'
- """
-
- regex = r"^[a-zA-Z_\-0-9]*$"
-
- messages = dict(
- invalid=_('Enter only letters, numbers, or _ (underscore)'))
-
-
-class OneOf(FancyValidator):
- """
- Tests that the value is one of the members of a given list.
-
- If ``testValueList=True``, then if the input value is a list or
- tuple, all the members of the sequence will be checked (i.e., the
- input must be a subset of the allowed values).
-
- Use ``hideList=True`` to keep the list of valid values out of the
- error message in exceptions.
-
- Examples::
-
- >>> oneof = OneOf([1, 2, 3])
- >>> oneof.to_python(1)
- 1
- >>> oneof.to_python(4)
- Traceback (most recent call last):
- ...
- Invalid: Value must be one of: 1; 2; 3 (not 4)
- >>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
- [2, 3, [1, 2, 3]]
- >>> oneof.to_python([2, 3, [1, 2, 3]])
- Traceback (most recent call last):
- ...
- Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
- """
-
- list = None
- testValueList = False
- hideList = False
-
- __unpackargs__ = ('list',)
-
- messages = dict(
- invalid=_('Invalid value'),
- notIn=_('Value must be one of: %(items)s (not %(value)r)'))
-
- def _validate_python(self, value, state):
- if self.testValueList and isinstance(value, (list, tuple)):
- for v in value:
- self._validate_python(v, state)
- else:
- if not value in self.list:
- if self.hideList:
- raise Invalid(self.message('invalid', state), value, state)
- else:
- try:
- items = '; '.join(map(str, self.list))
- except UnicodeError:
- items = '; '.join(map(str, self.list))
- raise Invalid(
- self.message('notIn', state,
- items=items, value=value), value, state)
-
- @property
- def accept_iterator(self):
- return self.testValueList
-
-
-class DictConverter(FancyValidator):
- """
- Converts values based on a dictionary which has values as keys for
- the resultant values.
-
- If ``allowNull`` is passed, it will not balk if a false value
- (e.g., '' or None) is given (it will return None in these cases).
-
- to_python takes keys and gives values, from_python takes values and
- gives keys.
-
- If you give hideDict=True, then the contents of the dictionary
- will not show up in error messages.
-
- Examples::
-
- >>> dc = DictConverter({1: 'one', 2: 'two'})
- >>> dc.to_python(1)
- 'one'
- >>> dc.from_python('one')
- 1
- >>> dc.to_python(3)
- Traceback (most recent call last):
- ....
- Invalid: Enter a value from: 1; 2
- >>> dc2 = dc(hideDict=True)
- >>> dc2.hideDict
- True
- >>> dc2.dict
- {1: 'one', 2: 'two'}
- >>> dc2.to_python(3)
- Traceback (most recent call last):
- ....
- Invalid: Choose something
- >>> dc.from_python('three')
- Traceback (most recent call last):
- ....
- Invalid: Nothing in my dictionary goes by the value 'three'. Choose one of: 'one'; 'two'
- """
-
- messages = dict(
- keyNotFound=_('Choose something'),
- chooseKey=_('Enter a value from: %(items)s'),
- valueNotFound=_('That value is not known'),
- chooseValue=_('Nothing in my dictionary goes by the value %(value)s.'
- ' Choose one of: %(items)s'))
-
- dict = None
- hideDict = False
-
- __unpackargs__ = ('dict',)
-
- def _convert_to_python(self, value, state):
- try:
- return self.dict[value]
- except KeyError:
- if self.hideDict:
- raise Invalid(self.message('keyNotFound', state), value, state)
- else:
- items = sorted(self.dict)
- items = '; '.join(map(repr, items))
- raise Invalid(self.message('chooseKey',
- state, items=items), value, state)
-
- def _convert_from_python(self, value, state):
- for k, v in self.dict.items():
- if value == v:
- return k
- if self.hideDict:
- raise Invalid(self.message('valueNotFound', state), value, state)
- else:
- items = '; '.join(map(repr, iter(self.dict.values())))
- raise Invalid(
- self.message('chooseValue', state,
- value=repr(value), items=items), value, state)
-
-
-class IndexListConverter(FancyValidator):
- """
- Converts a index (which may be a string like '2') to the value in
- the given list.
-
- Examples::
-
- >>> index = IndexListConverter(['zero', 'one', 'two'])
- >>> index.to_python(0)
- 'zero'
- >>> index.from_python('zero')
- 0
- >>> index.to_python('1')
- 'one'
- >>> index.to_python(5)
- Traceback (most recent call last):
- Invalid: Index out of range
- >>> index(not_empty=True).to_python(None)
- Traceback (most recent call last):
- Invalid: Please enter a value
- >>> index.from_python('five')
- Traceback (most recent call last):
- Invalid: Item 'five' was not found in the list
- """
-
- list = None
-
- __unpackargs__ = ('list',)
-
- messages = dict(
- integer=_('Must be an integer index'),
- outOfRange=_('Index out of range'),
- notFound=_('Item %(value)s was not found in the list'))
-
- def _convert_to_python(self, value, state):
- try:
- value = int(value)
- except (ValueError, TypeError):
- raise Invalid(self.message('integer', state), value, state)
- try:
- return self.list[value]
- except IndexError:
- raise Invalid(self.message('outOfRange', state), value, state)
-
- def _convert_from_python(self, value, state):
- for i, v in enumerate(self.list):
- if v == value:
- return i
- raise Invalid(
- self.message('notFound', state, value=repr(value)), value, state)
-
-
-class DateValidator(FancyValidator):
- """
- Validates that a date is within the given range. Be sure to call
- DateConverter first if you aren't expecting mxDateTime input.
-
- ``earliest_date`` and ``latest_date`` may be functions; if so,
- they will be called each time before validating.
-
- ``after_now`` means a time after the current timestamp; note that
- just a few milliseconds before now is invalid! ``today_or_after``
- is more permissive, and ignores hours and minutes.
-
- Examples::
-
- >>> from datetime import datetime, timedelta
- >>> d = DateValidator(earliest_date=datetime(2003, 1, 1))
- >>> d.to_python(datetime(2004, 1, 1))
- datetime.datetime(2004, 1, 1, 0, 0)
- >>> d.to_python(datetime(2002, 1, 1))
- Traceback (most recent call last):
- ...
- Invalid: Date must be after Wednesday, 01 January 2003
- >>> d.to_python(datetime(2003, 1, 1))
- datetime.datetime(2003, 1, 1, 0, 0)
- >>> d = DateValidator(after_now=True)
- >>> now = datetime.now()
- >>> d.to_python(now+timedelta(seconds=5)) == now+timedelta(seconds=5)
- True
- >>> d.to_python(now-timedelta(days=1))
- Traceback (most recent call last):
- ...
- Invalid: The date must be sometime in the future
- >>> d.to_python(now+timedelta(days=1)) > now
- True
- >>> d = DateValidator(today_or_after=True)
- >>> d.to_python(now) == now
- True
-
- """
-
- earliest_date = None
- latest_date = None
- after_now = False
- # Like after_now, but just after this morning:
- today_or_after = False
- # Use None or 'datetime' for the datetime module in the standard lib,
- # or 'mxDateTime' to force the mxDateTime module
- datetime_module = None
-
- messages = dict(
- after=_('Date must be after %(date)s'),
- before=_('Date must be before %(date)s'),
- # Double %'s, because this will be substituted twice:
- date_format=_('%%A, %%d %%B %%Y'),
- future=_('The date must be sometime in the future'))
-
- def _validate_python(self, value, state):
- date_format = self.message('date_format', state)
- if (str is not str # Python 2
- and isinstance(date_format, str)):
- # strftime uses the locale encoding, not Unicode
- encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
- date_format = date_format.encode(encoding)
- else:
- encoding = None
- if self.earliest_date:
- if callable(self.earliest_date):
- earliest_date = self.earliest_date()
- else:
- earliest_date = self.earliest_date
- if value < earliest_date:
- date_formatted = earliest_date.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
- raise Invalid(
- self.message('after', state, date=date_formatted),
- value, state)
- if self.latest_date:
- if callable(self.latest_date):
- latest_date = self.latest_date()
- else:
- latest_date = self.latest_date
- if value > latest_date:
- date_formatted = latest_date.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
- raise Invalid(
- self.message('before', state, date=date_formatted),
- value, state)
- if self.after_now:
- dt_mod = import_datetime(self.datetime_module)
- now = datetime_now(dt_mod)
- if value < now:
- date_formatted = now.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
- raise Invalid(
- self.message('future', state, date=date_formatted),
- value, state)
- if self.today_or_after:
- dt_mod = import_datetime(self.datetime_module)
- now = datetime_now(dt_mod)
- today = datetime_makedate(dt_mod,
- now.year, now.month, now.day)
- value_as_date = datetime_makedate(
- dt_mod, value.year, value.month, value.day)
- if value_as_date < today:
- date_formatted = now.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
- raise Invalid(
- self.message('future', state, date=date_formatted),
- value, state)
-
-
-class Bool(FancyValidator):
- """
- Always Valid, returns True or False based on the value and the
- existance of the value.
-
- If you want to convert strings like ``'true'`` to booleans, then
- use ``StringBool``.
-
- Examples::
-
- >>> Bool.to_python(0)
- False
- >>> Bool.to_python(1)
- True
- >>> Bool.to_python('')
- False
- >>> Bool.to_python(None)
- False
- """
-
- if_missing = False
-
- def _convert_to_python(self, value, state):
- return bool(value)
-
- _convert_from_python = _convert_to_python
-
- def empty_value(self, value):
- return False
-
-
-class RangeValidator(FancyValidator):
- """This is an abstract base class for Int and Number.
-
- It verifies that a value is within range. It accepts min and max
- values in the constructor.
-
- (Since this is an abstract base class, the tests are in Int and Number.)
-
- """
-
- messages = dict(
- tooLow=_('Please enter a number that is %(min)s or greater'),
- tooHigh=_('Please enter a number that is %(max)s or smaller'))
-
- min = None
- max = None
-
- def _validate_python(self, value, state):
- if self.min is not None:
- if value < self.min:
- msg = self.message('tooLow', state, min=self.min)
- raise Invalid(msg, value, state)
- if self.max is not None:
- if value > self.max:
- msg = self.message('tooHigh', state, max=self.max)
- raise Invalid(msg, value, state)
-
-
-class Int(RangeValidator):
- """Convert a value to an integer.
-
- Example::
-
- >>> Int.to_python('10')
- 10
- >>> Int.to_python('ten')
- Traceback (most recent call last):
- ...
- Invalid: Please enter an integer value
- >>> Int(min=5).to_python('6')
- 6
- >>> Int(max=10).to_python('11')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a number that is 10 or smaller
-
- """
-
- messages = dict(
- integer=_('Please enter an integer value'))
-
- def _convert_to_python(self, value, state):
- try:
- return int(value)
- except (ValueError, TypeError):
- raise Invalid(self.message('integer', state), value, state)
-
- _convert_from_python = _convert_to_python
-
-
-class Number(RangeValidator):
- """Convert a value to a float or integer.
-
- Tries to convert it to an integer if no information is lost.
-
- Example::
-
- >>> Number.to_python('10')
- 10
- >>> Number.to_python('10.5')
- 10.5
- >>> Number.to_python('ten')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a number
- >>> Number.to_python([1.2])
- Traceback (most recent call last):
- ...
- Invalid: Please enter a number
- >>> Number(min=5).to_python('6.5')
- 6.5
- >>> Number(max=10.5).to_python('11.5')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a number that is 10.5 or smaller
-
- """
-
- messages = dict(
- number=_('Please enter a number'))
-
- def _convert_to_python(self, value, state):
- try:
- value = float(value)
- try:
- int_value = int(value)
- except OverflowError:
- int_value = None
- if value == int_value:
- return int_value
- return value
- except (ValueError, TypeError):
- raise Invalid(self.message('number', state), value, state)
-
-
-class ByteString(FancyValidator):
- """Convert to byte string, treating empty things as the empty string.
-
- Under Python 2.x you can also use the alias `String` for this validator.
-
- Also takes a `max` and `min` argument, and the string length must fall
- in that range.
-
- Also you may give an `encoding` argument, which will encode any unicode
- that is found. Lists and tuples are joined with `list_joiner`
- (default ``', '``) in ``from_python``.
-
- ::
-
- >>> ByteString(min=2).to_python('a')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value 2 characters long or more
- >>> ByteString(max=10).to_python('xxxxxxxxxxx')
- Traceback (most recent call last):
- ...
- Invalid: Enter a value not more than 10 characters long
- >>> ByteString().from_python(None)
- ''
- >>> ByteString().from_python([])
- ''
- >>> ByteString().to_python(None)
- ''
- >>> ByteString(min=3).to_python(None)
- Traceback (most recent call last):
- ...
- Invalid: Please enter a value
- >>> ByteString(min=1).to_python('')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a value
-
- """
-
- min = None
- max = None
- not_empty = None
- encoding = None
- list_joiner = ', '
-
- messages = dict(
- tooLong=_('Enter a value not more than %(max)i characters long'),
- tooShort=_('Enter a value %(min)i characters long or more'))
-
- def __initargs__(self, new_attrs):
- if self.not_empty is None and self.min:
- self.not_empty = True
-
- def _convert_to_python(self, value, state):
- if value is None:
- value = ''
- elif not isinstance(value, str):
- try:
- value = bytes(value)
- except UnicodeEncodeError:
- value = str(value)
- if self.encoding is not None and isinstance(value, str):
- value = value.encode(self.encoding)
- return value
-
- def _convert_from_python(self, value, state):
- if value is None:
- value = ''
- elif not isinstance(value, str):
- if isinstance(value, (list, tuple)):
- value = self.list_joiner.join(
- self._convert_from_python(v, state) for v in value)
- try:
- value = str(value)
- except UnicodeEncodeError:
- value = str(value)
- if self.encoding is not None and isinstance(value, str):
- value = value.encode(self.encoding)
- if self.strip:
- value = value.strip()
- return value
-
- def _validate_other(self, value, state):
- if self.max is None and self.min is None:
- return
- if value is None:
- value = ''
- elif not isinstance(value, str):
- try:
- value = str(value)
- except UnicodeEncodeError:
- value = str(value)
- if self.max is not None and len(value) > self.max:
- raise Invalid(
- self.message('tooLong', state, max=self.max), value, state)
- if self.min is not None and len(value) < self.min:
- raise Invalid(
- self.message('tooShort', state, min=self.min), value, state)
-
- def empty_value(self, value):
- return ''
-
-
-class UnicodeString(ByteString):
- """Convert things to unicode string.
-
- This is implemented as a specialization of the ByteString class.
-
- Under Python 3.x you can also use the alias `String` for this validator.
-
- In addition to the String arguments, an encoding argument is also
- accepted. By default the encoding will be utf-8. You can overwrite
- this using the encoding parameter. You can also set inputEncoding
- and outputEncoding differently. An inputEncoding of None means
- "do not decode", an outputEncoding of None means "do not encode".
-
- All converted strings are returned as Unicode strings.
-
- ::
-
- >>> UnicodeString().to_python(None)
- u''
- >>> UnicodeString().to_python([])
- u''
- >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
- u'Ni Ni Ni'
-
- """
- encoding = 'utf-8'
- inputEncoding = NoDefault
- outputEncoding = NoDefault
- messages = dict(
- badEncoding=_('Invalid data or incorrect encoding'))
-
- def __init__(self, **kw):
- ByteString.__init__(self, **kw)
- if self.inputEncoding is NoDefault:
- self.inputEncoding = self.encoding
- if self.outputEncoding is NoDefault:
- self.outputEncoding = self.encoding
-
- def _convert_to_python(self, value, state):
- if not value:
- return ''
- if isinstance(value, str):
- return value
- if not isinstance(value, str):
- if hasattr(value, '__unicode__'):
- value = str(value)
- return value
- if not (str is str # Python 3
- and isinstance(value, bytes) and self.inputEncoding):
- value = str(value)
- if self.inputEncoding and not isinstance(value, str):
- try:
- value = str(value, self.inputEncoding)
- except UnicodeDecodeError:
- raise Invalid(self.message('badEncoding', state), value, state)
- except TypeError:
- raise Invalid(
- self.message('badType', state,
- type=type(value), value=value), value, state)
- return value
-
- def _convert_from_python(self, value, state):
- if not isinstance(value, str):
- if hasattr(value, '__unicode__'):
- value = str(value)
- else:
- value = str(value)
- if self.outputEncoding and isinstance(value, str):
- value = value.encode(self.outputEncoding)
- return value
-
- def empty_value(self, value):
- return ''
-
-
-# Provide proper alias for native strings
-
-String = UnicodeString if str is str else ByteString
-
-
-class Set(FancyValidator):
- """
- This is for when you think you may return multiple values for a
- certain field.
-
- This way the result will always be a list, even if there's only
- one result. It's equivalent to ForEach(convert_to_list=True).
-
- If you give ``use_set=True``, then it will return an actual
- ``set`` object.
-
- ::
-
- >>> Set.to_python(None)
- []
- >>> Set.to_python('this')
- ['this']
- >>> Set.to_python(('this', 'that'))
- ['this', 'that']
- >>> s = Set(use_set=True)
- >>> s.to_python(None)
- set([])
- >>> s.to_python('this')
- set(['this'])
- >>> s.to_python(('this',))
- set(['this'])
- """
-
- use_set = False
-
- if_missing = ()
- accept_iterator = True
-
- def _convert_to_python(self, value, state):
- if self.use_set:
- if isinstance(value, set):
- return value
- elif isinstance(value, (list, tuple)):
- return set(value)
- elif value is None:
- return set()
- else:
- return set([value])
- else:
- if isinstance(value, list):
- return value
- elif isinstance(value, set):
- return list(value)
- elif isinstance(value, tuple):
- return list(value)
- elif value is None:
- return []
- else:
- return [value]
-
- def empty_value(self, value):
- if self.use_set:
- return set()
- else:
- return []
-
-
-class Email(FancyValidator):
- r"""
- Validate an email address.
-
- If you pass ``resolve_domain=True``, then it will try to resolve
- the domain name to make sure it's valid. This takes longer, of
- course. You must have the `dnspython <http://www.dnspython.org/>`__ modules
- installed to look up DNS (MX and A) records.
-
- ::
-
- >>> e = Email()
- >>> e.to_python(' test@foo.com ')
- 'test@foo.com'
- >>> e.to_python('test')
- Traceback (most recent call last):
- ...
- Invalid: An email address must contain a single @
- >>> e.to_python('test@foobar')
- Traceback (most recent call last):
- ...
- Invalid: The domain portion of the email address is invalid (the portion after the @: foobar)
- >>> e.to_python('test@foobar.com.5')
- Traceback (most recent call last):
- ...
- Invalid: The domain portion of the email address is invalid (the portion after the @: foobar.com.5)
- >>> e.to_python('test@foo..bar.com')
- Traceback (most recent call last):
- ...
- Invalid: The domain portion of the email address is invalid (the portion after the @: foo..bar.com)
- >>> e.to_python('test@.foo.bar.com')
- Traceback (most recent call last):
- ...
- Invalid: The domain portion of the email address is invalid (the portion after the @: .foo.bar.com)
- >>> e.to_python('nobody@xn--m7r7ml7t24h.com')
- 'nobody@xn--m7r7ml7t24h.com'
- >>> e.to_python('o*reilly@test.com')
- 'o*reilly@test.com'
- >>> e = Email(resolve_domain=True)
- >>> e.resolve_domain
- True
- >>> e.to_python('doesnotexist@colorstudy.com')
- 'doesnotexist@colorstudy.com'
- >>> e.to_python('test@nyu.edu')
- 'test@nyu.edu'
- >>> # NOTE: If you do not have dnspython installed this example won't work:
- >>> e.to_python('test@thisdomaindoesnotexistithinkforsure.com')
- Traceback (most recent call last):
- ...
- Invalid: The domain of the email address does not exist (the portion after the @: thisdomaindoesnotexistithinkforsure.com)
- >>> e.to_python('test@google.com')
- u'test@google.com'
- >>> e = Email(not_empty=False)
- >>> e.to_python('')
-
- """
-
- resolve_domain = False
- resolve_timeout = 10 # timeout in seconds when resolving domains
-
- usernameRE = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
- domainRE = re.compile(r'''
- ^(?:[a-z0-9][a-z0-9\-]{,62}\.)+ # subdomain
- (?:[a-z]{2,63}|xn--[a-z0-9\-]{2,59})$ # top level domain
- ''', re.I | re.VERBOSE)
-
- messages = dict(
- empty=_('Please enter an email address'),
- noAt=_('An email address must contain a single @'),
- badUsername=_('The username portion of the email address is invalid'
- ' (the portion before the @: %(username)s)'),
- socketError=_('An error occured when trying to connect to the server:'
- ' %(error)s'),
- badDomain=_('The domain portion of the email address is invalid'
- ' (the portion after the @: %(domain)s)'),
- domainDoesNotExist=_('The domain of the email address does not exist'
- ' (the portion after the @: %(domain)s)'))
-
- def __init__(self, *args, **kw):
- FancyValidator.__init__(self, *args, **kw)
- if self.resolve_domain:
- if not have_dns:
- warnings.warn(
- "dnspython <http://www.dnspython.org/> is not installed on"
- " your system (or the dns.resolver package cannot be found)."
- " I cannot resolve domain names in addresses")
- raise ImportError("no module named dns.resolver")
-
- def _validate_python(self, value, state):
- if not value:
- raise Invalid(self.message('empty', state), value, state)
- value = value.strip()
- splitted = value.split('@', 1)
- try:
- username, domain = splitted
- except ValueError:
- raise Invalid(self.message('noAt', state), value, state)
- if not self.usernameRE.search(username):
- raise Invalid(
- self.message('badUsername', state, username=username),
- value, state)
- try:
- idna_domain = [idna.ToASCII(p) for p in domain.split('.')]
- if str is str: # Python 3
- idna_domain = [p.decode('ascii') for p in idna_domain]
- idna_domain = '.'.join(idna_domain)
- except UnicodeError:
- # UnicodeError: label empty or too long
- # This exception might happen if we have an invalid domain name part
- # (for example test@.foo.bar.com)
- raise Invalid(
- self.message('badDomain', state, domain=domain),
- value, state)
- if not self.domainRE.search(idna_domain):
- raise Invalid(
- self.message('badDomain', state, domain=domain),
- value, state)
- if self.resolve_domain:
- assert have_dns, "dnspython should be available"
- global socket
- if socket is None:
- import socket
- try:
- try:
- dns.resolver.query(domain, 'MX')
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
- try:
- dns.resolver.query(domain, 'A')
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
- raise Invalid(
- self.message('domainDoesNotExist',
- state, domain=domain), value, state)
- except (socket.error, dns.exception.DNSException) as e:
- raise Invalid(
- self.message('socketError', state, error=e), value, state)
-
- def _convert_to_python(self, value, state):
- return value.strip()
-
-
-class URL(FancyValidator):
- """
- Validate a URL, either http://... or https://. If check_exists
- is true, then we'll actually make a request for the page.
-
- If add_http is true, then if no scheme is present we'll add
- http://
-
- ::
-
- >>> u = URL(add_http=True)
- >>> u.to_python('foo.com')
- 'http://foo.com'
- >>> u.to_python('http://hahaha.ha/bar.html')
- 'http://hahaha.ha/bar.html'
- >>> u.to_python('http://xn--m7r7ml7t24h.com')
- 'http://xn--m7r7ml7t24h.com'
- >>> u.to_python('http://xn--c1aay4a.xn--p1ai')
- 'http://xn--c1aay4a.xn--p1ai'
- >>> u.to_python('http://foo.com/test?bar=baz&fleem=morx')
- 'http://foo.com/test?bar=baz&fleem=morx'
- >>> u.to_python('http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest')
- 'http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest'
- >>> u.to_python('http://foo.com:8000/test.html')
- 'http://foo.com:8000/test.html'
- >>> u.to_python('http://foo.com/something\\nelse')
- Traceback (most recent call last):
- ...
- Invalid: That is not a valid URL
- >>> u.to_python('https://test.com')
- 'https://test.com'
- >>> u.to_python('http://test')
- Traceback (most recent call last):
- ...
- Invalid: You must provide a full domain name (like test.com)
- >>> u.to_python('http://test..com')
- Traceback (most recent call last):
- ...
- Invalid: That is not a valid URL
- >>> u = URL(add_http=False, check_exists=True)
- >>> u.to_python('http://google.com')
- 'http://google.com'
- >>> u.to_python('google.com')
- Traceback (most recent call last):
- ...
- Invalid: You must start your URL with http://, https://, etc
- >>> u.to_python('http://www.formencode.org/does/not/exist/page.html')
- Traceback (most recent call last):
- ...
- Invalid: The server responded that the page could not be found
- >>> u.to_python('http://this.domain.does.not.exist.example.org/test.html')
- ... # doctest: +ELLIPSIS
- Traceback (most recent call last):
- ...
- Invalid: An error occured when trying to connect to the server: ...
-
- If you want to allow addresses without a TLD (e.g., ``localhost``) you can do::
-
- >>> URL(require_tld=False).to_python('http://localhost')
- 'http://localhost'
-
- By default, internationalized domain names (IDNA) in Unicode will be
- accepted and encoded to ASCII using Punycode (as described in RFC 3490).
- You may set allow_idna to False to change this behavior::
-
- >>> URL(allow_idna=True).to_python(
- ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
- 'http://xn--c1aay4a.xn--p1ai'
- >>> URL(allow_idna=True, add_http=True).to_python(
- ... '\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
- 'http://xn--c1aay4a.xn--p1ai'
- >>> URL(allow_idna=False).to_python(
- ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
- Traceback (most recent call last):
- ...
- Invalid: That is not a valid URL
-
- """
-
- add_http = True
- allow_idna = True
- check_exists = False
- require_tld = True
-
- url_re = re.compile(r'''
- ^(http|https)://
- (?:[%:\w]*@)? # authenticator
- (?: # ip or domain
- (?P<ip>(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|
- (?P<domain>[a-z0-9][a-z0-9\-]{,62}\.)* # subdomain
- (?P<tld>[a-z]{2,63}|xn--[a-z0-9\-]{2,59}) # top level domain
- )
- (?::[0-9]{1,5})? # port
- # files/delims/etc
- (?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
- $
- ''', re.I | re.VERBOSE)
-
- scheme_re = re.compile(r'^[a-zA-Z]+:')
-
- messages = dict(
- noScheme=_('You must start your URL with http://, https://, etc'),
- badURL=_('That is not a valid URL'),
- httpError=_('An error occurred when trying to access the URL:'
- ' %(error)s'),
- socketError=_('An error occured when trying to connect to the server:'
- ' %(error)s'),
- notFound=_('The server responded that the page could not be found'),
- status=_('The server responded with a bad status code (%(status)s)'),
- noTLD=_('You must provide a full domain name (like %(domain)s.com)'))
-
- def _convert_to_python(self, value, state):
- value = value.strip()
- if self.add_http:
- if not self.scheme_re.search(value):
- value = 'http://' + value
- if self.allow_idna:
- value = self._encode_idna(value)
- match = self.scheme_re.search(value)
- if not match:
- raise Invalid(self.message('noScheme', state), value, state)
- value = match.group(0).lower() + value[len(match.group(0)):]
- match = self.url_re.search(value)
- if not match:
- raise Invalid(self.message('badURL', state), value, state)
- if self.require_tld and not match.group('domain'):
- raise Invalid(
- self.message('noTLD', state, domain=match.group('tld')),
- value, state)
- if self.check_exists and value.startswith(('http://', 'https://')):
- self._check_url_exists(value, state)
- return value
-
- def _encode_idna(self, url):
- global urlparse
- if urlparse is None:
- import urllib.parse
- try:
- scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
- url)
- except ValueError:
- return url
- try:
- netloc = netloc.encode('idna')
- if str is str: # Python 3
- netloc = netloc.decode('ascii')
- return str(urllib.parse.urlunparse((scheme, netloc,
- path, params, query, fragment)))
- except UnicodeError:
- return url
-
- def _check_url_exists(self, url, state):
- global httplib, urlparse, socket
- if httplib is None:
- import http.client
- if urlparse is None:
- import urllib.parse
- if socket is None:
- import socket
- scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
- url, 'http')
- if scheme == 'https':
- ConnClass = http.client.HTTPSConnection
- else:
- ConnClass = http.client.HTTPConnection
- try:
- conn = ConnClass(netloc)
- if params:
- path += ';' + params
- if query:
- path += '?' + query
- conn.request('HEAD', path)
- res = conn.getresponse()
- except http.client.HTTPException as e:
- raise Invalid(
- self.message('httpError', state, error=e), state, url)
- except socket.error as e:
- raise Invalid(
- self.message('socketError', state, error=e), state, url)
- else:
- if res.status == 404:
- raise Invalid(
- self.message('notFound', state), state, url)
- if not 200 <= res.status < 500:
- raise Invalid(
- self.message('status', state, status=res.status),
- state, url)
-
-
-class XRI(FancyValidator):
- r"""
- Validator for XRIs.
-
- It supports both i-names and i-numbers, of the first version of the XRI
- standard.
-
- ::
-
- >>> inames = XRI(xri_type="i-name")
- >>> inames.to_python(" =John.Smith ")
- '=John.Smith'
- >>> inames.to_python("@Free.Software.Foundation")
- '@Free.Software.Foundation'
- >>> inames.to_python("Python.Software.Foundation")
- Traceback (most recent call last):
- ...
- Invalid: The type of i-name is not defined; it may be either individual or organizational
- >>> inames.to_python("http://example.org")
- Traceback (most recent call last):
- ...
- Invalid: The type of i-name is not defined; it may be either individual or organizational
- >>> inames.to_python("=!2C43.1A9F.B6F6.E8E6")
- Traceback (most recent call last):
- ...
- Invalid: "!2C43.1A9F.B6F6.E8E6" is an invalid i-name
- >>> iname_with_schema = XRI(True, xri_type="i-name")
- >>> iname_with_schema.to_python("=Richard.Stallman")
- 'xri://=Richard.Stallman'
- >>> inames.to_python("=John Smith")
- Traceback (most recent call last):
- ...
- Invalid: "John Smith" is an invalid i-name
- >>> inumbers = XRI(xri_type="i-number")
- >>> inumbers.to_python("!!1000!de21.4536.2cb2.8074")
- '!!1000!de21.4536.2cb2.8074'
- >>> inumbers.to_python("@!1000.9554.fabd.129c!2847.df3c")
- '@!1000.9554.fabd.129c!2847.df3c'
-
- """
-
- iname_valid_pattern = re.compile(r"""
- ^
- [\w]+ # A global alphanumeric i-name
- (\.[\w]+)* # An i-name with dots
- (\*[\w]+(\.[\w]+)*)* # A community i-name
- $
- """, re.VERBOSE | re.UNICODE)
-
- iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
- """@cvar: These characters must not be at the beggining of the i-name"""
-
- inumber_pattern = re.compile(r"""
- ^
- (
- [=@]! # It's a personal or organization i-number
- |
- !! # It's a network i-number
- )
- [\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3} # A global i-number
- (![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})* # Zero or more sub i-numbers
- $
- """, re.VERBOSE | re.IGNORECASE)
-
- messages = dict(
- noType=_('The type of i-name is not defined;'
- ' it may be either individual or organizational'),
- repeatedChar=_('Dots and dashes may not be repeated consecutively'),
- badIname=_('"%(iname)s" is an invalid i-name'),
- badInameStart=_('i-names may not start with numbers'
- ' nor punctuation marks'),
- badInumber=_('"%(inumber)s" is an invalid i-number'),
- badType=_('The XRI must be a string (not a %(type)s: %(value)r)'),
- badXri=_('"%(xri_type)s" is not a valid type of XRI'))
-
- def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
- """Create an XRI validator.
-
- @param add_xri: Should the schema be added if not present?
- Officially it's optional.
- @type add_xri: C{bool}
- @param xri_type: What type of XRI should be validated?
- Possible values: C{i-name} or C{i-number}.
- @type xri_type: C{str}
-
- """
- self.add_xri = add_xri
- assert xri_type in ('i-name', 'i-number'), (
- 'xri_type must be "i-name" or "i-number"')
- self.xri_type = xri_type
- super(XRI, self).__init__(**kwargs)
-
- def _convert_to_python(self, value, state):
- """Prepend the 'xri://' schema if needed and remove trailing spaces"""
- value = value.strip()
- if self.add_xri and not value.startswith('xri://'):
- value = 'xri://' + value
- return value
-
- def _validate_python(self, value, state=None):
- """Validate an XRI
-
- @raise Invalid: If at least one of the following conditions in met:
- - C{value} is not a string.
- - The XRI is not a personal, organizational or network one.
- - The relevant validator (i-name or i-number) considers the XRI
- is not valid.
-
- """
- if not isinstance(value, str):
- raise Invalid(
- self.message('badType', state,
- type=str(type(value)), value=value), value, state)
-
- # Let's remove the schema, if any
- if value.startswith('xri://'):
- value = value[6:]
-
- if not value[0] in ('@', '=') and not (
- self.xri_type == 'i-number' and value[0] == '!'):
- raise Invalid(self.message('noType', state), value, state)
-
- if self.xri_type == 'i-name':
- self._validate_iname(value, state)
- else:
- self._validate_inumber(value, state)
-
- def _validate_iname(self, iname, state):
- """Validate an i-name"""
- # The type is not required here:
- iname = iname[1:]
- if '..' in iname or '--' in iname:
- raise Invalid(self.message('repeatedChar', state), iname, state)
- if self.iname_invalid_start.match(iname):
- raise Invalid(self.message('badInameStart', state), iname, state)
- if not self.iname_valid_pattern.match(iname) or '_' in iname:
- raise Invalid(
- self.message('badIname', state, iname=iname), iname, state)
-
- def _validate_inumber(self, inumber, state):
- """Validate an i-number"""
- if not self.__class__.inumber_pattern.match(inumber):
- raise Invalid(
- self.message('badInumber', state,
- inumber=inumber, value=inumber), inumber, state)
-
-
-class OpenId(FancyValidator):
- r"""
- OpenId validator.
-
- ::
- >>> v = OpenId(add_schema=True)
- >>> v.to_python(' example.net ')
- 'http://example.net'
- >>> v.to_python('@TurboGears')
- 'xri://@TurboGears'
- >>> w = OpenId(add_schema=False)
- >>> w.to_python(' example.net ')
- Traceback (most recent call last):
- ...
- Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
- >>> w.to_python('!!1000')
- '!!1000'
- >>> w.to_python('look@me.com')
- Traceback (most recent call last):
- ...
- Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
-
- """
-
- messages = dict(
- badId=_('"%(id)s" is not a valid OpenId'
- ' (it is neither an URL nor an XRI)'))
-
- def __init__(self, add_schema=False, **kwargs):
- """Create an OpenId validator.
-
- @param add_schema: Should the schema be added if not present?
- @type add_schema: C{bool}
-
- """
- self.url_validator = URL(add_http=add_schema)
- self.iname_validator = XRI(add_schema, xri_type="i-name")
- self.inumber_validator = XRI(add_schema, xri_type="i-number")
-
- def _convert_to_python(self, value, state):
- value = value.strip()
- try:
- return self.url_validator.to_python(value, state)
- except Invalid:
- try:
- return self.iname_validator.to_python(value, state)
- except Invalid:
- try:
- return self.inumber_validator.to_python(value, state)
- except Invalid:
- pass
- # It's not an OpenId!
- raise Invalid(self.message('badId', state, id=value), value, state)
-
- def _validate_python(self, value, state):
- self._convert_to_python(value, state)
-
-
-def StateProvince(*kw, **kwargs):
- deprecation_warning("please use formencode.national.USStateProvince")
- from formencode.national import USStateProvince
- return USStateProvince(*kw, **kwargs)
-
-
-def PhoneNumber(*kw, **kwargs):
- deprecation_warning("please use formencode.national.USPhoneNumber")
- from formencode.national import USPhoneNumber
- return USPhoneNumber(*kw, **kwargs)
-
-
-def IPhoneNumberValidator(*kw, **kwargs):
- deprecation_warning(
- "please use formencode.national.InternationalPhoneNumber")
- from formencode.national import InternationalPhoneNumber
- return InternationalPhoneNumber(*kw, **kwargs)
-
-
-class FieldStorageUploadConverter(FancyValidator):
- """
- Handles cgi.FieldStorage instances that are file uploads.
-
- This doesn't do any conversion, but it can detect empty upload
- fields (which appear like normal fields, but have no filename when
- no upload was given).
- """
- def _convert_to_python(self, value, state=None):
- if isinstance(value, cgi.FieldStorage):
- if getattr(value, 'filename', None):
- return value
- raise Invalid('invalid', value, state)
- else:
- return value
-
- def is_empty(self, value):
- if isinstance(value, cgi.FieldStorage):
- return not bool(getattr(value, 'filename', None))
- return FancyValidator.is_empty(self, value)
-
-
-class FileUploadKeeper(FancyValidator):
- """
- Takes two inputs (a dictionary with keys ``static`` and
- ``upload``) and converts them into one value on the Python side (a
- dictionary with ``filename`` and ``content`` keys). The upload
- takes priority over the static value. The filename may be None if
- it can't be discovered.
-
- Handles uploads of both text and ``cgi.FieldStorage`` upload
- values.
-
- This is basically for use when you have an upload field, and you
- want to keep the upload around even if the rest of the form
- submission fails. When converting *back* to the form submission,
- there may be extra values ``'original_filename'`` and
- ``'original_content'``, which may want to use in your form to show
- the user you still have their content around.
-
- To use this, make sure you are using variabledecode, then use
- something like::
-
- <input type="file" name="myfield.upload">
- <input type="hidden" name="myfield.static">
-
- Then in your scheme::
-
- class MyScheme(Scheme):
- myfield = FileUploadKeeper()
-
- Note that big file uploads mean big hidden fields, and lots of
- bytes passed back and forth in the case of an error.
- """
-
- upload_key = 'upload'
- static_key = 'static'
-
- def _convert_to_python(self, value, state):
- upload = value.get(self.upload_key)
- static = value.get(self.static_key, '').strip()
- filename = content = None
- if isinstance(upload, cgi.FieldStorage):
- filename = upload.filename
- content = upload.value
- elif isinstance(upload, str) and upload:
- filename = None
- # @@: Should this encode upload if it is unicode?
- content = upload
- if not content and static:
- filename, content = static.split(None, 1)
- filename = '' if filename == '-' else filename.decode('base64')
- content = content.decode('base64')
- return {'filename': filename, 'content': content}
-
- def _convert_from_python(self, value, state):
- filename = value.get('filename', '')
- content = value.get('content', '')
- if filename or content:
- result = self.pack_content(filename, content)
- return {self.upload_key: '',
- self.static_key: result,
- 'original_filename': filename,
- 'original_content': content}
- else:
- return {self.upload_key: '',
- self.static_key: ''}
-
- def pack_content(self, filename, content):
- enc_filename = self.base64encode(filename) or '-'
- enc_content = (content or '').encode('base64')
- result = '%s %s' % (enc_filename, enc_content)
- return result
-
-
-class DateConverter(FancyValidator):
- """
- Validates and converts a string date, like mm/yy, dd/mm/yy,
- dd-mm-yy, etc. Using ``month_style`` you can support
- the three general styles ``mdy`` = ``us`` = ``mm/dd/yyyy``,
- ``dmy`` = ``euro`` = ``dd/mm/yyyy`` and
- ``ymd`` = ``iso`` = ``yyyy/mm/dd``.
-
- Accepts English month names, also abbreviated. Returns value as a
- datetime object (you can get mx.DateTime objects if you use
- ``datetime_module='mxDateTime'``). Two year dates are assumed to
- be within 1950-2020, with dates from 21-49 being ambiguous and
- signaling an error.
-
- Use accept_day=False if you just want a month/year (like for a
- credit card expiration date).
-
- ::
-
- >>> d = DateConverter()
- >>> d.to_python('12/3/09')
- datetime.date(2009, 12, 3)
- >>> d.to_python('12/3/2009')
- datetime.date(2009, 12, 3)
- >>> d.to_python('2/30/04')
- Traceback (most recent call last):
- ...
- Invalid: That month only has 29 days
- >>> d.to_python('13/2/05')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a month from 1 to 12
- >>> d.to_python('1/1/200')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a four-digit year after 1899
-
- If you change ``month_style`` you can get European-style dates::
-
- >>> d = DateConverter(month_style='dd/mm/yyyy')
- >>> date = d.to_python('12/3/09')
- >>> date
- datetime.date(2009, 3, 12)
- >>> d.from_python(date)
- '12/03/2009'
- """
-
- # set to False if you want only month and year
- accept_day = True
- # allowed month styles: 'mdy' = 'us', 'dmy' = 'euro', 'ymd' = 'iso'
- # also allowed: 'mm/dd/yyyy', 'dd/mm/yyyy', 'yyyy/mm/dd'
- month_style = 'mdy'
- # preferred separator for reverse conversion: '/', '.' or '-'
- separator = '/'
-
- # Use 'datetime' to force the Python datetime module, or
- # 'mxDateTime' to force the mxDateTime module (None means use
- # datetime, or if not present mxDateTime)
- datetime_module = None
-
- _month_names = {
- 'jan': 1, 'january': 1,
- 'feb': 2, 'febuary': 2,
- 'mar': 3, 'march': 3,
- 'apr': 4, 'april': 4,
- 'may': 5,
- 'jun': 6, 'june': 6,
- 'jul': 7, 'july': 7,
- 'aug': 8, 'august': 8,
- 'sep': 9, 'sept': 9, 'september': 9,
- 'oct': 10, 'october': 10,
- 'nov': 11, 'november': 11,
- 'dec': 12, 'december': 12,
- }
-
- _date_re = dict(
- dmy=re.compile(
- r'^\s*(\d\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
- mdy=re.compile(
- r'^\s*(\d\d?|%s)[\-\./\\](\d\d?)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
- ymd=re.compile(
- r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d?)\s*$'
- % '|'.join(_month_names), re.I),
- my=re.compile(
- r'^\s*(\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
- ym=re.compile(
- r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)\s*$'
- % '|'.join(_month_names), re.I))
-
- _formats = dict(d='%d', m='%m', y='%Y')
-
- _human_formats = dict(d=_('DD'), m=_('MM'), y=_('YYYY'))
-
- # Feb. should be leap-year aware (but mxDateTime does catch that)
- _monthDays = {
- 1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
- 9: 30, 10: 31, 11: 30, 12: 31}
-
- messages = dict(
- badFormat=_('Please enter the date in the form %(format)s'),
- monthRange=_('Please enter a month from 1 to 12'),
- invalidDay=_('Please enter a valid day'),
- dayRange=_('That month only has %(days)i days'),
- invalidDate=_('That is not a valid day (%(exception)s)'),
- unknownMonthName=_('Unknown month name: %(month)s'),
- invalidYear=_('Please enter a number for the year'),
- fourDigitYear=_('Please enter a four-digit year after 1899'),
- wrongFormat=_('Please enter the date in the form %(format)s'))
-
- def __init__(self, *args, **kw):
- super(DateConverter, self).__init__(*args, **kw)
- month_style = (self.month_style or DateConverter.month_style).lower()
- accept_day = bool(self.accept_day)
- self.accept_day = self.accept_day
- if month_style in ('mdy',
- 'md', 'mm/dd/yyyy', 'mm/dd', 'us', 'american'):
- month_style = 'mdy'
- elif month_style in ('dmy',
- 'dm', 'dd/mm/yyyy', 'dd/mm', 'euro', 'european'):
- month_style = 'dmy'
- elif month_style in ('ymd',
- 'ym', 'yyyy/mm/dd', 'yyyy/mm', 'iso', 'china', 'chinese'):
- month_style = 'ymd'
- else:
- raise TypeError('Bad month_style: %r' % month_style)
- self.month_style = month_style
- separator = self.separator
- if not separator or separator == 'auto':
- separator = dict(mdy='/', dmy='.', ymd='-')[month_style]
- elif separator not in ('-', '.', '/', '\\'):
- raise TypeError('Bad separator: %r' % separator)
- self.separator = separator
- self.format = separator.join(self._formats[part]
- for part in month_style if part != 'd' or accept_day)
- self.human_format = separator.join(self._human_formats[part]
- for part in month_style if part != 'd' or accept_day)
-
- def _convert_to_python(self, value, state):
- self.assert_string(value, state)
- month_style = self.month_style
- if not self.accept_day:
- month_style = 'ym' if month_style == 'ymd' else 'my'
- match = self._date_re[month_style].search(value)
- if not match:
- raise Invalid(
- self.message('badFormat', state,
- format=self.human_format), value, state)
- groups = match.groups()
- if self.accept_day:
- if month_style == 'mdy':
- month, day, year = groups
- elif month_style == 'dmy':
- day, month, year = groups
- else:
- year, month, day = groups
- day = int(day)
- if not 1 <= day <= 31:
- raise Invalid(self.message('invalidDay', state), value, state)
- else:
- day = 1
- if month_style == 'my':
- month, year = groups
- else:
- year, month = groups
- month = self.make_month(month, state)
- if not 1 <= month <= 12:
- raise Invalid(self.message('monthRange', state), value, state)
- if self._monthDays[month] < day:
- raise Invalid(
- self.message('dayRange', state,
- days=self._monthDays[month]), value, state)
- year = self.make_year(year, state)
- dt_mod = import_datetime(self.datetime_module)
- try:
- return datetime_makedate(dt_mod, year, month, day)
- except ValueError as v:
- raise Invalid(
- self.message('invalidDate', state,
- exception=str(v)), value, state)
-
- def make_month(self, value, state):
- try:
- return int(value)
- except ValueError:
- try:
- return self._month_names[value.lower().strip()]
- except KeyError:
- raise Invalid(
- self.message('unknownMonthName', state,
- month=value), value, state)
-
- def make_year(self, year, state):
- try:
- year = int(year)
- except ValueError:
- raise Invalid(self.message('invalidYear', state), year, state)
- if year <= 20:
- year += 2000
- elif 50 <= year < 100:
- year += 1900
- if 20 < year < 50 or 99 < year < 1900:
- raise Invalid(self.message('fourDigitYear', state), year, state)
- return year
-
- def _convert_from_python(self, value, state):
- if self.if_empty is not NoDefault and not value:
- return ''
- return value.strftime(self.format)
-
-
-class TimeConverter(FancyValidator):
- """
- Converts times in the format HH:MM:SSampm to (h, m, s).
- Seconds are optional.
-
- For ampm, set use_ampm = True. For seconds, use_seconds = True.
- Use 'optional' for either of these to make them optional.
-
- Examples::
-
- >>> tim = TimeConverter()
- >>> tim.to_python('8:30')
- (8, 30)
- >>> tim.to_python('20:30')
- (20, 30)
- >>> tim.to_python('30:00')
- Traceback (most recent call last):
- ...
- Invalid: You must enter an hour in the range 0-23
- >>> tim.to_python('13:00pm')
- Traceback (most recent call last):
- ...
- Invalid: You must enter an hour in the range 1-12
- >>> tim.to_python('12:-1')
- Traceback (most recent call last):
- ...
- Invalid: You must enter a minute in the range 0-59
- >>> tim.to_python('12:02pm')
- (12, 2)
- >>> tim.to_python('12:02am')
- (0, 2)
- >>> tim.to_python('1:00PM')
- (13, 0)
- >>> tim.from_python((13, 0))
- '13:00:00'
- >>> tim2 = tim(use_ampm=True, use_seconds=False)
- >>> tim2.from_python((13, 0))
- '1:00pm'
- >>> tim2.from_python((0, 0))
- '12:00am'
- >>> tim2.from_python((12, 0))
- '12:00pm'
-
- Examples with ``datetime.time``::
-
- >>> v = TimeConverter(use_datetime=True)
- >>> a = v.to_python('18:00')
- >>> a
- datetime.time(18, 0)
- >>> b = v.to_python('30:00')
- Traceback (most recent call last):
- ...
- Invalid: You must enter an hour in the range 0-23
- >>> v2 = TimeConverter(prefer_ampm=True, use_datetime=True)
- >>> v2.from_python(a)
- '6:00:00pm'
- >>> v3 = TimeConverter(prefer_ampm=True,
- ... use_seconds=False, use_datetime=True)
- >>> a = v3.to_python('18:00')
- >>> a
- datetime.time(18, 0)
- >>> v3.from_python(a)
- '6:00pm'
- >>> a = v3.to_python('18:00:00')
- Traceback (most recent call last):
- ...
- Invalid: You may not enter seconds
- """
-
- use_ampm = 'optional'
- prefer_ampm = False
- use_seconds = 'optional'
- use_datetime = False
- # This can be set to make it prefer mxDateTime:
- datetime_module = None
-
- messages = dict(
- noAMPM=_('You must indicate AM or PM'),
- tooManyColon=_('There are too many :\'s'),
- noSeconds=_('You may not enter seconds'),
- secondsRequired=_('You must enter seconds'),
- minutesRequired=_('You must enter minutes (after a :)'),
- badNumber=_('The %(part)s value you gave is not a number: %(number)r'),
- badHour=_('You must enter an hour in the range %(range)s'),
- badMinute=_('You must enter a minute in the range 0-59'),
- badSecond=_('You must enter a second in the range 0-59'))
-
- def _convert_to_python(self, value, state):
- result = self._to_python_tuple(value, state)
- if self.use_datetime:
- dt_mod = import_datetime(self.datetime_module)
- time_class = datetime_time(dt_mod)
- return time_class(*result)
- else:
- return result
-
- def _to_python_tuple(self, value, state):
- time = value.strip()
- explicit_ampm = False
- if self.use_ampm:
- last_two = time[-2:].lower()
- if last_two not in ('am', 'pm'):
- if self.use_ampm != 'optional':
- raise Invalid(self.message('noAMPM', state), value, state)
- offset = 0
- else:
- explicit_ampm = True
- offset = 12 if last_two == 'pm' else 0
- time = time[:-2]
- else:
- offset = 0
- parts = time.split(':', 3)
- if len(parts) > 3:
- raise Invalid(self.message('tooManyColon', state), value, state)
- if len(parts) == 3 and not self.use_seconds:
- raise Invalid(self.message('noSeconds', state), value, state)
- if (len(parts) == 2
- and self.use_seconds and self.use_seconds != 'optional'):
- raise Invalid(self.message('secondsRequired', state), value, state)
- if len(parts) == 1:
- raise Invalid(self.message('minutesRequired', state), value, state)
- try:
- hour = int(parts[0])
- except ValueError:
- raise Invalid(
- self.message('badNumber', state,
- number=parts[0], part='hour'), value, state)
- if explicit_ampm:
- if not 1 <= hour <= 12:
- raise Invalid(
- self.message('badHour', state,
- number=hour, range='1-12'), value, state)
- if hour == 12 and offset == 12:
- # 12pm == 12
- pass
- elif hour == 12 and offset == 0:
- # 12am == 0
- hour = 0
- else:
- hour += offset
- else:
- if not 0 <= hour < 24:
- raise Invalid(
- self.message('badHour', state,
- number=hour, range='0-23'), value, state)
- try:
- minute = int(parts[1])
- except ValueError:
- raise Invalid(
- self.message('badNumber', state,
- number=parts[1], part='minute'), value, state)
- if not 0 <= minute < 60:
- raise Invalid(
- self.message('badMinute', state, number=minute),
- value, state)
- if len(parts) == 3:
- try:
- second = int(parts[2])
- except ValueError:
- raise Invalid(
- self.message('badNumber', state,
- number=parts[2], part='second'), value, state)
- if not 0 <= second < 60:
- raise Invalid(
- self.message('badSecond', state, number=second),
- value, state)
- else:
- second = None
- if second is None:
- return (hour, minute)
- else:
- return (hour, minute, second)
-
- def _convert_from_python(self, value, state):
- if isinstance(value, str):
- return value
- if hasattr(value, 'hour'):
- hour, minute = value.hour, value.minute
- second = value.second
- elif len(value) == 3:
- hour, minute, second = value
- elif len(value) == 2:
- hour, minute = value
- second = 0
- ampm = ''
- if (self.use_ampm == 'optional' and self.prefer_ampm) or (
- self.use_ampm and self.use_ampm != 'optional'):
- ampm = 'am'
- if hour > 12:
- hour -= 12
- ampm = 'pm'
- elif hour == 12:
- ampm = 'pm'
- elif hour == 0:
- hour = 12
- if self.use_seconds:
- return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
- else:
- return '%i:%02i%s' % (hour, minute, ampm)
-
-
-def PostalCode(*kw, **kwargs):
- deprecation_warning("please use formencode.national.USPostalCode")
- from formencode.national import USPostalCode
- return USPostalCode(*kw, **kwargs)
-
-
-class StripField(FancyValidator):
- """
- Take a field from a dictionary, removing the key from the dictionary.
-
- ``name`` is the key. The field value and a new copy of the dictionary
- with that field removed are returned.
-
- >>> StripField('test').to_python({'a': 1, 'test': 2})
- (2, {'a': 1})
- >>> StripField('test').to_python({})
- Traceback (most recent call last):
- ...
- Invalid: The name 'test' is missing
-
- """
-
- __unpackargs__ = ('name',)
-
- messages = dict(
- missing=_('The name %(name)s is missing'))
-
- def _convert_to_python(self, valueDict, state):
- v = valueDict.copy()
- try:
- field = v.pop(self.name)
- except KeyError:
- raise Invalid(
- self.message('missing', state, name=repr(self.name)),
- valueDict, state)
- return field, v
-
- def is_empty(self, value):
- # empty dictionaries don't really apply here
- return False
-
-
-class StringBool(FancyValidator): # originally from TurboGears 1
- """
- Converts a string to a boolean.
-
- Values like 'true' and 'false' are considered True and False,
- respectively; anything in ``true_values`` is true, anything in
- ``false_values`` is false, case-insensitive). The first item of
- those lists is considered the preferred form.
-
- ::
-
- >>> s = StringBool()
- >>> s.to_python('yes'), s.to_python('no')
- (True, False)
- >>> s.to_python(1), s.to_python('N')
- (True, False)
- >>> s.to_python('ye')
- Traceback (most recent call last):
- ...
- Invalid: Value should be 'true' or 'false'
- """
-
- true_values = ['true', 't', 'yes', 'y', 'on', '1']
- false_values = ['false', 'f', 'no', 'n', 'off', '0']
-
- messages = dict(
- string=_('Value should be %(true)r or %(false)r'))
-
- def _convert_to_python(self, value, state):
- if isinstance(value, str):
- value = value.strip().lower()
- if value in self.true_values:
- return True
- if not value or value in self.false_values:
- return False
- raise Invalid(
- self.message('string', state,
- true=self.true_values[0], false=self.false_values[0]),
- value, state)
- return bool(value)
-
- def _convert_from_python(self, value, state):
- return (self.true_values if value else self.false_values)[0]
-
-# Should deprecate:
-StringBoolean = StringBool
-
-
-class SignedString(FancyValidator):
- """
- Encodes a string into a signed string, and base64 encodes both the
- signature string and a random nonce.
-
- It is up to you to provide a secret, and to keep the secret handy
- and consistent.
- """
-
- messages = dict(
- malformed=_('Value does not contain a signature'),
- badsig=_('Signature is not correct'))
-
- secret = None
- nonce_length = 4
-
- def _convert_to_python(self, value, state):
- global sha1
- if not sha1:
- from hashlib import sha1
- assert self.secret is not None, "You must give a secret"
- parts = value.split(None, 1)
- if not parts or len(parts) == 1:
- raise Invalid(self.message('malformed', state), value, state)
- sig, rest = parts
- sig = sig.decode('base64')
- rest = rest.decode('base64')
- nonce = rest[:self.nonce_length]
- rest = rest[self.nonce_length:]
- expected = sha1(str(self.secret) + nonce + rest).digest()
- if expected != sig:
- raise Invalid(self.message('badsig', state), value, state)
- return rest
-
- def _convert_from_python(self, value, state):
- global sha1
- if not sha1:
- from hashlib import sha1
- nonce = self.make_nonce()
- value = str(value)
- digest = sha1(self.secret + nonce + value).digest()
- return self.encode(digest) + ' ' + self.encode(nonce + value)
-
- def encode(self, value):
- return value.encode('base64').strip().replace('\n', '')
-
- def make_nonce(self):
- global random
- if not random:
- import random
- return ''.join(chr(random.randrange(256))
- for _i in range(self.nonce_length))
-
-
-class IPAddress(FancyValidator):
- """
- Formencode validator to check whether a string is a correct IP address.
-
- Examples::
-
- >>> ip = IPAddress()
- >>> ip.to_python('127.0.0.1')
- '127.0.0.1'
- >>> ip.to_python('299.0.0.1')
- Traceback (most recent call last):
- ...
- Invalid: The octets must be within the range of 0-255 (not '299')
- >>> ip.to_python('192.168.0.1/1')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a valid IP address (a.b.c.d)
- >>> ip.to_python('asdf')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a valid IP address (a.b.c.d)
- """
-
- messages = dict(
- badFormat=_('Please enter a valid IP address (a.b.c.d)'),
- leadingZeros=_('The octets must not have leading zeros'),
- illegalOctets=_('The octets must be within the range of 0-255'
- ' (not %(octet)r)'))
-
- leading_zeros = False
-
- def _validate_python(self, value, state=None):
- try:
- if not value:
- raise ValueError
- octets = value.split('.', 5)
- # Only 4 octets?
- if len(octets) != 4:
- raise ValueError
- # Correct octets?
- for octet in octets:
- if octet.startswith('0') and octet != '0':
- if not self.leading_zeros:
- raise Invalid(
- self.message('leadingZeros', state), value, state)
- # strip zeros so this won't be an octal number
- octet = octet.lstrip('0')
- if not 0 <= int(octet) < 256:
- raise Invalid(
- self.message('illegalOctets', state, octet=octet),
- value, state)
- # Splitting faild: wrong syntax
- except ValueError:
- raise Invalid(self.message('badFormat', state), value, state)
-
-
-class CIDR(IPAddress):
- """
- Formencode validator to check whether a string is in correct CIDR
- notation (IP address, or IP address plus /mask).
-
- Examples::
-
- >>> cidr = CIDR()
- >>> cidr.to_python('127.0.0.1')
- '127.0.0.1'
- >>> cidr.to_python('299.0.0.1')
- Traceback (most recent call last):
- ...
- Invalid: The octets must be within the range of 0-255 (not '299')
- >>> cidr.to_python('192.168.0.1/1')
- Traceback (most recent call last):
- ...
- Invalid: The network size (bits) must be within the range of 8-32 (not '1')
- >>> cidr.to_python('asdf')
- Traceback (most recent call last):
- ...
- Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
- """
-
- messages = dict(IPAddress._messages,
- badFormat=_('Please enter a valid IP address (a.b.c.d)'
- ' or IP network (a.b.c.d/e)'),
- illegalBits=_('The network size (bits) must be within the range'
- ' of 8-32 (not %(bits)r)'))
-
- def _validate_python(self, value, state):
- try:
- # Split into octets and bits
- if '/' in value: # a.b.c.d/e
- addr, bits = value.split('/')
- else: # a.b.c.d
- addr, bits = value, 32
- # Use IPAddress validator to validate the IP part
- IPAddress._validate_python(self, addr, state)
- # Bits (netmask) correct?
- if not 8 <= int(bits) <= 32:
- raise Invalid(
- self.message('illegalBits', state, bits=bits),
- value, state)
- # Splitting faild: wrong syntax
- except ValueError:
- raise Invalid(self.message('badFormat', state), value, state)
-
-
-class MACAddress(FancyValidator):
- """
- Formencode validator to check whether a string is a correct hardware
- (MAC) address.
-
- Examples::
-
- >>> mac = MACAddress()
- >>> mac.to_python('aa:bb:cc:dd:ee:ff')
- 'aabbccddeeff'
- >>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
- Traceback (most recent call last):
- ...
- Invalid: A MAC address must contain 12 digits and A-F; the value you gave has 13 characters
- >>> mac.to_python('aa:bb:cc:dd:ee:fx')
- Traceback (most recent call last):
- ...
- Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
- >>> MACAddress(add_colons=True).to_python('aabbccddeeff')
- 'aa:bb:cc:dd:ee:ff'
- """
-
- strip = True
- valid_characters = '0123456789abcdefABCDEF'
- add_colons = False
-
- messages = dict(
- badLength=_('A MAC address must contain 12 digits and A-F;'
- ' the value you gave has %(length)s characters'),
- badCharacter=_('MAC addresses may only contain 0-9 and A-F'
- ' (and optionally :), not %(char)r'))
-
- def _convert_to_python(self, value, state):
- address = value.replace(':', '').lower() # remove colons
- if len(address) != 12:
- raise Invalid(
- self.message('badLength', state,
- length=len(address)), address, state)
- for char in address:
- if char not in self.valid_characters:
- raise Invalid(
- self.message('badCharacter', state,
- char=char), address, state)
- if self.add_colons:
- address = '%s:%s:%s:%s:%s:%s' % (
- address[0:2], address[2:4], address[4:6],
- address[6:8], address[8:10], address[10:12])
- return address
-
- _convert_from_python = _convert_to_python
-
-
-class FormValidator(FancyValidator):
- """
- A FormValidator is something that can be chained with a Schema.
-
- Unlike normal chaining the FormValidator can validate forms that
- aren't entirely valid.
-
- The important method is .validate(), of course. It gets passed a
- dictionary of the (processed) values from the form. If you have
- .validate_partial_form set to True, then it will get the incomplete
- values as well -- check with the "in" operator if the form was able
- to process any particular field.
-
- Anyway, .validate() should return a string or a dictionary. If a
- string, it's an error message that applies to the whole form. If
- not, then it should be a dictionary of fieldName: errorMessage.
- The special key "form" is the error message for the form as a whole
- (i.e., a string is equivalent to {"form": string}).
-
- Returns None on no errors.
- """
-
- validate_partial_form = False
-
- validate_partial_python = None
- validate_partial_other = None
-
- def is_empty(self, value):
- return False
-
- def field_is_empty(self, value):
- return is_empty(value)
-
-
-class RequireIfMissing(FormValidator):
- """
- Require one field based on another field being present or missing.
-
- This validator is applied to a form, not an individual field (usually
- using a Schema's ``pre_validators`` or ``chained_validators``) and is
- available under both names ``RequireIfMissing`` and ``RequireIfPresent``.
-
- If you provide a ``missing`` value (a string key name) then
- if that field is missing the field must be entered.
- This gives you an either/or situation.
-
- If you provide a ``present`` value (another string key name) then
- if that field is present, the required field must also be present.
-
- ::
-
- >>> from formencode import validators
- >>> v = validators.RequireIfPresent('phone_type', present='phone')
- >>> v.to_python(dict(phone_type='', phone='510 420 4577'))
- Traceback (most recent call last):
- ...
- Invalid: You must give a value for phone_type
- >>> v.to_python(dict(phone=''))
- {'phone': ''}
-
- Note that if you have a validator on the optionally-required
- field, you should probably use ``if_missing=None``. This way you
- won't get an error from the Schema about a missing value. For example::
-
- class PhoneInput(Schema):
- phone = PhoneNumber()
- phone_type = String(if_missing=None)
- chained_validators = [RequireIfPresent('phone_type', present='phone')]
- """
-
- # Field that potentially is required:
- required = None
- # If this field is missing, then it is required:
- missing = None
- # If this field is present, then it is required:
- present = None
-
- __unpackargs__ = ('required',)
-
- def _convert_to_python(self, value_dict, state):
- is_empty = self.field_is_empty
- if is_empty(value_dict.get(self.required)) and (
- (self.missing and is_empty(value_dict.get(self.missing))) or
- (self.present and not is_empty(value_dict.get(self.present)))):
- raise Invalid(
- _('You must give a value for %s') % self.required,
- value_dict, state,
- error_dict={self.required:
- Invalid(self.message('empty', state),
- value_dict.get(self.required), state)})
- return value_dict
-
-RequireIfPresent = RequireIfMissing
-
-class RequireIfMatching(FormValidator):
- """
- Require a list of fields based on the value of another field.
-
- This validator is applied to a form, not an individual field (usually
- using a Schema's ``pre_validators`` or ``chained_validators``).
-
- You provide a field name, an expected value and a list of required fields
- (a list of string key names). If the value of the field, if present,
- matches the value of ``expected_value``, then the validator will raise an
- ``Invalid`` exception for every field in ``required_fields`` that is
- missing.
-
- ::
-
- >>> from formencode import validators
- >>> v = validators.RequireIfMatching('phone_type', expected_value='mobile', required_fields=['mobile'])
- >>> v.to_python(dict(phone_type='mobile'))
- Traceback (most recent call last):
- ...
- formencode.api.Invalid: You must give a value for mobile
- >>> v.to_python(dict(phone_type='someothervalue'))
- {'phone_type': 'someothervalue'}
- """
-
- # Field that we will check for its value:
- field = None
- # Value that the field shall have
- expected_value = None
- # If this field is present, then these fields are required:
- required_fields = []
-
- __unpackargs__ = ('field', 'expected_value')
-
- def _convert_to_python(self, value_dict, state):
- is_empty = self.field_is_empty
-
- if self.field in value_dict and value_dict.get(self.field) == self.expected_value:
- for required_field in self.required_fields:
- if required_field not in value_dict or is_empty(value_dict.get(required_field)):
- raise Invalid(
- _('You must give a value for %s') % required_field,
- value_dict, state,
- error_dict={required_field:
- Invalid(self.message('empty', state),
- value_dict.get(required_field), state)})
- return value_dict
-
-class FieldsMatch(FormValidator):
- """
- Tests that the given fields match, i.e., are identical. Useful
- for password+confirmation fields. Pass the list of field names in
- as `field_names`.
-
- ::
-
- >>> f = FieldsMatch('pass', 'conf')
- >>> sorted(f.to_python({'pass': 'xx', 'conf': 'xx'}).items())
- [('conf', 'xx'), ('pass', 'xx')]
- >>> f.to_python({'pass': 'xx', 'conf': 'yy'})
- Traceback (most recent call last):
- ...
- Invalid: conf: Fields do not match
- """
-
- show_match = False
- field_names = None
- validate_partial_form = True
-
- __unpackargs__ = ('*', 'field_names')
-
- messages = dict(
- invalid=_('Fields do not match (should be %(match)s)'),
- invalidNoMatch=_('Fields do not match'),
- notDict=_('Fields should be a dictionary'))
-
- def __init__(self, *args, **kw):
- super(FieldsMatch, self).__init__(*args, **kw)
- if len(self.field_names) < 2:
- raise TypeError('FieldsMatch() requires at least two field names')
-
- def validate_partial(self, field_dict, state):
- for name in self.field_names:
- if name not in field_dict:
- return
- self._validate_python(field_dict, state)
-
- def _validate_python(self, field_dict, state):
- try:
- ref = field_dict[self.field_names[0]]
- except TypeError:
- # Generally because field_dict isn't a dict
- raise Invalid(self.message('notDict', state), field_dict, state)
- except KeyError:
- ref = ''
- errors = {}
- for name in self.field_names[1:]:
- if field_dict.get(name, '') != ref:
- if self.show_match:
- errors[name] = self.message('invalid', state,
- match=ref)
- else:
- errors[name] = self.message('invalidNoMatch', state)
- if errors:
- error_list = sorted(errors.items())
- error_message = '<br>\n'.join(
- '%s: %s' % (name, value) for name, value in error_list)
- raise Invalid(error_message, field_dict, state, error_dict=errors)
-
-
-class CreditCardValidator(FormValidator):
- """
- Checks that credit card numbers are valid (if not real).
-
- You pass in the name of the field that has the credit card
- type and the field with the credit card number. The credit
- card type should be one of "visa", "mastercard", "amex",
- "dinersclub", "discover", "jcb".
-
- You must check the expiration date yourself (there is no
- relation between CC number/types and expiration dates).
-
- ::
-
- >>> cc = CreditCardValidator()
- >>> sorted(cc.to_python({'ccType': 'visa', 'ccNumber': '4111111111111111'}).items())
- [('ccNumber', '4111111111111111'), ('ccType', 'visa')]
- >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111111'})
- Traceback (most recent call last):
- ...
- Invalid: ccNumber: You did not enter a valid number of digits
- >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111112'})
- Traceback (most recent call last):
- ...
- Invalid: ccNumber: You did not enter a valid number of digits
- >>> cc().to_python({})
- Traceback (most recent call last):
- ...
- Invalid: The field ccType is missing
- """
-
- validate_partial_form = True
-
- cc_type_field = 'ccType'
- cc_number_field = 'ccNumber'
-
- __unpackargs__ = ('cc_type_field', 'cc_number_field')
-
- messages = dict(
- notANumber=_('Please enter only the number, no other characters'),
- badLength=_('You did not enter a valid number of digits'),
- invalidNumber=_('That number is not valid'),
- missing_key=_('The field %(key)s is missing'))
-
- def validate_partial(self, field_dict, state):
- if not field_dict.get(self.cc_type_field, None) \
- or not field_dict.get(self.cc_number_field, None):
- return None
- self._validate_python(field_dict, state)
-
- def _validate_python(self, field_dict, state):
- errors = self._validateReturn(field_dict, state)
- if errors:
- error_list = sorted(errors.items())
- raise Invalid(
- '<br>\n'.join('%s: %s' % (name, value)
- for name, value in error_list),
- field_dict, state, error_dict=errors)
-
- def _validateReturn(self, field_dict, state):
- for field in self.cc_type_field, self.cc_number_field:
- if field not in field_dict:
- raise Invalid(
- self.message('missing_key', state, key=field),
- field_dict, state)
- ccType = field_dict[self.cc_type_field].lower().strip()
- number = field_dict[self.cc_number_field].strip()
- number = number.replace(' ', '')
- number = number.replace('-', '')
- try:
- int(number)
- except ValueError:
- return {self.cc_number_field: self.message('notANumber', state)}
- assert ccType in self._cardInfo, (
- "I can't validate that type of credit card")
- foundValid = False
- validLength = False
- for prefix, length in self._cardInfo[ccType]:
- if len(number) == length:
- validLength = True
- if number.startswith(prefix):
- foundValid = True
- break
- if not validLength:
- return {self.cc_number_field: self.message('badLength', state)}
- if not foundValid:
- return {self.cc_number_field: self.message('invalidNumber', state)}
- if not self._validateMod10(number):
- return {self.cc_number_field: self.message('invalidNumber', state)}
- return None
-
- def _validateMod10(self, s):
- """Check string with the mod 10 algorithm (aka "Luhn formula")."""
- checksum, factor = 0, 1
- for c in reversed(s):
- for c in str(factor * int(c)):
- checksum += int(c)
- factor = 3 - factor
- return checksum % 10 == 0
-
- _cardInfo = {
- "visa": [('4', 16),
- ('4', 13)],
- "mastercard": [('51', 16),
- ('52', 16),
- ('53', 16),
- ('54', 16),
- ('55', 16)],
- "discover": [('6011', 16)],
- "amex": [('34', 15),
- ('37', 15)],
- "dinersclub": [('300', 14),
- ('301', 14),
- ('302', 14),
- ('303', 14),
- ('304', 14),
- ('305', 14),
- ('36', 14),
- ('38', 14)],
- "jcb": [('3', 16),
- ('2131', 15),
- ('1800', 15)],
- }
-
-
-class CreditCardExpires(FormValidator):
- """
- Checks that credit card expiration date is valid relative to
- the current date.
-
- You pass in the name of the field that has the credit card
- expiration month and the field with the credit card expiration
- year.
-
- ::
-
- >>> ed = CreditCardExpires()
- >>> sorted(ed.to_python({'ccExpiresMonth': '11', 'ccExpiresYear': '2250'}).items())
- [('ccExpiresMonth', '11'), ('ccExpiresYear', '2250')]
- >>> ed.to_python({'ccExpiresMonth': '10', 'ccExpiresYear': '2005'})
- Traceback (most recent call last):
- ...
- Invalid: ccExpiresMonth: Invalid Expiration Date<br>
- ccExpiresYear: Invalid Expiration Date
- """
-
- validate_partial_form = True
-
- cc_expires_month_field = 'ccExpiresMonth'
- cc_expires_year_field = 'ccExpiresYear'
-
- __unpackargs__ = ('cc_expires_month_field', 'cc_expires_year_field')
-
- datetime_module = None
-
- messages = dict(
- notANumber=_('Please enter numbers only for month and year'),
- invalidNumber=_('Invalid Expiration Date'))
-
- def validate_partial(self, field_dict, state):
- if not field_dict.get(self.cc_expires_month_field, None) \
- or not field_dict.get(self.cc_expires_year_field, None):
- return None
- self._validate_python(field_dict, state)
-
- def _validate_python(self, field_dict, state):
- errors = self._validateReturn(field_dict, state)
- if errors:
- error_list = sorted(errors.items())
- raise Invalid(
- '<br>\n'.join('%s: %s' % (name, value)
- for name, value in error_list),
- field_dict, state, error_dict=errors)
-
- def _validateReturn(self, field_dict, state):
- ccExpiresMonth = str(field_dict[self.cc_expires_month_field]).strip()
- ccExpiresYear = str(field_dict[self.cc_expires_year_field]).strip()
-
- try:
- ccExpiresMonth = int(ccExpiresMonth)
- ccExpiresYear = int(ccExpiresYear)
- dt_mod = import_datetime(self.datetime_module)
- now = datetime_now(dt_mod)
- today = datetime_makedate(dt_mod, now.year, now.month, now.day)
- next_month = ccExpiresMonth % 12 + 1
- next_month_year = ccExpiresYear
- if next_month == 1:
- next_month_year += 1
- expires_date = datetime_makedate(
- dt_mod, next_month_year, next_month, 1)
- assert expires_date > today
- except ValueError:
- return {self.cc_expires_month_field:
- self.message('notANumber', state),
- self.cc_expires_year_field:
- self.message('notANumber', state)}
- except AssertionError:
- return {self.cc_expires_month_field:
- self.message('invalidNumber', state),
- self.cc_expires_year_field:
- self.message('invalidNumber', state)}
-
-
-class CreditCardSecurityCode(FormValidator):
- """
- Checks that credit card security code has the correct number
- of digits for the given credit card type.
-
- You pass in the name of the field that has the credit card
- type and the field with the credit card security code.
-
- ::
-
- >>> code = CreditCardSecurityCode()
- >>> sorted(code.to_python({'ccType': 'visa', 'ccCode': '111'}).items())
- [('ccCode', '111'), ('ccType', 'visa')]
- >>> code.to_python({'ccType': 'visa', 'ccCode': '1111'})
- Traceback (most recent call last):
- ...
- Invalid: ccCode: Invalid credit card security code length
- """
-
- validate_partial_form = True
-
- cc_type_field = 'ccType'
- cc_code_field = 'ccCode'
-
- __unpackargs__ = ('cc_type_field', 'cc_code_field')
-
- messages = dict(
- notANumber=_('Please enter numbers only for credit card security code'),
- badLength=_('Invalid credit card security code length'))
-
- def validate_partial(self, field_dict, state):
- if (not field_dict.get(self.cc_type_field, None)
- or not field_dict.get(self.cc_code_field, None)):
- return None
- self._validate_python(field_dict, state)
-
- def _validate_python(self, field_dict, state):
- errors = self._validateReturn(field_dict, state)
- if errors:
- error_list = sorted(errors.items())
- raise Invalid(
- '<br>\n'.join('%s: %s' % (name, value)
- for name, value in error_list),
- field_dict, state, error_dict=errors)
-
- def _validateReturn(self, field_dict, state):
- ccType = str(field_dict[self.cc_type_field]).strip()
- ccCode = str(field_dict[self.cc_code_field]).strip()
- try:
- int(ccCode)
- except ValueError:
- return {self.cc_code_field: self.message('notANumber', state)}
- length = self._cardInfo[ccType]
- if len(ccCode) != length:
- return {self.cc_code_field: self.message('badLength', state)}
-
- # key = credit card type, value = length of security code
- _cardInfo = dict(visa=3, mastercard=3, discover=3, amex=4)
-
-
-def validators():
- """Return the names of all validators in this module."""
- return [name for name, value in globals().items()
- if isinstance(value, type) and issubclass(value, Validator)]
-
-__all__ = ['Invalid'] + validators()
-