1#!/usr/bin/env python 2# encoding: utf-8 3# Thomas Nagy, 2005-2018 (ita) 4 5""" 6 7ConfigSet: a special dict 8 9The values put in :py:class:`ConfigSet` must be serializable (dicts, lists, strings) 10""" 11 12import copy, re, os 13from waflib import Logs, Utils 14re_imp = re.compile(r'^(#)*?([^#=]*?)\ =\ (.*?)$', re.M) 15 16class ConfigSet(object): 17 """ 18 A copy-on-write dict with human-readable serialized format. The serialization format 19 is human-readable (python-like) and performed by using eval() and repr(). 20 For high performance prefer pickle. Do not store functions as they are not serializable. 21 22 The values can be accessed by attributes or by keys:: 23 24 from waflib.ConfigSet import ConfigSet 25 env = ConfigSet() 26 env.FOO = 'test' 27 env['FOO'] = 'test' 28 """ 29 __slots__ = ('table', 'parent') 30 def __init__(self, filename=None): 31 self.table = {} 32 """ 33 Internal dict holding the object values 34 """ 35 #self.parent = None 36 37 if filename: 38 self.load(filename) 39 40 def __contains__(self, key): 41 """ 42 Enables the *in* syntax:: 43 44 if 'foo' in env: 45 print(env['foo']) 46 """ 47 if key in self.table: 48 return True 49 try: 50 return self.parent.__contains__(key) 51 except AttributeError: 52 return False # parent may not exist 53 54 def keys(self): 55 """Dict interface""" 56 keys = set() 57 cur = self 58 while cur: 59 keys.update(cur.table.keys()) 60 cur = getattr(cur, 'parent', None) 61 keys = list(keys) 62 keys.sort() 63 return keys 64 65 def __iter__(self): 66 return iter(self.keys()) 67 68 def __str__(self): 69 """Text representation of the ConfigSet (for debugging purposes)""" 70 return "\n".join(["%r %r" % (x, self.__getitem__(x)) for x in self.keys()]) 71 72 def __getitem__(self, key): 73 """ 74 Dictionary interface: get value from key:: 75 76 def configure(conf): 77 conf.env['foo'] = {} 78 print(env['foo']) 79 """ 80 try: 81 while 1: 82 x = self.table.get(key) 83 if not x is None: 84 return x 85 self = self.parent 86 except AttributeError: 87 return [] 88 89 def __setitem__(self, key, value): 90 """ 91 Dictionary interface: set value from key 92 """ 93 self.table[key] = value 94 95 def __delitem__(self, key): 96 """ 97 Dictionary interface: mark the value as missing 98 """ 99 self[key] = [] 100 101 def __getattr__(self, name): 102 """ 103 Attribute access provided for convenience. The following forms are equivalent:: 104 105 def configure(conf): 106 conf.env.value 107 conf.env['value'] 108 """ 109 if name in self.__slots__: 110 return object.__getattribute__(self, name) 111 else: 112 return self[name] 113 114 def __setattr__(self, name, value): 115 """ 116 Attribute access provided for convenience. The following forms are equivalent:: 117 118 def configure(conf): 119 conf.env.value = x 120 env['value'] = x 121 """ 122 if name in self.__slots__: 123 object.__setattr__(self, name, value) 124 else: 125 self[name] = value 126 127 def __delattr__(self, name): 128 """ 129 Attribute access provided for convenience. The following forms are equivalent:: 130 131 def configure(conf): 132 del env.value 133 del env['value'] 134 """ 135 if name in self.__slots__: 136 object.__delattr__(self, name) 137 else: 138 del self[name] 139 140 def derive(self): 141 """ 142 Returns a new ConfigSet deriving from self. The copy returned 143 will be a shallow copy:: 144 145 from waflib.ConfigSet import ConfigSet 146 env = ConfigSet() 147 env.append_value('CFLAGS', ['-O2']) 148 child = env.derive() 149 child.CFLAGS.append('test') # warning! this will modify 'env' 150 child.CFLAGS = ['-O3'] # new list, ok 151 child.append_value('CFLAGS', ['-O3']) # ok 152 153 Use :py:func:`ConfigSet.detach` to detach the child from the parent. 154 """ 155 newenv = ConfigSet() 156 newenv.parent = self 157 return newenv 158 159 def detach(self): 160 """ 161 Detaches this instance from its parent (if present) 162 163 Modifying the parent :py:class:`ConfigSet` will not change the current object 164 Modifying this :py:class:`ConfigSet` will not modify the parent one. 165 """ 166 tbl = self.get_merged_dict() 167 try: 168 delattr(self, 'parent') 169 except AttributeError: 170 pass 171 else: 172 keys = tbl.keys() 173 for x in keys: 174 tbl[x] = copy.deepcopy(tbl[x]) 175 self.table = tbl 176 return self 177 178 def get_flat(self, key): 179 """ 180 Returns a value as a string. If the input is a list, the value returned is space-separated. 181 182 :param key: key to use 183 :type key: string 184 """ 185 s = self[key] 186 if isinstance(s, str): 187 return s 188 return ' '.join(s) 189 190 def _get_list_value_for_modification(self, key): 191 """ 192 Returns a list value for further modification. 193 194 The list may be modified inplace and there is no need to do this afterwards:: 195 196 self.table[var] = value 197 """ 198 try: 199 value = self.table[key] 200 except KeyError: 201 try: 202 value = self.parent[key] 203 except AttributeError: 204 value = [] 205 else: 206 if isinstance(value, list): 207 # force a copy 208 value = value[:] 209 else: 210 value = [value] 211 self.table[key] = value 212 else: 213 if not isinstance(value, list): 214 self.table[key] = value = [value] 215 return value 216 217 def append_value(self, var, val): 218 """ 219 Appends a value to the specified config key:: 220 221 def build(bld): 222 bld.env.append_value('CFLAGS', ['-O2']) 223 224 The value must be a list or a tuple 225 """ 226 if isinstance(val, str): # if there were string everywhere we could optimize this 227 val = [val] 228 current_value = self._get_list_value_for_modification(var) 229 current_value.extend(val) 230 231 def prepend_value(self, var, val): 232 """ 233 Prepends a value to the specified item:: 234 235 def configure(conf): 236 conf.env.prepend_value('CFLAGS', ['-O2']) 237 238 The value must be a list or a tuple 239 """ 240 if isinstance(val, str): 241 val = [val] 242 self.table[var] = val + self._get_list_value_for_modification(var) 243 244 def append_unique(self, var, val): 245 """ 246 Appends a value to the specified item only if it's not already present:: 247 248 def build(bld): 249 bld.env.append_unique('CFLAGS', ['-O2', '-g']) 250 251 The value must be a list or a tuple 252 """ 253 if isinstance(val, str): 254 val = [val] 255 current_value = self._get_list_value_for_modification(var) 256 257 for x in val: 258 if x not in current_value: 259 current_value.append(x) 260 261 def get_merged_dict(self): 262 """ 263 Computes the merged dictionary from the fusion of self and all its parent 264 265 :rtype: a ConfigSet object 266 """ 267 table_list = [] 268 env = self 269 while 1: 270 table_list.insert(0, env.table) 271 try: 272 env = env.parent 273 except AttributeError: 274 break 275 merged_table = {} 276 for table in table_list: 277 merged_table.update(table) 278 return merged_table 279 280 def store(self, filename): 281 """ 282 Serializes the :py:class:`ConfigSet` data to a file. See :py:meth:`ConfigSet.load` for reading such files. 283 284 :param filename: file to use 285 :type filename: string 286 """ 287 try: 288 os.makedirs(os.path.split(filename)[0]) 289 except OSError: 290 pass 291 292 buf = [] 293 merged_table = self.get_merged_dict() 294 keys = list(merged_table.keys()) 295 keys.sort() 296 297 try: 298 fun = ascii 299 except NameError: 300 fun = repr 301 302 for k in keys: 303 if k != 'undo_stack': 304 buf.append('%s = %s\n' % (k, fun(merged_table[k]))) 305 Utils.writef(filename, ''.join(buf)) 306 307 def load(self, filename): 308 """ 309 Restores contents from a file (current values are not cleared). Files are written using :py:meth:`ConfigSet.store`. 310 311 :param filename: file to use 312 :type filename: string 313 """ 314 tbl = self.table 315 code = Utils.readf(filename, m='r') 316 for m in re_imp.finditer(code): 317 g = m.group 318 tbl[g(2)] = eval(g(3)) 319 Logs.debug('env: %s', self.table) 320 321 def update(self, d): 322 """ 323 Dictionary interface: replace values with the ones from another dict 324 325 :param d: object to use the value from 326 :type d: dict-like object 327 """ 328 self.table.update(d) 329 330 def stash(self): 331 """ 332 Stores the object state to provide transactionality semantics:: 333 334 env = ConfigSet() 335 env.stash() 336 try: 337 env.append_value('CFLAGS', '-O3') 338 call_some_method(env) 339 finally: 340 env.revert() 341 342 The history is kept in a stack, and is lost during the serialization by :py:meth:`ConfigSet.store` 343 """ 344 orig = self.table 345 tbl = self.table = self.table.copy() 346 for x in tbl.keys(): 347 tbl[x] = copy.deepcopy(tbl[x]) 348 self.undo_stack = self.undo_stack + [orig] 349 350 def commit(self): 351 """ 352 Commits transactional changes. See :py:meth:`ConfigSet.stash` 353 """ 354 self.undo_stack.pop(-1) 355 356 def revert(self): 357 """ 358 Reverts the object to a previous state. See :py:meth:`ConfigSet.stash` 359 """ 360 self.table = self.undo_stack.pop(-1) 361 362