Skip to content
Snippets Groups Projects
bpy_introspect_ui.py 14.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python3
    
    # ***** 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.
    #
    # Contributor(s): Campbell Barton
    #
    # ***** END GPL LICENSE BLOCK *****
    
    # <pep8 compliant>
    
    # This script dumps ui definitions as XML.
    # useful for finding bad api usage.
    
    
    """
    Example usage:
    
      python3 source/tools/utils_api/bpy_introspect_ui.py
    """
    
    
    import sys
    ModuleType = type(sys)
    
    
    def module_add(name):
        mod = sys.modules[name] = ModuleType(name)
        return mod
    
    
    class AttributeBuilder:
        """__slots__ = (
            "_attr", "_attr_list", "_item_set", "_args",
            "active", "operator_context", "enabled", "index", "data"
            )"""
    
        def _as_py(self):
            data = [self._attr_single, self._args, [child._as_py() for child in self._attr_list]]
            return data
    
    
        def _as_xml(self, indent="  "):
    
    
            def to_xml_str(value):
                if type(value) == str:
                    # quick shoddy clean
                    value = value.replace("&", " ")
                    value = value.replace("<", " ")
                    value = value.replace(">", " ")
    
                    return '"' + value + '"'
                else:
                    return '"' + str(value) + '"'
    
            def dict_to_kw(args, dict_args):
                args_str = ""
                if args:
                    # args_str += " ".join([to_xml_str(a) for a in args])
                    for i, a in enumerate(args):
                        args_str += "arg" + str(i + 1) + "=" + to_xml_str(a) + " "
    
                if dict_args:
                    args_str += " ".join(["%s=%s" % (key, to_xml_str(value)) for key, value in sorted(dict_args.items())])
    
                if args_str:
                    return " " + args_str
    
                return ""
    
            lines = []
    
            def py_to_xml(item, indent_ctx):
                if item._attr_list:
                    lines.append("%s<%s%s>" % (indent_ctx, item._attr_single, dict_to_kw(item._args_tuple, item._args)))
                    for child in item._attr_list:
                        # print(child._attr)
                        py_to_xml(child, indent_ctx + indent)
                    lines.append("%s</%s>" % (indent_ctx, item._attr_single))
                else:
                    lines.append("%s<%s%s/>" % (indent_ctx, item._attr_single, dict_to_kw(item._args_tuple, item._args)))
    
            py_to_xml(self, indent)
    
            return "\n".join(lines)
    
        def __init__(self, attr, attr_single):
            self._attr = attr
            self._attr_single = attr_single
            self._attr_list = []
            self._item_set = []
            self._args = {}
            self._args_tuple = ()
    
        def __call__(self, *args, **kwargs):
            # print(self._attr, args, kwargs)
            self._args_tuple = args
            self._args = kwargs
            return self
    
        def __getattr__(self, attr):
    
            attr_next = self._attr + "." + attr
            # Useful for debugging.
            # print(attr_next)
            attr_obj = _attribute_builder_overrides.get(attr_next, ...)
            if attr_obj is ...:
                attr_obj = NewAttr(attr_next, attr)
    
            self._attr_list.append(attr_obj)
            return attr_obj
    
        # def __setattr__(self, attr, value):
        #     setatte
    
        def __getitem__(self, item):
            item_obj = NewAttr(self._attr + "[" + repr(item) + "]", item)
            self._item_set.append(item_obj)
            return item_obj
    
        def __setitem__(self, item, value):
            pass  # TODO?
    
        def __repr__(self):
            return self._attr
    
        def __iter__(self):
            return iter([])
    
        # def __len__(self):
        #     return 0
    
        def __int__(self):
            return 0
    
        def __cmp__(self, other):
            return -1
    
        def __lt__(self, other):
            return -1
    
        def __gt__(self, other):
            return -1
    
        def __le__(self, other):
            return -1
    
        def __add__(self, other):
            return self
    
        def __sub__(self, other):
            return self
    
        def __truediv__(self, other):
            return self
    
        def __floordiv__(self, other):
            return self
    
        def __round__(self, other):
            return self
    
        def __float__(self):
            return 0.0
    
        # Custom functions
        def lower(self):
            return ""
    
        def upper(self):
            return ""
    
        def keys(self):
            return []
    
    
    
    class AttributeBuilder_Seq(AttributeBuilder):
        def __len__(self):
            return 0
    
    
    
    _attribute_builder_overrides = {
        "context.gpencil.layers": AttributeBuilder_Seq("context.gpencil.layers", "layers"),
        "context.gpencil_data.layers": AttributeBuilder_Seq("context.gpencil_data.layers", "layers"),
        "context.object.material_slots": (),
        "context.selected_nodes": (),
        "context.selected_sequences": (),
        "context.space_data.bookmarks": (),
        "context.space_data.text.filepath": "",
        "context.preferences.filepaths.script_directory": "",
        "context.tool_settings.snap_elements": (True, ) * 3,
        "context.selected_objects": (),
        "context.tool_settings.mesh_select_mode": (True, ) * 3,
        "context.mode": 'PAINT_TEXTURE',
    }
    
    
    
    def NewAttr(attr, attr_single):
        obj = AttributeBuilder(attr, attr_single)
        return obj
    
    
    def NewAttr_Seq(attr, attr_single):
        obj = AttributeBuilder_Seq(attr, attr_single)
        return obj
    
    
    Campbell Barton's avatar
    Campbell Barton committed
    class BaseFakeUI:
    
    Campbell Barton's avatar
    Campbell Barton committed
    
    
        def __init__(self):
            self.layout = NewAttr("self.layout", "layout")
    
    
    class Panel(BaseFakeUI):
    
    
        @property
        def is_popover(self):
            return False
    
    Campbell Barton's avatar
    Campbell Barton committed
    class UIList:
    
        pass
    
    
    class Header(BaseFakeUI):
        pass
    
    
    class Menu(BaseFakeUI):
    
    Campbell Barton's avatar
    Campbell Barton committed
    
    
        def draw_preset(self, context):
            pass
    
    
        def path_menu(
                self, searchpaths, operator, *,
                props_default=None, prop_filepath="filepath",
                filter_ext=None, filter_path=None, display_name=None,
                add_operator=None
        ):
    
            pass
    
        @classmethod
        def draw_collapsible(cls, context, layout):
    
            cls.draw(layout, context)
    
        @classmethod
        def is_extended(cls):
            return False
    
    Campbell Barton's avatar
    Campbell Barton committed
    class PropertyGroup:
    
        pass
    
    
    # setup fake module
    def fake_main():
        bpy = module_add("bpy")
    
        # Registerable Subclasses
        bpy.types = module_add("bpy.types")
        bpy.types.Panel = Panel
        bpy.types.Header = Header
        bpy.types.Menu = Menu
        bpy.types.UIList = UIList
        bpy.types.PropertyGroup = PropertyGroup
        bpy.types.Operator = Operator
    
        # ID Subclasses
        bpy.types.Armature = type("Armature", (), {})
    
        bpy.types.Brush = type("Brush", (), {})
    
        bpy.types.Camera = type("Camera", (), {})
        bpy.types.Curve = type("Curve", (), {})
    
        bpy.types.GreasePencil = type("GreasePencil", (), {})
    
        bpy.types.Lattice = type("Lattice", (), {})
    
        bpy.types.Light = type("Light", (), {})
        bpy.types.Material = type("Material", (), {})
    
        bpy.types.Mesh = type("Mesh", (), {})
        bpy.types.MetaBall = type("MetaBall", (), {})
        bpy.types.Object = type("Object", (), {})
    
        bpy.types.Object.bl_rna = NewAttr("bpy.types.Object.bl_rna", "bl_rna")
        bpy.types.ParticleSettings = type("ParticleSettings", (), {})
        bpy.types.Scene = type("Scene", (), {})
    
        bpy.types.Sequence = type("Sequence", (), {})
    
        bpy.types.Speaker = type("Speaker", (), {})
    
        bpy.types.SurfaceCurve = type("SurfaceCurve", (), {})
        bpy.types.TextCurve = type("SurfaceCurve", (), {})
    
        bpy.types.Texture = type("Texture", (), {})
        bpy.types.WindowManager = type("WindowManager", (), {})
    
        bpy.types.WorkSpace = type("WorkSpace", (), {})
    
        bpy.types.World = type("World", (), {})
    
        # Other types
        bpy.types.Bone = type("Bone", (), {})
        bpy.types.EditBone = type("EditBone", (), {})
    
        bpy.types.Event = type("Event", (), {})
        bpy.types.Event.bl_rna = NewAttr("bpy.types.Event.bl_rna", "bl_rna")
    
        bpy.types.FreestyleLineStyle = type("FreestyleLineStyle", (), {})
        bpy.types.PoseBone = type("PoseBone", (), {})
        bpy.types.Theme = type("Theme", (), {})
        bpy.types.Theme.bl_rna = NewAttr("bpy.types.Theme.bl_rna", "bl_rna")
        bpy.types.ToolSettings = type("bpy.types.ToolSettings", (), {})
        bpy.types.ToolSettings.bl_rna = NewAttr("bpy.types.ToolSettings.bl_rna", "bl_rna")
    
    
        bpy.props = module_add("bpy.props")
        bpy.props.StringProperty = dict
        bpy.props.BoolProperty = dict
    
        bpy.props.BoolVectorProperty = dict
    
        bpy.props.IntProperty = dict
        bpy.props.EnumProperty = dict
    
        bpy.props.FloatProperty = dict
        bpy.props.FloatVectorProperty = dict
        bpy.props.CollectionProperty = dict
    
    
        bpy.app = module_add("bpy.app")
        bpy.app.build_options = module_add("bpy.app.build_options")
        bpy.app.build_options.freestyle = True
        bpy.app.build_options.mod_fluid = True
        bpy.app.build_options.collada = True
        bpy.app.build_options.international = True
    
        bpy.app.build_options.mod_smoke = True
        bpy.app.build_options.alembic = True
        bpy.app.build_options.bullet = True
    
    
        bpy.app.translations = module_add("bpy.app.translations")
    
        bpy.app.translations.pgettext_iface = lambda s, context="": s
    
        bpy.app.translations.pgettext_data = lambda s: s
        bpy.app.translations.pgettext_tip = lambda s: s
    
        # id's are chosen at random here...
        bpy.app.translations.contexts = module_add("bpy.app.translations.contexts")
        bpy.app.translations.contexts.default = "CONTEXT_DEFAULT"
        bpy.app.translations.contexts.id_movieclip = "CONTEXT_ID_MOVIECLIP"
        bpy.app.translations.contexts.id_windowmanager = "CONTEXT_ID_WM"
        bpy.app.translations.contexts.plural = "CONTEXT_PLURAL"
    
        bpy.utils = module_add("bpy.utils")
        bpy.utils.register_class = lambda cls: ()
    
        bpy.utils.app_template_paths = lambda: ()
    
    Campbell Barton's avatar
    Campbell Barton committed
        class PropertyPanel:
    
            pass
    
        rna_prop_ui = module_add("rna_prop_ui")
        rna_prop_ui.PropertyPanel = PropertyPanel
        rna_prop_ui.draw = NewAttr("rna_prop_ui.draw", "draw")
    
        rigify = module_add("rigify")
        rigify.get_submodule_types = lambda: []
    
    
    def fake_runtime():
        """Only call this before `draw()` functions."""
    
        # Misc Subclasses
        bpy.types.EffectSequence = type("EffectSequence", (), {})
    
        # Operator Subclases
        bpy.types.WM_OT_doc_view = type("WM_OT_doc_view", (), {"_prefix": ""})
    
        bpy.data = module_add("bpy.data")
        bpy.data.scenes = ()
        bpy.data.speakers = ()
    
        bpy.data.collections = ()
    
        bpy.data.meshes = ()
        bpy.data.shape_keys = ()
        bpy.data.materials = ()
        bpy.data.lattices = ()
    
        bpy.data.lights = ()
        bpy.data.lightprobes = ()
        bpy.data.fonts = ()
    
        bpy.data.textures = ()
        bpy.data.cameras = ()
        bpy.data.curves = ()
        bpy.data.linestyles = ()
        bpy.data.masks = ()
        bpy.data.metaballs = ()
        bpy.data.movieclips = ()
        bpy.data.armatures = ()
        bpy.data.particles = ()
    
        bpy.data.grease_pencils = ()
    
        bpy.data.cache_files = ()
        bpy.data.workspaces = ()
    
    
        bpy.data.is_dirty = True
    
        bpy.data.is_saved = True
    
        bpy.data.use_autopack = True
    
        # defined in fake_main()
        bpy.utils.smpte_from_frame = lambda f: ""
        bpy.utils.script_paths = lambda f: ()
        bpy.utils.user_resource = lambda a, b: ()
    
        bpy.app.debug = False
        bpy.app.version = 2, 55, 1
        bpy.app.autoexec_fail = False
    
        bpy.path = module_add("bpy.path")
    
        bpy.path.display_name = lambda f, has_ext=False: ""
    
    
        bpy_extras = module_add("bpy_extras")
        bpy_extras.keyconfig_utils = module_add("bpy_extras.keyconfig_utils")
        bpy_extras.keyconfig_utils.KM_HIERARCHY = ()
        bpy_extras.keyconfig_utils.keyconfig_merge = lambda a, b: ()
    
        addon_utils = module_add("addon_utils")
        # addon_utils.modules = lambda f: []
    
        def _(refresh=False):
            return ()
        addon_utils.modules = _
        del _
        addon_utils.modules_refresh = lambda f: None
        addon_utils.module_bl_info = lambda f: None
        addon_utils.addons_fake_modules = {}
        addon_utils.error_duplicates = ()
        addon_utils.error_encoding = ()
    
    
    fake_main()
    fake_helper()
    # fake_runtime()  # call after initial import so we can catch
    #                 # bad use of modules outside of draw() functions.
    
    import bpy
    
    
    def module_classes(mod):
        classes = []
        for key, value in mod.__dict__.items():
            try:
                is_subclass = issubclass(value, BaseFakeUI)
            except:
                is_subclass = False
    
            if is_subclass:
                classes.append(value)
    
        return classes
    
    
    def main():
    
        import os
        BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "..")
        BASE_DIR = os.path.normpath(os.path.abspath(BASE_DIR))
        MODULE_DIR_UI = os.path.join(BASE_DIR, "release", "scripts", "startup")
        MODULE_DIR_MOD = os.path.join(BASE_DIR, "release", "scripts", "modules")
    
        print("Using base dir: %r" % BASE_DIR)
        print("Using module dir: %r" % MODULE_DIR_UI)
    
        sys.path.insert(0, MODULE_DIR_UI)
        sys.path.insert(0, MODULE_DIR_MOD)
    
        scripts_dir = os.path.join(MODULE_DIR_UI, "bl_ui")
        for f in sorted(os.listdir(scripts_dir)):
            if f.endswith(".py") and not f.startswith("__init__"):
                # print(f)
                mod = __import__("bl_ui." + f[:-3]).__dict__[f[:-3]]
    
                classes = module_classes(mod)
    
                for cls in classes:
                    setattr(bpy.types, cls.__name__, cls)
    
        fake_runtime()
    
        # print("running...")
        print("<ui>")
        for f in sorted(os.listdir(scripts_dir)):
            if f.endswith(".py") and not f.startswith("__init__"):
                # print(f)
                mod = __import__("bl_ui." + f[:-3]).__dict__[f[:-3]]
    
                classes = module_classes(mod)
    
                for cls in classes:
                    # want to check if the draw function is directly in the class
                    # print("draw")
                    if "draw" in cls.__dict__:
                        self = cls()
                        self.draw(NewAttr("context", "context"))
                        # print(self.layout._as_py())
                        self.layout._args['id'] = mod.__name__ + "." + cls.__name__
                        print(self.layout._as_xml())
        print("</ui>")
    
    
    if __name__ == "__main__":
        main()