Combine clean() and validate()
parent
cf0b56c04f
commit
635a3ec389
|
@ -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__,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue