Skip to content

Commit

Permalink
Update config (#1903)
Browse files Browse the repository at this point in the history
* New aproach for uploading sanic app config.

* Update config.rst

Co-authored-by: tigerthelion <[email protected]>
Co-authored-by: Adam Hopkins <[email protected]>
  • Loading branch information
3 people authored Sep 30, 2020
1 parent 7b75593 commit 1de4bce
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 88 deletions.
91 changes: 88 additions & 3 deletions docs/sanic/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Sanic holds the configuration in the `config` attribute of the application objec
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config.DB_USER = 'appuser'
app.config['DB_USER'] = 'appuser'
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
Since the config object has a type that inherits from dictionary, you can use its ``update`` method in order to set several values at once:

.. code-block:: python
Expand Down Expand Up @@ -45,11 +45,92 @@ Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable

.. code-block:: python
app = Sanic(__name__, load_env=False)
app = Sanic(__name__, load_env=False)
From file, dict, or any object (having __dict__ attribute).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can store app configurations in: (1) a Python file, (2) a dictionary, or (3) in some other type of custom object.

In order to load configuration from ove of those, you can use ``app.upload_config()``.

**1) From file**


Let's say you have ``my_config.py`` file that looks like this:

.. code-block:: python
# my_config.py
A = 1
B = 2
Loading config from this file is as easy as:

.. code-block:: python
app.update_config("/path/to/my_config.py")
You can also use environment variables in the path name here.

Let's say you have an environment variable like this:

.. code-block:: shell
$ export my_path="/path/to"
Then you can use it like this:

.. code-block:: python
app.update_config("${my_path}/my_config.py")
.. note::

Just remember that you have to provide environment variables in the format ${environment_variable} and that $environment_variable is not expanded (is treated as "plain" text).

**2) From dict**

You can also set your app config by providing a ``dict``:

.. code-block:: python
d = {"A": 1, "B": 2}
app.update_config(d)
**3) From _any_ object**

App config can be taken from an object. Internally, it uses ``__dict__`` to retrieve keys and values.

For example, pass the class:

.. code-block:: python
class C:
A = 1
B = 2
app.update_config(C)
or, it can be instantiated:

.. code-block:: python
c = C()
app.update_config(c)
- From an object (having __dict__ attribute)


From an Object
~~~~~~~~~~~~~~

.. note::

Deprecated, will be removed in version 21.3.

If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:

.. code-block:: python
Expand All @@ -71,6 +152,10 @@ You could use a class or any other object as well.
From a File
~~~~~~~~~~~

.. note::

Deprecated, will be removed in version 21.3.

Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:

.. code-block:: python
Expand Down
10 changes: 10 additions & 0 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,3 +1452,13 @@ async def __call__(self, scope, receive, send):
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)
146 changes: 62 additions & 84 deletions sanic/config.py
Original file line number Diff line number Diff line change
@@ -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_"
Expand Down Expand Up @@ -59,76 +66,23 @@ def __getattr__(self, attr):
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:
Expand All @@ -138,23 +92,47 @@ def load_environment_vars(self, prefix=SANIC_PREFIX):
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.
Note:: only upper case settings are considered.
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()))

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,))
self.update(config)
Loading

0 comments on commit 1de4bce

Please sign in to comment.