Skip to content

Commit 2a5bffc

Browse files
committed
added __str__ to view ErrorDict for human-readable formatting, added getters on various levels
1 parent 826bc4d commit 2a5bffc

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

mmif/serialize/mmif.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,16 @@ def get_views_for_document(self, doc_id: str) -> List[View]:
447447
pass
448448
return views
449449

450+
def get_all_views_with_error(self) -> List[View]:
451+
"""
452+
Returns the list of all views in the MMIF that have errors.
453+
454+
:return: the list of views that contain errors but no annotations
455+
"""
456+
return [v for v in self.views if v.has_error()]
457+
458+
get_views_with_error = get_all_views_with_error
459+
450460
def get_all_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> List[View]:
451461
"""
452462
Returns the list of all views in the MMIF if given types
@@ -461,11 +471,27 @@ def get_all_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[
461471
else:
462472
return [view for view in self.views if at_types in view.metadata.contains]
463473

464-
def get_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> List[View]:
474+
get_views_contain = get_all_views_contain
475+
476+
def get_view_with_error(self) -> Optional[View]:
477+
"""
478+
Returns the last view appended that contains an error.
479+
480+
:return: the view, or None if no error is found
465481
"""
466-
An alias to `get_all_views_contain` method.
482+
for view in reversed(self.views):
483+
if view.has_error():
484+
return view
485+
return None
486+
487+
def get_last_error(self) -> Optional[str]:
488+
"""
489+
Returns the last error message found in the views.
490+
491+
:return: the error message in human-readable format, or None if no error is found
467492
"""
468-
return self.get_all_views_contain(at_types)
493+
v = self.get_view_with_error()
494+
return v.get_error() if v is not None else None
469495

470496
def get_view_contains(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> Optional[View]:
471497
"""

mmif/serialize/view.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
In MMIF, views are created by apps in a pipeline that are annotating
66
data that was previously present in the MMIF file.
77
"""
8+
import json
89
from datetime import datetime
910
from typing import Dict, Union, Optional, Generator, List, cast
1011

@@ -230,6 +231,23 @@ def __getitem__(self, key: str) -> 'Annotation':
230231
def set_error(self, err_message: str, err_trace: str) -> None:
231232
self.metadata.set_error(err_message, err_trace)
232233
self.annotations.empty()
234+
235+
def get_error(self) -> Optional[str]:
236+
"""
237+
Get the "text" representation of the error occurred during
238+
processing. Text representation is supposed to be human-readable.
239+
When ths view does not have any error, returns None.
240+
"""
241+
if self.has_error():
242+
return self.metadata.get_error_as_text()
243+
else:
244+
return None
245+
246+
def has_error(self) -> bool:
247+
return self.metadata.has_error()
248+
249+
def has_warnings(self):
250+
return self.metadata.has_warnings()
233251

234252

235253
class ViewMetadata(MmifObject):
@@ -267,7 +285,24 @@ def _serialize(self, alt_container: Optional[Dict] = None) -> dict:
267285
if not (self.contains.items() or self.error or self.warnings):
268286
serialized['contains'] = {}
269287
return serialized
270-
288+
289+
def has_error(self) -> bool:
290+
return len(self.error) > 0
291+
292+
def has_warnings(self):
293+
return len(self.warnings) > 0
294+
295+
def get_error_as_text(self) -> str:
296+
if self.has_error():
297+
if isinstance(self.error, ErrorDict):
298+
return str(self.error)
299+
elif isinstance(self.error, dict):
300+
return f"Error: {json.dumps(self.error, indent=2)}"
301+
else:
302+
return f"Error (unknown error format): {self.error}"
303+
else:
304+
raise KeyError(f"No error found")
305+
271306
def new_contain(self, at_type: Union[str, ThingTypesBase], **contains_metadata) -> Optional['Contain']:
272307
"""
273308
Adds a new element to the ``contains`` dictionary.
@@ -346,6 +381,9 @@ def __init__(self, error_obj: Optional[Union[bytes, str, dict]] = None) -> None:
346381
self.message: str = ''
347382
self.stackTrace: str = ''
348383
super().__init__(error_obj)
384+
385+
def __str__(self):
386+
return f"({self.message})\n\n{self.stackTrace}"
349387

350388

351389
class Contain(DataDict[str, str]):

tests/test_serialize.py

+36
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,42 @@ def test_errordict(self):
763763
self.assertTrue("error" in roundtrip.metadata)
764764
self.assertFalse("contains" in roundtrip.metadata)
765765

766+
def test_error_to_text(self):
767+
err_msg = "some message"
768+
err_trace = "some trace line1\n\ttrace line 2\n\ttrace line 3"
769+
error = ErrorDict({"message": err_msg, "stackTrace": err_trace})
770+
mmif_obj = Mmif(validate=False)
771+
aview = mmif_obj.new_view()
772+
aview.set_error(err_msg, err_trace)
773+
self.assertTrue(aview.has_error())
774+
aview.metadata.error = error
775+
self.assertTrue(aview.has_error())
776+
self.assertFalse(aview.has_warnings())
777+
# assumes the "text" representation of the error contains the message and the trace
778+
self.assertGreaterEqual(len(aview.metadata.get_error_as_text()), len(err_msg) + len(err_trace))
779+
self.assertIn(err_msg, aview.metadata.get_error_as_text())
780+
self.assertIn(err_trace, aview.metadata.get_error_as_text())
781+
self.assertEqual(aview.metadata.get_error_as_text(), aview.get_error())
782+
self.assertEqual(1, len(mmif_obj.get_views_with_error()))
783+
self.assertEqual(aview.id, mmif_obj.get_view_with_error().id)
784+
self.assertEqual(aview.metadata.get_error_as_text(), mmif_obj.get_last_error())
785+
786+
# and then test for custom error objects
787+
aview.metadata.error = {'errorName': 'custom error', 'errorDetails': 'some details', 'errorTimestamp': 'now'}
788+
self.assertTrue(aview.has_error())
789+
self.assertTrue(isinstance(mmif_obj.get_last_error(), str))
790+
err_str = 'custom error as a single long string'
791+
aview.metadata.error = err_str
792+
self.assertTrue(aview.has_error())
793+
self.assertTrue(isinstance(mmif_obj.get_last_error(), str))
794+
self.assertIn(err_str, mmif_obj.get_last_error())
795+
aview.metadata.error = {} # no error
796+
self.assertFalse(aview.has_error())
797+
with self.assertRaises(KeyError):
798+
_ = aview.metadata.get_error_as_text()
799+
self.assertIsNone(aview.get_error())
800+
self.assertIsNone(mmif_obj.get_last_error())
801+
766802

767803
class TestAnnotation(unittest.TestCase):
768804
# TODO (angus-lherrou @ 7/27/2020): testing should include validation for required attrs

0 commit comments

Comments
 (0)