diff options
author | Paul McGuire <ptmcg@austin.rr.com> | 2019-04-15 18:53:05 -0500 |
---|---|---|
committer | Paul McGuire <ptmcg@austin.rr.com> | 2019-04-15 18:53:05 -0500 |
commit | 6c447c4d663d0badddd84976033504c855325ce5 (patch) | |
tree | 81c7f74073236b499c3656f82a8179033c02f8b9 /examples | |
parent | adf8dd00b736ba1934914e1db78528af02662b65 (diff) | |
download | pyparsing-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.py | 21 | ||||
-rw-r--r-- | examples/statemachine/libraryBookDemo.py | 28 | ||||
-rw-r--r-- | examples/statemachine/statemachine.py | 134 | ||||
-rw-r--r-- | examples/statemachine/trafficLightDemo.py | 12 |
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): |