X-Git-Url: https://git.yukkurigames.com/?p=python-bulletml.git;a=blobdiff_plain;f=bulletml%2Fparser.py;h=9b2fbf8577a94d6796b0b7f209dd291e4e34b4fc;hp=a89d1c5b8ce207c7f8a5a37db780f0d38ba43d7f;hb=8186eda32d3764ceb7b75fac65bd968f598c4ac8;hpb=4f0e7df685d8d25cd0f1bbce7d6330f87c8cf178 diff --git a/bulletml/parser.py b/bulletml/parser.py index a89d1c5..9b2fbf8 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -24,6 +24,7 @@ except ImportError: from bulletml.errors import Error from bulletml.expr import NumberDef, INumberDef + __all_ = ["ParseError", "BulletML"] class ParseError(Error): @@ -44,20 +45,11 @@ class ParamList(object): self.params = list(params) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls([NumberDef(subelem.text) for subelem in element if realtag(subelem) == "param"]) - def __getstate__(self): - if self.params: - return dict(params=[param.expr for param in self.params]) - else: - return {} - - def __setstate__(self, state): - self.__init__(NumberDef(param) for param in state.get("params", [])) - def __call__(self, params, rank): return [param(params, rank) for param in self.params] @@ -76,13 +68,14 @@ class Direction(object): self.value = value def __getstate__(self): - return dict(type=self.type, value=self.value.expr) + return [('type', self.type), ('value', self.value.expr)] def __setstate__(self, state): + state = dict(state) self.__init__(state["type"], NumberDef(state["value"])) @classmethod - def FromElement(cls, doc, element, default="absolute"): + def FromXML(cls, doc, element, default="absolute"): """Construct using an ElementTree-style element.""" return cls(element.get("type", default), NumberDef(element.text)) @@ -101,21 +94,22 @@ class ChangeDirection(object): self.direction = direction def __getstate__(self): - return dict(frames=self.term.expr, - type=self.direction.type, - value=self.direction.value.expr) + return [('frames', self.term.expr), + ('type', self.direction.type), + ('value', self.direction.value.expr)] def __setstate__(self, state): + state = dict(state) self.__init__(INumberDef(state["frames"]), Direction(state["type"], NumberDef(state["value"]))) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": - direction = Direction.FromElement(doc, subelem) + direction = Direction.FromXML(doc, subelem) elif tag == "term": term = INumberDef(subelem.text) try: @@ -142,13 +136,14 @@ class Speed(object): self.value = value def __getstate__(self): - return dict(type=self.type, value=self.value.expr) + return [('type', self.type), ('value', self.value.expr)] def __setstate__(self, state): + state = dict(state) self.__init__(state["type"], NumberDef(state["value"])) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.get("type", "absolute"), NumberDef(element.text)) @@ -166,21 +161,22 @@ class ChangeSpeed(object): self.speed = speed def __getstate__(self): - return dict(frames=self.term.expr, - type=self.speed.type, - value=self.speed.value.expr) + return [('frames', self.term.expr), + ('type', self.speed.type), + ('value', self.speed.value.expr)] def __setstate__(self, state): + state = dict(state) self.__init__(INumberDef(state["frames"]), Speed(state["type"], NumberDef(state["value"]))) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "speed": - speed = Speed.FromElement(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "term": term = INumberDef(subelem.text) try: @@ -208,7 +204,7 @@ class Wait(object): self.__init__(INumberDef(state["frames"])) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(INumberDef(element.text)) @@ -231,7 +227,7 @@ class Tag(object): self.__init__(state["tag"]) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.text) @@ -248,7 +244,7 @@ class Untag(object): self.__init__(state["tag"]) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.text) @@ -259,7 +255,7 @@ class Vanish(object): pass @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls() @@ -274,22 +270,23 @@ class Repeat(object): self.action = action def __getstate__(self): - return dict(times=self.times.expr, action=self.action) + return [('times', self.times.expr), ('action', self.action)] def __setstate__(self, state): + state = dict(state) self.__init__(INumberDef(state["times"]), state["action"]) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "times": times = INumberDef(subelem.text) elif tag == "action": - action = ActionDef.FromElement(doc, subelem) + action = ActionDef.FromXML(doc, subelem) elif tag == "actionRef": - action = ActionRef.FromElement(doc, subelem) + action = ActionRef.FromXML(doc, subelem) try: return cls(times, action) except UnboundLocalError as exc: @@ -313,19 +310,20 @@ class Accel(object): self.vertical = vertical def __getstate__(self): - state = dict(frames=self.term.expr) + state = [('frames', self.term.expr)] if self.horizontal: - state["horizontal"] = self.horizontal + state.append(('horizontal', self.horizontal)) if self.vertical: - state["vertical"] = self.vertical + state.append(('vertical', self.vertical)) return state def __setstate__(self, state): - self.__init__(INumberDef(state["term"]), state.get("horizontal"), + state = dict(state) + self.__init__(INumberDef(state["frames"]), state.get("horizontal"), state.get("vertical")) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" horizontal = None vertical = None @@ -335,9 +333,9 @@ class Accel(object): if tag == "term": term = INumberDef(subelem.text) elif tag == "horizontal": - horizontal = Speed.FromElement(doc, subelem) + horizontal = Speed.FromXML(doc, subelem) elif tag == "vertical": - vertical = Speed.FromElement(doc, subelem) + vertical = Speed.FromXML(doc, subelem) try: return cls(term, horizontal, vertical) @@ -366,20 +364,21 @@ class BulletDef(object): self.actions = list(actions) def __getstate__(self): - state = dict() + state = [] if self.direction: - state["direction"] = self.direction + state.append(("direction", self.direction)) if self.speed: - state["speed"] = self.speed + state.append(("speed", self.speed)) if self.actions: - state["actions"] = self.actions + state.append(("actions", self.actions)) return state def __setstate__(self, state): + state = dict(state) self.__init__(**state) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" actions = [] speed = None @@ -387,15 +386,15 @@ class BulletDef(object): for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": - direction = Direction.FromElement(doc, subelem) + direction = Direction.FromXML(doc, subelem) elif tag == "speed": - speed = Speed.FromElement(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "action": - actions.append(ActionDef.FromElement(doc, subelem)) + actions.append(ActionDef.FromXML(doc, subelem)) elif tag == "actionRef": - actions.append(ActionRef.FromElement(doc, subelem)) + actions.append(ActionRef.FromXML(doc, subelem)) dfn = cls(actions, direction, speed) - doc.bullets[element.get("label")] = dfn + doc._bullets[element.get("label")] = dfn return dfn def __call__(self, params, rank): @@ -417,20 +416,23 @@ class BulletRef(object): self.params = ParamList() if params is None else params def __getstate__(self): - state = dict(bullet=self.bullet) + state = [] if self.params.params: - state["params"] = self.params.__getstate__() + params = [param.expr for param in self.params.params] + state.append(("params", params)) + state.append(('bullet', self.bullet)) return state def __setstate__(self, state): + state = dict(state) bullet = state["bullet"] params = [NumberDef(param) for param in state.get("params", [])] - self.__init__(bullet, params) + self.__init__(bullet, ParamList(params)) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" - bullet = cls(element.get("label"), ParamList.FromElement(doc, element)) + bullet = cls(element.get("label"), ParamList.FromXML(doc, element)) doc._bullet_refs.append(bullet) return bullet @@ -446,7 +448,7 @@ class ActionDef(object): To support parsing new actions, add tags to ActionDef.CONSTRUCTORS. It maps tag names to classes with a - FromElement classmethod, which take the BulletML instance and + FromXML classmethod, which take the BulletML instance and ElementTree element as arguments. """ @@ -460,10 +462,11 @@ class ActionDef(object): return dict(actions=self.actions) def __setstate__(self, state): - self.__init__(state) + state = dict(state) + self.__init__(state["actions"]) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" actions = [] for subelem in element.getchildren(): @@ -473,9 +476,9 @@ class ActionDef(object): except KeyError: continue else: - actions.append(ctr.FromElement(doc, subelem)) + actions.append(ctr.FromXML(doc, subelem)) dfn = cls(actions) - doc.actions[element.get("label")] = dfn + doc._actions[element.get("label")] = dfn return dfn def __call__(self, params, rank): @@ -492,20 +495,23 @@ class ActionRef(object): self.params = params or ParamList() def __getstate__(self): - state = dict(action=self.action) + state = [] if self.params.params: - state["params"] = self.params.__getstate__() + params = [param.expr for param in self.params.params] + state.append(("params", params)) + state.append(('action', self.action)) return state def __setstate__(self, state): + state = dict(state) action = state["action"] params = [NumberDef(param) for param in state.get("params", [])] - self.__init__(action, params) + self.__init__(action, ParamList(params)) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" - action = cls(element.get("label"), ParamList.FromElement(doc, element)) + action = cls(element.get("label"), ParamList.FromXML(doc, element)) doc._action_refs.append(action) return action @@ -529,18 +535,19 @@ class Offset(object): self.y = y def __getstate__(self): - state = dict(type=self.type) + state = [('type', self.type)] if self.x: - state["x"] = self.x.expr + state.append(('x', self.x.expr)) if self.y: - state["y"] = self.y.expr + state.append(('y', self.y.expr)) return state def __setstate__(self, state): + state = dict(state) self.__init__(state["type"], state.get("x"), state.get("y")) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" type = element.get("type", "relative") x = None @@ -567,25 +574,31 @@ class FireDef(object): self.offset = offset def __getstate__(self): + state = [] + if self.direction: + state.append(("direction", self.direction)) + if self.speed: + state.append(("speed", self.speed)) + if self.offset: + state.append(("offset", self.offset)) try: params = self.bullet.params except AttributeError: - state = dict(bullet=self.bullet) + state.append(('bullet', self.bullet)) else: if params.params: - state = dict(bullet=self.bullet) + state.append(('bullet', self.bullet)) else: - state = dict(bullet=self.bullet.bullet) - if self.direction: - state["direction"] = self.direction - if self.speed: - state["speed"] = self.speed - if self.offset: - state["offset"] = self.offset + # Strip out empty BulletRefs. + state.append(('bullet', self.bullet.bullet)) return state + def __setstate__(self, state): + state = dict(state) + self.__init__(**state) + @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" direction = None speed = None @@ -594,21 +607,21 @@ class FireDef(object): for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": - direction = Direction.FromElement(doc, subelem, "aim") + direction = Direction.FromXML(doc, subelem, "aim") elif tag == "speed": - speed = Speed.FromElement(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "bullet": - bullet = BulletDef.FromElement(doc, subelem) + bullet = BulletDef.FromXML(doc, subelem) elif tag == "bulletRef": - bullet = BulletRef.FromElement(doc, subelem) + bullet = BulletRef.FromXML(doc, subelem) elif tag == "offset": - offset = Offset.FromElement(doc, subelem) + offset = Offset.FromXML(doc, subelem) try: fire = cls(bullet, direction, speed, offset) except UnboundLocalError as exc: raise ParseError(str(exc)) else: - doc.fires[element.get("label")] = fire + doc._fires[element.get("label")] = fire return fire def __call__(self, params, rank): @@ -631,20 +644,23 @@ class FireRef(object): self.params = params or ParamList() def __getstate__(self): - state = dict(fire=self.fire) + state = [] if self.params.params: - state["params"] = self.params.__getstate__() + params = [param.expr for param in self.params.params] + state.append(("params", params)) + state.append(('fire', self.fire)) return state def __setstate__(self, state): + state = dict(state) fire = state["fire"] params = [NumberDef(param) for param in state.get("params", [])] - self.__init__(fire, params) + self.__init__(fire, ParamList(params)) @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" - fired = cls(element.get("label"), ParamList.FromElement(doc, element)) + fired = cls(element.get("label"), ParamList.FromXML(doc, element)) doc._fire_refs.append(fired) return fired @@ -658,11 +674,11 @@ class FireRef(object): class BulletML(object): """BulletML document. - A BulletML document is a collection of bullets, actions, and - firings, as well as a base game type. + A BulletML document is a collection of top-level actions and the + base game type. You can add tags to the BulletML.CONSTRUCTORS dictionary to extend - its parsing. It maps tag names to classes with a FromElement + its parsing. It maps tag names to classes with a FromXML classmethod, which take the BulletML instance and ElementTree element as arguments. @@ -674,21 +690,20 @@ class BulletML(object): fire=FireDef, ) - def __init__(self, type="none", bullets=None, fires=None, actions=None): + def __init__(self, type="none", actions=None): self.type = type - self.bullets = {} if bullets is None else bullets - self.actions = {} if actions is None else actions - self.fires = {} if fires is None else fires + self.actions = [] if actions is None else actions def __getstate__(self): - return dict(type=self.type, actions=self.actions) + return [('type', self.type), ('actions', self.actions)] def __setstate__(self, state): - self.__init__(state["type"], state.get("actions")) + state = dict(state) + self.__init__(state["type"], actions=state.get("actions")) @classmethod - def FromDocument(cls, source): - """Return a BulletML instance based on a string or file-like.""" + def FromXML(cls, source): + """Return a BulletML instance based on XML.""" if not hasattr(source, 'read'): source = StringIO(source) @@ -697,6 +712,9 @@ class BulletML(object): self = cls(type=root.get("type", "none")) + self._bullets = {} + self._actions = {} + self._fires = {} self._bullet_refs = [] self._action_refs = [] self._fire_refs = [] @@ -704,38 +722,66 @@ class BulletML(object): for element in root.getchildren(): tag = realtag(element) if tag in self.CONSTRUCTORS: - self.CONSTRUCTORS[tag].FromElement(self, element) + self.CONSTRUCTORS[tag].FromXML(self, element) try: for ref in self._bullet_refs: - ref.bullet = self.bullets[ref.bullet] + ref.bullet = self._bullets[ref.bullet] for ref in self._fire_refs: - ref.fire = self.fires[ref.fire] + ref.fire = self._fires[ref.fire] for ref in self._action_refs: - ref.action = self.actions[ref.action] + ref.action = self._actions[ref.action] except KeyError as exc: raise ParseError("unknown reference %s" % exc) + self.actions = [act for name, act in self._actions.items() + if name and name.startswith("top")] + del(self._bullet_refs) del(self._action_refs) del(self._fire_refs) + del(self._bullets) + del(self._actions) + del(self._fires) + + return self - self.bullets.pop(None, None) - self.actions.pop(None, None) - self.fires.pop(None, None) + @classmethod + def FromYAML(cls, source): + """Create a BulletML instance based on YAML.""" - return self + # Late import to avoid a circular dependency. + try: + import bulletml.bulletyaml + import yaml + except ImportError: + raise ParseError("PyYAML is not available") + else: + try: + return yaml.load(source) + except Exception, exc: + raise ParseError(str(exc)) + + @classmethod + def FromDocument(cls, source): + """Create a BulletML instance based on a seekable file or string. - @property - def top(self): - """Get a list of all top-level actions.""" - return [dfn for name, dfn in self.actions.items() - if name and name.startswith("top")] + This attempts to autodetect if the stream is XML or YAML. + """ + if not hasattr(source, 'read'): + source = StringIO(source) + start = source.read(1) + source.seek(0) + if start == "<": + return cls.FromXML(source) + elif start == "!" or start == "#": + return cls.FromYAML(source) + else: + raise ParseError("unknown initial character %r" % start) def __repr__(self): - return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % ( - type(self).__name__, self.type, self.bullets, self.actions, - self.fires) + return "%s(type=%r, actions=%r)" % ( + type(self).__name__, self.type, self.actions) ActionDef.CONSTRUCTORS = dict( repeat=Repeat,