Skip to main content

Chameleon Partials

Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks. (There is also a Jinja2/Flask version here.)

Overview

When building real-world web apps with Chameleon, it's easy to end up with repeated HTML fragments. Just like organizing code for reuse, it would be ideal to reuse smaller sections of HTML template code. That's what this library is all about.

Documentation

Full documentation lives at mkennedy.codes/docs/chameleon-partials. The user guide expands each section of this README:

  • Getting started: install, register the template folder, wire render_partial into your framework, render a first fragment.
  • Writing partials: how paths resolve, the model, nesting, loops, METAL layouts, rendering from Python, and choosing partials safely.
  • Testing: a pytest fixture for the one-time registration and what the error types promise.
  • Troubleshooting: every error you can hit in normal use, its cause, and the fix.
  • API reference, generated from the library's docstrings.

Coding agents can read the same material from llms-full.txt or install the agent skill.

Example

This project comes with a sample Pyramid application (see the example folder). This app displays videos that can be played on YouTube. The image, author subtitle, and view count are reused throughout the app. Here's a visual:

Video image, author, and view count HTML reused across pages of the demo app

Check out the demo / example application to see it in action.

Installation

It's just pip install chameleon-partials (or uv add chameleon-partials) and you're all set with this pure Python package. It needs Python 3.10 or newer and depends only on Chameleon.

Usage

Using the library is incredibly easy. The first step is to register the partial method with Chameleon. Do this once at app startup:

from pathlib import Path

from pyramid.config import Configurator

import chameleon_partials


def main(_, **settings):
    """This function returns a Pyramid WSGI application."""
    with Configurator(settings=settings) as config:
        config.include('pyramid_chameleon')
        config.include('.routes')
        config.scan()

        # Register the extension for working with Chameleon.
        folder = (Path(__file__).parent / 'templates').as_posix()
        chameleon_partials.register_extensions(folder, auto_reload=True, cache_init=True)

    return config.make_wsgi_app()

Set auto_reload=True while developing so template edits show up without a restart, and leave it at the default False in production. Calling register_extensions again after it has succeeded does nothing, so repeated startup calls are safe (see Testing for pointing tests at their own folder).

Next, you define your main HTML (Chameleon) templates as usual. Then define your partial templates. I recommend locating and naming them accordingly:

├── templates
│   ├── errors
│   │   └── 404.pt
│   ├── home
│   │   ├── index.pt
│   │   └── listing.pt
│   └── shared
│       ├── _layout.pt
│       └── partials
│           ├── video_image.pt
│           └── video_square.pt

Notice the partials subfolder in the templates/shared folder.

The templates are just HTML fragments. Here is a stand-alone one for the YouTube thumbnail from the example app:

<img src="https://img.youtube.com/vi/${ video.id }/maxresdefault.jpg"
     class="img img-responsive ${ ' '.join(classes or []) }"
     alt="${ video.title }"
     title="${ video.title }">

Notice that an object called video and a list of classes are passed in as the model.

Templates can also be nested. Here is the whole single video fragment with the image as well as other info linking out to YouTube:

<div>
    <a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank">
        ${ render_partial('shared/partials/video_image.pt', video=video, classes=[]) }
    </a>
    <a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank"
       class="author">${ video.author }</a>
    <div class="views">${ "{:,}".format(video.views) } views</div>
</div>

Now you see the render_partial() method. It takes the subpath into the templates folder and any model data passed in as keyword arguments. The path is always relative to the registered folder, even when the call is inside another partial. It returns an HTML object that Chameleon inserts unescaped; read its html_text attribute when you need the string, for example in a test or an HTMX response.

We can finally generate the list of video blocks as follows:

<span class="video" tal:repeat="v videos">
    ${ render_partial('shared/partials/video_square.pt', video=v) }
</span>

This time, we reframe each item in the list from the outer template (called v) as the video model in the inner HTML section.

Choosing Partials Safely

Choose the partial in your code, never from request data. render_partial() refuses a name that would reach outside the registered template folder (an absolute path, a .. out of the folder, or a package:path spec), but a request that picks the fragment should still be mapped to a name you wrote rather than used to build the path:

PARTIALS = {'card': 'shared/partials/video_square.pt', 'row': 'shared/partials/video_row.pt'}
html = render_partial(PARTIALS[request.params['style']], video=video)

Pass the model as named keyword arguments too. Spreading a request's parameters into it (render_partial(name, **request.params)) lets a parameter replace a name the template relies on, including render_partial itself.

The View Methods

In order to share the render_partial() function with your template, you'll need to pass it along to the template with your model (dictionary).

If you are using the Pyramid web framework, you can add this file as middleware. Just drop it into your views folder:

# views/partials_middleware.py
from pyramid.events import subscriber, BeforeRender

import chameleon_partials


@subscriber(BeforeRender)
def add_global(event):
    event['render_partial'] = chameleon_partials.render_partial

For other frameworks using Chameleon (e.g. FastAPI), you can add render_partial to the resulting dictionary. We've built a simple function to keep this fool-proof:

chameleon_partials.extend_model(model)

Here's a typical view method that uses render_partial, notice the use of extending the model before passing it to the template (the example app's views skip this because the middleware above handles it):

@view_config(route_name='listing', renderer='demo_chameleon_partials:templates/home/listing.pt')
def listing(_):
    videos = video_service.all_videos()
    model = dict(videos=videos)
    return chameleon_partials.extend_model(model)

Again: If you are using Pyramid, use the middleware. Otherwise, use the extend_model() method or something similar to the middleware in your framework.

Testing

Partials are plain functions over plain files, so tests need no web framework. Register the test template folder in a fixture and reset the registration flag afterwards, because a second register_extensions() call is a no-op by default:

@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


def test_render_with_data(registered_extension):
    html = chameleon_partials.render_partial('render/with_data.pt', name='Sarah', age=32)
    assert '<span>Your name is Sarah and age is 32</span>' in html.html_text

The Testing page covers the error contract: PartialsException is raised for misuse only, and Chameleon's own errors (such as the ValueError for a missing template) pass through unchanged.

Troubleshooting

Every error you can hit in normal use, with its cause and fix, is on the Troubleshooting page: rendering before registration, Template not found, NameError: render_partial, markup that shows up escaped, and template edits that do not appear.

Metadata

Release files for chameleon_partials 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for chameleon_partials 0.2.2
File Size Uploaded Provenance Actions
chameleon_partials-0.2.2.tar.gz 13.0 kB Without provenance Details for chameleon_partials-0.2.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for chameleon_partials 0.2.2
File Interpreter ABI Platform Actions
chameleon_partials-0.2.2-py3-none-any.whl Python 3 none any Details for chameleon_partials-0.2.2-py3-none-any.whl

Total release size: 23.8 kB

Release files / chameleon_partials-0.2.2.tar.gz

Download URL chameleon_partials-0.2.2.tar.gz
Size 13.0 kB
Tags Source
SHA-256 checksum
How to use checksums
2132219b6e206e1d9b13475f6a7baa72090f20144b0e68e17c44c8be0cab535f
BLAKE2b-256 checksum
How to use checksums
0a3cec5c0d7fad029d3e532d2e32d094e66b21f9a0fb1b41d701289c14af6475
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / chameleon_partials-0.2.2-py3-none-any.whl

Download URL chameleon_partials-0.2.2-py3-none-any.whl
Size 10.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
89440607927da638bbdf5533185b02cbcbc65260ee34d11175b82daed031af4d
BLAKE2b-256 checksum
How to use checksums
b309d106069d27509342ed36a61f48e982c54132587c70ff181e5a74e6d106ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for chameleon_partials 0.2.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page