""" Compatibility Layer - Bridge between new and legacy interfaces. Provides translation and adaptation mechanisms to ensure legacy interfaces can interact with modern implementations while maintaining backward compatibility. """ import functools import inspect from typing import Any, Callable, Dict, List, Optional, Tuple, Type from dataclasses import dataclass from enum import Enum from .exceptions import CompatibilityError class CompatibilityMode(Enum): """Modes of compatibility translation.""" STRICT = "strict" # Exact parameter matching required ADAPTIVE = "adaptive" # Automatic parameter adaptation PERMISSIVE = "permissive" # Allow missing/extra parameters @dataclass class ParameterMapping: """Mapping between legacy and modern parameter names/formats.""" legacy_name: str modern_name: str transformer: Optional[Callable] = None default_value: Any = None required: bool = True @dataclass class InterfaceAdapter: """Configuration for adapting a legacy interface to modern implementation.""" legacy_version: str parameter_mappings: List[ParameterMapping] return_transformer: Optional[Callable] = None compatibility_mode: CompatibilityMode = CompatibilityMode.ADAPTIVE pre_processor: Optional[Callable] = None post_processor: Optional[Callable] = None class CompatibilityLayer: """ Provides compatibility translation between legacy and modern interfaces. Responsibilities: - Map legacy parameter names/formats to modern equivalents - Transform parameter values between versions - Adapt return values for legacy expectations - Provide fallback behavior for missing functionality - Maintain compatibility shims for breaking changes """ def __init__(self): self._adapters: Dict[str, Dict[str, InterfaceAdapter]] = {} self._transformers: Dict[str, Callable] = {} self._setup_default_transformers() def register_adapter(self, command: str, adapter: InterfaceAdapter): """ Register a compatibility adapter for a command version. Args: command: Command name adapter: Interface adapter configuration """ if command not in self._adapters: self._adapters[command] = {} self._adapters[command][adapter.legacy_version] = adapter def create_legacy_wrapper( self, command: str, version: str, modern_implementation: Callable ) -> Callable: """ Create a wrapper function that adapts legacy calls to modern implementation. Args: command: Command name version: Legacy version modern_implementation: Modern function to wrap Returns: Wrapped function that accepts legacy parameters """ adapter = self._adapters.get(command, {}).get(version) if not adapter: # No specific adapter, return modern implementation with warning @functools.wraps(modern_implementation) def passthrough_wrapper(*args, **kwargs): return modern_implementation(*args, **kwargs) return passthrough_wrapper @functools.wraps(modern_implementation) def compatibility_wrapper(*args, **kwargs): # Pre-process if configured if adapter.pre_processor: args, kwargs = adapter.pre_processor(args, kwargs) # Transform parameters adapted_kwargs = self._adapt_parameters(kwargs, adapter) try: # Call modern implementation result = modern_implementation(*args, **adapted_kwargs) # Transform return value if configured if adapter.return_transformer: result = adapter.return_transformer(result) # Post-process if configured if adapter.post_processor: result = adapter.post_processor(result) return result except Exception as e: if adapter.compatibility_mode == CompatibilityMode.PERMISSIVE: # Try fallback behavior return self._handle_compatibility_error(command, version, e, args, kwargs) else: raise CompatibilityError(f"Legacy compatibility failed: {e}") return compatibility_wrapper def _adapt_parameters(self, kwargs: Dict[str, Any], adapter: InterfaceAdapter) -> Dict[str, Any]: """Adapt legacy parameters to modern format.""" adapted = {} # Apply parameter mappings for mapping in adapter.parameter_mappings: if mapping.legacy_name in kwargs: value = kwargs[mapping.legacy_name] # Apply transformer if available if mapping.transformer: value = mapping.transformer(value) adapted[mapping.modern_name] = value elif mapping.required and mapping.default_value is not None: adapted[mapping.modern_name] = mapping.default_value # Handle unmapped parameters based on compatibility mode mapped_legacy_names = {m.legacy_name for m in adapter.parameter_mappings} unmapped = {k: v for k, v in kwargs.items() if k not in mapped_legacy_names} if adapter.compatibility_mode == CompatibilityMode.STRICT and unmapped: raise CompatibilityError(f"Unmapped legacy parameters: {list(unmapped.keys())}") elif adapter.compatibility_mode in [CompatibilityMode.ADAPTIVE, CompatibilityMode.PERMISSIVE]: # Pass through unmapped parameters adapted.update(unmapped) return adapted def _handle_compatibility_error( self, command: str, version: str, error: Exception, args: Tuple, kwargs: Dict[str, Any] ) -> Any: """Handle compatibility errors in permissive mode.""" # This could implement fallback behaviors, degraded functionality, etc. # For now, we'll return a default response indicating the issue return { 'error': 'legacy_compatibility_issue', 'message': f"Legacy {command} {version} encountered compatibility issue: {error}", 'fallback_used': True } def _setup_default_transformers(self): """Setup default parameter transformers.""" # Format transformers self._transformers['format_table_to_simple'] = lambda x: 'simple' if x == 'table' else x self._transformers['format_simple_to_table'] = lambda x: 'table' if x == 'simple' else x # Path transformers self._transformers['string_to_path'] = lambda x: str(x) if hasattr(x, '__fspath__') else x self._transformers['path_to_string'] = lambda x: x if isinstance(x, str) else str(x) # Boolean transformers self._transformers['string_to_bool'] = lambda x: x.lower() in ('true', '1', 'yes') if isinstance(x, str) else bool(x) self._transformers['bool_to_string'] = lambda x: 'true' if x else 'false' # Output format legacy mappings self._transformers['legacy_output_format'] = lambda x: { 'pretty': 'table', 'raw': 'simple', 'structured': 'json' }.get(x, x) def create_format_adapter(self, command: str, version: str) -> InterfaceAdapter: """Create a standard adapter for format parameter changes.""" return InterfaceAdapter( legacy_version=version, parameter_mappings=[ ParameterMapping( legacy_name='output_format', modern_name='format', transformer=self._transformers['legacy_output_format'], required=False ), ParameterMapping( legacy_name='pretty_print', modern_name='format', transformer=lambda x: 'table' if x else 'simple', required=False ) ], compatibility_mode=CompatibilityMode.ADAPTIVE ) def create_query_v1_adapter(self) -> InterfaceAdapter: """Create adapter for legacy query command v1.0.""" return InterfaceAdapter( legacy_version='v1.0', parameter_mappings=[ ParameterMapping( legacy_name='sql_query', modern_name='sql', required=True ), ParameterMapping( legacy_name='output_format', modern_name='format', transformer=self._transformers['legacy_output_format'], default_value='simple' ), ParameterMapping( legacy_name='verbose_output', modern_name='verbose', transformer=self._transformers['string_to_bool'], default_value=False ) ], return_transformer=self._legacy_query_return_transformer, compatibility_mode=CompatibilityMode.ADAPTIVE ) def create_schema_v1_adapter(self) -> InterfaceAdapter: """Create adapter for legacy schema command v1.0.""" return InterfaceAdapter( legacy_version='v1.0', parameter_mappings=[ ParameterMapping( legacy_name='show_schema', modern_name='format', transformer=lambda x: 'table' if x else 'simple', default_value='table' ), ParameterMapping( legacy_name='include_metadata', modern_name='verbose', transformer=self._transformers['string_to_bool'], default_value=False ) ], return_transformer=self._legacy_schema_return_transformer, compatibility_mode=CompatibilityMode.ADAPTIVE ) def _legacy_query_return_transformer(self, result: Any) -> Any: """Transform modern query results for legacy v1.0 expectations.""" # Legacy v1.0 expected results in a specific format if isinstance(result, list) and result and isinstance(result[0], dict): # Convert modern list of dicts to legacy format return { 'status': 'success', 'row_count': len(result), 'data': result, 'format_version': 'v1.0' } return result def _legacy_schema_return_transformer(self, result: Any) -> Any: """Transform modern schema results for legacy v1.0 expectations.""" # Legacy v1.0 expected schema in a different structure if isinstance(result, list): return { 'schema_version': 'v1.0', 'tables': result, 'table_count': len(result) } return result def register_breaking_change_handler( self, command: str, version: str, breaking_change: str, handler: Callable ): """ Register a handler for a specific breaking change. Args: command: Command name version: Version where breaking change was introduced breaking_change: Description of the breaking change handler: Function to handle the compatibility issue """ # This could be extended to maintain a registry of breaking change handlers pass def get_compatibility_report(self, command: str, version: str) -> Dict[str, Any]: """ Generate a compatibility report for a legacy version. Args: command: Command name version: Legacy version Returns: Compatibility analysis report """ adapter = self._adapters.get(command, {}).get(version) if not adapter: return { 'command': command, 'version': version, 'compatibility': 'unknown', 'has_adapter': False, 'mappings': [], 'recommendations': ['Create a compatibility adapter for this version'] } return { 'command': command, 'version': version, 'compatibility': 'supported', 'has_adapter': True, 'compatibility_mode': adapter.compatibility_mode.value, 'mappings': [ { 'legacy_parameter': mapping.legacy_name, 'modern_parameter': mapping.modern_name, 'has_transformer': mapping.transformer is not None, 'required': mapping.required } for mapping in adapter.parameter_mappings ], 'has_return_transformer': adapter.return_transformer is not None, 'recommendations': [] } def test_compatibility( self, command: str, version: str, test_parameters: Dict[str, Any] ) -> Dict[str, Any]: """ Test compatibility adaptation with sample parameters. Args: command: Command name version: Legacy version test_parameters: Sample legacy parameters Returns: Test results showing parameter transformations """ adapter = self._adapters.get(command, {}).get(version) if not adapter: return { 'success': False, 'error': 'No adapter found for this version' } try: adapted_params = self._adapt_parameters(test_parameters, adapter) return { 'success': True, 'original_parameters': test_parameters, 'adapted_parameters': adapted_params, 'transformations_applied': len(adapter.parameter_mappings) } except Exception as e: return { 'success': False, 'error': str(e), 'original_parameters': test_parameters } def setup_standard_adapters(self): """Setup standard adapters for common legacy patterns.""" # Register query command adapters self.register_adapter('query', self.create_query_v1_adapter()) # Register schema command adapters self.register_adapter('schema', self.create_schema_v1_adapter()) # Register format adapters for various commands for command in ['list', 'metadata', 'db-query', 'db-schema']: self.register_adapter(command, self.create_format_adapter(command, 'v1.0')) def get_adapter_statistics(self) -> Dict[str, Any]: """Get statistics about registered compatibility adapters.""" stats = { 'total_commands': len(self._adapters), 'total_adapters': sum(len(versions) for versions in self._adapters.values()), 'by_command': {}, 'by_compatibility_mode': {} } for command, versions in self._adapters.items(): stats['by_command'][command] = { 'versions': list(versions.keys()), 'count': len(versions) } for version, adapter in versions.items(): mode = adapter.compatibility_mode.value stats['by_compatibility_mode'][mode] = stats['by_compatibility_mode'].get(mode, 0) + 1 return stats