diff --git a/README.rst b/README.rst index 0542220..631c382 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Basic Usage .. code:: python - @acts_as_state_machine + @acts_as_state_machine() class Person(): name = 'Billy' @@ -41,20 +41,20 @@ Basic Usage sleep = Event(from_states=(running, cleaning), to_state=sleeping) @before('sleep') - def do_one_thing(self): + def do_one_thing(self, param): print "{} is sleepy".format(self.name) @before('sleep') - def do_another_thing(self): - print "{} is REALLY sleepy".format(self.name) + def do_another_thing(self, param): + print "{} is REALLY sleepy and will sleep {}".format(self.name, param) @after('sleep') - def snore(self): + def snore(self, param): print "Zzzzzzzzzzzz" @after('sleep') - def big_snore(self): - print "Zzzzzzzzzzzzzzzzzzzzzz" + def big_snore(self, param): + print "Zzzzzzzzzzzzzzzzzzzzzz (%r)"%param person = Person() print person.current_state == Person.sleeping # True @@ -62,12 +62,12 @@ Basic Usage print person.is_running # False person.run() print person.is_running # True - person.sleep() + person.sleep('a long time') # Billy is sleepy - # Billy is REALLY sleepy + # Billy is REALLY sleepy and will sleep a long time # Zzzzzzzzzzzz - # Zzzzzzzzzzzzzzzzzzzzzz + # Zzzzzzzzzzzzzzzzzzzzzz (a long time) print person.is_sleeping # True @@ -79,6 +79,7 @@ Before / After Callback Decorators You can add callback hooks that get executed before or after an event (see example above). +If a event is called with parameters, all the before/after callback must be defined with a compatible signature *Important:* if the *before* event causes an exception or returns ``False``, the state will not change (transition is blocked) and the @@ -90,87 +91,26 @@ Blocks invalid state transitions An *InvalidStateTransition Exception* will be thrown if you try to move into an invalid state. -ORM support ------------ - -We have basic support for `mongoengine`_, and `sqlalchemy`_. - -Mongoengine -~~~~~~~~~~~ - -Just have your object inherit from ``mongoengine.Document`` and -state\_machine will add a StringField for state. - -*Note:* You must explicitly call #save to persist the document to the -datastore. - -.. code:: python - - @acts_as_state_machine - class Person(mongoengine.Document): - name = mongoengine.StringField(default='Billy') - - 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) - - @before('sleep') - def do_one_thing(self): - print "{} is sleepy".format(self.name) +Name of the state field +~~~~~~~~~~~~~~~~~~~~~~~ - @before('sleep') - def do_another_thing(self): - print "{} is REALLY sleepy".format(self.name) - - @after('sleep') - def snore(self): - print "Zzzzzzzzzzzz" - - @after('sleep') - def snore(self): - print "Zzzzzzzzzzzzzzzzzzzzzz" - - - person = Person() - person.save() - eq_(person.current_state, Person.sleeping) - assert person.is_sleeping - assert not person.is_running - person.run() - assert person.is_running - person.sleep() - assert person.is_sleeping - person.run() - person.save() - - person2 = Person.objects(id=person.id).first() - assert person2.is_running - -.. _mongoengine: http://mongoengine.org/ -.. _sqlalchemy: http://www.sqlalchemy.org/ - -Sqlalchemy -~~~~~~~~~~ +The default name of the state field is "aasm_state". +If the field already exist in the object il will be used as so (beware to initialize it with the default state value yourself in the init function) +If you want to change it, you just have to give it as an argument to the acts_as_state_machine decorator: + @acts_as_state_machine('my_field_name') + class Person(): + ... -All you need to do is have sqlalchemy manage your object. For example: +ORM support +----------- -.. code:: python +It should be done independently - from sqlalchemy.ext.declarative import declarative_base - Base = declarative_base() - @acts_as_state_machine - class Puppy(Base): - ... Issues / Roadmap: ----------------- - Allow multiple state\_machines per object -- Be able to configure the state field Questions / Issues ------------------ diff --git a/docs/conf.py b/docs/conf.py index ea3f84e..d5e7e58 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.2.9' +version = '0.3.0' # The full version, including alpha/beta/rc tags. -release = '0.2.9' +release = '0.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index b0be356..fa6eb57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ Basic Usage .. code:: python - @acts_as_state_machine + @acts_as_state_machine() class Person(): name = 'Billy' @@ -41,20 +41,20 @@ Basic Usage sleep = Event(from_states=(running, cleaning), to_state=sleeping) @before('sleep') - def do_one_thing(self): + def do_one_thing(self, param): print "{} is sleepy".format(self.name) @before('sleep') - def do_another_thing(self): - print "{} is REALLY sleepy".format(self.name) + def do_another_thing(self, param): + print "{} is REALLY sleepy and will sleep {}".format(self.name, param) @after('sleep') - def snore(self): + def snore(self, param): print "Zzzzzzzzzzzz" @after('sleep') - def big_snore(self): - print "Zzzzzzzzzzzzzzzzzzzzzz" + def big_snore(self, param): + print "Zzzzzzzzzzzzzzzzzzzzzz (%r)"%param person = Person() print person.current_state == Person.sleeping # True @@ -62,12 +62,12 @@ Basic Usage print person.is_running # False person.run() print person.is_running # True - person.sleep() + person.sleep('a long time') # Billy is sleepy - # Billy is REALLY sleepy + # Billy is REALLY sleepy and will sleep a long time # Zzzzzzzzzzzz - # Zzzzzzzzzzzzzzzzzzzzzz + # Zzzzzzzzzzzzzzzzzzzzzz (a long time) print person.is_sleeping # True @@ -79,6 +79,7 @@ Before / After Callback Decorators You can add callback hooks that get executed before or after an event (see example above). +If a event is called with parameters, all the before/after callback must be defined with a compatible signature *Important:* if the *before* event causes an exception or returns ``False``, the state will not change (transition is blocked) and the @@ -90,87 +91,25 @@ Blocks invalid state transitions An *InvalidStateTransition Exception* will be thrown if you try to move into an invalid state. -ORM support ------------ - -We have basic support for `mongoengine`_, and `sqlalchemy`_. - -Mongoengine -~~~~~~~~~~~ - -Just have your object inherit from ``mongoengine.Document`` and -state\_machine will add a StringField for state. - -*Note:* You must explicitly call #save to persist the document to the -datastore. - -.. code:: python - - @acts_as_state_machine - class Person(mongoengine.Document): - name = mongoengine.StringField(default='Billy') - - 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) - - @before('sleep') - def do_one_thing(self): - print "{} is sleepy".format(self.name) +Name of the state field +~~~~~~~~~~~~~~~~~~~~~~~ - @before('sleep') - def do_another_thing(self): - print "{} is REALLY sleepy".format(self.name) - - @after('sleep') - def snore(self): - print "Zzzzzzzzzzzz" - - @after('sleep') - def snore(self): - print "Zzzzzzzzzzzzzzzzzzzzzz" - - - person = Person() - person.save() - eq_(person.current_state, Person.sleeping) - assert person.is_sleeping - assert not person.is_running - person.run() - assert person.is_running - person.sleep() - assert person.is_sleeping - person.run() - person.save() - - person2 = Person.objects(id=person.id).first() - assert person2.is_running - -.. _mongoengine: http://mongoengine.org/ -.. _sqlalchemy: http://www.sqlalchemy.org/ - -Sqlalchemy -~~~~~~~~~~ +The default name of the state field is "aasm_state". If you want to chane it, you just have to give it as an argument to the acts_as_state_machine decorator: + @acts_as_state_machine('my_field_name') + class Person(): + ... -All you need to do is have sqlalchemy manage your object. For example: +ORM support +----------- -.. code:: python +No more support for ORM, just plain old object. +If the state field preexists, it's reused and you must give the default value at the initialization else it's created and initialized to the name of the default State. - from sqlalchemy.ext.declarative import declarative_base - Base = declarative_base() - @acts_as_state_machine - class Puppy(Base): - ... Issues / Roadmap: ----------------- - Allow multiple state\_machines per object -- Be able to configure the state field Questions / Issues ------------------ diff --git a/setup.py b/setup.py index 62f38ab..e589092 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ 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', + url='https://github.com/egillet/state_machine', author='Jonathan Tushman', author_email='jonathan@zefr.com', install_requires=required_modules, diff --git a/state_machine/__init__.py b/state_machine/__init__.py index 911d0f3..fb280f3 100644 --- a/state_machine/__init__.py +++ b/state_machine/__init__.py @@ -1,7 +1,7 @@ import inspect from state_machine.models import Event, State, InvalidStateTransition -from state_machine.orm import get_adaptor +from state_machine.orm.base import BaseAdaptor _temp_callback_cache = None @@ -41,9 +41,11 @@ def wrapper(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 +def acts_as_state_machine(state_field_name='aasm_state'): + def f(original_class): + adaptor = BaseAdaptor(original_class) + global _temp_callback_cache + adaptor.modifed_class(original_class, _temp_callback_cache, state_field_name) + _temp_callback_cache = None + return original_class + return f diff --git a/state_machine/orm/__init__.py b/state_machine/orm/__init__.py index 31ef682..e69de29 100644 --- a/state_machine/orm/__init__.py +++ b/state_machine/orm/__init__.py @@ -1,26 +0,0 @@ -from __future__ import absolute_import - -from state_machine.orm.base import BaseAdaptor -from state_machine.orm.mongoengine import get_mongo_adaptor -from state_machine.orm.sqlalchemy import get_sqlalchemy_adaptor - -_adaptors = [get_mongo_adaptor, get_sqlalchemy_adaptor] - - -def get_adaptor(original_class): - # if none, then just keep state in memory - for get_adaptor in _adaptors: - adaptor = get_adaptor(original_class) - if adaptor is not None: - break - else: - adaptor = NullAdaptor(original_class) - return adaptor - - -class NullAdaptor(BaseAdaptor): - def extra_class_members(self, initial_state): - return {"aasm_state": initial_state.name} - - def update(self, document, state_name): - document.aasm_state = state_name diff --git a/state_machine/orm/base.py b/state_machine/orm/base.py index fafe3c2..f8dc65c 100644 --- a/state_machine/orm/base.py +++ b/state_machine/orm/base.py @@ -9,103 +9,84 @@ def __init__(self, original_class): self.original_class = original_class def get_potential_state_machine_attributes(self, clazz): - return inspect.getmembers(clazz) + results = [] + for key in dir(clazz): + try: + value = getattr(clazz, key) + except AttributeError: + continue + results.append((key, value)) + return results def process_states(self, original_class): initial_state = None - is_method_dict = dict() for member, value in self.get_potential_state_machine_attributes(original_class): - if isinstance(value, State): if value.initial: if initial_state is not None: raise ValueError("multiple initial states!") initial_state = value - #add its name to itself: setattr(value, 'name', member) is_method_string = "is_" + member - def is_method_builder(member): def f(self): - return self.aasm_state == str(member) - + return getattr(self, self.__class__.state_field_name) == str(member) return property(f) - is_method_dict[is_method_string] = is_method_builder(member) - - return is_method_dict, initial_state + setattr(original_class, is_method_string, is_method_builder(member)) + return initial_state def process_events(self, original_class): _adaptor = self - event_method_dict = dict() for member, value in self.get_potential_state_machine_attributes(original_class): if isinstance(value, Event): # Create event methods - def event_meta_method(event_name, event_description): - def f(self): + def f(self, *args, **kwargs): #assert current state if self.current_state not in event_description.from_states: raise InvalidStateTransition - # fire before_change failed = False if self.__class__.callback_cache and \ event_name in self.__class__.callback_cache[_adaptor.original_class.__name__]['before']: for callback in self.__class__.callback_cache[_adaptor.original_class.__name__]['before'][event_name]: - result = callback(self) + result = callback(self, *args, **kwargs) if result is False: - print("One of the 'before' callbacks returned false, breaking") failed = True break - #change state - if not failed: - _adaptor.update(self, event_description.to_state.name) - + if failed: + return False + else: + setattr(self, self.__class__.state_field_name, event_description.to_state.name) + #fire after_change if self.__class__.callback_cache and \ event_name in self.__class__.callback_cache[_adaptor.original_class.__name__]['after']: for callback in self.__class__.callback_cache[_adaptor.original_class.__name__]['after'][event_name]: - callback(self) - + callback(self, *args, **kwargs) + return True return f + + setattr(original_class, member, event_meta_method(member, value)) - event_method_dict[member] = event_meta_method(member, value) - return event_method_dict - - def modifed_class(self, original_class, callback_cache): - - class_name = original_class.__name__ - class_dict = dict() - - class_dict['callback_cache'] = callback_cache + def modifed_class(self, original_class, callback_cache, state_field_name): + setattr(original_class, 'callback_cache', callback_cache) def current_state_method(): def f(self): - return self.aasm_state + return getattr(self, self.__class__.state_field_name) return property(f) - - class_dict['current_state'] = current_state_method() - - class_dict.update(original_class.__dict__) + setattr(original_class, 'current_state', current_state_method()) # Get states - state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(initial_state)) - class_dict.update(state_method_dict) + setattr(original_class, 'state_field_name', state_field_name) + initial_state = self.process_states(original_class) + if not state_field_name in original_class.__dict__: + setattr(original_class, state_field_name, initial_state.name) # Get events - event_method_dict = self.process_events(original_class) - class_dict.update(event_method_dict) - - clazz = type(class_name, original_class.__bases__, class_dict) - return clazz - - def extra_class_members(self, initial_state): - raise NotImplementedError - - def update(self, document, state_name): - raise NotImplementedError + self.process_events(original_class) diff --git a/state_machine/orm/mongoengine.py b/state_machine/orm/mongoengine.py deleted file mode 100644 index dfbacae..0000000 --- a/state_machine/orm/mongoengine.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import absolute_import - -try: - import mongoengine -except ImportError as e: - mongoengine = None - -from state_machine.orm.base import BaseAdaptor - - -class MongoAdaptor(BaseAdaptor): - - def get_potential_state_machine_attributes(self, clazz): - # reimplementing inspect.getmembers to swallow ConnectionError - results = [] - for key in dir(clazz): - try: - value = getattr(clazz, key) - except (AttributeError, mongoengine.ConnectionError): - continue - results.append((key, value)) - results.sort() - return results - - - def extra_class_members(self, initial_state): - return {'aasm_state': mongoengine.StringField(default=initial_state.name)} - - def update(self, document, state_name): - document.aasm_state = state_name - - -def get_mongo_adaptor(original_class): - if mongoengine is not None and issubclass(original_class, mongoengine.Document): - return MongoAdaptor(original_class) - return None diff --git a/state_machine/orm/sqlalchemy.py b/state_machine/orm/sqlalchemy.py deleted file mode 100644 index bc1401c..0000000 --- a/state_machine/orm/sqlalchemy.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import - -try: - import sqlalchemy - from sqlalchemy import inspection - from sqlalchemy.orm import instrumentation - from sqlalchemy.orm import Session -except ImportError: - sqlalchemy = None - instrumentation = None - -from state_machine.orm.base import BaseAdaptor - - -class SqlAlchemyAdaptor(BaseAdaptor): - def extra_class_members(self, initial_state): - return {'aasm_state': sqlalchemy.Column(sqlalchemy.String)} - - def update(self, document, state_name): - document.aasm_state = state_name - - def modifed_class(self, original_class, callback_cache): - class_dict = dict() - - class_dict['callback_cache'] = callback_cache - - def current_state_method(): - def f(self): - return self.aasm_state - - return property(f) - - class_dict['current_state'] = current_state_method() - - # Get states - state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(initial_state)) - class_dict.update(state_method_dict) - - orig_init = original_class.__init__ - - def new_init(self, *args, **kwargs): - orig_init(self, *args, **kwargs) - self.aasm_state = initial_state.name - - class_dict['__init__'] = new_init - - # Get events - event_method_dict = self.process_events(original_class) - class_dict.update(event_method_dict) - - for key in class_dict: - setattr(original_class, key, class_dict[key]) - - return original_class - - -def get_sqlalchemy_adaptor(original_class): - if sqlalchemy is not None and hasattr(original_class, '_sa_class_manager') and isinstance( - original_class._sa_class_manager, instrumentation.ClassManager): - return SqlAlchemyAdaptor(original_class) - return None diff --git a/tests.py b/tests.py index 736a4c5..407b488 100644 --- a/tests.py +++ b/tests.py @@ -5,51 +5,15 @@ from nose.tools import * from nose.tools import assert_raises -try: - import mongoengine -except ImportError: - mongoengine = None - -def establish_mongo_connection(): - mongo_name = os.environ.get('AASM_MONGO_DB_NAME', 'test_acts_as_state_machine') - mongo_port = int(os.environ.get('AASM_MONGO_DB_PORT', 27017)) - mongoengine.connect(mongo_name, port=mongo_port) - -try: - import sqlalchemy - - engine = sqlalchemy.create_engine('sqlite:///:memory:', echo=True) -except ImportError: - sqlalchemy = None from state_machine import acts_as_state_machine, before, State, Event, after, InvalidStateTransition - -def requires_mongoengine(func): - @functools.wraps(func) - def wrapper(*args, **kw): - if mongoengine is None: - raise SkipTest("mongoengine is not installed") - return func(*args, **kw) - - return wrapper - - -def requires_sqlalchemy(func): - @functools.wraps(func) - def wrapper(*args, **kw): - if sqlalchemy is None: - raise SkipTest("sqlalchemy is not installed") - return func(*args, **kw) - - return wrapper - ################################################################################### ## Plain Old In Memory Tests ################################################################################### def test_state_machine(): - @acts_as_state_machine + @acts_as_state_machine() class Robot(): name = 'R2-D2' @@ -88,8 +52,9 @@ def snore(self): assert robot.is_sleeping -def test_state_machine_no_callbacks(): - @acts_as_state_machine + +def test_named_state_machine(): + @acts_as_state_machine('state') class Robot(): name = 'R2-D2' @@ -101,77 +66,6 @@ class Robot(): cleanup = Event(from_states=running, to_state=cleaning) sleep = Event(from_states=(running, cleaning), to_state=sleeping) - robot = Robot() - eq_(robot.current_state, 'sleeping') - assert robot.is_sleeping - assert not robot.is_running - robot.run() - assert robot.is_running - robot.sleep() - assert robot.is_sleeping - - -def test_multiple_machines(): - @acts_as_state_machine - class Person(object): - 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) - - @before('run') - def on_run(self): - things_done.append("Person.ran") - - @acts_as_state_machine - class Dog(object): - sleeping = State(initial=True) - running = State() - - run = Event(from_states=sleeping, to_state=running) - sleep = Event(from_states=(running,), to_state=sleeping) - - @before('run') - def on_run(self): - things_done.append("Dog.ran") - - things_done = [] - person = Person() - dog = Dog() - eq_(person.current_state, 'sleeping') - eq_(dog.current_state, 'sleeping') - assert person.is_sleeping - assert dog.is_sleeping - person.run() - eq_(things_done, ["Person.ran"]) - -################################################################################### -## SqlAlchemy Tests -################################################################################### -@requires_sqlalchemy -def test_sqlalchemy_state_machine(): - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import sessionmaker - - Base = declarative_base() - - @acts_as_state_machine - class Puppy(Base): - __tablename__ = 'puppies' - id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) - name = sqlalchemy.Column(sqlalchemy.String) - - 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) - @before('sleep') def do_one_thing(self): print("{} is sleepy".format(self.name)) @@ -189,41 +83,20 @@ def snore(self): print("Zzzzzzzzzzzzzzzzzzzzzz") - Base.metadata.create_all(engine) - - Session = sessionmaker(bind=engine) - session = Session() - - puppy = Puppy(name='Ralph') - - eq_(puppy.current_state, Puppy.sleeping) - assert puppy.is_sleeping - assert not puppy.is_running - puppy.run() - assert puppy.is_running - - session.add(puppy) - session.commit() - - puppy2 = session.query(Puppy).filter_by(id=puppy.id)[0] - - assert puppy2.is_running - - -@requires_sqlalchemy -def test_sqlalchemy_state_machine_no_callbacks(): - ''' This is to make sure that the state change will still work even if no callbacks are registered. - ''' - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import sessionmaker + robot = Robot() + eq_(robot.current_state, 'sleeping') + assert robot.is_sleeping + assert not robot.is_running + robot.run() + assert robot.is_running + robot.sleep() + assert robot.is_sleeping - Base = declarative_base() - @acts_as_state_machine - class Kitten(Base): - __tablename__ = 'kittens' - id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) - name = sqlalchemy.Column(sqlalchemy.String) +def test_state_machine_no_callbacks(): + @acts_as_state_machine() + class Robot(): + name = 'R2-D2' sleeping = State(initial=True) running = State() @@ -233,42 +106,20 @@ class Kitten(Base): cleanup = Event(from_states=running, to_state=cleaning) sleep = Event(from_states=(running, cleaning), to_state=sleeping) + robot = Robot() + eq_(robot.current_state, 'sleeping') + assert robot.is_sleeping + assert not robot.is_running + robot.run() + assert robot.is_running + robot.sleep() + assert robot.is_sleeping - Base.metadata.create_all(engine) - - Session = sessionmaker(bind=engine) - session = Session() - - kitten = Kitten(name='Kit-Kat') - - eq_(kitten.current_state, Kitten.sleeping) - assert kitten.is_sleeping - assert not kitten.is_running - kitten.run() - assert kitten.is_running - - session.add(kitten) - session.commit() - - kitten2 = session.query(Kitten).filter_by(id=kitten.id)[0] - - assert kitten2.is_running - - -@requires_sqlalchemy -def test_sqlalchemy_state_machine_using_initial_state(): - ''' This is to make sure that the database will save the object with the initial state. - ''' - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import sessionmaker - - Base = declarative_base() - @acts_as_state_machine - class Penguin(Base): - __tablename__ = 'penguins' - id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) - name = sqlalchemy.Column(sqlalchemy.String) +def test_named_state_machine_no_callbacks(): + @acts_as_state_machine('state') + class Robot(): + name = 'R2-D2' sleeping = State(initial=True) running = State() @@ -278,36 +129,19 @@ class Penguin(Base): cleanup = Event(from_states=running, to_state=cleaning) sleep = Event(from_states=(running, cleaning), to_state=sleeping) + robot = Robot() + eq_(robot.current_state, 'sleeping') + assert robot.is_sleeping + assert not robot.is_running + robot.run() + assert robot.is_running + robot.sleep() + assert robot.is_sleeping - Base.metadata.create_all(engine) - - Session = sessionmaker(bind=engine) - session = Session() - - # Note: No state transition occurs between the initial state and when it's saved to the database. - penguin = Penguin(name='Tux') - eq_(penguin.current_state, Penguin.sleeping) - assert penguin.is_sleeping - - session.add(penguin) - session.commit() - - penguin2 = session.query(Penguin).filter_by(id=penguin.id)[0] - - assert penguin2.is_sleeping - - -################################################################################### -## Mongo Engine Tests -################################################################################### - -@requires_mongoengine -def test_mongoengine_state_machine(): - - @acts_as_state_machine - class Person(mongoengine.Document): - name = mongoengine.StringField(default='Billy') +def test_multiple_machines(): + @acts_as_state_machine() + class Person(object): sleeping = State(initial=True) running = State() cleaning = State() @@ -316,46 +150,35 @@ class Person(mongoengine.Document): cleanup = Event(from_states=running, to_state=cleaning) sleep = Event(from_states=(running, cleaning), to_state=sleeping) - @before('sleep') - def do_one_thing(self): - print("{} is sleepy".format(self.name)) - - @before('sleep') - def do_another_thing(self): - print("{} is REALLY sleepy".format(self.name)) + @before('run') + def on_run(self): + things_done.append("Person.ran") - @after('sleep') - def snore(self): - print("Zzzzzzzzzzzz") + @acts_as_state_machine() + class Dog(object): + sleeping = State(initial=True) + running = State() - @after('sleep') - def snore(self): - print("Zzzzzzzzzzzzzzzzzzzzzz") + run = Event(from_states=sleeping, to_state=running) + sleep = Event(from_states=(running,), to_state=sleeping) - establish_mongo_connection() + @before('run') + def on_run(self): + things_done.append("Dog.ran") + things_done = [] person = Person() - person.save() - eq_(person.current_state, Person.sleeping) - assert person.is_sleeping - assert not person.is_running - person.run() - assert person.is_running - person.sleep() + dog = Dog() + eq_(person.current_state, 'sleeping') + eq_(dog.current_state, 'sleeping') assert person.is_sleeping + assert dog.is_sleeping person.run() - person.save() - - person2 = Person.objects(id=person.id).first() - assert person2.is_running - - -@requires_mongoengine -def test_invalid_state_transition(): - @acts_as_state_machine - class Person(mongoengine.Document): - name = mongoengine.StringField(default='Billy') + eq_(things_done, ["Person.ran"]) +def test_multiple_named_machines(): + @acts_as_state_machine('state0') + class Person(object): sleeping = State(initial=True) running = State() cleaning = State() @@ -364,42 +187,31 @@ class Person(mongoengine.Document): cleanup = Event(from_states=running, to_state=cleaning) sleep = Event(from_states=(running, cleaning), to_state=sleeping) - establish_mongo_connection() - person = Person() - person.save() - assert person.is_sleeping - - #should raise an invalid state exception - with assert_raises(InvalidStateTransition): - person.sleep() - - -@requires_mongoengine -def test_before_callback_blocking_transition(): - @acts_as_state_machine - class Runner(mongoengine.Document): - name = mongoengine.StringField(default='Billy') + @before('run') + def on_run(self): + things_done.append("Person.ran") + @acts_as_state_machine('state1') + class Dog(object): 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) + sleep = Event(from_states=(running,), to_state=sleeping) @before('run') - def check_sneakers(self): - return False - - establish_mongo_connection() - runner = Runner() - runner.save() - assert runner.is_sleeping - runner.run() - runner.reload() - assert runner.is_sleeping - assert not runner.is_running + def on_run(self): + things_done.append("Dog.ran") + + things_done = [] + person = Person() + dog = Dog() + eq_(person.current_state, 'sleeping') + eq_(dog.current_state, 'sleeping') + assert person.is_sleeping + assert dog.is_sleeping + person.run() + eq_(things_done, ["Person.ran"]) if __name__ == "__main__": diff --git a/tox.ini b/tox.ini index 654a984..0e7d672 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,10 @@ [tox] envlist=py27,py33,pypy [testenv] -deps=sqlalchemy commands=python setup.py test [testenv:py27-mongo] basepython=python2.7 -deps=sqlalchemy - mongoengine [testenv:py33-mongo] basepython=python3.3 -deps=sqlalchemy - mongoengine [testenv:pypy-mongo] basepython=pypy -deps=sqlalchemy - mongoengine