mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-18 14:27:17 +02:00
Every plug-and-play surface carried its own "import the modules beside me and collect the subclasses" loop: mcp tools, toolbox tools, the two scanner directories, and the stage, section, provider and connector registries. shared/plugins.py holds it once, as classes_in_modules for a directory of modules and classes_in_packages for a directory of sub-packages; each registry keeps its own naming and duplicate rules. The copies had drifted. stages/dast_scan checked obj.__module__ so a scanner imported from another module is not registered twice; stages/vulnerability_scan did not, and would have registered the wrong class the day a second scanner lands. Both now hold the guard. The scanner copies also suppressed ModuleNotFoundError in silence, so a typo inside a scanner read as "not installed" rather than an error. Every skipped module is logged. Registry contents are unchanged: stages 27, sections 30, providers 3, connectors 1, mcp tools 26, toolbox tools 6, vuln scanners 1, dast scanners 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
86 lines
2.7 KiB
Python
86 lines
2.7 KiB
Python
"""Plug-and-play discovery: the classes a directory of modules or sub-packages declares."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from collections.abc import Collection, Sequence
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
from shared.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
SKIP = ("base",)
|
|
|
|
|
|
def _import(name: str) -> ModuleType | None:
|
|
try:
|
|
return importlib.import_module(name)
|
|
except Exception as exc:
|
|
logger.warning("plugin module skipped", module=name, error=str(exc))
|
|
return None
|
|
|
|
|
|
def _declared[T](namespace: ModuleType, base: type[T]) -> list[type[T]]:
|
|
"""The subclasses this module declares itself, never the ones it imports."""
|
|
out = []
|
|
for obj in vars(namespace).values():
|
|
if (
|
|
isinstance(obj, type)
|
|
and issubclass(obj, base)
|
|
and obj is not base
|
|
and obj.__module__ == namespace.__name__
|
|
and not getattr(obj, "__abstractmethods__", None)
|
|
):
|
|
out.append(obj)
|
|
return out
|
|
|
|
|
|
def classes_in_modules[T](
|
|
package: str, directory: Path, base: type[T], *, skip: Collection[str] = SKIP
|
|
) -> list[type[T]]:
|
|
"""Every `base` subclass declared by a module in `directory`."""
|
|
found: list[type[T]] = []
|
|
for module in sorted(directory.glob("*.py")):
|
|
if module.stem.startswith("_") or module.stem in skip:
|
|
continue
|
|
namespace = _import(f"{package}.{module.stem}")
|
|
if namespace is not None:
|
|
found.extend(_declared(namespace, base))
|
|
return found
|
|
|
|
|
|
def classes_in_packages[T](
|
|
package: str,
|
|
directory: Path,
|
|
base: type[T],
|
|
*,
|
|
submodules: Sequence[str] = (),
|
|
) -> list[type[T]]:
|
|
"""Every `base` subclass declared by a sub-package of `directory`, or by `submodules` of one."""
|
|
found: list[type[T]] = []
|
|
for entry in sorted(directory.iterdir()):
|
|
if not entry.is_dir() or entry.name.startswith(("_", ".")):
|
|
continue
|
|
for suffix in ("", *submodules):
|
|
name = f"{package}.{entry.name}{'.' + suffix if suffix else ''}"
|
|
namespace = _import_optional(name)
|
|
if namespace is not None:
|
|
found.extend(_declared(namespace, base))
|
|
return found
|
|
|
|
|
|
def _import_optional(name: str) -> ModuleType | None:
|
|
"""A sub-package need not carry every module name a registry looks for."""
|
|
try:
|
|
return importlib.import_module(name)
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name == name:
|
|
return None
|
|
logger.warning("plugin module skipped", module=name, error=str(exc))
|
|
return None
|
|
except Exception as exc:
|
|
logger.warning("plugin module skipped", module=name, error=str(exc))
|
|
return None
|