Repository: oasis-open/cti-python-stix2

Issue:
Ignore empty values for optional fields
Latest version of the library will not allow me to create instances of STIX2 classes with `None` or `[]` values for optional fields.

Example:
```python
In [4]: stix2.Malware(name='test malware', labels=['foo', 'bar'], external_references=[])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/.p/lib/python3.7/site-packages/stix2/base.py in _check_property(self, prop_name, prop, kwargs)
     89             try:
---> 90                 kwargs[prop_name] = prop.clean(kwargs[prop_name])
     91             except ValueError as exc:

~/.p/lib/python3.7/site-packages/stix2/properties.py in clean(self, value)
    164         if len(result) < 1:
--> 165             raise ValueError("must not be empty.")
    166

ValueError: must not be empty.
```

This forces me to write code like this:
```python
external_references = get_external_references()
if external_references:
    obj = stix2.Malware(
        name='test malware',
        labels=['foo', 'bar'],
        external_references=external_references)
else:
    obj = stix2.Malware(
        name='test malware',
        labels=['foo', 'bar'])
```
which is inconvenient.

Proposed solution: the library should accept empty fields for optional fields but should not serialize these in the output JSON, treating these fields as unset

Discussion:

Patch:
diff --git a/stix2/base.py b/stix2/base.py
index a6bafff..9fcdf56 100644
--- a/stix2/base.py
+++ b/stix2/base.py
@@ -143,12 +143,12 @@ class _STIXBase(collections.Mapping):
         if custom_props:
             self.__allow_custom = True
 
-        # Remove any keyword arguments whose value is None
+        # Remove any keyword arguments whose value is None or [] (i.e. empty list)
         setting_kwargs = {}
         props = kwargs.copy()
         props.update(custom_props)
         for prop_name, prop_value in props.items():
-            if prop_value is not None:
+            if prop_value is not None and prop_value != []:
                 setting_kwargs[prop_name] = prop_value
 
         # Detect any missing required properties


Test patch:
diff --git a/stix2/test/v20/test_malware.py b/stix2/test/v20/test_malware.py
index d0c6d7e..900a4b9 100644
--- a/stix2/test/v20/test_malware.py
+++ b/stix2/test/v20/test_malware.py
@@ -35,6 +35,22 @@ def test_malware_with_all_required_properties():
     assert str(mal) == EXPECTED_MALWARE
 
 
+def test_malware_with_empty_optional_field():
+    now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
+
+    mal = stix2.v20.Malware(
+        type="malware",
+        id=MALWARE_ID,
+        created=now,
+        modified=now,
+        labels=["ransomware"],
+        name="Cryptolocker",
+        external_references=[],
+    )
+
+    assert str(mal) == EXPECTED_MALWARE
+
+
 def test_malware_autogenerated_properties(malware):
     assert malware.type == 'malware'
     assert malware.id == 'malware--00000000-0000-4000-8000-000000000001'
diff --git a/stix2/test/v20/test_object_markings.py b/stix2/test/v20/test_object_markings.py
index 495c45a..156c42d 100644
--- a/stix2/test/v20/test_object_markings.py
+++ b/stix2/test/v20/test_object_markings.py
@@ -107,7 +107,6 @@ def test_add_markings_combination():
     "data", [
         ([""]),
         (""),
-        ([]),
         ([MARKING_IDS[0], 456]),
     ],
 )
@@ -576,7 +575,6 @@ def test_set_marking():
 
 @pytest.mark.parametrize(
     "data", [
-        ([]),
         ([""]),
         (""),
         ([MARKING_IDS[4], 687]),
diff --git a/stix2/test/v21/test_object_markings.py b/stix2/test/v21/test_object_markings.py
index d43aad5..7b19d4f 100644
--- a/stix2/test/v21/test_object_markings.py
+++ b/stix2/test/v21/test_object_markings.py
@@ -106,7 +106,6 @@ def test_add_markings_combination():
     "data", [
         ([""]),
         (""),
-        ([]),
         ([MARKING_IDS[0], 456]),
     ],
 )
@@ -575,7 +574,6 @@ def test_set_marking():
 
 @pytest.mark.parametrize(
     "data", [
-        ([]),
         ([""]),
         (""),
         ([MARKING_IDS[4], 687]),