Adding a Subcommand#

Every PANORAMA feature is exposed as an argparse subcommand. Adding a new one requires three steps: implement the module, wire it into panorama.main, and register the common arguments.


1. Implement the module#

Create a new file (e.g. panorama/myfeature/myfeature.py). Every subcommand module must expose exactly two callables at the module level:

subparser(subparsers)#

Registers the subcommand and its arguments. Return the created parser so main.py can attach common arguments to it.

import argparse

def subparser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
    parser = subparsers.add_parser(
        "my_feature",
        description="One-line description shown in --help.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser_my_feature(parser)
    return parser


def parser_my_feature(parser: argparse.ArgumentParser) -> None:
    required = parser.add_argument_group("Required arguments")
    required.add_argument(
        "-p", "--pangenomes",
        required=True,
        type=Path,
        help="TSV file listing pangenome .h5 files.",
    )
    required.add_argument(
        "-o", "--output",
        required=True,
        type=Path,
        help="Output directory.",
    )

Separating the parser construction into a parser_my_feature helper keeps the file consistent with the rest of the codebase and makes unit-testing the argument logic easier.

launch(args)#

Called by main() with the parsed argparse.Namespace. This is where the feature runs.

from panorama.format.read_binaries import load_pangenomes
from panorama.utils import mkdir, set_verbosity_level

def launch(args: argparse.Namespace) -> None:
    # 1. validate args and build need_info
    need_info = check_my_feature_args(args)

    # 2. load pangenomes
    pangenomes = load_pangenomes(
        pangenomes_file=args.pangenomes,
        need_info=need_info,
        disable_bar=args.disable_prog_bar,
        cpu=args.cpus,
    )

    # 3. create output directory
    mkdir(args.output, force=args.force)

    # 4. run the feature
    my_feature(pangenomes, args.output)

Validating arguments#

Write a check_my_feature_args function that validates user input and returns the need_info dict controlling which data groups are loaded from each .h5 file:

from typing import Dict

def check_my_feature_args(args: argparse.Namespace) -> Dict:
    return {
        "need_annotations": True,
        "need_families": True,
        "need_families_info": False,
        "need_rgp": False,
        "need_spots": False,
        "need_systems": False,
    }

Only set keys to True for data your feature actually uses β€” unnecessary loads slow down startup on large pangenomes.


2. Register common arguments#

panorama.utils.add_common_arguments() injects the following flags into every subparser automatically (called by main.py for every registered subparser):

Flag

Type

Description

--verbose / -v

int

Logging verbosity level

--log

Path

Write log to file

-d / --disable_prog_bar

flag

Disable tqdm progress bars

--force

flag

Overwrite existing outputs

Do not declare these flags yourself β€” they are added by main.py after you return your parser from subparser().


3. Wire into main.py#

Open panorama.main and make three additions:

Import the module#

from panorama.myfeature.myfeature import launch as my_feature_launcher
from panorama.myfeature.myfeature import subparser as my_feature_subparser

Register the subparser#

Add your subparser to the subs list inside cmd_line():

subs = [
    info_subparser(subparsers),
    annotate_subparser(subparsers),
    ...
    my_feature_subparser(subparsers),   # ← add here
]

Dispatch in main()#

Add an elif branch to the dispatch chain:

elif args.subcommand == "my_feature":
    my_feature_launcher(args)

Also add a one-line description to the desc string at the top of cmd_line() so your subcommand appears in panorama --help.


4. Logging convention#

Use the single named logger β€” do not create a per-module logger:

import logging
logger = logging.getLogger("PANORAMA")

Call panorama.utils.set_verbosity_level() only once in main() before dispatch; never call it from launch().


Checklist#

  • [ ] subparser() returns the parser

  • [ ] parser_my_feature() separates argument definitions from registration

  • [ ] launch() calls check_my_feature_args() to validate and build need_info

  • [ ] need_info requests only the data groups actually needed

  • [ ] Common arguments (--force, --verbose, etc.) are not declared manually

  • [ ] Logger is logging.getLogger("PANORAMA")

  • [ ] Import and register in panorama/main.py (imports + subs list + elif dispatch)

  • [ ] One-line description added to cmd_line() help text

  • [ ] Unit test for check_my_feature_args() and at least one functional test via run_command()