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 Constraints Layout), or to access one of its attribute.
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.
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.
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.
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
orspam[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 aschange['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:
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, aself
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:
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
.
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
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 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.