Skip to content

Commit abfc1ef

Browse files
authored
Merge branch 'master' into create-pull-request/patch
2 parents 631b5b9 + db1ceab commit abfc1ef

2 files changed

Lines changed: 125 additions & 11 deletions

File tree

dynaconf/utils/parse_conf.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,12 @@ class Lazy:
166166

167167
_dynaconf_lazy_format = True
168168

169-
def __init__(self, value=empty, formatter=Formatters.python_formatter):
169+
def __init__(
170+
self, value=empty, formatter=Formatters.python_formatter, casting=None
171+
):
170172
self.value = value
171173
self.formatter = formatter
174+
self.casting = casting
172175

173176
@property
174177
def context(self):
@@ -179,7 +182,10 @@ def __call__(self, settings, validator_object=None):
179182
"""LazyValue triggers format lazily."""
180183
self.settings = settings
181184
self.context["_validator_object"] = validator_object
182-
return self.formatter(self.value, **self.context)
185+
result = self.formatter(self.value, **self.context)
186+
if self.casting is not None:
187+
result = self.casting(result)
188+
return result
183189

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

202+
def set_casting(self, casting):
203+
"""Set the casting and return the instance."""
204+
self.casting = casting
205+
return self
206+
196207

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

216227

217228
converters = {
218-
"@str": str,
219-
"@int": int,
220-
"@float": float,
221-
"@bool": lambda value: str(value).lower() in true_values,
222-
"@json": json.loads,
229+
"@str": lambda value: value.set_casting(str)
230+
if isinstance(value, Lazy)
231+
else str(value),
232+
"@int": lambda value: value.set_casting(int)
233+
if isinstance(value, Lazy)
234+
else int(value),
235+
"@float": lambda value: value.set_casting(float)
236+
if isinstance(value, Lazy)
237+
else float(value),
238+
"@bool": lambda value: value.set_casting(
239+
lambda x: str(x).lower() in true_values
240+
)
241+
if isinstance(value, Lazy)
242+
else str(value).lower() in true_values,
243+
"@json": lambda value: value.set_casting(
244+
lambda x: json.loads(x.replace("'", '"'))
245+
)
246+
if isinstance(value, Lazy)
247+
else json.loads(value),
223248
"@format": lambda value: Lazy(value),
224249
"@jinja": lambda value: Lazy(value, formatter=Formatters.jinja_formatter),
225250
# Meta Values to trigger pre assignment actions
@@ -279,10 +304,22 @@ def _parse_conf_data(data, tomlfy=False, box_settings=None):
279304
and isinstance(data, str)
280305
and data.startswith(tuple(converters.keys()))
281306
):
282-
parts = data.partition(" ")
283-
converter_key = parts[0]
284-
value = parts[-1]
285-
value = get_converter(converter_key, value, box_settings)
307+
# Check combination token is used
308+
comb_token = re.match(
309+
r"^@(str|int|float|bool|json) @(jinja|format)", data
310+
)
311+
if comb_token:
312+
tokens = comb_token.group(0)
313+
converter_key_list = tokens.split(" ")
314+
value = data.replace(tokens, "").strip()
315+
else:
316+
parts = data.partition(" ")
317+
converter_key_list = [parts[0]]
318+
value = parts[-1]
319+
320+
# Parse the converters iteratively
321+
for converter_key in converter_key_list[::-1]:
322+
value = get_converter(converter_key, value, box_settings)
286323
else:
287324
value = parse_with_toml(data) if tomlfy else data
288325

tests/test_utils.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,83 @@ def test_find_file(tmpdir):
105105
) == os.path.join(str(tmpdir), ".env")
106106

107107

108+
def test_casting_str(settings):
109+
res = parse_conf_data("@str 7")
110+
assert isinstance(res, str) and res == "7"
111+
112+
settings.set("value", 7)
113+
res = parse_conf_data("@str @jinja {{ this.value }}")(settings)
114+
assert isinstance(res, str) and res == "7"
115+
116+
res = parse_conf_data("@str @format {this.value}")(settings)
117+
assert isinstance(res, str) and res == "7"
118+
119+
120+
def test_casting_int(settings):
121+
res = parse_conf_data("@int 2")
122+
assert isinstance(res, int) and res == 2
123+
124+
settings.set("value", 2)
125+
res = parse_conf_data("@int @jinja {{ this.value }}")(settings)
126+
assert isinstance(res, int) and res == 2
127+
128+
res = parse_conf_data("@int @format {this.value}")(settings)
129+
assert isinstance(res, int) and res == 2
130+
131+
132+
def test_casting_float(settings):
133+
res = parse_conf_data("@float 0.3")
134+
assert isinstance(res, float) and abs(res - 0.3) < 1e-6
135+
136+
settings.set("value", 0.3)
137+
res = parse_conf_data("@float @jinja {{ this.value }}")(settings)
138+
assert isinstance(res, float) and abs(res - 0.3) < 1e-6
139+
140+
res = parse_conf_data("@float @format {this.value}")(settings)
141+
assert isinstance(res, float) and abs(res - 0.3) < 1e-6
142+
143+
144+
def test_casting_bool(settings):
145+
res = parse_conf_data("@bool true")
146+
assert isinstance(res, bool) and res is True
147+
148+
settings.set("value", "true")
149+
res = parse_conf_data("@bool @jinja {{ this.value }}")(settings)
150+
assert isinstance(res, bool) and res is True
151+
152+
settings.set("value", "false")
153+
res = parse_conf_data("@bool @format {this.value}")(settings)
154+
assert isinstance(res, bool) and res is False
155+
156+
157+
def test_casting_json(settings):
158+
res = parse_conf_data("""@json {"FOO": "bar"}""")
159+
assert isinstance(res, dict)
160+
assert "FOO" in res and "bar" in res.values()
161+
162+
# Test how single quotes cases are handled.
163+
# When jinja uses `attr` to render a json string,
164+
# it may covnert double quotes to single quotes.
165+
settings.set("value", "{'FOO': 'bar'}")
166+
res = parse_conf_data("@json @jinja {{ this.value }}")(settings)
167+
assert isinstance(res, dict)
168+
assert "FOO" in res and "bar" in res.values()
169+
170+
res = parse_conf_data("@json @format {this.value}")(settings)
171+
assert isinstance(res, dict)
172+
assert "FOO" in res and "bar" in res.values()
173+
174+
# Test jinja rendering a dict
175+
settings.set("value", "OPTION1")
176+
settings.set("OPTION1", {"bar": 1})
177+
settings.set("OPTION2", {"bar": 2})
178+
res = parse_conf_data("@jinja {{ this|attr(this.value) }}")(settings)
179+
assert isinstance(res, str)
180+
res = parse_conf_data("@json @jinja {{ this|attr(this.value) }}")(settings)
181+
assert isinstance(res, dict)
182+
assert "bar" in res and res["bar"] == 1
183+
184+
108185
def test_disable_cast(monkeypatch):
109186
# this casts for int
110187
assert parse_conf_data("@int 42", box_settings={}) == 42

0 commit comments

Comments
 (0)