Subcommands and Command Routing¶
Varlord focuses on configuration management and does not include built-in support for command-line subcommands. This guide explains why and shows you how to implement subcommands in your application using standard Python libraries.
Why No Built-in Subcommand Support?¶
Varlord’s CLI source is designed to handle flat configuration arguments only. This is a deliberate design decision:
Separation of Concerns: Configuration management and command routing are different concerns
Flexibility: Different applications need different subcommand structures
Simplicity: Keeping varlord focused makes it easier to use and maintain
Standard Libraries: Python’s
argparsealready provides excellent subcommand support
Recommended Approach¶
The recommended approach is to handle subcommands at the application layer using
standard argparse, then use varlord to load configuration for each subcommand.
Basic Pattern¶
The basic pattern is:
Use
argparseto parse subcommands at the application levelCreate separate configuration models for each subcommand (if needed)
Use varlord’s
Configclass to load configuration for the selected subcommandExecute the appropriate command handler
Example: Simple Two-Level Subcommands¶
Here’s a complete example showing how to implement subcommands:
1import argparse
2import sys
3from dataclasses import dataclass, field
4from typing import Optional
5
6from varlord import Config, sources
7from varlord.model_validation import RequiredFieldError
8
9# Configuration models for different subcommands
10@dataclass(frozen=True)
11class ConsoleConfig:
12 """Configuration for console commands."""
13 username: str = field(metadata={"description": "Username for login"})
14 password: Optional[str] = field(
15 default=None, metadata={"description": "Password (optional, may prompt)"}
16 )
17 verbose: bool = field(
18 default=False, metadata={"description": "Enable verbose output"}
19 )
20
21@dataclass(frozen=True)
22class GUIConfig:
23 """Configuration for GUI commands."""
24 position: str = field(metadata={"description": "Window position (e.g., 'fixed', 'floating')"})
25 width: int = field(default=800, metadata={"description": "Window width"})
26 height: int = field(default=600, metadata={"description": "Window height"})
27 fullscreen: bool = field(
28 default=False, metadata={"description": "Start in fullscreen mode"}
29 )
30
31def handle_console_login(args, remaining_args):
32 """Handle 'console login' subcommand."""
33 # Option 1: Set positional argument as environment variable for varlord
34 import os
35 if args.username:
36 os.environ["USERNAME"] = args.username
37
38 # Option 2: Or create a custom source that provides the username
39 # from argparse args (see "Advanced: Custom Source for Positional Args" below)
40
41 # Create config for console login
42 cfg = Config(
43 model=ConsoleConfig,
44 sources=[
45 sources.Env(),
46 sources.CLI(argv=remaining_args), # Pass remaining args to CLI source
47 ],
48 )
49
50 # Handle standard CLI commands (--help, --check-variables)
51 cfg.handle_cli_commands()
52
53 try:
54 config = cfg.load()
55 print(f"Logging in as: {config.username}")
56 if config.verbose:
57 print("Verbose mode enabled")
58 # Your login logic here
59 except RequiredFieldError as e:
60 print(f"Error: {e}")
61 sys.exit(1)
62
63def handle_gui_fixed_position(args, remaining_args):
64 """Handle 'gui fixed-position' subcommand."""
65 # Create config for GUI
66 cfg = Config(
67 model=GUIConfig,
68 sources=[
69 sources.Env(),
70 sources.CLI(argv=remaining_args),
71 ],
72 )
73
74 cfg.handle_cli_commands()
75
76 try:
77 config = cfg.load()
78 print(f"Starting GUI with fixed position: {config.position}")
79 print(f"Window size: {config.width}x{config.height}")
80 if config.fullscreen:
81 print("Fullscreen mode enabled")
82 # Your GUI logic here
83 except RequiredFieldError as e:
84 print(f"Error: {e}")
85 sys.exit(1)
86
87def main():
88 """Main entry point with subcommand routing."""
89 parser = argparse.ArgumentParser(
90 description="Example application with subcommands",
91 formatter_class=argparse.RawDescriptionHelpFormatter,
92 )
93
94 # Create subparsers for top-level commands
95 subparsers = parser.add_subparsers(dest="command", help="Available commands")
96
97 # Console command
98 console_parser = subparsers.add_parser("console", help="Console commands")
99 console_subparsers = console_parser.add_subparsers(
100 dest="console_command", help="Console subcommands"
101 )
102
103 # Console login subcommand
104 login_parser = console_subparsers.add_parser("login", help="Login to console")
105 login_parser.add_argument("username", help="Username to login")
106
107 # GUI command
108 gui_parser = subparsers.add_parser("gui", help="GUI commands")
109 gui_subparsers = gui_parser.add_subparsers(
110 dest="gui_command", help="GUI subcommands"
111 )
112
113 # GUI fixed-position subcommand
114 fixed_position_parser = gui_subparsers.add_parser(
115 "fixed-position", help="Start GUI with fixed position"
116 )
117 fixed_position_parser.add_argument(
118 "position", help="Window position (e.g., 'top-left', 'center')"
119 )
120
121 # Parse arguments
122 args, remaining = parser.parse_known_args()
123
124 # Route to appropriate handler
125 if args.command == "console":
126 if args.console_command == "login":
127 handle_console_login(args, remaining)
128 else:
129 console_parser.print_help()
130 sys.exit(1)
131 elif args.command == "gui":
132 if args.gui_command == "fixed-position":
133 handle_gui_fixed_position(args, remaining)
134 else:
135 gui_parser.print_help()
136 sys.exit(1)
137 else:
138 parser.print_help()
139 sys.exit(1)
140
141if __name__ == "__main__":
142 main()
Key Points:
Application-level routing:
argparsehandles subcommand parsingSeparate config models: Each subcommand can have its own configuration model
Pass remaining args: Use
sources.CLI(argv=remaining_args)to pass remaining arguments to varlord’s CLI sourceStandard CLI support: Each subcommand still supports
--helpand--check-variables
Usage Examples¶
With the above code, users can:
# Show main help
python app.py --help
# Show console help
python app.py console --help
# Show login help (includes varlord's --help and --check-variables)
python app.py console login --help
# Login with username (password from env or prompt)
python app.py console login lzjever --verbose
# Check configuration for login
python app.py console login lzjever --check-variables
# GUI with fixed position
python app.py gui fixed-position top-left --width 1024 --height 768
# GUI fullscreen
python app.py gui fixed-position center --fullscreen
Best Practices¶
Separate Configuration Models - Each subcommand should have its own configuration model if it has unique requirements - This provides type safety and clear documentation
Use ``parse_known_args()`` - Use
parser.parse_known_args()to separate subcommand arguments from configuration arguments - Pass remaining arguments to varlord’s CLI sourceHandle Standard Options - Always call
cfg.handle_cli_commands()for each subcommand - This ensures--helpand--check-variableswork correctlyError Handling - Catch
RequiredFieldErrorfor user-friendly error messages - Provide clear guidance on how to fix configuration issuesHelp Text - Use descriptive help text in argparse parsers - Use field metadata (
description,help) in configuration models - This provides comprehensive help at both the routing and configuration levelsTesting - Test each subcommand independently - Test with
--helpand--check-variables- Test error cases (missing required fields, invalid values)
Common Patterns¶
Pattern 1: Simple Command with Arguments¶
For simple commands that just need arguments (no sub-subcommands):
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["start", "stop", "status"])
args, remaining = parser.parse_known_args()
if args.command == "start":
cfg = Config(model=StartConfig, sources=[sources.Env(), sources.CLI(argv=remaining)])
cfg.handle_cli_commands()
config = cfg.load()
# Handle start
Pattern 2: Nested Subcommands¶
For deeply nested subcommands:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
db_parser = subparsers.add_parser("db")
db_subparsers = db_parser.add_subparsers(dest="db_command")
migrate_parser = db_subparsers.add_parser("migrate")
migrate_subparsers = migrate_parser.add_subparsers(dest="migrate_command")
up_parser = migrate_subparsers.add_parser("up")
# ... handle db migrate up
Pattern 3: Command with Positional Arguments¶
For commands that need positional arguments before configuration:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")
login_parser = subparsers.add_parser("login")
login_parser.add_argument("username", help="Username")
# username is handled by argparse, remaining args go to varlord
args, remaining = parser.parse_known_args()
if args.command == "login":
# args.username is available here
# remaining contains configuration arguments for varlord
cfg = Config(model=LoginConfig, sources=[sources.CLI(argv=remaining)])
Advanced: Custom Source for Positional Args¶
If you prefer not to set environment variables, you can create a custom source that provides positional arguments from argparse:
from varlord.sources.base import Source, normalize_key
from varlord.metadata import get_all_field_keys
from typing import Mapping, Any
class ArgparseSource(Source):
"""Source that provides values from argparse positional arguments."""
def __init__(self, values: dict[str, Any], model=None, source_id=None):
super().__init__(model=model, source_id=source_id or "argparse")
self._values = values
@property
def name(self) -> str:
return "argparse"
def _generate_id(self) -> str:
return "argparse"
def load(self) -> Mapping[str, Any]:
"""Load configuration from argparse positional arguments."""
# Reset status
self._load_status = "unknown"
self._load_error = None
try:
# Normalize keys to match model field names
result = {}
for key, value in self._values.items():
normalized = normalize_key(key)
if self._model:
# Only include keys that match model fields
valid_keys = get_all_field_keys(self._model)
if normalized in valid_keys:
result[normalized] = value
else:
result[normalized] = value
self._load_status = "success"
return result
except Exception as e:
self._load_status = "failed"
self._load_error = str(e)
raise
# Usage:
def handle_console_login(args, remaining_args):
# Create custom source for positional args
argparse_source = ArgparseSource(
{"username": args.username},
model=ConsoleConfig,
source_id="argparse",
)
cfg = Config(
model=ConsoleConfig,
sources=[
argparse_source, # Positional args (highest priority)
sources.Env(),
sources.CLI(argv=remaining_args),
],
)
cfg.handle_cli_commands()
config = cfg.load()
# ...
This approach is cleaner than setting environment variables and gives you more control over priority ordering.
Summary¶
Varlord handles configuration: Use varlord for loading and managing configuration
Application handles routing: Use
argparsefor subcommand routingSeparate models: Create separate configuration models for each subcommand
Pass remaining args: Use
sources.CLI(argv=remaining_args)to pass configuration argumentsStandard support: Each subcommand automatically supports
--helpand--check-variablesPositional arguments: Either set as environment variables or use a custom source
This approach keeps varlord simple and focused while giving you full flexibility to implement any subcommand structure your application needs.
See Subcommands Example for a complete working example.