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