1# -*- coding: UTF-8 -*-
2#
3# The MIT License
4#
5# Copyright (c) 2009-2012, 2014 Felix Schwarz <felix.schwarz@oss.schwarz.eu>
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23# THE SOFTWARE.
24
25import copy
26import inspect
27import types
28import warnings
29
30from pycerberus.errors import EmptyError, InvalidArgumentsError, InvalidDataError, \
31    ThreadSafetyError
32from pycerberus.i18n import _, GettextTranslation
33from pycerberus.lib import SuperProxy
34from pycerberus.lib import six
35
36
37__all__ = ['BaseValidator', 'Validator']
38
39
40class NoValueSet(object):
41    pass
42
43
44class EarlyBindForMethods(type):
45
46    super = SuperProxy()
47
48    def __new__(cls, classname, direct_superclasses, class_attributes_dict):
49        validator_class = type.__new__(cls, classname, direct_superclasses, class_attributes_dict)
50        cls._simulate_early_binding_for_message_methods(validator_class)
51        return validator_class
52
53    def _simulate_early_binding_for_message_methods(cls, validator_class):
54        # Need to create a dynamic method if messages are defined in a
55        # class-level dict.
56        if not callable(validator_class.messages):
57            messages_dict = validator_class.messages.copy()
58            def messages(self):
59                return messages_dict
60            validator_class.messages = messages
61
62        # We need to simulate 'early binding' so that we can reference the
63        # messages() method which is defined in the class to be created!
64        def keys(self):
65            return validator_class.messages(self)
66        # make sphinx happy
67        keys.__doc__ = validator_class.keys.__doc__
68        validator_class.keys = keys
69
70        if validator_class.__name__ == 'BaseValidator' or \
71            getattr(validator_class.message_for_key, 'autogenerated', False):
72            def message_for_key(self, key, context):
73                return validator_class.messages(self)[key]
74            message_for_key.autogenerated = True
75            # make sphinx happy
76            message_for_key.__doc__ = validator_class.message_for_key.__doc__
77            validator_class.message_for_key = message_for_key
78    _simulate_early_binding_for_message_methods = classmethod(_simulate_early_binding_for_message_methods)
79
80
81@six.add_metaclass(EarlyBindForMethods)
82class BaseValidator(object):
83    """The BaseValidator implements only the minimally required methods.
84    Therefore it does not put many constraints on you. Most users probably want
85    to use the ``Validator`` class which already implements some commonly used
86    features.
87
88    You can pass ``messages`` a dict of messages during instantiation to
89    overwrite messages specified in the validator without the need to create
90    a subclass."""
91
92    super = SuperProxy()
93
94    def __init__(self, messages=None):
95        if not messages:
96            return
97
98        old_messages = self.messages
99        old_message_for_key = self.message_for_key
100        def messages_(self):
101            all_messages = old_messages()
102            all_messages.update(messages)
103            return all_messages
104        def keys_(self):
105            return tuple(messages_(self).keys())
106        def message_for_key(self, key, context):
107            if key in messages:
108                return messages[key]
109            return old_message_for_key(key, context)
110        self.messages = self._new_instancemethod(messages_)
111        self.keys = self._new_instancemethod(keys_)
112        self.message_for_key = self._new_instancemethod(message_for_key)
113
114    def _new_instancemethod(self, method):
115        if six.PY2:
116            return types.MethodType(method, self, self.__class__)
117        return types.MethodType(method, self)
118
119    def messages(self):
120        """Return all messages which are defined by this validator as a
121        key/message dictionary. Alternatively you can create a class-level
122        dictionary which contains these keys/messages.
123
124        You must declare all your messages here so that all keys are known
125        after this method was called.
126
127        Calling this method might be costly when you have a lot of messages and
128        returning them is expensive. You can reduce the overhead in some
129        situations by implementing ``message_for_key()``"""
130        return {}
131
132    def copy(self):
133        """Return a copy of this instance."""
134        clone = copy.copy(self)
135        was_frozen = False
136        if hasattr(clone, 'is_internal_state_frozen'):
137            was_frozen = clone.is_internal_state_frozen()
138            clone.set_internal_state_freeze(False)
139        # deepcopy only copies instance-level attribute but we need to copy also
140        # class-level attributes to support the declarative syntax properly.
141        # I did not want to add more metaclass magic (that's already complicated
142        # enough).
143        klass = self.__class__
144        for name in dir(clone):
145            if name in ('__dict__', '__doc__', '__module__', '__slotnames__',
146                        '__weakref__', 'super'):
147                continue
148            elif not hasattr(klass, name):
149                # this is an instance-specific attribute/method, already copied
150                continue
151            clone_value = getattr(clone, name)
152            klass_value = getattr(klass, name)
153            if id(clone_value) != id(klass_value):
154                continue
155            if name.startswith('__') and callable(clone_value):
156                continue
157            elif inspect.isroutine(clone_value):
158                continue
159
160            if hasattr(clone_value, 'copy'):
161                copied_value = clone_value.copy()
162            else:
163                copied_value = copy.copy(clone_value)
164            setattr(clone, name, copied_value)
165        if was_frozen:
166            clone.set_internal_state_freeze(True)
167        return clone
168
169    def message_for_key(self, key, context):
170        """Return a message for a specific key. Implement this method if you
171        want to avoid calls to messages() which might be costly (otherwise
172        implementing this method is optional)."""
173        raise NotImplementedError('message_for_key() should have been replaced by a metaclass')
174
175    def keys(self):
176        """Return all keys defined by this specific validator class."""
177        raise NotImplementedError('keys() should have been replaced by a metaclass')
178
179    def raise_error(self, key, value, context, errorclass=InvalidDataError, **values):
180        """Raise an InvalidDataError for the given key."""
181        msg_template = self.message_for_key(key, context)
182        raise errorclass(msg_template % values, value, key=key, context=context)
183
184    def error(self, *args, **kwargs):
185        warnings.warn("BaseValidator.error() is deprecated. Please use 'raise_error' instead!", DeprecationWarning)
186        self.raise_error(*args, **kwargs)
187
188    def process(self, value, context=None):
189        """This is the method to validate your input. The validator returns a
190        (Python) representation of the given input ``value``.
191
192        In case of errors a ``InvalidDataError`` is thrown."""
193        return value
194
195    def revert_conversion(self, value, context=None):
196        """Undo the conversion of ``process()`` and return a "string-like"
197        representation. This method is especially useful for widget libraries
198        like ToscaWigets so they can render Python data types in a human
199        readable way.
200        The returned value does not have to be an actual Python string as long
201        as it has a meaningful unicode() result. Generally the validator
202        should accept the return value in its '.process()' method."""
203        if value is None:
204            return None
205        return six.text_type(value)
206
207    def to_string(self, *args, **kwargs):
208        warnings.warn("BaseValidator.to_string() is deprecated. Please use 'revert_conversion' instead!", DeprecationWarning)
209        self.revert_conversion(*args, **kwargs)
210
211
212class Validator(BaseValidator):
213    """The Validator is the base class of most validators and implements
214    some commonly used features like required values (raise exception if no
215    value was provided) or default values in case no value is given.
216
217    This validator splits conversion and validation into two separate steps:
218    When a value is ``process()``ed, the validator first calls ``convert()``
219    which performs some checks on the value and eventually returns the converted
220    value. Only if the value was converted correctly, the ``validate()``
221    function can do additional checks on the converted value and possibly raise
222    an Exception in case of errors. If you only want to do additional checks
223    (but no conversion) in your validator, you can implement ``validate()`` and
224    simply assume that you get the correct Python type (e.g. int).
225
226    Of course if you can also raise a ``ValidationError`` inside of ``convert()`` -
227    often errors can only be detected during the conversion process.
228
229    By default, a validator will raise an ``InvalidDataError`` if no value was
230    given (unless you set a default value). If ``required`` is False, the
231    default is None. All exceptions thrown by validators must be derived from
232    ``ValidationError``. Exceptions caused by invalid user input should use
233    ``InvalidDataError`` or one of the subclasses.
234
235    If ``strip`` is True (default is False) and the input value has a ``strip()``
236    method, the input will be stripped before it is tested for empty values and
237    passed to the ``convert()``/``validate()`` methods.
238
239    In order to prevent programmer errors, an exception will be raised if
240    you set ``required`` to True but provide a default value as well.
241    """
242
243    def __init__(self, default=NoValueSet, required=NoValueSet, strip=False, messages=None):
244        self.super(messages=messages)
245        self._default = default
246        self._required = required
247        self._check_argument_consistency()
248        self._strip_input = strip
249        self._implementations, self._implementation_by_class = self._freeze_implementations_for_class()
250        if self.is_internal_state_frozen() not in (True, False):
251            self._is_internal_state_frozen = True
252
253    # --------------------------------------------------------------------------
254    # initialization
255
256    def _check_argument_consistency(self):
257        if self.is_required(set_explicitely=True) and self._has_default_value_set():
258            msg = 'Set default value (%s) has no effect because a value is required.' % repr(self._default)
259            raise InvalidArgumentsError(msg)
260
261    def _has_default_value_set(self):
262        return (self._default is not NoValueSet)
263
264    def _freeze_implementations_for_class(self):
265        class_for_key = {}
266        implementations_for_class = {}
267        known_functions = set()
268        for cls in reversed(inspect.getmro(self.__class__)):
269            if not self._class_defines_custom_keys(cls, known_functions):
270                continue
271            defined_keys = cls.keys(self)
272            if cls == self.__class__:
273                cls = self
274                defined_keys = self.keys()
275            known_functions.add(cls.keys)
276            for key in defined_keys:
277                class_for_key[key] = self._implementations_by_key(cls)
278                implementations_for_class[cls] = class_for_key[key]
279        return class_for_key, implementations_for_class
280
281    def _implementations_by_key(self, cls):
282        implementations_by_key = dict()
283        for name in ['translation_parameters', 'keys', 'message_for_key', 'translate_message']:
284            implementations_by_key[name] = getattr(cls, name)
285        return implementations_by_key
286
287    def _class_defines_custom_keys(self, cls, known_functions):
288        return hasattr(cls, 'keys') and cls.keys not in known_functions
289
290    # --------------------------------------------------------------------------
291    # Implementation of BaseValidator API
292
293    def messages(self):
294        return {'empty': _('Value must not be empty.')}
295
296    def exception(self, key, value, context, errorclass=InvalidDataError,
297            error_dict=None, error_list=(), **values):
298        translated_message = self.message(key, context, **values)
299        return errorclass(translated_message, value, key=key, context=context,
300            error_dict=error_dict, error_list=error_list)
301
302    def raise_error(self, key, value, context, errorclass=InvalidDataError,
303            error_dict=None, error_list=(), **values):
304        raise self.exception(key, value, context, errorclass=errorclass,
305            error_dict=error_dict, error_list=error_list, **values)
306
307    def process(self, value, context=None):
308        if context is None:
309            context = {}
310        if self._strip_input and hasattr(value, 'strip'):
311            value = value.strip()
312        value = super(Validator, self).process(value, context)
313        if self.is_empty(value, context) == True:
314            if self.is_required() == True:
315                self.raise_error('empty', value, context, errorclass=EmptyError)
316            return self.empty_value(context)
317        converted_value = self.convert(value, context)
318        self.validate(converted_value, context)
319        return converted_value
320
321    # --------------------------------------------------------------------------
322    # Defining a convenience API
323
324    def convert(self, value, context):
325        """Convert the input value to a suitable Python instance which is
326        returned. If the input is invalid, raise an ``InvalidDataError``."""
327        return value
328
329    def validate(self, converted_value, context):
330        """Perform additional checks on the value which was processed
331        successfully before (otherwise this method is not called). Raise an
332        InvalidDataError if the input data is invalid.
333
334        You can implement only this method in your validator if you just want to
335        add additional restrictions without touching the actual conversion.
336
337        This method must not modify the ``converted_value``."""
338        pass
339
340    # REFACT: rename to default_value()
341    def empty_value(self, context):
342        """Return the 'empty' value for this validator (usually None)."""
343        if self._default is NoValueSet:
344            return None
345        return self._default
346
347    def is_empty(self, value, context):
348        """Decide if the value is considered an empty value."""
349        return (value is None)
350
351    def is_required(self, set_explicitely=False):
352        if self._required == True:
353            return True
354        elif (not set_explicitely) and (self._required == NoValueSet):
355            return True
356        return False
357
358    # -------------------------------------------------------------------------
359    # i18n: public API
360
361    def translation_parameters(self, context):
362        return {'domain': 'pycerberus'}
363
364    def translate_message(self, key, native_message, translation_parameters, context):
365        # This method can be overridden on a by-class basis to get translations
366        # to support non-gettext translation mechanisms (e.g. from a db)
367        return GettextTranslation(**translation_parameters).ugettext(native_message)
368
369    def message(self, key, context, **values):
370        # This method can be overridden globally to use a different message
371        # lookup / translation mechanism altogether
372        native_message = self._implementation(key, 'message_for_key', context)(key)
373        translation_parameters = self._implementation(key, 'translation_parameters', context)()
374        translation_function = self._implementation(key, 'translate_message', context)
375        translated_template = translation_function(key, native_message, translation_parameters)
376        return translated_template % values
377
378    # -------------------------------------------------------------------------
379    # private
380
381    def _implementation(self, key, methodname, context):
382        def context_key_wrapper(*args):
383            method = self._implementations[key][methodname]
384            args = list(args) + [context]
385            if self._is_unbound(method):
386                return method(self, *args)
387            return method(*args)
388        return context_key_wrapper
389
390    def _is_unbound(self, method):
391        if six.PY2:
392            return (method.im_self is None)
393        return (getattr(method, '__self__',  None) is None)
394
395    def is_internal_state_frozen(self):
396        is_frozen = getattr(self, '_is_internal_state_frozen', NoValueSet)
397        if is_frozen == NoValueSet:
398            return None
399        return bool(is_frozen)
400
401    def set_internal_state_freeze(self, is_frozen):
402        self.__dict__['_is_internal_state_frozen'] = is_frozen
403
404    def __setattr__(self, name, value):
405        "Prevent non-threadsafe use of Validators by unexperienced developers"
406        if self.is_internal_state_frozen():
407            raise ThreadSafetyError('Do not store state in a validator instance as this violates thread safety.')
408        self.__dict__[name] = value
409
410    # -------------------------------------------------------------------------
411
412
413