Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions Lib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,25 @@ def literal_eval(node_or_string):
"""
if isinstance(node_or_string, str):
node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval').body
return _convert_literal(node_or_string, True)
elif isinstance(node_or_string, Expression):
node_or_string = node_or_string.body
return _convert_literal(node_or_string)


def _convert_literal(node):
_permitted_literal_types = (str, bytes, int, float, complex,
bool, type(None), type(...))


def _convert_literal(node, omit_validation=False):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need this change? It may make life harder for some users.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could you be more precise, any example?

We need this, because literal_eval() accepts invalid AST for non-string input. Per documentation:

The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, None and Ellipsis.

"""
Used by `literal_eval` to convert an AST node into a value.
"""
if isinstance(node, Constant):
return node.value
if omit_validation:
return node.value
if type(value := node.value) in _permitted_literal_types:
return value
if isinstance(node, Dict) and len(node.keys) == len(node.values):
return dict(zip(
map(_convert_literal, node.keys),
Expand Down
31 changes: 13 additions & 18 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2209,7 +2209,7 @@ def wrap_value(s):

if isinstance(value, (str, int, float, bytes, bool, type(None),
sentinel)):
return ast.Constant(value)
return ast.parse(s, mode='eval').body
raise ValueError

class RewriteSymbolics(ast.NodeTransformer):
Expand All @@ -2230,28 +2230,23 @@ def visit_Name(self, node):
raise ValueError()
return wrap_value(node.id)

def visit_BinOp(self, node):
# Support constant folding of a couple simple binary operations
# commonly used to define default values in text signatures
left = self.visit(node.left)
right = self.visit(node.right)
if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant):
raise ValueError
if isinstance(node.op, ast.Add):
return ast.Constant(left.value + right.value)
elif isinstance(node.op, ast.Sub):
return ast.Constant(left.value - right.value)
elif isinstance(node.op, ast.BitOr):
return ast.Constant(left.value | right.value)
raise ValueError

def p(name_node, default_node, default=empty):
name = parse_name(name_node)
if default_node and default_node is not _empty:
try:
default_node = RewriteSymbolics().visit(default_node)
default = ast.literal_eval(default_node)
except ValueError:
default_source = ast.unparse(default_node)
try:
default = ast.literal_eval(default_source)
except ValueError:
try:
default = eval(default_source, module_dict)
except NameError:
try:
default = eval(default_source, sys_module_dict)
except NameError:
raise ValueError
except ValueError as exc:
raise ValueError("{!r} builtin has invalid signature".format(obj)) from None
parameters.append(Parameter(name, kind, default=default, annotation=empty))

Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1981,6 +1981,10 @@ def test_literal_eval(self):
self.assertRaises(ValueError, ast.literal_eval, '++6')
self.assertRaises(ValueError, ast.literal_eval, '+True')
self.assertRaises(ValueError, ast.literal_eval, '2+3')
# gh-141778: reject values of invalid types
node = ast.Expression(body=ast.Constant(object()))
ast.fix_missing_locations(node)
self.assertRaises(ValueError, ast.literal_eval, node)

def test_literal_eval_str_int_limit(self):
with support.adjust_int_max_str_digits(4000):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -6287,9 +6287,9 @@ def test_operator_module_has_signatures(self):
self._test_module_has_signatures(operator)

def test_os_module_has_signatures(self):
unsupported_signature = {'chmod', 'utime'}
unsupported_signature = {'utime'}
unsupported_signature |= {name for name in
['get_terminal_size', 'link', 'register_at_fork', 'startfile']
['get_terminal_size', 'register_at_fork', 'startfile']
if hasattr(os, name)}
self._test_module_has_signatures(os, unsupported_signature=unsupported_signature)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Validate value types of :class:`ast.Constant` nodes in the
:func:`ast.literal_eval`. Patch by Sergey B Kirpichev.
Loading