Random bug fixes while trying to get this out the door
[ejpi] / src / plugin_utils.py
1 #!/usr/bin/env python
2
3
4 from __future__ import with_statement
5
6
7 import sys
8 import os
9 import inspect
10 import ConfigParser
11
12 from libraries import gtkpie
13 from libraries import gtkpieboard
14 from libraries.recipes import io
15 import operation
16
17
18 class CommandStackHandler(object):
19
20         def __init__(self, stack, command, operator):
21                 self.command = command
22
23                 self.__stack = stack
24                 self.__operator = operator
25
26         def handler(self, commandName, activeModifiers):
27                 self.__stack.apply_operation(self.__operator)
28
29
30 class PieKeyboardPlugin(object):
31
32         def __init__(self, name, factory):
33                 self.name = name
34                 self.factory = factory
35                 self.__handler = None
36
37         def setup(self, calcStack, style, boardHandler):
38                 self.__handler = boardHandler
39
40                 with open(self.factory.mapFile, "r") as mapfile:
41                         boardTree = gtkpieboard.parse_keyboard_data("\n".join(mapfile.readlines()))
42
43                 rows, columns = boardTree["dimensions"]
44                 keyboardName = boardTree["name"]
45                 keyTree = boardTree["keys"]
46
47                 keyboard = gtkpieboard.PieKeyboard(style, rows, columns)
48                 gtkpieboard.load_keyboard(keyboardName, keyTree, keyboard, self.__handler)
49
50                 for commandName, operator in self.factory.commands.iteritems():
51                         handler = CommandStackHandler(calcStack, commandName, operator)
52                         self.__handler.register_command_handler(commandName, handler.handler)
53
54                 return keyboard
55
56         def tear_down(self):
57                 for commandName, operator in self.factory.commands.itervalues():
58                         self.__handler.unregister_command_handler(commandName)
59
60                 # Leave our self completely unusable
61                 self.name = None
62                 self.factory = None
63                 self.__handler = None
64
65
66 class PieKeyboardPluginFactory(object):
67
68         def __init__(self, pluginName, keyboardMapFile):
69                 self.name = pluginName
70                 self.mapFile = keyboardMapFile
71                 self.commands = {}
72
73         def register_operation(self, commandName, operator):
74                 self.commands[commandName] = operator
75
76         def construct_keyboard(self):
77                 plugin = PieKeyboardPlugin(self.name, self)
78                 return plugin
79
80
81 class PluginManager(object):
82
83         def __init__(self, pluginType):
84                 self._pluginType = pluginType
85                 self._plugins = {}
86                 self._enabled = set()
87
88                 self.__searchPaths = []
89
90         def add_path(self, *paths):
91                 self.__searchPaths.append(paths)
92                 self.__scan(paths)
93
94         def rescan(self):
95                 self._plugins = {}
96                 self.__scan(self.__searchPaths)
97
98         def plugin_info(self, pluginId):
99                 pluginData = self._plugins[pluginId]
100                 return pluginData["name"], pluginData["version"], pluginData["description"]
101
102         def plugins(self):
103                 for id, pluginData in self._plugins.iteritems():
104                         yield id, pluginData["name"], pluginData["version"], pluginData["description"]
105
106         def enable_plugin(self, id):
107                 assert id in self._plugins, "Can't find plugin %s in the search path %r" % (id, self.__searchPaths)
108                 self._load_module(id)
109                 self._enabled.add(id)
110
111         def disable_plugin(self, id):
112                 self._enabled.remove(id)
113
114         def lookup_plugin(self, name):
115                 for id, data in self._plugins.iteritems():
116                         if data["name"] == name:
117                                 return id
118
119         def _load_module(self, id):
120                 pluginData = self._plugins[id]
121
122                 if "module" not in pluginData:
123                         pluginPath = pluginData["pluginpath"]
124                         dataPath = pluginData["datapath"]
125                         assert dataPath.endswith(".ini")
126
127                         dataPath = io.relpath(pluginPath, dataPath)
128                         pythonPath = dataPath[0:-len(".ini")]
129                         modulePath = fspath_to_ipath(pythonPath, "")
130
131                         sys.path.append(pluginPath)
132                         try:
133                                 module = __import__(modulePath)
134                         finally:
135                                 sys.path.remove(pluginPath)
136                         pluginData["module"] = module
137                 else:
138                         # @todo Decide if want to call reload
139                         module = pluginData["module"]
140
141                 return module
142
143         def __scan(self, paths):
144                 pluginDataFiles = find_plugins(paths, ".ini")
145
146                 for pluginPath, pluginDataFile in pluginDataFiles:
147                         config = ConfigParser.SafeConfigParser()
148                         config.read(pluginDataFile)
149
150                         name = config.get(self._pluginType, "name")
151                         version = config.get(self._pluginType, "version")
152                         description = config.get(self._pluginType, "description")
153
154                         self._plugins[pluginDataFile] = {
155                                 "name": name,
156                                 "version": version,
157                                 "description": description,
158                                 "datapath": pluginDataFile,
159                                 "pluginpath": pluginPath,
160                         }
161
162
163 class ConstantPluginManager(PluginManager):
164
165         def __init__(self):
166                 super(ConstantPluginManager, self).__init__("Constants")
167                 self.__constants = {}
168                 self.__constantsCache = {}
169                 self.__isCacheDirty = False
170
171         def enable_plugin(self, id):
172                 super(ConstantPluginManager, self).enable_plugin(id)
173                 self.__constants[id] = dict(
174                         extract_instance_from_plugin(self._plugins[id]["module"], operation.Operation)
175                 )
176                 self.__isCacheDirty = True
177
178         def disable_plugin(self, id):
179                 super(ConstantPluginManager, self).disable_plugin(id)
180                 self.__isCacheDirty = True
181
182         @property
183         def constants(self):
184                 if self.__isCacheDirty:
185                         self.__update_cache()
186                 return self.__constantsCache
187
188         def __update_cache(self):
189                 self.__constantsCache.clear()
190                 for pluginId in self._enabled:
191                         self.__constantsCache.update(self.__constants[pluginId])
192                 self.__isCacheDirty = False
193
194
195 class OperatorPluginManager(PluginManager):
196
197         def __init__(self):
198                 super(OperatorPluginManager, self).__init__("Operator")
199                 self.__operators = {}
200                 self.__operatorsCache = {}
201                 self.__isCacheDirty = False
202
203         def enable_plugin(self, id):
204                 super(OperatorPluginManager, self).enable_plugin(id)
205                 operators = (
206                         extract_class_from_plugin(self._plugins[id]["module"], operation.Operation)
207                 )
208                 self.__operators[id] = dict(
209                         (op.symbol, op)
210                         for op in operators
211                 )
212                 self.__isCacheDirty = True
213
214         def disable_plugin(self, id):
215                 super(OperatorPluginManager, self).disable_plugin(id)
216                 self.__isCacheDirty = True
217
218         @property
219         def operators(self):
220                 if self.__isCacheDirty:
221                         self.__update_cache()
222                 return self.__operatorsCache
223
224         def __update_cache(self):
225                 self.__operatorsCache.clear()
226                 for pluginId in self._enabled:
227                         self.__operatorsCache.update(self.__operators[pluginId])
228                 self.__isCacheDirty = False
229
230
231 class KeyboardPluginManager(PluginManager):
232
233         def __init__(self):
234                 super(KeyboardPluginManager, self).__init__("Keyboard")
235                 self.__keyboards = {}
236                 self.__keyboardsCache = {}
237                 self.__isCacheDirty = False
238
239         def enable_plugin(self, id):
240                 super(KeyboardPluginManager, self).enable_plugin(id)
241                 keyboards = (
242                         extract_instance_from_plugin(self._plugins[id]["module"], PieKeyboardPluginFactory)
243                 )
244                 self.__keyboards[id] = dict(
245                         (board.name, board)
246                         for boardVariableName, board in keyboards
247                 )
248                 self.__isCacheDirty = True
249
250         def disable_plugin(self, id):
251                 super(KeyboardPluginManager, self).disable_plugin(id)
252                 self.__isCacheDirty = True
253
254         @property
255         def keyboards(self):
256                 if self.__isCacheDirty:
257                         self.__update_cache()
258                 return self.__keyboardsCache
259
260         def __update_cache(self):
261                 self.__keyboardsCache.clear()
262                 for pluginId in self._enabled:
263                         self.__keyboardsCache.update(self.__keyboards[pluginId])
264                 self.__isCacheDirty = False
265
266
267 def fspath_to_ipath(fsPath, extension = ".py"):
268         """
269         >>> fspath_to_ipath("user/test/file.py")
270         'user.test.file'
271         """
272         assert fsPath.endswith(extension)
273         CURRENT_DIR = "."+os.sep
274         CURRENT_DIR_LEN = len(CURRENT_DIR)
275         if fsPath.startswith(CURRENT_DIR):
276                 fsPath = fsPath[CURRENT_DIR_LEN:]
277
278         if extension:
279                 fsPath = fsPath[0:-len(extension)]
280         parts = fsPath.split(os.sep)
281         return ".".join(parts)
282
283
284 def find_plugins(searchPaths, fileType=".py"):
285         pythonFiles = (
286                 (path, os.path.join(root, file))
287                 for path in searchPaths
288                 for root, dirs, files in os.walk(path)
289                 for file in files
290                         if file.endswith(fileType)
291         )
292         return pythonFiles
293
294
295 def extract_class_from_plugin(pluginModule, cls):
296         try:
297                 for item in pluginModule.__dict__.itervalues():
298                         try:
299                                 if cls in inspect.getmro(item):
300                                         yield item
301                         except AttributeError:
302                                 pass
303         except AttributeError:
304                 pass
305
306
307 def extract_instance_from_plugin(pluginModule, cls):
308         try:
309                 for name, item in pluginModule.__dict__.iteritems():
310                         try:
311                                 if isinstance(item, cls):
312                                         yield name, item
313                         except AttributeError:
314                                 pass
315         except AttributeError:
316                 pass