Source code for virtual_ecosystem.core.registry

"""The :mod:`~virtual_ecosystem.core.registry` module is used to populate the
:data:`~virtual_ecosystem.core.registry.MODULE_REGISTRY`.

The registry is a dictionary, keyed using the short names of models, such as ``core`` or
``plants``. Each entry provides a :class:`~virtual_ecosystem.core.registry.ModuleInfo`
dataclass, which provides the BaseModel subclass for each model and its configuration
model. The ``core`` model has a configuration model but has no BaseModel subclass.

The module also provides the :func:`~virtual_ecosystem.core.registry.register_module`
function, which is used to populate the registry with the components of a given module.
"""  # noqa: D205

from dataclasses import dataclass
from importlib import import_module
from typing import Any

from virtual_ecosystem.core.base_model import to_camel_case
from virtual_ecosystem.core.configuration import Configuration
from virtual_ecosystem.core.logger import LOGGER


[docs] @dataclass class ModuleInfo: """Dataclass for module information. This dataclass holds references to Base subclasses and configuration class for a model or disturbance and is used to hold that information with the corresponding registry. Note that the :mod:`virtual_ecosystem.core` module does not have an associated BaseModel subclass and the ``model`` attribute for the ``core`` module will be None. """ # FIXME The typing below for model should be `None | type[BaseModel]`, but this is # circular. When core.base_model is imported, that imports core.config.Config, which # imports core.registry, which would then need to import core.base_model to use this # type. Not sure how to break out of this one, so for the moment, leaving as Any. # # Actually, model can be a BaseModel or a BaseDisturbance, so probably Any is OK. model: Any """The Base subclass associated with the module.""" config: type[Configuration] """A Configuration subclass that provides a pydantic model to populate and validate the model configuration.""" is_core: bool """Logical flag indicating if an instance contains registration information for the core module."""
MODULE_REGISTRY: dict[str, ModuleInfo] = {} """The global module registry. As each module is registered using :func:`~virtual_ecosystem.core.registry.register_module`, a :class:`~virtual_ecosystem.core.registry.ModuleInfo` dataclass will be added to this registry using the short name of the module being registered. """ DISTURBANCE_REGISTRY: dict[str, ModuleInfo] = {} """The global disturbance module registry. As each module is registered using :func:`~virtual_ecosystem.core.registry.register_disturbance`, a :class:`~virtual_ecosystem.core.registry.ModuleInfo` dataclass will be added to this registry using the short name of the module being registered. """
[docs] def register_module(module_name: str) -> None: """Register module components. This function loads the main :func:`~virtual_ecosystem.core.base_model.BaseModel` subclass for a module and the root configuration object for a module. It then adds a :class:`~virtual_ecosystem.core.registry.ModuleInfo` dataclass instance to the :data:`~virtual_ecosystem.core.registry.MODULE_REGISTRY` containing references to those classes. The :mod:`~virtual_ecosystem.core` module does not have an associated module. This function is primarily used within the :meth:`~virtual_ecosystem.core.config_builder.generate_configuration` method to register the components required to validate and setup the model configuration for a particular simulation. Args: module_name: The full name of the module to be registered (e.g. 'virtual_ecosystem.models.animal'). Raises: RuntimeError: if the requested module cannot be found or where a module does not provide a single subclass of the :class:`~virtual_ecosystem.core.base_model.BaseModel` class. """ from virtual_ecosystem.core.base_model import BaseModel _register_module( module_name, MODULE_REGISTRY, BaseModel, # type: ignore[type-abstract] )
[docs] def register_disturbance(module_name: str) -> None: """Register module components. This function loads the :func:`~virtual_ecosystem.core.base_model.BaseDisturbance` subclass for a module and its root configuration object. It then adds a :class:`~virtual_ecosystem.core.registry.ModuleInfo` dataclass instance to the :data:`~virtual_ecosystem.core.registry.DISTURBANCE_REGISTRY` containing references to those classes. The :mod:`~virtual_ecosystem.core` module does not have an associated module. This function is primarily used within the :meth:`~virtual_ecosystem.core.config_builder.generate_configuration` method to register the components required to validate and setup the model configuration for a particular simulation. Args: module_name: The full name of the module to be registered (e.g. 'virtual_ecosystem.disturbances.logging'). Raises: RuntimeError: if the requested module cannot be found or where a module does not provide a single subclass of the :class:`~virtual_ecosystem.core.base_model.BaseDisturbance` class. Exception: other exceptions can occur when loading the JSON schema fails. """ from virtual_ecosystem.core.base_model import BaseDisturbance _register_module( module_name, DISTURBANCE_REGISTRY, BaseDisturbance, # type: ignore[type-abstract] )
def _register_module[T]( module_name: str, registry: dict[str, ModuleInfo], of_type: type[T], ) -> None: """Internal function to register models and disturbances. Args: module_name: The full name of the module to be registered (e.g. 'virtual_ecosystem.model.animal'). registry: The registry to use to register this module. of_type: The type of the model that should be expected within the module (e.g. BaseModel). Raises: RuntimeError: if the requested module cannot be found or where a module does not provide a single subclass of the required parent class. Exception: other exceptions can occur when loading the JSON schema fails. """ # Extract the last component of the module name to act as unique short name module_name_short = module_name.rpartition(".")[-1] if module_name_short in registry: LOGGER.warning(f"Module already registered: {module_name}") return LOGGER.info(f"Registering module: {module_name}") if module_name_short == "core": is_core = True model = None else: is_core = False model = _get_model(module_name, module_name_short, of_type) # Find and register the model configuration model_config_class = get_model_configuration_class( module_name=module_name, module_name_short=module_name_short ) LOGGER.info("Configuration class registered for %s", module_name) registry[module_name_short] = ModuleInfo( model=model, config=model_config_class, is_core=is_core, ) def _get_model[T]( module_name: str, module_name_short: str, of_type: type[T], ): """Get the main model class for a model. Model classes are discovered by name, following the pattern below: * ``models.plants`` -> ``models.plants.plants_model.PlantsModel`` * ``models.abiotic_simple`` -> ``models.abiotic_simple.abiotic_simple_model.AbioticSimpleModel`` Args: module_name: The full module name (e.g. ``virtual_ecosystem.models.plants``) module_name_short: The short module name (e.g ``plants``) of_type: The type of the model that should be expected within the module (e.g. BaseModel). """ # Try and import the submodule containing the model model_submodule_name = module_name + f".{module_name_short}_model" try: module = import_module(model_submodule_name) except ModuleNotFoundError as excep: LOGGER.critical(f"Registration failed, cannot import {model_submodule_name}") raise excep # Try and get the model by name try: expected_model_name = to_camel_case(module_name_short) + "Model" model = getattr(module, expected_model_name) except AttributeError: raise RuntimeError( f"The {model_submodule_name} module does " f"not define the {expected_model_name} class." ) # Raises a runtime error if the retrieved class is not a Configuration. if not issubclass(model, of_type): raise RuntimeError(f"Model is not a BaseModel subclass: {expected_model_name}") # Trap models that do not follow the requirement that the BaseModel.model_name # attribute matches the virtual_ecosystem.models.model_name # TODO - can we retire the model_name attribute if it just duplicates the module # name or force it to match programmatically. if module_name_short != model.model_name: msg = f"Different model_name attribute and module name {module_name}" LOGGER.critical(msg) raise RuntimeError(msg) # Register the resulting single model class LOGGER.info(f"Registering model class for {module_name}: {model.__name__}") return model
[docs] def get_model_configuration_class(module_name: str, module_name_short: str): """Get the root configuration class for a model. Discovery is name based, with the function attempting to retrieve a class based on the model short name: * ``plants`` -> ``PlantsConfiguration``, * ``abiotic_simple`` -> ``AbioticSimpleConfiguration`` .. TODO: It would probably be cleaner and more flexible to use explicit setting of the configuration model name as a class attribute on the model definition, but that requires more changes in the BaseModel and definitions, so use this string pattern is being used to keep the rollout of the new config system simpler. Args: module_name: The full module name (e.g. ``virtual_ecosystem.models.plants``) module_name_short: The short module name (e.g ``plants``) """ try: # Raise ModuleNotFound if the configuration module is missing config_submodule = import_module(f"{module_name}.model_config") # Raises Attribute error if the expected name is not found. expected_config_class_name = ( "".join(word.capitalize() for word in module_name_short.split("_")) + "Configuration" ) model_config_class = getattr(config_submodule, expected_config_class_name) # Raises a runtime error if the retrieved class is not a Configuration. if not issubclass(model_config_class, Configuration): raise RuntimeError except ModuleNotFoundError: raise RuntimeError( f"Model {module_name} does not provide a model_config submodule." ) except AttributeError: raise RuntimeError( f"The {module_name}.model_config module does " f"not contain {expected_config_class_name}" ) except RuntimeError: raise RuntimeError( f"Model {module_name} config class does does inherit from `ConfigRoot`." ) return model_config_class