Added propagate failure flag for ops.
[dom-editor.git] / Dome / Program.py
blobf7ebd1d3928b2d4794a0be0e1422460fdf04b945
1 from __future__ import nested_scopes
2 import string
3 from constants import DOME_NS
4 from xml.dom import Node
6 def el_named(node, name):
7 for n in node.childNodes:
8 if n.localName == name:
9 return n
10 return None
12 def bool_attr(node, attr):
13 return {"0": False, "1": True,
14 "False": False, "True": True}[node.getAttributeNS(None, attr)]
16 # Converts a DOM <block> node to a Block object.
17 def load(node, parent, ns):
18 #assert node.localName == 'block'
20 block = Block(parent)
21 prev = block.start
22 try:
23 if bool_attr(node, 'foreach'):
24 block.toggle_foreach()
25 if bool_attr(node, 'enter'):
26 block.toggle_enter()
27 if node.hasAttributeNS(None, 'restore'):
28 if bool_attr(node, 'restore'):
29 block.toggle_restore()
30 comment = node.getAttributeNS(None, 'comment')
31 block.set_comment(comment)
32 except:
33 pass
35 id_hash = {} # id from file -> Op
36 to_link = []
37 def _load(chain, prev, exit):
38 for op_node in chain.childNodes:
39 if str(op_node.localName) == 'node':
40 attr = op_node.getAttributeNS(None, 'action')
41 action = eval(str(attr))
42 if action[0] == 'Start':
43 print "Skipping Start"
44 continue
45 if action[0] == 'chroot':
46 action[0] = 'enter'
47 elif action[0] == 'unchroot':
48 action[0] = 'leave'
49 #elif action[0] == 'set_attrib':
50 # if action[3] == '':
51 # action = ('add_attrib', action[1], action[2])
52 # else:
53 # action = ('set_attrib', action[3])
54 elif action[0] == 'playback':
55 action[0] = 'map'
56 elif action[0] == 'add_attrib':
57 action[1] = "UNUSED"
58 elif action[0] == 'do_search' and type(action[-1]) is dict:
59 print "Converting search namespaces..."
60 for p, u in action[-1].iteritems():
61 print "Convert", p, u
62 old = p
63 if p.startswith('_'): p = None
64 p = ns.ensure_ns(p, u)
65 action[1] = action[1].replace(old + ':',
66 p + ':')
67 action[2] = 'unused'
68 op = Op(action)
69 elif op_node.localName == 'block':
70 op = load(op_node, block, ns)
71 else:
72 if op_node.nodeType == Node.ELEMENT_NODE and op_node.localName != 'fail':
73 print "** WARNING ** Unknown op:", op_node
74 continue
76 try:
77 dx = int(float(op_node.getAttributeNS(None, 'dx')))
78 if dx:
79 op.dx = dx
80 dy = int(float(op_node.getAttributeNS(None, 'dy')))
81 if dy:
82 op.dy = dy
83 except:
84 pass
86 node_id = op_node.getAttributeNS(None, 'id')
87 if node_id:
88 id_hash[node_id] = op
90 if op_node.getAttributeNS(None, 'propagate_fail') == 'True':
91 op.propagate_fail = True
93 prev.link_to(op, exit)
94 exit = 'next'
95 prev = op
97 if op_node.localName == 'block':
98 # Block nodes have a special failure child
99 for x in op_node.childNodes:
100 if x.localName == 'fail':
101 _load(x, op, 'fail')
102 break
103 else:
104 # If the new node has children then they are the failure case
105 _load(op_node, op, 'fail')
107 link = op_node.getAttributeNS(None, 'target_fail')
108 if link:
109 to_link.append((op, 'fail', link))
110 link = op_node.getAttributeNS(None, 'target_next')
111 if link:
112 to_link.append((op, 'next', link))
113 _load(node, block.start, 'next')
114 for (op, exit, child) in to_link:
115 try:
116 to = id_hash[child]
117 except:
118 print "**** Not adding link to unknown ID ****"
119 else:
120 op.link_to(to, exit)
121 return block
123 def load_dome_program(prog, ns):
124 "prog should be a DOM 'dome-program' node. ns will be updated"
125 import Namespaces
126 assert isinstance(ns, Namespaces.Namespaces)
127 #print "Loading", prog
128 if prog.localName != 'dome-program':
129 raise Exception('Not a DOME program: %s!' % prog)
131 new = Program(str(prog.getAttributeNS(None, 'name')))
133 #print "Loading '%s'..." % new.name
134 done_update = 0
136 for node in prog.childNodes:
137 if node.localName == 'node' and not done_update:
138 print "*** Converting from old format ***"
139 new.code = load(prog, new, ns)
140 done_update = 1
141 if node.localName == 'block':
142 assert not done_update
143 new.code = load(node, new, ns)
144 if node.localName == 'dome-program':
145 new.add_sub(load_dome_program(node, ns))
147 new.modified = 0
148 return new
150 class Program:
151 "A program contains a code Block and any number of sub-programs."
152 def __init__(self, name):
153 assert '/' not in name
155 self.code = Block(self)
157 self.name = name
158 self.subprograms = {}
159 self.watchers = []
160 self.parent = None
161 self.modified = 0
163 def get_path(self):
164 path = ""
165 p = self
166 while p:
167 path = p.name + '/' + path
168 p = p.parent
169 return path[:-1]
171 def changed(self, op = None):
172 self.modified = 1
173 if self.parent:
174 self.parent.changed(op)
175 else:
176 for w in self.watchers:
177 w.program_changed(op)
179 def tree_changed(self):
180 self.modified = 1
181 if self.parent:
182 self.parent.tree_changed()
183 else:
184 for w in self.watchers:
185 w.prog_tree_changed()
187 def add_sub(self, prog):
188 if prog.parent:
189 raise Exception('%s already has a parent program!' % prog.name)
190 if self.subprograms.has_key(prog.name):
191 raise Exception('%s already has a child called %s!' %
192 (self.name, prog.name))
193 prog.parent = self
194 self.subprograms[prog.name] = prog
195 self.tree_changed()
197 def remove_sub(self, prog):
198 if prog.parent != self:
199 raise Exception('%s is no child of mime!' % prog)
200 prog.parent = None
201 del self.subprograms[prog.name]
202 self.tree_changed()
204 def rename(self, name):
205 p = self.parent
206 if p:
207 if p.subprograms.has_key(name):
208 raise Exception('%s already has a child called %s!' % (p.name, name))
209 p.remove_sub(self)
210 self.name = name
211 if p:
212 p.add_sub(self)
213 else:
214 self.tree_changed()
216 def to_xml(self, doc):
217 node = doc.createElementNS(DOME_NS, 'dome:dome-program')
218 node.setAttributeNS(None, 'name', self.name)
220 node.appendChild(self.code.to_xml(doc))
222 # Keep them in the same order to help with diffs...
223 progs = self.subprograms.keys()
224 progs.sort()
226 for name in progs:
227 p = self.subprograms[name]
228 node.appendChild(p.to_xml(doc))
230 return node
232 def __str__(self):
233 return "Program(%s)" % self.name
235 class Op:
236 "Each node in a chain is an Op. There is no graphical stuff in here."
238 def __init__(self, action = None):
239 "Creates a new node (can be linked into another node later)"
240 if not action:
241 action = ['Start']
242 else:
243 action = list(action)
244 self.parent = None
245 self.action = action
246 self.next = None
247 self.fail = None
248 self.prev = None
249 self.dx, self.dy = (0, 0)
250 self.propagate_fail = False
252 def set_parent(self, parent):
253 if self.parent == parent:
254 return
255 if parent and self.parent:
256 raise Exception('Already got a parent!')
257 if self.next:
258 self.next.set_parent(parent)
259 if self.fail:
260 self.fail.set_parent(parent)
261 self.parent = parent
263 def changed(self, op = None):
264 if hasattr(self, 'cached_code'):
265 del self.cached_code
266 print "(remove cached code)"
267 self.parent.changed(op or self)
269 def swap_nf(self):
270 assert self.action[0] != 'Start'
271 self.next, self.fail = (self.fail, self.next)
272 self.changed()
274 def link_to(self, child, exit):
275 # Create a link from this exit to this child Op
276 # Can't link both exits to the same node (bad for tree-walking code in List)
277 assert self.action[0] != 'Start' or exit == 'next'
278 assert child.action[0] != 'Start'
279 assert child is not self
280 assert child.prev is None
282 if (exit == 'next' and self.fail == child) or \
283 (exit == 'fail' and self.next == child):
284 raise Exception("Can't link both exits (of %s) to the same node!" % self)
286 #print "Link %s:%s -> %s" % (self, exit, child)
288 if child.parent and child.parent is not self.parent:
289 raise Exception('%s is from a different parent (%s vs %s)!' %
290 (child, child.parent, self.parent))
291 # If we already have something on this exit, and the new node has a
292 # clear next exit, move the rest of the chain there.
293 child.set_parent(self.parent)
294 current = getattr(self, exit)
295 if current:
296 if child.next:
297 raise Exception('%s already has a next exit' % child)
298 self.unlink(exit, may_delete = 0)
299 child.link_to(current, 'next')
300 child.prev = self
301 setattr(self, exit, child)
302 self.changed()
304 def unlink(self, exit, may_delete = 1):
305 "Remove link from us to child"
306 assert exit in ['next', 'fail']
307 self._unlink(exit, may_delete)
308 self.changed()
310 def _unlink(self, exit, may_delete = 1):
311 child = getattr(self, exit)
312 if not child:
313 raise Exception('%s has no child on exit %s' % (self, exit))
314 if self is not child.prev:
315 raise Exception('Internal error: %s not my child!' % child)
317 child.prev = None
318 setattr(self, exit, None)
320 if may_delete:
321 # There is no way to reach this child now, so unlink its children.
322 child.parent = None
323 if child.next:
324 child._unlink('next')
325 if child.fail:
326 child._unlink('fail')
328 def del_node(self):
329 """Remove this node. It there is exactly one out-going arc and one incoming one,
330 join them together. Error if:"
331 - There are multiple out-going arcs
332 - There is a single out-going arc but multiple parents"""
333 if self.next and self.fail:
334 raise Exception("Can't delete a node with both fail and next exits in use.")
335 if not self.prev:
336 raise Exception("Can't delete a Start node!")
338 prog = self.parent
340 # Find the chain to preserve (can't have both set here)
341 if self.next:
342 exit = 'next'
343 elif self.fail:
344 exit = 'fail'
345 else:
346 exit = None
348 if exit:
349 prev = self.prev
350 preserve = getattr(self, exit)
351 self.unlink(exit, may_delete = 0)
352 if prev.next == self:
353 exit = 'next'
354 else:
355 exit = 'fail'
357 # Remove all links to us
358 if self.prev.next == self:
359 self.prev.unlink('next')
360 else:
361 self.prev.unlink('fail')
363 # Exit is now our parent's exit that leads to us...
364 if exit:
365 # Relink following nodes to our (single) parent
366 prev.link_to(preserve, exit)
368 assert not self.prev
369 assert not self.next
370 assert not self.fail
371 doc = self.to_doc()
372 self.action = None
373 self.parent = None
375 prog.changed()
376 return doc
378 def to_doc(self):
379 from Ft.Xml.cDomlette import implementation
380 doc = implementation.createDocument(DOME_NS, 'dome:dome-program', None)
381 self.to_xml_int(doc.documentElement)
382 return doc
384 def to_xml(self, doc):
385 node = doc.createElementNS(DOME_NS, 'dome:node')
386 node.setAttributeNS(None, 'action', `self.action`)
387 node.setAttributeNS(None, 'dx', str(self.dx))
388 node.setAttributeNS(None, 'dy', str(self.dy))
389 if self.propagate_fail:
390 node.setAttributeNS(None, 'propagate_fail', 'True')
391 return node
393 def to_xml_int(self, parent):
394 """Adds a chain of <Node> elements to 'parent'. Links only followed when node is
395 first parent."""
396 node = self.to_xml(parent.ownerDocument)
397 parent.appendChild(node)
399 def add_link(op, parent):
400 node = parent.ownerDocument.createElementNS(DOME_NS, 'dome:link')
401 parent.appendChild(node)
402 node.setAttributeNS(None, 'target', str(id(op)))
404 if self.fail:
405 if isinstance(self, Block):
406 fail = parent.ownerDocument.createElementNS(DOME_NS, 'dome:fail')
407 self.fail.to_xml_int(fail)
408 node.appendChild(fail)
409 else:
410 self.fail.to_xml_int(node)
411 if self.next:
412 self.next.to_xml_int(parent)
414 def __str__(self):
415 return "{" + `self.action` + "}"
417 def __repr__(self):
418 return "{" + `self.action` + "}"
420 def get_program(self):
421 p = self.parent
422 while p and not isinstance(p, Program):
423 p = p.parent
424 return p
426 def set_propagate_fail(self, propagate_fail):
427 assert propagate_fail in (True, False)
428 if self.propagate_fail != propagate_fail:
429 self.propagate_fail = propagate_fail
430 self.changed()
433 class Block(Op):
434 """A Block is an Op which contains a group of Ops."""
436 def __init__(self, parent):
437 Op.__init__(self, action = ['Block'])
438 self.parent = parent
439 self.start = Op()
440 self.start.parent = self
441 self.foreach = 0
442 self.enter = 0
443 self.restore = 0
444 self.comment = ''
446 def set_start(self, start):
447 assert not start.prev
449 start.set_parent(self)
450 self.start = start
451 self.changed()
453 def is_toplevel(self):
454 return not isinstance(self.parent, Block)
456 def link_to(self, child, exit):
457 assert not self.is_toplevel()
458 Op.link_to(self, child, exit)
460 def to_xml(self, doc):
461 node = doc.createElementNS(DOME_NS, 'dome:block')
462 node.setAttributeNS(None, 'foreach', str(self.foreach))
463 node.setAttributeNS(None, 'enter', str(self.enter))
464 node.setAttributeNS(None, 'restore', str(self.restore))
465 node.setAttributeNS(None, 'comment', str(self.comment))
466 assert not self.start.fail
467 if self.start.next:
468 self.start.next.to_xml_int(node)
469 return node
471 def toggle_restore(self):
472 self.restore = not self.restore
473 self.changed()
475 def toggle_enter(self):
476 self.enter = not self.enter
477 self.changed()
479 def toggle_foreach(self):
480 self.foreach = not self.foreach
481 self.changed()
483 def set_comment(self, comment):
484 self.comment = comment
485 self.changed()