# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # """ This module contains utility functions specific to blender but not associated with blenders internal data. """ __all__ = ( "blend_paths", "escape_identifier", "keyconfig_set", "load_scripts", "modules_from_path", "preset_find", "preset_paths", "refresh_script_paths", "register_class", "register_module", "register_manual_map", "unregister_manual_map", "make_rna_paths", "manual_map", "resource_path", "script_path_user", "script_path_pref", "script_paths", "smpte_from_frame", "smpte_from_seconds", "units", "unregister_class", "unregister_module", "user_resource", ) from _bpy import ( escape_identifier, register_class, unregister_class, blend_paths, resource_path, ) from _bpy import script_paths as _bpy_script_paths from _bpy import user_resource as _user_resource from _bpy import _utils_units as units import bpy as _bpy import os as _os import sys as _sys import addon_utils as _addon_utils _user_preferences = _bpy.context.user_preferences _script_module_dirs = "startup", "modules" def _test_import(module_name, loaded_modules): use_time = _bpy.app.debug_python if module_name in loaded_modules: return None if "." in module_name: print("Ignoring '%s', can't import files containing " "multiple periods" % module_name) return None if use_time: import time t = time.time() try: mod = __import__(module_name) except: import traceback traceback.print_exc() return None if use_time: print("time %s %.4f" % (module_name, time.time() - t)) loaded_modules.add(mod.__name__) # should match mod.__name__ too return mod def _sys_path_ensure(path): if path not in _sys.path: # reloading would add twice _sys.path.insert(0, path) def modules_from_path(path, loaded_modules): """ Load all modules in a path and return them as a list. :arg path: this path is scanned for scripts and packages. :type path: string :arg loaded_modules: already loaded module names, files matching these names will be ignored. :type loaded_modules: set :return: all loaded modules. :rtype: list """ modules = [] for mod_name, mod_path in _bpy.path.module_names(path): mod = _test_import(mod_name, loaded_modules) if mod: modules.append(mod) return modules _global_loaded_modules = [] # store loaded module names for reloading. import bpy_types as _bpy_types # keep for comparisons, never ever reload this. def load_scripts(reload_scripts=False, refresh_scripts=False): """ Load scripts and run each modules register function. :arg reload_scripts: Causes all scripts to have their unregister method called before loading. :type reload_scripts: bool :arg refresh_scripts: only load scripts which are not already loaded as modules. :type refresh_scripts: bool """ use_time = _bpy.app.debug_python if use_time: import time t_main = time.time() loaded_modules = set() if refresh_scripts: original_modules = _sys.modules.values() if reload_scripts: _bpy_types.TypeMap.clear() # just unload, don't change user defaults, this means we can sync # to reload. note that they will only actually reload of the # modification time changes. This `won't` work for packages so... # its not perfect. for module_name in [ext.module for ext in _user_preferences.addons]: _addon_utils.disable(module_name, default_set=False) def register_module_call(mod): register = getattr(mod, "register", None) if register: try: register() except: import traceback traceback.print_exc() else: print("\nWarning! '%s' has no register function, " "this is now a requirement for registerable scripts" % mod.__file__) def unregister_module_call(mod): unregister = getattr(mod, "unregister", None) if unregister: try: unregister() except: import traceback traceback.print_exc() def test_reload(mod): import imp # reloading this causes internal errors # because the classes from this module are stored internally # possibly to refresh internal references too but for now, best not to. if mod == _bpy_types: return mod try: return imp.reload(mod) except: import traceback traceback.print_exc() def test_register(mod): if refresh_scripts and mod in original_modules: return if reload_scripts and mod: print("Reloading:", mod) mod = test_reload(mod) if mod: register_module_call(mod) _global_loaded_modules.append(mod.__name__) if reload_scripts: # module names -> modules _global_loaded_modules[:] = [_sys.modules[mod_name] for mod_name in _global_loaded_modules] # loop over and unload all scripts _global_loaded_modules.reverse() for mod in _global_loaded_modules: unregister_module_call(mod) for mod in _global_loaded_modules: test_reload(mod) del _global_loaded_modules[:] from bpy_restrict_state import RestrictBlend with RestrictBlend(): for base_path in script_paths(): for path_subdir in _script_module_dirs: path = _os.path.join(base_path, path_subdir) if _os.path.isdir(path): _sys_path_ensure(path) # only add this to sys.modules, don't run if path_subdir == "modules": continue for mod in modules_from_path(path, loaded_modules): test_register(mod) # deal with addons separately _addon_utils.reset_all(reload_scripts) # run the active integration preset filepath = preset_find(_user_preferences.inputs.active_keyconfig, "keyconfig") if filepath: keyconfig_set(filepath) if reload_scripts: import gc print("gc.collect() -> %d" % gc.collect()) if use_time: print("Python Script Load Time %.4f" % (time.time() - t_main)) # base scripts _scripts = _os.path.join(_os.path.dirname(__file__), _os.path.pardir, _os.path.pardir, ) _scripts = (_os.path.normpath(_scripts), ) def script_path_user(): """returns the env var and falls back to home dir or None""" path = _user_resource('SCRIPTS') return _os.path.normpath(path) if path else None def script_path_pref(): """returns the user preference or None""" path = _user_preferences.filepaths.script_directory return _os.path.normpath(path) if path else None def script_paths(subdir=None, user_pref=True, check_all=False): """ Returns a list of valid script paths. :arg subdir: Optional subdir. :type subdir: string :arg user_pref: Include the user preference script path. :type user_pref: bool :arg check_all: Include local, user and system paths rather just the paths blender uses. :type check_all: bool :return: script paths. :rtype: list """ scripts = list(_scripts) if check_all: # all possible paths base_paths = tuple(_os.path.join(resource_path(res), "scripts") for res in ('LOCAL', 'USER', 'SYSTEM')) else: # only paths blender uses base_paths = _bpy_script_paths() for path in base_paths + (script_path_user(), script_path_pref()): if path: path = _os.path.normpath(path) if path not in scripts and _os.path.isdir(path): scripts.append(path) if subdir is None: return scripts scripts_subdir = [] for path in scripts: path_subdir = _os.path.join(path, subdir) if _os.path.isdir(path_subdir): scripts_subdir.append(path_subdir) return scripts_subdir def refresh_script_paths(): """ Run this after creating new script paths to update sys.path """ for base_path in script_paths(): for path_subdir in _script_module_dirs: path = _os.path.join(base_path, path_subdir) if _os.path.isdir(path): _sys_path_ensure(path) for path in _addon_utils.paths(): _sys_path_ensure(path) path = _os.path.join(path, "modules") if _os.path.isdir(path): _sys_path_ensure(path) def preset_paths(subdir): """ Returns a list of paths for a specific preset. :arg subdir: preset subdirectory (must not be an absolute path). :type subdir: string :return: script paths. :rtype: list """ dirs = [] for path in script_paths("presets", check_all=True): directory = _os.path.join(path, subdir) if not directory.startswith(path): raise Exception("invalid subdir given %r" % subdir) elif _os.path.isdir(directory): dirs.append(directory) # Find addons preset paths for path in _addon_utils.paths(): directory = _os.path.join(path, "presets", subdir) if _os.path.isdir(directory): dirs.append(directory) return dirs def smpte_from_seconds(time, fps=None): """ Returns an SMPTE formatted string from the time in seconds: "HH:MM:SS:FF". If the *fps* is not given the current scene is used. """ import math if fps is None: fps = _bpy.context.scene.render.fps hours = minutes = seconds = frames = 0 if time < 0: time = - time neg = "-" else: neg = "" if time >= 3600.0: # hours hours = int(time / 3600.0) time = time % 3600.0 if time >= 60.0: # minutes minutes = int(time / 60.0) time = time % 60.0 seconds = int(time) frames = int(round(math.floor(((time - seconds) * fps)))) return "%s%02d:%02d:%02d:%02d" % (neg, hours, minutes, seconds, frames) def smpte_from_frame(frame, fps=None, fps_base=None): """ Returns an SMPTE formatted string from the frame: "HH:MM:SS:FF". If *fps* and *fps_base* are not given the current scene is used. :arg time: time in seconds. :type time: number or timedelta object :return: the frame. :rtype: float """ if fps is None: fps = _bpy.context.scene.render.fps if fps_base is None: fps_base = _bpy.context.scene.render.fps_base return smpte_from_seconds((frame * fps_base) / fps, fps) def time_from_frame(frame, fps=None, fps_base=None): """ Returns the time from a frame number . If *fps* and *fps_base* are not given the current scene is used. :arg frame: number. :type frame: the frame number :return: the time in seconds. :rtype: datetime.timedelta """ if fps is None: fps = _bpy.context.scene.render.fps if fps_base is None: fps_base = _bpy.context.scene.render.fps_base from datetime import timedelta return timedelta(0, (frame * fps_base) / fps) def time_to_frame(time, fps=None, fps_base=None): """ Returns a float frame number from a time given in seconds or as a datetime.timedelta object. If *fps* and *fps_base* are not given the current scene is used. :arg time: time in seconds. :type time: number or a datetime.timedelta object :return: the frame. :rtype: float """ if fps is None: fps = _bpy.context.scene.render.fps if fps_base is None: fps_base = _bpy.context.scene.render.fps_base from datetime import timedelta if isinstance(time, timedelta): time = time.total_seconds() return (time / fps_base) * fps def preset_find(name, preset_path, display_name=False, ext=".py"): if not name: return None for directory in preset_paths(preset_path): if display_name: filename = "" for fn in _os.listdir(directory): if fn.endswith(ext) and name == _bpy.path.display_name(fn): filename = fn break else: filename = name + ext if filename: filepath = _os.path.join(directory, filename) if _os.path.exists(filepath): return filepath def keyconfig_set(filepath, report=None): from os.path import basename, splitext from itertools import chain if _bpy.app.debug_python: print("loading preset:", filepath) keyconfigs = _bpy.context.window_manager.keyconfigs keyconfigs_old = keyconfigs[:] try: error_msg = "" with open(filepath, 'r', encoding='utf-8') as keyfile: exec(compile(keyfile.read(), filepath, "exec"), {"__file__": filepath}) except: import traceback error_msg = traceback.format_exc() if error_msg: if report is not None: report({'ERROR'}, error_msg) print(error_msg) kc_new = next(chain(iter(kc for kc in keyconfigs if kc not in keyconfigs_old), (None,))) if kc_new is None: if report is not None: report({'ERROR'}, "Failed to load keymap %r" % filepath) return False else: kc_new.name = "" # remove duplicates name = splitext(basename(filepath))[0] while True: kc_dupe = keyconfigs.get(name) if kc_dupe: keyconfigs.remove(kc_dupe) else: break kc_new.name = name keyconfigs.active = kc_new return True def user_resource(resource_type, path="", create=False): """ Return a user resource path (normally from the users home directory). :arg type: Resource type in ['DATAFILES', 'CONFIG', 'SCRIPTS', 'AUTOSAVE']. :type type: string :arg subdir: Optional subdirectory. :type subdir: string :arg create: Treat the path as a directory and create it if its not existing. :type create: boolean :return: a path. :rtype: string """ target_path = _user_resource(resource_type, path) if create: # should always be true. if target_path: # create path if not existing. if not _os.path.exists(target_path): try: _os.makedirs(target_path) except: import traceback traceback.print_exc() target_path = "" elif not _os.path.isdir(target_path): print("Path %r found but isn't a directory!" % target_path) target_path = "" return target_path def _bpy_module_classes(module, is_registered=False): typemap_list = _bpy_types.TypeMap.get(module, ()) i = 0 while i < len(typemap_list): cls_weakref = typemap_list[i] cls = cls_weakref() if cls is None: del typemap_list[i] else: if is_registered == cls.is_registered: yield cls i += 1 def register_module(module, verbose=False): if verbose: print("bpy.utils.register_module(%r): ..." % module) cls = None for cls in _bpy_module_classes(module, is_registered=False): if verbose: print(" %r" % cls) try: register_class(cls) except: print("bpy.utils.register_module(): " "failed to registering class %r" % cls) import traceback traceback.print_exc() if verbose: print("done.\n") if cls is None: raise Exception("register_module(%r): defines no classes" % module) def unregister_module(module, verbose=False): if verbose: print("bpy.utils.unregister_module(%r): ..." % module) for cls in _bpy_module_classes(module, is_registered=True): if verbose: print(" %r" % cls) try: unregister_class(cls) except: print("bpy.utils.unregister_module(): " "failed to unregistering class %r" % cls) import traceback traceback.print_exc() if verbose: print("done.\n") # ----------------------------------------------------------------------------- # Manual lookups, each function has to return a basepath and a sequence # of... # we start with the built-in default mapping def _blender_default_map(): import sys import rna_wiki_reference as ref_mod ret = (ref_mod.url_manual_prefix, ref_mod.url_manual_mapping) # avoid storing in memory del sys.modules["rna_wiki_reference"] return ret # hooks for doc lookups _manual_map = [_blender_default_map] def register_manual_map(manual_hook): _manual_map.append(manual_hook) def unregister_manual_map(manual_hook): _manual_map.remove(manual_hook) def manual_map(): # reverse so default is called last for cb in reversed(_manual_map): try: prefix, url_manual_mapping = cb() except: print("Error calling %r" % cb) import traceback traceback.print_exc() continue yield prefix, url_manual_mapping # Build an RNA path from struct/property/enum names. def make_rna_paths(struct_name, prop_name, enum_name): """ Create RNA "paths" from given names. :arg struct_name: Name of a RNA struct (like e.g. "Scene"). :type struct_name: string :arg prop_name: Name of a RNA struct's property. :type prop_name: string :arg enum_name: Name of a RNA enum identifier. :type enum_name: string :return: A triple of three "RNA paths" (most_complete_path, "struct.prop", "struct.prop:'enum'"). If no enum_name is given, the third element will always be void. :rtype: tuple of strings """ src = src_rna = src_enum = "" if struct_name: if prop_name: src = src_rna = ".".join((struct_name, prop_name)) if enum_name: src = src_enum = "%s:'%s'" % (src_rna, enum_name) else: src = src_rna = struct_name return src, src_rna, src_enum