thirdparty/google_appengine/google/appengine/api/validation.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/api/validation.py	Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,835 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Validation tools for generic object structures.
+
+This library is used for defining classes with constrained attributes.
+Attributes are defined on the class which contains them using validators.
+Although validators can be defined by any client of this library, a number
+of standard validators are provided here.
+
+Validators can be any callable that takes a single parameter which checks
+the new value before it is assigned to the attribute.  Validators are
+permitted to modify a received value so that it is appropriate for the
+attribute definition.  For example, using int as a validator will cast
+a correctly formatted string to a number, or raise an exception if it
+can not.  This is not recommended, however.  the correct way to use a
+validator that ensure the correct type is to use the Type validator.
+
+This validation library is mainly intended for use with the YAML object
+builder.  See yaml_object.py.
+"""
+
+
+
+
+
+import re
+
+import google
+import yaml
+
+
+class Error(Exception):
+  """Base class for all package errors."""
+
+
+class AttributeDefinitionError(Error):
+  """An error occurred in the definition of class attributes."""
+
+
+class ValidationError(Error):
+  """Base class for raising exceptions during validation."""
+
+  def __init__(self, message, cause=None):
+    """Initialize exception."""
+    if hasattr(cause, 'args'):
+      Error.__init__(self, message, *cause.args)
+    else:
+      Error.__init__(self, message)
+    self.message = message
+    self.cause = cause
+
+  def __str__(self):
+    return str(self.message)
+
+
+class MissingAttribute(ValidationError):
+  """Raised when a required attribute is missing from object."""
+
+
+def AsValidator(validator):
+  """Wrap various types as instances of a validator.
+
+  Used to allow shorthand for common validator types.  It
+  converts the following types to the following Validators.
+
+    strings -> Regex
+    type -> Type
+    collection -> Options
+    Validator -> Its self!
+
+  Args:
+    validator: Object to wrap in a validator.
+
+  Returns:
+    Validator instance that wraps the given value.
+
+  Raises:
+    AttributeDefinitionError if validator is not one of the above described
+    types.
+  """
+  if isinstance(validator, (str, unicode)):
+    return Regex(validator, type(validator))
+  if isinstance(validator, type):
+    return Type(validator)
+  if isinstance(validator, (list, tuple, set)):
+    return Options(*tuple(validator))
+  if isinstance(validator, Validator):
+    return validator
+  else:
+    raise AttributeDefinitionError('%s is not a valid validator' %
+                                   str(validator))
+
+
+class Validated(object):
+  """Base class for other classes that require validation.
+
+  A class which intends to use validated fields should sub-class itself from
+  this class.  Each class should define an 'ATTRIBUTES' class variable which
+  should be a map from attribute name to its validator.  For example:
+
+    class Story(Validated):
+      ATTRIBUTES = {'title': Type(str),
+                    'authors': Repeated(Type(str)),
+                    'isbn': Optional(Type(str)),
+                    'pages': Type(int),
+                    }
+
+  Attributes that are not listed under ATTRIBUTES work like normal and are
+  not validated upon assignment.
+  """
+
+  ATTRIBUTES = None
+
+  def __init__(self, **attributes):
+    """Constructor for Validated classes.
+
+    This constructor can optionally assign values to the class via its
+    keyword arguments.
+
+    Raises:
+      AttributeDefinitionError when class instance is missing ATTRIBUTE
+      definition or when ATTRIBUTE is of the wrong type.
+    """
+    if not isinstance(self.ATTRIBUTES, dict):
+      raise AttributeDefinitionError(
+          'The class %s does not define an ATTRIBUTE variable.'
+          % self.__class__)
+
+    for key in self.ATTRIBUTES.keys():
+      object.__setattr__(self, key, self.GetAttribute(key).default)
+
+    self.Set(**attributes)
+
+  @classmethod
+  def GetAttribute(self, key):
+    """Safely get the underlying attribute definition as a Validator.
+
+    Args:
+      key: Name of attribute to get.
+
+    Returns:
+      Validator associated with key or attribute value wrapped in a
+      validator.
+    """
+    return AsValidator(self.ATTRIBUTES[key])
+
+  def Set(self, **attributes):
+    """Set multiple values on Validated instance.
+
+    This method can only be used to assign validated methods.
+
+    Args:
+      attributes: Attributes to set on object.
+
+    Raises:
+      ValidationError when no validated attribute exists on class.
+    """
+    for key, value in attributes.iteritems():
+      if key not in self.ATTRIBUTES:
+        raise ValidationError('Class \'%s\' does not have attribute \'%s\''
+                               % (self.__class__, key))
+      setattr(self, key, value)
+
+  def CheckInitialized(self):
+    """Checks that all required fields are initialized.
+
+    Since an instance of Validated starts off in an uninitialized state, it
+    is sometimes necessary to check that it has been fully initialized.
+    The main problem this solves is how to validate that an instance has
+    all of its required fields set.  By default, Validator classes do not
+    allow None, but all attributes are initialized to None when instantiated.
+
+    Raises:
+      Exception relevant to the kind of validation.  The type of the exception
+      is determined by the validator.  Typically this will be ValueError or
+      TypeError.
+    """
+    for key in self.ATTRIBUTES.iterkeys():
+      try:
+        self.GetAttribute(key)(getattr(self, key))
+      except MissingAttribute, e:
+        e.message = "Missing required value '%s'." % key
+        raise e
+
+
+  def __setattr__(self, key, value):
+    """Set attribute.
+
+    Setting a value on an object of this type will only work for attributes
+    defined in ATTRIBUTES.  To make other assignments possible it is necessary
+    to override this method in subclasses.
+
+    It is important that assignment is restricted in this way because
+    this validation is used as validation for parsing.  Absent this restriction
+    it would be possible for method names to be overwritten.
+
+    Args:
+      key: Name of attribute to set.
+      value: Attributes new value.
+
+    Raises:
+      ValidationError when trying to assign to a value that does not exist.
+    """
+
+    if key in self.ATTRIBUTES:
+      value = self.GetAttribute(key)(value)
+      object.__setattr__(self, key, value)
+    else:
+      raise ValidationError('Class \'%s\' does not have attribute \'%s\''
+                            % (self.__class__, key))
+
+  def __eq__(self, other):
+    """Comparison operator."""
+    if isinstance(other, type(self)):
+      for attribute in self.ATTRIBUTES:
+        if getattr(self, attribute) != getattr(other, attribute):
+          return False
+      return True
+    else:
+      return False
+
+  def __str__(self):
+    """Formatted view of validated object and nested values."""
+    return repr(self)
+
+  def __repr__(self):
+    """Formatted view of validated object and nested values."""
+    values = [(attr, getattr(self, attr)) for attr in self.ATTRIBUTES]
+    dent = '    '
+    value_list = []
+    for attr, value in values:
+      value_list.append('\n%s%s=%s' % (dent, attr, value))
+
+    return "<%s %s\n%s>" % (self.__class__.__name__, ' '.join(value_list), dent)
+
+  def __eq__(self, other):
+    """Equality operator.
+
+    Comparison is done by comparing all attribute values to those in the other
+    instance.  Objects which are not of the same type are not equal.
+
+    Args:
+      other: Other object to compare against.
+
+    Returns:
+      True if validated objects are equal, else False.
+    """
+    if type(self) != type(other):
+      return False
+    for key in self.ATTRIBUTES.iterkeys():
+      if getattr(self, key) != getattr(other, key):
+        return False
+    return True
+
+  def __ne__(self, other):
+    """Inequality operator."""
+    return not self.__eq__(other)
+
+  def __hash__(self):
+    """Hash function for using Validated objects in sets and maps.
+
+    Hash is done by hashing all keys and values and xor'ing them together.
+
+    Returns:
+      Hash of validated object.
+    """
+    result = 0
+    for key in self.ATTRIBUTES.iterkeys():
+      value = getattr(self, key)
+      if isinstance(value, list):
+        value = tuple(value)
+      result = result ^ hash(key) ^ hash(value)
+    return result
+
+  @staticmethod
+  def _ToValue(validator, value):
+    """Convert any value to simplified collections and basic types.
+
+    Args:
+      validator: An instance of Validator that corresponds with 'value'.
+        May also be 'str' or 'int' if those were used instead of a full
+        Validator.
+      value: Value to convert to simplified collections.
+
+    Returns:
+      The value as a dictionary if it is a Validated object.
+      A list of items converted to simplified collections if value is a list
+        or a tuple.
+      Otherwise, just the value.
+    """
+    if isinstance(value, Validated):
+      return value.ToDict()
+    elif isinstance(value, (list, tuple)):
+      return [Validated._ToValue(validator, item) for item in value]
+    else:
+      if isinstance(validator, Validator):
+        return validator.ToValue(value)
+      return value
+
+  def ToDict(self):
+    """Convert Validated object to a dictionary.
+
+    Recursively traverses all of its elements and converts everything to
+    simplified collections.
+
+    Returns:
+      A dict of all attributes defined in this classes ATTRIBUTES mapped
+      to its value.  This structure is recursive in that Validated objects
+      that are referenced by this object and in lists are also converted to
+      dicts.
+    """
+    result = {}
+    for name, validator in self.ATTRIBUTES.iteritems():
+      value = getattr(self, name)
+      if not(isinstance(validator, Validator) and value == validator.default):
+        result[name] = Validated._ToValue(validator, value)
+    return result
+
+  def ToYAML(self):
+    """Print validated object as simplified YAML.
+
+    Returns:
+      Object as a simplified YAML string compatible with parsing using the
+      SafeLoader.
+    """
+    return yaml.dump(self.ToDict(),
+                     default_flow_style=False,
+                     Dumper=yaml.SafeDumper)
+
+
+
+class Validator(object):
+  """Validator base class.
+
+  Though any callable can be used as a validator, this class encapsulates the
+  case when a specific validator needs to hold a particular state or
+  configuration.
+
+  To implement Validator sub-class, override the validate method.
+
+  This class is permitted to change the ultimate value that is set to the
+  attribute if there is a reasonable way to perform the conversion.
+  """
+
+  expected_type = object
+
+  def __init__(self, default=None):
+    """Constructor.
+
+    Args:
+      default: Default assignment is made during initialization and will
+        not pass through validation.
+    """
+    self.default = default
+
+  def __call__(self, value):
+    """Main interface to validator is call mechanism."""
+    return self.Validate(value)
+
+  def Validate(self, value):
+    """Override this method to customize sub-class behavior.
+
+    Args:
+      value: Value to validate.
+
+    Returns:
+      Value if value is valid, or a valid representation of value.
+    """
+    return value
+
+  def ToValue(self, value):
+    """Convert 'value' to a simplified collection or basic type.
+
+    Subclasses of Validator should override this method when the dumped
+    representation of 'value' is not simply <type>(value) (e.g. a regex).
+
+    Args:
+      value: An object of the same type that was returned from Validate().
+
+    Returns:
+      An instance of a builtin type (e.g. int, str, dict, etc).  By default
+      it returns 'value' unmodified.
+    """
+    return value
+
+
+class Type(Validator):
+  """Verifies property is of expected type.
+
+  Can optionally convert value if it is not of the expected type.
+
+  It is possible to specify a required field of a specific type in shorthand
+  by merely providing the type.  This method is slightly less efficient than
+  providing an explicit type but is not significant unless parsing a large
+  amount of information:
+
+    class Person(Validated):
+      ATTRIBUTES = {'name': unicode,
+                    'age': int,
+                    }
+
+  However, in most instances it is best to use the type constants:
+
+    class Person(Validated):
+      ATTRIBUTES = {'name': TypeUnicode,
+                    'age': TypeInt,
+                    }
+  """
+
+  def __init__(self, expected_type, convert=True, default=None):
+    """Initialize Type validator.
+
+    Args:
+      expected_type: Type that attribute should validate against.
+      convert: Cause conversion if value is not the right type.
+        Conversion is done by calling the constructor of the type
+        with the value as its first parameter.
+    """
+    super(Type, self).__init__(default)
+    self.expected_type = expected_type
+    self.convert = convert
+
+  def Validate(self, value):
+    """Validate that value is correct type.
+
+    Args:
+      value: Value to validate.
+
+    Returns:
+      None if value is None, value if value is of correct type, converted
+      value if the validator is configured to convert.
+
+    Raises:
+      ValidationError if value is not of the right type and validator
+      is not configured to convert.
+    """
+    if not isinstance(value, self.expected_type):
+      if value is not None and self.convert:
+        try:
+          return self.expected_type(value)
+        except ValueError, e:
+          raise ValidationError('Type conversion failed for value \'%s\'.'
+                                % value,
+                                e)
+        except TypeError, e:
+          raise ValidationError('Expected value of type %s, but got \'%s\'.'
+                                % (self.expected_type, value))
+      else:
+        raise MissingAttribute('Missing value is required.')
+    else:
+      return value
+
+
+TYPE_BOOL = Type(bool)
+TYPE_INT = Type(int)
+TYPE_LONG = Type(long)
+TYPE_STR = Type(str)
+TYPE_UNICODE = Type(unicode)
+TYPE_FLOAT = Type(float)
+
+
+class Options(Validator):
+  """Limit field based on pre-determined values.
+
+  Options are used to make sure an enumerated set of values are the only
+  one permitted for assignment.  It is possible to define aliases which
+  map multiple string values to a single original.  An example of usage:
+
+    class ZooAnimal(validated.Class):
+      ATTRIBUTES = {
+        'name': str,
+        'kind': Options('platypus',                   # No aliases
+                        ('rhinoceros', ['rhino']),    # One alias
+                        ('canine', ('dog', 'puppy')), # Two aliases
+                        )
+  """
+
+  def __init__(self, *options, **kw):
+    """Initialize options.
+
+    Args:
+      options: List of allowed values.
+    """
+    if 'default' in kw:
+      default = kw['default']
+    else:
+      default = None
+
+    alias_map = {}
+    def AddAlias(alias, original):
+      """Set new alias on alias_map.
+
+      Raises:
+        AttributeDefinitionError when option already exists or if alias is
+        not of type str..
+      """
+      if not isinstance(alias, str):
+        raise AttributeDefinitionError(
+            'All option values must be of type str.')
+      elif alias in alias_map:
+        raise AttributeDefinitionError(
+            "Option '%s' already defined for options property." % alias)
+      alias_map[alias] = original
+
+    for option in options:
+      if isinstance(option, str):
+        AddAlias(option, option)
+
+      elif isinstance(option, (list, tuple)):
+        if len(option) != 2:
+          raise AttributeDefinitionError("Alias is defined as a list of tuple "
+                                         "with two items.  The first is the "
+                                         "original option, while the second "
+                                         "is a list or tuple of str aliases.\n"
+                                         "\n  Example:\n"
+                                         "      ('original', ('alias1', "
+                                         "'alias2'")
+        original, aliases = option
+        AddAlias(original, original)
+        if not isinstance(aliases, (list, tuple)):
+          raise AttributeDefinitionError('Alias lists must be a list or tuple')
+
+        for alias in aliases:
+          AddAlias(alias, original)
+
+      else:
+        raise AttributeDefinitionError("All options must be of type str "
+                                       "or of the form (str, [str...]).")
+    super(Options, self).__init__(default)
+    self.options = alias_map
+
+  def Validate(self, value):
+    """Validate options.
+
+    Returns:
+      Original value for provided alias.
+
+    Raises:
+      ValidationError when value is not one of predefined values.
+    """
+    if value is None:
+      raise ValidationError('Value for options field must not be None.')
+    value = str(value)
+    if value not in self.options:
+      raise ValidationError('Value \'%s\' not in %s.'
+                            % (value, self.options))
+    return self.options[value]
+
+
+class Optional(Validator):
+  """Definition of optional attributes.
+
+  Optional values are attributes which can be set to None or left
+  unset.  All values in a basic Validated class are set to None
+  at initialization.  Failure to assign to non-optional values
+  will result in a validation error when calling CheckInitialized.
+  """
+
+  def __init__(self, validator, default=None):
+    """Initializer.
+
+    This constructor will make a few guesses about the value passed in
+    as the validator:
+
+      - If the validator argument is a type, it automatically creates a Type
+        validator around it.
+
+      - If the validator argument is a list or tuple, it automatically
+        creates an Options validator around it.
+
+    Args:
+      validator: Optional validation condition.
+
+    Raises:
+      AttributeDefinitionError if validator is not callable.
+    """
+    self.validator = AsValidator(validator)
+    self.expected_type = self.validator.expected_type
+    self.default = default
+
+  def Validate(self, value):
+    """Optionally require a value.
+
+    Normal validators do not accept None.  This will accept none on
+    behalf of the contained validator.
+
+    Args:
+      value: Value to be validated as optional.
+
+    Returns:
+      None if value is None, else results of contained validation.
+    """
+    if value is None:
+      return None
+    return self.validator(value)
+
+
+class Regex(Validator):
+  """Regular expression validator.
+
+  Regular expression validator always converts value to string.  Note that
+  matches must be exact.  Partial matches will not validate.  For example:
+
+    class ClassDescr(Validated):
+      ATTRIBUTES = { 'name': Regex(r'[a-zA-Z_][a-zA-Z_0-9]*'),
+                     'parent': Type(type),
+                     }
+
+  Alternatively, any attribute that is defined as a string is automatically
+  interpreted to be of type Regex.  It is possible to specify unicode regex
+  strings as well.  This approach is slightly less efficient, but usually
+  is not significant unless parsing large amounts of data:
+
+    class ClassDescr(Validated):
+      ATTRIBUTES = { 'name': r'[a-zA-Z_][a-zA-Z_0-9]*',
+                     'parent': Type(type),
+                     }
+
+    # This will raise a ValidationError exception.
+    my_class(name='AName with space', parent=AnotherClass)
+  """
+
+  def __init__(self, regex, string_type=unicode, default=None):
+    """Initialized regex validator.
+
+    Args:
+      regex: Regular expression string to use for comparison.
+
+    Raises:
+      AttributeDefinitionError if string_type is not a kind of string.
+    """
+    super(Regex, self).__init__(default)
+    if (not issubclass(string_type, basestring) or
+        string_type is basestring):
+      raise AttributeDefinitionError(
+          'Regex fields must be a string type not %s.' % str(string_type))
+    if isinstance(regex, basestring):
+      self.re = re.compile('^%s$' % regex)
+    else:
+      raise AttributeDefinitionError(
+          'Regular expression must be string.  Found %s.' % str(regex))
+
+    self.expected_type = string_type
+
+  def Validate(self, value):
+    """Does validation of a string against a regular expression.
+
+    Args:
+      value: String to match against regular expression.
+
+    Raises:
+      ValidationError when value does not match regular expression or
+      when value does not match provided string type.
+    """
+    if issubclass(self.expected_type, str):
+      cast_value = TYPE_STR(value)
+    else:
+      cast_value = TYPE_UNICODE(value)
+
+    if self.re.match(cast_value) is None:
+      raise ValidationError('Value \'%s\' does not match expression \'%s\''
+                            % (value, self.re.pattern))
+    return cast_value
+
+
+class RegexStr(Validator):
+  """Validates that a string can compile as a regex without errors.
+
+  Use this validator when the value of a field should be a regex.  That
+  means that the value must be a string that can be compiled by re.compile().
+  The attribute will then be a compiled re object.
+  """
+
+  def __init__(self, string_type=unicode, default=None):
+    """Initialized regex validator.
+
+    Raises:
+      AttributeDefinitionError if string_type is not a kind of string.
+    """
+    if default is not None:
+      default = re.compile(default)
+    super(RegexStr, self).__init__(default)
+    if (not issubclass(string_type, basestring) or
+        string_type is basestring):
+      raise AttributeDefinitionError(
+          'RegexStr fields must be a string type not %s.' % str(string_type))
+
+    self.expected_type = string_type
+
+  def Validate(self, value):
+    """Validates that the string compiles as a regular expression.
+
+    Because the regular expression might have been expressed as a multiline
+    string, this function also strips newlines out of value.
+
+    Args:
+      value: String to compile as a regular expression.
+
+    Raises:
+      ValueError when value does not compile as a regular expression.  TypeError
+      when value does not match provided string type.
+    """
+    if issubclass(self.expected_type, str):
+      cast_value = TYPE_STR(value)
+    else:
+      cast_value = TYPE_UNICODE(value)
+
+    cast_value = cast_value.replace('\n', '')
+    cast_value = cast_value.replace('\r', '')
+    try:
+      compiled = re.compile(cast_value)
+    except re.error, e:
+      raise ValidationError('Value \'%s\' does not compile: %s' % (value, e), e)
+    return compiled
+
+  def ToValue(self, value):
+    """Returns the RE pattern for this validator."""
+    return value.pattern
+
+
+class Range(Validator):
+  """Validates that numbers fall within the correct range.
+
+  In theory this class can be emulated using Options, however error
+  messages generated from that class will not be very intelligible.
+  This class essentially does the same thing, but knows the intended
+  integer range.
+
+  Also, this range class supports floats and other types that implement
+  ordinality.
+
+  The range is inclusive, meaning 3 is considered in the range
+  in Range(1,3).
+  """
+
+  def __init__(self, minimum, maximum, range_type=int, default=None):
+    """Initializer for range.
+
+    Args:
+      minimum: Minimum for attribute.
+      maximum: Maximum for attribute.
+      range_type: Type of field.  Defaults to int.
+    """
+    super(Range, self).__init__(default)
+    if not isinstance(minimum, range_type):
+      raise AttributeDefinitionError(
+          'Minimum value must be of type %s, instead it is %s (%s).' %
+          (str(range_type), str(type(minimum)), str(minimum)))
+    if not isinstance(maximum, range_type):
+      raise AttributeDefinitionError(
+          'Maximum value must be of type %s, instead it is %s (%s).' %
+          (str(range_type), str(type(maximum)), str(maximum)))
+
+    self.minimum = minimum
+    self.maximum = maximum
+    self.expected_type = range_type
+    self._type_validator = Type(range_type)
+
+  def Validate(self, value):
+    """Validate that value is within range.
+
+    Validates against range-type then checks the range.
+
+    Args:
+      value: Value to validate.
+
+    Raises:
+      ValidationError when value is out of range.  ValidationError when value
+      is notd of the same range type.
+    """
+    cast_value = self._type_validator.Validate(value)
+    if cast_value < self.minimum or cast_value > self.maximum:
+      raise ValidationError('Value \'%s\' is out of range %s - %s'
+                            % (str(value),
+                               str(self.minimum),
+                               str(self.maximum)))
+    return cast_value
+
+
+class Repeated(Validator):
+  """Repeated field validator.
+
+  Indicates that attribute is expected to be a repeated value, ie,
+  a sequence.  This adds additional validation over just Type(list)
+  in that it retains information about what can be stored in the list by
+  use of its constructor field.
+  """
+
+  def __init__(self, constructor, default=None):
+    """Initializer for repeated field.
+
+    Args:
+      constructor: Type used for verifying elements of sequence attribute.
+    """
+    super(Repeated, self).__init__(default)
+    self.constructor = constructor
+    self.expected_type = list
+
+  def Validate(self, value):
+    """Do validation of sequence.
+
+    Value must be a list and all elements must be of type 'constructor'.
+
+    Args:
+      value: Value to validate.
+
+    Raises:
+      ValidationError if value is None, not a list or one of its elements is the
+      wrong type.
+    """
+    if not isinstance(value, list):
+      raise ValidationError('Repeated fields must be sequence, '
+                            'but found \'%s\'.' % value)
+
+    for item in value:
+      if not isinstance(item, self.constructor):
+        raise ValidationError('Repeated items must be %s, but found \'%s\'.'
+                              % (str(self.constructor), str(item)))
+
+    return value