Using figenv for configuration objects
figenv
provides a metaclass that makes smart decisions about variables to
cast them to the correct type when they are accessed on a configuration object.
It is a drop-in replacement for libraries like Flask-Env
, but instead of
setting the attributes at import time, the values are pulled from the
environment at access time, allowing for patching multiple attributes in the
test suite with one patch.dict()
request to os.environ
.
figenv
also allows for setting attributes as functions, so that the user can
take values that are broken up into multiple parts and construct them into a
single value.
You do not need to instantiate the Config class with the figenv.MetaConfig
object
because the attributes are set by the metaclass and are all classmethods.
Lets say you have the following
class Config:
DATABASE_URL = 'postgresql://username:password@localhost:5432/dbname'
This parameter can be converted to be able to be constructed from a list of attributes.
class Config(metaclass=figenv.MetaConfig):
POSTGRES_MAIN_HOST = 'localhost'
POSTGRES_MAIN_DATABASE = 'dbname'
POSTGRES_MAIN_PASSWORD = 'password'
POSTGRES_MAIN_USER = 'username'
POSTGRES_MAIN_PORT = 5432
def DATABASE_URL(cls):
return 'postgresql://{user}:{password}@{host}:{port}/{db}?sslmode=prefer'.format(
user=cls.POSTGRES_MAIN_USER,
password=cls.POSTGRES_MAIN_PASSWORD,
host=cls.POSTGRES_MAIN_HOST,
port=cls.POSTGRES_MAIN_PORT,
db=cls.POSTGRES_MAIN_DATABASE,
)
Now, if Config.DATABASE_URL
is requested, first the figenv metaclass functions
will check the DATABASE_URL
environment variable, and then if it is not set, it
will call the function.
Patching figenv for tests
Because figenv depends heavily on the environment, even if you patch the Config object directly, environment variables in the test suite can overwrite those patches. Instead you should patch the environment.
Note
It is not possible to directly edit the attributes, they are treated as defaults.
>>> import figenv, os, unittest.mock
>>> class Config(metaclass=figenv.MetaConfig):
... FOO = 'bar'
...
>>> assert Config.FOO == 'bar'
>>> with unittest.mock.patch.dict(os.environ, {'FOO': 'baz'}):
... assert Config.FOO == 'baz'
...
>>>
When the patch.dict
context manager is exited, the os.environ
dictionary is
reverted back to what it was previously.
Extra stuff figenv can do
Creating strict functions
If there is a function that should not be overwritable by the environment
variables, there is decorator figenv.strict()
which can be added to the
function, and this will make sure it is called, and not overwritten by
environment variables.
import os
import platform
from unittest.mock import patch
import figenv
class Config(metaclass=figenv.MetaConfig):
@figenv.strict
def HOSTNAME(cls):
return platform.node()
with patch.dict(os.environ, {'HOSTNAME': 'myfakehost'}):
assert Config.HOSTNAME == platform.node()
Coercion
This feature allows figenv
to convert strings from environment variables to
appropriate python objects automatically. Here are a list of the coercion that
figenv
handles by default:
Any capitalization of strings true or false in the environment variable will be turned into the the
True
orFalse
boolean value.Anything with all digits in it and only one period will be turned into a float
Anything that is all digits will be turned into an int
- You can also use type annotations
typing.Dict
ordict
gets converted into a dictionary object using json.loadsint
,bool
, andfloat
type annotations just get straight converted.any type annotation with the staticmethod
_coerce
is used to convert the object using that method
import typing
import figenv
class csv:
@staticmethod
def _coerce(value):
return value.split(',')
class Config(metaclass=figenv.MetaConfig)
DEBUG = 'false'
VERSION = '1.2.3'
FEATURE_FLAG_PERCENT = '0.24'
ALLOWLIST_USERID: csv = '123abc,456efg'
DATA: typing.Dict = '{"name":"george"}'
assert Config.DEBUG == False
assert Config.VERSION == '1.2.3'
assert Config.FEATURE_FLAG_PERCENT == 0.24
assert Config.ALLOWLIST_USERID == ['123abc', '456efg']
assert Config.DATA == {'name': 'george'}
Data Access
The main intended way to access environment variables from figenv is to access the config objects attributes.
Example:
import figenv
class Config(metaclass=figenv.MetaConfig):
TIMEOUT = 5
assert Config.TIMEOUT == 5
assert Config['TIMEOUT'] == 5
for key, value in Config:
if key == 'TIMEOUT':
assert value == 5
assert dict(Config) == {'TIMEOUT': 5}
But it can also see, you can use it by using the getitem method as if it was a dictionary as well. You can also access the config key values in a for loop and also convert the whole object to a dictionary if you want.
Load all environment variables
If for some reason you do not want to put all your configuration variables into the config object, but still want to access other environment variables, set the ENV_LOAD_ALL attribute on the config class, and it will pull values from the environment even if they do not have a default set on the class.
>>> import figenv
>>> class Config(metaclass=figenv.MetaConfig):
... ENV_LOAD_ALL = True
...
>>> Config.USER
'wallacda'
Prefixes
If you have want to add prefixes to all of the environment variables in your config object, you can specify the ENV_PREFIX attribute.
>>> import figenv, os
>>> class Config(metaclass=figenv.MetaConfig):
... ENV_PREFIX = 'FIGENV_'
... USER = 'daniel'
...
>>> os.getenv('USER')
'wallacda'
>>> Config.USER
'daniel'
>>> os.environ['FIGENV_USER'] = 'newuser'
>>> Config.USER
'newuser'