2023-10-14 21:56:21 +08:00
|
|
|
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
|
2024-03-04 01:15:23 +08:00
|
|
|
from dynaconf.loaders.base import SourceMetadata
|
2023-10-14 21:56:21 +08:00
|
|
|
from dynaconf.utils import DynaconfDict
|
|
|
|
from dynaconf.utils import object_merge
|
|
|
|
from dynaconf.utils import upperfy
|
|
|
|
from dynaconf.utils.files import find_file
|
2024-03-04 01:15:23 +08:00
|
|
|
from dynaconf.utils.functional import empty
|
2023-10-14 21:56:21 +08:00
|
|
|
|
|
|
|
|
2024-03-04 01:15:23 +08:00
|
|
|
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
|
|
|
|
"""
|
2023-10-14 21:56:21 +08:00
|
|
|
mod, loaded_from = get_module(obj, settings_module, silent)
|
|
|
|
if not (mod and loaded_from):
|
|
|
|
return
|
2024-03-04 01:15:23 +08:00
|
|
|
|
|
|
|
# setup SourceMetadata (for inspecting)
|
|
|
|
loader_identifier = SourceMetadata(identifier, mod.__name__, "global")
|
|
|
|
|
|
|
|
load_from_python_object(
|
|
|
|
obj, mod, settings_module, key, loader_identifier, validate=validate
|
|
|
|
)
|
2023-10-14 21:56:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
def load_from_python_object(
|
2024-03-04 01:15:23 +08:00
|
|
|
obj, mod, settings_module, key=None, identifier=None, validate=False
|
2023-10-14 21:56:21 +08:00
|
|
|
):
|
2024-03-04 01:15:23 +08:00
|
|
|
file_merge = getattr(mod, "dynaconf_merge", empty)
|
|
|
|
if file_merge is empty:
|
|
|
|
file_merge = getattr(mod, "DYNACONF_MERGE", empty)
|
|
|
|
|
2023-10-14 21:56:21 +08:00
|
|
|
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,
|
2024-03-04 01:15:23 +08:00
|
|
|
validate=validate,
|
2023-10-14 21:56:21 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
obj._loaded_py_modules.append(mod.__name__)
|
|
|
|
obj._loaded_files.append(mod.__file__)
|
|
|
|
|
|
|
|
|
|
|
|
def try_to_load_from_py_module_name(
|
2024-03-04 01:15:23 +08:00
|
|
|
obj, name, key=None, identifier="py", silent=False, validate=False
|
2023-10-14 21:56:21 +08:00
|
|
|
):
|
|
|
|
"""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()
|
|
|
|
|
2024-03-04 01:15:23 +08:00
|
|
|
# setup SourceMetadata (for inspecting)
|
|
|
|
loader_identifier = SourceMetadata(identifier, name, "global")
|
|
|
|
|
2023-10-14 21:56:21 +08:00
|
|
|
with ctx:
|
|
|
|
mod = importlib.import_module(str(name))
|
2024-03-04 01:15:23 +08:00
|
|
|
load_from_python_object(
|
|
|
|
obj, mod, name, key, loader_identifier, validate=validate
|
|
|
|
)
|
2023-10-14 21:56:21 +08:00
|
|
|
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()]
|
|
|
|
)
|