Work ongoing. Applying advices from review.
This commit is contained in:
parent
87de34d0bb
commit
3a4c217cae
26
sanic/app.py
26
sanic/app.py
@ -1461,30 +1461,6 @@ class Sanic:
|
|||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""Update app.config.
|
"""Update app.config.
|
||||||
|
|
||||||
Note:: only upper case settings are considered.
|
Please refer to config.py: Config class: update_config method for documentation."""
|
||||||
|
|
||||||
You can upload app config by providing path to py file holding settings.
|
|
||||||
|
|
||||||
# /some/py/file
|
|
||||||
A = 1
|
|
||||||
B = 2
|
|
||||||
|
|
||||||
app.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}
|
|
||||||
app.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
|
|
||||||
app.update_config(c)"""
|
|
||||||
|
|
||||||
self.config.update_config(config)
|
self.config.update_config(config)
|
||||||
|
173
sanic/config.py
173
sanic/config.py
@ -1,11 +1,18 @@
|
|||||||
from os import environ as os_environ
|
from os import environ as os_environ
|
||||||
from re import findall as re_findall
|
|
||||||
from importlib.util import spec_from_file_location, \
|
|
||||||
module_from_spec
|
|
||||||
|
|
||||||
from typing import Union, \
|
from typing import Union, \
|
||||||
Any
|
Any
|
||||||
|
|
||||||
|
from .utils import str_to_bool, \
|
||||||
|
load_module_from_file_location
|
||||||
|
|
||||||
|
# TODO: remove in version: 21.3
|
||||||
|
import types
|
||||||
|
from sanic.exceptions import PyFileError
|
||||||
|
from sanic.helpers import import_string
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SANIC_PREFIX = "SANIC_"
|
SANIC_PREFIX = "SANIC_"
|
||||||
BASE_LOGO = """
|
BASE_LOGO = """
|
||||||
@ -35,6 +42,7 @@ DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
@ -58,6 +66,84 @@ class Config(dict):
|
|||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 (%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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||||
"""
|
"""
|
||||||
@ -74,7 +160,7 @@ class Config(dict):
|
|||||||
self[config_key] = float(v)
|
self[config_key] = float(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
self[config_key] = strtobool(v)
|
self[config_key] = str_to_bool(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self[config_key] = v
|
self[config_key] = v
|
||||||
|
|
||||||
@ -82,7 +168,7 @@ class Config(dict):
|
|||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""Update app.config.
|
"""Update app.config.
|
||||||
|
|
||||||
Note only upper case settings are considered.
|
Note:: only upper case settings are considered.
|
||||||
|
|
||||||
You can upload app config by providing path to py file holding settings.
|
You can upload app config by providing path to py file holding settings.
|
||||||
|
|
||||||
@ -117,80 +203,3 @@ class Config(dict):
|
|||||||
config = dict(filter(lambda i: i[0].isupper(), config.items()))
|
config = dict(filter(lambda i: i[0].isupper(), config.items()))
|
||||||
|
|
||||||
self.update(config)
|
self.update(config)
|
||||||
|
|
||||||
|
|
||||||
# Is in Sanic any better place where to keep this ???
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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,))
|
|
||||||
|
|
||||||
|
|
||||||
# Is in Sanic any better place where to keep this ???
|
|
||||||
|
|
||||||
def load_module_from_file_location(*args, **kwargs):
|
|
||||||
"""Returns loaded module provided as a file path.
|
|
||||||
|
|
||||||
:param args: look for importlib.util.spec_from_file_location parameters specification
|
|
||||||
:param kwargs: look for importlib.util.spec_from_file_location parameters specification
|
|
||||||
|
|
||||||
So for example You can:
|
|
||||||
|
|
||||||
some_module = load_module_from_file_location("some_module_name", "/some/path/${some_env_var})
|
|
||||||
|
|
||||||
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."""
|
|
||||||
|
|
||||||
# 1) Get location parameter.
|
|
||||||
if "location" in kwargs:
|
|
||||||
location = kwargs["location"]
|
|
||||||
_l = "kwargs"
|
|
||||||
elif len(args) >= 2:
|
|
||||||
location = args[1]
|
|
||||||
_l = "args"
|
|
||||||
else:
|
|
||||||
raise Exception("Provided arguments must conform to importlib.util.spec_from_file_location arguments, \
|
|
||||||
nonetheless location parameter has to be provided.")
|
|
||||||
|
|
||||||
# 2) Parse location.
|
|
||||||
if isinstance(location, bytes):
|
|
||||||
location = location.decode()
|
|
||||||
|
|
||||||
# A) Check if location contains any environment variables in format ${some_env_var}.
|
|
||||||
env_vars_in_location = set(re_findall("\${(.+?)}", 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 Exception("There are no following environment variables: " + ", ".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])
|
|
||||||
|
|
||||||
# 3) Put back parsed location pareameter.
|
|
||||||
if _l == "kwargs":
|
|
||||||
kwargs["location"] = location
|
|
||||||
else:
|
|
||||||
_args = list(args)
|
|
||||||
_args[1] = location
|
|
||||||
args = tuple(_args)
|
|
||||||
|
|
||||||
# 4) Load and return module.
|
|
||||||
_mod_spec = spec_from_file_location(*args, **kwargs)
|
|
||||||
module = module_from_spec(_mod_spec)
|
|
||||||
_mod_spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
return module
|
|
||||||
|
69
sanic/utils.py
Normal file
69
sanic/utils.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from os import environ as os_environ
|
||||||
|
from re import findall as re_findall
|
||||||
|
from importlib.util import spec_from_file_location, \
|
||||||
|
module_from_spec
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_bool(val):
|
||||||
|
"""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("Invalid truth value %r" % (val,))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_module_from_file_location(name: str, location: Union[bytes, str], enc: str = "utf8", *args, **kwargs):
|
||||||
|
"""Returns loaded module provided as a file path.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
The same as importlib.util.spec_from_file_location name param.
|
||||||
|
: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.
|
||||||
|
:enc:
|
||||||
|
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(enc)
|
||||||
|
|
||||||
|
# A) Check if location contains any environment variables in format ${some_env_var}.
|
||||||
|
env_vars_in_location = set(re_findall("\${(.+?)}", 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 Exception("There are no following environment variables: " + ", ".join(not_defined_env_vars)) # TO ASK: Can we raise better exception type here ???
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
_mod_spec = spec_from_file_location(name, location, *args, **kwargs)
|
||||||
|
module = module_from_spec(_mod_spec)
|
||||||
|
_mod_spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
return module
|
Loading…
x
Reference in New Issue
Block a user