Simple yet powerful "Configuration as Code" library
Project description
Configuronic
Configuronic is a simple yet powerful "Configuration as Code" library designed for modern Python applications, particularly in robotics, machine learning, and complex system configurations. Born from the need for a cleaner alternative to existing configuration frameworks, configuronic embraces Python's native syntax while providing powerful CLI integration and hierarchical configuration management.
✨ Why Configuronic?
- 🎯 DRY Principle: Write configurations in Python, not YAML/JSON/XML
- 🚀 CLI-First: Automatic command-line interfaces with complex nested parameter support
- 🔧 Simple & Minimal: Clean API that gets out of your way
- 🌳 Hierarchical: Deep nesting and inheritance support
- 🔄 Dynamic: Runtime configuration resolution with relative imports
🚀 Quick Start
import configuronic as cfn
@cfn.config(learning_rate=0.001, epochs=100)
def train_model(learning_rate: float, epochs: int, model_name: str = "bert-base"):
print(f"Training {model_name} for {epochs} epochs with lr={learning_rate}")
# Your training logic here
if __name__ == "__main__":
cfn.cli(train_model)
Run from command line:
# Use defaults
python train.py
# Override parameters
python train.py --learning_rate=0.0001 --epochs=50 --model_name="gpt-2"
# See current configuration
python train.py --help
📖 Table of Contents
- Installation
- Core Concepts
- Real-World Examples
- Advanced Features
- CLI Usage
- API Reference
- Best Practices
- Contributing
📦 Installation
Using pip
pip install configuronic
Development Installation
git clone https://github.com/Positronic-Robotics/configuronic.git
cd configuronic
uv venv -p 3.10
source .venv/bin/activate
uv pip install -e .[dev]
🧠 Core Concepts
Configuration as Code
In configuronic, configurations are closures - callables that store both the function and its arguments. It is somewhat similar to functools.partial This functional approach enables powerful composition and inheritance patterns.
import configuronic as cfn
# Create a configuration
@cfn.config(batch_size=32, lr=0.001)
def create_optimizer(batch_size: int, lr: float):
return torch.optim.Adam(lr=lr)
# Override and create variants
fast_optimizer = create_optimizer.override(lr=0.01)
large_batch_optimizer = create_optimizer.override(batch_size=128)
# Instantiate when needed
optimizer = fast_optimizer.instantiate()
Two Main Operations
1. override(**kwargs) - Create configuration variants
base_config = cfn.Config(MyModel, layers=3, units=64)
deep_config = base_config.override(layers=6)
wide_config = base_config.override(units=128)
2. instantiate() - Execute function with configured arguments and get its result
model = deep_config.instantiate() # Returns MyModel(layers=6, units=64)
Callable Syntax - Config objects are callable, providing a shorthand for override + instantiate
# These are equivalent:
result1 = config.override(param=value).instantiate()
result2 = config(param=value)()
Nested Configuration Override
Support for deep parameter modification using dot notation:
# Configure a complex training pipeline
training_cfg = cfn.Config(
train_pipeline,
model=cfn.Config(TransformerModel, layers=6, hidden_size=512),
optimizer=cfn.Config(torch.optim.Adam, lr=0.001),
data=cfn.Config(DataLoader, batch_size=32)
)
# Override nested parameters
fast_training = training_cfg.override(**{
"optimizer.lr": 0.01,
"data.batch_size": 64,
"model.layers": 12
})
🌍 Real-World Examples
Robotics Hardware Configuration
import configuronic as cfn
@cfn.config(ip="172.168.0.2")
def robot_arm(ip: str, relative_dynamics_factor: float = 0.2):
from my_robots import FrankaArm
return FrankaArm(ip=ip, dynamics_factor=relative_dynamics_factor)
@cfn.config(device_path="/dev/video0", fps=30)
def camera(device_path: str, width: int = 1920, height: int = 1080, fps: int = 30):
from my_cameras import Camera
return Camera(device_path, width, height, fps)
# Create specific hardware configurations
left_camera = camera.override(device_path="/dev/video1")
right_camera = camera.override(device_path="/dev/video2")
# Main system configuration
@cfn.config(arm=robot_arm,
cameras={'left': left_cam, 'right': right_cam})
def main(arm, cameras, gripper=None):
from robot_library import RobotSystem
system = RobotSystem(arm=arm, cameras=cameras, gripper=gripper)
system.run()
if __name__ == "__main__":
cfn.cli(main)
Machine Learning Pipeline
@cfn.config(model_name="bert-base", max_length=512)
def create_tokenizer(model_name: str, max_length: int):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.model_max_length = max_length
return tokenizer
@cfn.config(hidden_size=768, num_layers=12)
def create_model(hidden_size: int, num_layers: int, tokenizer):
vocab_size = len(tokenizer)
return TransformerModel(vocab_size, hidden_size, num_layers)
# Configure the complete pipeline
@cfn.config()
def training_pipeline(
tokenizer=create_tokenizer,
model=create_model,
learning_rate: float = 1e-4,
batch_size: int = 16
):
# Pipeline implementation
return TrainingPipeline(tokenizer, model, learning_rate, batch_size)
if __name__ == "__main__":
cfn.cli(training_pipeline)
Run with different configurations:
# Use defaults
python train.py
# Quick experiments
python train.py --learning_rate=1e-3 --batch_size=32
# Override nested model parameters
python train.py --model.num_layers=6 --tokenizer.max_length=256
# Switch to different model entirely
python train.py --tokenizer.model_name="gpt2" --model.hidden_size=1024
🔧 Advanced Features
Import Resolution with @ and .
Configuronic provides powerful import resolution syntax that allows you to dynamically reference Python objects, especially useful for CLI usage.
Absolute Imports (@)
Direct import paths to any Python object. If you need to use a literal @ at the beginning of a string (not for imports), use @@:
# From command line - these import exact module paths
python train.py --model="@transformers.BertModel" # Import BertModel
python train.py --message="@@starts_with_at" # Literal string "@starts_with_at"
python train.py --text="foo@bar" # No escaping needed in the middle
Absolute imports could also be used in both Python decorators and classes. However, we don't suggest using them:
@cfn.config(model="@torchvision.models.resnet34")
def get_model_parameters(model: torch.nn.Module):
return list(model.named_parameters())
def get_model_parameters(model: torch.nn.Module):
return list(model.named_parameters())
resnet_model_parameters = cfn.Config(
get_model_parameters,
model="@torchvision.models.resnet34"
)
Relative Imports (.)
Navigate relative to the current module, similar to Python's relative import syntax:
# If default is myproject.models.BertEncoder
python train.py --encoder=".RobertaEncoder" # -> myproject.models.RobertaEncoder (same module)
python train.py --encoder="..utils.CustomEncoder" # -> myproject.utils.CustomEncoder (parent module)
python train.py --encoder="...shared.BaseEncoder" # -> myproject.shared.BaseEncoder (grandparent module)
How it works: Each . acts like ../ in file system navigation:
.= stay in current module (like./)..= go up one module level (like../)...= go up two module levels (like../../), etc.
The path after the dots specifies the target within that module hierarchy.
Configuration Copy Across Modules
The copy() method updates module context so relative imports (.) resolve from the new module location:
# configs/base.py
original_config = cfn.Config(SomeClass, value=1)
# experiments/vision.py
from configs.base import original_config
# Copy updates the config's module context to experiments.vision
copied_config = original_config.copy()
@cfn.config()
def local_function():
return "local result"
# When copied_config is used as default, '.' resolves in experiments.vision
env_cfg = cfn.Config(Environment, setup=copied_config)
specialized_cfg = env_cfg.override(setup=".local_function") # Finds local_function
Without copy(), .local_function would try to resolve in configs.base and fail.
Lists and Dictionaries
Configuronic seamlessly handles nested data structures:
simulation_cfg = cfn.Config(
run_simulation,
loaders=[
cfn.Config(AddCameras, camera_config=camera_cfg),
cfn.Config(AddObjects, objects=["cube", "sphere"]),
cfn.Config(SetLighting, intensity=0.8)
],
cameras={
'main': cfn.Config(Camera, position=[0, 0, 1]),
'side': cfn.Config(Camera, position=[1, 0, 0])
}
)
# Override specific items
modified_sim = simulation_cfg.override(**{
"loaders.0.camera_config.fps": 60, # First loader's camera FPS
"cameras.main.position": [0, 0, 2] # Main camera position
})
Configuration Inheritance
# Base configuration
base_camera = cfn.Config(Camera, width=1920, height=1080, fps=30)
# Derived configurations
hd_camera = base_camera.override(width=1280, height=720)
high_fps_camera = base_camera.override(fps=60)
webcam = base_camera.override(width=640, height=480, fps=15)
# All inherit base settings unless overridden
🖥️ CLI Usage
Configuronic leverages Python Fire for automatic CLI generation:
Basic CLI
@cfn.config(param1="default", param2=42)
def my_function(param1: str, param2: int):
return f"{param1}: {param2}"
if __name__ == "__main__":
cfn.cli(my_function)
Command Line Examples
# Show help and current config
python script.py --help
# Override parameters
python script.py --param1="hello" --param2=100
# Nested parameter override
python script.py --model.layers=6 --optimizer.lr=0.001
# Using absolute imports
python script.py --model="@my_models.CustomTransformer"
# To pass a string that starts with @, repeat it twice
python script.py --message="@@this_is_literal_at_sign"
# Using relative imports
python script.py --tokenizer=".CustomTokenizer"
# Complex nested overrides
python script.py --cameras.left.fps=60 --cameras.right.device="/dev/video2"
Multi-Command CLI
You can also provide multiple commands in a single script by passing a dictionary of configurations:
@cfn.config()
def sum_numbers(x: float, y: float) -> float:
"""Sum of two numbers"""
return x + y
@cfn.config()
def multiply_numbers(x: float, y: float) -> float:
"""Product of two numbers"""
return x * y
if __name__ == "__main__":
cfn.cli({'sum': sum_numbers, 'multiply': multiply_numbers})
This enables running different commands from the same script:
# Run sum command
python script.py sum --x=5 --y=10
# Output: 15.0
# Run multiply command
python script.py multiply --x=5 --y=10
# Output: 50.0
# Get help for all commands
python script.py --help
# Shows available commands with their descriptions
# Get help for specific command
python script.py sum --help
# Shows detailed help for the sum command
Parameter Override Order ⚠️
Important: Parameter overrides are executed in order of declaration. When overriding nested configurations, set the parent object first, then its properties:
# ✅ Correct: set camera first, then its resolution
python script.py --camera="@opencv.Camera" --camera.resolution="full_hd"
# ❌ Incorrect: this will reset camera after setting resolution
python script.py --camera.resolution="full_hd" --camera="@opencv.Camera"
In the incorrect example, the default camera's resolution gets updated first, but then the entire camera object is replaced, losing the resolution override.
📚 API Reference
Core Classes
Config(target, *args, **kwargs)
Main configuration class that stores a callable and its arguments.
Methods:
override(**kwargs) -> Config: Create new config with updated parametersinstantiate() -> Any: Execute the configuration and return resultcopy() -> Config: Deep copy the configuration__call__(**kwargs) -> Any:overrideconfig with**kwargsandinstantiateit. Note: only keyword specified arguments are supported.
@config Decorator
@cfn.config # No override, just turn function into config.
def print_greeting(greeting: str = 'Hello', entity: str = 'world'):
print(f'{greeting} {entity}!')
@cfn.config(arg1="default", arg2=42) # With defaults
def my_function(...):
pass
Utility Functions
cli(config: Config | dict[str, Config])
Generate automatic command-line interface for any configuration or multiple configurations.
Parameters:
config: Either a singleConfigobject or a dictionary mapping command names toConfigobjects
Examples:
# Single command
cfn.cli(my_config)
# Multiple commands
cfn.cli({'train': train_config, 'eval': eval_config, 'test': test_config})
get_required_args(config: Config) -> List[str]
Get list of required arguments for a configuration.
Special Syntax
@module.path.Class- Absolute import path to any Python object.RelativeClass- Relative import (same module, like./)..parent.Class- Relative import (up one level, like../)@@literal_string- Escape literal@characters in the beginning of strings
Path Resolution: The
.syntax works like file system navigation where each dot moves up one module level in the Python package hierarchy, then navigates down to the specified target.
💡 Best Practices
1. Avoid Positional Arguments (*args) ⚠️
Warning: Configuronic has limited support for positional arguments (
*args). Use them only in exceptional cases (like functions that accept variable-length lists) and always use explicitcfn.Config()construction, never the decorator.
Problems with positional arguments:
-
Implicit and unclear - Hard to understand what arguments represent:
# ❌ Unclear what 1, 2, 3 represent func = cfn.Config(func, 1, 2, 3)
-
Fragile - Changing argument order breaks all configurations:
# ❌ If you change parameter order, all configs break @cfn.config("robot", "camera") def compose(camera, robot): # Swapped order! ...
-
Ambiguous override behavior - Unclear what
override()should do:# ❌ What should sum_with45 contain? [4, 5] or [1, 2, 3, 4, 5]? sum_123 = cfn.Config(sum, 1, 2, 3) sum_with45 = sum_123.override(4, 5)
Recommended approach:
# ✅ Use keyword arguments for clarity
config = cfn.Config(func, param1=1, param2=2, param3=3)
# ✅ Only use *args for functions designed for them
@cfn.config() # No positional args in decorator
def sum_all(*numbers):
return sum(numbers)
variadic_sum = cfn.Config(sum_all, 1, 2, 3) # Explicit Config only
2. Create Separate Modules for Configurations
If you don't want your business logic modules to depend on configuronic, it's wise to have a separate package for configurations.
# configs/models.py
transformer_base = cfn.Config(TransformerModel, layers=6, hidden_size=512)
transformer_large = transformer_base.override(layers=12, hidden_size=1024)
# configs/training.py
from .models import transformer_base
training_pipeline = cfn.Config(TrainingPipeline, model=transformer_base)
3. Import Inside Configuration Functions
In robotic applications, some configurations may depend on parituclar hardware and Python packages that provide drivers, that are not always available. If you don't want to force your users to install all of them, consider making imports inside the functions that you configure.
@cfn.config()
def create_model(layers: int = 6):
from my_project.models import TransformerModel
return TransformerModel(layers=layers)
But in smaller projects it might be very convinient to put configurations alongside the methods they manage.
4. Use override to create as many custom configurations as you need
When working with text configuration files, it's natural to create separate files for different environments or use cases. In configuronic, just create new configuration variables that override the base config.
# Base training configuration
base_training = cfn.Config(
TrainingPipeline,
model=cfn.Config(TransformerModel, layers=6, hidden_size=512),
optimizer=cfn.Config(torch.optim.Adam, lr=0.001),
batch_size=32,
epochs=10
)
# Development environment - smaller, faster
dev_training = base_training.override(
batch_size=8, epochs=2,
**{"model.layers": 3, "model.hidden_size": 256})
# Production environment - optimized settings
prod_training = base_training.override(
batch_size=64, epochs=100,
**{"optimizer.lr": 0.0001})
# Experimental setup - large model
experimental_training = base_training.override(
**{
"model.layers": 12,
"model.hidden_size": 1024,
"optimizer.lr": 0.0005,
"batch_size": 16
})
# Quick debugging setup
debug_training = base_training.override(
epochs=1, batch_size=2, **{"model.layers": 1})
# Now you can easily switch between configurations
# TODO: Make this part of Configuronic
if __name__ == "__main__":
import sys
configs = {
'dev': dev_training,
'prod': prod_training,
'experimental': experimental_training,
'debug': debug_training
}
config_name = sys.argv[1] if len(sys.argv) > 1 else 'dev'
selected_config = configs.get(config_name, dev_training)
cfn.cli(selected_config)
Usage:
python train.py dev # Use development config
python train.py prod # Use production config
python train.py experimental # Use experimental config
python train.py debug # Use debug config
# Still supports all override capabilities
python train.py prod --epochs=50 --batch_size=128
🤝 Contributing
We welcome contributions! Here's how to get started:
Development Setup
git clone https://github.com/Positronic-Robotics/configuronic.git
cd configuronic
uv pip install -e ".[dev]"
Running Tests
pytest # Run all tests
pytest --cov=configuronic # Run with coverage
📋 Guidelines
- Follow existing code style and patterns
- Add tests for new functionality
- Ensure all tests pass before submitting
- Update documentation as needed
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
📞 Support
- 🐛 Bug Reports: GitHub Issues
- 💬 Discussions: GitHub Discussions FIXME: Create Discord
- 📧 Email: hi@positronic.ro
⭐ If you find Configuronic useful, please consider giving it a star on GitHub!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file configuronic-0.2.1.tar.gz.
File metadata
- Download URL: configuronic-0.2.1.tar.gz
- Upload date:
- Size: 29.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b084a3c3771100b88a4c58d8049a1dc603c4e646b05223854e8882c5ab38b416
|
|
| MD5 |
3993edaa2ca5b07705361c3df6119038
|
|
| BLAKE2b-256 |
54c2e20620237fe575c0afb14e2c07f2faaebc136b5522c35653f82145dba9d5
|