Source code for fairdm.registry.registry

"""The registry of Sample and Measurement models, and the ``register`` decorator."""

import inspect
from typing import TYPE_CHECKING

from django.apps import apps
from django.contrib import admin
from django.db.models import Model

if TYPE_CHECKING:
    from fairdm.core.measurement.models import Measurement
    from fairdm.core.sample.models import Sample

from fairdm.registry.config import ModelConfiguration


def _caller_location() -> str:
    """Return the module and qualified name that called into the registry.

    Import order decides which registration of a model arrives first, and that
    order is not visible from either file, so a duplicate-registration error has to
    say where the first one was.

    Returns:
        The caller as ``module.qualname``, the bare module for module-level code,
        or ``"an unknown module"`` when no caller outside the registry is found.
    """
    frame = inspect.currentframe()
    try:
        while frame is not None:
            module = frame.f_globals.get("__name__", "")
            if not module.startswith("fairdm.registry"):
                name = frame.f_code.co_qualname
                return f"{module}.{name}" if name != "<module>" else module
            frame = frame.f_back
        return "an unknown module"
    finally:
        del frame


[docs] class FairDMRegistry: """Registry of Sample and Measurement models and their configurations. A model is registered with a configuration class that auto-generates forms, serializers, filters and tables when they are not given explicitly. Usage: @fairdm.register class MySampleConfig: model = MySample display_name = "Water Sample" list_fields = ["name", "location", "collected_at"] detail_fields = ["name", "description", "metadata"] filter_fields = ["collected_at", "contributor"] """ def __init__(self) -> None: self._registry: dict[type[Model], ModelConfiguration] = {} # Where each model was registered from, so a duplicate can name the first. self._locations: dict[type[Model], str] = {} def _validate_model_is_registrable(self, model_class: type[Model]) -> None: """Require a concrete subclass of Sample or Measurement. Registering a polymorphic base would generate six components for a class no portal stores rows in, and register a second admin against it. Args: model_class: The model to check. Raises: ConfigurationError: The model is abstract, is one of the two polymorphic bases, or is not a subclass of either. """ from fairdm.registry.exceptions import ConfigurationError if model_class._meta.abstract: raise ConfigurationError( f"{model_class.__name__} is abstract. Only a concrete model can be " f"registered." ) from fairdm.core.measurement.models import Measurement from fairdm.core.sample.models import Sample if model_class in (Sample, Measurement): raise ConfigurationError( f"{model_class.__name__} is a polymorphic base class. Register a " f"concrete subclass of it instead." ) if not issubclass(model_class, (Sample, Measurement)): raise ConfigurationError( f"{model_class.__name__} must be a concrete subclass of " f"fairdm.core.sample.models.Sample or " f"fairdm.core.measurement.models.Measurement", model=model_class, )
[docs] def get_for_model(self, model_reference: type[Model] | str) -> ModelConfiguration: """Return the registered configuration for a model. Args: model_reference: A Django model class, or a string in the format ``"app_label.model_name"`` as accepted by ``apps.get_model``. Use the Django app label and the lowercase model name, for example ``"sample.sample"`` for the core Sample model. Returns: The configuration instance for the model. Raises: NotRegisteredError: The model is not registered. It is also a ``KeyError``. ValueError: The string is not in the format ``"app_label.model_name"``. LookupError: The model cannot be found in the Django apps. Example: config = registry.get_for_model(MySample) config = registry.get_for_model("myapp.mysample") """ if isinstance(model_reference, str): try: app_label, model_name = model_reference.split(".", 1) except ValueError as err: raise ValueError( f"Invalid model reference format '{model_reference}'. Expected 'app_label.model_name'" ) from err try: model_cls = apps.get_model(app_label, model_name) except LookupError as err: raise LookupError( f"Model '{model_reference}' not found in Django apps" ) from err else: model_cls = model_reference if model_cls not in self._registry: from fairdm.registry.exceptions import NotRegisteredError raise NotRegisteredError(model_cls) return self._registry[model_cls]
[docs] def is_registered(self, model_reference: type[Model] | str) -> bool: """Report whether a model is registered. Args: model_reference: A Django model class, or a string in the format ``"app_label.model_name"``. Returns: ``True`` if the model is registered, ``False`` otherwise, including when the reference does not resolve to a model. Example: if registry.is_registered("myapp.mysample"): ... """ try: self.get_for_model(model_reference) return True # noqa: TRY300 except (KeyError, ValueError, LookupError): return False
@property def samples(self) -> list[type["Sample"]]: """Return the registered Sample model classes.""" from fairdm.core.sample.models import Sample return [model for model in self._registry if issubclass(model, Sample)] @property def measurements(self) -> list[type["Measurement"]]: """Return the registered Measurement model classes.""" from fairdm.core.measurement.models import Measurement return [model for model in self._registry if issubclass(model, Measurement)] @property def models(self) -> list[type[Model]]: """Return every registered model class, Samples and Measurements together.""" return list(self._registry.keys())
[docs] def get_all_configs(self) -> list[ModelConfiguration]: """Return every registered configuration. Returns: The ``ModelConfiguration`` instances in registration order. Example: for config in registry.get_all_configs(): print(config.model.__name__, config.fields) """ return list(self._registry.values())
[docs] def register( self, model_class: type[Model], config: ModelConfiguration | None = None ) -> None: """Register a Sample or Measurement subclass with its configuration. A model that is not a concrete Sample or Measurement subclass raises ``ConfigurationError``. Args: model_class: The model class to register. config: The configuration instance for the model. A default one is built when omitted. Raises: DuplicateRegistrationError: The model is already registered. """ from fairdm.registry.exceptions import DuplicateRegistrationError self._validate_model_is_registrable(model_class) if model_class in self._registry: raise DuplicateRegistrationError( model=model_class, original_location=self._locations.get(model_class, "an unknown module"), new_location=_caller_location(), ) config_instance = self.get_config(model_class, config) self.register_admin(model_class, config_instance) self._registry[model_class] = config_instance self._locations[model_class] = _caller_location()
[docs] def register_admin( self, model_class: type[Model], config_instance: ModelConfiguration ) -> None: """Register the model's admin class with the Django admin site. A model already present in the admin site is left alone. A portal that wrote ``@admin.register(RockSample)`` has said which admin class it wants, and the registry does not overrule that. Autodiscovery runs before registration, so this is the normal path for any portal with a hand-written admin. Every other failure propagates. Args: model_class: The model to register. config_instance: The configuration that supplies the admin class. """ if model_class in admin.site._registry: return admin.site.register(model_class, config_instance.get_admin_class())
[docs] def get_config( self, model_class: type[Model], config: ModelConfiguration | type[ModelConfiguration] | None = None, ) -> ModelConfiguration: """Build the configuration instance for a model. Args: model_class: The Django model class. config: A configuration class, an instance, or ``None`` for the registered configuration or a default one. Returns: The configuration instance. """ if config is None: if model_class in self._registry: return self._registry[model_class] return ModelConfiguration(model_class) if isinstance(config, type): return config(model_class) return config
registry = FairDMRegistry()
[docs] def register(config_cls: type) -> type: """Register a Sample or Measurement model with its configuration. The configuration class must have a ``model`` attribute pointing to the Sample or Measurement subclass to register. A model that is not a concrete subclass of either raises ``ConfigurationError``. Usage: @fairdm.register class MySampleConfig(SampleConfig): model = MySample display_name = "My Sample Type" list_fields = ["name", "created"] Args: config_cls: The configuration class to register. Returns: The configuration class, unchanged. Raises: ValueError: The configuration class has no ``model`` attribute. """ if not hasattr(config_cls, "model") or not config_cls.model: raise ValueError( f"Configuration class {config_cls.__name__} must specify a 'model' attribute " f"pointing to the Sample or Measurement subclass to register" ) model_class = config_cls.model config_instance = config_cls() if isinstance(config_cls, type) else config_cls registry.register(model_class, config_instance) return config_cls
__all__ = [ "FairDMRegistry", "register", "registry", ]