""" Deprecation Management System - Handle graduated deprecation warnings and lifecycle. Provides structured deprecation warnings, lifecycle management, and migration guidance for legacy interfaces moving through their deprecation phases. """ import sys import warnings from datetime import datetime, timedelta from enum import Enum from typing import Optional, Dict, List, Any from dataclasses import dataclass import click from .exceptions import DeprecationError class DeprecationLevel(Enum): """Levels of deprecation severity.""" INFO = "info" # Initial deprecation notice WARNING = "warning" # Standard deprecation warning CRITICAL = "critical" # Final warning before removal ERROR = "error" # Deprecation with error (blocks execution) @dataclass class DeprecationPolicy: """Policy configuration for deprecation management.""" info_duration_days: int = 90 # Days in INFO level warning_duration_days: int = 60 # Days in WARNING level critical_duration_days: int = 30 # Days in CRITICAL level show_migration_guide: bool = True block_on_error: bool = True quiet_mode: bool = False class DeprecationManager: """ Manages deprecation warnings and lifecycle progression. Responsibilities: - Display appropriate deprecation warnings based on level - Track deprecation progression through lifecycle phases - Provide migration guidance and recommendations - Support quiet mode and warning suppression - Handle automatic progression of deprecation levels """ def __init__(self, policy: Optional[DeprecationPolicy] = None): """ Initialize the deprecation manager. Args: policy: Deprecation policy configuration """ self.policy = policy or DeprecationPolicy() self._warning_counts: Dict[str, int] = {} self._last_warning: Dict[str, datetime] = {} def warn_deprecated_usage( self, command: str, version: str, status: str, removal_date: Optional[str] = None, migration_guide: Optional[str] = None, level: Optional[DeprecationLevel] = None ): """ Issue a deprecation warning for legacy usage. Args: command: Command name version: Legacy version being used status: Current status (deprecated, legacy, sunset) removal_date: When this version will be removed migration_guide: Migration instructions level: Override deprecation level """ if self.policy.quiet_mode: return # Determine deprecation level from status if not provided if level is None: level = self._get_level_from_status(status) # Track warning frequency warning_key = f"{command}:{version}" self._warning_counts[warning_key] = self._warning_counts.get(warning_key, 0) + 1 self._last_warning[warning_key] = datetime.now() # Format and display warning message = self._format_deprecation_message( command, version, status, removal_date, migration_guide, level ) if level == DeprecationLevel.ERROR: if self.policy.block_on_error: raise DeprecationError(f"{command} {version}", version, removal_date) else: click.echo(click.style(message, fg='red', bold=True), err=True) elif level == DeprecationLevel.CRITICAL: click.echo(click.style(message, fg='red'), err=True) elif level == DeprecationLevel.WARNING: click.echo(click.style(message, fg='yellow'), err=True) else: # INFO click.echo(click.style(message, fg='blue'), err=True) def _get_level_from_status(self, status: str) -> DeprecationLevel: """Determine deprecation level from status.""" status_map = { 'deprecated': DeprecationLevel.INFO, 'legacy': DeprecationLevel.WARNING, 'sunset': DeprecationLevel.CRITICAL, 'removed': DeprecationLevel.ERROR } return status_map.get(status.lower(), DeprecationLevel.WARNING) def _format_deprecation_message( self, command: str, version: str, status: str, removal_date: Optional[str] = None, migration_guide: Optional[str] = None, level: DeprecationLevel = DeprecationLevel.WARNING ) -> str: """Format a deprecation warning message.""" # Choose emoji/prefix based on level prefixes = { DeprecationLevel.INFO: "â„šī¸", DeprecationLevel.WARNING: "âš ī¸", DeprecationLevel.CRITICAL: "🚨", DeprecationLevel.ERROR: "❌" } prefix = prefixes.get(level, "âš ī¸") # Build main message lines = [f"{prefix} DEPRECATION WARNING: Using legacy {command} {version}"] # Add status-specific information if status == 'deprecated': lines.append(f" This version is deprecated and will become legacy-only.") elif status == 'legacy': lines.append(f" This version requires the --legacy-{version} flag.") elif status == 'sunset': lines.append(f" This version is in sunset phase and will be removed soon.") elif status == 'removed': lines.append(f" This version has been removed.") # Add removal date if available if removal_date: try: removal_dt = datetime.fromisoformat(removal_date.replace('Z', '+00:00')) days_left = (removal_dt - datetime.now()).days if days_left > 0: lines.append(f" Scheduled for removal in {days_left} days ({removal_date[:10]})") else: lines.append(f" Removal date passed ({removal_date[:10]})") except ValueError: lines.append(f" Scheduled for removal: {removal_date}") # Add migration guide if available if migration_guide and self.policy.show_migration_guide: lines.append(f" Migration guide: {migration_guide}") # Add recommendation lines.append(f" Recommendation: Update to the latest version of '{command}'") return '\n'.join(lines) def get_deprecation_timeline( self, deprecated_date: str, removal_date: Optional[str] = None ) -> Dict[str, Any]: """ Calculate deprecation timeline and current phase. Args: deprecated_date: When deprecation started removal_date: When removal is scheduled Returns: Timeline information with current phase """ try: dep_date = datetime.fromisoformat(deprecated_date.replace('Z', '+00:00')) except ValueError: dep_date = datetime.now() timeline = { 'deprecated_date': deprecated_date, 'removal_date': removal_date, 'phases': {} } # Calculate phase dates info_end = dep_date + timedelta(days=self.policy.info_duration_days) warning_end = info_end + timedelta(days=self.policy.warning_duration_days) critical_end = warning_end + timedelta(days=self.policy.critical_duration_days) timeline['phases'] = { 'info': { 'start': dep_date.isoformat(), 'end': info_end.isoformat(), 'level': DeprecationLevel.INFO.value }, 'warning': { 'start': info_end.isoformat(), 'end': warning_end.isoformat(), 'level': DeprecationLevel.WARNING.value }, 'critical': { 'start': warning_end.isoformat(), 'end': critical_end.isoformat(), 'level': DeprecationLevel.CRITICAL.value } } # Determine current phase now = datetime.now() if now < info_end: timeline['current_phase'] = 'info' timeline['current_level'] = DeprecationLevel.INFO elif now < warning_end: timeline['current_phase'] = 'warning' timeline['current_level'] = DeprecationLevel.WARNING elif now < critical_end: timeline['current_phase'] = 'critical' timeline['current_level'] = DeprecationLevel.CRITICAL else: timeline['current_phase'] = 'expired' timeline['current_level'] = DeprecationLevel.ERROR # Override with explicit removal date if provided if removal_date: try: removal_dt = datetime.fromisoformat(removal_date.replace('Z', '+00:00')) if now >= removal_dt: timeline['current_phase'] = 'removed' timeline['current_level'] = DeprecationLevel.ERROR except ValueError: pass return timeline def should_progress_deprecation( self, command: str, version: str, current_status: str, deprecated_date: str ) -> Optional[str]: """ Determine if a deprecation should progress to the next phase. Args: command: Command name version: Version identifier current_status: Current deprecation status deprecated_date: When deprecation started Returns: Next status if progression is needed, None otherwise """ timeline = self.get_deprecation_timeline(deprecated_date) current_phase = timeline['current_phase'] progression_map = { ('deprecated', 'warning'): 'legacy', ('legacy', 'critical'): 'sunset', ('sunset', 'expired'): 'removed', ('sunset', 'removed'): 'removed' } return progression_map.get((current_status, current_phase)) def generate_migration_report( self, command: str, from_version: str, to_version: str = "current" ) -> Dict[str, Any]: """ Generate a detailed migration report. Args: command: Command name from_version: Source version to_version: Target version Returns: Detailed migration report """ report = { 'command': command, 'from_version': from_version, 'to_version': to_version, 'generated_at': datetime.now().isoformat(), 'urgency': 'low', 'steps': [], 'breaking_changes': [], 'resources': [] } # Determine urgency based on usage tracking warning_key = f"{command}:{from_version}" usage_count = self._warning_counts.get(warning_key, 0) last_used = self._last_warning.get(warning_key) if usage_count > 10: report['urgency'] = 'high' report['steps'].append("High usage detected - prioritize migration") elif usage_count > 5: report['urgency'] = 'medium' # Add general migration steps report['steps'].extend([ f"Review current usage of {command} {from_version}", f"Test {command} functionality with current version", f"Update scripts/automation to remove --legacy-{from_version} flags", "Validate that new behavior meets requirements", "Update documentation to reflect changes" ]) # Add resources report['resources'].extend([ f"Legacy documentation: markitect help {command}", "Migration support: markitect legacy-help", "Version comparison: markitect legacy-compare" ]) return report def get_warning_statistics(self) -> Dict[str, Any]: """Get statistics about deprecation warnings issued.""" stats = { 'total_warnings': sum(self._warning_counts.values()), 'unique_combinations': len(self._warning_counts), 'by_command': {}, 'most_used_legacy': [], 'recent_warnings': [] } # Group by command for key, count in self._warning_counts.items(): command, version = key.split(':', 1) if command not in stats['by_command']: stats['by_command'][command] = {} stats['by_command'][command][version] = count # Find most used legacy versions sorted_usage = sorted( self._warning_counts.items(), key=lambda x: x[1], reverse=True ) stats['most_used_legacy'] = [ {'command_version': key, 'count': count} for key, count in sorted_usage[:5] ] # Recent warnings recent_cutoff = datetime.now() - timedelta(days=7) for key, last_time in self._last_warning.items(): if last_time >= recent_cutoff: stats['recent_warnings'].append({ 'command_version': key, 'last_used': last_time.isoformat(), 'count': self._warning_counts[key] }) return stats def suppress_warnings(self, command: str = None, version: str = None): """ Suppress deprecation warnings temporarily. Args: command: Specific command to suppress (None for all) version: Specific version to suppress (None for all versions of command) """ # This could be extended to support more sophisticated suppression # For now, we'll set quiet mode self.policy.quiet_mode = True def enable_warnings(self): """Re-enable deprecation warnings.""" self.policy.quiet_mode = False