summaryrefslogtreecommitdiffstats
path: root/powerline/lib/config.py
blob: 0c95e47658e20fe236c486404e7a1cbff3cd8bdd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import json
import codecs

from copy import deepcopy
from threading import Event, Lock
from collections import defaultdict

from powerline.lib.threaded import MultiRunnedThread
from powerline.lib.watcher import create_file_watcher


def open_file(path):
	return codecs.open(path, encoding='utf-8')


def load_json_config(config_file_path, load=json.load, open_file=open_file):
	with open_file(config_file_path) as config_file_fp:
		return load(config_file_fp)


class DummyWatcher(object):
	def __call__(self, *args, **kwargs):
		return False

	def watch(self, *args, **kwargs):
		pass


class DeferredWatcher(object):
	def __init__(self, *args, **kwargs):
		self.args = args
		self.kwargs = kwargs
		self.calls = []

	def __call__(self, *args, **kwargs):
		self.calls.append(('__call__', args, kwargs))

	def watch(self, *args, **kwargs):
		self.calls.append(('watch', args, kwargs))

	def unwatch(self, *args, **kwargs):
		self.calls.append(('unwatch', args, kwargs))

	def transfer_calls(self, watcher):
		for attr, args, kwargs in self.calls:
			getattr(watcher, attr)(*args, **kwargs)


class ConfigLoader(MultiRunnedThread):
	def __init__(self, shutdown_event=None, watcher=None, watcher_type=None, load=load_json_config, run_once=False):
		super(ConfigLoader, self).__init__()
		self.shutdown_event = shutdown_event or Event()
		if run_once:
			self.watcher = DummyWatcher()
			self.watcher_type = 'dummy'
		else:
			self.watcher = watcher or DeferredWatcher()
			if watcher:
				if not watcher_type:
					raise ValueError('When specifying watcher you must also specify watcher type')
				self.watcher_type = watcher_type
			else:
				self.watcher_type = 'deferred'
		self._load = load

		self.pl = None
		self.interval = None

		self.lock = Lock()

		self.watched = defaultdict(set)
		self.missing = defaultdict(set)
		self.loaded = {}

	def set_watcher(self, watcher_type, force=False):
		if watcher_type == self.watcher_type:
			return
		watcher = create_file_watcher(self.pl, watcher_type)
		with self.lock:
			if self.watcher_type == 'deferred':
				self.watcher.transfer_calls(watcher)
			self.watcher = watcher
			self.watcher_type = watcher_type

	def set_pl(self, pl):
		self.pl = pl

	def set_interval(self, interval):
		self.interval = interval

	def register(self, function, path):
		'''Register function that will be run when file changes.

		:param function function:
			Function that will be called when file at the given path changes.
		:param str path:
			Path that will be watched for.
		'''
		with self.lock:
			self.watched[path].add(function)
			self.watcher.watch(path)

	def register_missing(self, condition_function, function, key):
		'''Register any function that will be called with given key each 
		interval seconds (interval is defined at __init__). Its result is then 
		passed to ``function``, but only if the result is true.

		:param function condition_function:
			Function which will be called each ``interval`` seconds. All 
			exceptions from it will be logged and ignored. IOError exception 
			will be ignored without logging.
		:param function function:
			Function which will be called if condition_function returns 
			something that is true. Accepts result of condition_function as an 
			argument.
		:param str key:
			Any value, it will be passed to condition_function on each call.

		Note: registered functions will be automatically removed if 
		condition_function results in something true.
		'''
		with self.lock:
			self.missing[key].add((condition_function, function))

	def unregister_functions(self, removed_functions):
		'''Unregister files handled by these functions.

		:param set removed_functions:
			Set of functions previously passed to ``.register()`` method.
		'''
		with self.lock:
			for path, functions in list(self.watched.items()):
				functions -= removed_functions
				if not functions:
					self.watched.pop(path)
					self.loaded.pop(path, None)

	def unregister_missing(self, removed_functions):
		'''Unregister files handled by these functions.

		:param set removed_functions:
			Set of pairs (2-tuples) representing ``(condition_function, 
			function)`` function pairs previously passed as an arguments to 
			``.register_missing()`` method.
		'''
		with self.lock:
			for key, functions in list(self.missing.items()):
				functions -= removed_functions
				if not functions:
					self.missing.pop(key)

	def load(self, path):
		try:
			# No locks: GIL does what we need
			return deepcopy(self.loaded[path])
		except KeyError:
			r = self._load(path)
			self.loaded[path] = deepcopy(r)
			return r

	def update(self):
		toload = []
		with self.lock:
			for path, functions in self.watched.items():
				for function in functions:
					try:
						modified = self.watcher(path)
					except OSError as e:
						modified = True
						self.exception('Error while running watcher for path {0}: {1}', path, str(e))
					else:
						if modified:
							toload.append(path)
					if modified:
						function(path)
		with self.lock:
			for key, functions in list(self.missing.items()):
				for condition_function, function in list(functions):
					try:
						path = condition_function(key)
					except IOError:
						pass
					except Exception as e:
						self.exception('Error while running condition function for key {0}: {1}', key, str(e))
					else:
						if path:
							toload.append(path)
							function(path)
							functions.remove((condition_function, function))
				if not functions:
					self.missing.pop(key)
		for path in toload:
			try:
				self.loaded[path] = deepcopy(self._load(path))
			except Exception as e:
				self.exception('Error while loading {0}: {1}', path, str(e))
				try:
					self.loaded.pop(path)
				except KeyError:
					pass
				try:
					self.loaded.pop(path)
				except KeyError:
					pass

	def run(self):
		while self.interval is not None and not self.shutdown_event.is_set():
			self.update()
			self.shutdown_event.wait(self.interval)

	def exception(self, msg, *args, **kwargs):
		if self.pl:
			self.pl.exception(msg, prefix='config_loader', *args, **kwargs)
		else:
			raise