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

Feature: add @jinja and @format casting #704

Merged
merged 20 commits into from
Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/update_contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
commit-message: Update Contributors
title: "[automated] Update Contributors File"
token: ${{ secrets.GITHUB_TOKEN }}

# - name: Update resources
# uses: test-room-7/action-update-file@v1
# with:
Expand Down
50 changes: 39 additions & 11 deletions dynaconf/utils/parse_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,12 @@ class Lazy:

_dynaconf_lazy_format = True

def __init__(self, value=empty, formatter=Formatters.python_formatter):
def __init__(
self, value=empty, formatter=Formatters.python_formatter, casting=None
):
self.value = value
self.formatter = formatter
self.casting = casting

@property
def context(self):
Expand All @@ -179,7 +182,10 @@ def __call__(self, settings, validator_object=None):
"""LazyValue triggers format lazily."""
self.settings = settings
self.context["_validator_object"] = validator_object
return self.formatter(self.value, **self.context)
result = self.formatter(self.value, **self.context)
if self.casting is not None:
result = self.casting(result)
return result

def __str__(self):
"""Gives string representation for the object."""
Expand All @@ -193,6 +199,11 @@ def _dynaconf_encode(self):
"""Encodes this object values to be serializable to json"""
return f"@{self.formatter} {self.value}"

def set_casting(self, casting):
"""Set the casting and return the instance."""
self.casting = casting
return self


def try_to_encode(value, callback=str):
"""Tries to encode a value by verifying existence of `_dynaconf_encode`"""
Expand All @@ -215,11 +226,25 @@ def evaluate(settings, *args, **kwargs):


converters = {
"@str": str,
"@int": int,
"@float": float,
"@bool": lambda value: str(value).lower() in true_values,
"@json": json.loads,
"@str": lambda value: value.set_casting(str)
if isinstance(value, Lazy)
else str(value),
"@int": lambda value: value.set_casting(int)
if isinstance(value, Lazy)
else int(value),
"@float": lambda value: value.set_casting(float)
if isinstance(value, Lazy)
else float(value),
"@bool": lambda value: value.set_casting(
lambda x: str(x).lower() in true_values
)
if isinstance(value, Lazy)
else str(value).lower() in true_values,
"@json": lambda value: value.set_casting(
lambda x: json.loads(x.replace("'", '"'))
)
if isinstance(value, Lazy)
else json.loads(value),
"@format": lambda value: Lazy(value),
EdwardCuiPeacock marked this conversation as resolved.
Show resolved Hide resolved
"@jinja": lambda value: Lazy(value, formatter=Formatters.jinja_formatter),
# Meta Values to trigger pre assignment actions
Expand Down Expand Up @@ -279,10 +304,13 @@ def _parse_conf_data(data, tomlfy=False, box_settings=None):
and isinstance(data, str)
and data.startswith(tuple(converters.keys()))
):
parts = data.partition(" ")
converter_key = parts[0]
value = parts[-1]
value = get_converter(converter_key, value, box_settings)
# Parse the converters iteratively
num_converters = data.count("@")
parts = data.split(" ", num_converters)
converter_key_list = parts[:num_converters]
value = parts[-1] if len(parts) > 1 else "" # for @del
for converter_key in converter_key_list[::-1]:
value = get_converter(converter_key, value, box_settings)
else:
value = parse_with_toml(data) if tomlfy else data

Expand Down
64 changes: 64 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,70 @@ def test_find_file(tmpdir):
) == os.path.join(str(tmpdir), ".env")


def test_casting_str(settings):
res = parse_conf_data("@str 7")
assert isinstance(res, str) and res == "7"

settings.set("value", 7)
res = parse_conf_data("@str @jinja {{ this.value }}")(settings)
assert isinstance(res, str) and res == "7"

res = parse_conf_data("@str @format {this.value}")(settings)
assert isinstance(res, str) and res == "7"


def test_casting_int(settings):
res = parse_conf_data("@int 2")
assert isinstance(res, int) and res == 2

settings.set("value", 2)
res = parse_conf_data("@int @jinja {{ this.value }}")(settings)
assert isinstance(res, int) and res == 2

res = parse_conf_data("@int @format {this.value}")(settings)
assert isinstance(res, int) and res == 2


def test_casting_float(settings):
res = parse_conf_data("@float 0.3")
assert isinstance(res, float) and abs(res - 0.3) < 1e-6

settings.set("value", 0.3)
res = parse_conf_data("@float @jinja {{ this.value }}")(settings)
assert isinstance(res, float) and abs(res - 0.3) < 1e-6

res = parse_conf_data("@float @format {this.value}")(settings)
assert isinstance(res, float) and abs(res - 0.3) < 1e-6


def test_casting_bool(settings):
res = parse_conf_data("@bool true")
assert isinstance(res, bool) and res is True

settings.set("value", "true")
res = parse_conf_data("@bool @jinja {{ this.value }}")(settings)
assert isinstance(res, bool) and res is True

settings.set("value", "false")
res = parse_conf_data("@bool @format {this.value}")(settings)
assert isinstance(res, bool) and res is False


def test_casting_json(settings):
res = parse_conf_data("@json {'FOO': 'bar'}")
assert isinstance(res, dict)
assert "FOO" in res and "bar" in res.values()

settings.set("value", "{'FOO': 'bar'}")
res = parse_conf_data("@json @jinja {{ this.value }}")(settings)
EdwardCuiPeacock marked this conversation as resolved.
Show resolved Hide resolved
assert isinstance(res, dict)
assert "FOO" in res and "bar" in res.values()

res = parse_conf_data("@json @format {this.value}")(settings)
assert isinstance(res, dict)
assert "FOO" in res and "bar" in res.values()


def test_disable_cast(monkeypatch):
# this casts for int
assert parse_conf_data("@int 42", box_settings={}) == 42
Expand Down