diff --git a/README.md b/README.md index e9030583..31be40f2 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,14 @@ A lightweight, object-oriented state machine implementation in Python with many - [Quickstart](#quickstart) - [Non-Quickstart](#the-non-quickstart) + - [Some key concepts](#key-concepts) - [Basic initialization](#basic-initialization) - [States](#states) - [Callbacks](#state-callbacks) - [Checking state](#checking-state) - [Enumerations](#enum-state) - [Transitions](#transitions) + - [Triggering a transition](#triggers) - [Automatic transitions](#automatic-transitions-for-all-states) - [Transitioning from multiple states](#transitioning-from-multiple-states) - [Reflexive transitions from multiple states](#reflexive-from-multiple-states) @@ -184,6 +186,22 @@ Have a look at the [Diagrams](#diagrams) extensions if you want to know how. ## The non-quickstart +A state machine is a _model_ of behavior composed of a finite number of _states_ and _transitions_ between those states. Within each state and transition some _action_ can be performed. A state machine needs to start at some _initial state_. + +### Some key concepts + +- **State**. A state represents a particular condition or stage in the state machine. It's a distinct mode of behavior or phase in a process. + +- **Transition**. This is the process or event that causes the state machine to change from one state to another. + +- **Model**. Blueprint or structure that holds the state machine. It's the entity that gets updated as new states and transitions are added. + +- **Machine**. This is the entity that manages and controls the model, states, transitions, and actions. It's the conductor that orchestrates the entire process of the state machine. + +- **Trigger**. This is the event that initiates a transition, the method that sends the signal to start a transition. + +- **Action**. Specific operation or task that is performed when a certain state is entered, exited, or during a transition. The action is implemented through _callbacks_, which are functions that get executed when some event happens. + ### Basic initialization Getting a state machine up and running is pretty simple. Let's say you have the object `lump` (an instance of class `Matter`), and you want to manage its states: @@ -195,18 +213,31 @@ class Matter(object): lump = Matter() ``` -You can initialize a (_minimal_) working state machine bound to `lump` like this: +You can initialize a (_minimal_) working state machine bound to the model `lump` like this: ```python from transitions import Machine machine = Machine(model=lump, states=['solid', 'liquid', 'gas', 'plasma'], initial='solid') -# Lump now has state! +# Lump now has a new state attribute! lump.state >>> 'solid' ``` -I say "minimal", because while this state machine is technically operational, it doesn't actually _do_ anything. It starts in the `'solid'` state, but won't ever move into another state, because no transitions are defined... yet! +An alternative is to not explicitly pass a model to the `Machine` initializer: + +```python + +machine = Machine(states=['solid', 'liquid', 'gas', 'plasma'], initial='solid') + +# The machine instance itself now acts as a model +machine.state +>>> 'solid' +``` + +Note that this time I did not pass the `lump` model as an argument. The first argument passed to `Machine` acts as a model. So when I pass something there, all the convenience functions will be added to the object. If no model is provided then the `machine` instance itself acts as a model. + +When at the beginning I said "minimal", it was because while this state machine is technically operational, it doesn't actually _do_ anything. It starts in the `'solid'` state, but won't ever move into another state, because no transitions are defined... yet! Let's try again. @@ -231,19 +262,19 @@ lump.state >>> 'liquid' # And that state can change... +# Either calling the shiny new trigger methods lump.evaporate() lump.state >>> 'gas' + +# Or by calling the trigger method directly lump.trigger('ionize') lump.state >>> 'plasma' ``` -Notice the shiny new methods attached to the `Matter` instance (`evaporate()`, `ionize()`, etc.). Each method triggers the corresponding transition. You don't have to explicitly define these methods anywhere; the name of each transition is bound to the model passed to the `Machine` initializer (in this case, `lump`). -To be more precise, your model **should not** already contain methods with the same name as event triggers since `transitions` will only attach convenience methods to your model if the spot is not already taken. -If you want to modify that behaviour, have a look at the [FAQ](examples/Frequently%20asked%20questions.ipynb). -Furthermore, there is a method called `trigger` now attached to your model (if it hasn't been there before). -This method lets you execute transitions by name in case dynamic triggering is required. +Notice the shiny new methods attached to the `Matter` instance (`evaporate()`, `ionize()`, etc.). Each method triggers the corresponding transition. Transitions can also be triggered _dynamically_ by calling the `trigger()` method provided with the name of the transition, as shown above. More on this in the [Triggering a transition](#triggers) section. + ### States @@ -285,6 +316,8 @@ States are initialized _once_ when added to the machine and will persist until t #### Callbacks +But just having states and being able to move around between them (transitions) isn't very useful by itself. What if you want to do something, perform some _action_ when you enter or exit a state? This is where _callbacks_ come in. + A `State` can also be associated with a list of `enter` and `exit` callbacks, which are called whenever the state machine enters or leaves that state. You can specify callbacks during initialization by passing them to a `State` object constructor, in a state property dictionary, or add them later. For convenience, whenever a new `State` is added to a `Machine`, the methods `on_enter_«state name»` and `on_exit_«state name»` are dynamically created on the Machine (not on the model!), which allow you to dynamically add new enter and exit callbacks later if you need them. @@ -441,15 +474,34 @@ machine = Machine(model=lump, states=states, initial='solid') machine.add_transition('melt', source='solid', dest='liquid') ``` -The `trigger` argument defines the name of the new triggering method that gets attached to the base model. When this method is called, it will try to execute the transition: - -```python ->>> lump.melt() ->>> lump.state -'liquid' -``` - -By default, calling an invalid trigger will raise an exception: +#### Triggering a transition + +For a transition to be executed, some event needs to _trigger_ it. There are two ways to do this: + +1. Using the automatically attached method in the base model: + ```python + >>> lump.melt() + >>> lump.state + 'liquid' + >>> lump.evaporate() + >>> lump.state + 'gas' + ``` + + Note how you don't have to explicitly define these methods anywhere; the name of each transition is bound to the model passed to the `Machine` initializer (in this case, `lump`). This also means that your model **should not** already contain methods with the same name as event triggers since `transitions` will only attach convenience methods to your model if the spot is not already taken. If you want to modify that behaviour, have a look at the [FAQ](examples/Frequently%20asked%20questions.ipynb). +2. Using the `trigger` method, now attached to your model (if it hasn't been there before). This method lets you execute transitions by name in case dynamic triggering is required: + ```python + >>> lump.trigger('melt') + >>> lump.state + 'liquid' + >>> lump.trigger('evaporate') + >>> lump.state + 'gas' + ``` + +#### Triggering invalid transitions + +By default, triggering an invalid transition will raise an exception: ```python >>> lump.to_gas()