From 9d1b5d4113f5fb67bf5ded5b67a8a0a748bb3036 Mon Sep 17 00:00:00 2001 From: anansutiawan <77756125+anansutiawan@users.noreply.github.com> Date: Tue, 12 May 2026 23:25:03 +0700 Subject: [PATCH] Remove eval from schema parser --- yamale/syntax/parser.py | 37 ++++++++++++++++++++++++++---- yamale/syntax/tests/test_parser.py | 28 +++++++++------------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/yamale/syntax/parser.py b/yamale/syntax/parser.py index ae71ef9..0b0f8e8 100644 --- a/yamale/syntax/parser.py +++ b/yamale/syntax/parser.py @@ -2,9 +2,6 @@ from .. import validators as val -safe_globals = ("True", "False", "None") -safe_builtins = dict((f, __builtins__[f]) for f in safe_globals) - def _validate_expr(call_node, validators): # Validate that the expression uses a known, registered validator. @@ -28,12 +25,42 @@ def _validate_expr(call_node, validators): raise SyntaxError("Argument values must either be constant literals, or else " "reference other validators.") +def _eval_literal(node, validators): + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.Name) and node.id in validators: + return validators[node.id] + if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant): + value = node.operand.value + if isinstance(node.op, ast.UAdd): + return +value + if isinstance(node.op, ast.USub): + return -value + if isinstance(node.op, ast.Not): + return not value + if isinstance(node.op, ast.Invert): + return ~value + if isinstance(node, ast.Call): + return _construct_validator(node, validators) + raise SyntaxError("Argument values must either be constant literals, or else reference other validators.") + + +def _construct_validator(call_node, validators): + func_name = call_node.func.id + args = [_eval_literal(arg, validators) for arg in call_node.args] + kwargs = {} + for keyword in call_node.keywords: + if keyword.arg is None: + raise SyntaxError("Keyword argument unpacking is not supported.") + kwargs[keyword.arg] = _eval_literal(keyword.value, validators) + return validators[func_name](*args, **kwargs) + + def parse(validator_string, validators=None): validators = validators or val.DefaultValidators try: tree = ast.parse(validator_string, mode="eval") _validate_expr(tree.body, validators) - # evaluate with access to a limited global scope only - return eval(compile(tree, "", "eval"), {"__builtins__": safe_builtins}, validators) + return _construct_validator(tree.body, validators) except (SyntaxError, NameError, TypeError) as e: raise SyntaxError("Invalid schema expression: '%s'. " % validator_string + str(e)) diff --git a/yamale/syntax/tests/test_parser.py b/yamale/syntax/tests/test_parser.py index 405e9af..de00497 100644 --- a/yamale/syntax/tests/test_parser.py +++ b/yamale/syntax/tests/test_parser.py @@ -1,23 +1,7 @@ from pytest import raises from .. import parser as par -from yamale.validators.validators import ( - Validator, - String, - Regex, - Number, - Integer, - Boolean, - List, - Day, - Timestamp, - Ip, - Mac, -) - - -def test_eval(): - assert eval("String()") == String() +from yamale.validators.validators import Validator, String, Regex, Number, Integer, Boolean, List, Day, Timestamp, Ip, Mac def test_types(): @@ -34,6 +18,16 @@ def test_types(): assert par.parse("mac()") == Mac() +def test_nested_type_reference(): + parsed = par.parse("list(list(num, min=2, max=2), min=2, max=2)") + assert parsed == List(List(Number, min=2, max=2), min=2, max=2) + + +def test_unary_constants(): + parsed = par.parse("num(min=-1, max=+1)") + assert parsed == Number(min=-1, max=1) + + def test_custom_type(): class my_validator(Validator): pass