friendly_states

Declarative, explicit, tool-friendly finite state machines in Python

MIT License

Downloads
33
Stars
21
Committers
1

{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# friendly_states\n", "\n", " \n", "\n", "This is a Python library for writing finite state machines in a way that's easy to read and prevents mistakes with the help of standard linters and IDEs.\n", "\n", "You can try this README in an interactive notebook in binder.\n", "\n", " * Introduction\n", " * Basic usage steps\n", " * Abstract state classes\n", " * BaseState - configuring state storage and changes\n", " * State machine metadata\n", " * Slugs and labels\n", " * Troubleshooting\n", " * Recipes\n", " * Construct and draw a graph\n", " * Creating multiple similar machines\n", " * Dynamically changing the attribute name\n", " * On enter/exit state callbacks\n", " * Django integration\n", "\n", "## Introduction\n", "\n", "Here's a simple example of declaring a state machine:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "from future import annotations\n", "from friendly_states import AttributeState\n", "\n", "\n", "class TrafficLightMachine(AttributeState):\n", " is_machine = True\n", "\n", " class Summary:\n", " Green: [Yellow]\n", " Yellow: [Red]\n", " Red: [Green]\n", "\n", "\n", "class Green(TrafficLightMachine):\n", " def slow_down(self) -> [Yellow]:\n", " print("Slowing down...")\n", "\n", "\n", "class Yellow(TrafficLightMachine):\n", " def stop(self) -> [Red]:\n", " print("Stop.")\n", "\n", "\n", "class Red(TrafficLightMachine):\n", " def go(self) -> [Green]:\n", " print("Go!")\n", "\n", "\n", "TrafficLightMachine.complete()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That looks like a lot of code, but it's pretty simple:\n", "\n", "1. TrafficLightMachine declares the root of a state machine.\n", "2. There are three states, which are subclasses of the machine: Green, Yellow, and Red.\n", "3. Each state declares which transitions it allows to other states using functions with a return annotation (->). In this case, each state has one transition to one other state. For example, Green can transition to Yellow via the slow_down method.\n", "4. TrafficLightMachine.complete() makes the machine ready for use and checks that everything is correct. In particular it verifies that the Summary class inside TrafficLightMachine matches the actual states and transitions declared. Each line of the summary shows all the possible output states from each state.\n", "\n", "As we'll see later, a lot of the boilerplate can be generated for you, so don't worry about the effort of writing all those classes.\n", "\n", "To use this machine, first we need an object that can store its own state. When using this library, state can be stored however you want, but the way we've declared our machine means it expects an attribute called state:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "class TrafficLight:\n", " def init(self, state):\n", " self.state = state\n", "\n", "light = TrafficLight(Green)\n", "assert light.state is Green" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The magic happens when making transitions:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Slowing down...\n", "Stop.\n" ] } ], "source": [ "Green(light).slow_down()\n", "Yellow(light).stop()\n", "assert light.state is Red" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Making an instance of a state class such as Green(light) automatically checks that light is actually in the Green state and raises an exception if it's not.\n", "Then you can call methods just like any other class. The state will automatically be changed based on the method's return annotation.\n", "\n", "Why is this exciting? Because it's obvious to tools which transitions are available to each state. Your IDE can autocomplete Green(light).slo for you, and Green(light).stop() will be highlighted as an error before you even run the code.\n", "\n", "Compare this with the popular library transitions. States, transitions, and callbacks are all declared using strings, so it's easy to make typos and tools can't help you. In fact you have to stop your tools from warning you about all the attributes it magically generates which you have to use. There's no easy way to see all the transitions or output states from a particular state. Callbacks can be declared far away from transitions.\n", "\n", "By contrast, when using friendly_states, there are no strings or magic attributes anywhere. Code is always naturally grouped together: all the transitions specific to a state appear inside that class, and logic related to a transition is in that function where you'd expect it.\n", "\n", "Here is the equivalent of the main example in the transitions doc implemented using friendly_states.\n", "\n", "## Basic usage steps\n", "\n", "1) Ensure that you are using Python 3.7+.\n", "\n", "2) Install this library with pip install friendly_states.\n", "\n", "3) Add the line from __future__ import annotations at the top of the file where you define your state machine.\n", "\n", "4) Declare the root of your state machine by inheriting from the appropriate class (usually AttributeState, see the BaseState section) and setting is_machine = True in the body, like so:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "from future import annotations\n", "from friendly_states import AttributeState\n", " \n", "class MyMachine(AttributeState):\n", " is_machine = True\n", " \n", " class Summary:\n", " Waiting: [Doing, Finished]\n", " Doing: [Checking, Finished]\n", " Checking: [Finished]\n", " Finished: []\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Declaring a summary is optional, but highly recommended. The class must be named Summary.\n", "It should declare the name of every state your machine will have, \n", "each annotated with a list of every state that can be reached directly from that state by any transition.\n", " \n", "When you call MyMachine.complete(), this summary will be checked, and an exception will be raised explaining the problem if it doesn't match your state classes. In particular if a class is missing the exception will contain generated source code for that class so you can spend less time writing boilerplate. Let's try it now:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Missing states:\n", "\n", "class Waiting(MyMachine):\n", " def to_doing(self) -> [Doing]:\n", " pass\n", "\n", " def to_finished(self) -> [Finished]:\n", " pass\n", "\n", "\n", "class Doing(MyMachine):\n", " def to_checking(self) -> [Checking]:\n", " pass\n", "\n", " def to_finished(self) -> [Finished]:\n", " pass\n", "\n", "\n", "class Checking(MyMachine):\n", " def to_finished(self) -> [Finished]:\n", " pass\n", "\n", "\n", "class Finished(MyMachine):\n", " pass\n", "\n", "\n", "\n" ] } ], "source": [ "try:\n", " MyMachine.complete()\n", "except Exception as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can copy the code above and will have a working state machine matching the summary. It's usually not exactly what you need, but it should save you a lot of time for the next step.\n", "\n", "5) Write a class for each state. Make sure you call .complete() at the end." ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "class Waiting(MyMachine):\n", " def start_doing(self) -> [Doing]:\n", " print('Starting now!') \n", "\n", " def skip(self) -> [Finished]:\n", " pass \n", "\n", "\n", "class Doing(MyMachine):\n", " def done(self, result) -> [Checking, Finished]:\n", " self.obj.result = result\n", " if self.obj.needs_checking():\n", " return Checking\n", " else:\n", " return Finished\n", "\n", "\n", "class Checking(MyMachine):\n", " def check(self) -> [Finished]:\n", " print('Looks good!')\n", "\n", "\n", "class Finished(MyMachine):\n", " pass\n", "\n", "\n", "MyMachine.complete()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "State classes must inherit (directly or indirectly) from the machine class, e.g. class Waiting(MyMachine):. A class can have any number of transitions. Waiting has two separate transitions, while Finished has none, meaning you can't leave that state. It can also have any other methods or attributes which are not transitions, like a normal class.\n", "\n", "A transition is any method which has a return annotation (the bit after the -> in the function definition) which is a list of one or more states that will be the result of this transition. For example, this code:\n", "\n", "python\n", "class Waiting(MyMachine):\n", " def start_doing(self) -> [Doing]:\n", "\n", "\n", "means that start_doing is a transition from the state Waiting to the state Doing, and calling that method will change the state.\n", "\n", "The transition Doing.done demonstrates several interesting things:\n", "\n", "- A transition can have multiple possible output states. In that case the method must return one of those states to indicate which one to switch to.\n", "- Transitions are just like normal methods and can accept whatever arguments you want.\n", "- States have an attribute obj which is the object that was passed to the class when it was constructed. This lets you interact with the object whose state is changing.\n", "\n", "6) The machine doesn't store the state itself, make a different class for that:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "class Task:\n", " def init(self):\n", " self.state = Waiting\n", " self.result = None\n", " \n", " def needs_checking(self):\n", " return self.result < 5\n", "\n", " \n", "task = Task()\n", "assert task.result is None\n", "assert task.state is Waiting" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Our example machine expects to find an attribute called state on its objects, as we've provided here. If you have different needs, see the BaseState section.\n", "\n", "7) To change the state of your object, you first need to know what state it's in right now. Sometimes you'll need to check, but often it'll be obvious in the context of your application. For example, if we have a queue of fresh tasks, any task we pop from that queue will be in state Waiting.\n", "\n", "Construct an instance of the correct state class and pass your object:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Waiting(obj=<main.Task object at 0x1173677f0>)" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Waiting(task)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you get the current state of a task wrong, that means there's a bug in your code! \n", "It will throw an exception before you can even call any transitions:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<main.Task object at 0x1173677f0> should be in state Doing but is actually in state Waiting\n" ] } ], "source": [ "try:\n", " Doing(task)\n", "except Exception as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "8) Once you have an instance of the correct state, call whatever methods you want on it as usual. If the method is a transition, the state will automatically be changed afterwards:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "Waiting(task).skip()\n", "assert task.state is Finished" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you try calling a transition that doesn't exist for a state, the library doesn't even need to do anything. You'll just get the plain Python error you always get when calling a non-existent method, and your IDE/linter will warn you in advance:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "'Waiting' object has no attribute 'done'\n" ] } ], "source": [ "task = Task()\n", "try:\n", " Waiting(task).done(3)\n", "except AttributeError as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here are the other 2 possible paths for a task from waiting to finished:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Starting now!\n", "Looks good!\n" ] } ], "source": [ "task = Task()\n", "Waiting(task).start_doing()\n", "Doing(task).done(3)\n", "assert task.result == 3\n", "Checking(task).check()\n", "assert task.state is Finished" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Starting now!\n" ] } ], "source": [ "task = Task()\n", "Waiting(task).start_doing()\n", "Doing(task).done(7)\n", "# The result '7' doesn't need checking\n", "assert task.state is Finished" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Abstract state classes\n", "\n", "Sometimes you will have common behaviour that you need to share between state classes. In that case you can use class inheritance. Here's what you need to be aware of:\n", "\n", "1. You can't inherit from actual state classes.\n", "2. Transitions must live in classes that inherit (directly or indirectly) from the machine class.\n", "3. Classes that inherit from the machine (and thus can have transitions) but do not represent actual states (and thus can be inherited from) should have is_abstract = True in their body.\n", "\n", "Here's an example:" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "class TaskMachine2(AttributeState):\n", " is_machine = True\n", " \n", " class Summary:\n", " Waiting: [Doing, Finished]\n", " Doing: [Finished]\n", " Finished: []\n", "\n", " \n", "class Unfinished(TaskMachine2):\n", " is_abstract = True\n", " \n", " def finish(self) -> [Finished]:\n", " pass\n", "\n", " \n", "class Waiting(Unfinished):\n", " def start_doing(self) -> [Doing]:\n", " pass\n", "\n", "\n", "class Doing(Unfinished):\n", " pass\n", "\n", "\n", "class Finished(TaskMachine2):\n", " pass\n", "\n", "\n", "TaskMachine2.complete() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here the Waiting and Doing states are both subclasses of Unfinished so they get the finish transition for free, and you can see the result of this in the summary.\n", "\n", "If you have an object which is in one of these states but you're not sure which, and you'd like to call the finish transition, just use the Unfinished abstract class:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "import random\n", "\n", "for i in range(100):\n", " task = Task()\n", " \n", " # Randomly start doing about half the tasks\n", " if random.random() < 0.5:\n", " Waiting(task).start_doing()\n", " \n", " # Now the task might be either Waiting or Doing\n", " Unfinished(task).finish()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This will still fail if you try it on a finished task:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<main.Task object at 0x11401cb00> should be in state Unfinished but is actually in state Finished\n" ] } ], "source": [ "try:\n", " Unfinished(task).finish()\n", "except Exception as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, you may want to use methods and attributes in the class representing the actual current state. In particular you might have overridden methods in the concrete state classes and want the correct implementation to run. To allow this, instances of a state automatically change the class to the actual state of the object:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "task = Task()\n", "assert type(Unfinished(task)) is Waiting" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BaseState - configuring state storage and changes\n", "\n", "At the root of all the classes in the library is BaseState, which has two abstract methods get_state and set_state. Subclasses determine how the object stores state and what should happen when it changes.\n", "\n", "For example, here is the start of AttributeState which we've been using as the base of our machines so far:\n", "\n", "python\n", "class AttributeState(BaseState):\n", " attr_name = \"state\"\n", "\n", " def get_state(self):\n", " return getattr(self.obj, self.attr_name)\n", "\n", "\n", "You can declare a different attr_name in your machine class to store the state in that attribute of the object.\n", "\n", "If you're storing your state in a dict or similar object, you can instead use MappingKeyState, which starts like this:\n", "\n", "python\n", "class MappingKeyState(BaseState):\n", " key_name = \"state\"\n", "\n", " def get_state(self):\n", " return self.obj[self.key_name]\n", "\n", "\n", "It can often be useful to override set_state to add extra common behaviour when the state changes, e.g:" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [], "source": [ "class PrintStateChange(AttributeState):\n", " def set_state(self, previous_state, new_state):\n", " print(f"Changing {self.obj} from {previous_state} to {new_state}")\n", " super().set_state(previous_state, new_state)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "set_state is called after the transition method completes.\n", "\n", "So overall, your class hierarchy typically looks something like this:\n", "\n", "BaseState <- AttributeState <- Machine <- Abstract States <- Actual states" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## State machine metadata\n", "\n", "Machines, states, and transitions have a bunch of attributes that you can inspect:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "# All concrete (not abstract) states in the machine\n", "assert TaskMachine2.states == {Doing, Finished, Waiting}\n", "\n", "# All the transition functions available for this state\n", "assert Waiting.transitions == {Waiting.finish, Waiting.start_doing}\n", "\n", "# The transition functions defined directly on this class, i.e. not inherited\n", "assert Waiting.direct_transitions == {Waiting.start_doing}\n", "\n", "# Possible output states from this transition\n", "assert Waiting.start_doing.output_states == {Doing}\n", "\n", "# All possible output states from this state via any transition\n", "assert Waiting.output_states == {Doing, Finished}\n", "\n", "# Root of the state machine\n", "assert Waiting.machine is TaskMachine2\n", "\n", "# Booleans about the type of class\n", "assert TaskMachine2.is_machine and not Waiting.is_machine\n", "assert Waiting.is_state and not Unfinished.is_state\n", "assert Unfinished.is_abstract and not Waiting.is_abstract" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Slugs and labels\n", "\n", "Classes also have slug and label attributes which are mostly for use by Django but may be useful elsewhere. slug is for data storage and label is for human display.\n", "\n", "By default, slug is just the class name, while label is the class name with spaces inserted. Both can be overridden by declaring them in the class." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [], "source": [ "assert Waiting.slug == "Waiting"\n", "assert TaskMachine2.label == "Task Machine 2"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Troubleshooting\n", "\n", "If things are not working as expected, here are some things to check:\n", "\n", "- Check the attributes machine.states, state.transitions, and transition.output_states to see if they look as expected.\n", "- If you override set_state, remember to call super().set_state(...), unless you want to prevent the state from changing or you're subclassing BaseState directly.\n", "- Check that the annotation on your transition is a list, i.e. it starts and ends with []. For example this will not be recognised as a transition:\n", "\n", "python\n", " def start_doing(self) -> Doing:\n", "\n", "\n", "- If your transition has any decorators, make sure that the decorated function still has the original __annotations__ attribute. This is usually done by using functools.wraps when implementing the decorator.\n", "- Make sure that the object stores state the way the machine expects. Typically you'll be using AttributeState and you should make sure that attr_name ("state" by default) is correct. Note that a typical machine expects objects to have just one way of storing state - you can't use the same machine to change state stored in different attributes. To overcome this, see the recipe 'Dynamically changing the attribute name'.\n", "- Check that you've inherited your classes correctly. All states need to inherit from the machine." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Recipes\n", "\n", "The API of friendly_states is intentionally minimal. Here is how you can do some more complicated things.\n", "\n", "### Construct and draw a graph\n", "\n", "Here is how to create a graph with the popular library networkx:" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[Waiting, Finished, Doing, Checking]\n", "[(Waiting, Finished), (Waiting, Doing), (Doing, Finished), (Doing, Checking), (Checking, Finished)]\n" ] } ], "source": [ "import networkx as nx\n", "machine = MyMachine\n", "\n", "G = nx.DiGraph()\n", "for state in machine.states:\n", " for output_state in state.output_states:\n", " G.add_edge(state, output_state)\n", "\n", "print(G.nodes)\n", "print(G.edges)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To draw the graph with matplotlib:" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "<Figure size 432x288 with 1 Axes>" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%matplotlib inline\n", "nx.draw(G, with_labels=True, node_color='pink') " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To label each edge requires some more work:" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "<Figure size 432x288 with 1 Axes>" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "edge_labels = {}\n", "G = nx.DiGraph()\n", "for state in machine.states:\n", " for transition in state.transitions:\n", " for output_state in transition.output_states:\n", " edge = (state, output_state)\n", " G.add_edge(*edge)\n", " edge_labels[edge] = transition.name\n", "\n", "pos = nx.spring_layout(G)\n", "nx.draw(G, pos, with_labels=True, node_color='pink')\n", "nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating multiple similar machines\n", "\n", "Suppose you want to create several machines with similar states and transitions with duplicating code. You may think you can use inheritance somehow, but that won't work, and in fact a machine can't subclass another machine. Instead you must make a function which creates the classes locally. There are many ways this could be one depending on your needs. Here's an example with identical machines except that one has an additional state:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "from types import SimpleNamespace\n", "\n", "def machine_factory():\n", " class Machine(AttributeState):\n", " is_machine = True\n", "\n", " class CommonState1(Machine):\n", " def to_common_state_2(self) -> [CommonState2]:\n", " pass\n", "\n", " class CommonState2(Machine):\n", " pass\n", "\n", " return SimpleNamespace(\n", " Machine=Machine,\n", " CommonState1=CommonState1,\n", " CommonState2=CommonState2,\n", " )\n", "\n", "\n", "machine1 = machine_factory()\n", "\n", "class DifferentState(machine1.Machine):\n", " def to_common_state_2(self) -> [machine1.CommonState2]:\n", " pass\n", "\n", "machine1.Machine.complete()\n", "\n", "@machine1.Machine.check_summary\n", "class Summary:\n", " CommonState1: [CommonState2]\n", " CommonState2: []\n", " DifferentState: [CommonState2]\n", "\n", "\n", "machine2 = machine_factory()\n", "machine2.Machine.complete()\n", "\n", "@machine2.Machine.check_summary\n", "class Summary:\n", " CommonState1: [CommonState2]\n", " CommonState2: []" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dynamically changing the attribute name\n", "\n", "A typical AttributeState machine can only work with one attribute name. You may need to use the same machine with different object classes that use different attributes. One way is to follow a similar pattern to above with a configurable attribute name:\n", "\n", "python\n", "def machine_factory(name):\n", " class Machine(AttributeState):\n", " is_machine = True\n", " attr_name = name\n", "\n", " ...\n", "\n", "\n", "Another option, which may be useful for more complicated situations, is to subclass AttributeState to accept the attribute name on construction:" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [], "source": [ "class DynamicAttributeState(AttributeState):\n", " def init(self, obj, attr_name):\n", " # override the class attribute\n", " self.attr_name = attr_name\n", " \n", " # must call super after because it checks the state\n", " # in the attribute with the given name\n", " super().init(obj)\n", "\n", "class Machine(DynamicAttributeState):\n", " is_machine = True\n", "\n", "class Start(Machine):\n", " def to_end(self) -> [End]:\n", " pass\n", "\n", "class End(Machine):\n", " pass\n", "\n", "Machine.complete()\n", "\n", "thing = SimpleNamespace(state=Start, other_state=Start)\n", "\n", "assert thing.state is Start\n", "assert thing.other_state is Start\n", "\n", "Start(thing, "state").to_end()\n", "assert thing.state is End\n", "assert thing.other_state is Start\n", "\n", "Start(thing, "other_state").to_end()\n", "assert thing.state is End\n", "assert thing.other_state is End\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### On enter/exit state callbacks\n", "\n", "Whenever you want to execute some generic logic on every state transition, you should override set_state. But if you put all your code in there it may get long and confusing. If you want to group this logic into your state classes whenever a transition enters or exits that state, here's a mixin that you can apply to any machine:" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "class OnEnterExitMixin:\n", " def set_state(self, previous_state, new_state):\n", " previous_state(self.obj).on_exit(new_state)\n", " super().set_state(previous_state, new_state)\n", " new_state(self.obj).on_enter(previous_state)\n", "\n", " def on_exit(self, new_state):\n", " pass\n", "\n", " def on_enter(self, previous_state):\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then use it as follows:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Ending from Start\n" ] } ], "source": [ "class Machine(OnEnterExitMixin, AttributeState):\n", " is_machine = True\n", "\n", "class Start(Machine):\n", " def end(self) -> [End]:\n", " pass\n", "\n", "class End(Machine):\n", " def on_enter(self, previous_state):\n", " print(f"Ending from {previous_state}")\n", "\n", "Machine.complete()\n", "\n", "thing = SimpleNamespace(state=Start)\n", "Start(thing).end()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Django integration\n", "\n", "friendly_states can easily be used out of the box with Django. Basic usage looks like this:\n", "\n", "python\n", "from django.db import models\n", "from friendly_states.django import StateField, DjangoState\n", "\n", "class MyMachine(DjangoState):\n", " is_machine = True\n", " \n", "# ...\n", "\n", "class MyModel(models.Model):\n", " state = StateField(MyMachine)\n", "\n", "\n", "StateField is a CharField that stores the slug of the current state in the database while letting you use the actual state class objects in all your code, e.g:\n", "\n", "python\n", "obj = MyModel.objects.create(state=MyState)\n", "assert obj.state is MyState\n", "objects = MyModel.objects.filter(state=MyState)\n", "\n", "\n", "All keyword arguments are passed straight to CharField, except for max_length and choices which are ignored, see below.\n", "\n", "DjangoState will automatically save your model after state transitions. To disable this, set auto_save = False on your machine or state classes.\n", "\n", "StateField will automatically discover its name in the model and set that attr_name on the machine, so you don't need to set it. But as usual, beware that you can't use different attribute names for the same machine.\n", "\n", "Because the database stores slugs and the slug is the class name by default, if you rename your classes in code and you want existing data to remain valid, you should set the slug to the old class name:\n", "\n", "python\n", "class MyRenamedState(MyMachine):\n", " slug = \"MyState\"\n", " ...\n", "\n", "\n", "Similarly you mustn't delete a state class if you stop using it as long as your database contains objects in that state, or your code will fail when it tries to work with such an object.\n", "\n", "max_length is automatically set to the maximum length of all the slugs in the machine. If you want to save space in your database, override the slugs to something shorter.\n", "\n", "choices is constructed from the slug and label of every state. To customise how states are displayed in forms etc, override the label attribute on the class." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.1" } }, "nbformat": 4, "nbformat_minor": 2 }