from __future__ import annotations
import errno
import importlib
import inspect
import io
import types
from contextlib import suppress
from pathlib import Path
from dynaconf import default_settings
from dynaconf . loaders . base import SourceMetadata
from dynaconf . utils import DynaconfDict
from dynaconf . utils import object_merge
from dynaconf . utils import upperfy
from dynaconf . utils . files import find_file
from dynaconf . utils . functional import empty
def load (
obj ,
settings_module ,
identifier = " py " ,
silent = False ,
key = None ,
validate = False ,
) :
"""
Tries to import a python module
Notes :
It doesn ' t handle environment namespaces explicitly. Eg
[ default ] , [ development ] , etc
See tests / test_nested_loading . py sample python file
"""
mod , loaded_from = get_module ( obj , settings_module , silent )
if not ( mod and loaded_from ) :
return
# setup SourceMetadata (for inspecting)
loader_identifier = SourceMetadata ( identifier , mod . __name__ , " global " )
load_from_python_object (
obj , mod , settings_module , key , loader_identifier , validate = validate
)
def load_from_python_object (
obj , mod , settings_module , key = None , identifier = None , validate = False
) :
file_merge = getattr ( mod , " dynaconf_merge " , empty )
if file_merge is empty :
file_merge = getattr ( mod , " DYNACONF_MERGE " , empty )
for setting in dir ( mod ) :
# A setting var in a Python file should start with upper case
# valid: A_value=1, ABC_value=3 A_BBB__default=1
# invalid: a_value=1, MyValue=3
# This is to avoid loading functions, classes and built-ins
if setting . split ( " __ " ) [ 0 ] . isupper ( ) :
if key is None or key == setting :
setting_value = getattr ( mod , setting )
obj . set (
setting ,
setting_value ,
loader_identifier = identifier ,
merge = file_merge ,
validate = validate ,
)
obj . _loaded_py_modules . append ( mod . __name__ )
obj . _loaded_files . append ( mod . __file__ )
def try_to_load_from_py_module_name (
obj , name , key = None , identifier = " py " , silent = False , validate = False
) :
""" Try to load module by its string name.
Arguments :
obj { LAzySettings } - - Dynaconf settings instance
name { str } - - Name of the module e . g : foo . bar . zaz
Keyword Arguments :
key { str } - - Single key to be loaded ( default : { None } )
identifier { str } - - Name of identifier to store ( default : ' py ' )
silent { bool } - - Weather to raise or silence exceptions .
"""
ctx = suppress ( ImportError , TypeError ) if silent else suppress ( )
# setup SourceMetadata (for inspecting)
loader_identifier = SourceMetadata ( identifier , name , " global " )
with ctx :
mod = importlib . import_module ( str ( name ) )
load_from_python_object (
obj , mod , name , key , loader_identifier , validate = validate
)
return True # loaded ok!
# if it reaches this point that means exception occurred, module not found.
return False
def get_module ( obj , filename , silent = False ) :
try :
mod = importlib . import_module ( filename )
loaded_from = " module "
mod . is_error = False
except ( ImportError , TypeError ) :
mod = import_from_filename ( obj , filename , silent = silent )
if mod and not mod . _is_error :
loaded_from = " filename "
else :
# it is important to return None in case of not loaded
loaded_from = None
return mod , loaded_from
def import_from_filename ( obj , filename , silent = False ) : # pragma: no cover
""" If settings_module is a filename path import it. """
if filename in [ item . filename for item in inspect . stack ( ) ] :
raise ImportError (
" Looks like you are loading dynaconf "
f " from inside the { filename } file and then it is trying "
" to load itself entering in a circular reference "
" problem. To solve it you have to "
" invoke your program from another root folder "
" or rename your program file. "
)
_find_file = getattr ( obj , " find_file " , find_file )
if not filename . endswith ( " .py " ) :
filename = f " { filename } .py "
if filename in default_settings . SETTINGS_FILE_FOR_DYNACONF :
silent = True
mod = types . ModuleType ( filename . rstrip ( " .py " ) )
mod . __file__ = filename
mod . _is_error = False
mod . _error = None
try :
with open (
_find_file ( filename ) ,
encoding = default_settings . ENCODING_FOR_DYNACONF ,
) as config_file :
exec ( compile ( config_file . read ( ) , filename , " exec " ) , mod . __dict__ )
except OSError as e :
e . strerror = (
f " py_loader: error loading file " f " ( { e . strerror } { filename } ) \n "
)
if silent and e . errno in ( errno . ENOENT , errno . EISDIR ) :
return
mod . _is_error = True
mod . _error = e
return mod
def write ( settings_path , settings_data , merge = True ) :
""" Write data to a settings file.
: param settings_path : the filepath
: param settings_data : a dictionary with data
: param merge : boolean if existing file should be merged with new data
"""
settings_path = Path ( settings_path )
if settings_path . exists ( ) and merge : # pragma: no cover
existing = DynaconfDict ( )
load ( existing , str ( settings_path ) )
object_merge ( existing , settings_data )
with open (
str ( settings_path ) ,
" w " ,
encoding = default_settings . ENCODING_FOR_DYNACONF ,
) as f :
f . writelines (
[ f " { upperfy ( k ) } = { repr ( v ) } \n " for k , v in settings_data . items ( ) ]
)