From 114b1b0679afc28f6752358ca124eb3d28f9e077 Mon Sep 17 00:00:00 2001 From: Franklin Koch Date: Fri, 7 Dec 2018 10:48:22 -0700 Subject: [PATCH 1/2] Pass kwargs through from error to ValidationError --- properties/basic.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/properties/basic.py b/properties/basic.py index 7ea9446..22a7221 100644 --- a/properties/basic.py +++ b/properties/basic.py @@ -325,7 +325,7 @@ def from_json(value, **kwargs): #pyli """Statically load a Property value from JSON value""" return value - def error(self, instance, value, error_class=None, extra=''): + def error(self, instance, value, error_class=None, extra='', **kwargs): """Generate a :code:`ValueError` for invalid value assignment The instance is the containing HasProperties instance, but it may @@ -354,8 +354,13 @@ def error(self, instance, value, error_class=None, extra=''): extra=' {}'.format(extra) if extra else '', ) ) + kwargs.update({ + 'reason': 'invalid', + 'prop': self.name, + 'instance': instance, + }) if issubclass(error_class, ValidationError): - raise error_class(message, 'invalid', self.name, instance) + raise error_class(message, **kwargs) raise error_class(message) def sphinx(self): From 3d102244a7c5e2dd7577b1bde26351bf91a1fc3c Mon Sep 17 00:00:00 2001 From: Franklin Koch Date: Fri, 7 Dec 2018 11:07:04 -0700 Subject: [PATCH 2/2] Validation errors hold related errors, not error tuples Verbose error messages are now constructed through the __str__ method. Error tuples still exist as an @property for backwards compatibility --- properties/base/base.py | 58 ++++++++++++++++++++----------------- properties/base/instance.py | 23 ++++++++++----- properties/base/union.py | 31 +++++++++++--------- properties/utils.py | 53 ++++++++++++++++++++++++++++----- 4 files changed, 111 insertions(+), 54 deletions(-) diff --git a/properties/base/base.py b/properties/base/base.py index db80ca9..781609b 100644 --- a/properties/base/base.py +++ b/properties/base/base.py @@ -278,7 +278,7 @@ class construction and validation. def __init__(self, **kwargs): # Set the keyword arguments with change notifications self._getting_validated = True - self._validation_error_tuples = [] + self._validation_errors = [] self._non_validation_error = None try: for key, val in iteritems(kwargs): @@ -296,24 +296,25 @@ def __init__(self, **kwargs): try: setattr(self, key, val) except utils.ValidationError as val_err: - self._validation_error_tuples += val_err.error_tuples + self._validation_errors.append(val_err) except GENERIC_ERRORS as err: if not self._non_validation_error: self._non_validation_error = err - continue - if self._validation_error_tuples: - self._error_hook(self._validation_error_tuples) - msgs = ['Initialization failed:'] - msgs += [val.message for val in self._validation_error_tuples] + if self._validation_errors: + self._error_hook( + [err.error_tuples for err in self._validation_errors] + ) raise utils.ValidationError( - message='\n- '.join(msgs), - _error_tuples=self._validation_error_tuples, + message='Error initializing {} instance:'.format( + self.__class__.__name__ + ), + _related_errors=self._validation_errors, ) elif self._non_validation_error: raise self._non_validation_error #pylint: disable=raising-bad-type finally: self._getting_validated = False - self._validation_error_tuples = None + self._validation_errors = None self._non_validation_error = None def _get(self, name): @@ -381,7 +382,7 @@ def validate(self): if getattr(self, '_getting_validated', False): return True self._getting_validated = True - self._validation_error_tuples = [] + self._validation_errors = [] self._non_validation_error = None try: for val in itervalues(self._class_validators): @@ -395,24 +396,26 @@ def validate(self): 'Validation failed', None, None, self ) except utils.ValidationError as val_err: - self._validation_error_tuples += val_err.error_tuples + self._validation_errors.append(val_err) except GENERIC_ERRORS as err: if not self._non_validation_error: self._non_validation_error = err - if self._validation_error_tuples: - self._error_hook(self._validation_error_tuples) - msgs = ['Validation failed:'] - msgs += [val.message for val in self._validation_error_tuples] + if self._validation_errors: + self._error_hook( + [err.error_tuples for err in self._validation_errors] + ) raise utils.ValidationError( - message='\n- '.join(msgs), - _error_tuples=self._validation_error_tuples, + message='Error validating {} instance:'.format( + self.__class__.__name__ + ), + _related_errors=self._validation_errors, ) elif self._non_validation_error: raise self._non_validation_error #pylint: disable=raising-bad-type return True finally: self._getting_validated = False - self._validation_error_tuples = None + self._validation_errors = None self._non_validation_error = None @handlers.validator @@ -421,20 +424,21 @@ def _validate_props(self): for key, prop in iteritems(self._props): try: value = self._get(key) - err_msg = 'Invalid value for property {}: {}'.format(key, value) if value is not None: change = dict(name=key, previous=value, value=value, mode='validate') self._notify(change) if not prop.equal(value, change['value']): - raise utils.ValidationError(err_msg, 'invalid', - prop.name, self) + prop.error( + instance=self, + value=value, + extra='Attempting to re-validate failed.', + ) if not prop.assert_valid(self): - raise utils.ValidationError(err_msg, 'invalid', - prop.name, self) - except utils.ValidationError as val_err: - if getattr(self, '_validation_error_tuples', None) is not None: - self._validation_error_tuples += val_err.error_tuples + prop.error(self, value) + except utils.ValidationError as err: + if getattr(self, '_validation_errors', None) is not None: + self._validation_errors.append(err) else: raise return True diff --git a/properties/base/instance.py b/properties/base/instance.py index 2b622fc..fbeefa2 100644 --- a/properties/base/instance.py +++ b/properties/base/instance.py @@ -102,13 +102,14 @@ def validate(self, instance, value): return self.instance_class(**value) return self.instance_class(value) except GENERIC_ERRORS as err: - if hasattr(err, 'error_tuples'): - extra = '({})'.format(' & '.join( - err_tup.message for err_tup in err.error_tuples - )) - else: - extra = '' - self.error(instance, value, extra=extra) + self.error( + instance=instance, + value=value, + error_class=InstanceValidationError, + _related_errors=( + [err] if isinstance(err, utils.ValidationError) else None + ), + ) def assert_valid(self, instance, value=None): """Checks if valid, including HasProperty instances pass validation""" @@ -187,3 +188,11 @@ def sphinx_class(self): pref=self.instance_class.__module__, ) return classdoc + + +class InstanceValidationError(utils.ValidationError): + """ValidationERror to be raised by Instance properties""" + + def __str__(self, tab=1, prefix='- Cause: '): + return super(InstanceValidationError, self).__str__(tab, prefix) + diff --git a/properties/base/union.py b/properties/base/union.py index 1c800c3..2f19a69 100644 --- a/properties/base/union.py +++ b/properties/base/union.py @@ -167,22 +167,20 @@ def _try_prop_method(self, instance, value, method_name): This method gathers all errors and returns them at the end if the method on each of the props fails. """ - error_messages = [] + errors = [] for prop in self.props: try: return getattr(prop, method_name)(instance, value) - except GENERIC_ERRORS as err: - if hasattr(err, 'error_tuples'): - error_messages += [ - err_tup.message for err_tup in err.error_tuples - ] - if error_messages: - extra = 'Possible explanation:' - for message in error_messages: - extra += '\n - {}'.format(message) - else: - extra = '' - self.error(instance, value, extra=extra) + except utils.ValidationError as err: + errors.append(err) + except GENERIC_ERRORS: + pass + self.error( + instance=instance, + value=value, + error_class=UnionValidationError, + _related_errors=errors or None, + ) def validate(self, instance, value): """Check if value is a valid type of one of the Union props""" @@ -264,3 +262,10 @@ def to_json(value, **kwargs): def sphinx_class(self): """Redefine sphinx class to provide doc links to types of props""" return ', '.join(p.sphinx_class() for p in self.props) + + +class UnionValidationError(utils.ValidationError): + """Validation error to be raised by Union properties""" + + def __str__(self, tab=1, prefix='- Possible cause: '): + return super(UnionValidationError, self).__str__(tab, prefix) diff --git a/properties/utils.py b/properties/utils.py index c727e60..14240ca 100644 --- a/properties/utils.py +++ b/properties/utils.py @@ -146,12 +146,12 @@ class ValidationError(ValueError): """ def __init__(self, message, reason=None, prop=None, instance=None, - _error_tuples=None): + _related_errors=None): super(ValidationError, self).__init__(message) - if _error_tuples is None: - self.error_tuples = [] + if _related_errors is None: + self.related_errors = [] else: - self.error_tuples = _error_tuples + self.related_errors = _related_errors if reason is not None and not isinstance(reason, string_types): raise TypeError('ValidationError reason must be a string') if prop is not None and not isinstance(prop, string_types): @@ -159,12 +159,51 @@ def __init__(self, message, reason=None, prop=None, instance=None, if instance is not None and not hasattr(instance, '_error_hook'): raise TypeError('ValidationError instance must be a ' 'HasProperties instance') - if reason or prop or instance: - error_tuple = ErrorTuple(message, reason, prop, instance) - self.error_tuples.append(error_tuple) + self.message = message + self.reason = reason + self.prop = prop + self.instance = instance if not getattr(instance, '_getting_validated', True): instance._error_hook(self.error_tuples) #pylint: disable=protected-access + def __str__(self, tab=1, prefix='- '): + """Verbose string representation of the error + + Includes related error messages. + """ + lines = [self.args[0]] + ['{indent}{prefix}{message}'.format( + indent=tab*4*' ', + prefix=prefix, + message=( + err.__str__(tab=tab+1) if isinstance(err, ValidationError) + else str(err) + ), + ) for err in self.related_errors] + return '\n'.join(lines) + + @property + def error_tuples(self): + """Construct error tuples out of related errors""" + if self.related_errors: + error_tuples = [ + ErrorTuple( + message=str(err), + reason=err.reason, + prop=err.prop, + instance=err.instance, + ) for err in self.related_errors + ] + else: + error_tuples = [ + ErrorTuple( + message=str(self), + reason=self.reason, + prop=self.prop, + instance=self.instance, + ) + ] + return error_tuples + class Sentinel(object): #pylint: disable=too-few-public-methods """Basic object with name and doc for specifying singletons