CM3D2 Converter.translations.extract_messages

   1# ***** BEGIN GPL LICENSE BLOCK *****
   2#
   3# This program is free software; you can redistribute it and/or
   4# modify it under the terms of the GNU General Public License
   5# as published by the Free Software Foundation; either version 2
   6# of the License, or (at your option) any later version.
   7#
   8# This program is distributed in the hope that it will be useful,
   9# but WITHOUT ANY WARRANTY; without even the implied warranty of
  10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  11# GNU General Public License for more details.
  12#
  13# You should have received a copy of the GNU General Public License
  14# along with this program; if not, write to the Free Software Foundation,
  15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16#
  17# ***** END GPL LICENSE BLOCK *****
  18
  19# <pep8 compliant>
  20
  21# Populate a template file (POT format currently) from Blender RNA/py/C data.
  22# XXX: This script is meant to be used from inside Blender!
  23#      You should not directly use this script, rather use update_msg.py!
  24
  25import collections
  26import copy
  27import datetime
  28import os
  29import re
  30import sys
  31
  32# XXX Relative import does not work here when used from Blender...
  33from bl_i18n_utils import settings as settings_i18n, utils
  34
  35import bpy
  36
  37##### Utils #####
  38
  39# check for strings like "+%f°"
  40ignore_reg = re.compile(r"^(?:[-*.()/\\+%°0-9]|%d|%f|%s|%r|\s)*$")
  41filter_message = ignore_reg.match
  42
  43
  44def init_spell_check(settings, lang="en_US"):
  45    try:
  46        from bl_i18n_utils import utils_spell_check
  47        return utils_spell_check.SpellChecker(settings, lang)
  48    except Exception as e:
  49        print("Failed to import utils_spell_check ({})".format(str(e)))
  50        return None
  51
  52
  53def _gen_check_ctxt(settings):
  54    return {
  55        "multi_rnatip": set(),
  56        "multi_lines": set(),
  57        "py_in_rna": set(),
  58        "not_capitalized": set(),
  59        "end_point": set(),
  60        "undoc_ops": set(),
  61        "spell_checker": init_spell_check(settings),
  62        "spell_errors": {},
  63    }
  64
  65
  66def _diff_check_ctxt(check_ctxt, minus_check_ctxt):
  67    """Removes minus_check_ctxt from check_ctxt"""
  68    for key in check_ctxt:
  69        if isinstance(check_ctxt[key], set):
  70            for warning in minus_check_ctxt[key]:
  71                if warning in check_ctxt[key]:
  72                    check_ctxt[key].remove(warning)
  73        elif isinstance(check_ctxt[key], dict):
  74            for warning in minus_check_ctxt[key]:
  75                if warning in check_ctxt[key]:
  76                    del check_ctxt[key][warning]
  77
  78
  79def _gen_reports(check_ctxt):
  80    return {
  81        "check_ctxt": check_ctxt,
  82        "rna_structs": [],
  83        "rna_structs_skipped": [],
  84        "rna_props": [],
  85        "rna_props_skipped": [],
  86        "py_messages": [],
  87        "py_messages_skipped": [],
  88        "src_messages": [],
  89        "src_messages_skipped": [],
  90        "messages_skipped": set(),
  91    }
  92
  93
  94def check(check_ctxt, msgs, key, msgsrc, settings):
  95    """
  96    Performs a set of checks over the given key (context, message)...
  97    """
  98    if check_ctxt is None:
  99        return
 100    multi_rnatip = check_ctxt.get("multi_rnatip")
 101    multi_lines = check_ctxt.get("multi_lines")
 102    py_in_rna = check_ctxt.get("py_in_rna")
 103    not_capitalized = check_ctxt.get("not_capitalized")
 104    end_point = check_ctxt.get("end_point")
 105    undoc_ops = check_ctxt.get("undoc_ops")
 106    spell_checker = check_ctxt.get("spell_checker")
 107    spell_errors = check_ctxt.get("spell_errors")
 108
 109    if multi_rnatip is not None:
 110        if key in msgs and key not in multi_rnatip:
 111            multi_rnatip.add(key)
 112    if multi_lines is not None:
 113        if '\n' in key[1]:
 114            multi_lines.add(key)
 115    if py_in_rna is not None:
 116        if key in py_in_rna[1]:
 117            py_in_rna[0].add(key)
 118    if not_capitalized is not None:
 119        if(key[1] not in settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED and
 120           key[1][0].isalpha() and not key[1][0].isupper()):
 121            not_capitalized.add(key)
 122    if end_point is not None:
 123        if (
 124                key[1].strip().endswith('.') and
 125                (not key[1].strip().endswith('...')) and
 126                key[1] not in settings.WARN_MSGID_END_POINT_ALLOWED
 127        ):
 128            end_point.add(key)
 129    if undoc_ops is not None:
 130        if key[1] == settings.UNDOC_OPS_STR:
 131            undoc_ops.add(key)
 132    if spell_checker is not None and spell_errors is not None:
 133        err = spell_checker.check(key[1])
 134        if err:
 135            spell_errors[key] = err
 136
 137
 138def print_info(reports, pot):
 139    def _print(*args, **kwargs):
 140        kwargs["file"] = sys.stderr
 141        print(*args, **kwargs)
 142
 143    pot.update_info()
 144
 145    _print("{} RNA structs were processed (among which {} were skipped), containing {} RNA properties "
 146           "(among which {} were skipped).".format(len(reports["rna_structs"]), len(reports["rna_structs_skipped"]),
 147                                                   len(reports["rna_props"]), len(reports["rna_props_skipped"])))
 148    _print("{} messages were extracted from Python UI code (among which {} were skipped), and {} from C source code "
 149           "(among which {} were skipped).".format(len(reports["py_messages"]), len(reports["py_messages_skipped"]),
 150                                                   len(reports["src_messages"]), len(reports["src_messages_skipped"])))
 151    _print("{} messages were rejected.".format(len(reports["messages_skipped"])))
 152    _print("\n")
 153    _print("Current POT stats:")
 154    pot.print_info(prefix="\t", output=_print)
 155    _print("\n")
 156
 157    check_ctxt = reports["check_ctxt"]
 158    if check_ctxt is None:
 159        return
 160    multi_rnatip = check_ctxt.get("multi_rnatip")
 161    multi_lines = check_ctxt.get("multi_lines")
 162    py_in_rna = check_ctxt.get("py_in_rna")
 163    not_capitalized = check_ctxt.get("not_capitalized")
 164    end_point = check_ctxt.get("end_point")
 165    undoc_ops = check_ctxt.get("undoc_ops")
 166    spell_errors = check_ctxt.get("spell_errors")
 167
 168    # XXX Temp, no multi_rnatip nor py_in_rna, see below.
 169    keys = multi_lines | not_capitalized | end_point | undoc_ops | spell_errors.keys()
 170    if keys:
 171        _print("WARNINGS:")
 172        for key in keys:
 173            if undoc_ops and key in undoc_ops:
 174                _print("\tThe following operators are undocumented!")
 175            else:
 176                _print("\t{}”|“{}”:".format(*key))
 177                # We support multi-lines tooltips now...
 178                # ~ if multi_lines and key in multi_lines:
 179                    # ~ _print("\t\t-> newline in this message!")
 180                if not_capitalized and key in not_capitalized:
 181                    _print("\t\t-> message not capitalized!")
 182                if end_point and key in end_point:
 183                    _print("\t\t-> message with endpoint!")
 184                # XXX Hide this one for now, too much false positives.
 185                #if multi_rnatip and key in multi_rnatip:
 186                #   _print("\t\t-> tip used in several RNA items")
 187                #if py_in_rna and key in py_in_rna:
 188                #   _print("\t\t-> RNA message also used in py UI code!")
 189                if spell_errors and spell_errors.get(key):
 190                    lines = [
 191                        "\t\t-> {}: misspelled, suggestions are ({})".format(w, "'" + "', '".join(errs) + "'")
 192                        for w, errs in spell_errors[key]
 193                    ]
 194                    _print("\n".join(lines))
 195            _print("\t\t{}".format("\n\t\t".join(pot.msgs[key].sources)))
 196
 197
 198def process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt, settings):
 199    if filter_message(msgid):
 200        reports["messages_skipped"].add((msgid, msgsrc))
 201        return
 202    if not msgctxt:
 203        # We do *not* want any "" context!
 204        msgctxt = settings.DEFAULT_CONTEXT
 205    # Always unescape keys!
 206    msgctxt = utils.I18nMessage.do_unescape(msgctxt)
 207    msgid = utils.I18nMessage.do_unescape(msgid)
 208    key = (msgctxt, msgid)
 209    check(check_ctxt, msgs, key, msgsrc, settings)
 210    msgsrc = settings.PO_COMMENT_PREFIX_SOURCE_CUSTOM + msgsrc
 211    if key not in msgs:
 212        msgs[key] = utils.I18nMessage([msgctxt], [msgid], [], [msgsrc], settings=settings)
 213    else:
 214        msgs[key].comment_lines.append(msgsrc)
 215
 216
 217##### RNA #####   # CM3D2 Converter: added class_list param
 218def dump_rna_messages(msgs, reports, settings, verbose=False, class_list=bpy.types.ID.__base__.__subclasses__()): 
 219    """
 220    Dump into messages dict all RNA-defined UI messages (labels en tooltips).
 221    """
 222    def class_blacklist():
 223        blacklist_rna_class = {getattr(bpy.types, cls_id) for cls_id in (
 224            # core classes
 225            "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct",
 226            # registerable classes
 227            "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo",
 228            # window classes
 229            "Window",
 230        )
 231        }
 232
 233        # More builtin classes we don't need to parse.
 234        blacklist_rna_class |= {cls for cls in bpy.types.Property.__subclasses__()}
 235
 236        return blacklist_rna_class
 237
 238    check_ctxt_rna = check_ctxt_rna_tip = None
 239    check_ctxt = reports["check_ctxt"]
 240    if check_ctxt:
 241        check_ctxt_rna = {
 242            "multi_lines": check_ctxt.get("multi_lines"),
 243            "not_capitalized": check_ctxt.get("not_capitalized"),
 244            "end_point": check_ctxt.get("end_point"),
 245            "undoc_ops": check_ctxt.get("undoc_ops"),
 246            "spell_checker": check_ctxt.get("spell_checker"),
 247            "spell_errors": check_ctxt.get("spell_errors"),
 248        }
 249        check_ctxt_rna_tip = check_ctxt_rna
 250        check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip")
 251
 252    default_context = settings.DEFAULT_CONTEXT
 253
 254    # Function definitions
 255    def walk_properties(cls):
 256        bl_rna = cls.bl_rna
 257        # Get our parents' properties, to not export them multiple times.
 258        bl_rna_base = bl_rna.base
 259        if bl_rna_base:
 260            bl_rna_base_props = set(bl_rna_base.properties.values())
 261        else:
 262            bl_rna_base_props = set()
 263
 264        props = sorted(bl_rna.properties, key=lambda p: p.identifier)
 265        for prop in props:
 266            # Only write this property if our parent hasn't got it.
 267            if prop in bl_rna_base_props:
 268                continue
 269            if prop.identifier == "rna_type":
 270                continue
 271            reports["rna_props"].append((cls, prop))
 272
 273            msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier)
 274            msgctxt = prop.translation_context or default_context
 275
 276            if prop.name and (prop.name != prop.identifier or msgctxt != default_context):
 277                process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings)
 278            if prop.description:
 279                process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings)
 280
 281            if isinstance(prop, bpy.types.EnumProperty):
 282                done_items = set()
 283                for item in prop.enum_items:
 284                    msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier)
 285                    done_items.add(item.identifier)
 286                    if item.name and item.name != item.identifier:
 287                        process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings)
 288                    if item.description:
 289                        process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip,
 290                                    settings)
 291                for item in prop.enum_items_static:
 292                    if item.identifier in done_items:
 293                        continue
 294                    msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier)
 295                    done_items.add(item.identifier)
 296                    if item.name and item.name != item.identifier:
 297                        process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings)
 298                    if item.description:
 299                        process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip,
 300                                    settings)
 301
 302    def walk_tools_definitions(cls):
 303        from bl_ui.space_toolsystem_common import ToolDef
 304
 305        bl_rna = cls.bl_rna
 306        op_default_context = bpy.app.translations.contexts.operator_default
 307
 308        def process_tooldef(tool_context, tool):
 309            if not isinstance(tool, ToolDef):
 310                if callable(tool):
 311                    for t in tool(None):
 312                        process_tooldef(tool_context, t)
 313                return
 314            msgsrc = "bpy.types.{} Tools: '{}', '{}'".format(bl_rna.identifier, tool_context, tool.idname)
 315            if tool.label:
 316                process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings)
 317            # Callable (function) descriptions must handle their translations themselves.
 318            if tool.description and not callable(tool.description):
 319                process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings)
 320
 321        for tool_context, tools_defs in cls.tools_all():
 322            for tools_group in tools_defs:
 323                if tools_group is None:
 324                    continue
 325                elif isinstance(tools_group, tuple) and not isinstance(tools_group, ToolDef):
 326                    for tool in tools_group:
 327                        process_tooldef(tool_context, tool)
 328                else:
 329                    process_tooldef(tool_context, tools_group)
 330
 331    blacklist_rna_class = class_blacklist()
 332
 333    def walk_class(cls):
 334        if not hasattr(cls, 'bl_rna'):
 335            reports["rna_structs_skipped"].append(cls)
 336            return
 337        bl_rna = cls.bl_rna
 338        msgsrc = "bpy.types." + bl_rna.identifier # CM3D2 Converter: this does crazy things like change the name of our operators!
 339        #msgsrc = "bpy.types." + cls.__name__
 340        # Catch operators and redirect through bpy.ops
 341        if '_OT_' in bl_rna.identifier:
 342            module, operator = cls.bl_idname.split('.')
 343            instance = getattr(getattr(bpy.ops, module), operator)
 344            cls = instance.get_rna_type().__class__
 345            bl_rna = cls.bl_rna
 346        #print(cls, msgsrc)
 347        msgctxt = bl_rna.translation_context or default_context
 348
 349        if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context):
 350            process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings)
 351        
 352        if hasattr(bl_rna, 'bl_description'):
 353            process_msg(msgs, default_context, bl_rna.bl_description, msgsrc, reports, check_ctxt_rna_tip, settings)
 354        if bl_rna.description:
 355            process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings)
 356        elif cls.__doc__:  # XXX Some classes (like KeyingSetInfo subclasses) have void description... :(
 357            process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings)
 358
 359        # Panels' "tabs" system.
 360        if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category:
 361            process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings)
 362
 363        if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label:
 364            process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings)
 365
 366        # Tools Panels definitions.
 367        if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all:
 368            walk_tools_definitions(cls)
 369
 370        walk_properties(cls)
 371
 372    def walk_keymap_hierarchy(hier, msgsrc_prev):
 373        km_i18n_context = bpy.app.translations.contexts.id_windowmanager
 374        for lvl in hier:
 375            msgsrc = msgsrc_prev + "." + lvl[1]
 376            if isinstance(lvl[0], str):  # Can be a function too, now, with tool system...
 377                process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings)
 378            if lvl[3]:
 379                walk_keymap_hierarchy(lvl[3], msgsrc)
 380
 381    # Dump Messages
 382    operator_categories = {}
 383
 384    def process_cls_list(cls_list):
 385        if not cls_list:
 386            return
 387
 388        def full_class_id(cls):
 389            """Gives us 'ID.Light.AreaLight' which is best for sorting."""
 390            # Always the same issue, some classes listed in blacklist should actually no more exist (they have been
 391            # unregistered), but are still listed by __subclasses__() calls... :/
 392            if cls in blacklist_rna_class:
 393                return cls.__name__
 394            cls_id = ""
 395            bl_rna = None
 396            if hasattr(cls, 'bl_rna'):
 397                bl_rna = cls.bl_rna
 398            while bl_rna:
 399                cls_id = bl_rna.identifier + "." + cls_id
 400                bl_rna = bl_rna.base
 401            return cls_id
 402
 403        def operator_category(cls):
 404            """Extract operators' categories, as displayed in 'search' space menu."""
 405            # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator().
 406            if issubclass(cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__:
 407                cat_id = cls.__name__.split("_OT_")[0]
 408                if cat_id not in operator_categories:
 409                    cat_str = cat_id.capitalize() + ":"
 410                    operator_categories[cat_id] = cat_str
 411
 412        if verbose:
 413            print(cls_list)
 414        cls_list.sort(key=full_class_id)
 415        for cls in cls_list:
 416            if verbose:
 417                print(cls)
 418            reports["rna_structs"].append(cls)
 419            # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)...
 420            if (cls in blacklist_rna_class): #or issubclass(cls, bpy.types.Operator):
 421                reports["rna_structs_skipped"].append(cls)
 422            else:
 423                operator_category(cls)
 424                walk_class(cls)
 425            # Recursively process subclasses.
 426            process_cls_list(cls.__subclasses__())
 427
 428    # Parse everything (recursively parsing from bpy_struct "class"...).
 429    process_cls_list(class_list)
 430
 431    # Finalize generated 'operator categories' messages.
 432    for cat_str in operator_categories.values():
 433        process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category",
 434                    reports, check_ctxt_rna, settings)
 435
 436    # And parse keymaps!
 437    from bl_keymap_utils import keymap_hierarchy
 438    walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")
 439
 440
 441##### Python source code #####
 442def dump_py_messages_from_files(msgs, reports, files, settings):
 443    """
 444    Dump text inlined in the python files given, e.g. 'My Name' in:
 445        layout.prop("someprop", text="My Name")
 446    """
 447    import ast
 448
 449    bpy_struct = bpy.types.ID.__base__
 450    i18n_contexts = bpy.app.translations.contexts
 451
 452    root_paths = tuple(bpy.utils.resource_path(t) for t in ('USER', 'LOCAL', 'SYSTEM'))
 453
 454    def make_rel(path):
 455        for rp in root_paths:
 456            if path.startswith(rp):
 457                try:  # can't always find the relative path (between drive letters on windows)
 458                    return os.path.relpath(path, rp)
 459                except ValueError:
 460                    return path
 461        # Use binary's dir as fallback...
 462        try:  # can't always find the relative path (between drive letters on windows)
 463            return os.path.relpath(path, os.path.dirname(bpy.app.binary_path))
 464        except ValueError:
 465            return path
 466
 467    # Helper function
 468    def extract_strings_ex(node, is_split=False):
 469        """
 470        Recursively get strings, needed in case we have "Blah" + "Blah", passed as an argument in that case it won't
 471        evaluate to a string. However, break on some kind of stopper nodes, like e.g. Subscript.
 472        """
 473        if type(node) == ast.Str:
 474            eval_str = ast.literal_eval(node)
 475            if eval_str:
 476                yield (is_split, eval_str, (node,))
 477        else:
 478            is_split = (type(node) in separate_nodes)
 479            for nd in ast.iter_child_nodes(node):
 480                if type(nd) not in stopper_nodes:
 481                    yield from extract_strings_ex(nd, is_split=is_split)
 482
 483    def _extract_string_merge(estr_ls, nds_ls):
 484        return "".join(s for s in estr_ls if s is not None), tuple(n for n in nds_ls if n is not None)
 485
 486    def extract_strings(node):
 487        estr_ls = []
 488        nds_ls = []
 489        for is_split, estr, nds in extract_strings_ex(node):
 490            estr_ls.append(estr)
 491            nds_ls.extend(nds)
 492        ret = _extract_string_merge(estr_ls, nds_ls)
 493        return ret
 494
 495    def extract_strings_split(node):
 496        """
 497        Returns a list args as returned by 'extract_strings()', but split into groups based on separate_nodes, this way
 498        expressions like ("A" if test else "B") won't be merged but "A" + "B" will.
 499        """
 500        estr_ls = []
 501        nds_ls = []
 502        bag = []
 503        for is_split, estr, nds in extract_strings_ex(node):
 504            if is_split:
 505                bag.append((estr_ls, nds_ls))
 506                estr_ls = []
 507                nds_ls = []
 508
 509            estr_ls.append(estr)
 510            nds_ls.extend(nds)
 511
 512        bag.append((estr_ls, nds_ls))
 513
 514        return [_extract_string_merge(estr_ls, nds_ls) for estr_ls, nds_ls in bag]
 515
 516    i18n_ctxt_ids = {v for v in bpy.app.translations.contexts_C_to_py.values()}
 517
 518    def _ctxt_to_ctxt(node):
 519        # We must try, to some extend, to get contexts from vars instead of only literal strings...
 520        ctxt = extract_strings(node)[0]
 521        if ctxt:
 522            return ctxt
 523        # Basically, we search for attributes matching py context names, for now.
 524        # So non-literal contexts should be used that way:
 525        #     i18n_ctxt = bpy.app.translations.contexts
 526        #     foobar(text="Foo", text_ctxt=i18n_ctxt.id_object)
 527        if type(node) == ast.Attribute:
 528            if node.attr in i18n_ctxt_ids:
 529                #print(node, node.attr, getattr(i18n_contexts, node.attr))
 530                return getattr(i18n_contexts, node.attr)
 531        return i18n_contexts.default
 532
 533    def _op_to_ctxt(node):
 534        # Some smart coders like things like:
 535        #    >>> row.operator("preferences.addon_disable" if is_enabled else "preferences.addon_enable", ...)
 536        # We only take first arg into account here!
 537        bag = extract_strings_split(node)
 538        opname, _ = bag[0]
 539        if not opname:
 540            return i18n_contexts.default
 541        op = bpy.ops
 542        for n in opname.split('.'):
 543            op = getattr(op, n)
 544        try:
 545            return op.get_rna_type().translation_context
 546        except Exception as e:
 547            default_op_context = i18n_contexts.operator_default
 548            print("ERROR: ", str(e))
 549            print("       Assuming default operator context '{}'".format(default_op_context))
 550            return default_op_context
 551
 552    # Gather function names.
 553    # In addition of UI func, also parse pgettext ones...
 554    # Tuples of (module name, (short names, ...)).
 555    pgettext_variants = (
 556        ("pgettext",       ("_"     , "f_"      , )),
 557        ("pgettext_iface", ("iface_", "f_iface_", )),
 558        ("pgettext_tip",   ("tip_"  , "f_tip_"  , )),
 559        ("pgettext_data",  ("data_" , "f_data_" , )),
 560    )
 561    pgettext_variants_args = {"msgid": (0, {"msgctxt": 1})}
 562
 563    # key: msgid keywords.
 564    # val: tuples of ((keywords,), context_getter_func) to get a context for that msgid.
 565    #      Note: order is important, first one wins!
 566    translate_kw = {
 567        "text": (
 568            (("text_ctxt",), _ctxt_to_ctxt),
 569            (("operator",), _op_to_ctxt),
 570        ),
 571        "msgid": (
 572            (("msgctxt",), _ctxt_to_ctxt),
 573        ),
 574        "message": (),
 575        "report_message": (), # CM3D2 Converter : for report_cancel()
 576    }
 577
 578    context_kw_set = {}
 579    for k, ctxts in translate_kw.items():
 580        s = set()
 581        for c, _ in ctxts:
 582            s |= set(c)
 583        context_kw_set[k] = s
 584
 585    # {func_id: {msgid: (arg_pos,
 586    #                    {msgctxt: arg_pos,
 587    #                     ...
 588    #                    }
 589    #                   ),
 590    #            ...
 591    #           },
 592    #  ...
 593    # }
 594    func_translate_args = {}
 595
 596    # First, functions from UILayout
 597    # First loop is for msgid args, second one is for msgctxt args.
 598    for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
 599        # check it has one or more arguments as defined in translate_kw
 600        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
 601            if ((arg_kw in translate_kw) and (not arg.is_output) and (arg.type == 'STRING')):
 602                func_translate_args.setdefault(func_id, {})[arg_kw] = (arg_pos, {})
 603    for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
 604        if func_id not in func_translate_args:
 605            continue
 606        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
 607            if (not arg.is_output) and (arg.type == 'STRING'):
 608                for msgid, msgctxts in context_kw_set.items():
 609                    if arg_kw in msgctxts:
 610                        func_translate_args[func_id][msgid][1][arg_kw] = arg_pos
 611    # The report() func of operators.
 612    for func_id, func in bpy.types.Operator.bl_rna.functions.items():
 613        # check it has one or more arguments as defined in translate_kw
 614        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
 615            if ((arg_kw in translate_kw) and (not arg.is_output) and (arg.type == 'STRING')):
 616                func_translate_args.setdefault(func_id, {})[arg_kw] = (arg_pos, {})
 617    # We manually add funcs from bpy.app.translations
 618    for func_id, func_ids in pgettext_variants:
 619        func_translate_args[func_id] = pgettext_variants_args
 620        for func_id in func_ids:
 621            func_translate_args[func_id] = pgettext_variants_args
 622    
 623    # CM3D2 Converter : manually add report_cancel()
 624    # XXX Consider removing the use of report_cancel() entierly in model/anim import/export
 625    func_translate_args.setdefault('report_cancel', {})['report_message'] = (1, {})
 626
 627    # Break recursive nodes look up on some kind of nodes.
 628    # E.g. we don't want to get strings inside subscripts (blah["foo"])!
 629    #      we don't want to get strings from comparisons (foo.type == 'BAR').
 630    stopper_nodes = {ast.Subscript, ast.Compare}
 631    # Consider strings separate: ("a" if test else "b")
 632    separate_nodes = {ast.IfExp}
 633    
 634    check_ctxt_py = None
 635    if reports["check_ctxt"]:
 636        check_ctxt = reports["check_ctxt"]
 637        check_ctxt_py = {
 638            "py_in_rna": (check_ctxt.get("py_in_rna"), set(msgs.keys())),
 639            "multi_lines": check_ctxt.get("multi_lines"),
 640            "not_capitalized": check_ctxt.get("not_capitalized"),
 641            "end_point": check_ctxt.get("end_point"),
 642            "spell_checker": check_ctxt.get("spell_checker"),
 643            "spell_errors": check_ctxt.get("spell_errors"),
 644        }
 645
 646    for fp in files:
 647        with open(fp, 'r', encoding="utf8") as filedata:
 648            root_node = ast.parse(filedata.read(), fp, 'exec')
 649
 650        fp_rel = make_rel(fp)
 651
 652        for node in ast.walk(root_node):
 653            if type(node) == ast.Call:
 654                # print("found function at")
 655                # print("%s:%d" % (fp, node.lineno))
 656
 657                # We can't skip such situations! from blah import foo\nfoo("bar") would also be an ast.Name func!
 658                if type(node.func) == ast.Name:
 659                    func_id = node.func.id
 660                elif hasattr(node.func, "attr"):
 661                    func_id = node.func.attr
 662                # Ugly things like getattr(self, con.type)(context, box, con)
 663                else:
 664                    continue
 665
 666                func_args = func_translate_args.get(func_id, {})
 667
 668                # First try to get i18n contexts, for every possible msgid id.
 669                msgctxts = dict.fromkeys(func_args.keys(), "")
 670                for msgid, (_, context_args) in func_args.items():
 671                    context_elements = {}
 672                    for arg_kw, arg_pos in context_args.items():
 673                        if arg_pos < len(node.args):
 674                            context_elements[arg_kw] = node.args[arg_pos]
 675                        else:
 676                            for kw in node.keywords:
 677                                if kw.arg == arg_kw:
 678                                    context_elements[arg_kw] = kw.value
 679                                    break
 680                    # print(context_elements)
 681                    for kws, proc in translate_kw[msgid]:
 682                        if set(kws) <= context_elements.keys():
 683                            args = tuple(context_elements[k] for k in kws)
 684                            #print("running ", proc, " with ", args)
 685                            ctxt = proc(*args)
 686                            if ctxt:
 687                                msgctxts[msgid] = ctxt
 688                                break
 689
 690                # print(translate_args)
 691                # do nothing if not found
 692                for arg_kw, (arg_pos, _) in func_args.items():
 693                    msgctxt = msgctxts[arg_kw]
 694                    estr_lst = [(None, ())]
 695                    if arg_pos < len(node.args):
 696                        estr_lst = extract_strings_split(node.args[arg_pos])
 697                        #print(estr, nds)
 698                    else:
 699                        for kw in node.keywords:
 700                            if kw.arg == arg_kw:
 701                                estr_lst = extract_strings_split(kw.value)
 702                                break
 703                        #print(estr, nds)
 704                    for estr, nds in estr_lst:
 705                        if estr:
 706                            if nds:
 707                                msgsrc = "{}:{}".format(fp_rel, sorted({nd.lineno for nd in nds})[0])
 708                            else:
 709                                msgsrc = "{}:???".format(fp_rel)
 710                            process_msg(msgs, msgctxt, estr, msgsrc, reports, check_ctxt_py, settings)
 711                            reports["py_messages"].append((msgctxt, estr, msgsrc))
 712
 713
 714def dump_py_messages(msgs, reports, addons, settings, addons_only=False):
 715    def _get_files(path):
 716        if not os.path.exists(path):
 717            return []
 718        if os.path.isdir(path):
 719            return [os.path.join(dpath, fn) for dpath, _, fnames in os.walk(path) for fn in fnames
 720                    if not fn.startswith("_") and fn.endswith(".py")]
 721        return [path]
 722
 723    files = []
 724    if not addons_only:
 725        for path in settings.CUSTOM_PY_UI_FILES:
 726            for root in (bpy.utils.resource_path(t) for t in ('USER', 'LOCAL', 'SYSTEM')):
 727                files += _get_files(os.path.join(root, path))
 728
 729    # Add all given addons.
 730    for mod in addons:
 731        fn = mod.__file__
 732        if os.path.basename(fn) == "__init__.py":
 733            files += _get_files(os.path.dirname(fn))
 734        else:
 735            files.append(fn)
 736
 737    dump_py_messages_from_files(msgs, reports, sorted(files), settings)
 738
 739
 740##### C source code #####
 741def dump_src_messages(msgs, reports, settings):
 742    def get_contexts():
 743        """Return a mapping {C_CTXT_NAME: ctxt_value}."""
 744        return {k: getattr(bpy.app.translations.contexts, n) for k, n in bpy.app.translations.contexts_C_to_py.items()}
 745
 746    contexts = get_contexts()
 747
 748    # Build regexes to extract messages (with optional contexts) from C source.
 749    pygettexts = tuple(re.compile(r).search for r in settings.PYGETTEXT_KEYWORDS)
 750
 751    _clean_str = re.compile(settings.str_clean_re).finditer
 752
 753    def clean_str(s):
 754        return "".join(m.group("clean") for m in _clean_str(s))
 755
 756    def dump_src_file(path, rel_path, msgs, reports, settings):
 757        def process_entry(_msgctxt, _msgid):
 758            # Context.
 759            msgctxt = settings.DEFAULT_CONTEXT
 760            if _msgctxt:
 761                if _msgctxt in contexts:
 762                    msgctxt = contexts[_msgctxt]
 763                elif '"' in _msgctxt or "'" in _msgctxt:
 764                    msgctxt = clean_str(_msgctxt)
 765                else:
 766                    print("WARNING: raw context “{}” couldn’t be resolved!".format(_msgctxt))
 767            # Message.
 768            msgid = ""
 769            if _msgid:
 770                if '"' in _msgid or "'" in _msgid:
 771                    msgid = clean_str(_msgid)
 772                else:
 773                    print("WARNING: raw message “{}” couldn’t be resolved!".format(_msgid))
 774            return msgctxt, msgid
 775
 776        check_ctxt_src = None
 777        if reports["check_ctxt"]:
 778            check_ctxt = reports["check_ctxt"]
 779            check_ctxt_src = {
 780                "multi_lines": check_ctxt.get("multi_lines"),
 781                "not_capitalized": check_ctxt.get("not_capitalized"),
 782                "end_point": check_ctxt.get("end_point"),
 783                "spell_checker": check_ctxt.get("spell_checker"),
 784                "spell_errors": check_ctxt.get("spell_errors"),
 785            }
 786
 787        data = ""
 788        with open(path) as f:
 789            data = f.read()
 790        for srch in pygettexts:
 791            m = srch(data)
 792            line = pos = 0
 793            while m:
 794                d = m.groupdict()
 795                # Line.
 796                line += data[pos:m.start()].count('\n')
 797                msgsrc = rel_path + ":" + str(line)
 798                _msgid = d.get("msg_raw")
 799                # First, try the "multi-contexts" stuff!
 800                _msgctxts = tuple(d.get("ctxt_raw{}".format(i)) for i in range(settings.PYGETTEXT_MAX_MULTI_CTXT))
 801                if _msgctxts[0]:
 802                    for _msgctxt in _msgctxts:
 803                        if not _msgctxt:
 804                            break
 805                        msgctxt, msgid = process_entry(_msgctxt, _msgid)
 806                        process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
 807                        reports["src_messages"].append((msgctxt, msgid, msgsrc))
 808                else:
 809                    _msgctxt = d.get("ctxt_raw")
 810                    msgctxt, msgid = process_entry(_msgctxt, _msgid)
 811                    process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
 812                    reports["src_messages"].append((msgctxt, msgid, msgsrc))
 813
 814                pos = m.end()
 815                line += data[m.start():pos].count('\n')
 816                m = srch(data, pos)
 817
 818    forbidden = set()
 819    forced = set()
 820    if os.path.isfile(settings.SRC_POTFILES):
 821        with open(settings.SRC_POTFILES) as src:
 822            for l in src:
 823                if l[0] == '-':
 824                    forbidden.add(l[1:].rstrip('\n'))
 825                elif l[0] != '#':
 826                    forced.add(l.rstrip('\n'))
 827    for root, dirs, files in os.walk(settings.POTFILES_SOURCE_DIR):
 828        if "/.svn" in root:
 829            continue
 830        for fname in files:
 831            if os.path.splitext(fname)[1] not in settings.PYGETTEXT_ALLOWED_EXTS:
 832                continue
 833            path = os.path.join(root, fname)
 834            try:  # can't always find the relative path (between drive letters on windows)
 835                rel_path = os.path.relpath(path, settings.SOURCE_DIR)
 836            except ValueError:
 837                rel_path = path
 838            if rel_path in forbidden:
 839                continue
 840            elif rel_path not in forced:
 841                forced.add(rel_path)
 842    for rel_path in sorted(forced):
 843        path = os.path.join(settings.SOURCE_DIR, rel_path)
 844        if os.path.exists(path):
 845            dump_src_file(path, rel_path, msgs, reports, settings)
 846
 847
 848##### Main functions! #####
 849def dump_messages(do_messages, do_checks, settings):
 850    bl_ver = "Blender " + bpy.app.version_string
 851    bl_hash = bpy.app.build_hash
 852    bl_date = datetime.datetime.strptime(bpy.app.build_date.decode() + "T" + bpy.app.build_time.decode(),
 853                                         "%Y-%m-%dT%H:%M:%S")
 854    pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, bl_ver, bl_hash, bl_date, bl_date.year,
 855                                                settings=settings)
 856    msgs = pot.msgs
 857
 858    # Enable all wanted addons.
 859    # For now, enable all official addons, before extracting msgids.
 860    addons = utils.enable_addons(support={"OFFICIAL"})
 861    # Note this is not needed if we have been started with factory settings, but just in case...
 862    # XXX This is not working well, spent a whole day trying to understand *why* we still have references of
 863    #     those removed calsses in things like `bpy.types.OperatorProperties.__subclasses__()`
 864    #     (could not even reproduce it from regular py console in Blender with UI...).
 865    #     For some reasons, cleanup does not happen properly, *and* we have no way to tell which class is valid
 866    #     and which has been unregistered. So for now, just go for the dirty, easy way: do not disable add-ons. :(
 867    # ~ utils.enable_addons(support={"COMMUNITY", "TESTING"}, disable=True)
 868
 869    reports = _gen_reports(_gen_check_ctxt(settings) if do_checks else None)
 870
 871    # Get strings from RNA.
 872    dump_rna_messages(msgs, reports, settings)
 873
 874    # Get strings from UI layout definitions text="..." args.
 875    dump_py_messages(msgs, reports, addons, settings)
 876
 877    # Get strings from C source code.
 878    dump_src_messages(msgs, reports, settings)
 879
 880    # Get strings from addons' categories.
 881    for uid, label, tip in bpy.types.WindowManager.addon_filter[1]['items'](bpy.context.window_manager, bpy.context):
 882        process_msg(msgs, settings.DEFAULT_CONTEXT, label, "Add-ons' categories", reports, None, settings)
 883        if tip:
 884            process_msg(msgs, settings.DEFAULT_CONTEXT, tip, "Add-ons' categories", reports, None, settings)
 885
 886    # Get strings specific to translations' menu.
 887    for lng in settings.LANGUAGES:
 888        process_msg(msgs, settings.DEFAULT_CONTEXT, lng[1], "Languages’ labels from bl_i18n_utils/settings.py",
 889                    reports, None, settings)
 890    for cat in settings.LANGUAGES_CATEGORIES:
 891        process_msg(msgs, settings.DEFAULT_CONTEXT, cat[1],
 892                    "Language categories’ labels from bl_i18n_utils/settings.py", reports, None, settings)
 893
 894    # pot.check()
 895    pot.unescape()  # Strings gathered in py/C source code may contain escaped chars...
 896    print_info(reports, pot)
 897    # pot.check()
 898
 899    if do_messages:
 900        print("Writing messages…")
 901        pot.write('PO', settings.FILE_NAME_POT)
 902
 903    print("Finished extracting UI messages!")
 904
 905    return pot  # Not used currently, but may be useful later (and to be consistent with dump_addon_messages!).
 906
 907
 908def dump_addon_messages(module_name, do_checks, settings):
 909    import addon_utils
 910
 911    # Get current addon state (loaded or not):
 912    was_loaded = addon_utils.check(module_name)[1]
 913
 914    # Enable our addon.
 915    addon = utils.enable_addons(addons={module_name})[0]
 916
 917    addon_info = addon_utils.module_bl_info(addon)
 918    ver = addon_info["name"] + " " + ".".join(str(v) for v in addon_info["version"])
 919    rev = 0
 920    date = datetime.datetime.now()
 921    pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year,
 922                                                settings=settings)
 923    msgs = pot.msgs
 924
 925    minus_pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year,
 926                                                      settings=settings)
 927    minus_msgs = minus_pot.msgs
 928
 929    check_ctxt = _gen_check_ctxt(settings) if do_checks else None
 930    minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None
 931
 932    # Get strings from RNA, our addon being enabled.
 933    print("A")
 934    reports = _gen_reports(check_ctxt)
 935    print("B")
 936    dump_rna_messages(msgs, reports, settings)
 937    print("C")
 938
 939    # Now disable our addon, and rescan RNA.
 940    utils.enable_addons(addons={module_name}, disable=True)
 941    print("D")
 942    reports["check_ctxt"] = minus_check_ctxt
 943    print("E")
 944    dump_rna_messages(minus_msgs, reports, settings)
 945    print("F")
 946
 947    # Restore previous state if needed!
 948    if was_loaded:
 949        utils.enable_addons(addons={module_name})
 950
 951    # and make the diff!
 952    for key in minus_msgs:
 953        if key != settings.PO_HEADER_KEY:
 954            if key in msgs.keys():
 955                del msgs[key]
 956
 957    if check_ctxt:
 958        _diff_check_ctxt(check_ctxt, minus_check_ctxt)
 959
 960    # and we are done with those!
 961    del minus_pot
 962    del minus_msgs
 963    del minus_check_ctxt
 964
 965    # get strings from UI layout definitions text="..." args
 966    reports["check_ctxt"] = check_ctxt
 967    dump_py_messages(msgs, reports, {addon}, settings, addons_only=True)
 968
 969    pot.unescape()  # Strings gathered in py/C source code may contain escaped chars...
 970    print_info(reports, pot)
 971
 972    print("Finished extracting UI messages!")
 973
 974    return pot
 975
 976
 977def main():
 978    try:
 979        import bpy
 980    except ImportError:
 981        print("This script must run from inside blender")
 982        return
 983
 984    import sys
 985    import argparse
 986
 987    # Get rid of Blender args!
 988    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
 989
 990    parser = argparse.ArgumentParser(description="Process UI messages from inside Blender.")
 991    parser.add_argument('-c', '--no_checks', default=True, action="store_false", help="No checks over UI messages.")
 992    parser.add_argument('-m', '--no_messages', default=True, action="store_false", help="No export of UI messages.")
 993    parser.add_argument('-o', '--output', default=None, help="Output POT file path.")
 994    parser.add_argument('-s', '--settings', default=None,
 995                        help="Override (some) default settings. Either a JSon file name, or a JSon string.")
 996    args = parser.parse_args(argv)
 997
 998    settings = settings_i18n.I18nSettings()
 999    settings.load(args.settings)
1000
1001    if args.output:
1002        settings.FILE_NAME_POT = args.output
1003
1004    dump_messages(do_messages=args.no_messages, do_checks=args.no_checks, settings=settings)
1005
1006
1007if __name__ == "__main__":
1008    print("\n\n *** Running {} *** \n".format(__file__))
1009    main()
ignore_reg = re.compile('^(?:[-*.()/\\\\+%°0-9]|%d|%f|%s|%r|\\s)*$')
def filter_message(string, pos=0, endpos=9223372036854775807):

Matches zero or more characters at the beginning of the string.

def init_spell_check(settings, lang='en_US'):
45def init_spell_check(settings, lang="en_US"):
46    try:
47        from bl_i18n_utils import utils_spell_check
48        return utils_spell_check.SpellChecker(settings, lang)
49    except Exception as e:
50        print("Failed to import utils_spell_check ({})".format(str(e)))
51        return None
def check(check_ctxt, msgs, key, msgsrc, settings):
 95def check(check_ctxt, msgs, key, msgsrc, settings):
 96    """
 97    Performs a set of checks over the given key (context, message)...
 98    """
 99    if check_ctxt is None:
100        return
101    multi_rnatip = check_ctxt.get("multi_rnatip")
102    multi_lines = check_ctxt.get("multi_lines")
103    py_in_rna = check_ctxt.get("py_in_rna")
104    not_capitalized = check_ctxt.get("not_capitalized")
105    end_point = check_ctxt.get("end_point")
106    undoc_ops = check_ctxt.get("undoc_ops")
107    spell_checker = check_ctxt.get("spell_checker")
108    spell_errors = check_ctxt.get("spell_errors")
109
110    if multi_rnatip is not None:
111        if key in msgs and key not in multi_rnatip:
112            multi_rnatip.add(key)
113    if multi_lines is not None:
114        if '\n' in key[1]:
115            multi_lines.add(key)
116    if py_in_rna is not None:
117        if key in py_in_rna[1]:
118            py_in_rna[0].add(key)
119    if not_capitalized is not None:
120        if(key[1] not in settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED and
121           key[1][0].isalpha() and not key[1][0].isupper()):
122            not_capitalized.add(key)
123    if end_point is not None:
124        if (
125                key[1].strip().endswith('.') and
126                (not key[1].strip().endswith('...')) and
127                key[1] not in settings.WARN_MSGID_END_POINT_ALLOWED
128        ):
129            end_point.add(key)
130    if undoc_ops is not None:
131        if key[1] == settings.UNDOC_OPS_STR:
132            undoc_ops.add(key)
133    if spell_checker is not None and spell_errors is not None:
134        err = spell_checker.check(key[1])
135        if err:
136            spell_errors[key] = err

Performs a set of checks over the given key (context, message)...

def process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt, settings):
199def process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt, settings):
200    if filter_message(msgid):
201        reports["messages_skipped"].add((msgid, msgsrc))
202        return
203    if not msgctxt:
204        # We do *not* want any "" context!
205        msgctxt = settings.DEFAULT_CONTEXT
206    # Always unescape keys!
207    msgctxt = utils.I18nMessage.do_unescape(msgctxt)
208    msgid = utils.I18nMessage.do_unescape(msgid)
209    key = (msgctxt, msgid)
210    check(check_ctxt, msgs, key, msgsrc, settings)
211    msgsrc = settings.PO_COMMENT_PREFIX_SOURCE_CUSTOM + msgsrc
212    if key not in msgs:
213        msgs[key] = utils.I18nMessage([msgctxt], [msgid], [], [msgsrc], settings=settings)
214    else:
215        msgs[key].comment_lines.append(msgsrc)
def dump_rna_messages( msgs, reports, settings, verbose=False, class_list=[<class 'bpy_types.Context'>, <class 'bpy.types.ID'>, <class 'bpy_types.PoseBone'>, <class 'bpy_types.Bone'>, <class 'bpy_types.EditBone'>, <class 'bpy_types.MeshEdge'>, <class 'bpy_types.MeshLoopTriangle'>, <class 'bpy_types.MeshPolygon'>, <class 'bpy_types.Gizmo'>, <class 'bpy_types.GizmoGroup'>, <class 'bpy_types.Operator'>, <class 'bpy_types.Macro'>, <class 'bpy_types.PropertyGroup'>, <class 'bpy_types.RenderEngine'>, <class 'bpy_types.KeyingSetInfo'>, <class 'bpy_types.AddonPreferences'>, <class 'bpy_types.Panel'>, <class 'bpy_types.UIList'>, <class 'bpy_types.Header'>, <class 'bpy_types.Menu'>, <class 'bpy_types.Node'>, <class 'bpy_types.NodeSocket'>, <class 'bpy_types.NodeSocketInterface'>, <class 'bpy.types.BlendData'>, <class 'bpy.types.BlendDataLibraries'>, <class 'bpy.types.UILayout'>, <class 'bpy.types.Space'>, <class 'bpy.types.Preferences'>, <class 'bpy.types.PreferencesFilePaths'>, <class 'bpy.types.RigidBodyConstraint'>, <class 'bpy.types.Property'>, <class 'bpy.types.EnumPropertyItem'>, <class 'bpy.types.ViewLayer'>, <class 'bpy.types.Sequence'>, <class 'bpy.types.Theme'>, <class 'bpy.types.ThemeView3D'>, <class 'bpy.types.ThemeSpaceGradient'>, <class 'bpy.types.ThemeGradientColors'>, <class 'bpy.types.ThemePanelColors'>, <class 'bpy.types.ThemeGraphEditor'>, <class 'bpy.types.ThemeSpaceGeneric'>, <class 'bpy.types.ThemeSpaceListGeneric'>, <class 'bpy.types.ThemeDopeSheet'>, <class 'bpy.types.ThemeNLAEditor'>, <class 'bpy.types.ThemeImageEditor'>, <class 'bpy.types.ThemeSequenceEditor'>, <class 'bpy.types.ThemeTextEditor'>, <class 'bpy.types.ThemeNodeEditor'>, <class 'bpy.types.ThemeProperties'>, <class 'bpy.types.ThemeOutliner'>, <class 'bpy.types.ThemePreferences'>, <class 'bpy.types.ThemeInfo'>, <class 'bpy.types.ThemeFileBrowser'>, <class 'bpy.types.ThemeConsole'>, <class 'bpy.types.ThemeClipEditor'>, <class 'bpy.types.ThemeTopBar'>, <class 'bpy.types.ThemeStatusBar'>, <class 'bpy.types.ThemeSpreadsheet'>, <class 'bpy.types.Addon'>, <class 'bpy.types.View3DShading'>, <class 'bpy.types.KeyMap'>, <class 'bpy.types.KeyMapItem'>, <class 'bpy.types.Area'>, <class 'bpy.types.FileSelectParams'>, <class 'bpy.types.FCurve'>, <class 'bpy.types.Keyframe'>, <class 'bpy.types.Event'>, <class 'bpy.types.FileSelectEntry'>, <class 'bpy.types.OperatorProperties'>, <class 'bpy.types.TimelineMarker'>, <class 'bpy.types.AOV'>, <class 'bpy.types.AOVs'>, <class 'bpy.types.Constraint'>, <class 'bpy.types.ActionFCurves'>, <class 'bpy.types.ActionGroup'>, <class 'bpy.types.ActionGroups'>, <class 'bpy.types.ActionPoseMarkers'>, <class 'bpy.types.Addons'>, <class 'bpy.types.AnimData'>, <class 'bpy.types.AnimDataDrivers'>, <class 'bpy.types.AnimViz'>, <class 'bpy.types.AnimVizMotionPaths'>, <class 'bpy.types.AnyType'>, <class 'bpy.types.AreaSpaces'>, <class 'bpy.types.ArmatureBones'>, <class 'bpy.types.ArmatureConstraintTargets'>, <class 'bpy.types.ArmatureEditBones'>, <class 'bpy.types.GpencilModifier'>, <class 'bpy.types.Modifier'>, <class 'bpy.types.AssetCatalogPath'>, <class 'bpy.types.AssetLibraryReference'>, <class 'bpy.types.AssetMetaData'>, <class 'bpy.types.AssetTag'>, <class 'bpy.types.AssetTags'>, <class 'bpy.types.Attribute'>, <class 'bpy.types.AttributeGroup'>, <class 'bpy.types.BakeSettings'>, <class 'bpy.types.RenderSettings'>, <class 'bpy.types.BezierSplinePoint'>, <class 'bpy.types.BlendDataActions'>, <class 'bpy.types.BlendDataArmatures'>, <class 'bpy.types.BlendDataBrushes'>, <class 'bpy.types.BlendDataCacheFiles'>, <class 'bpy.types.BlendDataCameras'>, <class 'bpy.types.BlendDataCollections'>, <class 'bpy.types.BlendDataCurves'>, <class 'bpy.types.BlendDataFonts'>, <class 'bpy.types.BlendDataGreasePencils'>, <class 'bpy.types.BlendDataHairCurves'>, <class 'bpy.types.BlendDataImages'>, <class 'bpy.types.BlendDataLattices'>, <class 'bpy.types.BlendDataLights'>, <class 'bpy.types.BlendDataLineStyles'>, <class 'bpy.types.BlendDataMasks'>, <class 'bpy.types.BlendDataMaterials'>, <class 'bpy.types.BlendDataMeshes'>, <class 'bpy.types.BlendDataMetaBalls'>, <class 'bpy.types.BlendDataMovieClips'>, <class 'bpy.types.BlendDataNodeTrees'>, <class 'bpy.types.BlendDataObjects'>, <class 'bpy.types.BlendDataPaintCurves'>, <class 'bpy.types.BlendDataPalettes'>, <class 'bpy.types.BlendDataParticles'>, <class 'bpy.types.BlendDataPointClouds'>, <class 'bpy.types.BlendDataProbes'>, <class 'bpy.types.BlendDataScenes'>, <class 'bpy.types.BlendDataScreens'>, <class 'bpy.types.BlendDataSounds'>, <class 'bpy.types.BlendDataSpeakers'>, <class 'bpy.types.BlendDataTexts'>, <class 'bpy.types.BlendDataTextures'>, <class 'bpy.types.BlendDataVolumes'>, <class 'bpy.types.BlendDataWindowManagers'>, <class 'bpy.types.BlendDataWorkSpaces'>, <class 'bpy.types.BlendDataWorlds'>, <class 'bpy.types.BlenderRNA'>, <class 'bpy.types.BoidRule'>, <class 'bpy.types.BoidSettings'>, <class 'bpy.types.BoidState'>, <class 'bpy.types.BoneGroup'>, <class 'bpy.types.BoneGroups'>, <class 'bpy.types.BoolAttributeValue'>, <class 'bpy.types.SequenceModifier'>, <class 'bpy.types.BrushCapabilities'>, <class 'bpy.types.BrushCapabilitiesImagePaint'>, <class 'bpy.types.BrushCapabilitiesSculpt'>, <class 'bpy.types.BrushCapabilitiesVertexPaint'>, <class 'bpy.types.BrushCapabilitiesWeightPaint'>, <class 'bpy.types.BrushCurvesSculptSettings'>, <class 'bpy.types.BrushGpencilSettings'>, <class 'bpy.types.TextureSlot'>, <class 'bpy.types.ByteColorAttributeValue'>, <class 'bpy.types.ByteIntAttributeValue'>, <class 'bpy.types.CacheFileLayer'>, <class 'bpy.types.CacheFileLayers'>, <class 'bpy.types.CacheObjectPath'>, <class 'bpy.types.CacheObjectPaths'>, <class 'bpy.types.CameraBackgroundImage'>, <class 'bpy.types.CameraBackgroundImages'>, <class 'bpy.types.CameraDOFSettings'>, <class 'bpy.types.CameraStereoData'>, <class 'bpy.types.ChannelDriverVariables'>, <class 'bpy.types.ChildParticle'>, <class 'bpy.types.ClothCollisionSettings'>, <class 'bpy.types.ClothSettings'>, <class 'bpy.types.ClothSolverResult'>, <class 'bpy.types.CollectionChildren'>, <class 'bpy.types.CollectionObjects'>, <class 'bpy.types.CollisionSettings'>, <class 'bpy.types.ColorManagedDisplaySettings'>, <class 'bpy.types.ColorManagedInputColorspaceSettings'>, <class 'bpy.types.ColorManagedSequencerColorspaceSettings'>, <class 'bpy.types.ColorManagedViewSettings'>, <class 'bpy.types.ColorMapping'>, <class 'bpy.types.ColorRamp'>, <class 'bpy.types.ColorRampElement'>, <class 'bpy.types.ColorRampElements'>, <class 'bpy.types.CompositorNodeOutputFileFileSlots'>, <class 'bpy.types.CompositorNodeOutputFileLayerSlots'>, <class 'bpy.types.ConsoleLine'>, <class 'bpy.types.ConstraintTarget'>, <class 'bpy.types.ConstraintTargetBone'>, <class 'bpy.types.CryptomatteEntry'>, <class 'bpy.types.CurveMap'>, <class 'bpy.types.CurveMapPoint'>, <class 'bpy.types.CurveMapPoints'>, <class 'bpy.types.CurveMapping'>, <class 'bpy.types.CurvePaintSettings'>, <class 'bpy.types.CurvePoint'>, <class 'bpy.types.CurveProfile'>, <class 'bpy.types.CurveProfilePoint'>, <class 'bpy.types.CurveProfilePoints'>, <class 'bpy.types.CurveSlice'>, <class 'bpy.types.CurveSplines'>, <class 'bpy.types.Paint'>, <class 'bpy.types.DashGpencilModifierSegment'>, <class 'bpy.types.Depsgraph'>, <class 'bpy.types.DepsgraphObjectInstance'>, <class 'bpy.types.DepsgraphUpdate'>, <class 'bpy.types.DisplaySafeAreas'>, <class 'bpy.types.DopeSheet'>, <class 'bpy.types.Driver'>, <class 'bpy.types.DriverTarget'>, <class 'bpy.types.DriverVariable'>, <class 'bpy.types.DynamicPaintBrushSettings'>, <class 'bpy.types.DynamicPaintCanvasSettings'>, <class 'bpy.types.DynamicPaintSurface'>, <class 'bpy.types.DynamicPaintSurfaces'>, <class 'bpy.types.EffectorWeights'>, <class 'bpy.types.FCurveKeyframePoints'>, <class 'bpy.types.FCurveModifiers'>, <class 'bpy.types.FCurveSample'>, <class 'bpy.types.FFmpegSettings'>, <class 'bpy.types.FModifier'>, <class 'bpy.types.FModifierEnvelopeControlPoint'>, <class 'bpy.types.FModifierEnvelopeControlPoints'>, <class 'bpy.types.FaceMap'>, <class 'bpy.types.FaceMaps'>, <class 'bpy.types.FieldSettings'>, <class 'bpy.types.FileAssetSelectIDFilter'>, <class 'bpy.types.FileBrowserFSMenuEntry'>, <class 'bpy.types.FileSelectIDFilter'>, <class 'bpy.types.Float2AttributeValue'>, <class 'bpy.types.FloatAttributeValue'>, <class 'bpy.types.FloatColorAttributeValue'>, <class 'bpy.types.FloatVectorAttributeValue'>, <class 'bpy.types.FluidDomainSettings'>, <class 'bpy.types.FluidEffectorSettings'>, <class 'bpy.types.FluidFlowSettings'>, <class 'bpy.types.FreestyleLineSet'>, <class 'bpy.types.FreestyleModuleSettings'>, <class 'bpy.types.FreestyleModules'>, <class 'bpy.types.FreestyleSettings'>, <class 'bpy.types.Function'>, <class 'bpy.types.GPencilEditCurve'>, <class 'bpy.types.GPencilEditCurvePoint'>, <class 'bpy.types.GPencilFrame'>, <class 'bpy.types.GPencilFrames'>, <class 'bpy.types.GPencilInterpolateSettings'>, <class 'bpy.types.GPencilLayer'>, <class 'bpy.types.GPencilLayerMask'>, <class 'bpy.types.GPencilSculptGuide'>, <class 'bpy.types.GPencilSculptSettings'>, <class 'bpy.types.GPencilStroke'>, <class 'bpy.types.GPencilStrokePoint'>, <class 'bpy.types.GPencilStrokePoints'>, <class 'bpy.types.GPencilStrokes'>, <class 'bpy.types.GPencilTriangle'>, <class 'bpy.types.GizmoGroupProperties'>, <class 'bpy.types.GizmoProperties'>, <class 'bpy.types.Gizmos'>, <class 'bpy.types.GpencilVertexGroupElement'>, <class 'bpy.types.GreasePencilGrid'>, <class 'bpy.types.GreasePencilLayers'>, <class 'bpy.types.GreasePencilMaskLayers'>, <class 'bpy.types.Histogram'>, <class 'bpy.types.IDMaterials'>, <class 'bpy.types.IDOverrideLibrary'>, <class 'bpy.types.IDOverrideLibraryProperties'>, <class 'bpy.types.IDOverrideLibraryProperty'>, <class 'bpy.types.IDOverrideLibraryPropertyOperation'>, <class 'bpy.types.IDOverrideLibraryPropertyOperations'>, <class 'bpy.types.IDPropertyWrapPtr'>, <class 'bpy.types.ViewerPathElem'>, <class 'bpy.types.IKParam'>, <class 'bpy.types.ImageFormatSettings'>, <class 'bpy.types.ImagePackedFile'>, <class 'bpy.types.ImagePreview'>, <class 'bpy.types.ImageUser'>, <class 'bpy.types.IntAttributeValue'>, <class 'bpy.types.KeyConfig'>, <class 'bpy.types.KeyConfigPreferences'>, <class 'bpy.types.KeyConfigurations'>, <class 'bpy.types.KeyMapItems'>, <class 'bpy.types.KeyMaps'>, <class 'bpy.types.KeyingSet'>, <class 'bpy.types.KeyingSetPath'>, <class 'bpy.types.KeyingSetPaths'>, <class 'bpy.types.KeyingSets'>, <class 'bpy.types.KeyingSetsAll'>, <class 'bpy.types.LatticePoint'>, <class 'bpy.types.LayerCollection'>, <class 'bpy.types.LayerObjects'>, <class 'bpy.types.LibraryWeakReference'>, <class 'bpy.types.Lightgroup'>, <class 'bpy.types.Lightgroups'>, <class 'bpy.types.LineStyleModifier'>, <class 'bpy.types.LineStyleAlphaModifiers'>, <class 'bpy.types.LineStyleColorModifiers'>, <class 'bpy.types.LineStyleGeometryModifiers'>, <class 'bpy.types.LineStyleTextureSlots'>, <class 'bpy.types.LineStyleThicknessModifiers'>, <class 'bpy.types.Linesets'>, <class 'bpy.types.LoopColors'>, <class 'bpy.types.MaskLayer'>, <class 'bpy.types.MaskLayers'>, <class 'bpy.types.MaskParent'>, <class 'bpy.types.MaskSpline'>, <class 'bpy.types.MaskSplinePoint'>, <class 'bpy.types.MaskSplinePointUW'>, <class 'bpy.types.MaskSplinePoints'>, <class 'bpy.types.MaskSplines'>, <class 'bpy.types.MaterialGPencilStyle'>, <class 'bpy.types.MaterialLineArt'>, <class 'bpy.types.MaterialSlot'>, <class 'bpy.types.MeshEdgeCrease'>, <class 'bpy.types.MeshEdgeCreaseLayer'>, <class 'bpy.types.MeshEdges'>, <class 'bpy.types.MeshFaceMap'>, <class 'bpy.types.MeshFaceMapLayer'>, <class 'bpy.types.MeshFaceMapLayers'>, <class 'bpy.types.MeshLoop'>, <class 'bpy.types.MeshLoopColor'>, <class 'bpy.types.MeshLoopColorLayer'>, <class 'bpy.types.MeshLoopTriangles'>, <class 'bpy.types.MeshLoops'>, <class 'bpy.types.MeshNormalValue'>, <class 'bpy.types.MeshPaintMaskLayer'>, <class 'bpy.types.MeshPaintMaskProperty'>, <class 'bpy.types.MeshPolygonFloatProperty'>, <class 'bpy.types.MeshPolygonFloatPropertyLayer'>, <class 'bpy.types.MeshPolygonIntProperty'>, <class 'bpy.types.MeshPolygonIntPropertyLayer'>, <class 'bpy.types.MeshPolygonStringProperty'>, <class 'bpy.types.MeshPolygonStringPropertyLayer'>, <class 'bpy.types.MeshPolygons'>, <class 'bpy.types.MeshSkinVertex'>, <class 'bpy.types.MeshSkinVertexLayer'>, <class 'bpy.types.MeshStatVis'>, <class 'bpy.types.MeshUVLoop'>, <class 'bpy.types.MeshUVLoopLayer'>, <class 'bpy.types.MeshVertColor'>, <class 'bpy.types.MeshVertColorLayer'>, <class 'bpy.types.MeshVertex'>, <class 'bpy.types.MeshVertexCrease'>, <class 'bpy.types.MeshVertexCreaseLayer'>, <class 'bpy.types.MeshVertexFloatProperty'>, <class 'bpy.types.MeshVertexFloatPropertyLayer'>, <class 'bpy.types.MeshVertexIntProperty'>, <class 'bpy.types.MeshVertexIntPropertyLayer'>, <class 'bpy.types.MeshVertexStringProperty'>, <class 'bpy.types.MeshVertexStringPropertyLayer'>, <class 'bpy.types.MeshVertices'>, <class 'bpy.types.MetaBallElements'>, <class 'bpy.types.MetaElement'>, <class 'bpy.types.MotionPath'>, <class 'bpy.types.MotionPathVert'>, <class 'bpy.types.MovieClipProxy'>, <class 'bpy.types.MovieClipScopes'>, <class 'bpy.types.MovieClipUser'>, <class 'bpy.types.MovieReconstructedCamera'>, <class 'bpy.types.MovieTracking'>, <class 'bpy.types.MovieTrackingCamera'>, <class 'bpy.types.MovieTrackingDopesheet'>, <class 'bpy.types.MovieTrackingMarker'>, <class 'bpy.types.MovieTrackingMarkers'>, <class 'bpy.types.MovieTrackingObject'>, <class 'bpy.types.MovieTrackingObjectPlaneTracks'>, <class 'bpy.types.MovieTrackingObjectTracks'>, <class 'bpy.types.MovieTrackingObjects'>, <class 'bpy.types.MovieTrackingPlaneMarker'>, <class 'bpy.types.MovieTrackingPlaneMarkers'>, <class 'bpy.types.MovieTrackingPlaneTrack'>, <class 'bpy.types.MovieTrackingPlaneTracks'>, <class 'bpy.types.MovieTrackingReconstructedCameras'>, <class 'bpy.types.MovieTrackingReconstruction'>, <class 'bpy.types.MovieTrackingSettings'>, <class 'bpy.types.MovieTrackingStabilization'>, <class 'bpy.types.MovieTrackingTrack'>, <class 'bpy.types.MovieTrackingTracks'>, <class 'bpy.types.NlaStrip'>, <class 'bpy.types.NlaStripFCurves'>, <class 'bpy.types.NlaStrips'>, <class 'bpy.types.NlaTrack'>, <class 'bpy.types.NlaTracks'>, <class 'bpy.types.NodeInputs'>, <class 'bpy.types.NodeInstanceHash'>, <class 'bpy.types.NodeInternalSocketTemplate'>, <class 'bpy.types.NodeLink'>, <class 'bpy.types.NodeLinks'>, <class 'bpy.types.NodeOutputFileSlotFile'>, <class 'bpy.types.NodeOutputFileSlotLayer'>, <class 'bpy.types.NodeOutputs'>, <class 'bpy.types.NodeTreeInputs'>, <class 'bpy.types.NodeTreeOutputs'>, <class 'bpy.types.NodeTreePath'>, <class 'bpy.types.Nodes'>, <class 'bpy.types.ObjectBase'>, <class 'bpy.types.ObjectConstraints'>, <class 'bpy.types.ObjectDisplay'>, <class 'bpy.types.ObjectGpencilModifiers'>, <class 'bpy.types.ObjectLineArt'>, <class 'bpy.types.ObjectModifiers'>, <class 'bpy.types.ObjectShaderFx'>, <class 'bpy.types.OperatorMacro'>, <class 'bpy.types.OperatorOptions'>, <class 'bpy.types.PackedFile'>, <class 'bpy.types.PaintModeSettings'>, <class 'bpy.types.PaintToolSlot'>, <class 'bpy.types.PaletteColor'>, <class 'bpy.types.PaletteColors'>, <class 'bpy.types.Particle'>, <class 'bpy.types.ParticleBrush'>, <class 'bpy.types.ParticleDupliWeight'>, <class 'bpy.types.ParticleEdit'>, <class 'bpy.types.ParticleHairKey'>, <class 'bpy.types.ParticleKey'>, <class 'bpy.types.ParticleSettingsTextureSlots'>, <class 'bpy.types.ParticleSystem'>, <class 'bpy.types.ParticleSystems'>, <class 'bpy.types.ParticleTarget'>, <class 'bpy.types.PathCompare'>, <class 'bpy.types.PathCompareCollection'>, <class 'bpy.types.Point'>, <class 'bpy.types.PointCache'>, <class 'bpy.types.PointCacheItem'>, <class 'bpy.types.PointCaches'>, <class 'bpy.types.PolygonFloatProperties'>, <class 'bpy.types.PolygonIntProperties'>, <class 'bpy.types.PolygonStringProperties'>, <class 'bpy.types.Pose'>, <class 'bpy.types.PoseBoneConstraints'>, <class 'bpy.types.PreferencesApps'>, <class 'bpy.types.PreferencesEdit'>, <class 'bpy.types.PreferencesExperimental'>, <class 'bpy.types.PreferencesInput'>, <class 'bpy.types.PreferencesKeymap'>, <class 'bpy.types.PreferencesSystem'>, <class 'bpy.types.PreferencesView'>, <class 'bpy.types.PropertyGroupItem'>, <class 'bpy.types.Region'>, <class 'bpy.types.RegionView3D'>, <class 'bpy.types.RenderLayer'>, <class 'bpy.types.RenderPass'>, <class 'bpy.types.RenderPasses'>, <class 'bpy.types.RenderResult'>, <class 'bpy.types.RenderSlot'>, <class 'bpy.types.RenderSlots'>, <class 'bpy.types.RenderView'>, <class 'bpy.types.RenderViews'>, <class 'bpy.types.RigidBodyObject'>, <class 'bpy.types.RigidBodyWorld'>, <class 'bpy.types.SPHFluidSettings'>, <class 'bpy.types.SceneDisplay'>, <class 'bpy.types.SceneEEVEE'>, <class 'bpy.types.SceneGpencil'>, <class 'bpy.types.SceneObjects'>, <class 'bpy.types.SceneRenderView'>, <class 'bpy.types.Scopes'>, <class 'bpy.types.SequenceColorBalanceData'>, <class 'bpy.types.SequenceCrop'>, <class 'bpy.types.SequenceEditor'>, <class 'bpy.types.SequenceElement'>, <class 'bpy.types.SequenceElements'>, <class 'bpy.types.SequenceModifiers'>, <class 'bpy.types.SequenceProxy'>, <class 'bpy.types.SequenceTimelineChannel'>, <class 'bpy.types.SequenceTransform'>, <class 'bpy.types.SequencerPreviewOverlay'>, <class 'bpy.types.SequencerTimelineOverlay'>, <class 'bpy.types.SequencerToolSettings'>, <class 'bpy.types.SequencesMeta'>, <class 'bpy.types.SequencesTopLevel'>, <class 'bpy.types.ShaderFx'>, <class 'bpy.types.ShapeKey'>, <class 'bpy.types.ShapeKeyBezierPoint'>, <class 'bpy.types.ShapeKeyCurvePoint'>, <class 'bpy.types.ShapeKeyPoint'>, <class 'bpy.types.SoftBodySettings'>, <class 'bpy.types.SpaceImageOverlay'>, <class 'bpy.types.SpaceNodeEditorPath'>, <class 'bpy.types.SpaceNodeOverlay'>, <class 'bpy.types.SpaceUVEditor'>, <class 'bpy.types.Spline'>, <class 'bpy.types.SplineBezierPoints'>, <class 'bpy.types.SplinePoint'>, <class 'bpy.types.SplinePoints'>, <class 'bpy.types.SpreadsheetColumn'>, <class 'bpy.types.SpreadsheetColumnID'>, <class 'bpy.types.SpreadsheetRowFilter'>, <class 'bpy.types.Stereo3dDisplay'>, <class 'bpy.types.Stereo3dFormat'>, <class 'bpy.types.StringAttributeValue'>, <class 'bpy.types.Struct'>, <class 'bpy.types.StudioLight'>, <class 'bpy.types.StudioLights'>, <class 'bpy.types.TexMapping'>, <class 'bpy.types.TexPaintSlot'>, <class 'bpy.types.TextBox'>, <class 'bpy.types.TextCharacterFormat'>, <class 'bpy.types.TextLine'>, <class 'bpy.types.ThemeBoneColorSet'>, <class 'bpy.types.ThemeCollectionColor'>, <class 'bpy.types.ThemeFontStyle'>, <class 'bpy.types.ThemeStripColor'>, <class 'bpy.types.ThemeStyle'>, <class 'bpy.types.ThemeUserInterface'>, <class 'bpy.types.ThemeWidgetColors'>, <class 'bpy.types.ThemeWidgetStateColors'>, <class 'bpy.types.TimeGpencilModifierSegment'>, <class 'bpy.types.TimelineMarkers'>, <class 'bpy.types.Timer'>, <class 'bpy.types.ToolSettings'>, <class 'bpy.types.TransformOrientation'>, <class 'bpy.types.TransformOrientationSlot'>, <class 'bpy.types.UDIMTile'>, <class 'bpy.types.UDIMTiles'>, <class 'bpy.types.UIPieMenu'>, <class 'bpy.types.UIPopover'>, <class 'bpy.types.UIPopupMenu'>, <class 'bpy.types.UVLoopLayers'>, <class 'bpy.types.UVProjector'>, <class 'bpy.types.UnifiedPaintSettings'>, <class 'bpy.types.UnitSettings'>, <class 'bpy.types.UnknownType'>, <class 'bpy.types.UserAssetLibrary'>, <class 'bpy.types.UserSolidLight'>, <class 'bpy.types.VertColors'>, <class 'bpy.types.VertexFloatProperties'>, <class 'bpy.types.VertexGroup'>, <class 'bpy.types.VertexGroupElement'>, <class 'bpy.types.VertexGroups'>, <class 'bpy.types.VertexIntProperties'>, <class 'bpy.types.VertexStringProperties'>, <class 'bpy.types.View2D'>, <class 'bpy.types.View3DCursor'>, <class 'bpy.types.View3DOverlay'>, <class 'bpy.types.ViewLayerEEVEE'>, <class 'bpy.types.ViewLayers'>, <class 'bpy.types.ViewerPath'>, <class 'bpy.types.VolumeDisplay'>, <class 'bpy.types.VolumeGrid'>, <class 'bpy.types.VolumeGrids'>, <class 'bpy.types.VolumeRender'>, <class 'bpy.types.WalkNavigation'>, <class 'bpy.types.Window'>, <class 'bpy.types.WorkSpaceTool'>, <class 'bpy.types.WorldLighting'>, <class 'bpy.types.WorldMistSettings'>, <class 'bpy.types.XrActionMap'>, <class 'bpy.types.XrActionMapBinding'>, <class 'bpy.types.XrActionMapBindings'>, <class 'bpy.types.XrActionMapItem'>, <class 'bpy.types.XrActionMapItems'>, <class 'bpy.types.XrActionMaps'>, <class 'bpy.types.XrComponentPath'>, <class 'bpy.types.XrComponentPaths'>, <class 'bpy.types.XrEventData'>, <class 'bpy.types.XrSessionSettings'>, <class 'bpy.types.XrSessionState'>, <class 'bpy.types.XrUserPath'>, <class 'bpy.types.XrUserPaths'>, <class 'bpy.types.wmOwnerID'>, <class 'bpy.types.wmOwnerIDs'>, <class 'bpy.types.wmTools'>]):
219def dump_rna_messages(msgs, reports, settings, verbose=False, class_list=bpy.types.ID.__base__.__subclasses__()): 
220    """
221    Dump into messages dict all RNA-defined UI messages (labels en tooltips).
222    """
223    def class_blacklist():
224        blacklist_rna_class = {getattr(bpy.types, cls_id) for cls_id in (
225            # core classes
226            "Context", "Event", "Function", "UILayout", "UnknownType", "Property", "Struct",
227            # registerable classes
228            "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo",
229            # window classes
230            "Window",
231        )
232        }
233
234        # More builtin classes we don't need to parse.
235        blacklist_rna_class |= {cls for cls in bpy.types.Property.__subclasses__()}
236
237        return blacklist_rna_class
238
239    check_ctxt_rna = check_ctxt_rna_tip = None
240    check_ctxt = reports["check_ctxt"]
241    if check_ctxt:
242        check_ctxt_rna = {
243            "multi_lines": check_ctxt.get("multi_lines"),
244            "not_capitalized": check_ctxt.get("not_capitalized"),
245            "end_point": check_ctxt.get("end_point"),
246            "undoc_ops": check_ctxt.get("undoc_ops"),
247            "spell_checker": check_ctxt.get("spell_checker"),
248            "spell_errors": check_ctxt.get("spell_errors"),
249        }
250        check_ctxt_rna_tip = check_ctxt_rna
251        check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip")
252
253    default_context = settings.DEFAULT_CONTEXT
254
255    # Function definitions
256    def walk_properties(cls):
257        bl_rna = cls.bl_rna
258        # Get our parents' properties, to not export them multiple times.
259        bl_rna_base = bl_rna.base
260        if bl_rna_base:
261            bl_rna_base_props = set(bl_rna_base.properties.values())
262        else:
263            bl_rna_base_props = set()
264
265        props = sorted(bl_rna.properties, key=lambda p: p.identifier)
266        for prop in props:
267            # Only write this property if our parent hasn't got it.
268            if prop in bl_rna_base_props:
269                continue
270            if prop.identifier == "rna_type":
271                continue
272            reports["rna_props"].append((cls, prop))
273
274            msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier)
275            msgctxt = prop.translation_context or default_context
276
277            if prop.name and (prop.name != prop.identifier or msgctxt != default_context):
278                process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings)
279            if prop.description:
280                process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings)
281
282            if isinstance(prop, bpy.types.EnumProperty):
283                done_items = set()
284                for item in prop.enum_items:
285                    msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier)
286                    done_items.add(item.identifier)
287                    if item.name and item.name != item.identifier:
288                        process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings)
289                    if item.description:
290                        process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip,
291                                    settings)
292                for item in prop.enum_items_static:
293                    if item.identifier in done_items:
294                        continue
295                    msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier)
296                    done_items.add(item.identifier)
297                    if item.name and item.name != item.identifier:
298                        process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings)
299                    if item.description:
300                        process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip,
301                                    settings)
302
303    def walk_tools_definitions(cls):
304        from bl_ui.space_toolsystem_common import ToolDef
305
306        bl_rna = cls.bl_rna
307        op_default_context = bpy.app.translations.contexts.operator_default
308
309        def process_tooldef(tool_context, tool):
310            if not isinstance(tool, ToolDef):
311                if callable(tool):
312                    for t in tool(None):
313                        process_tooldef(tool_context, t)
314                return
315            msgsrc = "bpy.types.{} Tools: '{}', '{}'".format(bl_rna.identifier, tool_context, tool.idname)
316            if tool.label:
317                process_msg(msgs, op_default_context, tool.label, msgsrc, reports, check_ctxt_rna, settings)
318            # Callable (function) descriptions must handle their translations themselves.
319            if tool.description and not callable(tool.description):
320                process_msg(msgs, default_context, tool.description, msgsrc, reports, check_ctxt_rna_tip, settings)
321
322        for tool_context, tools_defs in cls.tools_all():
323            for tools_group in tools_defs:
324                if tools_group is None:
325                    continue
326                elif isinstance(tools_group, tuple) and not isinstance(tools_group, ToolDef):
327                    for tool in tools_group:
328                        process_tooldef(tool_context, tool)
329                else:
330                    process_tooldef(tool_context, tools_group)
331
332    blacklist_rna_class = class_blacklist()
333
334    def walk_class(cls):
335        if not hasattr(cls, 'bl_rna'):
336            reports["rna_structs_skipped"].append(cls)
337            return
338        bl_rna = cls.bl_rna
339        msgsrc = "bpy.types." + bl_rna.identifier # CM3D2 Converter: this does crazy things like change the name of our operators!
340        #msgsrc = "bpy.types." + cls.__name__
341        # Catch operators and redirect through bpy.ops
342        if '_OT_' in bl_rna.identifier:
343            module, operator = cls.bl_idname.split('.')
344            instance = getattr(getattr(bpy.ops, module), operator)
345            cls = instance.get_rna_type().__class__
346            bl_rna = cls.bl_rna
347        #print(cls, msgsrc)
348        msgctxt = bl_rna.translation_context or default_context
349
350        if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context):
351            process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings)
352        
353        if hasattr(bl_rna, 'bl_description'):
354            process_msg(msgs, default_context, bl_rna.bl_description, msgsrc, reports, check_ctxt_rna_tip, settings)
355        if bl_rna.description:
356            process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings)
357        elif cls.__doc__:  # XXX Some classes (like KeyingSetInfo subclasses) have void description... :(
358            process_msg(msgs, default_context, cls.__doc__, msgsrc, reports, check_ctxt_rna_tip, settings)
359
360        # Panels' "tabs" system.
361        if hasattr(bl_rna, 'bl_category') and bl_rna.bl_category:
362            process_msg(msgs, default_context, bl_rna.bl_category, msgsrc, reports, check_ctxt_rna, settings)
363
364        if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label:
365            process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings)
366
367        # Tools Panels definitions.
368        if hasattr(bl_rna, 'tools_all') and bl_rna.tools_all:
369            walk_tools_definitions(cls)
370
371        walk_properties(cls)
372
373    def walk_keymap_hierarchy(hier, msgsrc_prev):
374        km_i18n_context = bpy.app.translations.contexts.id_windowmanager
375        for lvl in hier:
376            msgsrc = msgsrc_prev + "." + lvl[1]
377            if isinstance(lvl[0], str):  # Can be a function too, now, with tool system...
378                process_msg(msgs, km_i18n_context, lvl[0], msgsrc, reports, None, settings)
379            if lvl[3]:
380                walk_keymap_hierarchy(lvl[3], msgsrc)
381
382    # Dump Messages
383    operator_categories = {}
384
385    def process_cls_list(cls_list):
386        if not cls_list:
387            return
388
389        def full_class_id(cls):
390            """Gives us 'ID.Light.AreaLight' which is best for sorting."""
391            # Always the same issue, some classes listed in blacklist should actually no more exist (they have been
392            # unregistered), but are still listed by __subclasses__() calls... :/
393            if cls in blacklist_rna_class:
394                return cls.__name__
395            cls_id = ""
396            bl_rna = None
397            if hasattr(cls, 'bl_rna'):
398                bl_rna = cls.bl_rna
399            while bl_rna:
400                cls_id = bl_rna.identifier + "." + cls_id
401                bl_rna = bl_rna.base
402            return cls_id
403
404        def operator_category(cls):
405            """Extract operators' categories, as displayed in 'search' space menu."""
406            # NOTE: keep in sync with C code in ui_searchbox_region_draw_cb__operator().
407            if issubclass(cls, bpy.types.OperatorProperties) and "_OT_" in cls.__name__:
408                cat_id = cls.__name__.split("_OT_")[0]
409                if cat_id not in operator_categories:
410                    cat_str = cat_id.capitalize() + ":"
411                    operator_categories[cat_id] = cat_str
412
413        if verbose:
414            print(cls_list)
415        cls_list.sort(key=full_class_id)
416        for cls in cls_list:
417            if verbose:
418                print(cls)
419            reports["rna_structs"].append(cls)
420            # Ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)...
421            if (cls in blacklist_rna_class): #or issubclass(cls, bpy.types.Operator):
422                reports["rna_structs_skipped"].append(cls)
423            else:
424                operator_category(cls)
425                walk_class(cls)
426            # Recursively process subclasses.
427            process_cls_list(cls.__subclasses__())
428
429    # Parse everything (recursively parsing from bpy_struct "class"...).
430    process_cls_list(class_list)
431
432    # Finalize generated 'operator categories' messages.
433    for cat_str in operator_categories.values():
434        process_msg(msgs, bpy.app.translations.contexts.operator_default, cat_str, "Generated operator category",
435                    reports, check_ctxt_rna, settings)
436
437    # And parse keymaps!
438    from bl_keymap_utils import keymap_hierarchy
439    walk_keymap_hierarchy(keymap_hierarchy.generate(), "KM_HIERARCHY")

Dump into messages dict all RNA-defined UI messages (labels en tooltips).

def dump_py_messages_from_files(msgs, reports, files, settings):
443def dump_py_messages_from_files(msgs, reports, files, settings):
444    """
445    Dump text inlined in the python files given, e.g. 'My Name' in:
446        layout.prop("someprop", text="My Name")
447    """
448    import ast
449
450    bpy_struct = bpy.types.ID.__base__
451    i18n_contexts = bpy.app.translations.contexts
452
453    root_paths = tuple(bpy.utils.resource_path(t) for t in ('USER', 'LOCAL', 'SYSTEM'))
454
455    def make_rel(path):
456        for rp in root_paths:
457            if path.startswith(rp):
458                try:  # can't always find the relative path (between drive letters on windows)
459                    return os.path.relpath(path, rp)
460                except ValueError:
461                    return path
462        # Use binary's dir as fallback...
463        try:  # can't always find the relative path (between drive letters on windows)
464            return os.path.relpath(path, os.path.dirname(bpy.app.binary_path))
465        except ValueError:
466            return path
467
468    # Helper function
469    def extract_strings_ex(node, is_split=False):
470        """
471        Recursively get strings, needed in case we have "Blah" + "Blah", passed as an argument in that case it won't
472        evaluate to a string. However, break on some kind of stopper nodes, like e.g. Subscript.
473        """
474        if type(node) == ast.Str:
475            eval_str = ast.literal_eval(node)
476            if eval_str:
477                yield (is_split, eval_str, (node,))
478        else:
479            is_split = (type(node) in separate_nodes)
480            for nd in ast.iter_child_nodes(node):
481                if type(nd) not in stopper_nodes:
482                    yield from extract_strings_ex(nd, is_split=is_split)
483
484    def _extract_string_merge(estr_ls, nds_ls):
485        return "".join(s for s in estr_ls if s is not None), tuple(n for n in nds_ls if n is not None)
486
487    def extract_strings(node):
488        estr_ls = []
489        nds_ls = []
490        for is_split, estr, nds in extract_strings_ex(node):
491            estr_ls.append(estr)
492            nds_ls.extend(nds)
493        ret = _extract_string_merge(estr_ls, nds_ls)
494        return ret
495
496    def extract_strings_split(node):
497        """
498        Returns a list args as returned by 'extract_strings()', but split into groups based on separate_nodes, this way
499        expressions like ("A" if test else "B") won't be merged but "A" + "B" will.
500        """
501        estr_ls = []
502        nds_ls = []
503        bag = []
504        for is_split, estr, nds in extract_strings_ex(node):
505            if is_split:
506                bag.append((estr_ls, nds_ls))
507                estr_ls = []
508                nds_ls = []
509
510            estr_ls.append(estr)
511            nds_ls.extend(nds)
512
513        bag.append((estr_ls, nds_ls))
514
515        return [_extract_string_merge(estr_ls, nds_ls) for estr_ls, nds_ls in bag]
516
517    i18n_ctxt_ids = {v for v in bpy.app.translations.contexts_C_to_py.values()}
518
519    def _ctxt_to_ctxt(node):
520        # We must try, to some extend, to get contexts from vars instead of only literal strings...
521        ctxt = extract_strings(node)[0]
522        if ctxt:
523            return ctxt
524        # Basically, we search for attributes matching py context names, for now.
525        # So non-literal contexts should be used that way:
526        #     i18n_ctxt = bpy.app.translations.contexts
527        #     foobar(text="Foo", text_ctxt=i18n_ctxt.id_object)
528        if type(node) == ast.Attribute:
529            if node.attr in i18n_ctxt_ids:
530                #print(node, node.attr, getattr(i18n_contexts, node.attr))
531                return getattr(i18n_contexts, node.attr)
532        return i18n_contexts.default
533
534    def _op_to_ctxt(node):
535        # Some smart coders like things like:
536        #    >>> row.operator("preferences.addon_disable" if is_enabled else "preferences.addon_enable", ...)
537        # We only take first arg into account here!
538        bag = extract_strings_split(node)
539        opname, _ = bag[0]
540        if not opname:
541            return i18n_contexts.default
542        op = bpy.ops
543        for n in opname.split('.'):
544            op = getattr(op, n)
545        try:
546            return op.get_rna_type().translation_context
547        except Exception as e:
548            default_op_context = i18n_contexts.operator_default
549            print("ERROR: ", str(e))
550            print("       Assuming default operator context '{}'".format(default_op_context))
551            return default_op_context
552
553    # Gather function names.
554    # In addition of UI func, also parse pgettext ones...
555    # Tuples of (module name, (short names, ...)).
556    pgettext_variants = (
557        ("pgettext",       ("_"     , "f_"      , )),
558        ("pgettext_iface", ("iface_", "f_iface_", )),
559        ("pgettext_tip",   ("tip_"  , "f_tip_"  , )),
560        ("pgettext_data",  ("data_" , "f_data_" , )),
561    )
562    pgettext_variants_args = {"msgid": (0, {"msgctxt": 1})}
563
564    # key: msgid keywords.
565    # val: tuples of ((keywords,), context_getter_func) to get a context for that msgid.
566    #      Note: order is important, first one wins!
567    translate_kw = {
568        "text": (
569            (("text_ctxt",), _ctxt_to_ctxt),
570            (("operator",), _op_to_ctxt),
571        ),
572        "msgid": (
573            (("msgctxt",), _ctxt_to_ctxt),
574        ),
575        "message": (),
576        "report_message": (), # CM3D2 Converter : for report_cancel()
577    }
578
579    context_kw_set = {}
580    for k, ctxts in translate_kw.items():
581        s = set()
582        for c, _ in ctxts:
583            s |= set(c)
584        context_kw_set[k] = s
585
586    # {func_id: {msgid: (arg_pos,
587    #                    {msgctxt: arg_pos,
588    #                     ...
589    #                    }
590    #                   ),
591    #            ...
592    #           },
593    #  ...
594    # }
595    func_translate_args = {}
596
597    # First, functions from UILayout
598    # First loop is for msgid args, second one is for msgctxt args.
599    for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
600        # check it has one or more arguments as defined in translate_kw
601        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
602            if ((arg_kw in translate_kw) and (not arg.is_output) and (arg.type == 'STRING')):
603                func_translate_args.setdefault(func_id, {})[arg_kw] = (arg_pos, {})
604    for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
605        if func_id not in func_translate_args:
606            continue
607        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
608            if (not arg.is_output) and (arg.type == 'STRING'):
609                for msgid, msgctxts in context_kw_set.items():
610                    if arg_kw in msgctxts:
611                        func_translate_args[func_id][msgid][1][arg_kw] = arg_pos
612    # The report() func of operators.
613    for func_id, func in bpy.types.Operator.bl_rna.functions.items():
614        # check it has one or more arguments as defined in translate_kw
615        for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
616            if ((arg_kw in translate_kw) and (not arg.is_output) and (arg.type == 'STRING')):
617                func_translate_args.setdefault(func_id, {})[arg_kw] = (arg_pos, {})
618    # We manually add funcs from bpy.app.translations
619    for func_id, func_ids in pgettext_variants:
620        func_translate_args[func_id] = pgettext_variants_args
621        for func_id in func_ids:
622            func_translate_args[func_id] = pgettext_variants_args
623    
624    # CM3D2 Converter : manually add report_cancel()
625    # XXX Consider removing the use of report_cancel() entierly in model/anim import/export
626    func_translate_args.setdefault('report_cancel', {})['report_message'] = (1, {})
627
628    # Break recursive nodes look up on some kind of nodes.
629    # E.g. we don't want to get strings inside subscripts (blah["foo"])!
630    #      we don't want to get strings from comparisons (foo.type == 'BAR').
631    stopper_nodes = {ast.Subscript, ast.Compare}
632    # Consider strings separate: ("a" if test else "b")
633    separate_nodes = {ast.IfExp}
634    
635    check_ctxt_py = None
636    if reports["check_ctxt"]:
637        check_ctxt = reports["check_ctxt"]
638        check_ctxt_py = {
639            "py_in_rna": (check_ctxt.get("py_in_rna"), set(msgs.keys())),
640            "multi_lines": check_ctxt.get("multi_lines"),
641            "not_capitalized": check_ctxt.get("not_capitalized"),
642            "end_point": check_ctxt.get("end_point"),
643            "spell_checker": check_ctxt.get("spell_checker"),
644            "spell_errors": check_ctxt.get("spell_errors"),
645        }
646
647    for fp in files:
648        with open(fp, 'r', encoding="utf8") as filedata:
649            root_node = ast.parse(filedata.read(), fp, 'exec')
650
651        fp_rel = make_rel(fp)
652
653        for node in ast.walk(root_node):
654            if type(node) == ast.Call:
655                # print("found function at")
656                # print("%s:%d" % (fp, node.lineno))
657
658                # We can't skip such situations! from blah import foo\nfoo("bar") would also be an ast.Name func!
659                if type(node.func) == ast.Name:
660                    func_id = node.func.id
661                elif hasattr(node.func, "attr"):
662                    func_id = node.func.attr
663                # Ugly things like getattr(self, con.type)(context, box, con)
664                else:
665                    continue
666
667                func_args = func_translate_args.get(func_id, {})
668
669                # First try to get i18n contexts, for every possible msgid id.
670                msgctxts = dict.fromkeys(func_args.keys(), "")
671                for msgid, (_, context_args) in func_args.items():
672                    context_elements = {}
673                    for arg_kw, arg_pos in context_args.items():
674                        if arg_pos < len(node.args):
675                            context_elements[arg_kw] = node.args[arg_pos]
676                        else:
677                            for kw in node.keywords:
678                                if kw.arg == arg_kw:
679                                    context_elements[arg_kw] = kw.value
680                                    break
681                    # print(context_elements)
682                    for kws, proc in translate_kw[msgid]:
683                        if set(kws) <= context_elements.keys():
684                            args = tuple(context_elements[k] for k in kws)
685                            #print("running ", proc, " with ", args)
686                            ctxt = proc(*args)
687                            if ctxt:
688                                msgctxts[msgid] = ctxt
689                                break
690
691                # print(translate_args)
692                # do nothing if not found
693                for arg_kw, (arg_pos, _) in func_args.items():
694                    msgctxt = msgctxts[arg_kw]
695                    estr_lst = [(None, ())]
696                    if arg_pos < len(node.args):
697                        estr_lst = extract_strings_split(node.args[arg_pos])
698                        #print(estr, nds)
699                    else:
700                        for kw in node.keywords:
701                            if kw.arg == arg_kw:
702                                estr_lst = extract_strings_split(kw.value)
703                                break
704                        #print(estr, nds)
705                    for estr, nds in estr_lst:
706                        if estr:
707                            if nds:
708                                msgsrc = "{}:{}".format(fp_rel, sorted({nd.lineno for nd in nds})[0])
709                            else:
710                                msgsrc = "{}:???".format(fp_rel)
711                            process_msg(msgs, msgctxt, estr, msgsrc, reports, check_ctxt_py, settings)
712                            reports["py_messages"].append((msgctxt, estr, msgsrc))

Dump text inlined in the python files given, e.g. 'My Name' in: layout.prop("someprop", text="My Name")

def dump_py_messages(msgs, reports, addons, settings, addons_only=False):
715def dump_py_messages(msgs, reports, addons, settings, addons_only=False):
716    def _get_files(path):
717        if not os.path.exists(path):
718            return []
719        if os.path.isdir(path):
720            return [os.path.join(dpath, fn) for dpath, _, fnames in os.walk(path) for fn in fnames
721                    if not fn.startswith("_") and fn.endswith(".py")]
722        return [path]
723
724    files = []
725    if not addons_only:
726        for path in settings.CUSTOM_PY_UI_FILES:
727            for root in (bpy.utils.resource_path(t) for t in ('USER', 'LOCAL', 'SYSTEM')):
728                files += _get_files(os.path.join(root, path))
729
730    # Add all given addons.
731    for mod in addons:
732        fn = mod.__file__
733        if os.path.basename(fn) == "__init__.py":
734            files += _get_files(os.path.dirname(fn))
735        else:
736            files.append(fn)
737
738    dump_py_messages_from_files(msgs, reports, sorted(files), settings)
def dump_src_messages(msgs, reports, settings):
742def dump_src_messages(msgs, reports, settings):
743    def get_contexts():
744        """Return a mapping {C_CTXT_NAME: ctxt_value}."""
745        return {k: getattr(bpy.app.translations.contexts, n) for k, n in bpy.app.translations.contexts_C_to_py.items()}
746
747    contexts = get_contexts()
748
749    # Build regexes to extract messages (with optional contexts) from C source.
750    pygettexts = tuple(re.compile(r).search for r in settings.PYGETTEXT_KEYWORDS)
751
752    _clean_str = re.compile(settings.str_clean_re).finditer
753
754    def clean_str(s):
755        return "".join(m.group("clean") for m in _clean_str(s))
756
757    def dump_src_file(path, rel_path, msgs, reports, settings):
758        def process_entry(_msgctxt, _msgid):
759            # Context.
760            msgctxt = settings.DEFAULT_CONTEXT
761            if _msgctxt:
762                if _msgctxt in contexts:
763                    msgctxt = contexts[_msgctxt]
764                elif '"' in _msgctxt or "'" in _msgctxt:
765                    msgctxt = clean_str(_msgctxt)
766                else:
767                    print("WARNING: raw context “{}” couldn’t be resolved!".format(_msgctxt))
768            # Message.
769            msgid = ""
770            if _msgid:
771                if '"' in _msgid or "'" in _msgid:
772                    msgid = clean_str(_msgid)
773                else:
774                    print("WARNING: raw message “{}” couldn’t be resolved!".format(_msgid))
775            return msgctxt, msgid
776
777        check_ctxt_src = None
778        if reports["check_ctxt"]:
779            check_ctxt = reports["check_ctxt"]
780            check_ctxt_src = {
781                "multi_lines": check_ctxt.get("multi_lines"),
782                "not_capitalized": check_ctxt.get("not_capitalized"),
783                "end_point": check_ctxt.get("end_point"),
784                "spell_checker": check_ctxt.get("spell_checker"),
785                "spell_errors": check_ctxt.get("spell_errors"),
786            }
787
788        data = ""
789        with open(path) as f:
790            data = f.read()
791        for srch in pygettexts:
792            m = srch(data)
793            line = pos = 0
794            while m:
795                d = m.groupdict()
796                # Line.
797                line += data[pos:m.start()].count('\n')
798                msgsrc = rel_path + ":" + str(line)
799                _msgid = d.get("msg_raw")
800                # First, try the "multi-contexts" stuff!
801                _msgctxts = tuple(d.get("ctxt_raw{}".format(i)) for i in range(settings.PYGETTEXT_MAX_MULTI_CTXT))
802                if _msgctxts[0]:
803                    for _msgctxt in _msgctxts:
804                        if not _msgctxt:
805                            break
806                        msgctxt, msgid = process_entry(_msgctxt, _msgid)
807                        process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
808                        reports["src_messages"].append((msgctxt, msgid, msgsrc))
809                else:
810                    _msgctxt = d.get("ctxt_raw")
811                    msgctxt, msgid = process_entry(_msgctxt, _msgid)
812                    process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
813                    reports["src_messages"].append((msgctxt, msgid, msgsrc))
814
815                pos = m.end()
816                line += data[m.start():pos].count('\n')
817                m = srch(data, pos)
818
819    forbidden = set()
820    forced = set()
821    if os.path.isfile(settings.SRC_POTFILES):
822        with open(settings.SRC_POTFILES) as src:
823            for l in src:
824                if l[0] == '-':
825                    forbidden.add(l[1:].rstrip('\n'))
826                elif l[0] != '#':
827                    forced.add(l.rstrip('\n'))
828    for root, dirs, files in os.walk(settings.POTFILES_SOURCE_DIR):
829        if "/.svn" in root:
830            continue
831        for fname in files:
832            if os.path.splitext(fname)[1] not in settings.PYGETTEXT_ALLOWED_EXTS:
833                continue
834            path = os.path.join(root, fname)
835            try:  # can't always find the relative path (between drive letters on windows)
836                rel_path = os.path.relpath(path, settings.SOURCE_DIR)
837            except ValueError:
838                rel_path = path
839            if rel_path in forbidden:
840                continue
841            elif rel_path not in forced:
842                forced.add(rel_path)
843    for rel_path in sorted(forced):
844        path = os.path.join(settings.SOURCE_DIR, rel_path)
845        if os.path.exists(path):
846            dump_src_file(path, rel_path, msgs, reports, settings)
def dump_messages(do_messages, do_checks, settings):
850def dump_messages(do_messages, do_checks, settings):
851    bl_ver = "Blender " + bpy.app.version_string
852    bl_hash = bpy.app.build_hash
853    bl_date = datetime.datetime.strptime(bpy.app.build_date.decode() + "T" + bpy.app.build_time.decode(),
854                                         "%Y-%m-%dT%H:%M:%S")
855    pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, bl_ver, bl_hash, bl_date, bl_date.year,
856                                                settings=settings)
857    msgs = pot.msgs
858
859    # Enable all wanted addons.
860    # For now, enable all official addons, before extracting msgids.
861    addons = utils.enable_addons(support={"OFFICIAL"})
862    # Note this is not needed if we have been started with factory settings, but just in case...
863    # XXX This is not working well, spent a whole day trying to understand *why* we still have references of
864    #     those removed calsses in things like `bpy.types.OperatorProperties.__subclasses__()`
865    #     (could not even reproduce it from regular py console in Blender with UI...).
866    #     For some reasons, cleanup does not happen properly, *and* we have no way to tell which class is valid
867    #     and which has been unregistered. So for now, just go for the dirty, easy way: do not disable add-ons. :(
868    # ~ utils.enable_addons(support={"COMMUNITY", "TESTING"}, disable=True)
869
870    reports = _gen_reports(_gen_check_ctxt(settings) if do_checks else None)
871
872    # Get strings from RNA.
873    dump_rna_messages(msgs, reports, settings)
874
875    # Get strings from UI layout definitions text="..." args.
876    dump_py_messages(msgs, reports, addons, settings)
877
878    # Get strings from C source code.
879    dump_src_messages(msgs, reports, settings)
880
881    # Get strings from addons' categories.
882    for uid, label, tip in bpy.types.WindowManager.addon_filter[1]['items'](bpy.context.window_manager, bpy.context):
883        process_msg(msgs, settings.DEFAULT_CONTEXT, label, "Add-ons' categories", reports, None, settings)
884        if tip:
885            process_msg(msgs, settings.DEFAULT_CONTEXT, tip, "Add-ons' categories", reports, None, settings)
886
887    # Get strings specific to translations' menu.
888    for lng in settings.LANGUAGES:
889        process_msg(msgs, settings.DEFAULT_CONTEXT, lng[1], "Languages’ labels from bl_i18n_utils/settings.py",
890                    reports, None, settings)
891    for cat in settings.LANGUAGES_CATEGORIES:
892        process_msg(msgs, settings.DEFAULT_CONTEXT, cat[1],
893                    "Language categories’ labels from bl_i18n_utils/settings.py", reports, None, settings)
894
895    # pot.check()
896    pot.unescape()  # Strings gathered in py/C source code may contain escaped chars...
897    print_info(reports, pot)
898    # pot.check()
899
900    if do_messages:
901        print("Writing messages…")
902        pot.write('PO', settings.FILE_NAME_POT)
903
904    print("Finished extracting UI messages!")
905
906    return pot  # Not used currently, but may be useful later (and to be consistent with dump_addon_messages!).
def dump_addon_messages(module_name, do_checks, settings):
909def dump_addon_messages(module_name, do_checks, settings):
910    import addon_utils
911
912    # Get current addon state (loaded or not):
913    was_loaded = addon_utils.check(module_name)[1]
914
915    # Enable our addon.
916    addon = utils.enable_addons(addons={module_name})[0]
917
918    addon_info = addon_utils.module_bl_info(addon)
919    ver = addon_info["name"] + " " + ".".join(str(v) for v in addon_info["version"])
920    rev = 0
921    date = datetime.datetime.now()
922    pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year,
923                                                settings=settings)
924    msgs = pot.msgs
925
926    minus_pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year,
927                                                      settings=settings)
928    minus_msgs = minus_pot.msgs
929
930    check_ctxt = _gen_check_ctxt(settings) if do_checks else None
931    minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None
932
933    # Get strings from RNA, our addon being enabled.
934    print("A")
935    reports = _gen_reports(check_ctxt)
936    print("B")
937    dump_rna_messages(msgs, reports, settings)
938    print("C")
939
940    # Now disable our addon, and rescan RNA.
941    utils.enable_addons(addons={module_name}, disable=True)
942    print("D")
943    reports["check_ctxt"] = minus_check_ctxt
944    print("E")
945    dump_rna_messages(minus_msgs, reports, settings)
946    print("F")
947
948    # Restore previous state if needed!
949    if was_loaded:
950        utils.enable_addons(addons={module_name})
951
952    # and make the diff!
953    for key in minus_msgs:
954        if key != settings.PO_HEADER_KEY:
955            if key in msgs.keys():
956                del msgs[key]
957
958    if check_ctxt:
959        _diff_check_ctxt(check_ctxt, minus_check_ctxt)
960
961    # and we are done with those!
962    del minus_pot
963    del minus_msgs
964    del minus_check_ctxt
965
966    # get strings from UI layout definitions text="..." args
967    reports["check_ctxt"] = check_ctxt
968    dump_py_messages(msgs, reports, {addon}, settings, addons_only=True)
969
970    pot.unescape()  # Strings gathered in py/C source code may contain escaped chars...
971    print_info(reports, pot)
972
973    print("Finished extracting UI messages!")
974
975    return pot
def main():
 978def main():
 979    try:
 980        import bpy
 981    except ImportError:
 982        print("This script must run from inside blender")
 983        return
 984
 985    import sys
 986    import argparse
 987
 988    # Get rid of Blender args!
 989    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
 990
 991    parser = argparse.ArgumentParser(description="Process UI messages from inside Blender.")
 992    parser.add_argument('-c', '--no_checks', default=True, action="store_false", help="No checks over UI messages.")
 993    parser.add_argument('-m', '--no_messages', default=True, action="store_false", help="No export of UI messages.")
 994    parser.add_argument('-o', '--output', default=None, help="Output POT file path.")
 995    parser.add_argument('-s', '--settings', default=None,
 996                        help="Override (some) default settings. Either a JSon file name, or a JSon string.")
 997    args = parser.parse_args(argv)
 998
 999    settings = settings_i18n.I18nSettings()
1000    settings.load(args.settings)
1001
1002    if args.output:
1003        settings.FILE_NAME_POT = args.output
1004
1005    dump_messages(do_messages=args.no_messages, do_checks=args.no_checks, settings=settings)