Merge lp:~milo/linaro-ci-dashboard/xml-to-dict into lp:linaro-ci-dashboard

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
Reviewer Review Type Date Requested Status
Stevan Radaković Pending
Данило Шеган Pending
Linaro Infrastructure Pending
Review via email: mp+122916@code.launchpad.net

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
=== modified file 'dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html'
--- dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-08-31 15:21:11 +0000
+++ dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-09-06 08:33:21 +0000
@@ -6,14 +6,6 @@
6 <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post">6 <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post">
7 {% csrf_token %}7 {% csrf_token %}
8{% endblock create_form %}8{% endblock create_form %}
9
10{% if form.non_field_errors %}
11 <div class="form_error">
12 {% for err in form.non_field_errors %}
13 <div class="error_message">{{ err }}</div>
14 {% endfor %}
15 </div>
16{% endif %}
17{{ form.as_p }}9{{ form.as_p }}
18 <div><input type="submit" value="Submit" /></div>10 <div><input type="submit" value="Submit" /></div>
19 </form>11 </form>
2012
=== modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py'
--- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000
+++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-06 08:33:21 +0000
@@ -24,9 +24,9 @@
2424
25 A_NAME = 'a-build'25 A_NAME = 'a-build'
26 B_NAME = 'b-build'26 B_NAME = 'b-build'
27 VALID_VALUES = 'a=2\nb=3'27 VALID_VALUES = u'a=2\nb=3'
28 VALID_LINES = ['a=2', 'b=3']28 VALID_LINES = ['a=2', 'b=3']
29 NON_VALID_VALUES = 'a:2\nb=3'29 NON_VALID_VALUES = u'a:2\nb=3'
30 NON_VALID_LINES = ['a:2', 'b=3']30 NON_VALID_LINES = ['a:2', 'b=3']
31 VALID_DICT = {'a': '2', 'b': '3'}31 VALID_DICT = {'a': '2', 'b': '3'}
3232
@@ -52,6 +52,7 @@
52 def test_valid_values_wrong(self):52 def test_valid_values_wrong(self):
53 self.assertEqual((False, self.NON_VALID_LINES),53 self.assertEqual((False, self.NON_VALID_LINES),
54 AndroidTextFieldLoop.valid_values(54 AndroidTextFieldLoop.valid_values(
55<<<<<<< TREE
55 self.NON_VALID_VALUES))56 self.NON_VALID_VALUES))
5657
57 def test_schedule_build(self):58 def test_schedule_build(self):
@@ -62,3 +63,27 @@
62 def test_schedule_build_invalid(self):63 def test_schedule_build_invalid(self):
63 build = self.non_valid_android_loop.schedule_build()64 build = self.non_valid_android_loop.schedule_build()
64 self.assertEqual(build.result_xml, {})65 self.assertEqual(build.result_xml, {})
66=======
67 self.NON_VALID_VALUES))
68
69 def test_schedule_build(self):
70 build = self.android_loop.schedule_build()
71 expected_out = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ' \
72 'loop [\n <!ELEMENT loop (description?,fields)>\n' \
73 ' <!ELEMENT description (#PCDATA)>\n ' \
74 '<!ELEMENT fields (field+)>\n <!ELEMENT field ' \
75 '(#PCDATA)>\n <!ATTLIST field name CDATA ' \
76 '#REQUIRED>\n <!ATTLIST field type (text|int|bool) ' \
77 '"text">\n]><loop><fields><field name="a">2</field>' \
78 '<field name="b">3</field><field name="name">a-build' \
79 '</field><field name="is_restricted">False</field>' \
80 '<field name="is_official">False</field>' \
81 '<field name="type">AndroidTextFieldLoop</field>' \
82 '</fields></loop>'
83 self.assertEqual(expected_out, build.result_xml)
84 self.assertEqual(build.status, "success")
85
86 def test_schedule_build_invalid(self):
87 build = self.non_valid_android_loop.schedule_build()
88 self.assertEqual("", build.result_xml)
89>>>>>>> MERGE-SOURCE
6590
=== modified file 'dashboard/frontend/models/loop_build.py'
--- dashboard/frontend/models/loop_build.py 2012-09-05 13:23:05 +0000
+++ dashboard/frontend/models/loop_build.py 2012-09-06 08:33:21 +0000
@@ -18,6 +18,7 @@
1818
19from django.db import models19from django.db import models
20from frontend.models.loop import Loop20from frontend.models.loop import Loop
21from dashboard.lib.xml_to_dict import XmlToDict
2122
2223
23class LoopBuild(models.Model):24class LoopBuild(models.Model):
@@ -47,3 +48,15 @@
47 else:48 else:
48 self.build_number = 149 self.build_number = 1
49 super(LoopBuild, self).save(*args, **kwargs)50 super(LoopBuild, self).save(*args, **kwargs)
51
52 def get_build_result(self):
53 """
54 Returns a Python dictionary representation of the build XML stored.
55
56 :return A dictionary of the XML result.
57 """
58 dict_result = {}
59 if self.result_xml:
60 xml_to_dict = XmlToDict(self.result_xml)
61 dict_result = xml_to_dict.tree_to_dict()
62 return dict_result
5063
=== modified file 'dashboard/frontend/models/textfield_loop.py'
--- dashboard/frontend/models/textfield_loop.py 2012-09-05 13:59:57 +0000
+++ dashboard/frontend/models/textfield_loop.py 2012-09-06 08:33:21 +0000
@@ -17,6 +17,7 @@
1717
18from django.db import models18from django.db import models
19from frontend.models.loop import Loop19from frontend.models.loop import Loop
20from dashboard.lib.xml_to_dict import DictToXml
2021
2122
22# Default delimiter to separate values from keys in the text field.23# Default delimiter to separate values from keys in the text field.
@@ -40,6 +41,7 @@
40 values = models.TextField()41 values = models.TextField()
4142
42 def schedule_build(self, parameters=None):43 def schedule_build(self, parameters=None):
44<<<<<<< TREE
43 from frontend.models.loop_build import LoopBuild45 from frontend.models.loop_build import LoopBuild
44 build = LoopBuild()46 build = LoopBuild()
45 build.loop = self47 build.loop = self
@@ -55,14 +57,40 @@
55 return build57 return build
5658
57 def values_to_dict(self):59 def values_to_dict(self):
60=======
61 from frontend.models.loop_build import LoopBuild
62 build = LoopBuild()
63 build.loop = self
64 build.duration = 0.00
65
66 try:
67 build.result_xml = self.dict_to_xml()
68 build.status = 'success'
69 except:
70 build.status = 'failure'
71
72 build.save()
73 return build
74
75 def values_to_dict(self, valid=None, lines=None):
76>>>>>>> MERGE-SOURCE
58 """77 """
59 Returns a dictionary representation of the values inserted. The78 Returns a dictionary representation of the values inserted. The
60 key<>value pairs are split on DEFAULT_DELIMITER.79 key<>value pairs are split on DEFAULT_DELIMITER.
6180
62 :return A dictionary of the values inserted.81 The default parameters are used for a second iteration of this function
82 if we do not want to parse again the lines, but we only want to get the
83 dictionary.
84
85 :param valid: If the lines are valid or not.
86 :type valid bool
87 :param lines: The list of lines in the text field.
88 :type lines list
89 :return A dictionary of the values inserted in the text field.
63 """90 """
64 text_to_dict = {}91 text_to_dict = {}
65 valid, lines = self.valid_values(self.values)92 if valid is None and lines is None:
93 valid, lines = self.valid_values(self.values)
6694
67 if valid:95 if valid:
68 for line in lines:96 for line in lines:
@@ -71,6 +99,19 @@
7199
72 return text_to_dict100 return text_to_dict
73101
102 def _all_values_to_dict(self):
103 """
104 Returns a dict representation of the necessary fields for the loops.
105
106 :return A Python dictionary with the necessary fields.
107 """
108 # TODO need a better way to serialize the model
109 all_values_dict = {'is_official': self.is_official,
110 'is_restricted': self.is_restricted,
111 'name': self.name,
112 'type': self.type}
113 return all_values_dict
114
74 @staticmethod115 @staticmethod
75 def valid_values(values):116 def valid_values(values):
76 """117 """
@@ -79,7 +120,7 @@
79120
80 :param values: the string with all the key<>value pairs, separated with121 :param values: the string with all the key<>value pairs, separated with
81 a newline character.122 a newline character.
82 :type str123 :type values unicode
83 :return a boolean for the validity, and the list of lines.124 :return a boolean for the validity, and the list of lines.
84 """125 """
85 valid = True126 valid = True
@@ -91,3 +132,18 @@
91 valid = False132 valid = False
92 break133 break
93 return valid, lines134 return valid, lines
135
136 def dict_to_xml(self):
137 """
138 Converts the necessary values into an XML tree.
139
140 :return The XML tree as a string, or an empty string if the inserted
141 values are not valid.
142 """
143 xml_string = ""
144 valid, lines = self.valid_values(self.values)
145 if valid:
146 values = self.values_to_dict(valid, lines)
147 values.update(self._all_values_to_dict())
148 xml_string = DictToXml(values).dict_to_tree()
149 return xml_string
94150
=== modified file 'dashboard/frontend/tests/__init__.py'
--- dashboard/frontend/tests/__init__.py 2012-09-03 08:18:10 +0000
+++ dashboard/frontend/tests/__init__.py 2012-09-06 08:33:21 +0000
@@ -2,6 +2,7 @@
2from dashboard.frontend.tests.test_models import *2from dashboard.frontend.tests.test_models import *
3from dashboard.frontend.tests.test_clientresponse import *3from dashboard.frontend.tests.test_clientresponse import *
4from dashboard.frontend.tests.test_custom_commands import *4from dashboard.frontend.tests.test_custom_commands import *
5from dashboard.frontend.tests.test_xml_to_dict import *
56
67
7#starts the test suite8#starts the test suite
@@ -14,4 +15,6 @@
14 'LoopTests': LoopTest,15 'LoopTests': LoopTest,
15 'ClientResponseTests': ClientResponseTests,16 'ClientResponseTests': ClientResponseTests,
16 'JenkinsCommandTest': JenkinsCommandTest,17 'JenkinsCommandTest': JenkinsCommandTest,
18 'XmlToDictTest': XmlToDictTest,
19 'DictToXmlTest': DictToXmlTest,
17 }20 }
1821
=== added file 'dashboard/frontend/tests/test_xml_to_dict.py'
--- dashboard/frontend/tests/test_xml_to_dict.py 1970-01-01 00:00:00 +0000
+++ dashboard/frontend/tests/test_xml_to_dict.py 2012-09-06 08:33:21 +0000
@@ -0,0 +1,286 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17
18from django.test import TestCase
19import os
20from dashboard.lib.xml_to_dict import (
21 XmlToDict,
22 DictToXml
23)
24from dashboard.lib.xml_fields import (
25 LOOP_ELEMENT,
26 FIELDS_ELEMENT,
27 TYPE_ATTR,
28 NAME_ATTR,
29 XML_START,
30 INT_TYPE,
31 BOOL_TYPE,
32)
33
34
35# XML tags
36ROOT_TAG_OPEN = '<loop>'
37ROOT_TAG_CLOSE = '</loop>'
38FIELDS_TAG_OPEN = '<fields>'
39FIELDS_TAG_CLOSE = '</fields>'
40VALID_FIELD = '<field name="%(field_name)s">%(field_value)s</field>'
41VALID_FIELD_WITH_TYPE = ('<field name="%(field_name)s" '
42 'type="%(field_type)s">%(field_value)s</field>')
43
44# Field names, types and values used as defaults for the tests.
45LOOP_TYPE_VAL = 'text-build'
46LOOP_NAME_VAL = 'build-test'
47BUILD_TYPE = 'build_type'
48BUILD_TYPE_VAL = 'build-android'
49REPO_QUIET = 'repo_quiet'
50REPO_QUIET_VAL = True
51BUILD_FS_IMAGE = 'build_fs_image'
52BUILD_FS_IMAGE_VAL = False
53MAKE_JOBS = 'make_jobs'
54MAKE_JOBS_VAL = 2
55
56NEWLINE = os.linesep
57# Used to create the needed dictionary for fields creation.
58# The dictionary has to be:
59# {FIELD_NAME: value, FIELD_VALUE: value [, FIELD_TYPE: value]}
60# FIELD_TYPE is optional.
61FIELD_NAME = 'field_name'
62FIELD_VALUE = 'field_value'
63FIELD_TYPE = 'field_type'
64
65FIELDS = [
66 {FIELD_NAME: BUILD_TYPE, FIELD_VALUE: BUILD_TYPE_VAL},
67 {FIELD_NAME: REPO_QUIET, FIELD_TYPE: BOOL_TYPE,
68 FIELD_VALUE: REPO_QUIET_VAL},
69 {FIELD_NAME: BUILD_FS_IMAGE, FIELD_TYPE: BOOL_TYPE,
70 FIELD_VALUE: BUILD_FS_IMAGE_VAL},
71 {FIELD_NAME: MAKE_JOBS, FIELD_TYPE: INT_TYPE,
72 FIELD_VALUE: MAKE_JOBS_VAL},
73 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL},
74 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL}
75]
76
77VALID_START = XML_START + ROOT_TAG_OPEN
78VALID_CLOSE = ROOT_TAG_CLOSE
79
80
81def create_xml_fields(fields_list):
82 """
83 Support functions to create the <fields><field>... hierarchy.
84
85 :param fields_list: the list of fields to create. Each field has to be
86 a dictionary with FIELD_NAME, FIELD_VALUE and optional FIELD_TYPE keys.
87 :type fields_list list
88 """
89 assert isinstance(fields_list, list)
90 fields_xml = FIELDS_TAG_OPEN
91
92 for field in fields_list:
93 if field.get(FIELD_TYPE, None) is not None:
94 fields_xml += VALID_FIELD_WITH_TYPE % field
95 else:
96 fields_xml += VALID_FIELD % field
97 fields_xml += FIELDS_TAG_CLOSE
98 return fields_xml
99
100
101class XmlToDictTest(TestCase):
102 """
103 Test class for the conversion from XML to Python dictionary.
104 The XML specification is taken from the HACKING file.
105 """
106
107 def setUp(self):
108 super(XmlToDictTest, self).setUp()
109 self.xml_string = VALID_START
110 self.xml_string += create_xml_fields(FIELDS)
111 self.xml_string += VALID_CLOSE
112 self.xml_to_dict = XmlToDict(self.xml_string)
113
114 def test_get_element_from_root(self):
115 """
116 Tests retrieval of the elements in the XML tree, but the root.
117 """
118 self.assertEqual(self.xml_to_dict.get_elements_from_root()[0].tag,
119 FIELDS_ELEMENT)
120
121 def test_get_element_by_name_correct(self):
122 """Tests retrieving of an element with a correct name."""
123 self.assertEqual(FIELDS_ELEMENT,
124 self.xml_to_dict.get_element_by_name(
125 FIELDS_ELEMENT).tag)
126
127 def test_get_element_by_name_wrong(self):
128 """ Tests retrieving of an element with a wrong name."""
129 self.assertEqual(None,
130 self.xml_to_dict.get_element_by_name('wrong'))
131
132 def test_elements_to_dict(self):
133 """
134 Tests creation of a dictionary with multiple elements.
135 """
136 expected_output = {MAKE_JOBS: MAKE_JOBS_VAL,
137 BUILD_TYPE: BUILD_TYPE_VAL,
138 BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL,
139 REPO_QUIET: REPO_QUIET_VAL,
140 NAME_ATTR: LOOP_NAME_VAL,
141 TYPE_ATTR: LOOP_TYPE_VAL}
142 element = self.xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
143 self.assertEqual(expected_output,
144 self.xml_to_dict.element_to_dict(element))
145
146 def test_bool_true_conversion(self):
147 """
148 Tests the True bool conversion.
149 """
150 expected_out = {'a-field': True}
151 for value in ['1', 1, 'y', 'Yes', True, 'True']:
152 local_xml = VALID_START
153 fields_list = [{FIELD_NAME: 'a-field',
154 FIELD_TYPE: BOOL_TYPE,
155 FIELD_VALUE: value}]
156 local_xml += create_xml_fields(fields_list)
157 local_xml += VALID_CLOSE
158 local_xml_to_dict = XmlToDict(local_xml)
159 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
160 self.assertEqual(expected_out,
161 local_xml_to_dict.element_to_dict(element))
162
163 def test_bool_false_conversion(self):
164 """
165 Tests the False bool conversion.
166 """
167 expected_out = {'a-field': False}
168 for value in ['n', 0, 42, 'Yep', 'No', False, 'False', '?']:
169 local_xml = VALID_START
170 fields_list = [{FIELD_NAME: 'a-field',
171 FIELD_TYPE: BOOL_TYPE,
172 FIELD_VALUE: value}]
173 local_xml += create_xml_fields(fields_list)
174 local_xml += VALID_CLOSE
175 local_xml_to_dict = XmlToDict(local_xml)
176 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
177 self.assertEqual(expected_out,
178 local_xml_to_dict.element_to_dict(element))
179
180 def test_int_conversion(self):
181 """
182 Tests the int conversion.
183 """
184 expected_out = {'a-field': 42}
185 for value in [42, '42', '42 ', ' 42']:
186 local_xml = VALID_START
187 fields_list = [{FIELD_NAME: 'a-field',
188 FIELD_TYPE: INT_TYPE,
189 FIELD_VALUE: value}]
190 local_xml += create_xml_fields(fields_list)
191 local_xml += VALID_CLOSE
192 local_xml_to_dict = XmlToDict(local_xml)
193 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
194 self.assertEqual(expected_out,
195 local_xml_to_dict.element_to_dict(element))
196
197 def test_str_conversion(self):
198 """
199 Tests the str conversion.
200 """
201 expected_out = {'a-field': '42'}
202 for value in [42, '42', '42 ', ' 42']:
203 local_xml = VALID_START
204 fields_list = [{FIELD_NAME: 'a-field',
205 FIELD_VALUE: value}]
206 local_xml += create_xml_fields(fields_list)
207 local_xml += VALID_CLOSE
208 local_xml_to_dict = XmlToDict(local_xml)
209 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
210 self.assertEqual(expected_out,
211 local_xml_to_dict.element_to_dict(element))
212
213 def test_tree_to_dict(self):
214 """
215 Tests the conversion of a dictionary into a full XML tree.
216 """
217 expected_output = {
218 REPO_QUIET: REPO_QUIET_VAL,
219 NAME_ATTR: LOOP_NAME_VAL,
220 MAKE_JOBS: MAKE_JOBS_VAL,
221 TYPE_ATTR: LOOP_TYPE_VAL,
222 BUILD_TYPE: BUILD_TYPE_VAL,
223 BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL
224 }
225 self.assertEqual(expected_output, self.xml_to_dict.tree_to_dict())
226
227 def test_get_root_name(self):
228 """
229 Tests retrieving of the root element.
230 """
231 self.assertEqual(self.xml_to_dict.get_root(), LOOP_ELEMENT)
232
233 def test_get_root_attributes(self):
234 """
235 Tests retrieving of the root attributes.
236 """
237 self.assertEqual({}, self.xml_to_dict.get_root_attributes())
238
239
240class DictToXmlTest(TestCase):
241 """
242 Test class for the conversion from Python dictionary into XML.
243 The XML specification is taken from the HACKING file.
244 """
245
246 FIELDS_LIST = [
247 {FIELD_NAME: MAKE_JOBS, FIELD_VALUE: MAKE_JOBS_VAL},
248 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL},
249 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL},
250 ]
251
252 VALID_DICT = {
253 NAME_ATTR: LOOP_NAME_VAL, TYPE_ATTR: LOOP_TYPE_VAL,
254 MAKE_JOBS: str(MAKE_JOBS_VAL)
255 }
256
257 def setUp(self):
258 super(DictToXmlTest, self).setUp()
259 self.valid_xml = VALID_START
260 self.valid_xml += create_xml_fields(self.FIELDS_LIST)
261 self.valid_xml += VALID_CLOSE
262 self.dict_to_xml = DictToXml(self.VALID_DICT)
263
264 def test_dict_to_tree(self):
265 """
266 Tests dictionary to XML conversion.
267 """
268 self.assertEqual(self.valid_xml, self.dict_to_xml.dict_to_tree())
269
270 def test_dict_to_tree_with_bool(self):
271 """
272 Tests dictionary to XML conversion, with a bool inside, ElementTree
273 does not cope very well with bool type.
274 """
275 fields = [
276 {FIELD_NAME: REPO_QUIET, FIELD_VALUE: REPO_QUIET_VAL},
277 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL},
278 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL},
279 ]
280 dictionary = {TYPE_ATTR: LOOP_TYPE_VAL, NAME_ATTR: LOOP_NAME_VAL,
281 REPO_QUIET: REPO_QUIET_VAL}
282 dict_2_xml = DictToXml(dictionary)
283 xml_string = VALID_START
284 xml_string += create_xml_fields(fields)
285 xml_string += VALID_CLOSE
286 self.assertEqual(xml_string, dict_2_xml.dict_to_tree())
0287
=== added file 'dashboard/lib/xml_fields.py'
--- dashboard/lib/xml_fields.py 1970-01-01 00:00:00 +0000
+++ dashboard/lib/xml_fields.py 2012-09-06 08:33:21 +0000
@@ -0,0 +1,51 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17# Django settings for dashboard project.
18
19# The file contains all the necessary elements, tags and everything else
20# related to the XML representation of a chain-build.
21
22# All the XML valid elements.
23FIELD_ELEMENT = 'field'
24FIELDS_ELEMENT = 'fields'
25LOOP_ELEMENT = 'loop'
26DESCRIPTION_ELEMENT = 'description'
27
28# All the XML attributes.
29NAME_ATTR = 'name'
30TYPE_ATTR = 'type'
31
32# The valid types for element values.
33BOOL_TYPE = 'bool'
34INT_TYPE = 'int'
35STR_TYPE = 'text'
36VALID_TYPES = [BOOL_TYPE, INT_TYPE, STR_TYPE]
37
38# List of valid values considered as boolean True.
39VALID_TRUES = ['1', 'y', 'yes', 'Yes', 'True', 'true']
40
41# This is the start of the XML file. The DTD, if updated, needs to be updated
42# also in the HACKING file. This one is taken from the HACKING file.
43XML_START = '''<?xml version="1.0" encoding="UTF-8"?>
44<!DOCTYPE loop [
45 <!ELEMENT loop (description?,fields)>
46 <!ELEMENT description (#PCDATA)>
47 <!ELEMENT fields (field+)>
48 <!ELEMENT field (#PCDATA)>
49 <!ATTLIST field name CDATA #REQUIRED>
50 <!ATTLIST field type (text|int|bool) "text">
51]>'''
052
=== added file 'dashboard/lib/xml_to_dict.py'
--- dashboard/lib/xml_to_dict.py 1970-01-01 00:00:00 +0000
+++ dashboard/lib/xml_to_dict.py 2012-09-06 08:33:21 +0000
@@ -0,0 +1,230 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17# Django settings for dashboard project.
18
19import xml.etree.ElementTree as ET
20from dashboard.lib.xml_fields import *
21
22
23class XmlToDict(object):
24 """
25 Class to convert an XML string into a Python dictionary.
26 This applies to the XML as defined for the loop chaining.
27
28 Simple usage:
29 xml_2_dict = XmlToDict(xml_string).tree_to_dict()
30 """
31
32 def __init__(self, xml):
33 """
34 Initialize the XmlToDict class.
35
36 :param xml: The XML tree as a string.
37 :type xml str
38 """
39 assert isinstance(xml, str)
40
41 super(XmlToDict, self).__init__()
42 self.tree = ET.fromstring(xml)
43
44 def get_root(self):
45 """
46 Returns the name of the root of the XML tree.
47 :return The name of the root element.
48 """
49 return self.tree.tag
50
51 def get_root_attributes(self):
52 """
53 Returns a dictionary containing the attributes of the root element.
54
55 :return A dictionary with the attributes of the root element.
56 """
57 root_attr = {}
58 for item in self.tree.items():
59 root_attr[item[0]] = item[1]
60 return root_attr
61
62 def get_elements_from_root(self):
63 """
64 Returns the list of elements of the XML tree starting from the root.
65 :return A list with Element instances.
66 """
67 return list(self.tree)
68
69 def get_element_by_name(self, name):
70 """
71 Returns an element from the XML tree by its name.
72 :param name: the name of the element to get.
73 :type name str
74 :return An Element instance, None if not found.
75 """
76 return self.tree.find(name)
77
78 def element_to_dict(self, element):
79 """
80 Returns a dictionary representation of the provided XML element.
81
82 :param element: The Element to convert.
83 :type element Element
84 :return The dictionary representation of element.
85 """
86 assert isinstance(element, ET.Element)
87
88 element_dict = {}
89 element_tag = element.tag
90 element_text = element.text
91
92 if element_text:
93 element_text = self._check_element_text(element_text)
94
95 attributes = element.items()
96 children = list(element)
97
98 # Special case for XML 'field' tag.
99 # 'field' tag must have a value and at least 'name' attribute set.
100 if element_tag == FIELD_ELEMENT:
101 if attributes:
102 element_name = element.get(NAME_ATTR)
103 element_type = element.get(TYPE_ATTR)
104 # Convert the value into the specified type.
105 value = self.convert_to_type(element_type, element.text)
106 element_dict[element_name] = value
107 else:
108 if attributes:
109 # Other case, we store all attributes as key<>value, plus also
110 # the tag name and its value if OK.
111 # There might be name collisions if XML is not well done.
112 for attribute in attributes:
113 element_dict[attribute[0]] = attribute[1]
114 if element_text:
115 element_dict[element_tag] = element_text
116 elif element_text:
117 element_dict[element_tag] = element_text
118 if children:
119 for child in children:
120 element_dict.update(self.element_to_dict(child))
121 return element_dict
122
123 def _check_element_text(self, text):
124 """
125 Check that the value contained in an XML element is valid, meaning it
126 is not an empty space, a newline. In these cases, we return None since
127 we do not want to have that element in the final dictionary.
128
129 :param text: The value contained between opening and closing XML tag.
130 :type text str
131 :return None if text is empty spaces or newlines, text otherwise.
132 """
133 # We do this since when we get nested elements, there might be 'valid'
134 # characters, like newlines or empty spaces, between the opening and
135 # closing tags, but we do not want to have such fields in the resulting
136 # dictionary.
137 import os
138 text = str(text).strip()
139 if text == os.linesep or len(text) == 0:
140 text = None
141 return text
142
143 def convert_to_type(self, type, value):
144 """
145 Converts a value of an XML element into the provided type. If type is
146 None, tha value is converted into a string.
147
148 :param type: The type of the resulting value.
149 :type type str
150 :param value: The value associated with the XML element.
151 :return The value converted into the specified type.
152 """
153 converted = str(value).strip()
154 if type == BOOL_TYPE:
155 if converted in VALID_TRUES:
156 converted = True
157 else:
158 converted = False
159 elif type == INT_TYPE:
160 converted = int(converted)
161
162 return converted
163
164 def tree_to_dict(self):
165 """
166 Create a dictionary out of the XML tree.
167
168 :return A python dictionary of the XML tree.
169 """
170 tree_dict = {}
171
172 root_dict = self.get_root_attributes()
173 elements_dict = {}
174 for element in self.get_elements_from_root():
175 elements_dict.update(self.element_to_dict(element))
176
177 if root_dict:
178 tree_dict.update(root_dict)
179
180 if elements_dict:
181 if tree_dict:
182 tree_dict.update(elements_dict)
183 else:
184 tree_dict = elements_dict
185
186 return tree_dict
187
188
189class DictToXml(object):
190 """
191 Class to convert a Python dictionary into an XML string .
192
193 Simple usage:
194 dict_2_xml = DictToXml(dictionary).dict_to_tree()
195 """
196
197 def __init__(self, dictionary):
198 """
199 Initialize the DictToXml class.
200
201 :param dictionary: The dictionary to convert.
202 :type dictionary dict
203 """
204 assert isinstance(dictionary, dict)
205 super(DictToXml, self).__init__()
206 self.dictionary = dictionary
207
208 def dict_to_tree(self):
209 """
210 Create an XML tree out of the provided dictionary. The XML structure
211 is defined in the HACKING file.
212
213 :return A string with the XML representation of the dictionary.
214 """
215 dict_to_xml = XML_START
216
217 loop = ET.Element(LOOP_ELEMENT)
218 fields = ET.SubElement(loop, FIELDS_ELEMENT)
219 for key, value in self.dictionary.iteritems():
220 # TODO need to find a way to reflect the type of the data.
221 attrib = {NAME_ATTR: key}
222 field = ET.SubElement(fields, FIELD_ELEMENT, attrib)
223 # ElementTree cannot serialize boolean types.
224 field.text = str(value)
225
226 # Dump the XML and add it to the valid start with its DTD.
227 xml_dump = ET.tostring(loop, encoding="utf-8")
228 dict_to_xml += xml_dump
229
230 return dict_to_xml

Subscribers

People subscribed via source and target branches