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'):
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
print_info(reports, pot):
139def print_info(reports, pot): 140 def _print(*args, **kwargs): 141 kwargs["file"] = sys.stderr 142 print(*args, **kwargs) 143 144 pot.update_info() 145 146 _print("{} RNA structs were processed (among which {} were skipped), containing {} RNA properties " 147 "(among which {} were skipped).".format(len(reports["rna_structs"]), len(reports["rna_structs_skipped"]), 148 len(reports["rna_props"]), len(reports["rna_props_skipped"]))) 149 _print("{} messages were extracted from Python UI code (among which {} were skipped), and {} from C source code " 150 "(among which {} were skipped).".format(len(reports["py_messages"]), len(reports["py_messages_skipped"]), 151 len(reports["src_messages"]), len(reports["src_messages_skipped"]))) 152 _print("{} messages were rejected.".format(len(reports["messages_skipped"]))) 153 _print("\n") 154 _print("Current POT stats:") 155 pot.print_info(prefix="\t", output=_print) 156 _print("\n") 157 158 check_ctxt = reports["check_ctxt"] 159 if check_ctxt is None: 160 return 161 multi_rnatip = check_ctxt.get("multi_rnatip") 162 multi_lines = check_ctxt.get("multi_lines") 163 py_in_rna = check_ctxt.get("py_in_rna") 164 not_capitalized = check_ctxt.get("not_capitalized") 165 end_point = check_ctxt.get("end_point") 166 undoc_ops = check_ctxt.get("undoc_ops") 167 spell_errors = check_ctxt.get("spell_errors") 168 169 # XXX Temp, no multi_rnatip nor py_in_rna, see below. 170 keys = multi_lines | not_capitalized | end_point | undoc_ops | spell_errors.keys() 171 if keys: 172 _print("WARNINGS:") 173 for key in keys: 174 if undoc_ops and key in undoc_ops: 175 _print("\tThe following operators are undocumented!") 176 else: 177 _print("\t“{}”|“{}”:".format(*key)) 178 # We support multi-lines tooltips now... 179 # ~ if multi_lines and key in multi_lines: 180 # ~ _print("\t\t-> newline in this message!") 181 if not_capitalized and key in not_capitalized: 182 _print("\t\t-> message not capitalized!") 183 if end_point and key in end_point: 184 _print("\t\t-> message with endpoint!") 185 # XXX Hide this one for now, too much false positives. 186 #if multi_rnatip and key in multi_rnatip: 187 # _print("\t\t-> tip used in several RNA items") 188 #if py_in_rna and key in py_in_rna: 189 # _print("\t\t-> RNA message also used in py UI code!") 190 if spell_errors and spell_errors.get(key): 191 lines = [ 192 "\t\t-> {}: misspelled, suggestions are ({})".format(w, "'" + "', '".join(errs) + "'") 193 for w, errs in spell_errors[key] 194 ] 195 _print("\n".join(lines)) 196 _print("\t\t{}".format("\n\t\t".join(pot.msgs[key].sources)))
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)