Skip to content

factory

Base factory class for managing pluggable components in Merlin.

This module defines an abstract MerlinBaseFactory class that provides a reusable infrastructure for registering, discovering, and instantiating pluggable components. It supports alias resolution, entry-point-based plugin discovery, and runtime introspection of registered components.

Subclasses must define how to register built-in components, validate component classes, and identify the appropriate entry point group for plugin discovery.

MerlinBaseFactory

Bases: ABC

Abstract base factory for managing and instantiating pluggable components.

This class provides the infrastructure for: - Registering components and their aliases - Discovering plugins via Python entry points - Creating instances of registered components - Listing and introspecting available components

Subclasses are required to
  • Implement _register_builtins() to register default implementations
  • Implement _validate_component() to enforce interface/type constraints
  • Define _entry_point_group() to identify the entry point namespace for discovery

Attributes:

Name Type Description
_registry Dict[str, Any]

Maps canonical component names to their classes.

_aliases Dict[str, str]

Maps alias names to canonical component names.

Methods:

Name Description
register

Register a new component and its optional aliases.

list_available

Return a list of all registered component names.

create

Instantiate a registered component by name or alias.

get_component_info

Return introspection metadata for a registered component.

_discover_plugins

Discover and register plugin components using entry points.

_register_builtins

Abstract method for registering built-in/default components.

_validate_component

Abstract method for enforcing type/interface constraints.

_entry_point_group

Abstract method for returning the entry point namespace.

Source code in merlin/abstracts/factory.py
class MerlinBaseFactory(ABC):
    """
    Abstract base factory for managing and instantiating pluggable components.

    This class provides the infrastructure for:
    - Registering components and their aliases
    - Discovering plugins via Python entry points
    - Creating instances of registered components
    - Listing and introspecting available components

    Subclasses are required to:
        - Implement `_register_builtins()` to register default implementations
        - Implement `_validate_component()` to enforce interface/type constraints
        - Define `_entry_point_group()` to identify the entry point namespace for discovery

    Attributes:
        _registry (Dict[str, Any]): Maps canonical component names to their classes.
        _aliases (Dict[str, str]): Maps alias names to canonical component names.

    Methods:
        register: Register a new component and its optional aliases.
        list_available: Return a list of all registered component names.
        create: Instantiate a registered component by name or alias.
        get_component_info: Return introspection metadata for a registered component.
        _discover_plugins: Discover and register plugin components using entry points.
        _register_builtins: Abstract method for registering built-in/default components.
        _validate_component: Abstract method for enforcing type/interface constraints.
        _entry_point_group: Abstract method for returning the entry point namespace.
    """

    def __init__(self):
        """
        Initialize the base factory.

        This base class provides common functionality for managing
        a registry of available implementations and any aliases for them.
        Subclasses can extend this to register built-in or default items.
        """
        # Map canonical names to implementation classes or instances
        self._registry: Dict[str, Any] = {}

        # Map aliases to canonical names (e.g., legacy names or shorthand)
        self._aliases: Dict[str, str] = {}

        # Register built-in implementations, if any
        self._register_builtins()

    @abstractmethod
    def _register_builtins(self):
        """
        Register built-in components.

        Subclasses must implement this to register relevant components.
        """
        raise NotImplementedError("Subclasses of `MerlinBaseFactory` must implement a `_register_builtins` method.")

    @abstractmethod
    def _validate_component(self, component_class: Any):
        """
        Validate the component class before registration.

        Subclasses must implement this to enforce type or interface constraints.

        Args:
            component_class: The class to validate.

        Raises:
            TypeError: If `component_class` is not valid.
        """
        raise NotImplementedError("Subclasses of `MerlinBaseFactory` must implement a `_validate_component` method.")

    @abstractmethod
    def _entry_point_group(self) -> str:
        """
        Return the entry point group used for plugin discovery.

        Subclasses must override this.

        Returns:
            The entry point group used for plugin discovery.
        """
        raise NotImplementedError("Subclasses must define an entry point group.")

    def _discover_plugins_via_entry_points(self):
        """
        Discover and register plugins via Python entry points.
        """
        try:
            for entry_point in pkg_resources.iter_entry_points(self._entry_point_group()):
                try:
                    plugin_class = entry_point.load()
                    self.register(entry_point.name, plugin_class)
                    LOG.info(f"Loaded plugin via entry point: {entry_point.name}")
                except Exception as e:  # pylint: disable=broad-exception-caught
                    LOG.warning(f"Failed to load plugin '{entry_point.name}': {e}")
        except ImportError:
            LOG.debug("pkg_resources not available for plugin discovery")

    def _discover_plugins(self):
        """
        Discover and register plugin components via entry points.

        Subclasses can override this to support more discovery mechanisms.
        """
        self._discover_plugins_via_entry_points()

    def _raise_component_error_class(self, msg: str) -> Type[Exception]:
        """
        Raise an appropriate exception when an invalid component is requested.

        Subclasses should override this to raise more specific exceptions.

        Args:
            msg: The message to add to the error being raised.

        Raises:
            A subclass of Exception (e.g., ValueError by default).
        """
        raise ValueError(msg)

    def register(self, name: str, component_class: Any, aliases: List[str] = None) -> None:
        """
        Register a new component implementation.

        Args:
            name: Canonical name for the component.
            component_class: The class or implementation to register.
            aliases: Optional alternative names for this component.

        Raises:
            TypeError: If the component_class fails validation.
        """
        self._validate_component(component_class)

        self._registry[name] = component_class
        LOG.debug(f"Registered component: {name}")

        if aliases:
            for alias in aliases:
                self._aliases[alias] = name
                LOG.debug(f"Registered alias '{alias}' for component '{name}'")

    def list_available(self) -> List[str]:
        """
        Return a list of supported component names.

        This includes both built-in and dynamically discovered components.

        Returns:
            A list of canonical names for all available components.
        """
        self._discover_plugins()
        return list(self._registry.keys())

    def _get_component_class(self, canonical_name: str, component_type: str) -> Any:
        """
        Retrieve a registered component class by its canonical name.

        This method ensures that all plugin discovery mechanisms have been invoked
        before attempting to look up the component. If the requested component is
        not found in the registry, it raises a descriptive error with a list of
        available components.

        Args:
            canonical_name: The canonical name of the component (resolved from alias).
            component_type: The original name or alias provided by the user (used in error messages).

        Returns:
            The class object corresponding to the requested component.

        Raises:
            Exception: Raises the result of `_raise_component_error_class` if the component is not registered.
        """
        # Discover plugins if needed
        if canonical_name not in self._registry:
            self._discover_plugins()

        # Grab the component class from the registry and ensure it's supported
        component_class = self._registry.get(canonical_name)
        if component_class is None:
            available = ", ".join(self.list_available())
            self._raise_component_error_class(
                f"Component '{component_type}' is not supported. " f"Available components: {available}"
            )

        return component_class

    # TODO should we change 'config' to 'kwargs'?
    def create(self, component_type: str, config: Dict = None) -> Any:
        """
        Instantiate and return a component of the specified type.

        Args:
            component_type: The name or alias of the component to create.
            config: Optional configuration for initializing the component.

        Returns:
            An instance of the requested component.

        Raises:
            Exception: If the component is not registered or instantiation fails.
        """
        # Resolve alias
        canonical_name = self._aliases.get(component_type, component_type)

        # Get the class associated with the name
        component_class = self._get_component_class(canonical_name, component_type)

        # Create and return an instance of the component_class
        try:
            instance = component_class() if config is None else component_class(**config)
            LOG.info(f"Created component '{canonical_name}'")
            return instance
        except Exception as e:
            raise ValueError(f"Failed to create component '{canonical_name}': {e}") from e

    def get_component_info(self, component_type: str) -> Dict:
        """
        Get introspection information about a registered component.

        Args:
            component_type: The name or alias of the component.

        Returns:
            Dictionary containing metadata such as name, class, module, and docstring.

        Raises:
            Exception: If the component is not registered.
        """
        canonical_name = self._aliases.get(component_type, component_type)

        component_class = self._get_component_class(canonical_name, component_type)

        return {
            "name": canonical_name,
            "class": component_class.__name__,
            "module": component_class.__module__,
            "description": component_class.__doc__ or "No description available",
        }

__init__()

Initialize the base factory.

This base class provides common functionality for managing a registry of available implementations and any aliases for them. Subclasses can extend this to register built-in or default items.

Source code in merlin/abstracts/factory.py
def __init__(self):
    """
    Initialize the base factory.

    This base class provides common functionality for managing
    a registry of available implementations and any aliases for them.
    Subclasses can extend this to register built-in or default items.
    """
    # Map canonical names to implementation classes or instances
    self._registry: Dict[str, Any] = {}

    # Map aliases to canonical names (e.g., legacy names or shorthand)
    self._aliases: Dict[str, str] = {}

    # Register built-in implementations, if any
    self._register_builtins()

create(component_type, config=None)

Instantiate and return a component of the specified type.

Parameters:

Name Type Description Default
component_type str

The name or alias of the component to create.

required
config Dict

Optional configuration for initializing the component.

None

Returns:

Type Description
Any

An instance of the requested component.

Raises:

Type Description
Exception

If the component is not registered or instantiation fails.

Source code in merlin/abstracts/factory.py
def create(self, component_type: str, config: Dict = None) -> Any:
    """
    Instantiate and return a component of the specified type.

    Args:
        component_type: The name or alias of the component to create.
        config: Optional configuration for initializing the component.

    Returns:
        An instance of the requested component.

    Raises:
        Exception: If the component is not registered or instantiation fails.
    """
    # Resolve alias
    canonical_name = self._aliases.get(component_type, component_type)

    # Get the class associated with the name
    component_class = self._get_component_class(canonical_name, component_type)

    # Create and return an instance of the component_class
    try:
        instance = component_class() if config is None else component_class(**config)
        LOG.info(f"Created component '{canonical_name}'")
        return instance
    except Exception as e:
        raise ValueError(f"Failed to create component '{canonical_name}': {e}") from e

get_component_info(component_type)

Get introspection information about a registered component.

Parameters:

Name Type Description Default
component_type str

The name or alias of the component.

required

Returns:

Type Description
Dict

Dictionary containing metadata such as name, class, module, and docstring.

Raises:

Type Description
Exception

If the component is not registered.

Source code in merlin/abstracts/factory.py
def get_component_info(self, component_type: str) -> Dict:
    """
    Get introspection information about a registered component.

    Args:
        component_type: The name or alias of the component.

    Returns:
        Dictionary containing metadata such as name, class, module, and docstring.

    Raises:
        Exception: If the component is not registered.
    """
    canonical_name = self._aliases.get(component_type, component_type)

    component_class = self._get_component_class(canonical_name, component_type)

    return {
        "name": canonical_name,
        "class": component_class.__name__,
        "module": component_class.__module__,
        "description": component_class.__doc__ or "No description available",
    }

list_available()

Return a list of supported component names.

This includes both built-in and dynamically discovered components.

Returns:

Type Description
List[str]

A list of canonical names for all available components.

Source code in merlin/abstracts/factory.py
def list_available(self) -> List[str]:
    """
    Return a list of supported component names.

    This includes both built-in and dynamically discovered components.

    Returns:
        A list of canonical names for all available components.
    """
    self._discover_plugins()
    return list(self._registry.keys())

register(name, component_class, aliases=None)

Register a new component implementation.

Parameters:

Name Type Description Default
name str

Canonical name for the component.

required
component_class Any

The class or implementation to register.

required
aliases List[str]

Optional alternative names for this component.

None

Raises:

Type Description
TypeError

If the component_class fails validation.

Source code in merlin/abstracts/factory.py
def register(self, name: str, component_class: Any, aliases: List[str] = None) -> None:
    """
    Register a new component implementation.

    Args:
        name: Canonical name for the component.
        component_class: The class or implementation to register.
        aliases: Optional alternative names for this component.

    Raises:
        TypeError: If the component_class fails validation.
    """
    self._validate_component(component_class)

    self._registry[name] = component_class
    LOG.debug(f"Registered component: {name}")

    if aliases:
        for alias in aliases:
            self._aliases[alias] = name
            LOG.debug(f"Registered alias '{alias}' for component '{name}'")