---
myst:
html_meta:
"description lang=en": |
Step-by-step guide for adding a new subcommand to the PANORAMA CLI.
"keywords": |
Pangenomics, Python, Developer, CLI, Subcommand, argparse
---
# 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 {py:mod}`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.
```python
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.
```python
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:
```python
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
{py:func}`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 {py:mod}`panorama.main` and make three additions:
### Import the module
```python
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()`:
```python
subs = [
info_subparser(subparsers),
annotate_subparser(subparsers),
...
my_feature_subparser(subparsers), # ← add here
]
```
### Dispatch in `main()`
Add an `elif` branch to the dispatch chain:
```python
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:
```python
import logging
logger = logging.getLogger("PANORAMA")
```
Call {py:func}`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()`