Files
markitect-main/markitect/legacy/switches.py
tegwick a367628cab feat: Complete Issue #39 - Database CLI Reorganization with Comprehensive Legacy Compatibility System
## Database Command Reorganization
- Add new db-prefixed commands: db-query, db-schema, db-delete, db-status
- Maintain backward compatibility with deprecation warnings for query/schema commands
- Implement lazy database initialization to reduce CLI coupling
- Add command-specific --database options for flexibility

## Legacy Compatibility Framework
- Create comprehensive legacy compatibility system in markitect/legacy_compat.py
- Support versioned legacy switches (--legacy-v39-pre) for smooth transitions
- Implement git commit binding for version tracking (Issue #39: v39-pre → 3168de4)
- Add environment-based legacy mode detection for test environments
- Create graduated deprecation warning system (DEPRECATED → LEGACY → SUNSET)

## Legacy Agent System
- Implement intelligent legacy lifecycle management agent
- Add 8 CLI commands for legacy interface management (status, analyze, migrate, cleanup, etc.)
- Create automated maintenance with usage analytics and data-driven decisions
- Provide comprehensive safety features with backup and rollback capabilities

## Test Architecture Enhancement
- Add 18 comprehensive tests for Issue #39 functionality (16 passing, 2 skipped by design)
- Configure pytest.ini with MARKITECT_LEGACY_MODE=39-pre for automatic legacy support
- Update test count to 466 total tests across 7 architectural layers
- Identify 5 legacy interface tests for future recreation without legacy dependencies

## Documentation & Roadmap Updates
- Update NEXT.md with completed Issues #39 and #40
- Document failing tests requiring recreation with pure db- commands
- Add comprehensive legacy agent documentation
- Update development priorities and capability descriptions

## Architecture Achievements
- Simplified CLI architecture with reduced coupling between commands and global state
- Created reusable legacy compatibility framework for future breaking changes
- Established systematic approach to interface deprecation and migration
- Maintained 461/466 tests passing (5 legacy interface tests flagged for recreation)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 17:28:39 +02:00

330 lines
11 KiB
Python

"""
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