About
In the following we will briefly describe how the components in comp component system work.
Components
In comp, a component is a typed function such that:
it returns a jinja string, so that its returning type is the
Jinjatype;it is decorated with the
@componentdecorator.
Thus, a typical component is defined as follows:
1from typed import SomeType, OtherType, AnotherType ...
2from comp import component, Jinja
3
4@component
5def my_comp(x: SomeType, y: OtherType, z: AnotherType ...) -> Jinja:
6 ...
7 return f"""jinja
8[% for i in x %]
9<some html>
10 [% if y is True %]
11 <more html>
12 ...
13 { z }
14 ...
15 </more html>
16 [% endif %]
17</some html>
18[% endfor %]
19"""
Context
As we will see in the rendering documentation, not only jinja strings but also components can be rendered. Recall that to render a jinja string we need to fix all their jinja free vars, which is done by providing a jinja context. Similarly, to render a component we need to provide a component context.
Instead of providing the entire context while rendering a component, one can endow it with a local context along its definition, which is automatically included in the {lib:component context}, simplifying a lot the rendering process. This is done through a special {lib:parameter} __context__.
There are two cases where the use of __context__ is strongly recommended:
when using local vars inside the jinja syntax of the jinja string;
when calling external components inside the jinja string.
For example, these cases occur when one needs to (see below):
loop with
[% for ... %]through a {lib:local var};add something conditionally with
[% if ... %]depending on a local var or an external component.
1from typed import SomeType, ...
2from comp import component, Jinja
3from some.where import some_comp
4
5@component
6def my_comp(x: SomeType,...,__context__={"some_comp": some_comp}) -> Jinja:
7 ...
8 local_var = [ ... ]
9 __context__.update({"local_var": local_var})
10 ...
11 return f"""jinja
12[% for i in local_var %]
13<some html>
14 [% if some_comp() == "something" %]
15 <more html>
16 ...
17 </more html>
18 [% endif %]
19</some html>
20[% endfor %]
21"""
Remark 2. Recall that a component is a typed function, which means that all their parameters need to have a type annotation. An exception is the parameter
__context__, whose type annotationDict(Any)is automatically introduced by the decorator@component.
Bounding
In a component, each parameter appearing in its jinja string is a free jinja var, so that it need to be fixed in the jinja context. A leak in a component is a jinja free var of the underlying jinja string which does not corresponds to a component parameter. For example, the cases listed in the previous section are typical situations where leaks occur.
A component is said to be bounded if every leak is declared in the {lib:local context} __context__.
The best practice is to always try to build bounded components.
Factories
Some times one needs to build parameterized families of components. This can be realized through component factories, i.e, type factories which constructs subtypes of COMPONENT, hence that take values into SUB(COMPONENT).
One can construct component factories by first constructing jinja factories. Indeed, if factory: X -> SUB(Jinja) is a jinja factory, then one can obtain a subtype FACTORY(x) of COMPONENT, by considering all components that take values into factory(x) for a given x in X. So, varying x we build a type factory factory FACTORY: X -> SUB(COMPONENT).
Inners
In comp, the components have another special kind of parameters: the inner ones. They are necessarily of type Inner, and work as placeholders for future inserts inside the component.
1from comp 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. This means that it only accept jinja strings. It is typically used to receive the jinja string of other component, in some sort of {lib:component concatenation}.
One can filter the type COMPONENT by looking at components that have a fixed number of inner vars. Indeed, when viewed as a callable entity, the type COMPONENT accepts an integer inners in Int such that:
for
inners >= 0,COMPONENT(inners)is the subtype ofCOMPONENTconsisting of all components that have preciselyinnersinner vars;for
inners < 0,COMPONENT(inners)is the subtype ofCOMPONENTof all components that have any number of inner vars. Thus, of course, there is a {lib:type equivalence} betweenCOMPONENT(inners<0)andCOMPONENT.
Contents
There is one more special kind of parameters in components: that of type Content. They can receive:
MarkdowncontentReStructuredTextcontentor a path to some
.mdor.rstfile.
Therefore, they are used to receive static content, which will be automatically converted to raw HTML in the rendering process. This opens the possibility to use comp as a component system to both dynamic and static websites.
As happens with inner vars, the content vars also filter the type COMPONENT through a integer parameter, here called contents:
for
contents >= 0,COMPONENT(contents)is the subtype ofCOMPONENTgiven by all components that have preciselycontentscontent vars;for
contents < 0,COMPONENT(contents){lib:type equivalent} toCOMPONENT.
Of course, the type of all components can be simultaneously filtered by both inner vars and content vars, producing subtypes
COMPONENT(inners, contents).
Attributes
The type COMPONENT of all components also come equipped with some properties, which define some component attributes:
attribute |
description |
|---|---|
.jinja |
the component jinja string |
.leaks |
the component leaks |
.inner_params |
tuple of inner args |
.content_params |
tuple of content_args |
.tags |
the tuple of defining tags |
.is_page |
if the component is a page |
From the {lib:functions over methods} philosophy of typed, such attributes can also be obtained from typed functions:
attribute |
typed function |
|---|---|
.jinja |
get_jinja |
.leaks |
get_leaks |
.inner_params |
get_inner_params |
.content_params |
get_content_params |
.tags |
get_tags |
.is_page |
is_page |
Operations
There are four main operations involving components:
join: Prod(COMPONENT, n) -> COMPONENT:receives a tuple of components and creates a new component whose jinja string is the join of the jinja strings of the provided components;
concat: Prod(COMPONENT(1), COMPONENT) -> COMPONENT:receives a component with a single inner var and another arbitrary component, producing a new component whose jinja string is obtained by replacing the placeholder given by inner var in the first component with the jinja string of the second component.
evalib: Prod(COMPONENT, Dict(Any)) -> COMPONENT:receives a component and a list with keys and values, returning the component obtained by fixing each variable associated to a key with the corresponding value, leaving the other variables unchanged.
copy: Prod(COMPONENT, Dict(Str)) -> COMPONENT:receives a component and a list with keys and values, returning a copy of the given component such that each parameter whose name is a key is renamed with the corresponding value, maintaining its type.
The intuition for each of such component operations is as follows:
join: put a component after other component;concat: put a component inside other component;eval: from a component, fixes some part of it;copy: from a component, copy it.
So, for example, consider the following generic components:
1from typed import SomeType, OtherType
2from comp import Jinja, component, Tag, Inner
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 f"""jinja
15<some-tag>
16 { inner }
17</some-tag>
18"""
Applying the component join to them we get a new component join(some_comp, inner_comp) which is equivalent to the following:
1@component
2def joined_comp(x: SomeType, ..., a: OtherType, inner: Inner, ...) -> Jinja:
3 return f"""jinja
4[[ contents of 'some_comp' jinja string ]]
5<some-tag>
6 { inner }
7</some-tag>
8"""
On the other hand, the component concat produces a component concat(inner_comp, some_comp) which 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"""
Also, eval(inner_comp, inner=Jinja(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"""
Finally, copy(inner_comp, {"inner": "other_name"}) produces the following component:
1@component
2def copied_comp(a: OtherType, other_name: Inner, ...) -> Jinja:
3 return f"""jinja
4<some-tag>
5 { other_name }
6</some-tag>
7"""
Overlapping
One should note that the components subject to the component operations could have overlapping parameters. About that, some remarks:
The parameters of the component join and the component concat are obtained from the union of tuple of parameters of the underlying components. This means that their multiplicity is not considered. More precisely, if components
comp_1andcomp_2share some parameter, then it will appear a single time injoin(comp_1, comp_2)and inconcat(comp_1, comp_2).If the shared parameters are of different types, a type error will be raised.
So, for example, consider the following components:
1from typed import SomeType, OtherType, AnotherType, ...
2from comp import component, Jinja
3
4@component
5def comp_1(x: SomeType, y: OtherType, ...) -> Jinja:
6 ...
7
8@component
9def comp_2(x: SomeType, z: AnotherType, ...) -> Jinja:
10 ...
11
12@component
13def comp_3(x: SomeType, y: AnotherType, ...) -> Jinja:
14 ...
Then join(comp_1, comp_3) will raise a type error, while join(comp_1, comp_2) and join(comp_2, comp_3) will generate components equivalent to the following ones:
1from typed import SomeType, OtherType, AnotherType, ...
2from comp import component, Jinja
3
4@component
5def comp_1_2(x: SomeType, y: OtherType, z: AnotherType) -> Jinja:
6 ...
7
8@component
9def comp_2_3(x: SomeType, z: AnotherType, y: OtherType) -> Jinja:
10 ...
To avoid the overlapping behavior, you should make a component copy of some of the components before the component join and component concat, changing the name of the repeating parameter, as follows (this is precisely the typical use case of the component copy operation):
1from comp import copy
2from some.where import comp_1, comp_2, comp_3
3
4copied_2 = copy(comp_2, x="x_2")
5copied_3 = copy(comp_2, x="x_3", y="y_3")
In this case, join(comp_1, copied_2, copied_3) has no overlapping, being equivalent to the following component:
1from typed import SomeType, OtherType, AnotherType, ...
2from comp import component, Jinja, copy
3
4@component
5def joined_comp(
6 x: SomeType,
7 y: OtherType,
8 x_2: OtherType,
9 z: AnotherType,
10 x_3: OtherType,
11 y_3: AnotherType
12 ) -> Jinja:
13 ...
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.