Simple and extensible IRC bot framework written in Python with plugin architecture and database support
—
Sopel's configuration system provides hierarchical, section-based configuration with type validation, environment variable support, and runtime management. The system is built around the Config class and StaticSection subclasses that define configuration schemas.
Core configuration functionality for loading, saving, and managing bot settings.
class Config:
"""Main configuration management class."""
def __init__(self, filename: str, validate: bool = True):
"""
Initialize configuration from file.
Args:
filename (str): Path to configuration file
validate (bool): Whether to validate configuration on load
"""
@property
def filename(self) -> str:
"""Path to the configuration file."""
@property
def basename(self) -> str:
"""Configuration file basename without extension."""
@property
def homedir(self) -> str:
"""Configuration home directory path."""
@property
def parser(self) -> 'configparser.RawConfigParser':
"""Underlying configuration parser object."""
def save(self) -> None:
"""
Save all configuration changes to file.
Note: This removes any comments from the configuration file.
"""
def add_section(self, name: str) -> None | bool:
"""
Add a new empty section to configuration.
Args:
name (str): Section name (should use snake_case)
Returns:
None if successful, False if section already exists
"""
def define_section(self, name: str, cls_, validate: bool = True) -> None:
"""
Define a configuration section with a StaticSection class.
Args:
name (str): Section name (should use snake_case)
cls_: StaticSection subclass defining the section schema
validate (bool): Whether to validate section values
Raises:
ValueError: If section already defined with different class
"""
def get_defined_sections(self) -> list:
"""
Get all defined StaticSection instances.
Returns:
List of (name, section) tuples for all defined sections
"""
def option(self, question: str, default: bool = False) -> bool:
"""
Ask user a yes/no question interactively.
Args:
question (str): Question to ask user
default (bool): Default answer (True for 'y', False for 'n')
Returns:
User's boolean response
"""
def get(self, section: str, option: str) -> str:
"""Shortcut to parser.get() method."""Base classes and system for defining structured configuration sections.
class StaticSection:
"""
Base class for defining configuration sections with typed attributes.
Subclass this to create configuration sections with validation and
type conversion.
"""
def __init__(self, config: Config, section_name: str, validate: bool = True):
"""
Initialize section from configuration.
Args:
config (Config): Parent configuration object
section_name (str): Name of this section
validate (bool): Whether to validate attributes
"""Typed attribute classes for configuration values with validation and conversion.
class ValidatedAttribute:
"""
Base attribute class with validation and type conversion.
Use for configuration values that need type checking or validation.
"""
def __init__(self, name: str, cls_=None, default=None, parse=None):
"""
Create a validated configuration attribute.
Args:
name (str): Configuration key name
cls_: Type class for validation (int, str, bool, etc.)
default: Default value if not set
parse (callable): Custom parsing function
"""
class ListAttribute(ValidatedAttribute):
"""
Attribute for configuration values that are lists.
Automatically splits comma-separated values into lists.
"""
def __init__(self, name: str, strip: bool = True, default=None):
"""
Create a list configuration attribute.
Args:
name (str): Configuration key name
strip (bool): Whether to strip whitespace from list items
default: Default list value
"""
class ChoiceAttribute(ValidatedAttribute):
"""
Attribute that must be one of a predefined set of choices.
"""
def __init__(self, name: str, choices: list, default=None):
"""
Create a choice configuration attribute.
Args:
name (str): Configuration key name
choices (list): List of valid choices
default: Default choice value
"""
class FilenameAttribute(ValidatedAttribute):
"""
Attribute for file path configuration values.
Handles path resolution and validation.
"""
def __init__(self, name: str, relative: bool = True, directory: str = None, default=None):
"""
Create a filename configuration attribute.
Args:
name (str): Configuration key name
relative (bool): Whether paths are relative to config directory
directory (str): Base directory for relative paths
default: Default filename
"""The built-in core configuration section that defines essential bot settings.
class CoreSection(StaticSection):
"""Core bot configuration section with essential settings."""
# Connection settings
nick: str # Bot's nickname
host: str # IRC server hostname
port: int # IRC server port (default: 6667)
use_ssl: bool # Whether to use SSL/TLS (default: False)
verify_ssl: bool # Whether to verify SSL certificates (default: True)
# Authentication
password: str # Server password (optional)
auth_method: str # Authentication method
auth_username: str # Authentication username
auth_password: str # Authentication password
# Bot identity
user: str # IRC username/ident
name: str # Real name field
channels: list # Auto-join channels
# Bot operators
owner: str # Bot owner nickname
admins: list # Bot admin nicknames
# Database
db_type: str # Database type (sqlite, mysql, postgresql, etc.)
db_filename: str # Database filename (SQLite only)
db_host: str # Database host
db_port: int # Database port
db_user: str # Database username
db_pass: str # Database password
db_name: str # Database name
# Logging
logdir: str # Log directory
logging_level: str # Logging level (DEBUG, INFO, WARNING, ERROR)
logging_format: str # Log message format
# Plugin settings
extra: list # Additional directories to search for plugins
exclude: list # Plugins to exclude from loading
# Runtime settings
homedir: str # Bot home directory
pid_dir: str # Directory for PID files
help_prefix: str # Prefix for help commands
reply_errors: bool # Whether to reply with error messagesfrom sopel.config import Config
# Load configuration from file
config = Config('/path/to/bot.cfg')
# Access core settings
print(f"Bot nick: {config.core.nick}")
print(f"IRC server: {config.core.host}")
print(f"Channels: {config.core.channels}")
# Save configuration changes
config.core.nick = "NewBotName"
config.save()from sopel import config
class WeatherSection(config.types.StaticSection):
"""Configuration for weather plugin."""
api_key = config.types.ValidatedAttribute('api_key')
default_location = config.types.ValidatedAttribute('default_location', default='London')
units = config.types.ChoiceAttribute('units', choices=['metric', 'imperial'], default='metric')
max_locations = config.types.ValidatedAttribute('max_locations', int, default=5)
timeout = config.types.ValidatedAttribute('timeout', int, default=30)
# In plugin setup function
def setup(bot):
"""Setup weather plugin configuration."""
bot.settings.define_section('weather', WeatherSection)
# Validate required settings
if not bot.settings.weather.api_key:
raise Exception("Weather plugin requires api_key in [weather] section")
# In plugin functions
@plugin.command('weather')
def weather_command(bot, trigger):
"""Get weather using configured settings."""
api_key = bot.settings.weather.api_key
units = bot.settings.weather.units
timeout = bot.settings.weather.timeout
# Use configuration values...from sopel import config
import os
class DatabaseSection(config.types.StaticSection):
"""Advanced database configuration with validation."""
type = config.types.ChoiceAttribute(
'type',
choices=['sqlite', 'mysql', 'postgresql'],
default='sqlite'
)
filename = config.types.FilenameAttribute(
'filename',
relative=True,
default='bot.db'
)
host = config.types.ValidatedAttribute('host', default='localhost')
port = config.types.ValidatedAttribute('port', int, default=5432)
username = config.types.ValidatedAttribute('username')
password = config.types.ValidatedAttribute('password')
name = config.types.ValidatedAttribute('name', default='sopel')
# Custom validation
def __init__(self, config, section_name, validate=True):
super().__init__(config, section_name, validate)
if validate and self.type != 'sqlite':
if not self.username or not self.password:
raise ValueError("Database username and password required for non-SQLite databases")
# Using the configuration
def setup(bot):
bot.settings.define_section('database', DatabaseSection)
# Access configuration
db_config = bot.settings.database
if db_config.type == 'sqlite':
db_path = os.path.join(bot.settings.core.homedir, db_config.filename)
print(f"Using SQLite database: {db_path}")
else:
print(f"Using {db_config.type} database on {db_config.host}:{db_config.port}")# Configuration values can be overridden with environment variables
# Format: SOPEL_<SECTION>_<OPTION>
# Override core.nick
# export SOPEL_CORE_NICK="MyBot"
# Override custom section values
# export SOPEL_WEATHER_API_KEY="your_api_key_here"
from sopel.config import Config
# Load config with environment overrides
config = Config('/path/to/bot.cfg')
# Values are automatically overridden from environment
print(config.core.nick) # Uses SOPEL_CORE_NICK if setfrom sopel.config import Config
def configure_weather_plugin(config):
"""Interactively configure weather plugin."""
# Ask yes/no questions
enable_weather = config.option("Enable weather plugin?", default=True)
if not enable_weather:
return
# Get user input for settings
api_key = input("Enter weather API key: ")
default_location = input("Enter default location [London]: ") or "London"
# Save configuration
if 'weather' not in config:
config.add_section('weather')
config.weather.api_key = api_key
config.weather.default_location = default_location
config.save()
print("Weather plugin configured successfully!")# Default configuration directory
DEFAULT_HOMEDIR: str = "~/.sopel"class ConfigurationError(Exception):
"""Base exception for configuration errors."""
def __init__(self, value: str):
"""
Initialize configuration error.
Args:
value (str): Error description
"""
class ConfigurationNotFound(ConfigurationError):
"""Exception raised when configuration file cannot be found."""
def __init__(self, filename: str):
"""
Initialize configuration not found error.
Args:
filename (str): Path to missing configuration file
"""
@property
def filename(self) -> str:
"""Path to the configuration file that could not be found."""Install with Tessl CLI
npx tessl i tessl/pypi-sopel