diff options
Diffstat (limited to 'release/scripts/modules/bpy/utils/__init__.py')
-rw-r--r-- | release/scripts/modules/bpy/utils/__init__.py | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/release/scripts/modules/bpy/utils/__init__.py b/release/scripts/modules/bpy/utils/__init__.py new file mode 100644 index 00000000000..7a1224db226 --- /dev/null +++ b/release/scripts/modules/bpy/utils/__init__.py @@ -0,0 +1,700 @@ +# ##### 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 ##### + +# <pep8-80 compliant> + +""" +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", + "previews", + "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) + + 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 importlib + # 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 importlib.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 + _initialize = getattr(_addon_utils, "_initialize", None) + if _initialize is not None: + # first time, use fast-path + _initialize() + del _addon_utils._initialize + else: + _addon_utils.reset_all(reload_scripts) + del _initialize + + # 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_manual_reference as ref_mod + ret = (ref_mod.url_manual_prefix, ref_mod.url_manual_mapping) + # avoid storing in memory + del sys.modules["rna_manual_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 |