Skip to main content

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 .js files 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 BaseComponent is used as long as the template file can be found.

JavaScript & extra assets

  • Component-local JS: if a component class MyWidget has a sibling file my-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.htmlextra_content.html wrapper). Missing files raise FileNotFoundError.

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.

Reactivity (dependency-aware OOB swaps)

Declare a component's state dependencies once and let mutation routes re-emit exactly the mounted regions that depend on what changed:

from typing import ClassVar
from pyjinhx import ReactiveComponent

class Counter(ReactiveComponent):
    remaining: int
    reacts_to: ClassVar[set[str]] = {"todos"}

    @classmethod
    def load(cls) -> "Counter":
        return cls(id="counter", remaining=db.remaining())

@app.post("/todos/toggle")
def toggle(request):
    db.toggle_all()
    # render() loads the Counter itself — you never call load(). dirtied defaults
    # to the primary's own reacts_to; pass dirtied={...} to dirty more.
    return Counter.render(dirtied={"todos"}, mounted=request)

Instance-keyed components declare instance-tier stems as bare strings ({"todo"} expands to "todo:<key>"). All framework keys (reacts_to, dirtied, and instance keys passed to load() / render()) accept strings or enums and normalize to str (enums via .value).

See the reactivity guide for details.

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

  • The builtins gallery (tests/integration/app.py) exposes GET /sse/panel-demo: a slow HTMX SSE stream wired to the Beta slot of the demo Panel, so you can confirm hidden slots stay mounted and keep receiving events. Coverage lives in tests/integration/test_panel_sse.py (runs a short-lived uvicorn bound to localhost).
  • Test suite imports are stabilized by adding the project root to sys.path in tests/conftest.py. This keeps absolute imports like from pyjinhx import ... and from tests.ui... working across different pytest import 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. Importing pyjinhx.builtins registers those classes with the global registry like any other BaseComponent subclass.
  • Built-in components live under site-packages; Jinja’s FileSystemLoader does not load templates outside the configured root. The renderer therefore falls back to reading adjacent template files from disk for classes in the pyjinhx.builtins package 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. TabGrouptab-group.js, PanelTriggerpanel-trigger.css, Panelpanel.js / panel.css). Using snake_case filenames such as tab_group.js will not be picked up; templates may still resolve via both snake and kebab candidates.
  • Built-in template slots: Wrappers that accept arbitrary inner markup use the field and Jinja name content ({{ content }}) — e.g. Notification, Popover, PanelTrigger. Shell-style blocks use body (Modal, Card, Alert, Drawer). Short static chrome uses label (Badge, Breadcrumb, TabGroup tab titles).

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

pyjinhx-0.5.2.tar.gz (141.1 kB view details)

Uploaded Source

Built Distribution

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

pyjinhx-0.5.2-py3-none-any.whl (70.2 kB view details)

Uploaded Python 3

File details

Details for the file pyjinhx-0.5.2.tar.gz.

File metadata

  • Download URL: pyjinhx-0.5.2.tar.gz
  • Upload date:
  • Size: 141.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for pyjinhx-0.5.2.tar.gz
Algorithm Hash digest
SHA256 d885159086c6dc2e1e88c9aa96d0f82c2995b1974de38388a1549a495943015c
MD5 31434719408715f3570ed8cbed97efeb
BLAKE2b-256 52661c60e893f8ff96b518ecf95f4d6433df1c61007058f450e0ae2ba2b9ce36

See more details on using hashes here.

File details

Details for the file pyjinhx-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: pyjinhx-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 70.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for pyjinhx-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 676f72008345d9aa76dfdb0137003bf68fa5074876a2cdc35336e29b9f493bfe
MD5 6cb4a732dab9934c45fb983093f77654
BLAKE2b-256 fac62ec42ad340f19c367164673c41ffcd04652d1694836e61f911cbcc8acf0d

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