= xml_to_data(
struct: Data """
<model name="User">
<field name="id" type="integer"/>
<field name="username" type="char"/>
<field name="email" type="email"/>
</model>
"""
)
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”
…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"
)
= "{{ name }} = models.{{ type | title }}Field()" 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"
)
= "{{ name }} = models.{{ type | title }}Field()" 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:
= tempfile.NamedTemporaryFile()
destination
= xml_to_data(
s_file 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.from_config(template_directories=["/tmp/templates"])
sal 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.from_config(template_directories=["/tmp/templates"])
sal
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.from_config(template_directories=["/tmp/templates"])
sal
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_to_data(
xml """
<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):
= [d.attrs.get("imports") for d, _ in data]
imports_ = [d for d in imports_ if d]
imports_ return imports_
= Sal.from_config(
sal =["/tmp/templates"], filters={"imports": imports}
template_directories
)= sal.process(xml)
res
assert (
res.strip()== dedent(
"""
from AAA import A
from BBB import B
class W:
a = AAA()
a = AAA()
b = BBB()
"""
).strip() )