Metadata-Version: 2.4
Name: micro-statemachine
Version: 0.1.2
Summary: A tiny generic state machine with implicit states and C#-style events.
Project-URL: Homepage, https://github.com/arefmq/micro-statemachine
Project-URL: Repository, https://github.com/arefmq/micro-statemachine
Project-URL: Issues, https://github.com/arefmq/micro-statemachine/issues
Author: Aref Mehr
License-Expression: MIT
License-File: LICENSE
Keywords: events,fsm,state,state machine,transition
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: test
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

<p align="center">
  <img src="logo.png" alt="micro-statemachine logo" width="320">
</p>

# 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`, and `remain` per state with the
  `+=` / `-=` operator, just like C# events.
- **Zero dependencies**, fully type-hinted, single small module.

## Install

```bash
pip install micro-statemachine
```

## Quick start

```python
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:

```python
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):

```python
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:

```python
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:

```python
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 `-=`:

```python
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:

```python
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](https://mermaid.js.org/) 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):

```python
sm.happy_enter += lambda: None
sm.failed_enter += lambda: None
sm.set_transitions(happy_to_failed=False)
sm.to_happy()
print(sm.generate_chart())
```

````text
```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
