1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
from __future__ import annotations
import errno
import importlib
import inspect
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)
if isinstance(identifier, SourceMetadata):
loader_identifier = SourceMetadata(
identifier.loader, mod.__name__, identifier.env
)
else:
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):
setting_value = getattr(mod, setting)
# 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:
obj.set(
setting,
setting_value,
loader_identifier=identifier,
merge=file_merge,
validate=validate,
)
# if setting (name) starts with _dynaconf_hook
# and the value is a callable
# then we want to add it to the post_hooks list on the obj
# we use the name instead checking on an attribute to avoid
# loading a lazy object early in the process
elif setting.startswith("_dynaconf_hook") and callable(setting_value):
if setting_value not in obj._post_hooks:
obj._post_hooks.append(setting_value)
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)
if isinstance(identifier, SourceMetadata):
loader_identifier = identifier
else:
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()]
)
|