From 4569459ed952311db273ae1f3995d455f3419976 Mon Sep 17 00:00:00 2001 From: Eric Gillet Date: Sat, 6 Feb 2016 21:38:47 +0100 Subject: [PATCH 1/4] offer the possibility to change the name of the state field --- README.rst | 14 +- docs/index.rst | 14 +- state_machine/__init__.py | 14 +- state_machine/orm/__init__.py | 7 +- state_machine/orm/base.py | 11 +- state_machine/orm/mongoengine.py | 7 +- state_machine/orm/sqlalchemy.py | 15 +- tests.py | 269 +++++++++++++++++++++++++++++-- 8 files changed, 311 insertions(+), 40 deletions(-) diff --git a/README.rst b/README.rst index 0542220..deab1e9 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' @@ -90,6 +90,14 @@ Blocks invalid state transitions An *InvalidStateTransition Exception* will be thrown if you try to move into an invalid state. +Name of the state field +~~~~~~~~~~~~~~~~~~~~~~~ + +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(): + ... + ORM support ----------- @@ -106,7 +114,7 @@ datastore. .. code:: python - @acts_as_state_machine + @acts_as_state_machine() class Person(mongoengine.Document): name = mongoengine.StringField(default='Billy') @@ -162,7 +170,7 @@ All you need to do is have sqlalchemy manage your object. For example: from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() - @acts_as_state_machine + @acts_as_state_machine() class Puppy(Base): ... diff --git a/docs/index.rst b/docs/index.rst index b0be356..6da70f4 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' @@ -90,6 +90,14 @@ Blocks invalid state transitions An *InvalidStateTransition Exception* will be thrown if you try to move into an invalid state. +Name of the state field +~~~~~~~~~~~~~~~~~~~~~~~ + +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(): + ... + ORM support ----------- @@ -106,7 +114,7 @@ datastore. .. code:: python - @acts_as_state_machine + @acts_as_state_machine() class Person(mongoengine.Document): name = mongoengine.StringField(default='Billy') @@ -162,7 +170,7 @@ All you need to do is have sqlalchemy manage your object. For example: from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() - @acts_as_state_machine + @acts_as_state_machine() class Puppy(Base): ... diff --git a/state_machine/__init__.py b/state_machine/__init__.py index 911d0f3..361cd23 100644 --- a/state_machine/__init__.py +++ b/state_machine/__init__.py @@ -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 = get_adaptor(original_class) + global _temp_callback_cache + modified_class = adaptor.modifed_class(original_class, _temp_callback_cache, state_field_name) + _temp_callback_cache = None + return modified_class + return f diff --git a/state_machine/orm/__init__.py b/state_machine/orm/__init__.py index 31ef682..a29be12 100644 --- a/state_machine/orm/__init__.py +++ b/state_machine/orm/__init__.py @@ -19,8 +19,9 @@ def get_adaptor(original_class): class NullAdaptor(BaseAdaptor): - def extra_class_members(self, initial_state): - return {"aasm_state": initial_state.name} + def extra_class_members(self, original_class, state_field_name, initial_state): + return {state_field_name: initial_state.name} def update(self, document, state_name): - document.aasm_state = state_name + setattr(document, document.__class__.state_field_name, state_name) + diff --git a/state_machine/orm/base.py b/state_machine/orm/base.py index fafe3c2..ba854b2 100644 --- a/state_machine/orm/base.py +++ b/state_machine/orm/base.py @@ -29,7 +29,7 @@ def process_states(self, original_class): 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) @@ -76,7 +76,7 @@ def f(self): event_method_dict[member] = event_meta_method(member, value) return event_method_dict - def modifed_class(self, original_class, callback_cache): + def modifed_class(self, original_class, callback_cache, state_field_name): class_name = original_class.__name__ class_dict = dict() @@ -85,7 +85,7 @@ def modifed_class(self, original_class, 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() @@ -93,8 +93,9 @@ def f(self): class_dict.update(original_class.__dict__) # Get states + class_dict['state_field_name'] = state_field_name state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(initial_state)) + class_dict.update(self.extra_class_members(original_class, state_field_name, initial_state)) class_dict.update(state_method_dict) # Get events @@ -104,7 +105,7 @@ def f(self): clazz = type(class_name, original_class.__bases__, class_dict) return clazz - def extra_class_members(self, initial_state): + def extra_class_members(self, original_class, state_field_name, initial_state): raise NotImplementedError def update(self, document, state_name): diff --git a/state_machine/orm/mongoengine.py b/state_machine/orm/mongoengine.py index dfbacae..e2f30c3 100644 --- a/state_machine/orm/mongoengine.py +++ b/state_machine/orm/mongoengine.py @@ -23,11 +23,12 @@ def get_potential_state_machine_attributes(self, clazz): return results - def extra_class_members(self, initial_state): - return {'aasm_state': mongoengine.StringField(default=initial_state.name)} + + def extra_class_members(self, original_class, state_field_name, initial_state): + return {state_field_name: mongoengine.StringField(default=initial_state.name), 'id': original_class.pk} def update(self, document, state_name): - document.aasm_state = state_name + setattr(document, document.__class__.state_field_name, state_name) def get_mongo_adaptor(original_class): diff --git a/state_machine/orm/sqlalchemy.py b/state_machine/orm/sqlalchemy.py index bc1401c..08d6d90 100644 --- a/state_machine/orm/sqlalchemy.py +++ b/state_machine/orm/sqlalchemy.py @@ -13,35 +13,36 @@ class SqlAlchemyAdaptor(BaseAdaptor): - def extra_class_members(self, initial_state): - return {'aasm_state': sqlalchemy.Column(sqlalchemy.String)} + def extra_class_members(self, original_class, state_field_name, initial_state): + return {state_field_name: sqlalchemy.Column(sqlalchemy.String)} def update(self, document, state_name): - document.aasm_state = state_name + setattr(document, document.__class__.state_field_name, state_name) - def modifed_class(self, original_class, callback_cache): + def modifed_class(self, original_class, callback_cache, state_field_name): class_dict = dict() class_dict['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() # Get states + class_dict['state_field_name'] = state_field_name state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(initial_state)) + class_dict.update(self.extra_class_members(original_class, state_field_name, 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 + setattr(self, self.__class__.state_field_name, initial_state.name) class_dict['__init__'] = new_init diff --git a/tests.py b/tests.py index 736a4c5..7bf3754 100644 --- a/tests.py +++ b/tests.py @@ -49,7 +49,48 @@ def wrapper(*args, **kw): ################################################################################### def test_state_machine(): - @acts_as_state_machine + @acts_as_state_machine() + class Robot(): + name = 'R2-D2' + + 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)) + + @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") + + + 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_named_state_machine(): + @acts_as_state_machine('state') class Robot(): name = 'R2-D2' @@ -89,7 +130,30 @@ def snore(self): def test_state_machine_no_callbacks(): - @acts_as_state_machine + @acts_as_state_machine() + class Robot(): + name = 'R2-D2' + + 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) + + 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_named_state_machine_no_callbacks(): + @acts_as_state_machine('state') class Robot(): name = 'R2-D2' @@ -112,7 +176,44 @@ class Robot(): def test_multiple_machines(): - @acts_as_state_machine + @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"]) + +def test_multiple_named_machines(): + @acts_as_state_machine('state0') class Person(object): sleeping = State(initial=True) running = State() @@ -126,7 +227,7 @@ class Person(object): def on_run(self): things_done.append("Person.ran") - @acts_as_state_machine + @acts_as_state_machine('state1') class Dog(object): sleeping = State(initial=True) running = State() @@ -158,7 +259,7 @@ def test_sqlalchemy_state_machine(): Base = declarative_base() - @acts_as_state_machine + @acts_as_state_machine() class Puppy(Base): __tablename__ = 'puppies' id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) @@ -210,6 +311,65 @@ def snore(self): assert puppy2.is_running +@requires_sqlalchemy +def test_sqlalchemy_named_state_machine(): + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import sessionmaker + + Base = declarative_base() + + @acts_as_state_machine('state') + class Puppy(Base): + __tablename__ = 'named_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)) + + @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") + + + 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. @@ -219,7 +379,7 @@ def test_sqlalchemy_state_machine_no_callbacks(): Base = declarative_base() - @acts_as_state_machine + @acts_as_state_machine() class Kitten(Base): __tablename__ = 'kittens' id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) @@ -255,6 +415,52 @@ class Kitten(Base): assert kitten2.is_running + +@requires_sqlalchemy +def test_sqlalchemy_named_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 + + Base = declarative_base() + + @acts_as_state_machine('state') + class Kitten(Base): + __tablename__ = 'named_kittens' + 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) + + + 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. @@ -264,7 +470,7 @@ def test_sqlalchemy_state_machine_using_initial_state(): Base = declarative_base() - @acts_as_state_machine + @acts_as_state_machine() class Penguin(Base): __tablename__ = 'penguins' id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) @@ -297,6 +503,49 @@ class Penguin(Base): assert penguin2.is_sleeping + +@requires_sqlalchemy +def test_sqlalchemy_named_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('state') + class Penguin(Base): + __tablename__ = 'named_penguins' + 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) + + + 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 ################################################################################### @@ -304,7 +553,7 @@ class Penguin(Base): @requires_mongoengine def test_mongoengine_state_machine(): - @acts_as_state_machine + @acts_as_state_machine() class Person(mongoengine.Document): name = mongoengine.StringField(default='Billy') @@ -352,7 +601,7 @@ def snore(self): @requires_mongoengine def test_invalid_state_transition(): - @acts_as_state_machine + @acts_as_state_machine() class Person(mongoengine.Document): name = mongoengine.StringField(default='Billy') @@ -376,7 +625,7 @@ class Person(mongoengine.Document): @requires_mongoengine def test_before_callback_blocking_transition(): - @acts_as_state_machine + @acts_as_state_machine() class Runner(mongoengine.Document): name = mongoengine.StringField(default='Billy') From 7f91393c70f8c299d8b03d0d854785bcbf7e75c0 Mon Sep 17 00:00:00 2001 From: Eric Gillet Date: Sun, 6 Mar 2016 11:52:25 +0100 Subject: [PATCH 2/4] back to a PlainOldObject --- README.rst | 77 +----- setup.py | 4 +- state_machine/__init__.py | 8 +- state_machine/orm/__init__.py | 27 -- state_machine/orm/base.py | 63 ++--- state_machine/orm/mongoengine.py | 37 --- state_machine/orm/sqlalchemy.py | 63 ----- tests.py | 437 ------------------------------- tox.ini | 7 - 9 files changed, 31 insertions(+), 692 deletions(-) delete mode 100644 state_machine/orm/mongoengine.py delete mode 100644 state_machine/orm/sqlalchemy.py diff --git a/README.rst b/README.rst index deab1e9..a8821a3 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,9 @@ into an invalid state. Name of the state field ~~~~~~~~~~~~~~~~~~~~~~~ -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: +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(): ... @@ -101,84 +103,13 @@ The default name of the state field is "aasm_state". If you want to chane it, yo ORM support ----------- -We have basic support for `mongoengine`_, and `sqlalchemy`_. +It should be done independently -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) - - @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 -~~~~~~~~~~ - -All you need to do is have sqlalchemy manage your object. For example: - -.. code:: python - - 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 361cd23..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 @@ -43,9 +43,9 @@ def wrapper(func): def acts_as_state_machine(state_field_name='aasm_state'): def f(original_class): - adaptor = get_adaptor(original_class) + adaptor = BaseAdaptor(original_class) global _temp_callback_cache - modified_class = adaptor.modifed_class(original_class, _temp_callback_cache, state_field_name) + adaptor.modifed_class(original_class, _temp_callback_cache, state_field_name) _temp_callback_cache = None - return modified_class + return original_class return f diff --git a/state_machine/orm/__init__.py b/state_machine/orm/__init__.py index a29be12..e69de29 100644 --- a/state_machine/orm/__init__.py +++ b/state_machine/orm/__init__.py @@ -1,27 +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, original_class, state_field_name, initial_state): - return {state_field_name: initial_state.name} - - def update(self, document, state_name): - setattr(document, document.__class__.state_field_name, state_name) - diff --git a/state_machine/orm/base.py b/state_machine/orm/base.py index ba854b2..0beb279 100644 --- a/state_machine/orm/base.py +++ b/state_machine/orm/base.py @@ -9,47 +9,45 @@ 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 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): #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 \ @@ -60,11 +58,10 @@ def f(self): 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) - + 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']: @@ -72,41 +69,23 @@ def f(self): callback(self) return f - - event_method_dict[member] = event_meta_method(member, value) - return event_method_dict + + setattr(original_class, member, event_meta_method(member, value)) def modifed_class(self, original_class, callback_cache, state_field_name): - - class_name = original_class.__name__ - class_dict = dict() - - class_dict['callback_cache'] = callback_cache + setattr(original_class, 'callback_cache', callback_cache) def current_state_method(): def f(self): 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 - class_dict['state_field_name'] = state_field_name - state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(original_class, state_field_name, 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, original_class, state_field_name, 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 e2f30c3..0000000 --- a/state_machine/orm/mongoengine.py +++ /dev/null @@ -1,37 +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, original_class, state_field_name, initial_state): - return {state_field_name: mongoengine.StringField(default=initial_state.name), 'id': original_class.pk} - - def update(self, document, state_name): - setattr(document, document.__class__.state_field_name, 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 08d6d90..0000000 --- a/state_machine/orm/sqlalchemy.py +++ /dev/null @@ -1,63 +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, original_class, state_field_name, initial_state): - return {state_field_name: sqlalchemy.Column(sqlalchemy.String)} - - def update(self, document, state_name): - setattr(document, document.__class__.state_field_name, state_name) - - def modifed_class(self, original_class, callback_cache, state_field_name): - class_dict = dict() - - class_dict['callback_cache'] = callback_cache - - def current_state_method(): - def f(self): - return getattr(self, self.__class__.state_field_name) - - return property(f) - - class_dict['current_state'] = current_state_method() - - # Get states - class_dict['state_field_name'] = state_field_name - state_method_dict, initial_state = self.process_states(original_class) - class_dict.update(self.extra_class_members(original_class, state_field_name, initial_state)) - class_dict.update(state_method_dict) - - orig_init = original_class.__init__ - - def new_init(self, *args, **kwargs): - orig_init(self, *args, **kwargs) - setattr(self, self.__class__.state_field_name, 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 7bf3754..407b488 100644 --- a/tests.py +++ b/tests.py @@ -5,45 +5,9 @@ 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 ################################################################################### @@ -249,407 +213,6 @@ def on_run(self): 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)) - - @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") - - - 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_named_state_machine(): - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import sessionmaker - - Base = declarative_base() - - @acts_as_state_machine('state') - class Puppy(Base): - __tablename__ = 'named_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)) - - @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") - - - 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 - - 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) - - 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) - - - 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_named_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 - - Base = declarative_base() - - @acts_as_state_machine('state') - class Kitten(Base): - __tablename__ = 'named_kittens' - 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) - - - 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) - - 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) - - - 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 - - - -@requires_sqlalchemy -def test_sqlalchemy_named_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('state') - class Penguin(Base): - __tablename__ = 'named_penguins' - 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) - - - 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') - - 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)) - - @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") - - establish_mongo_connection() - - 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 - - -@requires_mongoengine -def test_invalid_state_transition(): - @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) - - 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') - - 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 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 - if __name__ == "__main__": nose.run() 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 From 7c8f252de4e46a97f697fa179ae392d711abbc6d Mon Sep 17 00:00:00 2001 From: Eric Gillet Date: Sun, 6 Mar 2016 12:36:44 +0100 Subject: [PATCH 3/4] give parameters to the events --- README.rst | 19 ++++---- docs/conf.py | 4 +- docs/index.rst | 93 +++++---------------------------------- state_machine/orm/base.py | 7 ++- 4 files changed, 27 insertions(+), 96 deletions(-) diff --git a/README.rst b/README.rst index a8821a3..631c382 100644 --- a/README.rst +++ b/README.rst @@ -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 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 6da70f4..fa6eb57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 @@ -101,84 +102,14 @@ The default name of the state field is "aasm_state". If you want to chane it, yo ORM support ----------- -We have basic support for `mongoengine`_, and `sqlalchemy`_. +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. -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) - - @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 -~~~~~~~~~~ - -All you need to do is have sqlalchemy manage your object. For example: - -.. code:: python - - 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/state_machine/orm/base.py b/state_machine/orm/base.py index 0beb279..bf7e270 100644 --- a/state_machine/orm/base.py +++ b/state_machine/orm/base.py @@ -44,7 +44,7 @@ def process_events(self, 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 @@ -53,9 +53,8 @@ def f(self): 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 @@ -66,7 +65,7 @@ def f(self): 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 f From f8189f3b46dcd0e7b0a6e1ff930fe45ae1bc371c Mon Sep 17 00:00:00 2001 From: Eric Gillet Date: Mon, 7 Mar 2016 14:34:45 +0100 Subject: [PATCH 4/4] return true if an event succeeds --- state_machine/orm/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/state_machine/orm/base.py b/state_machine/orm/base.py index bf7e270..f8dc65c 100644 --- a/state_machine/orm/base.py +++ b/state_machine/orm/base.py @@ -58,7 +58,9 @@ def f(self, *args, **kwargs): failed = True break #change state - if not failed: + if failed: + return False + else: setattr(self, self.__class__.state_field_name, event_description.to_state.name) #fire after_change @@ -66,7 +68,7 @@ def f(self, *args, **kwargs): 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, *args, **kwargs) - + return True return f setattr(original_class, member, event_meta_method(member, value))