# SPDX-License-Identifier: GPL-2.0-or-later # classes for extracting info from blenders internal classes import bpy # use to strip python paths script_paths = bpy.utils.script_paths() _FAKE_STRUCT_SUBCLASS = True def _get_direct_attr(rna_type, attr): props = getattr(rna_type, attr) base = rna_type.base if not base: return [prop for prop in props] else: props_base = getattr(base, attr).values() return [prop for prop in props if prop not in props_base] def get_direct_properties(rna_type): return _get_direct_attr(rna_type, "properties") def get_direct_functions(rna_type): return _get_direct_attr(rna_type, "functions") def rna_id_ignore(rna_id): if rna_id == "rna_type": return True if "_OT_" in rna_id: return True if "_MT_" in rna_id: return True if "_PT_" in rna_id: return True if "_HT_" in rna_id: return True if "_KSI_" in rna_id: return True return False def range_str(val): if val < -10000000: return "-inf" elif val > 10000000: return "inf" elif type(val) == float: return '%g' % val else: return str(val) def float_as_string(f): val_str = "%g" % f # Ensure a `.0` suffix for whole numbers, excluding scientific notation such as `1e-05` or `1e+5`. if '.' not in val_str and 'e' not in val_str: val_str += '.0' return val_str def get_py_class_from_rna(rna_type): """ Gets the Python type for a class which isn't necessarily added to ``bpy.types``. """ identifier = rna_type.identifier py_class = getattr(bpy.types, identifier, None) if py_class is not None: return py_class def subclasses_recurse(cls): for c in cls.__subclasses__(): # is_registered if "bl_rna" in cls.__dict__: yield c yield from subclasses_recurse(c) while py_class is None: base = rna_type.base if base is None: raise Exception("can't find type") py_class_base = getattr(bpy.types, base.identifier, None) if py_class_base is not None: for cls in subclasses_recurse(py_class_base): if cls.bl_rna.identifier == identifier: return cls class InfoStructRNA: __slots__ = ( "bl_rna", "identifier", "name", "description", "base", "nested", "full_path", "functions", "children", "references", "properties", "py_class", "module_name", ) global_lookup = {} def __init__(self, rna_type): self.bl_rna = rna_type self.identifier = rna_type.identifier self.name = rna_type.name self.description = rna_type.description.strip() # set later self.base = None self.nested = None self.full_path = "" self.functions = [] self.children = [] self.references = [] self.properties = [] self.py_class = get_py_class_from_rna(self.bl_rna) self.module_name = ( self.py_class.__module__ if (self.py_class and not hasattr(bpy.types, self.identifier)) else "bpy.types" ) if self.module_name == "bpy_types": self.module_name = "bpy.types" def build(self): rna_type = self.bl_rna parent_id = self.identifier self.properties[:] = [GetInfoPropertyRNA(rna_prop, parent_id) for rna_prop in get_direct_properties(rna_type) if rna_prop.identifier != "rna_type"] self.functions[:] = [GetInfoFunctionRNA(rna_prop, parent_id) for rna_prop in get_direct_functions(rna_type)] def get_bases(self): bases = [] item = self while item: item = item.base if item: bases.append(item) return bases def get_nested_properties(self, ls=None): if not ls: ls = self.properties[:] if self.nested: self.nested.get_nested_properties(ls) return ls def _get_py_visible_attrs(self): attrs = [] py_class = self.py_class for attr_str in dir(py_class): if attr_str.startswith("_"): continue attrs.append((attr_str, getattr(py_class, attr_str))) return attrs def get_py_properties(self): properties = [] for identifier, attr in self._get_py_visible_attrs(): if type(attr) is property: properties.append((identifier, attr)) return properties def get_py_functions(self): import types functions = [] for identifier, attr in self._get_py_visible_attrs(): # methods may be python wrappers to C functions attr_func = getattr(attr, "__func__", attr) if type(attr_func) in {types.FunctionType, types.MethodType}: functions.append((identifier, attr)) return functions def get_py_c_functions(self): import types functions = [] for identifier, attr in self._get_py_visible_attrs(): # methods may be python wrappers to C functions attr_func = getattr(attr, "__func__", attr) if ( (type(attr_func) in {types.BuiltinMethodType, types.BuiltinFunctionType}) or # Without the `objclass` check, many inherited methods are included. (type(attr_func) == types.MethodDescriptorType and attr_func.__objclass__ == self.py_class) ): functions.append((identifier, attr)) return functions def get_py_c_properties_getset(self): import types properties_getset = [] for identifier, descr in self.py_class.__dict__.items(): if type(descr) == types.GetSetDescriptorType: properties_getset.append((identifier, descr)) return properties_getset def __str__(self): txt = "" txt += self.identifier if self.base: txt += "(%s)" % self.base.identifier txt += ": " + self.description + "\n" for prop in self.properties: txt += prop.__repr__() + "\n" for func in self.functions: txt += func.__repr__() + "\n" return txt class InfoPropertyRNA: __slots__ = ( "bl_prop", "srna", "identifier", "name", "description", "default_str", "default", "enum_items", "enum_pointer", "min", "max", "array_length", "array_dimensions", "collection_type", "type", "fixed_type", "is_argument_optional", "is_enum_flag", "is_required", "is_readonly", "is_never_none", ) global_lookup = {} def __init__(self, rna_prop): self.bl_prop = rna_prop self.identifier = rna_prop.identifier self.name = rna_prop.name self.description = rna_prop.description.strip() self.default_str = "" def build(self): rna_prop = self.bl_prop self.enum_items = [] self.min = getattr(rna_prop, "hard_min", -1) self.max = getattr(rna_prop, "hard_max", -1) self.array_length = getattr(rna_prop, "array_length", 0) self.array_dimensions = getattr(rna_prop, "array_dimensions", ())[:] self.collection_type = GetInfoStructRNA(rna_prop.srna) self.is_required = rna_prop.is_required self.is_readonly = rna_prop.is_readonly self.is_never_none = rna_prop.is_never_none self.is_argument_optional = rna_prop.is_argument_optional self.type = rna_prop.type.lower() fixed_type = getattr(rna_prop, "fixed_type", "") if fixed_type: self.fixed_type = GetInfoStructRNA(fixed_type) # valid for pointer/collections else: self.fixed_type = None self.enum_pointer = 0 if self.type == "enum": # WARNING: don't convert to a tuple as this causes dynamically allocated enums to access freed memory # since freeing the iterator may free the memory used to store the internal `EnumPropertyItem` array. # To support this properly RNA would have to support owning the dynamically allocated memory. items = rna_prop.enum_items items_static = tuple(rna_prop.enum_items_static) self.enum_items[:] = [(item.identifier, item.name, item.description) for item in items] self.is_enum_flag = rna_prop.is_enum_flag # Prioritize static items as this is never going to be allocated data and is therefor # will be a stable match to compare against. item = (items_static or items) if item: self.enum_pointer = item[0].as_pointer() del items, items_static, item else: self.is_enum_flag = False self.default_str = "" # fallback if self.array_length: self.default = tuple(getattr(rna_prop, "default_array", ())) if self.array_dimensions[1] != 0: # Multi-dimensional array, convert default flat one accordingly. self.default_str = tuple(float_as_string(v) if self.type == "float" else str(v) for v in self.default) for dim in self.array_dimensions[::-1]: if dim != 0: self.default = tuple(zip(*((iter(self.default),) * dim))) self.default_str = tuple( "(%s)" % ", ".join(s for s in b) for b in zip(*((iter(self.default_str),) * dim)) ) self.default_str = self.default_str[0] elif self.type == "enum" and self.is_enum_flag: self.default = getattr(rna_prop, "default_flag", set()) else: self.default = getattr(rna_prop, "default", None) if self.type == "pointer": # pointer has no default, just set as None self.default = None self.default_str = "None" elif self.type == "string": self.default_str = "\"%s\"" % self.default elif self.type == "enum": if self.is_enum_flag: # self.default_str = "%r" % self.default # repr or set() self.default_str = "{%s}" % repr(list(sorted(self.default)))[1:-1] else: self.default_str = "'%s'" % self.default elif self.array_length: if self.array_dimensions[1] == 0: # single dimension array, we already took care of multi-dimensions ones. # special case for floats if self.type == "float" and len(self.default) > 0: self.default_str = "(%s)" % ", ".join(float_as_string(f) for f in self.default) else: self.default_str = str(self.default) else: if self.type == "float": self.default_str = float_as_string(self.default) else: self.default_str = str(self.default) self.srna = GetInfoStructRNA(rna_prop.srna) # valid for pointer/collections def get_arg_default(self, force=True): default = self.default_str if default and (force or self.is_required is False): return "%s=%s" % (self.identifier, default) return self.identifier def get_type_description( self, *, as_ret=False, as_arg=False, class_fmt="%s", collection_id="Collection", enum_descr_override=None, ): """ :arg enum_descr_override: Optionally override items for enum. Otherwise expand the literal items. :type enum_descr_override: string or None when unset. """ type_str = "" if self.fixed_type is None: type_str += self.type if self.array_length: if self.array_dimensions[1] != 0: type_str += " multi-dimensional array of %s items" % ( " * ".join(str(d) for d in self.array_dimensions if d != 0) ) else: type_str += " array of %d items" % (self.array_length) if self.type in {"float", "int"}: type_str += " in [%s, %s]" % (range_str(self.min), range_str(self.max)) elif self.type == "enum": enum_descr = enum_descr_override if not enum_descr: if self.is_enum_flag: enum_descr = "{%s}" % ", ".join(("'%s'" % s[0]) for s in self.enum_items) else: enum_descr = "[%s]" % ", ".join(("'%s'" % s[0]) for s in self.enum_items) if self.is_enum_flag: type_str += " set in %s" % enum_descr else: type_str += " in %s" % enum_descr del enum_descr if not (as_arg or as_ret): # write default property, ignore function args for this if self.type != "pointer": if self.default_str: type_str += ", default %s" % self.default_str else: if self.type == "collection": if self.collection_type: collection_str = (class_fmt % self.collection_type.identifier) + (" %s of " % collection_id) else: collection_str = "%s of " % collection_id else: collection_str = "" type_str += collection_str + (class_fmt % self.fixed_type.identifier) # setup qualifiers for this value. type_info = [] if as_ret: pass elif as_arg: if not self.is_required: type_info.append("optional") if self.is_argument_optional: type_info.append("optional argument") else: # readonly is only useful for self's, not args if self.is_readonly: type_info.append("readonly") if self.is_never_none: type_info.append("never None") if type_info: type_str += (", (%s)" % ", ".join(type_info)) return type_str def __str__(self): txt = "" txt += " * " + self.identifier + ": " + self.description return txt class InfoFunctionRNA: __slots__ = ( "bl_func", "identifier", "description", "args", "return_values", "is_classmethod", ) global_lookup = {} def __init__(self, rna_func): self.bl_func = rna_func self.identifier = rna_func.identifier # self.name = rna_func.name # functions have no name! self.description = rna_func.description.strip() self.is_classmethod = not rna_func.use_self self.args = [] self.return_values = () def build(self): rna_func = self.bl_func parent_id = rna_func self.return_values = [] for rna_prop in rna_func.parameters.values(): prop = GetInfoPropertyRNA(rna_prop, parent_id) if rna_prop.is_output: self.return_values.append(prop) else: self.args.append(prop) self.return_values = tuple(self.return_values) def __str__(self): txt = '' txt += ' * ' + self.identifier + '(' for arg in self.args: txt += arg.identifier + ', ' txt += '): ' + self.description return txt class InfoOperatorRNA: __slots__ = ( "bl_op", "identifier", "name", "module_name", "func_name", "description", "args", ) global_lookup = {} def __init__(self, rna_op): self.bl_op = rna_op self.identifier = rna_op.identifier mod, name = self.identifier.split("_OT_", 1) self.module_name = mod.lower() self.func_name = name # self.name = rna_func.name # functions have no name! self.description = rna_op.description.strip() self.args = [] def build(self): rna_op = self.bl_op parent_id = self.identifier for rna_id, rna_prop in rna_op.properties.items(): if rna_id == "rna_type": continue prop = GetInfoPropertyRNA(rna_prop, parent_id) self.args.append(prop) def get_location(self): try: op_class = getattr(bpy.types, self.identifier) except AttributeError: # defined in C. return None, None op_func = getattr(op_class, "execute", None) if op_func is None: op_func = getattr(op_class, "invoke", None) if op_func is None: op_func = getattr(op_class, "poll", None) if op_func: op_code = op_func.__code__ source_path = op_code.co_filename # clear the prefix for p in script_paths: source_path = source_path.split(p)[-1] if source_path[0] in "/\\": source_path = source_path[1:] return source_path, op_code.co_firstlineno else: return None, None def _GetInfoRNA(bl_rna, cls, parent_id=""): if bl_rna is None: return None key = parent_id, bl_rna.identifier try: return cls.global_lookup[key] except KeyError: instance = cls.global_lookup[key] = cls(bl_rna) return instance def GetInfoStructRNA(bl_rna): return _GetInfoRNA(bl_rna, InfoStructRNA) def GetInfoPropertyRNA(bl_rna, parent_id): return _GetInfoRNA(bl_rna, InfoPropertyRNA, parent_id) def GetInfoFunctionRNA(bl_rna, parent_id): return _GetInfoRNA(bl_rna, InfoFunctionRNA, parent_id) def GetInfoOperatorRNA(bl_rna): return _GetInfoRNA(bl_rna, InfoOperatorRNA) def BuildRNAInfo(): # needed on successive calls to prevent stale data access for cls in (InfoStructRNA, InfoFunctionRNA, InfoOperatorRNA, InfoPropertyRNA): cls.global_lookup.clear() del cls # Use for faster lookups # use rna_struct.identifier as the key for each dict rna_struct_dict = {} # store identifier:rna lookups rna_full_path_dict = {} # store the result of full_rna_struct_path(rna_struct) rna_children_dict = {} # store all rna_structs nested from here rna_references_dict = {} # store a list of rna path strings that reference this type # rna_functions_dict = {} # store all functions directly in this type (not inherited) def full_rna_struct_path(rna_struct): """ Needed when referencing one struct from another """ nested = rna_struct.nested if nested: return "%s.%s" % (full_rna_struct_path(nested), rna_struct.identifier) else: return rna_struct.identifier # def write_func(rna_func, ident): def base_id(rna_struct): try: return rna_struct.base.identifier except: return "" # invalid id #structs = [(base_id(rna_struct), rna_struct.identifier, rna_struct) for rna_struct in bpy.doc.structs.values()] ''' structs = [] for rna_struct in bpy.doc.structs.values(): structs.append( (base_id(rna_struct), rna_struct.identifier, rna_struct) ) ''' structs = [] def _bpy_types_iterator(): # Don't report when these types are ignored. suppress_warning = { "bpy_func", "bpy_prop", "bpy_prop_array", "bpy_prop_collection", "bpy_struct", "bpy_struct_meta_idprop", } names_unique = set() rna_type_list = [] for rna_type_name in dir(bpy.types): names_unique.add(rna_type_name) rna_type = getattr(bpy.types, rna_type_name) rna_struct = getattr(rna_type, "bl_rna", None) if rna_struct is not None: rna_type_list.append(rna_type) yield (rna_type_name, rna_struct) elif rna_type_name.startswith("_"): # Ignore "__dir__", "__getattr__" .. etc. pass elif rna_type_name in suppress_warning: pass else: print("rna_info.BuildRNAInfo(..): ignoring type", repr(rna_type_name)) # Now, there are some sub-classes in add-ons we also want to include. # Cycles for e.g. these are referenced from the Scene, but not part of # bpy.types module. # Include all sub-classes we didn't already get from 'bpy.types'. i = 0 while i < len(rna_type_list): rna_type = rna_type_list[i] for rna_sub_type in rna_type.__subclasses__(): rna_sub_struct = getattr(rna_sub_type, "bl_rna", None) if rna_sub_struct is not None: rna_sub_type_name = rna_sub_struct.identifier if rna_sub_type_name not in names_unique: names_unique.add(rna_sub_type_name) rna_type_list.append(rna_sub_type) # The bl_idname may not match the class name in the file. # Always use the 'bl_idname' because using the Python # class name causes confusion - having two names for the same thing. # Since having two names for the same thing is trickier to support # without a significant benefit. yield (rna_sub_type_name, rna_sub_struct) i += 1 for (_rna_type_name, rna_struct) in _bpy_types_iterator(): # if not _rna_type_name.startswith('__'): identifier = rna_struct.identifier if not rna_id_ignore(identifier): structs.append((base_id(rna_struct), identifier, rna_struct)) # Simple lookup rna_struct_dict[identifier] = rna_struct # Store full rna path 'GameObjectSettings' -> 'Object.GameObjectSettings' rna_full_path_dict[identifier] = full_rna_struct_path(rna_struct) # Store a list of functions, remove inherited later # NOT USED YET ## rna_functions_dict[identifier] = get_direct_functions(rna_struct) # fill in these later rna_children_dict[identifier] = [] rna_references_dict[identifier] = [] del _bpy_types_iterator structs.sort() # not needed but speeds up sort below, setting items without an inheritance first # Arrange so classes are always defined in the correct order deps_ok = False while deps_ok is False: deps_ok = True rna_done = set() for i, (rna_base, identifier, rna_struct) in enumerate(structs): rna_done.add(identifier) if rna_base and rna_base not in rna_done: deps_ok = False data = structs.pop(i) ok = False while i < len(structs): if structs[i][1] == rna_base: structs.insert(i + 1, data) # insert after the item we depend on. ok = True break i += 1 if not ok: print('Dependancy "%s" could not be found for "%s"' % (identifier, rna_base)) break # Done ordering structs # precalculate vars to avoid a lot of looping for (rna_base, identifier, rna_struct) in structs: # rna_struct_path = full_rna_struct_path(rna_struct) rna_struct_path = rna_full_path_dict[identifier] for rna_prop in get_direct_properties(rna_struct): rna_prop_identifier = rna_prop.identifier if rna_prop_identifier == 'RNA' or rna_id_ignore(rna_prop_identifier): continue for rna_prop_ptr in (getattr(rna_prop, "fixed_type", None), getattr(rna_prop, "srna", None)): # Does this property point to me? if rna_prop_ptr and rna_prop_ptr.identifier in rna_references_dict: rna_references_dict[rna_prop_ptr.identifier].append( "%s.%s" % (rna_struct_path, rna_prop_identifier)) for rna_func in get_direct_functions(rna_struct): for rna_prop_identifier, rna_prop in rna_func.parameters.items(): if rna_prop_identifier == 'RNA' or rna_id_ignore(rna_prop_identifier): continue rna_prop_ptr = getattr(rna_prop, "fixed_type", None) # Does this property point to me? if rna_prop_ptr and rna_prop_ptr.identifier in rna_references_dict: rna_references_dict[rna_prop_ptr.identifier].append( "%s.%s" % (rna_struct_path, rna_func.identifier)) # Store nested children nested = rna_struct.nested if nested: rna_children_dict[nested.identifier].append(rna_struct) # Sort the refs, just reads nicer for rna_refs in rna_references_dict.values(): rna_refs.sort() info_structs = [] for (rna_base, identifier, rna_struct) in structs: # if rna_struct.nested: # continue #write_struct(rna_struct, '') info_struct = GetInfoStructRNA(rna_struct) if rna_base: info_struct.base = GetInfoStructRNA(rna_struct_dict[rna_base]) info_struct.nested = GetInfoStructRNA(rna_struct.nested) info_struct.children[:] = rna_children_dict[identifier] info_struct.references[:] = rna_references_dict[identifier] info_struct.full_path = rna_full_path_dict[identifier] info_structs.append(info_struct) for rna_info_prop in InfoPropertyRNA.global_lookup.values(): rna_info_prop.build() for rna_info_prop in InfoFunctionRNA.global_lookup.values(): rna_info_prop.build() done_keys = set() new_keys = set(InfoStructRNA.global_lookup.keys()) while new_keys: for rna_key in new_keys: rna_info = InfoStructRNA.global_lookup[rna_key] rna_info.build() for prop in rna_info.properties: prop.build() for func in rna_info.functions: func.build() for prop in func.args: prop.build() for prop in func.return_values: prop.build() done_keys |= new_keys new_keys = set(InfoStructRNA.global_lookup.keys()) - done_keys # there are too many invalid defaults, unless we intend to fix, leave this off if 0: for rna_info in InfoStructRNA.global_lookup.values(): for prop in rna_info.properties: # ERROR CHECK default = prop.default if type(default) in {float, int}: if default < prop.min or default > prop.max: print("\t %s.%s, %s not in [%s - %s]" % (rna_info.identifier, prop.identifier, default, prop.min, prop.max)) # now for operators op_mods = dir(bpy.ops) for op_mod_name in sorted(op_mods): if op_mod_name.startswith('__'): continue op_mod = getattr(bpy.ops, op_mod_name) operators = dir(op_mod) for op in sorted(operators): try: rna_prop = getattr(op_mod, op).get_rna_type() except AttributeError: rna_prop = None except TypeError: rna_prop = None if rna_prop: GetInfoOperatorRNA(rna_prop) for rna_info in InfoOperatorRNA.global_lookup.values(): rna_info.build() for rna_prop in rna_info.args: rna_prop.build() # for rna_info in InfoStructRNA.global_lookup.values(): # print(rna_info) return ( InfoStructRNA.global_lookup, InfoFunctionRNA.global_lookup, InfoOperatorRNA.global_lookup, InfoPropertyRNA.global_lookup, ) def main(): struct = BuildRNAInfo()[0] data = [] for _struct_id, v in sorted(struct.items()): struct_id_str = v.identifier # "".join(sid for sid in struct_id if struct_id) for base in v.get_bases(): struct_id_str = base.identifier + "|" + struct_id_str props = [(prop.identifier, prop) for prop in v.properties] for _prop_id, prop in sorted(props): # if prop.type == "boolean": # continue prop_type = prop.type if prop.array_length > 0: prop_type += "[%d]" % prop.array_length data.append( "%s.%s -> %s: %s%s %s" % (struct_id_str, prop.identifier, prop.identifier, prop_type, ", (read-only)" if prop.is_readonly else "", prop.description)) data.sort() if bpy.app.background: import sys sys.stderr.write("\n".join(data)) sys.stderr.write("\n\nEOF\n") else: text = bpy.data.texts.new(name="api.py") text.from_string("\n".join(data)) if __name__ == "__main__": main()