Skip to content

Commit 1083db4

Browse files
authored
Merge pull request #386 from mpsonntag/dictParseIgnore
Implement ignore_errors for DictParser.Reader LGTM
2 parents 206cbba + 90a5810 commit 1083db4

File tree

6 files changed

+440
-21
lines changed

6 files changed

+440
-21
lines changed

odml/format.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def revmap(self, name):
8282
else:
8383
for k, val in self._map.items():
8484
self._rev_map[val] = k
85-
return self._rev_map.get(name, name)
85+
86+
return self._rev_map.get(name)
8687

8788
def __iter__(self):
8889
""" Iterates each python property name """

odml/tools/dict_parser.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
The dict_parser module provides access to the DictWriter and DictReader class.
33
Both handle the conversion of odML documents from and to Python dictionary objects.
44
"""
5+
import sys
56

67
from .. import format as odmlfmt
78
from ..info import FORMAT_VERSION
89
from .parser_utils import InvalidVersionException, ParserException, odml_tuple_export
910

11+
LABEL_ERROR = "Error"
12+
LABEL_WARNING = "Warning"
13+
1014

1115
def parse_cardinality(vals):
1216
"""
@@ -169,16 +173,22 @@ class DictReader:
169173
A reader to parse dictionaries with odML content into an odml.Document.
170174
"""
171175

172-
def __init__(self, show_warnings=True):
176+
def __init__(self, show_warnings=True, ignore_errors=False):
173177
"""
174178
:param show_warnings: Toggle whether to print warnings to the command line.
175179
Any warnings can be accessed via the Reader's class
176180
warnings attribute after parsing is done.
181+
:param ignore_errors: To allow loading and fixing of invalid odml files
182+
encountered errors can be converted to warnings
183+
instead. Such a document can only be saved when
184+
all errors have been addressed though.
177185
"""
178186
self.parsed_doc = None # Python dictionary object equivalent
179-
self.show_warnings = show_warnings
180187
self.warnings = []
181188

189+
self.show_warnings = show_warnings
190+
self.ignore_errors = ignore_errors
191+
182192
def is_valid_attribute(self, attr, fmt):
183193
"""
184194
Checks whether a provided attribute is valid for a provided odml class
@@ -196,13 +206,40 @@ def is_valid_attribute(self, attr, fmt):
196206
if fmt.revmap(attr):
197207
return attr
198208

199-
msg = "Invalid element <%s> inside <%s> tag" % (attr, fmt.__class__.__name__)
200-
self.warnings.append(msg)
201-
if self.show_warnings:
202-
print(msg)
209+
msg = "Invalid element '%s' inside <%s> tag" % (attr, fmt.__class__.__name__)
210+
self.error(msg)
203211

204212
return None
205213

214+
def error(self, msg):
215+
"""
216+
If the parsers ignore_errors property is set to False, a ParserException
217+
will be raised. Otherwise the message is passed to the parsers warning
218+
method.
219+
220+
:param msg: Error message.
221+
"""
222+
if self.ignore_errors:
223+
return self.warn(msg, LABEL_ERROR)
224+
225+
raise ParserException(msg)
226+
227+
def warn(self, msg, label=LABEL_WARNING):
228+
"""
229+
Adds a message to the parsers warnings property. If the parsers show_warnings
230+
property is set to True, an additional error message will be written
231+
to sys.stderr.
232+
233+
:param msg: Warning message.
234+
:param label: Defined message level, can be 'Error' or 'Warning'. Default is 'Warning'.
235+
"""
236+
msg = "%s: %s" % (label, msg)
237+
238+
self.warnings.append(msg)
239+
240+
if self.show_warnings:
241+
sys.stderr.write("Parser%s\n" % msg)
242+
206243
def to_odml(self, parsed_doc):
207244
"""
208245
Parses a Python dictionary object containing an odML document to an odml.Document.
@@ -280,14 +317,19 @@ def parse_sections(self, section_list):
280317
# Make sure to always use the correct odml format attribute name
281318
sec_attrs[odmlfmt.Section.map(attr)] = content
282319

283-
sec = odmlfmt.Section.create(**sec_attrs)
284-
for prop in sec_props:
285-
sec.append(prop)
320+
try:
321+
sec = odmlfmt.Section.create(**sec_attrs)
322+
323+
for prop in sec_props:
324+
sec.append(prop)
286325

287-
for child_sec in children_secs:
288-
sec.append(child_sec)
326+
for child_sec in children_secs:
327+
sec.append(child_sec)
289328

290-
odml_sections.append(sec)
329+
odml_sections.append(sec)
330+
except Exception as exc:
331+
msg = "Section not created (%s)\n %s" % (sec_attrs, str(exc))
332+
self.error(msg)
291333

292334
return odml_sections
293335

@@ -316,7 +358,11 @@ def parse_properties(self, props_list):
316358
# Make sure to always use the correct odml format attribute name
317359
prop_attrs[odmlfmt.Property.map(attr)] = content
318360

319-
prop = odmlfmt.Property.create(**prop_attrs)
320-
odml_props.append(prop)
361+
try:
362+
prop = odmlfmt.Property.create(**prop_attrs)
363+
odml_props.append(prop)
364+
except Exception as exc:
365+
msg = "Property not created (%s)\n%s" % (prop_attrs, str(exc))
366+
self.error(msg)
321367

322368
return odml_props

odml/tools/odmlparser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ def from_file(self, file, doc_format=None):
183183
print(err)
184184
return None
185185

186-
par = DictReader(show_warnings=self.show_warnings)
186+
par = DictReader(ignore_errors=True,
187+
show_warnings=self.show_warnings)
187188
self.doc = par.to_odml(self.parsed_doc)
188189
# Provide original file name via the in memory document
189190
self.doc.origin_file_name = basename(file)

test/test_parser_json.py

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,140 @@
1+
"""
2+
This module supplies basic tests for the odml.dict_parser.DictReader
3+
reading from json files.
4+
"""
5+
16
import json
27
import os
8+
import tempfile
39
import unittest
410

511
from odml.tools import dict_parser
612
from odml.tools.parser_utils import ParserException, InvalidVersionException
713

814

15+
_INVALID_ATTRIBUTE_HANDLING_DOC = """
16+
{
17+
"Document": {
18+
"id": "6af2a3ec-9f3a-42d6-a59d-95f3ccbaa383",
19+
"%s": "i_do_not_exist_on_doc_level",
20+
"sections": [
21+
{
22+
"id": "51f3c79c-a7d7-471e-be16-e085caa851fb",
23+
"%s": "i_do_not_exist_on_sec_level",
24+
"type": "test",
25+
"name": "sec",
26+
"sections": [],
27+
"properties": [
28+
{
29+
"id": "c5aed35a-d9ff-437d-82d6-68f0cda8ea94",
30+
"%s": "i_do_not_exist_on_prop_level",
31+
"name": "prop",
32+
"value": [
33+
1,
34+
2,
35+
3
36+
],
37+
"type": "int"
38+
}
39+
]
40+
}
41+
]
42+
},
43+
"odml-version": "1.1"
44+
}
45+
""".strip()
46+
47+
_SEC_CREATION_ERROR_DOC = """
48+
{
49+
"Document": {
50+
"id": "6af2a3ec-9f3a-42d6-a59d-95f3ccbaa383",
51+
"sections": [
52+
{
53+
"id": "1113c79c-a7d7-471e-be16-e085caa851fb",
54+
"name": "sec",
55+
"type": "test",
56+
"sections": [
57+
{
58+
"id": "1213c79c-a7d7-471e-be16-e085caa851fb",
59+
"name": "%s",
60+
"type": "test"
61+
},
62+
{
63+
"id": [
64+
"I-am-so-kaputt"
65+
],
66+
"name": "%s",
67+
"type": "test"
68+
}
69+
]
70+
}
71+
]
72+
},
73+
"odml-version": "1.1"
74+
}
75+
""".strip()
76+
77+
_PROP_CREATION_ERROR_DOC = """
78+
{
79+
"Document": {
80+
"id": "6af2a3ec-9f3a-42d6-a59d-95f3ccbaa383",
81+
"sections": [
82+
{
83+
"id": "51f3c79c-a7d7-471e-be16-e085caa851fb",
84+
"type": "test",
85+
"name": "sec",
86+
"properties": [
87+
{
88+
"id": "121ed35a-d9ff-437d-82d6-68f0cda8ea94",
89+
"name": "valid_prop"
90+
},
91+
{
92+
"id": "122ed35a-d9ff-437d-82d6-68f0cda8ea94",
93+
"name": "invalid_prop",
94+
"value": [
95+
"a",
96+
"b"
97+
],
98+
"type": "int"
99+
}
100+
]
101+
}
102+
]
103+
},
104+
"odml-version": "1.1"
105+
}
106+
""".strip()
107+
108+
9109
class TestJSONParser(unittest.TestCase):
10110

11111
def setUp(self):
12112
dir_path = os.path.dirname(os.path.realpath(__file__))
13113
self.basepath = os.path.join(dir_path, "resources")
14114

15-
self.json_reader = dict_parser.DictReader()
115+
self.json_reader = dict_parser.DictReader(show_warnings=False)
116+
117+
dir_name = os.path.basename(os.path.splitext(__file__)[0])
118+
tmp_base_path = os.path.join(tempfile.gettempdir(), "odml_test")
119+
if not os.path.exists(tmp_base_path):
120+
os.mkdir(tmp_base_path)
121+
122+
tmp_dir_path = os.path.join(tmp_base_path, dir_name)
123+
if not os.path.exists(tmp_dir_path):
124+
os.mkdir(tmp_dir_path)
125+
126+
self.tmp_dir_path = tmp_dir_path
127+
128+
def _prepare_doc(self, file_name, file_content):
129+
file_path = os.path.join(self.tmp_dir_path, file_name)
130+
131+
with open(file_path, "w") as dump_file:
132+
dump_file.write(file_content)
133+
134+
with open(file_path) as raw_data:
135+
parsed_doc = json.load(raw_data)
136+
137+
return parsed_doc
16138

17139
def test_missing_root(self):
18140
filename = "missing_root.json"
@@ -46,3 +168,83 @@ def test_invalid_version(self):
46168

47169
with self.assertRaises(InvalidVersionException):
48170
_ = self.json_reader.to_odml(parsed_doc)
171+
172+
def test_invalid_attribute_handling(self):
173+
filename = "invalid_attributes.yaml"
174+
175+
inv_doc_attr = "invalid_doc_attr"
176+
inv_sec_attr = "invalid_sec_attr"
177+
inv_prop_attr = "invalid_prop_attr"
178+
179+
file_content = _INVALID_ATTRIBUTE_HANDLING_DOC % (inv_doc_attr, inv_sec_attr, inv_prop_attr)
180+
parsed_doc = self._prepare_doc(filename, file_content)
181+
182+
# Test ParserException on default parse
183+
exc_msg = "Invalid element"
184+
with self.assertRaises(ParserException) as exc:
185+
_ = self.json_reader.to_odml(parsed_doc)
186+
self.assertIn(exc_msg, str(exc.exception))
187+
188+
# Test success on ignore_error parse
189+
self.json_reader.ignore_errors = True
190+
doc = self.json_reader.to_odml(parsed_doc)
191+
192+
self.assertEqual(len(doc.sections), 1)
193+
self.assertEqual(len(doc.sections["sec"].properties), 1)
194+
195+
self.assertEqual(len(self.json_reader.warnings), 3)
196+
for msg in self.json_reader.warnings:
197+
self.assertIn("Error", msg)
198+
self.assertTrue(inv_doc_attr in msg or inv_sec_attr in msg or inv_prop_attr in msg)
199+
200+
def test_sec_creation_error(self):
201+
filename = "broken_section.yaml"
202+
203+
valid = "valid_sec"
204+
invalid = "invalid_sec"
205+
206+
file_content = _SEC_CREATION_ERROR_DOC % (valid, invalid)
207+
parsed_doc = self._prepare_doc(filename, file_content)
208+
209+
# Test ParserException on default parse
210+
exc_msg = "Section not created"
211+
with self.assertRaises(ParserException) as exc:
212+
_ = self.json_reader.to_odml(parsed_doc)
213+
self.assertIn(exc_msg, str(exc.exception))
214+
215+
# Test success on ignore_error parse
216+
self.json_reader.ignore_errors = True
217+
doc = self.json_reader.to_odml(parsed_doc)
218+
219+
self.assertEqual(len(doc.sections["sec"].sections), 1)
220+
self.assertIn(valid, doc.sections["sec"].sections)
221+
self.assertNotIn(invalid, doc.sections["sec"].sections)
222+
223+
self.assertEqual(len(self.json_reader.warnings), 1)
224+
for msg in self.json_reader.warnings:
225+
self.assertIn("Error", msg)
226+
self.assertIn(exc_msg, msg)
227+
228+
def test_prop_creation_error(self):
229+
filename = "broken_property.yaml"
230+
231+
parsed_doc = self._prepare_doc(filename, _PROP_CREATION_ERROR_DOC)
232+
233+
# Test ParserException on default parse
234+
exc_msg = "Property not created"
235+
with self.assertRaises(ParserException) as exc:
236+
_ = self.json_reader.to_odml(parsed_doc)
237+
self.assertIn(exc_msg, str(exc.exception))
238+
239+
# Test success on ignore_error parse
240+
self.json_reader.ignore_errors = True
241+
doc = self.json_reader.to_odml(parsed_doc)
242+
243+
self.assertEqual(len(doc.sections["sec"].properties), 1)
244+
self.assertIn("valid_prop", doc.sections["sec"].properties)
245+
self.assertNotIn("invalid_prop", doc.sections["sec"].properties)
246+
247+
self.assertEqual(len(self.json_reader.warnings), 1)
248+
for msg in self.json_reader.warnings:
249+
self.assertIn("Error", msg)
250+
self.assertIn(exc_msg, msg)

0 commit comments

Comments
 (0)