Subcommands Example¶
Example demonstrating how to implement command-line subcommands at the application layer while using Varlord for configuration management.
1"""
2Example demonstrating subcommands with Varlord.
3
4This example shows how to implement command-line subcommands at the application layer
5while using Varlord for configuration management.
6
7Run with:
8 python subcommands_example.py --help
9 python subcommands_example.py console --help
10 python subcommands_example.py console login --help
11 python subcommands_example.py console login lzjever --verbose
12 python subcommands_example.py console login lzjever --check-variables
13 python subcommands_example.py gui fixed-position top-left --width 1024
14 python subcommands_example.py gui fixed-position center --fullscreen
15"""
16
17import argparse
18import sys
19from dataclasses import dataclass, field
20from typing import Any, Mapping, Optional
21
22from varlord import Config, sources
23from varlord.metadata import get_all_field_keys
24from varlord.model_validation import RequiredFieldError
25from varlord.sources.base import Source, normalize_key
26
27
28# Configuration models for different subcommands
29@dataclass(frozen=True)
30class ConsoleConfig:
31 """Configuration for console commands."""
32
33 username: str = field(metadata={"description": "Username for login"})
34 password: Optional[str] = field(
35 default=None, metadata={"description": "Password (optional, may prompt)"}
36 )
37 verbose: bool = field(default=False, metadata={"description": "Enable verbose output"})
38
39
40class ArgparseSource(Source):
41 """Source that provides values from argparse positional arguments.
42
43 This is a cleaner alternative to setting environment variables for
44 positional arguments from argparse.
45 """
46
47 def __init__(self, values: dict[str, Any], model=None, source_id=None):
48 super().__init__(model=model, source_id=source_id or "argparse")
49 self._values = values
50
51 @property
52 def name(self) -> str:
53 return "argparse"
54
55 def _generate_id(self) -> str:
56 """Generate unique ID for argparse source."""
57 return "argparse"
58
59 def load(self) -> Mapping[str, Any]:
60 """Load configuration from argparse positional arguments."""
61 # Reset status
62 self._load_status = "unknown"
63 self._load_error = None
64
65 try:
66 # Normalize keys to match model field names
67 result = {}
68 for key, value in self._values.items():
69 normalized = normalize_key(key)
70 if self._model:
71 # Only include keys that match model fields
72 valid_keys = get_all_field_keys(self._model)
73 if normalized in valid_keys:
74 result[normalized] = value
75 else:
76 result[normalized] = value
77
78 self._load_status = "success"
79 return result
80 except Exception as e:
81 self._load_status = "failed"
82 self._load_error = str(e)
83 raise
84
85
86@dataclass(frozen=True)
87class GUIConfig:
88 """Configuration for GUI commands."""
89
90 position: str = field(metadata={"description": "Window position (e.g., 'fixed', 'floating')"})
91 width: int = field(default=800, metadata={"description": "Window width"})
92 height: int = field(default=600, metadata={"description": "Window height"})
93 fullscreen: bool = field(default=False, metadata={"description": "Start in fullscreen mode"})
94
95
96def handle_console_login(args, remaining_args):
97 """Handle 'console login' subcommand."""
98 # Option 1: Use custom source for positional arguments (recommended)
99 # This is cleaner than setting environment variables
100 argparse_source = ArgparseSource(
101 {"username": args.username},
102 model=ConsoleConfig,
103 source_id="argparse",
104 )
105
106 # Create config for console login
107 cfg = Config(
108 model=ConsoleConfig,
109 sources=[
110 argparse_source, # Positional args from argparse (highest priority)
111 sources.Env(),
112 sources.CLI(argv=remaining_args), # Pass remaining args to CLI source
113 ],
114 )
115
116 # Option 2: Alternative approach using environment variables
117 # import os
118 # if args.username:
119 # os.environ["USERNAME"] = args.username
120 # cfg = Config(
121 # model=ConsoleConfig,
122 # sources=[
123 # sources.Env(),
124 # sources.CLI(argv=remaining_args),
125 # ],
126 # )
127
128 # Handle standard CLI commands (--help, --check-variables)
129 cfg.handle_cli_commands()
130
131 try:
132 config = cfg.load()
133 print(f"✅ Logging in as: {config.username}")
134 if config.password:
135 print(f" Password provided: {'*' * len(config.password)}")
136 if config.verbose:
137 print(" Verbose mode enabled")
138 # Your login logic here
139 except RequiredFieldError as e:
140 print(f"❌ Configuration error: {e}")
141 sys.exit(1)
142
143
144def handle_gui_fixed_position(args, remaining_args):
145 """Handle 'gui fixed-position' subcommand."""
146 # Use custom source for positional arguments (recommended)
147 argparse_source = ArgparseSource(
148 {"position": args.position},
149 model=GUIConfig,
150 source_id="argparse",
151 )
152
153 # Create config for GUI
154 cfg = Config(
155 model=GUIConfig,
156 sources=[
157 argparse_source, # Positional args from argparse (highest priority)
158 sources.Env(),
159 sources.CLI(argv=remaining_args),
160 ],
161 )
162
163 cfg.handle_cli_commands()
164
165 try:
166 config = cfg.load()
167 print(f"✅ Starting GUI with fixed position: {config.position}")
168 print(f" Window size: {config.width}x{config.height}")
169 if config.fullscreen:
170 print(" Fullscreen mode enabled")
171 # Your GUI logic here
172 except RequiredFieldError as e:
173 print(f"❌ Configuration error: {e}")
174 sys.exit(1)
175
176
177def main():
178 """Main entry point with subcommand routing."""
179 parser = argparse.ArgumentParser(
180 description="Example application with subcommands",
181 formatter_class=argparse.RawDescriptionHelpFormatter,
182 )
183
184 # Create subparsers for top-level commands
185 subparsers = parser.add_subparsers(dest="command", help="Available commands")
186
187 # Console command
188 console_parser = subparsers.add_parser("console", help="Console commands")
189 console_subparsers = console_parser.add_subparsers(
190 dest="console_command", help="Console subcommands"
191 )
192
193 # Console login subcommand
194 login_parser = console_subparsers.add_parser("login", help="Login to console")
195 login_parser.add_argument("username", help="Username to login")
196
197 # GUI command
198 gui_parser = subparsers.add_parser("gui", help="GUI commands")
199 gui_subparsers = gui_parser.add_subparsers(dest="gui_command", help="GUI subcommands")
200
201 # GUI fixed-position subcommand
202 fixed_position_parser = gui_subparsers.add_parser(
203 "fixed-position", help="Start GUI with fixed position"
204 )
205 fixed_position_parser.add_argument(
206 "position", help="Window position (e.g., 'top-left', 'center')"
207 )
208
209 # Parse arguments
210 args, remaining = parser.parse_known_args()
211
212 # Route to appropriate handler
213 if args.command == "console":
214 if args.console_command == "login":
215 handle_console_login(args, remaining)
216 else:
217 console_parser.print_help()
218 sys.exit(1)
219 elif args.command == "gui":
220 if args.gui_command == "fixed-position":
221 handle_gui_fixed_position(args, remaining)
222 else:
223 gui_parser.print_help()
224 sys.exit(1)
225 else:
226 parser.print_help()
227 sys.exit(1)
228
229
230if __name__ == "__main__":
231 main()