Repository navigation
bpo-40355: ast.literal_eval rejects malformed Dict nodes - #19868
Conversation
remilapeyre
left a comment
There was a problem hiding this comment.
Hi @curtisbucher, the patch looks appropriate but if the length must be known it seems to me like you need to make sure that keys and values are actual sequences and not just iterables, this used to work but now doesn't:
from ast import Constant, Dict, literal_eval, unparse
keys = iter([Constant('a')])
values = iter([Constant('b')])
nasty_dict = Dict(keys=keys, values=values)
literal_eval(nasty_dict)Here's the difference between 3.8 and this patch:
➜ cpython git:(bpo-40355) python3
Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from ast import Constant, Dict, literal_eval, unparse
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'unparse' from 'ast' (/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ast.py)
>>> keys = iter([Constant('a')])
>>> values = iter([Constant('b')])
>>> nasty_dict = Dict(keys=keys, values=values)
>>> literal_eval(nasty_dict)
{'a': 'b'}
>>> ^D
➜ cpython git:(bpo-40355) ./python
Python 3.9.0a6+ (heads/bpo-40355:36eb5b8c4b, May 2 2020, 23:57:18)
[Clang 11.0.3 (clang-1103.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from ast import Constant, Dict, literal_eval, unparse
>>> keys = iter([Constant('a')])
>>> values = iter([Constant('b')])
>>> nasty_dict = Dict(keys=keys, values=values)
>>> literal_eval(nasty_dict)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/remi/src/cpython/Lib/ast.py", line 104, in literal_eval
return _convert(node_or_string)
File "/Users/remi/src/cpython/Lib/ast.py", line 91, in _convert
if len(node.keys) != len(node.values):
TypeError: object of type 'list_iterator' has no len()
>>> ^D
Hm, this seems like a somewhat unusual pattern. Any examples out in the wild of clients generating ASTs with consumable iterators of children? Either way, I agree that it's easy enough to be defensive here, so we might as well. |
| if len(node.keys) != len(node.values): | ||
| raise ValueError(f'malformed node or string: {node!r}') | ||
| return dict(zip(map(_convert, node.keys), | ||
| map(_convert, node.values))) |
There was a problem hiding this comment.
Collecting node.keys and node.values into lists should be fine:
| if len(node.keys) != len(node.values): | |
| raise ValueError(f'malformed node or string: {node!r}') | |
| return dict(zip(map(_convert, node.keys), | |
| map(_convert, node.values))) | |
| keys, values = list(node.keys), list(node.values) | |
| if len(keys) != len(values): | |
| raise ValueError(f'malformed node or string: {node!r}') | |
| return dict(zip(map(_convert, keys), | |
| map(_convert, values))) |
I disagree. That was a side effect which used to work, and now it isn't. |
I would have expected it to raise |
Well it is not possible to raise ValueError on all invalid cases, at least without putting everything inside of a big try/except block and raising ValueError from given error. A simple example would be evaluating something like evaluating |
|
@remilapeyre by the way, you can check out this issue to see all possible errors that can be raised from ast.literal_eval https://bugs.python.org/issue39159 |
|
Oh thanks, I get it now. Indeed there is not much improvement to just check for that. Thanks for the explanantion @isidentical, sorry for the useless noise @brandtbucher. |
isidentical
left a comment
There was a problem hiding this comment.
It looks good, but I'd prefer not having 2 same raise statements. Because it might be change in the future and changing both of them seems unreasonable so IMHO it would be cool to create a subfunction called malformed() or invalid() inside of literal_eval and raise from it.
Abstracted calling ValueError in ast.py
|
@pablogsal, does this look good to you? |
| if isinstance(node, Constant): | ||
| if type(node.value) in (int, float, complex): | ||
| return node.value | ||
| raise ValueError('malformed node or string: ' + repr(node)) | ||
| _malformed(node) |
There was a problem hiding this comment.
Suggestion: I suppose at this point we can invert the check to make the intent a bit more clear:
if not isinstance(node, Constant) or type(node.value) not in (int, float, complex):
_raise_malformed_node(node)
return node.value
| node_or_string = parse(node_or_string, mode='eval') | ||
| if isinstance(node_or_string, Expression): | ||
| node_or_string = node_or_string.body | ||
| def _malformed(node): |
There was a problem hiding this comment.
Please, call this function with a verb, not a noun. Finding malformed in the middle of the code is a bit surprising compared with report_malformed_node or raise_malformed_exception or something similar.
| self.assertRaises(ValueError, ast.literal_eval, '3+(0+6j)') | ||
| self.assertRaises(ValueError, ast.literal_eval, '-(3+6j)') | ||
|
|
||
| def test_literal_eval_malformed(self): |
There was a problem hiding this comment.
I find this name is too generic. Maybe test_literal_eval_malformed_dict_nodes ?
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
Thanks for pinging me :) I left some comments but in general LGTM @isidentical Could you check again the PR also to see if looks good to you as well? |
Co-authored-by: Pablo Galindo <Pablogsal@gmail.com>
Address requested changes.
|
Thanks, @curtisbucher, for the quick turnaround! |
isidentical
left a comment
There was a problem hiding this comment.
Looks great, thanks @curtisbucher
commented
May 5, 2020
|
Thanks @curtisbucher for the PR, and @pablogsal for merging it 🌮🎉.. I'm working now to backport this PR to: 3.7, 3.8. |
commented
May 5, 2020
|
GH-19947 is a backport of this pull request to the 3.8 branch. |
commented
May 5, 2020
|
Sorry, @curtisbucher and @pablogsal, I could not cleanly backport this to |
commented
May 5, 2020
|
Thanks for the PR @curtisbucher ! 🎉 |
commented
May 5, 2020
|
Hummm, seems that the backport to 3.7 failed? @curtisbucher could you follow @miss-islington instructions to do the manual backport? Ping me there so I can merge it |
commented
May 5, 2020
|
Actually....is this something we should backport? This can break some code in 3.8 and 3.7 that was previously working (somehow incorrectly). Not sure if is worth the trouble.... |
commented
May 5, 2020
|
@brandtbucher @isidentical opinions? |
commented
May 5, 2020
Agreed. IMHO it is an improvement rather then a bugfix, so we may skip backporting. |
This doesn't fix
ast.unparse.@brandtbucher
https://bugs.python.org/issue40355