Combine clean() and validate()

stix2.1
clenk 2017-04-17 15:13:11 -04:00
parent cf0b56c04f
commit 635a3ec389
3 changed files with 48 additions and 75 deletions

View File

@ -39,7 +39,7 @@ class _STIXBase(collections.Mapping):
if prop_name in kwargs:
try:
kwargs[prop_name] = prop.validate(kwargs[prop_name])
kwargs[prop_name] = prop.clean(kwargs[prop_name])
except ValueError as exc:
msg = "Invalid value for {0} '{1}': {2}"
raise ValueError(msg.format(self.__class__.__name__,

View File

@ -22,7 +22,7 @@ class Property(object):
you to copy *all* values from an existing object to a new object), but
if the user provides a value other than the `fixed` value, it will raise
an error. This is semantically equivalent to defining both:
- a `validate()` function that checks if the value matches the fixed
- a `clean()` function that checks if the value matches the fixed
value, and
- a `default()` function that returns the fixed value.
(Default: `None`)
@ -30,15 +30,10 @@ class Property(object):
Subclasses can also define the following functions.
- `def clean(self, value) -> any:`
- Transform `value` into a valid value for this property. This should
raise a ValueError if such no such transformation is possible.
- `def validate(self, value) -> any:`
- check that `value` is valid for this property. This should return
a valid value (possibly modified) for this property, or raise a
ValueError if the value is not valid.
(Default: if `clean` is defined, it will attempt to call `clean` and
return the result or pass on a ValueError that `clean` raises. If
`clean` is not defined, this will return `value` unmodified).
- Return a value that is valid for this property. If `value` is not
valid for this property, this will attempt to transform it first. If
`value` is not valid and no such transformation is possible, it should
raise a ValueError.
- `def default(self):`
- provide a default value for this property.
- `default()` can return the special value `NOW` to use the current
@ -46,36 +41,27 @@ class Property(object):
to use the same default value, so calling now() for each field--
likely several microseconds apart-- does not work.
Subclasses can instead provide lambda functions for `clean`, and `default`
as keyword arguments. `validate` should not be provided as a lambda since
lambdas cannot raise their own exceptions.
Subclasses can instead provide a lambda function for `default as a keyword
argument. `clean` should not be provided as a lambda since lambdas cannot
raise their own exceptions.
"""
def _default_validate(self, value):
def _default_clean(self, value):
if value != self._fixed_value:
raise ValueError("must equal '{0}'.".format(self._fixed_value))
return value
def __init__(self, required=False, fixed=None, clean=None, default=None, type=None):
def __init__(self, required=False, fixed=None, default=None, type=None):
self.required = required
self.type = type
if fixed:
self._fixed_value = fixed
self.validate = self._default_validate
self.clean = self._default_clean
self.default = lambda: fixed
if clean:
self.clean = clean
if default:
self.default = default
def clean(self, value):
raise NotImplementedError
def validate(self, value):
try:
value = self.clean(value)
except NotImplementedError:
pass
return value
def __call__(self, value=None):
@ -95,14 +81,14 @@ class ListProperty(Property):
self.contained = bool
elif inspect.isclass(contained) and issubclass(contained, Property):
# If it's a class and not an instance, instantiate it so that
# validate() can be called on it, and ListProperty.validate() will
# clean() can be called on it, and ListProperty.clean() will
# use __call__ when it appends the item.
self.contained = contained()
else:
self.contained = contained
super(ListProperty, self).__init__(**kwargs)
def validate(self, value):
def clean(self, value):
try:
iter(value)
except TypeError:
@ -111,11 +97,11 @@ class ListProperty(Property):
result = []
for item in value:
try:
valid = self.contained.validate(item)
valid = self.contained.clean(item)
except ValueError:
raise
except AttributeError:
# type of list has no validate() function (eg. built in Python types)
# type of list has no clean() function (eg. built in Python types)
# TODO Should we raise an error here?
valid = item
@ -140,13 +126,6 @@ class StringProperty(Property):
def clean(self, value):
return self.string_type(value)
def validate(self, value):
try:
val = self.clean(value)
except ValueError:
raise
return val
class TypeProperty(Property):
def __init__(self, type):
@ -159,7 +138,7 @@ class IDProperty(Property):
self.required_prefix = type + "--"
super(IDProperty, self).__init__()
def validate(self, value):
def clean(self, value):
if not value.startswith(self.required_prefix):
raise ValueError("must start with '{0}'.".format(self.required_prefix))
try:
@ -191,18 +170,12 @@ class BooleanProperty(Property):
if value == 0:
return False
raise ValueError("not a coercible boolean value.")
def validate(self, value):
try:
return self.clean(value)
except ValueError:
raise ValueError("must be a boolean value.")
raise ValueError("must be a boolean value.")
class TimestampProperty(Property):
def validate(self, value):
def clean(self, value):
if isinstance(value, dt.date):
if hasattr(value, 'hour'):
return value
@ -236,7 +209,7 @@ class ReferenceProperty(Property):
self.type = type
super(ReferenceProperty, self).__init__(required, type=type)
def validate(self, value):
def clean(self, value):
if isinstance(value, _STIXBase):
value = value.id
if self.type:
@ -255,7 +228,7 @@ class SelectorProperty(Property):
# ignore type
super(SelectorProperty, self).__init__()
def validate(self, value):
def clean(self, value):
if not SELECTOR_REGEX.match(value):
raise ValueError("values must adhere to selector syntax")
return value

View File

@ -12,10 +12,10 @@ def test_property():
assert p.required is False
def test_basic_validate():
def test_basic_clean():
class Prop(Property):
def validate(self, value):
def clean(self, value):
if value == 42:
return value
else:
@ -23,9 +23,9 @@ def test_basic_validate():
p = Prop()
assert p.validate(42) == 42
assert p.clean(42) == 42
with pytest.raises(ValueError):
p.validate(41)
p.clean(41)
def test_default_field():
@ -42,53 +42,53 @@ def test_default_field():
def test_fixed_property():
p = Property(fixed="2.0")
assert p.validate("2.0")
assert p.clean("2.0")
with pytest.raises(ValueError):
assert p.validate("x") is False
assert p.clean("x") is False
with pytest.raises(ValueError):
assert p.validate(2.0) is False
assert p.clean(2.0) is False
assert p.default() == "2.0"
assert p.validate(p.default())
assert p.clean(p.default())
def test_list_property():
p = ListProperty(StringProperty)
assert p.validate(['abc', 'xyz'])
assert p.clean(['abc', 'xyz'])
with pytest.raises(ValueError):
p.validate([])
p.clean([])
def test_string_property():
prop = StringProperty()
assert prop.validate('foobar')
assert prop.validate(1)
assert prop.validate([1, 2, 3])
assert prop.clean('foobar')
assert prop.clean(1)
assert prop.clean([1, 2, 3])
def test_type_property():
prop = TypeProperty('my-type')
assert prop.validate('my-type')
assert prop.clean('my-type')
with pytest.raises(ValueError):
prop.validate('not-my-type')
assert prop.validate(prop.default())
prop.clean('not-my-type')
assert prop.clean(prop.default())
def test_id_property():
idprop = IDProperty('my-type')
assert idprop.validate('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
assert idprop.clean('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
with pytest.raises(ValueError) as excinfo:
idprop.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
idprop.clean('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
assert str(excinfo.value) == "must start with 'my-type--'."
with pytest.raises(ValueError) as excinfo:
idprop.validate('my-type--foo')
idprop.clean('my-type--foo')
assert str(excinfo.value) == "must have a valid version 4 UUID after the prefix."
assert idprop.validate(idprop.default())
assert idprop.clean(idprop.default())
@pytest.mark.parametrize("value", [
@ -110,7 +110,7 @@ def test_id_property():
def test_boolean_property_valid(value):
bool_prop = BooleanProperty()
assert bool_prop.validate(value) is not None
assert bool_prop.clean(value) is not None
@pytest.mark.parametrize("value", [
@ -123,15 +123,15 @@ def test_boolean_property_valid(value):
def test_boolean_property_invalid(value):
bool_prop = BooleanProperty()
with pytest.raises(ValueError):
bool_prop.validate(value)
bool_prop.clean(value)
def test_reference_property():
ref_prop = ReferenceProperty()
assert ref_prop.validate("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300")
assert ref_prop.clean("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300")
with pytest.raises(ValueError):
ref_prop.validate("foo")
ref_prop.clean("foo")
@pytest.mark.parametrize("value", [
@ -141,12 +141,12 @@ def test_reference_property():
])
def test_timestamp_property_valid(value):
ts_prop = TimestampProperty()
assert ts_prop.validate(value) == FAKE_TIME
assert ts_prop.clean(value) == FAKE_TIME
def test_timestamp_property_invalid():
ts_prop = TimestampProperty()
with pytest.raises(ValueError):
ts_prop.validate(1)
ts_prop.clean(1)
with pytest.raises(ValueError):
ts_prop.validate("someday sometime")
ts_prop.clean("someday sometime")