Skip to content

Refactor trackers/scriptstrackers/cli and evaluate CLI framework #406

@Borda

Description

@Borda

Motivation

The trackers/scripts package name is misleading. Its content is a fully structured CLI package: an entry point (__main__.py), four subcommand handlers (track, eval, tune, download), a presentation utility (progress.py), and a registry-driven argument builder that auto-exposes every tracker parameter as a --tracker.<param> flag by reading BaseTracker._registry at parser-construction time.

The current implementation (argparse, ~1 280 lines across six files) works correctly but carries two pain points:

  1. Name mismatchscripts/ implies one-off build utilities; the actual role is a user-facing CLI.
  2. Verbosity — argparse requires 4–8 lines per argument; no typed handler signatures; Namespace attributes are invisible to mypy and IDEs.

A rename to trackers/cli/ and an optional light refactor to argparse + dataclasses would improve readability, type safety, and maintainability with zero new dependencies.

Goal

  • Rename trackers/scripts/trackers/cli/ and update pyproject.toml console-scripts entry.
  • Evaluate whether switching the argument-parsing backend to a third-party CLI framework is worthwhile given the project's strict dependency-weight policy.
  • If no third-party framework wins, apply a minimal stdlib-only refactor (typed dataclass per command) to improve handler type safety.

Hard constraint

Any new dependency must itself have zero transitive dependencies (pure Python, no further Requires). The project already avoids heavy optional extras for good reason.

CLI surface (current)

status?

Four subcommands, ~22 static args in track plus N dynamic --tracker.<param> flags generated at runtime from the tracker registry, plus 6–10 args each in eval, tune, download.

Framework comparison

Framework Pkgs Depth Size Registry Ergonomics Type safety Pros Cons
argparse (current) 0 0 stdlib ✓ native baseline; 4–8 lines/arg ✗ bare Namespace stdlib, zero risk verbose, no mypy gain
argparse + dataclasses 0 0 stdlib ✓ native ~10–15%; typed handler bodies, same parser boilerplate ~ handler typed, parser not zero risk, mypy gain modest improvement only
click 8.x 1 0 ~170 KB ~ cmd.params.append() ~30–40%; decorator per arg, reads like --help ~ runtime ok; mypy weak on decorators best ergonomics, mature registry awkward, 1 dep
fire 0.7 1 0 ~208 KB **kwargs loses types/choices/help ~50–60% (static only); function = command ✗ untyped **kwargs lowest boilerplate registry broken
docopt-ng 1 0 ~20 KB ✗ docstring fixed at import ~50% (static only); docstring = spec dict return tiny, self-documenting registry incompatible, unmaintained lineage
typer 0.16 4 3 ~8 MB ✗ fights annotation model ~40–50%; annotation = spec ✓ annotations drive both best IDE/DX for static cmds 8 MB, registry hostile
jsonargparse 4.48 2 1 ~313 KB ✓ drop-in add_argument ~40–50%; dataclass/annotation superset ✓ types drive parsing + typed Namespace YAML/JSON config free, argparse superset depth 1 via PyYAML, fails strict constraint
defopt 7.x 0 0 ~30 KB ✗ no parser access ~40% static-only; function unchanged, docstring = spec ✓ annotations drive both zero deps, no decorators, reuses existing Google/NumPy docstrings registry incompatible (**kwargs unsupported, no internal parser exposed), dynamic args impossible

Package descriptions

  • argparse — Python stdlib argument parser (since 3.2). No deps. Verbose but universal; every Python environment already has it.
  • dataclasses — Python stdlib decorator (since 3.7) that generates __init__, __repr__, etc. from annotated fields. Used here as a typed wrapper over argparse.Namespace.
  • click 8.x · GitHub — Decorator-based CLI toolkit by Pallets. Single dependency (optionally colorama on Windows). Mature, widely adopted, excellent --help generation.
  • fire 0.7 · Guide — Google library that turns any Python object into a CLI with zero boilerplate. Derives the interface from function/class signatures at runtime; no explicit arg definitions needed.
  • docopt-ng — Jazzband-maintained fork of the original docopt. Parses a usage docstring to produce a dict of arguments. Single small file (~20 KB), zero deps.
  • typer 0.16 · GitHub — Built on Click by the FastAPI author; uses Python type annotations as the single source of truth for both parsing and documentation. Pulls in rich + shellingham (~8 MB total).
  • jsonargparse 4.48 · GitHub — Drop-in superset of argparse that adds YAML/JSON config-file loading, dataclass types, and typed Namespace. Only hard dep is PyYAML; all other extras are opt-in.
  • defopt 7.x · GitHub — Zero-dep library that builds an argparse parser from a function's type annotations and Google/NumPy/Sphinx docstring. No decorators; call defopt.run(func) to expose a function as a CLI command.

Notes

click — depth 0 on Linux/macOS; pulls colorama (25 KB, 0 own deps) on Windows. Registry pattern works via cmd.params.append(click.Option(...)) but loses decorator ergonomics on the most complex command.

typerclick + rich + shellingham + typing-extensions (7 packages, ~8 MB); rich subtree alone 5 MB+ (pygments). Fails dep constraint and fights registry pattern.

jsonargparse — only required dep is PyYAML>=3.13 (PyYAML itself has zero own deps); everything else (jsonschema, omegaconf, fsspec, etc.) is opt-in extras. Drop-in superset — registry-driven parser.add_argument("--tracker.<param>", ...) loop ports verbatim. Key differentiator: action="config" makes track/eval/tune runs expressible as YAML files (e.g. --config sportsmot-bytetrack.yaml) without any config-loading code.

defopt — zero deps, pure Python. Calls defopt.run(func) or defopt.run([f1, f2]) for flat subcommands; nested dicts for hierarchical subcommands. Parses Sphinx/RST, Google-style, and NumPy docstrings natively — our existing Google-style docstrings are already compatible. Builds argparse internally but does not expose the parser, so runtime injection of dynamic --tracker.<param> flags is impossible. **kwargs parameters raise ValueError, ruling out any kwargs-based dynamic arg pattern. Best fit would be purely static commands (eval, download) but the registry-driven track command disqualifies the whole framework.

Recommendation

  1. Rename trackers/scripts/trackers/cli/ unconditionally — name should reflect role.
  2. Framework: stay on argparse; apply a thin dataclasses layer (stdlib) to give each command handler a typed Args dataclass instead of raw Namespace. Zero new deps, mypy gain, ~10–15 % line reduction.
  3. Biggest win is not the parser swap but extracting track.py's execution logic (645 of 745 lines are runtime, not arg definitions) into trackers/io/ or a new trackers/runtime/ module. That alone would shrink track.py more than any framework change.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions