UI components for Python using Pydantic and Jinja2 templates
Project description
PyJinHx
Build reusable, type-safe UI components for template-based web apps in Python. PyJinHx combines Pydantic models with Jinja2 templates to give you template discovery, component composition, and JavaScript bundling.
- Automatic Template Discovery: Place templates next to component files—no manual paths
- JavaScript Bundling: Automatically collects and bundles
.jsfiles from component directories - Composability: Nest components easily—works with single components, lists, and dictionaries
- Flexible: Use Python classes for reusable components, HTML syntax for quick page composition
- Type Safety: Pydantic models provide validation and IDE support
Installation
pip install pyjinhx
Example
This single example shows the full setup (Python classes + templates) and both ways to render:
- Python-side: instantiate a component class and call
.render(). - Template-side: render an HTML-like string with custom tags via
Renderer.
Step 1: Define component classes
# components/ui/button.py
from pyjinhx import BaseComponent
class Button(BaseComponent):
id: str
text: str
variant: str = "default"
# components/ui/card.py
from pyjinhx import BaseComponent
from components.ui.button import Button
class Card(BaseComponent):
id: str
title: str
action_button: Button
# components/ui/page.py
from pyjinhx import BaseComponent
from components.ui.card import Card
class Page(BaseComponent):
id: str
title: str
main_card: Card
Step 2: Create templates (auto-discovered next to the class files)
<!-- components/ui/button.html -->
<button id="{{ id }}" class="btn btn-{{ variant }}">{{ text }}</button>
<!-- components/ui/card.html -->
<div id="{{ id }}" class="card">
<h2>{{ title }}</h2>
<div class="action">{{ action_button }}</div>
</div>
<!-- components/ui/page.html -->
<main id="{{ id }}">
<h1>{{ title }}</h1>
{{ main_card }}
</main>
Step 3A: Python-side rendering (BaseComponent.render())
from components.ui.button import Button
from components.ui.card import Card
from components.ui.page import Page
page = Page(
id="home",
title="Welcome",
main_card=Card(
id="hero",
title="Get Started",
action_button=Button(id="cta", text="Sign up", variant="primary"),
),
)
html = page.render()
Step 3B: Template-side rendering (Renderer.render(source))
from pyjinhx import Renderer
# Set template directory once
Renderer.set_default_environment("./components")
# Use the default renderer with auto_id enabled
html = Renderer.get_default_renderer(auto_id=True).render(
"""
<Page title="Welcome">
<Card title="Get Started">
<Button text="Sign up" variant="primary"/>
</Card>
</Page>
"""
)
Template-side rendering supports:
- Type safety for registered classes: if
Button(BaseComponent)exists, its fields are validated when<Button .../>is instantiated. - Generic tags: if there is no registered class, a generic
BaseComponentis used as long as the template file can be found.
JavaScript & extra assets
- Component-local JS: if a component class
MyWidgethas a sibling filemy-widget.js, it is auto-collected and injected once at the root render level. - Extra JS: pass
js=[...]with file paths; missing files are ignored. - Extra HTML files: pass
html=[...]with file paths; they are rendered and exposed in the template context by filename stem (e.g.extra_content.html→extra_content.htmlwrapper). Missing files raiseFileNotFoundError.
Optional UI components ship in pyjinhx.builtins. Full reference (fields, px- CSS classes, --px-* tokens, template fallback): docs/guide/builtins.md, or the published site under Guide → Built-in UI components.
FastAPI + HTMX example
Component class
# components/ui/button.py
from pyjinhx import BaseComponent
class Button(BaseComponent):
id: str
text: str
Component template (with HTMX)
<!-- components/ui/button.html -->
<button
id="{{ id }}"
hx-post="/clicked"
hx-vals='{"button_id": "{{ id }}"}'
hx-target="#click-result"
hx-swap="innerHTML"
>
{{ text }}
</button>
FastAPI app (two routes)
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from components.ui.button import Button
app = FastAPI()
@app.get("/button", response_class=HTMLResponse)
def button() -> str:
return (
Button(id="save-btn", text="Click me").render()
+ '<div id="click-result"></div>'
)
@app.post("/clicked", response_class=HTMLResponse)
def clicked(button_id: str = "unknown") -> str:
return f"Clicked: {button_id}"
Design decisions
- Test suite imports are stabilized by adding the project root to
sys.pathintests/conftest.py. This keeps absolute imports likefrom pyjinhx import ...andfrom tests.ui...working across differentpytestimport modes and Python runners. - Optional builtins (
pyjinhx.builtins) ship twenty UI components with sibling templates, CSS, and JS where needed; documentation lives in docs/guide/builtins.md. Importingpyjinhx.builtinsregisters those classes with the global registry like any otherBaseComponentsubclass. - Built-in components live under
site-packages; Jinja’sFileSystemLoaderdoes not load templates outside the configured root. The renderer therefore falls back to reading adjacent template files from disk for classes in thepyjinhx.builtinspackage when normal relative lookup fails, so your app’s loader can stay pointed at your own template directory. - Co-located JS/CSS names: Auto-collection looks for
pascal_case_to_kebab_case(ClassName) + ".js"/".css"next to the class (e.g.TabGroup→tab-group.js, same idea asLoadingOverlay→loading-overlay.js). Using snake_case filenames such astab_group.jswill not be picked up; templates may still resolve via both snake and kebab candidates. - Dev gallery: tests/builtins_gallery/ is a FastAPI page that renders every builtin for manual or automated smoke checks. FastAPI, httpx, and uvicorn are listed only under
[dependency-groups] devinpyproject.toml; they are not required to use the library at runtime. Runuv sync --group devthenuv run pytest tests/test_builtins_gallery.py. To browse locally, runuv run python -m tests.builtins_gallery(defaults to http://127.0.0.1:9000;PYJINHX_GALLERY_PORToverrides the port).uv runtakes a command, not a path—uv run tests/builtins_gallery/will fail because a directory is not an executable.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyjinhx-0.4.0.tar.gz.
File metadata
- Download URL: pyjinhx-0.4.0.tar.gz
- Upload date:
- Size: 107.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
565845d1d3b5dadcb1c6aa0031a3dcb687b80635a3861cf1c671d8e8bc16e4df
|
|
| MD5 |
8499b5e0124b6e5b64641397b407703b
|
|
| BLAKE2b-256 |
0a675582a7e007ea57caa637f678c9f44d46d46ab105812f91071cd520669d0e
|
File details
Details for the file pyjinhx-0.4.0-py3-none-any.whl.
File metadata
- Download URL: pyjinhx-0.4.0-py3-none-any.whl
- Upload date:
- Size: 60.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
607686def36cf692e49422ab90373a6515cba442139a5f3a347f86429aca4501
|
|
| MD5 |
a5e94ccf5b110dbcb18514232e634efe
|
|
| BLAKE2b-256 |
11022373d492a6e405be67384484db7e1e713ead9123b205ff78359573937c2d
|