summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorPaul McGuire <ptmcg@austin.rr.com>2019-04-15 18:53:05 -0500
committerPaul McGuire <ptmcg@austin.rr.com>2019-04-15 18:53:05 -0500
commit6c447c4d663d0badddd84976033504c855325ce5 (patch)
tree81c7f74073236b499c3656f82a8179033c02f8b9 /examples
parentadf8dd00b736ba1934914e1db78528af02662b65 (diff)
downloadpyparsing-git-6c447c4d663d0badddd84976033504c855325ce5.tar.gz
Refactor generated State code to use overridden transition methods instead of overriding getattr; add generation of state-managing mixin class to delegate to _state instance variable, and reworked demos to use mixin instead of replicating state code
Diffstat (limited to 'examples')
-rw-r--r--examples/statemachine/documentSignoffDemo.py21
-rw-r--r--examples/statemachine/libraryBookDemo.py28
-rw-r--r--examples/statemachine/statemachine.py134
-rw-r--r--examples/statemachine/trafficLightDemo.py12
4 files changed, 117 insertions, 78 deletions
diff --git a/examples/statemachine/documentSignoffDemo.py b/examples/statemachine/documentSignoffDemo.py
index 58aa208..0cbfc1f 100644
--- a/examples/statemachine/documentSignoffDemo.py
+++ b/examples/statemachine/documentSignoffDemo.py
@@ -7,26 +7,11 @@
import statemachine
import documentsignoffstate
+print('\n'.join(t.__name__ for t in documentsignoffstate.DocumentRevisionState.transitions()))
-class Document:
+class Document(documentsignoffstate.DocumentRevisionStateMixin):
def __init__(self):
- # start light in Red state
- self._state = documentsignoffstate.New()
-
- @property
- def state(self):
- return self._state
-
- # get behavior/properties from current state
- def __getattr__(self, attrname):
- attr = getattr(self._state, attrname)
- if isinstance(getattr(documentsignoffstate, attrname, None),
- documentsignoffstate.DocumentRevisionStateTransition):
- return lambda : setattr(self, '_state', attr())
- return attr
-
- def __str__(self):
- return "{0}: {1}".format(self.__class__.__name__, self._state)
+ self.initialize_state(documentsignoffstate.New())
def run_demo():
diff --git a/examples/statemachine/libraryBookDemo.py b/examples/statemachine/libraryBookDemo.py
index 2e60eac..95e5996 100644
--- a/examples/statemachine/libraryBookDemo.py
+++ b/examples/statemachine/libraryBookDemo.py
@@ -1,25 +1,16 @@
+#
+# libraryBookDemo.py
+#
+# Simple statemachine demo, based on the state transitions given in librarybookstate.pystate
+#
+
import statemachine
import librarybookstate
-class Book:
+class Book(librarybookstate.BookStateMixin):
def __init__(self):
- self._state = librarybookstate.New()
-
- @property
- def state(self):
- return self._state
-
- # get behavior/properties from current state
- def __getattr__(self, attrname):
- attr = getattr(self._state, attrname)
- if isinstance(getattr(librarybookstate, attrname, None),
- librarybookstate.BookStateTransition):
- return lambda: setattr(self, '_state', attr())
- return attr
-
- def __str__(self):
- return "{0}: {1}".format(self.__class__.__name__, self._state)
+ self.initialize_state(librarybookstate.New())
class RestrictedBook(Book):
@@ -50,7 +41,8 @@ def run_demo():
print(book)
try:
book.checkout()
- except statemachine.InvalidTransitionException:
+ except Exception as e: # statemachine.InvalidTransitionException:
+ print(e)
print('..cannot check out reserved book')
book.release()
print(book)
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)
diff --git a/examples/statemachine/trafficLightDemo.py b/examples/statemachine/trafficLightDemo.py
index 4d70541..8358750 100644
--- a/examples/statemachine/trafficLightDemo.py
+++ b/examples/statemachine/trafficLightDemo.py
@@ -8,21 +8,13 @@ import statemachine
import trafficlightstate
-class TrafficLight:
+class TrafficLight(trafficlightstate.TrafficLightStateMixin):
def __init__(self):
- # start light in Red state
- self._state = trafficlightstate.Red()
+ self.initialize_state(trafficlightstate.Red())
def change(self):
self._state = self._state.next_state()
- # get light behavior/properties from current state
- def __getattr__(self, attrname):
- return getattr(self._state, attrname)
-
- def __str__(self):
- return "{0}: {1}".format(self.__class__.__name__, self._state)
-
light = TrafficLight()
for i in range(10):