From: Oleg Broytman Date: Thu, 23 Sep 2021 13:49:24 +0000 (+0300) Subject: CI: The hack with `validators.py` is no longer required X-Git-Tag: 0.3.0~1 X-Git-Url: https://git.phdru.name/?p=sqlconvert.git;a=commitdiff_plain;h=1307e86db157aa817386d3a8c62582245b3f30be CI: The hack with `validators.py` is no longer required --- diff --git a/devscripts/CI/validators.py b/devscripts/CI/validators.py deleted file mode 100644 index 233d5dd..0000000 --- a/devscripts/CI/validators.py +++ /dev/null @@ -1,3089 +0,0 @@ -## 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() -