Separate element constructors from regular constructors.
authorJoe Wreschnig <joe.wreschnig@gmail.com>
Wed, 17 Mar 2010 04:16:45 +0000 (21:16 -0700)
committerJoe Wreschnig <joe.wreschnig@gmail.com>
Wed, 17 Mar 2010 04:16:45 +0000 (21:16 -0700)
bulletml/expr.py
bulletml/impl.py
bulletml/parser.py

index 7654fed4b6c10a7c37b998f1e6fb6d00b3525be7..3c80b0652619556ceb6961a8ddb413a0cbca4b06 100644 (file)
@@ -31,12 +31,13 @@ class NumberDef(object):
     """
 
     GLOBALS = dict(random=random.random, __builtins__={})
+
     def __init__(self, expr):
         try:
-            expr = expr.__original
+            expr = expr.string
         except AttributeError:
             pass
-        self.__original = expr
+        self.string = expr
         repl = lambda match: "params[%d]" % (int(match.group()[1:]) - 1)
         expr = re.sub(r"\$\d+", repl, expr.lower())
         self.__expr = expr.replace("$rand", "random()").replace("$rank", "rank")
@@ -61,7 +62,7 @@ class NumberDef(object):
         return eval(self.__expr, self.GLOBALS, variables)
 
     def __repr__(self):
-        return "%s(%r)" % (type(self).__name__, self.__original)
+        return "%s(%r)" % (type(self).__name__, self.string)
 
 class INumberDef(NumberDef):
     """A NumberDef, but returns rounded integer results."""
index 9139f9c6b463eb6bb920c46ccb05d92b66930e50..92982db02afd4d128f8b475b7f42937b0baabd42 100644 (file)
@@ -7,7 +7,7 @@ from __future__ import division
 
 import math
 
-from bulletml import parser, errors
+from bulletml import parser
 
 # TODO(jfw): This is very non-Pythonic, it's pretty much just the
 # BulletML reference ActionImpl translated to Python.
@@ -297,6 +297,7 @@ class Bullet(object):
     @classmethod
     def FromDoc(cls, doc, params=(), x=0, y=0, speed=0, direction=0,
                 target=None, rank=0.5):
+        """Construct a bullet from top-level actions in a document."""
         actions = [act(params, rank) for act in doc.top]
         return cls(x, y, direction, speed, target, actions, rank=rank)
 
index 933be80847c0b31c186173b82c2ee418712f93a2..bf850a7aae753d5f7b52592b113965043ed0a402 100644 (file)
@@ -28,12 +28,15 @@ def realtag(element):
 
 class ParamList(object):
     """List of parameter definitions."""
-    def __init__(self, element):
-        self.params = []
-        if element:
-            for subelem in element:
-                if realtag(subelem) == "param":
-                    self.params.append(NumberDef(subelem.text))
+
+    def __init__(self, params=[]):
+        self.params = list(params)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls([NumberDef(subelem.text) for subelem in element
+                    if realtag(subelem) == "param"])
 
     def __call__(self, params, rank):
         return [param(params, rank) for param in self.params]
@@ -44,9 +47,18 @@ class ParamList(object):
 class Direction(object):
     """Raw direction value."""
 
-    def __init__(self, doc, element, type="absolute"):
-        self.type = element.get("type", type)
-        self.value = NumberDef(element.text)
+    VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
+
+    def __init__(self, type, value):
+        if type not in self.VALID_TYPES:
+            raise ValueError("invalid type %r" % type)
+        self.type = type
+        self.value = value
+
+    @classmethod
+    def FromElement(cls, doc, element, default="absolute"):
+        """Construct using an ElementTree-style element."""
+        return cls(element.get("type", default), NumberDef(element.text))
 
     def __call__(self, params, rank):
         return (self.value(params, rank), self.type)
@@ -58,17 +70,23 @@ class Direction(object):
 class ChangeDirection(object):
     """Direction change over time."""
 
-    def __init__(self, doc, element):
+    def __init__(self, term, direction):
+        self.term = term
+        self.direction = direction
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem)
+                direction = Direction.FromElement(doc, subelem)
             elif tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
         try:
-            self.term, self.direction
-        except AttributeError:
-            raise ParseError
+            return cls(term, direction)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.term(params, rank), self.direction(params, rank)
@@ -80,9 +98,18 @@ class ChangeDirection(object):
 class Speed(object):
     """Raw speed value."""
 
-    def __init__(self, doc, element, type="absolute"):
-        self.type = element.get("type", type)
-        self.value = NumberDef(element.text)
+    VALID_TYPES = ["relative", "absolute", "sequence"]
+
+    def __init__(self, type, value):
+        if type not in self.VALID_TYPES:
+            raise ValueError("invalid type %r" % type)
+        self.type = type
+        self.value = value
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls(element.get("type", "absolute"), NumberDef(element.text))
 
     def __call__(self, params, rank):
         return (self.value(params, rank), self.type)
@@ -93,17 +120,23 @@ class Speed(object):
 class ChangeSpeed(object):
     """Speed change over time."""
 
-    def __init__(self, doc, element):
+    def __init__(self, term, speed):
+        self.term = term
+        self.speed = speed
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
         try:
-            self.term, self.speed
-        except AttributeError:
-            raise ParseError
+            return cls(term, speed)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.term(params, rank), self.speed(params, rank)
@@ -114,8 +147,14 @@ class ChangeSpeed(object):
 
 class Wait(object):
     """Wait for some frames."""
-    def __init__(self, doc, element):
-        self.frames = INumberDef(element.text)
+
+    def __init__(self, frames):
+        self.frames = frames
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls(INumberDef(element.text))
 
     def __call__(self, params, rank):
         return self.frames(params, rank)
@@ -125,29 +164,39 @@ class Wait(object):
 
 class Vanish(object):
     """Make the owner disappear."""
-    def __init__(self, doc, element):
+
+    def __init__(self):
         pass
 
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls()
+
     def __repr__(self):
         return "%s()" % (type(self).__name__)
 
 class Repeat(object):
     """Repeat an action definition."""
 
-    def __init__(self, doc, element):
+    def __init__(self, times, action):
+        self.times = times
+        self.action = action
+    
+    @classmethod
+    def FromElement(cls, doc, element):
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "times":
-                self.times = INumberDef(subelem.text)
+                times = INumberDef(subelem.text)
             elif tag == "action":
-                self.action = ActionDef(doc, subelem)
+                action = ActionDef.FromElement(doc, subelem)
             elif tag == "actionRef":
-                self.action = ActionRef(doc, subelem)
-
+                action = ActionRef.FromElement(doc, subelem)
         try:
-            self.times, self.action
-        except AttributeError:
-            raise ParseError
+            return cls(times, action)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.times(params, rank), self.action(params, rank)
@@ -161,18 +210,28 @@ class Accel(object):
     horizontal = None
     vertical = None
 
-    def __init__(self, doc, element):
+    def __init__(self, term, horizontal=None, vertical=None):
+        self.term = term
+        self.horizontal = horizontal
+        self.vertical = vertical
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        horizontal = None
+        vertical = None
+
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
             elif tag == "horizontal":
-                self.horizontal = Speed(doc, subelem)
+                horizontal = Speed.FromElement(doc, subelem)
             elif tag == "vertical":
-                self.vertical = Speed(doc, subelem)
+                vertical = Speed.FromElement(doc, subelem)
 
         try:
-            self.term
+            return cls(term, horizontal, vertical)
         except AttributeError:
             raise ParseError
 
@@ -192,19 +251,30 @@ class BulletDef(object):
     direction = None
     speed = None
 
-    def __init__(self, doc, element):
-        self.actions = []
-        doc.bullets[element.get("label")] = self
+    def __init__(self, actions=[], direction=None, speed=None):
+        self.direction = direction
+        self.speed = speed
+        self.actions = list(actions)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        actions = []
+        speed = None
+        direction = None
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem)
+                direction = Direction.FromElement(doc, subelem)
             elif tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "action":
-                self.actions.append(ActionDef(doc, subelem))
+                actions.append(ActionDef.FromElement(doc, subelem))
             elif tag == "actionRef":
-                self.actions.append(ActionRef(doc, subelem))
+                actions.append(ActionRef.FromElement(doc, subelem))
+        dfn = cls(actions, direction, speed)
+        doc.bullets[element.get("label")] = dfn
+        return dfn
 
     def __call__(self, params, rank):
         actions = [action(params, rank) for action in self.actions]
@@ -220,10 +290,16 @@ class BulletDef(object):
 class BulletRef(object):
     """Create a bullet by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.bullet = element.get("label")
-        self.params = ParamList(element)
-        doc._bullet_refs.append(self)
+    def __init__(self, bullet, params=None):
+        self.bullet = bullet
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._bullet_refs.append(bullet)
+        return bullet
 
     def __call__(self, params, rank):
         return self.bullet(self.params(params, rank), rank)
@@ -235,27 +311,27 @@ class BulletRef(object):
 class ActionDef(object):
     """Action definition."""
 
-    def __init__(self, doc, element):
-        doc.actions[element.get("label")] = self
-        self.actions = []
+    # This is self-referential, so it's filled in later.
+    CONSTRUCTORS = dict()
+
+    def __init__(self, actions):
+        self.actions = list(actions)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        actions = []
         for subelem in element.getchildren():
             tag = realtag(subelem)
             try:
-                ctr = dict(
-                    repeat=Repeat,
-                    fire=FireDef,
-                    fireRef=FireRef,
-                    changeSpeed=ChangeSpeed,
-                    changeDirection=ChangeDirection,
-                    accel=Accel,
-                    wait=Wait,
-                    vanish=Vanish,
-                    action=ActionDef,
-                    actionRef=ActionRef)[tag]
+                ctr = cls.CONSTRUCTORS[tag]
             except KeyError:
                 continue
             else:
-                self.actions.append(ctr(doc, subelem))
+                actions.append(ctr.FromElement(doc, subelem))
+        dfn = cls(actions)
+        doc.actions[element.get("label")] = dfn
+        return dfn
 
     def __call__(self, params, rank):
         return self.actions, params
@@ -266,10 +342,16 @@ class ActionDef(object):
 class ActionRef(object):
     """Run an action by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.action = element.get("label")
-        self.params = ParamList(element)
-        doc._action_refs.append(self)
+    def __init__(self, action, params=None):
+        self.action = action
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        action = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._action_refs.append(action)
+        return action
 
     def __call__(self, params, rank):
         return self.action(self.params(params, rank), rank)
@@ -281,25 +363,35 @@ class ActionRef(object):
 class FireDef(object):
     """Fire definition (creates a bullet)."""
 
-    direction = None
-    speed = None
+    def __init__(self, bullet, direction=None, speed=None):
+        self.bullet = bullet
+        self.direction = direction
+        self.speed = speed
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        direction = None
+        speed = None
 
-    def __init__(self, doc, element):
-        doc.fires[element.get("label")] = self
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem, "aim")
+                direction = Direction.FromElement(doc, subelem, "aim")
             elif tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "bullet":
-                self.bullet = BulletDef(doc, subelem)
+                bullet = BulletDef.FromElement(doc, subelem)
             elif tag == "bulletRef":
-                self.bullet = BulletRef(doc, subelem)
+                bullet = BulletRef.FromElement(doc, subelem)
+
         try:
-            self.bullet
-        except AttributeError:
-            raise ParseError
+            fire = cls(bullet, direction, speed)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
+        else:
+            doc.fires[element.get("label")] = fire
+            return fire
 
     def __call__(self, params, rank):
         direction, speed, actions = self.bullet(params, rank)
@@ -316,10 +408,16 @@ class FireDef(object):
 class FireRef(object):
     """Fire a bullet by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.fire = element.get("label")
-        self.params = ParamList(element)
-        doc._fire_refs.append(self)
+    def __init__(self, fire, params=None):
+        self.fire = fire
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        fired = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._fire_refs.append(fired)
+        return fired
 
     def __call__(self, params, rank):
         """Generate a Bullet from the FireDef and params."""
@@ -362,7 +460,7 @@ class BulletML(object):
         for element in root.getchildren():
             tag = realtag(element)
             if tag in self.CONSTRUCTORS:
-                self.CONSTRUCTORS[tag](self, element)
+                self.CONSTRUCTORS[tag].FromElement(self, element)
 
         try:
             for ref in self._bullet_refs:
@@ -378,6 +476,10 @@ class BulletML(object):
         del(self._action_refs)
         del(self._fire_refs)
 
+        self.bullets.pop(None, None)
+        self.actions.pop(None, None)
+        self.fires.pop(None, None)
+
     @property
     def top(self):
         """Get a list of all top-level actions."""
@@ -388,3 +490,15 @@ class BulletML(object):
         return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
             type(self).__name__, self.type, self.bullets, self.actions,
             self.fires)
+
+ActionDef.CONSTRUCTORS = dict(
+        repeat=Repeat,
+        fire=FireDef,
+        fireRef=FireRef,
+        changeSpeed=ChangeSpeed,
+        changeDirection=ChangeDirection,
+        accel=Accel,
+        wait=Wait,
+        vanish=Vanish,
+        action=ActionDef,
+        actionRef=ActionRef)