""" Legacy Switch System - CLI switches for legacy version control. Provides decorators and utilities for adding legacy switches to CLI commands. Supports --legacy-v1, --legacy-v2 style switches with automatic version detection and deprecation warnings. """ import click import functools from typing import Optional, Callable, Any, Dict, List from dataclasses import dataclass from .registry import LegacyRegistry, LegacyStatus from .deprecation import DeprecationManager, DeprecationLevel from .exceptions import LegacyVersionNotFoundError @dataclass class LegacySwitch: """Configuration for a legacy CLI switch.""" version: str flag_name: str help_text: str deprecation_level: DeprecationLevel = DeprecationLevel.WARNING hidden: bool = False class LegacySwitchManager: """Manages legacy switches for CLI commands.""" def __init__(self): self.registry = LegacyRegistry() self.deprecation_manager = DeprecationManager() def create_switch_options(self, command: str) -> List[LegacySwitch]: """ Create legacy switch options for a command. Args: command: Command name Returns: List of LegacySwitch configurations """ switches = [] legacy_versions = self.registry.get_legacy_versions(command) for interface in legacy_versions: if interface.status == LegacyStatus.REMOVED: continue # Determine deprecation level based on status if interface.status == LegacyStatus.SUNSET: level = DeprecationLevel.CRITICAL elif interface.status == LegacyStatus.LEGACY: level = DeprecationLevel.WARNING else: level = DeprecationLevel.INFO # Create flag name flag_name = f"legacy-{interface.version}" if flag_name.startswith("legacy-v"): # Already has v prefix pass elif interface.version.startswith("v"): flag_name = f"legacy-{interface.version}" else: flag_name = f"legacy-v{interface.version}" help_text = f"Use {interface.version} legacy behavior" if interface.description: help_text += f" - {interface.description}" switches.append(LegacySwitch( version=interface.version, flag_name=flag_name, help_text=help_text, deprecation_level=level, hidden=(interface.status == LegacyStatus.SUNSET) )) return switches def execute_with_legacy_support( self, command: str, modern_implementation: Callable, legacy_options: Dict[str, bool], *args, **kwargs ) -> Any: """ Execute a command with legacy support. Args: command: Command name modern_implementation: Modern implementation function legacy_options: Dictionary of legacy option flags *args, **kwargs: Arguments for the implementation Returns: Result of the appropriate implementation """ # Find which legacy option is enabled active_legacy = None for option_name, is_enabled in legacy_options.items(): if is_enabled: if active_legacy: raise click.ClickException( "Cannot specify multiple legacy options simultaneously" ) # Extract version from option name (e.g., legacy_v1_0 -> v1.0) version = self._extract_version_from_option(option_name) active_legacy = version if not active_legacy: # Use modern implementation return modern_implementation(*args, **kwargs) # Use legacy implementation try: interface = self.registry.get_legacy_interface(command, active_legacy) if not interface: available = self.registry.get_available_versions(command) raise LegacyVersionNotFoundError(command, active_legacy, available) # Show deprecation warning self.deprecation_manager.warn_deprecated_usage( command, active_legacy, interface.status.value, interface.removal_date, interface.migration_guide ) # Execute legacy implementation return self.registry.execute_legacy(command, active_legacy, *args, **kwargs) except LegacyVersionNotFoundError: # Fallback to modern implementation with warning click.echo( f"Warning: Legacy version {active_legacy} not available for {command}. " f"Using current implementation.", err=True ) return modern_implementation(*args, **kwargs) def _extract_version_from_option(self, option_name: str) -> str: """Extract version identifier from option name.""" # Convert legacy_v1_0 -> v1.0, legacy_v2 -> v2, etc. if option_name.startswith("legacy_v"): version_part = option_name[8:] # Remove "legacy_v" return "v" + version_part.replace("_", ".") elif option_name.startswith("legacy_"): return option_name[7:] # Remove "legacy_" else: return option_name def legacy_option(version: str, help_text: str = None) -> Callable: """ Decorator to add a legacy option to a CLI command. Args: version: Legacy version identifier (e.g., "v1.0", "v2") help_text: Custom help text for the option Returns: Click option decorator Example: @legacy_option("v1.0", "Use v1.0 legacy query behavior") @click.command() def my_command(legacy_v1_0): if legacy_v1_0: # Use legacy implementation pass """ # Create flag name flag_name = f"legacy-{version}" if not flag_name.startswith("legacy-v") and not version.startswith("v"): flag_name = f"legacy-v{version}" # Create option name (replace hyphens with underscores) option_name = flag_name.replace("-", "_") # Default help text if not help_text: help_text = f"Use {version} legacy behavior (deprecated)" return click.option( f'--{flag_name}', option_name, is_flag=True, help=help_text ) def legacy_command(command_name: str, auto_switches: bool = True): """ Decorator to add automatic legacy support to a CLI command. Args: command_name: Name of the command for legacy registry lookup auto_switches: Whether to automatically add legacy switches Returns: Decorator function Example: @legacy_command("query") @click.command() def query_command(**kwargs): # Implementation with automatic legacy support pass """ def decorator(func: Callable) -> Callable: if auto_switches: # Add legacy switches automatically switch_manager = LegacySwitchManager() switches = switch_manager.create_switch_options(command_name) # Apply switches in reverse order (click applies decorators bottom-up) for switch in reversed(switches): option_name = switch.flag_name.replace("-", "_") func = click.option( f'--{switch.flag_name}', option_name, is_flag=True, help=switch.help_text, hidden=switch.hidden )(func) @functools.wraps(func) def wrapper(*args, **kwargs): switch_manager = LegacySwitchManager() # Extract legacy options from kwargs legacy_options = {} func_kwargs = {} for key, value in kwargs.items(): if key.startswith("legacy_"): legacy_options[key] = value else: func_kwargs[key] = value # Define modern implementation def modern_impl(*impl_args, **impl_kwargs): return func(*impl_args, **impl_kwargs) # Execute with legacy support return switch_manager.execute_with_legacy_support( command_name, modern_impl, legacy_options, *args, **func_kwargs ) return wrapper return decorator def create_legacy_switches_for_command(command: str) -> List[Callable]: """ Create a list of legacy option decorators for a command. Args: command: Command name Returns: List of click option decorators Example: decorators = create_legacy_switches_for_command("query") for decorator in decorators: my_command = decorator(my_command) """ switch_manager = LegacySwitchManager() switches = switch_manager.create_switch_options(command) decorators = [] for switch in switches: option_name = switch.flag_name.replace("-", "_") decorator = click.option( f'--{switch.flag_name}', option_name, is_flag=True, help=switch.help_text, hidden=switch.hidden ) decorators.append(decorator) return decorators def with_legacy_support(command_name: str): """ Simplified decorator for adding legacy support to existing commands. Args: command_name: Name of the command for legacy registry lookup Returns: Decorator function Example: @with_legacy_support("query") def my_existing_function(*args, **kwargs): # Your existing implementation pass """ def decorator(func: Callable) -> Callable: @functools.wraps(func) def wrapper(*args, **kwargs): # Check if any legacy options are present in kwargs legacy_options = {k: v for k, v in kwargs.items() if k.startswith("legacy_") and v} if not legacy_options: # No legacy options, use modern implementation return func(*args, **kwargs) # Legacy option detected, use legacy manager switch_manager = LegacySwitchManager() def modern_impl(*impl_args, **impl_kwargs): # Remove legacy options from kwargs before calling modern implementation clean_kwargs = {k: v for k, v in impl_kwargs.items() if not k.startswith("legacy_")} return func(*impl_args, **clean_kwargs) return switch_manager.execute_with_legacy_support( command_name, modern_impl, legacy_options, *args, **kwargs ) return wrapper return decorator