Beispiel #1
0
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,
        })
Beispiel #2
0
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,
        })
Beispiel #3
0
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
        })
Beispiel #4
0
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 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,
        })
Beispiel #6
0
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),
        })
Beispiel #7
0
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,
        })
Beispiel #8
0
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,
        })
Beispiel #9
0
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),
        })
Beispiel #10
0
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]
Beispiel #11
0
class Sequence(_BaseSequenceOrSet):
    additional_validator = attr.ib(
        default=None,
        validator=attr_is_optional(
            attr_is_instance(AdditionalCollectionValidator)),
    )  # type: Optional[AdditionalCollectionValidator[SequenceType]]

    valid_types = SequenceType
    introspect_type = 'sequence'
    type_error = 'Not a sequence'
Beispiel #12
0
class List(_BaseSequenceOrSet):
    additional_validator = attr.ib(
        default=None,
        validator=attr_is_optional(
            attr_is_instance(AdditionalCollectionValidator)),
    )  # type: Optional[AdditionalCollectionValidator[list]]

    valid_types = list
    introspect_type = 'list'
    type_error = 'Not a list'
Beispiel #13
0
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]
Beispiel #14
0
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,
        })
Beispiel #15
0
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,
        })
Beispiel #16
0
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,
        })
Beispiel #17
0
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()
            },
        })
Beispiel #18
0
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,
        })
Beispiel #19
0
class Set(_BaseSequenceOrSet):
    """
    Conformity field that ensures that the value is an abstract set 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.
    """
    additional_validator = attr.ib(
        default=None,
        validator=attr_is_optional(
            attr_is_instance(AdditionalCollectionValidator)),
    )  # type: Optional[AdditionalCollectionValidator[AbstractSet]]

    valid_types = AbstractSet
    introspect_type = 'set'
    type_error = 'Not a set or frozenset'

    class LazyPointer(object):
        def __init__(self, _, value):
            self.get = lambda: '[{}]'.format(str(value))
Beispiel #20
0
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,
        })
Beispiel #21
0
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
Beispiel #22
0
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,
        })
Beispiel #23
0
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,
        })
Beispiel #24
0
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,
        })
Beispiel #25
0
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),
        })
Beispiel #26
0
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
Beispiel #27
0
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
Beispiel #28
0
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()
            },
        })
Beispiel #29
0
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
Beispiel #30
0
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)