Theme
Version
GitHub PyPI Discord
On this page

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-ui 0.4.0 and citry-lsp 0.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-tr rule, and citry-lsp 0.2.0 cannot read your app's lint data once security_csp is 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-python to ">=3.11" before upgrading.
  • Inputs of the wrong type raise TypeError: fix the value or widen the annotation. To keep the old behavior, set check_input_types = False in a component's nested class Lint: or in LintSettings.
  • alt="" on a component tag passes "": write a bare attribute, such as <c-Grid compact />, to pass True. <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 or c-if/c-for attributes. Move a deeper part into its own component.
  • $c-tr bindings are written in the template: a binding that comes from Python data, through c-$c-tr:..., a $c-tr key in c-bind, or an on_attrs_resolved hook, raises VueCodeError.
  • More values are checked before they reach the browser: for example, actions.Redirect to a scheme other than http: or https:, and a c-bind key such as onclick or innerHTML, raise ValueError. Write such attributes in the template, and use an http: or https: URL for Redirect.
  • 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 a data: URL for a component script.
  • Some expressions are now errors: {{ }} and an expression with an unmatched ) raise SyntaxError.
  • citry check may fail on a project that passed before: the new HTML, Vue, and Events rules can fail the check; each has a rule_* setting. With security_csp set, the check also reports inline scripts, styles, and on* attributes.
  • citry check --format json ranges count from 0, with columns in UTF-16 code units.
  • Forms send text: with @c-submit, number and range inputs send "3", not 3, 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 Events data class are converted: a handler gets data.scores[3] for a dict[int, ...] field, not data.scores["3"].
  • Extension routes that serve JavaScript or CSS need the right content_type, because every Citry route now sends nosniff.
  • citry.contrib.django.secret() returns a list when SECRET_KEY_FALLBACKS is set.
  • ext-preview needs Playwright 1.64.1 or newer; run playwright install chromium again.
  • 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 calls mount() or urlpatterns(), 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 State as a Pydantic v2 or v1 model, by hand or through model_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 Events data class a Pydantic model that subclasses your own BaseModel subclass, so its model_config applies to all of them: pass it as Citry(model_base=MyModel). A dataclass base or typing.NamedTuple works too (details).
  • citry check and the editor now report a <script> or <style> element, an on* attribute such as onclick, or a javascript: URL written directly in a template, when security_csp is "warn" or "strict". They are warnings with "warn" and errors with "strict", so a project that uses "strict" can see citry check fail after the upgrade (details). If your editor's language server runs an older citry-lsp, update it too: once security_csp is 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 check warns when v-slot="{ ... }" reads a name the slot does not pass, which Vue would leave undefined.
  • Citry now supports Python 3.15.
  • You can now use .outside, .window, .document, .debounce, and .throttle on Vue listeners such as @click on 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, and Download, but not a render or a State change (details).
  • When template_data() builds its result through a helper, you can list its keys in Lint.template_data_keys. citry check and the editor then report only names outside the list, not every name in the template. citry check also warns about a literal key the list leaves out. Rendering ignores the list (details).
  • citry check now fails on a misspelled directive such as <div c-iff="show">, which Citry would render as an iff attribute; set rule_misspelled_directive to change the severity.
  • citry check --static now uses the severity a component's own Lint class sets as a plain string, such as rule_alpine_attribute = "ignore". Editor tools can read the same values from PythonTemplateRegion.owner_lint, a tuple of ComponentSourceLint.
  • citry check and the editor now report HTML mistakes that the browser passes over without an error: a misspelled attribute, tag, or aria-* name (titel, <dvi>, aria-expandd), a block such as <div> inside a <p>, two elements with the same id, and an <img> without alt text. The image check is a warning and the others are errors, so citry check may now fail on an existing project; each rule has a rule_* setting (details). With your application loaded, a component's only top-level <img> is reported when the component sets inheritAttrs: false, because no parent's alt can reach it then.
  • citry check and the editor now report Vue mistakes that would stop a component from rendering: an expression that is not valid JavaScript, such as v-if="open &&" (rule_invalid_vue_expression), where the message names the JavaScript parser's error and the editor underlines the place it points to, and v-model on 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 check and 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 a v-else with no v-if before 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-iff or v-elif, which otherwise stops the page with an error in the browser. With your application loaded, a custom directive that the component registers in the directives option of $component() is not reported.
  • With your application loaded, citry check now reports more mistakes that fail only when the page runs (details):

    • a component that <c-for> repeats without #c-key on 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 or this.open = false in 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 as save({amount: '1'}) for an int field (rule_unknown_server_event_field, rule_missing_server_event_field, rule_incompatible_server_event_field);
    • a control in a form whose @c-submit sends the form's fields, when the handler's data class does not declare its name, 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 a css_data() key, such as var(--acent), as a warning (rule_misspelled_css_variable).

    These are errors by default except the CSS check, so citry check may now fail on an existing project.

  • With your application loaded, citry check now 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 through citry.contrib.flask.mount(), citry.contrib.fastapi.mount(), or citry.contrib.django.urlpatterns(), so point --app at 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 check reads the sub-path from FastAPI's root_path, Flask's APPLICATION_ROOT, or Django's FORCE_SCRIPT_NAME.

  • With your application loaded, citry check now warns about a self.inject("bord") whose key no component above it provides, and suggests the key the page does provide, such as board (rule_unknown_inject_key, details).
  • If you build editor tooling on citry.analysis, you can now check a fill's data name against the slot's SlotInput[T]: pass slot_data_types to build_schema_template_shadow() or build_inferred_template_shadow(), keyed by the slot_target that TemplatePythonControl now records.
  • With your application loaded, citry check now warns when js_data() sends a module-level value that JSON cannot carry, such as a PurePosixPath, including one nested in a dict or list, behind a conditional or Const(...), or read as an attribute such as config.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 --static now lists, as a note, each template it skips because the component's base class comes from another module of your project.
  • discover_python_templates() and discover_python_component_assets() accept app_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 --types now 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 typed emits declares, 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 $emit and 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 when js_data(), css_data(), or State holds one, citry check warns about one in js_data(), and a template shows it as ***. Annotate a Kwargs field as Secret[str] to reject a plain string. A cached component cannot take a Secret input unless its Cache.vary() leaves the secret out (details).
  • With your application loaded, citry check now warns when a template class misspells one the component's CSS styles, such as class="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_class reports every template class that no CSS Citry reads declares, and rule_undeclared_css_variable reports every var(--name) without a fallback that nothing sets. Turn them on in LintSettings or in a component's Lint class (details).
  • With Citry(mode="development"), turning a page into HTML now warns when two of its elements share an id, such as two components that both write id="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 a created() 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 Events data class, as it already did for each item of a list. A key such as "3" fills a dict[int, ...] field as the number 3, and a value typed as a dataclass arrives as an instance of it. If a handler reads such a field as data.scores["3"] or data.points["a"]["x"], use the declared types instead: data.scores[3] and data.points["a"].x. A Pydantic data class already did this.
  • $error(handler).fieldErrors now lists at most 20 errors for fields that the call's data class does not declare (or State fields it may not write), and at most 20 item errors in each list or dict field. message then 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 an EventError you raise, are always listed in full.
  • Every response from a Citry route now has the standard X-Content-Type-Options: nosniff header, so the browser uses the route's Content-Type as given. If an extension route of yours serves JavaScript or CSS, give it the right content_type.
  • citry.contrib.django.secret() now includes Django's SECRET_KEY_FALLBACKS, so Citry(secret=secret()) keeps accepting values signed with an old key while you rotate SECRET_KEY. With fallbacks set it returns a list of keys, so its return type is now str | list[str]; without fallbacks it still returns the plain string.
  • citry check now leaves out findings in the components of a library you registered with register_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 set LintSettings(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 each c-if=, c-elif=, c-else, c-for= or c-empty attribute. A deeper template raises SyntaxError at the tag that goes too deep, and citry check reports it as citry.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 of True, so a str input accepts it. To pass True to a bool input, write the bare attribute, as in <c-Grid compact />; compact="" now fails with a TypeError that 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 with c-bind now writes alt="" there instead of a bare alt, 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_render hook passes to another component, as in return Frame(body=result), or that an on_component_rendered hook passes on as ctx.render, no longer carries the component's own data-cid-* and data-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 same css_data variable, 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 TypeError when the component renders, in development and in production. Fix the value or widen the annotation; to keep the old behavior, set check_input_types = False in a component's nested class Lint:, or pass Citry(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 a Literal member, such as a StrEnum member or Markup("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 Events State field, so list[str] accepts any list. A field's own default always passes, so title: str = None keeps working, and Pydantic models keep their own rules (details).

  • citry check now fails, and the editor shows an error, on an HTML attribute value the browser does not accept, such as <button type="sumbit">. Set rule_invalid_attribute_value="warning" in LintSettings to keep the previous behavior.

  • citry check now fails on a boolean attribute with a value such as disabled="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 check now 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 @click still pass. Set rule_undeclared_component_event to change the severity, which is a warning by default.
  • citry check and 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-key clashes with another key now names its template, line and column.
  • A server expression that reads a Vue v-for variable, such as c-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 bare KeyError. It is still a KeyError, so an existing except KeyError keeps catching it.
  • In citry check --format json, every finding's range now 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. Each range also says which text it counts in, "source_kind": "file" or "inline" (a string written on the class, such as template), and a JsData field's origin now names its Python file (details).
  • The render error for v-model on an element that holds no value, such as <div> or <form>, now names each such element with its template and line and lists the elements v-model works on, instead of an internal Vue compiler message.
  • A form submitted through @c-submit now gives the handler the same values as the same form posted without JavaScript: text such as "3" fills an int or float field, a checked checkbox fills a bool field, and the submit button that sent the form adds its name. Number and range inputs now send their text, so a handler that reads event.args directly, or a Pydantic model in strict mode, gets "3" instead of 3, as it already did without JavaScript.
  • The ext-preview extra now needs Playwright 1.64.1 or newer, whose bundled Node.js does not trust the affected emSign root certificate when playwright install downloads browsers. After upgrading, run playwright install chromium again 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" or ref. 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 a c- binding such as c-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 raises SyntaxError, 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-tr translation binding and its values in the template. A binding that comes from Python data, through c-$c-tr:..., a $c-tr key in a c-bind mapping, or an on_attrs_resolved hook, now raises VueCodeError, because its value is browser JavaScript. To let the server decide whether the browser translates an element, return a flag from js_data() and make the binding's values expression return null where 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 unless State._max_age sets another lifetime, so a page left open longer asks the visitor to reload. A browser write that would make it larger than State._max_bytes, now 64 KiB by default for server storage, answers 422 before the handler runs; set _max_bytes higher, or to None, to allow more. The default InMemoryCache counts this State against a 64 MiB budget, apart from other entries, and drops the entry used longest ago when full; set InMemoryCache(state_max_bytes=...) to change that. max_entries no 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 ValueError unless the item says otherwise:
  • actions.Redirect(url) with a scheme other than http: or https:. A URL with no scheme, such as /projects/, still works. Use a full https: URL or a URL with no scheme instead. A URL containing a control character, such as a line break, also raises.
  • A c-bind mapping key, or a name an on_attrs_resolved hook adds, that starts with on, such as onclick, or that is innerHTML, outerHTML, or srcdoc. The same holds for such a name passed to <c-element> through c-bind, <c-component c-is>, or Python code. Write the attribute in the template instead: @click on a Vue page, @c-click to call a handler of your component, or onclick="...". A hook can still change the value of a handler the template wrote, but it cannot add one. A <c-element> with c-is or c-bind also 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 @event in a component with js, or write a plain element in a c-if branch.
  • An attribute name that your code supplies, such as a c-bind key, 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, or xmp, whose contents the browser reads as raw text or code, passed to a <c-element> that has c-is or c-bind, or to the built-in element component in a Python call. Write such an element directly in the template.
  • Markup inside a <c-element> that renders textarea or title in an interactive component raises TypeError: a tag, a child component, <c-raw>, or a Markup value. Put only text there.
  • A css_data() value that has a comment, a brace, <, or a backslash outside a string or url(); a ; or ! outside brackets; an unbalanced bracket; or a malformed string or url(). A null or surrogate character, `
Citry version: 0.9.0