9d0f540b08b7e9cd98426041ec31c37067394ecd
[blender.git] / release / scripts / modules / addon_utils.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8-80 compliant>
20
21 __all__ = (
22     "paths",
23     "modules",
24     "check",
25     "enable",
26     "disable",
27     "reset_all",
28     "module_bl_info",
29     )
30
31 import bpy as _bpy
32
33
34 error_duplicates = False
35 error_encoding = False
36
37 _addons_fake_modules = {}
38
39 def module_get(mod_name):
40     return _addons_fake_modules[mod_name]
41
42
43 def paths():
44     # RELEASE SCRIPTS: official scripts distributed in Blender releases
45     paths = _bpy.utils.script_paths("addons")
46
47     # CONTRIB SCRIPTS: good for testing but not official scripts yet
48     # if folder addons_contrib/ exists, scripts in there will be loaded too
49     paths += _bpy.utils.script_paths("addons_contrib")
50
51     # EXTERN SCRIPTS: external projects scripts
52     # if folder addons_extern/ exists, scripts in there will be loaded too
53     paths += _bpy.utils.script_paths("addons_extern")
54
55     return paths
56
57
58 def modules(module_cache):
59     global error_duplicates
60     global error_encoding
61     import os
62
63     error_duplicates = False
64     error_encoding = False
65
66     path_list = paths()
67
68     # fake module importing
69     def fake_module(mod_name, mod_path, speedy=True):
70         global error_encoding
71
72         if _bpy.app.debug:
73             print("fake_module", mod_path, mod_name)
74         import ast
75         ModuleType = type(ast)
76         file_mod = open(mod_path, "r", encoding='UTF-8')
77         if speedy:
78             lines = []
79             line_iter = iter(file_mod)
80             l = ""
81             while not l.startswith("bl_info"):
82                 try:
83                     l = line_iter.readline()
84                 except UnicodeDecodeError as e:
85                     if not error_encoding:
86                         error_encoding = True
87                         print("Error reading file as UTF-8:", mod_path, e)
88                     file_mod.close()
89                     return None
90
91                 if len(l) == 0:
92                     break
93             while l.rstrip():
94                 lines.append(l)
95                 try:
96                     l = line_iter.readline()
97                 except UnicodeDecodeError as e:
98                     if not error_encoding:
99                         error_encoding = True
100                         print("Error reading file as UTF-8:", mod_path, e)
101                     file_mod.close()
102                     return None
103
104             data = "".join(lines)
105
106         else:
107             data = file_mod.read()
108
109         file_mod.close()
110
111         try:
112             ast_data = ast.parse(data, filename=mod_path)
113         except:
114             print("Syntax error 'ast.parse' can't read %r" % mod_path)
115             import traceback
116             traceback.print_exc()
117             ast_data = None
118
119         body_info = None
120
121         if ast_data:
122             for body in ast_data.body:
123                 if body.__class__ == ast.Assign:
124                     if len(body.targets) == 1:
125                         if getattr(body.targets[0], "id", "") == "bl_info":
126                             body_info = body
127                             break
128
129         if body_info:
130             try:
131                 mod = ModuleType(mod_name)
132                 mod.bl_info = ast.literal_eval(body.value)
133                 mod.__file__ = mod_path
134                 mod.__time__ = os.path.getmtime(mod_path)
135             except:
136                 print("AST error in module %s" % mod_name)
137                 import traceback
138                 traceback.print_exc()
139                 raise
140
141             return mod
142         else:
143             return None
144
145     modules_stale = set(module_cache.keys())
146
147     for path in path_list:
148         for mod_name, mod_path in _bpy.path.module_names(path):
149             modules_stale -= {mod_name}
150             mod = module_cache.get(mod_name)
151             if mod:
152                 if mod.__file__ != mod_path:
153                     print("multiple addons with the same name:\n  %r\n  %r" %
154                           (mod.__file__, mod_path))
155                     error_duplicates = True
156
157                 elif mod.__time__ != os.path.getmtime(mod_path):
158                     print("reloading addon:",
159                           mod_name,
160                           mod.__time__,
161                           os.path.getmtime(mod_path),
162                           mod_path,
163                           )
164                     del module_cache[mod_name]
165                     mod = None
166
167             if mod is None:
168                 mod = fake_module(mod_name, mod_path)
169                 if mod:
170                     module_cache[mod_name] = mod
171
172     # just incase we get stale modules, not likely
173     for mod_stale in modules_stale:
174         del module_cache[mod_stale]
175     del modules_stale
176
177     mod_list = list(module_cache.values())
178     mod_list.sort(key=lambda mod: (mod.bl_info['category'],
179                                    mod.bl_info['name'],
180                                    ))
181     return mod_list
182
183
184 def check(module_name):
185     """
186     Returns the loaded state of the addon.
187
188     :arg module_name: The name of the addon and module.
189     :type module_name: string
190     :return: (loaded_default, loaded_state)
191     :rtype: tuple of booleans
192     """
193     import sys
194     loaded_default = module_name in _bpy.context.user_preferences.addons
195
196     mod = sys.modules.get(module_name)
197     loaded_state = mod and getattr(mod, "__addon_enabled__", Ellipsis)
198
199     if loaded_state is Ellipsis:
200         print("Warning: addon-module %r found module "
201                "but without __addon_enabled__ field, "
202                "possible name collision from file: %r" %
203                (module_name, getattr(mod, "__file__", "<unknown>")))
204
205         loaded_state = False
206
207     return loaded_default, loaded_state
208
209
210 def enable(module_name, default_set=True):
211     """
212     Enables an addon by name.
213
214     :arg module_name: The name of the addon and module.
215     :type module_name: string
216     :return: the loaded module or None on failier.
217     :rtype: module
218     """
219
220     import os
221     import sys
222     import imp
223
224     def handle_error():
225         import traceback
226         traceback.print_exc()
227
228     # reload if the mtime changes
229     mod = sys.modules.get(module_name)
230     if mod:
231         mod.__addon_enabled__ = False
232         mtime_orig = getattr(mod, "__time__", 0)
233         mtime_new = os.path.getmtime(mod.__file__)
234         if mtime_orig != mtime_new:
235             print("module changed on disk:", mod.__file__, "reloading...")
236
237             try:
238                 imp.reload(mod)
239             except:
240                 handle_error()
241                 del sys.modules[module_name]
242                 return None
243             mod.__addon_enabled__ = False
244
245     # Split registering up into 3 steps so we can undo
246     # if it fails par way through.
247     # 1) try import
248     try:
249         mod = __import__(module_name)
250         mod.__time__ = os.path.getmtime(mod.__file__)
251         mod.__addon_enabled__ = False
252     except:
253         handle_error()
254         return None
255
256     # 2) try register collected modules
257     # removed, addons need to handle own registration now.
258
259     # 3) try run the modules register function
260     try:
261         mod.register()
262     except:
263         handle_error()
264         del sys.modules[module_name]
265         return None
266
267     # * OK loaded successfully! *
268     if default_set:
269         # just incase its enabled alredy
270         ext = _bpy.context.user_preferences.addons.get(module_name)
271         if not ext:
272             ext = _bpy.context.user_preferences.addons.new()
273             ext.module = module_name
274
275     mod.__addon_enabled__ = True
276
277     if _bpy.app.debug:
278         print("\taddon_utils.enable", mod.__name__)
279
280     return mod
281
282
283 def disable(module_name, default_set=True):
284     """
285     Disables an addon by name.
286
287     :arg module_name: The name of the addon and module.
288     :type module_name: string
289     """
290     import sys
291     mod = sys.modules.get(module_name)
292
293     # possible this addon is from a previous session and didnt load a
294     # module this time. So even if the module is not found, still disable
295     # the addon in the user prefs.
296     if mod:
297         mod.__addon_enabled__ = False
298
299         try:
300             mod.unregister()
301         except:
302             import traceback
303             traceback.print_exc()
304     else:
305         print("addon_utils.disable", module_name, "not loaded")
306
307     # could be in more then once, unlikely but better do this just incase.
308     addons = _bpy.context.user_preferences.addons
309
310     if default_set:
311         while module_name in addons:
312             addon = addons.get(module_name)
313             if addon:
314                 addons.remove(addon)
315
316     if _bpy.app.debug:
317         print("\taddon_utils.disable", module_name)
318
319
320 def reset_all(reload_scripts=False):
321     """
322     Sets the addon state based on the user preferences.
323     """
324     import sys
325     import imp
326
327     # RELEASE SCRIPTS: official scripts distributed in Blender releases
328     paths_list = paths()
329
330     for path in paths_list:
331         _bpy.utils._sys_path_ensure(path)
332         for mod_name, mod_path in _bpy.path.module_names(path):
333             is_enabled, is_loaded = check(mod_name)
334
335             # first check if reload is needed before changing state.
336             if reload_scripts:
337                 mod = sys.modules.get(mod_name)
338                 if mod:
339                     imp.reload(mod)
340
341             if is_enabled == is_loaded:
342                 pass
343             elif is_enabled:
344                 enable(mod_name)
345             elif is_loaded:
346                 print("\taddon_utils.reset_all unloading", mod_name)
347                 disable(mod_name)
348
349
350 def module_bl_info(mod, info_basis={"name": "",
351                                     "author": "",
352                                     "version": (),
353                                     "blender": (),
354                                     "api": 0,
355                                     "location": "",
356                                     "description": "",
357                                     "wiki_url": "",
358                                     "tracker_url": "",
359                                     "support": 'COMMUNITY',
360                                     "category": "",
361                                     "warning": "",
362                                     "show_expanded": False,
363                                     }
364                    ):
365
366     addon_info = getattr(mod, "bl_info", {})
367
368     # avoid re-initializing
369     if "_init" in addon_info:
370         return addon_info
371
372     if not addon_info:
373         mod.bl_info = addon_info
374
375     for key, value in info_basis.items():
376         addon_info.setdefault(key, value)
377
378     if not addon_info["name"]:
379         addon_info["name"] = mod.__name__
380
381     addon_info["_init"] = None
382     return addon_info