From 7e50261c2cd2868778937d06e51bc52712d0365f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Hajdan=2C=20Jr?= Date: Mon, 12 Jun 2017 16:58:38 +0200 Subject: [PATCH] gclient: freeze vars to prevent accidental modification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will also be useful for other values (deps_os, hooks_os) we may want to store and keep constant. Freeze code copied from luci/recipes-py @ 944125e6d1e8c831d09517bde658a38d8f81db37 Bug: 570091 Change-Id: I3365cf2b267c478316870bbb3fd41e9955aa4ddf Reviewed-on: https://chromium-review.googlesource.com/531166 Commit-Queue: Paweł Hajdan Jr. Reviewed-by: Andrii Shyshkalov --- gclient.py | 7 ++--- gclient_utils.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/gclient.py b/gclient.py index fcdc1b8ffb..0fb51d9879 100755 --- a/gclient.py +++ b/gclient.py @@ -616,7 +616,9 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): 'ParseDepsFile(%s): Strict mode disallows %r -> %r' % (self.name, key, val)) - self._vars = local_scope.get('vars', {}) + # Since we heavily post-process things, freeze ones which should + # reflect original state of DEPS. + self._vars = gclient_utils.freeze(local_scope.get('vars', {})) deps = local_scope.get('deps', {}) if 'recursion' in local_scope: @@ -718,8 +720,7 @@ class Dependency(gclient_utils.WorkItem, DependencySettings): condition = dep_value.get('condition') if condition: # TODO(phajdan.jr): should we also take custom vars into account? - condition_value = gclient_eval.EvaluateCondition( - condition, local_scope.get('vars', {})) + condition_value = gclient_eval.EvaluateCondition(condition, self._vars) should_process = should_process and condition_value deps_to_add.append(Dependency( self, name, url, None, None, self.custom_vars, None, diff --git a/gclient_utils.py b/gclient_utils.py index 8fe0c5ed83..e60c069397 100644 --- a/gclient_utils.py +++ b/gclient_utils.py @@ -5,10 +5,12 @@ """Generic utils.""" import codecs +import collections import contextlib import cStringIO import datetime import logging +import operator import os import pipes import platform @@ -1236,3 +1238,67 @@ def FindExecutable(executable): if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK): return alt_target return None + + +def freeze(obj): + """Takes a generic object ``obj``, and returns an immutable version of it. + + Supported types: + * dict / OrderedDict -> FrozenDict + * list -> tuple + * set -> frozenset + * any object with a working __hash__ implementation (assumes that hashable + means immutable) + + Will raise TypeError if you pass an object which is not hashable. + """ + if isinstance(obj, dict): + return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems()) + elif isinstance(obj, (list, tuple)): + return tuple(freeze(i) for i in obj) + elif isinstance(obj, set): + return frozenset(freeze(i) for i in obj) + else: + hash(obj) + return obj + + +class FrozenDict(collections.Mapping): + """An immutable OrderedDict. + + Modified From: http://stackoverflow.com/a/2704866 + """ + def __init__(self, *args, **kwargs): + self._d = collections.OrderedDict(*args, **kwargs) + + # Calculate the hash immediately so that we know all the items are + # hashable too. + self._hash = reduce(operator.xor, + (hash(i) for i in enumerate(self._d.iteritems())), 0) + + def __eq__(self, other): + if not isinstance(other, collections.Mapping): + return NotImplemented + if self is other: + return True + if len(self) != len(other): + return False + for k, v in self.iteritems(): + if k not in other or other[k] != v: + return False + return True + + def __iter__(self): + return iter(self._d) + + def __len__(self): + return len(self._d) + + def __getitem__(self, key): + return self._d[key] + + def __hash__(self): + return self._hash + + def __repr__(self): + return 'FrozenDict(%r)' % (self._d.items(),)