| name | chameleon-partials |
| description | Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks. Use when writing Python code that uses the chameleon_partials package.
|
| license | MIT |
| compatibility | Requires Python >=3.9. |
Chameleon Partials
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
Installation
pip install chameleon-partials
When to use what
| Need | Use |
|---|
| Insert a reusable HTML fragment inside a Chameleon template | ${ render_partial('shared/partials/fragment.pt', key=value) } |
| Make render_partial available in every Pyramid template | BeforeRender subscriber: event['render_partial'] = chameleon_partials.render_partial |
| Make render_partial available in other frameworks (FastAPI, plain WSGI, ...) | return chameleon_partials.extend_model(model) from the view |
| Get a rendered fragment as a plain string from Python (tests, HTMX, email) | render_partial(template_file, **model).html_text |
| Point tests or a second setup call at a different template folder | register_extensions(folder, cache_init=False), or reset has_registered_extensions first |
API overview
Setup
Register chameleon_partials with Chameleon. Call this once at application startup.
register_extensions: Register chameleon_partials with Chameleon so partials can be rendered
Rendering partials
Render partial templates and expose render_partial to your view models.
render_partial: Render a partial template to an HTML fragment
extend_model: Add render_partial to a view model so templates can call it
Exceptions
Errors raised by the library.
PartialsException: Raised when chameleon_partials is configured or used incorrectly
Gotchas
- register_extensions() is one-shot by default: with cache_init=True, later calls after one successful registration are silent no-ops. Pass cache_init=False or reset chameleon_partials.has_registered_extensions to re-point it.
- render_partial() paths are always relative to the registered template folder — never to the calling template — even when called from inside another partial.
- PartialsException covers misuse only (rendering before registration, empty/non-directory folder, non-dict model). A missing template file raises Chameleon's ValueError, unwrapped.
- render_partial() returns an HTML wrapper, not a str — Chameleon inserts it unescaped via html(); read .html_text for the raw markup. The HTML class is not exported in all.
- The model name 'encoding' is reserved (render_partial passes encoding='utf-8' internally); 'translate', 'target_language', and 'repeat' have special meaning to Chameleon.
- extend_model() unconditionally replaces any existing 'render_partial' key in the model; render_partial() itself only injects the key when it is missing.
- Registration is process-wide module state and is not lock-guarded — call register_extensions() from single-threaded startup code, not from concurrent request handlers.
Best practices
- Call register_extensions(template_folder, auto_reload=dev_mode) exactly once at application startup, before any template renders; leave auto_reload=False in production.
- In Pyramid, wire render_partial globally with a BeforeRender subscriber; in other frameworks, return chameleon_partials.extend_model(model) from each view.
- Keep partials as bare HTML fragments under shared/partials/ and prefix METAL layouts with an underscore (shared/_layout.pt).
- Build the folder path with (Path(file).parent / 'templates').as_posix() so it is package-relative and uses forward slashes on every platform.
- In test fixtures, reset chameleon_partials.has_registered_extensions = False in teardown so the next test can register its own template folder.
End-to-end wiring
Register the template folder once at startup, then call render_partial(...) from inside any Chameleon template. Template paths are always relative to the registered folder — even when called from inside another partial.
from pathlib import Path
import chameleon_partials
folder = (Path(__file__).parent / 'templates').as_posix()
chameleon_partials.register_extensions(folder, auto_reload=True)
<div class="greeting">
<h2>Hello, ${ name }!</h2>
</div>
${ render_partial('shared/partials/greeting.pt', name='Michael') }
The template needs render_partial in its model. Pyramid apps wire it globally with a BeforeRender subscriber; other frameworks (FastAPI, plain WSGI, ...) call extend_model(model) on the view-model dict before rendering:
from pyramid.events import subscriber, BeforeRender
import chameleon_partials
@subscriber(BeforeRender)
def add_global(event):
event['render_partial'] = chameleon_partials.render_partial
model = dict(name='Michael')
return chameleon_partials.extend_model(model)
Inside a partial, render_partial is injected into the model automatically, so partials can nest further partials with no extra wiring — nesting depth is unlimited.
Chameleon template syntax (this is TAL, not Jinja)
Chameleon templates are valid HTML/XML where directives live in tal: and metal: attributes. There is no {% ... %} or {{ ... }} — do not use Jinja/Django syntax. Interpolation uses ${ ... } and may contain arbitrary Python expressions.
<h1>${ video.title }</h1>
<div class="views">${ "{:,}".format(video.views) } views</div>
<div class="video" tal:repeat="v videos">
${ render_partial('shared/partials/video_square.pt', video=v) }
</div>
<img tal:condition="show_avatar" src="${ user.avatar_url }" />
Shared layouts with METAL macros
Partials compose with Chameleon's METAL layout inheritance — pages fill a layout's slot and call partials inside it:
<!DOCTYPE html metal:define-macro="layout">
<html lang="en">
<body>
<div metal:define-slot="content">No content</div>
</body>
</html>
<div metal:use-macro="load: ../shared/_layout.pt">
<div metal:fill-slot="content">
<div tal:repeat="v videos">
${ render_partial('shared/partials/video_square.pt', video=v) }
</div>
</div>
</div>
Note the asymmetry: metal:use-macro="load: ..." paths are relative to the current template file, while render_partial(...) paths are always relative to the registered template folder.
Project layout conventions
your_app/
├── __init__.py # register_extensions() here, at startup
├── views/
│ └── partials_middleware.py # BeforeRender subscriber (Pyramid)
└── templates/ # the registered folder
├── home/index.pt # page templates
└── shared/
├── _layout.pt # METAL layout (underscore prefix by convention)
└── partials/ # reusable fragments live here
└── video_square.pt
Registration semantics & testing
Registration is process-wide module state. With the default cache_init=True, calling register_extensions() again after a previous successful registration is a silent no-op — safe for repeated startup calls, but a test pointing at a different folder must pass cache_init=False or reset the flag. A registration that raised does not mark the module as registered (since 0.2.0), so it can simply be retried.
@pytest.fixture
def registered_extension():
folder = (Path(__file__).parent / 'test_templates').as_posix()
chameleon_partials.register_extensions(folder, auto_reload=True)
yield
chameleon_partials.has_registered_extensions = False
render_partial returns an HTML wrapper (its __html__() makes engines insert the markup unescaped); in tests or HTMX/email code, read the raw string from .html_text.
Fetching these docs as Markdown
Every page on the documentation site has a plain-Markdown twin: swap the .html extension for .md to get token-efficient source without the site chrome. For example https://mkennedy.codes/docs/chameleon-partials/reference/render_partial.html is also available at https://mkennedy.codes/docs/chameleon-partials/reference/render_partial.md. Prefer the .md form when reading these docs programmatically.
Resources