在一个完整的 Python 项目生命周期中,通常涉及本地开发环境、测试环境以及生产环境。其中,本地开发环境往往因开发者个人习惯或系统差异而存在不同配置;而测试与生产环境则多为团队共享,配置要求相对统一且稳定。在这种情况下,如何实现项目在不同环境(服务器)之间部署时,无需手动修改配置文件呢?
方式一:dynaconf
dynaconf(https://www.dynaconf.com/) 支持多种配置文件格式,如 TOML、YAML、JSON、INI、Python、Dotenv 等,并且支持动态加载配置。
目录结构如下:
|——config
| .secrets.toml
| dev.toml
| dev.local.toml
| test.toml
| settings.toml
| online.toml
| __init__.py
| .gitignore
settings.toml
[global] # 全局配置,优先级高于默认配置
TABLE_PREFIX = "wd_"
[default] # 默认配置,优先级最低
TABLE_PREFIX = "df_"
LOG_LEVEL = "DEBUG"
db_url = "@format {this.db_config.host}:{this.db_config.port}"
dev.toml
[dev]
TABLE_PREFIX = "dev_" # 环境配置,优先级高于全局配置
LOG_LEVEL = "DEBUG"
DEBUG = true
# 第一种方式配置嵌套对象
[dev.db_config]
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"
test.toml
[test]
TABLE_PREFIX="test_"
LOG_LEVEL = "INFO"
DEBUG = false
# 第二种方式配置嵌套对象
db_config.host = "172.168.1.105"
db_config.port = 3306
db_config.name = "toml"
db_config.user = "test"
db_config.pwd = "xxxxx"
__init__.py
from dynaconf import Dynaconf
Settings = Dynaconf(
# 环境变量的前缀,比如你有一个配置项为 DEBUG,那么可以通过设置环境变量 WDADMIN_DEBUG = True 来设置 DEBUG 的值
envvar_prefix="WDADMIN",
# 配置文件的根路径
root_path="",
settings_files=['settings.toml', 'dev.toml', 'test.toml', '.secrets.toml'],
# 使用分层的环境配置,为True时 default、global等分层的配置才生效
environments=True,
# 分层环境默认使用的配置,默认为 development
env="dev", # env 对应的环境变量 ENV_FOR_DYNACONF,如设置环境变量 ENV_FOR_DYNACONF=dev
# 修改切换env的环境变量名,默认是 ENV_FOR_DYNACONF,现在改为 ENV_WDADMIN
env_switcher="ENV_WDADMIN", # env_switcher 对应的环境变量 ENVVAR_SWITCHER_FOR_DYNACONF
# 是否加载.env文件
load_dotenv=False,
)
使用方式如下:
from config import settings
import os
# 通过环境变量设置某个配置项
# os.environ["WDADMIN_DEBUG"] = True
# 使用不同的环境配置:开发、测试、生产环境
os.environ["ENV_WDADMIN"] = "test"
print(f"{settings.TABLE_PREFIX=}")
print(f"{settings.LOG_LEVEL=}")
print(f"{settings.db_config=}")
print(f"{settings.db_url=}")
## 输出结果:
## settings.TABLE_PREFIX='test_'
## settings.LOG_LEVEL='INFO'
## settings.db_config=<Box: {'host': '172.168.1.105', 'port': 3306, 'name': 'toml', 'user': 'test', 'pwd': 'xxxxx'}>
## settings.db_url='172.168.1.105:3306'
- 1. 多种配置方式,dynaconf后加载的配置文件会覆盖前面的配置,所以文件的加载顺序和优先级顺序是相反的。
- 2. 如果存在对应的
*.local.toml文件,dynaconf会自动加载,不需要写额外的代码。 - 3. 优先级: load_dotenv【.env文件】 > environments【环境变量】 > settings_files(local)【.local配置文件】 > settings_files【配置文件】
- 4. 事实上,不同环境的配置可以全部写在同一个配置文件中。
例如:settings.toml
[global] # 全局配置,优先级低于当前环境配置
TABLE_PREFIX = "wd_"
[default] # 默认配置,优先级低于全局配置
TABLE_PREFIX = "df_"
LOG_LEVEL = "DEBUG"
db_url = "@format {this.db_config.host}:{this.db_config.port}"
[dev]
TABLE_PREFIX = "dev_"
LOG_LEVEL = "DEBUG"
DEBUG = true
[dev.db_config] # 第一种方式配置嵌套对象
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"
[test]
TABLE_PREFIX="test_"
LOG_LEVEL = "DEBUG"
DEBUG = false
# 第二种方式配置嵌套对象
db_config.host = "172.168.1.105"
db_config.port = 3306
db_config.name = "toml"
db_config.user = "test"
db_config.pwd = "xxxxx"
方式二:python文件
目录结构如下:
|——config
│ dev.py
| dev_local.py
│ online.py
│ test.py
│ __init__.py
| .gitignore
dev.py
DEBUG = True
DB_CONF = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
test.py
DEBUG = False
DB_CONF = {
"ENGINE": 'django.db.backends.mysql',
'NAME': 'server',
'USER': 'root',
'PASSWORD': 'root123456',
'HOST': '127.0.0.1',
'PORT': '3306'
}
__init__.py
import os
ENV_NAME = "WDADMIN_ENV"
env = os.getenv(ENV_NAME)
if not env:
raise Exception(f"get {ENV_NAME} failed, set it IN CAUTION!!!")
elif env == "online":
from .online import *
elif env == "test":
from .test import *
elif env == "dev":
try:
from .dev_local import *
except ImportError:
from .dev import *
else:
raise Exception(f"{ENV_NAME} not in (online, test, dev)")
MEDIA_URL = "media/"
在需要配置项的地方直接导入即可,简单粗暴。这也是在这些配置库没出来前,我们团队一直使用的方式。
方式三:pydantic_settings
pydantic_settings(https://docs.pydantic.org.cn/latest/api/pydantic_settings/) 不仅可以对配置字段进行检查,并且可以使用.来提示访问属性,使用上更方便。其支持 .env、toml、yaml、json 等文件,并支持自定义扩展,比如从配置中心获取配置。
使用.env文件
目录结构大差不差:
|——settings
│ .env.dev
| .env.dev.local
│ .env.test
│ .env.online
│ __init__.py
| .gitignore
.env.dev
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=env
DB_USER=dev
DB_PWD=123456
APP_LOG_LEVEL=DEBUG
.env.test
DB_HOST=172.168.1.105
DB_PORT=3306
DB_NAME=env
DB_USER=test
DB_PWD=xxxxx
APP_LOG_LEVEL=INFO
__init__.py
import os
from pathlib import Path
from functools import partial
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, IPvAnyAddress, computed_field
def get_env_file():
env = os.getenv("MY_ENV", "dev")
file_map = {
"dev": ".env.dev",
"test": ".env.test",
"online": ".env.online",
}
file = file_map.get(env)
if file is None:
raise Exception(f"Unknow {env}")
cur_dir = Path(__file__).resolve().parent
if env == "dev" and (cur_dir / ".env.dev.local").exists():
return cur_dir / ".env.dev.local"
return cur_dir / file
# .env文件中配置项名称规范是全部大写
PSettingsConfigDict = partial(
SettingsConfigDict,
env_file=get_env_file(),
case_sensitive=False,
extra="ignore")
class DBConfig(BaseSettings):
host: IPvAnyAddress = "127.0.0.1"
port: int = Field(default=3306, gt=1000, lt=65535)
name: str = None
user: str = None
pwd: str = None
@computed_field
@property
def url(self) -> str:
return f"{self.host}:{self.port}@{self.user}:{self.pwd}"
model_config = PSettingsConfigDict(env_prefix="DB_")
class MySettings(BaseSettings):
APP_NAME: str = "MyAPP"
LOG_LEVEL: str = "INFO"
DB_CONFIG: DBConfig = DBConfig()
model_config = PSettingsConfigDict(env_prefix="APP_", case_sensitive=True)
Settings = MySettings()
用起来爽,就是代码量有点多。为防止变量名冲突,需要使用前缀来定义不同的配置项。
使用方式如下:
from settings import Settings
print(Settings.LOG_LEVEL)
print(Settings.DB_CONFIG.url)
print(Settings.DB_CONFIG.model_dump())
## 输出:
## DEBUG
## 127.0.0.1:3307@devlocal:123456
## {'host': IPv4Address('127.0.0.1'), 'port': 3307, 'name': 'env', 'user': 'devlocal', 'pwd': '123456', 'url': '127.0.0.1:3307@devlocal:123456'}
使用toml文件
目录结构如下:
|——settings
│ dev.toml
| dev.local.toml
│ test.toml
│ online.toml
│ __init__.py
|——.gitignore
dev.toml
LOG_LEVEL = "DEBUG"
DEBUG = true
[DB_CONFIG]
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"
test.toml
LOG_LEVEL = "DEBUG"
DEBUG = false
[DB_CONFIG]
host = "172.168.1.105"
port = 3306
name = "toml"
user = "test"
pwd = "xxxxx"
__init__.py
import os
from pathlib import Path
from pydantic_settings import (
BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource,
TomlConfigSettingsSource,
)
from pydantic import Field, IPvAnyAddress, computed_field, BaseModel
def get_toml_file():
env = os.getenv("MY_ENV", "dev")
file_map = {
"dev": "dev.toml",
"test": "test.toml",
"online": "online.toml",
}
file = file_map.get(env)
if file is None:
raise Exception(f"Unknow {env}")
cur_dir = Path(__file__).resolve().parent
if env == "dev" and (cur_dir / "dev.local.toml").exists():
return cur_dir / "dev.local.toml"
return cur_dir / file
class DBConfig(BaseModel):
host: IPvAnyAddress = None
port: int = Field(default=3306, gt=1000, lt=65535)
name: str = None
user: str = None
pwd: str = None
@computed_field
@property
def url(self) -> str:
return f"{self.host}:{self.port}@{self.user}:{self.pwd}"
class MySettings(BaseSettings):
APP_NAME: str = "MyAPP"
LOG_LEVEL: str = ""
DB_CONFIG: DBConfig = None
DEBUG: bool = False
model_config = SettingsConfigDict(
toml_file=get_toml_file(),
extra="ignore",
)
@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,
env_settings,
TomlConfigSettingsSource(settings_cls),
dotenv_settings,
file_secret_settings,
)
Settings = MySettings()
可以看到,利用toml文件的嵌套特性 (例如[DB_CONFIG]) 配置结构化对象比较简单。
总结
本质上是在不同的服务器中设置某个环境变量的不同值来加载不同的配置文件。此外,还要注意.gitignore 忽略掉密钥文件和.local文件。