3d705a0cf79e8a8cb21147683b3a5c072b85afe5
[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 _user_preferences = _bpy.context.user_preferences
33
34 class _RestrictedContext():
35     __slots__ = ()
36     @property
37     def window_manager(self):
38         return _bpy.data.window_managers[0]
39 _ctx_restricted = _RestrictedContext()
40
41
42 error_duplicates = False
43 error_encoding = False
44 addons_fake_modules = {}
45
46
47 def paths():
48     # RELEASE SCRIPTS: official scripts distributed in Blender releases
49     addon_paths = _bpy.utils.script_paths("addons")
50
51     # CONTRIB SCRIPTS: good for testing but not official scripts yet
52     # if folder addons_contrib/ exists, scripts in there will be loaded too
53     addon_paths += _bpy.utils.script_paths("addons_contrib")
54
55     # EXTERN SCRIPTS: external projects scripts
56     # if folder addons_extern/ exists, scripts in there will be loaded too
57     addon_paths += _bpy.utils.script_paths("addons_extern")
58
59     return addon_paths
60
61
62 def modules(module_cache):
63     global error_duplicates
64     global error_encoding
65     import os
66
67     error_duplicates = False
68     error_encoding = False
69
70     path_list = paths()
71
72     # fake module importing
73     def fake_module(mod_name, mod_path, speedy=True, force_support=None):
74         global error_encoding
75
76         if _bpy.app.debug_python:
77             print("fake_module", mod_path, mod_name)
78         import ast
79         ModuleType = type(ast)
80         file_mod = open(mod_path, "r", encoding='UTF-8')
81         if speedy:
82             lines = []
83             line_iter = iter(file_mod)
84             l = ""
85             while not l.startswith("bl_info"):
86                 try:
87                     l = line_iter.readline()
88                 except UnicodeDecodeError as e:
89                     if not error_encoding:
90                         error_encoding = True
91                         print("Error reading file as UTF-8:", mod_path, e)
92                     file_mod.close()
93                     return None
94
95                 if len(l) == 0:
96                     break
97             while l.rstrip():
98                 lines.append(l)
99                 try:
100                     l = line_iter.readline()
101                 except UnicodeDecodeError as e:
102                     if not error_encoding:
103                         error_encoding = True
104                         print("Error reading file as UTF-8:", mod_path, e)
105                     file_mod.close()
106                     return None
107
108             data = "".join(lines)
109
110         else:
111             data = file_mod.read()
112
113         file_mod.close()
114
115         try:
116             ast_data = ast.parse(data, filename=mod_path)
117         except:
118             print("Syntax error 'ast.parse' can't read %r" % mod_path)
119             import traceback
120             traceback.print_exc()
121             ast_data = None
122
123         body_info = None
124
125         if ast_data:
126             for body in ast_data.body:
127                 if body.__class__ == ast.Assign:
128                     if len(body.targets) == 1:
129                         if getattr(body.targets[0], "id", "") == "bl_info":
130                             body_info = body
131                             break
132
133         if body_info:
134             try:
135                 mod = ModuleType(mod_name)
136                 mod.bl_info = ast.literal_eval(body.value)
137                 mod.__file__ = mod_path
138                 mod.__time__ = os.path.getmtime(mod_path)
139             except:
140                 print("AST error in module %s" % mod_name)
141                 import traceback
142                 traceback.print_exc()
143                 raise
144
145             if force_support is not None:
146                 mod.bl_info["support"] = force_support
147
148             return mod
149         else:
150             print("fake_module: addon missing 'bl_info' "
151                   "gives bad performance!: %r" % mod_path)
152             return None
153
154     modules_stale = set(module_cache.keys())
155
156     for path in path_list:
157
158         # force all contrib addons to be 'TESTING'
159         if path.endswith(("addons_contrib", "addons_extern")):
160             force_support = 'TESTING'
161         else:
162             force_support = None
163
164         for mod_name, mod_path in _bpy.path.module_names(path):
165             modules_stale -= {mod_name}
166             mod = module_cache.get(mod_name)
167             if mod:
168                 if mod.__file__ != mod_path:
169                     print("multiple addons with the same name:\n  %r\n  %r" %
170                           (mod.__file__, mod_path))
171                     error_duplicates = True
172
173                 elif mod.__time__ != os.path.getmtime(mod_path):
174                     print("reloading addon:",
175                           mod_name,
176                           mod.__time__,
177                           os.path.getmtime(mod_path),
178                           mod_path,
179                           )
180                     del module_cache[mod_name]
181                     mod = None
182
183             if mod is None:
184                 mod = fake_module(mod_name,
185                                   mod_path,
186                                   force_support=force_support)
187                 if mod:
188                     module_cache[mod_name] = mod
189
190     # just in case we get stale modules, not likely
191     for mod_stale in modules_stale:
192         del module_cache[mod_stale]
193     del modules_stale
194
195     mod_list = list(module_cache.values())
196     mod_list.sort(key=lambda mod: (mod.bl_info["category"],
197                                    mod.bl_info["name"],
198                                    ))
199     return mod_list
200
201
202 def check(module_name):
203     """
204     Returns the loaded state of the addon.
205
206     :arg module_name: The name of the addon and module.
207     :type module_name: string
208     :return: (loaded_default, loaded_state)
209     :rtype: tuple of booleans
210     """
211     import sys
212     loaded_default = module_name in _user_preferences.addons
213
214     mod = sys.modules.get(module_name)
215     loaded_state = mod and getattr(mod, "__addon_enabled__", Ellipsis)
216
217     if loaded_state is Ellipsis:
218         print("Warning: addon-module %r found module "
219                "but without __addon_enabled__ field, "
220                "possible name collision from file: %r" %
221                (module_name, getattr(mod, "__file__", "<unknown>")))
222
223         loaded_state = False
224
225     if mod and getattr(mod, "__addon_persistent__", False):
226         loaded_default = True
227
228     return loaded_default, loaded_state
229
230
231 def enable(module_name, default_set=True, persistent=False):
232     """
233     Enables an addon by name.
234
235     :arg module_name: The name of the addon and module.
236     :type module_name: string
237     :return: the loaded module or None on failure.
238     :rtype: module
239     """
240
241     import os
242     import sys
243
244     def handle_error():
245         import traceback
246         traceback.print_exc()
247
248     # reload if the mtime changes
249     mod = sys.modules.get(module_name)
250     # chances of the file _not_ existing are low, but it could be removed
251     if mod and os.path.exists(mod.__file__):
252         mod.__addon_enabled__ = False
253         mtime_orig = getattr(mod, "__time__", 0)
254         mtime_new = os.path.getmtime(mod.__file__)
255         if mtime_orig != mtime_new:
256             import imp
257             print("module changed on disk:", mod.__file__, "reloading...")
258
259             try:
260                 imp.reload(mod)
261             except:
262                 handle_error()
263                 del sys.modules[module_name]
264                 return None
265             mod.__addon_enabled__ = False
266
267     # Split registering up into 3 steps so we can undo
268     # if it fails par way through.
269
270     # first disable the context, using the context at all is
271     # really bad while loading an addon, don't do it!
272     ctx = _bpy.context
273     _bpy.context = _ctx_restricted
274
275     # 1) try import
276     try:
277         mod = __import__(module_name)
278         mod.__time__ = os.path.getmtime(mod.__file__)
279         mod.__addon_enabled__ = False
280     except:
281         handle_error()
282         _bpy.context = ctx
283         return None
284
285     # 2) try register collected modules
286     # removed, addons need to handle own registration now.
287
288     # 3) try run the modules register function
289     try:
290         mod.register()
291     except:
292         print("Exception in module register(): %r" %
293               getattr(mod, "__file__", module_name))
294         handle_error()
295         del sys.modules[module_name]
296         _bpy.context = ctx
297         return None
298
299     # finally restore the context
300     _bpy.context = ctx
301
302     # * OK loaded successfully! *
303     if default_set:
304         # just in case its enabled already
305         ext = _user_preferences.addons.get(module_name)
306         if not ext:
307             ext = _user_preferences.addons.new()
308             ext.module = module_name
309
310     mod.__addon_enabled__ = True
311     mod.__addon_persistent__ = persistent
312
313     if _bpy.app.debug_python:
314         print("\taddon_utils.enable", mod.__name__)
315
316     return mod
317
318
319 def disable(module_name, default_set=True):
320     """
321     Disables an addon by name.
322
323     :arg module_name: The name of the addon and module.
324     :type module_name: string
325     """
326     import sys
327     mod = sys.modules.get(module_name)
328
329     # possible this addon is from a previous session and didn't load a
330     # module this time. So even if the module is not found, still disable
331     # the addon in the user prefs.
332     if mod and getattr(mod, "__addon_enabled__", False) is not False:
333         mod.__addon_enabled__ = False
334         mod.__addon_persistent = False
335
336         try:
337             mod.unregister()
338         except:
339             print("Exception in module unregister(): %r" %
340                   getattr(mod, "__file__", module_name))
341             import traceback
342             traceback.print_exc()
343     else:
344         print("addon_utils.disable: %s not %s." %
345               (module_name, "disabled" if mod is None else "loaded"))
346
347     # could be in more then once, unlikely but better do this just in case.
348     addons = _user_preferences.addons
349
350     if default_set:
351         while module_name in addons:
352             addon = addons.get(module_name)
353             if addon:
354                 addons.remove(addon)
355
356     if _bpy.app.debug_python:
357         print("\taddon_utils.disable", module_name)
358
359
360 def reset_all(reload_scripts=False):
361     """
362     Sets the addon state based on the user preferences.
363     """
364     import sys
365     import imp
366
367     # RELEASE SCRIPTS: official scripts distributed in Blender releases
368     paths_list = paths()
369
370     for path in paths_list:
371         _bpy.utils._sys_path_ensure(path)
372         for mod_name, mod_path in _bpy.path.module_names(path):
373             is_enabled, is_loaded = check(mod_name)
374
375             # first check if reload is needed before changing state.
376             if reload_scripts:
377                 mod = sys.modules.get(mod_name)
378                 if mod:
379                     imp.reload(mod)
380
381             if is_enabled == is_loaded:
382                 pass
383             elif is_enabled:
384                 enable(mod_name)
385             elif is_loaded:
386                 print("\taddon_utils.reset_all unloading", mod_name)
387                 disable(mod_name)
388
389
390 def module_bl_info(mod, info_basis={"name": "",
391                                     "author": "",
392                                     "version": (),
393                                     "blender": (),
394                                     "location": "",
395                                     "description": "",
396                                     "wiki_url": "",
397                                     "tracker_url": "",
398                                     "support": 'COMMUNITY',
399                                     "category": "",
400                                     "warning": "",
401                                     "show_expanded": False,
402                                     }
403                    ):
404
405     addon_info = getattr(mod, "bl_info", {})
406
407     # avoid re-initializing
408     if "_init" in addon_info:
409         return addon_info
410
411     if not addon_info:
412         mod.bl_info = addon_info
413
414     for key, value in info_basis.items():
415         addon_info.setdefault(key, value)
416
417     if not addon_info["name"]:
418         addon_info["name"] = mod.__name__
419
420     addon_info["_init"] = None
421     return addon_info