使用文件强制转换和键入 env 变量

Cast and type env variables using file

对于我所有的项目,我在开始时加载所有环境变量,并检查所有预期的键是否存在,如 dotenv-safe approach.

之后的 .env.example 文件所描述的那样

但是,env 变量是字符串,无论何时在 Python 代码中使用它们都必须手动转换。这很烦人且容易出错。我想使用 .env.example 文件中的信息来转换 env 变量并在我的 IDE(VS 代码)中获得 Python 键入支持。我该怎么做?

env.example

PORT: int
SSL: boolean

Python理想行为

# Set the env in some way (doesn't matter)
import os
os.environment["SSL"] = "0"
os.environment["PORT"] = "99999"

env = type_env()
if not env["SSL"]: # <-- I'd like this to be cast to boolean and typed as a boolean
    print("Connecting w/o SSL!")
if 65535 < env["PORT"]:  # <-- I'd like this to be cast to int and typed as an int
    print("Invalid port!")

在此代码示例中,假设 type_env() 函数仅支持 booleanintfloatstr,它会是什么样子?

如示例中所示,进行转换并不难。 ,但我不清楚如何让它与输入支持一起工作。

鉴于我上面的评论,我建议您的配置文件采用以下格式:

settings.py、config.py、等等

from dotenv import load_dotenv
import os

load_dotenv()

SSL = os.getenv("SSL").lower() == 'true'
PORT = int(os.getenv("PORT", 5555)) # <-- can also set default

# Check all your other variables and expected keys here...

不需要type_env()函数,因为它全部在python文件中完成,可以在任何模块中导入。

现在无论你想要那些变量,只需导入它们,因为它们已经被转换为正确的类型。例如:

main.py

import config

if not config.SSL:
    print("Connecting w/o SSL!")
if 65535 < config.PORT:
    print("Invalid port!")

以上将起作用,因为所有转换都已在加载 .env 文件时完成。

这样做,你可以看到如果.env文件中使用的值不能转换为你期望的类型,配置就会失败。

以下解决方案提供了到所需类型的运行时转换编辑器提供的类型提示帮助不使用外部依赖.

另请检查 for an awesome alternative using pydantic,它会为您处理所有这些。


直接使用非Python dotenv 文件即使不是不可能,也会非常困难。处理某些 Python 数据结构中的所有信息要容易得多,因为这让类型检查器无需任何修改即可完成工作。

我认为要走的路是使用 Python dataclasses. Note that although we specify types in the definition, they are only for the type checkers, not enforced at runtime. This is a problem for environment variables, as they are external string mapping information basically. To overcome this, we can

实施

首先,出于代码组织的原因,我们可以创建一个具有类型强制逻辑的 Mixin。 请注意,bool 的情况很特殊,因为它的构造函数将为任何非空字符串输出 True,包括 "False"。如果您想要处理其他一些非内置类型,您也需要为它添加特殊处理(尽管我不建议让这个逻辑处理比这些简单类型更多)。

import dataclasses
from distutils.util import strtobool

class EnforcedDataclassMixin:

    def __post_init__(self):
        # Enforce types at runtime
        for field in dataclasses.fields(self):
            value = getattr(self, field.name)
            # Special case handling, since bool('False') is True
            if field.type == bool:
                value = strtobool(value)
            setattr(self, field.name, field.type(value))

此实现也可以使用装饰器完成,请参阅

然后,我们可以像这样创建一个“.env.example”文件的等价物:

import dataclasses

@dataclasses.dataclass
class EnvironmentVariables(EnforcedDataclassMixin):
    SSL: bool
    PORT: int
    DOMAIN: str

为了方便从 os.environ 解析,我们可以创建一个像

这样的函数
from typing import Mapping

def get_config_from_map(environment_map: Mapping) -> EnvironmentVariables:
    field_names = [field.name for field in dataclasses.fields(EnvironmentVariables)]
    # We need to ignore the extra keys in the environment,
    # otherwise the dataclass construction will fail.
    env_vars = {
        key: value for key, value in environment_map.items() if key in field_names
    }
    return EnvironmentVariables(**env_vars)


用法

最后,把这些东西放在一起,我们可以写在一个设置文件中:

import os
from env_description import get_config_from_map


env_vars = get_config_from_map(os.environ)

if 65535 < env_vars.PORT:
    print("Invalid port!")

if not env_vars.SSL:
    print("Connecting w/o SSL!")

静态类型检查在 VS Code 和 mypy 中正常工作。如果您将 PORT(这是一个 int)分配给类型为 str 的变量,您将收到一个警报!

为了假装它是一个字典,Python 在 dataclasses 模块中提供了 asdict 方法。

env_vars_dict = dataclasses.asdict(env_vars)
if 65535 < env_vars_dict['PORT']:
    print("Invalid port!")

但遗憾的是(截至本回答时)您这样做失去了静态类型检查支持。好像是work in progress for mypy.

  • 假设你的env.example是yaml格式(至少你写的是有效的yaml)
  • 并假设您安装了 PyYaml (pip install pyyaml)

... 那么下面的代码就可以工作了:

# do this or anything else to make a dict from your env.example
import yaml
example=yaml.safe_load("""
PORT: int
SSL: bool
""")

# the missing implementation
def type_env():
    env={}
    for k, v in os.environ.items():
        t=example.get(k)
        if t == "bool":
            env[k] = v.lower() not in ["false", "no", "0", ""] # whatever you want to consider as False
            # or env[k] = v.lower() in ["true", "yes", "1"] # whatever you want to consider as True
        elif t == "int":
            env[k] = int(v)
        elif t == "float":
            env[k] = float(v)
        else:
            env[k] = v
    return env

# From now on your code (exactly your code, except amending os.environment to os.environ)

# Set the env in some way (doesn't matter)
import os
os.environ["SSL"] = "0"
os.environ["PORT"] = "9999"

env = type_env()
if not env["SSL"]: # <-- I'd like this to be cast to boolean and typed as a boolean
    print("Connecting w/o SSL!")
if 65535 < env["PORT"]:  # <-- I'd like this to be cast to int and typed as an int
    print("Invalid port!")

我会建议使用 pydantic

来自 Whosebug pydantic tag info

Pydantic is a library for data validation and settings management based on Python type hinting (PEP484) and variable annotations (PEP526). It allows for defining schemas in Python for complex structures.

假设您有一个包含 SSLPORT 环境的文件:

with open('.env', 'w') as fp:
    fp.write('PORT=5000\nSSL=0')

那么你可以使用:

from pydantic import BaseSettings

class Settings(BaseSettings):
    PORT : int
    SSL : bool
    class Config:
        env_file = '.env'

config = Settings()

print(type(config.SSL),  config.SSL)
print(type(config.PORT),  config.PORT)
# <class 'bool'> False
# <class 'int'> 5000

使用您的代码:

env = Settings()

if not env.SSL:
    print("Connecting w/o SSL!")
if 65535 < env.PORT: 
    print("Invalid port!")

输出:

Connecting w/o SSL!

您有 2 个选择 - 显式指定变量类型,或者让 type_env 函数从实际值推断类型。其他评论者已经提供了如何使用显式类型的示例,我个人会使用 PORT = int(os.getenv("PORT", 5555))dataclass 方法,具体取决于您必须使用的变量数量。

但是,明确指定类型会增加一些开销。这是我的贡献如何推断。它不会让 mypy 知道确切的类型,它们都是 Any.

import os
from distutils.util import strtobool
from typing import Dict, Any

os.environ["SSL"] = "0"
os.environ["PORT"] = "99999"


def type_env() -> Dict[str, Any]:
    d: Dict[str, Any] = dict(os.environ)
    for key in d:
        try:
            d[key] = bool(strtobool(d[key]))
            continue
        except ValueError:
            pass
        try:
            d[key] = int(d[key])
            continue
        except ValueError:
            pass
        try:
            d[key] = float(d[key])
            continue
        except ValueError:
            pass
    return d


env = type_env()
print(type(env["SSL"]))
print(type(env["PORT"]))

if not env["SSL"]:  # <-- I'd like this to be cast to boolean and typed as a boolean
    print("Connecting w/o SSL!")
if 65535 < env["PORT"]:  # <-- I'd like this to be cast to int and typed as an int
    print("Invalid port!")