Contributor Workflow

This page provides practical contribution guidance: environment setup, the test system, and three common implementation scenarios—adding a CLI command, adding a configuration-step schema, and using the Web GUI bridge. See Contributing for general pull-request and commit conventions.

Environment setup

conda activate habit          # Python 3.10
pip install -e .              # editable install
pytest tests/                 # Run the tests

Code conventions

Convention

Description

Type annotations

Annotate function inputs and outputs explicitly, for example x_train: np.ndarray.

Comment language

Write detailed code comments in English.

Plot text

All text generated inside programmatic plots must be English.

Utility organization

Put shared utilities in habit/utils/ and use habit/utils/progress_utils.py for progress bars.

Code style

Follow PEP 8 and add docstrings to public functions.

Testing

Tests are located in the repository-level tests/ directory and configured by tests/pytest.ini. There are two main categories:

  1. pytest unit and CLI tests (test_*.py), using click.testing.CliRunner or direct API calls.

  2. Executable demo scripts, such as tests/habitat/habitat_two_step_voxel_radiomics_train.py. These use demo YAML files under config/ and provide end-to-end smoke coverage.

Select tests by marker (markers are defined in pytest.ini):

pytest tests/ -m unit                 # Unit tests only
pytest tests/ -m "habitat and not slow"
pytest tests/ -m cli                  # CLI tests only
pytest tests/test_architecture_contracts.py -m unit  # Architecture contracts

Available markers: slow / integration / unit / preprocessing / habitat / ml / utils / cli.

Architecture contract tests

tests/test_architecture_contracts.py protects two cross-subsystem contracts. It must pass after adding a factory or orchestrator:

        flowchart TD
  TST["test_architecture_contracts.py"]
  TST --> R1["test_registry_subclasses_class_registry"]
  TST --> R2["test_registry_exposes_uniform_contract"]
  TST --> R3["test_registries_do_not_share_storage"]
  TST --> O1["test_orchestrator_exposes_terminal_methods"]

  R1 & R2 & R3 --> CR["ClassRegistry + 6 factories"]
  O1 --> OC["ORCHESTRATOR_CONTRACT + 7 orchestrators"]
    

For a new class-based factory, subclass ClassRegistry and add the factory to the CLASS_REGISTRIES dictionary in the test file. For a new orchestrator, append an (import_path, class_name, terminal_methods) tuple to ORCHESTRATOR_CONTRACT. Tests for missing optional dependencies are automatically skipped, so the minimal installation remains usable.

Demo data flow

tests/conftest.py provides fixtures such as project_root and demo_data_dir; example data is stored in demo_data/. End-to-end examples are in tests/integration/ (for example, workflow_preprocess_to_compare.py runs preprocess → get-habitat → extract → model → compare). Configuration paths are resolved relative to the YAML file; see PathResolver in Configuration System.

Scenario 1: Add a CLI command

Keep the command declaration separate from its implementation: habit/cli.py declares options and forwards the call, while the implementation lives in habit/commands/cmd_*.py.

1. Declare the command in ``habit/cli.py`` (use lazy imports to keep startup fast):

@cli.command('my-task')
@config_option()
def my_task(config):
    """One-line help shown in `habit --help`."""
    from habit.commands.cmd_my_task import run_my_task
    run_my_task(config)

2. Implement it in ``habit/commands/cmd_my_task.py`` (follow the structure of cmd_preprocess.py):

from habit.commands.common import (
    echo_success, exit_with_error, load_config_or_exit,
)
from habit.core.my_subsystem.config_schemas import MyConfig
from habit.core.my_subsystem.run import run_my_task_from_config

def run_my_task(config_path: str) -> None:
    """Run my task from a config file."""
    config = load_config_or_exit(MyConfig, config_path)
    try:
        run_my_task_from_config(config)
    except Exception as exc:  # noqa: BLE001
        exit_with_error(f"Error: {exc}")
    echo_success("My task completed successfully!")

3. Put the core logic in a run_*_from_config function under habit/core/.../run.py. Keep the command layer thin and the core layer focused; Python API users can then call the core function directly.

4. Add a CLI test in tests/.../test_cli_my_task.py using CliRunner.

Scenario 2: Add a configuration-step schema

If a new algorithm has configurable params, define and register a parameter schema to obtain type validation and GUI form generation.

1. Define the parameter model under habit/core/schemas/steps/:

from pydantic import BaseModel, Field

class MyStepParams(BaseModel):
    # Each field: type + default + constraint gives validation + GUI widget.
    n_clusters: int = Field(3, ge=2, description="Number of clusters")
    metric: str = Field("euclidean", description="Distance metric")

2. Register it with ``ParamSchemaRegistry`` (see the initialization logic in schemas/registry.py):

ParamSchemaRegistry.register("habitat", "my_step", MyStepParams)

3. Result: validate_step_params() validates the step params while loading configuration, and the GUI reflects the corresponding form fields from schemas/reflect.py. See Configuration System for details.

Tip

BaseConfig uses extra='forbid'. Declare every new field in the schema; otherwise the configuration will report an unknown field.

Scenario 3: Web GUI and bridge

habit-gui/ is an optional, separate Web GUI (not checked out by default; see Code Map). It reuses the core through two channels and does not duplicate business logic:

        flowchart LR
  WEB["React UI (habit-gui/web)"] -->|HTTP| API["FastAPI (habit-gui/api)"]
  API -->|subprocess| CLI["habit CLI (habit <cmd> --config)"]
  API -->|bridge worker| BR["habit-gui/bridge"]
  BR -->|import| SCH["habit.core.schemas (reflect)"]
    
  • Execution channel: The GUI generates YAML configuration and invokes the habit CLI in a subprocess, producing the same behavior as the CLI.

  • Schema channel: habit-gui/bridge reflects parameter models from habit.core.schemas and sends field descriptors to the frontend for dynamic form rendering. Form fields therefore remain aligned with the CLI YAML schema.

This design means that adding a core component and parameter schema usually adds the corresponding GUI inputs without frontend changes.

See also