Code Generation

What code generation means with sal?

In it’s basic form, it will combine xml files converted to Data structures, with jinja templates, to render code. Later we will also introduce some frontmatter.

For this, we need a basic structure to work with for generating code. As an example, we’ll be working with an hypotetical “model”

struct: Data = xml_to_data(
    """
    <model name="User">
        <field name="id" type="integer"/>
        <field name="username" type="char"/>
        <field name="email" type="email"/>
    </model>
"""
)

…and the basic templates used with this structure are:

model = (
    "class {{ name }}Model(models.Model):\n"
    "    {%- for child in children %}\n"
    "    {{ child | render }}\n"
    "    {%- endfor %}\n"
)

field = "{{ name }} = models.{{ type | title }}Field()"

Code generator I (jinja only)

Now that we can render jinja2, we can make a basic code generator


Sal

 Sal (config:__main__.Config, renderer:sal.templates.Renderer)

Initialize self. See help(type(self)) for accurate signature.


Config

 Config (template_directories:list[pathlib.Path],
         filters:dict[str,typing.Callable]={})

*Usage docs: https://docs.pydantic.dev/2.8/concepts/models/

A base class for creating Pydantic models.

Attributes: class_vars: The names of classvars defined on the model. private_attributes: Metadata about the private attributes of the model. signature: The signature for instantiating the model.

__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
    This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
    __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a `RootModel`.
__pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
__pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.

__pydantic_extra__: An instance attribute with the values of extra fields from validation when
    `model_config['extra'] == 'allow'`.
__pydantic_fields_set__: An instance attribute with the names of fields explicitly set.
__pydantic_private__: Instance attribute with the values of private attributes set on the model instance.*

It’s important to note that a parent should be able the trigger the rendering of his children (this enures the recursive nature of the template rendering). Look at the model template for an example:

model = (
    "class {{ name }}Model(models.Model):\n"
    "    {%- for child in children %}\n"
    "    {{ child | render }}\n"
    "    {%- endfor %}\n"
)

field = "{{ name }} = models.{{ type | title }}Field()"

todo: document to-file

todo: document group>

We are missing one more thing, we need to be able to save the result to a file and we’d like to have that info in the xml and not mess with code to get the job done. So, here’s a new struct:

destination = tempfile.NamedTemporaryFile()

s_file = xml_to_data(
    f"""
    <model name="User" to-file="{destination.name}">
        <field name="id" type="integer"/>
        <field name="username" type="char"/>
        <field name="email" type="email"/>
    </model>
"""
)

with files(
    {
        "/tmp/templates/model.jinja2": model,
        "/tmp/templates/field.jinja2": field,
    }
):
    sal = Sal.from_config(template_directories=["/tmp/templates"])
    print(sal.process(s_file.clone()))

To make this even more powerful, we can use frontmatter to embed meta data into the templates themself and merge those with the attributes of the node.

To make it even more powerful, the frontmatter can contain any attribute from the struct so it needs to be extracted in a raw formar, rendered and then extracted. But first, we need new templates..

model = """
---
reference:  "sigla-{{ node.attrs.name | lower }}-model"
---
class {{ name }}Model(models.Model): # {{ reference }}
    {% for child in children -%}
    {{ child | render }}
    {% endfor %}
"""

field = """
---
reference:  "sigla-{{ node.name | lower }}-model"
---
{{ name }} = models.{{ type | title }}Field() 
"""


with files(
    {
        "/tmp/templates/model.jinja2": model,
        "/tmp/templates/field.jinja2": field,
    }
):
    sal = Sal.from_config(template_directories=["/tmp/templates"])
    test_eq(
        sal.process(struct.clone()).strip(),
        dedent(
            """
    class UserModel(models.Model): # sigla-user-model
        id = models.IntegerField()
        username = models.CharField()
        email = models.EmailField()
    """
        ).strip(),
    )
with files(
    {
        "/tmp/templates/model.jinja2": model,
        "/tmp/templates/field.jinja2": field,
    }
):
    sal = Sal.from_config(template_directories=["/tmp/templates"])
    sal.process(s_file)

    with open(destination.name, "r") as h:
        test_eq(
            h.read().strip(),
            dedent(
                """
    class UserModel(models.Model): # sigla-user-model
        id = models.IntegerField()
        username = models.CharField()
        email = models.EmailField()
        """
            ).strip(),
        )
xml = xml_to_data(
    """
<W to-file="/tmp/results.txt">
    <a/>
    <a/>
    <b/>
</W>
"""
)


w = """
---
---
{%- for i in node|imports|sum(None, [])|unique %}
{{ i }}
{%- endfor %}


class W:
    {%- for child in children %}
    {{ child | render }}
    {%- endfor %}
    
"""


a = """
---
imports: 
    - from AAA import A
---
a = AAA()
"""

b = """
---
imports: 
    - from BBB import B
---
b = BBB()
"""

with files(
    {
        "/tmp/templates/W.jinja2": w,
        "/tmp/templates/a.jinja2": a,
        "/tmp/templates/b.jinja2": b,
        "/tmp/results.txt": " ",
    }
):

    def imports(data: Data):
        imports_ = [d.attrs.get("imports") for d, _ in data]
        imports_ = [d for d in imports_ if d]
        return imports_

    sal = Sal.from_config(
        template_directories=["/tmp/templates"], filters={"imports": imports}
    )
    res = sal.process(xml)

    assert (
        res.strip()
        == dedent(
            """
    from AAA import A
    from BBB import B


    class W:
        a = AAA()
        a = AAA()
        b = BBB()
    """
        ).strip()
    )