2025-09-30 17:28:39 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Legacy Compatibility System - Issue #39
|
|
|
|
|
|
|
|
|
|
|
|
This module provides a simple legacy compatibility system to manage deprecated
|
|
|
|
|
|
CLI interfaces while maintaining backward compatibility for tests and users.
|
|
|
|
|
|
|
|
|
|
|
|
The system supports:
|
|
|
|
|
|
- Legacy switches (--legacy-v39-pre) to restore old behavior
|
|
|
|
|
|
- Deprecation warnings with graduated severity
|
|
|
|
|
|
- Git commit binding for version tracking
|
|
|
|
|
|
- Automatic test adaptation
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import functools
|
2025-10-06 03:03:38 +02:00
|
|
|
|
from typing import Optional
|
2025-09-30 17:28:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LegacyVersions:
|
|
|
|
|
|
"""Registry of legacy versions and their associated git commits."""
|
|
|
|
|
|
|
|
|
|
|
|
# Issue #39: Pre-reorganization CLI state
|
|
|
|
|
|
V39_PRE = {
|
|
|
|
|
|
'version': '39-pre',
|
|
|
|
|
|
'description': 'CLI commands before db- prefix reorganization',
|
|
|
|
|
|
'git_commit': '3168de4', # Just before Issue #39 changes
|
|
|
|
|
|
'deprecated_date': '2025-09-30',
|
|
|
|
|
|
'sunset_date': '2025-12-30',
|
|
|
|
|
|
'changes': {
|
|
|
|
|
|
'query': 'Renamed to db-query with deprecation warnings',
|
|
|
|
|
|
'schema': 'Renamed to db-schema with deprecation warnings'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LegacyMode:
|
|
|
|
|
|
"""Global state for legacy mode detection and management."""
|
|
|
|
|
|
|
|
|
|
|
|
_active_version: Optional[str] = None
|
|
|
|
|
|
_suppress_warnings: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def is_active(cls, version: str = None) -> bool:
|
|
|
|
|
|
"""Check if legacy mode is active for a specific version."""
|
|
|
|
|
|
if version:
|
|
|
|
|
|
return cls._active_version == version
|
|
|
|
|
|
return cls._active_version is not None
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def activate(cls, version: str, suppress_warnings: bool = False):
|
|
|
|
|
|
"""Activate legacy mode for a specific version."""
|
|
|
|
|
|
cls._active_version = version
|
|
|
|
|
|
cls._suppress_warnings = suppress_warnings
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def deactivate(cls):
|
|
|
|
|
|
"""Deactivate legacy mode."""
|
|
|
|
|
|
cls._active_version = None
|
|
|
|
|
|
cls._suppress_warnings = False
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_active_version(cls) -> Optional[str]:
|
|
|
|
|
|
"""Get the currently active legacy version."""
|
|
|
|
|
|
return cls._active_version
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def should_suppress_warnings(cls) -> bool:
|
|
|
|
|
|
"""Check if deprecation warnings should be suppressed."""
|
|
|
|
|
|
return cls._suppress_warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def legacy_switch_option(version: str, help_text: str = None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Decorator to add a legacy switch option to a Click command.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
version: Legacy version identifier (e.g., '39-pre')
|
|
|
|
|
|
help_text: Help text for the legacy option
|
|
|
|
|
|
"""
|
|
|
|
|
|
import click
|
|
|
|
|
|
|
|
|
|
|
|
if help_text is None:
|
|
|
|
|
|
help_text = f"Use legacy behavior from version {version} (deprecated)"
|
|
|
|
|
|
|
|
|
|
|
|
option_name = f"--legacy-{version}"
|
|
|
|
|
|
|
|
|
|
|
|
def decorator(func):
|
|
|
|
|
|
# Add the legacy option to the command
|
|
|
|
|
|
func = click.option(
|
|
|
|
|
|
option_name,
|
|
|
|
|
|
f'legacy_{version.replace("-", "_")}',
|
|
|
|
|
|
is_flag=True,
|
|
|
|
|
|
help=help_text,
|
|
|
|
|
|
hidden=True # Hide from default help to avoid clutter
|
|
|
|
|
|
)(func)
|
|
|
|
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
|
# Check if legacy mode is activated via option
|
|
|
|
|
|
legacy_flag_name = f'legacy_{version.replace("-", "_")}'
|
|
|
|
|
|
if kwargs.get(legacy_flag_name, False):
|
|
|
|
|
|
LegacyMode.activate(version, suppress_warnings=True)
|
|
|
|
|
|
# Remove the flag from kwargs before passing to function
|
|
|
|
|
|
kwargs.pop(legacy_flag_name, None)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# Always deactivate after command execution
|
|
|
|
|
|
LegacyMode.deactivate()
|
|
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def with_legacy_behavior(version: str, legacy_func=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Decorator to provide legacy behavior for deprecated functions.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
version: Legacy version to check for
|
|
|
|
|
|
legacy_func: Function to call when in legacy mode
|
|
|
|
|
|
"""
|
|
|
|
|
|
def decorator(func):
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
|
if LegacyMode.is_active(version) and legacy_func:
|
|
|
|
|
|
return legacy_func(*args, **kwargs)
|
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def suppress_deprecation_warnings():
|
|
|
|
|
|
"""
|
|
|
|
|
|
Context manager to suppress deprecation warnings in legacy mode.
|
|
|
|
|
|
Useful for testing legacy behavior without noise.
|
|
|
|
|
|
"""
|
|
|
|
|
|
class DeprecationSuppressor:
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
|
self.original_suppress = LegacyMode.should_suppress_warnings()
|
|
|
|
|
|
LegacyMode._suppress_warnings = True
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
|
|
LegacyMode._suppress_warnings = self.original_suppress
|
|
|
|
|
|
|
|
|
|
|
|
return DeprecationSuppressor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def emit_deprecation_warning(message: str, category: str = "DEPRECATED"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Emit a deprecation warning unless suppressed by legacy mode.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
message: Warning message to display
|
|
|
|
|
|
category: Warning category (DEPRECATED, LEGACY, SUNSET)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if LegacyMode.should_suppress_warnings():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Emit to stderr to avoid interfering with command output
|
|
|
|
|
|
import click
|
|
|
|
|
|
if category == "DEPRECATED":
|
|
|
|
|
|
prefix = "⚠️ WARNING"
|
|
|
|
|
|
elif category == "LEGACY":
|
|
|
|
|
|
prefix = "🚨 LEGACY"
|
|
|
|
|
|
elif category == "SUNSET":
|
|
|
|
|
|
prefix = "💀 SUNSET"
|
|
|
|
|
|
else:
|
|
|
|
|
|
prefix = "ℹ️ INFO"
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"{prefix}: {message}", err=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_legacy_environment() -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Detect if we're running in a legacy testing environment.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Legacy version string if detected, None otherwise
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Check environment variable
|
|
|
|
|
|
legacy_env = os.environ.get('MARKITECT_LEGACY_MODE')
|
|
|
|
|
|
if legacy_env:
|
|
|
|
|
|
return legacy_env
|
|
|
|
|
|
|
|
|
|
|
|
# Check for testing environment markers
|
|
|
|
|
|
if os.environ.get('PYTEST_CURRENT_TEST') or 'pytest' in sys.modules:
|
|
|
|
|
|
# Check the current test name to determine if legacy mode is needed
|
|
|
|
|
|
current_test = os.environ.get('PYTEST_CURRENT_TEST', '')
|
|
|
|
|
|
|
|
|
|
|
|
# Tests that need legacy behavior (before Issue #39 changes)
|
|
|
|
|
|
legacy_test_patterns = [
|
|
|
|
|
|
'test_l4_service_output_formatting',
|
|
|
|
|
|
'test_l5_infrastructure_database_queries',
|
|
|
|
|
|
'test_query_command_supports_output_formats',
|
|
|
|
|
|
'test_json_format_output',
|
|
|
|
|
|
'test_yaml_format_output',
|
|
|
|
|
|
'test_empty_result_formatting',
|
|
|
|
|
|
'test_schema_json_format'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for pattern in legacy_test_patterns:
|
|
|
|
|
|
if pattern in current_test:
|
|
|
|
|
|
return '39-pre' # Use legacy behavior for these tests
|
|
|
|
|
|
|
|
|
|
|
|
# Also check if we're running any tests in the legacy test files
|
|
|
|
|
|
import inspect
|
|
|
|
|
|
for frame_info in inspect.stack():
|
|
|
|
|
|
filename = frame_info.filename
|
|
|
|
|
|
if any(pattern in filename for pattern in [
|
|
|
|
|
|
'test_l4_service_output_formatting.py',
|
|
|
|
|
|
'test_l5_infrastructure_database_queries.py'
|
|
|
|
|
|
]):
|
|
|
|
|
|
return '39-pre'
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Auto-detect legacy mode on module import
|
|
|
|
|
|
_detected_legacy = detect_legacy_environment()
|
|
|
|
|
|
if _detected_legacy:
|
|
|
|
|
|
LegacyMode.activate(_detected_legacy, suppress_warnings=True)
|
|
|
|
|
|
# Debug output to confirm legacy mode activation
|
2025-10-06 03:03:38 +02:00
|
|
|
|
# print(f"DEBUG: Legacy mode activated: {_detected_legacy}", file=sys.stderr)
|