Skip to main content

A pure python small Mealy state machine library

Project description

SNanoSM

A pure Python, minimalistic, and fully typed library for the implementation of Mealy Finite State Machines.

Part of Nobody Industry's MFFP (Made From First Principles) set of libraries.


Table of Contents

  1. Overview & Core Concept
  2. Installation
  3. Quick Start (Binary Inverter)
  4. Advanced Usage (Sequence Detector & Fallback Matching)
  5. API Reference
  6. Testing & Verification

Overview & Core Concept

A Mealy State Machine is a finite-state machine whose output values are determined both by its current state and its current inputs.

In mealy.py, this is modeled by:

  • States: Unique nodes in the machine.
  • Transitions: Directed connections between states triggered by specific inputs.
  • Action Functions (Outputs): Arbitrary callbacks associated with transitions that receive a mutable user-defined context object.

[!NOTE] By passing a mutable context down to transition actions, you can build rich state-dependent behavior while keeping the machine's state logic simple and decoupled.


Installation

Since the library uses pyproject.toml with the Hatchling build backend, you can install it locally in editable mode or build it using standard tools:

# Install in editable mode
pip install -e .

# Or build the package
python -m build

Quick Start (Binary Inverter)

Here is a simple example demonstrating how to invert a binary string ("0" becomes "1", "1" becomes "0") using inverter.py.

from typing import Tuple, TypedDict
from snanosm.mealy import Machine

# Define a context to hold our state machine's output and metadata
class Context(TypedDict):
    result: str
    n_chars: int

def add_to_result(context: Context, c: str) -> None:
    context["result"] += c
    context["n_chars"] += 1

def inverter(input_string: str) -> Tuple[str, int]:
    # Initialize the mutable context
    context: Context = {
        "result": "",
        "n_chars": 0
    }
    
    # Create the machine with the context
    m = Machine(context)
    
    # Add a start state "S"
    m.add_state("S", is_start_state=True)
    
    # Define transitions: when in state "S" and input is "0", execute action and stay in "S"
    m.add_transition("0", "S", "S", lambda ctx: add_to_result(ctx, "1"))
    m.add_transition("1", "S", "S", lambda ctx: add_to_result(ctx, "0"))
    
    # Process inputs sequentially
    for c in input_string:
        m.process_input(c)
        
    return context["result"], context["n_chars"]

if __name__ == '__main__':
    res, count = inverter("000011110010")
    print(f"Result: {res}, Characters processed: {count}")
    # Output: Result: 111100001101, Characters processed: 12

Advanced Usage (Sequence Detector & Fallback Matching)

The sequence detector in detector.py searches for the substring "AB" within a stream of characters. It illustrates the use of TransitionInputEnum to define catch-all transitions when no specific input matches.

from typing import TypedDict
from snanosm.mealy import Machine, TransitionInputEnum

class Context(TypedDict):
    count: int
    position: int
    matches: int

def dinc(context: Context):
    context["count"] += 1

def dset(context: Context):
    context["position"] = context["count"]
    context["count"] += 1

def dprn(context: Context):
    context["count"] += 1
    print(f"SUBSTRING FOUND AT POSITION: {context['position']}")
    context["matches"] += 1

def detector(input_string: str) -> int:
    initial_context: Context = {
        "count": 0,
        "position": 0,
        "matches": 0,
    }
    
    m = Machine(initial_context)
    m.add_state("Q0", is_start_state=True)
    m.add_state("Q1")
    
    # Q0 -> Q1 on 'A', saving the start position
    m.add_transition("A", "Q0", "Q1", lambda context: dset(context))
    # Catch-all transition: Q0 -> Q0 for any input other than 'A'
    m.add_transition(TransitionInputEnum.MATCH_REST, "Q0", "Q0", lambda context: dinc(context))
    
    # Q1 -> Q0 on 'B', printing match information
    m.add_transition("B", "Q1", "Q0", lambda context: dprn(context))
    # Catch-all transition: Q1 -> Q0 for any input other than 'B'
    m.add_transition(TransitionInputEnum.MATCH_REST, "Q1", "Q0", lambda context: dinc(context))
    
    for c in input_string:
        m.process_input(c)
        
    return initial_context["matches"]

API Reference

Core Abstractions

Class/Type Description
InputProtocol A typing protocol requiring __eq__ and __hash__. Any hashable, equatable Python object can serve as machine input.
TransitionInputEnum Enum containing special transition inputs (e.g., MATCH_REST).
State Represents a state node in the state machine.
Transition Represents an edge between states triggered by a transition input.
Machine The core finite state machine runner.

API Details

InputProtocol

class InputProtocol(Protocol):
    def __eq__(self, __o: Self) -> bool: ...
    def __hash__(self) -> int: ...

Any custom object used as an input to Machine.process_input must implement this protocol (or be natively hashable and equatable, e.g. strings, integers, frozen dataclasses).

TransitionInputEnum

  • MATCH_REST: Activates if no matching transition input is found for the current state. Useful for defining default fallback transitions.

State

  • get_name() -> str: Returns the state's name.
  • __str__() -> str: Returns [State <state_name>].

Transition

  • execute_output(context): Executes the output callback if it was supplied.
  • get_destination_name_hash() -> int: Returns the hash of the destination state name.
  • __str__() -> str: Returns [Transition (<origin>, <destination>, <input>)].

Machine

  • __init__(initial_context: object = None): Initializes the machine. Sets up the configuration using an optional initial context. If not provided, an empty dict {} is instantiated.
  • add_state(state_name: str, is_start_state: bool = False, is_end_state: bool = False) -> None: Registers a new state node in the machine.

    [!WARNING] State names must not start with # (reserved for internal configurations). A machine cannot have multiple start states.

  • add_transition(transition_input: TransitionInput, origin_name: str, destination_name: str, output_function: Optional[Callable[[Optional[object]], None]]) -> None: Registers a transition edge between two existing states.
    • transition_input: An input conforming to InputProtocol or TransitionInputEnum.
    • output_function: A callable accepting context, run upon transitioning.
  • process_input(i: Input) -> None: Processes a single input token. It evaluates transitions registered under the current state.
    • If a transition matching i is registered, it will be executed.
    • If no matching transition is found but a MATCH_REST transition is registered, that fallback is executed.
    • If no valid transition is found, it raises a ValueError.
  • reset() -> None: Resets the state machine's active state back to the start state, and re-assigns the context back to initial_context.
  • get_current_state() -> Optional[State]: Returns the current State object, or None if the machine has not started or processed any inputs.
  • is_in_final_state() -> bool: Returns True if the machine's current state is registered as an end state.
  • __str__() -> str: Returns a structured string layout of the machine structure, lists of states, and transitions.

Testing & Verification

Unit tests are located in test_mealy.py. To run the test suite, navigate to the project directory and execute:

PYTHONPATH=src python -m unittest discover -s tests

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snanosm-1.0.1.tar.gz (7.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

snanosm-1.0.1-py3-none-any.whl (6.7 kB view details)

Uploaded Python 3

File details

Details for the file snanosm-1.0.1.tar.gz.

File metadata

  • Download URL: snanosm-1.0.1.tar.gz
  • Upload date:
  • Size: 7.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for snanosm-1.0.1.tar.gz
Algorithm Hash digest
SHA256 19f320bfe1a960b0b06eaf4bc31546237684fec11d2c1cb8308aabb9ffea20d0
MD5 49bda206dd07ddbf1d3951513fe31da9
BLAKE2b-256 2a725520108ca230e643e5f40c41629974c27a30ce858429e76782ce5594cac5

See more details on using hashes here.

File details

Details for the file snanosm-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: snanosm-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 6.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for snanosm-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d54fa8f78b746853f4b872007c04bb28e4114c102c76cb0961eedea981dd4867
MD5 92f44539519878d2696139ff7eb6a599
BLAKE2b-256 797955305cb249e51fb8484248ebdd92837019e923d1c0d90610e0650cdf58fc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page