You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

METADATA 17KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. Metadata-Version: 2.1
  2. Name: Automat
  3. Version: 0.8.0
  4. Summary: Self-service finite-state machines for the programmer on the go.
  5. Home-page: https://github.com/glyph/Automat
  6. Author: Glyph
  7. Author-email: glyph@twistedmatrix.com
  8. License: MIT
  9. Keywords: fsm finite state machine automata
  10. Platform: UNKNOWN
  11. Classifier: Intended Audience :: Developers
  12. Classifier: License :: OSI Approved :: MIT License
  13. Classifier: Operating System :: OS Independent
  14. Classifier: Programming Language :: Python
  15. Classifier: Programming Language :: Python :: 2
  16. Classifier: Programming Language :: Python :: 2.7
  17. Classifier: Programming Language :: Python :: 3
  18. Classifier: Programming Language :: Python :: 3.5
  19. Classifier: Programming Language :: Python :: 3.6
  20. Classifier: Programming Language :: Python :: 3.7
  21. Classifier: Programming Language :: Python :: 3.8
  22. Requires-Dist: attrs (>=16.1.0)
  23. Requires-Dist: six
  24. Provides-Extra: visualize
  25. Requires-Dist: graphviz (>0.5.1) ; extra == 'visualize'
  26. Requires-Dist: Twisted (>=16.1.1) ; extra == 'visualize'
  27. Automat
  28. =======
  29. .. image:: https://readthedocs.org/projects/automat/badge/?version=latest
  30. :target: http://automat.readthedocs.io/en/latest/
  31. :alt: Documentation Status
  32. .. image:: https://travis-ci.org/glyph/automat.svg?branch=master
  33. :target: https://travis-ci.org/glyph/automat
  34. :alt: Build Status
  35. .. image:: https://coveralls.io/repos/glyph/automat/badge.png
  36. :target: https://coveralls.io/r/glyph/automat
  37. :alt: Coverage Status
  38. Self-service finite-state machines for the programmer on the go.
  39. ----------------------------------------------------------------
  40. Automat is a library for concise, idiomatic Python expression of finite-state
  41. automata (particularly deterministic finite-state transducers).
  42. Read more here, or on `Read the Docs <https://automat.readthedocs.io/>`_\ , or watch the following videos for an overview and presentation
  43. Overview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017:
  44. .. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg
  45. :target: https://www.youtube.com/watch?v=0wOZBpD1VVk
  46. :alt: Glyph Lefkowitz - Automat - Pyninsula #0
  47. Presentation by **Clinton Roy** at PyCon Australia, on August 6th 2017:
  48. .. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg
  49. :target: https://www.youtube.com/watch?v=TedUKXhu9kE
  50. :alt: Clinton Roy - State Machines - Pycon Australia 2017
  51. Why use state machines?
  52. ^^^^^^^^^^^^^^^^^^^^^^^
  53. Sometimes you have to create an object whose behavior varies with its state,
  54. but still wishes to present a consistent interface to its callers.
  55. For example, let's say you're writing the software for a coffee machine. It
  56. has a lid that can be opened or closed, a chamber for water, a chamber for
  57. coffee beans, and a button for "brew".
  58. There are a number of possible states for the coffee machine. It might or
  59. might not have water. It might or might not have beans. The lid might be open
  60. or closed. The "brew" button should only actually attempt to brew coffee in
  61. one of these configurations, and the "open lid" button should only work if the
  62. coffee is not, in fact, brewing.
  63. With diligence and attention to detail, you can implement this correctly using
  64. a collection of attributes on an object; ``has_water``\ , ``has_beans``\ ,
  65. ``is_lid_open`` and so on. However, you have to keep all these attributes
  66. consistent. As the coffee maker becomes more complex - perhaps you add an
  67. additional chamber for flavorings so you can make hazelnut coffee, for
  68. example - you have to keep adding more and more checks and more and more
  69. reasoning about which combinations of states are allowed.
  70. Rather than adding tedious 'if' checks to every single method to make sure that
  71. each of these flags are exactly what you expect, you can use a state machine to
  72. ensure that if your code runs at all, it will be run with all the required
  73. values initialized, because they have to be called in the order you declare
  74. them.
  75. You can read about state machines and their advantages for Python programmers
  76. in considerably more detail
  77. `in this excellent series of articles from ClusterHQ <https://clusterhq.com/blog/what-is-a-state-machine/>`_.
  78. What makes Automat different?
  79. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  80. There are
  81. `dozens of libraries on PyPI implementing state machines <https://pypi.org/search/?q=finite+state+machine>`_.
  82. So it behooves me to say why yet another one would be a good idea.
  83. Automat is designed around this principle: while organizing your code around
  84. state machines is a good idea, your callers don't, and shouldn't have to, care
  85. that you've done so. In Python, the "input" to a stateful system is a method
  86. call; the "output" may be a method call, if you need to invoke a side effect,
  87. or a return value, if you are just performing a computation in memory. Most
  88. other state-machine libraries require you to explicitly create an input object,
  89. provide that object to a generic "input" method, and then receive results,
  90. sometimes in terms of that library's interfaces and sometimes in terms of
  91. classes you define yourself.
  92. For example, a snippet of the coffee-machine example above might be implemented
  93. as follows in naive Python:
  94. .. code-block:: python
  95. class CoffeeMachine(object):
  96. def brew_button(self):
  97. if self.has_water and self.has_beans and not self.is_lid_open:
  98. self.heat_the_heating_element()
  99. # ...
  100. With Automat, you'd create a class with a ``MethodicalMachine`` attribute:
  101. .. code-block:: python
  102. from automat import MethodicalMachine
  103. class CoffeeBrewer(object):
  104. _machine = MethodicalMachine()
  105. and then you would break the above logic into two pieces - the ``brew_button``
  106. *input*\ , declared like so:
  107. .. code-block:: python
  108. @_machine.input()
  109. def brew_button(self):
  110. "The user pressed the 'brew' button."
  111. It wouldn't do any good to declare a method *body* on this, however, because
  112. input methods don't actually execute their bodies when called; doing actual
  113. work is the *output*\ 's job:
  114. .. code-block:: python
  115. @_machine.output()
  116. def _heat_the_heating_element(self):
  117. "Heat up the heating element, which should cause coffee to happen."
  118. self._heating_element.turn_on()
  119. As well as a couple of *states* - and for simplicity's sake let's say that the
  120. only two states are ``have_beans`` and ``dont_have_beans``\ :
  121. .. code-block:: python
  122. @_machine.state()
  123. def have_beans(self):
  124. "In this state, you have some beans."
  125. @_machine.state(initial=True)
  126. def dont_have_beans(self):
  127. "In this state, you don't have any beans."
  128. ``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans
  129. in it.
  130. (And another input to put some beans in:)
  131. .. code-block:: python
  132. @_machine.input()
  133. def put_in_beans(self):
  134. "The user put in some beans."
  135. Finally, you hook everything together with the ``upon`` method of the functions
  136. decorated with ``_machine.state``\ :
  137. .. code-block:: python
  138. # When we don't have beans, upon putting in beans, we will then have beans
  139. # (and produce no output)
  140. dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[])
  141. # When we have beans, upon pressing the brew button, we will then not have
  142. # beans any more (as they have been entered into the brewing chamber) and
  143. # our output will be heating the heating element.
  144. have_beans.upon(brew_button, enter=dont_have_beans,
  145. outputs=[_heat_the_heating_element])
  146. To *users* of this coffee machine class though, it still looks like a POPO
  147. (Plain Old Python Object):
  148. .. code-block:: python
  149. >>> coffee_machine = CoffeeMachine()
  150. >>> coffee_machine.put_in_beans()
  151. >>> coffee_machine.brew_button()
  152. All of the *inputs* are provided by calling them like methods, all of the
  153. *outputs* are automatically invoked when they are produced according to the
  154. outputs specified to ``upon`` and all of the states are simply opaque tokens -
  155. although the fact that they're defined as methods like inputs and outputs
  156. allows you to put docstrings on them easily to document them.
  157. How do I get the current state of a state machine?
  158. --------------------------------------------------
  159. Don't do that.
  160. One major reason for having a state machine is that you want the callers of the
  161. state machine to just provide the appropriate input to the machine at the
  162. appropriate time, and *not have to check themselves* what state the machine is
  163. in. So if you are tempted to write some code like this:
  164. .. code-block:: python
  165. if connection_state_machine.state == "CONNECTED":
  166. connection_state_machine.send_message()
  167. else:
  168. print("not connected")
  169. Instead, just make your calling code do this:
  170. .. code-block:: python
  171. connection_state_machine.send_message()
  172. and then change your state machine to look like this:
  173. .. code-block:: python
  174. @_machine.state()
  175. def connected(self):
  176. "connected"
  177. @_machine.state()
  178. def not_connected(self):
  179. "not connected"
  180. @_machine.input()
  181. def send_message(self):
  182. "send a message"
  183. @_machine.output()
  184. def _actually_send_message(self):
  185. self._transport.send(b"message")
  186. @_machine.output()
  187. def _report_sending_failure(self):
  188. print("not connected")
  189. connected.upon(send_message, enter=connected, [_actually_send_message])
  190. not_connected.upon(send_message, enter=not_connected, [_report_sending_failure])
  191. so that the responsibility for knowing which state the state machine is in
  192. remains within the state machine itself.
  193. Input for Inputs and Output for Outputs
  194. ---------------------------------------
  195. Quite often you want to be able to pass parameters to your methods, as well as
  196. inspecting their results. For example, when you brew the coffee, you might
  197. expect a cup of coffee to result, and you would like to see what kind of coffee
  198. it is. And if you were to put delicious hand-roasted small-batch artisanal
  199. beans into the machine, you would expect a *better* cup of coffee than if you
  200. were to use mass-produced beans. You would do this in plain old Python by
  201. adding a parameter, so that's how you do it in Automat as well.
  202. .. code-block:: python
  203. @_machine.input()
  204. def put_in_beans(self, beans):
  205. "The user put in some beans."
  206. However, one important difference here is that *we can't add any
  207. implementation code to the input method*. Inputs are purely a declaration of
  208. the interface; the behavior must all come from outputs. Therefore, the change
  209. in the state of the coffee machine must be represented as an output. We can
  210. add an output method like this:
  211. .. code-block:: python
  212. @_machine.output()
  213. def _save_beans(self, beans):
  214. "The beans are now in the machine; save them."
  215. self._beans = beans
  216. and then connect it to the ``put_in_beans`` by changing the transition from
  217. ``dont_have_beans`` to ``have_beans`` like so:
  218. .. code-block:: python
  219. dont_have_beans.upon(put_in_beans, enter=have_beans,
  220. outputs=[_save_beans])
  221. Now, when you call:
  222. .. code-block:: python
  223. coffee_machine.put_in_beans("real good beans")
  224. the machine will remember the beans for later.
  225. So how do we get the beans back out again? One of our outputs needs to have a
  226. return value. It would make sense if our ``brew_button`` method returned the cup
  227. of coffee that it made, so we should add an output. So, in addition to heating
  228. the heating element, let's add a return value that describes the coffee. First
  229. a new output:
  230. .. code-block:: python
  231. @_machine.output()
  232. def _describe_coffee(self):
  233. return "A cup of coffee made with {}.".format(self._beans)
  234. Note that we don't need to check first whether ``self._beans`` exists or not,
  235. because we can only reach this output method if the state machine says we've
  236. gone through a set of states that sets this attribute.
  237. Now, we need to hook up ``_describe_coffee`` to the process of brewing, so change
  238. the brewing transition to:
  239. .. code-block:: python
  240. have_beans.upon(brew_button, enter=dont_have_beans,
  241. outputs=[_heat_the_heating_element,
  242. _describe_coffee])
  243. Now, we can call it:
  244. .. code-block:: python
  245. >>> coffee_machine.brew_button()
  246. [None, 'A cup of coffee made with real good beans.']
  247. Except... wait a second, what's that ``None`` doing there?
  248. Since every input can produce multiple outputs, in automat, the default return
  249. value from every input invocation is a ``list``. In this case, we have both
  250. ``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing
  251. both of their return values. However, this can be customized, with the
  252. ``collector`` argument to ``upon``\ ; the ``collector`` is a callable which takes an
  253. iterable of all the outputs' return values and "collects" a single return value
  254. to return to the caller of the state machine.
  255. In this case, we only care about the last output, so we can adjust the call to
  256. ``upon`` like this:
  257. .. code-block:: python
  258. have_beans.upon(brew_button, enter=dont_have_beans,
  259. outputs=[_heat_the_heating_element,
  260. _describe_coffee],
  261. collector=lambda iterable: list(iterable)[-1]
  262. )
  263. And now, we'll get just the return value we want:
  264. .. code-block:: python
  265. >>> coffee_machine.brew_button()
  266. 'A cup of coffee made with real good beans.'
  267. If I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...)
  268. --------------------------------------------------------------------------------------------------------------------
  269. There are APIs for serializing the state machine.
  270. First, you have to decide on a persistent representation of each state, via the
  271. ``serialized=`` argument to the ``MethodicalMachine.state()`` decorator.
  272. Let's take this very simple "light switch" state machine, which can be on or
  273. off, and flipped to reverse its state:
  274. .. code-block:: python
  275. class LightSwitch(object):
  276. _machine = MethodicalMachine()
  277. @_machine.state(serialized="on")
  278. def on_state(self):
  279. "the switch is on"
  280. @_machine.state(serialized="off", initial=True)
  281. def off_state(self):
  282. "the switch is off"
  283. @_machine.input()
  284. def flip(self):
  285. "flip the switch"
  286. on_state.upon(flip, enter=off_state, outputs=[])
  287. off_state.upon(flip, enter=on_state, outputs=[])
  288. In this case, we've chosen a serialized representation for each state via the
  289. ``serialized`` argument. The on state is represented by the string ``"on"``\ , and
  290. the off state is represented by the string ``"off"``.
  291. Now, let's just add an input that lets us tell if the switch is on or not.
  292. .. code-block:: python
  293. @_machine.input()
  294. def query_power(self):
  295. "return True if powered, False otherwise"
  296. @_machine.output()
  297. def _is_powered(self):
  298. return True
  299. @_machine.output()
  300. def _not_powered(self):
  301. return False
  302. on_state.upon(query_power, enter=on_state, outputs=[_is_powered],
  303. collector=next)
  304. off_state.upon(query_power, enter=off_state, outputs=[_not_powered],
  305. collector=next)
  306. To save the state, we have the ``MethodicalMachine.serializer()`` method. A
  307. method decorated with ``@serializer()`` gets an extra argument injected at the
  308. beginning of its argument list: the serialized identifier for the state. In
  309. this case, either ``"on"`` or ``"off"``. Since state machine output methods can
  310. also affect other state on the object, a serializer method is expected to
  311. return *all* relevant state for serialization.
  312. For our simple light switch, such a method might look like this:
  313. .. code-block:: python
  314. @_machine.serializer()
  315. def save(self, state):
  316. return {"is-it-on": state}
  317. Serializers can be public methods, and they can return whatever you like. If
  318. necessary, you can have different serializers - just multiple methods decorated
  319. with ``@_machine.serializer()`` - for different formats; return one data-structure
  320. for JSON, one for XML, one for a database row, and so on.
  321. When it comes time to unserialize, though, you generally want a private method,
  322. because an unserializer has to take a not-fully-initialized instance and
  323. populate it with state. It is expected to *return* the serialized machine
  324. state token that was passed to the serializer, but it can take whatever
  325. arguments you like. Of course, in order to return that, it probably has to
  326. take it somewhere in its arguments, so it will generally take whatever a paired
  327. serializer has returned as an argument.
  328. So our unserializer would look like this:
  329. .. code-block:: python
  330. @_machine.unserializer()
  331. def _restore(self, blob):
  332. return blob["is-it-on"]
  333. Generally you will want a classmethod deserialization constructor which you
  334. write yourself to call this, so that you know how to create an instance of your
  335. own object, like so:
  336. .. code-block:: python
  337. @classmethod
  338. def from_blob(cls, blob):
  339. self = cls()
  340. self._restore(blob)
  341. return self
  342. Saving and loading our ``LightSwitch`` along with its state-machine state can now
  343. be accomplished as follows:
  344. .. code-block:: python
  345. >>> switch1 = LightSwitch()
  346. >>> switch1.query_power()
  347. False
  348. >>> switch1.flip()
  349. []
  350. >>> switch1.query_power()
  351. True
  352. >>> blob = switch1.save()
  353. >>> switch2 = LightSwitch.from_blob(blob)
  354. >>> switch2.query_power()
  355. True
  356. More comprehensive (tested, working) examples are present in ``docs/examples``.
  357. Go forth and machine all the state!