From 7bc628931bf0b70ec21f1ba657c71f223c3d01ea Mon Sep 17 00:00:00 2001 From: Oleg Broytman Date: Fri, 1 Feb 2019 01:59:40 +0300 Subject: [PATCH] CI(AppVeyor): Work around a strange problem with Py3.7 Work around a very strange problem with Python 3.7 at AppVeyor by copying correct validators.py to formencode. --- setup.cfg | 2 +- tox.ini | 9 +- validators.py | 3089 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3097 insertions(+), 3 deletions(-) create mode 100644 validators.py diff --git a/setup.cfg b/setup.cfg index 708bf5e..9e6f371 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,5 +10,5 @@ tag_date = 0 tag_svn_revision = 0 [flake8] -exclude = .git,.tox,docs/conf.py +exclude = .git,.tox,docs/conf.py,validators.py diff --git a/tox.ini b/tox.ini index 030496a..a185a37 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ passenv = PGPASSWORD platform = linux # Don't fail or warn on uninstalled commands whitelist_externals = + cmd createdb dropdb @@ -97,7 +98,9 @@ commands = {[postgres-w32]commands} [testenv:py37-postgres-w32] platform = win32 -commands = {[postgres-w32]commands} +commands = + cmd /c "copy validators.py {envsitepackagesdir}\\formencode\\validators.py" + {[postgres-w32]commands} # SQLite test environments [sqlite] @@ -148,7 +151,9 @@ commands = {[sqlite-w32]commands} [testenv:py37-sqlite-w32] platform = win32 -commands = {[sqlite-w32]commands} +commands = + cmd /c "copy validators.py {envsitepackagesdir}\\formencode\\validators.py" + {[sqlite-w32]commands} # flake8 [testenv:py27-flake8] diff --git a/validators.py b/validators.py new file mode 100644 index 0000000..233d5dd --- /dev/null +++ b/validators.py @@ -0,0 +1,3089 @@ +## FormEncode, a Form processor +## Copyright (C) 2003, Ian Bicking + +""" +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 + >>> 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 , + >>> cint2 = ConfirmType(type=int) + >>> cint2(accept_python=False).from_python(True) + Traceback (most recent call last): + ... + Invalid: True must be of the type + """ + + 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 : 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 `__ 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 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(?:(?: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[a-z0-9][a-z0-9\-]{,62}\.)* # subdomain + (?P[a-z]{2,63}|xn--[a-z0-9\-]{2,59}) # top level domain + ) + (?::[0-9]{1,5})? # port + # files/delims/etc + (?P/[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:: + + + + + 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 = '
\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( + '
\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
+ 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( + '
\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( + '
\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() + -- 2.39.2