Skip to content

配置

Starlette 鼓励将配置与代码严格分离,遵循 十二因素应用模式.

配置应存储在环境变量中,或者在一个不提交到源代码管理的 .env 文件中。

main.py
from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette.config import Config
from starlette.datastructures import CommaSeparatedStrings, Secret

# 配置将从环境变量和/或 ".env" 文件中读取。
config = Config(".env")

DEBUG = config('DEBUG', cast=bool, default=False)
DATABASE_URL = config('DATABASE_URL')
SECRET_KEY = config('SECRET_KEY', cast=Secret)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeparatedStrings)

app = Starlette(debug=DEBUG)
engine = create_engine(DATABASE_URL)
...
.env
# 请不要将此文件提交到源代码管理中。
# 例如,将 ".env" 包含在 `.gitignore` 文件中。
DEBUG=True
DATABASE_URL=postgresql://user:password@localhost:5432/database
SECRET_KEY=43n080musdfjt54t-09sdgr
ALLOWED_HOSTS=127.0.0.1, localhost

配置优先级

配置值读取的顺序如下:

  • 从环境变量中读取。
  • .env 文件中读取。
  • config 中给出的默认值读取。

如果这些都没有匹配到,config(...) 会引发错误。

秘密

对于敏感密钥,Secret 类非常有用,因为它有助于减少值在回溯或其他代码 introspection 时泄漏的机会。

要获取 Secret 实例的值,您必须显式将其转换为字符串。应该仅在使用该值的地方进行转换。

>>> from myproject import settings
>>> settings.SECRET_KEY
Secret('**********')
>>> str(settings.SECRET_KEY)
'98n349$%8b8-7yjn0n8y93T$23r'

提示

您可以使用 databases 包中的 DatabaseURL 这里 来存储数据库 URL,并避免它们在日志中泄漏。

CommaSeparatedStrings

对于在单个配置键中存储多个值,CommaSeparatedStrings 类型非常有用。

>>> from myproject import settings
>>> print(settings.ALLOWED_HOSTS)
CommaSeparatedStrings(['127.0.0.1', 'localhost'])
>>> print(list(settings.ALLOWED_HOSTS))
['127.0.0.1', 'localhost']
>>> print(len(settings.ALLOWED_HOSTS))
2
>>> print(settings.ALLOWED_HOSTS[0])
'127.0.0.1'

读取或修改环境变量

在某些情况下,您可能需要以编程方式读取或修改环境变量。这在测试中尤其有用,您可能希望覆盖环境中的某些键。

与其直接从 os.environ 读取或写入,您应使用 Starlette 的 environ 实例。这个实例是对标准 os.environ 的映射,它通过在读取配置后如果有任何环境变量被设置时抛出错误,额外为您提供保护。

如果您正在使用 pytest,则可以在 tests/conftest.py 中设置任何初始环境。

tests/conftest.py
from starlette.config import environ

environ['DEBUG'] = 'TRUE'

读取带前缀的环境变量

您可以通过设置 env_prefix 参数来为环境变量指定命名空间。

myproject/settings.py
import os

from starlette.config import Config

os.environ['APP_DEBUG'] = 'yes'
os.environ['ENVIRONMENT'] = 'dev'

config = Config(env_prefix='APP_')

DEBUG = config('DEBUG')  # 查找 APP_DEBUG,返回 "yes"
ENVIRONMENT = config('ENVIRONMENT')  # 查找 APP_ENVIRONMENT,抛出 KeyError,因为变量未定义

完整示例

构建大型应用程序可能很复杂。您需要适当分离配置和代码、在测试期间进行数据库隔离、分开测试和生产数据库等...

在这里,我们将展示一个完整的示例,演示如何开始构建应用程序的结构。

首先,让我们将设置、数据库表定义和应用程序逻辑分开:

myproject/settings.py
from starlette.config import Config
from starlette.datastructures import Secret

config = Config(".env")

DEBUG = config('DEBUG', cast=bool, default=False)
SECRET_KEY = config('SECRET_KEY', cast=Secret)

DATABASE_URL = config('DATABASE_URL')
myproject/tables.py
import sqlalchemy

# 数据库表定义
metadata = sqlalchemy.MetaData()

organisations = sqlalchemy.Table(
    ...
)
myproject/app.py
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.routing import Route

from myproject import settings


async def homepage(request):
    ...

routes = [
    Route("/", endpoint=homepage)
]

middleware = [
    Middleware(
        SessionMiddleware,
        secret_key=settings.SECRET_KEY,
    )
]

app = Starlette(debug=settings.DEBUG, routes=routes, middleware=middleware)

接下来处理我们的测试配置。 我们希望在每次运行测试套件时创建一个新的测试数据库,并在测试完成后删除它。我们还希望确保:

tests/conftest.py
from starlette.config import environ
from starlette.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy_utils import create_database, database_exists, drop_database

# 如果在导入 'settings' 后使用这行代码,它将抛出错误。
environ['DEBUG'] = 'TRUE'

from myproject import settings
from myproject.app import app
from myproject.tables import metadata


@pytest.fixture(autouse=True, scope="session")
def setup_test_database():
    """
    每次运行测试时创建一个干净的测试数据库。
    """
    url = settings.DATABASE_URL
    engine = create_engine(url)
    assert not database_exists(url), '测试数据库已经存在,终止测试。'
    create_database(url)             # 创建测试数据库。
    metadata.create_all(engine)      # 创建表。
    yield                            # 运行测试。
    drop_database(url)               # 删除测试数据库。


@pytest.fixture()
def client():
    """
    为测试用例提供一个 'client' 固定装置。
    """
    # 我们的固定装置在上下文管理器内创建。这确保了
    # 应用程序的生命周期在每个测试用例中都会运行。
    with TestClient(app) as test_client:
        yield test_client