Skip to content

How Tools Work

The run() Contract

Every tool is a Python module with a run() function:

def run(cmd, arg1: str, arg2: str = "default") -> str:
    """Execute the tool and return a result message."""
    # Use cmd.* to call the rendering API
    result = cmd.some_function(arg1, arg2)
    return f"Result: {result}"

Parameters

Parameter Description
cmd cmd module instance (or None in dry-run mode)
Positional args Tool-specific arguments (selections, values, names)
Return value String message displayed in the console

Minimal Example: tools/measurements/distance.py

"""Measure distance between two atoms.

Usage: /measurements distance <atom1> <atom2>
"""

def run(cmd, atom1: str, atom2: str) -> str:
    if cmd is None:  # (1)!
        return f"[dry-run] distance {atom1}{atom2}"

    from codemol.measurement_registry import register, next_name
    name = next_name("dist")
    dist = cmd.distance(name, atom1, atom2)  # (2)!
    register(name, "distance", dist, [atom1, atom2], [atom1, atom2])

    return f"Distance: {dist:.2f} Å"
  1. Dry-run support: when cmd is None, return a description without calling the rendering engine
  2. cmd.distance() creates a distance object in the viewer

Canonical Example: tools/io/load.py

The load tool (~230 lines) demonstrates a full-featured tool:

  • PDB ID detection (4-character alphanumeric)
  • File path handling
  • Metal detection and special styling
  • Multi-structure session registration
  • Comprehensive error handling

Tool Discovery

At startup, discover_tools() scans the tools/ directory:

def discover_tools() -> dict[str, dict[str, module]]:
    """Scan tools/ and return {group: {name: module}}."""
graph LR
    A[tools/] --> B[io/]
    A --> C[measurements/]
    A --> D[representations/]
    A --> E[...]
    B --> F[load.py → module]
    B --> G[clear.py → module]
    C --> H[distance.py → module]
    C --> I[angle.py → module]

Discovery uses pkgutil.iter_modules() — no registration needed. Drop a .py file with a run() function into a group directory and it's available immediately.

Dispatch

dispatch() is the runtime that connects parsed commands to tool modules:

def dispatch(tools, group, name, args, cmd=None, scope=None) -> str:

What dispatch does:

  1. Look up the tooltools[group][name]
  2. Fit arguments_fit_args() joins excess args for multi-word selections
  3. Resolve aliasesproteinpolymer.protein
  4. Apply scoping — wraps selections with model X and (...) when scope is active
  5. Call run() — passes fitted args and returns the result string

Argument Fitting

Selections can be multi-word (chain A and resi 45-50), but the parser splits on spaces. _fit_args() detects this and joins excess args into the last parameter:

# Parser produces: ["chain", "A", "and", "resi", "45"]
# Function expects: run(cmd, selection)
# After fitting:   ["chain A and resi 45"]

*args Support

Tools that accept variable arguments (e.g., run(cmd, *atoms)) are detected via inspect.Parameter.VAR_POSITIONAL, and args are passed through without joining.

Selection Detection

_is_selection() determines if an argument is a selection expression (vs. a number or plain string) by checking for:

  • Known aliases (protein, ligand, backbone)
  • Selection keywords (chain, resi, resn, within, etc.)
  • Chain/residue patterns (A/45/CA, A:45)
  • Logical operators (and, or, not)

This matters because only selections get scoped — numeric arguments and names pass through unchanged.