addon_utils.py 16 KB

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