Usage
Installation
You can install it using pip:
pip install classconfig
Basic Usage
The core of classconfig is the ConfigurableMixin class and ConfigurableValue/ConfigurableFactory descriptors.
Here is a simple example:
from classconfig import ConfigurableMixin, ConfigurableValue, Config
class MyConfig(ConfigurableMixin):
host: str = ConfigurableValue(desc="Host address", user_default="localhost")
port: int = ConfigurableValue(desc="Port number", user_default=8080)
# Create an instance with default values
config = MyConfig()
print(f"Host: {config.host}, Port: {config.port}")
# Output: Host: localhost, Port: 8080
# Create an instance with custom values
config = MyConfig(host="127.0.0.1", port=9090)
print(f"Host: {config.host}, Port: {config.port}")
# Output: Host: 127.0.0.1, Port: 9090
Configuration Files
Generating Configuration
You can easily generate a YAML configuration file from your class definition:
from classconfig import Config
# Generate YAML config
Config(MyConfig).save("config.yaml")
The generated config.yaml will look like this:
host: localhost # Host address
port: 8080 # Port number
Loading Configuration
You can load the configuration back into your class:
from classconfig import Config, ConfigurableFactory
# Load config
loaded_config = Config(MyConfig).load("config.yaml")
# Create instance
my_obj = ConfigurableFactory(MyConfig).create(loaded_config)
print(f"Host: {my_obj.host}, Port: {my_obj.port}")
Dataclasses
classconfig also supports Python dataclasses via ConfigurableDataclassMixin.
from dataclasses import dataclass, field
from classconfig.classes import ConfigurableDataclassMixin
@dataclass
class ServerConfig(ConfigurableDataclassMixin):
host: str = field(default="localhost", metadata={"desc": "Host address"})
port: int = field(default=8080, metadata={"desc": "Port number"})
# Usage is similar to ConfigurableMixin
config = ServerConfig()
print(config.host)
Advanced Features
Configurable Attributes
classconfig provides several descriptors to define configurable attributes:
ConfigurableValue: For simple values (int, str, float, bool, etc.).
ConfigurableFactory: For nested configurable objects.
ConfigurableSubclassFactory: For selecting and configuring a subclass of a given parent class.
ListOfConfigurableSubclassFactoryAttributes: For a list of configurable objects (subclasses).
Mixins
ConfigurableMixin: The base mixin for standard classes.
ConfigurableDataclassMixin: The mixin for dataclasses.
Validators
Validators ensure that the configuration values meet specific criteria. They are callables that take a value and return True or raise an exception.
Available validators in classconfig.validators:
TypeValidator: Checks if value is of specific type.
IntegerValidator, FloatValidator, StringValidator, BoolValidator: Type-specific validators.
MinValueIntegerValidator, MinValueFloatValidator: Checks minimum value.
ValueInIntervalIntegerValidator: Checks if value is within an interval.
AllValidator: Combines multiple validators (AND logic).
AnyValidator: Combines multiple validators (OR logic).
Transformers
Transformers modify the input value before validation.
Available transformers in classconfig.transforms:
TryTransforms: Tries multiple transformers in order.
RelativePathTransformer: Resolves relative paths.
CPUWorkersTransformer: Handles CPU count (e.g., -1 for all cores).
EnumTransformer: Converts string to Enum member.
SubclassTransformer: Converts string to class type.
TransformIfNotNone: Applies transformer only if value is not None.
Comprehensive Example
Let’s put it all together with an example using ConfigurableSubclassFactory, validators, and transformers.
from classconfig import (
ConfigurableMixin, ConfigurableValue, ConfigurableSubclassFactory,
ListOfConfigurableSubclassFactoryAttributes, Config, ConfigurableFactory
)
from classconfig.validators import MinValueIntegerValidator
from classconfig.transforms import EnumTransformer
from enum import Enum
from abc import ABC, abstractmethod
# 1. Define an Enum and a Transformer for it
class LogLevel(Enum):
INFO = "INFO"
DEBUG = "DEBUG"
ERROR = "ERROR"
# 2. Define an abstract base class for plugins
class Plugin(ConfigurableMixin, ABC):
@abstractmethod
def run(self):
pass
# 3. Define concrete plugins
class FilePlugin(Plugin):
path: str = ConfigurableValue(desc="Path to log file")
def run(self):
print(f"Logging to file: {self.path}")
class ConsolePlugin(Plugin):
prefix: str = ConfigurableValue(desc="Log prefix", user_default="[LOG]")
def run(self):
print(f"{self.prefix} Logging to console")
# 4. Define the main application configuration
class AppConfig(ConfigurableMixin):
# Simple value with validator and transformer
log_level: LogLevel = ConfigurableValue(
desc="Logging level",
user_default="INFO",
transform=EnumTransformer(LogLevel)
)
# Value with validator
max_retries: int = ConfigurableValue(
desc="Maximum retries",
user_default=3,
validator=MinValueIntegerValidator(0)
)
# Subclass factory: allows choosing between FilePlugin and ConsolePlugin
primary_plugin: Plugin = ConfigurableSubclassFactory(
parent_cls_type=Plugin,
desc="Primary plugin to use",
user_default=ConsolePlugin
)
# List of subclass factories
extra_plugins: list[Plugin] = ListOfConfigurableSubclassFactoryAttributes(
ConfigurableSubclassFactory(Plugin),
desc="List of additional plugins",
user_defaults=[ConsolePlugin]
)
# --- Usage ---
# Generate config file
Config(AppConfig).save("advanced_config.yaml")
# The generated YAML will look like this:
# log_level: INFO # Logging level
# max_retries: 3 # Maximum retries
# primary_plugin: # Primary plugin to use
# cls: ConsolePlugin # name of class that is subclass of Plugin
# config: # configuration for defined class
# prefix: [LOG] # Log prefix
# extra_plugins: # List of additional plugins
# - cls: ConsolePlugin # name of class that is subclass of Plugin
# config: # configuration for defined class
# prefix: [LOG] # Log prefix
# Load config (assuming we modified it to use FilePlugin for primary)
# Let's simulate loading a modified config dict
config_data = {
"log_level": "DEBUG",
"max_retries": 5,
"primary_plugin": {
"cls": "FilePlugin",
"config": {"path": "/var/log/app.log"}
},
"extra_plugins": []
}
# In real usage: loaded_config = Config(AppConfig).load("advanced_config.yaml")
# Here we mock the loading for demonstration
from classconfig import LoadedConfig
loaded_config = Config(AppConfig).trans_and_val(config_data, None)
app_config = ConfigurableFactory(AppConfig).create(loaded_config)
print(f"Log Level: {app_config.log_level}")
print(f"Max Retries: {app_config.max_retries}")
print(f"Primary Plugin Type: {type(app_config.primary_plugin)}")
if isinstance(app_config.primary_plugin, FilePlugin):
print(f"Primary Plugin Path: {app_config.primary_plugin.path}")
# Output:
# Log Level: LogLevel.DEBUG
# Max Retries: 5
# Primary Plugin Type: <class '__main__.FilePlugin'>
# Primary Plugin Path: /var/log/app.log
Why YAML?
YAML is a human-readable data serialization language. It is easy to read and write. It is also easy to parse and generate.
It supports hierarchical data structures, which are very useful when you need to represent configuration of a class that has other configurable classes as members.
It supports comments, unlike e.g. json, which is also a big advantage.