diff --git a/README.md b/README.md index 5651a45..a300f4e 100644 --- a/README.md +++ b/README.md @@ -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] +``` + +*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. diff --git a/pyproject.toml b/pyproject.toml index bcd718f..ce85dbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "deigma" version = "0.1.0" -description = "Add your description here" +description = "Type-safe templating library for python" readme = "README.md" authors = [ { name = "Sören Nikolaus", email = "soeren@getml.com" }