from __future__ import annotations import warnings from collections import ChainMap from contextlib import suppress try: from flask.config import Config flask_installed = True except ImportError: # pragma: no cover flask_installed = False Config = object import dynaconf import pkg_resources class FlaskDynaconf: """The arguments are. app = The created app dynaconf_args = Extra args to be passed to Dynaconf (validator for example) All other values are stored as config vars specially:: ENVVAR_PREFIX_FOR_DYNACONF = env prefix for your envvars to be loaded example: if you set to `MYSITE` then export MYSITE_SQL_PORT='@int 5445' with that exported to env you access using: app.config.SQL_PORT app.config.get('SQL_PORT') app.config.get('sql_port') # get is case insensitive app.config['SQL_PORT'] Dynaconf uses `@int, @bool, @float, @json` to cast env vars SETTINGS_FILE_FOR_DYNACONF = The name of the module or file to use as default to load settings. If nothing is passed it will be `settings.*` or value found in `ENVVAR_FOR_DYNACONF` Dynaconf supports .py, .yml, .toml, ini, json ATTENTION: Take a look at `settings.yml` and `.secrets.yml` to know the required settings format. Settings load order in Dynaconf: - Load all defaults and Flask defaults - Load all passed variables when applying FlaskDynaconf - Update with data in settings files - Update with data in environment vars `ENVVAR_FOR_DYNACONF_` TOML files are very useful to have `envd` settings, lets say, `production` and `development`. You can also achieve the same using multiple `.py` files naming as `settings.py`, `production_settings.py` and `development_settings.py` (see examples/validator) Example:: app = Flask(__name__) FlaskDynaconf( app, ENV='MYSITE', SETTINGS_FILE='settings.yml', EXTRA_VALUE='You can add additional config vars here' ) Take a look at examples/flask in Dynaconf repository """ def __init__( self, app=None, instance_relative_config=False, dynaconf_instance=None, extensions_list=False, **kwargs, ): """kwargs holds initial dynaconf configuration""" if not flask_installed: # pragma: no cover raise RuntimeError( "To use this extension Flask must be installed " "install it with: pip install flask" ) self.kwargs = {k.upper(): v for k, v in kwargs.items()} kwargs.setdefault("ENVVAR_PREFIX", "FLASK") env_prefix = f"{kwargs['ENVVAR_PREFIX']}_ENV" # FLASK_ENV kwargs.setdefault("ENV_SWITCHER", env_prefix) kwargs.setdefault("ENVIRONMENTS", True) kwargs.setdefault("load_dotenv", True) kwargs.setdefault( "default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES ) self.dynaconf_instance = dynaconf_instance self.instance_relative_config = instance_relative_config self.extensions_list = extensions_list if app: self.init_app(app, **kwargs) def init_app(self, app, **kwargs): """kwargs holds initial dynaconf configuration""" self.kwargs.update(kwargs) self.settings = self.dynaconf_instance or dynaconf.LazySettings( **self.kwargs ) dynaconf.settings = self.settings # rebind customized settings app.config = self.make_config(app) app.dynaconf = self.settings if self.extensions_list: if not isinstance(self.extensions_list, str): self.extensions_list = "EXTENSIONS" app.config.load_extensions(self.extensions_list) def make_config(self, app): root_path = app.root_path if self.instance_relative_config: # pragma: no cover root_path = app.instance_path if self.dynaconf_instance: self.settings.update(self.kwargs) return DynaconfConfig( root_path=root_path, defaults=app.config, _settings=self.settings, _app=app, ) class DynaconfConfig(Config): """ Replacement for flask.config_class that responds as a Dynaconf instance. """ def __init__(self, _settings, _app, *args, **kwargs): """perform the initial load""" super().__init__(*args, **kwargs) # Bring Dynaconf instance value to Flask Config Config.update(self, _settings.store) self._settings = _settings self._app = _app def __contains__(self, item): return hasattr(self, item) def __getitem__(self, key): try: return self._settings[key] except KeyError: return Config.__getitem__(self, key) def __setitem__(self, key, value): """ Allows app.config['key'] = 'foo' """ return self._settings.__setitem__(key, value) def _chain_map(self): return ChainMap(self._settings, dict(dict.items(self))) def keys(self): return self._chain_map().keys() def values(self): return self._chain_map().values() def items(self): return self._chain_map().items() def setdefault(self, key, value=None): return self._chain_map().setdefault(key, value) def __iter__(self): return self._chain_map().__iter__() def __getattr__(self, name): """ First try to get value from dynaconf then from Flask Config """ with suppress(AttributeError): return getattr(self._settings, name) with suppress(KeyError): return self[name] raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'" ) def __call__(self, name, *args, **kwargs): return self.get(name, *args, **kwargs) def get(self, key, default=None): """Gets config from dynaconf variables if variables does not exists in dynaconf try getting from `app.config` to support runtime settings.""" return self._settings.get(key, Config.get(self, key, default)) def load_extensions(self, key="EXTENSIONS", app=None): """Loads flask extensions dynamically.""" app = app or self._app extensions = app.config.get(key) if not extensions: warnings.warn( f"Settings is missing {key} to load Flask Extensions", RuntimeWarning, ) return for object_reference in app.config[key]: # add a placeholder `name` to create a valid entry point entry_point_spec = f"__name = {object_reference}" # parse the entry point specification entry_point = pkg_resources.EntryPoint.parse(entry_point_spec) # dynamically resolve the entry point initializer = entry_point.resolve() # Invoke extension initializer initializer(app)