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