diff --git a/utils_api/bpy_introspect_ui.py b/utils_api/bpy_introspect_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..d629e8df3e7eb876308765272f18c344212bfa75
--- /dev/null
+++ b/utils_api/bpy_introspect_ui.py
@@ -0,0 +1,433 @@
+#!/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_obj = NewAttr(self._attr + "." + attr, 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 []
+
+
+def NewAttr(attr, attr_single):
+    obj = AttributeBuilder(attr, attr_single)
+    return obj
+
+
+class BaseFakeUI():
+    def __init__(self):
+        self.layout = NewAttr("self.layout", "layout")
+
+
+class Panel(BaseFakeUI):
+    pass
+
+
+class UIList():
+    pass
+
+
+class Header(BaseFakeUI):
+    pass
+
+
+class Menu(BaseFakeUI):
+    def draw_preset(self, context):
+        pass
+
+    def path_menu(self, a, b, c):
+        pass
+
+    @classmethod
+    def draw_collapsible(cls, context, layout):
+        cls.draw_menus(layout, context)
+
+
+class Operator(BaseFakeUI):
+    pass
+
+
+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.Bone = type("Bone", (), {})
+    bpy.types.EditBone = type("EditBone", (), {})
+    bpy.types.FreestyleLineStyle = type("FreestyleLineStyle", (), {})
+    bpy.types.PoseBone = type("PoseBone", (), {})
+    bpy.types.Material = type("Material", (), {})
+    bpy.types.Lamp = type("Lamp", (), {})
+    bpy.types.Camera = type("Camera", (), {})
+    bpy.types.Curve = type("Curve", (), {})
+    bpy.types.SurfaceCurve = type("SurfaceCurve", (), {})
+    bpy.types.TextCurve = type("SurfaceCurve", (), {})
+    bpy.types.Lattice = type("Lattice", (), {})
+    bpy.types.Mesh = type("Mesh", (), {})
+    bpy.types.MetaBall = type("MetaBall", (), {})
+    bpy.types.Object = type("Object", (), {})
+    bpy.types.Speaker = type("Speaker", (), {})
+    bpy.types.Texture = type("Texture", (), {})
+    bpy.types.ParticleSettings = type("ParticleSettings", (), {})
+    bpy.types.World = type("World", (), {})
+    bpy.types.Brush = type("Brush", (), {})
+    bpy.types.WindowManager = type("WindowManager", (), {})
+    bpy.types.Scene = type("Scene", (), {})
+    bpy.types.Scene.EnumProperty = NewAttr("bpy.types.Scene.EnumProperty", "EnumProperty")
+    bpy.types.Scene.StringProperty = NewAttr("bpy.types.Scene.StringProperty", "StringProperty")
+    bpy.types.Event = type("Event", (), {})
+    bpy.types.Event.bl_rna = NewAttr("bpy.types.Event.bl_rna", "bl_rna")
+
+    bpy.props = module_add("bpy.props")
+    bpy.props.StringProperty = dict
+    bpy.props.BoolProperty = dict
+    bpy.props.IntProperty = dict
+    bpy.props.EnumProperty = 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.translations = module_add("bpy.app.translations")
+    bpy.app.translations.pgettext_iface = 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: ()
+
+
+def fake_helper():
+
+    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.groups = ()
+    bpy.data.meshes = ()
+    bpy.data.shape_keys = ()
+    bpy.data.materials = ()
+    bpy.data.lattices = ()
+    bpy.data.lamps = ()
+    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.is_dirty = 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: ""
+
+    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()