About

In the following we will briefly describe how the components in app component system work.

  1. About

  2. Jinja Strings

  3. Components

  4. Tags

  5. Dependences

  6. Free Variables

  7. Inners

  8. Component Factory

  9. Attributes

  10. Operations

  11. Arithmetic

  12. Other Docs

Jinja Strings

The component system of app is based in jinja2. This means that the components constructs strings following jinja2 syntax. We have a specific type Jinja (which is a subtype of Str) whose instances are the so-called jinja strings: Python strings preppended with the “jinja” keyword.

So, more precisely, an instance of Jinja is a string as follows (see jinja2 to discover the full valid syntax):

 1my_jinja_string = """jinja
 2{% for i in x %}
 3<some html>
 4    {% if y is True %}
 5    <more html>
 6    ...
 7    </more html>
 8    {% endif %}
 9</some html>
10{% endfor %}
11"""

Above, x and y are jinja variables, which must be defined inside some context, for example as part of a component, before converting the jinja string to HTML (see the rendering documentation).

Components

The basic unities of app component systems are, of course, the components. A component is a typed function (in the sense of typed) such that:

  1. it returns a jinja string, so that its returning type is the Jinja type;

  2. it is decorated with the @component decorator.

Thus, a typical component is defined as follows:

 1from typed import SomeType, OtherType
 2from app import component, Jinja
 3
 4@component
 5def my_comp(x: SomeType, y: OtherType, ...) -> Jinja:
 6    ...
 7    return """jinja
 8{% for i in x %}
 9<some html>
10    {% if y is True %}
11    <more html>
12    ...
13    </more html>
14    {% endif %}
15</some html>
16{% endfor %}
17"""

One should note that local variables of a component are automatically included in the context of the returning jinja string. This means that local variables defined in the body of a component can be directly used as jinja vars:

 1from typed import SomeType, OtherType
 2from app import component, Jinja
 3
 4@component
 5def my_comp(x: SomeType, y: OtherType, ...) -> Jinja:
 6    ...
 7    some_var = some_value
 8    ...
 9    return """jinja
10{% for i in x %}
11<some html>
12    {% if y is True %}
13    <more html>
14        {{ some_var }}
15    </more html>
16    {% endif %}
17</some html>
18{% endfor %}
19"""

Remark 1. There is the type COMPONENT whose instances are precisely the app components. It is constructed as a subtype of TypedFunc(Any, cod=Jinja), i.e., of all typed functions whose codomain is Jinja.

Tags

Typically, components are delimited by a HTML tag. In app there is a type factory (in the sense of typed) Tag: Tuple(Str) -> SUB(Jinja) that receives a tuple of HTML tag names and returns the subtype Tag(*tags) of Jinja of all jinja strings enclosed by one of the given tags.

1# example of instance of 'Tag('some-tag')'
2"""jinja
3<some-tag>
4...
5</some-tag>
6"""

With such type factory one can construct type safe tag-based components:

 1from app import component, Tag
 2
 3@component
 4def my_tag_comp(...) -> Tag('some-tag'):
 5    ...
 6    return """jinja
 7<some-tag>
 8...
 9</some-tag>
10"""

For each tuple of tags there is a subtype TAG(*tags) of COMPONENT of all components whose returning string belongs to Tag(*tags). There is, actually, a supplementary type factory TAG: Tuple(Str) -> SUB(COMPONENT) that associates to each tuple the corresponding type TAG(*tags).

So, for the component my_tag_comp above:

1from app import TAG
2
3# the following returns True
4print(isinstance(my_tag_comp, TAG('some-tag')))

Dependences

Components can depend one each others. More precisely, a component has a special optional variable __depends_on__, which lists other already defined components of which the defining component depends on.

One time listed in the __depends_on__ variable, the dependent components are included into the context, so that they are turned into jinja vars and can be called inside the jinja string of the defining component, as follows:

 1from app import Jinja, Tag, component
 2
 3@component
 4def comp_1(...) -> Jinja:
 5    ...
 6    return """jinja
 7    ...
 8"""
 9
10@component
11def comp_2(...) -> Tag('some-tag'):
12    ...
13    return """jinja
14<some-tag>
15    ...
16</some-tag>
17"""
18
19@component
20def main_comp(..., __depends_on__=[comp_1, comp_2]) -> Jinja:
21    ...
22    return """jinja
23    ...
24{{ comp_1(...) }}
25    ...
26{{ comp_2(...) }}
27"""

Recall that components are typed functions, which means that all their arguments must have type annotations, which are checked at runtime. The __depends_on__, however, is an exception: if a type annotation is not provided, then List(COMPONENT) is automatically attached to it. On the other hand, if a type annotation is provided, it must be a subtype of List(COMPONENT).

Free Variables

When a jinja var is in the jinja string returned by a component, it typically corresponds to one of the following cases:

  1. it is component argument;

  2. it is a local variable defined in the body of the component;

  3. it is another component listed in the __depends_on__ argument.

A jinja var which does not satisfies some of the conditions above is called a free jinja var.

Remark 2. It is not a best practice to have free jinja vars in a component. Indeed, as will de discussed in Rendering, a free jinja var need to be explicitly included in the context during the rendering of a context. The problem is that, unlike component arguments, they are not true Python components, which makes difficult to remember their existence.

Inners

In app, the components may have another special kind of variables: the inner ones. They are necessarily of type Inner, and work as placeholders for future inserts inside the component.

 1from app import component, Tag, Inner
 2
 3@component
 4def my_inner_comp(..., inner: Inner, ...) -> Tag('some-tag'):
 5    ...
 6    return """jinja
 7<some-tag>
 8    {{ inner }}
 9</some-tag>
10"""

The type Inner is a presentation of the Jinja type.

The components may also admit content vars (of type Content), characterizing them as static components, which can be used to introduced Markdown and RST content inside the component. This will be discussed in Statics.

Component Factory

There is a type factory Component: Int -> SUB(COMPONENT) that to each integer n>=0 returns the subtype Component(n) of COMPONENT of all components that have exactly n inner vars.

This decomposes the type COMPONENT into distinct subtypes of components with a fixed amount of inner variables.

So, for example, for the my_inner_comp defined above one would have:

1from app import Component
2
3print(isinstance(my_inner_comp, Component(1)) # will return 'True'
4print(isinstance(my_inner_comp, Component(0)) # will return 'False' 

By definition, if n<0, then Component(n) is set precisely to COMPONENT, meaning that the component may have any number of inner variables, including zero.

Attributes

The type COMPONENT of all components has some properties, which define some attributes to the components:

attribute

description

.jinja

code of the component _jinja string_

.jinja_vars

tuple of _jinja vars_

.jinja_free_vars

tuple of _jinja free vars_

.inner_args

tuple of _inner args_

.content_args

tuple of _content_args_

.tags

the tuple of tags that defines the component

table 1: component attributes

Operations

There are three main operations involving components:

  1. join: COMPONENT x COMPONENT -> COMPONENT:

    • receive a tuple of components and creates a new component whose jinja string is the join of the jinja strings of the provided components;

  2. concat: Component(1) x COMPONENT -> COMPONENT:

    • receive a component with a single inner var and another arbitrary component, producing a new component whose jinja str is obtained by replacing the placeholder given by inner var in the first component with the jinja string of the second component.

  3. eval: COMPONENT x Dict(Any) -> COMPONENT:

    • receive a component and a list of key-values and returns the component obtained by fixing each variable associated to a key with the corresponding value, leaving the other variables unchanged.

The intuition for each of such operations is as follows:

  1. join: put a component after other component

  2. concat: put a component inside other component

  3. eval: from a component, fixes some part

So, for example, consider the following generic components:

 1from typed import SomeType, OtherType
 2from app import Jinja, component, Tag, Inner, join, concat, eval
 3
 4@component
 5def some_comp(x: SomeType, ...) -> Jinja
 6    ...
 7    return """jinja
 8{{ contents of 'some_comp' jinja string }}
 9"""
10
11@component
12def inner_comp(a: OtherType, inner: Inner, ...) -> Tag('some-tag')
13    ...
14    return """jinja
15<some-tag>
16    {{ inner }}
17</some-tag>
18"""

The resulting joined component join(some_comp, inner_comp) is equivalent to the following component:

1@component
2def joined_comp(x: SomeType, ..., a: OtherType, inner: Inner, ...) -> Jinja:
3    return """jinja
4{{ contents of 'some_comp' jinja string }}
5<some-tag>
6    {{ inner }}
7</some-tag>
8"""

On the other hand, concat(inner_comp, some_comp) is equivalent to:

1@component
2def concat_comp(a: OtherType, x: SomeType, ...) -> Tag('some-tag'):
3    return """jinja
4<some-tag>
5    {{ contents of 'some_comp' jinja string }}
6</some-tag>
7"""

Finally, eval(inner_comp, inner="blablabla") is the same as defining the component below.

1@component
2def eval_comp(a: OtherType, ...) -> Tag('some-tag'):
3    return """jinja
4<some-tag>
5    blablabla
6</some-tag>
7"""

In both join and concat operations, the __depends_on__ variable is the obtained as the concatenation of the __depends_on__ of the underlying components. In the eval operation, on the other hand, the __depends_on__ is maintained the same.

Arithmetic

In the type COMPONENT, the operations join and concat corresponds, respectively to implementations of the class functions __sum__ and __mul__. This means that instead of writing join(comp_1, comp_2) you can just write comp_1 + comp_2. Similarly, instead of concat(comp_1, comp_2), you can write comp_1 * comp_2.

Therefore, the COMPONENT type has an internal “component arithmetic” that can be used to build complex components from smaller ones.

Other Docs

  1. models

  2. rendering

  3. statics

  4. compatibility

  5. list of entities