BulletML.FromDocument: Type detector. Various setstate bug fixes.
[python-bulletml.git] / bulletml / parser.py
1 """BulletML parser.
2
3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
5
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
8 """
9
10 from __future__ import division
11
12 import math
13
14 from xml.etree.ElementTree import ElementTree
15
16 try:
17 from io import StringIO
18 except ImportError:
19 try:
20 from cStringIO import StringIO
21 except ImportError:
22 from StringIO import StringIO
23
24 from bulletml.errors import Error
25 from bulletml.expr import NumberDef, INumberDef
26
27
28 __all_ = ["ParseError", "BulletML"]
29
30 class ParseError(Error):
31 """Raised when an error occurs parsing the XML structure."""
32 pass
33
34 def realtag(element):
35 """Strip namespace poop off the front of a tag."""
36 try:
37 return element.tag.rsplit('}', 1)[1]
38 except ValueError:
39 return element.tag
40
41 class ParamList(object):
42 """List of parameter definitions."""
43
44 def __init__(self, params=()):
45 self.params = list(params)
46
47 @classmethod
48 def FromElement(cls, doc, element):
49 """Construct using an ElementTree-style element."""
50 return cls([NumberDef(subelem.text) for subelem in element
51 if realtag(subelem) == "param"])
52
53 def __call__(self, params, rank):
54 return [param(params, rank) for param in self.params]
55
56 def __repr__(self):
57 return "%s(%r)" % (type(self).__name__, self.params)
58
59 class Direction(object):
60 """Raw direction value."""
61
62 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
63
64 def __init__(self, type, value):
65 if type not in self.VALID_TYPES:
66 raise ValueError("invalid type %r" % type)
67 self.type = type
68 self.value = value
69
70 def __getstate__(self):
71 return dict(type=self.type, value=self.value.expr)
72
73 def __setstate__(self, state):
74 self.__init__(state["type"], NumberDef(state["value"]))
75
76 @classmethod
77 def FromElement(cls, doc, element, default="absolute"):
78 """Construct using an ElementTree-style element."""
79 return cls(element.get("type", default), NumberDef(element.text))
80
81 def __call__(self, params, rank):
82 return (math.radians(self.value(params, rank)), self.type)
83
84 def __repr__(self):
85 return "%s(%r, type=%r)" % (
86 type(self).__name__, self.value, self.type)
87
88 class ChangeDirection(object):
89 """Direction change over time."""
90
91 def __init__(self, term, direction):
92 self.term = term
93 self.direction = direction
94
95 def __getstate__(self):
96 return dict(frames=self.term.expr,
97 type=self.direction.type,
98 value=self.direction.value.expr)
99
100 def __setstate__(self, state):
101 self.__init__(INumberDef(state["frames"]),
102 Direction(state["type"], NumberDef(state["value"])))
103
104 @classmethod
105 def FromElement(cls, doc, element):
106 """Construct using an ElementTree-style element."""
107 for subelem in element.getchildren():
108 tag = realtag(subelem)
109 if tag == "direction":
110 direction = Direction.FromElement(doc, subelem)
111 elif tag == "term":
112 term = INumberDef(subelem.text)
113 try:
114 return cls(term, direction)
115 except UnboundLocalError as exc:
116 raise ParseError(str(exc))
117
118 def __call__(self, params, rank):
119 return self.term(params, rank), self.direction(params, rank)
120
121 def __repr__(self):
122 return "%s(term=%r, direction=%r)" % (
123 type(self).__name__, self.term, self.direction)
124
125 class Speed(object):
126 """Raw speed value."""
127
128 VALID_TYPES = ["relative", "absolute", "sequence"]
129
130 def __init__(self, type, value):
131 if type not in self.VALID_TYPES:
132 raise ValueError("invalid type %r" % type)
133 self.type = type
134 self.value = value
135
136 def __getstate__(self):
137 return dict(type=self.type, value=self.value.expr)
138
139 def __setstate__(self, state):
140 self.__init__(state["type"], NumberDef(state["value"]))
141
142 @classmethod
143 def FromElement(cls, doc, element):
144 """Construct using an ElementTree-style element."""
145 return cls(element.get("type", "absolute"), NumberDef(element.text))
146
147 def __call__(self, params, rank):
148 return (self.value(params, rank), self.type)
149
150 def __repr__(self):
151 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
152
153 class ChangeSpeed(object):
154 """Speed change over time."""
155
156 def __init__(self, term, speed):
157 self.term = term
158 self.speed = speed
159
160 def __getstate__(self):
161 return dict(frames=self.term.expr,
162 type=self.speed.type,
163 value=self.speed.value.expr)
164
165 def __setstate__(self, state):
166 self.__init__(INumberDef(state["frames"]),
167 Speed(state["type"], NumberDef(state["value"])))
168
169 @classmethod
170 def FromElement(cls, doc, element):
171 """Construct using an ElementTree-style element."""
172 for subelem in element.getchildren():
173 tag = realtag(subelem)
174 if tag == "speed":
175 speed = Speed.FromElement(doc, subelem)
176 elif tag == "term":
177 term = INumberDef(subelem.text)
178 try:
179 return cls(term, speed)
180 except UnboundLocalError as exc:
181 raise ParseError(str(exc))
182
183 def __call__(self, params, rank):
184 return self.term(params, rank), self.speed(params, rank)
185
186 def __repr__(self):
187 return "%s(term=%r, speed=%r)" % (
188 type(self).__name__, self.term, self.speed)
189
190 class Wait(object):
191 """Wait for some frames."""
192
193 def __init__(self, frames):
194 self.frames = frames
195
196 def __getstate__(self):
197 return dict(frames=self.frames.expr)
198
199 def __setstate__(self, state):
200 self.__init__(INumberDef(state["frames"]))
201
202 @classmethod
203 def FromElement(cls, doc, element):
204 """Construct using an ElementTree-style element."""
205 return cls(INumberDef(element.text))
206
207 def __call__(self, params, rank):
208 return self.frames(params, rank)
209
210 def __repr__(self):
211 return "%s(%r)" % (type(self).__name__, self.frames)
212
213 class Tag(object):
214 """Set a bullet tag."""
215
216 def __init__(self, tag):
217 self.tag = tag
218
219 def __getstate__(self):
220 return dict(tag=self.tag)
221
222 def __setstate__(self, state):
223 self.__init__(state["tag"])
224
225 @classmethod
226 def FromElement(cls, doc, element):
227 """Construct using an ElementTree-style element."""
228 return cls(element.text)
229
230 class Untag(object):
231 """Unset a bullet tag."""
232
233 def __init__(self, tag):
234 self.tag = tag
235
236 def __getstate__(self):
237 return dict(tag=self.tag)
238
239 def __setstate__(self, state):
240 self.__init__(state["tag"])
241
242 @classmethod
243 def FromElement(cls, doc, element):
244 """Construct using an ElementTree-style element."""
245 return cls(element.text)
246
247 class Vanish(object):
248 """Make the owner disappear."""
249
250 def __init__(self):
251 pass
252
253 @classmethod
254 def FromElement(cls, doc, element):
255 """Construct using an ElementTree-style element."""
256 return cls()
257
258 def __repr__(self):
259 return "%s()" % (type(self).__name__)
260
261 class Repeat(object):
262 """Repeat an action definition."""
263
264 def __init__(self, times, action):
265 self.times = times
266 self.action = action
267
268 def __getstate__(self):
269 return dict(times=self.times.expr, action=self.action)
270
271 def __setstate__(self, state):
272 self.__init__(INumberDef(state["times"]), state["action"])
273
274 @classmethod
275 def FromElement(cls, doc, element):
276 """Construct using an ElementTree-style element."""
277 for subelem in element.getchildren():
278 tag = realtag(subelem)
279 if tag == "times":
280 times = INumberDef(subelem.text)
281 elif tag == "action":
282 action = ActionDef.FromElement(doc, subelem)
283 elif tag == "actionRef":
284 action = ActionRef.FromElement(doc, subelem)
285 try:
286 return cls(times, action)
287 except UnboundLocalError as exc:
288 raise ParseError(str(exc))
289
290 def __call__(self, params, rank):
291 return self.times(params, rank), self.action(params, rank)
292
293 def __repr__(self):
294 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
295
296 class Accel(object):
297 """Accelerate over some time."""
298
299 horizontal = None
300 vertical = None
301
302 def __init__(self, term, horizontal=None, vertical=None):
303 self.term = term
304 self.horizontal = horizontal
305 self.vertical = vertical
306
307 def __getstate__(self):
308 state = dict(frames=self.term.expr)
309 if self.horizontal:
310 state["horizontal"] = self.horizontal
311 if self.vertical:
312 state["vertical"] = self.vertical
313 return state
314
315 def __setstate__(self, state):
316 self.__init__(INumberDef(state["frames"]), state.get("horizontal"),
317 state.get("vertical"))
318
319 @classmethod
320 def FromElement(cls, doc, element):
321 """Construct using an ElementTree-style element."""
322 horizontal = None
323 vertical = None
324
325 for subelem in element.getchildren():
326 tag = realtag(subelem)
327 if tag == "term":
328 term = INumberDef(subelem.text)
329 elif tag == "horizontal":
330 horizontal = Speed.FromElement(doc, subelem)
331 elif tag == "vertical":
332 vertical = Speed.FromElement(doc, subelem)
333
334 try:
335 return cls(term, horizontal, vertical)
336 except AttributeError:
337 raise ParseError
338
339 def __call__(self, params, rank):
340 frames = self.term(params, rank)
341 horizontal = self.horizontal and self.horizontal(params, rank)
342 vertical = self.vertical and self.vertical(params, rank)
343 return frames, horizontal, vertical
344
345 def __repr__(self):
346 return "%s(%r, horizontal=%r, vertical=%r)" % (
347 type(self).__name__, self.term, self.horizontal, self.vertical)
348
349 class BulletDef(object):
350 """Bullet definition."""
351
352 direction = None
353 speed = None
354
355 def __init__(self, actions=[], direction=None, speed=None):
356 self.direction = direction
357 self.speed = speed
358 self.actions = list(actions)
359
360 def __getstate__(self):
361 state = dict()
362 if self.direction:
363 state["direction"] = self.direction
364 if self.speed:
365 state["speed"] = self.speed
366 if self.actions:
367 state["actions"] = self.actions
368 return state
369
370 def __setstate__(self, state):
371 self.__init__(**state)
372
373 @classmethod
374 def FromElement(cls, doc, element):
375 """Construct using an ElementTree-style element."""
376 actions = []
377 speed = None
378 direction = None
379 for subelem in element.getchildren():
380 tag = realtag(subelem)
381 if tag == "direction":
382 direction = Direction.FromElement(doc, subelem)
383 elif tag == "speed":
384 speed = Speed.FromElement(doc, subelem)
385 elif tag == "action":
386 actions.append(ActionDef.FromElement(doc, subelem))
387 elif tag == "actionRef":
388 actions.append(ActionRef.FromElement(doc, subelem))
389 dfn = cls(actions, direction, speed)
390 doc.bullets[element.get("label")] = dfn
391 return dfn
392
393 def __call__(self, params, rank):
394 actions = [action(params, rank) for action in self.actions]
395 return (
396 self.direction and self.direction(params, rank),
397 self.speed and self.speed(params, rank),
398 actions)
399
400 def __repr__(self):
401 return "%s(direction=%r, speed=%r, actions=%r)" % (
402 type(self).__name__, self.direction, self.speed, self.actions)
403
404 class BulletRef(object):
405 """Create a bullet by name with parameters."""
406
407 def __init__(self, bullet, params=None):
408 self.bullet = bullet
409 self.params = ParamList() if params is None else params
410
411 def __getstate__(self):
412 state = dict(bullet=self.bullet)
413 if self.params.params:
414 state["params"] = [param.expr for param in self.params.params]
415 return state
416
417 def __setstate__(self, state):
418 bullet = state["bullet"]
419 params = [NumberDef(param) for param in state.get("params", [])]
420 self.__init__(bullet, ParamList(params))
421
422 @classmethod
423 def FromElement(cls, doc, element):
424 """Construct using an ElementTree-style element."""
425 bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
426 doc._bullet_refs.append(bullet)
427 return bullet
428
429 def __call__(self, params, rank):
430 return self.bullet(self.params(params, rank), rank)
431
432 def __repr__(self):
433 return "%s(params=%r, bullet=%r)" % (
434 type(self).__name__, self.params, self.bullet)
435
436 class ActionDef(object):
437 """Action definition.
438
439 To support parsing new actions, add tags to
440 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
441 FromElement classmethod, which take the BulletML instance and
442 ElementTree element as arguments.
443 """
444
445 # This is self-referential, so it's filled in later.
446 CONSTRUCTORS = dict()
447
448 def __init__(self, actions):
449 self.actions = list(actions)
450
451 def __getstate__(self):
452 return dict(actions=self.actions)
453
454 def __setstate__(self, state):
455 self.__init__(state["actions"])
456
457 @classmethod
458 def FromElement(cls, doc, element):
459 """Construct using an ElementTree-style element."""
460 actions = []
461 for subelem in element.getchildren():
462 tag = realtag(subelem)
463 try:
464 ctr = cls.CONSTRUCTORS[tag]
465 except KeyError:
466 continue
467 else:
468 actions.append(ctr.FromElement(doc, subelem))
469 dfn = cls(actions)
470 doc.actions[element.get("label")] = dfn
471 return dfn
472
473 def __call__(self, params, rank):
474 return self.actions, params
475
476 def __repr__(self):
477 return "%s(%r)" % (type(self).__name__, self.actions)
478
479 class ActionRef(object):
480 """Run an action by name with parameters."""
481
482 def __init__(self, action, params=None):
483 self.action = action
484 self.params = params or ParamList()
485
486 def __getstate__(self):
487 state = dict(action=self.action)
488 if self.params.params:
489 state["params"] = [param.expr for param in self.params.params]
490 return state
491
492 def __setstate__(self, state):
493 action = state["action"]
494 params = [NumberDef(param) for param in state.get("params", [])]
495 self.__init__(action, ParamList(params))
496
497 @classmethod
498 def FromElement(cls, doc, element):
499 """Construct using an ElementTree-style element."""
500 action = cls(element.get("label"), ParamList.FromElement(doc, element))
501 doc._action_refs.append(action)
502 return action
503
504 def __call__(self, params, rank):
505 return self.action(self.params(params, rank), rank)
506
507 def __repr__(self):
508 return "%s(params=%r, action=%r)" % (
509 type(self).__name__, self.params, self.action)
510
511 class Offset(object):
512 """Provide an offset to a bullet's initial position."""
513
514 VALID_TYPES = ["relative", "absolute"]
515
516 def __init__(self, type, x, y):
517 if type not in self.VALID_TYPES:
518 raise ValueError("invalid type %r" % type)
519 self.type = type
520 self.x = x
521 self.y = y
522
523 def __getstate__(self):
524 state = dict(type=self.type)
525 if self.x:
526 state["x"] = self.x.expr
527 if self.y:
528 state["y"] = self.y.expr
529 return state
530
531 def __setstate__(self, state):
532 self.__init__(state["type"], state.get("x"), state.get("y"))
533
534 @classmethod
535 def FromElement(cls, doc, element):
536 """Construct using an ElementTree-style element."""
537 type = element.get("type", "relative")
538 x = None
539 y = None
540 for subelem in element:
541 tag = realtag(subelem)
542 if tag == "x":
543 x = NumberDef(subelem.text)
544 elif tag == "y":
545 y = NumberDef(subelem.text)
546 return cls(type, x, y)
547
548 def __call__(self, params, rank):
549 return (self.x(params, rank) if self.x else 0,
550 self.y(params, rank) if self.y else 0)
551
552 class FireDef(object):
553 """Fire definition (creates a bullet)."""
554
555 def __init__(self, bullet, direction=None, speed=None, offset=None):
556 self.bullet = bullet
557 self.direction = direction
558 self.speed = speed
559 self.offset = offset
560
561 def __getstate__(self):
562 try:
563 params = self.bullet.params
564 except AttributeError:
565 state = dict(bullet=self.bullet)
566 else:
567 if params.params:
568 state = dict(bullet=self.bullet)
569 else:
570 state = dict(bullet=self.bullet.bullet)
571 if self.direction:
572 state["direction"] = self.direction
573 if self.speed:
574 state["speed"] = self.speed
575 if self.offset:
576 state["offset"] = self.offset
577 return state
578
579 def __setstate__(self, state):
580 self.__init__(**state)
581
582 @classmethod
583 def FromElement(cls, doc, element):
584 """Construct using an ElementTree-style element."""
585 direction = None
586 speed = None
587 offset = None
588
589 for subelem in element.getchildren():
590 tag = realtag(subelem)
591 if tag == "direction":
592 direction = Direction.FromElement(doc, subelem, "aim")
593 elif tag == "speed":
594 speed = Speed.FromElement(doc, subelem)
595 elif tag == "bullet":
596 bullet = BulletDef.FromElement(doc, subelem)
597 elif tag == "bulletRef":
598 bullet = BulletRef.FromElement(doc, subelem)
599 elif tag == "offset":
600 offset = Offset.FromElement(doc, subelem)
601 try:
602 fire = cls(bullet, direction, speed, offset)
603 except UnboundLocalError as exc:
604 raise ParseError(str(exc))
605 else:
606 doc.fires[element.get("label")] = fire
607 return fire
608
609 def __call__(self, params, rank):
610 direction, speed, actions = self.bullet(params, rank)
611 if self.direction:
612 direction = self.direction(params, rank)
613 if self.speed:
614 speed = self.speed(params, rank)
615 return direction, speed, actions, self.offset
616
617 def __repr__(self):
618 return "%s(direction=%r, speed=%r, bullet=%r)" % (
619 type(self).__name__, self.direction, self.speed, self.bullet)
620
621 class FireRef(object):
622 """Fire a bullet by name with parameters."""
623
624 def __init__(self, fire, params=None):
625 self.fire = fire
626 self.params = params or ParamList()
627
628 def __getstate__(self):
629 state = dict(fire=self.fire)
630 if self.params.params:
631 state["params"] = [param.expr for param in self.params.params]
632 return state
633
634 def __setstate__(self, state):
635 fire = state["fire"]
636 params = [NumberDef(param) for param in state.get("params", [])]
637 self.__init__(fire, ParamList(params))
638
639 @classmethod
640 def FromElement(cls, doc, element):
641 """Construct using an ElementTree-style element."""
642 fired = cls(element.get("label"), ParamList.FromElement(doc, element))
643 doc._fire_refs.append(fired)
644 return fired
645
646 def __call__(self, params, rank):
647 return self.fire(self.params(params, rank), rank)
648
649 def __repr__(self):
650 return "%s(params=%r, fire=%r)" % (
651 type(self).__name__, self.params, self.fire)
652
653 class BulletML(object):
654 """BulletML document.
655
656 A BulletML document is a collection of bullets, actions, and
657 firings, as well as a base game type.
658
659 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
660 its parsing. It maps tag names to classes with a FromElement
661 classmethod, which take the BulletML instance and ElementTree
662 element as arguments.
663
664 """
665
666 CONSTRUCTORS = dict(
667 bullet=BulletDef,
668 action=ActionDef,
669 fire=FireDef,
670 )
671
672 def __init__(self, type="none", bullets=None, fires=None, actions=None):
673 self.type = type
674 self.bullets = {} if bullets is None else bullets
675 self.actions = {} if actions is None else actions
676 self.fires = {} if fires is None else fires
677
678 def __getstate__(self):
679 return dict(type=self.type, actions=self.actions)
680
681 def __setstate__(self, state):
682 self.__init__(state["type"], actions=state.get("actions"))
683
684 @classmethod
685 def FromXML(cls, source):
686 """Return a BulletML instance based on XML."""
687 if not hasattr(source, 'read'):
688 source = StringIO(source)
689
690 tree = ElementTree()
691 root = tree.parse(source)
692
693 self = cls(type=root.get("type", "none"))
694
695 self._bullet_refs = []
696 self._action_refs = []
697 self._fire_refs = []
698
699 for element in root.getchildren():
700 tag = realtag(element)
701 if tag in self.CONSTRUCTORS:
702 self.CONSTRUCTORS[tag].FromElement(self, element)
703
704 try:
705 for ref in self._bullet_refs:
706 ref.bullet = self.bullets[ref.bullet]
707 for ref in self._fire_refs:
708 ref.fire = self.fires[ref.fire]
709 for ref in self._action_refs:
710 ref.action = self.actions[ref.action]
711 except KeyError as exc:
712 raise ParseError("unknown reference %s" % exc)
713
714 del(self._bullet_refs)
715 del(self._action_refs)
716 del(self._fire_refs)
717
718 self.bullets.pop(None, None)
719 self.actions.pop(None, None)
720 self.fires.pop(None, None)
721
722 return self
723
724 @classmethod
725 def FromYAML(cls, source):
726 """Create a BulletML instance based on YAML."""
727
728 # Late import to avoid a circular dependency.
729 try:
730 import bulletml.bulletyaml
731 import yaml
732 except ImportError:
733 raise ParseError("PyYAML is not available")
734 else:
735 try:
736 return yaml.load(source)
737 except Exception, exc:
738 raise ParseError(str(exc))
739
740 @classmethod
741 def FromDocument(cls, source):
742 """Create a BulletML instance based on a seekable file or string.
743
744 This attempts to autodetect if the stream is XML or YAML.
745 """
746 if not hasattr(source, 'read'):
747 source = StringIO(source)
748 start = source.read(1)
749 source.seek(0)
750 if start == "<":
751 return cls.FromXML(source)
752 elif start == "!":
753 return cls.FromYAML(source)
754 else:
755 raise ParseError("unknown initial character %r" % start)
756
757 @property
758 def top(self):
759 """Get a list of all top-level actions."""
760 return [dfn for name, dfn in self.actions.items()
761 if name and name.startswith("top")]
762
763 def __repr__(self):
764 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
765 type(self).__name__, self.type, self.bullets, self.actions,
766 self.fires)
767
768 ActionDef.CONSTRUCTORS = dict(
769 repeat=Repeat,
770 fire=FireDef,
771 fireRef=FireRef,
772 changeSpeed=ChangeSpeed,
773 changeDirection=ChangeDirection,
774 accel=Accel,
775 wait=Wait,
776 vanish=Vanish,
777 tag=Tag,
778 untag=Untag,
779 action=ActionDef,
780 actionRef=ActionRef)