Skip to content

Commit

Permalink
Merge branch 'release/2.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
fgmacedo committed Jun 12, 2023
2 parents 2ae3223 + 3a16db1 commit f95391a
Show file tree
Hide file tree
Showing 30 changed files with 1,216 additions and 1,498 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/codesee-arch-diagram.yml
@@ -0,0 +1,23 @@
# This workflow was added by CodeSee. Learn more at https://codesee.io/
# This is v2.0 of this workflow file
on:
push:
branches:
- develop
pull_request_target:
types: [opened, synchronize, reopened]

name: CodeSee

permissions: read-all

jobs:
codesee:
runs-on: ubuntu-latest
continue-on-error: true
name: Analyze the repo with CodeSee
steps:
- uses: Codesee-io/codesee-action@v2
with:
codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
codesee-url: https://app.codesee.io
4 changes: 1 addition & 3 deletions .pre-commit-config.yaml
Expand Up @@ -9,11 +9,9 @@ repos:
exclude: docs/auto_examples
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.220'
rev: 'v0.0.257'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
args: ["--force-exclude"]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
Expand Down
113 changes: 76 additions & 37 deletions README.md
Expand Up @@ -59,11 +59,11 @@ Define your state machine:
... yellow = State()
... red = State()
...
... cycle = green.to(yellow) | yellow.to(red) | red.to(green)
...
... slowdown = green.to(yellow)
... stop = yellow.to(red)
... go = red.to(green)
... cycle = (
... green.to(yellow)
... | yellow.to(red)
... | red.to(green)
... )
...
... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
... message = ". " + message if message else ""
Expand All @@ -80,124 +80,163 @@ Define your state machine:
You can now create an instance:

```py
>>> traffic_light = TrafficLightMachine()
>>> sm = TrafficLightMachine()

```

Then start sending events:
This state machine can be represented graphically as follows:

```py
>>> traffic_light.cycle()
'Running cycle from green to yellow'
>>> img_path = "docs/images/readme_trafficlightmachine.png"
>>> sm._graph().write_png(img_path)

```

You can inspect the current state:
![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png)


Where on the `TrafficLightMachine`, we've defined `green`, `yellow`, and `red` as states, and
one event called `cycle`, which is bound to the transitions from `green` to `yellow`, `yellow` to `red`,
and `red` to `green`. We also have defined three callbacks by name convention, `before_cycle`, `on_enter_red`, and `on_exit_red`.


Then start sending events to your new state machine:

```py
>>> traffic_light.current_state.id
'yellow'
>>> sm.send("cycle")
'Running cycle from green to yellow'

```

A `State` human-readable name is automatically derived from the `State.id`:
That's it. This is all an external object needs to know about your state machine: How to send events.
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.

But if your use case needs, you can inspect state machine properties, like the current state:

```py
>>> traffic_light.current_state.name
'Yellow'
>>> sm.current_state.id
'yellow'

```

Or get a complete state representation for debugging purposes:

```py
>>> traffic_light.current_state
>>> sm.current_state
State('Yellow', id='yellow', value='yellow', initial=False, final=False)

```

The ``State`` instance can also be checked by equality:
The `State` instance can also be checked by equality:

```py
>>> traffic_light.current_state == TrafficLightMachine.yellow
>>> sm.current_state == TrafficLightMachine.yellow
True

>>> traffic_light.current_state == traffic_light.yellow
>>> sm.current_state == sm.yellow
True

```

But for your convenience, can easily ask if a state is active at any time:
Or you can check if a state is active at any time:

```py
>>> traffic_light.green.is_active
>>> sm.green.is_active
False

>>> traffic_light.yellow.is_active
>>> sm.yellow.is_active
True

>>> traffic_light.red.is_active
>>> sm.red.is_active
False

```

Easily iterate over all states:

```py
>>> [s.id for s in traffic_light.states]
>>> [s.id for s in sm.states]
['green', 'red', 'yellow']

```

Or over events:

```py
>>> [t.name for t in traffic_light.events]
['cycle', 'go', 'slowdown', 'stop']
>>> [t.name for t in sm.events]
['cycle']

```

Call an event by its name:

```py
>>> traffic_light.cycle()
>>> sm.cycle()
Don't move.
'Running cycle from yellow to red'

```
Or send an event with the event name:

```py
>>> traffic_light.send('cycle')
>>> sm.send('cycle')
Go ahead!
'Running cycle from red to green'

>>> traffic_light.green.is_active
>>> sm.green.is_active
True

```
You can't run a transition from an invalid state:

You can pass arbitrary positional or keyword arguments to the event, and
they will be propagated to all actions and callbacks using something similar to dependency injection. In other words, the library will only inject the parameters declared on the
callback method.

Note how `before_cycle` was declared:

```py
>>> traffic_light.go()
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
message = ". " + message if message else ""
return f"Running {event} from {source.id} to {target.id}{message}"
```

The params `event`, `source`, `target` (and others) are available built-in to be used on any action.
The param `message` is user-defined, in our example we made it default empty so we can call `cycle` with
or without a `message` parameter.

If we pass a `message` parameter, it will be used on the `before_cycle` action:

```py
>>> sm.send("cycle", message="Please, now slowdown.")
'Running cycle from green to yellow. Please, now slowdown.'

```


By default, events with transitions that cannot run from the current state or unknown events
raise a `TransitionNotAllowed` exception:

```py
>>> sm.send("go")
Traceback (most recent call last):
statemachine.exceptions.TransitionNotAllowed: Can't go when in Green.
statemachine.exceptions.TransitionNotAllowed: Can't go when in Yellow.

```

Keeping the same state as expected:

```py
>>> traffic_light.green.is_active
>>> sm.yellow.is_active
True

```

And you can pass arbitrary positional or keyword arguments to the event, and
they will be propagated to all actions and callbacks:
A human-readable name is automatically derived from the `State.id`, which is used on the messages
and in diagrams:

```py
>>> traffic_light.cycle(message="Please, now slowdown.")
'Running cycle from green to yellow. Please, now slowdown.'
>>> sm.current_state.name
'Yellow'

```

Expand Down
48 changes: 45 additions & 3 deletions docs/actions.md
Expand Up @@ -225,9 +225,9 @@ It's also possible to use an event name as action to chain transitions.
**Be careful to not introduce recursion errors**, like `loop = initial.to.itself(after="loop")`, that will raise `RecursionError` exception.
```

### Bind event actions using decorator syntax
### Bind transition actions using decorator syntax

The action will be registered for every {ref}`transition` associated with the event.
The action will be registered for every {ref}`transition` in the list associated with the event.


```py
Expand Down Expand Up @@ -333,6 +333,48 @@ Actions and Guards will be executed in the following order:
- `after_transition()`


## Return values

Currently only certain actions' return values will be combined as a list and returned for
a triggered transition:

- `before_transition()`

- `before_<event>()`

- `on_transition()`

- `on_<event>()`

Note that `None` will be used if the action callback does not return anything, but only when it is
defined explicitly. The following provides an example:

```py
>>> class ExampleStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself()
...
... def before_loop(self):
... return "Before loop"
...
... def on_transition(self):
... pass
...
... def on_loop(self):
... return "On loop"
...

>>> sm = ExampleStateMachine()

>>> sm.loop()
['Before loop', None, 'On loop']

```

For {ref}`RTC model`, only the main event will get its value list, while the chained ones simply get
`None` returned. For {ref}`Non-RTC model`, results for every event will always be collected and returned.


(dynamic-dispatch)=
## Dynamic dispatch
Expand All @@ -341,7 +383,7 @@ Actions and Guards will be executed in the following order:
Guards. This means that you can declare an arbitrary number of `*args` and `**kwargs`, and the
library will match your method signature of what's expected to receive with the provided arguments.

This means that if on your `on_enter_<state.id>()` or `on_execute_<event>()` method, you need to know
This means that if on your `on_enter_<state.id>()` or `on_<event>()` method, you need to know
the `source` ({ref}`state`), or the `event` ({ref}`event`), or access a keyword
argument passed with the trigger, just add this parameter to the method and It will be passed
by the dispatch mechanics.
Expand Down
8 changes: 8 additions & 0 deletions docs/api.md
Expand Up @@ -20,6 +20,14 @@
:members:
```

## States (class)

```{eval-rst}
.. autoclass:: statemachine.states.States
:noindex:
:members:
```

## Transition

```{seealso}
Expand Down
Binary file added docs/images/readme_trafficlightmachine.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f95391a

Please sign in to comment.