Cycles: svn merge -r40934:41157 ^/trunk/blender
[blender-staging.git] / po / update_msg.py
1 # $Id$
2 # ***** BEGIN GPL LICENSE BLOCK *****
3 #
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.
8 #
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.
13 #
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.
17 #
18 # ***** END GPL LICENSE BLOCK *****
19
20 # <pep8-80 compliant>
21
22 # Write out messages.txt from blender
23
24 # Execite:
25 #   blender --background --python po/update_msg.py
26
27 import os
28
29 CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
30 SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(CURRENT_DIR, "..")))
31
32 FILE_NAME_MESSAGES = os.path.join(CURRENT_DIR, "messages.txt")
33 COMMENT_PREFIX = "#~ "
34
35
36 def dump_messages_rna(messages):
37     import bpy
38
39     def classBlackList():
40         blacklist_rna_class = [
41                                # core classes
42                                "Context", "Event", "Function", "UILayout",
43                                "BlendData",
44                                # registerable classes
45                                "Panel", "Menu", "Header", "RenderEngine",
46                                "Operator", "OperatorMacro", "Macro",
47                                "KeyingSetInfo", "UnknownType",
48                                # window classes
49                                "WindowManager", "Window"
50                                ]
51
52         # ---------------------------------------------------------------------
53         # Collect internal operators
54
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__()]
65             )))
66
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)
73
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__()])
80
81         # ---------------------------------------------------------------------
82         # Collect classes which are attached to collections, these are api
83         # access only.
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':
89                     prop_cls = prop.srna
90                     if prop_cls is not None:
91                         collection_props.add(prop_cls.identifier)
92         blacklist_rna_class.extend(sorted(collection_props))
93
94         return blacklist_rna_class
95
96     blacklist_rna_class = classBlackList()
97
98     def filterRNA(bl_rna):
99         id = bl_rna.identifier
100         if id in blacklist_rna_class:
101             print("  skipping", id)
102             return True
103         return False
104
105     # -------------------------------------------------------------------------
106     # Function definitions
107
108     def walkProperties(bl_rna):
109         import bpy
110
111         # get our parents properties not to export them multiple times
112         bl_rna_base = bl_rna.base
113         if bl_rna_base:
114             bl_rna_base_props = bl_rna_base.properties.values()
115         else:
116             bl_rna_base_props = ()
117
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:
121                 continue
122             if prop.identifier == "rna_type":
123                 continue
124
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)
128             if prop.description:
129                 messages.setdefault(prop.description, []).append(msgsrc)
130
131             if isinstance(prop, bpy.types.EnumProperty):
132                 for item in prop.enum_items:
133                     msgsrc = "bpy.types.%s.%s, '%s'" % (bl_rna.identifier,
134                                                         prop.identifier,
135                                                         item.identifier,
136                                                         )
137                     # Here identifier and name can be the same!
138                     if item.name:  # and item.name != item.identifier:
139                         messages.setdefault(item.name,
140                                             []).append(msgsrc)
141                     if item.description:
142                         messages.setdefault(item.description,
143                                             []).append(msgsrc)
144
145     def walkRNA(bl_rna):
146
147         if filterRNA(bl_rna):
148             return
149
150         msgsrc = "bpy.types.%s" % bl_rna.identifier
151
152         if bl_rna.name and bl_rna.name != bl_rna.identifier:
153             messages.setdefault(bl_rna.name, []).append(msgsrc)
154
155         if bl_rna.description:
156             messages.setdefault(bl_rna.description, []).append(msgsrc)
157
158         if hasattr(bl_rna, 'bl_label') and  bl_rna.bl_label:
159             messages.setdefault(bl_rna.bl_label, []).append(msgsrc)
160
161         walkProperties(bl_rna)
162
163     def walkClass(cls):
164         walkRNA(cls.bl_rna)
165
166     def walk_keymap_hierarchy(hier, msgsrc_prev):
167         for lvl in hier:
168             msgsrc = "%s.%s" % (msgsrc_prev, lvl[1])
169             messages.setdefault(lvl[0], []).append(msgsrc)
170
171             if lvl[3]:
172                 walk_keymap_hierarchy(lvl[3], msgsrc)
173
174     # -------------------------------------------------------------------------
175     # Dump Messages
176
177     def full_class_id(cls):
178         """ gives us 'ID.Lamp.AreaLamp' which is best for sorting.
179         """
180         cls_id = ""
181         bl_rna = cls.bl_rna
182         while bl_rna:
183             cls_id = "%s.%s" % (bl_rna.identifier, cls_id)
184             bl_rna = bl_rna.base
185         return cls_id
186
187     cls_list = type(bpy.context).__base__.__subclasses__()
188     cls_list.sort(key=full_class_id)
189     for cls in cls_list:
190         walkClass(cls)
191
192     cls_list = bpy.types.Space.__subclasses__()
193     cls_list.sort(key=full_class_id)
194     for cls in cls_list:
195         walkClass(cls)
196
197     cls_list = bpy.types.Operator.__subclasses__()
198     cls_list.sort(key=full_class_id)
199     for cls in cls_list:
200         walkClass(cls)
201
202     cls_list = bpy.types.OperatorProperties.__subclasses__()
203     cls_list.sort(key=full_class_id)
204     for cls in cls_list:
205         walkClass(cls)
206
207     cls_list = bpy.types.Menu.__subclasses__()
208     cls_list.sort(key=full_class_id)
209     for cls in cls_list:
210         walkClass(cls)
211
212     from bpy_extras.keyconfig_utils import KM_HIERARCHY
213
214     walk_keymap_hierarchy(KM_HIERARCHY, "KM_HIERARCHY")
215
216
217 def dump_messages_pytext(messages):
218     """ dumps text inlined in the python user interface: eg.
219
220         layout.prop("someprop", text="My Name")
221     """
222     import ast
223
224     # -------------------------------------------------------------------------
225     # Gather function names
226
227     import bpy
228     # key: func_id
229     # val: [(arg_kw, arg_pos), (arg_kw, arg_pos), ...]
230     func_translate_args = {}
231
232     # so far only 'text' keywords, but we may want others translated later
233     translate_kw = ("text", )
234
235     for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
236         # check it has a 'text' argument
237         for (arg_pos, (arg_kw, arg)) in enumerate(func.parameters.items()):
238             if ((arg_kw in translate_kw) and
239                 (arg.is_output == False) and
240                 (arg.type == 'STRING')):
241
242                 func_translate_args.setdefault(func_id, []).append((arg_kw,
243                                                                     arg_pos))
244     # print(func_translate_args)
245
246     # -------------------------------------------------------------------------
247     # Function definitions
248
249     def extract_strings(fp_rel, node_container):
250         """ Recursively get strings, needed incase we have "Blah" + "Blah",
251             passed as an argument in that case it wont evaluate to a string.
252         """
253
254         for node in ast.walk(node_container):
255             if type(node) == ast.Str:
256                 eval_str = ast.literal_eval(node)
257                 if eval_str:
258                     # print("%s:%d: %s" % (fp, node.lineno, eval_str))
259                     msgsrc = "%s:%s" % (fp_rel, node.lineno)
260                     messages.setdefault(eval_str, []).append(msgsrc)
261
262     def extract_strings_from_file(fp):
263         filedata = open(fp, 'r', encoding="utf8")
264         root_node = ast.parse(filedata.read(), fp, 'exec')
265         filedata.close()
266
267         fp_rel = os.path.relpath(fp, SOURCE_DIR)
268
269         for node in ast.walk(root_node):
270             if type(node) == ast.Call:
271                 # print("found function at")
272                 # print("%s:%d" % (fp, node.lineno))
273
274                 # lambda's
275                 if type(node.func) == ast.Name:
276                     continue
277
278                 # getattr(self, con.type)(context, box, con)
279                 if not hasattr(node.func, "attr"):
280                     continue
281
282                 translate_args = func_translate_args.get(node.func.attr, ())
283
284                 # do nothing if not found
285                 for arg_kw, arg_pos in translate_args:
286                     if arg_pos < len(node.args):
287                         extract_strings(fp_rel, node.args[arg_pos])
288                     else:
289                         for kw in node.keywords:
290                             if kw.arg == arg_kw:
291                                 extract_strings(fp_rel, kw.value)
292
293     # -------------------------------------------------------------------------
294     # Dump Messages
295
296     mod_dir = os.path.join(SOURCE_DIR,
297                            "release",
298                            "scripts",
299                            "startup",
300                            "bl_ui")
301
302     files = [os.path.join(mod_dir, fn)
303              for fn in sorted(os.listdir(mod_dir))
304              if not fn.startswith("_")
305              if fn.endswith("py")
306              ]
307
308     for fp in files:
309         extract_strings_from_file(fp)
310
311
312 def dump_messages():
313
314     def filter_message(msg):
315
316         # check for strings like ": %d"
317         msg_test = msg
318         for ignore in ("%d", "%s", "%r",  # string formatting
319                        "*", ".", "(", ")", "-", "/", "\\", "+", ":", "#", "%"
320                        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
321                        "x",  # used on its own eg: 100x200
322                        "X", "Y", "Z",  # used alone. no need to include
323                        ):
324             msg_test = msg_test.replace(ignore, "")
325         msg_test = msg_test.strip()
326         if not msg_test:
327             # print("Skipping: '%s'" % msg)
328             return True
329
330         # we could filter out different strings here
331
332         return False
333
334     if 1:
335         import collections
336         messages = collections.OrderedDict()
337     else:
338         messages = {}
339
340     messages[""] = []
341
342     # get strings from RNA
343     dump_messages_rna(messages)
344
345     # get strings from UI layout definitions text="..." args
346     dump_messages_pytext(messages)
347
348     del messages[""]
349
350     message_file = open(FILE_NAME_MESSAGES, 'w', encoding="utf8")
351     # message_file.writelines("\n".join(sorted(messages)))
352
353     for key, value in messages.items():
354
355         # filter out junk values
356         if filter_message(key):
357             continue
358
359         for msgsrc in value:
360             message_file.write("%s%s\n" % (COMMENT_PREFIX, msgsrc))
361         message_file.write("%s\n" % key)
362
363     message_file.close()
364
365     print("Written %d messages to: %r" % (len(messages), FILE_NAME_MESSAGES))
366
367
368 def main():
369
370     try:
371         import bpy
372     except ImportError:
373         print("This script must run from inside blender")
374         return
375
376     dump_messages()
377
378
379 if __name__ == "__main__":
380     print("\n\n *** Running %r *** \n" % __file__)
381     main()