Skip to content

Commit 693972a

Browse files
committed
fixed empty annotation props weren't get-able (#274)
1 parent 464a024 commit 693972a

File tree

3 files changed

+65
-36
lines changed

3 files changed

+65
-36
lines changed

mmif/serialize/annotation.py

+19-14
Original file line numberDiff line numberDiff line change
@@ -415,26 +415,31 @@ def __delitem__(self, key: str) -> None:
415415

416416
def __iter__(self) -> Iterator[str]:
417417
"""
418-
``__iter__`` on Mapping should basically work as ``keys()`` method of vanilla dict
419-
however, when MMIF objects are serialized, all optional (not in ``_req_atts``),
420-
empty props are ignored (note that emtpy but required props are serialized
421-
with the *emtpy* value).
422-
Hence, this ``__iter__`` method should also work in the same way and
423-
ignore empty optional props.
418+
``__iter__`` on Mapping should basically work as ``keys()`` method
419+
of vanilla dict.
424420
"""
425421
for key in itertools.chain(self._named_attributes(), self._unnamed_attributes):
426-
if key in self._required_attributes:
427-
yield key
428-
else:
429-
try:
430-
self.__getitem__(key)
431-
yield key
432-
except KeyError:
433-
pass
422+
yield key
423+
424+
def __getitem__(self, key):
425+
"""
426+
Parent MmifObject class has a __getitem__ method that checks if
427+
the value is empty when asked for an unnamed attribute. But for
428+
AnnotationProperties, any arbitrary property that's added
429+
explicitly by the user (developer) should not be ignored and
430+
returned even the value is empty.
431+
"""
432+
if key in self._named_attributes():
433+
return self.__dict__[key]
434+
else:
435+
return self._unnamed_attributes[key]
434436

435437
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
436438
self.id: str = ''
439+
# any individual at_type (subclassing this class) can have its own set of required attributes
437440
self._required_attributes = ["id"]
441+
# allowing additional attributes for arbitrary annotation properties
442+
self._unnamed_attributes = {}
438443
super().__init__(mmif_obj)
439444

440445

mmif/serialize/model.py

+22-18
Original file line numberDiff line numberDiff line change
@@ -44,35 +44,39 @@ class MmifObject(object):
4444
4545
1. _unnamed_attributes:
4646
Only can be either None or an empty dictionary. If it's set to None,
47-
it means the class won't take any ``Additional Attributes`` in the JSON
48-
schema sense. If it's a dict, users can throw any k-v pairs to the
49-
class, EXCEPT for the reserved two key names.
47+
it means the class won't take any ``Additional Attributes`` in the
48+
JSON schema sense. If it's an empty dict, users can throw any k-v
49+
pairs to the class, as long as the key is not a "reserved" name,
50+
and those additional attributes will be stored in this dict while
51+
in memory.
5052
2. _attribute_classes:
5153
This is a dict from a key name to a specific python class to use for
5254
deserialize the value. Note that a key name in this dict does NOT
5355
have to be a *named* attribute, but is recommended to be one.
5456
3. _required_attributes:
55-
This is a simple list of names of attributes that are required in the object.
56-
When serialize, an object will skip its *empty* (e.g. zero-length, or None)
57-
attributes unless they are in this list. Otherwise, the serialized JSON
58-
string would have empty representations (e.g. ``""``, ``[]``).
57+
This is a simple list of names of attributes that are required in
58+
the object. When serialize, an object will skip its *empty* (e.g.
59+
zero-length, or None) attributes unless they are in this list.
60+
Otherwise, the serialized JSON string would have empty
61+
representations (e.g. ``""``, ``[]``).
5962
4. _exclude_from_diff:
60-
This is a simple list of names of attributes that should be excluded from
61-
the diff calculation in ``__eq__``.
63+
This is a simple list of names of attributes that should be excluded
64+
from the diff calculation in ``__eq__``.
6265
6366
# TODO (krim @ 8/17/20): this dict is however, a duplicate with the type hints in the class definition.
64-
Maybe there is a better way to utilize type hints (e.g. getting them as a programmatically), but for now
65-
developers should be careful to add types to hints as well as to this dict.
67+
Maybe there is a better way to utilize type hints (e.g. getting them
68+
as a programmatically), but for now developers should be careful to
69+
add types to hints as well as to this dict.
6670
6771
Also note that those special attributes MUST be set in the __init__()
6872
before calling super method, otherwise deserialization will not work.
6973
7074
And also, a subclass that has one or more *named* attributes, it must
71-
set those attributes in the __init__() before calling super method. When
72-
serializing a MmifObject, all *empty* attributes will be ignored, so for
73-
optional named attributes, you must leave the values empty (len == 0), but
74-
NOT None. Any None-valued named attributes will cause issues with current
75-
implementation.
75+
set those attributes in the __init__() before calling super method.
76+
When serializing a MmifObject, all *empty* attributes will be ignored,
77+
so for optional named attributes, you must leave the values empty
78+
(len == 0), but NOT None. Any None-valued named attributes will cause
79+
issues with current implementation.
7680
7781
:param mmif_obj: JSON string or `dict` to initialize an object.
7882
If not given, an empty object will be initialized, sometimes with
@@ -272,8 +276,8 @@ def __len__(self) -> int:
272276
"""
273277
Returns number of attributes that are not *empty*.
274278
"""
275-
return sum([named in self and not self.is_empty(self[named]) for named in self._named_attributes()]) \
276-
+ (len(self._unnamed_attributes) if self._unnamed_attributes else 0)
279+
return (sum([named in self and not self.is_empty(self[named]) for named in self._named_attributes()])
280+
+ (len(self._unnamed_attributes) if self._unnamed_attributes else 0))
277281

278282
def __setitem__(self, key, value) -> None:
279283
if key in self.reserved_names:

tests/test_serialize.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from mmif.serialize import *
1616
from mmif.serialize.model import *
1717
from mmif.serialize.view import ContainsDict, ErrorDict
18-
from mmif.vocabulary import AnnotationTypes, DocumentTypes
18+
from mmif.vocabulary import AnnotationTypes, DocumentTypes, ThingType
1919
from tests.mmif_examples import *
2020

2121
# Flags for skipping tests
@@ -795,7 +795,29 @@ def test_annotation_properties(self):
795795
for k, v in ann_obj.properties.items():
796796
self.assertTrue(k is not None)
797797
self.assertTrue(v is not None)
798-
798+
799+
def test_empty_annotation_property(self):
800+
a = Annotation({
801+
'@type': ThingType.Thing,
802+
'properties': {
803+
'id': 'a1',
804+
'empty_str_prop': "",
805+
'nonempty_str_prop': "string",
806+
'empty_lst_prop': []
807+
}
808+
})
809+
self.assertEqual(a['empty_str_prop'], "")
810+
self.assertEqual(a['empty_lst_prop'], [])
811+
self.assertEqual(4, len(a.properties.keys()))
812+
a_serialized = a.serialize()
813+
json.loads(a_serialized)
814+
self.assertEqual(json.loads(a_serialized)['properties']['empty_str_prop'], "")
815+
self.assertEqual(json.loads(a_serialized)['properties']['empty_lst_prop'], [])
816+
a_roundtrip = Annotation(a_serialized)
817+
self.assertEqual(a_roundtrip.get_property('empty_str_prop'), "")
818+
self.assertEqual(a_roundtrip.get_property('empty_lst_prop'), [])
819+
self.assertEqual(4, len(a.properties.keys()))
820+
799821
def test_annotation_ephemeral_properties(self):
800822
mmif = self.data['everything']['mmif']
801823
first_view_first_ann = mmif['v1']['s1']
@@ -1116,8 +1138,6 @@ def test_capital_annotation_generation_viewfinder(self):
11161138
cap_anns = list(mmif_roundtrip.views[f'v{i}'].get_annotations(AnnotationTypes.Annotation))
11171139
self.assertEqual(1, len(cap_anns))
11181140
self.assertEqual(authors[i-1], cap_anns[0].get_property('author'))
1119-
1120-
11211141

11221142
def test_deserialize_with_whole_mmif(self):
11231143
for i, datum in self.data.items():

0 commit comments

Comments
 (0)