Merge pull request #417 from chisholm/fix_list_property

Fix ListProperty
pull/1/head
Chris Lenk 2020-06-24 09:52:08 -04:00 committed by GitHub
commit 3d3a513398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 125 additions and 39 deletions

View File

@ -189,15 +189,29 @@ class ListProperty(Property):
def __init__(self, contained, **kwargs): def __init__(self, contained, **kwargs):
""" """
``contained`` should be a function which returns an object from the value. ``contained`` should be a Property class or instance, or a _STIXBase
subclass.
""" """
if inspect.isclass(contained) and issubclass(contained, Property): self.contained = None
# If it's a class and not an instance, instantiate it so that
# clean() can be called on it, and ListProperty.clean() will if inspect.isclass(contained):
# use __call__ when it appends the item. # Property classes are instantiated; _STIXBase subclasses are left
# as-is.
if issubclass(contained, Property):
self.contained = contained() self.contained = contained()
else: elif issubclass(contained, _STIXBase):
self.contained = contained self.contained = contained
elif isinstance(contained, Property):
self.contained = contained
if not self.contained:
raise TypeError(
"Invalid list element type: {}".format(
str(contained),
),
)
super(ListProperty, self).__init__(**kwargs) super(ListProperty, self).__init__(**kwargs)
def clean(self, value): def clean(self, value):
@ -209,40 +223,28 @@ class ListProperty(Property):
if isinstance(value, (_STIXBase, string_types)): if isinstance(value, (_STIXBase, string_types)):
value = [value] value = [value]
if isinstance(self.contained, Property):
result = [
self.contained.clean(item)
for item in value
]
else: # self.contained must be a _STIXBase subclass
result = [] result = []
for item in value: for item in value:
try: if isinstance(item, self.contained):
valid = self.contained.clean(item)
except ValueError:
raise
except AttributeError:
# type of list has no clean() function (eg. built in Python types)
# TODO Should we raise an error here?
valid = item valid = item
if type(self.contained) is EmbeddedObjectProperty: elif isinstance(item, Mapping):
obj_type = self.contained.type # attempt a mapping-like usage...
elif type(self.contained).__name__ == "STIXObjectProperty": valid = self.contained(**item)
# ^ this way of checking doesn't require a circular import
# valid is already an instance of a python-stix2 class; no need
# to turn it into a dictionary and then pass it to the class
# constructor again
result.append(valid)
continue
elif type(self.contained) is DictionaryProperty:
obj_type = dict
else:
obj_type = self.contained
if isinstance(valid, Mapping):
try:
valid._allow_custom
except AttributeError:
result.append(obj_type(**valid))
else: else:
result.append(obj_type(allow_custom=True, **valid)) raise ValueError("Can't create a {} out of {}".format(
else: self.contained._type, str(item),
result.append(obj_type(valid)) ))
result.append(valid)
# STIX spec forbids empty lists # STIX spec forbids empty lists
if len(result) < 1: if len(result) < 1:

View File

@ -3,8 +3,10 @@ import uuid
import pytest import pytest
import stix2 import stix2
import stix2.base
from stix2.exceptions import ( from stix2.exceptions import (
AtLeastOnePropertyError, CustomContentError, DictionaryKeyError, AtLeastOnePropertyError, CustomContentError, DictionaryKeyError,
ExtraPropertiesError,
) )
from stix2.properties import ( from stix2.properties import (
BinaryProperty, BooleanProperty, DictionaryProperty, BinaryProperty, BooleanProperty, DictionaryProperty,
@ -66,7 +68,7 @@ def test_fixed_property():
assert p.clean(p.default()) assert p.clean(p.default())
def test_list_property(): def test_list_property_property_type():
p = ListProperty(StringProperty) p = ListProperty(StringProperty)
assert p.clean(['abc', 'xyz']) assert p.clean(['abc', 'xyz'])
@ -74,6 +76,88 @@ def test_list_property():
p.clean([]) p.clean([])
def test_list_property_property_type_custom():
class TestObj(stix2.base._STIXBase):
_type = "test"
_properties = {
"foo": StringProperty(),
}
p = ListProperty(EmbeddedObjectProperty(type=TestObj))
objs_custom = [
TestObj(foo="abc", bar=123, allow_custom=True),
TestObj(foo="xyz"),
]
assert p.clean(objs_custom)
dicts_custom = [
{"foo": "abc", "bar": 123},
{"foo": "xyz"},
]
# no opportunity to set allow_custom=True when using dicts
with pytest.raises(ExtraPropertiesError):
p.clean(dicts_custom)
def test_list_property_object_type():
class TestObj(stix2.base._STIXBase):
_type = "test"
_properties = {
"foo": StringProperty(),
}
p = ListProperty(TestObj)
objs = [TestObj(foo="abc"), TestObj(foo="xyz")]
assert p.clean(objs)
dicts = [{"foo": "abc"}, {"foo": "xyz"}]
assert p.clean(dicts)
def test_list_property_object_type_custom():
class TestObj(stix2.base._STIXBase):
_type = "test"
_properties = {
"foo": StringProperty(),
}
p = ListProperty(TestObj)
objs_custom = [
TestObj(foo="abc", bar=123, allow_custom=True),
TestObj(foo="xyz"),
]
assert p.clean(objs_custom)
dicts_custom = [
{"foo": "abc", "bar": 123},
{"foo": "xyz"},
]
# no opportunity to set allow_custom=True when using dicts
with pytest.raises(ExtraPropertiesError):
p.clean(dicts_custom)
def test_list_property_bad_element_type():
with pytest.raises(TypeError):
ListProperty(1)
def test_list_property_bad_value_type():
class TestObj(stix2.base._STIXBase):
_type = "test"
_properties = {
"foo": StringProperty(),
}
list_prop = ListProperty(TestObj)
with pytest.raises(ValueError):
list_prop.clean([1])
def test_string_property(): def test_string_property():
prop = StringProperty() prop = StringProperty()