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} Å"
- Dry-run support: when
cmd is None, return a description without calling the rendering engine 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:
What dispatch does:¶
- Look up the tool —
tools[group][name] - Fit arguments —
_fit_args()joins excess args for multi-word selections - Resolve aliases —
protein→polymer.protein - Apply scoping — wraps selections with
model X and (...)when scope is active - 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.