Source code for rbfenetmap.plugins.intermediates
"""Built-in intermediate generators and their registry.
The fifth plugin kind. Same shape as :mod:`rbfenetmap.plugins.mappers` -- a table of
:class:`~rbfenetmap.core.pluginregistry.PluginSpec` metadata, availability probed without
importing anything, and a PEP 562 ``__getattr__`` so an implementation class can be
imported by name without pulling its backend in for everyone else.
"""
from __future__ import annotations
from importlib import import_module
from typing import Any
from rbfenetmap.core.exceptions import PluginError
from rbfenetmap.core.pluginregistry import PluginRegistry, PluginSpec
__all__ = (
"BUILTIN_INTERMEDIATES",
"INTERMEDIATE_PROFILES",
"available_intermediates",
"create_intermediate",
"create_intermediate_registry",
"list_active_intermediates",
"register_intermediates",
"require_intermediates",
)
_KIND = "intermediate"
_MODULE_MAP = {
"FragmentSwapGenerator": "rbfenetmap.plugins.intermediates.fragment_swap",
"PairMapGenerator": "rbfenetmap.plugins.intermediates.pairmap_generator",
}
BUILTIN_INTERMEDIATES: dict[str, PluginSpec] = {
"fragment-swap": PluginSpec(
name="fragment-swap",
kind=_KIND,
target="rbfenetmap.plugins.intermediates.fragment_swap:FragmentSwapGenerator",
description="Hybrids swapping one substituent at a time between the two parents.",
requires=("rdkit", "numpy"),
),
"pairmap": PluginSpec(
name="pairmap",
kind=_KIND,
target="rbfenetmap.plugins.intermediates.pairmap_generator:PairMapGenerator",
description="Searched subnetwork of R-group recombinations, after Furui et al. (PairMap, JCIM 2025).",
requires=("rdkit", "numpy"),
),
}
INTERMEDIATE_PROFILES: dict[str, tuple[str, ...]] = {
"all": tuple(BUILTIN_INTERMEDIATES),
"core": ("fragment-swap", "pairmap"),
}
[docs]
def available_intermediates() -> dict[str, PluginSpec]:
"""Return the built-in generators whose requirements are importable."""
return {name: spec for name, spec in BUILTIN_INTERMEDIATES.items() if spec.available}
[docs]
def register_intermediates(registry: PluginRegistry, names: tuple[str, ...] | None = None) -> PluginRegistry:
"""Register the named generators (default: all built-ins) into *registry*."""
for name in names or tuple(BUILTIN_INTERMEDIATES):
try:
registry.register(BUILTIN_INTERMEDIATES[name])
except KeyError:
raise PluginError(
f"Unknown built-in intermediate generator {name!r}. Known: {sorted(BUILTIN_INTERMEDIATES)}."
) from None
return registry
[docs]
def create_intermediate_registry(profile: str = "all") -> PluginRegistry:
"""Return a registry with the generators of *profile* registered and activated."""
if profile not in INTERMEDIATE_PROFILES:
raise PluginError(f"Unknown intermediate profile {profile!r}. Known: {sorted(INTERMEDIATE_PROFILES)}.")
registry = register_intermediates(PluginRegistry(), INTERMEDIATE_PROFILES[profile])
for name in INTERMEDIATE_PROFILES[profile]:
registry.activate(name, _KIND)
return registry
[docs]
def create_intermediate(name: str, profile: str = "all", **kwargs: Any) -> Any:
"""Instantiate the intermediate generator *name*.
Raises
------
rbfenetmap.core.exceptions.PluginError
If the generator is unknown or its backend is not installed.
"""
return create_intermediate_registry(profile).create(name, _KIND, **kwargs)
[docs]
def list_active_intermediates(profile: str = "all") -> list[str]:
"""Return the names of the generators in *profile* that can actually be created."""
registry = create_intermediate_registry(profile)
return sorted(spec.name for spec in registry.list_plugins(_KIND, active_only=True) if spec.available)
[docs]
def require_intermediates(names: tuple[str, ...], profile: str = "all") -> None:
"""Raise unless every generator in *names* is available.
Raises
------
rbfenetmap.core.exceptions.PluginError
Naming the unavailable generators and the modules each is missing.
"""
available = available_intermediates()
missing = {n: BUILTIN_INTERMEDIATES[n].missing_requirements for n in names if n not in available}
if missing:
detail = "; ".join(f"{n} needs {list(mods)}" for n, mods in sorted(missing.items()))
raise PluginError(f"Required intermediate generator(s) unavailable: {detail}.")
def __getattr__(name: str) -> Any:
"""Import generator classes lazily (PEP 562)."""
if name not in _MODULE_MAP:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
return getattr(import_module(_MODULE_MAP[name]), name)