Skip to content

Conversation

@srnnkls
Copy link
Contributor

@srnnkls srnnkls commented Oct 26, 2025

No description provided.

This commit dramatically improves the developer experience for template
development, especially in IPython/Jupyter environments with %autoreload.

## Problem

The previous implementation stored template metadata (_source, _compiled_template,
_engine, etc.) as class attributes. This created issues with IPython's %autoreload
because:
- Closures captured stale references to class attributes
- Redefining a template class didn't update existing instances
- File-backed templates didn't reload when the file changed

## Solution

Use method defaults to capture template state in __str__'s default argument:

```python
def __str__(instance, _cache={"tmpl": None, "src": ..., ...}):
    # Lazy compilation and hot-reload logic
```

## Key Improvements

1. **IPython/Jupyter Compatibility**:
   - %autoreload replaces methods and their __defaults__
   - New template source is automatically picked up
   - Avoids "frozen closure" problem

2. **Lazy Compilation**:
   - Templates compile on first render, not at definition time
   - Faster template definition, especially for unused templates
   - Better for interactive development

3. **Hot-Reload for File Templates**:
   - Automatically detects file modifications via mtime
   - Reloads and recompiles changed templates
   - Perfect for iterative template development

4. **Old Instance Safety**:
   - Proxy rebuilt lazily in __str__ if missing
   - Old instances remain functional after class redefinition
   - More forgiving for live coding

5. **No Global State**:
   - No registry, no module-level tracking
   - Each __str__ has its own cache via defaults
   - Clean, isolated implementation

## Performance

- Same runtime cost as before (one compile per class per edit)
- Lazy compilation saves time for unused templates
- Hot-reload has negligible overhead (just an mtime check)

## Implementation Details

**Before (class attributes)**:
```python
cls._source = source
cls._compiled_template = engine.compile_template(source)
cls._engine = engine
cls._variables = variables

def __str__(instance):
    return instance._compiled_template.render(...)
```

**After (method defaults)**:
```python
def __str__(instance, _cache={"tmpl": None, "src": source, ...}):
    if _cache["path"]:  # File hot-reload
        mtime = os.path.getmtime(_cache["path"])
        if _cache["mtime"] != mtime:
            _cache["src"] = load_template_source(_cache["path"])
            _cache["tmpl"] = None  # force recompile
            _cache["mtime"] = mtime

    if _cache["tmpl"] is None:  # Lazy compile
        _cache["tmpl"] = _cache["engine"].compile_template(_cache["src"])

    # Build proxy lazily for old instances
    if not hasattr(instance, "_proxy"):
        instance._proxy = SerializationProxy.build(...)

    return _cache["tmpl"].render(...)
```

## Testing

Added 4 new tests in test_hot_reload.py:
- File template hot-reload when file changes
- Inline templates don't reload (expected)
- Lazy compilation on first render
- Multiple instances share compiled cache

All 65 tests pass (33 benchmarks + 32 integration).

This makes deigma much more pleasant to use in interactive development!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added comprehensive benchmarks to measure the overhead introduced by
the method-defaults approach with lazy compilation and hot-reload.

## Benchmark Results

**Template Definition**: ~1.1 ms (one-time cost)
- This is the cost of defining a template class
- Happens once per template definition

**Instance Creation**: ~3.4 μs (very fast!)
- No compilation happens at creation time
- Lazy compilation defers cost to first render

**First Render (Lazy Compilation)**:
- Inline template: ~30 μs
- File template: ~162 μs (includes mtime check + load + compile)

**Cached Renders**:
- Inline template: ~18 μs (pure rendering)
- File template: ~158 μs (includes mtime check)

**Mtime Check Overhead**: ~140 μs per render
- File templates: ~158 μs total
- Inline templates: ~18 μs total
- Overhead = 158 - 18 = 140 μs for stat() + comparison

**Multiple Instances** (10 instances):
- Inline: ~296 μs total (~29.6 μs per instance)
- File: ~1.7 ms total (~171 μs per instance)
- Instances share compiled template cache

**Repeated Renders** (100 renders of same instance):
- Inline: ~1.7 ms (~17 μs per render)
- File: ~16.5 ms (~165 μs per render)

## Analysis

1. **Lazy compilation is essentially free**:
   - Instance creation: 3.4 μs (vs ~1 ms for eager compile)
   - First render pays the cost once: 30 μs
   - Subsequent renders are fast: 18 μs

2. **Mtime check overhead is acceptable**:
   - ~140 μs per render for file templates
   - Only applies to file-backed templates
   - Enables hot-reload for development
   - For production, use inline templates

3. **Cache sharing works well**:
   - Multiple instances share compiled template
   - Lazy proxy building: ~29 μs (only on first render per instance)

## Conclusion

The overhead is minimal and acceptable for development workflows:
- Lazy compilation saves time for unused templates
- Hot-reload is worth the ~140 μs mtime check in development
- For production, inline templates have minimal overhead (18 μs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello @srnnkls, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the template system by introducing lazy compilation and hot-reloading capabilities, primarily for templates loaded from files. These features are designed to improve the development experience by allowing template modifications to be reflected immediately without requiring application restarts, while also optimizing performance by deferring resource-intensive compilation until it's absolutely necessary. The changes are thoroughly validated with new benchmarks to quantify performance impacts and comprehensive integration tests to ensure correct functionality.

Highlights

  • Lazy Compilation: Template compilation is now deferred until the first time a template instance is rendered (i.e., str(instance) is called), optimizing startup performance.
  • Hot-Reloading for File Templates: File-backed templates will automatically detect changes to their source file (via mtime checks) and recompile on the next render, enabling dynamic updates without restarting the application.
  • Performance Benchmarks Added: New benchmarks have been introduced to measure the overhead of template definition, instance creation, first render (lazy compilation), cached rendering, mtime checks, and lazy proxy building.
  • Integration Tests for New Features: Dedicated integration tests now verify the hot-reload functionality for file-backed templates, confirm non-reloading behavior for inline templates, and ensure lazy compilation and shared cache mechanisms work as expected.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@srnnkls srnnkls changed the title Add benchmarks for template overhead measurement Support autoreload for templates Oct 26, 2025
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces hot-reloading for file-based templates and adds a comprehensive set of benchmarks to measure performance overhead. The changes in src/deigma/template.py implement lazy compilation and caching, with support for detecting file modifications. The new test files tests/benches/test_template_overhead.py and tests/integration/test_hot_reload.py provide good coverage for the new functionality.

My review focuses on improving code maintainability and test robustness. I've identified significant code duplication in the __str__ method implementations that should be refactored. I also have a suggestion to make the hot-reload integration test more reliable.

Comment on lines 129 to 200
if use_proxy:

def __str__(instance):
proxied = {
field: getattr(instance._proxy, field) for field in cls._variables
def __str__(
instance,
_cache={
"tmpl": None,
"engine": Jinja2Engine(serialize=serialize),
"src": _src,
"vars": variables,
"path": _path,
"mtime": None,
},
):
# Optional file mtime check for hot-reload
if _cache["path"] and os.path.exists(_cache["path"]):
mtime = os.path.getmtime(_cache["path"])
if _cache["mtime"] != mtime:
_cache["src"] = load_template_source(_cache["path"])
_cache["tmpl"] = None # force recompile
_cache["mtime"] = mtime

if _cache["tmpl"] is None:
_cache["tmpl"] = _cache["engine"].compile_template(_cache["src"])

if not hasattr(instance, "_proxy"):
adapter = _get_adapter(instance.__class__)
instance._proxy = SerializationProxy.build(instance, adapter)

proxied = {name: getattr(instance._proxy, name) for name in _cache["vars"]}
return _cache["tmpl"].render(proxied)

else:

def __str__(
instance,
_cache={
"tmpl": None,
"engine": Jinja2Engine(serialize=serialize),
"src": _src,
"vars": variables,
"path": _path,
"mtime": None,
},
):
# Optional file mtime check for hot-reload
if _cache["path"] and os.path.exists(_cache["path"]):
mtime = os.path.getmtime(_cache["path"])
if _cache["mtime"] != mtime:
_cache["src"] = load_template_source(_cache["path"])
_cache["tmpl"] = None # force recompile
_cache["mtime"] = mtime

if _cache["tmpl"] is None:
_cache["tmpl"] = _cache["engine"].compile_template(_cache["src"])

adapter = _get_adapter(instance.__class__)
serialized = adapter.dump_python(instance)
rendered_fields = {
name: _render_field_maybe(getattr(instance, name), serialized[name])
for name in _cache["vars"]
}
return instance._compiled_template.render(proxied)
return _cache["tmpl"].render(rendered_fields)

Choose a reason for hiding this comment

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

high

There is significant code duplication between the __str__ method defined when use_proxy is True and when it is False. The logic for hot-reloading based on file modification time and for lazy template compilation is identical in both branches.

This duplication makes the code harder to maintain, as any changes to this logic would need to be applied in two places. To improve this, you could extract the duplicated logic into a shared helper function within the decorator scope. This function would handle checking for file modifications and compiling the template, and could be called from both __str__ implementations.

For example:

def _get_compiled_template(cache):
    # Optional file mtime check for hot-reload
    if cache["path"] and os.path.exists(cache["path"]):
        mtime = os.path.getmtime(cache["path"])
        if cache["mtime"] != mtime:
            cache["src"] = load_template_source(cache["path"])
            cache["tmpl"] = None  # force recompile
            cache["mtime"] = mtime

    if cache["tmpl"] is None:
        cache["tmpl"] = cache["engine"].compile_template(cache["src"])
    return cache["tmpl"]

# ... then in both __str__ implementations:
tmpl = _get_compiled_template(_cache)
# ... rest of the logic

assert str(greeting) == "Hello, World!"

# Modify the file
time.sleep(0.01) # Ensure mtime changes

Choose a reason for hiding this comment

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

medium

Using time.sleep() in tests can lead to flakiness, as the required sleep duration can vary depending on the filesystem's modification time resolution and system load. A more robust way to test file modification is to manually set the modification time using os.utime() to a future timestamp. This would require importing the os module.

1. Extract duplicated hot-reload and lazy compilation logic into
   _get_compiled_template() helper function to reduce code duplication
   between use_proxy=True and use_proxy=False branches.

2. Replace time.sleep() with os.utime() in hot-reload tests for more
   robust and reliable testing that doesn't depend on filesystem
   mtime resolution or system load.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants