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>
This commit is contained in:
330
markitect/legacy/switches.py
Normal file
330
markitect/legacy/switches.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user