Citry instance and config
The Citry instance that scopes components, settings, and caches.
Citry class
Global instance that scopes all component state.
A Citry instance owns:
- A private component-name registry reached through the engine's methods
- Settings (to be expanded as the engine grows)
- Transient rendering state
All Component classes are assigned to a Citry instance at class definition time. If no instance is specified, the default instance is used.
Call :meth:initialize after startup-time registration and before a server starts request threads. Lazy initialization remains available, but a thread that encounters lifecycle work owned by another thread receives CitryLifecycleInProgress.
engine_id attribute
engine_id: strReturn this engine's opaque process-lifetime identity token.
The value is stable for this Citry instance, including across clear(), and differs for another instance in the same process. Component-introspection consumers combine it with Component.class_id and Component.definition_id when they need to confirm that retained metadata still describes an exact live component generation.
Returns
str: A non-time-derived token intended only for same-process identity
atomic_registration function
atomic_registration() -> Iterator[None]Publish a group of component registrations together.
Component classes defined inside the block register normally and fire the normal class and registration hooks. If the block raises, Citry restores its component names, class-ID and file indexes, tag rules, and discovery state to their values at entry. Another thread receives CitryLifecycleInProgress rather than observing or changing the group before it commits.
This context manager is additive: it publishes new classes and aliases; unregister() rejects removals inside the block. Start the block outside other component lifecycle operations; nested atomic-registration blocks are rejected. Ordinary component definitions and their hooks remain reentrant inside the block.
Rollback covers the Citry registration and installation indexes listed above. Rendered-output caches, side effects that extension hooks or ordinary Python write elsewhere, and registrations made to another Citry instance are outside the transaction.
Yields
- None - None. Component factories return their own created classes; the
- None - context manager only owns publication and rollback.
Example
with app.atomic_registration(): class AcmeButton(Component): citry = app
Raises
CitryLifecycleInProgress- If another thread owns component lifecycle work for this Citry instance.RuntimeError- If called inside another component lifecycle operation on the same thread.
register_library function
register_library(library: ComponentLibrary | ModuleType) -> LibraryInstallationMaterialize and publish an engine-neutral component library.
A library package may be passed directly when it exposes its manifest as __citry_library__. Registration creates a distinct concrete Component class for every definition and this Citry instance. The classes and immutable installation record become visible together.
Repeating the same manifest and exact definition generation returns the existing installation without rerunning component hooks. Clear the Citry instance before installing a reloaded or changed manifest with the same library name.
Example
import citry_ui from citry import Citry app = Citry() installed = app.register_library(citry_ui)
Parameters
libraryComponentLibrary | ModuleType- A [`ComponentLibrary`](/reference/component-libraries/#citry-componentlibrary) or imported package exposing one as ``__citry_library__``.
Returns
LibraryInstallation: The exact active [`LibraryInstallation`](/reference/component-libraries/#citry-libraryinstallation).
Raises
LibraryManifestChanged- If the library name is already associated with another manifest or definition generation.LibraryInstallationStale- If internal registry mutation damaged an active installation.ValueError- If a required extension is not installed or a component identity collides with existing state.AlreadyRegistered- If a registry name is already occupied.CitryLifecycleInProgress- If another thread owns lifecycle work.RuntimeError- If registration is attempted recursively.
get_library_installation function
get_library_installation(name: str) -> LibraryInstallationReturn one exact active component-library installation.
Parameters
namestr- The manifest's case-sensitive library name.
Returns
LibraryInstallation: The current immutable installation handle.
Raises
LibraryNotInstalled- If no library with ``name`` is active.LibraryInstallationStale- If private registry mutation damaged the installation record.CitryLifecycleInProgress- If another thread owns lifecycle work.
register function
Register an additional name for a component owned by this instance.
Component classes register automatically when they are defined. This method supports same-engine aliases and re-registering a class after it was removed. A class owned by another Citry instance is rejected.
Fires on_component_registered once per call, after the registry accepts the class.
Raises
AlreadyRegistered- If the requested name or class ID belongs to a different component, or the name is reserved.ValueError- If the component belongs to another Citry instance, is a retired built-in generation, or the requested name is invalid.CitryLifecycleInProgress- If another thread is changing component lifecycle state.
unregister function
Unregister one name or all names for a component owned by this instance.
Fires on_component_unregistered once per call, after the registry removes the class.
Raises
NotRegistered- If the requested class or name is not registered.ValueError- If asked to remove a built-in's canonical name or unregister the built-in class.CitryLifecycleInProgress- If another thread is changing component lifecycle state.RuntimeError- If called inside `atomic_registration()` on this thread; atomic registration is additive.
get function
Look up a component by name.
Parameters
namestr- Registered component name, matched case-insensitively.
Returns
type[Component]: The registered component class.
Raises
NotRegistered- If no component has this name after discovery.CitryLifecycleInProgress- If another thread is changing component lifecycle state.
components attribute
All registered components as a name-to-class mapping.
Raises
CitryLifecycleInProgress- If another thread is changing component lifecycle state.
inspect_components function
inspect_components(include_builtins: bool = False, resolve_assets: bool = False, include_default_values: bool = False, include_extensions: Iterable[str] = ()) -> ComponentCatalogReturn an immutable catalog of the currently registered components.
The method completes normal lazy discovery and built-in creation, then copies the registry once. Schema and asset metadata are built from that copy after lifecycle coordination is released. Asset inspection never reads source content or changes render and hot-reload caches.
Parameters
include_builtinsbool- Include Citry's framework component classes.resolve_assetsbool- Check declared asset paths on the filesystem and report resolved, missing, and searched paths.include_default_valuesbool- Copy portable literal schema defaults into field metadata. Default factories are never called.include_extensionsIterable[str]- Installed extensions whose versioned component metadata inspectors should run. Names are deduplicated and sorted; no inspector runs unless explicitly requested.
Returns
ComponentCatalog: A canonically ordered [`ComponentCatalog`](/reference/component-introspection/#citry-componentcatalog)
Raises
TypeError- If a boolean option is not a bool or ``include_extensions`` is a string or non-iterable.ComponentIntrospectionError- If a requested extension is missing, unsupported, fails, or publishes invalid metadata.CitryLifecycleInProgress- If another thread is changing component lifecycle state.
inspect_component function
inspect_component(component: str | type[Component], resolve_assets: bool = False, include_default_values: bool = False, include_extensions: Iterable[str] = ()) -> ComponentInfoInspect one component selected from a copied registry snapshot.
A string is matched case-insensitively as a registered component name. A class must be owned by this engine and present under at least one name. Looking up an alias does not change the record's deterministic primary name.
Parameters
componentstr | type[Component]- A registered name or exact registered component class.resolve_assetsbool- Check declared asset paths on the filesystem.include_default_valuesbool- Copy portable literal schema defaults.include_extensionsIterable[str]- Installed extensions whose versioned metadata inspectors should run. Names are deduplicated and sorted.
Returns
ComponentInfo: A value-only [`ComponentInfo`](/reference/component-introspection/#citry-componentinfo) record.
Raises
TypeError- If ``component`` is neither a string nor a class, a boolean option is not a bool, or ``include_extensions`` is a string or non-iterable.NotRegistered- If the name or exact class is absent from the copied registry snapshot.ComponentIntrospectionError- If a requested extension is missing, unsupported, fails, or publishes invalid metadata.CitryLifecycleInProgress- If another thread is changing component lifecycle state.
initialize function
initialize() -> NonePrepare component registration state before starting worker threads.
This imports configured component modules when autodiscovery is enabled, creates Citry's built-in components, and builds the current parse-time tag rules. Component template, JavaScript, and CSS asset files remain lazy and are loaded when rendering needs them.
Call this after startup-time component registration and before a server begins handling requests concurrently. Repeated successful calls are safe; a later registration invalidates tag rules, and another call rebuilds them.
Initialization is retryable rather than globally transactional. If a module import fails, components from earlier successful module imports may remain registered, while the incomplete work is retried later.
Returns
None: None.
Raises
CitryLifecycleInProgress- If another thread is changing component lifecycle state.RuntimeError- If called recursively from an active lifecycle hook or initialization on this thread.
autodiscover function
Import this instance's component modules so their classes register.
With no argument, imports every component module under the instance's dirs - the same scan the autodiscover setting performs automatically on first use - and marks that automatic scan done, so it will not run again. Pass dirs to import an extra set of directories on demand without affecting the automatic scan.
The directories must be importable: each one (or a parent of it) is on sys.path/PYTHONPATH, which is how a component file is mapped to the import name Python uses for it. A directory that holds component modules but is not importable raises ValueError.
Returns the dotted import paths of the modules that were imported. Safe to call more than once: an already-imported module has its components re-registered directly, so a call after clear() rebuilds the registry and a call that changes nothing is a no-op.
A scan is marked complete only after every module imports successfully. If one module raises, registrations it made to this Citry instance during that import are rolled back, and a later call can retry it. Earlier modules and dependency modules that imported successfully remain registered. Python side effects and registrations made to another Citry instance are outside this rollback. Calling autodiscover() recursively from a component module raises RuntimeError.
Raises
CitryLifecycleInProgress- If another thread is changing component lifecycle state.RuntimeError- If a component module starts another autodiscovery scan on the same instance.
urls attribute
This instance's HTTP route table (framework-neutral URLRoutes).
The web-integration adapters (citry.contrib.asgi and friends) mount these into the host application; the routes serve cached component JS/CSS, the client runtime, and extension endpoints.
commands attribute
commands: dict[str, tuple[type[ExtensionCommand], ...]]This instance's CLI commands, keyed by extension name.
Each registered extension contributes the commands it declares in Extension.commands; the citry command-line tool reaches one as citry ext run <extension name> <command name>. See ExtensionManager.commands for ordering and the uniqueness guarantee.
mounted_prefix attribute
mounted_prefix: str | NoneWhere this instance's routes are mounted (e.g. "/citry"), or None when nothing is mounted.
set_mounted_prefix function
set_mounted_prefix(prefix: str) -> NoneRecord where this instance's routes are mounted in the host app.
The adapters' mount() call this; call it directly only in a process that builds URLs without mounting the routes itself (for example a worker that renders fragments served by another process). prefix must start with /; a trailing / is dropped.
build_url function
An absolute URL path for one of this instance's routes.
path is the route's full path (no leading slash), e.g. "cache/Table_a1b2c3.js". Raises RuntimeError when no web integration is mounted, since the URL would point nowhere.
get_component_by_class_id function
Look up a registered component class by its class_id.
class_id is the stable identifier (MyComp.class_id) used in cache keys and script URLs. Raises KeyError when no registered class has that id.
Raises
KeyError- If no registered component has this class ID.CitryLifecycleInProgress- If another thread is changing component lifecycle state.
get_components_for_file function
The component classes whose assets resolved to path.
Most callers want :meth:invalidate_file, which both finds these classes and resets them. This lower-level lookup is for a caller that wants the classes without resetting (a custom hot-reload handler, a test). Dead weakrefs are pruned on read.
invalidate_file function
Drop cached template/JS/CSS for every component that loaded an asset from path, so the next render re-reads it from disk.
Returns the component classes it reset. An empty list means the file backs no loaded component, which a hot-reload handler can read as "not mine" and, if it wants, fall through to a full restart. This is the host-neutral call a file watcher drives; see the watcher in :mod:citry.reload and docs/design/hot_reload.md.
invalidate_all function
Reset cached template/JS/CSS for every component that has loaded a file, so the next render re-reads them all from disk. Returns the reset classes (in first-seen order).
For when a change cannot be mapped to a single path: a bulk edit, a branch switch, or a custom watcher reporting an event it cannot resolve to one file. Unlike :meth:clear, this leaves the registry and autodiscovery untouched.
clear function
clear() -> NoneClear registrations and caches, and re-arm autodiscovery.
Raises
CitryLifecycleInProgress- If another thread is changing component lifecycle state.RuntimeError- If called from another lifecycle operation on this thread.
citry module
The Citry global instance - scopes all component state.
A Citry instance owns a component registry, settings, and transient rendering state. Every Component subclass is assigned to a Citry instance, either by declaring citry = my_citry in its class body or by using the default instance.
Example
Using the default instance (most common)::
from citry import Component class MyTable(Component): template = "<table>...</table>"Using a custom instance::
from citry import Citry, Component my_citry = Citry() class MyTable(Component): citry = my_citry template = "<table>...</table>"Isolated instances for testing::
def test_my_component(): test_citry = Citry() # Components registered here don't leak to other tests class MyTable(Component): citry = test_citry template = "..."
AlreadyRegistered class
Bases: Exception
Raised when registering a component under a name that is already taken.
NotRegistered class
Bases: Exception
Raised when looking up a component name that is not registered.
CitryLifecycleInProgress class
Bases: RuntimeError
Raised when another thread is changing Citry's component lifecycle state.
Component discovery, registration, built-in creation, clearing, and tag-rule construction publish related state together. A competing thread receives this error rather than observing an incomplete registry or waiting in a way that can deadlock with Python's module-import locks.
Finish Citry.initialize() before starting request threads to avoid this error during normal server operation. An operation that encounters it may also be retried after the active lifecycle operation finishes.
CitrySettings class
Immutable settings for a Citry instance.
Attributes
extensionsSequence[type[Extension] | Extension | str]- The extensions to install on the instance. Each entry is an ``Extension`` subclass, a ready-made instance, or an import string like ``"myapp.extensions.MyExtension"``. The set is fixed once the instance is constructed.extensions_defaultsMapping[str, Mapping[str, Any]]- Default config values for extensions, keyed by extension name, e.g. ``{"events": {"_csrf": True}}``. When an extension reads a config field for a component, the component's own nested config class wins, a value given here fills in next, and the extension's built-in default comes last.dirstuple[Path, ...]- Directories searched when resolving a component's asset files (``template_file``, ``js_file``, ``css_file``, and ``Dependencies`` entries), after the directory of the component's own ``.py`` file. Entries are converted to ``Path`` and must be absolute; this is validated at construction (a relative entry raises ``ValueError``).cacheCitryCache | str | None- Where citry stores what it caches: a [`CitryCache`](/reference/citry/#citry-citrycache) object or an import string like ``"myapp.caching.MyCache"``. ``None`` gives the instance its own in-memory cache. The live backend built from this setting is ``Citry.cache``.sandbox_expressionsbool- Whether template expressions (``{{ ... }}`` and dynamic ``c-*`` attributes) are evaluated in the security sandbox. On by default. Turning it off evaluates expressions as plain Python, which is faster but removes security guardrails. Only do so when every template comes from a trusted source.autodiscoverbool- Whether to import the component modules under ``dirs`` the first time a component is looked up, so their classes register themselves without being imported by hand. On by default; when no ``dirs`` are set there is nothing to scan, so the default instance does nothing. The directories must be importable (on ``sys.path``/``PYTHONPATH``). See ``Citry.autodiscover`` and ``citry.autodiscovery``.modeMode- The build environment, ``"production"`` (the default) or ``"development"``. It is the single source of truth for whether the engine includes developer-only output: in ``"development"`` the built-in ``debug`` extension is auto-registered (visual component boundaries) and the client ownership graph carries source provenance. An unrecognized value raises ``ValueError`` at construction. See ``docs/design/dev_prod_mode.md``.template_globalsMapping[str, Any]- Variables exposed to every component's template without being returned from each ``template_data()``. They are merged into every component's template variables on render, so a template can reference one directly (``{{ site_name }}``). A component's own ``template_data`` wins when it returns a key of the same name, so globals act as defaults. The value given here is the starting set; the live, editable copy is ``Citry.template_globals``, which is how you add or change a global after the instance exists (including the default instance, created at import before your code runs).id_generatorCallable[[], str] | str | None- A function returning the per-render id stamped on each component instance (``component.id``, which drives the ``data-cid-<id>`` markers that scope a component's CSS and JS on the page). Given as a callable or a ``"path.to.func"`` import string; passing a class also works: it is called once, and the resulting object is used as the generator (handy when the generator keeps state, like a counter). ``None`` uses the built-in generator. Override it for stable ids in snapshot tests. The generator must return ids that are unique among the components on one page and contain only lowercase ASCII letters, digits, hyphens, and underscores. The lowercase rule is required because the id is embedded in an HTML attribute name. This does not touch ``class_id``, which stays a stable hash of the component's import path.secretstr | list[str] | None- The signing secret for values citry hands to the browser and must recognize when they come back, such as the state the Events extension round-trips on each event call. A single string is the common form. A list means key rotation: the first entry signs new values, and a value signed by any entry still verifies, so already-issued values stay valid while a new key rolls out. A bare string is stored as a one-element list. ``None`` (the default) means no secret is set. Django projects can reuse their existing key by passing ``citry.contrib.django.secret()``.event_result_resolversSequence[Any]- Result resolvers for the Events extension. When an event handler returns a value, citry converts it into the actions sent back to the browser (the instructions the client runtime applies: re-render this component, redirect, and so on). A resolver adds support for your own return types: it is given the handler's return value and either converts it into those actions or declines, letting the next resolver try. Resolvers run in order, after the built-in conversions; the first one to convert the value wins.event_payload_codecsSequence[Any]- Payload codecs for the Events extension's HTTP endpoints. A codec reads one request format (identified by its content type) into the event call the extension expects, so clients are not limited to the built-in JSON, form, and query formats. Codecs given here are tried before the built-in ones, in order.
CitryCache class
Bases: Protocol
The cache backend interface.
Implement these four methods to plug in any store (Redis, diskcache, Django's cache framework, ...). Keys and values are strings.
set function
Store value under key. ttl is seconds until expiry; None means keep forever.
InMemoryCache class
The default cache backend: a thread-safe in-process LRU store.
Unbounded by default. Pass max_entries to cap the size; when full, the entry that was read or written longest ago is dropped to make room.
Single-process only: each instance is its own store. For multi-worker deployments use a shared backend instead (see the module docstring).