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()