{"id":17167553,"url":"https://github.com/johnbywater/stackbased-fsm","last_synced_at":"2025-06-28T14:34:40.186Z","repository":{"id":62321527,"uuid":"559627954","full_name":"johnbywater/stackbased-fsm","owner":"johnbywater","description":null,"archived":false,"fork":false,"pushed_at":"2022-11-08T22:14:25.000Z","size":67,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-29T23:11:43.143Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnbywater.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-10-30T17:44:51.000Z","updated_at":"2022-10-30T17:47:23.000Z","dependencies_parsed_at":"2023-01-21T07:46:57.657Z","dependency_job_id":null,"html_url":"https://github.com/johnbywater/stackbased-fsm","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnbywater%2Fstackbased-fsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnbywater%2Fstackbased-fsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnbywater%2Fstackbased-fsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnbywater%2Fstackbased-fsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnbywater","download_url":"https://codeload.github.com/johnbywater/stackbased-fsm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245325349,"owners_count":20596843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T23:09:05.698Z","updated_at":"2025-03-24T18:21:59.001Z","avatar_url":"https://github.com/johnbywater.png","language":"Python","readme":"# Welcome to Stack-Based Finite State Machine\n\nThis project provides a stack-based finite state machine.\n\nA state machine has a stack. States are pushed onto and popped\nfrom stack. The state machine will call 'enter' and 'exit' methods on the\nstates when they are 'pushed' and 'popped'. The states will then push and pop\nother states, according to their individual implementation. States have\naccess to the stack, and also to a context object containing \"global\" variables.\n\nA few basic state classes are also provided, with which \"programs\"\ncan be defined. Programs define a nested set of state types, from which\nstates will be constructed.\n\n\n## Installation\n\n    $ pip install stackbased-fsm\n\n## Getting started\n\nWe can use the `Context` class to define an example context object.\n\n```python\nfrom stackbased_fsm import Context\n\nclass ExampleContext(Context):\n    def __init__(self):\n        self.a = 1\n        self.b = \"2\"\n        self.c = []\n\n\ncontext = ExampleContext()\n```\n\nWe can use the `StateMachine` class to construct state machine object that uses the\ncontext object.\n\n```python\nfrom stackbased_fsm import StateMachine\n\nsm = StateMachine(context=context)\n```\n\nThe state machine has a stack of states. All states on the state machine's stack will\nhave access to the context object. The attributes of the context object are like\nthe \"global\" variables for the states of the state machine.\n\nWe can use the `State` class to define types of state for our state machine. It is a\ngeneric class, which has one type variable that is expected to be a context class.\n\n```python\nfrom stackbased_fsm import State\n\nclass ExampleState(State[ExampleContext]):\n    pass\n```\n\nWe can use the `ExampleState` as a base class to define `IncrementA`, `AssignB`, and\n`AppendC` which will increment `a`, assign to `b`, and append to `c` respectively.\n\n```python\n\nclass IncrementA(ExampleState):\n    def enter(self) -\u003e None:\n        self.context.a += 1\n        self.pop()\n\n\nclass AssignB(ExampleState):\n    def enter(self) -\u003e None:\n        self.context.b = \"def\"\n        self.pop()\n\n\nclass AppendC(ExampleState):\n    def enter(self) -\u003e None:\n        self.context.c.append(\"xyz\")\n        self.pop()\n```\n\nThe state machine has a `run()` method which can be used by a client to pass\na \"program\" to the state machine. A program is a type construct, in the simplest\ncase a single state class, and more usually a nested set of subscripted generic\nstate classes (see below).\n\n```python\nsm.run(IncrementA)\n\nassert context.a == 2\n```\n\nWe can use the `SequenceOfStates` class to define a sequence of states. It is a\nvariadic generic state class, and so can take any number of state classes as its\ntype arguments.\n\n```python\nfrom stackbased_fsm import SequenceOfStates\n\nExampleSequence = SequenceOfStates[IncrementA, AssignB, AppendC]\n```\n\nWe can run the sequence and check the context has been updated.\n\n```python\nsm.run(ExampleSequence)\n\nassert context.a == 3\nassert context.b == \"def\"\nassert context.c == [\"xyz\"]\n```\n\nThe state machine object has methods to `push()`, `pop()`, and `poppush()`\nstates on the stack. State objects have four methods, `enter()`, `exit()`,\n`suspend()` and `resume()`.\n\nWhen a program is passed to the state machine using the `run()` method, a state object\nis constructed from the root type of the program (a type of state). The state object\nis then pushed onto the stack, and its `enter()` method is called. The state machine\nthen iterates over the stack, detecting when states have been pushed and popped,\ncalling methods on the stacked states accordingly, until the stack is empty.\n\nAfter a state has been pushed onto the stack, the state's `enter()` method\nwill be called. After a state is popped off the stack, the state's `exit()` method will\nbe called. A state's `suspend()` method will be called when another state is pushed\non top of it, and its `resume()` method will be called after that state is popped off.\n\nFor example, the `SequenceOfStates` works in the following way. When it is pushed onto\nthe stack, its `enter()` method is called. Its `enter()` method will push the first\nitem in the sequence onto the stack. Its `suspend()` method is then called, and then\nthe `enter()` method of the first item is called. If the pushed state neither pushes\nor pops another state, it will be automatically popped and its `exit()` method will\nbe called. When that state is popped, the `resume()` method of the sequence will be\ncalled, which will push the next item in the sequence onto the stack. After all items\nhave been pushed onto the stack, the sequence's `resume()` method will call `pop()`,\nwhich will result in itself being popped off the stack. This may result in an empty\nstack, and the end of a program.\n\n\n## Conditions and conditioned states\n\nThe \"condition\" state class `ConditionState` can be used to define conditions.\nWhen the `enter()` method of a condition state is called, its `condition()` method\nwill be called. The `condition()` method is abstract on the `ConditionState` class\nand is expected to be implemented on subclasses. This method is expected to return\na Boolean value (*true* or *false*). This value will be used by the condition state's `enter()`\nmethod to call the `set_condition_result()` method of state below it on the stack,\nwhich is expected to be a \"conditioned\" state, and therefore have such a method.\n\nIn the example below, the class `AIsLessThan5` has a `condition()` method that\nreturns `True` if `a` is less than 5.\n\n```python\nfrom stackbased_fsm import ConditionState\n\n\nclass AIsLessThan5(ConditionState):\n    def condition(self):\n        return self.context.a \u003c 5\n```\n\nConditions are used by conditioned states. For example, the classes\n`RepeatUntil`, `RepeatWhile` are conditioned states. These conditioned\nstates take two type variables. The first type variable is expected to\nbe a type of condition. The second type variable is expected to be a type\nof state. These conditioned states alternate between pushing the condition\nstate and then pushing the other state. `RepeatUntil` continues in this way\nuntil the condition is *true*. `RepeatWhile` continues in this way until\nthe condition is *false*.\n\nIn the example below, `ExampleLoop` will push `IncrementA` again and again\nso long as `a` is less than 5.\n\n```python\nfrom stackbased_fsm import RepeatWhile\n\nExampleLoop = RepeatWhile[AIsLessThan5, IncrementA]\n\nsm.run(ExampleLoop)\n```\n\nThe result is the value of `a` is 5.\n\n```python\nassert context.a == 5, context.a\n```\n\nConditions can be grouped with `AnyCondition` and `AllConditions` (aliased as\n`Or` and `And` respectively).\n\n\n## Developers\n\n### Install Poetry\n\nThe first thing is to check you have Poetry installed.\n\n    $ poetry --version\n\nIf you don't, then please [install Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer).\n\nIt will help to make sure Poetry's bin directory is in your `PATH` environment variable.\n\nBut in any case, make sure you know the path to the `poetry` executable. The Poetry\ninstaller tells you where it has been installed, and how to configure your shell.\n\nPlease refer to the [Poetry docs](https://python-poetry.org/docs/) for guidance on\nusing Poetry.\n\n### Setup for PyCharm users\n\nYou can easily obtain the project files using PyCharm (menu \"Git \u003e Clone...\").\nPyCharm will then usually prompt you to open the project.\n\nOpen the project in a new window. PyCharm will then usually prompt you to create\na new virtual environment.\n\nCreate a new Poetry virtual environment for the project. If PyCharm doesn't already\nknow where your `poetry` executable is, then set the path to your `poetry` executable\nin the \"New Poetry Environment\" form input field labelled \"Poetry executable\". In the\n\"New Poetry Environment\" form, you will also have the opportunity to select which\nPython executable will be used by the virtual environment.\n\nPyCharm will then create a new Poetry virtual environment for your project, using\na particular version of Python, and also install into this virtual environment the\nproject's package dependencies according to the `pyproject.toml` file, or the\n`poetry.lock` file if that exists in the project files.\n\nYou can add different Poetry environments for different Python versions, and switch\nbetween them using the \"Python Interpreter\" settings of PyCharm. If you want to use\na version of Python that isn't installed, either use your favourite package manager,\nor install Python by downloading an installer for recent versions of Python directly\nfrom the [Python website](https://www.python.org/downloads/).\n\nOnce project dependencies have been installed, you should be able to run tests\nfrom within PyCharm (right-click on the `tests` folder and select the 'Run' option).\n\nBecause of a conflict between pytest and PyCharm's debugger and the coverage tool,\nyou may need to add ``--no-cov`` as an option to the test runner template. Alternatively,\njust use the Python Standard Library's ``unittest`` module.\n\nYou should also be able to open a terminal window in PyCharm, and run the project's\nMakefile commands from the command line (see below).\n\n### Setup from command line\n\nObtain the project files, using Git or suitable alternative.\n\nIn a terminal application, change your current working directory\nto the root folder of the project files. There should be a Makefile\nin this folder.\n\nUse the Makefile to create a new Poetry virtual environment for the\nproject and install the project's package dependencies into it,\nusing the following command.\n\n    $ make install-packages\n\nIt's also possible to also install the project in 'editable mode'.\n\n    $ make install\n\nPlease note, if you create the virtual environment in this way, and then try to\nopen the project in PyCharm and configure the project to use this virtual\nenvironment as an \"Existing Poetry Environment\", PyCharm sometimes has some\nissues (don't know why) which might be problematic. If you encounter such\nissues, you can resolve these issues by deleting the virtual environment\nand creating the Poetry virtual environment using PyCharm (see above).\n\n### Project Makefile commands\n\nYou can run tests using the following command.\n\n    $ make test\n\nYou can check the formatting of the code using the following command.\n\n    $ make lint\n\nYou can reformat the code using the following command.\n\n    $ make fmt\n\nTests belong in `./tests`. Code-under-test belongs in `./stackbased_fsm`.\n\nSee the [Python eventsourcing project](https://github.com/pyeventsourcing/eventsourcing)\nfor more information and guidance about developing event-sourced applications.\n\nEdit package dependencies in `pyproject.toml`. Update installed packages (and the\n`poetry.lock` file) using the following command.\n\n    $ make update-packages\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnbywater%2Fstackbased-fsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnbywater%2Fstackbased-fsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnbywater%2Fstackbased-fsm/lists"}