Skip to content

v1.0.0 Task Final.2: API Stability + Backward Compatibility #92

@gbrennon

Description

@gbrennon

Task Description

Epic: v1.0.0 Epic Final: Production Release (#75)
Acceptance Criteria: API stability guarantees, Semantic versioning implementation, Backward compatibility testing, Migration guides for breaking changes

🔗 Dependencies: Requires all v0.6.0 tasks completion (feature-complete framework)

Implementation Details

API Stability Framework

# API stability annotations and versioning
from enum import Enum
from typing import Any, Dict, List
from dataclasses import dataclass

class StabilityLevel(Enum):
    EXPERIMENTAL = \"experimental\"  # May change without notice
    BETA = \"beta\"                 # Stable but may have minor changes
    STABLE = \"stable\"             # Guaranteed backward compatibility
    DEPRECATED = \"deprecated\"     # Will be removed in future version

@dataclass(frozen=True)
class APIVersion:
    major: int
    minor: int
    patch: int
    
    def __str__(self) -> str:
        return f\"{self.major}.{self.minor}.{self.patch}\"
    
    def is_compatible_with(self, other: 'APIVersion') -> bool:
        # Major version must match for compatibility
        return self.major == other.major

def api_stable(since: str):
    \"\"\"Mark API as stable since given version\"\"\"
    def decorator(cls_or_func):
        cls_or_func.__api_stable_since__ = since
        cls_or_func.__api_stability__ = StabilityLevel.STABLE
        return cls_or_func
    return decorator

def api_beta(since: str):
    \"\"\"Mark API as beta since given version\"\"\"
    def decorator(cls_or_func):
        cls_or_func.__api_beta_since__ = since
        cls_or_func.__api_stability__ = StabilityLevel.BETA
        return cls_or_func
    return decorator

def api_deprecated(since: str, removal_version: str, replacement: str = None):
    \"\"\"Mark API as deprecated\"\"\"
    def decorator(cls_or_func):
        cls_or_func.__api_deprecated_since__ = since
        cls_or_func.__api_removal_version__ = removal_version
        cls_or_func.__api_replacement__ = replacement
        cls_or_func.__api_stability__ = StabilityLevel.DEPRECATED
        
        # Add runtime warning
        import warnings
        import functools
        
        if hasattr(cls_or_func, '__call__'):
            @functools.wraps(cls_or_func)
            def wrapper(*args, **kwargs):
                warnings.warn(
                    f\"{cls_or_func.__name__} is deprecated since v{since} \"
                    f\"and will be removed in v{removal_version}. \"
                    f\"{'Use ' + replacement + ' instead.' if replacement else ''}\",
                    DeprecationWarning,
                    stacklevel=2
                )
                return cls_or_func(*args, **kwargs)
            return wrapper
        
        return cls_or_func
    return decorator

# Apply stability annotations to core APIs
@api_stable(since=\"1.0.0\")
class Entity(ABC, Generic[TId]):
    # Stable API - no breaking changes allowed
    pass

@api_stable(since=\"1.0.0\")
class Repository(ABC, Generic[TEntity, TId]):
    # Stable API - backward compatibility guaranteed
    pass

@api_beta(since=\"0.6.0\")
class PluginManager:
    # Beta API - minor changes possible
    pass

Backward Compatibility Testing

import importlib
import inspect
from typing import Set, Dict, Any

class CompatibilityChecker:
    \"\"\"Check backward compatibility between versions\"\"\"
    
    def __init__(self):
        self._previous_api: Dict[str, Any] = {}
        self._current_api: Dict[str, Any] = {}
    
    def load_previous_version_api(self, version: str):
        \"\"\"Load API from previous version for comparison\"\"\"
        # In practice, this would load from saved API snapshots
        self._previous_api = self._extract_public_api(version)
    
    def check_compatibility(self) -> List['CompatibilityIssue']:
        \"\"\"Check for compatibility issues\"\"\"
        issues = []
        
        # Check for removed classes/functions
        for name, obj in self._previous_api.items():
            if name not in self._current_api:
                if self._is_stable_api(obj):
                    issues.append(CompatibilityIssue(
                        type=IssueType.REMOVED_STABLE_API,
                        name=name,
                        description=f\"Stable API '{name}' was removed\"
                    ))
        
        # Check for signature changes
        for name, current_obj in self._current_api.items():
            if name in self._previous_api:
                previous_obj = self._previous_api[name]
                
                if self._signature_changed(previous_obj, current_obj):
                    if self._is_stable_api(previous_obj):
                        issues.append(CompatibilityIssue(
                            type=IssueType.SIGNATURE_CHANGED,
                            name=name,
                            description=f\"Stable API '{name}' signature changed\"
                        ))
        
        return issues
    
    def _extract_public_api(self, version: str) -> Dict[str, Any]:
        \"\"\"Extract public API from a version\"\"\"
        api = {}
        
        # Scan all public classes and functions
        for module_name in self._get_public_modules():
            module = importlib.import_module(module_name)
            
            for name, obj in inspect.getmembers(module):
                if not name.startswith('_'):  # Public API
                    full_name = f\"{module_name}.{name}\"
                    api[full_name] = obj
        
        return api
    
    def _is_stable_api(self, obj) -> bool:
        return getattr(obj, '__api_stability__', None) == StabilityLevel.STABLE
    
    def _signature_changed(self, old_obj, new_obj) -> bool:
        if not (callable(old_obj) and callable(new_obj)):
            return False
        
        try:
            old_sig = inspect.signature(old_obj)
            new_sig = inspect.signature(new_obj)
            return old_sig != new_sig
        except (ValueError, TypeError):
            return False

@dataclass
class CompatibilityIssue:
    type: 'IssueType'
    name: str
    description: str
    severity: str = \"error\"

class IssueType(Enum):
    REMOVED_STABLE_API = \"removed_stable_api\"
    SIGNATURE_CHANGED = \"signature_changed\"
    RETURN_TYPE_CHANGED = \"return_type_changed\"

Migration Framework

class Migration(ABC):
    \"\"\"Base class for migrations between versions\"\"\"
    
    @property
    @abstractmethod
    def from_version(self) -> str:
        pass
    
    @property
    @abstractmethod
    def to_version(self) -> str:
        pass
    
    @abstractmethod
    async def migrate(self, project_path: Path) -> Result[None, Exception]:
        pass
    
    @abstractmethod
    def get_migration_guide(self) -> str:
        \"\"\"Return human-readable migration guide\"\"\"
        pass

class MigrationManager:
    def __init__(self):
        self._migrations: List[Migration] = []
    
    def register_migration(self, migration: Migration):
        self._migrations.append(migration)
    
    def get_migration_path(self, from_version: str, to_version: str) -> List[Migration]:
        \"\"\"Find migration path between versions\"\"\"
        # Simple implementation - could use graph algorithms for complex paths
        relevant_migrations = []
        
        for migration in self._migrations:
            if (migration.from_version == from_version and 
                migration.to_version == to_version):
                relevant_migrations.append(migration)
        
        return relevant_migrations
    
    async def migrate_project(self, project_path: Path, from_version: str, to_version: str):
        migration_path = self.get_migration_path(from_version, to_version)
        
        for migration in migration_path:
            print(f\"Applying migration: {migration.from_version} -> {migration.to_version}\")
            
            result = await migration.migrate(project_path)
            if result.is_err():
                print(f\"Migration failed: {result.unwrap_err()}\")
                return result
        
        return Result.ok(None)

# Example migration
class V05ToV06Migration(Migration):
    @property
    def from_version(self) -> str:
        return \"0.5.0\"
    
    @property
    def to_version(self) -> str:
        return \"0.6.0\"
    
    async def migrate(self, project_path: Path) -> Result[None, Exception]:
        try:
            # Update import statements
            await self._update_imports(project_path)
            
            # Update configuration files
            await self._update_config(project_path)
            
            return Result.ok(None)
        except Exception as e:
            return Result.err(e)
    
    def get_migration_guide(self) -> str:
        return \"\"\"
        Migration from v0.5.0 to v0.6.0:
        
        1. Plugin system has been introduced
        2. Some import paths have changed:
           - OLD: from forging_blocks.application.handlers import MessageHandler
           - NEW: from forging_blocks.application.handlers.message_handler import MessageHandler
        
        3. Configuration format updated:
           - plugins.toml file added for plugin configuration
        
        Run: fb migrate --from 0.5.0 --to 0.6.0
        \"\"\"
    
    async def _update_imports(self, project_path: Path):
        # Update Python files with new import paths
        for python_file in project_path.glob(\"**/*.py\"):
            content = python_file.read_text()
            
            # Replace old import patterns
            content = content.replace(
                \"from forging_blocks.application.handlers import MessageHandler\",
                \"from forging_blocks.application.handlers.message_handler import MessageHandler\"
            )
            
            python_file.write_text(content)

Semantic Versioning Implementation

class SemanticVersion:
    def __init__(self, version_string: str):
        parts = version_string.split('.')
        self.major = int(parts[0])
        self.minor = int(parts[1])
        self.patch = int(parts[2]) if len(parts) > 2 else 0
    
    def is_breaking_change(self, new_version: 'SemanticVersion') -> bool:
        return new_version.major > self.major
    
    def is_feature_addition(self, new_version: 'SemanticVersion') -> bool:
        return (self.major == new_version.major and 
                new_version.minor > self.minor)
    
    def is_patch_update(self, new_version: 'SemanticVersion') -> bool:
        return (self.major == new_version.major and 
                self.minor == new_version.minor and
                new_version.patch > self.patch)

class VersionManager:
    def __init__(self, current_version: str):
        self.current = SemanticVersion(current_version)
    
    def validate_api_changes(self, api_changes: List[CompatibilityIssue]) -> bool:
        \"\"\"Validate that API changes match version bump\"\"\"
        
        has_breaking_changes = any(
            issue.type in [IssueType.REMOVED_STABLE_API, IssueType.SIGNATURE_CHANGED]
            for issue in api_changes
        )
        
        if has_breaking_changes:
            # Must be a major version bump
            return self._is_major_version_bump()
        
        return True
    
    def _is_major_version_bump(self) -> bool:
        # Check if this is a major version release
        # Implementation depends on your release process
        return True

Acceptance Criteria

  • API stability annotations on all public APIs
  • Backward compatibility testing framework
  • Migration system for version upgrades
  • Semantic versioning enforcement
  • Breaking change detection and documentation

Definition of Done

  • Stability annotations on all public APIs
  • Compatibility checker integrated into CI/CD
  • Migration framework with example migrations
  • Version validation preventing accidental breaking changes
  • Documentation - API stability guarantees and migration guides
  • Tests - compatibility testing across versions

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions