-
Notifications
You must be signed in to change notification settings - Fork 0
docs: Document template lifecycle and refine #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: satisfy-linters-and-type-checkers
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| # deigma | ||
| A type-safe templating library for python | ||
| Type-safe templating for python | ||
|
|
||
| > δεῖγμᾰ • (deîgmă) n (genitive δείγμᾰτος); third declension | ||
| > Pronounciation: IPA(key): /dêːŋ.ma/ (DHEEG-mah, THEEG-mah) | ||
|
|
@@ -262,9 +262,128 @@ print(LiteralSQLKeywordListingTemplate(keywords=keywords)) | |
| > If you encounter any issues, please report them. | ||
|
|
||
| ### Template lifecycle | ||
| _TODO_ | ||
| #### With `SerializationProxy` | ||
| Understanding the template lifecycle helps you reason about performance and when serialization happens. | ||
|
|
||
| #### Definition time (when `@template` is applied) | ||
| When you apply the `@template` decorator to a class, several things happen at import time: | ||
|
|
||
| 1. *Class transformation*: Your class is converted into a pydantic dataclass with validation support | ||
| 2. *Template compilation*: The template source is parsed and compiled into a Jinja2 template | ||
| 3. *Variable extraction*: Template variables (e.g., `{{ name }}`) are extracted from the source | ||
| 4. *Validation*: Template variables are validated against class fields to catch mismatches early | ||
| 5. *Schema building*: A `TypeAdapter` is created for the class to enable efficient serialization | ||
| 6. *Method injection*: Custom `__init__` and `__str__` methods are injected based on the `use_proxy` setting | ||
|
|
||
| This upfront work at definition time means templates are ready to render efficiently at runtime. | ||
|
|
||
| #### With `SerializationProxy` (default) | ||
| When `use_proxy=True` (the default), the lifecycle is optimized for performance through aggressive caching: | ||
|
|
||
| *Instantiation time* (when calling `HelloTemplate(name="world")`): | ||
| 1. The pydantic dataclass `__init__` validates your data | ||
| 2. A `SerializationProxy` is built for the entire instance | ||
| 3. The proxy serializes the complete object graph using `TypeAdapter.dump_python()` | ||
| 4. *All field serializers are applied immediately* and results are cached | ||
| 5. The proxy stores this serialized snapshot immutably (using `MappingProxyType`) | ||
| 6. A proxy-aware `CoreSchema` is created to handle nested object access | ||
|
|
||
| *Rendering time* (when calling `str(instance)`): | ||
| 1. For each template variable, the field is accessed via the proxy | ||
| 2. The proxy returns the *pre-serialized value* from its cache (dictionary lookup) | ||
| 3. For primitive fields (str, int, etc.), the cached value is returned directly | ||
| 4. For nested objects (lists, dicts, dataclasses), child proxies are created lazily and cached | ||
| 5. The compiled Jinja2 template renders using these values | ||
| 6. The `auto_serialize` filter is applied (but often becomes a no-op since values are pre-serialized) | ||
|
|
||
| *Key benefits*: | ||
| - *Performance*: Field serializers run once at instantiation, not on every access | ||
| - *Immutability*: The serialized snapshot is immutable, making templates thread-safe | ||
| - *Type preservation*: Nested objects maintain their structure for Jinja2 operations (loops, conditionals) | ||
|
|
||
| *Example*: | ||
| ```python | ||
| from pydantic import PlainSerializer | ||
| from typing import Annotated | ||
|
|
||
| SQLKeywordName = Annotated[str, PlainSerializer(lambda s: s.upper())] | ||
|
|
||
| @dataclass | ||
| class SQLKeyword: | ||
| name: SQLKeywordName # Serializer will run at instantiation | ||
| description: str | ||
|
|
||
| @template( | ||
| """ | ||
| {% for keyword in keywords %} | ||
| - {{ keyword.name }}: {{ keyword.description }} | ||
| {% endfor %} | ||
| """ | ||
| ) | ||
| class SQLKeywordListingTemplate: | ||
| keywords: list[SQLKeyword] | ||
|
|
||
| # When you create the instance, ALL serializers run immediately: | ||
| keywords = [SQLKeyword(name="select", description="Selects rows")] | ||
| t = SQLKeywordListingTemplate(keywords=keywords) # name="select" -> "SELECT" happens here | ||
|
|
||
| # Rendering just does cache lookups: | ||
| str(t) # Fast: just retrieves cached "SELECT" value | ||
| # - SELECT: Selects rows | ||
| ``` | ||
|
|
||
| #### Without `SerializationProxy` | ||
| When `use_proxy=False`, serialization is deferred until rendering time: | ||
|
|
||
| *Instantiation time* (when calling `HelloTemplate(name="world")`): | ||
| - Only the pydantic dataclass `__init__` runs | ||
| - No serialization happens yet | ||
| - The instance just stores the raw field values | ||
|
|
||
| *Rendering time* (when calling `str(instance)`): | ||
| 1. The entire instance is serialized using `TypeAdapter.dump_python()` | ||
| 2. *All field serializers are applied at this point* | ||
| 3. For each template variable: | ||
| - If it's a `Template`, recursively render it by calling `str(field)` | ||
| - Otherwise, use the serialized value (losing type information) | ||
| 4. The compiled Jinja2 template renders using these serialized values | ||
| 5. The `auto_serialize` filter is applied to each variable expression | ||
|
|
||
| *Trade-offs*: | ||
| - *Simpler*: No proxy overhead or caching complexity | ||
| - *Memory efficient*: No cached snapshot stored on each instance | ||
| - *Type information lost*: Once serialized, nested objects become plain dicts/lists | ||
| - *Repeated serialization*: Field serializers run on every `str()` call if called multiple times | ||
|
|
||
| *Example*: | ||
| ```python | ||
| @template("{{ user }}", use_proxy=False) | ||
| class UserTemplate: | ||
| user: User | ||
|
|
||
| t = UserTemplate(user=User(first_name="Li", last_name="Si")) | ||
| # At this point, nothing is serialized yet | ||
|
|
||
| str(t) # Serialization happens here | ||
| # If you call str(t) again, serialization runs again | ||
|
|
||
| # Note: In templates with loops/conditionals, you lose type info: | ||
| @template( | ||
| """ | ||
| {% for keyword in keywords %} | ||
| - {{ keyword.name }} {# keyword is now a plain dict, not SQLKeyword #} | ||
| {% endfor %} | ||
| """, | ||
| use_proxy=False | ||
| ) | ||
| class ListTemplate: | ||
| keywords: list[SQLKeyword] | ||
|
Comment on lines
+359
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example uses from dataclasses import dataclass
from deigma import template
# Assuming User and SQLKeyword are defined as dataclasses for this example
@dataclass
class User:
first_name: str
last_name: str
@dataclass
class SQLKeyword:
name: str
description: str
@template("{{ user }}", use_proxy=False)
class UserTemplate:
user: User
t = UserTemplate(user=User(first_name="Li", last_name="Si"))
# At this point, nothing is serialized yet
str(t) # Serialization happens here
# If you call str(t) again, serialization runs again
# Note: In templates with loops/conditionals, you lose type info:
@template(
"""
{% for keyword in keywords %}
- {{ keyword.name }} {# keyword is now a plain dict, not SQLKeyword #}
{% endfor %}
""",
use_proxy=False
)
class ListTemplate:
keywords: list[SQLKeyword] |
||
| ``` | ||
|
|
||
| *When to use `use_proxy=False`:* | ||
| - You're rendering templates only once and want minimal memory overhead | ||
| - You have very large object graphs and want to avoid caching | ||
| - You don't need field serializers to work in nested contexts | ||
| - You're debugging proxy-related issues | ||
| ### Custom serialization | ||
| By default, template variables are serialized using `str`. You can inject serializers | ||
| into templates in two ways. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code example is missing some necessary imports (
dataclassandtemplate). To make it easier for users to copy and run the code, it's best to include all required imports.