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