Skip to content

Commit

Permalink
Add "setdefault" & rewrite documentation about default values for par…
Browse files Browse the repository at this point in the history
…ameters
  • Loading branch information
AlexandreDecan committed Aug 23, 2018
1 parent 7993e7b commit e896b4b
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Unreleased
- (Added) Some documentation about running multiple statecharts.
- (Added) An ``unbind`` method for an ``Interpreter``.
- (Added) A ``remove`` method on ``EventQueue`` and a corresponding ``cancel`` method on ``Interpreter``.
- (Added) A ``setdefault`` function that can be used in the preamble of a statechart to assign default values to variables.
- (Changed) Meta-Event *step started* has a ``time`` attribute.
- (Changed) The current event queue can be consulted as a list using ``interpreter._event_queue``.
- (Fixed) Hook-errors reported by ``sismic-bdd`` CLI are a little bit more verbose (`#81 <https://github.com/AlexandreDecan/sismic/issues/81>`__).
Expand Down
118 changes: 88 additions & 30 deletions docs/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ In the following, we will implicitly assume that the code evaluator is an instan
Context of the Python code evaluator
------------------------------------

When a code evaluator is created or provided to an interpreter, its ``context`` is exposed through the ``context``
attribute of the interpreter. The context of an evaluator is a mapping between variable names and their values.
When a code evaluator is created or provided to an interpreter, all the variables that are defined or used by the
statechart are stored in an *execution context*. This context is exposed through the ``context``
attribute of the interpreter and can be seen as a mapping between variable names and their values.
When a piece of code contained in a statechart has to be evaluated or executed, the context of the evaluator is used to
populate the local and global variables that are available for this piece of code.

Expand All @@ -46,59 +47,116 @@ populated with ``{'x': 1, 'y': 0}``. When the statechart is further executed (in
*s1* is entered, the code ``x += 1`` contained in the ``on entry`` field of *s1* is then executed in this context.
After execution, the context is ``{'x': 2, 'y': 0}``.

The default code evaluator has a global context that is always exposed when a piece of code has to be evaluated
or executed. When a :py:class:`~sismic.code.PythonEvaluator` instance is initialized, an initial context can be specified.
For convenience, the initial context can be directly provided to the constructor of an :py:class:`~sismic.interpreter.Interpreter`.
The default code evaluator uses a global context, meaning that all variables that are defined in the statechart are
exposed by the evaluator when a piece of code has to be evaluated or executed. The main limitation of this approach
is that you cannot have distinct variables with a same name in different states or, in other words, there is
only one scope for all your variables.

It should be noticed that the initial context is set *before* executing the preamble of a statechart.
While this should be expected, it has the direct consequence that if a variable defined in the initial context is
also defined by the preamble, the latter will override its value, as illustrated by the following example:
The preamble of a statechart can be used to provide default values for some variables. However, the preamble is part of
the statechart and as such, cannot be used to *parametrize* the statechart. To circumvent this, an initial context
can be specified when a :py:class:`~sismic.code.PythonEvaluator` is created. For convenience, this initial context
can also be passed to the constructor of an :py:class:`~sismic.interpreter.Interpreter`.

.. testcode:: variable_override
Considered the following toy example:

.. testcode:: initial_context

from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
import math as my_favorite_module

yaml = """statechart:
name: example
preamble:
x = 1
x = DEFAULT_X
root state:
name: s
"""

statechart = import_from_yaml(yaml)
context = {
'x': 2,
'math': my_favorite_module
}

interpreter = Interpreter(statechart, initial_context=context)
Notice that variable ``DEFAULT_X`` is used in the preamble but not defined. The statechart expects this
variable to be provided in the initial context, as illustrated next:

.. testcode:: initial_context

interpreter = Interpreter(statechart, initial_context={'DEFAULT_X': 1})

We can check that the value of ``x`` is ``1`` by accessing the ``context`` attribute of the interpreter:

.. testcode:: initial_context

assert interpreter.context['x'] == 1

Omitting to provide the ``DEFAULT_X`` variable in the initial context leads to an error, as an unknown
variable is accessed by the preamble:

print(interpreter.context['x'])
.. testcode:: initial_context

.. testoutput:: variable_override
try:
Interpreter(statechart)
except Exception as e:
print(e)

1
.. testoutput:: initial_context

In this example, the value of ``x`` is eventually set to ``1``.
While the initial context provided to the interpreter defined the value of ``x`` to ``2``, the code contained in the
preamble overrode its value.
If you want to make use of the initial context to somehow *parametrize* the execution of the statechart, while still
providing *default* values for these parameters, you should check the existence of the variables before setting
their values. This can be done as follows:
"name 'DEFAULT_X' is not defined" occurred while executing "x = DEFAULT_X"

.. testcode::
It could be tempting to define a default value for ``x`` in the preamble **and** overriding this
value by providing an initial context where ``x`` is defined. However, the initial context of an
interpreter is set **before** executing the preamble of a statechart. As a consequence, if a variable
is defined both in the initial context and the preamble, its value will be overridden by the preamble.

if not 'x' in locals():
Consider the following example where ``x`` is both defined in the initial context and the preamble:

.. testcode:: initial_context

yaml = """statechart:
name: example
preamble:
x = 1
root state:
name: s
"""

or equivalently,
statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2})

assert interpreter.context['x'] == 1

The value of ``x`` is eventually set to ``1``.

While the initial context provided to the interpreter defined the value of ``x`` to ``2``, the code
contained in the preamble overrode its value. If you want to make use of the initial context to
somehow *parametrize* the execution of the statechart while still providing *default* values for
these parameters, you should either check the existence of the variables before setting their values
or rely on the ``setdefault`` function that is exposed by the Python code evaluator when the preamble
of a statechart is executed.

This function can be used to define (and return) a variable, very similarly to the
``setdefault`` method of a dictionary. Using this function, we can easily rewrite the preamble
of our statechart to deal with the optional default values of ``x`` (and ``y`` and ``z`` in this
example):

.. testcode:: initial_context

yaml = """statechart:
name: example
preamble: |
x = setdefault('x', 1)
setdefault('y', 1) # Value is affected to y implicitly
setdefault('z', 1) # Value is affected to z implicitly
root state:
name: s
on entry: print(x, y, z)
"""

statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2, 'z': 3})
interpreter.execute()

.. testcode::
.. testoutput:: initial_context

x = locals().get('x', 1)
2 1 3


.. warning::
Expand Down
20 changes: 18 additions & 2 deletions sismic/code/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class PythonEvaluator(Evaluator):
was sent during the current step.
- A *received(name: str) -> bool* function that takes an event name and return True if an event with the
same name is currently processed in this step.
- On preamble execution:
- A *default(name:str, value: Any) -> Any* function that defines and returns variable *name* in
the global scope if it is not yet defined.
If an exception occurred while executing or evaluating a piece of code, it is propagated by the
evaluator.
Expand Down Expand Up @@ -135,6 +138,16 @@ def on_step_starts(self, event: Optional[Event]=None) -> None:
self._sent_events.clear()
self._received_event = event

def _setdefault(self, name: str, value: Any) -> Any:
"""
Define and return variable "name".
:param name: name of the variable
:param value: value to use for that variable, if not defined
:return: value of the variable
"""
return self._context.setdefault(name, value)

def _received(self, name: str) -> bool:
"""
:param name: name of an event
Expand Down Expand Up @@ -234,7 +247,7 @@ def _execute_code(self, code: Optional[str], *, additional_context: Mapping=None
exec(compiled_code, exposed_context, self._context) # type: ignore
return sent_events
except Exception as e:
raise CodeEvaluationError('"{}" occurred while executing "{}"\n'.format(e, code)) from e
raise CodeEvaluationError('"{}" occurred while executing "{}"'.format(e, code)) from e

def execute_statechart(self, statechart: Statechart):
"""
Expand All @@ -244,7 +257,10 @@ def execute_statechart(self, statechart: Statechart):
:param statechart: statechart to consider
"""
if statechart.preamble:
events = self._execute_code(statechart.preamble)
additional_context = {
'setdefault': self._setdefault
}
events = self._execute_code(statechart.preamble, additional_context=additional_context)
if len(events) > 0:
raise CodeEvaluationError('Events cannot be raised by statechart preamble')

Expand Down
11 changes: 11 additions & 0 deletions tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ def test_condition_on_event(self, evaluator):
assert evaluator._evaluate_code('event.data[\'a\'] == 1', additional_context={'event': Event('test', a=1)})
assert evaluator._evaluate_code('event.name == \'test\'', additional_context={'event': Event('test')})

def test_setdefault(self, evaluator, mocker):
statechart = mocker.MagicMock()
statechart.preamble = 'setdefault("x", 2)'
evaluator.execute_statechart(statechart)
assert evaluator.context['x'] == 1

statechart.preamble = 'b = setdefault("a", 0)'
evaluator.execute_statechart(statechart)
assert evaluator.context['a'] == 0
assert evaluator.context['b'] == 0

def test_execution(self, evaluator):
evaluator._execute_code('a = 1')
assert evaluator.context['a'] == 1
Expand Down

0 comments on commit e896b4b

Please sign in to comment.