Migrating from the helper libraries
chameleon-rs builds in what chameleon_partials, chameleon-flask, fastapi-chameleon and chameleon-robyn provide, and has its own Litestar engine. With chameleon_rs.compat.install() you don’t have to change anything: the old imports give the built-ins (see Switching from Chameleon for Chameleon itself). This page is for when you want to move to the new names, and drop the old setup code that’s no longer needed.
At a glance
| Before | After |
|---|---|
from chameleon import PageTemplateLoader |
from chameleon_rs import PageTemplateLoader |
chameleon_partials.register_extensions(folder, auto_reload=dev) and chameleon_flask.global_init(folder, auto_reload=dev) |
One call: chameleon_rs.configure(template_folder=folder, auto_reload=dev) |
return chameleon_partials.extend_model(model) in every view |
return model: render_partial is in every template |
A Pyramid BeforeRender subscriber that adds render_partial |
Remove it |
import chameleon_flask |
import chameleon_rs.flask as chameleon (or as chameleon_flask) |
import fastapi_chameleon |
import chameleon_rs.fastapi as chameleon (or as fastapi_chameleon) |
import chameleon_robyn |
import chameleon_rs.robyn as chameleon (or as chameleon_robyn) |
from litestar_chameleon import ChameleonTemplateEngine |
from chameleon_rs.litestar import ChameleonEngine, and return chameleon_rs.litestar.Template |
chameleon_partials.render_partial(path, **m).html_text |
chameleon_rs.render_partial(path, **m).html_text |
except chameleon_partials.PartialsException, except chameleon_flask.exceptions.FlaskChameleonException |
Still work. except chameleon_rs.web.TemplateError catches both |
has_registered_extensions = False in test teardown |
chameleon_rs.partials.clear() |
chameleon_partials
# Before
import chameleon_partials
chameleon_partials.register_extensions(template_folder, auto_reload=dev_mode)
def index(request):
model = {'videos': videos}
return chameleon_partials.extend_model(model)# After
import chameleon_rs
chameleon_rs.configure(template_folder=template_folder, auto_reload=dev_mode)
def index(request):
return {'videos': videos}Templates don’t change. ${render_partial('shared/partials/video.pt', video=v)} works as before, and runs natively.
Things to know:
- Every configure() call applies. register_extensions ignored a second call unless you passed
cache_init=False. register_extensions itself still behaves the old way. - Partial paths can also be relative to the calling template. A path that isn’t in the template folder is looked for next to the calling template, where chameleon_partials would raise. Templates that worked before render exactly the same.
configure(partial_search='registered')turns the extra lookup off. - Configuring is thread-safe. Configuration is swapped in whole, so a render never sees half of one.
chameleon-flask
# Before
import chameleon_flask
chameleon_flask.global_init(template_folder, auto_reload=dev_mode)# After
import chameleon_rs.flask as chameleon
chameleon.configure(template_folder=template_folder, auto_reload=dev_mode)Views, @template, render, response and not_found stay the same. To leave existing call sites as they are, import it as chameleon_flask instead. New in chameleon-rs:
- configure() also sets the partials folder, so you need only one setup call.
error(template_file, status_code=500, **data)renders any error page.- Views can return mappings, dataclasses and Pydantic models, not only
dict. configure(..., app=app)gives one app its own configuration.
See Flask and Quart.
fastapi-chameleon
import fastapi_chameleonbecomes
import chameleon_rs.fastapi as chameleonImport it as fastapi_chameleon to leave existing call sites as they are, or keep import fastapi_chameleon unchanged with chameleon_rs.compat.install(). Every name keeps working (global_init, @template, render, response, not_found, generic_error, clear, the exception classes), and its test suite passes unmodified on the built-in. Differences:
- Async views render with render_async: an awaitable model value is awaited where the template uses it.
configure(..., app=app)(orglobal_init(..., app=app)) checks at startup that every@templatesits below its route decorator.- error(), render_async(), response_async(), and
status_code=on@template.
See FastAPI.
chameleon-robyn
import chameleon_robynbecomes
import chameleon_rs.robyn as chameleonImport it as chameleon_robyn to leave existing call sites as they are, or keep import chameleon_robyn unchanged with chameleon_rs.compat.install(). Every name keeps working (global_init, @template, render, response, not_found, clear, ChameleonTemplate, __response_callback__, the exception classes), and its test suite passes unmodified on the built-in. Differences:
- Async handlers render with render_async: an awaitable model value is awaited where the template uses it.
configure(..., app=app)(orglobal_init(..., app=app)) checks inapp.start()(and when Robyn’s test client is built) that every@templatesits below its route decorator.- error(), render_async(), response_async(), data for not_found(), and ChameleonTemplate.render_template_async().
See Robyn.
litestar-chameleon
litestar-chameleon is a third-party package, not built in: it keeps working as it is with chameleon_rs.compat.install(). To move to the built-in engine instead:
from litestar_chameleon import ChameleonTemplateEngine
from litestar.response import Templatebecomes
from chameleon_rs.litestar import ChameleonEngine as ChameleonTemplateEngine
from chameleon_rs.litestar import Template # .pt served as text/htmlDifferences: url_for_static_asset is registered too, a name outside the template folder is refused, a context key wins over a template callable of the same name (as for Jinja), the loader options are ChameleonEngine arguments (CHAMELEON_RELOAD rather than CHAMELEON_AUTO_RELOAD), and stock Chameleon need not be installed. See Litestar.
Timing renders
Instrumentation that replaces PageTemplate.render to time every render (common in apps that log slow templates) changes how async renders run on chameleon-rs. render_async() honours a render() that differs from the built-in one, because a subclass’s override may add defaults or post-process the output, and a monkeypatched render looks exactly like that override. So with the patch in place, every render_async(), including those of async @template views, calls your render synchronously, and awaitable model values are not awaited.
# Makes every render_async() render synchronously:
original = PageTemplate.render
def timed_render(self, *args, **kwargs):
start = time.perf_counter()
try:
return original(self, *args, **kwargs)
finally:
log_render(self.filename, time.perf_counter() - start)
PageTemplate.render = timed_renderWrapping render_async as well doesn’t help: the original render_async still sees the replaced render. Instead:
- Time the request or the view, in middleware or around the call that renders. The template’s share is then the view’s time minus its data loading.
- Use a sampling profiler such as py-spy for a breakdown by template and helper, with no code change.
- Keep the patch only in apps that render synchronously. There it changes nothing but the timing.
The same applies to a subclass that overrides render(): its render_async() renders synchronously through the override. See Compatibility.
Pitfalls
- Install the alias first. chameleon_rs.compat.install() must run before the first
import chameleon, including imports inside your framework integration. Put it in the entry point itself, above every other import. - A patched PageTemplate.render makes async renders synchronous. See Timing renders.
- Don’t mix shadow mode with the alias. Shadow mode needs stock Chameleon as chameleon.
template.sourceisn’t Python. Code that read the generated module (rare, mostly debugging tools) gets a program dump instead. See Compatibility.