Source code for cursus.step_catalog.adapters.config_class_detector
"""
Config class detector adapters for backward compatibility.
This module provides adapters that maintain existing config class detection APIs
during the migration from legacy discovery systems to the unified StepCatalog system.
"""
import logging
from pathlib import Path
from typing import Dict, Optional, Any, Type
from ..step_catalog import StepCatalog
logger = logging.getLogger(__name__)
[docs]
class ConfigClassDetectorAdapter:
"""
Adapter maintaining backward compatibility with ConfigClassDetector.
Replaces: src/cursus/core/config_fields/config_class_detector.py
MODERN APPROACH: Uses step catalog's superior AST-based config discovery
instead of legacy JSON parsing. This provides more accurate and comprehensive
config class detection.
"""
# Constants for backward compatibility (minimal legacy support)
MODEL_TYPE_FIELD = "__model_type__"
METADATA_FIELD = "metadata"
CONFIG_TYPES_FIELD = "config_types"
CONFIGURATION_FIELD = "configuration"
SPECIFIC_FIELD = "specific"
def __init__(self, workspace_root: Optional[Path] = None):
"""Initialize with unified catalog."""
# PORTABLE: Use package-only discovery by default (works in all deployment scenarios)
if workspace_root is None:
self.catalog = StepCatalog(workspace_dirs=None)
else:
self.catalog = StepCatalog(workspace_dirs=[workspace_root])
self.logger = logging.getLogger(__name__)
[docs]
@staticmethod
def detect_from_json(config_path: str) -> Dict[str, Any]:
"""
MODERN APPROACH: Use step catalog's AST-based discovery instead of JSON parsing.
This method now uses the superior AST-based discovery from the unified step catalog
rather than the legacy JSON parsing approach. This provides more accurate and
comprehensive config class detection by analyzing actual source code.
Real usage pattern (from dynamic_template.py):
detected_classes = detect_config_classes_from_json(self.config_path)
Args:
config_path: Path to configuration file (used to determine workspace root)
Returns:
Dictionary mapping config class names to config class types
"""
try:
# PORTABLE: Use package-only discovery (works in all deployment scenarios)
catalog = StepCatalog(workspace_dirs=None)
# Get complete config classes (AST discovery + ConfigClassStore integration)
config_classes = catalog.build_complete_config_classes()
logger = logging.getLogger(__name__)
logger.info(
f"Discovered {len(config_classes)} config classes via unified step catalog (AST-based)"
)
return config_classes
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error detecting config classes via step catalog: {e}")
# No fallback needed - return empty dict if step catalog fails
logger.warning("Step catalog discovery failed, returning empty dict")
return {}
@staticmethod
def _extract_class_names(config_data: Dict[str, Any], logger) -> set:
"""
LEGACY COMPATIBILITY: Minimal JSON parsing for backward compatibility only.
This method is maintained for any legacy code that might still use it,
but new code should use the step catalog's AST-based discovery instead.
NOTE: Analysis shows this is only used in tests, not in production code.
"""
class_names = set()
try:
# Extract from metadata.config_types (legacy pattern)
if ConfigClassDetectorAdapter.METADATA_FIELD in config_data:
metadata = config_data[ConfigClassDetectorAdapter.METADATA_FIELD]
if (
isinstance(metadata, dict)
and ConfigClassDetectorAdapter.CONFIG_TYPES_FIELD in metadata
):
config_types = metadata[
ConfigClassDetectorAdapter.CONFIG_TYPES_FIELD
]
if isinstance(config_types, dict):
class_names.update(config_types.values())
# Extract from configuration.specific sections (legacy pattern)
if ConfigClassDetectorAdapter.CONFIGURATION_FIELD in config_data:
configuration = config_data[
ConfigClassDetectorAdapter.CONFIGURATION_FIELD
]
if (
isinstance(configuration, dict)
and ConfigClassDetectorAdapter.SPECIFIC_FIELD in configuration
):
specific = configuration[ConfigClassDetectorAdapter.SPECIFIC_FIELD]
if isinstance(specific, dict):
for step_config in specific.values():
if (
isinstance(step_config, dict)
and ConfigClassDetectorAdapter.MODEL_TYPE_FIELD
in step_config
):
class_names.add(
step_config[
ConfigClassDetectorAdapter.MODEL_TYPE_FIELD
]
)
logger.debug(
f"Legacy JSON parsing extracted {len(class_names)} class names"
)
return class_names
except Exception as e:
logger.error(f"Error in legacy JSON parsing: {e}")
return set()
[docs]
@classmethod
def from_config_store(cls, config_path: str) -> Dict[str, Any]:
"""
MODERN APPROACH: Use step catalog's integrated discovery.
This method uses the step catalog's build_complete_config_classes which
integrates both AST-based discovery and ConfigClassStore registration,
providing the most comprehensive config class detection.
"""
return cls.detect_from_json(config_path)
[docs]
class ConfigClassStoreAdapter:
"""
Adapter maintaining backward compatibility with ConfigClassStore.
Replaces: src/cursus/core/config_fields/config_class_store.py (partial)
"""
# Single registry instance - implementing Single Source of Truth
_registry: Dict[str, Any] = {}
_logger = logging.getLogger(__name__)
[docs]
@classmethod
def register(cls, config_class: Optional[Any] = None) -> Any:
"""Legacy method: register a config class."""
def _register(cls_to_register: Any) -> Any:
cls_name = cls_to_register.__name__
if cls_name in cls._registry and cls._registry[cls_name] != cls_to_register:
cls._logger.warning(
f"Class {cls_name} is already registered and is being overwritten."
)
cls._registry[cls_name] = cls_to_register
cls._logger.debug(f"Registered class: {cls_name}")
return cls_to_register
if config_class is not None:
return _register(config_class)
return _register
[docs]
@classmethod
def get_class(cls, class_name: str) -> Optional[Any]:
"""Legacy method: get a registered class by name."""
class_obj = cls._registry.get(class_name)
if class_obj is None:
cls._logger.debug(f"Class not found in registry: {class_name}")
return class_obj
[docs]
@classmethod
def get_all_classes(cls) -> Dict[str, Any]:
"""Legacy method: get all registered classes."""
return cls._registry.copy()
[docs]
@classmethod
def register_many(cls, *config_classes: Any) -> None:
"""Legacy method: register multiple config classes at once."""
for config_class in config_classes:
cls.register(config_class)
[docs]
@classmethod
def clear(cls) -> None:
"""Legacy method: clear the registry."""
cls._registry.clear()
cls._logger.debug("Cleared config class registry")
[docs]
@classmethod
def registered_names(cls) -> set:
"""Legacy method: get all registered class names."""
return set(cls._registry.keys())
# Legacy functions for backward compatibility
[docs]
def build_complete_config_classes(project_id: Optional[str] = None) -> Dict[str, Any]:
"""Legacy function: build complete config classes using catalog."""
try:
# PORTABLE: Use package-only discovery (works in all deployment scenarios)
catalog = StepCatalog(workspace_dirs=None)
# Use catalog's build_complete_config_classes method
config_classes = catalog.build_complete_config_classes(project_id)
logger = logging.getLogger(__name__)
logger.info(
f"Built {len(config_classes)} complete config classes via unified catalog"
)
return config_classes
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error building complete config classes: {e}")
# Fallback to registered classes
return ConfigClassStoreAdapter.get_all_classes()
[docs]
def detect_config_classes_from_json(config_path: str) -> Dict[str, Any]:
"""Legacy function: detect config classes using catalog."""
return ConfigClassDetectorAdapter.detect_from_json(config_path)