X-Git-Url: https://git.yukkurigames.com/?p=python-bulletml.git;a=blobdiff_plain;f=bulletml%2Fparser.py;h=9d515a24b9e476343a9d829b1492454095850559;hp=2ad648c20c97eb16e53a08b2c17c7a453b19799a;hb=d72980b6368d0ac1de1ae1091bfb0582e9afcb1d;hpb=4bb0077fd274237fb81db63460620470b1f6d520 diff --git a/bulletml/parser.py b/bulletml/parser.py index 2ad648c..9d515a2 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -13,6 +13,12 @@ import math from xml.etree.ElementTree import ElementTree +# Python 3 moved this for no really good reason. +try: + from sys import intern +except ImportError: + pass + try: from io import StringIO except ImportError: @@ -24,7 +30,8 @@ except ImportError: from bulletml.errors import Error from bulletml.expr import NumberDef, INumberDef -__all_ = ["ParseError", "BulletML"] + +__all__ = ["ParseError", "BulletML"] class ParseError(Error): """Raised when an error occurs parsing the XML structure.""" @@ -44,7 +51,7 @@ 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"]) @@ -63,11 +70,18 @@ class Direction(object): def __init__(self, type, value): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) - self.type = type + self.type = intern(type) self.value = value + def __getstate__(self): + 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)) @@ -85,13 +99,23 @@ class ChangeDirection(object): self.term = term self.direction = direction + def __getstate__(self): + 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: @@ -114,11 +138,18 @@ class Speed(object): def __init__(self, type, value): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) - self.type = type + self.type = intern(type) self.value = value + def __getstate__(self): + 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)) @@ -135,13 +166,23 @@ class ChangeSpeed(object): self.term = term self.speed = speed + def __getstate__(self): + 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: @@ -162,8 +203,14 @@ class Wait(object): def __init__(self, frames): self.frames = frames + def __getstate__(self): + return dict(frames=self.frames.expr) + + def __setstate__(self, state): + 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)) @@ -179,8 +226,14 @@ class Tag(object): def __init__(self, tag): self.tag = tag + def __getstate__(self): + return dict(tag=self.tag) + + def __setstate__(self, state): + 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) @@ -189,9 +242,15 @@ class Untag(object): def __init__(self, tag): self.tag = tag + + def __getstate__(self): + return dict(tag=self.tag) + + def __setstate__(self, state): + 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) @@ -202,7 +261,7 @@ class Vanish(object): pass @classmethod - def FromElement(cls, doc, element): + def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls() @@ -215,18 +274,25 @@ class Repeat(object): def __init__(self, times, action): self.times = times self.action = action + + def __getstate__(self): + 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: @@ -249,8 +315,21 @@ class Accel(object): self.horizontal = horizontal self.vertical = vertical + def __getstate__(self): + state = [('frames', self.term.expr)] + if self.horizontal: + state.append(('horizontal', self.horizontal)) + if self.vertical: + state.append(('vertical', self.vertical)) + return state + + def __setstate__(self, state): + 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 @@ -260,9 +339,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) @@ -285,29 +364,49 @@ class BulletDef(object): direction = None speed = None - def __init__(self, actions=[], direction=None, speed=None): + def __init__(self, actions=(), direction=None, speed=None, tags=()): self.direction = direction self.speed = speed self.actions = list(actions) + self.tags = set(tags) + + def __getstate__(self): + state = [] + if self.direction: + state.append(("direction", self.direction)) + if self.speed: + state.append(("speed", self.speed)) + if self.actions: + state.append(("actions", self.actions)) + if self.tags: + state.append(("tags", list(self.tags))) + 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 direction = None + tags = set() 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)) - dfn = cls(actions, direction, speed) - doc.bullets[element.get("label")] = dfn + actions.append(ActionRef.FromXML(doc, subelem)) + elif tag == "tag": + tags.add(subelem.text) + dfn = cls(actions, direction, speed, tags) + doc._bullets[element.get("label")] = dfn return dfn def __call__(self, params, rank): @@ -315,6 +414,7 @@ class BulletDef(object): return ( self.direction and self.direction(params, rank), self.speed and self.speed(params, rank), + self.tags, actions) def __repr__(self): @@ -326,12 +426,26 @@ class BulletRef(object): def __init__(self, bullet, params=None): self.bullet = bullet - self.params = params or ParamList() + self.params = ParamList() if params is None else params + + def __getstate__(self): + state = [] + if self.params.params: + 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, 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 @@ -347,7 +461,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. """ @@ -357,8 +471,15 @@ class ActionDef(object): def __init__(self, actions): self.actions = list(actions) + def __getstate__(self): + return dict(actions=self.actions) + + def __setstate__(self, 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(): @@ -368,9 +489,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): @@ -386,10 +507,24 @@ class ActionRef(object): self.action = action self.params = params or ParamList() + def __getstate__(self): + state = [] + if self.params.params: + 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, 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 @@ -408,12 +543,24 @@ class Offset(object): def __init__(self, type, x, y): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) - self.type = type + self.type = intern(type) self.x = x self.y = y + def __getstate__(self): + state = [('type', self.type)] + if self.x: + state.append(('x', self.x.expr)) + if self.y: + 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 @@ -433,46 +580,78 @@ class Offset(object): class FireDef(object): """Fire definition (creates a bullet).""" - def __init__(self, bullet, direction=None, speed=None, offset=None): + def __init__( + self, bullet, direction=None, speed=None, offset=None, tags=()): self.bullet = bullet self.direction = direction self.speed = speed self.offset = offset + self.tags = set(tags) + + 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)) + if self.tags: + state.append(("tags", list(self.tags))) + try: + params = self.bullet.params + except AttributeError: + state.append(('bullet', self.bullet)) + else: + if params.params: + state.append(('bullet', self.bullet)) + else: + # 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 offset = None + tags = set() 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) + elif tag == "tag": + tags.add(subelem.text) try: - fire = cls(bullet, direction, speed, offset) + fire = cls(bullet, direction, speed, offset, tags) 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): - direction, speed, actions = self.bullet(params, rank) + direction, speed, tags, actions = self.bullet(params, rank) if self.direction: direction = self.direction(params, rank) if self.speed: speed = self.speed(params, rank) - return direction, speed, actions, self.offset + tags = tags.union(self.tags) + return direction, speed, self.offset, tags, actions def __repr__(self): return "%s(direction=%r, speed=%r, bullet=%r)" % ( @@ -485,10 +664,24 @@ class FireRef(object): self.fire = fire self.params = params or ParamList() + def __getstate__(self): + state = [] + if self.params.params: + 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, 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 @@ -502,11 +695,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. @@ -518,62 +711,98 @@ class BulletML(object): fire=FireDef, ) - def __init__(self, type="none", bullets=None, fires=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 + def __init__(self, type="none", actions=None): + self.type = intern(type) + self.actions = [] if actions is None else actions + + def __getstate__(self): + return [('type', self.type), ('actions', self.actions)] + + def __setstate__(self, state): + 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) tree = ElementTree() root = tree.parse(source) - self = cls(type=root.get("type", "none")) + doc = cls(type=root.get("type", "none")) - self._bullet_refs = [] - self._action_refs = [] - self._fire_refs = [] + doc._bullets = {} + doc._actions = {} + doc._fires = {} + doc._bullet_refs = [] + doc._action_refs = [] + doc._fire_refs = [] for element in root.getchildren(): tag = realtag(element) - if tag in self.CONSTRUCTORS: - self.CONSTRUCTORS[tag].FromElement(self, element) + if tag in doc.CONSTRUCTORS: + doc.CONSTRUCTORS[tag].FromXML(doc, element) try: - for ref in self._bullet_refs: - ref.bullet = self.bullets[ref.bullet] - for ref in self._fire_refs: - ref.fire = self.fires[ref.fire] - for ref in self._action_refs: - ref.action = self.actions[ref.action] + for ref in doc._bullet_refs: + ref.bullet = doc._bullets[ref.bullet] + for ref in doc._fire_refs: + ref.fire = doc._fires[ref.fire] + for ref in doc._action_refs: + ref.action = doc._actions[ref.action] except KeyError as exc: raise ParseError("unknown reference %s" % exc) - del(self._bullet_refs) - del(self._action_refs) - del(self._fire_refs) + doc.actions = [act for name, act in doc._actions.items() + if name and name.startswith("top")] + + del(doc._bullet_refs) + del(doc._action_refs) + del(doc._fire_refs) + del(doc._bullets) + del(doc._actions) + del(doc._fires) + + return doc - 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.""" + + # 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 as exc: + raise ParseError(str(exc)) - return self + @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,