summaryrefslogtreecommitdiff
path: root/examples/statemachine/statemachine.py
diff options
context:
space:
mode:
Diffstat (limited to 'examples/statemachine/statemachine.py')
-rw-r--r--examples/statemachine/statemachine.py134
1 files changed, 102 insertions, 32 deletions
diff --git a/examples/statemachine/statemachine.py b/examples/statemachine/statemachine.py
index 2e35869..c46e3d0 100644
--- a/examples/statemachine/statemachine.py
+++ b/examples/statemachine/statemachine.py
@@ -20,34 +20,36 @@ except ImportError:
DEBUG = False
-from pyparsing import Word, Group, ZeroOrMore, alphas, \
- alphanums, ParserElement, ParseException, ParseSyntaxException, \
- Empty, LineEnd, OneOrMore, col, Keyword, pythonStyleComment, \
- StringEnd, traceParseAction
+import pyparsing as pp
+
+# define basic exception for invalid state transitions - state machine classes will subclass to
+# define their own specific exception type
class InvalidTransitionException(Exception): pass
-ident = Word(alphas + "_", alphanums + "_$")
+ident = pp.Word(pp.alphas + "_", pp.alphanums + "_$")
+
+# add parse-time condition to make sure we do not allow any Python keywords to be used as
+# statemachine identifiers
def no_keywords_allowed(s, l, t):
wd = t[0]
return not keyword.iskeyword(wd)
-
ident.addCondition(no_keywords_allowed, message="cannot use a Python keyword for state or transition identifier")
stateTransition = ident("from_state") + "->" + ident("to_state")
-stateMachine = (Keyword("statemachine") + ident("name") + ":"
- + OneOrMore(Group(stateTransition))("transitions"))
+stateMachine = (pp.Keyword("statemachine") + ident("name") + ":"
+ + pp.OneOrMore(pp.Group(stateTransition))("transitions"))
namedStateTransition = (ident("from_state")
+ "-(" + ident("transition") + ")->"
+ ident("to_state"))
-namedStateMachine = (Keyword("statemachine") + ident("name") + ":"
- + OneOrMore(Group(namedStateTransition))("transitions"))
+namedStateMachine = (pp.Keyword("statemachine") + ident("name") + ":"
+ + pp.OneOrMore(pp.Group(namedStateTransition))("transitions"))
def expand_state_definition(source, loc, tokens):
- indent = " " * (col(loc, source) - 1)
+ indent = " " * (pp.col(loc, source) - 1)
statedef = []
# build list of states
@@ -66,7 +68,7 @@ def expand_state_definition(source, loc, tokens):
" return self.__class__.__name__",
" @classmethod",
" def states(cls):",
- " return list(cls.__subclasses__)",
+ " return list(cls.__subclasses__())",
" def next_state(self):",
" return self._next_state_class()",
])
@@ -77,13 +79,34 @@ def expand_state_definition(source, loc, tokens):
# define state->state transitions
statedef.extend("{0}._next_state_class = {1}".format(s, fromTo[s]) for s in states if s in fromTo)
+ statedef.extend([
+ "class {baseStateClass}Mixin:".format(baseStateClass=baseStateClass),
+ " def __init__(self):",
+ " self._state = None",
+
+ " def initialize_state(self, init_state):",
+ " self._state = init_state",
+
+ " @property",
+ " def state(self):",
+ " return self._state",
+
+ " # get behavior/properties from current state",
+ " def __getattr__(self, attrname):",
+ " attr = getattr(self._state, attrname)",
+ " return attr",
+
+ " def __str__(self):",
+ " return '{0}: {1}'.format(self.__class__.__name__, self._state)",
+ ])
+
return indent + ("\n" + indent).join(statedef) + "\n"
stateMachine.setParseAction(expand_state_definition)
def expand_named_state_definition(source, loc, tokens):
- indent = " " * (col(loc, source) - 1)
+ indent = " " * (pp.col(loc, source) - 1)
statedef = []
# build list of states and transitions
states = set()
@@ -108,45 +131,92 @@ def expand_named_state_definition(source, loc, tokens):
# define state transition class
statedef.extend([
- "class %sTransition:" % baseStateClass,
+ "class {baseStateClass}Transition:".format(baseStateClass=baseStateClass),
" def __str__(self):",
" return self.transitionName",
])
statedef.extend(
- "{0} = {1}Transition()".format(tn, baseStateClass)
+ "{tn_name} = {baseStateClass}Transition()".format(tn_name=tn,
+ baseStateClass=baseStateClass)
for tn in transitions)
- statedef.extend("{0}.transitionName = '{1}'".format(tn, tn)
+ statedef.extend("{tn_name}.transitionName = '{tn_name}'".format(tn_name=tn)
for tn in transitions)
+ statedef.extend([
+ "from statemachine import InvalidTransitionException as BaseTransitionException",
+ "class InvalidTransitionException(BaseTransitionException): pass",
+ ])
+
# define base class for state classes
- excmsg = "'" + tokens.name + \
- '.%s does not support transition "%s"' \
- "'% (self, name)"
statedef.extend([
"class %s(object):" % baseStateClass,
" def __str__(self):",
" return self.__class__.__name__",
- " def next_state(self, name):",
+ " @classmethod",
+ " def states(cls):",
+ " return list(cls.__subclasses__())",
+ " @classmethod",
+ " def next_state(cls, name):",
" try:",
- " return self.tnmap[tn]()",
+ " return cls.tnmap[name]()",
" except KeyError:",
- " import statemachine",
- " raise statemachine.InvalidTransitionException(%s)" % excmsg,
- " def __getattr__(self, name):",
- " import statemachine",
- " raise statemachine.InvalidTransitionException(%s)" % excmsg,
+ " raise InvalidTransitionException('%s does not support transition %r'% (cls.__name__, name))",
+ " def __bad_tn(name):",
+ " def _fn(cls):",
+ " raise InvalidTransitionException('%s does not support transition %r'% (cls.__name__, name))",
+ " _fn.__name__ = name",
+ " return _fn",
])
+ # define default 'invalid transition' methods in base class, valid transitions will be implemented in subclasses
+ statedef.extend(
+ " {tn_name} = classmethod(__bad_tn({tn_name!r}))".format(tn_name=tn)
+ for tn in transitions)
+
# define all state classes
statedef.extend("class %s(%s): pass" % (s, baseStateClass)
- for s in states)
+ for s in states)
- # define state transition maps and transition methods
+ # define state transition methods for valid transitions from each state
for s in states:
trns = list(fromTo[s].items())
- statedef.append("%s.tnmap = {%s}" % (s, ", ".join("%s:%s" % tn for tn in trns)))
- statedef.extend("%s.%s = staticmethod(lambda: %s())" % (s, tn_, to_)
- for tn_, to_ in trns)
+ # statedef.append("%s.tnmap = {%s}" % (s, ", ".join("%s:%s" % tn for tn in trns)))
+ statedef.extend("%s.%s = classmethod(lambda cls: %s())" % (s, tn_, to_)
+ for tn_, to_ in trns)
+
+ statedef.extend([
+ "{baseStateClass}.transitions = classmethod(lambda cls: [{transition_class_list}])".format(
+ baseStateClass=baseStateClass,
+ transition_class_list = ', '.join("cls.{0}".format(tn) for tn in transitions)
+ ),
+ "{baseStateClass}.transition_names = [tn.__name__ for tn in {baseStateClass}.transitions()]".format(
+ baseStateClass=baseStateClass
+ )
+ ])
+
+ statedef.extend([
+ "class {baseStateClass}Mixin:".format(baseStateClass=baseStateClass),
+ " def __init__(self):",
+ " self._state = None",
+
+ " def initialize_state(self, init_state):",
+ " self._state = init_state",
+
+ " @property",
+ " def state(self):",
+ " return self._state",
+
+ " # get behavior/properties from current state",
+ " def __getattr__(self, attrname):",
+ " attr = getattr(self._state, attrname)",
+ " if attrname in {baseStateClass}.transition_names:".format(baseStateClass=baseStateClass),
+ " return lambda x=self: setattr(x, '_state', attr())",
+ " return attr",
+
+ " def __str__(self):",
+ " return '{0}: {1}'.format(self.__class__.__name__, self._state)"
+ ])
+
return indent + ("\n" + indent).join(statedef) + "\n"
@@ -237,7 +307,7 @@ class PystateImporter(SuffixImporter):
# MATT-NOTE: re-worked :func:`get_state_machine`
# convert any statemachine expressions
- stateMachineExpr = (stateMachine | namedStateMachine).ignore(pythonStyleComment)
+ stateMachineExpr = (stateMachine | namedStateMachine).ignore(pp.pythonStyleComment)
generated_code = stateMachineExpr.transformString(data)
if DEBUG: print(generated_code)