# lazy-config
**Generic lazy dataclass configuration framework with dual-axis inheritance**
[](https://badge.fury.io/py/lazy-config)
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/MIT)
## Features
- **Lazy Dataclass Factory**: Dynamically create dataclasses with lazy field resolution
- **Dual-Axis Inheritance**:
- X-Axis: Context hierarchy (Step → Pipeline → Global)
- Y-Axis: Sibling inheritance within same context
- **Contextvars-Based**: Uses Python's `contextvars` for clean context management
- **UI Integration**: Placeholder text generation for configuration forms
- **Thread-Safe**: Thread-local global configuration storage
- **100% Generic**: No application-specific dependencies
- **Pure Stdlib**: No external dependencies
## Quick Start
### Simple Usage (Manual Factory)
```python
from dataclasses import dataclass
from lazy_config import LazyDataclassFactory, config_context, set_base_config_type
# Define your base configuration
@dataclass
class MyConfig:
output_dir: str = "/tmp"
num_workers: int = 4
debug: bool = False
# Initialize the framework with your base config type
set_base_config_type(MyConfig)
# Create lazy version manually
LazyMyConfig = LazyDataclassFactory.make_lazy_simple(MyConfig)
# Use with context
concrete_config = MyConfig(output_dir="/data", num_workers=8)
with config_context(concrete_config):
lazy_cfg = LazyMyConfig()
print(lazy_cfg.output_dir) # "/data" (resolved from context)
print(lazy_cfg.num_workers) # 8 (resolved from context)
print(lazy_cfg.debug) # False (inherited from defaults)
```
### Setting Up Global Config Context (For Advanced Usage)
When using the decorator pattern with `auto_create_decorator`, you need to establish the global configuration context for lazy resolution:
```python
from lazy_config import ensure_global_config_context
# After creating your global config instance
global_config = GlobalPipelineConfig(
num_workers=8,
# ... other fields
)
# REQUIRED: Establish global config context for lazy resolution
ensure_global_config_context(GlobalPipelineConfig, global_config)
# Now lazy configs can resolve from the global context
```
**Key differences:**
- `set_base_config_type(MyConfig)`: Sets the **type** (class) for the framework
- `ensure_global_config_context(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution
- Call `ensure_global_config_context()` at application startup (GUI) or before pipeline execution
## Installation
```bash
pip install lazy-config
```
## Automatic Lazy Config Generation with Decorators
For more complex applications with multiple config types, use the `auto_create_decorator` pattern to automatically generate lazy versions and field injection decorators:
```python
from dataclasses import dataclass
from lazy_config import auto_create_decorator, config_context
# Step 1: Create a global config class with "Global" prefix and apply auto_create_decorator
@auto_create_decorator
@dataclass
class GlobalPipelineConfig:
base_output_dir: str = "/tmp"
verbose: bool = False
# This automatically creates:
# - A decorator named `global_pipeline_config` (snake_case of class name)
# that you can use to decorate other config classes
# - A lazy class `PipelineConfig` (removes "Global" prefix) for lazy resolution
# Step 2: Use the generated decorator on other config classes
@global_pipeline_config # Automatically creates LazyStepConfig
@dataclass
class StepConfig:
step_name: str = "default_step"
iterations: int = 100
@global_pipeline_config # Automatically creates LazyDatabaseConfig
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
# The decorator automatically:
# - Creates lazy versions: LazyStepConfig, LazyDatabaseConfig
# - Registers them for potential field injection into GlobalPipelineConfig
# - Makes them available in your module namespace
```
**Key Benefits:**
- **Auto-generated lazy classes**: Each decorated config automatically gets a lazy version
- **Simplified imports**: Lazy classes are automatically added to your module
- **Decorator factory**: `auto_create_decorator` generates a decorator specific to your global config
- **Type-safe**: All generated classes are proper dataclasses with full IDE support
### Field Injection Behavior
When you use the generated decorator (e.g., `@global_pipeline_config`), the decorated class is automatically **injected as a field** into the global config class:
```python
from dataclasses import dataclass
from lazy_config import auto_create_decorator
# Create global config with auto_create_decorator
@auto_create_decorator
@dataclass
class GlobalPipelineConfig:
num_workers: int = 1
# This creates:
# - A decorator named `global_pipeline_config`
# - A lazy class named `PipelineConfig`
# Use the decorator on a new config class
@global_pipeline_config
@dataclass
class WellFilterConfig:
well_filter: str = None
mode: str = "include"
# After module loading, GlobalPipelineConfig automatically has:
# - well_filter_config: WellFilterConfig = WellFilterConfig()
# And LazyWellFilterConfig is auto-created
```
**How it works:**
- Decorated classes are injected as fields into `GlobalPipelineConfig`
- Field name is snake_case of class name (e.g., `WellFilterConfig` → `well_filter_config`)
- Lazy version is automatically created (e.g., `LazyWellFilterConfig`)
- Injection happens at end of module loading via `_inject_all_pending_fields()`
This enables a clean, modular configuration structure where each component's config is automatically part of the global configuration.
### Decorator Parameters
The generated decorator (e.g., `@global_pipeline_config`) supports optional parameters:
#### `inherit_as_none` (Default: `True`)
Sets all inherited fields from parent classes to `None` by default, enabling proper lazy resolution:
```python
@dataclass
class BaseConfig:
timeout: int = 30
retries: int = 3
@global_pipeline_config(inherit_as_none=True) # Default behavior
@dataclass
class ServiceConfig(BaseConfig):
service_name: str = "my-service"
# timeout and retries automatically set to None for lazy inheritance
# This allows ServiceConfig to inherit timeout/retries from context
# rather than using the base class defaults
```
**Why this matters:**
- Enables polymorphic access without type-specific attribute names
- Critical for dual-axis inheritance with multiple inheritance
- Uses `InheritAsNoneMeta` metaclass internally
#### `ui_hidden` (Default: `False`)
Hides configs from UI while still applying decorator behavior and keeping them in the resolution context:
```python
@global_pipeline_config(ui_hidden=True)
@dataclass
class InternalConfig:
internal_setting: str = "hidden"
# This config won't appear in UI but is still available for inheritance
```
**Use cases:**
- Intermediate configs that are only inherited by other configs
- Internal implementation details not meant for user configuration
- Base classes that should never be directly instantiated in UI
### Nested Dataclass Lazification
When creating a lazy dataclass, **nested dataclass fields are automatically converted** to their lazy versions:
```python
from dataclasses import dataclass
from lazy_config import LazyDataclassFactory
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
@dataclass
class AppConfig:
db_config: DatabaseConfig = DatabaseConfig()
app_name: str = "MyApp"
# Create lazy version - nested configs are automatically lazified
LazyAppConfig = LazyDataclassFactory.make_lazy_simple(AppConfig)
# The db_config field is automatically converted to LazyDatabaseConfig
# You don't need to manually create LazyDatabaseConfig first!
```
**Benefits:**
- No need to manually create lazy versions of nested configs
- Preserves field metadata (e.g., `ui_hidden` flag)
- Creates default factories for Optional dataclass fields
- Uses `register_lazy_type_mapping()` internally
## Why lazy-config?
**Before** (Manual parameter passing):
```python
def process_step(data, output_dir, num_workers, debug, ...):
# Pass 20+ parameters through every function
result = sub_process(data, output_dir, num_workers, debug, ...)
return result
def sub_process(data, output_dir, num_workers, debug, ...):
# Repeat parameter declarations everywhere
...
```
**After** (lazy-config):
```python
@dataclass
class StepConfig:
output_dir: str = None
num_workers: int = None
debug: bool = None
def process_step(data, config: LazyStepConfig):
# Config fields resolve automatically from context
print(config.output_dir) # Resolved from context hierarchy
result = sub_process(data, config)
return result
```
## Advanced Features
### Dual-Axis Inheritance
```python
# X-Axis: Context hierarchy
with config_context(global_config):
with config_context(pipeline_config):
with config_context(step_config):
# Resolves: step → pipeline → global → defaults
value = lazy_config.some_field
# Y-Axis: Sibling inheritance (MRO-based)
@dataclass
class BaseConfig:
field_a: str = "base"
@dataclass
class SpecializedConfig(BaseConfig):
field_b: str = "specialized"
# SpecializedConfig inherits field_a from BaseConfig
```
### Placeholder Generation for UI
```python
from lazy_config import LazyDefaultPlaceholderService
service = LazyDefaultPlaceholderService()
# Generate placeholder text showing inherited values
placeholder = service.get_placeholder_text(
lazy_config,
"output_dir",
available_configs
)
# Returns: "Inherited: /data (from GlobalConfig)"
```
### Cache Warming
```python
from lazy_config import prewarm_config_analysis_cache
# Pre-warm caches for faster runtime resolution
prewarm_config_analysis_cache([GlobalConfig, PipelineConfig, StepConfig])
```
## Architecture
### Dual-Axis Resolution
The framework uses pure MRO-based dual-axis resolution:
**X-Axis (Context Hierarchy)**:
```
Step Context → Pipeline Context → Global Context → Static Defaults
```
**Y-Axis (MRO Traversal)**:
```
Most specific class → Least specific class (following Python's MRO)
```
**How it works:**
1. Context hierarchy is flattened into a single `available_configs` dict
2. For each field resolution, traverse the requesting object's MRO from most to least specific
3. For each MRO class, check if there's a config instance in `available_configs` with a concrete (non-None) value
4. Return the first concrete value found
## Documentation
Full documentation available at [lazy-config.readthedocs.io](https://lazy-config.readthedocs.io)
## Requirements
- Python 3.10+
- No external dependencies (pure stdlib)
## License
MIT License - see LICENSE file for details
## Contributing
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
## Credits
Developed by Tristan Simas as part of the OpenHCS project.
Raw data
{
"_id": null,
"home_page": null,
"name": "hieraconf",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "configuration, contextvars, dataclass, hierarchical, inheritance, lazy",
"author": null,
"author_email": "Tristan Simas <tristan.simas@mail.mcgill.ca>",
"download_url": "https://files.pythonhosted.org/packages/1f/25/c84e888ad7019ec075d0d54f62d8f685132edc03cff70a41d04fe320694b/hieraconf-0.1.0.tar.gz",
"platform": null,
"description": "# lazy-config\n\n**Generic lazy dataclass configuration framework with dual-axis inheritance**\n\n[](https://badge.fury.io/py/lazy-config)\n[](https://www.python.org/downloads/)\n[](https://opensource.org/licenses/MIT)\n\n## Features\n\n- **Lazy Dataclass Factory**: Dynamically create dataclasses with lazy field resolution\n- **Dual-Axis Inheritance**: \n - X-Axis: Context hierarchy (Step \u2192 Pipeline \u2192 Global)\n - Y-Axis: Sibling inheritance within same context\n- **Contextvars-Based**: Uses Python's `contextvars` for clean context management\n- **UI Integration**: Placeholder text generation for configuration forms\n- **Thread-Safe**: Thread-local global configuration storage\n- **100% Generic**: No application-specific dependencies\n- **Pure Stdlib**: No external dependencies\n\n## Quick Start\n\n### Simple Usage (Manual Factory)\n\n```python\nfrom dataclasses import dataclass\nfrom lazy_config import LazyDataclassFactory, config_context, set_base_config_type\n\n# Define your base configuration\n@dataclass\nclass MyConfig:\n output_dir: str = \"/tmp\"\n num_workers: int = 4\n debug: bool = False\n\n# Initialize the framework with your base config type\nset_base_config_type(MyConfig)\n\n# Create lazy version manually\nLazyMyConfig = LazyDataclassFactory.make_lazy_simple(MyConfig)\n\n# Use with context\nconcrete_config = MyConfig(output_dir=\"/data\", num_workers=8)\n\nwith config_context(concrete_config):\n lazy_cfg = LazyMyConfig()\n print(lazy_cfg.output_dir) # \"/data\" (resolved from context)\n print(lazy_cfg.num_workers) # 8 (resolved from context)\n print(lazy_cfg.debug) # False (inherited from defaults)\n```\n\n### Setting Up Global Config Context (For Advanced Usage)\n\nWhen using the decorator pattern with `auto_create_decorator`, you need to establish the global configuration context for lazy resolution:\n\n```python\nfrom lazy_config import ensure_global_config_context\n\n# After creating your global config instance\nglobal_config = GlobalPipelineConfig(\n num_workers=8,\n # ... other fields\n)\n\n# REQUIRED: Establish global config context for lazy resolution\nensure_global_config_context(GlobalPipelineConfig, global_config)\n\n# Now lazy configs can resolve from the global context\n```\n\n**Key differences:**\n- `set_base_config_type(MyConfig)`: Sets the **type** (class) for the framework\n- `ensure_global_config_context(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution\n- Call `ensure_global_config_context()` at application startup (GUI) or before pipeline execution\n\n## Installation\n\n```bash\npip install lazy-config\n```\n\n## Automatic Lazy Config Generation with Decorators\n\nFor more complex applications with multiple config types, use the `auto_create_decorator` pattern to automatically generate lazy versions and field injection decorators:\n\n```python\nfrom dataclasses import dataclass\nfrom lazy_config import auto_create_decorator, config_context\n\n# Step 1: Create a global config class with \"Global\" prefix and apply auto_create_decorator\n@auto_create_decorator\n@dataclass\nclass GlobalPipelineConfig:\n base_output_dir: str = \"/tmp\"\n verbose: bool = False\n\n# This automatically creates:\n# - A decorator named `global_pipeline_config` (snake_case of class name)\n# that you can use to decorate other config classes\n# - A lazy class `PipelineConfig` (removes \"Global\" prefix) for lazy resolution\n\n# Step 2: Use the generated decorator on other config classes\n@global_pipeline_config # Automatically creates LazyStepConfig\n@dataclass\nclass StepConfig:\n step_name: str = \"default_step\"\n iterations: int = 100\n\n@global_pipeline_config # Automatically creates LazyDatabaseConfig \n@dataclass\nclass DatabaseConfig:\n host: str = \"localhost\"\n port: int = 5432\n\n# The decorator automatically:\n# - Creates lazy versions: LazyStepConfig, LazyDatabaseConfig\n# - Registers them for potential field injection into GlobalPipelineConfig\n# - Makes them available in your module namespace\n```\n\n**Key Benefits:**\n- **Auto-generated lazy classes**: Each decorated config automatically gets a lazy version\n- **Simplified imports**: Lazy classes are automatically added to your module\n- **Decorator factory**: `auto_create_decorator` generates a decorator specific to your global config\n- **Type-safe**: All generated classes are proper dataclasses with full IDE support\n\n### Field Injection Behavior\n\nWhen you use the generated decorator (e.g., `@global_pipeline_config`), the decorated class is automatically **injected as a field** into the global config class:\n\n```python\nfrom dataclasses import dataclass\nfrom lazy_config import auto_create_decorator\n\n# Create global config with auto_create_decorator\n@auto_create_decorator\n@dataclass\nclass GlobalPipelineConfig:\n num_workers: int = 1\n\n# This creates:\n# - A decorator named `global_pipeline_config`\n# - A lazy class named `PipelineConfig`\n\n# Use the decorator on a new config class\n@global_pipeline_config\n@dataclass\nclass WellFilterConfig:\n well_filter: str = None\n mode: str = \"include\"\n\n# After module loading, GlobalPipelineConfig automatically has:\n# - well_filter_config: WellFilterConfig = WellFilterConfig()\n# And LazyWellFilterConfig is auto-created\n```\n\n**How it works:**\n- Decorated classes are injected as fields into `GlobalPipelineConfig`\n- Field name is snake_case of class name (e.g., `WellFilterConfig` \u2192 `well_filter_config`)\n- Lazy version is automatically created (e.g., `LazyWellFilterConfig`)\n- Injection happens at end of module loading via `_inject_all_pending_fields()`\n\nThis enables a clean, modular configuration structure where each component's config is automatically part of the global configuration.\n\n### Decorator Parameters\n\nThe generated decorator (e.g., `@global_pipeline_config`) supports optional parameters:\n\n#### `inherit_as_none` (Default: `True`)\n\nSets all inherited fields from parent classes to `None` by default, enabling proper lazy resolution:\n\n```python\n@dataclass\nclass BaseConfig:\n timeout: int = 30\n retries: int = 3\n\n@global_pipeline_config(inherit_as_none=True) # Default behavior\n@dataclass\nclass ServiceConfig(BaseConfig):\n service_name: str = \"my-service\"\n # timeout and retries automatically set to None for lazy inheritance\n\n# This allows ServiceConfig to inherit timeout/retries from context\n# rather than using the base class defaults\n```\n\n**Why this matters:**\n- Enables polymorphic access without type-specific attribute names\n- Critical for dual-axis inheritance with multiple inheritance\n- Uses `InheritAsNoneMeta` metaclass internally\n\n#### `ui_hidden` (Default: `False`)\n\nHides configs from UI while still applying decorator behavior and keeping them in the resolution context:\n\n```python\n@global_pipeline_config(ui_hidden=True)\n@dataclass\nclass InternalConfig:\n internal_setting: str = \"hidden\"\n # This config won't appear in UI but is still available for inheritance\n```\n\n**Use cases:**\n- Intermediate configs that are only inherited by other configs\n- Internal implementation details not meant for user configuration\n- Base classes that should never be directly instantiated in UI\n\n### Nested Dataclass Lazification\n\nWhen creating a lazy dataclass, **nested dataclass fields are automatically converted** to their lazy versions:\n\n```python\nfrom dataclasses import dataclass\nfrom lazy_config import LazyDataclassFactory\n\n@dataclass\nclass DatabaseConfig:\n host: str = \"localhost\"\n port: int = 5432\n\n@dataclass\nclass AppConfig:\n db_config: DatabaseConfig = DatabaseConfig()\n app_name: str = \"MyApp\"\n\n# Create lazy version - nested configs are automatically lazified\nLazyAppConfig = LazyDataclassFactory.make_lazy_simple(AppConfig)\n\n# The db_config field is automatically converted to LazyDatabaseConfig\n# You don't need to manually create LazyDatabaseConfig first!\n```\n\n**Benefits:**\n- No need to manually create lazy versions of nested configs\n- Preserves field metadata (e.g., `ui_hidden` flag)\n- Creates default factories for Optional dataclass fields\n- Uses `register_lazy_type_mapping()` internally\n\n## Why lazy-config?\n\n**Before** (Manual parameter passing):\n```python\ndef process_step(data, output_dir, num_workers, debug, ...):\n # Pass 20+ parameters through every function\n result = sub_process(data, output_dir, num_workers, debug, ...)\n return result\n\ndef sub_process(data, output_dir, num_workers, debug, ...):\n # Repeat parameter declarations everywhere\n ...\n```\n\n**After** (lazy-config):\n```python\n@dataclass\nclass StepConfig:\n output_dir: str = None\n num_workers: int = None\n debug: bool = None\n\ndef process_step(data, config: LazyStepConfig):\n # Config fields resolve automatically from context\n print(config.output_dir) # Resolved from context hierarchy\n result = sub_process(data, config)\n return result\n```\n\n## Advanced Features\n\n### Dual-Axis Inheritance\n\n```python\n# X-Axis: Context hierarchy\nwith config_context(global_config):\n with config_context(pipeline_config):\n with config_context(step_config):\n # Resolves: step \u2192 pipeline \u2192 global \u2192 defaults\n value = lazy_config.some_field\n\n# Y-Axis: Sibling inheritance (MRO-based)\n@dataclass\nclass BaseConfig:\n field_a: str = \"base\"\n\n@dataclass\nclass SpecializedConfig(BaseConfig):\n field_b: str = \"specialized\"\n\n# SpecializedConfig inherits field_a from BaseConfig\n```\n\n### Placeholder Generation for UI\n\n```python\nfrom lazy_config import LazyDefaultPlaceholderService\n\nservice = LazyDefaultPlaceholderService()\n\n# Generate placeholder text showing inherited values\nplaceholder = service.get_placeholder_text(\n lazy_config,\n \"output_dir\",\n available_configs\n)\n# Returns: \"Inherited: /data (from GlobalConfig)\"\n```\n\n### Cache Warming\n\n```python\nfrom lazy_config import prewarm_config_analysis_cache\n\n# Pre-warm caches for faster runtime resolution\nprewarm_config_analysis_cache([GlobalConfig, PipelineConfig, StepConfig])\n```\n\n## Architecture\n\n### Dual-Axis Resolution\n\nThe framework uses pure MRO-based dual-axis resolution:\n\n**X-Axis (Context Hierarchy)**:\n```\nStep Context \u2192 Pipeline Context \u2192 Global Context \u2192 Static Defaults\n```\n\n**Y-Axis (MRO Traversal)**:\n```\nMost specific class \u2192 Least specific class (following Python's MRO)\n```\n\n**How it works:**\n1. Context hierarchy is flattened into a single `available_configs` dict\n2. For each field resolution, traverse the requesting object's MRO from most to least specific\n3. For each MRO class, check if there's a config instance in `available_configs` with a concrete (non-None) value\n4. Return the first concrete value found\n\n## Documentation\n\nFull documentation available at [lazy-config.readthedocs.io](https://lazy-config.readthedocs.io)\n\n## Requirements\n\n- Python 3.10+\n- No external dependencies (pure stdlib)\n\n## License\n\nMIT License - see LICENSE file for details\n\n## Contributing\n\nContributions welcome! Please see CONTRIBUTING.md for guidelines.\n\n## Credits\n\nDeveloped by Tristan Simas as part of the OpenHCS project.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Generic lazy dataclass configuration framework with dual-axis inheritance and contextvars-based resolution",
"version": "0.1.0",
"project_urls": {
"Documentation": "https://hieraconf.readthedocs.io",
"Homepage": "https://github.com/trissim/hieraconf",
"Issues": "https://github.com/trissim/hieraconf/issues",
"Repository": "https://github.com/trissim/hieraconf"
},
"split_keywords": [
"configuration",
" contextvars",
" dataclass",
" hierarchical",
" inheritance",
" lazy"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "00fb4b352e918bd0f4245b8aef677d5d405d9f6e5be462161065f9907fe2dcfa",
"md5": "250028d448ae36fe06e68f3d2578de63",
"sha256": "8e1121566f52ce7ef839941392834ef90afe41ebfa44ca7a7a3d95fc74e0930f"
},
"downloads": -1,
"filename": "hieraconf-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "250028d448ae36fe06e68f3d2578de63",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 34968,
"upload_time": "2025-11-01T23:14:17",
"upload_time_iso_8601": "2025-11-01T23:14:17.242362Z",
"url": "https://files.pythonhosted.org/packages/00/fb/4b352e918bd0f4245b8aef677d5d405d9f6e5be462161065f9907fe2dcfa/hieraconf-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "1f25c84e888ad7019ec075d0d54f62d8f685132edc03cff70a41d04fe320694b",
"md5": "bbde2b2ddc835b8a5463006bb36b15fc",
"sha256": "d0b278ca605f20502c9aaafb3bf268fcd55620002e7326be190f511e6413a222"
},
"downloads": -1,
"filename": "hieraconf-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "bbde2b2ddc835b8a5463006bb36b15fc",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 50275,
"upload_time": "2025-11-01T23:14:18",
"upload_time_iso_8601": "2025-11-01T23:14:18.671504Z",
"url": "https://files.pythonhosted.org/packages/1f/25/c84e888ad7019ec075d0d54f62d8f685132edc03cff70a41d04fe320694b/hieraconf-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-11-01 23:14:18",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "trissim",
"github_project": "hieraconf",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "hieraconf"
}