393 lines
14 KiB
Python
393 lines
14 KiB
Python
|
|
"""
|
|||
|
|
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
|