rfgen.core.registry¶
Plugin discovery and the metadata schema every plugin package declares. The
registry is what turns “an installed package on the Python path” into “an
emitter, channel, labeler, store, or command the framework can resolve from
config.” A single concrete registry ships:
EntryPointRegistry, which
reads importlib.metadata.entry_points. An optional second primitive,
pluggy, is gated behind the rfgen[plugin-hooks] extra for hook-based
discovery.
Module summary¶
from rfgen.core.registry import EntryPointRegistry, PluginMetadata
from rfgen.waveforms.base import BaseEmitter
emitters = EntryPointRegistry[BaseEmitter]("rfgen.emitters")
emitters.discover() # records names; does not import any plugin module
print(emitters.available()) # ('adsb', 'analog_fpv_video', 'apsk', 'ble', ...)
emitter = emitters.get("chirp_radar") # this call triggers the lazy import
Discovery is two-phase by design: discover() walks the entry-point index and
records the names; get(name) is the first call that imports the plugin
module. Importing rfgen.core.registry itself never imports any third-party plugin.
Class index¶
Class |
Kind |
Notes |
|---|---|---|
abc |
Generic ABC; implements |
|
concrete |
Single shipped subclass; backed by |
|
datatype (Pydantic) |
Frozen plugin descriptor. |
|
protocol |
Dispatch contract for services callable from the CLI. |
The constant EXIT_CODES_VALID = frozenset({0, 1, 2, 3, 130}) is also
exported. It is the closed set of process exit codes a
BaseCommand is allowed to return.
rfgen.core.registry.refresh_plugin_discovery¶
def refresh_plugin_discovery() -> tuple[str, ...]
Re-scan installed distributions for plugin entry points and return every discovered group name, sorted.
The entry-point index is read once per process, which is correct when
sys.path is fixed before rfgen is imported. A distributed worker breaks that
assumption: Spark appends a submission’s --py-files archives to sys.path
around the time it imports the driver, so an index built first would omit
exactly the plugins that submission shipped. Call this before the first
registry lookup in that situation; ordinary local runs never need it.
See Shipping plugin packages for the remote path that depends on it.
class rfgen.core.registry.BaseRegistry¶
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Generic, TypeVar
T = TypeVar("T")
class BaseRegistry(ABC, Generic[T]):
"""Generic ABC for plugin registries."""
def register(
self,
name: str,
factory: Callable[..., T],
*,
source: str = "<unknown>",
metadata: PluginMetadata | None = None,
) -> None: ...
def get(self, name: str) -> T: ...
def available(self) -> tuple[str, ...]: ...
def metadata_for(self, name: str) -> PluginMetadata | None: ...
@abstractmethod
def discover(self) -> None: ...
A generic ABC parameterized by the produced plugin type T. Subclasses
implement discover(); the shared registration and lookup API lives on the
base class so subclasses do not duplicate it.
register(name, factory, *, source, metadata)¶
Register a factory under a name.
name: canonical name for the plugin entry.factory: zero-or-more-argument callable returning aT.source: human-readable origin of the registration (entry-point module path, file path, …). Used in conflict error messages.metadata: optional PluginMetadata describing the plugin. If provided andmetadata.requireslists an extra whose marker package is not installed, registration raises BackendUnavailableError.
Raises:
RegistryError: if
nameis already registered. The message names both source modules.BackendUnavailableError: if
metadata.requireslists an extra whose marker package is not present.
get(name)¶
Resolve name and return the constructed plugin instance. Raises
PluginNotFoundError if name
is not registered; the message lists the available names for this registry.
available()¶
Return the sorted tuple of registered names. On EntryPointRegistry, this includes pending entry-point names that have been discovered but not yet loaded.
metadata_for(name)¶
Return the PluginMetadata
registered for name, or None if no metadata was registered.
discover()¶
Abstract. Concrete subclasses populate the registry from the underlying source. Must be idempotent.
class rfgen.core.registry.EntryPointRegistry¶
class EntryPointRegistry(BaseRegistry[T]):
"""A BaseRegistry backed by importlib.metadata.entry_points."""
def __init__(self, group: str) -> None: ...
@property
def group(self) -> str: ...
def discover(self) -> None: ...
def get(self, name: str) -> T: ...
def available(self) -> tuple[str, ...]: ...
The single concrete BaseRegistry subclass shipped with rfgen. It is constructed with a single entry-point group name. The framework instantiates one registry per documented group:
rfgen.emittersrfgen.channelsrfgen.augmentationsrfgen.radar_responsesrfgen.scene_renderersrfgen.scene_composersrfgen.channel_plan_sourcesrfgen.time_placementrfgen.freq_placementrfgen.grid_sourcesrfgen.labelersrfgen.dataset_storesrfgen.annotatorsrfgen.annotation_backendsrfgen.backend_capabilitiesrfgen.inference_clientsrfgen.geometry_ingestsrfgen.plan_exportersrfgen.metricsrfgen.executorsrfgen.commands
Two more groups are declared in code rather than in pyproject.toml, and a
plugin author publishing under either is loaded normally:
rfgen.observation_projections(rfgen.observation.runtime.resolution)rfgen.annotation.credentials(rfgen.annotation.credentials)
The two scene-seam groups¶
Two of the groups above were added by the external-scene-seam work and their contracts are stated on their own pages; what follows is what a plugin author needs to know about the groups themselves.
rfgen.geometry_ingests registers implementations of the geometry ingestion
contract: one name resolves to one class that declares which
GeometryAssetKind values it reads. The refusal is the dispatcher’s rather
than the class’s: it asserts the kind in hand against that declaration and
raises geometry_format_not_ingestible by name, so one assertion covers an
out-of-tree ingest as well as the in-repo one. The
default is sionna_mitsuba, which is what an absent world_ingest means, so
an absent value in stored provenance is itself a disclosure rather than a gap.
A registered ingest is selected by name from configuration and the selection is
asserted against the class’s own declaration, so a name that resolves to an
ingest which cannot read the configured kind fails before any engine work.
Contract: Engine.
rfgen.plan_exporters registers serializations of a ScenePlan into an
external scene description: one name resolves to one class declaring a name,
a filename suffix, and an export(plan, *, time_codes_per_second) returning
text. The in-repo entry is usda. The group exists because the format an
external engine wants is a property of that engine, so a second exporter, a
.usdc binary writer or a glTF one, is a registration rather than a core
change; rfgen export-plan --exporter NAME resolves through this group, so an
out-of-tree exporter is reachable from the shipped command line the day its
distribution is installed. Contract: Scene.
Lazy discovery¶
discover() walks the entry-point index and records every entry in the
configured group, but does not call EntryPoint.load(). The first get(name)
call for a pending name pops the entry, calls load(), and registers the
resulting object as the factory under source="entry_point:<group>:<name>".
The full entry-point index is fetched once per process via a private
_all_entry_points() helper wrapped in functools.lru_cache(maxsize=1).
Entry points are statically declared in installed distribution metadata and
do not change at runtime, so a single process-wide scan is safe. Across
every registry rfgen constructs, this avoids one full metadata scan each.
Same-name conflict¶
Two installed packages declaring the same name in the same entry-point
group raise
RegistryError at registration
time; the message names both source modules.
Missing-extra conflict¶
If a plugin’s
PluginMetadata.requires lists
an extra whose marker package is not installed, registration raises
BackendUnavailableError,
naming the missing extra. A legacy runtime error may spell pip install rfgen[<extra>], but that is not a supported source-checkout route; from the
checkout root use uv pip install -e '.[<extra>]', or reinstall a built
release wheel with its matching extra.
class rfgen.core.registry.PluginMetadata¶
from pydantic import BaseModel, ConfigDict, Field, field_validator
import packaging.version
class PluginMetadata(BaseModel):
"""Standardized descriptor for a plugin package."""
model_config = ConfigDict(frozen=True)
name: str = Field(min_length=1, pattern=r"^[A-Za-z][A-Za-z0-9_-]*$")
version: str
family: str | None = None
summary: str = Field(default="", max_length=500)
requires: list[str] = Field(default_factory=list)
homepage: str | None = None
license: str = "Apache-2.0"
A frozen Pydantic v2 model. Validates on construction and is the single source of truth for what a plugin claims to provide and what extras it needs. Frozen instances are read-only after construction; this matches the frozen-by-default posture of the dataclasses and prevents silent corruption of registry state by code that holds a shared reference.
Fields¶
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
str |
yes |
none (required) |
PyPI distribution name, e.g. |
|
str |
yes |
none (required) |
SemVer / PEP 440 version of the plugin package. Validated via |
|
str | None |
no |
|
Family tag (emitter family, channel backend tag, …). |
|
str |
no |
|
Short human-readable description, max 500 chars. |
|
list[str] |
no |
|
List of pip extras that must be installed for this plugin to operate. Registration fails with BackendUnavailableError if any required extra is missing. |
|
str | None |
no |
|
Project URL, typically the GitHub repo. |
|
str |
no |
|
SPDX license identifier. |
Validation¶
nameis rejected unless it matches^[A-Za-z][A-Za-z0-9_-]*$.versionis rejected ifpackaging.version.Version(v)raisesInvalidVersion.summarylonger than 500 characters is rejected.The model is frozen; assigning to a field after construction raises
pydantic.ValidationError.
Illustrative API-signature sketch (non-runnable)¶
from rfgen.core.registry import PluginMetadata
PLUGIN = PluginMetadata(
name="rfgen-myradar",
version="0.2.1",
family="radar",
summary="High-fidelity polyphase pulsed radar emitter for rfgen.",
requires=["torchsig"],
homepage="https://github.com/myorg/rfgen-myradar",
license="Apache-2.0",
)
class rfgen.core.registry.BaseCommand¶
from typing import Protocol, runtime_checkable
from pydantic import BaseModel
@runtime_checkable
class BaseCommand(Protocol):
"""Dispatch contract for services callable from the CLI."""
name: str
summary: str
params_schema: type[BaseModel]
def run(
self,
*,
params: BaseModel,
registry: BaseRegistry[object],
log_sink: object,
) -> int: ...
A runtime-checkable Protocol. Each service owns and registers its
BaseCommand implementation under
entry-point group rfgen.commands; the CLI dispatches to them by name
through the registry, never through build-time imports. Generation is owned by
rfgen.generation, annotation by its own Typer sub-app, and dataset
inspection by rfgen.inspection.audit. rfgen.commands is an entry-point
group, not a
Python module.
Attributes¶
name: canonical command name (used by the CLI for dispatch and listings).summary: one-line human description.params_schema: Pydantic model describing the parameter surface.
run(*, params, registry, log_sink)¶
params: validated parameters; an instance ofparams_schema.registry: the plugin registry the command should resolve names through.log_sink: structured-log sink, typed asobjecthere. The registry module cannot importBaseLogSinkwithout a circular dependency; the CLI dispatcher tightens the type where that import is already in scope.
Returns a process exit code from the closed set EXIT_CODES_VALID = frozenset({0, 1, 2, 3, 130}) (success, config error, runtime error, validation
failure, keyboard interrupt). Any other return value is a contract violation;
the contract test in validation-and-audit asserts every shipped command
returns a member of EXIT_CODES_VALID.
End-to-end: shipping a third-party emitter plugin¶
This is the canonical path from “I wrote an emitter” to “rfgen finds and
loads it.” No registry code in rfgen needs to change to support a new
plugin; only the plugin author’s pyproject.toml does.
2. They declare an entry point in their pyproject.toml¶
[project]
name = "rfgen-myradar"
version = "0.2.1"
[project.entry-points."rfgen.emitters"]
my_radar = "rfgen_myradar.emitter:MyRadarEmitter"
The entry-point group rfgen.emitters is one of the twenty-three documented
groups. The key (my_radar) is the canonical name the framework will
resolve through; the value is the dotted path to the factory.
3. A source-checkout user installs both packages¶
uv pip install -e . -e /path/to/rfgen-myradar
4. rfgen discovers it via EntryPointRegistry¶
from rfgen.core.registry import EntryPointRegistry
from rfgen.waveforms.base import BaseEmitter
emitters: EntryPointRegistry[BaseEmitter] = EntryPointRegistry("rfgen.emitters")
emitters.discover() # records 'my_radar' in the pending table
assert "my_radar" in emitters.available()
instance = emitters.get("my_radar") # this is the call that imports rfgen_myradar.emitter
assert isinstance(instance, BaseEmitter)
The discover() call walks the cached entry-point index, filters by group, and
records my_radar as pending. Importing rfgen_myradar.emitter does not
happen until get("my_radar") is called.
5. End users hit this path through config, not directly¶
In normal use, a user writes a YAML config that names the emitter as a string:
emitters:
- name: my_radar
params: {...}
Hydra parses the YAML, the Pydantic emitter config validates the string at instantiation time, and the framework resolves it through the same EntryPointRegistry instance. The user never calls get directly.
Lazy-import contract¶
import rfgen.core.registry does not import any plugin module. Even
registry.discover() does not. Only
get(name) triggers the import for that
specific entry. This keeps cold-start cost bounded by the number of plugins
the user actually resolves, not the number installed on the system.
Optional hook-based discovery via pluggy¶
pluggy is the optional second primitive, gated behind the
rfgen[plugin-hooks] extra. It is imported defensively in
rfgen.core.registry:
try:
import pluggy as _pluggy
_PLUGGY_AVAILABLE = True
except ImportError:
_PLUGGY_AVAILABLE = False
pluggy is the layered hook-mechanism behind pytest, tox, and
devpi. It complements
EntryPointRegistry for
discovery patterns that go beyond “one name, one factory”, for example, a
hook that lets multiple plugins register schema fragments under the same
config key, or one that lets plugins observe pipeline events. pluggy serves
the “observe annotation events” hook surface; entry points remain the primary
registration channel.
The base rfgen install does not require pluggy. Code that wants the
hook surface checks _PLUGGY_AVAILABLE and degrades cleanly when the extra
is not installed.
Why JsonManifestRegistry is intentionally omitted¶
JsonManifestRegistry, PrivateIndexRegistry, and CompositeRegistry are
not public registry implementations.
importlib.metadata.entry_points plus pluggy already cover wheel-time and
layered hook-based discovery. A manifest reader would grow maintenance
surface (a JSON schema, a fetcher, a cache, an offline-mode flag, a
validation pass on remote records) without adding capability the existing
two primitives do not already provide. Private corporate registries are
covered by configuring the local Python environment to install from a
private index (the standard pip workflow), at which point
EntryPointRegistry finds
the plugin like any other installed package.
The public registry contract is therefore entry-point discovery plus optional hook registration; no manifest-reader API is exposed.
See Also¶
Reference / Plugin Metadata: declaration site of
PLUGIN: PluginMetadataon plugin packages, plus thePLUGIN_CARD.mdconvention.Reference / Project layout: the package directories the entry-point groups resolve into. The list above is the authority for all twenty-three groups:
pyproject.tomldeclares twenty-one of them and the remaining two are declared in code. It declares built-in entries within those groups, but no group beyond them.BackendUnavailableError, PluginNotFoundError, RegistryError: the error surface this module raises.
Emitter API: the retained emitter contract for an installed plugin.
Legacy class names¶
Legacy: JsonManifestRegistry¶
Intentionally not shipped. See the “Why JsonManifestRegistry is intentionally
omitted” section above for the documented user-confirmed decision. Existing
cross-references (e.g., from docs/background/open-questions.md) resolve to
this anchor so the docs render; the class itself does not exist in
rfgen.core.registry.
Load a class or factory without constructing it¶
EntryPointRegistry.load_class(name: str) -> RegistryFactory[T] returns the
registered class or factory for name; it does not call the factory or return
an instance. Like get(name), it loads a pending entry point lazily before it
returns the registered target. It raises
PluginNotFoundError when the
name is not registered.
Use it when a caller must inspect a component type before construction. A caller can verify that a selected target implements the relevant emitter or channel ABC before instantiating it. Registry errors preserve the group, selector, and conflicting entry-point values in their error context.
Installed implementation ownership¶
implementation_identity returns the canonical module-qualified class name.
implementation_distribution resolves the single installed distribution that
owns that module, refusing ambiguous or absent production ownership. Graph
implementation bindings and bounded waveform execution share this primitive so
an unrelated installed package cannot be claimed as provenance.