Skip to main content

Adds integration of the Chameleon template language to Robyn.

Project description

chameleon-robyn

Note: This project is very much in the alpha and early stages of development. Feel free to use it, but no promises on API stability for a few versions.

Use Chameleon page templates in your Robyn web applications. If you've used Chameleon with Pyramid, Flask, or FastAPI, you'll feel right at home - same .pt templates, same TAL/TALES/METAL expressions, now running on Robyn's blazing-fast Rust runtime.

Installation

uv pip install chameleon-robyn

This pulls in chameleon and robyn as dependencies.

Quick start

1. Set up your project structure:

my_app/
├── app.py
└── templates/
    └── home/
        └── index.pt

2. Create a template (templates/home/index.pt):

<!DOCTYPE html>
<html>
<body>
    <h1>Hello, ${name}!</h1>
    <ul>
        <li tal:repeat="item items">${item}</li>
    </ul>
</body>
</html>

3. Wire it up (app.py):

from pathlib import Path
from robyn import Robyn
import chameleon_robyn

app = Robyn(__file__)

# Point Chameleon at your templates folder (do this once at startup)
template_folder = (Path(__file__).resolve().parent / 'templates').as_posix()
chameleon_robyn.global_init(template_folder, auto_reload=True)

@app.get('/')
@chameleon_robyn.template('home/index.pt')
async def index(request):
    return {'name': 'World', 'items': ['Robyn', 'Chameleon', 'Python']}

app.start(host='127.0.0.1', port=8000)

That's it. Your handler returns a dict, the @template decorator renders it through the Chameleon template and wraps the result in a Robyn Response.

The @template decorator

The decorator is the main way you'll use this package. It intercepts your handler's return value and renders it through a Chameleon template.

Explicit template path

@app.get('/episodes')
@chameleon_robyn.template('episodes/list.pt')
def episode_list(request):
    return {'episodes': get_all_episodes()}

Auto-naming (convention over configuration)

If you omit the template path, it's derived from the module and function name. A function called index in a module called home_views looks for home_views/index.pt (falling back to home_views/index.html):

# Looks for templates/home_views/index.pt
@app.get('/')
@chameleon_robyn.template()
def index(request):
    return {'message': 'Hello!'}

You can even skip the parentheses:

@app.get('/')
@chameleon_robyn.template
def index(request):
    return {'message': 'Hello!'}

Async handlers - just works

@app.get('/dashboard')
@chameleon_robyn.template('dashboard.pt')
async def dashboard(request):
    stats = await fetch_stats_from_db()
    return {'stats': stats}

Custom status codes and content types

# Return a 201 after creating a resource
@app.post('/items')
@chameleon_robyn.template('items/created.pt', status_code=201)
async def create_item(request):
    item = await save_item(request.json())
    return {'item': item}

# Serve an XML feed
@app.get('/feed.xml')
@chameleon_robyn.template('feed.xml', content_type='application/xml')
def xml_feed(request):
    return {'episodes': get_recent_episodes()}

Response pass-through (redirects, errors, etc.)

If your handler returns a Robyn Response object directly, the decorator passes it through untouched. This is how you handle redirects, custom error responses, or anything that shouldn't be rendered through a template:

from robyn import Response, Headers

@app.get('/old-page')
@chameleon_robyn.template('home/index.pt')
def old_page(request):
    # This Response is returned as-is - no template rendering
    return Response(
        status_code=302,
        description='',
        headers=Headers({'Location': '/new-page'}),
    )

Friendly 404 pages

Call not_found() from any decorated handler to render a 404 page:

@app.get('/episodes/:episode_id')
@chameleon_robyn.template('episodes/detail.pt')
async def episode_detail(request, episode_id: int):
    episode = await get_episode(episode_id)
    if not episode:
        chameleon_robyn.not_found()  # Renders errors/404.pt with status 404

    return {'episode': episode}

By default it renders errors/404.pt, but you can specify any template:

chameleon_robyn.not_found(four04template_file='errors/custom_404.pt')

Lower-level API

Sometimes you need to render a template outside of a decorator - in middleware, error handlers, or helper functions.

render() - get an HTML string

html = chameleon_robyn.render('emails/welcome.pt', username='Michael')
# Returns a string, no Response wrapping

response() - get a Robyn Response

resp = chameleon_robyn.response('errors/500.pt', status_code=500, error=str(e))
# Returns a fully-formed Robyn Response object

ChameleonTemplate class (Robyn's TemplateInterface)

If you prefer Robyn's built-in template pattern, ChameleonTemplate implements TemplateInterface - the same abstract base class that Robyn's JinjaTemplate uses:

from chameleon_robyn import ChameleonTemplate

chameleon = ChameleonTemplate('templates/', auto_reload=True)

@app.get('/page')
def page(request):
    return chameleon.render_template('page.pt', title='Hello')

This is a standalone class with its own template loader - it doesn't require global_init().

global_init() reference

chameleon_robyn.global_init(
    template_folder,              # Path to your templates directory (required)
    auto_reload=False,            # Watch for template changes - use True in development
    cache_init=True,              # Skip re-initialization if already called
    restricted_namespace=True,    # Set to False if using Alpine.js, htmx @attributes, etc.
)

Alpine.js / htmx compatibility

Chameleon's default namespace rules reject attributes like @click or :class because they look like XML namespace prefixes. If you're using Alpine.js, htmx, or similar libraries, set restricted_namespace=False:

chameleon_robyn.global_init(template_folder, restricted_namespace=False)

This tells Chameleon to allow any attribute syntax in your templates.

Works with chameleon-partials

chameleon-partials provides render_partial() for rendering sub-templates (think: reusable components). It works alongside chameleon-robyn with no extra configuration - just initialize both at startup:

import chameleon_partials
import chameleon_robyn

chameleon_robyn.global_init(template_folder, auto_reload=dev_mode)
chameleon_partials.register_extensions(template_folder, auto_reload=dev_mode)

Then pass render_partial through your template context:

import chameleon_partials

@app.get('/')
@chameleon_robyn.template('home/index.pt')
def index(request):
    return {
        'render_partial': chameleon_partials.render_partial,
        'episodes': get_episodes(),
    }

And use it in your templates with tal:replace="structure ...":

<div tal:repeat="ep episodes">
    <div tal:replace="structure render_partial('shared/episode_card.pt', ep=ep)" />
</div>

Note: use tal:replace="structure ..." (not ${structure ...}) when rendering partials. The structure keyword tells Chameleon to insert the HTML without escaping, and tal:replace swaps out the placeholder element entirely.

Example apps

The repo includes two example apps you can run to see everything in action.

Setup

Clone the repo and install with the extras you need:

git clone https://github.com/mikeckennedy/chameleon-robyn.git
cd chameleon-robyn
python -m venv venv
source venv/bin/activate      # Windows: venv\Scripts\activate

# Install the package with example dependencies
pip install -e ".[examples]"

example/ - Core features

A Robyn app demonstrating the @template decorator, sync and async handlers, METAL macro layout inheritance, response pass-through redirects, friendly 404 pages, search with query parameters, and an XML feed with a custom content type.

python example/app.py
# Open http://127.0.0.1:5555

example-partials/ - With chameleon-partials

The same app, but refactored to use chameleon-partials for reusable template components. The episode card markup lives in a single shared/episode_card.pt partial that's shared across the episodes list and search results pages.

python example-partials/app.py
# Open http://127.0.0.1:5555

Compare the two to see how partials reduce template duplication - the episode list and search results templates go from inline card markup to a single tal:replace call each.

Template language cheat sheet

Chameleon uses TAL (Template Attribute Language) - a few patterns to get you going:

<!-- Variable substitution -->
<h1>${title}</h1>
<p>${user.name}</p>

<!-- HTML-safe output (skip auto-escaping) -->
<div tal:replace="structure rich_html_content" />

<!-- Conditionals -->
<div tal:condition="show_banner">Special offer!</div>
<div tal:condition="not items">Nothing here yet.</div>

<!-- Loops -->
<li tal:repeat="item items">${item.name} - $${item.price}</li>

<!-- Repeat with index -->
<tr tal:repeat="row data" class="${'odd' if repeat.row.odd else 'even'}">
    <td>${repeat.row.number}. ${row.title}</td>
</tr>

<!-- Attributes -->
<a href="/items/${item.id}" class="${'active' if is_current else ''}">
    ${item.name}
</a>

<!-- Layout inheritance with METAL macros -->
<!-- layout.pt -->
<html>
<body>
    <main metal:define-slot="content">Default content</main>
</body>
</html>

<!-- page.pt -->
<metal:block use-macro="load: shared/layout.pt">
<div metal:fill-slot="content">
    <h1>My page content here</h1>
</div>
</metal:block>

Full docs: chameleon.readthedocs.io

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chameleon_robyn-0.1.0.tar.gz (9.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chameleon_robyn-0.1.0-py3-none-any.whl (8.7 kB view details)

Uploaded Python 3

File details

Details for the file chameleon_robyn-0.1.0.tar.gz.

File metadata

  • Download URL: chameleon_robyn-0.1.0.tar.gz
  • Upload date:
  • Size: 9.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","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}

File hashes

Hashes for chameleon_robyn-0.1.0.tar.gz
Algorithm Hash digest
SHA256 341fb78c60874a81fa0bc3e81efb4f13dfab7d7f1e5640742f548b9ecfb3f94f
MD5 19acd4da1459d1d873e75be28914650a
BLAKE2b-256 0579e9fc56e0877a524aa87404cf0b1a784ca51a73204c8fc41386bbe9d9176e

See more details on using hashes here.

File details

Details for the file chameleon_robyn-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: chameleon_robyn-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 8.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","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}

File hashes

Hashes for chameleon_robyn-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b8516c4a000b66555b13a617758d13c4ed0873be4f4626cb8126c965a094b974
MD5 638e881342f8d1876dd6d3c021719ced
BLAKE2b-256 34825f211cb84a99380cbaf4e160d9cc8ba2648221d3d125c69f90542af3178c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page