Skip to main content

ConfBase

Python CI License Tests Coverage

用类属性和 Section 声明配置。一个配置项就是一个类属性,读写时自动完成类型校验、校验函数检查和落盘,不用再手写读写文件的样板代码。

适用场景

适合

  • 桌面应用 / 游戏客户端的本地配置
  • CLI 工具、脚本的参数与偏好设置
  • 单机服务或小型 Web 服务的配置文件
  • 配置需要 JSON / YAML / TOML 多种格式
  • 需要配置加密、防篡改签名、版本迁移
  • 需要批量修改、变更回调、按分组重置

不适合

场景 更合适的选择
分布式配置中心、多节点实时同步 Nacos / Apollo / Consul / etcd
Redis / MySQL 等远程存储 配置中心,或自行实现 BaseFormat / BaseCipher 适配
每秒级高频写入 数据库、配置中心
超大配置或二进制数据 数据库、对象存储
复杂嵌套模型、字段级校验 pydantic / pydantic-settings
多用户并发编辑同一文件 数据库 + 乐观锁
运行时动态增删配置项 动态配置中心
配置类之间继承复用配置项 独立配置类 + 共享默认值常量
配置审计、权限、回滚 配置中心、密钥管理服务

ConfBase 是「本地文件 + 声明式配置」的轻量工具,不是配置中心客户端,也不是 pydantic 的替代品。

安装

pip install conf_base

按需安装可选依赖:

功能 安装命令
YAML 格式 pip install conf_base[yaml]
TOML 格式 pip install conf_base[toml]
加密 / 签名 pip install conf_base[aes]
多进程文件锁 pip install conf_base[filelock]

快速开始

以游戏客户端配置为例:

from conf_base import ConfigBase, Section


class GameConfig(ConfigBase):
    # 配置文件路径;格式按后缀推断(无后缀时默认 JSON)
    _path = 'config/game.json'

    # 画面
    screen = Section('Screen')
    width = screen.field(default=1280, validator=lambda v: v >= 800)
    height = screen.field(default=720)
    fullscreen = screen.field(default=False)

    # 声音
    audio = Section('Audio')
    bgm_volume = audio.field(default=0.8, validator=lambda v: 0 <= v <= 1)
    mute = audio.field(default=False)

    # 存档
    save = Section('Save')
    unlocked_levels = save.field(default=[1])

类定义时自动创建 config/game.json,并把缺失的默认值写入文件:

{
    "_schema_version": 1,
    "Screen": {
        "width": 1280,
        "height": 720,
        "fullscreen": false
    },
    "Audio": {
        "bgm_volume": 0.8,
        "mute": false
    },
    "Save": {
        "unlocked_levels": [
            1
        ]
    }
}

结构一目了然:顶层是 Section 名,第二层是配置项名,值与代码中的 default 完全对应。_schema_version 是内部字段,用于配置版本迁移,to_dict() 不会导出它。

之后像操作普通属性一样读写:

# 读取
GameConfig.width.value                      # 1280
GameConfig.screen.current_options           # {'width': 1280, 'height': 720, ...}

# 修改:自动校验类型与 validator,并落盘
GameConfig.width.value = 1920
GameConfig.bgm_volume.value = 0.5
GameConfig.bgm_volume.value = 1.5           # ❌ ConfigValueError(超出 0~1)

# 类型专属快捷操作
GameConfig.fullscreen.toggle()              # False -> True
GameConfig.width.increment(100)             # 1920 -> 2020
GameConfig.unlocked_levels.append(2)        # [1] -> [1, 2]

# 批量修改:所有配置项只落盘一次
GameConfig.audio.update(bgm_volume=0.6, mute=True)

# 全量导出
GameConfig.to_dict()
# {'Screen': {...}, 'Audio': {...}, 'Save': {...}}

核心用法

1. 声明配置项

Section.field() 的参数:

port = server.field(
    default=8080,                          # 默认值,同时决定配置值的类型
    option='port',                         # 配置键名,省略时自动使用左侧变量名
    validator=lambda v: 1 <= v <= 65535,   # 可选,返回 False 时拒绝写入
    read_only=False,                       # 可选,True 时禁止运行时修改
    env_key='APP_PORT',                    # 可选,允许环境变量覆盖
)
# 本例 option 与变量名相同,也可省略;需要不同键名时改成 option='listen_port'
# IDE 能通过 __set_name__ 自动推导属性名,field() 返回类型感知的 Proxy 对象(BoolProxy/NumberProxy/ListProxy/DictProxy),提供了 toggle()、increment()、append() 等语义化操作

配置值的类型始终锁定为 default 的类型,写入其他类型会抛 ConfigTypeError。

2. 读取

四种方式结果完全一致:

GameConfig.width.value                  # 描述符方式
GameConfig.screen['width']              # Section 索引
GameConfig.screen.width                 # Section 属性
GameConfig.screen.current_options       # 整个 Section

批量导出:

GameConfig.screen.default_options   # 默认值字典
GameConfig.to_dict()                # 全部配置的深拷贝

3. 修改

# 单项
GameConfig.width.value = 1920
GameConfig.width.set_value(1920)        # 等价写法

# 同一 Section 批量写(一次落盘)
GameConfig.audio.update(bgm_volume=0.6, mute=True)

# 任意操作合并落盘
with GameConfig.audio.batch():
    GameConfig.bgm_volume.value = 0.4
    GameConfig.mute.toggle()

循环里连续修改几十次配置时,用 batch() 包起来可以把几十次磁盘写入合并为一次。

4. 类型代理

配置项根据默认值类型自动获得专属操作:

默认值类型 代理类 专属方法
bool BoolProxy toggle()
int / float NumberProxy increment(n=1)
list ListProxy append extend insert remove pop clear sort reverse,以及索引读写
dict DictProxy pop update setdefault clear get keys values items,以及键读写
其他 PropertyProxy value default reset is_default on_change

所有代理都提供 value、reset()、is_default、on_change(),且写操作全部经过校验并落盘。

GameConfig.unlocked_levels.append(2)
GameConfig.unlocked_levels.remove(1)
GameConfig.unlocked_levels[0] = 3
GameConfig.screen['width'] = 1600

field() 的返回值带有泛型类型信息,IDE 会把 GameConfig.width 推断为 NumberProxy,可直接补全 increment()。

5. 校验与只读

写入时依次检查:环境变量覆盖 → read_only → 类型 → validator。

net = Section('Net')
port = net.field(default=8080, validator=lambda v: 1 <= v <= 65535)
token = net.field(default='', read_only=True)

port.value = 'big'      # ❌ ConfigTypeError:类型不匹配
port.value = 999999     # ❌ ConfigValueError:validator 不通过
token.value = 'new'     # ❌ WriteError:只读

validator 返回 False 时抛出 ConfigValueError;validator 自身抛出异常时同样包装为 ConfigValueError,原始异常保存在 __cause__,不会裸抛到业务代码。

6. 变更回调

@GameConfig.bgm_volume.on_change
def on_volume_change(section, option, value):
    print(f'{section}.{option} 变为 {value}')


# 普通调用也可以
GameConfig.width.on_change(lambda section, option, value: save_now())

回调签名为 callback(section, option, value),在 value=、set_value()、update()、reset()、代理写操作后触发。

7. 重置

GameConfig.width.reset()        # 单项恢复默认值
GameConfig.screen.reset()       # 整个 Section 批量恢复,一次落盘

多组同构数据

同一套数据格式需要多份实例时(例如多个存档、多套关卡数据),声明多个独立 Section:

DEFAULT_LEVEL = {'score': 0, 'stars': 0, 'cleared': False}

class GameConfig(ConfigBase):
    _path = 'config/game.json'

    level1 = Section('Level1')
    data1 = level1.field(default=dict(DEFAULT_LEVEL), option='data')

    level2 = Section('Level2')
    data2 = level2.field(default=dict(DEFAULT_LEVEL), option='data')


GameConfig.data1['score'] = 1200
GameConfig.data2['score'] = 800

GameConfig.level1.reset()       # 只重置 Level1,Level2 不受影响

dict(DEFAULT_LEVEL) 保证每组的默认值互不共享。

环境变量覆盖

部署时常用环境变量注入配置,优先级为:

环境变量  >  配置文件  >  默认值
class ServerConfig(ConfigBase):
    _path = 'config/server.enc'
    _cipher = AesGcmCipher()          # 密钥省略时读取 CONF_BASE_KEY

    net = Section('Net')
    host = net.field(default='0.0.0.0', env_key='APP_HOST')
    port = net.field(default=8080, env_key='APP_PORT')
    debug = net.field(default=False, env_key='APP_DEBUG')
    db_pass = net.field(default='', env_key='APP_DB_PASS', read_only=True)

环境变量永远是字符串,框架按 default 的类型自动解析:

默认值类型 环境变量示例 解析结果
int "8080" 8080
float "0.5" 0.5
bool "true" / "1" / "yes" True
bool "false" / "0" / "no" False
list '["10.0.0.1", "10.0.0.2"]' 列表(JSON 解析)
dict '{"host": "db.local"}' 字典(JSON 解析)
str "hello" "hello"

解析失败会立即抛出 ConfigValueError。list / dict 必须使用 JSON 语法,例如 APP_HOSTS='["10.0.0.1","10.0.0.2"]'。

default=None 的配置项无法从 default 推断类型,因此不支持 env_key;一旦设置对应环境变量,读取时会抛出 ConfigValueError。

被环境变量覆盖的配置项禁止回写文件,避免把部署注入的值写进配置:

ServerConfig.port.value = 9090   # ❌ WriteError:由 APP_PORT 覆盖
ServerConfig.port.has_env_override   # True

存储格式与加密

存储拆成两个正交的部分,分别用 _format 与 _cipher 配置:

写入:dict --format.serialize--> text --cipher.encrypt--> file
读取:file --cipher.decrypt--> text --format.deserialize--> dict
类 角色 说明 依赖
JsonFormat 格式 明文 JSON,默认 无
YamlFormat 格式 明文 YAML,按原生格式落盘 conf_base[yaml]
TomlFormat 格式 明文 TOML,按原生格式落盘 conf_base[toml]
NoCipher 加密 不加密,默认 无
HmacCipher 加密 明文 + HMAC-SHA256 签名,可读但不可改 无
AesCipher 加密 AES-128-CBC + HMAC(Fernet) conf_base[aes]
AesGcmCipher 加密 AES-256-GCM(AEAD),推荐 conf_base[aes]
ChaCha20Poly1305Cipher 加密 ChaCha20-Poly1305(AEAD) conf_base[aes]

路径、格式与加密

from conf_base import AesGcmCipher, ConfigBase, Section, YamlFormat


class AppConfig(ConfigBase):
    _path = 'config/app.yaml'      # 显式路径:原样使用,不做任何改写
    _format = YamlFormat()         # 可省略:按 _path 后缀推断
    _cipher = AesGcmCipher()       # 加密后写入同一路径

    s = Section('Server')
    port = s.field(default=8080)
  • _path 原样使用;未指定时按类名生成 {类名小写}{格式后缀}{加密后缀},如 appconfig.json、appconfig.yaml.enc
  • _format 未指定时按后缀推断(.json / .yaml / .yml / .toml),无法识别则用 JsonFormat
  • 路径后缀与显式格式冲突时(如 _path='app.yaml' 配 JsonFormat())立即抛 ValueError
  • _cipher 未指定时为 NoCipher
配置 生成的文件名
全省略 appconfig.json
_format = YamlFormat() appconfig.yaml
_format = YamlFormat() + _cipher = AesGcmCipher() appconfig.yaml.enc
_path = 'app.conf' app.conf(后缀无法识别,按 JSON 处理)

YAML / TOML 写入前会统一校验数据,无法表达的类型会给出明确错误:

数据情况 行为
tuple 自动转为 list
非字符串字典键 ConfigValueError,并指出位置
set / bytes / 自定义对象 ConfigValueError
None YAML 支持;TOML 不支持,抛 ConfigValueError
datetime / date / time 原生支持

文件按所选格式写入,但每次保存都会从内存字典重新序列化,因此手工添加的注释与排版不会保留。

签名(内容可读,禁止篡改)

_cipher = HmacCipher(key='signing-key')   # 省略 key 时读取 CONF_BASE_SIGN_KEY

签名以注释行附加在文件末尾(# conf_base-signature: v1:<hex>),不产生额外文件。加载时先校验签名再解析内容,文件被手工修改、签名缺失或密钥不符都会抛 DecryptError。

加密(内容不可见,自带完整性校验)

# AES-256-GCM,密钥省略时读取 CONF_BASE_KEY
_cipher = AesGcmCipher(key_id='v1')

# 无 AES 硬件加速的设备
_cipher = ChaCha20Poly1305Cipher()

# 兼容既有数据
_cipher = AesCipher(key='my-app-key')

AEAD 加密器每次写入使用随机 nonce,相同明文产生不同密文;密文信封中包含算法与 key_id,密钥错误、密文篡改、密钥版本不匹配都会解密失败。

密钥建议:

方式 写法
环境变量(推荐) AesGcmCipher(),配合 CONF_BASE_KEY
密钥管理服务 AesGcmCipher(key=vault.get('app/secret'))
硬编码 仅限本地调试,不要提交到仓库

自定义格式 / 加密

格式与加密互不影响,按需继承其中一个即可接入任意存储方式:

from typing import Any, Dict

from conf_base import BaseCipher, BaseFormat


class MyFormat(BaseFormat):
    extension = '.dat'

    def serialize(self, data: Dict[str, Any]) -> str:
        ...

    def deserialize(self, text: str) -> Dict[str, Any]:
        ...


class MyCipher(BaseCipher):
    extension = ''          # 自动生成文件名时追加的后缀,不改变文件名则为 ''

    def encrypt(self, text: str) -> str:
        ...

    def decrypt(self, text: str) -> str:
        ...


class AppConfig(ConfigBase):
    _format = MyFormat()
    _cipher = MyCipher()

配置版本迁移

配置文件会记录 _schema_version,启动时按 _migrate 中的函数链把旧数据升级到新结构:

def migrate_v1_to_v2(data: dict) -> dict:
    """把 bgm_volume 重命名为 bgm"""
    setting = data.get('Setting', {})
    if 'bgm_volume' in setting:
        setting['bgm'] = setting.pop('bgm_volume')
    return data


class GameConfig(ConfigBase):
    _path = 'config/game.json'
    _schema_version = 2
    _migrate = {1: migrate_v1_to_v2}

    audio = Section('Audio')
    bgm = audio.field(default=1.0)
情况 行为
新文件首次启动 直接按目标版本创建
文件版本 < 目标版本 依次执行迁移函数,完成后写回版本号
文件版本 = 目标版本 不迁移
文件版本 > 目标版本 不迁移,仅同步数据
缺少某一步迁移函数 启动时抛出 ValueError

迁移后新增的配置项不需要在迁移函数里填值,框架会用 default 自动补齐。文件中的未知 section / option 默认原样保留,避免误删手工添加的内容;需要与代码严格同步时设置 _prune_unknown = True,启动时会删除代码中已不存在的项。

_schema_version 是内部字段,to_dict() 不会导出。

并发与性能

线程安全:内置 threading.RLock,始终生效。

多进程:安装 filelock 后自动启用文件锁,避免多个进程同时写同一文件时互相覆盖。

class AppConfig(ConfigBase):
    _use_file_lock = False      # 关闭进程锁(各进程各用各的文件时)
    _lock_timeout = 30.0        # 等待锁的时间,默认 10 秒

外部修改自动重载:默认配置只在类定义时读取一次,之后全部走内存。需要运维改文件即时生效时开启:

class ServerConfig(ConfigBase):
    _path = 'config/server.json'
    _auto_reload = True         # 读取时比对文件指纹,变化才重新解析

参考量级(JSON 文件,约 300 字节):

操作 耗时
单值读取 ~0.3 µs(纯内存)
auto_reload 单值读取 ~5 µs(含文件指纹检查)
单次落盘 ~2 ms
batch() 合并 3 项写入 ~2.3 ms

读取几乎零成本,写入瓶颈在磁盘;高频修改优先用 batch()。

异常体系

所有异常继承 ConfBaseError,可统一捕获:

import conf_base.error as E

try:
    GameConfig.width.value = -1
except E.ConfBaseError as exc:
    print(f'配置错误: {exc}')
异常 触发场景
ConfBaseError 所有框架异常的基类
ConfigTypeError 写入类型与默认值不一致
ConfigValueError validator 校验失败、validator 自身抛异常、环境变量解析失败
WriteError 写入只读配置项,或写入被环境变量覆盖的配置项
ParseError 配置文件损坏,无法解析
DecryptError 解密失败、签名校验失败、文件被篡改

配置文件加载阶段,解密 / 签名失败直接抛 DecryptError;DecryptError 之外的读取或解析问题统一包装为 ParseError。原始异常保存在 __cause__ 中。

API 速查

ConfigBase(配置类)

成员 说明
_path 配置文件路径,省略时按类名生成
_format JsonFormat / YamlFormat / TomlFormat,省略时按路径后缀推断
_cipher 加密器,省略时为 NoCipher
_auto_reload 读取时检查文件指纹,变化后重新加载,默认 False
_prune_unknown 启动时删除代码中不存在的 section / option,默认 False(保留)
_use_file_lock / _lock_timeout 进程锁开关与超时,默认 True / 10.0
_schema_version / _migrate 目标版本号与迁移函数字典
to_dict() 导出全部配置的深拷贝
config_node 已注册的 section → option 列表

Section

成员 说明
field(default='', option=None, validator=None, read_only=False, env_key=None) 声明配置项,option 省略时使用类属性名
update(**kwargs) 批量写入,一次落盘
batch() 上下文管理器,退出时统一落盘
reset() 重置本 Section 所有配置项
current_options / default_options 当前值 / 默认值字典
section['key'] / section.key 读取单个配置项
'key' in section 判断配置项是否存在

配置项代理

成员 说明
.value 当前值,可读写
.set_value(value) 写入值
.default 默认值(只读)
.reset() 恢复默认值
.is_default 当前值是否等于默认值
.on_change(callback) 注册变更回调,支持装饰器
.section / .option 所属 Section 与配置项名
.has_env_override / .env_key 是否被环境变量覆盖 / 环境变量名

项目结构

conf_base/
├── __init__.py       # 对外导出
├── config_base.py    # ConfigBase 配置类引导
├── config_file.py    # 文件读写 + 批量落盘
├── format.py         # JsonFormat / YamlFormat / TomlFormat
├── cipher.py         # NoCipher / HmacCipher / AesCipher / AEAD
├── descriptor.py     # ConfigProperty + 类型代理
├── section.py        # Section 配置块
└── error.py          # 统一异常体系

测试与 CI

81 个用例,总覆盖率 85%。本地运行:

pip install -e .[dev]
python -m pytest test/ -v --cov=conf_base
python -m mypy conf_base

GitHub Actions 会在每次 push / PR 时执行 Python 3.8–3.12 的测试矩阵、mypy 严格检查与打包验证。

License

MIT License

Metadata

Release files for conf-base 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for conf-base 0.4.0
File Size Uploaded Provenance
conf_base-0.4.0.tar.gz 44.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for conf-base 0.4.0
File Interpreter ABI Platform
conf_base-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 89.5 kB

Release files / conf_base-0.4.0.tar.gz

Download URL conf_base-0.4.0.tar.gz
Size 44.6 kB
Tags Source
SHA-256 checksum
How to use checksums
7605a8b32dad7c4e0a8791a11e7ca9c5d4d0d699b4ed8621c56f9ea27a1c95e9
BLAKE2b-256 checksum
How to use checksums
01fe63487d6fb8625fc56f3336e48487e5914cb0bf9b7faf0885438c1fe5ba9f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / conf_base-0.4.0-py3-none-any.whl

Download URL conf_base-0.4.0-py3-none-any.whl
Size 44.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f89e24a971b0b6003ec3824ffbce59e55c39b7a178f104203895faebf456a9ef
BLAKE2b-256 checksum
How to use checksums
eba4d4abab614bcb4147ed879936af75d3cdf878987474922fb3d71fe66d53fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for conf-base 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for conf_base-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page