Skip to content
Merged
25 changes: 14 additions & 11 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ confkit is a Python library for type-safe configuration management using descrip
- `Config` descriptor (`config.py`): The main descriptor class that handles getting/setting values in config files
- `ConfigContainerMeta` (`config.py`): Metaclass that enables setting Config descriptors on class variables
- `BaseDataType` and implementations (`data_types.py`): Type converters for different data types
- `ConfkitParser` protocol (`ext/parsers.py`): Protocol defining the parser interface
- `MsgspecParser` (`ext/parsers.py`): Parser for JSON, YAML, and TOML files
- `EnvParser` (`ext/parsers.py`): Parser for environment variables and .env files
- `Parser` facade (`ext/parsers.py`): Unified facade for all configuration file formats (INI, JSON, YAML, TOML, .env)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc lists a Parser facade in ext/parsers.py, but there is no Parser type exported/implemented there (only ConfkitParser, IniParser, MsgspecParser, EnvParser). Please either add the facade described here or update the documentation to reflect the actual API surface.

Suggested change
- `Parser` facade (`ext/parsers.py`): Unified facade for all configuration file formats (INI, JSON, YAML, TOML, .env)
- `ConfkitParser` protocol (`ext/parsers.py`): Defines the unified parser interface for all configuration file formats (INI, JSON, YAML, TOML, .env)

Copilot uses AI. Check for mistakes.
- `IniParser` (`ext/parsers.py`): Adapter for Python's built-in ConfigParser (INI files)
- `MsgspecParser` (`ext/parsers.py`): Adapter for JSON, YAML, and TOML files using msgspec
- `EnvParser` (`ext/parsers.py`): Adapter for environment variables and .env files
- `sentinels.py`: Provides the `UNSET` sentinel value for representing unset values
- `exceptions.py`: Custom exceptions for configuration errors
- `watcher.py`: File watching functionality to detect config file changes
Expand Down Expand Up @@ -44,6 +45,8 @@ uv sync --group test
```bash
# Run linting
ruff check .
# Fix issues using
ruff check . --fix --unsafe-fixes
# Update dependencies and run tests
uv sync --upgrade --group dev; uv run pytest .
```
Expand Down Expand Up @@ -172,29 +175,29 @@ The `with_setting` approach is more type-safe as it references an actual descrip

### Required Initialization

Always initialize Config with a file path before use. The parser can be set explicitly or will be auto-detected based on file extension:
Always initialize Config with a file path before use. The Parser facade automatically detects and uses the appropriate adapter based on file extension:

```python
from pathlib import Path
from confkit import Config

# Option 1: Auto-detect parser based on file extension
Config.set_file(Path("config.ini")) # Uses ConfigParser
# Simplified approach - parser is auto-detected based on file extension
Config.set_file(Path("config.ini")) # Uses IniParser
Config.set_file(Path("config.json")) # Uses MsgspecParser
Config.set_file(Path("config.yaml")) # Uses MsgspecParser
Config.set_file(Path("config.toml")) # Uses MsgspecParser
Config.set_file(Path(".env")) # Uses EnvParser
Config.set_file(Path(".env")) # Uses EnvParser

# Option 2: Explicitly set parser (not recommended unless it's absolutely required.)
from configparser import ConfigParser
parser = ConfigParser()
# Option 2: Explicitly set parser (not recommended unless absolutely required)
from confkit.ext.parsers import Parser
parser = Parser(Path("config.ini"))
Config.set_parser(parser)
Config.set_file(Path("config.ini"))
```

### Supported File Formats

- **INI files** (`.ini`): Uses Python's `ConfigParser`, supports sections
- **INI files** (`.ini`): Uses `IniParser`, supports sections
- **JSON files** (`.json`): Uses `MsgspecParser`, requires `msgspec` extra
- **YAML files** (`.yaml`, `.yml`): Uses `MsgspecParser`, requires `msgspec` extra
- **TOML files** (`.toml`): Uses `MsgspecParser`, requires `msgspec` extra
Expand Down
4 changes: 1 addition & 3 deletions docs/examples/custom_data_type.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ Shows how to create and use a custom [`BaseDataType`](pdoc:confkit.BaseDataType)
## Implementation

```python
from configparser import ConfigParser
from pathlib import Path
from confkit import Config
from confkit.data_types import BaseDataType

# Configure parser + file
parser = ConfigParser()
# Configure parser + file (Parser is automatically detected)
Config.set_parser(parser)
Config.set_file(Path("config.ini"))

Expand Down
10 changes: 2 additions & 8 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ This page explains how to work with confkit and how the documentation is generat
## Descriptor Quickstart

```python
from configparser import ConfigParser
from pathlib import Path
from confkit import Config

# 1. Configure confkit parser/file (normally app bootstrap)
parser = ConfigParser()
Config.set_parser(parser)
# 1. Configure confkit file (parser is auto-detected)
Config.set_file(Path("config.ini"))

# 2. Define your (class)variables with their default values
Expand All @@ -36,14 +33,11 @@ print(AppConfig.port)
4. Use via `Config(CustomType(default_value))`

```python
from configparser import ConfigParser
from pathlib import Path
from confkit import Config
from confkit.data_types import BaseDataType

# 1. Configure confkit parser/file (normally app bootstrap)
parser = ConfigParser()
Config.set_parser(parser)
# 1. Configure confkit file (parser is auto-detected)
Config.set_file(Path("config.ini"))

# 2. Define the custom converter, with a convert method (from str -> your_type)
Expand Down
2 changes: 0 additions & 2 deletions examples/argparse_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
from confkit.data_types import List

# Setup a standard confkit config file (unrelated to argparse defaults file)
parser_ini = ConfigParser()
ini_path = Path("config.ini")
Config.write_on_edit = False
Config.set_parser(parser_ini)
Config.set_file(ini_path)

class AppConfig:
Expand Down
2 changes: 0 additions & 2 deletions examples/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
from confkit import Config

# Set up the parser and file
parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))

# Enable automatic writing when config values are changed (this is the default)
Expand Down
2 changes: 0 additions & 2 deletions examples/custom_data_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from confkit import Config
from confkit.data_types import BaseDataType

parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))

class UpperString(BaseDataType[str]):
Expand Down
2 changes: 0 additions & 2 deletions examples/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
from confkit import Config
from confkit.data_types import Binary, Boolean, Float, Hex, Integer, Octal, String

parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))


Expand Down
2 changes: 0 additions & 2 deletions examples/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

from confkit.config import Config

parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))


Expand Down
2 changes: 0 additions & 2 deletions examples/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from confkit.data_types import IntFlag as ConfigIntFlag
from confkit.data_types import Optional, StrEnum as ConfigStrEnum

parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))


Expand Down
2 changes: 0 additions & 2 deletions examples/file_change_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from confkit import Config

# Set up the parser and file
parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))

# Enable automatic writing when config values are changed (this is the default)
Expand Down
2 changes: 0 additions & 2 deletions examples/list_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
from confkit import Config
from confkit.data_types import List, String

parser = ConfigParser()
Config.set_parser(parser)
Config.set_file(Path("config.ini"))
List.escape_char = "\\" # Set escape character for lists, these are also the default.
List.separator = "," # Set separator character for lists, these are also the default.
Expand Down
2 changes: 0 additions & 2 deletions examples/multiple_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
class DatabaseConfig(Config[T]): ...
class ApiConfig(Config[T]): ...

DatabaseConfig.set_parser(ConfigParser())
DatabaseConfig.set_file(Path("database.ini"))
ApiConfig.set_parser(ConfigParser())
ApiConfig.set_file(Path("api.ini"))

class AppConfiguration:
Expand Down
174 changes: 174 additions & 0 deletions examples/nested_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""Example of nested configuration support in confkit.

This example demonstrates how to use nested classes to organize
configuration values hierarchically. This works with:
- JSON files (native nested objects)
- YAML files (native nested mappings)
- TOML files (nested tables)
- INI files (using dot notation in section names)
"""
from confkit.ext.parsers import IniParser

from pathlib import Path
from typing import TypeVar

from confkit import Config

T = TypeVar("T")

# Example 1: Nested configuration with JSON
print("=== Nested JSON Configuration ===")


class JsonConfig(Config[T]):
...


JsonConfig.set_file(Path("nested_example.json"))


class DatabaseConfig:
"""Database configuration with nested credentials."""

host = JsonConfig("localhost")
port = JsonConfig(5432)
name = JsonConfig("myapp_db")

class Credentials:
"""Nested credentials configuration."""

username = JsonConfig("admin")
password = JsonConfig("secret123")
use_ssl = JsonConfig(True)


# Access nested values
print(f"Database host: {DatabaseConfig.host}")
print(f"Database port: {DatabaseConfig.port}")
print(f"Username: {DatabaseConfig.Credentials.username}")
print(f"Password: {DatabaseConfig.Credentials.password}")
print(f"Use SSL: {DatabaseConfig.Credentials.use_ssl}")

# Example 2: Multiple levels of nesting with YAML
print("\n=== Multi-Level Nested YAML Configuration ===")


class YamlConfig(Config[T]):
...


YamlConfig.set_file(Path("nested_example.yaml"))


class ServerConfig:
"""Server configuration with multiple nesting levels."""

name = YamlConfig("web-server-01")

class Network:
"""Network configuration."""

interface = YamlConfig("eth0")

class IPv4:
"""IPv4 settings."""

address = YamlConfig("192.168.1.100")
netmask = YamlConfig("255.255.255.0")
gateway = YamlConfig("192.168.1.1")

class IPv6:
"""IPv6 settings."""

address = YamlConfig("fe80::1")
prefix = YamlConfig(64)


print(f"Server name: {ServerConfig.name}")
print(f"Network interface: {ServerConfig.Network.interface}")
print(f"IPv4 address: {ServerConfig.Network.IPv4.address}")
print(f"IPv4 netmask: {ServerConfig.Network.IPv4.netmask}")
print(f"IPv6 address: {ServerConfig.Network.IPv6.address}")
print(f"IPv6 prefix: {ServerConfig.Network.IPv6.prefix}")

# Example 3: INI files with dot notation
print("\n=== Nested INI Configuration (using dot notation) ===")


class IniConfig(Config[T]):
...


IniConfig._set_parser(IniParser())
IniConfig.set_file(Path("nested_example.ini"))


class AppConfig:
"""Application configuration."""

version = IniConfig("1.0.0")
debug = IniConfig(False)

class Logging:
"""Logging configuration."""

level = IniConfig("INFO")
file = IniConfig("app.log")

class Rotation:
"""Log rotation settings."""

max_size = IniConfig(10) # MB
backup_count = IniConfig(5)


print(f"App version: {AppConfig.version}")
print(f"Debug mode: {AppConfig.debug}")
print(f"Log level: {AppConfig.Logging.level}")
print(f"Log file: {AppConfig.Logging.file}")
print(f"Max log size: {AppConfig.Logging.Rotation.max_size} MB")
print(f"Backup count: {AppConfig.Logging.Rotation.backup_count}")

# Example 4: TOML with nested tables
print("\n=== Nested TOML Configuration ===")


class TomlConfig(Config[T]):
...


TomlConfig.set_file(Path("nested_example.toml"))


class ServiceConfig:
"""Service configuration."""

name = TomlConfig("my-service")
port = TomlConfig(8080)

class API:
"""API configuration."""

version = TomlConfig("v1")
timeout = TomlConfig(30)

class RateLimit:
"""Rate limiting configuration."""

enabled = TomlConfig(True)
requests_per_minute = TomlConfig(60)


print(f"Service name: {ServiceConfig.name}")
print(f"Service port: {ServiceConfig.port}")
print(f"API version: {ServiceConfig.API.version}")
print(f"API timeout: {ServiceConfig.API.timeout}s")
print(f"Rate limit enabled: {ServiceConfig.API.RateLimit.enabled}")
print(f"Requests per minute: {ServiceConfig.API.RateLimit.requests_per_minute}")

print("\n=== Configuration files created successfully! ===")
print("Check the following files to see the structure:")
print(" - nested_example.json")
print(" - nested_example.yaml")
print(" - nested_example.ini")
print(" - nested_example.toml")
12 changes: 12 additions & 0 deletions examples/nested_example.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[AppConfig.Logging.Rotation]
max_size = 10
backup_count = 5

[AppConfig.Logging]
level = INFO
file = app.log

[AppConfig]
version = 1.0.0
debug = False

12 changes: 12 additions & 0 deletions examples/nested_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"DatabaseConfig": {
"Credentials": {
"username": "admin",
"password": "secret123",
"use_ssl": true
},
"host": "localhost",
"port": 5432,
"name": "myapp_db"
}
}
Loading
Loading