Extensions
The plugin system: the extension base, its commands, and the hook context objects.
Extension class
Base class for all extensions.
Subclass this, set name (a lowercase Python identifier), and implement the on_* hooks you care about. Every hook has an empty default, so an extension only overrides what it needs (the manager calls only the hooks an extension actually overrides). The on_* methods below are the full hook catalog.
name attribute
name: strName of the extension. Lowercase, a valid Python identifier. Determines the attribute the per-component config is reachable under (component.<name>) and, via :attr:class_name, the nested class name.
class_name attribute
class_name: strPascalCase name of the per-component nested config class, derived from :attr:name at subclass creation (my_extension -> MyExtension).
Config attribute
Config: type[ExtensionConfig]Base class the per-component nested config inherits from.
commands attribute
commands: list[type[ExtensionCommand]]CLI commands this extension provides (see :class:ExtensionCommand).
introspection_version attribute
introspection_version: int | NonePositive schema version when this extension publishes component metadata.
render_cache_mode attribute
render_cache_mode: Literal['deny', 'stateless', 'payload']Whether settled render state from this extension can be replayed.
render_cache_version attribute
render_cache_version: int | NonePositive compatibility version for stateless or payload replay.
citry attribute
citry: CitryThe Citry instance this extension instance belongs to. Set by the manager when the extension is attached (extensions are per-instance, so the back-reference is unambiguous).
urls attribute
HTTP routes this extension provides (see citry/util/routing.py).
Mounted by the web-integration adapters as part of Citry.urls: a user extension's routes live under ext/<extension name>/; built-in extensions own their paths directly. Override as an attribute or property; handlers can reach engine state through self.citry.
validate_config_fields function
Check the config fields declared for this extension, at declaration time.
Users configure an extension in two places: a component's nested config class (class View: for an extension named "view"), and the engine-wide extensions_defaults setting. Citry calls this method once per declaration:
- At engine construction, with the extension's entry in the
extensions_defaultssetting (componentisNone). - At component class definition, with the fields declared on the component's nested config class, including fields from its user-written base classes (
componentis the component class).
The base implementation accepts everything: by default an extension's config may hold any fields, even methods. Override it to reject bad fields early, so a typo in a field name fails at startup or at class definition instead of surfacing later as a confusing downstream error. Because both declaration sites are checked, the fields are known-valid by the time the config class is instantiated.
Example
An extension whose config accepts exactly one field:
from citry import Extension class CacheExtension(Extension): name = "cache" def validate_config_fields(self, fields, *, component=None): for name in fields: if name != "ttl": msg = f"unknown config field {name!r}; the only field is 'ttl'" raise ValueError(msg)
Parameters
fieldsMapping[str, Any]- The declared fields, mapping field name to declared value. For a nested config class, dunder names, the ``Config`` base's members, and citry's own bookkeeping attributes are already filtered out.componenttype[Component] | None- The component class the fields were declared on, or ``None`` when the fields come from the ``extensions_defaults`` setting.
Raises
ValueError- When a field is not valid for this extension. Raise it with a message naming the offending field and what is valid; citry prefixes the declaration site (the component and its nested class, or the setting).
inspect_component function
inspect_component(ctx: ComponentIntrospectionContext) -> dict[str, object] | NonePublish this extension's allowlisted metadata for one component.
Citry calls this direct query method only when a caller explicitly requests the extension by name. Override it together with a positive :attr:introspection_version. Return an exact built-in dict made only from strict JSON values, or None when this component has no entry. The method must be observational, deterministic, reentrant, and thread-safe; it must not render, load assets, mutate registration, or depend on request state.
Parameters
ctxComponentIntrospectionContext- The owning engine, temporary live component class, and its already-built core metadata record. ``ctx.info.extensions`` is always empty.
Returns
dict[str, object] | None: An extension-owned JSON object, or ``None``.
on_extension_created function
on_extension_created(ctx: OnExtensionCreatedContext) -> NoneCalled once when this extension instance is created.
on_component_class_created function
on_component_class_created(ctx: OnComponentClassCreatedContext) -> NoneCalled after a Component class is defined, before it is registered.
on_component_registered function
on_component_registered(ctx: OnComponentRegisteredContext) -> NoneCalled after a Component class is registered.
on_component_unregistered function
on_component_unregistered(ctx: OnComponentUnregisteredContext) -> NoneCalled after a Component class is unregistered.
on_component_input function
on_component_input(ctx: OnComponentInputContext) -> NoneCalled when a component starts rendering, before template_data.
Inspect or mutate ctx.kwargs / ctx.slots in place. These are the authoritative raw mappings. Citry normalizes Slots and constructs the component's final typed kwargs and slots once all input hooks finish.
on_component_data function
on_component_data(ctx: OnComponentDataContext) -> NoneCalled after template_data; mutate ctx.template_data to add or change template variables.
on_component_rendered function
on_component_rendered(ctx: OnComponentRenderedContext) -> CitryRender | str | NoneCalled after a component (and its children) rendered. Return a new CitryRender / str to replace the output, raise to replace the error, or return None to keep the original.
on_slot_rendered function
on_slot_rendered(ctx: OnSlotRenderedContext) -> RenderPart | NoneCalled after a <c-slot> site rendered (a fill, or the fallback).
Return a new render part (str or CitryRender) to replace the output, or None to keep the original. Raising propagates.
on_attrs_resolved function
on_attrs_resolved(ctx: OnAttrsResolvedContext) -> dict[str, Any] | NoneCalled after an HTML element's dynamic attributes resolved to their final dict, before it is formatted into the output. Return a new dict to replace the attributes, or None to keep them.
Fires per element per render, only for elements with at least one dynamic attribute (a c-* value or a c-bind spread).
on_render_context_merge function
on_render_context_merge(ctx: OnRenderContextMergeContext) -> NoneCalled when a nested render's output is consumed by an enclosing render (a child component settling into its parent, or an already-rendered value embedded via an expression or slot).
Merge your extension's slice of ctx.child_context.extra into ctx.parent_context.extra, with your own policy (the dependencies extension, for example, appends records preserving order). The core does not merge anything itself.
export_render_cache function
Export this payload extension's selected strict-JSON contribution.
stage_render_cache function
stage_render_cache(ctx: OnRenderCacheStageContext) -> StagedRenderCacheContributionValidate a detached payload and return a mutation-free replay contribution.
on_serialize function
on_serialize(ctx: OnSerializeContext) -> str | NoneCalled at the end of CitryRender.serialize() with the joined HTML.
Return a new string to replace the output (threaded across extensions), or None to keep it. This is where serialize-time work that needs the whole page happens; the dependencies extension places the collected JS/CSS here, using ctx.placeholders for the <c-js>/<c-css> positions.
on_template_loaded function
on_template_loaded(ctx: OnTemplateLoadedContext) -> str | NoneCalled once per class with the template string before it is parsed. Return a new string to modify it.
on_template_compiled function
on_template_compiled(ctx: OnTemplateCompiledContext) -> list[BodyItem] | NoneCalled once per compiled body, with the generated node list. Mutate it in place or return a new list.
on_template_reset function
on_template_reset(ctx: OnTemplateResetContext) -> NoneCalled after a component class's loaded template is reset.
on_js_loaded function
on_js_loaded(ctx: OnJsLoadedContext) -> str | NoneCalled once per class with the component's primary JS content (inline or read from js_file). Return a new string to modify it.
on_css_loaded function
on_css_loaded(ctx: OnCssLoadedContext) -> str | NoneCalled once per class with the component's primary CSS content (inline or read from css_file). Return a new string to modify it.
ExtensionConfig class
Base for the per-component nested config class (reached as Extension.Config).
An extension named "view" (class_name == "View") lets a user define a nested class View: on a component. The manager rebuilds that nested class as a subclass of this base (binding component_class), then instantiates it per render and attaches it as component.view.
The component back-reference is a weakref, and the component may be None for extensions that run outside a component lifecycle (for example a future Storybook extension).
component_class attribute
The Component class this config is defined on (bound by the manager).
component attribute
component: ComponentThe owning Component instance.
Raises RuntimeError if this config runs outside a component lifecycle (no component), or if the component has been garbage-collected.
ExtensionManager class
Fans each lifecycle hook out across a Citry instance's extensions.
Owned by :class:~citry.citry.Citry and built once in its __init__. Unlike DJC's module-level singleton, there is no deferred-event machinery: a component class is bound to its Citry (and thus these extensions) at definition time, so the extensions are always present when a hook fires.
Dispatch is smart: for each hook name, only the extensions that actually override that hook are called (an extension that does not implement a hook costs nothing). The same name-keyed dispatch underlies :meth:emit, which extensions use for their own custom hooks (e.g. on_dependencies).
get_extension_command function
get_extension_command(name: str, command_name: str) -> type[ExtensionCommand] commands attribute
commands: dict[str, tuple[type[ExtensionCommand], ...]]Every extension's CLI commands, keyed by extension name (read as Citry.commands).
Built-in extensions come first (they are prepended at construction), then the user's extensions in spec order; only extensions that declare commands appear. Extension names are unique (enforced at construction), so the keys never collide. The CLI reaches a command as citry ext run <extension name> <command name>.
urls attribute
The combined route table of every extension (read as Citry.urls).
Built-in extensions own their paths directly (e.g. the dependencies extension's cache/... and citry.js); a user extension's routes are namespaced under ext/<extension name>/ so they cannot collide with citry's own or each other's.
emit function
Dispatch hook name to the extensions that define it, combining the hooks' returned values per result:
"none": call every extension, ignore returns; returnNone."first": return the first non-Nonereturn (short-circuit)."map": threadctx.<field>- each non-Nonereturn replaces it (viadataclasses.replace) and is passed to the next extension; the final field value is returned.
An extension defines name by overriding it (see _extensions_with_hook). name need not be a hook declared on :class:Extension, so an extension can fire its own custom hook for others to implement.
Example
Most named hooks delegate here. on_component_data notifies every extension that defines it ("none")::
manager.emit("on_component_data", ctx)
on_template_loaded threads ctx.content through the extensions ("map") and returns the final string::
manager.emit("on_template_loaded", ctx, result="map", field="content")
A custom hook can let an extension short-circuit ("first" returns the first non-None value)::
manager.emit("on_my_event", ctx, result="first")
on_render_context_merge function
on_render_context_merge(parent_context: CitryContext, child_context: CitryContext) -> None on_component_rendered function
on_component_rendered(component: Component, render: CitryRender | str | None, error: Exception | None) -> tuple[CitryRender | str | None, Exception | None, bool]Thread the rendered output through the extensions; a return replaces the render, a raise replaces the error.
on_slot_rendered function
on_slot_rendered(component: Component, slot: Slot, slot_name: str, slot_node: SlotNode, slot_is_required: bool, result: RenderPart) -> RenderPartThread a slot's rendered output through the extensions; a return replaces the result, a raise propagates.
on_attrs_resolved function
Thread an element's resolved attribute dict through the extensions; a return replaces the dict, a raise propagates.
This sits on a per-element per-render hot path, so when no extension implements the hook the dict is returned without building a context.
on_template_reset function
Notify extensions after a component class's template is reset.
on_files_reset function
Notify extensions that a component class's loaded asset files were reset, so each drops its own per-class state (the dependencies built-in drops its merged result here).
Deliberately not declared on the :class:Extension base: this is the first consumer of the duck-typed custom-hook dispatch (an extension subscribes by defining a method named on_files_reset).
ExtensionCommand class
Base class for an extension's CLI command.
Subclass this, set name (and usually help), declare any arguments, and define handle to do the work. A command that only groups subcommands leaves handle unset, and the runner prints its help instead of running anything. The declarations are turned into an argparse parser and dispatched by :mod:citry.command; an extension lists its command classes in Extension.commands and a user reaches one as citry ext run <extension> <command>. (Extension HTTP routes are a separate surface, Extension.urls.)
arguments attribute
arguments: Sequence[CommandArg | CommandArgGroup]Positional arguments and options, declared with :class:~citry.command.CommandArg.
subcommands attribute
subcommands: Sequence[type[ExtensionCommand]]Nested commands. A command with subcommands usually has no handle of its own.
subparser_input attribute
subparser_input: CommandSubcommand | NoneOptional customization of how this command appears when nested under a parent.
handle attribute
handle: CommandHandler | NoneRuns the command, called with the parsed options as keyword arguments. None (the default) marks a command that only groups subcommands; a real command overrides this with def handle(self, **kwargs).
citry attribute
citry: Citry | NoneThe engine the command runs against, bound by the runner before handle is called (mirrors :attr:Extension.citry). A command's handle reads it to reach the component registry and the installed extensions.
Debug class
Bases: Extension
Draw development-only boundaries around component and slot output.
Install this extension explicitly with Citry(extensions=[Debug]). Its per-component config has two exact boolean fields, highlight_components and highlight_slots. Set them globally in extensions_defaults["debug"] or override them in a component's nested class Debug.
The visual boundaries are real div elements. They are useful for inspecting ordinary page structure, but can affect layout, direct-child selectors, and restricted table or select content models. Do not enable them in production or use them for layout-sensitive verification.
Example
Enable both boundary types for one engine:
from citry import Citry from citry.ext.debug import Debug app = Citry( extensions=[Debug], extensions_defaults={ "debug": { "highlight_components": True, "highlight_slots": True, }, }, )
on_component_registered function
on_component_registered(ctx: OnComponentRegisteredContext) -> None on_component_unregistered function
on_component_unregistered(ctx: OnComponentUnregisteredContext) -> None on_component_rendered function
on_component_rendered(ctx: OnComponentRenderedContext) -> CitryRender | None on_slot_rendered function
on_slot_rendered(ctx: OnSlotRenderedContext) -> CitryRender | None on_render_context_merge function
on_render_context_merge(ctx: OnRenderContextMergeContext) -> None CommandArg class
One positional argument or option, mirroring ArgumentParser.add_argument.
Every field maps to the matching add_argument keyword, and :func:build_parser passes them through unchanged, so the field names must stay aligned with argparse.
name_or_flags attribute
A positional name ("path") or a list of option flags (["--shout", "-s"]).
to_add_argument_kwargs function
The add_argument keywords for this argument, minus name_or_flags.
name_or_flags is passed positionally by :func:build_parser, so it is not included here; unset (None) fields are dropped.
CommandArgGroup class
A titled group of arguments, mirroring ArgumentParser.add_argument_group.
Place one in a command's arguments list to group related options together in the --help output.
CommandSubcommand class
How a command appears when nested under a parent, mirroring add_subparsers().add_parser.
A command sets this as its subparser_input to customize its entry in the parent's subcommand list (for example a different help line or program name).
to_add_parser_kwargs function
The add_parser keywords for this subcommand entry (unset fields dropped).
OnAttrsResolvedContext class
attrs attribute
The resolved attribute dict: class/style already normalized to strings, booleans still True, omitted attributes already absent.
OnComponentClassCreatedContext class
nested_declarations function
nested_declarations(name: str) -> tuple[NestedClassDeclaration, ...]Return the exact authored bindings for name in component C3 order.
A record whose value is None is an explicit reset, distinct from the name being absent. The classes are the original source objects, even after Citry replaces the component attribute with an effective runtime config class.
Parameters
namestr- The nested declaration name, usually the extension's [`class_name`](/reference/extensions/#citry-extension-class-name).
Returns
tuple[NestedClassDeclaration, ...]: The declarations from the component through its bases in C3 order.
OnComponentDataContext class
context attribute
context: CitryContextThe render-scoped CitryContext for this component's render. Extensions stash tree-wide state in context.extra (for example the dependencies extension's render records); it bubbles up through on_render_context_merge as nested renders are consumed. context.provides is not yet populated when this hook fires.
template_data attribute
The template variables from Component.template_data() (mutable).
js_data attribute
The JS variables from Component.js_data() (mutable). Consumed by the built-in dependencies extension.
css_data attribute
The CSS variables from Component.css_data() (mutable). Consumed by the built-in dependencies extension.
OnComponentInputContext class
OnComponentRegisteredContext class
OnComponentRenderedContext class
render attribute
render: CitryRender | str | NoneThe rendered output, or None if rendering failed.
error attribute
error: Exception | NoneThe error raised during rendering, or None if it succeeded.
OnComponentUnregisteredContext class
OnCssLoadedContext class
OnFilesResetContext class
OnJsLoadedContext class
OnRenderContextMergeContext class
parent_context attribute
parent_context: CitryContextThe context of the render that consumed the nested one.
child_context attribute
child_context: CitryContextThe context of the consumed nested render.
OnSerializeContext class
context attribute
context: CitryContextThe root render's CitryContext (its extra carries everything that bubbled up during the render).
placeholders attribute
The placeholder parts found during serialization: unique placeholder id (the Placeholder.key plus a counter and a private serialization identity) to the exact text standing in for it in html. Match the key prefix rather than relying on the private suffix.
OnSlotRenderedContext class
component attribute
component: ComponentThe component whose template holds the <c-slot> that was rendered.
slot attribute
slot: SlotThe Slot that was rendered: the fill, or the fallback when no fill was given.
slot_name attribute
slot_name: strThe resolved slot name ("default" for an unnamed slot).
slot_node attribute
slot_node: SlotNodeThe runtime SlotNode at whose site the slot rendered.