Skip to content

Commit

Permalink
gclient eval: Get rid of global_scope, local_scope and functions othe…
Browse files Browse the repository at this point in the history
…r than Var.

Since we only support Var, and Var only does one thing, we hard code that
into the parser. Then, we don't need global_scope.

local_scope hasn't been needed for a while.

Bug: 760633
Change-Id: I21b171a8b71e7dcaf8e29c780ca88b9f46f368e8
Reviewed-on: https://chromium-review.googlesource.com/972611
Reviewed-by: Aaron Gable <agable@chromium.org>
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
  • Loading branch information
Edward Lesmes authored and Commit Bot committed Mar 21, 2018
1 parent b3065fb commit 9f53129
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 53 deletions.
16 changes: 6 additions & 10 deletions gclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,16 +765,14 @@ def ParseDepsFile(self):

local_scope = {}
if deps_content:
global_scope = {
'Var': lambda var_name: '{%s}' % var_name,
'deps_os': {},
}
# Eval the content.
try:
if self._get_option('validate_syntax', False):
local_scope = gclient_eval.Exec(
deps_content, global_scope, local_scope, filepath)
local_scope = gclient_eval.Exec(deps_content, filepath)
else:
global_scope = {
'Var': lambda var_name: '{%s}' % var_name,
}
exec(deps_content, global_scope, local_scope)
except SyntaxError as e:
gclient_utils.SyntaxErrorToError(filepath, e)
Expand Down Expand Up @@ -2880,14 +2878,12 @@ def CMDsetdep(parser, args):
'file in the current directory.')
(options, args) = parser.parse_args(args)

global_scope = {'Var': lambda var: '{%s}' % var}

if not os.path.isfile(options.deps_file):
raise gclient_utils.Error(
'DEPS file %s does not exist.' % options.deps_file)
with open(options.deps_file) as f:
contents = f.read()
local_scope = gclient_eval.Exec(contents, global_scope, {})
local_scope = gclient_eval.Exec(contents)

for var in options.vars:
name, _, value = var.partition('=')
Expand All @@ -2909,7 +2905,7 @@ def CMDsetdep(parser, args):
% (name, package))
gclient_eval.SetCIPD(local_scope, name, package, value)
else:
gclient_eval.SetRevision(local_scope, global_scope, name, value)
gclient_eval.SetRevision(local_scope, name, value)

with open(options.deps_file, 'w') as f:
f.write(gclient_eval.RenderDEPSFile(local_scope))
Expand Down
26 changes: 15 additions & 11 deletions gclient_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def validate(d):
}))


def _gclient_eval(node_or_string, global_scope, filename='<unknown>'):
def _gclient_eval(node_or_string, filename='<unknown>'):
"""Safely evaluates a single expression. Returns the result."""
_allowed_names = {'None': None, 'True': True, 'False': False}
if isinstance(node_or_string, basestring):
Expand All @@ -209,16 +209,20 @@ def _convert(node):
node.id, filename, getattr(node, 'lineno', '<unknown>')))
return _allowed_names[node.id]
elif isinstance(node, ast.Call):
if not isinstance(node.func, ast.Name):
if not isinstance(node.func, ast.Name) or node.func.id != 'Var':
raise ValueError(
'invalid call: func should be a name (file %r, line %s)' % (
'Var is the only allowed function (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
if node.keywords or node.starargs or node.kwargs:
if node.keywords or node.starargs or node.kwargs or len(node.args) != 1:
raise ValueError(
'invalid call: use only regular args (file %r, line %s)' % (
'Var takes exactly one argument (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
args = map(_convert, node.args)
return global_scope[node.func.id](*args)
arg = _convert(node.args[0])
if not isinstance(arg, basestring):
raise ValueError(
'Var\'s argument must be a variable name (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
return '{%s}' % arg
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
return _convert(node.left) + _convert(node.right)
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
Expand All @@ -231,7 +235,7 @@ def _convert(node):
return _convert(node_or_string)


def Exec(content, global_scope, local_scope, filename='<unknown>'):
def Exec(content, filename='<unknown>'):
"""Safely execs a set of assignments. Mutates |local_scope|."""
node_or_string = ast.parse(content, filename=filename, mode='exec')
if isinstance(node_or_string, ast.Expression):
Expand All @@ -249,7 +253,7 @@ def _visit_in_module(node):
raise ValueError(
'invalid assignment: target should be a name (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
value = _gclient_eval(node.value, global_scope, filename=filename)
value = _gclient_eval(node.value, filename=filename)

if target.id in defined_variables:
raise ValueError(
Expand Down Expand Up @@ -449,7 +453,7 @@ def SetCIPD(gclient_dict, dep_name, package_name, new_version):
packages[0]._SetNode('version', new_version, node)


def SetRevision(gclient_dict, global_scope, dep_name, new_revision):
def SetRevision(gclient_dict, dep_name, new_revision):
if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
raise ValueError(
"Can't use SetRevision for the given gclient dict. It contains no "
Expand All @@ -473,7 +477,7 @@ def _UpdateRevision(dep_dict, dep_key):
SetVar(gclient_dict, node.args[0].s, new_revision)
else:
_UpdateAstString(tokens, node, new_revision)
value = _gclient_eval(dep_node, global_scope)
value = _gclient_eval(dep_node)
dep_dict._SetNode(dep_key, value, dep_node)

if isinstance(gclient_dict['deps'][dep_name], _NodeDict):
Expand Down
56 changes: 24 additions & 32 deletions tests/gclient_eval_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,102 +60,96 @@

class GClientEvalTest(unittest.TestCase):
def test_str(self):
self.assertEqual('foo', gclient_eval._gclient_eval('"foo"', {}))
self.assertEqual('foo', gclient_eval._gclient_eval('"foo"'))

def test_tuple(self):
self.assertEqual(('a', 'b'), gclient_eval._gclient_eval('("a", "b")', {}))
self.assertEqual(('a', 'b'), gclient_eval._gclient_eval('("a", "b")'))

def test_list(self):
self.assertEqual(['a', 'b'], gclient_eval._gclient_eval('["a", "b"]', {}))
self.assertEqual(['a', 'b'], gclient_eval._gclient_eval('["a", "b"]'))

def test_dict(self):
self.assertEqual({'a': 'b'}, gclient_eval._gclient_eval('{"a": "b"}', {}))
self.assertEqual({'a': 'b'}, gclient_eval._gclient_eval('{"a": "b"}'))

def test_name_safe(self):
self.assertEqual(True, gclient_eval._gclient_eval('True', {}))
self.assertEqual(True, gclient_eval._gclient_eval('True'))

def test_name_unsafe(self):
with self.assertRaises(ValueError) as cm:
gclient_eval._gclient_eval('UnsafeName', {'UnsafeName': 'foo'})
gclient_eval._gclient_eval('UnsafeName')
self.assertIn('invalid name \'UnsafeName\'', str(cm.exception))

def test_call(self):
self.assertEqual(
'bar',
gclient_eval._gclient_eval('Foo("bar")', {'Foo': lambda x: x}))
'{bar}',
gclient_eval._gclient_eval('Var("bar")'))

def test_plus(self):
self.assertEqual('foo', gclient_eval._gclient_eval('"f" + "o" + "o"', {}))
self.assertEqual('foo', gclient_eval._gclient_eval('"f" + "o" + "o"'))

def test_format(self):
self.assertEqual('foo', gclient_eval._gclient_eval('"%s" % "foo"', {}))
self.assertEqual('foo', gclient_eval._gclient_eval('"%s" % "foo"'))

def test_not_expression(self):
with self.assertRaises(SyntaxError) as cm:
gclient_eval._gclient_eval('def foo():\n pass', {})
gclient_eval._gclient_eval('def foo():\n pass')
self.assertIn('invalid syntax', str(cm.exception))

def test_not_whitelisted(self):
with self.assertRaises(ValueError) as cm:
gclient_eval._gclient_eval('[x for x in [1, 2, 3]]', {})
gclient_eval._gclient_eval('[x for x in [1, 2, 3]]')
self.assertIn(
'unexpected AST node: <_ast.ListComp object', str(cm.exception))

def test_dict_ordered(self):
for test_case in itertools.permutations(range(4)):
input_data = ['{'] + ['"%s": "%s",' % (n, n) for n in test_case] + ['}']
expected = [(str(n), str(n)) for n in test_case]
result = gclient_eval._gclient_eval(''.join(input_data), {})
result = gclient_eval._gclient_eval(''.join(input_data))
self.assertEqual(expected, result.items())


class ExecTest(unittest.TestCase):
def test_multiple_assignment(self):
with self.assertRaises(ValueError) as cm:
gclient_eval.Exec('a, b, c = "a", "b", "c"', {}, {})
gclient_eval.Exec('a, b, c = "a", "b", "c"')
self.assertIn(
'invalid assignment: target should be a name', str(cm.exception))

def test_override(self):
with self.assertRaises(ValueError) as cm:
gclient_eval.Exec('a = "a"\na = "x"', {}, {})
gclient_eval.Exec('a = "a"\na = "x"')
self.assertIn(
'invalid assignment: overrides var \'a\'', str(cm.exception))

def test_schema_wrong_type(self):
with self.assertRaises(schema.SchemaError):
gclient_eval.Exec('include_rules = {}', {}, {}, '<string>')
gclient_eval.Exec('include_rules = {}', '<string>')

def test_recursedeps_list(self):
local_scope = {}
local_scope = gclient_eval.Exec(
'recursedeps = [["src/third_party/angle", "DEPS.chromium"]]',
{}, local_scope,
'<string>')
self.assertEqual(
{'recursedeps': [['src/third_party/angle', 'DEPS.chromium']]},
local_scope)

def test_var(self):
local_scope = {}
global_scope = {
'Var': lambda var_name: '{%s}' % var_name,
}
local_scope = gclient_eval.Exec('\n'.join([
'vars = {',
' "foo": "bar",',
'}',
'deps = {',
' "a_dep": "a" + Var("foo") + "b",',
'}',
]), global_scope, local_scope, '<string>')
]), '<string>')
self.assertEqual({
'vars': collections.OrderedDict([('foo', 'bar')]),
'deps': collections.OrderedDict([('a_dep', 'a{foo}b')]),
}, local_scope)

def test_empty_deps(self):
local_scope = gclient_eval.Exec('deps = {}', {}, {}, '<string>')
local_scope = gclient_eval.Exec('deps = {}', '<string>')
self.assertEqual({'deps': {}}, local_scope)


Expand Down Expand Up @@ -211,7 +205,7 @@ def test_string_bool_typo(self):

class SetVarTest(unittest.TestCase):
def testSetVar(self):
local_scope = gclient_eval.Exec(_SAMPLE_DEPS_FILE, {'Var': str}, {})
local_scope = gclient_eval.Exec(_SAMPLE_DEPS_FILE)

gclient_eval.SetVar(local_scope, 'dep_2_rev', 'c0ffee')
result = gclient_eval.RenderDEPSFile(local_scope)
Expand All @@ -223,7 +217,7 @@ def testSetVar(self):

class SetCipdTest(unittest.TestCase):
def testSetCIPD(self):
local_scope = gclient_eval.Exec(_SAMPLE_DEPS_FILE, {'Var': str}, {})
local_scope = gclient_eval.Exec(_SAMPLE_DEPS_FILE)

gclient_eval.SetCIPD(
local_scope, 'src/cipd/package', 'another/cipd/package', '6.789')
Expand All @@ -234,27 +228,25 @@ def testSetCIPD(self):

class SetRevisionTest(unittest.TestCase):
def setUp(self):
self.global_scope = {'Var': str}
self.local_scope = gclient_eval.Exec(
_SAMPLE_DEPS_FILE, self.global_scope, {})
self.local_scope = gclient_eval.Exec(_SAMPLE_DEPS_FILE)

def testSetRevision(self):
gclient_eval.SetRevision(
self.local_scope, self.global_scope, 'src/dep', 'deadfeed')
self.local_scope, 'src/dep', 'deadfeed')
result = gclient_eval.RenderDEPSFile(self.local_scope)

self.assertEqual(result, _SAMPLE_DEPS_FILE.replace('deadbeef', 'deadfeed'))

def testSetRevisionInUrl(self):
gclient_eval.SetRevision(
self.local_scope, self.global_scope, 'src/dep_3', '0ff1ce')
self.local_scope, 'src/dep_3', '0ff1ce')
result = gclient_eval.RenderDEPSFile(self.local_scope)

self.assertEqual(result, _SAMPLE_DEPS_FILE.replace('5p1e5', '0ff1ce'))

def testSetRevisionInVars(self):
gclient_eval.SetRevision(
self.local_scope, self.global_scope, 'src/android/dep_2', 'c0ffee')
self.local_scope, 'src/android/dep_2', 'c0ffee')
result = gclient_eval.RenderDEPSFile(self.local_scope)

self.assertEqual(result, _SAMPLE_DEPS_FILE.replace('1ced', 'c0ffee'))
Expand Down

0 comments on commit 9f53129

Please sign in to comment.