Exemplo n.º 1
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
        })
Exemplo n.º 2
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
        })
Exemplo n.º 3
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