A tiny generic state machine with implicit states and C#-style events.
Project description
micro-statemachine
A tiny generic state machine with implicit states.
- No boilerplate — states are never declared. Calling
sm.to_<state>()moves into that state, creating it on the fly. - Everything allowed by default — every transition is permitted unless you explicitly forbid it.
- Event hooks — subscribe to
enter,exit, andremainper state with the+=/-=operator, just like C# events. - Zero dependencies, fully type-hinted, single small module.
Install
pip install micro-statemachine
Quick start
import logging
from micro_statemachine import u_states
logger = logging.getLogger(__name__)
sm = u_states["connection"]
# Subscribe handlers with +=
sm.happy_enter += lambda: logger.info("entered happy state")
sm.failed_enter += lambda: logger.warning("Failed!")
# `remain` fires when you transition into the state you're already in.
sm.failed_remain += lambda: logger.warning("still in failed")
sm.to_failed() # fires failed_enter
sm.to_failed() # already failed -> fires failed_remain
sm.to_happy() # fires failed_exit, then happy_enter
Restricting transitions
By default any transition is allowed. Forbid specific ones with
set_transitions, using the <source>_to_<target> naming convention:
sm.set_transitions(happy_to_failed=True, failed_to_happy=False)
sm.to_failed() # ok
sm.to_happy() # raises TransitionError
A forbidden transition raises TransitionError.
Current state
Read the active state with sm.state or the sm.current_state alias
(both are None until the first transition):
sm.to_happy()
sm.current_state # "happy"
Assigning to current_state sets the state directly — no transition is run,
no enter/exit/remain events fire, and blocking rules are ignored. Handy for
restoring a machine to a known state:
sm.current_state = "happy" # jump straight to happy, silently
sm.current_state = None # reset to the initial state
none and any in transition rules
Either side of a <source>_to_<target> rule may also be one of two reserved
words:
none— the initial (pre-transition) state, so you can constrain the very first move.any— a wildcard matching every state (including the initial one).
Explicit rules always override wildcard ones, which lets you flip the default from "everything allowed" to an allowlist:
sm.set_transitions(
state_1_to_state_2=True, # plain explicit rule
none_to_state_1=True, # the first move may go to state_1...
none_to_any=False, # ...and nowhere else
)
sm.to_state_2() # raises TransitionError (none -> any is forbidden)
sm.to_state_1() # ok — the explicit rule wins over the wildcard
When several rules could apply, the most specific wins:
<source>_to_<target> > <source>_to_any > any_to_<target> > any_to_any.
Events
Each state exposes three events, resolved lazily on first access:
| Attribute | Fires when… |
|---|---|
sm.<state>_enter |
a transition into <state> from a different state |
sm.<state>_exit |
a transition out of <state> |
sm.<state>_remain |
to_<state>() is called while already in <state> |
Handlers are plain zero-argument callables. Add with +=, remove with -=:
def on_enter():
print("hi")
sm.happy_enter += on_enter
sm.happy_enter -= on_enter
Registry
u_states is a lazy registry — indexing it creates the machine on first
access, so different modules can share one machine by name:
from micro_statemachine import u_states
u_states["connection"] # same StateMachine everywhere
You can also construct machines directly with StateMachine(name).
Diagram
sm.generate_chart() returns a Mermaid state diagram
as a fenced markdown block — paste it into a README or GitHub issue and it
renders, with the current state highlighted and transition rules drawn as
labelled edges (none becomes the initial marker, any a wildcard node):
sm.happy_enter += lambda: None
sm.failed_enter += lambda: None
sm.set_transitions(happy_to_failed=False)
sm.to_happy()
print(sm.generate_chart())
```mermaid
stateDiagram-v2
[*] --> happy
failed
happy
happy --> failed: ✗ forbidden
classDef current fill:#ffd54f,stroke:#333,font-weight:bold;
class happy current
```
States are inferred from subscribed handlers, transition rules, and the current state (they're never declared explicitly).
Naming rules
State names may contain _ but must not be empty or contain the reserved words
to, enter, exit, or remain (they'd collide with the transition and event
syntax). none and any are reserved for transition rules, so they can't be a
state name on their own — but they're fine as parts of one (any_state,
none_selected). Invalid names raise ValueError.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file micro_statemachine-0.1.0.tar.gz.
File metadata
- Download URL: micro_statemachine-0.1.0.tar.gz
- Upload date:
- Size: 3.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edac18564ea7e170aee9f62350b7ff41fa997f50334fdf50f1f6c60c107fc258
|
|
| MD5 |
8ce8c997ded0e4675a79cbccc6f51d12
|
|
| BLAKE2b-256 |
e1b6f025044b3fabfc97d41c22f4f1a51af97aa4781f35089d490142c781c1e5
|
File details
Details for the file micro_statemachine-0.1.0-py3-none-any.whl.
File metadata
- Download URL: micro_statemachine-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7e0062ca73843e47cdb3b68e5e963a3f37e5330f6c63ca7e40f371b4445d2cb
|
|
| MD5 |
dd215e61d05ebe7bddaf7a7510d8f8ea
|
|
| BLAKE2b-256 |
682d3dc982e5dfd0c8d4f6029e0f91fea68037403020463b026c948805b4eb22
|