Skip to content

Commit b2014e3

Browse files
authored
Merge pull request #263 from seequent/beta
v0.5.5 Deployment - Better validation error messages
2 parents 1546b1b + fe28b97 commit b2014e3

File tree

15 files changed

+155
-61
lines changed

15 files changed

+155
-61
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[bumpversion]
2-
current_version = 0.5.4
2+
current_version = 0.5.5
33
files = properties/__init__.py setup.py docs/conf.py
44

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@
6464
# built documents.
6565
#
6666
# The short X.Y version.
67-
version = u'0.5.4'
67+
version = u'0.5.5'
6868
# The full version, including alpha/beta/rc tags.
69-
release = u'0.5.4'
69+
release = u'0.5.5'
7070

7171
# The language for content autogenerated by Sphinx. Refer to documentation
7272
# for a list of supported languages.

properties/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class Profile(properties.HasProperties):
8989
ValidationError,
9090
)
9191

92-
__version__ = '0.5.4'
92+
__version__ = '0.5.5'
9393
__author__ = 'Seequent'
9494
__license__ = 'MIT'
9595
__copyright__ = 'Copyright 2018 Seequent'

properties/base/containers.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,16 @@ def assert_valid(self, instance, value=None):
289289
value = instance._get(self.name)
290290
if value is None:
291291
return True
292-
if self.min_length is not None and len(value) < self.min_length:
293-
self.error(instance, value)
294-
if self.max_length is not None and len(value) > self.max_length:
295-
self.error(instance, value)
292+
if (
293+
(self.min_length is not None and len(value) < self.min_length)
294+
or
295+
(self.max_length is not None and len(value) > self.max_length)
296+
):
297+
self.error(
298+
instance=instance,
299+
value=value,
300+
extra='(Length is {})'.format(len(value)),
301+
)
296302
for val in value:
297303
if not self.prop.assert_valid(instance, val):
298304
return False
@@ -567,8 +573,12 @@ def validate(self, instance, value):
567573
if self.coerce:
568574
try:
569575
value = self._class_container(value)
570-
except TypeError:
571-
self.error(instance, value)
576+
except (TypeError, ValueError):
577+
self.error(
578+
instance=instance,
579+
value=value,
580+
extra='Cannot coerce to the correct type',
581+
)
572582
out = value.__class__()
573583
for key, val in iteritems(value):
574584
if self.key_prop:

properties/base/instance.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from six import PY2
1111

12-
from .base import HasProperties, equal
12+
from .base import GENERIC_ERRORS, HasProperties, equal
1313
from .. import basic
1414
from .. import utils
1515

@@ -101,8 +101,14 @@ def validate(self, instance, value):
101101
if isinstance(value, dict):
102102
return self.instance_class(**value)
103103
return self.instance_class(value)
104-
except (ValueError, KeyError, TypeError):
105-
self.error(instance, value)
104+
except GENERIC_ERRORS as err:
105+
if hasattr(err, 'error_tuples'):
106+
extra = '({})'.format(' & '.join(
107+
err_tup.message for err_tup in err.error_tuples
108+
))
109+
else:
110+
extra = ''
111+
self.error(instance, value, extra=extra)
106112

107113
def assert_valid(self, instance, value=None):
108114
"""Checks if valid, including HasProperty instances pass validation"""

properties/base/union.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
from six import PY2
1010

11-
from ..base import GENERIC_ERRORS, HasProperties, Instance
11+
from .base import GENERIC_ERRORS, HasProperties
12+
from .instance import Instance
1213
from .. import basic
1314
from .. import utils
1415

@@ -160,14 +161,32 @@ def _unused_default_warning(self):
160161
warn('Union prop default ignored: {}'.format(prop.default),
161162
RuntimeWarning)
162163

163-
def validate(self, instance, value):
164-
"""Check if value is a valid type of one of the Union props"""
164+
def _try_prop_method(self, instance, value, method_name):
165+
"""Helper method to perform a method on each of the union props
166+
167+
This method gathers all errors and returns them at the end
168+
if the method on each of the props fails.
169+
"""
170+
error_messages = []
165171
for prop in self.props:
166172
try:
167-
return prop.validate(instance, value)
168-
except GENERIC_ERRORS:
169-
continue
170-
self.error(instance, value)
173+
return getattr(prop, method_name)(instance, value)
174+
except GENERIC_ERRORS as err:
175+
if hasattr(err, 'error_tuples'):
176+
error_messages += [
177+
err_tup.message for err_tup in err.error_tuples
178+
]
179+
if error_messages:
180+
extra = 'Possible explanation:'
181+
for message in error_messages:
182+
extra += '\n - {}'.format(message)
183+
else:
184+
extra = ''
185+
self.error(instance, value, extra=extra)
186+
187+
def validate(self, instance, value):
188+
"""Check if value is a valid type of one of the Union props"""
189+
return self._try_prop_method(instance, value, 'validate')
171190

172191
def assert_valid(self, instance, value=None):
173192
"""Check if the Union has a valid value"""
@@ -178,19 +197,7 @@ def assert_valid(self, instance, value=None):
178197
value = instance._get(self.name)
179198
if value is None:
180199
return True
181-
for prop in self.props:
182-
try:
183-
return prop.assert_valid(instance, value)
184-
except GENERIC_ERRORS:
185-
continue
186-
message = (
187-
'The "{name}" property of a {cls} instance has not been set '
188-
'correctly'.format(
189-
name=self.name,
190-
cls=instance.__class__.__name__
191-
)
192-
)
193-
raise utils.ValidationError(message, 'invalid', self.name, instance)
200+
return self._try_prop_method(instance, value, 'assert_valid')
194201

195202
def serialize(self, value, **kwargs):
196203
"""Return a serialized value

properties/basic.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import collections
88
import datetime
9-
from functools import wraps
109
import math
1110
import random
1211
import re
@@ -45,7 +44,6 @@ def accept_kwargs(func):
4544
functions always receive kwargs from serialize, but by using this,
4645
the original functions may simply take a single value.
4746
"""
48-
@wraps(func)
4947
def wrapped(val, **kwargs):
5048
"""Perform a function on a value, ignoring kwargs if necessary"""
5149
try:
@@ -339,14 +337,19 @@ def error(self, instance, value, error_class=None, extra=''):
339337
prefix = prefix + ' of a {cls} instance'.format(
340338
cls=instance.__class__.__name__,
341339
)
340+
print_value = repr(value)
341+
if len(print_value) > 107:
342+
print_value = '{} ... {}'.format(
343+
print_value[:50], print_value[-50:]
344+
)
342345
message = (
343-
'{prefix} must be {info}. A value of {val!r} {vtype!r} was '
344-
'specified. {extra}'.format(
346+
'{prefix} must be {info}. An invalid value of {val} {vtype} was '
347+
'specified.{extra}'.format(
345348
prefix=prefix,
346349
info=self.info or 'corrected',
347-
val=value,
350+
val=print_value,
348351
vtype=type(value),
349-
extra=extra,
352+
extra=' {}'.format(extra) if extra else '',
350353
)
351354
)
352355
if issubclass(error_class, ValidationError):
@@ -732,7 +735,7 @@ def validate(self, instance, value):
732735
try:
733736
value = bool(value)
734737
except ValueError:
735-
self.error(instance, value)
738+
self.error(instance, value, extra='Cannot cast to boolean.')
736739
if not isinstance(value, BOOLEAN_TYPES):
737740
self.error(instance, value)
738741
return value
@@ -765,7 +768,7 @@ def _in_bounds(prop, instance, value):
765768
(prop.min is not None and value < prop.min) or
766769
(prop.max is not None and value > prop.max)
767770
):
768-
prop.error(instance, value)
771+
prop.error(instance, value, extra='Not within allowed range.')
769772

770773

771774
class Integer(Boolean):
@@ -811,9 +814,13 @@ def validate(self, instance, value):
811814
try:
812815
intval = int(value)
813816
if not self.cast and abs(value - intval) > TOL:
814-
self.error(instance, value)
817+
self.error(
818+
instance=instance,
819+
value=value,
820+
extra='Not within tolerance range of {}.'.format(TOL),
821+
)
815822
except (TypeError, ValueError):
816-
self.error(instance, value)
823+
self.error(instance, value, extra='Cannot cast to integer.')
817824
_in_bounds(self, instance, intval)
818825
return intval
819826

@@ -861,9 +868,13 @@ def validate(self, instance, value):
861868
try:
862869
floatval = float(value)
863870
if not self.cast and abs(value - floatval) > TOL:
864-
self.error(instance, value)
871+
self.error(
872+
instance=instance,
873+
value=value,
874+
extra='Not within tolerance range of {}.'.format(TOL),
875+
)
865876
except (TypeError, ValueError):
866-
self.error(instance, value)
877+
self.error(instance, value, extra='Cannot cast to float.')
867878
_in_bounds(self, instance, floatval)
868879
return floatval
869880

@@ -907,7 +918,11 @@ def validate(self, instance, value):
907918
abs(value.real - compval.real) > TOL or
908919
abs(value.imag - compval.imag) > TOL
909920
):
910-
self.error(instance, value)
921+
self.error(
922+
instance=instance,
923+
value=value,
924+
extra='Not within tolerance range of {}.'.format(TOL),
925+
)
911926
except (TypeError, ValueError, AttributeError):
912927
self.error(instance, value)
913928
return compval
@@ -1012,7 +1027,7 @@ def validate(self, instance, value):
10121027
if not isinstance(value, string_types):
10131028
self.error(instance, value)
10141029
if self.regex is not None and self.regex.search(value) is None: #pylint: disable=no-member
1015-
self.error(instance, value)
1030+
self.error(instance, value, extra='Regex does not match.')
10161031
value = value.strip(self.strip)
10171032
if self.change_case == 'upper':
10181033
value = value.upper()
@@ -1153,7 +1168,7 @@ def validate(self, instance, value): #pyli
11531168
test_val = val if self.case_sensitive else [_.upper() for _ in val]
11541169
if test_value == test_key or test_value in test_val:
11551170
return key
1156-
self.error(instance, value)
1171+
self.error(instance, value, extra='Not an available choice.')
11571172

11581173

11591174
class Color(Property):
@@ -1226,11 +1241,19 @@ def validate(self, instance, value):
12261241
if isinstance(value, datetime.datetime):
12271242
return value
12281243
if not isinstance(value, string_types):
1229-
self.error(instance, value)
1244+
self.error(
1245+
instance=instance,
1246+
value=value,
1247+
extra='Cannot convert non-strings to datetime.',
1248+
)
12301249
try:
12311250
return self.from_json(value)
12321251
except ValueError:
1233-
self.error(instance, value)
1252+
self.error(
1253+
instance=instance,
1254+
value=value,
1255+
extra='Invalid format for converting to datetime.',
1256+
)
12341257

12351258
@staticmethod
12361259
def to_json(value, **kwargs):

properties/extras/web.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def validate(self, instance, value):
4848
value = super(URL, self).validate(instance, value)
4949
parsed_url = urlparse(value)
5050
if not parsed_url.scheme or not parsed_url.netloc:
51-
self.error(instance, value)
51+
self.error(instance, value, extra='URL needs scheme and netloc.')
5252
parse_result = ParseResult(
5353
scheme=parsed_url.scheme,
5454
netloc=parsed_url.netloc,

properties/math.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def validate(self, instance, value):
133133
'subclasses of numpy.ndarray'
134134
)
135135
if value.dtype.kind not in (TYPE_MAPPINGS[typ] for typ in self.dtype):
136-
self.error(instance, value)
136+
self.error(instance, value, extra='Invalid dtype.')
137137
if self.shape is None:
138138
return value
139139
for shape in self.shape:
@@ -144,7 +144,7 @@ def validate(self, instance, value):
144144
break
145145
else:
146146
return value
147-
self.error(instance, value)
147+
self.error(instance, value, extra='Invalid shape.')
148148

149149
def equal(self, value_a, value_b):
150150
try:
@@ -421,7 +421,11 @@ def validate(self, instance, value):
421421
for i, val in enumerate(value):
422422
if isinstance(val, string_types):
423423
if val.upper() not in VECTOR_DIRECTIONS:
424-
self.error(instance, val)
424+
self.error(
425+
instance=instance,
426+
value=val,
427+
extra='This is an invalid Vector3 representation.',
428+
)
425429
value[i] = VECTOR_DIRECTIONS[val.upper()]
426430

427431
return super(Vector3Array, self).validate(instance, value)
@@ -482,11 +486,16 @@ def validate(self, instance, value):
482486
self.error(instance, value)
483487
if isinstance(value, (tuple, list)):
484488
for i, val in enumerate(value):
485-
if (
486-
isinstance(val, string_types) and
487-
val.upper() in VECTOR_DIRECTIONS and
488-
val.upper() not in ('Z', '-Z', 'UP', 'DOWN')
489-
):
489+
if isinstance(val, string_types):
490+
if (
491+
val.upper() not in VECTOR_DIRECTIONS or
492+
val.upper() in ('Z', '-Z', 'UP', 'DOWN')
493+
):
494+
self.error(
495+
instance=instance,
496+
value=val,
497+
extra='This is an invalid Vector2 representation.',
498+
)
490499
value[i] = VECTOR_DIRECTIONS[val.upper()][:2]
491500

492501
return super(Vector2Array, self).validate(instance, value)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
EXTRAS.update({'full': sum(EXTRAS.values(), [])})
3030
setup(
3131
name='properties',
32-
version='0.5.4',
32+
version='0.5.5',
3333
packages=find_packages(exclude=('tests',)),
3434
install_requires=['six>=1.7.3'],
3535
extras_require=EXTRAS,

0 commit comments

Comments
 (0)