chart_xml.py 18.6 KB
Newer Older
1
# -*- coding: utf-8 -*-
VIGNET Pierre's avatar
VIGNET Pierre committed
2
3
4
## Filename    : chart_xml.py
## Author(s)   : Geoffroy Andrieux
## Created     : 04/2010
5
6
## Revision    :
## Source      :
VIGNET Pierre's avatar
VIGNET Pierre committed
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
##
## Copyright 2010 : IRISA
##
## This library is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published
## by the Free Software Foundation; either version 2.1 of the License, or
## any later version.
##
## This library is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF
## MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  The software and
## documentation provided here under is on an "as is" basis, and IRISA has
## no obligations to provide maintenance, support, updates, enhancements
## or modifications.
## In no event shall IRISA be liable to any party for direct, indirect,
## special, incidental or consequential damages, including lost profits,
## arising out of the use of this software and its documentation, even if
## IRISA have been advised of the possibility of such damage.  See
## the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this library; if not, write to the Free Software Foundation,
## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
##
## The original code contained here was initially developed by:
##
##     Geoffroy Andrieux.
##     IRISA/IRSET
##     Symbiose team
##     IRISA  Campus de Beaulieu
37
38
##     35042 RENNES Cedex, FRANCE
##
VIGNET Pierre's avatar
VIGNET Pierre committed
39
40
41
##
## Contributor(s): Michel Le Borgne, Nolwenn Le Meur
##
VIGNET Pierre's avatar
VIGNET Pierre committed
42
43
44
45
46
47
48
49
50
"""Load and generate Cadbiom xml files

Classes::

    - :class:`XmlVisitor`: Visitor used to generate xml cadbiom code when the
    model is exported.
    - :class:`MakeHandler`: Make a handler for the parser when the model is loaded.
    - :class:`MakeModelFromXmlFile`: Handy class used to parse an xml file.
    - :class:`MakeModelFromXmlString`: Handy class used to parse an xml string.
VIGNET Pierre's avatar
VIGNET Pierre committed
51
"""
52
53
from __future__ import unicode_literals
from __future__ import print_function
VIGNET Pierre's avatar
VIGNET Pierre committed
54
55

from xml.sax import make_parser
56
from xml.sax import parseString
57
from xml.sax.handler import ContentHandler
VIGNET Pierre's avatar
VIGNET Pierre committed
58
59
60
from lxml import etree
from lxml import objectify

61
62
63
64
65
from cadbiom.models.guard_transitions.chart_model import ChartModel
import cadbiom.commons as cm

LOGGER = cm.logger()

VIGNET Pierre's avatar
VIGNET Pierre committed
66
67

class XmlVisitor:
68
    """Visitor used to generate xml cadbiom code when the model is exported."""
69

VIGNET Pierre's avatar
VIGNET Pierre committed
70
71
72
    def __init__(self, model):
        self.model = model
        self.fact_list = []
VIGNET Pierre's avatar
VIGNET Pierre committed
73
74
        self.xml = ""  # string: xml representation of model
        self.symb = set()  # symbol set used to check double naming of nodes
VIGNET Pierre's avatar
VIGNET Pierre committed
75
76
77
78
79
80
81
        self.visit_chart_model()

    def visit_chart_model(self):
        """
        Entrance point
        """
        self.visit_ctop_node(self.model.get_root())
82

VIGNET Pierre's avatar
VIGNET Pierre committed
83
84
85
86
    def check_name(self, name):
        """
        Detect double declarations
        """
87
88
89
90
        if name in self.symb:
            raise Exception("XML Parsing: Node double declaration")
        else:
            self.symb.add(name)
91

VIGNET Pierre's avatar
VIGNET Pierre committed
92
93
94
95
    def visit_cstart_node(self, snode):
        """
        Generate xml representation of a start node
        """
96
97

        tag = "CStartNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
98
        attrname = ["name", "xloc", "yloc"]
99

VIGNET Pierre's avatar
VIGNET Pierre committed
100
101
        attr = [snode.name, snode.xloc, snode.yloc]
        return [tag, attrname, attr]
102

VIGNET Pierre's avatar
VIGNET Pierre committed
103
104
105
106
    def visit_ctrap_node(self, tnode):
        """
        Generate xml representation of a trap node
        """
107
        tag = "CTrapNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
108
109
110
        attrname = ["name", "xloc", "yloc"]
        attr = [tnode.name, tnode.xloc, tnode.yloc]
        return [tag, attrname, attr]
111

VIGNET Pierre's avatar
VIGNET Pierre committed
112
113
114
115
116
    def visit_csimple_node(self, sin):
        """
        Generate xml representation of a simple node
        """
        self.check_name(sin.name)
117
        tag = "CSimpleNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
118
        attrname = ["name", "xloc", "yloc"]
119
        attr = [sin.name, sin.xloc, sin.yloc]
VIGNET Pierre's avatar
VIGNET Pierre committed
120
        return [tag, attrname, attr]
121

VIGNET Pierre's avatar
VIGNET Pierre committed
122
123
124
125
126
    def visit_cperm_node(self, pnode):
        """
        Generate xml representation of a perm node
        """
        self.check_name(pnode.name)
127
        tag = "CPermNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
128
129
        attrname = ["name", "xloc", "yloc"]
        attr = [pnode.name, pnode.xloc, pnode.yloc]
130
131
        return [tag, attrname, attr]

VIGNET Pierre's avatar
VIGNET Pierre committed
132
133
134
    def visit_cinput_node(self, inn):
        """
        Generate xml representation of an input node
135
        """
VIGNET Pierre's avatar
VIGNET Pierre committed
136
        # double declaration of input nodes is allowed"
137
        tag = "CInputNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
138
139
        attrname = ["name", "xloc", "yloc"]
        attr = [inn.name, inn.xloc, inn.yloc]
140
141
        return [tag, attrname, attr]

VIGNET Pierre's avatar
VIGNET Pierre committed
142
143
144
    def visit_cmacro_node(self, mnode):
        """
        Generate xml representation of a macro node
145
        """
VIGNET Pierre's avatar
VIGNET Pierre committed
146
147
        self.check_name(mnode.name)
        save_macro = self.current_element
148
        tag = "CMacroNode"
VIGNET Pierre's avatar
VIGNET Pierre committed
149
        attrname = ["name", "xloc", "yloc", "wloc", "hloc"]
150
        attr = [mnode.name, mnode.xloc, mnode.yloc, mnode.wloc, mnode.hloc]
VIGNET Pierre's avatar
VIGNET Pierre committed
151
        properties = [tag, attrname, attr]
152

VIGNET Pierre's avatar
VIGNET Pierre committed
153
154
        macro = etree.SubElement(self.current_element, properties[0])
        self.current_element = macro
155
        if len(properties) > 1:
VIGNET Pierre's avatar
VIGNET Pierre committed
156
157
158
159
            attrname = properties[1]
            attr = properties[2]
            attributes = macro.attrib
            for i in range(0, len(attrname)):
VIGNET Pierre's avatar
VIGNET Pierre committed
160
                attributes[attrname[i]] = str(attr[i])
161

VIGNET Pierre's avatar
VIGNET Pierre committed
162
163
164
        # nodes
        for snode in mnode.sub_nodes:
            properties = snode.accept(self)
VIGNET Pierre's avatar
VIGNET Pierre committed
165
            if properties[0] == "CMacroNode":
VIGNET Pierre's avatar
VIGNET Pierre committed
166
                self.current_element = macro
167

VIGNET Pierre's avatar
VIGNET Pierre committed
168
            if properties[0] != "CMacroNode":
VIGNET Pierre's avatar
VIGNET Pierre committed
169
                subel = etree.SubElement(self.current_element, properties[0])
170
                if len(properties) > 1:
VIGNET Pierre's avatar
VIGNET Pierre committed
171
172
173
174
                    attrname = properties[1]
                    attr = properties[2]
                    attributes = subel.attrib
                    for i in range(0, len(attrname)):
VIGNET Pierre's avatar
VIGNET Pierre committed
175
                        attributes[attrname[i]] = str(attr[i])
VIGNET Pierre's avatar
VIGNET Pierre committed
176
177
178
179
180
181

        # transitions
        for gtr in mnode.transitions:
            for trunlist in gtr:
                properties = trunlist.accept(self)
                sub_tr = etree.SubElement(self.current_element, properties[0])
182
                if len(properties) > 1:
VIGNET Pierre's avatar
VIGNET Pierre committed
183
184
185
186
                    attrname = properties[1]
                    attr = properties[2]
                    attributes = sub_tr.attrib
                    for i in range(0, len(attrname)):
VIGNET Pierre's avatar
VIGNET Pierre committed
187
                        attributes[attrname[i]] = str(attr[i])
188
        self.current_element = save_macro
VIGNET Pierre's avatar
VIGNET Pierre committed
189
        return [tag, attrname, attr]
190
191

    def visit_ctop_node(self, tnode):
192
193
194
195
196
        """Interative build of xml tree for model saving

        .. note:: namespace seems to be useless regarding nsmap here,
        because we use the default namespace without prefix...
        See http://lxml.de/tutorial.html#namespaces.
VIGNET Pierre's avatar
VIGNET Pierre committed
197
        """
198
199
        header = objectify.ElementMaker(
            annotate=False,
VIGNET Pierre's avatar
VIGNET Pierre committed
200
201
            # namespace="http://cadbiom.genouest.org/",
            # nsmap={None: "http://cadbiom.genouest.org/"}
202
203
            namespace=self.model.xml_namespace,
            # the default namespace (no prefix)
VIGNET Pierre's avatar
VIGNET Pierre committed
204
            nsmap={None: self.model.xml_namespace},
205
        )
206
        xmodel = header.model(name=self.model.name)
VIGNET Pierre's avatar
VIGNET Pierre committed
207
        self.current_element = xmodel
208
209
210
211
212

        def create_xml_element(entity):
            """Create XML element and add it to root object"""
            # get node or transition properties
            properties = entity.accept(self)
VIGNET Pierre's avatar
VIGNET Pierre committed
213
            if properties[0] != "CMacroNode":
214
                element = etree.Element(properties[0])
215
                if len(properties) > 1:
VIGNET Pierre's avatar
VIGNET Pierre committed
216
217
                    attrname = properties[1]
                    attr = properties[2]
218
219
                    attributes = element.attrib
                    # Set attributes and values (name, event, coords...)
VIGNET Pierre's avatar
VIGNET Pierre committed
220
                    for i in range(0, len(attrname)):
221
222
223
224
225
226
227
228
229
230
                        attributes[attrname[i]] = str(attr[i])
                # Add notes/text of element
                if entity.note:
                    element.text = entity.note
                # Attach element to the model
                xmodel.append(element)

        # nodes
        for snode in tnode.sub_nodes:
            create_xml_element(snode)
231

VIGNET Pierre's avatar
VIGNET Pierre committed
232
233
234
        # transitions
        for gtr in tnode.transitions:
            for trans in gtr:
235
                create_xml_element(trans)
236

VIGNET Pierre's avatar
VIGNET Pierre committed
237
238
239
240
241
        # constraints
        if len(tnode.model.constraints) > 0:
            const = etree.Element("constraints")
            const.text = tnode.model.constraints
            xmodel.append(const)
242

243
244
245
246
        # Add preamble
        self.xml = '<?xml version = "1.0" encoding="ASCII" standalone="yes" ?>\n'
        self.xml += etree.tostring(xmodel, pretty_print=True)
        # print(etree.tostring(xmodel,pretty_print=True))
247

VIGNET Pierre's avatar
VIGNET Pierre committed
248
249
250
    def visit_ctransition(self, trans):
        """
        Generate xml representation of a transition
251
252
        """
        tag = "transition"
VIGNET Pierre's avatar
VIGNET Pierre committed
253
254
255
256
257
258
259
260
261
        attrname = ["ori", "ext", "event", "condition", "action", "fact_ids"]
        attr = [
            trans.ori.name,
            trans.ext.name,
            trans.event,
            trans.condition,
            trans.action,
            trans.fact_ids,
        ]
VIGNET Pierre's avatar
VIGNET Pierre committed
262
263
264
265

        fact_ids = trans.fact_ids
        for fact in fact_ids:
            self.fact_list.append(fact)
266
267
268

        return [tag, attrname, attr]

VIGNET Pierre's avatar
VIGNET Pierre committed
269
    def return_xml(self):
270
271
272
        """Return the model as xml string.

        .. note:: Used when the model is saved in a .bcx file.
VIGNET Pierre's avatar
VIGNET Pierre committed
273
        """
274
275
        return self.xml

VIGNET Pierre's avatar
VIGNET Pierre committed
276
277
    def get_fact_ids(self):
        """
278
        get litterature references
VIGNET Pierre's avatar
VIGNET Pierre committed
279
280
281
282
283
        """
        model_fact = []
        for i in self.fact_list:
            if i in model_fact:
                continue
VIGNET Pierre's avatar
VIGNET Pierre committed
284
            else:
VIGNET Pierre's avatar
VIGNET Pierre committed
285
286
                model_fact.append(i)
        return model_fact
287
288


VIGNET Pierre's avatar
VIGNET Pierre committed
289
class MakeHandler(ContentHandler):
290
291
292
293
294
295
296
    """Make a handler for the parser when the model is loaded.

    Users are expected to subclass ContentHandler to support their application.
    The following methods are called by the parser on the appropriate events
    in the input document:

    https://docs.python.org/2/library/xml.sax.handler.html
VIGNET Pierre's avatar
VIGNET Pierre committed
297
    """
298

VIGNET Pierre's avatar
VIGNET Pierre committed
299
    def __init__(self, model=None):
VIGNET Pierre's avatar
VIGNET Pierre committed
300
301
302
303
304
305
306
        self.pile_node = []
        self.top_pile = None
        self.pile_dict = []
        self.node_dict = dict()
        self.in_constraints = False
        self.constraints = ""
        self.model = model
307
308
309
310
311
312
313
314
315
316
317
318
319
        self.nodes_types = (
            'CStartNode', 'CTrapNode', 'CSimpleNode', 'CPermNode', 'CInputNode'
        )
        # Memorize the current node/transition because of inner text (note)
        # processing
        self.current_element = None

    def init_node_functions(self):
        """Bind functions to add different types of nodes to the cadbiom model

        .. note:: Must be call after the init of self.top_pile with the xml root
            object.
        """
VIGNET Pierre's avatar
VIGNET Pierre committed
320
321
322
323
324
325
326
        self.add_node_functions = {
            "CStartNode": self.top_pile.add_start_node,
            "CTrapNode": self.top_pile.add_trap_node,
            "CSimpleNode": self.top_pile.add_simple_node,
            "CPermNode": self.top_pile.add_perm_node,
            "CInputNode": self.top_pile.add_input_node,
        }
VIGNET Pierre's avatar
VIGNET Pierre committed
327
328

    def startElement(self, name, att):
329
330
        """Signal the start of an element

331
332
333
334
335
336
        .. notes:: Nodes have to be at the top of the model (Before transitions)
            Transitions do not allow reflexive ones
            (as it could be said in the doc);
            Duplication of transitions are not authorized but only print a
            warning in the shell (they are not taken into account)

337
338
339
340
341
        :param arg1: Contains the raw XML 1.0 name of the element.
        :param arg2: Holds an object of the Attributes interface.
        :type arg1: <str>
        :type arg2: <xml.sax.xmlreader.AttributesImpl>
        """
VIGNET Pierre's avatar
VIGNET Pierre committed
342
        # print(att.getNames())
343

344
        if name in self.nodes_types:
345
346
            # TODO: Uniformization of API in CMacroNode() class;
            # the attribute 'name' should be at the same last position...
VIGNET Pierre's avatar
VIGNET Pierre committed
347
            element_name = att.get("name", "").encode("ascii")
348
349
            self.current_element = self.add_node_functions[name](
                name=element_name,
VIGNET Pierre's avatar
VIGNET Pierre committed
350
351
                xcoord=float(att.get("xloc", "0")),
                ycoord=float(att.get("yloc", "0")),
352
353
            )
            self.node_dict[element_name] = self.current_element
354

VIGNET Pierre's avatar
VIGNET Pierre committed
355
356
357
358
359
360
361
362
        elif name == "transition":
            # name = att.get('name', '').encode('ascii')
            ori = att.get("ori", "")
            ext = att.get("ext", "")
            event = att.get("event", "")
            condition = att.get("condition", "")
            action = att.get("action", "")
            fact_ids_text = att.get("fact_ids", "")[1:-1]
363
            if len(fact_ids_text) > 0:
VIGNET Pierre's avatar
VIGNET Pierre committed
364
                fact_ids = [int(id) for id in fact_ids_text.split(",")]
VIGNET Pierre's avatar
VIGNET Pierre committed
365
366
            else:
                fact_ids = []
367

368
369
370
            # Get nodes involved in the transition
            # If not present, raise an exception
            # => nodes have to be at the top of the model
VIGNET Pierre's avatar
VIGNET Pierre committed
371
372
373
            try:
                node_ori = self.node_dict[ori]
                node_ext = self.node_dict[ext]
374
            except Exception as exc:
VIGNET Pierre's avatar
VIGNET Pierre committed
375
                print("Bad xml file - missing nodes", ori, " -> ", ext)
376
377
                print(self.node_dict)
                print(exc)
378

379
            self.current_element = self.top_pile.add_transition(node_ori, node_ext)
380
381
382
383
384
            # The transition may not be created (origin = ext for example)
            # /!\ Transitions do not allow reflexive ones
            # (as it could be said in the doc)
            # Duplication of transitions are not authorized but only print a
            # warning in the shell (they are not taken into account)
385
386
387
388
389
            if self.current_element:
                self.current_element.set_event(event)
                self.current_element.set_condition(condition)
                self.current_element.set_action(action)
                self.current_element.fact_ids = fact_ids
390

VIGNET Pierre's avatar
VIGNET Pierre committed
391
392
393
394
395
396
        elif name == "CMacroNode":
            name = att.get("name", "").encode("ascii")
            xloc = float(att.get("xloc", "0"))
            yloc = float(att.get("yloc", "0"))
            wloc = float(att.get("wloc", "5"))
            hloc = float(att.get("hloc", "5"))
397

VIGNET Pierre's avatar
VIGNET Pierre committed
398
            node = self.top_pile.add_macro_subnode(name, xloc, yloc, wloc, hloc)
399
400
401
402
403
404
405
406
407
            self.node_dict[name] = node

            self.pile_node.append(node)
            # symbol table put on stack to preserve macro scope for inputs
            new_node_dict = dict()
            self.pile_dict.append(new_node_dict)
            self.top_pile = node
            self.node_dict = new_node_dict

VIGNET Pierre's avatar
VIGNET Pierre committed
408
        elif name == "constraints":
VIGNET Pierre's avatar
VIGNET Pierre committed
409
410
            self.in_constraints = True
            self.constraints = ""
411

VIGNET Pierre's avatar
VIGNET Pierre committed
412
        elif name == "model":
413
            if not self.model:
414
415
                # Init CharModel: get name and namespace (default v1)
                self.model = ChartModel(
VIGNET Pierre's avatar
VIGNET Pierre committed
416
417
                    att.get("name", ""),
                    att.get("xmlns", "http://cadbiom.genouest.org/"),
418
                )
419
420
421
422
423
424
425
426
427
            # Root is a virtual macronode on top of the hierarchy.
            # A model can be a list of hierarchy grouped under this node.
            root = self.model.get_root()
            self.pile_node.append(root)
            self.top_pile = root
            self.init_node_functions()
            new_dict = dict()
            self.pile_dict.append(new_dict)
            self.node_dict = new_dict
428

VIGNET Pierre's avatar
VIGNET Pierre committed
429
    def characters(self, chr):
430
431
432
433
434
435
436
437
438
        """Receive notification of character data.

        The Parser will call this method to report each chunk of character data.
        SAX parsers may return all contiguous character data in a single chunk,
        or they may split it into several chunks;
        => we need to do a concatenation

        :param arg1: chunck of characters.
        :type arg1: <str>
VIGNET Pierre's avatar
VIGNET Pierre committed
439
        """
440
441
        # The current elem is a constraint, a transition or a node
        # print("all", self.current_element, '<'+chr+'>')
VIGNET Pierre's avatar
VIGNET Pierre committed
442
        if self.in_constraints:
443
444
445
446
            self.constraints += chr
        elif self.current_element:
            # node or transition is currently opened in startElement()
            self.current_element.note += chr
447

VIGNET Pierre's avatar
VIGNET Pierre committed
448
    def endElement(self, name):
449
450
451
452
453
        """Called when an elements ends

        .. note:: We handle only elements that need post processing like
            transitions and nodes: reset self.current_element that is used
            to load notes (inner text of xml object).
VIGNET Pierre's avatar
VIGNET Pierre committed
454
        """
455

VIGNET Pierre's avatar
VIGNET Pierre committed
456
        if name == "transition" or name in self.nodes_types:
457
458
            # Close the current node or transition opened in startElement()
            self.current_element = None
VIGNET Pierre's avatar
VIGNET Pierre committed
459
460
        elif name == "CMacroNode":
            # self.top_pile = self.pile_node.pop()
VIGNET Pierre's avatar
VIGNET Pierre committed
461
462
            self.pile_node.remove(self.top_pile)
            self.top_pile = self.pile_node[-1]
VIGNET Pierre's avatar
VIGNET Pierre committed
463
            # self.node_dict = self.pile_dict.pop()
VIGNET Pierre's avatar
VIGNET Pierre committed
464
465
            self.pile_dict.remove(self.node_dict)
            self.node_dict = self.pile_dict[-1]
VIGNET Pierre's avatar
VIGNET Pierre committed
466
        elif name == "constraints":
VIGNET Pierre's avatar
VIGNET Pierre committed
467
            self.in_constraints = False
VIGNET Pierre's avatar
VIGNET Pierre committed
468
469
            self.model.constraints = self.constraints + "\n"
        # elif name == 'model':
470
471
        #    print(len([e for e in self.top_pile.transitions]))
        #    print(len(self.top_pile.new_transitions))
472
473
474


class MakeModelFromXmlFile:
VIGNET Pierre's avatar
VIGNET Pierre committed
475
    """Handy class used to parse an xml file"""
476
477
478
479
480
481
482
483

    def __init__(self, xml_file, model=None):
        """
        :param xml_file: Path of XML file
        :param model: Pre-computed model (never used)
        :type xml_file: <str>
        :type model: <ChartModel>
        """
VIGNET Pierre's avatar
VIGNET Pierre committed
484
        self.handler = MakeHandler(model=model)
485
        self.parser = make_parser()
VIGNET Pierre's avatar
VIGNET Pierre committed
486
        self.parser.setContentHandler(self.handler)
487

488
489
        try:
            self.parser.parse(xml_file)
490
491
492

            # The model is currently not modified in comparison to the file
            self.handler.model.modified = False
493
        except Exception:
494
            LOGGER.error("Error while reading the XML file <%s>", xml_file)
495
            raise
496

VIGNET Pierre's avatar
VIGNET Pierre committed
497
498
499
500
501
502
503
504
    @property
    def model(self):
        """Return the model generated from the XML file

        :rtype: <ChartModel>
        """
        return self.handler.model

VIGNET Pierre's avatar
VIGNET Pierre committed
505

506
class MakeModelFromXmlString:
VIGNET Pierre's avatar
VIGNET Pierre committed
507
    """Handy class used to parse an xml string."""
508

VIGNET Pierre's avatar
VIGNET Pierre committed
509
510
511
    def __init__(self, xml_string):
        self.model = None
        self.handler = MakeHandler()
512
        self.parser = make_parser()
VIGNET Pierre's avatar
VIGNET Pierre committed
513
        self.parser.setContentHandler(self.handler)
514

VIGNET Pierre's avatar
VIGNET Pierre committed
515
        try:
516
            parseString(xml_string, self.handler)
517

518
519
520
521
522
            # The model is currently not modified in comparison to the file
            self.handler.model.modified = False
        except Exception:
            LOGGER.error("Error while reading the XML string")
            raise
VIGNET Pierre's avatar
VIGNET Pierre committed
523
524
525
526
527
528
529
530

    @property
    def model(self):
        """Return the model generated from the XML string

        :rtype: <ChartModel>
        """
        return self.handler.model