PyAPI: use public API's for module & builtin access
[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     # Utility scripts not meant to be used as modules
41     os.path.join("power_sequencer", "scripts"),
42     # The unpacked wheel is only loaded when actually used, not directly on import:
43     os.path.join("io_blend_utils", "blender_bam-unpacked.whl"),
44 }
45
46 for mod in addon_utils.modules():
47     if addon_utils.module_bl_info(mod)['blender'] < (2, 80, 0):
48         BLACKLIST.add(mod.__name__)
49
50 # Some modules need to add to the `sys.path`.
51 MODULE_SYS_PATHS = {
52     # Runs in a Python subprocess, so its expected its basedir can be imported.
53     "io_blend_utils.blendfile_pack": ".",
54 }
55
56 if not bpy.app.build_options.freestyle:
57     BLACKLIST.add("render_freestyle_svg")
58
59 BLACKLIST_DIRS = (
60     os.path.join(bpy.utils.resource_path('USER'), "scripts"),
61 ) + tuple(addon_utils.paths()[1:])
62
63
64 def module_names_recursive(mod_dir, *, parent=None):
65     """
66     a version of bpy.path.module_names that includes non-packages
67     """
68
69     is_package = os.path.exists(os.path.join(mod_dir, "__init__.py"))
70
71     for n in os.listdir(mod_dir):
72         if not n.startswith((".", "_")):
73             submod_full = os.path.join(mod_dir, n)
74             if os.path.isdir(submod_full):
75                 if not parent:
76                     subparent = n
77                 else:
78                     subparent = parent + "." + n
79                 yield from module_names_recursive(submod_full, parent=subparent)
80             elif n.endswith(".py") and is_package is False:
81                 submod = n[:-3]
82                 if parent:
83                     submod = parent + "." + submod
84                 yield submod, submod_full
85
86
87 def module_names_all(mod_dir):
88     yield from bpy.path.module_names(mod_dir)
89     yield from module_names_recursive(mod_dir)
90
91
92 def source_list(path, filename_check=None):
93     from os.path import join
94     for dirpath, dirnames, filenames in os.walk(path):
95         # skip '.git'
96         dirnames[:] = [d for d in dirnames if not d.startswith(".")]
97
98         for filename in filenames:
99             filepath = join(dirpath, filename)
100             if filename_check is None or filename_check(filepath):
101                 yield filepath
102
103
104 def load_modules():
105     VERBOSE = os.environ.get("BLENDER_VERBOSE") is not None
106
107     modules = []
108     module_paths = []
109
110     # paths blender stores scripts in.
111     paths = bpy.utils.script_paths()
112
113     print("Paths:")
114     for script_path in paths:
115         print("\t'%s'" % script_path)
116
117     #
118     # find all sys.path we added
119     for script_path in paths:
120         for mod_dir in sys.path:
121             if mod_dir.startswith(script_path):
122                 if not mod_dir.startswith(BLACKLIST_DIRS):
123                     if mod_dir not in module_paths:
124                         if os.path.exists(mod_dir):
125                             module_paths.append(mod_dir)
126
127     #
128     # collect modules from our paths.
129     module_names = {}
130     for mod_dir in module_paths:
131         # print("mod_dir", mod_dir)
132         for mod, mod_full in bpy.path.module_names(mod_dir):
133             if mod in BLACKLIST:
134                 continue
135             if mod in module_names:
136                 mod_dir_prev, mod_full_prev = module_names[mod]
137                 raise Exception("Module found twice %r.\n    (%r -> %r, %r -> %r)" %
138                                 (mod, mod_dir, mod_full, mod_dir_prev, mod_full_prev))
139
140             modules.append(__import__(mod))
141
142             module_names[mod] = mod_dir, mod_full
143     del module_names
144
145     #
146     # test we tested all files except for presets and templates
147     ignore_paths = [
148         os.sep + "presets" + os.sep,
149         os.sep + "templates_osl" + os.sep,
150         os.sep + "templates_py" + os.sep,
151         os.sep + "bl_app_templates_system" + os.sep,
152     ] + ([(os.sep + f + os.sep) for f in BLACKLIST] +
153          [(os.sep + f + ".py") for f in BLACKLIST])
154
155     #
156     # now submodules
157     for m in modules:
158         filepath = m.__file__
159         if os.path.basename(filepath).startswith("__init__."):
160             mod_dir = os.path.dirname(filepath)
161             for submod, submod_full in module_names_all(mod_dir):
162                 # fromlist is ignored, ugh.
163                 mod_name_full = m.__name__ + "." + submod
164
165                 sys_path_back = sys.path[:]
166
167                 sys.path.extend([
168                     os.path.normpath(os.path.join(mod_dir, f))
169                     for f in MODULE_SYS_PATHS.get(mod_name_full, ())
170                 ])
171
172                 try:
173                     __import__(mod_name_full)
174                     mod_imp = sys.modules[mod_name_full]
175
176                     sys.path[:] = sys_path_back
177
178                     # check we load what we ask for.
179                     assert(os.path.samefile(mod_imp.__file__, submod_full))
180
181                     modules.append(mod_imp)
182                 except Exception:
183                     import traceback
184                     # Module might fail to import, but we don't want whole test to fail here.
185                     # Reasoning:
186                     # - This module might be in ignored list (for example, preset or template),
187                     #   so failing here will cause false-positive test failure.
188                     # - If this is module which should not be ignored, it is not added to list
189                     #   of successfully loaded modules, meaning the test will catch this
190                     #   import failure.
191                     # - We want to catch all failures of this script instead of stopping on
192                     #   a first big failure.
193                     do_print = True
194                     if not VERBOSE:
195                         for ignore in ignore_paths:
196                             if ignore in submod_full:
197                                 do_print = False
198                                 break
199                     if do_print:
200                         traceback.print_exc()
201
202     #
203     # check which filepaths we didn't load
204     source_files = []
205     for mod_dir in module_paths:
206         source_files.extend(source_list(mod_dir, filename_check=lambda f: f.endswith(".py")))
207
208     source_files = list(set(source_files))
209     source_files.sort()
210
211     #
212     # remove loaded files
213     loaded_files = list({m.__file__ for m in modules})
214     loaded_files.sort()
215
216     for f in loaded_files:
217         source_files.remove(f)
218
219     for f in source_files:
220         for ignore in ignore_paths:
221             if ignore in f:
222                 break
223         else:
224             raise Exception("Source file %r not loaded in test" % f)
225
226     print("loaded %d modules" % len(loaded_files))
227
228
229 def main():
230     load_modules()
231
232
233 if __name__ == "__main__":
234     # So a python error exits(1)
235     try:
236         main()
237     except:
238         import traceback
239         traceback.print_exc()
240         sys.exit(1)