fix(docs): document JinjaX _attrs= for attrs passthrough (#78) - #81
Conversation
JinjaX reserves the prop name `attrs` for its own extra-kwargs
collector and unconditionally overwrites whatever a component's
`{#def}` declares for it. docs/primitives.md documented
`:attrs="{...}"` for JinjaX usage, which compiles and runs but
silently discards the dict under a real Catalog — no error, no
passthrough. `_attrs=`/`__attrs=` is JinjaX's own escape hatch for
this collision and is what actually works; docs now show that instead.
Added tests/integration/test_jinja_attrs_passthrough.py against a real
Catalog (the unit tier's Jinja2-Environment-only tests never exercise
this: without a real Catalog, {#def} is a plain comment and `attrs`
never collides with anything). Investigating this surfaced a second,
narrower gap worth pinning down rather than leaving implicit: the
RESERVED_ATTRS collision guard itself only reliably fires when the
colliding key is *not* also one of the component's own declared prop
names. For a key that is both (`type`/`href` for Button; `name` for
the form controls), JinjaX's arg-filtering routes it straight into
that prop before render_attrs ever runs, so PrimitiveConfigError never
fires — the caller's real prop, if also passed, silently wins instead.
Documented as a JinjaX-level limitation, not a cf-ui bug to patch, and
locked in with a test so a future JinjaX version change would be
caught rather than assumed away.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs
…l case /review on PR #81 found two issues: - docs/primitives.md still cited closed issues #71/#72 as pending follow-up for rolling attrs passthrough out past Button, contradicting this same PR's own new text ("name for the form controls") two sentences earlier — the rollout already shipped via #76/#77. Corrected the claim to name what actually ships today. - The declared-prop-name collision-bypass gap was only pinned by a Button test, but both the docs and this test file's docstring claim it for "the form controls" generally. Added a FormField test (name= collision) so a future JinjaX change affecting a different prop signature would be caught, not silently assumed away. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs
/review findings2 findings, both fixed in baa48eb:
All gates re-verified: |
attrs passthrough on the interactive set — Button, Select, Textarea, FormField, Icon, Badge, Box, CheckboxGroup (#70, #72, #73, #76, #77, #82) — plus the two coverage-gap fixes found along the way (#78/#81 JinjaX docs, #79/#80 cotton integration compiler). Fully additive: attrs defaults to {} everywhere, so every existing call site renders byte-identically. #70 stays open for the remaining components. Verified before tagging: 3186 passed / 13 skipped (unit), 40 passed (integration), 236 passed / 33 skipped (E2E chromium, both js modes), ruff check/format clean, djlint clean on both template trees (126 cotton + 105 jinja, 0 errors), mkdocs build --strict clean, and the release workflow's own wheel guard run locally against `uv build` — 237 shipped templates and assets, all present, twine check passed on both sdist and wheel.
Summary
docs/primitives.mddocumented<Cf:Button :attrs="{...}">for JinjaX usage. JinjaX reserves the prop nameattrsfor its own extra-kwargs collector and unconditionally overwrites whatever a component's{#def}declares for it — that syntax compiles and runs but silently discards the dict under a realCatalog, with no error. Fixed the docs to show:_attrs="{...}"instead, JinjaX's own escape hatch for this exact collision, and explained why in a callout.tests/integration/test_jinja_attrs_passthrough.py, against a realjinjax.Catalog(mirroringtest_jinja_autoescape.py's pattern). The existing unit-tier attrs tests never caught this bug: without a realCatalog,{#def}is a plain Jinja2 comment, soattrsnever actually collides with anything there — only a realCatalogreproduces it, which is why these live at the integration tier.RESERVED_ATTRScollision guard (PrimitiveConfigError) only reliably fires when the colliding key is not also one of the component's own declared prop names. For a key that's both (type/hrefforButton;namefor the form controls), JinjaX's own arg-filtering routes it straight into that prop beforerender_attrsever runs — the guard never fires, and the caller's real prop (if also passed) silently wins instead. This is a JinjaX-level limitation, not a cf-ui bug to patch around; documented explicitly in both the docs and the test module's docstring, and locked in with a test so a future JinjaX version change would be caught rather than assumed away.Test plan
pytest tests/integration/test_jinja_attrs_passthrough.py -v— 6/6 pass: bare kwargs passthrough,_attrs=dict passthrough,attrs=silently discarded (pins the bug), reserved-undeclared-name collision still raises, reserved-declared-name collision silently bypasses the guard (documented limitation), hostile value still escapedpytest tests/unit -q— 2826 passed, 13 skippedpytest tests/integration -q— 31 passed (25 existing + 6 new)mkdocs build --strict— cleanruff check/ruff format --check— clean🤖 Generated with Claude Code
https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs