Dual-Axis Inheritance
Examples demonstrating the dual-axis inheritance system.
Understanding Dual-Axis Inheritance
Lazy-config uses two axes for configuration resolution:
X-Axis: Context hierarchy (Global → Pipeline → Step)
Y-Axis: Class inheritance (MRO-based)
X-Axis: Context Hierarchy
Context hierarchy allows nested contexts to override outer contexts:
from dataclasses import dataclass
from objectstate import LazyDataclassFactory, config_context
@dataclass
class GlobalConfig:
environment: str = "dev"
log_level: str = "INFO"
timeout: int = 30
@dataclass
class PipelineConfig:
pipeline_name: str = "default"
timeout: int = 60 # Override global timeout
@dataclass
class StepConfig:
step_name: str = "process"
timeout: int = 10 # Override again for this step
# Create lazy versions
LazyGlobal = LazyDataclassFactory.make_lazy_simple(GlobalConfig)
LazyPipeline = LazyDataclassFactory.make_lazy_simple(PipelineConfig)
LazyStep = LazyDataclassFactory.make_lazy_simple(StepConfig)
# Setup configs
global_cfg = GlobalConfig(
environment="production",
log_level="WARNING",
timeout=30
)
pipeline_cfg = PipelineConfig(
pipeline_name="data-processing",
timeout=60
)
step_cfg = StepConfig(
step_name="transform",
timeout=10
)
# Nested contexts demonstrate X-axis resolution
with config_context(global_cfg):
lazy_global = LazyGlobal()
print(f"Global timeout: {lazy_global.timeout}") # 30
with config_context(pipeline_cfg):
lazy_pipeline = LazyPipeline()
print(f"Pipeline timeout: {lazy_pipeline.timeout}") # 60 (overrides global)
with config_context(step_cfg):
lazy_step = LazyStep()
print(f"Step timeout: {lazy_step.timeout}") # 10 (overrides pipeline)
Y-Axis: Class Inheritance
Class inheritance allows child classes to inherit and override parent fields:
from dataclasses import dataclass
from objectstate import LazyDataclassFactory, config_context
@dataclass
class BaseProcessConfig:
"""Base configuration for all processes."""
input_dir: str = "/data/input"
output_dir: str = "/data/output"
format: str = "json"
@dataclass
class DataProcessConfig(BaseProcessConfig):
"""Configuration for data processing."""
batch_size: int = 100
parallel: bool = False
@dataclass
class MLProcessConfig(DataProcessConfig):
"""Configuration for ML processing."""
model_path: str = "/models/default"
use_gpu: bool = False
# Create lazy version of the most specific class
LazyML = LazyDataclassFactory.make_lazy_simple(MLProcessConfig)
# Set up configuration
ml_config = MLProcessConfig(
input_dir="/data/ml/input", # From BaseProcessConfig
output_dir="/data/ml/output", # From BaseProcessConfig
format="parquet", # From BaseProcessConfig
batch_size=500, # From DataProcessConfig
parallel=True, # From DataProcessConfig
model_path="/models/bert", # From MLProcessConfig
use_gpu=True # From MLProcessConfig
)
with config_context(ml_config):
lazy = LazyML()
# All fields accessible through MRO
print(f"Input: {lazy.input_dir}") # BaseProcessConfig
print(f"Output: {lazy.output_dir}") # BaseProcessConfig
print(f"Format: {lazy.format}") # BaseProcessConfig
print(f"Batch: {lazy.batch_size}") # DataProcessConfig
print(f"Parallel: {lazy.parallel}") # DataProcessConfig
print(f"Model: {lazy.model_path}") # MLProcessConfig
print(f"GPU: {lazy.use_gpu}") # MLProcessConfig
Selective Inheritance
Using None to enable inheritance from parent contexts:
from dataclasses import dataclass
from objectstate import LazyDataclassFactory, config_context
@dataclass
class GlobalConfig:
database_url: str = "postgresql://global"
cache_enabled: bool = True
timeout: int = 30
@dataclass
class ServiceConfig:
service_name: str = "my-service"
database_url: str = None # Will inherit from GlobalConfig
cache_enabled: bool = None # Will inherit from GlobalConfig
timeout: int = 5 # Override global timeout
# Create lazy versions
LazyGlobal = LazyDataclassFactory.make_lazy_simple(GlobalConfig)
LazyService = LazyDataclassFactory.make_lazy_simple(ServiceConfig)
# Setup
global_cfg = GlobalConfig(
database_url="postgresql://prod.db:5432/app",
cache_enabled=True,
timeout=30
)
service_cfg = ServiceConfig(
service_name="payment-service",
# database_url is None - will inherit
# cache_enabled is None - will inherit
timeout=5 # explicit override
)
with config_context(global_cfg):
with config_context(service_cfg):
lazy = LazyService()
print(f"Service: {lazy.service_name}") # "payment-service" (explicit)
print(f"Database: {lazy.database_url}") # Inherited from global
print(f"Cache: {lazy.cache_enabled}") # Inherited from global
print(f"Timeout: {lazy.timeout}") # 5 (explicit override)
Multiple Inheritance with inherit_as_none
The inherit_as_none parameter enables proper dual-axis inheritance when using multiple inheritance:
from dataclasses import dataclass
from objectstate import auto_create_decorator
# Create global config with decorator
@auto_create_decorator
@dataclass
class GlobalPipelineConfig:
num_workers: int = 1
# This creates the `global_pipeline_config` decorator
# Base class with some fields
@dataclass
class StepWellFilterConfig:
persistent: bool = True
well_filter: str = None
timeout: int = 30
# Another base class with different fields
@dataclass
class StreamingDefaults:
host: str = "localhost"
port: int = 5000
transport_mode: str = "tcp"
# Use inherit_as_none for multiple inheritance
@global_pipeline_config(inherit_as_none=True) # Default behavior
@dataclass
class StreamingConfig(StepWellFilterConfig, StreamingDefaults):
"""Config using multiple inheritance.
With inherit_as_none=True, all inherited fields
(persistent, well_filter, timeout, host, port, transport_mode)
are automatically set to None for proper lazy resolution.
"""
stream_type: str = "napari"
buffer_size: int = 1024
# Check the resulting defaults
config = StreamingConfig()
print(f"stream_type: {config.stream_type}") # "napari" (explicit)
print(f"buffer_size: {config.buffer_size}") # 1024 (explicit)
print(f"persistent: {config.persistent}") # None (inherited, set to None)
print(f"well_filter: {config.well_filter}") # None (was already None)
print(f"timeout: {config.timeout}") # None (inherited, set to None)
print(f"host: {config.host}") # None (inherited, set to None)
print(f"port: {config.port}") # None (inherited, set to None)
print(f"transport_mode: {config.transport_mode}") # None (inherited, set to None)
Why this matters:
This enables polymorphic access to inherited fields without type-specific attribute names. All inherited fields can be resolved lazily from the context hierarchy:
from objectstate import config_context
# Create a base config with values
base_config = StepWellFilterConfig(
persistent=False,
well_filter="A01",
timeout=60
)
# Create streaming config without specifying inherited fields
streaming_config = StreamingConfig(
stream_type="custom",
buffer_size=2048
)
# In context, inherited fields resolve from base config
with config_context(base_config):
with config_context(streaming_config):
# LazyStreamingConfig would resolve:
# - stream_type from streaming_config ("custom")
# - buffer_size from streaming_config (2048)
# - persistent from base_config (False)
# - well_filter from base_config ("A01")
# - timeout from base_config (60)
# - host, port, transport_mode from StreamingDefaults
pass
Disabling inherit_as_none
You can disable this behavior if you want inherited fields to keep their defaults:
@global_pipeline_config(inherit_as_none=False)
@dataclass
class ConcreteStreamingConfig(StepWellFilterConfig, StreamingDefaults):
"""Config with concrete inherited defaults.
Inherited fields keep their default values from parent classes.
"""
stream_type: str = "napari"
config = ConcreteStreamingConfig()
print(f"persistent: {config.persistent}") # True (from StepWellFilterConfig)
print(f"timeout: {config.timeout}") # 30 (from StepWellFilterConfig)
print(f"host: {config.host}") # "localhost" (from StreamingDefaults)
print(f"port: {config.port}") # 5000 (from StreamingDefaults)
When to use each:
inherit_as_none=True(default): For lazy resolution with dual-axis inheritanceinherit_as_none=False: When you want concrete defaults from parent classes