You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/dynaconf/loaders/py_loader.py

178 lines
5.6 KiB

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()]
)