Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update config #1903

Merged
merged 30 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ae12420
New aproach for uploading sanic app config.
tomaszdrozdz Aug 4, 2020
8a51b97
New aproach for uploading sanic app config.
tomaszdrozdz Aug 4, 2020
84cb859
New aproach for uploading sanic app config.
tomaszdrozdz Aug 4, 2020
87de34d
New aproach for uploading sanic app config.
tomaszdrozdz Aug 4, 2020
3a4c217
Work ongoing. Applying advices from review.
tomaszdrozdz Aug 13, 2020
e81a266
Work ongoing.
tomaszdrozdz Aug 13, 2020
b3baaf0
Work ongoing.
tomaszdrozdz Aug 13, 2020
d79a90c
Work ongoing.
tomaszdrozdz Aug 13, 2020
48b0354
Work ongoing. Aplying advices from Code Review.
tomaszdrozdz Aug 18, 2020
a61641b
Work ongoing. Aplying advices from Code Review.
tomaszdrozdz Aug 18, 2020
2e78087
Work ongoing. Aplying advices from Code Review.
tomaszdrozdz Aug 25, 2020
d5fce53
make fix-import
tomaszdrozdz Aug 25, 2020
9f4fdff
Merge branch 'master' into update_config
ahopkins Aug 25, 2020
cb6a472
Merge branch 'master' into update_config
ahopkins Aug 27, 2020
64387b4
Update config.rst
tomaszdrozdz Aug 27, 2020
8c491c2
Working on documentation.
tomaszdrozdz Aug 27, 2020
3d54b10
Working on documentation.
tomaszdrozdz Aug 28, 2020
f81ee8c
Working on unit tests.
tomaszdrozdz Sep 1, 2020
3f52b13
Working on unit tests.
tomaszdrozdz Sep 1, 2020
00be0b9
Working on unit tests.
tomaszdrozdz Sep 3, 2020
5a4c0bb
Working on unit tests.
tomaszdrozdz Sep 3, 2020
332e207
Working on unit tests.
tomaszdrozdz Sep 4, 2020
0587c94
Work ongoing. Iplementing code reviev advices.
tomaszdrozdz Sep 7, 2020
fd8c34e
Work ongoing. Implementing advices from Code Review.
tomaszdrozdz Sep 8, 2020
b6b54e2
Implementig code reviev advices.
tomaszdrozdz Sep 24, 2020
c2e2eb5
Implementing changes from code review.
tomaszdrozdz Sep 28, 2020
e19b18b
Merge branch 'master' into update_config
ahopkins Sep 30, 2020
4b28e42
Cleanup tests and linting
ahopkins Sep 30, 2020
606e034
squash
ahopkins Sep 30, 2020
3d8bd64
squash
ahopkins Sep 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1457,8 +1457,8 @@ async def __call__(self, scope, receive, send):
# Configuration
# -------------------------------------------------------------------- #
def update_config(self, config: Union[bytes, str, dict, Any]):
"""Update app.config.
Please refer to config.py: Config class: update_config method for documentation."""
"""Update app.config.

Please refer to config.py::Config.update_config for documentation."""

self.config.update_config(config)
65 changes: 33 additions & 32 deletions sanic/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from os import environ as os_environ
from os import environ
from typing import Any, Union

from .utils import load_module_from_file_location, str_to_bool

# NOTE(tomaszdrozdz): remove in version: 21.3
# We replace from_envvar(), from_object(), from_pyfile() config object methods
# with one simpler update_config() method.
Expand All @@ -11,6 +9,7 @@
# 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 @@ -68,8 +67,8 @@ def __setattr__(self, attr, value):
self[attr] = value

# 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 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
Expand All @@ -83,7 +82,7 @@ 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 @@ -98,32 +97,34 @@ def load_environment_vars(self, prefix=SANIC_PREFIX):
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
"""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)):
Expand Down
22 changes: 13 additions & 9 deletions sanic/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
# 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

from typing import Any


def from_envvar(self, variable_name: str) -> bool:
"""Load a configuration from an environment variable pointing to
Expand All @@ -24,18 +24,19 @@ def from_envvar(self, variable_name: str) -> bool:
"""

warn(
"Using `from_envvar` method is deprecated and will be removed in v21.3, use `app.update_config` method instead.",
"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)
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)
return self.from_pyfile(config_file)


def from_pyfile(self, filename: str) -> True:
Expand All @@ -46,7 +47,8 @@ def from_pyfile(self, filename: str) -> True:
"""

warn(
"Using `from_pyfile` method is deprecated and will be removed in v21.3, use `app.update_config` method instead.",
"Using `from_pyfile` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
Expand All @@ -56,10 +58,11 @@ def from_pyfile(self, filename: str) -> True:
try:
with open(filename) as config_file:
exec( # nosec
compile(config_file.read(), filename, "exec"), module.__dict__,
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = f"Unable to load configuration file (e.strerror)"
e.strerror = "Unable to load configuration file (e.strerror)"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be ?:

        e.strerror = "Unable to load configuration file {e.strerror}"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahopkins @myusko
Do not know if it is to late, but this is not "bleeding bug", but please also take a look.

raise
except Exception as e:
raise PyFileError(filename) from e
Expand Down Expand Up @@ -90,7 +93,8 @@ def from_object(self, obj: Any) -> None:
"""

warn(
"Using `from_object` method is deprecated and will be removed in v21.3, use `app.update_config` method instead.",
"Using `from_object` method is deprecated and will be removed in "
"v21.3, use `app.update_config` method instead.",
DeprecationWarning,
stacklevel=2,
)
Expand Down
2 changes: 0 additions & 2 deletions sanic/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,6 @@ def __init__(self, message, status_code=None, scheme=None, **kwargs):


class LoadFileException(SanicException):
"""Raised from within utils.py: load_module_from_file_location() function."""

pass


Expand Down
70 changes: 44 additions & 26 deletions sanic/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@


def str_to_bool(val: str) -> bool:
"""Takes string and tries to turn it into bool as human would do.
"""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.
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()
Expand All @@ -33,47 +38,60 @@ def str_to_bool(val: str) -> bool:
raise ValueError(f"Invalid truth value {val}")


def load_module_from_file_location(location: Union[bytes, str], encoding: str = "utf8", *args, **kwargs
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}")"""
"""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("\${(.+?)}", location))
# 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(
f"The following environment variables are not set: {', '.join(not_defined_env_vars)}"
"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
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)
Expand Down
15 changes: 8 additions & 7 deletions tests/test_load_module_from_file_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@
from sanic.utils import load_module_from_file_location



@pytest.fixture
def loaded_module_from_file_location():
return load_module_from_file_location(str(Path(__file__).parent / "static/app_test_config.py"))

return load_module_from_file_location(
str(Path(__file__).parent / "static/app_test_config.py")
)


@pytest.mark.dependency(name="test_load_module_from_file_location")
def test_load_module_from_file_location(loaded_module_from_file_location):
assert isinstance(loaded_module_from_file_location, ModuleType)



@pytest.mark.dependency(depends=["test_load_module_from_file_location"])
def test_loaded_module_from_file_location_name(loaded_module_from_file_location):
def test_loaded_module_from_file_location_name(
loaded_module_from_file_location,
):
assert loaded_module_from_file_location.__name__ == "app_test_config"



def test_load_module_from_file_location_with_non_existing_env_variable():
with pytest.raises(
LoadFileException,
match="The following environment variables are not set: MuuMilk"):
match="The following environment variables are not set: MuuMilk",
):

load_module_from_file_location("${MuuMilk}")
25 changes: 16 additions & 9 deletions tests/test_update_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@

_test_setting_as_dict = {"TEST_SETTING_VALUE": 1}
_test_setting_as_class = type("C", (), {"TEST_SETTING_VALUE": 1})
_test_setting_as_module = str(Path(__file__).parent / "static/app_test_config.py")
_test_setting_as_module = str(
Path(__file__).parent / "static/app_test_config.py"
)


@pytest.mark.parametrize(
"conf_object",
[_test_setting_as_dict,
_test_setting_as_class,
pytest.param(_test_setting_as_module,
marks=pytest.mark.dependency(
depends=["test_load_module_from_file_location"],
scope="session"))],
ids=["from_dict", "from_class", "from_file"])
[
_test_setting_as_dict,
_test_setting_as_class,
pytest.param(
_test_setting_as_module,
marks=pytest.mark.dependency(
depends=["test_load_module_from_file_location"],
scope="session",
),
),
],
ids=["from_dict", "from_class", "from_file"],
)
def test_update(app, conf_object):
app.update_config(conf_object)
assert app.config["TEST_SETTING_VALUE"] == 1



def test_update_from_lowercase_key(app):
d = {"test_setting_value": 1}
app.update_config(d)
Expand Down