2 # ***** BEGIN GPL LICENSE BLOCK *****
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # ***** END GPL LICENSE BLOCK *****
22 # Write out messages.txt from blender
25 # blender --background --python po/update_msg.py
29 CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
30 SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(CURRENT_DIR, "..")))
32 FILE_NAME_MESSAGES = os.path.join(CURRENT_DIR, "messages.txt")
33 COMMENT_PREFIX = "#~ "
36 def dump_messages_rna(messages):
40 blacklist_rna_class = [
42 "Context", "Event", "Function", "UILayout",
44 # registerable classes
45 "Panel", "Menu", "Header", "RenderEngine",
46 "Operator", "OperatorMacro", "Macro",
47 "KeyingSetInfo", "UnknownType",
49 "WindowManager", "Window"
52 # ---------------------------------------------------------------------
53 # Collect internal operators
55 # extend with all internal operators
56 # note that this uses internal api introspection functions
57 # all possible operator names
58 op_names = list(sorted(set(
59 [cls.bl_rna.identifier for cls in
60 bpy.types.OperatorProperties.__subclasses__()] +
61 [cls.bl_rna.identifier for cls in
62 bpy.types.Operator.__subclasses__()] +
63 [cls.bl_rna.identifier for cls in
64 bpy.types.OperatorMacro.__subclasses__()]
67 get_inatance = __import__("_bpy").ops.get_instance
68 path_resolve = type(bpy.context).__base__.path_resolve
69 for idname in op_names:
70 op = get_inatance(idname)
71 if 'INTERNAL' in path_resolve(op, "bl_options"):
72 blacklist_rna_class.append(idname)
74 # ---------------------------------------------------------------------
75 # Collect builtin classes we dont need to doc
76 blacklist_rna_class.append("Property")
77 blacklist_rna_class.extend(
78 [cls.__name__ for cls in
79 bpy.types.Property.__subclasses__()])
81 # ---------------------------------------------------------------------
82 # Collect classes which are attached to collections, these are api
84 collection_props = set()
85 for cls_id in dir(bpy.types):
86 cls = getattr(bpy.types, cls_id)
87 for prop in cls.bl_rna.properties:
88 if prop.type == 'COLLECTION':
90 if prop_cls is not None:
91 collection_props.add(prop_cls.identifier)
92 blacklist_rna_class.extend(sorted(collection_props))
94 return blacklist_rna_class
96 blacklist_rna_class = classBlackList()
98 def filterRNA(bl_rna):
99 id = bl_rna.identifier
100 if id in blacklist_rna_class:
101 print(" skipping", id)
105 # -------------------------------------------------------------------------
106 # Function definitions
108 def walkProperties(bl_rna):
111 # get our parents properties not to export them multiple times
112 bl_rna_base = bl_rna.base
114 bl_rna_base_props = bl_rna_base.properties.values()
116 bl_rna_base_props = ()
118 for prop in bl_rna.properties:
119 # only write this property is our parent hasn't got it.
120 if prop in bl_rna_base_props:
122 if prop.identifier == "rna_type":
125 msgsrc = "bpy.types.%s.%s" % (bl_rna.identifier, prop.identifier)
126 if prop.name and prop.name != prop.identifier:
127 messages.setdefault(prop.name, []).append(msgsrc)
129 messages.setdefault(prop.description, []).append(msgsrc)
131 if isinstance(prop, bpy.types.EnumProperty):
132 for item in prop.enum_items:
133 msgsrc = "bpy.types.%s.%s, '%s'" % (bl_rna.identifier,
137 # Here identifier and name can be the same!
138 if item.name: # and item.name != item.identifier:
139 messages.setdefault(item.name, []).append(msgsrc)
141 messages.setdefault(item.description, []).append(msgsrc)
145 if filterRNA(bl_rna):
148 msgsrc = "bpy.types.%s" % bl_rna.identifier
150 if bl_rna.name and bl_rna.name != bl_rna.identifier:
151 messages.setdefault(bl_rna.name, []).append(msgsrc)
153 if bl_rna.description:
154 messages.setdefault(bl_rna.description, []).append(msgsrc)
156 if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label:
157 messages.setdefault(bl_rna.bl_label, []).append(msgsrc)
159 walkProperties(bl_rna)
164 def walk_keymap_hierarchy(hier, msgsrc_prev):
166 msgsrc = "%s.%s" % (msgsrc_prev, lvl[1])
167 messages.setdefault(lvl[0], []).append(msgsrc)
170 walk_keymap_hierarchy(lvl[3], msgsrc)
172 # -------------------------------------------------------------------------
175 def full_class_id(cls):
176 """ gives us 'ID.Lamp.AreaLamp' which is best for sorting.
181 cls_id = "%s.%s" % (bl_rna.identifier, cls_id)
185 cls_list = type(bpy.context).__base__.__subclasses__()
186 cls_list.sort(key=full_class_id)
190 cls_list = bpy.types.Space.__subclasses__()
191 cls_list.sort(key=full_class_id)
195 cls_list = bpy.types.Operator.__subclasses__()
196 cls_list.sort(key=full_class_id)
200 cls_list = bpy.types.OperatorProperties.__subclasses__()
201 cls_list.sort(key=full_class_id)
205 cls_list = bpy.types.Menu.__subclasses__()
206 cls_list.sort(key=full_class_id)
210 from bpy_extras.keyconfig_utils import KM_HIERARCHY
212 walk_keymap_hierarchy(KM_HIERARCHY, "KM_HIERARCHY")
215 def dump_messages_pytext(messages):
216 """ dumps text inlined in the python user interface: eg.
218 layout.prop("someprop", text="My Name")
222 # -------------------------------------------------------------------------
223 # Gather function names
227 # val: [(arg_kw, arg_pos), (arg_kw, arg_pos), ...]
228 func_translate_args = {}
230 # so far only 'text' keywords, but we may want others translated later
231 translate_kw = ("text", )
233 for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
234 # check it has a 'text' argument
235 for (arg_pos, (arg_kw, arg)) in enumerate(func.parameters.items()):
236 if ((arg_kw in translate_kw) and
237 (arg.is_output == False) and
238 (arg.type == 'STRING')):
240 func_translate_args.setdefault(func_id, []).append((arg_kw,
242 # print(func_translate_args)
244 # -------------------------------------------------------------------------
245 # Function definitions
247 def extract_strings(fp_rel, node_container):
248 """ Recursively get strings, needed incase we have "Blah" + "Blah",
249 passed as an argument in that case it wont evaluate to a string.
252 for node in ast.walk(node_container):
253 if type(node) == ast.Str:
254 eval_str = ast.literal_eval(node)
256 # print("%s:%d: %s" % (fp, node.lineno, eval_str))
257 msgsrc = "%s:%s" % (fp_rel, node.lineno)
258 messages.setdefault(eval_str, []).append(msgsrc)
260 def extract_strings_from_file(fp):
261 filedata = open(fp, 'r', encoding="utf8")
262 root_node = ast.parse(filedata.read(), fp, 'exec')
265 fp_rel = os.path.relpath(fp, SOURCE_DIR)
267 for node in ast.walk(root_node):
268 if type(node) == ast.Call:
269 # print("found function at")
270 # print("%s:%d" % (fp, node.lineno))
273 if type(node.func) == ast.Name:
276 # getattr(self, con.type)(context, box, con)
277 if not hasattr(node.func, "attr"):
280 translate_args = func_translate_args.get(node.func.attr, ())
282 # do nothing if not found
283 for arg_kw, arg_pos in translate_args:
284 if arg_pos < len(node.args):
285 extract_strings(fp_rel, node.args[arg_pos])
287 for kw in node.keywords:
289 extract_strings(fp_rel, kw.value)
291 # -------------------------------------------------------------------------
294 mod_dir = os.path.join(SOURCE_DIR,
300 files = [os.path.join(mod_dir, fn)
301 for fn in sorted(os.listdir(mod_dir))
302 if not fn.startswith("_")
307 extract_strings_from_file(fp)
312 def filter_message(msg):
314 # check for strings like ": %d"
316 for ignore in ("%d", "%s", "%r", # string formatting
317 "*", ".", "(", ")", "-", "/", "\\", "+", ":", "#", "%"
318 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
319 "x", # used on its own eg: 100x200
320 "X", "Y", "Z", # used alone. no need to include
322 msg_test = msg_test.replace(ignore, "")
323 msg_test = msg_test.strip()
325 # print("Skipping: '%s'" % msg)
328 # we could filter out different strings here
334 messages = collections.OrderedDict()
340 # get strings from RNA
341 dump_messages_rna(messages)
343 # get strings from UI layout definitions text="..." args
344 dump_messages_pytext(messages)
348 message_file = open(FILE_NAME_MESSAGES, 'w', encoding="utf8")
349 # message_file.writelines("\n".join(sorted(messages)))
351 for key, value in messages.items():
353 # filter out junk values
354 if filter_message(key):
358 message_file.write("%s%s\n" % (COMMENT_PREFIX, msgsrc))
359 message_file.write("%s\n" % key)
363 print("Written %d messages to: %r" % (len(messages), FILE_NAME_MESSAGES))
371 print("This script must run from inside blender")
377 if __name__ == "__main__":
378 print("\n\n *** Running %r *** \n" % __file__)