Update config (#1903)
* New aproach for uploading sanic app config. * Update config.rst Co-authored-by: tigerthelion <bjt.thompson@gmail.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
This commit is contained in:
10
sanic/app.py
10
sanic/app.py
@@ -1452,3 +1452,13 @@ class Sanic:
|
||||
self.asgi = True
|
||||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
await asgi_app()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Configuration
|
||||
# -------------------------------------------------------------------- #
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""Update app.config.
|
||||
|
||||
Please refer to config.py::Config.update_config for documentation."""
|
||||
|
||||
self.config.update_config(config)
|
||||
|
||||
144
sanic/config.py
144
sanic/config.py
@@ -1,8 +1,15 @@
|
||||
import os
|
||||
import types
|
||||
from os import environ
|
||||
from typing import Any, Union
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object methods
|
||||
# with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
from .deprecated import from_envvar, from_object, from_pyfile # noqa
|
||||
from .utils import load_module_from_file_location, str_to_bool
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
@@ -59,76 +66,23 @@ class Config(dict):
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError(
|
||||
"The environment variable %r is not set and "
|
||||
"thus configuration could not be loaded." % variable_name
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec( # nosec
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
or also:
|
||||
app.config.from_object('myproject.config.MyConfigClass')
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object
|
||||
# methods with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
from_envvar = from_envvar
|
||||
from_pyfile = from_pyfile
|
||||
from_object = from_object
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
for k, v in environ.items():
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
@@ -138,23 +92,47 @@ class Config(dict):
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = strtobool(v)
|
||||
self[config_key] = str_to_bool(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||
"""Update app.config.
|
||||
|
||||
def strtobool(val):
|
||||
"""
|
||||
This function was borrowed from distutils.utils. While distutils
|
||||
is part of stdlib, it feels odd to use distutils in main application code.
|
||||
Note:: only upper case settings are considered.
|
||||
|
||||
The function was modified to walk its talk and actually return bool
|
||||
and not int.
|
||||
"""
|
||||
val = val.lower()
|
||||
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||
return True
|
||||
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||
return False
|
||||
else:
|
||||
raise ValueError("invalid truth value %r" % (val,))
|
||||
You can upload app config by providing path to py file
|
||||
holding settings.
|
||||
|
||||
# /some/py/file
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
config.update_config("${some}/py/file")
|
||||
|
||||
Yes you can put environment variable here, but they must be provided
|
||||
in format: ${some_env_var}, and mark that $some_env_var is treated
|
||||
as plain string.
|
||||
|
||||
You can upload app config by providing dict holding settings.
|
||||
|
||||
d = {"A": 1, "B": 2}
|
||||
config.update_config(d)
|
||||
|
||||
You can upload app config by providing any object holding settings,
|
||||
but in such case config.__dict__ will be used as dict holding settings.
|
||||
|
||||
class C:
|
||||
A = 1
|
||||
B = 2
|
||||
config.update_config(C)"""
|
||||
|
||||
if isinstance(config, (bytes, str)):
|
||||
config = load_module_from_file_location(location=config)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
config = config.__dict__
|
||||
|
||||
config = dict(filter(lambda i: i[0].isupper(), config.items()))
|
||||
|
||||
self.update(config)
|
||||
|
||||
106
sanic/deprecated.py
Normal file
106
sanic/deprecated.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# NOTE(tomaszdrozdz): remove in version: 21.3
|
||||
# We replace from_envvar(), from_object(), from_pyfile() config object methods
|
||||
# with one simpler update_config() method.
|
||||
# We also replace "loading module from file code" in from_pyfile()
|
||||
# in a favour of load_module_from_file_location().
|
||||
# Please see pull request: 1903
|
||||
# and issue: 1895
|
||||
import types
|
||||
|
||||
from os import environ
|
||||
from typing import Any
|
||||
from warnings import warn
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
|
||||
|
||||
def from_envvar(self, variable_name: str) -> bool:
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_envvar` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
config_file = environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError(
|
||||
f"The environment variable {variable_name} is not set and "
|
||||
f"thus configuration could not be loaded."
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
|
||||
def from_pyfile(self, filename: str) -> bool:
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_pyfile` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec( # nosec
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = "Unable to load configuration file (e.strerror)"
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
|
||||
def from_object(self, obj: Any) -> None:
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
or also:
|
||||
app.config.from_object('myproject.config.MyConfigClass')
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
|
||||
warn(
|
||||
"Using `from_object` method is deprecated and will be removed in "
|
||||
"v21.3, use `app.update_config` method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if isinstance(obj, str):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
@@ -169,6 +169,10 @@ class Unauthorized(SanicException):
|
||||
}
|
||||
|
||||
|
||||
class LoadFileException(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
|
||||
99
sanic/utils.py
Normal file
99
sanic/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from os import environ as os_environ
|
||||
from re import findall as re_findall
|
||||
from typing import Union
|
||||
|
||||
from .exceptions import LoadFileException
|
||||
|
||||
|
||||
def str_to_bool(val: str) -> bool:
|
||||
"""Takes string and tries to turn it into bool as human would do.
|
||||
|
||||
If val is in case insensitive (
|
||||
"y", "yes", "yep", "yup", "t",
|
||||
"true", "on", "enable", "enabled", "1"
|
||||
) returns True.
|
||||
If val is in case insensitive (
|
||||
"n", "no", "f", "false", "off", "disable", "disabled", "0"
|
||||
) returns False.
|
||||
Else Raise ValueError."""
|
||||
|
||||
val = val.lower()
|
||||
if val in {
|
||||
"y",
|
||||
"yes",
|
||||
"yep",
|
||||
"yup",
|
||||
"t",
|
||||
"true",
|
||||
"on",
|
||||
"enable",
|
||||
"enabled",
|
||||
"1",
|
||||
}:
|
||||
return True
|
||||
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"Invalid truth value {val}")
|
||||
|
||||
|
||||
def load_module_from_file_location(
|
||||
location: Union[bytes, str], encoding: str = "utf8", *args, **kwargs
|
||||
):
|
||||
"""Returns loaded module provided as a file path.
|
||||
|
||||
:param args:
|
||||
Coresponds to importlib.util.spec_from_file_location location
|
||||
parameters,but with this differences:
|
||||
- It has to be of a string or bytes type.
|
||||
- You can also use here environment variables
|
||||
in format ${some_env_var}.
|
||||
Mark that $some_env_var will not be resolved as environment variable.
|
||||
:encoding:
|
||||
If location parameter is of a bytes type, then use this encoding
|
||||
to decode it into string.
|
||||
:param args:
|
||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
||||
parameters.
|
||||
:param kwargs:
|
||||
Coresponds to the rest of importlib.util.spec_from_file_location
|
||||
parameters.
|
||||
|
||||
For example You can:
|
||||
|
||||
some_module = load_module_from_file_location(
|
||||
"some_module_name",
|
||||
"/some/path/${some_env_var}"
|
||||
)
|
||||
"""
|
||||
|
||||
# 1) Parse location.
|
||||
if isinstance(location, bytes):
|
||||
location = location.decode(encoding)
|
||||
|
||||
# A) Check if location contains any environment variables
|
||||
# in format ${some_env_var}.
|
||||
env_vars_in_location = set(re_findall(r"\${(.+?)}", location))
|
||||
|
||||
# B) Check these variables exists in environment.
|
||||
not_defined_env_vars = env_vars_in_location.difference(os_environ.keys())
|
||||
if not_defined_env_vars:
|
||||
raise LoadFileException(
|
||||
"The following environment variables are not set: "
|
||||
f"{', '.join(not_defined_env_vars)}"
|
||||
)
|
||||
|
||||
# C) Substitute them in location.
|
||||
for env_var in env_vars_in_location:
|
||||
location = location.replace("${" + env_var + "}", os_environ[env_var])
|
||||
|
||||
# 2) Load and return module.
|
||||
name = location.split("/")[-1].split(".")[
|
||||
0
|
||||
] # get just the file name without path and .py extension
|
||||
_mod_spec = spec_from_file_location(name, location, *args, **kwargs)
|
||||
module = module_from_spec(_mod_spec)
|
||||
_mod_spec.loader.exec_module(module) # type: ignore
|
||||
|
||||
return module
|
||||
Reference in New Issue
Block a user