Merge lp:~milo/linaro-ci-dashboard/xml-to-dict into lp:linaro-ci-dashboard
- xml-to-dict
- Merge into trunk
Proposed by
Milo Casagrande
Status: | Superseded |
---|---|
Proposed branch: | lp:~milo/linaro-ci-dashboard/xml-to-dict |
Merge into: | lp:linaro-ci-dashboard |
Prerequisite: | lp:~stevanr/linaro-ci-dashboard/build_results_xml |
Diff against target: |
810 lines (+669/-13) (has conflicts) 8 files modified
dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html (+0/-8) dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py (+27/-2) dashboard/frontend/models/loop_build.py (+13/-0) dashboard/frontend/models/textfield_loop.py (+59/-3) dashboard/frontend/tests/__init__.py (+3/-0) dashboard/frontend/tests/test_xml_to_dict.py (+286/-0) dashboard/lib/xml_fields.py (+51/-0) dashboard/lib/xml_to_dict.py (+230/-0) Text conflict in dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py Text conflict in dashboard/frontend/models/textfield_loop.py |
To merge this branch: | bzr merge lp:~milo/linaro-ci-dashboard/xml-to-dict |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stevan Radaković | Pending | ||
Данило Шеган | Pending | ||
Linaro Infrastructure | Pending | ||
Review via email:
|
Commit message
Description of the change
Here there is all the logic for XML to dictionary and dictionary to XML conversions. Some tests and their values are provided.
To post a comment you must log in.
- 42. By Milo Casagrande
-
Removed print statements.
- 43. By Milo Casagrande
-
Fixed tests regressions, modified function.
- 44. By Milo Casagrande
-
Removed redundant errors div.
- 45. By Milo Casagrande
-
Merged from trunk.
- 46. By Milo Casagrande
-
Fixes for review.
- 47. By Milo Casagrande
-
Fixed assertion.
- 48. By Milo Casagrande
-
PEP8 fixes.
- 49. By Milo Casagrande
-
Added some tests.
- 50. By Milo Casagrande
-
Added note, fixed some tests.
- 51. By Milo Casagrande
-
Fixed reference.
- 52. By Milo Casagrande
-
Removed comment.
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html' | |||
2 | --- dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-08-31 15:21:11 +0000 | |||
3 | +++ dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-09-06 08:33:21 +0000 | |||
4 | @@ -6,14 +6,6 @@ | |||
5 | 6 | <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post"> | 6 | <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post"> |
6 | 7 | {% csrf_token %} | 7 | {% csrf_token %} |
7 | 8 | {% endblock create_form %} | 8 | {% endblock create_form %} |
8 | 9 | |||
9 | 10 | {% if form.non_field_errors %} | ||
10 | 11 | <div class="form_error"> | ||
11 | 12 | {% for err in form.non_field_errors %} | ||
12 | 13 | <div class="error_message">{{ err }}</div> | ||
13 | 14 | {% endfor %} | ||
14 | 15 | </div> | ||
15 | 16 | {% endif %} | ||
16 | 17 | {{ form.as_p }} | 9 | {{ form.as_p }} |
17 | 18 | <div><input type="submit" value="Submit" /></div> | 10 | <div><input type="submit" value="Submit" /></div> |
18 | 19 | </form> | 11 | </form> |
19 | 20 | 12 | ||
20 | === modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py' | |||
21 | --- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000 | |||
22 | +++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-06 08:33:21 +0000 | |||
23 | @@ -24,9 +24,9 @@ | |||
24 | 24 | 24 | ||
25 | 25 | A_NAME = 'a-build' | 25 | A_NAME = 'a-build' |
26 | 26 | B_NAME = 'b-build' | 26 | B_NAME = 'b-build' |
28 | 27 | VALID_VALUES = 'a=2\nb=3' | 27 | VALID_VALUES = u'a=2\nb=3' |
29 | 28 | VALID_LINES = ['a=2', 'b=3'] | 28 | VALID_LINES = ['a=2', 'b=3'] |
31 | 29 | NON_VALID_VALUES = 'a:2\nb=3' | 29 | NON_VALID_VALUES = u'a:2\nb=3' |
32 | 30 | NON_VALID_LINES = ['a:2', 'b=3'] | 30 | NON_VALID_LINES = ['a:2', 'b=3'] |
33 | 31 | VALID_DICT = {'a': '2', 'b': '3'} | 31 | VALID_DICT = {'a': '2', 'b': '3'} |
34 | 32 | 32 | ||
35 | @@ -52,6 +52,7 @@ | |||
36 | 52 | def test_valid_values_wrong(self): | 52 | def test_valid_values_wrong(self): |
37 | 53 | self.assertEqual((False, self.NON_VALID_LINES), | 53 | self.assertEqual((False, self.NON_VALID_LINES), |
38 | 54 | AndroidTextFieldLoop.valid_values( | 54 | AndroidTextFieldLoop.valid_values( |
39 | 55 | <<<<<<< TREE | ||
40 | 55 | self.NON_VALID_VALUES)) | 56 | self.NON_VALID_VALUES)) |
41 | 56 | 57 | ||
42 | 57 | def test_schedule_build(self): | 58 | def test_schedule_build(self): |
43 | @@ -62,3 +63,27 @@ | |||
44 | 62 | def test_schedule_build_invalid(self): | 63 | def test_schedule_build_invalid(self): |
45 | 63 | build = self.non_valid_android_loop.schedule_build() | 64 | build = self.non_valid_android_loop.schedule_build() |
46 | 64 | self.assertEqual(build.result_xml, {}) | 65 | self.assertEqual(build.result_xml, {}) |
47 | 66 | ======= | ||
48 | 67 | self.NON_VALID_VALUES)) | ||
49 | 68 | |||
50 | 69 | def test_schedule_build(self): | ||
51 | 70 | build = self.android_loop.schedule_build() | ||
52 | 71 | expected_out = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ' \ | ||
53 | 72 | 'loop [\n <!ELEMENT loop (description?,fields)>\n' \ | ||
54 | 73 | ' <!ELEMENT description (#PCDATA)>\n ' \ | ||
55 | 74 | '<!ELEMENT fields (field+)>\n <!ELEMENT field ' \ | ||
56 | 75 | '(#PCDATA)>\n <!ATTLIST field name CDATA ' \ | ||
57 | 76 | '#REQUIRED>\n <!ATTLIST field type (text|int|bool) ' \ | ||
58 | 77 | '"text">\n]><loop><fields><field name="a">2</field>' \ | ||
59 | 78 | '<field name="b">3</field><field name="name">a-build' \ | ||
60 | 79 | '</field><field name="is_restricted">False</field>' \ | ||
61 | 80 | '<field name="is_official">False</field>' \ | ||
62 | 81 | '<field name="type">AndroidTextFieldLoop</field>' \ | ||
63 | 82 | '</fields></loop>' | ||
64 | 83 | self.assertEqual(expected_out, build.result_xml) | ||
65 | 84 | self.assertEqual(build.status, "success") | ||
66 | 85 | |||
67 | 86 | def test_schedule_build_invalid(self): | ||
68 | 87 | build = self.non_valid_android_loop.schedule_build() | ||
69 | 88 | self.assertEqual("", build.result_xml) | ||
70 | 89 | >>>>>>> MERGE-SOURCE | ||
71 | 65 | 90 | ||
72 | === modified file 'dashboard/frontend/models/loop_build.py' | |||
73 | --- dashboard/frontend/models/loop_build.py 2012-09-05 13:23:05 +0000 | |||
74 | +++ dashboard/frontend/models/loop_build.py 2012-09-06 08:33:21 +0000 | |||
75 | @@ -18,6 +18,7 @@ | |||
76 | 18 | 18 | ||
77 | 19 | from django.db import models | 19 | from django.db import models |
78 | 20 | from frontend.models.loop import Loop | 20 | from frontend.models.loop import Loop |
79 | 21 | from dashboard.lib.xml_to_dict import XmlToDict | ||
80 | 21 | 22 | ||
81 | 22 | 23 | ||
82 | 23 | class LoopBuild(models.Model): | 24 | class LoopBuild(models.Model): |
83 | @@ -47,3 +48,15 @@ | |||
84 | 47 | else: | 48 | else: |
85 | 48 | self.build_number = 1 | 49 | self.build_number = 1 |
86 | 49 | super(LoopBuild, self).save(*args, **kwargs) | 50 | super(LoopBuild, self).save(*args, **kwargs) |
87 | 51 | |||
88 | 52 | def get_build_result(self): | ||
89 | 53 | """ | ||
90 | 54 | Returns a Python dictionary representation of the build XML stored. | ||
91 | 55 | |||
92 | 56 | :return A dictionary of the XML result. | ||
93 | 57 | """ | ||
94 | 58 | dict_result = {} | ||
95 | 59 | if self.result_xml: | ||
96 | 60 | xml_to_dict = XmlToDict(self.result_xml) | ||
97 | 61 | dict_result = xml_to_dict.tree_to_dict() | ||
98 | 62 | return dict_result | ||
99 | 50 | 63 | ||
100 | === modified file 'dashboard/frontend/models/textfield_loop.py' | |||
101 | --- dashboard/frontend/models/textfield_loop.py 2012-09-05 13:59:57 +0000 | |||
102 | +++ dashboard/frontend/models/textfield_loop.py 2012-09-06 08:33:21 +0000 | |||
103 | @@ -17,6 +17,7 @@ | |||
104 | 17 | 17 | ||
105 | 18 | from django.db import models | 18 | from django.db import models |
106 | 19 | from frontend.models.loop import Loop | 19 | from frontend.models.loop import Loop |
107 | 20 | from dashboard.lib.xml_to_dict import DictToXml | ||
108 | 20 | 21 | ||
109 | 21 | 22 | ||
110 | 22 | # Default delimiter to separate values from keys in the text field. | 23 | # Default delimiter to separate values from keys in the text field. |
111 | @@ -40,6 +41,7 @@ | |||
112 | 40 | values = models.TextField() | 41 | values = models.TextField() |
113 | 41 | 42 | ||
114 | 42 | def schedule_build(self, parameters=None): | 43 | def schedule_build(self, parameters=None): |
115 | 44 | <<<<<<< TREE | ||
116 | 43 | from frontend.models.loop_build import LoopBuild | 45 | from frontend.models.loop_build import LoopBuild |
117 | 44 | build = LoopBuild() | 46 | build = LoopBuild() |
118 | 45 | build.loop = self | 47 | build.loop = self |
119 | @@ -55,14 +57,40 @@ | |||
120 | 55 | return build | 57 | return build |
121 | 56 | 58 | ||
122 | 57 | def values_to_dict(self): | 59 | def values_to_dict(self): |
123 | 60 | ======= | ||
124 | 61 | from frontend.models.loop_build import LoopBuild | ||
125 | 62 | build = LoopBuild() | ||
126 | 63 | build.loop = self | ||
127 | 64 | build.duration = 0.00 | ||
128 | 65 | |||
129 | 66 | try: | ||
130 | 67 | build.result_xml = self.dict_to_xml() | ||
131 | 68 | build.status = 'success' | ||
132 | 69 | except: | ||
133 | 70 | build.status = 'failure' | ||
134 | 71 | |||
135 | 72 | build.save() | ||
136 | 73 | return build | ||
137 | 74 | |||
138 | 75 | def values_to_dict(self, valid=None, lines=None): | ||
139 | 76 | >>>>>>> MERGE-SOURCE | ||
140 | 58 | """ | 77 | """ |
141 | 59 | Returns a dictionary representation of the values inserted. The | 78 | Returns a dictionary representation of the values inserted. The |
142 | 60 | key<>value pairs are split on DEFAULT_DELIMITER. | 79 | key<>value pairs are split on DEFAULT_DELIMITER. |
143 | 61 | 80 | ||
145 | 62 | :return A dictionary of the values inserted. | 81 | The default parameters are used for a second iteration of this function |
146 | 82 | if we do not want to parse again the lines, but we only want to get the | ||
147 | 83 | dictionary. | ||
148 | 84 | |||
149 | 85 | :param valid: If the lines are valid or not. | ||
150 | 86 | :type valid bool | ||
151 | 87 | :param lines: The list of lines in the text field. | ||
152 | 88 | :type lines list | ||
153 | 89 | :return A dictionary of the values inserted in the text field. | ||
154 | 63 | """ | 90 | """ |
155 | 64 | text_to_dict = {} | 91 | text_to_dict = {} |
157 | 65 | valid, lines = self.valid_values(self.values) | 92 | if valid is None and lines is None: |
158 | 93 | valid, lines = self.valid_values(self.values) | ||
159 | 66 | 94 | ||
160 | 67 | if valid: | 95 | if valid: |
161 | 68 | for line in lines: | 96 | for line in lines: |
162 | @@ -71,6 +99,19 @@ | |||
163 | 71 | 99 | ||
164 | 72 | return text_to_dict | 100 | return text_to_dict |
165 | 73 | 101 | ||
166 | 102 | def _all_values_to_dict(self): | ||
167 | 103 | """ | ||
168 | 104 | Returns a dict representation of the necessary fields for the loops. | ||
169 | 105 | |||
170 | 106 | :return A Python dictionary with the necessary fields. | ||
171 | 107 | """ | ||
172 | 108 | # TODO need a better way to serialize the model | ||
173 | 109 | all_values_dict = {'is_official': self.is_official, | ||
174 | 110 | 'is_restricted': self.is_restricted, | ||
175 | 111 | 'name': self.name, | ||
176 | 112 | 'type': self.type} | ||
177 | 113 | return all_values_dict | ||
178 | 114 | |||
179 | 74 | @staticmethod | 115 | @staticmethod |
180 | 75 | def valid_values(values): | 116 | def valid_values(values): |
181 | 76 | """ | 117 | """ |
182 | @@ -79,7 +120,7 @@ | |||
183 | 79 | 120 | ||
184 | 80 | :param values: the string with all the key<>value pairs, separated with | 121 | :param values: the string with all the key<>value pairs, separated with |
185 | 81 | a newline character. | 122 | a newline character. |
187 | 82 | :type str | 123 | :type values unicode |
188 | 83 | :return a boolean for the validity, and the list of lines. | 124 | :return a boolean for the validity, and the list of lines. |
189 | 84 | """ | 125 | """ |
190 | 85 | valid = True | 126 | valid = True |
191 | @@ -91,3 +132,18 @@ | |||
192 | 91 | valid = False | 132 | valid = False |
193 | 92 | break | 133 | break |
194 | 93 | return valid, lines | 134 | return valid, lines |
195 | 135 | |||
196 | 136 | def dict_to_xml(self): | ||
197 | 137 | """ | ||
198 | 138 | Converts the necessary values into an XML tree. | ||
199 | 139 | |||
200 | 140 | :return The XML tree as a string, or an empty string if the inserted | ||
201 | 141 | values are not valid. | ||
202 | 142 | """ | ||
203 | 143 | xml_string = "" | ||
204 | 144 | valid, lines = self.valid_values(self.values) | ||
205 | 145 | if valid: | ||
206 | 146 | values = self.values_to_dict(valid, lines) | ||
207 | 147 | values.update(self._all_values_to_dict()) | ||
208 | 148 | xml_string = DictToXml(values).dict_to_tree() | ||
209 | 149 | return xml_string | ||
210 | 94 | 150 | ||
211 | === modified file 'dashboard/frontend/tests/__init__.py' | |||
212 | --- dashboard/frontend/tests/__init__.py 2012-09-03 08:18:10 +0000 | |||
213 | +++ dashboard/frontend/tests/__init__.py 2012-09-06 08:33:21 +0000 | |||
214 | @@ -2,6 +2,7 @@ | |||
215 | 2 | from dashboard.frontend.tests.test_models import * | 2 | from dashboard.frontend.tests.test_models import * |
216 | 3 | from dashboard.frontend.tests.test_clientresponse import * | 3 | from dashboard.frontend.tests.test_clientresponse import * |
217 | 4 | from dashboard.frontend.tests.test_custom_commands import * | 4 | from dashboard.frontend.tests.test_custom_commands import * |
218 | 5 | from dashboard.frontend.tests.test_xml_to_dict import * | ||
219 | 5 | 6 | ||
220 | 6 | 7 | ||
221 | 7 | #starts the test suite | 8 | #starts the test suite |
222 | @@ -14,4 +15,6 @@ | |||
223 | 14 | 'LoopTests': LoopTest, | 15 | 'LoopTests': LoopTest, |
224 | 15 | 'ClientResponseTests': ClientResponseTests, | 16 | 'ClientResponseTests': ClientResponseTests, |
225 | 16 | 'JenkinsCommandTest': JenkinsCommandTest, | 17 | 'JenkinsCommandTest': JenkinsCommandTest, |
226 | 18 | 'XmlToDictTest': XmlToDictTest, | ||
227 | 19 | 'DictToXmlTest': DictToXmlTest, | ||
228 | 17 | } | 20 | } |
229 | 18 | 21 | ||
230 | === added file 'dashboard/frontend/tests/test_xml_to_dict.py' | |||
231 | --- dashboard/frontend/tests/test_xml_to_dict.py 1970-01-01 00:00:00 +0000 | |||
232 | +++ dashboard/frontend/tests/test_xml_to_dict.py 2012-09-06 08:33:21 +0000 | |||
233 | @@ -0,0 +1,286 @@ | |||
234 | 1 | # Copyright (C) 2012 Linaro | ||
235 | 2 | # | ||
236 | 3 | # This file is part of linaro-ci-dashboard. | ||
237 | 4 | # | ||
238 | 5 | # linaro-ci-dashboard is free software: you can redistribute it and/or modify | ||
239 | 6 | # it under the terms of the GNU Affero General Public License as published by | ||
240 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
241 | 8 | # (at your option) any later version. | ||
242 | 9 | # | ||
243 | 10 | # linaro-ci-dashboard is distributed in the hope that it will be useful, | ||
244 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
245 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
246 | 13 | # GNU Affero General Public License for more details. | ||
247 | 14 | # | ||
248 | 15 | # You should have received a copy of the GNU Affero General Public License | ||
249 | 16 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. | ||
250 | 17 | |||
251 | 18 | from django.test import TestCase | ||
252 | 19 | import os | ||
253 | 20 | from dashboard.lib.xml_to_dict import ( | ||
254 | 21 | XmlToDict, | ||
255 | 22 | DictToXml | ||
256 | 23 | ) | ||
257 | 24 | from dashboard.lib.xml_fields import ( | ||
258 | 25 | LOOP_ELEMENT, | ||
259 | 26 | FIELDS_ELEMENT, | ||
260 | 27 | TYPE_ATTR, | ||
261 | 28 | NAME_ATTR, | ||
262 | 29 | XML_START, | ||
263 | 30 | INT_TYPE, | ||
264 | 31 | BOOL_TYPE, | ||
265 | 32 | ) | ||
266 | 33 | |||
267 | 34 | |||
268 | 35 | # XML tags | ||
269 | 36 | ROOT_TAG_OPEN = '<loop>' | ||
270 | 37 | ROOT_TAG_CLOSE = '</loop>' | ||
271 | 38 | FIELDS_TAG_OPEN = '<fields>' | ||
272 | 39 | FIELDS_TAG_CLOSE = '</fields>' | ||
273 | 40 | VALID_FIELD = '<field name="%(field_name)s">%(field_value)s</field>' | ||
274 | 41 | VALID_FIELD_WITH_TYPE = ('<field name="%(field_name)s" ' | ||
275 | 42 | 'type="%(field_type)s">%(field_value)s</field>') | ||
276 | 43 | |||
277 | 44 | # Field names, types and values used as defaults for the tests. | ||
278 | 45 | LOOP_TYPE_VAL = 'text-build' | ||
279 | 46 | LOOP_NAME_VAL = 'build-test' | ||
280 | 47 | BUILD_TYPE = 'build_type' | ||
281 | 48 | BUILD_TYPE_VAL = 'build-android' | ||
282 | 49 | REPO_QUIET = 'repo_quiet' | ||
283 | 50 | REPO_QUIET_VAL = True | ||
284 | 51 | BUILD_FS_IMAGE = 'build_fs_image' | ||
285 | 52 | BUILD_FS_IMAGE_VAL = False | ||
286 | 53 | MAKE_JOBS = 'make_jobs' | ||
287 | 54 | MAKE_JOBS_VAL = 2 | ||
288 | 55 | |||
289 | 56 | NEWLINE = os.linesep | ||
290 | 57 | # Used to create the needed dictionary for fields creation. | ||
291 | 58 | # The dictionary has to be: | ||
292 | 59 | # {FIELD_NAME: value, FIELD_VALUE: value [, FIELD_TYPE: value]} | ||
293 | 60 | # FIELD_TYPE is optional. | ||
294 | 61 | FIELD_NAME = 'field_name' | ||
295 | 62 | FIELD_VALUE = 'field_value' | ||
296 | 63 | FIELD_TYPE = 'field_type' | ||
297 | 64 | |||
298 | 65 | FIELDS = [ | ||
299 | 66 | {FIELD_NAME: BUILD_TYPE, FIELD_VALUE: BUILD_TYPE_VAL}, | ||
300 | 67 | {FIELD_NAME: REPO_QUIET, FIELD_TYPE: BOOL_TYPE, | ||
301 | 68 | FIELD_VALUE: REPO_QUIET_VAL}, | ||
302 | 69 | {FIELD_NAME: BUILD_FS_IMAGE, FIELD_TYPE: BOOL_TYPE, | ||
303 | 70 | FIELD_VALUE: BUILD_FS_IMAGE_VAL}, | ||
304 | 71 | {FIELD_NAME: MAKE_JOBS, FIELD_TYPE: INT_TYPE, | ||
305 | 72 | FIELD_VALUE: MAKE_JOBS_VAL}, | ||
306 | 73 | {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL}, | ||
307 | 74 | {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL} | ||
308 | 75 | ] | ||
309 | 76 | |||
310 | 77 | VALID_START = XML_START + ROOT_TAG_OPEN | ||
311 | 78 | VALID_CLOSE = ROOT_TAG_CLOSE | ||
312 | 79 | |||
313 | 80 | |||
314 | 81 | def create_xml_fields(fields_list): | ||
315 | 82 | """ | ||
316 | 83 | Support functions to create the <fields><field>... hierarchy. | ||
317 | 84 | |||
318 | 85 | :param fields_list: the list of fields to create. Each field has to be | ||
319 | 86 | a dictionary with FIELD_NAME, FIELD_VALUE and optional FIELD_TYPE keys. | ||
320 | 87 | :type fields_list list | ||
321 | 88 | """ | ||
322 | 89 | assert isinstance(fields_list, list) | ||
323 | 90 | fields_xml = FIELDS_TAG_OPEN | ||
324 | 91 | |||
325 | 92 | for field in fields_list: | ||
326 | 93 | if field.get(FIELD_TYPE, None) is not None: | ||
327 | 94 | fields_xml += VALID_FIELD_WITH_TYPE % field | ||
328 | 95 | else: | ||
329 | 96 | fields_xml += VALID_FIELD % field | ||
330 | 97 | fields_xml += FIELDS_TAG_CLOSE | ||
331 | 98 | return fields_xml | ||
332 | 99 | |||
333 | 100 | |||
334 | 101 | class XmlToDictTest(TestCase): | ||
335 | 102 | """ | ||
336 | 103 | Test class for the conversion from XML to Python dictionary. | ||
337 | 104 | The XML specification is taken from the HACKING file. | ||
338 | 105 | """ | ||
339 | 106 | |||
340 | 107 | def setUp(self): | ||
341 | 108 | super(XmlToDictTest, self).setUp() | ||
342 | 109 | self.xml_string = VALID_START | ||
343 | 110 | self.xml_string += create_xml_fields(FIELDS) | ||
344 | 111 | self.xml_string += VALID_CLOSE | ||
345 | 112 | self.xml_to_dict = XmlToDict(self.xml_string) | ||
346 | 113 | |||
347 | 114 | def test_get_element_from_root(self): | ||
348 | 115 | """ | ||
349 | 116 | Tests retrieval of the elements in the XML tree, but the root. | ||
350 | 117 | """ | ||
351 | 118 | self.assertEqual(self.xml_to_dict.get_elements_from_root()[0].tag, | ||
352 | 119 | FIELDS_ELEMENT) | ||
353 | 120 | |||
354 | 121 | def test_get_element_by_name_correct(self): | ||
355 | 122 | """Tests retrieving of an element with a correct name.""" | ||
356 | 123 | self.assertEqual(FIELDS_ELEMENT, | ||
357 | 124 | self.xml_to_dict.get_element_by_name( | ||
358 | 125 | FIELDS_ELEMENT).tag) | ||
359 | 126 | |||
360 | 127 | def test_get_element_by_name_wrong(self): | ||
361 | 128 | """ Tests retrieving of an element with a wrong name.""" | ||
362 | 129 | self.assertEqual(None, | ||
363 | 130 | self.xml_to_dict.get_element_by_name('wrong')) | ||
364 | 131 | |||
365 | 132 | def test_elements_to_dict(self): | ||
366 | 133 | """ | ||
367 | 134 | Tests creation of a dictionary with multiple elements. | ||
368 | 135 | """ | ||
369 | 136 | expected_output = {MAKE_JOBS: MAKE_JOBS_VAL, | ||
370 | 137 | BUILD_TYPE: BUILD_TYPE_VAL, | ||
371 | 138 | BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL, | ||
372 | 139 | REPO_QUIET: REPO_QUIET_VAL, | ||
373 | 140 | NAME_ATTR: LOOP_NAME_VAL, | ||
374 | 141 | TYPE_ATTR: LOOP_TYPE_VAL} | ||
375 | 142 | element = self.xml_to_dict.get_element_by_name(FIELDS_ELEMENT) | ||
376 | 143 | self.assertEqual(expected_output, | ||
377 | 144 | self.xml_to_dict.element_to_dict(element)) | ||
378 | 145 | |||
379 | 146 | def test_bool_true_conversion(self): | ||
380 | 147 | """ | ||
381 | 148 | Tests the True bool conversion. | ||
382 | 149 | """ | ||
383 | 150 | expected_out = {'a-field': True} | ||
384 | 151 | for value in ['1', 1, 'y', 'Yes', True, 'True']: | ||
385 | 152 | local_xml = VALID_START | ||
386 | 153 | fields_list = [{FIELD_NAME: 'a-field', | ||
387 | 154 | FIELD_TYPE: BOOL_TYPE, | ||
388 | 155 | FIELD_VALUE: value}] | ||
389 | 156 | local_xml += create_xml_fields(fields_list) | ||
390 | 157 | local_xml += VALID_CLOSE | ||
391 | 158 | local_xml_to_dict = XmlToDict(local_xml) | ||
392 | 159 | element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT) | ||
393 | 160 | self.assertEqual(expected_out, | ||
394 | 161 | local_xml_to_dict.element_to_dict(element)) | ||
395 | 162 | |||
396 | 163 | def test_bool_false_conversion(self): | ||
397 | 164 | """ | ||
398 | 165 | Tests the False bool conversion. | ||
399 | 166 | """ | ||
400 | 167 | expected_out = {'a-field': False} | ||
401 | 168 | for value in ['n', 0, 42, 'Yep', 'No', False, 'False', '?']: | ||
402 | 169 | local_xml = VALID_START | ||
403 | 170 | fields_list = [{FIELD_NAME: 'a-field', | ||
404 | 171 | FIELD_TYPE: BOOL_TYPE, | ||
405 | 172 | FIELD_VALUE: value}] | ||
406 | 173 | local_xml += create_xml_fields(fields_list) | ||
407 | 174 | local_xml += VALID_CLOSE | ||
408 | 175 | local_xml_to_dict = XmlToDict(local_xml) | ||
409 | 176 | element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT) | ||
410 | 177 | self.assertEqual(expected_out, | ||
411 | 178 | local_xml_to_dict.element_to_dict(element)) | ||
412 | 179 | |||
413 | 180 | def test_int_conversion(self): | ||
414 | 181 | """ | ||
415 | 182 | Tests the int conversion. | ||
416 | 183 | """ | ||
417 | 184 | expected_out = {'a-field': 42} | ||
418 | 185 | for value in [42, '42', '42 ', ' 42']: | ||
419 | 186 | local_xml = VALID_START | ||
420 | 187 | fields_list = [{FIELD_NAME: 'a-field', | ||
421 | 188 | FIELD_TYPE: INT_TYPE, | ||
422 | 189 | FIELD_VALUE: value}] | ||
423 | 190 | local_xml += create_xml_fields(fields_list) | ||
424 | 191 | local_xml += VALID_CLOSE | ||
425 | 192 | local_xml_to_dict = XmlToDict(local_xml) | ||
426 | 193 | element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT) | ||
427 | 194 | self.assertEqual(expected_out, | ||
428 | 195 | local_xml_to_dict.element_to_dict(element)) | ||
429 | 196 | |||
430 | 197 | def test_str_conversion(self): | ||
431 | 198 | """ | ||
432 | 199 | Tests the str conversion. | ||
433 | 200 | """ | ||
434 | 201 | expected_out = {'a-field': '42'} | ||
435 | 202 | for value in [42, '42', '42 ', ' 42']: | ||
436 | 203 | local_xml = VALID_START | ||
437 | 204 | fields_list = [{FIELD_NAME: 'a-field', | ||
438 | 205 | FIELD_VALUE: value}] | ||
439 | 206 | local_xml += create_xml_fields(fields_list) | ||
440 | 207 | local_xml += VALID_CLOSE | ||
441 | 208 | local_xml_to_dict = XmlToDict(local_xml) | ||
442 | 209 | element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT) | ||
443 | 210 | self.assertEqual(expected_out, | ||
444 | 211 | local_xml_to_dict.element_to_dict(element)) | ||
445 | 212 | |||
446 | 213 | def test_tree_to_dict(self): | ||
447 | 214 | """ | ||
448 | 215 | Tests the conversion of a dictionary into a full XML tree. | ||
449 | 216 | """ | ||
450 | 217 | expected_output = { | ||
451 | 218 | REPO_QUIET: REPO_QUIET_VAL, | ||
452 | 219 | NAME_ATTR: LOOP_NAME_VAL, | ||
453 | 220 | MAKE_JOBS: MAKE_JOBS_VAL, | ||
454 | 221 | TYPE_ATTR: LOOP_TYPE_VAL, | ||
455 | 222 | BUILD_TYPE: BUILD_TYPE_VAL, | ||
456 | 223 | BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL | ||
457 | 224 | } | ||
458 | 225 | self.assertEqual(expected_output, self.xml_to_dict.tree_to_dict()) | ||
459 | 226 | |||
460 | 227 | def test_get_root_name(self): | ||
461 | 228 | """ | ||
462 | 229 | Tests retrieving of the root element. | ||
463 | 230 | """ | ||
464 | 231 | self.assertEqual(self.xml_to_dict.get_root(), LOOP_ELEMENT) | ||
465 | 232 | |||
466 | 233 | def test_get_root_attributes(self): | ||
467 | 234 | """ | ||
468 | 235 | Tests retrieving of the root attributes. | ||
469 | 236 | """ | ||
470 | 237 | self.assertEqual({}, self.xml_to_dict.get_root_attributes()) | ||
471 | 238 | |||
472 | 239 | |||
473 | 240 | class DictToXmlTest(TestCase): | ||
474 | 241 | """ | ||
475 | 242 | Test class for the conversion from Python dictionary into XML. | ||
476 | 243 | The XML specification is taken from the HACKING file. | ||
477 | 244 | """ | ||
478 | 245 | |||
479 | 246 | FIELDS_LIST = [ | ||
480 | 247 | {FIELD_NAME: MAKE_JOBS, FIELD_VALUE: MAKE_JOBS_VAL}, | ||
481 | 248 | {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL}, | ||
482 | 249 | {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL}, | ||
483 | 250 | ] | ||
484 | 251 | |||
485 | 252 | VALID_DICT = { | ||
486 | 253 | NAME_ATTR: LOOP_NAME_VAL, TYPE_ATTR: LOOP_TYPE_VAL, | ||
487 | 254 | MAKE_JOBS: str(MAKE_JOBS_VAL) | ||
488 | 255 | } | ||
489 | 256 | |||
490 | 257 | def setUp(self): | ||
491 | 258 | super(DictToXmlTest, self).setUp() | ||
492 | 259 | self.valid_xml = VALID_START | ||
493 | 260 | self.valid_xml += create_xml_fields(self.FIELDS_LIST) | ||
494 | 261 | self.valid_xml += VALID_CLOSE | ||
495 | 262 | self.dict_to_xml = DictToXml(self.VALID_DICT) | ||
496 | 263 | |||
497 | 264 | def test_dict_to_tree(self): | ||
498 | 265 | """ | ||
499 | 266 | Tests dictionary to XML conversion. | ||
500 | 267 | """ | ||
501 | 268 | self.assertEqual(self.valid_xml, self.dict_to_xml.dict_to_tree()) | ||
502 | 269 | |||
503 | 270 | def test_dict_to_tree_with_bool(self): | ||
504 | 271 | """ | ||
505 | 272 | Tests dictionary to XML conversion, with a bool inside, ElementTree | ||
506 | 273 | does not cope very well with bool type. | ||
507 | 274 | """ | ||
508 | 275 | fields = [ | ||
509 | 276 | {FIELD_NAME: REPO_QUIET, FIELD_VALUE: REPO_QUIET_VAL}, | ||
510 | 277 | {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL}, | ||
511 | 278 | {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL}, | ||
512 | 279 | ] | ||
513 | 280 | dictionary = {TYPE_ATTR: LOOP_TYPE_VAL, NAME_ATTR: LOOP_NAME_VAL, | ||
514 | 281 | REPO_QUIET: REPO_QUIET_VAL} | ||
515 | 282 | dict_2_xml = DictToXml(dictionary) | ||
516 | 283 | xml_string = VALID_START | ||
517 | 284 | xml_string += create_xml_fields(fields) | ||
518 | 285 | xml_string += VALID_CLOSE | ||
519 | 286 | self.assertEqual(xml_string, dict_2_xml.dict_to_tree()) | ||
520 | 0 | 287 | ||
521 | === added file 'dashboard/lib/xml_fields.py' | |||
522 | --- dashboard/lib/xml_fields.py 1970-01-01 00:00:00 +0000 | |||
523 | +++ dashboard/lib/xml_fields.py 2012-09-06 08:33:21 +0000 | |||
524 | @@ -0,0 +1,51 @@ | |||
525 | 1 | # Copyright (C) 2012 Linaro | ||
526 | 2 | # | ||
527 | 3 | # This file is part of linaro-ci-dashboard. | ||
528 | 4 | # | ||
529 | 5 | # linaro-ci-dashboard is free software: you can redistribute it and/or modify | ||
530 | 6 | # it under the terms of the GNU Affero General Public License as published by | ||
531 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
532 | 8 | # (at your option) any later version. | ||
533 | 9 | # | ||
534 | 10 | # linaro-ci-dashboard is distributed in the hope that it will be useful, | ||
535 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
536 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
537 | 13 | # GNU Affero General Public License for more details. | ||
538 | 14 | # | ||
539 | 15 | # You should have received a copy of the GNU Affero General Public License | ||
540 | 16 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. | ||
541 | 17 | # Django settings for dashboard project. | ||
542 | 18 | |||
543 | 19 | # The file contains all the necessary elements, tags and everything else | ||
544 | 20 | # related to the XML representation of a chain-build. | ||
545 | 21 | |||
546 | 22 | # All the XML valid elements. | ||
547 | 23 | FIELD_ELEMENT = 'field' | ||
548 | 24 | FIELDS_ELEMENT = 'fields' | ||
549 | 25 | LOOP_ELEMENT = 'loop' | ||
550 | 26 | DESCRIPTION_ELEMENT = 'description' | ||
551 | 27 | |||
552 | 28 | # All the XML attributes. | ||
553 | 29 | NAME_ATTR = 'name' | ||
554 | 30 | TYPE_ATTR = 'type' | ||
555 | 31 | |||
556 | 32 | # The valid types for element values. | ||
557 | 33 | BOOL_TYPE = 'bool' | ||
558 | 34 | INT_TYPE = 'int' | ||
559 | 35 | STR_TYPE = 'text' | ||
560 | 36 | VALID_TYPES = [BOOL_TYPE, INT_TYPE, STR_TYPE] | ||
561 | 37 | |||
562 | 38 | # List of valid values considered as boolean True. | ||
563 | 39 | VALID_TRUES = ['1', 'y', 'yes', 'Yes', 'True', 'true'] | ||
564 | 40 | |||
565 | 41 | # This is the start of the XML file. The DTD, if updated, needs to be updated | ||
566 | 42 | # also in the HACKING file. This one is taken from the HACKING file. | ||
567 | 43 | XML_START = '''<?xml version="1.0" encoding="UTF-8"?> | ||
568 | 44 | <!DOCTYPE loop [ | ||
569 | 45 | <!ELEMENT loop (description?,fields)> | ||
570 | 46 | <!ELEMENT description (#PCDATA)> | ||
571 | 47 | <!ELEMENT fields (field+)> | ||
572 | 48 | <!ELEMENT field (#PCDATA)> | ||
573 | 49 | <!ATTLIST field name CDATA #REQUIRED> | ||
574 | 50 | <!ATTLIST field type (text|int|bool) "text"> | ||
575 | 51 | ]>''' | ||
576 | 0 | 52 | ||
577 | === added file 'dashboard/lib/xml_to_dict.py' | |||
578 | --- dashboard/lib/xml_to_dict.py 1970-01-01 00:00:00 +0000 | |||
579 | +++ dashboard/lib/xml_to_dict.py 2012-09-06 08:33:21 +0000 | |||
580 | @@ -0,0 +1,230 @@ | |||
581 | 1 | # Copyright (C) 2012 Linaro | ||
582 | 2 | # | ||
583 | 3 | # This file is part of linaro-ci-dashboard. | ||
584 | 4 | # | ||
585 | 5 | # linaro-ci-dashboard is free software: you can redistribute it and/or modify | ||
586 | 6 | # it under the terms of the GNU Affero General Public License as published by | ||
587 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
588 | 8 | # (at your option) any later version. | ||
589 | 9 | # | ||
590 | 10 | # linaro-ci-dashboard is distributed in the hope that it will be useful, | ||
591 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
592 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
593 | 13 | # GNU Affero General Public License for more details. | ||
594 | 14 | # | ||
595 | 15 | # You should have received a copy of the GNU Affero General Public License | ||
596 | 16 | # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>. | ||
597 | 17 | # Django settings for dashboard project. | ||
598 | 18 | |||
599 | 19 | import xml.etree.ElementTree as ET | ||
600 | 20 | from dashboard.lib.xml_fields import * | ||
601 | 21 | |||
602 | 22 | |||
603 | 23 | class XmlToDict(object): | ||
604 | 24 | """ | ||
605 | 25 | Class to convert an XML string into a Python dictionary. | ||
606 | 26 | This applies to the XML as defined for the loop chaining. | ||
607 | 27 | |||
608 | 28 | Simple usage: | ||
609 | 29 | xml_2_dict = XmlToDict(xml_string).tree_to_dict() | ||
610 | 30 | """ | ||
611 | 31 | |||
612 | 32 | def __init__(self, xml): | ||
613 | 33 | """ | ||
614 | 34 | Initialize the XmlToDict class. | ||
615 | 35 | |||
616 | 36 | :param xml: The XML tree as a string. | ||
617 | 37 | :type xml str | ||
618 | 38 | """ | ||
619 | 39 | assert isinstance(xml, str) | ||
620 | 40 | |||
621 | 41 | super(XmlToDict, self).__init__() | ||
622 | 42 | self.tree = ET.fromstring(xml) | ||
623 | 43 | |||
624 | 44 | def get_root(self): | ||
625 | 45 | """ | ||
626 | 46 | Returns the name of the root of the XML tree. | ||
627 | 47 | :return The name of the root element. | ||
628 | 48 | """ | ||
629 | 49 | return self.tree.tag | ||
630 | 50 | |||
631 | 51 | def get_root_attributes(self): | ||
632 | 52 | """ | ||
633 | 53 | Returns a dictionary containing the attributes of the root element. | ||
634 | 54 | |||
635 | 55 | :return A dictionary with the attributes of the root element. | ||
636 | 56 | """ | ||
637 | 57 | root_attr = {} | ||
638 | 58 | for item in self.tree.items(): | ||
639 | 59 | root_attr[item[0]] = item[1] | ||
640 | 60 | return root_attr | ||
641 | 61 | |||
642 | 62 | def get_elements_from_root(self): | ||
643 | 63 | """ | ||
644 | 64 | Returns the list of elements of the XML tree starting from the root. | ||
645 | 65 | :return A list with Element instances. | ||
646 | 66 | """ | ||
647 | 67 | return list(self.tree) | ||
648 | 68 | |||
649 | 69 | def get_element_by_name(self, name): | ||
650 | 70 | """ | ||
651 | 71 | Returns an element from the XML tree by its name. | ||
652 | 72 | :param name: the name of the element to get. | ||
653 | 73 | :type name str | ||
654 | 74 | :return An Element instance, None if not found. | ||
655 | 75 | """ | ||
656 | 76 | return self.tree.find(name) | ||
657 | 77 | |||
658 | 78 | def element_to_dict(self, element): | ||
659 | 79 | """ | ||
660 | 80 | Returns a dictionary representation of the provided XML element. | ||
661 | 81 | |||
662 | 82 | :param element: The Element to convert. | ||
663 | 83 | :type element Element | ||
664 | 84 | :return The dictionary representation of element. | ||
665 | 85 | """ | ||
666 | 86 | assert isinstance(element, ET.Element) | ||
667 | 87 | |||
668 | 88 | element_dict = {} | ||
669 | 89 | element_tag = element.tag | ||
670 | 90 | element_text = element.text | ||
671 | 91 | |||
672 | 92 | if element_text: | ||
673 | 93 | element_text = self._check_element_text(element_text) | ||
674 | 94 | |||
675 | 95 | attributes = element.items() | ||
676 | 96 | children = list(element) | ||
677 | 97 | |||
678 | 98 | # Special case for XML 'field' tag. | ||
679 | 99 | # 'field' tag must have a value and at least 'name' attribute set. | ||
680 | 100 | if element_tag == FIELD_ELEMENT: | ||
681 | 101 | if attributes: | ||
682 | 102 | element_name = element.get(NAME_ATTR) | ||
683 | 103 | element_type = element.get(TYPE_ATTR) | ||
684 | 104 | # Convert the value into the specified type. | ||
685 | 105 | value = self.convert_to_type(element_type, element.text) | ||
686 | 106 | element_dict[element_name] = value | ||
687 | 107 | else: | ||
688 | 108 | if attributes: | ||
689 | 109 | # Other case, we store all attributes as key<>value, plus also | ||
690 | 110 | # the tag name and its value if OK. | ||
691 | 111 | # There might be name collisions if XML is not well done. | ||
692 | 112 | for attribute in attributes: | ||
693 | 113 | element_dict[attribute[0]] = attribute[1] | ||
694 | 114 | if element_text: | ||
695 | 115 | element_dict[element_tag] = element_text | ||
696 | 116 | elif element_text: | ||
697 | 117 | element_dict[element_tag] = element_text | ||
698 | 118 | if children: | ||
699 | 119 | for child in children: | ||
700 | 120 | element_dict.update(self.element_to_dict(child)) | ||
701 | 121 | return element_dict | ||
702 | 122 | |||
703 | 123 | def _check_element_text(self, text): | ||
704 | 124 | """ | ||
705 | 125 | Check that the value contained in an XML element is valid, meaning it | ||
706 | 126 | is not an empty space, a newline. In these cases, we return None since | ||
707 | 127 | we do not want to have that element in the final dictionary. | ||
708 | 128 | |||
709 | 129 | :param text: The value contained between opening and closing XML tag. | ||
710 | 130 | :type text str | ||
711 | 131 | :return None if text is empty spaces or newlines, text otherwise. | ||
712 | 132 | """ | ||
713 | 133 | # We do this since when we get nested elements, there might be 'valid' | ||
714 | 134 | # characters, like newlines or empty spaces, between the opening and | ||
715 | 135 | # closing tags, but we do not want to have such fields in the resulting | ||
716 | 136 | # dictionary. | ||
717 | 137 | import os | ||
718 | 138 | text = str(text).strip() | ||
719 | 139 | if text == os.linesep or len(text) == 0: | ||
720 | 140 | text = None | ||
721 | 141 | return text | ||
722 | 142 | |||
723 | 143 | def convert_to_type(self, type, value): | ||
724 | 144 | """ | ||
725 | 145 | Converts a value of an XML element into the provided type. If type is | ||
726 | 146 | None, tha value is converted into a string. | ||
727 | 147 | |||
728 | 148 | :param type: The type of the resulting value. | ||
729 | 149 | :type type str | ||
730 | 150 | :param value: The value associated with the XML element. | ||
731 | 151 | :return The value converted into the specified type. | ||
732 | 152 | """ | ||
733 | 153 | converted = str(value).strip() | ||
734 | 154 | if type == BOOL_TYPE: | ||
735 | 155 | if converted in VALID_TRUES: | ||
736 | 156 | converted = True | ||
737 | 157 | else: | ||
738 | 158 | converted = False | ||
739 | 159 | elif type == INT_TYPE: | ||
740 | 160 | converted = int(converted) | ||
741 | 161 | |||
742 | 162 | return converted | ||
743 | 163 | |||
744 | 164 | def tree_to_dict(self): | ||
745 | 165 | """ | ||
746 | 166 | Create a dictionary out of the XML tree. | ||
747 | 167 | |||
748 | 168 | :return A python dictionary of the XML tree. | ||
749 | 169 | """ | ||
750 | 170 | tree_dict = {} | ||
751 | 171 | |||
752 | 172 | root_dict = self.get_root_attributes() | ||
753 | 173 | elements_dict = {} | ||
754 | 174 | for element in self.get_elements_from_root(): | ||
755 | 175 | elements_dict.update(self.element_to_dict(element)) | ||
756 | 176 | |||
757 | 177 | if root_dict: | ||
758 | 178 | tree_dict.update(root_dict) | ||
759 | 179 | |||
760 | 180 | if elements_dict: | ||
761 | 181 | if tree_dict: | ||
762 | 182 | tree_dict.update(elements_dict) | ||
763 | 183 | else: | ||
764 | 184 | tree_dict = elements_dict | ||
765 | 185 | |||
766 | 186 | return tree_dict | ||
767 | 187 | |||
768 | 188 | |||
769 | 189 | class DictToXml(object): | ||
770 | 190 | """ | ||
771 | 191 | Class to convert a Python dictionary into an XML string . | ||
772 | 192 | |||
773 | 193 | Simple usage: | ||
774 | 194 | dict_2_xml = DictToXml(dictionary).dict_to_tree() | ||
775 | 195 | """ | ||
776 | 196 | |||
777 | 197 | def __init__(self, dictionary): | ||
778 | 198 | """ | ||
779 | 199 | Initialize the DictToXml class. | ||
780 | 200 | |||
781 | 201 | :param dictionary: The dictionary to convert. | ||
782 | 202 | :type dictionary dict | ||
783 | 203 | """ | ||
784 | 204 | assert isinstance(dictionary, dict) | ||
785 | 205 | super(DictToXml, self).__init__() | ||
786 | 206 | self.dictionary = dictionary | ||
787 | 207 | |||
788 | 208 | def dict_to_tree(self): | ||
789 | 209 | """ | ||
790 | 210 | Create an XML tree out of the provided dictionary. The XML structure | ||
791 | 211 | is defined in the HACKING file. | ||
792 | 212 | |||
793 | 213 | :return A string with the XML representation of the dictionary. | ||
794 | 214 | """ | ||
795 | 215 | dict_to_xml = XML_START | ||
796 | 216 | |||
797 | 217 | loop = ET.Element(LOOP_ELEMENT) | ||
798 | 218 | fields = ET.SubElement(loop, FIELDS_ELEMENT) | ||
799 | 219 | for key, value in self.dictionary.iteritems(): | ||
800 | 220 | # TODO need to find a way to reflect the type of the data. | ||
801 | 221 | attrib = {NAME_ATTR: key} | ||
802 | 222 | field = ET.SubElement(fields, FIELD_ELEMENT, attrib) | ||
803 | 223 | # ElementTree cannot serialize boolean types. | ||
804 | 224 | field.text = str(value) | ||
805 | 225 | |||
806 | 226 | # Dump the XML and add it to the valid start with its DTD. | ||
807 | 227 | xml_dump = ET.tostring(loop, encoding="utf-8") | ||
808 | 228 | dict_to_xml += xml_dump | ||
809 | 229 | |||
810 | 230 | return dict_to_xml |