About

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

  1. Components

  2. Context

  3. Bounding

  4. Factories

  5. Tags

  6. Inners

  7. Contents

  8. Attributes

  9. Operations

  10. Overlapping

  11. Arithmetic

  12. Other Docs

Components

In comp, a component is a typed function 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, 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"""

Remark 1.

  1. Instead of using the @component decorator to define a component, you can use its short alias @comp.

  2. The components form a type COMPONENT.

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:

  1. when using local vars inside the jinja syntax of the jinja string;

  2. when calling external components inside the jinja string.

For example, these cases occur when one needs to (see below):

  1. loop with [% for ...  %] through a {lib:local var};

  2. 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 annotation Dict(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).

Tags

Typically, when one think of a “component” one imagine something which is delimited by a HTML tag. There are a class of components in comp that realizes that feeling.

More precisely, there is jinja factory Tag: Tuple(Str) -> SUB(Jinja) that receives a tuple of HTML tag names and returns the subtype Tag(*tags) of Jinja consisting of all jinja strings enclosed by one of the given tags.

So, for example, the following is instance of Tag('some-tag'):

1"""jinja
2<some-tag>
3...
4</some-tag>
5"""

With this jinja factory one can construct type safe tag-based components, the so-called tag components, by making use of the strategy described above:

 1from comp 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"""

This means that 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, also, a type factory TAG: Tuple(Str) -> SUB(COMPONENT) that associates to each tuple the corresponding type TAG(*tags).

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:

  1. for inners >= 0, COMPONENT(inners) is the subtype of COMPONENT consisting of all components that have precisely inners inner vars;

  2. for inners < 0, COMPONENT(inners) is the subtype of COMPONENT of all components that have any number of inner vars. Thus, of course, there is a {lib:type equivalence} between COMPONENT(inners<0) and COMPONENT.

Contents

There is one more special kind of parameters in components: that of type Content. They can receive:

  1. Markdown content

  2. ReStructuredText content

  3. or a path to some .md or .rst file.

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:

  1. for contents >= 0, COMPONENT(contents) is the subtype of COMPONENT given by all components that have precisely contents content vars;

  2. for contents < 0, COMPONENT(contents) {lib:type equivalent} to COMPONENT.

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

table 1: component attributes

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

table 1: component attributes and their typed functions

Operations

There are four main operations involving components:

  1. 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;

  2. 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.

  3. 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.

  4. 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:

  1. join: put a component after other component;

  2. concat: put a component inside other component;

  3. eval: from a component, fixes some part of it;

  4. 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:

  1. 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_1 and comp_2 share some parameter, then it will appear a single time in join(comp_1, comp_2) and in concat(comp_1, comp_2).

  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.

Other Docs

  1. overview

  2. jinja

  3. models

  4. rendering

  5. compatibility

  6. styles

  7. glossary

  8. changelog