v0.7.0
Citry 0.7.0 catches more mistakes. A component input of the wrong type now raises TypeError when the component renders, and citry check and the editor report many HTML, Vue, and Events mistakes they used to miss. Pages that repeat a simple = "vue" component render much faster on standard CPython. Vue scoped slots and listener modifiers such as .outside now work in templates. Citry supports Python 3.15 and needs 3.11 or newer.
Upgrading from 0.6.x
- Upgrade the packages together: install
citry-ui0.4.0 andcitry-lsp0.3.0 with Citry 0.7.0. Citry UI 0.3.0 rejects some valid inputs under the new type check and fails to render under the new$c-trrule, and citry-lsp 0.2.0 cannot read your app's lint data oncesecurity_cspis set. citry-lsp 0.3.0 runs over stdio only. The VS Code extension 0.3.0 accepts citry-lsp 0.3.0. It also stays off, highlighting included, in VS Code's Restricted Mode until you trust the workspace. - Python 3.11 or newer: raise
requires-pythonto">=3.11"before upgrading. - Inputs of the wrong type raise
TypeError: fix the value or widen the annotation. To keep the old behavior, setcheck_input_types = Falsein a component's nestedclass Lint:or inLintSettings. alt=""on a component tag passes"": write a bare attribute, such as<c-Grid compact />, to passTrue.<c-cache enabled="">and<c-i18n client="">now raise.- Deep templates have a limit: a template can nest tags at most 512 levels deep, and at most 256 of those levels can be
c-tags orc-if/c-forattributes. Move a deeper part into its own component. $c-trbindings are written in the template: a binding that comes from Python data, throughc-$c-tr:..., a$c-trkey inc-bind, or anon_attrs_resolvedhook, raisesVueCodeError.- More values are checked before they reach the browser: for example,
actions.Redirectto a scheme other thanhttp:orhttps:, and ac-bindkey such asonclickorinnerHTML, raiseValueError. Write such attributes in the template, and use anhttp:orhttps:URL forRedirect. - HTML fragments on a page with a CSP nonce: serialize them with
csp_nonce=page_nonce, or they do not start. An interactive page also refuses adata:URL for a component script. - Some expressions are now errors:
{{ }}and an expression with an unmatched)raiseSyntaxError. citry checkmay fail on a project that passed before: the new HTML, Vue, and Events rules can fail the check; each has arule_*setting. Withsecurity_cspset, the check also reports inline scripts, styles, andon*attributes.citry check --format jsonranges count from 0, with columns in UTF-16 code units.- Forms send text: with
@c-submit, number and range inputs send"3", not3, so a Pydantic model in strict mode needs a change. - A
<c-fill fallback="...">with a@c-handler or a Vue binding that reads Vue data now raises. dict[K, V]fields in an Eventsdataclass are converted: a handler getsdata.scores[3]for adict[int, ...]field, notdata.scores["3"].- Extension routes that serve JavaScript or CSS need the right
content_type, because every Citry route now sendsnosniff. citry.contrib.django.secret()returns a list whenSECRET_KEY_FALLBACKSis set.ext-previewneeds Playwright 1.64.1 or newer; runplaywright install chromiumagain.- Event request limits: if you set
_max_envelope_bytes, rename it to_max_body_bytes. - Sub-path URLs: if you passed your sub-path to
set_mounted_prefix()in a process that also callsmount()orurlpatterns(), remove it there, or the URLs start with it twice.
The sections below list every change.
Added
- An extension's route can limit how much of a request body Citry reads: pass
URLRoute(max_body_bytes=...)a function that returns the limit in bytes. Citry reads at most one byte past the limit, so the handler must still reject a longer body (details). - You can write an Events
Stateas a Pydantic v2 or v1 model, by hand or throughmodel_base. A handler that sets a field to a value the model rejects fails that call, before Citry stores it (details). - You can make every plain
class Kwargs:,TemplateData,JsData,CssData,State, and Eventsdataclass a Pydantic model that subclasses your ownBaseModelsubclass, so itsmodel_configapplies to all of them: pass it asCitry(model_base=MyModel). A dataclass base ortyping.NamedTupleworks too (details). citry checkand the editor now report a<script>or<style>element, anon*attribute such asonclick, or ajavascript:URL written directly in a template, whensecurity_cspis"warn"or"strict". They are warnings with"warn"and errors with"strict", so a project that uses"strict"can seecitry checkfail after the upgrade (details). If your editor's language server runs an older citry-lsp, update it too: oncesecurity_cspis set, it reports "App worker protocol mismatch" and stops reading your app.- A component can pass Vue data to a fill, as a Vue scoped slot: write
<c-slot name="item" :row="row" />, and read it in the fill with<c-fill name="item" v-slot="{ row }">. Python slot data (c-item,data="...") works beside it (details). When the server knows the slot's values, it writes the fill's HTML with them.citry checkwarns whenv-slot="{ ... }"reads a name the slot does not pass, which Vue would leaveundefined. - Citry now supports Python 3.15.
- You can now use
.outside,.window,.document,.debounce, and.throttleon Vue listeners such as@clickon HTML elements, as in@click.outside="open = false"or@input.debounce.300ms="search()"(details). - A handler declared with
@event(detached=True)can be called with$sendEvent(name, args, {wait: false}), so a read-only call such as a spell check no longer waits behind the app's slower calls. A detached handler may return data,Dispatch,Redirect, URL changes, andDownload, but not a render or a State change (details). - When
template_data()builds its result through a helper, you can list its keys inLint.template_data_keys.citry checkand the editor then report only names outside the list, not every name in the template.citry checkalso warns about a literal key the list leaves out. Rendering ignores the list (details). citry checknow fails on a misspelled directive such as<div c-iff="show">, which Citry would render as aniffattribute; setrule_misspelled_directiveto change the severity.citry check --staticnow uses the severity a component's ownLintclass sets as a plain string, such asrule_alpine_attribute = "ignore". Editor tools can read the same values fromPythonTemplateRegion.owner_lint, a tuple ofComponentSourceLint.citry checkand the editor now report HTML mistakes that the browser passes over without an error: a misspelled attribute, tag, oraria-*name (titel,<dvi>,aria-expandd), a block such as<div>inside a<p>, two elements with the sameid, and an<img>without alt text. The image check is a warning and the others are errors, socitry checkmay now fail on an existing project; each rule has arule_*setting (details). With your application loaded, a component's only top-level<img>is reported when the component setsinheritAttrs: false, because no parent'saltcan reach it then.citry checkand the editor now report Vue mistakes that would stop a component from rendering: an expression that is not valid JavaScript, such asv-if="open &&"(rule_invalid_vue_expression), where the message names the JavaScript parser's error and the editor underlines the place it points to, andv-modelon an element that holds no value, such as<form>(rule_invalid_model_target). A misspelled DOM event in a listener, such as@clik, is a warning (rule_misspelled_event).citry checkand the editor now run Vue's compiler over every template, every<c-if>branch included, and report what it rejects at the line you wrote, such as a<div>directly inside a<table>, a<button>inside a<button>, or av-elsewith nov-ifbefore it, before a component that uses Vue fails to render (rule_vue_compile_error, an error by default) (details).- The misspelled-directive check now also reports a typo in a Vue directive, such as
v-ifforv-elif, which otherwise stops the page with an error in the browser. With your application loaded, a custom directive that the component registers in thedirectivesoption of$component()is not reported. With your application loaded,
citry checknow reports more mistakes that fail only when the page runs (details):- a component that
<c-for>repeats without#c-keyon a page that uses Vue (rule_missing_component_key); - a component that assigns to its own prop, such as
@click="open = false"in its template orthis.open = falsein its JavaScript (rule_prop_assignment), which the editor reports too; @c-*arguments the server would reject: an unknown key, a missing required field, or a literal of the wrong type, such assave({amount: '1'})for anintfield (rule_unknown_server_event_field,rule_missing_server_event_field,rule_incompatible_server_event_field);- a control in a form whose
@c-submitsends the form's fields, when the handler's data class does not declare itsname, such as<input name="qeury">, or no control is named after a required field (rule_unknown_form_field,rule_missing_form_field); - a
var(--name)that misspells acss_data()key, such asvar(--acent), as a warning (rule_misspelled_css_variable).
These are errors by default except the CSS check, so
citry checkmay now fail on an existing project.- a component that
With your application loaded,
citry checknow warns about a link to a page or file your app does not serve, such as<a href="/abuot">or<img src="/static/avtar.svg">, and suggests the path you likely meant (rule_unknown_url). It asks the web framework you mount Citry with throughcitry.contrib.flask.mount(),citry.contrib.fastapi.mount(), orcitry.contrib.django.urlpatterns(), so point--appat the module that mounts Citry (details). Links such as/shop/about, in an app served below a sub-path such as/shop, are checked too:citry checkreads the sub-path from FastAPI'sroot_path, Flask'sAPPLICATION_ROOT, or Django'sFORCE_SCRIPT_NAME.- With your application loaded,
citry checknow warns about aself.inject("bord")whose key no component above it provides, and suggests the key the page does provide, such asboard(rule_unknown_inject_key, details). - If you build editor tooling on
citry.analysis, you can now check a fill'sdataname against the slot'sSlotInput[T]: passslot_data_typestobuild_schema_template_shadow()orbuild_inferred_template_shadow(), keyed by theslot_targetthatTemplatePythonControlnow records. - With your application loaded,
citry checknow warns whenjs_data()sends a module-level value that JSON cannot carry, such as aPurePosixPath, including one nested in a dict or list, behind a conditional orConst(...), or read as an attribute such asconfig.EXPORT_DIR. Citry.inspect_components()now says, for each Events handler, whether an argument key the data class does not declare fails the call (extra_fields) and whether Citry or Pydantic checks the values (validated_by).citry check --staticnow lists, as a note, each template it skips because the component's base class comes from another module of your project.discover_python_templates()anddiscover_python_component_assets()acceptapp_components, the classes a loaded application registers in the file, so a tool can find the templates of components whose base class comes from another module.citry check --typesnow reports a parent method used as a child component's event listener, such as@hide="hideCard", when it cannot accept the values the child's typedemitsdeclares, for example a method that needs a second value the child never sends. The props and events guide shows how to type an event's values so both the child's$emitand the parent's method are checked.- You can now wrap a server-only value, such as an API token, in
citry.Secret, and Citry refuses to send it to the browser: rendering fails with an error naming the component and key whenjs_data(),css_data(), or State holds one,citry checkwarns about one injs_data(), and a template shows it as***. Annotate aKwargsfield asSecret[str]to reject a plain string. A cached component cannot take aSecretinput unless itsCache.vary()leaves the secret out (details). - With your application loaded,
citry checknow warns when a template class misspells one the component's CSS styles, such asclass="bord"for.board; such an element renders unstyled with no error (rule_misspelled_css_class, details). - You can now turn on two stricter CSS checks, off by default:
rule_undeclared_css_classreports every template class that no CSS Citry reads declares, andrule_undeclared_css_variablereports everyvar(--name)without a fallback that nothing sets. Turn them on inLintSettingsor in a component'sLintclass (details). - With
Citry(mode="development"), turning a page into HTML now warns when two of its elements share anid, such as two components that both writeid="board"or one component rendered twice, and names the components (details). - With
Citry(mode="development"), the browser console now warns when Vue's first render of a part it builds in the browser differs from the HTML the server sent, for example acreated()hook that opens a panel on wide windows. A production page does not load the check (details).
Changed
- Breaking: Citry now checks and converts each key and value of a
dict[K, V]field in an Eventsdataclass, as it already did for each item of a list. A key such as"3"fills adict[int, ...]field as the number3, and a value typed as a dataclass arrives as an instance of it. If a handler reads such a field asdata.scores["3"]ordata.points["a"]["x"], use the declared types instead:data.scores[3]anddata.points["a"].x. A Pydanticdataclass already did this. $error(handler).fieldErrorsnow lists at most 20 errors for fields that the call'sdataclass does not declare (orStatefields it may not write), and at most 20 item errors in each list or dict field.messagethen ends with how many more there were, as in "1,000 more field errors are not shown." Errors for other fields, and the fields of anEventErroryou raise, are always listed in full.- Every response from a Citry route now has the standard
X-Content-Type-Options: nosniffheader, so the browser uses the route'sContent-Typeas given. If an extension route of yours serves JavaScript or CSS, give it the rightcontent_type. citry.contrib.django.secret()now includes Django'sSECRET_KEY_FALLBACKS, soCitry(secret=secret())keeps accepting values signed with an old key while you rotateSECRET_KEY. With fallbacks set it returns a list of keys, so its return type is nowstr | list[str]; without fallbacks it still returns the plain string.citry checknow leaves out findings in the components of a library you registered withregister_library(), such as Citry UI, so an app that uses Citry UI no longer fails the check on code it cannot change. One note says how many findings it left out. Pass--include-libraries, or setLintSettings(include_libraries=True), to report them (details).- Breaking: a template can now nest tags at most 512 levels deep, and at most 256 of those levels can be Citry logic: every
c-tag except<c-raw>, and eachc-if=,c-elif=,c-else,c-for=orc-emptyattribute. A deeper template raisesSyntaxErrorat the tag that goes too deep, andcitry checkreports it ascitry.parse.too-deep; move the inner part into its own component. Plain elements nested more than 512 deep used to render. Breaking: a written empty value on a component tag, such as
<c-Avatar alt="" />, now passes""instead ofTrue, so astrinput accepts it. To passTrueto aboolinput, write the bare attribute, as in<c-Grid compact />;compact=""now fails with aTypeErrorthat says so. The same holds for data passed to a fill (<c-slot x="" />gives the fill""), and<c-cache enabled="">and<c-i18n client="">now raise too, with the same advice to write the bare attribute. HTML elements are unchanged, and so is the tag form<c-if cond="">. A component that forwards its inputs to an element withc-bindnow writesalt=""there instead of a barealt, which HTML reads the same way.<c-Avatar alt="" /> <!-- before: True; now "" --> <c-Grid compact="" /> <!-- now "": a bool input rejects it --> <c-Grid compact /> <!-- True, as before -->Output that an
on_renderhook passes to another component, as inreturn Frame(body=result), or that anon_component_renderedhook passes on asctx.render, no longer carries the component's owndata-cid-*anddata-ccss-*markers on its top elements, the same as content passed as a nested template. The other component's root element, which is now the component's root, still carries them, so scoped CSS and[data-cid-*]selectors match that root instead. When both components set the samecss_datavariable, the passed output takes the value from that root.- Breaking: Citry now requires Python 3.11 or newer, because Python 3.10 reached its end of life on October 1, 2026. On Python 3.10, pip keeps installing Citry 0.6.x. If your project declares
requires-python = ">=3.10", raise it to">=3.11"before upgrading, or uv and other lockfile tools cannot pick this release. Breaking: a component input of the wrong type now raises
TypeErrorwhen the component renders, in development and in production. Fix the value or widen the annotation; to keep the old behavior, setcheck_input_types = Falsein a component's nestedclass Lint:, or passCitry(lint=LintSettings(check_input_types=False)). If you use Citry UI, upgrade it at the same time: earlier Citry UI releases reject some valid inputs under this check. A value that equals aLiteralmember, such as aStrEnummember orMarkup("sm"), passes the check. For a component from a registered library such as Citry UI, the error names the library and asks you to change the value you pass.class Card(Component): class Kwargs: title: str card = Card(title=123) str(card) # TypeError: Card.Kwargs.title expects str, got int (123). ...Citry checks only the outer type of each
Kwargs,TemplateData,JsData,CssData, and EventsStatefield, solist[str]accepts any list. A field's own default always passes, sotitle: str = Nonekeeps working, and Pydantic models keep their own rules (details).citry checknow fails, and the editor shows an error, on an HTML attribute value the browser does not accept, such as<button type="sumbit">. Setrule_invalid_attribute_value="warning"inLintSettingsto keep the previous behavior.citry checknow fails on a boolean attribute with a value such asdisabled="false", which still disables the element. Leave the attribute out to turn it off, or let Vue set it from a JavaScript boolean, as in:disabled="saving".citry checknow also reports a listener on a component tag whose lowercase name the child does not declare, such as@hdie, and suggests the declared event it probably means (hide). DOM event names such as@clickstill pass. Setrule_undeclared_component_eventto change the severity, which is a warning by default.citry checkand the editor now report a string for a Boolean Vue prop, such as:open="open ? 'open' : 'closed'", unless the string is empty or the prop's own name. They also check both results of a condition whose results are literals.- The render error for a component that
<c-for>repeats without#c-key, or with a duplicate one, now names the tag, its template and line, and the repeated value, and says to combine the loop values into one key when loops are nested. An element whose#c-keyclashes with another key now names its template, line and column. - A server expression that reads a Vue
v-forvariable, such asc-name="member.name"inside<li v-for="member in members">, now fails with an error that names the tag, its template, and the line, says the variable exists only in the browser, and shows the<c-for>that fixes it, instead of a bareKeyError. It is still aKeyError, so an existingexcept KeyErrorkeeps catching it. - In
citry check --format json, every finding'srangenow counts lines and columns from 0, with columns in UTF-16 code units, as the Language Server Protocol does. Syntax errors, unknown variables, unknown components, and i18n catalog errors used to count from 1; if your tool subtracted 1 from those, stop. Eachrangealso says which text it counts in,"source_kind": "file"or"inline"(a string written on the class, such astemplate), and aJsDatafield'soriginnow names its Python file (details). - The render error for
v-modelon an element that holds no value, such as<div>or<form>, now names each such element with its template and line and lists the elementsv-modelworks on, instead of an internal Vue compiler message. - A form submitted through
@c-submitnow gives the handler the same values as the same form posted without JavaScript: text such as"3"fills anintorfloatfield, a checked checkbox fills aboolfield, and the submit button that sent the form adds itsname. Number and range inputs now send their text, so a handler that readsevent.argsdirectly, or a Pydantic model in strict mode, gets"3"instead of3, as it already did without JavaScript. - The
ext-previewextra now needs Playwright 1.64.1 or newer, whose bundled Node.js does not trust the affected emSign root certificate whenplaywright installdownloads browsers. After upgrading, runplaywright install chromiumagain to fetch the matching browser. - In a component that uses Vue, a slot's fallback that a caller shows through
<c-fill fallback="...">now raises an error when it holds a@c-event handler or a Vue attribute that uses Vue data, such as:title="message"orref. These used to read the caller's Vue data and run the caller's handler of the same name. To fix it, leave the slot unfilled so the receiving component shows its own fallback. Or pass the value with ac-binding such asc-title, which reads the receiving component's Python values. Or put the Vue binding on an element in the caller's fill around{{ fallback }}, where it reads the caller's Vue data. - A template expression with an unmatched
), such as{{ a) or (b }}, now raisesSyntaxError, and so does an empty one such as{{ }}. Citry used to run the first as(a) or (b)and render the second as(). Breaking: write a
$c-trtranslation binding and its values in the template. A binding that comes from Python data, throughc-$c-tr:..., a$c-trkey in ac-bindmapping, or anon_attrs_resolvedhook, now raisesVueCodeError, because its value is browser JavaScript. To let the server decide whether the browser translates an element, return a flag fromjs_data()and make the binding's values expression returnnullwhere the server-rendered text should stay (details):<!-- Before --> <span c-$c-tr:my-app-save="True if use_catalog else None"> <!-- After: useCatalog comes from js_data() --> <span $c-tr:my-app-save="useCatalog ? {} : null">Events State kept on the server (
_storage = "server") now expires after 24 hours unlessState._max_agesets another lifetime, so a page left open longer asks the visitor to reload. A browser write that would make it larger thanState._max_bytes, now 64 KiB by default for server storage, answers 422 before the handler runs; set_max_byteshigher, or toNone, to allow more. The defaultInMemoryCachecounts this State against a 64 MiB budget, apart from other entries, and drops the entry used longest ago when full; setInMemoryCache(state_max_bytes=...)to change that.max_entriesno longer counts these entries (details).- Breaking: Citry now checks more of the values it sends to the browser. Each of these raises an error, a
ValueErrorunless the item says otherwise: actions.Redirect(url)with a scheme other thanhttp:orhttps:. A URL with no scheme, such as/projects/, still works. Use a fullhttps:URL or a URL with no scheme instead. A URL containing a control character, such as a line break, also raises.- A
c-bindmapping key, or a name anon_attrs_resolvedhook adds, that starts withon, such asonclick, or that isinnerHTML,outerHTML, orsrcdoc. The same holds for such a name passed to<c-element>throughc-bind,<c-component c-is>, or Python code. Write the attribute in the template instead:@clickon a Vue page,@c-clickto call a handler of your component, oronclick="...". A hook can still change the value of a handler the template wrote, but it cannot add one. A<c-element>withc-isorc-bindalso refuses a handler written in the template, because it cannot tell a name you wrote from one passed in as data. There, use a Vue@eventin a component withjs, or write a plain element in ac-ifbranch. - An attribute name that your code supplies, such as a
c-bindkey, and that contains whitespace, a control character, a quote, a byte-order mark, a lone surrogate, or a Unicode noncharacter. Rename the key. - A tag such as
script,style,iframe, orxmp, whose contents the browser reads as raw text or code, passed to a<c-element>that hasc-isorc-bind, or to the built-inelementcomponent in a Python call. Write such an element directly in the template. - Markup inside a
<c-element>that renderstextareaortitlein an interactive component raisesTypeError: a tag, a child component,<c-raw>, or aMarkupvalue. Put only text there. - A
css_data()value that has a comment, a brace,<, or a backslash outside a string orurl(); a;or!outside brackets; an unbalanced bracket; or a malformed string orurl(). A null or surrogate character, `