def keyconfig_test(kc): def testEntry(kc, entry, src=None, parent=None): result = False idname, spaceid, regionid, children = entry km = kc.keymaps.find(idname, space_type=spaceid, region_type=regionid) if km: km = km.active() is_modal = km.is_modal if src: for item in km.keymap_items: if src.compare(item): print("===========") print(parent.name) print(_kmistr(src, is_modal).strip()) print(km.name) print(_kmistr(item, is_modal).strip()) result = True for child in children: if testEntry(kc, child, src, parent): result = True else: for i in range(len(km.keymap_items)): src = km.keymap_items[i] for child in children: if testEntry(kc, child, src, km): result = True for j in range(len(km.keymap_items) - i - 1): item = km.keymap_items[j + i + 1] if src.compare(item): print("===========") print(km.name) print(_kmistr(src, is_modal).strip()) print(_kmistr(item, is_modal).strip()) result = True for child in children: if testEntry(kc, child): result = True return result # ------------------------------------------------------------------------- # Function body from bl_keymap_utils import keymap_hierarchy result = False for entry in keymap_hierarchy.generate(): if testEntry(kc, entry): result = True return result
def check_maps(): maps = {} def fill_maps(seq): for km_name, km_space_type, km_region_type, km_sub in seq: maps[km_name] = (km_space_type, km_region_type) fill_maps(km_sub) fill_maps(keymap_hierarchy.generate()) import bpy keyconf = bpy.context.window_manager.keyconfigs.active maps_bl = set(keyconf.keymaps.keys()) maps_py = set(maps.keys()) err = False # Check keyconfig contains only maps that exist in blender test = maps_py - maps_bl if test: print( "Keymaps that are in 'bl_keymap_utils.keymap_hierarchy' but not blender" ) for km_id in test: if callable(km_id): # Keymap functions of tools are not in blender anyway... continue print("\t%s" % km_id) # TODO T65963, broken keymap hierarchy tests disabled until fixed. # err = True test = maps_bl - maps_py if test: print( "Keymaps that are in blender but not in 'bl_keymap_utils.keymap_hierarchy'" ) for km_id in test: km = keyconf.keymaps[km_id] print(" ('%s', '%s', '%s', [])," % (km_id, km.space_type, km.region_type)) # TODO T65963, broken keymap hierarchy tests disabled until fixed. # err = True # Check space/region's are OK print("Comparing keymap space/region types...") for km_id, km in keyconf.keymaps.items(): km_py = maps.get(km_id) if km_py is not None: km_space_type, km_region_type = km_py if km_space_type != km.space_type or km_region_type != km.region_type: print(" Error:") print(" expected -- ('%s', '%s', '%s', [])," % (km_id, km.space_type, km.region_type)) print(" got -- ('%s', '%s', '%s', [])," % (km_id, km_space_type, km_region_type)) print("done!") return err
def get_keymap_dict(keyconfig): """ get dictionary of key_name(str) and path_to_config_file(str) Args: keyconfig (bpy.types.KeyConfig): Returns: dict: keymap dict to display """ keymaps = [(km, keyconfig) for km in keyconfig.keymaps] keymap_dict = {} level = 0 for entry in keymap_hierarchy.generate(): add_entry_to_dict(keymaps, entry, keymap_dict, level) return keymap_dict
def check_maps(): maps = {} def fill_maps(seq): for km_name, km_space_type, km_region_type, km_sub in seq: maps[km_name] = (km_space_type, km_region_type) fill_maps(km_sub) fill_maps(keymap_hierarchy.generate()) import bpy keyconf = bpy.context.window_manager.keyconfigs.active maps_bl = set(keyconf.keymaps.keys()) maps_py = set(maps.keys()) err = False # Check keyconfig contains only maps that exist in blender test = maps_py - maps_bl if test: print("Keymaps that are in 'bl_keymap_utils.keymap_hierarchy' but not blender") for km_id in sorted(test): print("\t%s" % km_id) err = True test = maps_bl - maps_py if test: print("Keymaps that are in blender but not in 'bl_keymap_utils.keymap_hierarchy'") for km_id in sorted(test): km = keyconf.keymaps[km_id] print(" ('%s', '%s', '%s', [])," % (km_id, km.space_type, km.region_type)) err = True # Check space/region's are OK print("Comparing keymap space/region types...") for km_id, km in keyconf.keymaps.items(): km_py = maps.get(km_id) if km_py is not None: km_space_type, km_region_type = km_py if km_space_type != km.space_type or km_region_type != km.region_type: print(" Error:") print(" expected -- ('%s', '%s', '%s', [])," % (km_id, km.space_type, km.region_type)) print(" got -- ('%s', '%s', '%s', [])," % (km_id, km_space_type, km_region_type)) print("done!") return err
def dump_rna_messages(msgs, reports, settings, verbose=False): """ Dump into messages dict all RNA-defined UI messages (labels en tooltips). """ def class_blacklist(): blacklist_rna_class = { getattr(bpy.types, cls_id) for cls_id in ( # core classes "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct", # registerable classes "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo", # window classes "Window", ) } # More builtin classes we don't need to parse. blacklist_rna_class |= { cls for cls in bpy.types.Property.__subclasses__() } return blacklist_rna_class check_ctxt_rna = check_ctxt_rna_tip = None check_ctxt = reports["check_ctxt"] if check_ctxt: check_ctxt_rna = { "multi_lines": check_ctxt.get("multi_lines"), "not_capitalized": check_ctxt.get("not_capitalized"), "end_point": check_ctxt.get("end_point"), "undoc_ops": check_ctxt.get("undoc_ops"), "spell_checker": check_ctxt.get("spell_checker"), "spell_errors": check_ctxt.get("spell_errors"), } check_ctxt_rna_tip = check_ctxt_rna check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip") default_context = settings.DEFAULT_CONTEXT # Function definitions def walk_properties(cls): # This handles properties whose name is the same as their identifier. # Usually, it means that those are internal properties not exposed in the UI, however there are some cases # where the UI label is actually defined and same as the identifier (color spaces e.g., `RGB` etc.). # So we only exclude those properties in case they belong to an operator for now. def prop_name_validate(cls, prop_name, prop_identifier): if prop_name != prop_identifier: return True # Heuristic: A lot of operator's HIDDEN properties have no UI label/description. # While this is not ideal (for API doc purposes, description should always be provided), # for now skip those properties. # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator(). if issubclass( cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__: return False # Heuristic: If UI label is not capitalized, it is likely a private (undocumented) property, # that can be skipped. if prop_name and not prop_name[0].isupper(): return False return True bl_rna = cls.bl_rna # Get our parents' properties, to not export them multiple times. bl_rna_base = bl_rna.base bl_rna_base_props = set() if bl_rna_base: bl_rna_base_props |= set(bl_rna_base.properties.values()) for cls_base in cls.__bases__: bl_rna_base = getattr(cls_base, "bl_rna", None) if not bl_rna_base: continue bl_rna_base_props |= set(bl_rna_base.properties.values()) props = sorted(bl_rna.properties, key=lambda p: p.identifier) for prop in props: # Only write this property if our parent hasn't got it. if prop in bl_rna_base_props: continue if prop.identifier in {"rna_type", "bl_icon", "icon"}: continue reports["rna_props"].append((cls, prop)) msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier) msgctxt = prop.translation_context or default_context if prop.name and prop_name_validate(cls, prop.name, prop.identifier): process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings) if prop.description: process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings) if isinstance(prop, bpy.types.EnumProperty): done_items = set() for item in prop.enum_items: msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and prop_name_validate( cls, item.name, item.identifier): process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) for item in prop.enum_items_static: if item.identifier in done_items: continue msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and prop_name_validate( cls, item.name, item.identifier): process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) def walk_tools_definitions(cls): from bl_ui.space_toolsystem_common import ToolDef bl_rna = cls.bl_rna op_default_context = bpy.app.translations.contexts.operator_default def process_tooldef(tool_context, tool): if not isinstance(tool, ToolDef): if callable(tool): for t in tool(None): process_tooldef(tool_context, t) return msgsrc = "bpy.types.{} Tools: '{}', '{}'".format( bl_rna.identifier, tool_context, tool.idname) if tool.label: process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings) # Callable (function) descriptions must handle their translations themselves. if tool.description and not callable(tool.description): process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings) for tool_context, tools_defs in cls.tools_all(): for tools_group in tools_defs: if tools_group is None: continue elif isinstance(tools_group, tuple) and not isinstance( tools_group, ToolDef): for tool in tools_group: process_tooldef(tool_context, tool) else: process_tooldef(tool_context, tools_group) blacklist_rna_class = class_blacklist() def walk_class(cls): bl_rna = cls.bl_rna msgsrc = "bpy.types." + bl_rna.identifier msgctxt = bl_rna.translation_context or default_context if bl_rna.name and (bl_rna.name != bl_rna.identifier or (msgctxt != default_context and not hasattr(bl_rna, 'bl_label'))): process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings) if bl_rna.description: process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings) elif cls.__doc__: # XXX Some classes (like KeyingSetInfo subclasses) have void description... :( process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings) # Panels' "tabs" system. if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category: process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings) if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label: process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings) # Tools Panels definitions. if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all: walk_tools_definitions(cls) walk_properties(cls) def walk_keymap_hierarchy(hier, msgsrc_prev): km_i18n_context = bpy.app.translations.contexts.id_windowmanager for lvl in hier: msgsrc = msgsrc_prev + "." + lvl[1] if isinstance( lvl[0], str): # Can be a function too, now, with tool system... process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings) if lvl[3]: walk_keymap_hierarchy(lvl[3], msgsrc) # Dump Messages operator_categories = {} def process_cls_list(cls_list): if not cls_list: return def full_class_id(cls): """Gives us 'ID.Light.AreaLight' which is best for sorting.""" # Always the same issue, some classes listed in blacklist should actually no more exist (they have been # unregistered), but are still listed by __subclasses__() calls... :/ if cls in blacklist_rna_class: return cls.__name__ cls_id = "" bl_rna = getattr(cls, "bl_rna", None) # It seems that py-defined 'wrappers' RNA classes (like `MeshEdge` in `bpy_types.py`) need to be accessed # once from `bpy.types` before they have a valid `bl_rna` member. # Weirdly enough, this is only triggered on release builds, debug builds somehow do not have that issue. if bl_rna is None: if getattr(bpy.types, cls.__name__, None) is not None: bl_rna = getattr(cls, "bl_rna", None) if bl_rna is None: raise TypeError("Unknown RNA class") while bl_rna: cls_id = bl_rna.identifier + "." + cls_id bl_rna = bl_rna.base return cls_id def operator_category(cls): """Extract operators' categories, as displayed in 'search' space menu.""" # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator(). if issubclass( cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__: cat_id = cls.__name__.split("_OT_")[0] if cat_id not in operator_categories: cat_str = cat_id.capitalize() + ":" operator_categories[cat_id] = cat_str if verbose: print(cls_list) cls_list.sort(key=full_class_id) for cls in cls_list: if verbose: print(cls) reports["rna_structs"].append(cls) # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)... if (cls in blacklist_rna_class) or issubclass( cls, bpy.types.Operator): reports["rna_structs_skipped"].append(cls) else: operator_category(cls) walk_class(cls) # Recursively process subclasses. process_cls_list(cls.__subclasses__()) # FIXME Workaround weird new (blender 3.2) issue where some classes (like `bpy.types.Modifier`) # are not listed by `bpy.types.ID.__base__.__subclasses__()` until they are accessed from # `bpy.types` (eg just executing `bpy.types.Modifier`). cls_dir = dir(bpy.types) for cls_name in cls_dir: getattr(bpy.types, cls_name) # Parse everything (recursively parsing from bpy_struct "class"...). process_cls_list(bpy.types.ID.__base__.__subclasses__()) # Finalize generated 'operator categories' messages. for cat_str in operator_categories.values(): process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category", reports, check_ctxt_rna, settings) # And parse keymaps! from bl_keymap_utils import keymap_hierarchy walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")
def draw_hierarchy(display_keymaps, layout): from bl_keymap_utils import keymap_hierarchy for entry in keymap_hierarchy.generate(): draw_entry(display_keymaps, entry, layout)
def dump_rna_messages(msgs, reports, settings, verbose=False): """ Dump into messages dict all RNA-defined UI messages (labels en tooltips). """ def class_blacklist(): blacklist_rna_class = {getattr(bpy.types, cls_id) for cls_id in ( # core classes "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct", # registerable classes "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo", # window classes "Window", ) } # More builtin classes we don't need to parse. blacklist_rna_class |= {cls for cls in bpy.types.Property.__subclasses__()} # None of this seems needed anymore, and it's broken anyway with current master (blender 2.79.1)... """ _rna = {getattr(bpy.types, cls) for cls in dir(bpy.types)} # Classes which are attached to collections can be skipped too, these are api access only. # XXX This is not true, some of those show in UI, see e.g. tooltip of KeyingSets.active... #~ for cls in _rna: #~ for prop in cls.bl_rna.properties: #~ if prop.type == 'COLLECTION': #~ prop_cls = prop.srna #~ if prop_cls is not None: #~ blacklist_rna_class.add(prop_cls.__class__) # Now here is the *ugly* hack! # Unfortunately, all classes we want to access are not available from bpy.types (OperatorProperties subclasses # are not here, as they have the same name as matching Operator ones :( ). So we use __subclasses__() calls # to walk through all rna hierarchy. # But unregistered classes remain listed by relevant __subclasses__() calls (be it a Py or BPY/RNA bug), # and obviously the matching RNA struct exists no more, so trying to access their data (even the identifier) # quickly leads to segfault! # To address this, we have to blacklist classes which __name__ does not match any __name__ from bpy.types # (we can't use only RNA identifiers, as some py-defined classes has a different name that rna id, # and we can't use class object themselves, because OperatorProperties subclasses are not in bpy.types!)... _rna_clss_ids = {cls.__name__ for cls in _rna} | {cls.bl_rna.identifier for cls in _rna} # All registrable types. blacklist_rna_class |= {cls for cls in bpy.types.OperatorProperties.__subclasses__() + bpy.types.Operator.__subclasses__() + bpy.types.OperatorMacro.__subclasses__() + bpy.types.Header.__subclasses__() + bpy.types.Panel.__subclasses__() + bpy.types.Menu.__subclasses__() + bpy.types.UIList.__subclasses__() if cls.__name__ not in _rna_clss_ids} # Collect internal operators # extend with all internal operators # note that this uses internal api introspection functions # XXX Do not skip INTERNAL's anymore, some of those ops show up in UI now! # all possible operator names #op_ids = (set(cls.bl_rna.identifier for cls in bpy.types.OperatorProperties.__subclasses__()) | # set(cls.bl_rna.identifier for cls in bpy.types.Operator.__subclasses__()) | # set(cls.bl_rna.identifier for cls in bpy.types.OperatorMacro.__subclasses__())) #get_instance = __import__("_bpy").ops.get_instance #path_resolve = type(bpy.context).__base__.path_resolve #for idname in op_ids: #op = get_instance(idname) #if 'INTERNAL' in path_resolve(op, "bl_options"): #blacklist_rna_class.add(idname) """ return blacklist_rna_class check_ctxt_rna = check_ctxt_rna_tip = None check_ctxt = reports["check_ctxt"] if check_ctxt: check_ctxt_rna = { "multi_lines": check_ctxt.get("multi_lines"), "not_capitalized": check_ctxt.get("not_capitalized"), "end_point": check_ctxt.get("end_point"), "undoc_ops": check_ctxt.get("undoc_ops"), "spell_checker": check_ctxt.get("spell_checker"), "spell_errors": check_ctxt.get("spell_errors"), } check_ctxt_rna_tip = check_ctxt_rna check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip") default_context = settings.DEFAULT_CONTEXT # Function definitions def walk_properties(cls): bl_rna = cls.bl_rna # Get our parents' properties, to not export them multiple times. bl_rna_base = bl_rna.base if bl_rna_base: bl_rna_base_props = set(bl_rna_base.properties.values()) else: bl_rna_base_props = set() props = sorted(bl_rna.properties, key=lambda p: p.identifier) for prop in props: # Only write this property if our parent hasn't got it. if prop in bl_rna_base_props: continue if prop.identifier == "rna_type": continue reports["rna_props"].append((cls, prop)) msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier) msgctxt = prop.translation_context or default_context if prop.name and (prop.name != prop.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings) if prop.description: process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings) if isinstance(prop, bpy.types.EnumProperty): done_items = set() for item in prop.enum_items: msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) for item in prop.enum_items_static: if item.identifier in done_items: continue msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) def walk_tools_definitions(cls): from bl_ui.space_toolsystem_common import ToolDef bl_rna = cls.bl_rna op_default_context = bpy.app.translations.contexts.operator_default def process_tooldef(tool_context, tool): if not isinstance(tool, ToolDef): if callable(tool): for t in tool(None): process_tooldef(tool_context, t) return msgsrc = "bpy.types.{} Tools: '{}', '{}'".format(bl_rna.identifier, tool_context, tool.idname) if tool.label: process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings) # Callable (function) descriptions must handle their translations themselves. if tool.description and not callable(tool.description): process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings) for tool_context, tools_defs in cls.tools_all(): for tools_group in tools_defs: if tools_group is None: continue elif isinstance(tools_group, tuple) and not isinstance(tools_group, ToolDef): for tool in tools_group: process_tooldef(tool_context, tool) else: process_tooldef(tool_context, tools_group) blacklist_rna_class = class_blacklist() def walk_class(cls): bl_rna = cls.bl_rna msgsrc = "bpy.types." + bl_rna.identifier msgctxt = bl_rna.translation_context or default_context if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings) if bl_rna.description: process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings) elif cls.__doc__: # XXX Some classes (like KeyingSetInfo subclasses) have void description... :( process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings) # Panels' "tabs" system. if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category: process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings) if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label: process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings) # Tools Panels definitions. if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all: walk_tools_definitions(cls) walk_properties(cls) def walk_keymap_hierarchy(hier, msgsrc_prev): km_i18n_context = bpy.app.translations.contexts.id_windowmanager for lvl in hier: msgsrc = msgsrc_prev + "." + lvl[1] if isinstance(lvl[0], str): # Can be a function too, now, with tool system... process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings) if lvl[3]: walk_keymap_hierarchy(lvl[3], msgsrc) # Dump Messages operator_categories = {} def process_cls_list(cls_list): if not cls_list: return def full_class_id(cls): """Gives us 'ID.Light.AreaLight' which is best for sorting.""" # Always the same issue, some classes listed in blacklist should actually no more exist (they have been # unregistered), but are still listed by __subclasses__() calls... :/ if cls in blacklist_rna_class: return cls.__name__ cls_id = "" bl_rna = cls.bl_rna while bl_rna: cls_id = bl_rna.identifier + "." + cls_id bl_rna = bl_rna.base return cls_id def operator_category(cls): """Extract operators' categories, as displayed in 'search' space menu.""" # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator(). if issubclass(cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__: cat_id = cls.__name__.split("_OT_")[0] if cat_id not in operator_categories: cat_str = cat_id.capitalize() + ":" operator_categories[cat_id] = cat_str if verbose: print(cls_list) cls_list.sort(key=full_class_id) for cls in cls_list: if verbose: print(cls) reports["rna_structs"].append(cls) # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)... if (cls in blacklist_rna_class) or issubclass(cls, bpy.types.Operator): reports["rna_structs_skipped"].append(cls) else: operator_category(cls) walk_class(cls) # Recursively process subclasses. process_cls_list(cls.__subclasses__()) # Parse everything (recursively parsing from bpy_struct "class"...). process_cls_list(bpy.types.ID.__base__.__subclasses__()) # Finalize generated 'operator categories' messages. for cat_str in operator_categories.values(): process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category", reports, check_ctxt_rna, settings) # And parse keymaps! from bl_keymap_utils import keymap_hierarchy walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")
def dump_rna_messages(msgs, reports, settings, verbose=False): """ Dump into messages dict all RNA-defined UI messages (labels en tooltips). """ def class_blacklist(): blacklist_rna_class = { getattr(bpy.types, cls_id) for cls_id in ( # core classes "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct", # registerable classes "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo", # window classes "Window", ) } # More builtin classes we don't need to parse. blacklist_rna_class |= { cls for cls in bpy.types.Property.__subclasses__() } # None of this seems needed anymore, and it's broken anyway with current master (blender 2.79.1)... """ _rna = {getattr(bpy.types, cls) for cls in dir(bpy.types)} # Classes which are attached to collections can be skipped too, these are api access only. # XXX This is not true, some of those show in UI, see e.g. tooltip of KeyingSets.active... #~ for cls in _rna: #~ for prop in cls.bl_rna.properties: #~ if prop.type == 'COLLECTION': #~ prop_cls = prop.srna #~ if prop_cls is not None: #~ blacklist_rna_class.add(prop_cls.__class__) # Now here is the *ugly* hack! # Unfortunately, all classes we want to access are not available from bpy.types (OperatorProperties subclasses # are not here, as they have the same name as matching Operator ones :( ). So we use __subclasses__() calls # to walk through all rna hierarchy. # But unregistered classes remain listed by relevant __subclasses__() calls (be it a Py or BPY/RNA bug), # and obviously the matching RNA struct exists no more, so trying to access their data (even the identifier) # quickly leads to segfault! # To address this, we have to blacklist classes which __name__ does not match any __name__ from bpy.types # (we can't use only RNA identifiers, as some py-defined classes has a different name that rna id, # and we can't use class object themselves, because OperatorProperties subclasses are not in bpy.types!)... _rna_clss_ids = {cls.__name__ for cls in _rna} | {cls.bl_rna.identifier for cls in _rna} # All registrable types. blacklist_rna_class |= {cls for cls in bpy.types.OperatorProperties.__subclasses__() + bpy.types.Operator.__subclasses__() + bpy.types.OperatorMacro.__subclasses__() + bpy.types.Header.__subclasses__() + bpy.types.Panel.__subclasses__() + bpy.types.Menu.__subclasses__() + bpy.types.UIList.__subclasses__() if cls.__name__ not in _rna_clss_ids} # Collect internal operators # extend with all internal operators # note that this uses internal api introspection functions # XXX Do not skip INTERNAL's anymore, some of those ops show up in UI now! # all possible operator names #op_ids = (set(cls.bl_rna.identifier for cls in bpy.types.OperatorProperties.__subclasses__()) | # set(cls.bl_rna.identifier for cls in bpy.types.Operator.__subclasses__()) | # set(cls.bl_rna.identifier for cls in bpy.types.OperatorMacro.__subclasses__())) #get_instance = __import__("_bpy").ops.get_instance #path_resolve = type(bpy.context).__base__.path_resolve #for idname in op_ids: #op = get_instance(idname) #if 'INTERNAL' in path_resolve(op, "bl_options"): #blacklist_rna_class.add(idname) """ return blacklist_rna_class check_ctxt_rna = check_ctxt_rna_tip = None check_ctxt = reports["check_ctxt"] if check_ctxt: check_ctxt_rna = { "multi_lines": check_ctxt.get("multi_lines"), "not_capitalized": check_ctxt.get("not_capitalized"), "end_point": check_ctxt.get("end_point"), "undoc_ops": check_ctxt.get("undoc_ops"), "spell_checker": check_ctxt.get("spell_checker"), "spell_errors": check_ctxt.get("spell_errors"), } check_ctxt_rna_tip = check_ctxt_rna check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip") default_context = settings.DEFAULT_CONTEXT # Function definitions def walk_properties(cls): bl_rna = cls.bl_rna # Get our parents' properties, to not export them multiple times. bl_rna_base = bl_rna.base if bl_rna_base: bl_rna_base_props = set(bl_rna_base.properties.values()) else: bl_rna_base_props = set() props = sorted(bl_rna.properties, key=lambda p: p.identifier) for prop in props: # Only write this property if our parent hasn't got it. if prop in bl_rna_base_props: continue if prop.identifier == "rna_type": continue reports["rna_props"].append((cls, prop)) msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier) msgctxt = prop.translation_context or default_context if prop.name and (prop.name != prop.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings) if prop.description: process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings) if isinstance(prop, bpy.types.EnumProperty): done_items = set() for item in prop.enum_items: msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) for item in prop.enum_items_static: if item.identifier in done_items: continue msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) def walk_tools_definitions(cls): from bl_ui.space_toolsystem_common import ToolDef bl_rna = cls.bl_rna op_default_context = bpy.app.translations.contexts.operator_default def process_tooldef(tool_context, tool): if not isinstance(tool, ToolDef): if callable(tool): for t in tool(None): process_tooldef(tool_context, t) return msgsrc = "bpy.types.{} Tools: '{}', '{}'".format( bl_rna.identifier, tool_context, tool.idname) if tool.label: process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings) # Callable (function) descriptions must handle their translations themselves. if tool.description and not callable(tool.description): process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings) for tool_context, tools_defs in cls.tools_all(): for tools_group in tools_defs: if tools_group is None: continue elif isinstance(tools_group, tuple) and not isinstance( tools_group, ToolDef): for tool in tools_group: process_tooldef(tool_context, tool) else: process_tooldef(tool_context, tools_group) blacklist_rna_class = class_blacklist() def walk_class(cls): bl_rna = cls.bl_rna msgsrc = "bpy.types." + bl_rna.identifier msgctxt = bl_rna.translation_context or default_context if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings) if bl_rna.description: process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings) elif cls.__doc__: # XXX Some classes (like KeyingSetInfo subclasses) have void description... :( process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings) # Panels' "tabs" system. if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category: process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings) if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label: process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings) # Tools Panels definitions. if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all: walk_tools_definitions(cls) walk_properties(cls) def walk_keymap_hierarchy(hier, msgsrc_prev): km_i18n_context = bpy.app.translations.contexts.id_windowmanager for lvl in hier: msgsrc = msgsrc_prev + "." + lvl[1] if isinstance( lvl[0], str): # Can be a function too, now, with tool system... process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings) if lvl[3]: walk_keymap_hierarchy(lvl[3], msgsrc) # Dump Messages operator_categories = {} def process_cls_list(cls_list): if not cls_list: return def full_class_id(cls): """Gives us 'ID.Light.AreaLight' which is best for sorting.""" # Always the same issue, some classes listed in blacklist should actually no more exist (they have been # unregistered), but are still listed by __subclasses__() calls... :/ if cls in blacklist_rna_class: return cls.__name__ cls_id = "" bl_rna = cls.bl_rna while bl_rna: cls_id = bl_rna.identifier + "." + cls_id bl_rna = bl_rna.base return cls_id def operator_category(cls): """Extract operators' categories, as displayed in 'search' space menu.""" # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator(). if issubclass( cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__: cat_id = cls.__name__.split("_OT_")[0] if cat_id not in operator_categories: cat_str = cat_id.capitalize() + ":" operator_categories[cat_id] = cat_str if verbose: print(cls_list) cls_list.sort(key=full_class_id) for cls in cls_list: if verbose: print(cls) reports["rna_structs"].append(cls) # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)... if (cls in blacklist_rna_class) or issubclass( cls, bpy.types.Operator): reports["rna_structs_skipped"].append(cls) else: operator_category(cls) walk_class(cls) # Recursively process subclasses. process_cls_list(cls.__subclasses__()) # Parse everything (recursively parsing from bpy_struct "class"...). process_cls_list(bpy.types.ID.__base__.__subclasses__()) # Finalize generated 'operator categories' messages. for cat_str in operator_categories.values(): process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category", reports, check_ctxt_rna, settings) # And parse keymaps! from bl_keymap_utils import keymap_hierarchy walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")
def dump_rna_messages(msgs, reports, settings, verbose=False): """ Dump into messages dict all RNA-defined UI messages (labels en tooltips). """ def class_blacklist(): blacklist_rna_class = { getattr(bpy.types, cls_id) for cls_id in ( # core classes "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct", # registerable classes "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo", # window classes "Window", ) } # More builtin classes we don't need to parse. blacklist_rna_class |= { cls for cls in bpy.types.Property.__subclasses__() } return blacklist_rna_class check_ctxt_rna = check_ctxt_rna_tip = None check_ctxt = reports["check_ctxt"] if check_ctxt: check_ctxt_rna = { "multi_lines": check_ctxt.get("multi_lines"), "not_capitalized": check_ctxt.get("not_capitalized"), "end_point": check_ctxt.get("end_point"), "undoc_ops": check_ctxt.get("undoc_ops"), "spell_checker": check_ctxt.get("spell_checker"), "spell_errors": check_ctxt.get("spell_errors"), } check_ctxt_rna_tip = check_ctxt_rna check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip") default_context = settings.DEFAULT_CONTEXT # Function definitions def walk_properties(cls): bl_rna = cls.bl_rna # Get our parents' properties, to not export them multiple times. bl_rna_base = bl_rna.base if bl_rna_base: bl_rna_base_props = set(bl_rna_base.properties.values()) else: bl_rna_base_props = set() props = sorted(bl_rna.properties, key=lambda p: p.identifier) for prop in props: # Only write this property if our parent hasn't got it. if prop in bl_rna_base_props: continue if prop.identifier == "rna_type": continue reports["rna_props"].append((cls, prop)) msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier) msgctxt = prop.translation_context or default_context if prop.name and (prop.name != prop.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings) if prop.description: process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings) if isinstance(prop, bpy.types.EnumProperty): done_items = set() for item in prop.enum_items: msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) for item in prop.enum_items_static: if item.identifier in done_items: continue msgsrc = "bpy.types.{}.{}:'{}'".format( bl_rna.identifier, prop.identifier, item.identifier) done_items.add(item.identifier) if item.name and item.name != item.identifier: process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings) if item.description: process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip, settings) def walk_tools_definitions(cls): from bl_ui.space_toolsystem_common import ToolDef bl_rna = cls.bl_rna op_default_context = bpy.app.translations.contexts.operator_default def process_tooldef(tool_context, tool): if not isinstance(tool, ToolDef): if callable(tool): for t in tool(None): process_tooldef(tool_context, t) return msgsrc = "bpy.types.{} Tools: '{}', '{}'".format( bl_rna.identifier, tool_context, tool.idname) if tool.label: process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings) # Callable (function) descriptions must handle their translations themselves. if tool.description and not callable(tool.description): process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings) for tool_context, tools_defs in cls.tools_all(): for tools_group in tools_defs: if tools_group is None: continue elif isinstance(tools_group, tuple) and not isinstance( tools_group, ToolDef): for tool in tools_group: process_tooldef(tool_context, tool) else: process_tooldef(tool_context, tools_group) blacklist_rna_class = class_blacklist() def walk_class(cls): bl_rna = cls.bl_rna msgsrc = "bpy.types." + bl_rna.identifier msgctxt = bl_rna.translation_context or default_context if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context): process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings) if bl_rna.description: process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings) elif cls.__doc__: # XXX Some classes (like KeyingSetInfo subclasses) have void description... :( process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings) # Panels' "tabs" system. if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category: process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings) if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label: process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings) # Tools Panels definitions. if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all: walk_tools_definitions(cls) walk_properties(cls) def walk_keymap_hierarchy(hier, msgsrc_prev): km_i18n_context = bpy.app.translations.contexts.id_windowmanager for lvl in hier: msgsrc = msgsrc_prev + "." + lvl[1] if isinstance( lvl[0], str): # Can be a function too, now, with tool system... process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings) if lvl[3]: walk_keymap_hierarchy(lvl[3], msgsrc) # Dump Messages operator_categories = {} def process_cls_list(cls_list): if not cls_list: return def full_class_id(cls): """Gives us 'ID.Light.AreaLight' which is best for sorting.""" # Always the same issue, some classes listed in blacklist should actually no more exist (they have been # unregistered), but are still listed by __subclasses__() calls... :/ if cls in blacklist_rna_class: return cls.__name__ cls_id = "" bl_rna = cls.bl_rna while bl_rna: cls_id = bl_rna.identifier + "." + cls_id bl_rna = bl_rna.base return cls_id def operator_category(cls): """Extract operators' categories, as displayed in 'search' space menu.""" # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator(). if issubclass( cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__: cat_id = cls.__name__.split("_OT_")[0] if cat_id not in operator_categories: cat_str = cat_id.capitalize() + ":" operator_categories[cat_id] = cat_str if verbose: print(cls_list) cls_list.sort(key=full_class_id) for cls in cls_list: if verbose: print(cls) reports["rna_structs"].append(cls) # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)... if (cls in blacklist_rna_class) or issubclass( cls, bpy.types.Operator): reports["rna_structs_skipped"].append(cls) else: operator_category(cls) walk_class(cls) # Recursively process subclasses. process_cls_list(cls.__subclasses__()) # Parse everything (recursively parsing from bpy_struct "class"...). process_cls_list(bpy.types.ID.__base__.__subclasses__()) # Finalize generated 'operator categories' messages. for cat_str in operator_categories.values(): process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category", reports, check_ctxt_rna, settings) # And parse keymaps! from bl_keymap_utils import keymap_hierarchy walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")