跳到内容

设置管理

Pydantic Settings 提供了可选的 Pydantic 功能,用于从环境变量或密钥文件加载设置或配置类。

安装

安装非常简单,只需:

pip install pydantic-settings

用法

如果您创建一个继承自 BaseSettings 的模型,模型初始化器将尝试通过从环境中读取来确定任何未作为关键字参数传递的字段的值。(如果未设置匹配的环境变量,仍将使用默认值。)

这使其易于:

  • 创建明确定义的、类型提示的应用程序配置类
  • 自动从环境变量读取对配置的修改
  • 在需要时(例如,在单元测试中)手动覆盖初始化器中的特定设置

例如:

from collections.abc import Callable
from typing import Any

from pydantic import (
    AliasChoices,
    AmqpDsn,
    BaseModel,
    Field,
    ImportString,
    PostgresDsn,
    RedisDsn,
)

from pydantic_settings import BaseSettings, SettingsConfigDict


class SubModel(BaseModel):
    foo: str = 'bar'
    apple: int = 1


class Settings(BaseSettings):
    auth_key: str = Field(validation_alias='my_auth_key')  # (1)!

    api_key: str = Field(alias='my_api_key')  # (2)!

    redis_dsn: RedisDsn = Field(
        'redis://user:pass@localhost:6379/1',
        validation_alias=AliasChoices('service_redis_dsn', 'redis_url'),  # (3)!
    )
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'
    amqp_dsn: AmqpDsn = 'amqp://user:pass@localhost:5672/'

    special_function: ImportString[Callable[[Any], Any]] = 'math.cos'  # (4)!

    # to override domains:
    # export my_prefix_domains='["foo.com", "bar.com"]'
    domains: set[str] = set()

    # to override more_settings:
    # export my_prefix_more_settings='{"foo": "x", "apple": 1}'
    more_settings: SubModel = SubModel()

    model_config = SettingsConfigDict(env_prefix='my_prefix_')  # (5)!


print(Settings().model_dump())
"""
{
    'auth_key': 'xxx',
    'api_key': 'xxx',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1'),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar'),
    'amqp_dsn': AmqpDsn('amqp://user:pass@localhost:5672/'),
    'special_function': math.cos,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}
"""
  1. 环境变量名称使用 validation_alias 覆盖。 在这种情况下,将读取环境变量 my_auth_key 而不是 auth_key

    查看 Field 文档 以获取更多信息。

  2. 环境变量名称使用 alias 覆盖。 在这种情况下,环境变量 my_api_key 将用于验证和序列化,而不是 api_key

    查看 Field 文档 以获取更多信息。

  3. AliasChoices 类允许为单个字段设置多个环境变量名称。 将使用找到的第一个环境变量。

    查看 关于别名选择的文档 以获取更多信息。

  4. ImportString 类允许从字符串导入对象。 在这种情况下,将读取环境变量 special_function,并将导入函数 math.cos

  5. env_prefix 配置设置允许为所有环境变量设置前缀。

    查看 环境变量名称文档 以获取更多信息。

默认值验证

与 pydantic BaseModel 不同,默认情况下会验证 BaseSettings 字段的默认值。 您可以通过在 model_config 中或在字段级别通过 Field(validate_default=False) 设置 validate_default=False 来禁用此行为。

from pydantic import Field

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(validate_default=False)

    # default won't be validated
    foo: int = 'test'


print(Settings())
#> foo='test'


class Settings1(BaseSettings):
    # default won't be validated
    foo: int = Field('test', validate_default=False)


print(Settings1())
#> foo='test'

查看 默认值验证 以获取更多信息。

环境变量名称

默认情况下,环境变量名称与字段名称相同。

您可以通过设置 env_prefix 配置设置,或通过实例化时的 _env_prefix 关键字参数来更改所有环境变量的前缀。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix='my_prefix_')

    auth_key: str = 'xxx'  # will be read from `my_prefix_auth_key`

注意

默认的 env_prefix''(空字符串)。 env_prefix 不仅适用于 env 设置,还适用于 dotenv 文件、密钥和其他来源。

如果要更改单个字段的环境变量名称,可以使用别名。

有两种方法可以做到这一点:

  • 使用 Field(alias=...)(参见上面的 api_key
  • 使用 Field(validation_alias=...)(参见上面的 auth_key

查看 Field 别名文档 以获取有关别名的更多信息。

env_prefix 不适用于带有别名的字段。 这意味着环境变量名称与字段别名相同。

from pydantic import Field

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix='my_prefix_')

    foo: str = Field('xxx', alias='FooAlias')  # (1)!
  1. env_prefix 将被忽略,并且值将从 FooAlias 环境变量中读取。

大小写敏感性

默认情况下,环境变量名称不区分大小写。

如果要使环境变量名称区分大小写,可以设置 case_sensitive 配置设置。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(case_sensitive=True)

    redis_host: str = 'localhost'

case_sensitiveTrue 时,环境变量名称必须与字段名称匹配(可选地带有前缀),因此在本示例中,redis_host 只能通过 export redis_host 修改。 如果要将环境变量全部命名为大写,也应将属性命名为全部大写。 您仍然可以通过 Field(validation_alias=...) 将环境变量命名为您喜欢的任何名称。

大小写敏感性也可以通过实例化时的 _case_sensitive 关键字参数来设置。

在嵌套模型的情况下,case_sensitive 设置将应用于所有嵌套模型。

import os

from pydantic import BaseModel, ValidationError

from pydantic_settings import BaseSettings


class RedisSettings(BaseModel):
    host: str
    port: int


class Settings(BaseSettings, case_sensitive=True):
    redis: RedisSettings


os.environ['redis'] = '{"host": "localhost", "port": 6379}'
print(Settings().model_dump())
#> {'redis': {'host': 'localhost', 'port': 6379}}
os.environ['redis'] = '{"HOST": "localhost", "port": 6379}'  # (1)!
try:
    Settings()
except ValidationError as e:
    print(e)
    """
    1 validation error for Settings
    redis.host
      Field required [type=missing, input_value={'HOST': 'localhost', 'port': 6379}, input_type=dict]
        For further information visit https://errors.pydantic.dev/2/v/missing
    """
  1. 请注意,由于环境变量名称为 HOST(全部大写),因此找不到 host 字段。

注意

在 Windows 上,Python 的 os 模块始终将环境变量视为不区分大小写,因此 case_sensitive 配置设置将不起作用 - 设置将始终在忽略大小写的情况下更新。

解析环境变量值

默认情况下,环境变量按字面意思解析,包括值为空的情况。 您可以选择通过将 env_ignore_empty 配置设置设置为 True 来忽略空环境变量。 如果您希望使用字段的默认值而不是来自环境的空值,这将非常有用。

对于大多数简单字段类型(例如 intfloatstr 等),环境变量值的解析方式与直接传递给初始化器(作为字符串)的方式相同。

诸如 listsetdict 和子模型之类的复杂类型是通过将环境变量的值视为 JSON 编码的字符串从环境中填充的。

填充嵌套复杂变量的另一种方法是使用 env_nested_delimiter 配置设置配置模型,然后使用名称指向嵌套模块字段的环境变量。 它所做的只是将您的变量分解为嵌套模型或字典。 因此,如果您定义一个变量 FOO__BAR__BAZ=123,它将将其转换为 FOO={'BAR': {'BAZ': 123}}。 如果您有多个具有相同结构的变量,它们将被合并。

注意

子模型必须继承自 pydantic.BaseModel,否则 pydantic-settings 将初始化子模型,分别收集子模型字段的值,您可能会得到意想不到的结果。

例如,给定以下环境变量:

# your environment
export V0=0
export SUB_MODEL='{"v1": "json-1", "v2": "json-2"}'
export SUB_MODEL__V2=nested-2
export SUB_MODEL__V3=3
export SUB_MODEL__DEEP__V4=v4

您可以将它们加载到以下设置模型中:

from pydantic import BaseModel

from pydantic_settings import BaseSettings, SettingsConfigDict


class DeepSubModel(BaseModel):  # (1)!
    v4: str


class SubModel(BaseModel):  # (2)!
    v1: str
    v2: bytes
    v3: int
    deep: DeepSubModel


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_nested_delimiter='__')

    v0: str
    sub_model: SubModel


print(Settings().model_dump())
"""
{
    'v0': '0',
    'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}},
}
"""
  1. 子模型必须继承自 pydantic.BaseModel

  2. 子模型必须继承自 pydantic.BaseModel

env_nested_delimiter 可以通过如上所示的 model_config 进行配置,也可以通过实例化时的 _env_nested_delimiter 关键字参数进行配置。

默认情况下,环境变量通过 env_nested_delimiter 拆分为任意深度的嵌套字段。 您可以使用 env_nested_max_split 配置设置限制嵌套字段的深度。 一个常见的用例是,这对于两层深度的设置特别有用,其中 env_nested_delimiter(通常是单个 _)可能是模型字段名称的子字符串。 例如:

# your environment
export GENERATION_LLM_PROVIDER='anthropic'
export GENERATION_LLM_API_KEY='your-api-key'
export GENERATION_LLM_API_VERSION='2024-03-15'

您可以将它们加载到以下设置模型中:

from pydantic import BaseModel

from pydantic_settings import BaseSettings, SettingsConfigDict


class LLMConfig(BaseModel):
    provider: str = 'openai'
    api_key: str
    api_type: str = 'azure'
    api_version: str = '2023-03-15-preview'


class GenerationConfig(BaseSettings):
    model_config = SettingsConfigDict(
        env_nested_delimiter='_', env_nested_max_split=1, env_prefix='GENERATION_'
    )

    llm: LLMConfig
    ...


print(GenerationConfig().model_dump())
"""
{
    'llm': {
        'provider': 'anthropic',
        'api_key': 'your-api-key',
        'api_type': 'azure',
        'api_version': '2024-03-15',
    }
}
"""

如果不设置 env_nested_max_split=1GENERATION_LLM_API_KEY 将被解析为 llm.api.key 而不是 llm.api_key,并且会引发 ValidationError

嵌套环境变量优先于顶层环境变量 JSON(例如,在上面的示例中,SUB_MODEL__V2 优先于 SUB_MODEL)。

您还可以通过提供您自己的来源类来填充复杂类型。

import json
import os
from typing import Any

from pydantic.fields import FieldInfo

from pydantic_settings import (
    BaseSettings,
    EnvSettingsSource,
    PydanticBaseSettingsSource,
)


class MyCustomSource(EnvSettingsSource):
    def prepare_field_value(
        self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
    ) -> Any:
        if field_name == 'numbers':
            return [int(x) for x in value.split(',')]
        return json.loads(value)


class Settings(BaseSettings):
    numbers: list[int]

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (MyCustomSource(settings_cls),)


os.environ['numbers'] = '1,2,3'
print(Settings().model_dump())
#> {'numbers': [1, 2, 3]}

禁用 JSON 解析

默认情况下,pydantic-settings 将环境变量中的复杂类型解析为 JSON 字符串。 如果要为字段禁用此行为并在您自己的验证器中解析该值,则可以使用 NoDecode 注解字段。

import os
from typing import Annotated

from pydantic import field_validator

from pydantic_settings import BaseSettings, NoDecode


class Settings(BaseSettings):
    numbers: Annotated[list[int], NoDecode]  # (1)!

    @field_validator('numbers', mode='before')
    @classmethod
    def decode_numbers(cls, v: str) -> list[int]:
        return [int(x) for x in v.split(',')]


os.environ['numbers'] = '1,2,3'
print(Settings().model_dump())
#> {'numbers': [1, 2, 3]}
  1. NoDecode 注解禁用 numbers 字段的 JSON 解析。 将调用 decode_numbers 字段验证器来解析该值。

您还可以通过将 enable_decoding 配置设置设置为 False 来禁用所有字段的 JSON 解析。

import os

from pydantic import field_validator

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(enable_decoding=False)

    numbers: list[int]

    @field_validator('numbers', mode='before')
    @classmethod
    def decode_numbers(cls, v: str) -> list[int]:
        return [int(x) for x in v.split(',')]


os.environ['numbers'] = '1,2,3'
print(Settings().model_dump())
#> {'numbers': [1, 2, 3]}

您可以通过使用 ForceDecode 注解字段来强制对字段进行 JSON 解析。 这将绕过 enable_decoding 配置设置。

import os
from typing import Annotated

from pydantic import field_validator

from pydantic_settings import BaseSettings, ForceDecode, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(enable_decoding=False)

    numbers: Annotated[list[int], ForceDecode]
    numbers1: list[int]  # (1)!

    @field_validator('numbers1', mode='before')
    @classmethod
    def decode_numbers1(cls, v: str) -> list[int]:
        return [int(x) for x in v.split(',')]


os.environ['numbers'] = '["1","2","3"]'
os.environ['numbers1'] = '1,2,3'
print(Settings().model_dump())
#> {'numbers': [1, 2, 3], 'numbers1': [1, 2, 3]}
  1. numbers1 字段未用 ForceDecode 注解,因此它不会被解析为 JSON。 我们必须提供自定义验证器来解析该值。

嵌套模型默认局部更新

默认情况下,Pydantic settings 不允许对嵌套模型默认对象进行局部更新。 可以通过将 nested_model_default_partial_update 标志设置为 True 来覆盖此行为,这将允许对嵌套模型默认对象字段进行局部更新。

import os

from pydantic import BaseModel

from pydantic_settings import BaseSettings, SettingsConfigDict


class SubModel(BaseModel):
    val: int = 0
    flag: bool = False


class SettingsPartialUpdate(BaseSettings):
    model_config = SettingsConfigDict(
        env_nested_delimiter='__', nested_model_default_partial_update=True
    )

    nested_model: SubModel = SubModel(val=1)


class SettingsNoPartialUpdate(BaseSettings):
    model_config = SettingsConfigDict(
        env_nested_delimiter='__', nested_model_default_partial_update=False
    )

    nested_model: SubModel = SubModel(val=1)


# Apply a partial update to the default object using environment variables
os.environ['NESTED_MODEL__FLAG'] = 'True'

# When partial update is enabled, the existing SubModel instance is updated
# with nested_model.flag=True change
assert SettingsPartialUpdate().model_dump() == {
    'nested_model': {'val': 1, 'flag': True}
}

# When partial update is disabled, a new SubModel instance is instantiated
# with nested_model.flag=True change
assert SettingsNoPartialUpdate().model_dump() == {
    'nested_model': {'val': 0, 'flag': True}
}

Dotenv (.env) 支持

Dotenv 文件(通常命名为 .env)是一种常见的模式,它使以平台无关的方式使用环境变量变得容易。

dotenv 文件遵循所有环境变量的相同一般原则,它看起来像这样:

.env
# ignore comment
ENVIRONMENT="production"
REDIS_ADDRESS=localhost:6379
MEANING_OF_LIFE=42
MY_VAR='Hello world'

一旦您的 .env 文件中填充了变量,pydantic 支持两种加载方式:

  1. BaseSettings 类中的 model_config 上设置 env_file(和 env_file_encoding,如果您不想要操作系统的默认编码)
    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    
    class Settings(BaseSettings):
        model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
    
  2. 使用 _env_file 关键字参数(以及需要的 _env_file_encoding)实例化 BaseSettings 派生类
    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    
    class Settings(BaseSettings):
        model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
    
    
    settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')
    
    在任何一种情况下,传递的参数值都可以是任何有效路径或文件名,绝对路径或相对于当前工作目录的相对路径。 从那里,pydantic 将为您处理一切,加载您的变量并验证它们。

注意

如果为 env_file 指定了文件名,Pydantic 将仅检查当前工作目录,而不会检查任何父目录中的 .env 文件。

即使在使用 dotenv 文件时,pydantic 仍将读取环境变量以及 dotenv 文件,**环境变量将始终优先于从 dotenv 文件加载的值**。

通过实例化时(方法 2)的 _env_file 关键字参数传递文件路径将覆盖在 model_config 类上设置的值(如果有)。 如果上述代码片段结合使用,则将加载 prod.env,而 .env 将被忽略。

如果您需要加载多个 dotenv 文件,则可以将多个文件路径作为元组或列表传递。 文件将按顺序加载,每个文件都会覆盖前一个文件。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        # `.env.prod` takes priority over `.env`
        env_file=('.env', '.env.prod')
    )

您还可以使用关键字参数覆盖来告诉 Pydantic 不加载任何文件(即使在 model_config 类中设置了一个文件),方法是传递 None 作为实例化关键字参数,例如 settings = Settings(_env_file=None)

由于 python-dotenv 用于解析文件,因此可以使用类似 bash 的语义,例如 export,这(取决于您的操作系统和环境)可能允许您的 dotenv 文件也与 source 一起使用,有关更多详细信息,请参见 python-dotenv 的文档

对于 dotenv 文件,Pydantic settings 考虑 extra 配置。 这意味着,如果您在 model_config 上设置 extra=forbid默认),并且您的 dotenv 文件包含未在设置模型中定义的字段的条目,则在设置构造中将引发 ValidationError

为了与 pydantic 1.x BaseSettings 兼容,您应该使用 extra=ignore

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file='.env', extra='ignore')

注意

Pydantic settings 从 dotenv 文件加载所有值并将其传递给模型,而与模型的 env_prefix 无关。 因此,如果您在 dotenv 文件中提供额外的值,无论它们是否以 env_prefix 开头,都会引发 ValidationError

命令行支持

Pydantic settings 提供了集成的 CLI 支持,可以轻松地使用 Pydantic 模型快速定义 CLI 应用程序。 Pydantic settings CLI 有两个主要用例:

  1. 当使用 CLI 覆盖 Pydantic 模型中的字段时。
  2. 当使用 Pydantic 模型定义 CLI 时。

默认情况下,体验是为用例 #1 量身定制的,并建立在 解析环境变量 中建立的基础之上。 如果您的用例主要属于 #2,则您可能需要启用 创建 CLI 应用程序 末尾概述的大多数默认设置。

基础知识

为了开始使用,让我们回顾一下 解析环境变量 中介绍的示例,但这次使用 Pydantic settings CLI。

import sys

from pydantic import BaseModel

from pydantic_settings import BaseSettings, SettingsConfigDict


class DeepSubModel(BaseModel):
    v4: str


class SubModel(BaseModel):
    v1: str
    v2: bytes
    v3: int
    deep: DeepSubModel


class Settings(BaseSettings):
    model_config = SettingsConfigDict(cli_parse_args=True)

    v0: str
    sub_model: SubModel


sys.argv = [
    'example.py',
    '--v0=0',
    '--sub_model={"v1": "json-1", "v2": "json-2"}',
    '--sub_model.v2=nested-2',
    '--sub_model.v3=3',
    '--sub_model.deep.v4=v4',
]

print(Settings().model_dump())
"""
{
    'v0': '0',
    'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}},
}
"""

要启用 CLI 解析,我们只需将 cli_parse_args 标志设置为有效值,该值保留了与 argparse 中定义的相似的含义。

请注意,默认情况下,CLI 设置源是 最顶层的来源,除非其 优先级值已自定义

import os
import sys

from pydantic_settings import (
    BaseSettings,
    CliSettingsSource,
    PydanticBaseSettingsSource,
)


class Settings(BaseSettings):
    my_foo: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return env_settings, CliSettingsSource(settings_cls, cli_parse_args=True)


os.environ['MY_FOO'] = 'from environment'

sys.argv = ['example.py', '--my_foo=from cli']

print(Settings().model_dump())
#> {'my_foo': 'from environment'}

列表

列表的 CLI 参数解析支持混合使用以下三种样式中的任何一种:

  • JSON 样式 --field='[1,2]'
  • Argparse 样式 --field 1 --field 2
  • 惰性样式 --field=1,2
import sys

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True):
    my_list: list[int]


sys.argv = ['example.py', '--my_list', '[1,2]']
print(Settings().model_dump())
#> {'my_list': [1, 2]}

sys.argv = ['example.py', '--my_list', '1', '--my_list', '2']
print(Settings().model_dump())
#> {'my_list': [1, 2]}

sys.argv = ['example.py', '--my_list', '1,2']
print(Settings().model_dump())
#> {'my_list': [1, 2]}

字典

字典的 CLI 参数解析支持混合使用以下两种样式中的任何一种:

  • JSON 样式 --field='{"k1": 1, "k2": 2}'
  • 环境变量样式 --field k1=1 --field k2=2

这些也可以与列表形式结合使用,例如:

  • --field k1=1,k2=2 --field k3=3 --field '{"k4": 4}' 等。
import sys

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True):
    my_dict: dict[str, int]


sys.argv = ['example.py', '--my_dict', '{"k1":1,"k2":2}']
print(Settings().model_dump())
#> {'my_dict': {'k1': 1, 'k2': 2}}

sys.argv = ['example.py', '--my_dict', 'k1=1', '--my_dict', 'k2=2']
print(Settings().model_dump())
#> {'my_dict': {'k1': 1, 'k2': 2}}

字面量和枚举

字面量和枚举的 CLI 参数解析将转换为 CLI 选项。

import sys
from enum import IntEnum
from typing import Literal

from pydantic_settings import BaseSettings


class Fruit(IntEnum):
    pear = 0
    kiwi = 1
    lime = 2


class Settings(BaseSettings, cli_parse_args=True):
    fruit: Fruit
    pet: Literal['dog', 'cat', 'bird']


sys.argv = ['example.py', '--fruit', 'lime', '--pet', 'cat']
print(Settings().model_dump())
#> {'fruit': <Fruit.lime: 2>, 'pet': 'cat'}

别名

Pydantic 字段别名将添加为 CLI 参数别名。 长度为一的别名将转换为短选项。

import sys

from pydantic import AliasChoices, AliasPath, Field

from pydantic_settings import BaseSettings


class User(BaseSettings, cli_parse_args=True):
    first_name: str = Field(
        validation_alias=AliasChoices('f', 'fname', AliasPath('name', 0))
    )
    last_name: str = Field(
        validation_alias=AliasChoices('l', 'lname', AliasPath('name', 1))
    )


sys.argv = ['example.py', '--fname', 'John', '--lname', 'Doe']
print(User().model_dump())
#> {'first_name': 'John', 'last_name': 'Doe'}

sys.argv = ['example.py', '-f', 'John', '-l', 'Doe']
print(User().model_dump())
#> {'first_name': 'John', 'last_name': 'Doe'}

sys.argv = ['example.py', '--name', 'John,Doe']
print(User().model_dump())
#> {'first_name': 'John', 'last_name': 'Doe'}

sys.argv = ['example.py', '--name', 'John', '--lname', 'Doe']
print(User().model_dump())
#> {'first_name': 'John', 'last_name': 'Doe'}

子命令和位置参数

子命令和位置参数使用 CliSubCommandCliPositionalArg 注解表示。 子命令注解只能应用于必需字段(即没有默认值的字段)。 此外,子命令必须是从 pydantic BaseModel 或 pydantic.dataclasses dataclass 派生的有效类型。

可以使用 get_subcommand 实用程序函数从模型实例中检索已解析的子命令。 如果子命令不是必需的,请将 is_required 标志设置为 False,以禁用在未找到子命令时引发错误。

注意

CLI 设置子命令限制为每个模型一个子解析器。 换句话说,模型的全部子命令都分组在单个子解析器下; 它不允许有多个子解析器,每个子解析器都有自己的一组子命令。 有关子解析器的更多信息,请参见 argparse 子命令

注意

CliSubCommandCliPositionalArg 始终区分大小写。

import sys

from pydantic import BaseModel

from pydantic_settings import (
    BaseSettings,
    CliPositionalArg,
    CliSubCommand,
    SettingsError,
    get_subcommand,
)


class Init(BaseModel):
    directory: CliPositionalArg[str]


class Clone(BaseModel):
    repository: CliPositionalArg[str]
    directory: CliPositionalArg[str]


class Git(BaseSettings, cli_parse_args=True, cli_exit_on_error=False):
    clone: CliSubCommand[Clone]
    init: CliSubCommand[Init]


# Run without subcommands
sys.argv = ['example.py']
cmd = Git()
assert cmd.model_dump() == {'clone': None, 'init': None}

try:
    # Will raise an error since no subcommand was provided
    get_subcommand(cmd).model_dump()
except SettingsError as err:
    assert str(err) == 'Error: CLI subcommand is required {clone, init}'

# Will not raise an error since subcommand is not required
assert get_subcommand(cmd, is_required=False) is None


# Run the clone subcommand
sys.argv = ['example.py', 'clone', 'repo', 'dest']
cmd = Git()
assert cmd.model_dump() == {
    'clone': {'repository': 'repo', 'directory': 'dest'},
    'init': None,
}

# Returns the subcommand model instance (in this case, 'clone')
assert get_subcommand(cmd).model_dump() == {
    'directory': 'dest',
    'repository': 'repo',
}

CliSubCommandCliPositionalArg 注解还支持联合操作和别名。 对于 Pydantic 模型的联合,重要的是要记住验证期间可能出现的细微差别。 具体来说,对于内容相同的子命令的联合,建议将它们分解为单独的 CliSubCommand 字段,以避免任何复杂情况。 最后,从联合派生的子命令名称将是 Pydantic 模型类本身的名称。

当为 CliSubCommandCliPositionalArg 字段分配别名时,只能分配单个别名。 对于非联合子命令,别名将更改显示的帮助文本和子命令名称。 相反,对于联合子命令,从 CLI 设置源的角度来看,别名将没有实际效果。 最后,对于位置参数,别名将更改为字段显示的 CLI 帮助文本。

import sys
from typing import Union

from pydantic import BaseModel, Field

from pydantic_settings import (
    BaseSettings,
    CliPositionalArg,
    CliSubCommand,
    get_subcommand,
)


class Alpha(BaseModel):
    """Apha Help"""

    cmd_alpha: CliPositionalArg[str] = Field(alias='alpha-cmd')


class Beta(BaseModel):
    """Beta Help"""

    opt_beta: str = Field(alias='opt-beta')


class Gamma(BaseModel):
    """Gamma Help"""

    opt_gamma: str = Field(alias='opt-gamma')


class Root(BaseSettings, cli_parse_args=True, cli_exit_on_error=False):
    alpha_or_beta: CliSubCommand[Union[Alpha, Beta]] = Field(alias='alpha-or-beta-cmd')
    gamma: CliSubCommand[Gamma] = Field(alias='gamma-cmd')


sys.argv = ['example.py', 'Alpha', 'hello']
assert get_subcommand(Root()).model_dump() == {'cmd_alpha': 'hello'}

sys.argv = ['example.py', 'Beta', '--opt-beta=hey']
assert get_subcommand(Root()).model_dump() == {'opt_beta': 'hey'}

sys.argv = ['example.py', 'gamma-cmd', '--opt-gamma=hi']
assert get_subcommand(Root()).model_dump() == {'opt_gamma': 'hi'}

创建 CLI 应用程序

CliApp 类提供了两个实用程序方法 CliApp.runCliApp.run_subcommand,可用于将 Pydantic BaseSettingsBaseModelpydantic.dataclasses.dataclass 作为 CLI 应用程序运行。 主要地,这些方法为运行与模型关联的 cli_cmd 方法提供了结构。

CliApp.run 可用于直接提供要解析的 cli_args,并且将在实例化后运行模型 cli_cmd 方法(如果已定义)。

from pydantic_settings import BaseSettings, CliApp


class Settings(BaseSettings):
    this_foo: str

    def cli_cmd(self) -> None:
        # Print the parsed data
        print(self.model_dump())
        #> {'this_foo': 'is such a foo'}

        # Update the parsed data showing cli_cmd ran
        self.this_foo = 'ran the foo cli cmd'


s = CliApp.run(Settings, cli_args=['--this_foo', 'is such a foo'])
print(s.model_dump())
#> {'this_foo': 'ran the foo cli cmd'}

类似地,CliApp.run_subcommand 可以以递归方式用于运行子命令的 cli_cmd 方法。

from pydantic import BaseModel

from pydantic_settings import CliApp, CliPositionalArg, CliSubCommand


class Init(BaseModel):
    directory: CliPositionalArg[str]

    def cli_cmd(self) -> None:
        print(f'git init "{self.directory}"')
        #> git init "dir"
        self.directory = 'ran the git init cli cmd'


class Clone(BaseModel):
    repository: CliPositionalArg[str]
    directory: CliPositionalArg[str]

    def cli_cmd(self) -> None:
        print(f'git clone from "{self.repository}" into "{self.directory}"')
        self.directory = 'ran the clone cli cmd'


class Git(BaseModel):
    clone: CliSubCommand[Clone]
    init: CliSubCommand[Init]

    def cli_cmd(self) -> None:
        CliApp.run_subcommand(self)


cmd = CliApp.run(Git, cli_args=['init', 'dir'])
assert cmd.model_dump() == {
    'clone': None,
    'init': {'directory': 'ran the git init cli cmd'},
}

注意

CliApp.run 不同,CliApp.run_subcommand 需要子命令模型具有已定义的 cli_cmd 方法。

对于 BaseModelpydantic.dataclasses.dataclass 类型,CliApp.run 将在内部使用以下 BaseSettings 配置默认值:

  • nested_model_default_partial_update=True
  • case_sensitive=True
  • cli_hide_none_type=True
  • cli_avoid_json=True
  • cli_enforce_required=True
  • cli_implicit_flags=True
  • cli_kebab_case=True

异步 CLI 命令

Pydantic settings 支持通过 CliApp.runCliApp.run_subcommand 运行异步 CLI 命令。 借助此功能,您可以在 Pydantic 模型(包括子命令)中定义 async def 方法,并像执行同步方法一样执行它们。 具体来说:

  1. 支持异步方法:您现在可以将您的 cli_cmd 或类似的 CLI 入口点方法标记为 async def,并让 CliApp 执行它们。
  2. 子命令也可能是异步的:如果您有嵌套的 CLI 子命令,则最终(最低级别)子命令方法同样可以是异步的。
  3. 将异步方法限制为最终子命令:不建议将父命令定义为异步,因为它可能导致创建额外的线程和事件循环。 为了获得最佳性能并避免不必要的资源使用,仅将您最深层(子级)的子命令实现为 async def。

以下是一个简单的示例,演示了异步顶级命令:

from pydantic_settings import BaseSettings, CliApp


class AsyncSettings(BaseSettings):
    async def cli_cmd(self) -> None:
        print('Hello from an async CLI method!')
        #> Hello from an async CLI method!


# If an event loop is already running, a new thread will be used;
# otherwise, asyncio.run() is used to execute this async method.
assert CliApp.run(AsyncSettings, cli_args=[]).model_dump() == {}

异步子命令

如上所述,您也可以将子命令定义为异步。 但是,仅对叶(最低级别)子命令执行此操作,以避免在父命令中不必要地生成新线程和事件循环。

from pydantic import BaseModel

from pydantic_settings import (
    BaseSettings,
    CliApp,
    CliPositionalArg,
    CliSubCommand,
)


class Clone(BaseModel):
    repository: CliPositionalArg[str]
    directory: CliPositionalArg[str]

    async def cli_cmd(self) -> None:
        # Perform async tasks here, e.g. network or I/O operations
        print(f'Cloning async from "{self.repository}" into "{self.directory}"')
        #> Cloning async from "repo" into "dir"


class Git(BaseSettings):
    clone: CliSubCommand[Clone]

    def cli_cmd(self) -> None:
        # Run the final subcommand (clone/init). It is recommended to define async methods only at the deepest level.
        CliApp.run_subcommand(self)


CliApp.run(Git, cli_args=['clone', 'repo', 'dir']).model_dump() == {
    'repository': 'repo',
    'directory': 'dir',
}

当执行带有异步 cli_cmd 的子命令时,Pydantic settings 会自动检测当前线程是否已具有活动的事件循环。 如果是这样,则异步命令将在新线程中运行以避免冲突。 否则,它将在当前线程中使用 asyncio.run()。 此处理确保您的异步子命令“只是工作”,而无需额外的手动设置。

互斥组

可以通过继承 CliMutuallyExclusiveGroup 类来创建 CLI 互斥组。

注意

CliMutuallyExclusiveGroup 不能在联合中使用,也不能包含嵌套模型。

from typing import Optional

from pydantic import BaseModel

from pydantic_settings import CliApp, CliMutuallyExclusiveGroup, SettingsError


class Circle(CliMutuallyExclusiveGroup):
    radius: Optional[float] = None
    diameter: Optional[float] = None
    perimeter: Optional[float] = None


class Settings(BaseModel):
    circle: Circle


try:
    CliApp.run(
        Settings,
        cli_args=['--circle.radius=1', '--circle.diameter=2'],
        cli_exit_on_error=False,
    )
except SettingsError as e:
    print(e)
    """
    error parsing CLI: argument --circle.diameter: not allowed with argument --circle.radius
    """

自定义 CLI 体验

可以使用以下标志来根据您的需要自定义 CLI 体验。

更改显示的程序名称

通过设置 cli_prog_name 更改帮助文本用法中显示的默认程序名称。 默认情况下,它将像 argparse 一样,从 sys.argv[0] 派生当前正在执行的程序的名称。

import sys

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_prog_name='appdantic'):
    pass


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: appdantic [-h]

options:
  -h, --help  show this help message and exit
"""

CLI 布尔标志

使用 cli_implicit_flags 设置更改布尔字段默认应为显式还是隐式。 默认情况下,布尔字段是“显式”的,这意味着必须在 CLI 上显式提供布尔值,例如 --flag=True。 相反,“隐式”布尔字段从标志本身派生值,例如 --flag,--no-flag,这消除了传递显式值的需要。

此外,提供的 CliImplicitFlagCliExplicitFlag 注解可以在必要时用于更精细的控制。

from pydantic_settings import BaseSettings, CliExplicitFlag, CliImplicitFlag


class ExplicitSettings(BaseSettings, cli_parse_args=True):
    """Boolean fields are explicit by default."""

    explicit_req: bool
    """
    --explicit_req bool   (required)
    """

    explicit_opt: bool = False
    """
    --explicit_opt bool   (default: False)
    """

    # Booleans are explicit by default, so must override implicit flags with annotation
    implicit_req: CliImplicitFlag[bool]
    """
    --implicit_req, --no-implicit_req (required)
    """

    implicit_opt: CliImplicitFlag[bool] = False
    """
    --implicit_opt, --no-implicit_opt (default: False)
    """


class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=True):
    """With cli_implicit_flags=True, boolean fields are implicit by default."""

    # Booleans are implicit by default, so must override explicit flags with annotation
    explicit_req: CliExplicitFlag[bool]
    """
    --explicit_req bool   (required)
    """

    explicit_opt: CliExplicitFlag[bool] = False
    """
    --explicit_opt bool   (default: False)
    """

    implicit_req: bool
    """
    --implicit_req, --no-implicit_req (required)
    """

    implicit_opt: bool = False
    """
    --implicit_opt, --no-implicit_opt (default: False)
    """

忽略未知参数

使用 cli_ignore_unknown_args 更改是否忽略未知的 CLI 参数并仅解析已知的参数。 默认情况下,CLI 不会忽略任何参数。

import sys

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_ignore_unknown_args=True):
    good_arg: str


sys.argv = ['example.py', '--bad-arg=bad', 'ANOTHER_BAD_ARG', '--good_arg=hello world']
print(Settings().model_dump())
#> {'good_arg': 'hello world'}

CLI 烤串命名法参数

通过启用 cli_kebab_case 更改 CLI 参数是否应使用烤串命名法。

import sys

from pydantic import Field

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_kebab_case=True):
    my_option: str = Field(description='will show as kebab case on CLI')


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: example.py [-h] [--my-option str]

options:
  -h, --help       show this help message and exit
  --my-option str  will show as kebab case on CLI (required)
"""

更改 CLI 是否应在错误时退出

通过使用 cli_exit_on_error 更改 CLI 内部解析器是否会在错误时退出或引发 SettingsError 异常。 默认情况下,CLI 内部解析器将在错误时退出。

import sys

from pydantic_settings import BaseSettings, SettingsError


class Settings(BaseSettings, cli_parse_args=True, cli_exit_on_error=False): ...


try:
    sys.argv = ['example.py', '--bad-arg']
    Settings()
except SettingsError as e:
    print(e)
    #> error parsing CLI: unrecognized arguments: --bad-arg

在 CLI 强制执行必需参数

Pydantic settings 旨在在实例化模型时从各种来源提取值。 这意味着,从任何单个来源(例如 CLI)来看,必需字段不是严格必需的。 相反,重要的是其中一个来源提供了必需的值。

但是,如果您的用例更符合 #2,即使用 Pydantic 模型定义 CLI,您可能希望必需字段在 CLI 上是严格必需的。 我们可以通过使用 cli_enforce_required 来启用此行为。

注意

必需的 CliPositionalArg 字段始终在 CLI 上严格要求(强制执行)。

import os
import sys

from pydantic import Field

from pydantic_settings import BaseSettings, SettingsError


class Settings(
    BaseSettings,
    cli_parse_args=True,
    cli_enforce_required=True,
    cli_exit_on_error=False,
):
    my_required_field: str = Field(description='a top level required field')


os.environ['MY_REQUIRED_FIELD'] = 'hello from environment'

try:
    sys.argv = ['example.py']
    Settings()
except SettingsError as e:
    print(e)
    #> error parsing CLI: the following arguments are required: --my_required_field

更改 None 类型解析字符串

通过设置 cli_parse_none_str 更改将解析为 None 的 CLI 字符串值(例如 “null”、“void”、“None” 等)。 默认情况下,如果设置了 env_parse_none_str 值,它将使用该值。 否则,如果 cli_avoid_jsonFalse,则默认为 “null”,如果 cli_avoid_jsonTrue,则默认为 “None”。

import sys
from typing import Optional

from pydantic import Field

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_parse_none_str='void'):
    v1: Optional[int] = Field(description='the top level v0 option')


sys.argv = ['example.py', '--v1', 'void']
print(Settings().model_dump())
#> {'v1': None}

隐藏 None 类型值

通过启用 cli_hide_none_type 从 CLI 帮助文本中隐藏 None 值。

import sys
from typing import Optional

from pydantic import Field

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_hide_none_type=True):
    v0: Optional[str] = Field(description='the top level v0 option')


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: example.py [-h] [--v0 str]

options:
  -h, --help  show this help message and exit
  --v0 str    the top level v0 option (required)
"""

避免添加 JSON CLI 选项

通过启用 cli_avoid_json 避免在 CLI 上添加导致 JSON 字符串的复杂字段。

import sys

from pydantic import BaseModel, Field

from pydantic_settings import BaseSettings


class SubModel(BaseModel):
    v1: int = Field(description='the sub model v1 option')


class Settings(BaseSettings, cli_parse_args=True, cli_avoid_json=True):
    sub_model: SubModel = Field(
        description='The help summary for SubModel related options'
    )


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: example.py [-h] [--sub_model.v1 int]

options:
  -h, --help          show this help message and exit

sub_model options:
  The help summary for SubModel related options

  --sub_model.v1 int  the sub model v1 option (required)
"""

使用类文档字符串作为组帮助文本

默认情况下,在填充嵌套模型的组帮助文本时,它将从字段描述中提取。 或者,我们也可以配置 CLI 设置以从类文档字符串中提取。

注意

如果字段是嵌套模型的联合,则组帮助文本将始终从字段描述中提取; 即使 cli_use_class_docs_for_groups 设置为 True 也是如此。

import sys

from pydantic import BaseModel, Field

from pydantic_settings import BaseSettings


class SubModel(BaseModel):
    """The help text from the class docstring."""

    v1: int = Field(description='the sub model v1 option')


class Settings(BaseSettings, cli_parse_args=True, cli_use_class_docs_for_groups=True):
    """My application help text."""

    sub_model: SubModel = Field(description='The help text from the field description')


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int]

My application help text.

options:
  -h, --help          show this help message and exit

sub_model options:
  The help text from the class docstring.

  --sub_model JSON    set sub_model from JSON string
  --sub_model.v1 int  the sub model v1 option (required)
"""

更改 CLI 标志前缀字符

通过设置 cli_flag_prefix_char 更改 CLI 可选参数中使用的 CLI 标志前缀字符。

import sys

from pydantic import AliasChoices, Field

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_flag_prefix_char='+'):
    my_arg: str = Field(validation_alias=AliasChoices('m', 'my-arg'))


sys.argv = ['example.py', '++my-arg', 'hi']
print(Settings().model_dump())
#> {'my_arg': 'hi'}

sys.argv = ['example.py', '+m', 'hi']
print(Settings().model_dump())
#> {'my_arg': 'hi'}

从 CLI 帮助文本中抑制字段

要从 CLI 帮助文本中抑制字段,可以将 CliSuppress 注解用于字段类型,或者将 CLI_SUPPRESS 字符串常量用于字段描述。

import sys

from pydantic import Field

from pydantic_settings import CLI_SUPPRESS, BaseSettings, CliSuppress


class Settings(BaseSettings, cli_parse_args=True):
    """Suppress fields from CLI help text."""

    field_a: CliSuppress[int] = 0
    field_b: str = Field(default=1, description=CLI_SUPPRESS)


try:
    sys.argv = ['example.py', '--help']
    Settings()
except SystemExit as e:
    print(e)
    #> 0
"""
usage: example.py [-h]

Suppress fields from CLI help text.

options:
  -h, --help          show this help message and exit
"""

与现有解析器集成

可以通过使用用户定义的 CLI 设置源(指定 root_parser 对象)覆盖默认的 CLI 设置源,从而将 CLI 设置源与现有解析器集成。

import sys
from argparse import ArgumentParser

from pydantic_settings import BaseSettings, CliApp, CliSettingsSource

parser = ArgumentParser()
parser.add_argument('--food', choices=['pear', 'kiwi', 'lime'])


class Settings(BaseSettings):
    name: str = 'Bob'


# Set existing `parser` as the `root_parser` object for the user defined settings source
cli_settings = CliSettingsSource(Settings, root_parser=parser)

# Parse and load CLI settings from the command line into the settings source.
sys.argv = ['example.py', '--food', 'kiwi', '--name', 'waldo']
s = CliApp.run(Settings, cli_settings_source=cli_settings)
print(s.model_dump())
#> {'name': 'waldo'}

# Load CLI settings from pre-parsed arguments. i.e., the parsing occurs elsewhere and we
# just need to load the pre-parsed args into the settings source.
parsed_args = parser.parse_args(['--food', 'kiwi', '--name', 'ralph'])
s = CliApp.run(Settings, cli_args=parsed_args, cli_settings_source=cli_settings)
print(s.model_dump())
#> {'name': 'ralph'}

CliSettingsSource 通过使用解析器方法将 settings_cls 字段添加为命令行参数来与 root_parser 对象连接。 CliSettingsSource 内部解析器表示形式基于 argparse 库,因此,需要支持与其 argparse 对等项相同属性的解析器方法。 可以自定义的可用解析器方法及其 argparse 对等项(默认值)如下所示:

  • parse_args_method - (argparse.ArgumentParser.parse_args)
  • add_argument_method - (argparse.ArgumentParser.add_argument)
  • add_argument_group_method - (argparse.ArgumentParser.add_argument_group)
  • add_parser_method - (argparse._SubParsersAction.add_parser)
  • add_subparsers_method - (argparse.ArgumentParser.add_subparsers)
  • formatter_class - (argparse.RawDescriptionHelpFormatter)

对于非 argparse 解析器,如果不支持解析器方法,则可以将解析器方法设置为 None。 仅当解析器方法是必需的但设置为 None 时,CLI 设置才会在连接到根解析器时引发错误。

注意

formatter_class 仅应用于子命令。 CliSettingsSource 永远不会触及或修改任何外部解析器设置,以避免破坏性更改。 由于子命令驻留在它们自己的内部解析器树上,因此我们可以安全地应用 formatter_class 设置,而不会破坏外部解析器逻辑。

密钥

将密钥值放在文件中是为应用程序提供敏感配置的常见模式。

密钥文件遵循与 dotenv 文件相同的原则,不同之处在于它仅包含单个值,并且文件名用作键。 密钥文件看起来像这样:

/var/run/database_password
super_secret_database_password

一旦您拥有了密钥文件,pydantic 支持两种加载方式:

  1. BaseSettings 类中的 model_config 上设置 secrets_dir 到您的密钥文件存储的目录。
    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    
    class Settings(BaseSettings):
        model_config = SettingsConfigDict(secrets_dir='/var/run')
    
        database_password: str
    
  2. 使用 _secrets_dir 关键字参数实例化 BaseSettings 派生类。
    settings = Settings(_secrets_dir='/var/run')
    

在任何一种情况下,传递的参数值都可以是任何有效目录,绝对路径或相对于当前工作目录的相对路径。 **请注意,不存在的目录只会生成警告**。 从那里,pydantic 将为您处理一切,加载您的变量并验证它们。

即使在使用密钥目录时,pydantic 仍将从 dotenv 文件或环境中读取环境变量,**dotenv 文件和环境变量将始终优先于从密钥目录加载的值**。

通过实例化时(方法 2)的 _secrets_dir 关键字参数传递文件路径将覆盖在 model_config 类上设置的值(如果有)。

如果您需要从多个密钥目录加载设置,则可以将多个路径作为元组或列表传递。 就像 env_file 一样,来自后续路径的值会覆盖先前路径的值。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # files in '/run/secrets' take priority over '/var/run'
    model_config = SettingsConfigDict(secrets_dir=('/var/run', '/run/secrets'))

    database_password: str

如果缺少任何 secrets_dir,则会将其忽略,并显示警告。 如果任何 secrets_dir 是文件,则会引发错误。

用例:Docker 密钥

Docker 密钥可用于为在 Docker 容器中运行的应用程序提供敏感配置。 要在 pydantic 应用程序中使用这些密钥,过程很简单。 有关在 Docker 中创建、管理和使用密钥的更多信息,请参见官方 Docker 文档

首先,使用指定密钥目录的 SettingsConfigDict 定义您的 Settings 类。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(secrets_dir='/run/secrets')

    my_secret_data: str

注意

默认情况下,Docker 使用 /run/secrets 作为目标挂载点。 如果要使用其他位置,请相应地更改 Config.secrets_dir

然后,通过 Docker CLI 创建您的密钥

printf "This is a secret" | docker secret create my_secret_data -

最后,在 Docker 容器中运行您的应用程序并提供您新创建的密钥

docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest

AWS Secrets Manager

您必须设置一个参数

  • secret_id:AWS 密钥 ID

您必须在密钥的值中以及字段名称中采用相同的命名约定。例如,如果密钥中的键名为 SqlServerPassword,则字段名称必须相同。您也可以使用别名。

在 AWS Secrets Manager 中,嵌套模型通过键名中的 -- 分隔符来支持。例如,SqlServer--Password

不支持数组(例如,MySecret--0MySecret--1)。

import os

from pydantic import BaseModel

from pydantic_settings import (
    AWSSecretsManagerSettingsSource,
    BaseSettings,
    PydanticBaseSettingsSource,
)


class SubModel(BaseModel):
    a: str


class AWSSecretsManagerSettings(BaseSettings):
    foo: str
    bar: int
    sub: SubModel

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        aws_secrets_manager_settings = AWSSecretsManagerSettingsSource(
            settings_cls,
            os.environ['AWS_SECRETS_MANAGER_SECRET_ID'],
        )
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            file_secret_settings,
            aws_secrets_manager_settings,
        )

Azure Key Vault

您必须设置两个参数

  • url:例如,https://my-resource.vault.azure.net/
  • credential:如果您使用 DefaultAzureCredential,在本地您可以执行 az login 来获取您的身份凭证。身份必须具有角色分配(推荐的角色是 Key Vault Secrets User),以便您可以访问密钥。

您必须在字段名称中以及 Key Vault 密钥名称中采用相同的命名约定。例如,如果密钥名为 SqlServerPassword,则字段名称必须相同。您也可以使用别名。

在 Key Vault 中,嵌套模型通过 -- 分隔符来支持。例如,SqlServer--Password

Key Vault 数组(例如,MySecret--0MySecret--1)不受支持。

import os

from azure.identity import DefaultAzureCredential
from pydantic import BaseModel

from pydantic_settings import (
    AzureKeyVaultSettingsSource,
    BaseSettings,
    PydanticBaseSettingsSource,
)


class SubModel(BaseModel):
    a: str


class AzureKeyVaultSettings(BaseSettings):
    foo: str
    bar: int
    sub: SubModel

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        az_key_vault_settings = AzureKeyVaultSettingsSource(
            settings_cls,
            os.environ['AZURE_KEY_VAULT_URL'],
            DefaultAzureCredential(),
        )
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            file_secret_settings,
            az_key_vault_settings,
        )

其他设置源

其他设置源可用于常见的配置文件

  • 使用 json_filejson_file_encoding 参数的 JsonConfigSettingsSource
  • 使用 (可选) pyproject_toml_depth(可选) pyproject_toml_table_header 参数的 PyprojectTomlConfigSettingsSource
  • 使用 toml_file 参数的 TomlConfigSettingsSource
  • 使用 yaml_file 和 yaml_file_encoding 参数的 YamlConfigSettingsSource

您还可以通过提供路径列表来提供多个文件

toml_file = ['config.default.toml', 'config.custom.toml']
要使用它们,您可以使用 此处 描述的相同机制

from pydantic import BaseModel

from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
    TomlConfigSettingsSource,
)


class Nested(BaseModel):
    nested_field: str


class Settings(BaseSettings):
    foobar: str
    nested: Nested
    model_config = SettingsConfigDict(toml_file='config.toml')

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (TomlConfigSettingsSource(settings_cls),)

这将能够读取位于您的工作目录中的以下 “config.toml” 文件

foobar = "Hello"
[nested]
nested_field = "world!"

pyproject.toml

“pyproject.toml” 是一个标准化的文件,用于在 Python 项目中提供配置值。PEP 518 定义了一个 [tool] 表,可用于提供任意工具配置。虽然鼓励使用 [tool] 表,但 PyprojectTomlConfigSettingsSource 可用于从 “pyproject.toml” 文件中的任何位置加载变量。

这通过提供 SettingsConfigDict(pyproject_toml_table_header=tuple[str, ...]) 来控制,其中该值是标头部分的元组。默认情况下,pyproject_toml_table_header=('tool', 'pydantic-settings'),这将从 [tool.pydantic-settings] 表加载变量。

from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    PyprojectTomlConfigSettingsSource,
    SettingsConfigDict,
)


class Settings(BaseSettings):
    """Example loading values from the table used by default."""

    field: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (PyprojectTomlConfigSettingsSource(settings_cls),)


class SomeTableSettings(Settings):
    """Example loading values from a user defined table."""

    model_config = SettingsConfigDict(
        pyproject_toml_table_header=('tool', 'some-table')
    )


class RootSettings(Settings):
    """Example loading values from the root of a pyproject.toml file."""

    model_config = SettingsConfigDict(extra='ignore', pyproject_toml_table_header=())

这将能够读取位于您的工作目录中的以下 “pyproject.toml” 文件,从而得到 Settings(field='default-table')SomeTableSettings(field='some-table')RootSettings(field='root')

field = "root"

[tool.pydantic-settings]
field = "default-table"

[tool.some-table]
field = "some-table"

默认情况下,PyprojectTomlConfigSettingsSource 将仅在您当前的工作目录中查找 “pyproject.toml”。但是,有两种选项可以更改此行为。

  • 可以提供 SettingsConfigDict(pyproject_toml_depth=<int>) 以检查目录树中向上 <int> 个目录的数量,以查找 “pyproject.toml”(如果在当前工作目录中未找到)。默认情况下,不检查父目录。
  • 显式文件路径可以在实例化源时提供给它(例如,PyprojectTomlConfigSettingsSource(settings_cls, Path('~/.config').resolve() / 'pyproject.toml'))。如果以这种方式提供文件路径,它将被视为绝对路径(不检查其他位置)。
from pathlib import Path

from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    PyprojectTomlConfigSettingsSource,
    SettingsConfigDict,
)


class DiscoverSettings(BaseSettings):
    """Example of discovering a pyproject.toml in parent directories in not in `Path.cwd()`."""

    model_config = SettingsConfigDict(pyproject_toml_depth=2)

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (PyprojectTomlConfigSettingsSource(settings_cls),)


class ExplicitFilePathSettings(BaseSettings):
    """Example of explicitly providing the path to the file to load."""

    field: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (
            PyprojectTomlConfigSettingsSource(
                settings_cls, Path('~/.config').resolve() / 'pyproject.toml'
            ),
        )

字段值优先级

在为同一 Settings 字段以多种方式指定值的情况下,选定的值按以下方式确定(按优先级降序排列)

  1. 如果启用 cli_parse_args,则为在 CLI 中传入的参数。
  2. 传递给 Settings 类初始化程序的参数。
  3. 环境变量,例如上面描述的 my_prefix_special_function
  4. 从 dotenv (.env) 文件加载的变量。
  5. 从 secrets 目录加载的变量。
  6. Settings 模型的默认字段值。

自定义设置源

如果默认的优先级顺序不符合您的需求,可以通过覆盖 Settingssettings_customise_sources 方法来更改它。

settings_customise_sources 接受四个可调用对象作为参数,并返回任意数量的可调用对象作为元组。反过来,调用这些可调用对象来构建设置类的字段的输入。

每个可调用对象都应将设置类的实例作为其唯一参数,并返回一个 dict

更改优先级

返回的可调用对象的顺序决定了输入的优先级;第一项是最高优先级。

from pydantic import PostgresDsn

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource


class Settings(BaseSettings):
    database_dsn: PostgresDsn

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return env_settings, init_settings, file_secret_settings


print(Settings(database_dsn='postgres://postgres@localhost:5432/kwargs_db'))
#> database_dsn=PostgresDsn('postgres://postgres@localhost:5432/kwargs_db')

通过翻转 env_settingsinit_settings,环境变量现在比 __init__ kwargs 具有更高的优先级。

添加源

如前所述,pydantic 附带多个内置设置源。但是,您有时可能需要添加自己的自定义源,settings_customise_sources 使这变得非常容易

import json
from pathlib import Path
from typing import Any

from pydantic.fields import FieldInfo

from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)


class JsonConfigSettingsSource(PydanticBaseSettingsSource):
    """
    A simple settings source class that loads variables from a JSON file
    at the project's root.

    Here we happen to choose to use the `env_file_encoding` from Config
    when reading `config.json`
    """

    def get_field_value(
        self, field: FieldInfo, field_name: str
    ) -> tuple[Any, str, bool]:
        encoding = self.config.get('env_file_encoding')
        file_content_json = json.loads(
            Path('tests/example_test_config.json').read_text(encoding)
        )
        field_value = file_content_json.get(field_name)
        return field_value, field_name, False

    def prepare_field_value(
        self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
    ) -> Any:
        return value

    def __call__(self) -> dict[str, Any]:
        d: dict[str, Any] = {}

        for field_name, field in self.settings_cls.model_fields.items():
            field_value, field_key, value_is_complex = self.get_field_value(
                field, field_name
            )
            field_value = self.prepare_field_value(
                field_name, field, field_value, value_is_complex
            )
            if field_value is not None:
                d[field_key] = field_value

        return d


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file_encoding='utf-8')

    foobar: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (
            init_settings,
            JsonConfigSettingsSource(settings_cls),
            env_settings,
            file_secret_settings,
        )


print(Settings())
#> foobar='test'

访问先前源的结果

每个设置源都可以访问先前源的输出。

from typing import Any

from pydantic.fields import FieldInfo

from pydantic_settings import PydanticBaseSettingsSource


class MyCustomSource(PydanticBaseSettingsSource):
    def get_field_value(
        self, field: FieldInfo, field_name: str
    ) -> tuple[Any, str, bool]: ...

    def __call__(self) -> dict[str, Any]:
        # Retrieve the aggregated settings from previous sources
        current_state = self.current_state
        current_state.get('some_setting')

        # Retrive settings from all sources individually
        # self.settings_sources_data["SettingsSourceName"]: dict[str, Any]
        settings_sources_data = self.settings_sources_data
        settings_sources_data['SomeSettingsSource'].get('some_setting')

        # Your code here...

删除源

您可能还想禁用源

from pydantic import ValidationError

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource


class Settings(BaseSettings):
    my_api_key: str

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        # here we choose to ignore arguments from init_settings
        return env_settings, file_secret_settings


try:
    Settings(my_api_key='this is ignored')
except ValidationError as exc_info:
    print(exc_info)
    """
    1 validation error for Settings
    my_api_key
      Field required [type=missing, input_value={}, input_type=dict]
        For further information visit https://errors.pydantic.dev/2/v/missing
    """

就地重新加载

如果您想就地重新加载现有设置,您可以使用其 __init__ 方法来执行此操作

import os

from pydantic import Field

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    foo: str = Field('foo')


mutable_settings = Settings()

print(mutable_settings.foo)
#> foo

os.environ['foo'] = 'bar'
print(mutable_settings.foo)
#> foo

mutable_settings.__init__()
print(mutable_settings.foo)
#> bar

os.environ.pop('foo')
mutable_settings.__init__()
print(mutable_settings.foo)
#> foo