""" Core classes for validation. """ import gettext import os import re import textwrap import warnings from . import declarative __all__ = ['NoDefault', 'Invalid', 'Validator', 'Identity', 'FancyValidator', 'is_empty', 'is_validator'] def get_localedir(): """Retrieve the location of locales.""" file_dir = os.path.join(os.path.dirname(__file__), 'i18n') if not hasattr(os, 'access'): # this happens on Google App Engine return file_dir import importlib.resources as resources # try to get it via the resource pkg_name = __name__.split('.', 1)[0] try: resource_dir = resources.files(pkg_name) / 'i18n' except AttributeError: # Python < 3.11 with resources.path(pkg_name, 'i18n') as resource_dir: pass if os.access(resource_dir, os.R_OK | os.X_OK): # if the resource is present, use it return resource_dir # otherwise, search the filesystem if os.access(file_dir, os.R_OK | os.X_OK): return file_dir # fallback on the system catalog return os.path.normpath('/usr/share/locale') def set_stdtranslation(domain="FormEncode", languages=None, localedir=get_localedir()): t = gettext.translation(domain=domain, languages=languages, localedir=localedir, fallback=True) global _stdtrans _stdtrans = t.gettext set_stdtranslation() # Dummy i18n translation function, nothing is translated here. # Instead, this is actually done in api.Validator.message. # The surrounding _('string') of the strings is only for extracting # the strings automatically. # If you run pygettext with this source, comment this function out temporarily. _ = lambda s: s def deprecation_warning(old, new=None, stacklevel=3): """Show a deprecation warning.""" msg = '%s is deprecated' % old if new: msg += '; use %s instead' % new warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) def deprecated(old=None, new=None): """A decorator which can be used to mark functions as deprecated.""" def outer(func): def inner(*args, **kwargs): deprecation_warning(old or func.__name__, new) return func(*args, **kwargs) return inner return outer class NoDefault: """A dummy value used for parameters with no default.""" def is_empty(value): """Check whether the given value should be considered "empty".""" return value is None or value == '' or value == b'' or ( isinstance(value, (list, tuple, dict)) and not value) def is_validator(obj): """Check whether obj is a Validator instance or class.""" return (isinstance(obj, Validator) or (isinstance(obj, type) and issubclass(obj, Validator))) class Invalid(Exception): """ This is raised in response to invalid input. It has several public attributes: ``msg``: The message, *without* values substituted. For instance, if you want HTML quoting of values, you can apply that. ``substituteArgs``: The arguments (a dictionary) to go with ``msg``. ``str(self)``: The message describing the error, with values substituted. ``value``: The offending (invalid) value. ``state``: The state that went with this validator. This is an application-specific object. ``error_list``: If this was a compound validator that takes a repeating value, and sub-validator(s) had errors, then this is a list of those exceptions. The list will be the same length as the number of values -- valid values will have None instead of an exception. ``error_dict``: Like ``error_list``, but for dictionary compound validators. """ def __init__(self, msg, value, state, error_list=None, error_dict=None): Exception.__init__(self, msg, value, state, error_list, error_dict) self.msg = msg self.value = value self.state = state self.error_list = error_list self.error_dict = error_dict assert (not self.error_list or not self.error_dict), ( "Errors shouldn't have both error dicts and lists " "(error %s has %s and %s)" % (self, self.error_list, self.error_dict)) def __str__(self): return self.msg def unpack_errors(self, encode_variables=False, dict_char='.', list_char='-'): """ Returns the error as a simple data structure -- lists, dictionaries, and strings. If ``encode_variables`` is true, then this will return a flat dictionary, encoded with variable_encode """ if self.error_list: assert not encode_variables, ( "You can only encode dictionary errors") assert not self.error_dict return [item.unpack_errors() if item else item for item in self.error_list] if self.error_dict: result = {} for name, item in self.error_dict.items(): result[name] = item if isinstance( item, str) else item.unpack_errors() if encode_variables: from . import variabledecode result = variabledecode.variable_encode( result, add_repetitions=False, dict_char=dict_char, list_char=list_char) for key in list(result.keys()): if not result[key]: del result[key] return result assert not encode_variables, ( "You can only encode dictionary errors") return self.msg # Base Classes class Validator(declarative.Declarative): """ The base class of most validators. See ``IValidator`` for more, and ``FancyValidator`` for the more common (and more featureful) class. """ _messages = {} if_missing = NoDefault repeating = False compound = False accept_iterator = False gettextargs = {} # In case you don't want to use __builtins__._ # although it may be defined, set use_builtins_gettext to False: use_builtins_gettext = True __singletonmethods__ = ( 'to_python', 'from_python', 'message', 'all_messages', 'subvalidators') @staticmethod def __classinit__(cls, new_attrs): if 'messages' in new_attrs: cls._messages = cls._messages.copy() cls._messages.update(cls.messages) del cls.messages cls._initialize_docstring() def __init__(self, *args, **kw): if 'messages' in kw: self._messages = self._messages.copy() self._messages.update(kw.pop('messages')) declarative.Declarative.__init__(self, *args, **kw) def to_python(self, value, state=None): return value def from_python(self, value, state=None): return value def message(self, msgName, state, **kw): # determine translation function try: trans = state._ except AttributeError: try: if self.use_builtins_gettext: import builtins trans = builtins._ else: trans = _stdtrans except AttributeError: trans = _stdtrans if not callable(trans): trans = _stdtrans msg = trans(self._messages[msgName], **self.gettextargs) try: return msg % kw except KeyError as e: raise KeyError( "Key not found (%s) for %r=%r %% %r (from: %s)" % (e, msgName, self._messages.get(msgName), kw, ', '.join(self._messages))) def all_messages(self): """ Return a dictionary of all the messages of this validator, and any subvalidators if present. Keys are message names, values may be a message or list of messages. This is really just intended for documentation purposes, to show someone all the messages that a validator or compound validator (like Schemas) can produce. @@: Should this produce a more structured set of messages, so that messages could be unpacked into a rendered form to see the placement of all the messages? Well, probably so. """ msgs = self._messages.copy() for v in self.subvalidators(): inner = v.all_messages() for key, msg in inner: if key in msgs: if msgs[key] == msg: continue if isinstance(msgs[key], list): msgs[key].append(msg) else: msgs[key] = [msgs[key], msg] else: msgs[key] = msg return msgs def subvalidators(self): """ Return any validators that this validator contains. This is not useful for functional, except to inspect what values are available. Specifically the ``.all_messages()`` method uses this to accumulate all possible messages. """ return [] @classmethod def _initialize_docstring(cls): """ This changes the class's docstring to include information about all the messages this validator uses. """ doc = [ textwrap.dedent(cls.__doc__ or '').rstrip(), '\n\n**Messages**\n\n'] for name, default in sorted(cls._messages.items()): default = re.sub(r'(%\(.*?\)[rsifcx])', r'``\1``', default) doc.append('``' + name + '``:\n') doc.append(' ' + default + '\n\n') cls.__doc__ = ''.join(doc) class _Identity(Validator): def __repr__(self): return 'validators.Identity' Identity = _Identity() class FancyValidator(Validator): """ FancyValidator is the (abstract) superclass for various validators and converters. A subclass can validate, convert, or do both. There is no formal distinction made here. Validators have two important external methods: ``.to_python(value, state)``: Attempts to convert the value. If there is a problem, or the value is not valid, an Invalid exception is raised. The argument for this exception is the (potentially HTML-formatted) error message to give the user. ``.from_python(value, state)``: Reverses ``.to_python()``. These two external methods make use of the following four important internal methods that can be overridden. However, none of these *have* to be overridden, only the ones that are appropriate for the validator. ``._convert_to_python(value, state)``: This method converts the source to a Python value. It returns the converted value, or raises an Invalid exception if the conversion cannot be done. The argument to this exception should be the error message. Contrary to ``.to_python()`` it is only meant to convert the value, not to fully validate it. ``._convert_from_python(value, state)``: Should undo ``._convert_to_python()`` in some reasonable way, returning a string. ``._validate_other(value, state)``: Validates the source, before ``._convert_to_python()``, or after ``._convert_from_python()``. It's usually more convenient to use ``._validate_python()`` however. ``._validate_python(value, state)``: Validates a Python value, either the result of ``._convert_to_python()``, or the input to ``._convert_from_python()``. You should make sure that all possible validation errors are raised in at least one these four methods, not matter which. Subclasses can also override the ``__init__()`` method if the ``declarative.Declarative`` model doesn't work for this. Validators should have no internal state besides the values given at instantiation. They should be reusable and reentrant. All subclasses can take the arguments/instance variables: ``if_empty``: If set, then this value will be returned if the input evaluates to false (empty list, empty string, None, etc), but not the 0 or False objects. This only applies to ``.to_python()``. ``not_empty``: If true, then if an empty value is given raise an error. (Both with ``.to_python()`` and also ``.from_python()`` if ``._validate_python`` is true). ``strip``: If true and the input is a string, strip it (occurs before empty tests). ``if_invalid``: If set, then when this validator would raise Invalid during ``.to_python()``, instead return this value. ``if_invalid_python``: If set, when the Python value (converted with ``.from_python()``) is invalid, this value will be returned. ``accept_python``: If True (the default), then ``._validate_python()`` and ``._validate_other()`` will not be called when ``.from_python()`` is used. These parameters are handled at the level of the external methods ``.to_python()`` and ``.from_python`` already; if you overwrite one of the internal methods, you usually don't need to care about them. """ if_invalid = NoDefault if_invalid_python = NoDefault if_empty = NoDefault not_empty = False accept_python = True strip = False messages = dict( empty=_("Please enter a value"), badType=_("The input must be a string (not a %(type)s: %(value)r)"), noneType=_("The input must be a string (not None)")) _inheritance_level = 0 _deprecated_methods = ( ('_to_python', '_convert_to_python'), ('_from_python', '_convert_from_python'), ('validate_python', '_validate_python'), ('validate_other', '_validate_other')) @staticmethod def __classinit__(cls, new_attrs): Validator.__classinit__(cls, new_attrs) # account for deprecated methods cls._inheritance_level += 1 if '_deprecated_methods' in new_attrs: cls._deprecated_methods = cls._deprecated_methods + new_attrs[ '_deprecated_methods'] for old, new in cls._deprecated_methods: if old in new_attrs: if new not in new_attrs: deprecation_warning(old, new, stacklevel=cls._inheritance_level + 2) setattr(cls, new, new_attrs[old]) elif new in new_attrs: setattr(cls, old, deprecated(old=old, new=new)(new_attrs[new])) def to_python(self, value, state=None): try: if self.strip and isinstance(value, str): value = value.strip() elif hasattr(value, 'mixed'): # Support Paste's MultiDict value = value.mixed() if self.is_empty(value): if self.not_empty: raise Invalid(self.message('empty', state), value, state) if self.if_empty is not NoDefault: return self.if_empty return self.empty_value(value) vo = self._validate_other if vo and vo is not self._validate_noop: vo(value, state) tp = self._convert_to_python if tp: value = tp(value, state) vp = self._validate_python if vp and vp is not self._validate_noop: vp(value, state) except Invalid: value = self.if_invalid if value is NoDefault: raise return value def from_python(self, value, state=None): try: if self.strip and isinstance(value, str): value = value.strip() if not self.accept_python: if self.is_empty(value): if self.not_empty: raise Invalid(self.message('empty', state), value, state) return self.empty_value(value) vp = self._validate_python if vp and vp is not self._validate_noop: vp(value, state) fp = self._convert_from_python if fp: value = fp(value, state) vo = self._validate_other if vo and vo is not self._validate_noop: vo(value, state) else: if self.is_empty(value): return self.empty_value(value) fp = self._convert_from_python if fp: value = fp(value, state) except Invalid: value = self.if_invalid_python if value is NoDefault: raise return value def is_empty(self, value): return is_empty(value) def empty_value(self, value): return None def assert_string(self, value, state): if not isinstance(value, str): raise Invalid(self.message('badType', state, type=type(value), value=value), value, state) def base64encode(self, value): """ Encode a string in base64, stripping whitespace and removing newlines. """ return value.encode('base64').strip().replace('\n', '') def _validate_noop(self, value, state=None): """ A validation method that doesn't do anything. """ pass _validate_python = _validate_other = _validate_noop _convert_to_python = _convert_from_python = None