File: syntax.rst

package info (click to toggle)
python-enaml 0.19.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 13,284 kB
  • sloc: python: 31,443; cpp: 4,499; makefile: 140; javascript: 68; lisp: 53; sh: 20
file content (254 lines) | stat: -rw-r--r-- 8,894 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
.. _syntax:

============================
Enaml syntax and Data Models
============================


Enaml defines a superset of the Python language, which means that any valid
Python code is valid in an enaml file, including function and class definitions.
The following sections present the extension provided by Enaml to declare views
and bind it to a model.

.. note::

    Just like in any Python file, you need to import the definitions of the
    objects you use. One minor difference between standard Python files and
    Enaml files is that inside an enaml file you do not need to use the
    `enaml.imports()` context manager when importing objects defined in an
    Enaml file.

Enamldef syntax
---------------

To define a view element, one uses the `enamldef` keyword in a way similar to
the `class` keyword in a normal Python file. Your widget must inherit from
a widget, either a builtin one or one defined using an `enamldef` and cannot
inherit from several widgets.

In the body of the declaration, you add widget by simply declaring it. The
parent/child is directly encoded in the indentation. Furthermore, you can
add to each widget an id which must unique inside the declaration. This id
can be used to reference it in the layout (see :ref:`layout`), or to access
one of its attribute.

.. code-block:: enaml

    enamldef MyWindow(Window):

        Container: cont:

            Field: field:
                pass


Defining attributes and aliases
-------------------------------

When defining a widget using the `enamldef` keyword, one can add custom
attributes to the widget using the `attr` keyword. Furthermore, you can enforce
type validation using the following syntax.

.. code-block:: enaml

    attr my_attr : set = {1, 2, 3}

.. note::

    One can specify a default value using `=` even if no type validation is
    specified.

One can also define the equivalent of atom.Event, that is to say an attribute
that does not store the value it is assigned but simply fires a notification
each time it is assigned a value. To do so, simply replace the `attr` keyword
with the `event` keyword. Type validation works in the same way as for regular
`attr` defined attributes.

Additionally, one can define an attribute that simply allows access to a child widget
or a child widget attribute in a transparent way using the `alias`
keyword. The syntax is presented below.

.. code-block:: enaml

    enamldef MyWindow(Window):

        alias child_widget : cont

        alias child_widget_attr : field.text

        Container: cont:
            Field: field:
                pass

.. note::

    To avoid clashes between the ids attributed to child widgets in different
    widgets, one cannot access a widget by its id outside of the declaration.
    This means that the following does not work and raises an `AttributeError`.
    This is why you need an alias if you need to access to the inner widget.

    .. code-block:: enaml

        enamldef MyWindow(Window):

            Container: cont:
                pass

        MyWindow().cont


Binding Operators
-----------------

To describe how a widget should be connected to the model driving it, Enaml
uses a set of four operators:


`=`
    *Assignment*. Right hand side can be any expression. The assignment will be
    the default value, but the value can be changed later through Python code
    or other expression execution.

`:=`
    *Delegation*. Right hand side must be a simple lvalue, like ``foo.bar`` or
    ``spam[idx]``. Non-lvalue expressions here are a syntax error. The
    value of the view property and value of the attribute are synced,
    but the type checking of the view property is enforced.

`<<`
    *Subscription*. Right hand side can be any expression or statement.
    In the case of an expression, the expression will be parsed for
    dependencies, and any dependency which is a member attribute on a Atom
    class will have a listener attached. When the listener fires, the
    expression will be re-evaluated and the value of the view property
    will be updated.
    The behavior for statements is quite similar. Whichever value the statement
    return will be set to the left-hand side.

`>>`
    *Update*. Right hand side must be a simple lvalue. The attribute will
    receive the view property's value any time it changes.

`::`
    *Notification*. Right hand side can be any statement. Additionally, an
    indented block of code can also be used. The statement/block will be
    evaluated any time the view property changes. Inside this block, one can
    access the notification that triggered the execution under the name
    ``change``. In  particular when using Atom object for the model, the new
    value can be accessed as ``change['value']``


Declarative function definition and overriding
----------------------------------------------

In addition to defining attributes inside an ``enamldef`` declaration, one can
define the equivalent of methods, or override them. In the context of
``enamldef`` objects, we will refer to them as declarative functions.

Such functions are defined using the ``func`` keyword, and obey the scoping
rules described in the next section. In particular, ``self`` can be used to
access the instance of the widget on which they are defined but does not need
to be listed explicitly in the arguments (and should not be).

Such functions can be overridden using a slightly different syntax, as
illustrated below:

.. code-block:: enaml

    enamldef MyWindow(Window):

        attr a = 2

        func my_func():
            return 3*self.a

    enamldef MyCustomWindow(MyWindow):

        attr a = 2

        my_func => ():
            return 3*a


Scoping Rules
-------------

- Imports are global and accessible to everything in the file.
- Each top-level item defines its own local namespace. This namespace
  includes all elements that have a declared identifier.
- Each expression has its local namespace that is the union of the block
  locals and the attribute namespace of the object to which the expression
  is bound. In other words, ``self`` is implicit. However, a ``self`` exists in
  this local namespace in order to break naming conflicts between block
  locals and attribute names. To any C++ or Java developers, this will seem
  natural.
- Each expression has a dynamic scope which exists between its local scope
  and the global scope. This scope is the chained union of all attribute
  namespaces of the ancestor tree of the object (i.e. the parents of the widget
  on which teh expression is defined) to which the expression is bound.

We illustrate these rules and of their consequences below:

.. code-block:: enaml

    enamldef MyWindow(Window):

        attr a = 2

        attr b: str = ""

        a::
            b = 1
            self.b = "test"

        b::
            print(change)
            print(f"{a}")

In the example above, in the notification handler for ``a``, we first create a
new local variable ``b``, which is scoped to the handler (i.e. it does not
exist outside). In order to set the attribute ``b`` of the widget we need to
use the implicit ``self`` referencing the widget. As a consequence, under
Python 3.8, the walrus operator ``:=`` will always create a local variable and
will never modify the state of the widget.

In the notification handler of ``b``, we first access the implicit ``change``
dictionary which is provided by the model. Second, we access the variable ``a``
which does not exist in the local namespace and is hence found in the widget
namespace. This is equivalent to ``self.a``.

.. code-block:: enaml

    enamldef MyLabel(Label):

        text << f"{a}"

    enamldef MyWindow(Window):

        attr a = 2

        MyLabel:
            pass

    enamldef MyWindow2(Window):

        attr b = 2

        MyLabel:
            pass

In the above example, ``MyWindow`` can be instantiated and the label text will
be ``"2"``. It is because the parent of ``MyLabel`` i.e. ``MyWindow`` has the
name ``a`` in its namespace. On the other end, ``MyWindow2`` will generate a
:class:`NameError` since ``a`` is not defined in ``MyLabel`` scope nor in any
of its parents scope.

Using this feature requires to properly document the expectation of the widget
since otherwise the reason for the :class:`NameError` may be hard to track. It
is however a powerful feature to avoid manually propagating a name through the
whole hierarchy. One typical example is when using the Enaml workbench with the
UI plugin to build a plugin application. In such application, accessing the
workbench object which is set on the main window is very common since the
workbench orchestrate the interaction between plugins. Since it is set on the
main window, any widget which can trace its ancestry to it, which is in the
general the case for all widgets, can use the name ``workbench`` safely.