Merge remote-tracking branch 'origin/master' into blender2.8
[blender.git] / tests / python / bl_load_py_modules.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 compliant>
20
21 # simple script to enable all addons, and disable
22
23 """
24 ./blender.bin --background -noaudio --factory-startup --python tests/python/bl_load_py_modules.py
25 """
26
27 import bpy
28 import addon_utils
29
30 import sys
31 import os
32
33 BLACKLIST = {
34     "bl_i18n_utils",
35     "bl_previews_utils",
36     "cycles",
37     "io_export_dxf",  # TODO, check on why this fails
38     'io_import_dxf',  # Because of cydxfentity.so dependency
39
40     # The unpacked wheel is only loaded when actually used, not directly on import:
41     "io_blend_utils/blender_bam-unpacked.whl",
42     }
43
44 # Some modules need to add to the `sys.path`.
45 MODULE_SYS_PATHS = {
46     # Runs in a Python subprocess, so its expected its basedir can be imported.
47     "io_blend_utils.blendfile_pack": ".",
48     }
49
50 if not bpy.app.build_options.freestyle:
51     BLACKLIST.add("render_freestyle_svg")
52
53 BLACKLIST_DIRS = (
54     os.path.join(bpy.utils.resource_path('USER'), "scripts"),
55     ) + tuple(addon_utils.paths()[1:])
56
57
58 def module_names_recursive(mod_dir, *, parent=None):
59     """
60     a version of bpy.path.module_names that includes non-packages
61     """
62
63     is_package = os.path.exists(os.path.join(mod_dir, "__init__.py"))
64
65     for n in os.listdir(mod_dir):
66         if not n.startswith((".", "_")):
67             submod_full = os.path.join(mod_dir, n)
68             if os.path.isdir(submod_full):
69                 if not parent:
70                     subparent = n
71                 else:
72                     subparent = parent + "." + n
73                 yield from module_names_recursive(submod_full, parent=subparent)
74             elif n.endswith(".py") and is_package is False:
75                 submod = n[:-3]
76                 if parent:
77                     submod = parent + "." + submod
78                 yield submod, submod_full
79
80
81 def module_names_all(mod_dir):
82     yield from bpy.path.module_names(mod_dir)
83     yield from module_names_recursive(mod_dir)
84
85
86 def addon_modules_sorted():
87     modules = addon_utils.modules({})
88     modules[:] = [mod for mod in modules if not mod.__file__.startswith(BLACKLIST_DIRS)]
89     modules.sort(key=lambda mod: mod.__name__)
90     return modules
91
92
93 def source_list(path, filename_check=None):
94     from os.path import join
95     for dirpath, dirnames, filenames in os.walk(path):
96         # skip '.svn'
97         if dirpath.startswith("."):
98             continue
99
100         for filename in filenames:
101             filepath = join(dirpath, filename)
102             if filename_check is None or filename_check(filepath):
103                 yield filepath
104
105
106 def load_addons():
107     modules = addon_modules_sorted()
108     addons = bpy.context.user_preferences.addons
109
110     # first disable all
111     for mod_name in list(addons.keys()):
112         addon_utils.disable(mod_name, default_set=True)
113
114     assert(bool(addons) is False)
115
116     for mod in modules:
117         mod_name = mod.__name__
118         if mod_name in BLACKLIST:
119             continue
120         addon_utils.enable(mod_name, default_set=True)
121         if not (mod_name in addons):
122             raise Exception("'addon_utils.enable(%r)' call failed" % mod_name)
123
124
125 def load_modules():
126     modules = []
127     module_paths = []
128
129     # paths blender stores scripts in.
130     paths = bpy.utils.script_paths()
131
132     print("Paths:")
133     for script_path in paths:
134         print("\t'%s'" % script_path)
135
136     #
137     # find all sys.path we added
138     for script_path in paths:
139         for mod_dir in sys.path:
140             if mod_dir.startswith(script_path):
141                 if not mod_dir.startswith(BLACKLIST_DIRS):
142                     if mod_dir not in module_paths:
143                         if os.path.exists(mod_dir):
144                             module_paths.append(mod_dir)
145
146     #
147     # collect modules from our paths.
148     module_names = {}
149     for mod_dir in module_paths:
150         # print("mod_dir", mod_dir)
151         for mod, mod_full in bpy.path.module_names(mod_dir):
152             if mod in BLACKLIST:
153                 continue
154             if mod in module_names:
155                 mod_dir_prev, mod_full_prev = module_names[mod]
156                 raise Exception("Module found twice %r.\n    (%r -> %r, %r -> %r)" %
157                                 (mod, mod_dir, mod_full, mod_dir_prev, mod_full_prev))
158
159             modules.append(__import__(mod))
160
161             module_names[mod] = mod_dir, mod_full
162     del module_names
163
164     #
165     # now submodules
166     for m in modules:
167         filepath = m.__file__
168         if os.path.basename(filepath).startswith("__init__."):
169             mod_dir = os.path.dirname(filepath)
170             for submod, submod_full in module_names_all(mod_dir):
171                 # fromlist is ignored, ugh.
172                 mod_name_full = m.__name__ + "." + submod
173
174                 sys_path_back = sys.path[:]
175
176                 sys.path.extend([
177                     os.path.normpath(os.path.join(mod_dir, f))
178                     for f in MODULE_SYS_PATHS.get(mod_name_full, ())
179                     ])
180
181                 __import__(mod_name_full)
182                 mod_imp = sys.modules[mod_name_full]
183
184                 sys.path[:] = sys_path_back
185
186                 # check we load what we ask for.
187                 assert(os.path.samefile(mod_imp.__file__, submod_full))
188
189                 modules.append(mod_imp)
190
191     #
192     # check which filepaths we didn't load
193     source_files = []
194     for mod_dir in module_paths:
195         source_files.extend(source_list(mod_dir, filename_check=lambda f: f.endswith(".py")))
196
197     source_files = list(set(source_files))
198     source_files.sort()
199
200     #
201     # remove loaded files
202     loaded_files = list({m.__file__ for m in modules})
203     loaded_files.sort()
204
205     for f in loaded_files:
206         source_files.remove(f)
207
208     #
209     # test we tested all files except for presets and templates
210     ignore_paths = [
211         os.sep + "presets" + os.sep,
212         os.sep + "templates" + os.sep,
213     ] + ([(os.sep + f + os.sep) for f in BLACKLIST] +
214          [(os.sep + f + ".py")  for f in BLACKLIST])
215
216     for f in source_files:
217         for ignore in ignore_paths:
218             if ignore in f:
219                 break
220         else:
221             raise Exception("Source file %r not loaded in test" % f)
222
223     print("loaded %d modules" % len(loaded_files))
224
225
226 def main():
227     load_addons()
228     load_modules()
229
230 if __name__ == "__main__":
231     # So a python error exits(1)
232     try:
233         main()
234     except:
235         import traceback
236         traceback.print_exc()
237         sys.exit(1)