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
Allow json caster to parse boolean correctly #978
base: master
Are you sure you want to change the base?
Allow json caster to parse boolean correctly #978
Conversation
dynaconf/utils/parse_conf.py
Outdated
def _parse_json_strings(x): | ||
x = x.replace("'", '"') # replace single with double quotes | ||
# replace unquoted True / False with lower case true / false | ||
if "True" in x: | ||
x = re.sub(r'(?<!")\bTrue\b', "true", x) | ||
if "False" in x: | ||
x = re.sub(r'(?<!")\bFalse\b', "false", x) | ||
|
||
return json.loads(x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would happen in this corner case ?
VALUE = '@json {"TrueKey": False}'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"TrueKey" won't be replaced, but I think it is worth adding this case to the tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need regex here?
what if a simple equality check?
if value in ["True", "False"]:
value = value.lower()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't work because value is the whole serialized '{"TrueKey": False}'
string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know, the simple equality makes more sense. I tested this a little bit. The string to parse is probably almost always converted from a dict after parsed by jinja. For example,
value = str({"field": True})
gives me
"{'field': True}"
So like what you have above, I can potentially just match the substring ": True
, and ": False
, and replace them with ": true
, and ": false
. I will work on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case I think the regex may be a good idea
This is related to #976 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestions are:
- rename
_parse_json_string
paramx
tovalue
- add a test to assert the replace won't affect undesired
True
andFalse
occurrences. e.g
'{"KeyTrue": True}' # ok, will keep "KeyTrue"
'{"Key-True": True}' # fail, will change to "Key-true"
dynaconf/utils/parse_conf.py
Outdated
def _parse_json_strings(x): | ||
x = x.replace("'", '"') # replace single with double quotes | ||
# replace unquoted True / False with lower case true / false | ||
if "True" in x: | ||
x = re.sub(r'(?<!")\bTrue\b', "true", x) | ||
if "False" in x: | ||
x = re.sub(r'(?<!")\bFalse\b', "false", x) | ||
|
||
return json.loads(x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"TrueKey" won't be replaced, but I think it is worth adding this case to the tests
Will do. Will add the test cases. |
dynaconf/utils/parse_conf.py
Outdated
@@ -239,6 +239,17 @@ def evaluate(settings, *args, **kwargs): | |||
return evaluate | |||
|
|||
|
|||
def _parse_json_strings(value): | |||
value = value.replace("'", '"') # replace single with double quotes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩
What will happen with a JSON like:
{"somekey": "This is a 'value' containing single' quotes"}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, I can't come up with a good regex to replace all the patterns I have considered,
input_strings =[
'''{"somekey": "This is a 'value' containing single' quotes", "someotherkey": "Another 'value'"}''',
"""{'somekey': "This is a 'value' containing single' quotes", 'someotherkey': "Another 'value'"}""",
#"{'somekey': 'This is a 'value' containing single' quotes'}", # this should never happend when converting from dict to str
"""{'somekey': "This is a 'value' containing single' quotes", 'someotherkey': 'Another value'}""",
"""{'somekey': 'This is a value not containing single quotes', 'someotherkey': 'Another value'}""",
# """{"somekey": 'This is a 'value', 'containing single' quotes'}""",
]
Then how about using ast.literal_eval
? This would also fix the issue with True / False parsing.
Codecov Report
❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more. @@ Coverage Diff @@
## master #978 +/- ##
=======================================
Coverage 98.95% 98.95%
=======================================
Files 23 23
Lines 2196 2197 +1
=======================================
+ Hits 2173 2174 +1
Misses 23 23
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
if isinstance(value, Lazy) | ||
else json.loads(value), | ||
else ast.literal_eval(value), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will address all the cases, look:
Case 1 OK
>>> ast.literal_eval("{'foo': True}")
{'foo': True}
Case 2 - NOK
>>> ast.literal_eval("{'foo': true}")
ValueError: malformed node or string on line 1: <ast.Name object at 0x7f4e111cb6d0>
It is really trick that we need to make both true
(valid json) and True
(valid python) working.
I am rethinking this issue
Should we allow invalid json as '{"foo": True}' to be passed to @json
?
Maybe we want to make it strict, and add a new marker @dict
to accept python dict literals.
Another option is adding a filter to Jinja to address your use case @EdwardCuiPeacock
"@json @jinja {{this.FOO | as_bool }}"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can instead of dict
add a @py_literal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I didn't consider that case. The tricky part is, when first rendered with @jinja
, the dictionary will be converted to strings, probably by str(value)
, before being further casted by @json
. I guess this is why we have True
instead of true
, or single quotes '
instead of double quotes "
. I am not familiar with the code enough to pinpoint where this string conversion is (maybe under Lazy._dynaconf_encode
?). But if we can make special cases when converting Python dict
to strings using json.dumps
instead of str
, that may also solve the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EdwardCuiPeacock there is another option, custom JSONDEcoder
In [37]: class BoolDecoder(json.JSONDecoder):
...: def raw_decode(self, s, idx=0):
...: try:
...: return super().raw_decode(s, idx=idx)
...: except json.JSONDecodeError:
...: # Handle the replacement here
...: # find :True : True, :False, : False
...: # pattern and replace with lowercase
# BETTER USE A REGEX HERE
...: if "True" in s:
...: s = s.replace("True", "true")
...: if "False" in s:
...: s = s.replace("False", "false")
...: return super().raw_decode(s, idx=idx)
...:
In [38]: json.loads('{"foo": True, "a": 1}', cls=BoolDecoder)
Out[38]: {'foo': True, 'a': 1}
In [39]: json.loads('{"foo": False, "a": 1}', cls=BoolDecoder)
Out[39]: {'foo': False, 'a': 1}
In [40]: json.loads('{"foo": false, "a": 1}', cls=BoolDecoder)
Out[40]: {'foo': False, 'a': 1}
In [41]: json.loads('{"foo": true, "a": 1}', cls=BoolDecoder)
Out[41]: {'foo': True, 'a': 1}
@rochacbruno I think the casting idea y_field:
key:
attribute: True
field_name: my_field
value: "@json @jinja {{ (this|attr(this.field_name)).get('key') | tojson or {} }}"
Here is what I have so far after the update:
|
✅ Deploy Preview for dynaconf ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM,
However we might keep this PR unmerged until we are ready for 4.0.0 as it includes a breking change.
@@ -254,11 +255,12 @@ def evaluate(settings, *args, **kwargs): | |||
) | |||
if isinstance(value, Lazy) | |||
else str(value).lower() in true_values, | |||
"@json": lambda value: value.set_casting( | |||
lambda x: json.loads(x.replace("'", '"')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this change, to be more strict, however this is a breaking change and we will merge this only for 4.0.0
dynaconf/utils/parse_conf.py
Outdated
if isinstance(value, Lazy) | ||
else json.loads(value), | ||
"@py_literal": lambda value: value.set_casting(ast.literal_eval) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can just short it to @pyliteral
# Testing list | ||
res = parse_conf_data("""@py_literal ["a", "b", 'c', 1]""") | ||
assert isinstance(res, list) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to add a test case for @pyliteral @jinja ...
combination
@rochacbruno Sounds good to me. Let me know when that happens. |
BTW we need to document this casting idea y_field:
key:
attribute: True
field_name: my_field
value: "@json @jinja {{ (this|attr(this.field_name)).get('key') | tojson or {} }}"
|
Added the new features in docs with examples. |
About a year ago, I made a contribution to add a feature to allow type casting (#704). In my more recent use case, I wanted to use
@json
to get a dictionary that contains boolean values, like the following:However, this throws the following error:
Which suggests to me that the json string parsing was not able to handle boolean values correctly. The json string to be parsed are in the form of
which
json.loads
cannot handle. It necessarily needs to be the lower casetrue
forjson.loads
to recognize this variable as a boolean.The change I made allows
@json
caster to correctly parse the boolean variables by looking for strings "True" and "False" (without the quote; if it is quoted, then it stays as strings), and replace them with the lower case form.