diff options
Diffstat (limited to 'release/scripts/modules/i18n/bl_process_msg.py')
-rw-r--r-- | release/scripts/modules/i18n/bl_process_msg.py | 544 |
1 files changed, 544 insertions, 0 deletions
diff --git a/release/scripts/modules/i18n/bl_process_msg.py b/release/scripts/modules/i18n/bl_process_msg.py new file mode 100644 index 00000000000..0a28a6768e6 --- /dev/null +++ b/release/scripts/modules/i18n/bl_process_msg.py @@ -0,0 +1,544 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENSE BLOCK ***** + +# <pep8-80 compliant> + +# Write out messages.txt from Blender. +# XXX: This script is meant to be used from inside Blender! +# You should not directly use this script, rather use update_msg.py! + +import os + +# Quite an ugly hack… But the simplest solution for now! +#import sys +#sys.path.append(os.path.abspath(os.path.dirname(__file__))) +import i18n.settings as settings + + +#classes = set() + + +SOURCE_DIR = settings.SOURCE_DIR + +CUSTOM_PY_UI_FILES = [os.path.abspath(os.path.join(SOURCE_DIR, p)) + for p in settings.CUSTOM_PY_UI_FILES] +FILE_NAME_MESSAGES = settings.FILE_NAME_MESSAGES +COMMENT_PREFIX = settings.COMMENT_PREFIX +CONTEXT_PREFIX = settings.CONTEXT_PREFIX +CONTEXT_DEFAULT = settings.CONTEXT_DEFAULT +UNDOC_OPS_STR = settings.UNDOC_OPS_STR + +NC_ALLOWED = settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED + +def check(check_ctxt, messages, key, msgsrc): + if check_ctxt is None: + return + multi_rnatip = check_ctxt.get("multi_rnatip") + multi_lines = check_ctxt.get("multi_lines") + py_in_rna = check_ctxt.get("py_in_rna") + not_capitalized = check_ctxt.get("not_capitalized") + end_point = check_ctxt.get("end_point") + undoc_ops = check_ctxt.get("undoc_ops") + + if multi_rnatip is not None: + if key in messages and key not in multi_rnatip: + multi_rnatip.add(key) + if multi_lines is not None: + if '\n' in key[1]: + multi_lines.add(key) + if py_in_rna is not None: + if key in py_in_rna[1]: + py_in_rna[0].add(key) + if not_capitalized is not None: + if(key[1] not in NC_ALLOWED and key[1][0].isalpha() and + not key[1][0].isupper()): + not_capitalized.add(key) + if end_point is not None: + if key[1].strip().endswith('.'): + end_point.add(key) + if undoc_ops is not None: + if key[1] == UNDOC_OPS_STR: + undoc_ops.add(key) + + +def dump_messages_rna(messages, check_ctxt): + import bpy + + def classBlackList(): + blacklist_rna_class = [# core classes + "Context", "Event", "Function", "UILayout", + "BlendData", + # registerable classes + "Panel", "Menu", "Header", "RenderEngine", + "Operator", "OperatorMacro", "Macro", + "KeyingSetInfo", "UnknownType", + # window classes + "Window", + ] + + # --------------------------------------------------------------------- + # Collect internal operators + + # extend with all internal operators + # note that this uses internal api introspection functions + # all possible operator names + op_ids = set(cls.bl_rna.identifier for cls in + bpy.types.OperatorProperties.__subclasses__()) | \ + set(cls.bl_rna.identifier for cls in + bpy.types.Operator.__subclasses__()) | \ + set(cls.bl_rna.identifier for cls in + bpy.types.OperatorMacro.__subclasses__()) + + get_instance = __import__("_bpy").ops.get_instance + path_resolve = type(bpy.context).__base__.path_resolve + for idname in op_ids: + op = get_instance(idname) + if 'INTERNAL' in path_resolve(op, "bl_options"): + blacklist_rna_class.append(idname) + + # --------------------------------------------------------------------- + # Collect builtin classes we don't need to doc + blacklist_rna_class.append("Property") + blacklist_rna_class.extend( + [cls.__name__ for cls in + bpy.types.Property.__subclasses__()]) + + # --------------------------------------------------------------------- + # Collect classes which are attached to collections, these are api + # access only. + collection_props = set() + for cls_id in dir(bpy.types): + cls = getattr(bpy.types, cls_id) + for prop in cls.bl_rna.properties: + if prop.type == 'COLLECTION': + prop_cls = prop.srna + if prop_cls is not None: + collection_props.add(prop_cls.identifier) + blacklist_rna_class.extend(sorted(collection_props)) + + return blacklist_rna_class + + blacklist_rna_class = classBlackList() + + def filterRNA(bl_rna): + rid = bl_rna.identifier + if rid in blacklist_rna_class: + print(" skipping", rid) + return True + return False + + check_ctxt_rna = check_ctxt_rna_tip = None + if check_ctxt: + check_ctxt_rna = {"multi_lines": check_ctxt.get("multi_lines"), + "not_capitalized": check_ctxt.get("not_capitalized"), + "end_point": check_ctxt.get("end_point"), + "undoc_ops": check_ctxt.get("undoc_ops")} + check_ctxt_rna_tip = check_ctxt_rna + check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip") + + # ------------------------------------------------------------------------- + # Function definitions + + def walkProperties(bl_rna): + import bpy + + # Get our parents' properties, to not export them multiple times. + bl_rna_base = bl_rna.base + if bl_rna_base: + bl_rna_base_props = bl_rna_base.properties.values() + else: + bl_rna_base_props = () + + for prop in bl_rna.properties: + # Only write this property if our parent hasn't got it. + if prop in bl_rna_base_props: + continue + if prop.identifier == "rna_type": + continue + + msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier) + context = getattr(prop, "translation_context", CONTEXT_DEFAULT) + if prop.name and (prop.name != prop.identifier or context): + key = (context, prop.name) + check(check_ctxt_rna, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + if prop.description: + key = (CONTEXT_DEFAULT, prop.description) + check(check_ctxt_rna_tip, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + if isinstance(prop, bpy.types.EnumProperty): + for item in prop.enum_items: + msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, + prop.identifier, + item.identifier) + if item.name and item.name != item.identifier: + key = (CONTEXT_DEFAULT, item.name) + check(check_ctxt_rna, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + if item.description: + key = (CONTEXT_DEFAULT, item.description) + check(check_ctxt_rna_tip, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + + def walkRNA(bl_rna): + if filterRNA(bl_rna): + return + + msgsrc = ".".join(("bpy.types", bl_rna.identifier)) + context = getattr(bl_rna, "translation_context", CONTEXT_DEFAULT) + + if bl_rna.name and (bl_rna.name != bl_rna.identifier or context): + key = (context, bl_rna.name) + check(check_ctxt_rna, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + + if bl_rna.description: + key = (CONTEXT_DEFAULT, bl_rna.description) + check(check_ctxt_rna_tip, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + + if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label: + key = (context, bl_rna.bl_label) + check(check_ctxt_rna, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + + walkProperties(bl_rna) + + def walkClass(cls): + walkRNA(cls.bl_rna) + + def walk_keymap_hierarchy(hier, msgsrc_prev): + for lvl in hier: + msgsrc = "{}.{}".format(msgsrc_prev, lvl[1]) + messages.setdefault((CONTEXT_DEFAULT, lvl[0]), []).append(msgsrc) + + if lvl[3]: + walk_keymap_hierarchy(lvl[3], msgsrc) + + # ------------------------------------------------------------------------- + # Dump Messages + + def process_cls_list(cls_list): + if not cls_list: + return 0 + + def full_class_id(cls): + """ gives us 'ID.Lamp.AreaLamp' which is best for sorting. + """ + cls_id = "" + bl_rna = cls.bl_rna + while bl_rna: + cls_id = "{}.{}".format(bl_rna.identifier, cls_id) + bl_rna = bl_rna.base + return cls_id + + cls_list.sort(key=full_class_id) + processed = 0 + for cls in cls_list: + walkClass(cls) +# classes.add(cls) + # Recursively process subclasses. + processed += process_cls_list(cls.__subclasses__()) + 1 + return processed + + # Parse everything (recursively parsing from bpy_struct "class"...). + processed = process_cls_list(type(bpy.context).__base__.__subclasses__()) + print("{} classes processed!".format(processed)) +# import pickle +# global classes +# classes = {str(c) for c in classes} +# with open("/home/i7deb64/Bureau/tpck_2", "wb") as f: +# pickle.dump(classes, f, protocol=0) + + from bpy_extras.keyconfig_utils import KM_HIERARCHY + + walk_keymap_hierarchy(KM_HIERARCHY, "KM_HIERARCHY") + + + +def dump_messages_pytext(messages, check_ctxt): + """ dumps text inlined in the python user interface: eg. + + layout.prop("someprop", text="My Name") + """ + import ast + + # ------------------------------------------------------------------------- + # Gather function names + + import bpy + # key: func_id + # val: [(arg_kw, arg_pos), (arg_kw, arg_pos), ...] + func_translate_args = {} + + # so far only 'text' keywords, but we may want others translated later + translate_kw = ("text", ) + + # Break recursive nodes look up on some kind of nodes. + # E.g. we don’t want to get strings inside subscripts (blah["foo"])! + stopper_nodes = {ast.Subscript,} + + for func_id, func in bpy.types.UILayout.bl_rna.functions.items(): + # check it has a 'text' argument + for (arg_pos, (arg_kw, arg)) in enumerate(func.parameters.items()): + if ((arg_kw in translate_kw) and + (arg.is_output == False) and + (arg.type == 'STRING')): + + func_translate_args.setdefault(func_id, []).append((arg_kw, + arg_pos)) + # print(func_translate_args) + + check_ctxt_py = None + if check_ctxt: + check_ctxt_py = {"py_in_rna": (check_ctxt["py_in_rna"], messages.copy()), + "multi_lines": check_ctxt["multi_lines"], + "not_capitalized": check_ctxt["not_capitalized"], + "end_point": check_ctxt["end_point"]} + + # ------------------------------------------------------------------------- + # Function definitions + + def extract_strings(fp_rel, node): + """ Recursively get strings, needed in case we have "Blah" + "Blah", + passed as an argument in that case it wont evaluate to a string. + However, break on some kind of stopper nodes, like e.g. Subscript. + """ + + if type(node) == ast.Str: + eval_str = ast.literal_eval(node) + if eval_str: + key = (CONTEXT_DEFAULT, eval_str) + msgsrc = "{}:{}".format(fp_rel, node.lineno) + check(check_ctxt_py, messages, key, msgsrc) + messages.setdefault(key, []).append(msgsrc) + return + + for nd in ast.iter_child_nodes(node): + if type(nd) not in stopper_nodes: + extract_strings(fp_rel, nd) + + def extract_strings_from_file(fp): + filedata = open(fp, 'r', encoding="utf8") + root_node = ast.parse(filedata.read(), fp, 'exec') + filedata.close() + + fp_rel = os.path.relpath(fp, SOURCE_DIR) + + for node in ast.walk(root_node): + if type(node) == ast.Call: + # print("found function at") + # print("%s:%d" % (fp, node.lineno)) + + # lambda's + if type(node.func) == ast.Name: + continue + + # getattr(self, con.type)(context, box, con) + if not hasattr(node.func, "attr"): + continue + + translate_args = func_translate_args.get(node.func.attr, ()) + + # do nothing if not found + for arg_kw, arg_pos in translate_args: + if arg_pos < len(node.args): + extract_strings(fp_rel, node.args[arg_pos]) + else: + for kw in node.keywords: + if kw.arg == arg_kw: + extract_strings(fp_rel, kw.value) + + # ------------------------------------------------------------------------- + # Dump Messages + + mod_dir = os.path.join(SOURCE_DIR, + "release", + "scripts", + "startup", + "bl_ui") + + files = [os.path.join(mod_dir, fn) + for fn in sorted(os.listdir(mod_dir)) + if not fn.startswith("_") + if fn.endswith("py") + ] + + # Dummy Cycles has its py addon in its own dir! + files += CUSTOM_PY_UI_FILES + + for fp in files: + extract_strings_from_file(fp) + + +def dump_messages(do_messages, do_checks): + import collections + + def enable_addons(): + """For now, enable all official addons, before extracting msgids.""" + import addon_utils + import bpy + + userpref = bpy.context.user_preferences + used_ext = {ext.module for ext in userpref.addons} + support = {"OFFICIAL"} + # collect the categories that can be filtered on + addons = [(mod, addon_utils.module_bl_info(mod)) for mod in + addon_utils.modules(addon_utils.addons_fake_modules)] + + for mod, info in addons: + module_name = mod.__name__ + if module_name in used_ext or info["support"] not in support: + continue + print(" Enabling module ", module_name) + bpy.ops.wm.addon_enable(module=module_name) + + # XXX There are currently some problems with bpy/rna... + # *Very* tricky to solve! + # So this is a hack to make all newly added operator visible by + # bpy.types.OperatorProperties.__subclasses__() + for cat in dir(bpy.ops): + cat = getattr(bpy.ops, cat) + for op in dir(cat): + getattr(cat, op).get_rna() + + # check for strings like ": %d" + ignore = ("%d", "%f", "%s", "%r", # string formatting + "*", ".", "(", ")", "-", "/", "\\", "+", ":", "#", "%" + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "x", # used on its own eg: 100x200 + "X", "Y", "Z", "W", # used alone. no need to include + ) + + def filter_message(msg): + msg_tmp = msg + for ign in ignore: + msg_tmp = msg_tmp.replace(ign, "") + if not msg_tmp.strip(): + return True + # we could filter out different strings here + return False + + if hasattr(collections, 'OrderedDict'): + messages = collections.OrderedDict() + else: + messages = {} + + messages[(CONTEXT_DEFAULT, "")] = [] + + # Enable all wanted addons. + enable_addons() + + check_ctxt = None + if do_checks: + check_ctxt = {"multi_rnatip": set(), + "multi_lines": set(), + "py_in_rna": set(), + "not_capitalized": set(), + "end_point": set(), + "undoc_ops": set()} + + # get strings from RNA + dump_messages_rna(messages, check_ctxt) + + # get strings from UI layout definitions text="..." args + dump_messages_pytext(messages, check_ctxt) + + del messages[(CONTEXT_DEFAULT, "")] + + if do_checks: + print("WARNINGS:") + keys = set() + for c in check_ctxt.values(): + keys |= c + for key in keys: + if key in check_ctxt["undoc_ops"]: + print("\tThe following operators are undocumented:") + else: + print("\t“{}”|“{}”:".format(*key)) + if key in check_ctxt["multi_lines"]: + print("\t\t-> newline in this message!") + if key in check_ctxt["not_capitalized"]: + print("\t\t-> message not capitalized!") + if key in check_ctxt["end_point"]: + print("\t\t-> message with endpoint!") + # XXX Hide this one for now, too much false positives. +# if key in check_ctxt["multi_rnatip"]: +# print("\t\t-> tip used in several RNA items") + if key in check_ctxt["py_in_rna"]: + print("\t\t-> RNA message also used in py UI code:") + print("\t\t{}".format("\n\t\t".join(messages[key]))) + + if do_messages: + print("Writing messages…") + num_written = 0 + num_filtered = 0 + with open(FILE_NAME_MESSAGES, 'w', encoding="utf8") as message_file: + for (ctx, key), value in messages.items(): + # filter out junk values + if filter_message(key): + num_filtered += 1 + continue + + # Remove newlines in key and values! + message_file.write("\n".join(COMMENT_PREFIX + msgsrc.replace("\n", "") for msgsrc in value)) + message_file.write("\n") + if ctx: + message_file.write(CONTEXT_PREFIX + ctx.replace("\n", "") + "\n") + message_file.write(key.replace("\n", "") + "\n") + num_written += 1 + + print("Written {} messages to: {} ({} were filtered out)." \ + "".format(num_written, FILE_NAME_MESSAGES, num_filtered)) + + +def main(): + try: + import bpy + except ImportError: + print("This script must run from inside blender") + return + + import sys + back_argv = sys.argv + sys.argv = sys.argv[sys.argv.index("--") + 1:] + + import argparse + parser = argparse.ArgumentParser(description="Process UI messages " \ + "from inside Blender.") + parser.add_argument('-c', '--no_checks', default=True, + action="store_false", + help="No checks over UI messages.") + parser.add_argument('-m', '--no_messages', default=True, + action="store_false", + help="No export of UI messages.") + parser.add_argument('-o', '--output', help="Output messages file path.") + args = parser.parse_args() + + if args.output: + global FILE_NAME_MESSAGES + FILE_NAME_MESSAGES = args.output + + dump_messages(do_messages=args.no_messages, do_checks=args.no_checks) + + sys.argv = back_argv + + +if __name__ == "__main__": + print("\n\n *** Running {} *** \n".format(__file__)) + main() |