Source code for interpretune.config.extensions

from typing import Any, Callable, NamedTuple
from dataclasses import field, make_dataclass, dataclass

from interpretune.config import ITSerializableCfg
from interpretune.utils import MisconfigurationException, _import_class, _NEURONPEDIA_AVAILABLE


[docs] class ITExtension(NamedTuple): ext_attr: str ext_cls_fqn: str ext_cfg_fqn: str
@dataclass class ExtensionsContext: """Context for managing extensions in Interpretune. This class provides a structured way to handle extensions, including their configuration and instantiation. It allows for easy integration of new extensions into the Interpretune framework. """ SUPPORTED_EXTENSIONS: dict[str, Callable] = field(default_factory=dict) SUPPORTED_EXTENSION_CFGS: dict[str, Any] = field(default_factory=dict) BASE_EXTENSIONS: tuple = ( ITExtension( "debug_lm", "interpretune.extensions.debug_generation.DebugGeneration", "interpretune.extensions.debug_generation.DebugLMConfig", ), ITExtension( "memprofiler", "interpretune.extensions.memprofiler.MemProfiler", "interpretune.extensions.memprofiler.MemProfilerCfg", ), ) OPTIONAL_EXTENSIONS: list = field(default_factory=list) DEFAULT_EXTENSIONS: tuple = field(init=False) def __post_init__(self): if _NEURONPEDIA_AVAILABLE: self.OPTIONAL_EXTENSIONS.append( ITExtension( "neuronpedia", "interpretune.extensions.neuronpedia.NeuronpediaIntegration", "interpretune.extensions.neuronpedia.NeuronpediaConfig", ) ) self.DEFAULT_EXTENSIONS = self.BASE_EXTENSIONS + tuple(self.OPTIONAL_EXTENSIONS) class ITExtensionsConfigMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.extensions_context = ExtensionsContext() def _detect_extensions(self): # TODO: update custom extensions to be read/added from it_cfg once interface stabilizes for it_ext in self.extensions_context.DEFAULT_EXTENSIONS: try: ext_class = _import_class(it_ext.ext_cls_fqn) ext_cfg_class = _import_class(it_ext.ext_cfg_fqn) except (ImportError, AttributeError) as e: err_msg = ( f"Unable to import and resolve specified extension and/or its configuration class from" f" `ext_cls_fqn`: {it_ext.ext_cls_fqn}, `ext_cfg_fqn`: {it_ext.ext_cfg_fqn}, error: {e}" ) raise MisconfigurationException(err_msg) self.extensions_context.SUPPORTED_EXTENSIONS[it_ext.ext_attr] = ext_class self.extensions_context.SUPPORTED_EXTENSION_CFGS[it_ext.ext_attr] = ext_cfg_class def _connect_extensions(self): self._detect_extensions() for ext_name, ext_class in self.extensions_context.SUPPORTED_EXTENSIONS.items(): if getattr(self.it_cfg, f"{ext_name}_cfg").enabled: self._it_state._extensions[ext_name] = ext_class() getattr(self, ext_name).connect(self) else: self._it_state._extensions[ext_name] = None def __getattr__(self, name: str) -> Any: # we make extension handles available as direct root module attributes for convenience # filter only the supported extension attributes, ensuring both `extensions_context` and `_it_state` have # been initialized (guard against __getattr__ calls during cooperative __init__ before extensions_context # is set, which would otherwise cause infinite recursion) if ( "extensions_context" in self.__dict__ and self.extensions_context.SUPPORTED_EXTENSIONS.get(name) and (ext_attrs := self.__dict__.get("_it_state", None)) is not None ): return ext_attrs._extensions[name] return super().__getattr__(name) # type: ignore[attr-defined] # MRO pattern for mixins # TODO: rather than load extensions from DEFAULT_EXTENSIONS, use a registry and the entry_point API to potentially load # external extensions if ever advertising such an API supported_ext_cfgs = [] TMP_EXT_REGISTRY = ITExtensionsConfigMixin() TMP_EXT_REGISTRY._detect_extensions() for ext_attr, ext_cfg_cls in TMP_EXT_REGISTRY.extensions_context.SUPPORTED_EXTENSION_CFGS.items(): supported_ext_cfgs.append((f"{ext_attr}_cfg", ext_cfg_cls, field(default_factory=ext_cfg_cls))) ExtensionConf = make_dataclass("ExtensionsConf", supported_ext_cfgs, bases=(ITSerializableCfg,), kw_only=True)