Skip to content

Commit

Permalink
Attach listeners to support advanced binding schemes
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Oct 16, 2018
1 parent 9b8d257 commit b9765fb
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 132 deletions.
20 changes: 12 additions & 8 deletions CHANGELOG.rst
Expand Up @@ -8,13 +8,17 @@ This new release contains many internal changes. While the public API is stable
compatible, expect some breaking changes if you relied on Sismic internal API.

A new binding system has been deployed on ``Interpreter``, allowing listeners to be notified about
meta-events as well. Property statecharts are now implemented based on this new system:

- (Added) Boolean parameters ``internal`` (default to true) and ``meta`` (default to false) for ``Interpreter.bind``.
If ``meta`` is set, meta-events are also propagated to given listener.
- (Added) ``Interpreter.unbind`` method to remove a previously bound listener.
- (Added) Meta-Event *step started* has a ``time`` attribute.
- (Changed) Property statecharts rely on ``Interpreter.bind(..., internal=False, meta=True)``.
meta-events. Listeners are simply callables that accept meta-events instances.

- (Added) An ``Interpreter.attach`` method that accepts any callable. Meta-events raised by the interpreter
are propagated to attached listeners.
- (Added) An ``Interpreter.detach`` method to detach a previously attached listener.
- (Added) Module ``sismic.interpreter.listener`` with two convenient listeners for the newly introduced ``Interpreter.attach`` method.
The ``InternalEventListener`` identifies sent events and propagates them as external events. The ``PropertyStatechartListener``
propagates meta-events, executes and checks property statecharts.
- (Changed) ``Interpreter.bind`` is built on top of ``attach`` and ``InternalEventListener``.
- (Changed) ``Interpreter.bind_property_statechart`` is built on top of ``attach`` and ``PropertyStatechartListener``.
- (Changed) Meta-Event *step started* has a ``time`` attribute.
- (Changed) Property statecharts are checked for each meta-events, not only at the end of the step.
- (Changed) Meta-events *step started* and *step ended* are sent even if no step can be processed.
- (Deprecated) Passing an interpreter to ``bind_property_statechart`` is deprecated, use ``interpreter_klass`` instead.
Expand All @@ -37,8 +41,8 @@ The main visible consequences are:
- (Fixed) Internal events are processed before external ones (regression introduced in 1.3.0).
- (Fixed) Optional transition for ``testing.transition_is_processed``, as promised by its documentation but not implemented.
- (Removed) Internal module ``sismic.interpreter.queue``.
- (Deprecated) BDD step *delayed event sent*, use *event sent* instead.
- (Deprecated) ``DelayedEvent``, use ``Event`` with a ``delay`` parameter instead.
- (Deprecated) BDD step *delayed event sent*, use *event sent* instead.

And some other small changes:

Expand Down
10 changes: 8 additions & 2 deletions docs/concurrent.rst
Expand Up @@ -94,8 +94,14 @@ is sent both to ``interpreter_1`` and ``interpreter_2``.

.. note::

Bound interpreters or callables can be unbound using the :py:meth:`~sismic.interpreter.Interpreter.unbind` method.

The :py:meth:`~sismic.interpreter.Interpreter.bind` method is a high-level interface for
:py:meth:`~sismic.interpreter.Interpreter.attach`. Internally, the former wraps given
interpreter or callable with an appropriate listener before calling
:py:meth:`~sismic.interpreter.Interpreter.attach`. You can unbound a previously
bound interpreter with :py:meth:`~sismic.interpreter.Interpreter.detach` method.
This method accepts a previously attached listener, so you'll need to keep track of the listener returned
by the initial call to :py:meth:`~sismic.interpreter.Interpreter.bind`.


Example of communicating statecharts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
15 changes: 10 additions & 5 deletions docs/properties.rst
Expand Up @@ -51,24 +51,29 @@ Meta-events generated by the interpreter
----------------------------------------

The complete list of :py:class:`~sismic.model.MetaEvent` that are created by the interpreter is described in the
documentation of the :py:meth:`~sismic.interpreter.Interpreter.bind` method:
documentation of the :py:meth:`~sismic.interpreter.Interpreter.attach` method:

.. automethod:: sismic.interpreter.Interpreter.bind
.. automethod:: sismic.interpreter.Interpreter.attach
:noindex:

.. note::

Property statecharts are not the only way to listen to these meta-events. Any listener
that is bound with :py:meth:`~sismic.interpreter.Interpreter.bind` could receive these
meta-events if parameter ``meta`` is set to ``True``.
that is attached with :py:meth:`~sismic.interpreter.Interpreter.attach` will receive these
meta-events.

Property statecharts can listen to what happens in an interpreter when they are bound to
this interpreter, using :py:meth:`~sismic.interpreter.Interpreter.bind_property_statechart` method:

.. automethod:: sismic.interpreter.Interpreter.bind_property_statechart
:noindex:

Internally, this method calls :py:meth:`~sismic.interpreter.Interpreter.bind` so you don't have to.
Internally, this method wraps given property statechart to an appropriate listener, and
calls :py:meth:`~sismic.interpreter.Interpreter.attach` so you don't have to.
Bound property statecharts can be unbound from the interpreter by calling the
:py:meth:`~sismic.interpreter.Interpreter.detach` method. This method accepts a
previously attached listener, so you'll need to keep track of the listener returned
by the initial call to :py:meth:`~sismic.interpreter.Interpreter.bind_property_statechart`.


Examples of property statecharts
Expand Down
4 changes: 2 additions & 2 deletions sismic/code/context.py
Expand Up @@ -15,7 +15,7 @@ class TimeContextProvider:
This context exposes time, after, idle, and active.
Look at their respective documentation for more information.
This provider needs to be bound to the meta-events of an interpreter.
This provider needs to be attached to an interpreter.
"""
def __init__(self) -> None:
self._entry_time = dict() # type: Dict[str, float]
Expand Down Expand Up @@ -85,7 +85,7 @@ class EventContextProvider:
available through the ``pending`` attribute. This list should be returned
by the evaluator on code execution for the events to be effectively sent.
This provider needs to be bound to the meta-events of an interpreter.
This provider needs to be attached to an interpreter.
"""
def __init__(self) -> None:
self.pending = [] # type: List[Event]
Expand Down
4 changes: 2 additions & 2 deletions sismic/code/python.py
Expand Up @@ -65,8 +65,8 @@ def __init__(self, interpreter=None, *, initial_context: Mapping[str, Any]=None)
self._event_provider = EventContextProvider()
self._time_provider = TimeContextProvider()

self._interpreter.bind(self._event_provider, internal=False, meta=True)
self._interpreter.bind(self._time_provider, internal=False, meta=True)
self._interpreter.attach(self._event_provider)
self._interpreter.attach(self._time_provider)

# Precompiled code
self._evaluable_code = {} # type: Dict[str, CodeType]
Expand Down
130 changes: 56 additions & 74 deletions sismic/interpreter/default.py
Expand Up @@ -5,6 +5,7 @@
from typing import (Any, Callable, Dict, Iterable, List, Mapping, Optional,
Set, Tuple, Union, cast)

from .listener import InternalEventListener, PropertyStatechartListener
from ..utilities import sorted_groupby
from ..clock import Clock, SimulatedClock, SynchronizedClock
from ..code import Evaluator, PythonEvaluator
Expand Down Expand Up @@ -72,12 +73,8 @@ def __init__(self, statechart: Statechart, *,
self._internal_queue = [] # type: List[Tuple[float, InternalEvent]]
self._external_queue = [] # type: List[Tuple[float, Event]]

# Bound callables
self._bound_internal = [] # type: List[Callable[[Event], Any]]
self._bound_meta = [] # type: List[Callable[[MetaEvent], Any]]

# Bound property statecharts
self._bound_properties = [] # type: List[Interpreter]
# Bound listeners
self._listeners = [] # type: List[Callable[[MetaEvent], Any]]

# Evaluator
self._evaluator = evaluator_klass(self, initial_context=initial_context)
Expand Down Expand Up @@ -124,19 +121,13 @@ def statechart(self) -> Statechart:
"""
return self._statechart

def bind(self, interpreter_or_callable: Union['Interpreter', Callable[[Event], Any]], *, internal=True, meta=False) -> None:
def attach(self, listener: Callable[[MetaEvent], Any]) -> None:
"""
Bind an event listener the current interpreter.
Attach given listener to the current interpreter.
If *interpreter_or_callable* is an *Interpreter* instance, its *queue* method is called.
This is, if *i1* and *i2* are interpreters, *i1.bind(i2)* is equivalent to *i1.bind(i2.queue)*.
The listener is called each time a meta-event is emitted by current interpreter.
Emitted meta-events are:
By default, all internal events sent by the current interpreter are propagated to the
listener, unless ``internal`` is set to False.
If `meta` is set (not set by default), meta events sent by this interpreter will be propagated to
the listener. Here is a list of supported meta-events:
- *step started*: when a (possibly empty) macro step starts. The current time of the step is available through the ``time`` attribute.
- *step ended*: when a (possibly empty) macro step ends.
- *event consumed*: when an event is consumed. The consumed event is exposed through the ``event`` attribute.
Expand All @@ -145,51 +136,54 @@ def bind(self, interpreter_or_callable: Union['Interpreter', Callable[[Event], A
- *state entered*: when a state is entered. The entered state is exposed through the ``state`` attribute.
- *transition processed*: when a transition is processed. The source state, target state and the event are
exposed respectively through the ``source``, ``target`` and ``event`` attribute.
- Every meta-event that is sent from within the statechart.
This is a low-level interface for ``self.bind`` and ``self.bind_property_statechart``.
Additionally, MetaEvent instances that are sent from within the statechart are also passed to all
bound listeners. Internally, these meta-events are used by property statecharts.
Consult ``sismic.interpreter.listener`` for common listeners/wrappers.
:param interpreter_or_callable: interpreter or callable to bind
:param internal: if set, propagates internal events
:param meta: if set, propagates meta events
:param listener: A callable that accepts meta-event instances.
"""
if isinstance(interpreter_or_callable, Interpreter):
listener = cast(Callable[[Event], Any], interpreter_or_callable.queue)
else:
listener = interpreter_or_callable
self._listeners.append(listener)

if internal:
self._bound_internal.append(listener)
if meta:
self._bound_meta.append(listener)
def detach(self, listener: Callable[[MetaEvent], Any]) -> None:
"""
Remove given listener from the ones that are currently attached to this interpreter.
:param listener: A previously attached listener.
"""
self._listeners.remove(listener)

def unbind(self, interpreter_or_callable: Union['Interpreter', Callable[[Event], Any]]) -> None:
def bind(self, interpreter_or_callable: Union['Interpreter', Callable[[Event], Any]]) -> Callable[[MetaEvent], Any]:
"""
Unbind a previously bound listener.
Bind an interpreter (or a callable) to the current interpreter.
:param interpreter_or_callable: interpreter or callable to unbind
Internal events sent by this interpreter will be propagated as external events.
If *interpreter_or_callable* is an *Interpreter* instance, its *queue* method is called.
This is, if *i1* and *i2* are interpreters, *i1.bind(i2)* is equivalent to *i1.bind(i2.queue)*.
This method is a higher-level interface for ``self.attach``.
If ``x = interpreter.bind(...)``, use ``interpreter.detach(x)`` to unbind a
previously bound interpreter.
:param interpreter_or_callable: interpreter or callable to bind.
:return: the resulting attached listener.
"""
if isinstance(interpreter_or_callable, Interpreter):
listener = cast(Callable[[Event], Any], interpreter_or_callable.queue)
listener = InternalEventListener(interpreter_or_callable.queue)
else:
listener = interpreter_or_callable
listener = InternalEventListener(interpreter_or_callable)

try:
self._bound_internal.remove(listener)
except ValueError:
pass

try:
self._bound_meta.remove(listener)
except ValueError:
pass
self.attach(listener)

return listener

def bind_property_statechart(self, statechart: Statechart, *, interpreter_klass: Callable=None) -> None:
def bind_property_statechart(self, statechart: Statechart, *, interpreter_klass: Callable=None) -> Callable[[MetaEvent], Any]:
"""
Bind a property statechart to the current interpreter.
A property statechart receives meta-events from the current interpreter depending on what happens.
See ``bind`` method for a full list of meta-events.
See ``attach`` method for a full list of meta-events.
The internal clock of all property statecharts is synced with the one of the current interpreter.
As soon as a property statechart reaches a final state, a ``PropertyStatechartError`` will be raised,
Expand All @@ -198,20 +192,27 @@ def bind_property_statechart(self, statechart: Statechart, *, interpreter_klass:
Since Sismic 1.4.0: passing an interpreter as first argument is deprecated.
This method is a higher-level interface for ``self.attach``.
If ``x = interpreter.bind_property_statechart(...)``, use ``interpreter.detach(x)`` to unbind a
previously bound property statechart.
:param statechart: A statechart instance.
:param interpreter_klass: An optional callable that accepts a statechart as first parameter and a
named parameter clock. Default to Interpreter.
:return: the resulting attached listener.
"""
if isinstance(statechart, Interpreter):
warnings.warn('Passing an interpreter to bind_property_statechart is deprecated since 1.4.0. Use interpreter_klass instead.', DeprecationWarning)
p_interpreter = statechart
p_interpreter.clock = SynchronizedClock(self)
interpreter = statechart
interpreter.clock = SynchronizedClock(self)
else:
interpreter_klass = Interpreter if interpreter_klass is None else interpreter_klass
p_interpreter = interpreter_klass(statechart, clock=SynchronizedClock(self))
interpreter = interpreter_klass(statechart, clock=SynchronizedClock(self))

self._bound_properties.append(p_interpreter)
self.bind(p_interpreter, internal=False, meta=True)
listener = PropertyStatechartListener(interpreter)
self.attach(listener)

return listener

def queue(self, event_or_name:Union[str, Event], *event_or_names:Union[str, Event], **parameters) -> 'Interpreter':
"""
Expand Down Expand Up @@ -330,37 +331,18 @@ def _raise_event(self, event: Union[InternalEvent, MetaEvent]) -> None:
Raise an event from the statechart.
Only InternalEvent and MetaEvent (and their subclasses) are accepted.
InternalEvent instances are propagated to bound listeners as normal events, and added to
the event queue of the current interpreter as InternalEvent instance. If given event is
delayed, it is propagated as DelayedEvent to bound listeners, and put into current
event queue as a DelayedInternalEvent.
MetaEvent instances are propagated to bound listeners that subscribed to
meta events, and to bound property statecharts.
:param event: event to be sent by the statechart.
"""
if isinstance(event, InternalEvent):
self._queue_event(event)
external_event = Event(event.name, **event.data)

for internal_listener in self._bound_internal:
internal_listener(external_event)

self._raise_event(MetaEvent('event sent', event=external_event))
self._raise_event(MetaEvent('event sent', event=event))
if hasattr(event, 'delay'):
# Deprecated since 1.4.0:
self._raise_event(MetaEvent('delayed event sent', event=external_event))
# Deprecated since 1.4.0
self._raise_event(MetaEvent('delayed event sent', event=event))
elif isinstance(event, MetaEvent):
for meta_listener in self._bound_meta:
meta_listener(event)

# Check for property statechart violations
for property_statechart in self._bound_properties:
property_statechart.execute()
if property_statechart.final:
raise PropertyStatechartError(property_statechart)
for listener in self._listeners:
listener(event)
else:
raise ValueError('Only InternalEvent and MetaEvent can be sent by a statechart, not {}'.format(type(event)))

Expand Down
35 changes: 35 additions & 0 deletions sismic/interpreter/listener.py
@@ -0,0 +1,35 @@
from typing import Callable, Any

from ..model import MetaEvent, InternalEvent, Event

from ..exceptions import PropertyStatechartError


__all__ = ['InternalEventListener', 'PropertyStatechartListener']


class InternalEventListener:
"""
Listener that filters and propagates internal events as external events.
"""
def __init__(self, callable: Callable[[Event], Any]) -> None:
self._callable = callable

def __call__(self, event: MetaEvent) -> None:
if event.name == 'event sent':
self._callable(Event(event.event.name, **event.event.data))


class PropertyStatechartListener:
"""
Listener that propagates meta-events to given property statechart, executes
the property statechart, and checks it.
"""
def __init__(self, interpreter) -> None:
self._interpreter = interpreter

def __call__(self, event: MetaEvent) -> None:
self._interpreter.queue(event)
self._interpreter.execute()
if self._interpreter.final:
raise PropertyStatechartError(self._interpreter)

0 comments on commit b9765fb

Please sign in to comment.