Skip to content

Tool Auto-Discovery

Convention Over Configuration

codemol discovers tools automatically — no registration, no manifest file. Drop a Python file with a run() function into the right directory and it's available immediately.

How It Works

discover_tools() in tool_loader.py uses pkgutil.iter_modules():

import pkgutil
import importlib
from pathlib import Path

def discover_tools() -> dict[str, dict[str, object]]:
    tools_dir = Path(__file__).parent.parent / "tools"
    result = {}

    for group_info in pkgutil.iter_modules([str(tools_dir)]):
        if not group_info.ispkg:
            continue
        group_name = group_info.name
        group_path = tools_dir / group_name
        result[group_name] = {}

        for tool_info in pkgutil.iter_modules([str(group_path)]):
            if tool_info.ispkg:
                continue
            module = importlib.import_module(f"tools.{group_name}.{tool_info.name}")
            if hasattr(module, "run"):
                result[group_name][tool_info.name] = module

    return result

Discovery Flow

graph TD
    A[tools/] --> B{Scan directories}
    B --> C[tools/io/]
    B --> D[tools/measurements/]
    B --> E[tools/representations/]
    B --> F[...]

    C --> G{Scan .py files}
    G --> H[load.py]
    G --> I[clear.py]

    H --> J{Has run()?}
    J -->|Yes| K["tools['io']['load'] = module"]
    J -->|No| L[Skip]

The Contract

A valid tool module must have:

  1. A file in tools/<group>/ (not a subdirectory)
  2. A run() function as a module-level attribute

That's it. Everything else (docstrings, type hints, default values) is optional but recommended.

Argument Fitting: _fit_args()

Since the parser splits on spaces, multi-word selections get split into multiple args. _fit_args() uses inspect to examine the tool's run() signature and joins excess args:

import inspect

def _fit_args(func, args):
    sig = inspect.signature(func)
    params = [p for p in sig.parameters.values()
              if p.name != "cmd"]  # skip cmd parameter

    # If tool accepts *args, pass through
    if any(p.kind == p.VAR_POSITIONAL for p in params):
        return args

    # If more args than params, join extras into last param
    max_positional = len(params)
    if len(args) > max_positional and max_positional > 0:
        fitted = args[:max_positional - 1]
        fitted.append(" ".join(args[max_positional - 1:]))
        return fitted

    return args

Benefits of Auto-Discovery

Benefit Description
Zero boilerplate No registration code needed
Instant availability Drop file → command works
Natural grouping Directory = tool group
Easy navigation File name = command name
Testable conftest.py discovers all tools for parametrized tests