Metadata-Version: 2.4
Name: yaml-config-loader
Version: 1.3.0
Summary: 轻量级 Python 配置加载工具包，提供类似 Spring Boot 的 @configuration_properties 注解，支持 YAML 配置绑定、单例 Bean 容器、点访问字典等功能。
Author-email: wangxz <wangxz@example.com>
License: MIT
Project-URL: Homepage, https://github.com/wangxz/yaml-config-loader
Project-URL: Repository, https://github.com/wangxz/yaml-config-loader
Project-URL: Documentation, https://github.com/wangxz/yaml-config-loader#readme
Project-URL: Issues, https://github.com/wangxz/yaml-config-loader/issues
Keywords: yaml,config,configuration,spring-boot,dataclass,ioc,properties
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Dynamic: license-file

# yaml-config-loader

轻量级 Python 配置加载工具包，提供类似 Spring Boot 的配置管理能力。

## 功能特性

- **YAML 配置加载**：自动递归查找 `application.yml`，全局缓存配置
- **环境变量解析**：yml 中的 `${ENV_VAR}` / `${ENV_VAR:-default}` 自动替换为环境变量值
- **@configuration_properties**：类似 Spring Boot 的配置属性绑定，支持嵌套 dataclass 自动转换
- **标量类型强转**：按字段注解将配置值强转为 `str/int/float/bool`
- **List[X] 泛型转换**：`Optional[List[SkillConfig]]` 递归转换为 dataclass 实例列表
- **@Value**：类似 Spring Boot 的 `@Value("${...}")`，单个配置值注入，支持嵌套路径和默认值
- **aliases 别名映射**：yml 中的数字 key（如 `"1002"`）、关键字等映射到合法的 Python 属性名
- **aliases 嵌套路径**：aliases 值含 `.`（如 `"otherMath.100.a.100"`）时视为完整层级路径，严格按 yml 层级钻取，解决不同层级同名 key 的歧义
- **字段级 yml_key 显式声明**：嵌套 dataclass 字段用 `field(metadata={"yml_key": "100"})` 精确声明映射，变量名任意，不依赖命名约定
- **自动 kebab-case → snake_case 转换**：yml 中的 `api-key` 自动映射到 Python 的 `api_key`，无需手动配置
- **@component 单例容器**：轻量级 IoC 容器，线程安全单例，自动注册和获取 Bean
- **DotDict 点访问字典**：支持 `config.user.name` 方式访问嵌套字典
- **零配置**：开箱即用，无需复杂设置

## 安装

```bash
pip install yaml-config-loader
```

> **v1.1.0 包名变更说明**：import 包名已从 `config_loader` 更名为 `yaml_config_loader`，与 PyPI 包名保持一致，这样 PyCharm 等 IDE 能自动识别并提示"安装软件包"。
>
> 旧代码 `from config_loader import ...` 仍然可用（兼容别名包），但推荐新代码使用 `from yaml_config_loader import ...`。

## 更新日志

### v1.3.0（最新）

**新增功能：**

1. **@Value 单值注入**：类似 Spring Boot `@Value("${...}")`，支持两种用法
   - `Annotated + @value_config`（推荐，保留类型注解）
   - 描述符方式（简洁，无需类型注解）
2. **aliases 别名映射**：解决 yml 中数字 key（如 `"1002"`）、Python 关键字（如 `class`）无法作为属性名的问题
3. **自动 kebab-case → snake_case 转换**：yml 中的 `api-key` 自动映射到 Python 的 `api_key`，嵌套对象也自动转换，无需手动写 aliases
4. **aliases 嵌套路径**：aliases 值含 `.`（如 `"b_a": "otherMath.100.a.100"`）视为相对前缀的完整层级路径，严格按 yml 层级钻取绑定，不同层级的同名数字 key 不会混淆
5. **字段级 yml_key 显式声明**：嵌套 dataclass 字段用 `field(metadata={"yml_key": "100"})` 精确声明对应的 yml key，变量名任意命名，不依赖 `X_100` 命名约定

**修复的问题：**

1. **`${ENV_VAR}` 环境变量不解析**：yaml_loader 新增 `_resolve_env_vars` 递归解析 `${ENV_VAR}` 与 `${ENV_VAR:-default}`，加载与回退读取均生效，不再把字面量传给下游
2. **缓存为空时回退读文件依赖相对路径 + 静默吞错**：回退改为 `find_yaml()` 项目级搜索（绝对路径），不依赖 CWD；读取失败/文件未找到均输出 `logger.warning`，不再静默绑成 `{}`
3. **绑定失败完全静默**：前缀不存在、字段缺 key、文件未找到、aliases 目标属性未声明、嵌套路径不存在，五种情况均输出 `logger.warning` 告警
4. **不做标量类型强转**：新增 `_coerce_scalar`，按字段注解将配置值强转为 `str/int/float/bool`（bool 支持 `"false"/"0"/"no"/"off"` 等字符串），注解为 `float` 时 `0.8` 不再变字符串
5. **List[dataclass] 泛型不转换**：新增 `List[X]` 递归转换，`Optional[List[SkillConfig]]` 实际拿到的是 `List[SkillConfig]` 实例列表，注解与运行时类型一致
6. **@component 返回工厂函数**：改为返回类本身 + `__new__` 单例，`isinstance`、继承、`get_type_hints`、反射全部正常，`get_bean()` 直接返回实例
7. **functools.wraps 污染类属性**：移除 `@component` 中的 `functools.wraps(cls)`；`@configuration_properties` 使用 `functools.wraps(cls, updated=[])` 避免类 `__dict__` 复制
8. **init_config import 即执行 + verbose 打印**：`init_config(verbose=False)` 默认静默（print 仅在 verbose=True），日志统一走 logging 体系，不污染控制台
9. **单例非线程安全 / 注册表无锁**：`__new__` 双检锁 + 注册表锁保证线程安全；同名类覆盖注册表时输出 `logger.warning` 提示
10. **_get_caller_dir 依赖 inspect.stack**：支持 `set_config_search_dir()` 显式指定搜索起点 + 调用方目录缓存（同进程只计算一次），inspect.stack 仅兜底且失败回退 cwd，兼容 PyInstaller 等场景
11. **get_session 直接崩**：非标识符 yml key（纯数字 `"1003"` 等）绑定到实例 `__dict__`，`getattr(self, "1003")` 动态访问可用，`get_session` 不再崩溃

**优化：**
- `get_bean()` 支持直接传类对象 `get_bean(MyService)`，不再只能传类名字符串
- `DotDict` 构造函数递归转换嵌套 dict，`DotDict(config)` 直接支持点访问

### v1.1.0

- import 包名从 `config_loader` 更名为 `yaml_config_loader`（与 PyPI 包名一致）
- 保留 `config_loader` 兼容别名包，老用户代码无需修改

### v1.0.0

- 初始版本：YAML 配置加载、@configuration_properties、@component、DotDict

## 快速开始

### 1. 创建 application.yml

```yaml
user:
  name: "张三"
  age: 25
  email: "zhangsan@example.com"
  address:
    city: "北京"
    street: "长安街"
  # kebab-case（连字符）自动转换为 snake_case（下划线）
  api-key: "abc123"
  server-timeout: 30
  # 纯数字 key 自动后缀匹配（如 _1002 / item_1002），也可用 aliases 显式声明
  "1002": "编号1002的值"

server:
  port: 8080
  host: "localhost"
```

### 2. 初始化配置

```python
from yaml_config_loader import init_config

# 一行初始化配置（类似 load_dotenv(find_dotenv())）
# 自动递归查找 application.yml 并加载到全局缓存
init_config()
```

### 3. 使用 @configuration_properties 绑定配置

```python
from dataclasses import dataclass
from typing import Optional
from yaml_config_loader import configuration_properties, component, DotDict

@component
@configuration_properties(prefix="user")
@dataclass
class UserConfig:
    name: str = None
    age: int = None
    email: str = None
    address: Optional[DotDict] = None  # 嵌套配置自动转为 DotDict

    def show(self):
        print(f"姓名: {self.name}")
        print(f"年龄: {self.age}")
        print(f"城市: {self.address.city}")  # 点访问嵌套配置

user = UserConfig()
user.show()
```

### 4. 嵌套 dataclass 自动转换

```python
from dataclasses import dataclass
from typing import Optional
from yaml_config_loader import configuration_properties

@dataclass
class Address:
    city: str = None
    street: str = None

@configuration_properties(prefix="user")
@dataclass
class UserConfig:
    name: str = None
    address: Optional[Address] = None  # 自动转为 Address 实例

user = UserConfig()
print(user.address.city)  # 北京
```

### 5. 自动 kebab-case → snake_case 转换

yml 中的 `api-key`（连字符）自动映射到 Python 的 `api_key`（下划线），**不需要写 aliases**，嵌套对象也自动转换。

**yml 配置：**
```yaml
user:
  api-key: "abc123"
  server-timeout: 30
  nested:
    my-name: "嵌套配置"
```

**Python 代码：**
```python
from dataclasses import dataclass
from yaml_config_loader import configuration_properties

@configuration_properties(prefix="user")
@dataclass
class UserConfig:
    # yml 中的 api-key → 自动映射到 api_key
    api_key: str = None
    # yml 中的 server-timeout → 自动映射到 server_timeout
    server_timeout: int = None
    # 嵌套对象中的 my-name → 自动映射到 my_name
    nested: dict = None

user = UserConfig()
print(user.api_key)          # abc123
print(user.server_timeout)   # 30
print(user.nested.my_name)   # 嵌套配置
```

**转换规则：**

| yml 中的 key | Python 属性名 | 是否需要 aliases |
|-------------|--------------|------------------|
| `api-key` | `api_key` | ❌ 自动转换 |
| `server-timeout` | `server_timeout` | ❌ 自动转换 |
| `my-name` | `my_name` | ❌ 自动转换 |
| `"1002"`（纯数字） | `_1002` / `item_1002` 等任意 `X_1002` 形式 | ❌ 自动后缀匹配；也可用 aliases / metadata 显式声明 |
| `"class"`（关键字） | `class_type` | ✅ 需要 aliases |

### 6. aliases 别名映射

当 yml 中的 key 是**纯数字**（如 `"1002"`）或 **Python 关键字**（如 `class`）时，无法作为 Python 属性名，用 `aliases` 参数做映射。

**yml 配置：**
```yaml
user:
  "1002": "编号1002的值"
  "1003": 1003
  "class": "用户类"
```

**Python 代码：**
```python
from dataclasses import dataclass
from yaml_config_loader import configuration_properties

@configuration_properties(
    prefix="user",
    aliases={
        "item_1002": "1002",   # Python 属性名 item_1002 ← yml key "1002"
        "item_1003": "1003",   # Python 属性名 item_1003 ← yml key "1003"
        "class_type": "class",  # Python 属性名 class_type ← yml key "class"
    },
)
@dataclass
class UserConfig:
    item_1002: str = None
    item_1003: int = None
    class_type: str = None

user = UserConfig()
print(user.item_1002)   # 编号1002的值
print(user.item_1003)   # 1003
print(user.class_type)   # 用户类
```

**aliases 格式：**
```python
aliases={
    "Python属性名": "yml中的key名",
}
```

**优先级：**
1. `aliases` 显式映射（优先级最高，同层 key）
2. `aliases` 嵌套路径（值含 `.`，从前缀向下钻取完整层级）
3. 自动 kebab-case → snake_case 转换
4. 纯数字 key 后缀匹配（`"100"` 匹配任意 `X_100` 形式字段）
5. 直接用 yml key

### 6.1 aliases 嵌套路径（完整层级映射）

当 yml 中**不同层级出现同名 key**（如 `otherMath.100` 与 `otherMath.100.a.100` 都有 `"100"`）时，同层 key 别名无法区分。aliases 的值含 `.` 时视为**相对前缀的完整层级路径**，严格按 yml 层级钻取；变量名（键）可任意命名，被映射者（值）必须写清 yml 层级。

**yml 配置：**
```yaml
ai:
  otherMath:
    "100":
      model: qwen3-max
      a:
        "100":
          b: qwen3-max-plus
```

**Python 代码：**
```python
from dataclasses import dataclass
from typing import Optional
from yaml_config_loader import configuration_properties

@dataclass
class BConfig:
    b: str = None

@configuration_properties(prefix="ai", aliases={
    # 变量名任意；值写清 yml 完整层级（相对 ai 前缀），精确指向 a 层下的 "100"
    "b_a": "otherMath.100.a.100",
})
class AiConfig:
    b_a: Optional[BConfig] = None

conf = AiConfig()
print(conf.b_a.b)   # qwen3-max-plus（精确取到 a 层下的 "100"，不会与 otherMath 层的 "100" 混淆）
```

### 6.2 字段级 yml_key 显式声明（嵌套 dataclass）

嵌套 dataclass 内部也可以用 `field(metadata={"yml_key": "..."})` 显式声明字段对应的 yml key：**变量名任意命名，映射精确锁定当前层级**，不依赖 `X_100` 之类的命名约定，不同层级出现同名 key 也不会错乱。

**yml 配置：**
```yaml
ai:
  otherMath:
    "100":
      model: qwen3-max
      a:
        "100":
          b: qwen3-max-plus
```

**Python 代码：**
```python
from dataclasses import dataclass, field
from typing import Optional
from yaml_config_loader import configuration_properties

@dataclass
class BConfig:
    b: str = None

@dataclass
class AConfig:
    # 变量名任意；yml_key 精确指向当前层级（a 层）的 "100"
    b_a: Optional[BConfig] = field(default=None, metadata={"yml_key": "100"})

@dataclass
class ModelConfig:
    model: str = None
    a: Optional[AConfig] = None

@dataclass
class OtherMathConfig:
    item_100: Optional[ModelConfig] = None   # 数字 key "100" 自动映射到 item_100

@configuration_properties(prefix="ai")
class AiConfig:
    otherMath: Optional[OtherMathConfig] = None

conf = AiConfig()
print(conf.otherMath.item_100.a.b_a.b)   # qwen3-max-plus
```

**嵌套 dataclass 内字段的匹配优先级：**
1. `metadata={"yml_key": "..."}` 显式声明（最高，精确匹配当前层级的 key，含 int 型数字 key）
2. 自动 kebab-case → snake_case 转换
3. 纯数字 key 后缀匹配（`"100"` 匹配任意 `X_100` 形式字段）
4. 直接用 yml key

### 7. 使用 @Value 注入单个配置值

类似 Spring Boot 的 `@Value("${...}")`，只读取单个配置值，更轻量灵活。

**推荐用法（Annotated + @value_config，保留类型注解）：**

```python
from typing import Annotated
from yaml_config_loader import init_config, Value, value_config

init_config()

@value_config
class AppConfig:
    # 基本用法：读取 user.name
    name: Annotated[str, Value(key="user.name", default="未知用户")] = None

    # 带默认值：配置不存在时返回 18
    age: Annotated[int, Value(key="user.age", default=18)] = None

    # 嵌套路径：支持点分隔，如 user.address.city
    city: Annotated[str, Value(key="user.address.city", default="未知城市")] = None

    # 读取其他前缀的配置
    server_port: Annotated[int, Value(key="server.port", default=8080)] = None

app = AppConfig()
print(app.name)         # 张三
print(app.age)          # 25
print(app.city)         # 北京
print(app.server_port)  # 8080
```

**简洁用法（描述符方式，无需类型注解）：**

```python
from yaml_config_loader import init_config, Value

init_config()

class AppConfig:
    name = Value("user.name")
    age = Value("user.age", default=18)
    city = Value("user.address.city", default="未知城市")
```

> **说明**：Python 语法不支持 `@Value` 直接写在类属性上（`@装饰器` 只能用于 `def` 和 `class`），因此用 `Annotated + @value_config` 的方式实现等价效果，同时保留类型提示。

**@Value 与 @configuration_properties 的区别：**

| 特性 | @Value | @configuration_properties |
|------|--------|--------------------------|
| 适用场景 | 只需要几个配置值 | 需要绑定一整组配置 |
| 用法 | `Annotated[str, Value(key="...")]` + `@value_config` | 类装饰器 `@configuration_properties(prefix="user")` |
| 默认值 | 支持 `default=xxx` | 字段默认值 |
| 嵌套路径 | 支持点分隔 `user.address.city` | 自动绑定嵌套 dataclass |
| 类型注解 | ✅ 保留 | ✅ 配合 @dataclass |
| 只读 | ✅ 禁止直接赋值 | ❌ 可修改 |
| kebab-case 自动转换 | ❌ 需手动写完整路径 | ✅ 自动转换 |
| aliases 支持 | ❌ 需手动写完整路径 | ✅ 支持 |

### 8. 使用单例容器

```python
from yaml_config_loader import component, get_bean

@component
class MyService:
    def hello(self):
        return "Hello, World!"

# 获取单例实例（支持直接传类对象）
service = get_bean(MyService)
print(service.hello())  # Hello, World!

# 也支持传类名字符串
service2 = get_bean("MyService")
print(service is service2)  # True（单例）
```

### 9. 直接使用 YAML 加载工具

```python
from yaml_config_loader import load_yaml, get_yaml_config, find_yaml

# 自动查找并加载 application.yml
load_yaml()

# 获取全局配置
config = get_yaml_config()
print(config["user"]["name"])  # 张三

# 查找配置文件路径
path = find_yaml()
print(path)
```

## API 参考

### 装饰器/类

| 装饰器/类 | 说明 |
|--------|------|
| `@configuration_properties(prefix="xxx", aliases={...})` | 绑定 YAML 配置到类属性，支持 aliases 别名映射（含嵌套路径）和自动 kebab-case 转换 |
| `@value_config` | 类装饰器，扫描 Annotated 中的 Value 元数据，自动创建描述符 |
| `@component` | 注册为单例 Bean |
| `Value(key="...", default=xxx)` | 单个配置值注入描述符（类似 Spring Boot @Value） |

### 函数

| 函数 | 说明 |
|------|------|
| `init_config(filename="application.yml", verbose=False)` | 一行初始化配置（查找 + 加载，默认静默，print 仅在 verbose=True 时输出） |
| `set_config_search_dir(dir_path)` | 显式指定配置文件搜索起点目录（适用于 PyInstaller 打包等 inspect.stack 不可靠场景） |
| `load_yaml(path=None)` | 加载 YAML 配置到全局缓存 |
| `find_yaml(filename="application.yml")` | 递归查找 application.yml 文件路径 |
| `get_yaml_config()` | 获取全局配置字典 |
| `get_loaded_yaml_path()` | 获取已加载的配置文件路径 |
| `get_bean(cls_or_name)` | 获取单例 Bean 实例（支持类对象或类名） |
| `list_beans()` | 列出所有已注册的 Bean |
| `to_dot_dict(d, kebab_to_snake=False)` | 将普通字典转为 DotDict，可选 kebab-case 转换 |

### 类

| 类 | 说明 |
|----|------|
| `DotDict` | 支持点访问的字典，构造时递归转换嵌套 dict |
| `Value` | 单个配置值注入描述符，支持嵌套路径和默认值，只读 |

## 配置文件查找规则

`find_yaml()` 会按以下顺序递归查找：
1. 用户显式指定的目录（调用 `set_config_search_dir()` 时，优先级最高）
2. 当前脚本所在目录
3. 当前目录下的常见配置子目录（`config/`、`conf/`、`resources/` 等）
4. 定位项目根目录（通过 `.git`、`pyproject.toml` 等标记）
5. 在整个项目范围内递归搜索（自动排除 `.venv`、`__pycache__`、`.git` 等目录）

支持的文件名：`application.yml`、`application.yaml`

> 搜索起点说明：调用方脚本目录会缓存（同进程只计算一次），`inspect.stack()` 仅作兜底（失败时回退 cwd）。若运行环境特殊（PyInstaller 打包、C 扩展调用栈），建议显式调用 `set_config_search_dir()` 指定搜索起点。

## 环境变量解析

加载配置时自动递归解析 `${ENV_VAR}` 环境变量引用，支持默认值语法 `${ENV_VAR:-default}`。

```yaml
ai:
  openai:
    api_key: ${ALIYUN_API_KEY}          # 读取环境变量 ALIYUN_API_KEY
    timeout: ${AI_TIMEOUT:-30}          # 环境变量不存在时使用默认值 30
```

- 环境变量存在：替换为环境变量值
- 环境变量不存在但有默认值：使用默认值
- 环境变量不存在且无默认值：保留原字面量（与 Spring Boot 行为一致）

## 许可证

MIT License
