Migration guide¶
0.12 → 0.13¶
setup() keyword load_context_factory → context_factory¶
The setup() keyword load_context_factory is renamed context_factory:
# BEFORE (0.12)
setup(app, load_context_factory=lambda: AppContext(db=...))
# AFTER (0.13)
setup(app, context_factory=lambda: AppContext(db=...))
The old name is silently ignored (absorbed by **kwargs) — no error is
raised, so your context factory simply stops being installed. Update every call
site.
Non-reactive renders now fan out OOB swaps¶
Any component's .render() now appends out-of-band swaps for dirtied mounted
reactive regions when a client backend is active and mutations occurred — not
only ReactiveComponent.render(). A command-result view returned from a mutating
route now updates mounted read-models with no wrapper:
@app.post("/generate")
def generate():
report = controller.generate() # @mutates dirties "reports", "quota"
return ReportSummary(report=report).render() # non-reactive; counters fan out OOB
Fan-out happens once per request scope and never double-swaps a region already
present in the response body. For a response that renders no component (a raw
string, a 204), use from pyjinhx.reactive import reactive_response.
0.11 → 0.12 (breaking: PJX prefix on all builtins)¶
Every builtin component is renamed with a PJX prefix, in Python and in tag form:
# BEFORE (0.11)
from pyjinhx.builtins import Avatar, Modal
html = renderer.render('<Modal id="m" title="Hi"/>')
# AFTER (0.12)
from pyjinhx.builtins import PJXAvatar, PJXModal
html = renderer.render('<PJXModal id="m" title="Hi"/>')
Related renames, all mechanical:
- Builtin CSS classes:
px-*→pjx-*(e.g.px-modal__inner→pjx-modal__inner). Update any custom CSS targeting builtin classes. - The browser API namespace:
window.px→window.pjx(px.modal.open(...)→pjx.modal.open(...)), and DOM eventspx:*→pjx:*(e.g.px:toast→pjx:toast). - Auto-generated component ids:
px-<n>→pjx-<n>. - Template auto-discovery is now acronym-aware:
HTMLBlockresolves tohtml_block.html(previouslyh_t_m_l_block.html). Rename template files for your own components whose class names contain consecutive capitals. - Single-capital tags (e.g.
<X/>) are no longer parsed as components.
Your own component names no longer risk colliding with builtins — Avatar, Card, Modal, etc. are free for application code.
0.8 → 0.9 (breaking: react= class keyword + strict @mutates)¶
reacts_to → react= class keyword¶
The reacts_to: ClassVar[set[str]] attribute is removed. Declare state keys as a class keyword instead:
# BEFORE (0.8)
from typing import ClassVar
from pyjinhx import ReactiveComponent, MutationKey
class Keys(MutationKey):
TODOS = "todos"
class Counter(ReactiveComponent):
remaining: int
reacts_to: ClassVar[set[str]] = {Keys.TODOS}
@classmethod
def load(cls) -> "Counter":
return cls(remaining=db.remaining())
# AFTER (0.9)
from pyjinhx import ReactiveComponent, MutationKey
class Keys(MutationKey):
TODOS = "todos"
class Counter(ReactiveComponent, react={Keys.TODOS}):
remaining: int
@classmethod
def load(cls) -> "Counter":
return cls(remaining=db.remaining())
Using the old reacts_to attribute raises at class-definition time:
TypeError: Counter: reacts_to was replaced by the react class keyword:
class Counter(ReactiveComponent, react={...})
Strict MutationKey for both react= and @mutates¶
Both react= and @mutates now only accept MutationKey members. Bare strings raise TypeError:
# Raises at class-definition time:
class Bad(ReactiveComponent, react={"todos"}): # bare string
...
# TypeError: Bad: react only accepts MutationKey members; got 'todos'
# Raises at decoration time:
@mutates("todos") # bare string
def save(): ...
# TypeError: @mutates only accepts MutationKey members; got 'todos'
Fix: define a MutationKey subclass and use its members everywhere.
Inheritance¶
A subclass without react= inherits the parent's keys through the MRO. Re-declaring react= on a subclass replaces the parent's set (no union).
Migrating from 0.4.x to the latest¶
Audience: humans and AI coding agents upgrading a codebase built against the old (
~0.4.2) render-only pyjinhx to the current release. It tells you what still works untouched, the handful of things that must change, and how to adopt the new reactive layer that did not exist in 0.4.x.
The one-paragraph mental model¶
pyjinhx 0.4.x was a render-only library: you defined BaseComponent subclasses
(Pydantic models with an adjacent Jinja template) and called .render(). Reactivity —
re-rendering parts of a page when state changed — was something you wired by hand:
route handlers built hx-swap-oob="..." HTML strings (render_*_oob() functions) and
returned them. The current release keeps all of that working and adds an opt-in
reactive layer (ReactiveComponent + @mutates + setup()) that does the OOB fan-out
for you. So migration is two separable jobs:
- Mechanical fixes — a small, fixed list of moved/renamed/changed APIs (below). Do these and your existing app runs on the latest release unchanged in behavior.
- Optional adoption — replace hand-written OOB string-building with
ReactiveComponentwhere you want declarative reactivity. Incremental; do it one region at a time.
Does my existing code still work? (compatibility matrix)¶
| Area | Status | Action |
|---|---|---|
from pyjinhx import BaseComponent + Pydantic fields + adjacent *.html |
✅ Unchanged | None |
component.render() → Markup, and {{ component }} in templates |
✅ Unchanged | None |
_update_context_(self, context, field_name, field_value, *, renderer, session) hook |
✅ Unchanged | None |
from pyjinhx.renderer import Renderer, RenderSession |
✅ Still re-exported | None |
Registry, Registry.request_scope() request isolation middleware |
✅ Unchanged | None |
from pyjinhx.builtins import Card, Tooltip, Panel, PanelTrigger, Notification |
⚠️ Renamed in 0.12 | Add the PJX prefix: PJXCard, PJXTooltip, … (see 0.11 → 0.12 above) |
from pyjinhx.parser import Parser (and Tag) |
⚠️ Moved | Import from pyjinhx.tags |
Renderer.get_default_renderer(inline_css=...) |
⚠️ Changed | Use css_mode=AssetMode.… |
Renderer.set_default_environment("some_package") |
⚠️ Behavior change | Pass an explicit path/Environment |
Reactivity (react={...}, @mutates, OOB fan-out, setup()) |
🆕 New since 0.4.x | Opt in (see below) |
Pre-0.7 reactive names (StateKey, PyJinhxSettings, LoadContext, PjxLoad, client_script) |
⚠️ Renamed/removed | See cheat sheet — only if you used them |
Step 1 — Mechanical fixes¶
These are the only changes required to keep a 0.4.x app running on the latest release.
1a. pyjinhx.parser → pyjinhx.tags¶
The internal HTML/PascalCase-tag parser moved. Parser and Tag now live in
pyjinhx.tags; the class API (including the handle_decl hook that 0.4.x apps sometimes
monkey-patched to preserve <!DOCTYPE>) is unchanged.
If you monkey-patched Parser.handle_decl for <!DOCTYPE> preservation, just re-point it
at pyjinhx.tags.Parser — the method still exists.
1b. inline_css= → asset modes¶
Renderer.get_default_renderer() no longer takes inline_css. Assets are now governed by
AssetMode (INLINE, REFERENCE, NONE) per asset kind.
from pyjinhx import AssetMode, Renderer
# OLD: inline_css=False meant "emit <link> tags instead of inlining CSS"
renderer = Renderer.get_default_renderer(inline_css=False)
# NEW: choose a CSS mode explicitly
renderer = Renderer.get_default_renderer(css_mode=AssetMode.REFERENCE) # <link>/<script src>
# or AssetMode.NONE to skip emitting CSS entirely
# (the old default, inline_css=True, is AssetMode.INLINE)
Process-wide defaults moved to dedicated setters:
Renderer.set_default_css_mode(AssetMode.REFERENCE)
Renderer.set_default_js_mode(AssetMode.INLINE)
Renderer.set_default_runtime_url("/static/pjx.js") # used by REFERENCE mode
1c. set_default_environment is now path-based for strings¶
set_default_environment accepts an Environment, a filesystem path, or None. A bare
string is now treated as a filesystem path (FileSystemLoader(os.fspath(value))), not a
Python package name. If you relied on package-name resolution, pass an explicit directory:
from pathlib import Path
from pyjinhx import Renderer
# Safe across versions: hand it a concrete directory (or a prebuilt Environment)
Renderer.set_default_environment(Path(__file__).parent)
1d. Pre-0.7 reactive renames (only if you touched them)¶
0.4.x predates reactivity, so most 0.4.x apps skip this. If your code passed through an
intermediate 0.5–0.7 reactive API, apply these renames:
| Old | New |
|---|---|
StateKey |
MutationKey |
PyJinhxSettings |
PjxSettings |
LoadContext |
PjxContext |
PjxLoad |
PjxKey |
client_script() |
no longer top-level — from pyjinhx.client import client_script |
Also note the public surface was curated from ~45 down to ~11 symbols. Advanced/internal
symbols are no longer top-level — import them from their submodule
(e.g. from pyjinhx.cache import LoadCache, from pyjinhx.tags import Parser). The 11
top-level exports are: BaseComponent, ReactiveComponent, Renderer, setup,
Registry, mutates, MutationKey, PjxKey, PjxContext, PjxSettings, AssetMode.
Step 2 — Wire setup() (prerequisite for reactivity)¶
Reactivity needs the client runtime, request scoping, and (optionally) a cross-worker
invalidation backend. One call wires all of it into a FastAPI/Starlette app. This
replaces a hand-rolled Registry.request_scope() middleware — setup() installs request
scoping for you.
from pathlib import Path
from fastapi import FastAPI
from pyjinhx import setup, PjxSettings, Renderer
Renderer.set_default_environment(Path(__file__).parent) # template root
app = FastAPI()
setup(
app,
settings=PjxSettings.from_env(), # REDIS_URL / PJX_INVALIDATION_DB / PJX_REACTIVE_DEV
# Inject per-request data reachable inside reactive load():
context_factory=lambda request: AppLoadContext(db=get_db(request)),
)
Cache scope is derived from the settings: with no invalidation backend you get
per-request caching (safe for any number of workers); configuring a RedisInvalidationBackend
(multi-host) or SqliteInvalidationBackend (single-host, zero-infra) switches to per-worker
process caching with cross-worker invalidation fan-out.
If you are not adopting reactivity yet, you can keep your existing
Registry.request_scope() middleware and skip setup() entirely — render-only usage is
unaffected.
Step 3 — Replace manual OOB with ReactiveComponent¶
This is the heart of the upgrade. In 0.4.x you refreshed dependent regions by building OOB HTML by hand in the route:
# BEFORE (0.4.x): the route knows every dependent region and hand-builds its OOB swap
def render_member_count_oob(*, members_count: int, members_total: int) -> str:
return (
f'<span id="members-counter" hx-swap-oob="outerHTML:#members-counter">'
f'{members_count} of {members_total}</span>'
f'<span id="nav-members-badge" hx-swap-oob="outerHTML:#nav-members-badge">'
f'{members_total}</span>'
)
@app.post("/orgs/{slug}/members/{mid}/remove")
def remove_member(slug: str, mid: str):
org.remove_member(slug, mid)
# You must remember to refresh the counter AND the badge AND the subtitle…
return render_member_row_removed(mid) + render_member_count_oob(
members_count=org.active_count(slug), members_total=org.total(slug)
)
In the current release, each region declares what state it derives from and how to rebuild itself, and the framework computes and emits the OOB swaps:
# AFTER (latest): declare dependencies once; OOB fan-out is automatic
from pyjinhx import ReactiveComponent, MutationKey, mutates
class Keys(MutationKey):
MEMBERS = "members"
# 1. The store marks which state it dirties
@mutates(Keys.MEMBERS)
def remove_member(slug: str, mid: str) -> None:
...
# 2. Each region rebuilds itself from the current world and lists its triggers
class MembersCounter(ReactiveComponent, react={Keys.MEMBERS}):
count: int = 0
total: int = 0
@classmethod
def load(cls) -> "MembersCounter":
return cls(count=org.active_count(), total=org.total())
class NavMembersBadge(ReactiveComponent, react={Keys.MEMBERS}):
total: int = 0
@classmethod
def load(cls) -> "NavMembersBadge":
return cls(total=org.total())
# 3. The route renders only the primary; dependents swap themselves
@app.post("/orgs/{slug}/members/{mid}/remove")
def remove_member_route(slug: str, mid: str):
remove_member(slug, mid) # @mutates records Keys.MEMBERS as dirtied
return MembersCounter.render() # framework reloads every mounted region whose
# react keys ∩ {MEMBERS} ≠ ∅, hashes them, and
# appends an hx-swap-oob fragment for each *changed* one
What you delete: the bespoke render_*_oob() string builders and the route's burden of
remembering every dependent. What you gain: a region added later that reacts to MEMBERS
updates automatically, with no route change, and unchanged regions are skipped via state
hashing.
Migration recipe per region:
- Identify a piece of state and give it a
MutationKeymember. - Decorate the store function that changes it with
@mutates(Keys.THAT_KEY). - Turn the region's
render_*()function into aReactiveComponentwith aclassmethod load()that rebuilds it from the current world, and areact={...}class keyword listing theMutationKeymembers it depends on. - For a multi-instance region (a row keyed by id), mark the key field
Annotated[int, PjxKey()]and giveload(cls, key)that one parameter. - Render the primary with
Cls.render(...); delete the manual OOB plumbing.
See Reactivity for the full model (state hashing, nested-region
deduplication, keyed instances) and Configuration for setup()
and invalidation backends.
Quick cheat sheet¶
# Imports that move / change
from pyjinhx.parser import Parser → from pyjinhx.tags import Parser, Tag
Renderer.get_default_renderer(inline_css=False)
→ Renderer.get_default_renderer(css_mode=AssetMode.REFERENCE)
Renderer.set_default_environment("pkg") → Renderer.set_default_environment(Path(".../templates"))
# Pre-0.7 reactive renames (skip if you never used them)
StateKey → MutationKey
PyJinhxSettings → PjxSettings
LoadContext → PjxContext
PjxLoad → PjxKey
client_script() → no longer top-level — from pyjinhx.client import client_script
# Advanced symbols are no longer top-level — import from submodules
from pyjinhx.cache import LoadCache, CacheScope
from pyjinhx.tags import Parser, Tag
What deliberately did not change¶
To keep migration cheap, these 0.4.x idioms are untouched:
BaseComponentauthoring: Pydantic models + adjacentname.htmltemplate, auto-registered..render()returningMarkup, and{{ component }}rendering in templates.- The
_update_context_(self, context, field_name, field_value, *, renderer, session)context-injection hook andRenderSession. Registry.request_scope()for per-request component isolation.pyjinhx.builtinscomponents — though as of 0.12 they carry aPJXprefix (PJXCard,PJXTooltip,PJXPanel, …).- Child components as rendered strings,
id-based addressing, and manualhx-swap-oobstrings — still valid if you are not ready to adoptReactiveComponentfor a given region.
You can migrate the mechanical fixes today and adopt reactivity region-by-region later; the two layers coexist.