Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Release 0.3.0
#############

Not backwards compatible with 0.2 version
Now supports multiple machines per class


46 changes: 24 additions & 22 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,44 +32,46 @@ Basic Usage
class Person():
name = 'Billy'

sleeping = State(initial=True)
running = State()
cleaning = State()
status = MongoEngineStateMachine(
sleeping=State(initial=True),
running=State(),
cleaning=State(),

run = Event(from_states=sleeping, to_state=running)
cleanup = Event(from_states=running, to_state=cleaning)
sleep = Event(from_states=(running, cleaning), to_state=sleeping)
run=Event(from_states='sleeping', to_state='running'),
cleanup=Event(from_states='running', to_state='cleaning'),
sleep=Event(from_states=('running', 'cleaning'), to_state='sleeping')
)

@before('sleep')
@status.before('sleep')
def do_one_thing(self):
print "{} is sleepy".format(self.name)
print("{} is sleepy".format(self.name))

@before('sleep')
@status.before('sleep')
def do_another_thing(self):
print "{} is REALLY sleepy".format(self.name)
print("{} is REALLY sleepy".format(self.name))

@after('sleep')
@status.after('sleep')
def snore(self):
print "Zzzzzzzzzzzz"
print("Zzzzzzzzzzzz")

@after('sleep')
def big_snore(self):
print "Zzzzzzzzzzzzzzzzzzzzzz"
@status.after('sleep')
def snore(self):
print("Zzzzzzzzzzzzzzzzzzzzzz")

person = Person()
print person.current_state == Person.sleeping # True
print person.is_sleeping # True
print person.is_running # False
person.run()
print person.is_running # True
person.sleep()
print person.status == Person.status.sleeping # True
print person.status.is_sleeping # True
print person.status.is_running # False
person.status.run()
print person.status.is_running # True
person.status.sleep()

# Billy is sleepy
# Billy is REALLY sleepy
# Zzzzzzzzzzzz
# Zzzzzzzzzzzzzzzzzzzzzz

print person.is_sleeping # True
print person.status.is_sleeping # True

Features
--------
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def get_packages():
required_modules = []

setup(name='state_machine',
version='0.2.9',
version='0.3.0',
description='Python State Machines for Humans',
url='http://github.com/jtushman/state_machine',
author='Jonathan Tushman',
Expand Down
112 changes: 80 additions & 32 deletions state_machine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,97 @@
import inspect

from state_machine.models import Event, State, InvalidStateTransition
from state_machine.orm import get_adaptor
from state_machine.models import Event, State, StateMachine, InvalidStateTransition, MongoEngineStateMachine, \
SqlAlchemyStateMachine, AbstractStateMachine, StateTransitionFailure

_temp_callback_cache = None
try:
import mongoengine
except ImportError as e:
mongoengine = None

def get_callback_cache():
global _temp_callback_cache
if _temp_callback_cache is None:
_temp_callback_cache = dict()
return _temp_callback_cache
try:
import sqlalchemy
from sqlalchemy import inspection
from sqlalchemy.orm import instrumentation
from sqlalchemy.orm import Session
except ImportError:
sqlalchemy = None
instrumentation = None

def get_function_name(frame):
return inspect.getouterframes(frame)[1][3]

def before(before_what):
def wrapper(func):
frame = inspect.currentframe()
calling_class = get_function_name(frame)
def acts_as_state_machine(original_class):
""" Decorates classes that contain StateMachines to update the classes constructors and underlying
structure if needed.

calling_class_dict = get_callback_cache().setdefault(calling_class, {'before': {}, 'after': {}})
calling_class_dict['before'].setdefault(before_what, []).append(func)
For example for mongoengine it will add the necessary fields needed,
and for sqlalchemy it updates the constructure to set the default states
"""

return func
if mongoengine and issubclass(original_class, mongoengine.Document):
return _modified_class_for_mongoengine(original_class)
elif sqlalchemy is not None and hasattr(original_class, '_sa_class_manager') and isinstance(
original_class._sa_class_manager, instrumentation.ClassManager):
return _modified_class_for_sqlalchemy(original_class)
else:
return modified_class(original_class)

return wrapper

def modified_class(original_class):
for member, value in inspect.getmembers(original_class):

if isinstance(value, AbstractStateMachine):
name, machine = member, value
setattr(machine, 'name', name)

# add extra_class memebers is necessary as such the case for mongo and sqlalchemy
for name in machine.extra_class_members:
setattr(original_class, name, machine.extra_class_members[name])
return original_class


def _modified_class_for_mongoengine(original_class):
class_name = original_class.__name__
class_dict = dict()
class_dict.update(original_class.__dict__)
extra_members = {}
for member in class_dict:
value = class_dict[member]

if isinstance(value, AbstractStateMachine):
name, machine = member, value
setattr(machine, 'name', name)

# add extra_class memebers is necessary as such the case for mongo and sqlalchemy
for name in machine.extra_class_members:
extra_members[name] = machine.extra_class_members[name]

class_dict.update(extra_members)
clazz = type(class_name, original_class.__bases__, class_dict)
return clazz


def _modified_class_for_sqlalchemy(original_class):
mod_class = modified_class(original_class)

orig_init = mod_class.__init__

def new_init_builder():
def new_init(self, *args, **kwargs):
orig_init(self, *args, **kwargs)

for member, value in inspect.getmembers(mod_class):

if isinstance(value, AbstractStateMachine):
machine = value
setattr(self, machine.underlying_name, machine.initial_state.name)

return new_init

mod_class.__init__ = new_init_builder()
return mod_class

def after(after_what):
def wrapper(func):

frame = inspect.currentframe()
calling_class = get_function_name(frame)

calling_class_dict = get_callback_cache().setdefault(calling_class, {'before': {}, 'after': {}})
calling_class_dict['after'].setdefault(after_what, []).append(func)

return func

return wrapper


def acts_as_state_machine(original_class):
adaptor = get_adaptor(original_class)
global _temp_callback_cache
modified_class = adaptor.modifed_class(original_class, _temp_callback_cache)
_temp_callback_cache = None
return modified_class
Loading