Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Structured errors #267

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 31 additions & 27 deletions properties/base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down
23 changes: 16 additions & 7 deletions properties/base/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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)

31 changes: 18 additions & 13 deletions properties/base/union.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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)
9 changes: 7 additions & 2 deletions properties/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
53 changes: 46 additions & 7 deletions properties/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,25 +146,64 @@ 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):
raise TypeError('ValidationError prop must be a string')
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='- '):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest __unicode__ and having a __str__ stub that returns the str version of the __unicode__

"""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
Expand Down