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