Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 122 additions & 3 deletions README.md
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)
Expand Down Expand Up @@ -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
Comment on lines +305 to +306

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This code example is missing some necessary imports (dataclass and template). To make it easier for users to copy and run the code, it's best to include all required imports.

Suggested change
from pydantic import PlainSerializer
from typing import Annotated
from dataclasses import dataclass
from deigma import template
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This example uses User and SQLKeyword types without defining them, and is also missing the import for @template. This makes the example incomplete and could be confusing for readers. I suggest adding minimal definitions for these types and the necessary import to make the snippet self-contained and runnable.

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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down