Приведите и введите переменные env, используя файл - PullRequest
3 голосов
/ 12 апреля 2020

Для всех моих проектов я загружаю все переменные env при запуске и проверяю, что все ожидаемые ключи существуют, как описано в файле .env.example, следуя dotenv-safe подходу .

Однако переменные env - это строки, которые необходимо приводить вручную, когда они используются внутри кода Python. Это раздражает и подвержено ошибкам. Я хотел бы использовать информацию из файла .env.example для приведения переменных env и получения поддержки ввода Python в моей IDE (VS Code). Как мне это сделать?

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(), если предположить, что она поддерживает только boolean, int, float и str?

Не так сложно выполнить приведение например, в { ссылка }, но мне неясно, как заставить его работать с поддержкой набора текста.

Ответы [ 5 ]

3 голосов
/ 17 апреля 2020

Я предлагаю использовать pydanti c.

Из StackOverflow pydanti c информация тега

Pydanti c - это библиотека для проверки данных и управления настройками на основе подсказок типа Python ( PEP484 ) и переменных аннотаций ( PEP526 ). Это позволяет определять схемы в Python для сложных структур.

давайте предположим, что у вас есть файл с вашими envs SSL и PORT:

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!
3 голосов
/ 15 апреля 2020

Следующее решение предлагает как приведение во время выполнения к нужным типам , так и подсказки подсказок типа редактором без использования внешних зависимостей .

Также проверьте ответ kederra c , чтобы найти отличную альтернативу, используя pydantic, которая позаботится обо всем этом для вас.


Работа напрямую с файлом не-1076 * dotenv будет слишком сложной, если не невозможной. Намного проще обрабатывать всю информацию в некоторой Python структуре данных, поскольку это позволяет средствам проверки типов выполнять свою работу без каких-либо изменений.

Я думаю, что путь к go состоит в использовании Python классы данных . Обратите внимание, что хотя мы указываем типы в определении, они только для контролеров типов, не применяются во время выполнения . Это проблема для переменных среды, поскольку они в основном являются внешней string отображаемой информацией. Чтобы преодолеть это, мы можем форсировать приведение в методе __post_init__ .

Реализация

Во-первых, по причинам организации кода мы можем создать Mixin с типом логики c. Обратите внимание, что случай bool является особенным, поскольку его конструктор выведет True для любой непустой строки, включая "False". Если есть какой-то другой не встроенный тип, который вы хотите обработать, вам также потребуется добавить для него специальную обработку (хотя я бы не советовал делать так, чтобы этот logi c обрабатывал больше, чем эти простые типы).

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!")

Stati c проверка типов работает правильно в VS Code и mypy. Если вы назначите PORT (то есть int) переменной типа str, вы получите предупреждение!

Type hinting working

Чтобы сделать вид, что это словарь, Python предоставляет метод asdict в модуле dataclasses.

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

Но, к сожалению (на момент этого ответа) вы теряете проверку типа stati c Поддержите это. Кажется, для mypy работа в процессе .

2 голосов
/ 14 апреля 2020

Учитывая мой комментарий выше, я бы предложил следующий формат для вашего конфигурационного файла:

settings.py, config.py, et c

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, не может быть преобразовано в ожидаемый вами тип, конфигурация не будет выполнена.

0 голосов
/ 21 апреля 2020

У вас есть 2 варианта - либо вы явно указываете типы переменных, либо вы позволяете функции type_env выводить типы из фактических значений. Другие комментаторы уже предоставили примеры того, как go с явными типами, и я бы лично использовал 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!")
0 голосов
/ 15 апреля 2020
  • Предполагая, что ваш 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!")

...