class Issue(object): """ Represents an issue found during validation of a value. """ message = attr.ib(validator=attr_is_string()) # type: six.text_type pointer = attr.ib(default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type]
class Error(object): """ Represents an error found validating against the schema. """ message = attr.ib(validator=attr_is_string()) # type: six.text_type code = attr.ib(default=ERROR_CODE_INVALID, validator=attr_is_string()) # type: six.text_type pointer = attr.ib(default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type]
class Amount(Base): """ Conformity field that ensures that the value is an instance of `currint.Amount` and optionally enforces boundaries for that amount with the `valid_currencies`, `gt`, `gte`, `lt`, and `lte` arguments. This field requires that Currint be installed. """ introspect_type = 'currint.Amount' valid_currencies = attr.ib( default=frozenset(), validator=attr_is_iterable(attr_is_string(), attr_is_set()), ) # type: AbstractSet[six.text_type] gt = attr.ib(default=None, validator=attr_is_optional(attr_is_int())) # type: Optional[int] gte = attr.ib(default=None, validator=attr_is_optional(attr_is_int())) # type: Optional[int] lt = attr.ib(default=None, validator=attr_is_optional(attr_is_int())) # type: Optional[int] lte = attr.ib(default=None, validator=attr_is_optional(attr_is_int())) # type: Optional[int] description = attr.ib(default=None, validator=attr_is_optional(attr_is_string())) # type: Optional[six.text_type] def __attrs_post_init__(self): # type: () -> None if not self.valid_currencies: self.valid_currencies = DEFAULT_CURRENCY_CODES def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, currint.Amount): return [Error( 'Not a currint.Amount instance', code=ERROR_CODE_INVALID, )] return _get_errors_for_currency_amount( value.currency.code, value.value, self.valid_currencies, self.gt, self.gte, self.lt, self.lte, ) def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'valid_currencies': ( '(all currencies)' if self.valid_currencies is DEFAULT_CURRENCY_CODES else sorted(self.valid_currencies) ), 'gt': self.gt, 'gte': self.gte, 'lt': self.lt, 'lte': self.lte, })
class ObjectInstance(Base): """ Accepts only instances of a given class or type """ introspect_type = 'object_instance' valid_type = attr.ib(validator=attr_is_instance_or_instance_tuple( type)) # type: Union[Type, TupleType[Type, ...]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): if not isinstance(value, self.valid_type): return [ Error('Not an instance of {}'.format(self.valid_type.__name__)) ] return [] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, # Unfortunately, this is the one sort of thing we can't represent # super well. Maybe add some dotted path stuff in here. 'valid_type': repr(self.valid_type), })
class UnicodeDecimal(Base): """ A decimal value represented as its base-10 unicode string. """ introspect_type = 'unicode_decimal' description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): if not isinstance(value, six.text_type): return [ Error('Invalid decimal value (not unicode string)'), ] try: decimal.Decimal(value) except decimal.InvalidOperation: return [ Error('Invalid decimal value (parse error)'), ] return [] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class UnicodeDecimal(Base): """ Conformity field that ensures that the value is a unicode string that is also a valid decimal and can successfully be converted to a `decimal.Decimal`. """ introspect_type = 'unicode_decimal' description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, six.text_type): return [ Error('Invalid decimal value (not unicode string)'), ] try: decimal.Decimal(value) except decimal.InvalidOperation: return [ Error('Invalid decimal value (parse error)'), ] return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class ObjectInstance(Base): """ Conformity field that ensures that the value is an instance of the given `valid_type`. """ introspect_type = 'object_instance' valid_type = attr.ib(validator=attr_is_instance_or_instance_tuple( type)) # type: Union[Type, TupleType[Type, ...]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, self.valid_type): return [ Error('Not an instance of {}'.format( getattr(self.valid_type, '__name__', repr(self.valid_type)))) ] return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, # Unfortunately, this is the one sort of thing we can't represent # super well. Maybe add some dotted path stuff in here. 'valid_type': repr(self.valid_type), })
class Polymorph(Base): """ A field which has one of a set of possible contents based on a field within it (which must be accessible via dictionary lookups) """ introspect_type = 'polymorph' switch_field = attr.ib(validator=attr_is_string()) # type: six.text_type contents_map = attr.ib( validator=attr_is_instance(dict)) # type: Mapping[HashableType, Base] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # Get switch field value bits = self.switch_field.split('.') switch_value = value for bit in bits: switch_value = switch_value[bit] # Get field if switch_value not in self.contents_map: if '__default__' in self.contents_map: switch_value = '__default__' else: return [ Error("Invalid switch value '{}'".format(switch_value), code=ERROR_CODE_UNKNOWN) ] field = self.contents_map[switch_value] # Run field errors return field.errors(value) def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, 'switch_field': self.switch_field, 'contents_map': { key: value.introspect() for key, value in self.contents_map.items() }, })
class Integer(Base): """ Conformity field that ensures that the value is an integer and optionally enforces boundaries for that integer with the `gt`, `gte`, `lt`, and `lte` arguments. """ valid_type = six.integer_types # type: Union[Type, TupleType[Type, ...]] valid_noun = 'an integer' # type: six.text_type introspect_type = 'integer' # type: six.text_type gt = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] gte = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] lt = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] lte = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, self.valid_type) or isinstance(value, bool): return [Error('Not {}'.format(self.valid_noun))] errors = [] if self.gt is not None and value <= self.gt: errors.append(Error('Value not > {}'.format(self.gt))) if self.lt is not None and value >= self.lt: errors.append(Error('Value not < {}'.format(self.lt))) if self.gte is not None and value < self.gte: errors.append(Error('Value not >= {}'.format(self.gte))) if self.lte is not None and value > self.lte: errors.append(Error('Value not <= {}'.format(self.lte))) return errors def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'gt': self.gt, 'gte': self.gte, 'lt': self.lt, 'lte': self.lte, })
class Integer(Base): """ Accepts valid integers, with optional range limits. """ valid_type = six.integer_types # type: Union[Type, TupleType[Type, ...]] valid_noun = 'an integer' # type: six.text_type introspect_type = 'integer' # type: six.text_type gt = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] gte = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] lt = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] lte = attr.ib( default=None, validator=attr_is_optional(attr_is_number()), ) # type: Optional[Union[int, float, decimal.Decimal]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): if not isinstance(value, self.valid_type) or isinstance(value, bool): return [Error('Not {}'.format(self.valid_noun))] errors = [] if self.gt is not None and value <= self.gt: errors.append(Error('Value not > {}'.format(self.gt))) if self.lt is not None and value >= self.lt: errors.append(Error('Value not < {}'.format(self.lt))) if self.gte is not None and value < self.gte: errors.append(Error('Value not >= {}'.format(self.gte))) if self.lte is not None and value > self.lte: errors.append(Error('Value not <= {}'.format(self.lte))) return errors def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, 'gt': self.gt, 'gte': self.gte, 'lt': self.lt, 'lte': self.lte, })
class UnicodeString(Base): """ Conformity field that ensures that the value is a unicode string (`str` in Python 3, `unicode` in Python 2) and optionally enforces minimum and maximum lengths with the `min_length`, `max_length`, and `allow_blank` arguments. """ valid_type = six.text_type # type: Type valid_noun = 'unicode string' introspect_type = 'unicode' min_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] max_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] allow_blank = attr.ib(default=True, validator=attr_is_bool()) # type: bool def __attrs_post_init__(self): # type: () -> None if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length: raise ValueError( 'min_length cannot be greater than max_length in UnicodeString' ) def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, self.valid_type): return [Error('Not a {}'.format(self.valid_noun))] elif self.min_length is not None and len(value) < self.min_length: return [ Error('String must have a length of at least {}'.format( self.min_length)) ] elif self.max_length is not None and len(value) > self.max_length: return [ Error('String must have a length no more than {}'.format( self.max_length)) ] elif not (self.allow_blank or value.strip()): return [Error('String cannot be blank')] return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'min_length': self.min_length, 'max_length': self.max_length, 'allow_blank': self.allow_blank and None, # if the default True, hide it from introspection })
class UnicodeString(Base): """ Accepts only unicode strings """ valid_type = six.text_type # type: Type valid_noun = 'unicode string' introspect_type = 'unicode' min_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] max_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] allow_blank = attr.ib(default=True, validator=attr_is_bool()) # type: bool def __attrs_post_init__(self): if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length: raise ValueError( 'min_length cannot be greater than max_length in UnicodeString' ) def errors(self, value): if not isinstance(value, self.valid_type): return [Error('Not a {}'.format(self.valid_noun))] elif self.min_length is not None and len(value) < self.min_length: return [ Error('String must have a length of at least {}'.format( self.min_length)) ] elif self.max_length is not None and len(value) > self.max_length: return [ Error('String must have a length no more than {}'.format( self.max_length)) ] elif not (self.allow_blank or value.strip()): return [Error('String cannot be blank')] return [] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, 'min_length': self.min_length, 'max_length': self.max_length, 'allow_blank': self.allow_blank and None, # if the default True, hide it from introspection })
class BooleanValidator(Base): """ Conformity field that ensures that the value passes validation with the `typing.Callable[[typing.Any], bool]` `validator` argument passed in to it. """ introspect_type = 'boolean_validator' validator = attr.ib() # type: Callable[[AnyType], bool] validator_description = attr.ib( validator=attr_is_string()) # type: six.text_type error = attr.ib(validator=attr_is_string()) # type: six.text_type description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] # Run the validator, but catch any errors and return them as an error. try: ok = self.validator(value) except Exception as e: return [ Error('Validator encountered an error (invalid type?): {!r}'. format(e)) ] if ok: return [] else: return [Error(self.error)] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'validator': self.validator_description, })
class BooleanValidator(Base): """ Uses a boolean callable (probably lambda) passed in to validate the value based on if it returns True (valid) or False (invalid). """ introspect_type = 'boolean_validator' validator = attr.ib() # type: Callable[[AnyType], bool] validator_description = attr.ib( validator=attr_is_string()) # type: six.text_type error = attr.ib(validator=attr_is_string()) # type: six.text_type description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # Run the validator, but catch any errors and return them as an error. try: ok = self.validator(value) except Exception as e: return [ Error('Validator encountered an error (invalid type?): {!r}'. format(e)) ] if ok: return [] else: return [Error(self.error)] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, 'validator': self.validator_description, })
class Anything(Base): """ Conformity field that allows the value to be literally anything. """ introspect_type = 'anything' description = attr.ib(default=None, validator=attr_is_optional(attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class Anything(Base): """ Accepts any value. """ introspect_type = 'anything' description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): return [] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class TypeReference(Base): """ Conformity field that ensures that the value is an instance of `type` and, optionally, that the value is a subclass of the type or types specified by `base_classes`. """ introspect_type = 'type_reference' base_classes = attr.ib( default=None, validator=attr_is_optional(attr_is_instance_or_instance_tuple(type)), ) # type: Optional[Union[Type, TupleType[Type, ...]]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, type): return [Error('Not a type')] if self.base_classes and not issubclass(value, self.base_classes): return [ Error( 'Type {} is not one of or a subclass of one of: {}'.format( value, self.base_classes)) ] return [] def introspect(self): # type: () -> Introspection base_classes = None if self.base_classes: if isinstance(self.base_classes, type): base_classes = [six.text_type(self.base_classes)] else: base_classes = [six.text_type(c) for c in self.base_classes] return strip_none({ 'type': self.introspect_type, 'description': self.description, 'base_classes': base_classes, })
class Boolean(Base): """ Conformity field that ensures that the value is a boolean. """ introspect_type = 'boolean' description = attr.ib(default=None, validator=attr_is_optional(attr_is_string())) # type: Optional[six.text_type] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, bool): return [ Error('Not a boolean'), ] return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class TypeReference(Base): """ Accepts only type references, optionally types that must be a subclass of a given type or types. """ introspect_type = 'type_reference' base_classes = attr.ib( default=None, validator=attr_is_optional(attr_is_instance_or_instance_tuple(type)), ) # type: Optional[Union[Type, TupleType[Type, ...]]] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): if not isinstance(value, type): return [Error('Not a type')] if self.base_classes and not issubclass(value, self.base_classes): return [ Error( 'Type {} is not one of or a subclass of one of: {}'.format( value, self.base_classes)) ] return [] def introspect(self): base_classes = None if self.base_classes: if isinstance(self.base_classes, type): base_classes = [six.text_type(self.base_classes)] else: base_classes = [six.text_type(c) for c in self.base_classes] return strip_none({ 'type': self.introspect_type, 'description': self.description, 'base_classes': base_classes, })
class Deprecated(Base): field = attr.ib() # type: Base message = attr.ib( default='This field has been deprecated', validator=attr_is_optional(attr_is_string()), ) # type: six.text_type def warnings(self, value): # type: (AnyType) -> ListType[Warning] warnings = self.field.warnings(value) warnings.append( Warning( code=WARNING_CODE_FIELD_DEPRECATED, message=self.message, )) return warnings def introspect(self): # type: () -> Introspection field_introspection = self.field.introspect() field_introspection['deprecated'] = True return field_introspection
class Boolean(Base): """ Accepts boolean values only """ introspect_type = 'boolean' description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def errors(self, value): if not isinstance(value, bool): return [ Error('Not a boolean'), ] return [] def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, })
class SchemalessDictionary(Base): """ Conformity field that ensures that the value is a dictionary of any keys and values, but optionally enforcing that the keys pass the Conformity validation specified with the `key_type` argument and/or that the values pass the Conformity validation specified with the `value_type` argument. Size of the dictionary can also be constrained with the optional `max_length` and `min_length` arguments. """ introspect_type = 'schemaless_dictionary' # Makes MyPy allow key_type and value_type have type Base _default_key_type = attr.Factory(Hashable) # type: Base _default_value_type = attr.Factory(Anything) # type: Base key_type = attr.ib(default=_default_key_type, validator=attr_is_instance(Base)) # type: Base value_type = attr.ib(default=_default_value_type, validator=attr_is_instance(Base)) # type: Base max_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] min_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] additional_validator = attr.ib( default=None, validator=attr_is_optional( attr_is_instance(AdditionalCollectionValidator)), ) # type: Optional[AdditionalCollectionValidator[Mapping[HashableType, AnyType]]] def __attrs_post_init__(self): # type: () -> None if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length: raise ValueError( 'min_length cannot be greater than max_length in UnicodeString' ) def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, dict): return [Error('Not a dict')] result = [] if self.max_length is not None and len(value) > self.max_length: result.append( Error('Dict contains more than {} value(s)'.format( self.max_length))) elif self.min_length is not None and len(value) < self.min_length: result.append( Error('Dict contains fewer than {} value(s)'.format( self.min_length))) for key, field in value.items(): result.extend( update_error_pointer(error, key) for error in (self.key_type.errors(key) or [])) result.extend( update_error_pointer(error, key) for error in (self.value_type.errors(field) or [])) if not result and self.additional_validator: return self.additional_validator.errors(value) return result def introspect(self): # type: () -> Introspection result = { 'type': self.introspect_type, 'max_length': self.max_length, 'min_length': self.min_length, 'description': self.description, 'additional_validation': (self.additional_validator.__class__.__name__ if self.additional_validator else None), } # type: Introspection # We avoid using isinstance() here as that would also match subclass instances if not self.key_type.__class__ == Hashable: result['key_type'] = self.key_type.introspect() if not self.value_type.__class__ == Anything: result['value_type'] = self.value_type.introspect() return strip_none(result)
class ClassConfigurationSchema(Base): """ A special-case dictionary field that accepts exactly two keys: `path` (a `TypePath`-validated string) and `kwargs` (a `Dictionary`-or-subclass-validated dict) that can discover initialization schema from classes and validate that schema prior to instantiation. By default, the dictionary is mutated to add an `object` key containing the resolved class, but this behavior can be disabled by specifying `add_class_object_to_dict=False` to the field arguments. If you experience circular dependency errors when using this field, you can mitigate this by specifying `eager_default_validation=False` to the field arguments. Typical usage would be as follows, in Python pseudocode: class BaseThing: ... @ClassConfigurationSchema.provider(fields.Dictionary({...}, ...)) class Thing1(BaseThing): ... @ClassConfigurationSchema.provider(fields.Dictionary({...}, ...)) class Thing2(BaseThing): ... settings = get_settings_from_something() schema = ClassConfigurationSchema(base_class=BaseThing) errors = schema.errors(**settings[kwargs]) if errors: ... handle errors ... thing = settings['object'](settings) Another approach, using the helper method on the schema, simplifies that last part: schema = ClassConfigurationSchema(base_class=BaseThing) thing = schema.instantiate_from(get_settings_from_something()) # raises ValidationError However, note that, in both cases, instantiation is not nested. If the settings schema Dictionary on some class has a key (or further down) whose value is another ClassConfigurationSchema, code that consumes those settings will also have to instantiate objects from those settings. Validation, however, will be nested as it all other things Conformity. """ introspect_type = 'class_config_dictionary' switch_field_schema = TypePath(base_classes=object) _init_schema_attribute = '_conformity_initialization_schema' base_class = attr.ib(default=None, validator=attr_is_optional( attr_is_instance(type))) # type: Optional[Type] default_path = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] eager_default_validation = attr.ib(default=True, validator=attr_is_bool()) # type: bool add_class_object_to_dict = attr.ib(default=True, validator=attr_is_bool()) # type: bool def __attrs_post_init__(self): self._schema_cache = {} # type: Dict[six.text_type, Dictionary] if not self.base_class: if getattr(self.__class__, 'base_class', None): # If the base class was defaulted but a subclass has hard-coded a base class, use that. self.base_class = self.__class__.base_class else: self.base_class = object if self.base_class is not object: # If the base class is not the default, create a new schema instance to validate paths. self.switch_field_schema = TypePath(base_classes=self.base_class) else: self.switch_field_schema = self.__class__.switch_field_schema if not self.description and getattr(self.__class__, 'description', None): # If the description is not specified but a subclass has hard-coded a base class, use that. self.description = self.__class__.description if not self.default_path and getattr(self.__class__, 'default_path', None): # If the default path is not specified but a subclass has hard-coded a default path, use that. self.default_path = self.__class__.default_path if self.default_path and self.eager_default_validation: # If the default path is specified and eager validation is not disabled, validate the default path. self.initiate_cache_for(self.default_path) def errors(self, value): if not isinstance(value, Mapping): return [Error('Not a mapping (dictionary)')] # check for extra keys (object is allowed in case this gets validated twice) extra_keys = [ k for k in six.iterkeys(value) if k not in ('path', 'kwargs', 'object') ] if extra_keys: return [ Error( 'Extra keys present: {}'.format(', '.join( six.text_type(k) for k in sorted(extra_keys))), code=ERROR_CODE_UNKNOWN, ) ] sentinel = object() path = value.get('path', sentinel) if path is sentinel and not self.default_path: return [ Error('Missing key (and no default specified): path', code=ERROR_CODE_MISSING, pointer='path') ] if not path or path is sentinel: path = self.default_path errors = self._populate_schema_cache_if_necessary(path) if errors: return [update_error_pointer(e, 'path') for e in errors] if isinstance(value, MutableMapping): value['path'] = path # in case it was defaulted if self.add_class_object_to_dict: value['object'] = PythonPath.resolve_python_path(path) return [ update_error_pointer(e, 'kwargs') for e in self._schema_cache[path].errors(value.get('kwargs', {})) ] def initiate_cache_for(self, path): # type: (six.text_type) -> None errors = self._populate_schema_cache_if_necessary(path) if errors: raise ValidationError(errors) def _populate_schema_cache_if_necessary( self, path): # type: (six.text_type) -> ListType[Error] if path in self._schema_cache: return [] errors = self.switch_field_schema.errors(path) if errors: return errors clazz = PythonPath.resolve_python_path(path) if not hasattr(clazz, self._init_schema_attribute): return [ Error( "Neither class '{}' nor one of its superclasses was decorated with " "@ClassConfigurationSchema.provider".format(path), ) ] schema = getattr(clazz, self._init_schema_attribute) if not isinstance(schema, Dictionary): return [ Error( "Class '{}' attribute '{}' should be a Dictionary Conformity field or one of its subclasses" .format( path, self._init_schema_attribute, ), ) ] self._schema_cache[path] = schema return [] def instantiate_from( self, configuration ): # type: (MutableMapping[HashableType, AnyType]) -> AnyType if not isinstance(configuration, MutableMapping): raise ValidationError( [Error('Not a mutable mapping (dictionary)')]) errors = self.errors(configuration) if errors: raise ValidationError(errors) clazz = configuration.get('object') if not clazz: clazz = PythonPath.resolve_python_path(configuration['path']) return clazz(**configuration.get('kwargs', {})) def introspect(self): return strip_none({ 'type': self.introspect_type, 'description': self.description, 'base_class': six.text_type(self.base_class.__name__), 'default_path': self.default_path, 'switch_field': 'path', 'switch_field_schema': self.switch_field_schema.introspect(), 'kwargs_field': 'kwargs', 'kwargs_contents_map': {k: v.introspect() for k, v in six.iteritems(self._schema_cache)}, }) @staticmethod def provider(schema): # type: (Dictionary) -> Callable[[Type], Type] if not isinstance(schema, Dictionary): raise TypeError( "'schema' must be an instance of the Dictionary Conformity field or one of its subclasses" ) def wrapper(cls): # type: (Type) -> Type if not isinstance(cls, type): raise TypeError( "ClassConfigurationSchema.provider can only decorate classes" ) setattr(cls, ClassConfigurationSchema._init_schema_attribute, schema) return cls return wrapper
class PythonPath(Base): """ Accepts only a unicode path to an importable Python type, function, or variable, including the full path to the enclosing module. Both '.' and ':' are recognized as valid separators between module name and item name, but if the item is not a top-level member of the module, it can only be accessed by using ':' as the separator. All of the following are valid type name formats: foo.bar.MyClass foo.bar:MyClass foo.bar.my_function foo.bar.MY_CONSTANT foo.bar:MyClass.MY_CONSTANT baz.qux:ParentClass.SubClass This field performs two validations: First that the path is a unicode string, and second that the item is importable (exists). If you later need to actually access that item, you can use the `resolve_python_path` static method. Imported items are cached for faster future lookup. You can optionally specify a `value_schema` argument to this field, itself a Conformity field, which will perform further validation on the value of the imported item. """ introspect_type = 'python_path' value_schema = attr.ib( default=None, validator=attr_is_optional( attr_is_conformity_field())) # type: Optional[Base] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] _module_cache = {} # type: Dict[six.text_type, ModuleType] _import_cache = { } # type: Dict[TupleType[six.text_type, six.text_type], AnyType] def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, six.text_type): return [Error('Not a unicode string')] try: thing = self.resolve_python_path(value) except ValueError: return [ Error('Value "{}" is not a valid Python import path'.format( value)) ] except ImportError as e: return [Error(six.text_type(e.args[0]))] except AttributeError as e: return [Error(six.text_type(e.args[0]))] if self.value_schema: return self.value_schema.errors(thing) return [] def introspect(self): # type: () -> Dict[six.text_type, AnyType] return strip_none({ 'type': self.introspect_type, 'description': self.description, 'value_schema': self.value_schema.introspect() if self.value_schema else None, }) @classmethod def resolve_python_path(cls, type_path): # type: (six.text_type) -> AnyType if ':' in type_path: module_name, local_path = type_path.split(':', 1) else: module_name, local_path = type_path.rsplit('.', 1) cache_key = (module_name, local_path) if cache_key in cls._import_cache: return cls._import_cache[cache_key] if module_name not in cls._module_cache: cls._module_cache[module_name] = importlib.import_module( module_name) thing = cls._module_cache[module_name] # type: AnyType for bit in local_path.split('.'): thing = getattr(thing, bit) cls._import_cache[cache_key] = thing return thing
class TemporalBase(Base): """ Common base class for all temporal types. Cannot be used on its own without extension. """ # These four must be overridden introspect_type = None # type: six.text_type valid_isinstance = None # type: Optional[Union[Type, TupleType[Type, ...]]] valid_noun = None # type: six.text_type valid_types = None # type: FrozenSet[Type] gt = attr.ib( default=None ) # type: Union[datetime.date, datetime.time, datetime.datetime, datetime.timedelta] gte = attr.ib( default=None ) # type: Union[datetime.date, datetime.time, datetime.datetime, datetime.timedelta] lt = attr.ib( default=None ) # type: Union[datetime.date, datetime.time, datetime.datetime, datetime.timedelta] lte = attr.ib( default=None ) # type: Union[datetime.date, datetime.time, datetime.datetime, datetime.timedelta] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def __attrs_post_init__(self): # type: () -> None if self.gt is not None and self._invalid(self.gt): raise TypeError( "'gt' value {!r} cannot be used for comparisons in this type". format(self.gt)) if self.gte is not None and self._invalid(self.gte): raise TypeError( "'gte' value {!r} cannot be used for comparisons in this type". format(self.gte)) if self.lt is not None and self._invalid(self.lt): raise TypeError( "'lt' value {!r} cannot be used for comparisons in this type". format(self.lt)) if self.lte is not None and self._invalid(self.lte): raise TypeError( "'lte' value {!r} cannot be used for comparisons in this type". format(self.lte)) @classmethod def _invalid(cls, value): return type(value) not in cls.valid_types and ( not cls.valid_isinstance or not isinstance(value, cls.valid_isinstance)) def errors(self, value): # type: (AnyType) -> ListType[Error] if self._invalid(value): # using stricter type checking, because date is subclass of datetime, but they're not comparable return [Error('Not a {} instance'.format(self.valid_noun))] errors = [] if self.gt is not None and value <= self.gt: errors.append(Error('Value not > {}'.format(self.gt))) if self.lt is not None and value >= self.lt: errors.append(Error('Value not < {}'.format(self.lt))) if self.gte is not None and value < self.gte: errors.append(Error('Value not >= {}'.format(self.gte))) elif self.lte is not None and value > self.lte: errors.append(Error('Value not <= {}'.format(self.lte))) return errors def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'gt': six.text_type(self.gt) if self.gt else None, 'gte': six.text_type(self.gte) if self.gte else None, 'lt': six.text_type(self.lt) if self.lt else None, 'lte': six.text_type(self.lte) if self.lte else None, })
class Dictionary(Base): """ Conformity field that ensures that the value is a dictionary with a specific set of keys and value that validate with the Conformity fields associated with those keys (`contents`). Keys are required unless they are listed in the `optional_keys` argument. No extra keys are allowed unless the `allow_extra_keys` argument is set to `True`. If the `contents` argument is an instance of `OrderedDict`, the field introspection will include a `display_order` list of keys matching the order they exist in the `OrderedDict`, and errors will be reported in the order the keys exist in the `OrderedDict`. Order will be maintained for any calls to `extend` as long as those calls also use `OrderedDict`. Ordering behavior is undefined otherwise. This field does NOT enforce that the value it validates presents keys in the same order. `OrderedDict` is used strictly for documentation and error-object-ordering purposes only. """ introspect_type = 'dictionary' # Makes MyPy allow optional_keys to have this type _optional_keys_default = frozenset( ) # type: Union[TupleType[HashableType, ...], FrozenSet[HashableType]] contents = attr.ib( default=None, validator=attr_is_optional(attr_is_instance(dict)), ) # type: Mapping[HashableType, Base] optional_keys = attr.ib( default=_optional_keys_default, validator=attr_is_iterable(attr_is_instance(object)), ) # type: Union[TupleType[HashableType, ...], FrozenSet[HashableType]] allow_extra_keys = attr.ib(default=None) # type: bool description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] additional_validator = attr.ib( default=None, validator=attr_is_optional( attr_is_instance(AdditionalCollectionValidator)), ) # type: Optional[AdditionalCollectionValidator[Mapping[HashableType, AnyType]]] def __attrs_post_init__(self): # type: () -> None if self.contents is None and getattr(self.__class__, 'contents', None) is not None: # If no contents were provided but a subclass has hard-coded contents, use those self.contents = self.__class__.contents if self.contents is None: # If there are still no contents, raise an error raise ValueError("'contents' is a required argument") if not isinstance(self.contents, dict): raise TypeError("'contents' must be a dict") if (self.optional_keys is self._optional_keys_default and getattr( self.__class__, 'optional_keys', None) is not None): # If the optional_keys argument was defaulted (not specified) but a subclass has it hard-coded, use that self.optional_keys = self.__class__.optional_keys if not isinstance(self.optional_keys, frozenset): self.optional_keys = frozenset(self.optional_keys) if self.allow_extra_keys is None and getattr( self.__class__, 'allow_extra_keys', None) is not None: # If the allow_extra_keys argument was not specified but a subclass has it hard-coded, use that value self.allow_extra_keys = self.__class__.allow_extra_keys if self.allow_extra_keys is None: # If no value is found, default to False self.allow_extra_keys = False if not isinstance(self.allow_extra_keys, bool): raise TypeError("'allow_extra_keys' must be a boolean") if self.description is None and getattr(self.__class__, 'description', None): # If the description was not specified but a subclass has it hard-coded, use that value self.description = self.__class__.description if self.description is not None and not isinstance( self.description, six.text_type): raise TypeError("'description' must be a unicode string") def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, dict): return [Error('Not a dict')] result = [] for key, field in self.contents.items(): # Check key is present if key not in value: if key not in self.optional_keys: result.append( Error('Missing key: {}'.format(key), code=ERROR_CODE_MISSING, pointer=six.text_type(key)), ) else: # Check key type result.extend( update_error_pointer(error, key) for error in (field.errors(value[key]) or [])) # Check for extra keys extra_keys = set(value.keys()) - set(self.contents.keys()) if extra_keys and not self.allow_extra_keys: result.append( Error( 'Extra keys present: {}'.format(', '.join( six.text_type(key) for key in sorted(extra_keys))), code=ERROR_CODE_UNKNOWN, ), ) if not result and self.additional_validator: return self.additional_validator.errors(value) return result def extend( self, contents=None, # type: Optional[Mapping[HashableType, Base]] optional_keys=None, # type: Optional[Union[TupleType[HashableType, ...], FrozenSet[HashableType]]] allow_extra_keys=None, # type: Optional[bool] description=None, # type: Optional[six.text_type] replace_optional_keys=False, # type: bool additional_validator=None, # type: Optional[AdditionalCollectionValidator[Mapping[HashableType, AnyType]]] ): # type: (...) -> Dictionary """ This method allows you to create a new `Dictionary` that extends the current `Dictionary` with additional contents and/or optional keys, and/or replaces the `allow_extra_keys` and/or `description` attributes. :param contents: More contents, if any, to extend the current contents :param optional_keys: More optional keys, if any, to extend the current optional keys :param allow_extra_keys: If non-`None`, this overrides the current `allow_extra_keys` attribute :param description: If non-`None`, this overrides the current `description` attribute :param replace_optional_keys: If `True`, then the `optional_keys` argument will completely replace, instead of extend, the current optional keys :param additional_validator: If non-`None`, this overrides the current `additional_validator` attribute :return: A new `Dictionary` extended from the current `Dictionary` based on the supplied arguments """ optional_keys = frozenset(optional_keys or ()) return Dictionary( contents=cast(Type[Union[Dict, OrderedDict]], type(self.contents))( (k, v) for d in (self.contents, contents) for k, v in six.iteritems(d)) if contents else self.contents, optional_keys=optional_keys if replace_optional_keys else frozenset(self.optional_keys) | optional_keys, allow_extra_keys=self.allow_extra_keys if allow_extra_keys is None else allow_extra_keys, description=self.description if description is None else description, additional_validator=self.additional_validator if additional_validator is None else additional_validator, ) def introspect(self): # type: () -> Introspection display_order = None # type: Optional[ListType[AnyType]] if isinstance(self.contents, OrderedDict): display_order = list(self.contents.keys()) return strip_none({ 'type': self.introspect_type, 'contents': {key: value.introspect() for key, value in self.contents.items()}, 'optional_keys': sorted(self.optional_keys), 'allow_extra_keys': self.allow_extra_keys, 'description': self.description, 'display_order': display_order, 'additional_validation': (self.additional_validator.__class__.__name__ if self.additional_validator else None), })
class Error(Issue): """ Represents an error found during validation of a value. """ code = attr.ib(default=ERROR_CODE_INVALID, validator=attr_is_string()) # type: six.text_type
class Polymorph(Base): """ A Conformity field which has one of a set of possible contents based on a field within it (which must be accessible via `Mapping` key lookups). """ introspect_type = 'polymorph' switch_field = attr.ib(validator=attr_is_string()) # type: six.text_type contents_map = attr.ib( validator=attr_is_instance(dict)) # type: Mapping[HashableType, Base] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] def _get_switch_value(self, value): # type: (AnyType) -> TupleType[six.text_type, bool] # Get switch field value bits = self.switch_field.split('.') switch_value = value valid = True for bit in bits: switch_value = switch_value[bit] if switch_value not in self.contents_map: if '__default__' in self.contents_map: switch_value = '__default__' else: valid = False return switch_value, valid def errors(self, value): # type: (AnyType) -> ListType[Error] switch_value, valid = self._get_switch_value(value) if not valid: return [ Error("Invalid switch value '{}'".format(switch_value), code=ERROR_CODE_UNKNOWN) ] # Get field field = self.contents_map[switch_value] # Run field errors return field.errors(value) def warnings(self, value): # type: (AnyType) -> ListType[Warning] switch_value, valid = self._get_switch_value(value) if valid: field = self.contents_map[switch_value] return field.warnings(value) return [] def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'description': self.description, 'switch_field': self.switch_field, 'contents_map': { key: value.introspect() for key, value in self.contents_map.items() }, })
class Warning(Issue): """ Represents a warning found during validation of a value. """ code = attr.ib(default=WARNING_CODE_WARNING, validator=attr_is_string()) # type: six.text_type
class _BaseSequenceOrSet(Base): """ Conformity field that ensures that the value is a list of items that all pass validation with the Conformity field passed to the `contents` argument and optionally establishes boundaries for that list with the `max_length` and `min_length` arguments. """ contents = attr.ib() max_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] min_length = attr.ib(default=None, validator=attr_is_optional( attr_is_int())) # type: Optional[int] description = attr.ib( default=None, validator=attr_is_optional( attr_is_string())) # type: Optional[six.text_type] additional_validator = attr.ib( default=None, validator=attr_is_optional( attr_is_instance(AdditionalCollectionValidator)), ) # type: Optional[AdditionalCollectionValidator[AnyType]] valid_types = None # type: Union[Type[Sized], TupleType[Type[Sized], ...]] type_noun = None # deprecated, will be removed in Conformity 2.0 introspect_type = None # type: six.text_type type_error = None # type: six.text_type def __attrs_post_init__(self): # type: () -> None if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length: raise ValueError( 'min_length cannot be greater than max_length in UnicodeString' ) def errors(self, value): # type: (AnyType) -> ListType[Error] if not isinstance(value, self.valid_types): return [Error(self.type_error)] result = [] if self.max_length is not None and len(value) > self.max_length: result.append( Error('List is longer than {}'.format(self.max_length)), ) elif self.min_length is not None and len(value) < self.min_length: result.append( Error('List is shorter than {}'.format(self.min_length)), ) for lazy_pointer, element in self._enumerate(value): result.extend( update_error_pointer(error, lazy_pointer.get()) for error in (self.contents.errors(element) or [])) if not result and self.additional_validator: return self.additional_validator.errors(value) return result @classmethod def _enumerate(cls, values): # We use a lazy pointer here so that we don't evaluate the pointer for every item that doesn't generate an # error. We only evaluate the pointer for each item that does generate an error. This is critical in sets, # where the pointer is the value converted to a string instead of an index. return ((cls.LazyPointer(i, value), value) for i, value in enumerate(values)) def introspect(self): # type: () -> Introspection return strip_none({ 'type': self.introspect_type, 'contents': self.contents.introspect(), 'max_length': self.max_length, 'min_length': self.min_length, 'description': self.description, 'additional_validation': (self.additional_validator.__class__.__name__ if self.additional_validator else None), }) class LazyPointer(object): def __init__(self, index, _): self.get = lambda: index