Skip to content

Commit f2f2999

Browse files
committed
docs: clarify that Block.info() is inherited, not implemented per block
1 parent 4afe72c commit f2f2999

1 file changed

Lines changed: 61 additions & 27 deletions

File tree

docs/toolbox-spec.md

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Existing toolboxes: `pathsim` (core simulation blocks), `pathsim-chem` (chemical
4242

4343
Each toolbox provides a set of block types (and optionally event types) that PathView discovers, extracts metadata from, and presents in the Block Library panel. The integration requires three things:
4444

45-
1. **A Python package** with blocks that implement the `Block.info()` classmethod (and optionally an `events` submodule).
45+
1. **A Python package** with block classes that inherit from PathSim's base `Block` class (and optionally an `events` submodule).
4646
2. **A config directory** at `scripts/config/<toolbox-name>/` containing `blocks.json` (and optionally `events.json`).
4747
3. **An entry** in `scripts/config/requirements-pyodide.txt` so both the Pyodide and Flask backends install the package at runtime.
4848

@@ -54,32 +54,45 @@ The extraction pipeline (`npm run extract`) reads the config files, imports the
5454

5555
### 2.1 Block Classes
5656

57-
The toolbox Python package must have an importable module containing block classes. For example, a package named `pathsim-controls` (installed as `pathsim_controls`) would expose blocks via `pathsim_controls.blocks`.
57+
The toolbox Python package must have an importable module containing block classes that inherit from PathSim's base `Block` class. For example, a package named `pathsim-controls` (installed as `pathsim_controls`) would expose blocks via `pathsim_controls.blocks`.
5858

59-
Each block class must implement the `info()` classmethod returning a dict with the following keys:
59+
The `info()` classmethod is **inherited from the base `Block` class** — individual block classes do **not** need to implement it. The base implementation automatically:
60+
61+
1. Reads `cls.input_port_labels` and `cls.output_port_labels` class attributes
62+
2. Introspects `cls.__init__` via `inspect.signature()` to discover parameters and their defaults
63+
3. Uses `cls.__doc__` as the description
64+
65+
Block classes only need to:
66+
- **Set class attributes** `input_port_labels` and `output_port_labels` (defaults to `None` if not set, meaning variable ports)
67+
- **Define `__init__` parameters** with appropriate defaults
68+
- **Write a docstring** (RST-formatted, used for documentation)
6069

6170
```python
62-
@classmethod
63-
def info(cls):
64-
return {
65-
"input_port_labels": {"x": 0, "y": 1}, # or None or {}
66-
"output_port_labels": {"out": 0}, # or None or {}
67-
"parameters": {
68-
"gain": {"default": 1.0},
69-
"mode": {"default": "linear"}
70-
},
71-
"description": "A proportional controller block."
72-
}
71+
class MyBlock(Block):
72+
"""A proportional controller block.
73+
74+
:param gain: Proportional gain factor.
75+
:param mode: Operating mode.
76+
"""
77+
78+
input_port_labels = {"x": 0, "y": 1}
79+
output_port_labels = {"out": 0}
80+
81+
def __init__(self, gain=1.0, mode="linear"):
82+
super().__init__()
83+
# ... block initialization
7384
```
7485

86+
The inherited `info()` method returns a dict with the following keys:
87+
7588
| Key | Type | Description |
7689
|-----|------|-------------|
7790
| `input_port_labels` | `dict`, `None`, or `{}` | Defines input port names and indices. See [Port Label Semantics](#91-port-label-semantics). |
7891
| `output_port_labels` | `dict`, `None`, or `{}` | Defines output port names and indices. See [Port Label Semantics](#91-port-label-semantics). |
79-
| `parameters` | `dict` | Map of parameter names to dicts containing at minimum a `"default"` key. |
92+
| `parameters` | `dict` | Map of parameter names to dicts containing at minimum a `"default"` key. Derived from `__init__` signature. |
8093
| `description` | `str` | RST-formatted docstring. The first line/sentence is used as the short description. |
8194

82-
If a block does not implement `info()`, the extractor falls back to `__init__` signature introspection, but this is less reliable. All new toolbox blocks should implement `info()`.
95+
If a block does not inherit from `Block` (or the `info()` call fails), the extractor falls back to direct `__init__` signature introspection.
8396

8497
### 2.2 Event Classes
8598

@@ -421,22 +434,43 @@ This file also exports `PYODIDE_VERSION`, `PYODIDE_CDN_URL`, `PYODIDE_PRELOAD`,
421434

422435
## 9. Block Metadata Contract (Block.info())
423436

424-
The `info()` classmethod is the primary interface between a toolbox's Python code and PathView's extraction pipeline.
437+
The `info()` classmethod is the primary interface between a toolbox's Python code and PathView's extraction pipeline. It is **defined on the base `Block` class** and inherited by all subclasses — block authors do not need to override it.
438+
439+
The base implementation (in `pathsim.blocks._block.Block`) works as follows:
425440

426441
```python
427442
@classmethod
443+
@lru_cache()
428444
def info(cls):
445+
sig = inspect.signature(cls.__init__)
446+
params = {
447+
name: {"default": None if param.default is inspect.Parameter.empty else param.default}
448+
for name, param in sig.parameters.items()
449+
if name not in ("self", "kwargs", "args")
450+
}
429451
return {
430-
"input_port_labels": {"x": 0, "y": 1},
431-
"output_port_labels": {"out": 0},
432-
"parameters": {
433-
"gain": {"default": 1.0},
434-
"mode": {"default": "linear"}
435-
},
436-
"description": "A proportional controller block."
452+
"type": cls.__name__,
453+
"description": cls.__doc__,
454+
"input_port_labels": cls.input_port_labels,
455+
"output_port_labels": cls.output_port_labels,
456+
"parameters": params,
437457
}
438458
```
439459

460+
Block classes control their metadata by setting class attributes and `__init__` parameters:
461+
462+
```python
463+
class MyBlock(Block):
464+
"""A proportional controller block."""
465+
466+
input_port_labels = {"x": 0, "y": 1} # fixed labeled ports
467+
output_port_labels = {"out": 0} # fixed labeled ports
468+
469+
def __init__(self, gain=1.0, mode="linear"):
470+
super().__init__()
471+
# ...
472+
```
473+
440474
### 9.1 Port Label Semantics
441475

442476
The values of `input_port_labels` and `output_port_labels` have precise semantics that control both extraction and UI behavior:
@@ -553,11 +587,11 @@ The extraction script needs to import the package. Install it in your developmen
553587
pip install pathsim-controls
554588
```
555589

556-
Verify the blocks module is importable and `info()` works:
590+
Verify the blocks module is importable and the inherited `info()` returns correct metadata:
557591

558592
```python
559593
from pathsim_controls.blocks import PIDController
560-
print(PIDController.info())
594+
print(PIDController.info()) # inherited from Block base class
561595
```
562596

563597
### Step 4: Run the extraction
@@ -686,7 +720,7 @@ Categories not in the map use the `default` shape.
686720

687721
## Notes for Toolbox Authors
688722

689-
1. **`info()` is the contract.** Implement it on every block class. The extractor falls back to `__init__` introspection, but `info()` gives you explicit control over port definitions and parameter metadata.
723+
1. **`info()` is inherited.** The base `Block` class provides the `info()` classmethod — you do not need to override it. It automatically discovers ports from class attributes (`input_port_labels`, `output_port_labels`) and parameters from the `__init__` signature. Set these correctly and the extraction pipeline will pick them up.
690724

691725
2. **Port labels must be consistent.** The index values in port label dicts must be zero-based and contiguous. The extractor sorts by index, so `{"y": 1, "x": 0}` correctly produces `["x", "y"]`.
692726

0 commit comments

Comments
 (0)