Fix: don't use `BLI_strncpy_utf8` for copying file paths
[blender.git] / source / blender / makesrna / rna_cleanup / rna_cleaner.py
1 #!/usr/bin/env python3
2
3 """
4 This script is used to help cleaning RNA api.
5
6 Typical line in the input file (elements in [] are optional).
7
8 [comment *] ToolSettings.snap_align_rotation -> use_snap_align_rotation:    boolean    [Align rotation with the snapping target]
9
10 Geterate output format from blender run this:
11  ./blender.bin --background -noaudio --python ./release/scripts/modules/rna_info.py 2> source/blender/makesrna/rna_cleanup/out.txt
12 """
13
14
15 def font_bold(mystring):
16     """
17     Formats the string as bold, to be used in printouts.
18     """
19     font_bold = "\033[1m"
20     font_reset = "\033[0;0m"
21     return font_bold + mystring + font_reset
22
23
24 def usage():
25     """
26     Prints script usage.
27     """
28     import sys
29     scriptname = sys.argv[0]
30     sort_choices_string = '|'.join(sort_choices)
31     message = "\nUSAGE:"
32     message += "\n%s input-file (.txt|.py) order-priority (%s).\n" % (font_bold(scriptname), sort_choices_string)
33     message += "%s -h for help\n" % font_bold(scriptname)
34     print(message)
35     exit()
36
37
38 def help():
39     """
40     Prints script' help.
41     """
42     message = '\nHELP:'
43     message += '\nRun this script to re-format the edits you make in the input file.\n'
44     message += 'Do quick modification to important fields like \'to\' and don\'t care about fields like \'changed\' or \'description\' and save.\n'
45     message += 'The script outputs 3 files:\n'
46     message += '   1) *_clean.txt: is formatted same as the .txt input, can be edited by user.\n'
47     message += '   2) *_clean.py: is formatted same as the .py input, can be edited by user.\n'
48     message += '   3) rna_api.py is not formatted for readability and go under complete check. Can be used for rna cleanup.\n'
49     print(message)
50     usage()
51
52
53 def check_commandline():
54     """
55     Takes parameters from the commandline.
56     """
57     import sys
58     # Usage
59     if len(sys.argv) == 1 or len(sys.argv) > 3:
60         usage()
61     if sys.argv[1] == '-h':
62         help()
63     elif not sys.argv[1].endswith((".txt", ".py")):
64         print('\nBad input file extension... exiting.')
65         usage()
66     else:
67         inputfile = sys.argv[1]
68     if len(sys.argv) == 2:
69         sort_priority = default_sort_choice
70         print('\nSecond parameter missing: choosing to order by %s.' % font_bold(sort_priority))
71     elif len(sys.argv) == 3:
72         sort_priority = sys.argv[2]
73         if sort_priority not in sort_choices:
74             print('\nWrong sort_priority... exiting.')
75             usage()
76     return (inputfile, sort_priority)
77
78
79 def check_prefix(prop, btype):
80     # reminder: props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
81     if btype == "boolean":
82         if '_' in prop:
83             prefix = prop.split('_')[0]
84             if prefix not in kw_prefixes:
85                 return 'BAD-PREFIX: ' + prefix
86             else:
87                 return prefix + '_'
88         elif prop in kw:
89             return 'SPECIAL-KEYWORD: ' + prop
90         else:
91             return 'BAD-KEYWORD: ' + prop
92     else:
93         return ""
94
95
96 def check_if_changed(a, b):
97     if a != b:
98         return 'changed'
99     else:
100         return 'same'
101
102
103 def get_props_from_txt(input_filename):
104     """
105     If the file is *.txt, the script assumes it is formatted as outlined in this script docstring
106     """
107
108     file = open(input_filename, 'r')
109     file_lines = file.readlines()
110     file.close()
111
112     props_list = []
113     props_length_max = [0, 0, 0, 0, 0, 0, 0, 0]
114
115     done_text = "+"
116     done = 0
117     tot = 0
118
119     for iii, line in enumerate(file_lines):
120
121         # debug
122         # print(line)
123         line_strip = line.strip()
124         # empty line or comment
125         if not line_strip:
126             continue
127
128         if line_strip == "EOF":
129             break
130
131         if line.startswith("#"):
132             line = line[1:]
133
134         # class
135         bclass, tail = [x.strip() for x in line.split('.', 1)]
136
137         # comment
138         if '*' in bclass:
139             comment, bclass = [x.strip() for x in bclass.split('*', 1)]
140         else:
141             comment = ''
142
143         # skipping the header if we have one.
144         # the header is assumed to be "NOTE * CLASS.FROM -> TO:   TYPE  DESCRIPTION"
145         if comment == 'NOTE' and bclass == 'CLASS':
146             continue
147
148         # from
149         bfrom, tail = [x.strip() for x in tail.split('->', 1)]
150
151         # to
152         bto, tail = [x.strip() for x in tail.split(':', 1)]
153
154         # type, description
155         try:
156             btype, description = tail.split(None, 1)
157             # make life easy and strip quotes
158             description = description.replace("'", "").replace('"', "").replace("\\", "").strip()
159         except ValueError:
160             btype, description = [tail, 'NO DESCRIPTION']
161
162         # keyword-check
163         kwcheck = check_prefix(bto, btype)
164
165         # changed
166         changed = check_if_changed(bfrom, bto)
167
168         # lists formatting
169         props = [comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
170         props_list.append(props)
171         props_length_max = list(map(max, zip(props_length_max, list(map(len, props)))))
172
173         if done_text in comment:
174             done += 1
175         tot += 1
176
177     print("Total done %.2f" % (done / tot * 100.0))
178
179     return (props_list, props_length_max)
180
181
182 def get_props_from_py(input_filename):
183     """
184     If the file is *.py, the script assumes it contains a python list (as "rna_api=[...]")
185     This means that this script executes the text in the py file with an exec(text).
186     """
187     # adds the list "rna_api" to this function's scope
188     rna_api = __import__(input_filename[:-3]).rna_api
189
190     props_length_max = [0 for i in rna_api[0]]  # this way if the vector will take more elements we are safe
191     for index, props in enumerate(rna_api):
192         comment, changed, bclass, bfrom, bto, kwcheck, btype, description = props
193         kwcheck = check_prefix(bto, btype)   # keyword-check
194         changed = check_if_changed(bfrom, bto)  # changed?
195         description = repr(description)
196         description = description.replace("'", "").replace('"', "").replace("\\", "").strip()
197         rna_api[index] = [comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
198         props_length = list(map(len, props))  # lengths
199         props_length_max = list(map(max, zip(props_length_max, props_length)))    # max lengths
200     return (rna_api, props_length_max)
201
202
203 def get_props(input_filename):
204     if input_filename.endswith(".txt"):
205         props_list, props_length_max = get_props_from_txt(input_filename)
206     elif input_filename.endswith(".py"):
207         props_list, props_length_max = get_props_from_py(input_filename)
208     return (props_list, props_length_max)
209
210
211 def sort(props_list, sort_priority):
212     """
213     reminder
214     props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
215     """
216
217     # order based on the i-th element in lists
218     if sort_priority == "class.to":
219         props_list = sorted(props_list, key=lambda p: (p[2], p[4]))
220     else:
221         i = sort_choices.index(sort_priority)
222         if i == 0:
223             props_list = sorted(props_list, key=lambda p: p[i], reverse=True)
224         else:
225             props_list = sorted(props_list, key=lambda p: p[i])
226
227     print('\nSorted by %s.' % font_bold(sort_priority))
228     return props_list
229
230
231 def file_basename(input_filename):
232     # If needed will use `os.path`.
233     if input_filename.endswith(".txt"):
234         if input_filename.endswith("_work.txt"):
235             base_filename = input_filename.replace("_work.txt", "")
236         else:
237             base_filename = input_filename.replace(".txt", "")
238     elif input_filename.endswith(".py"):
239         if input_filename.endswith("_work.py"):
240             base_filename = input_filename.replace("_work.py", "")
241         else:
242             base_filename = input_filename.replace(".py", "")
243
244     return base_filename
245
246
247 def write_files(basename, props_list, props_length_max):
248     """
249     Writes in 3 files:
250       * output_filename_work.txt: formatted as txt input file (can be edited)
251       * output_filename_work.py:  formatted for readability (can be edited)
252       * rna_api.py: unformatted, just as final output
253     """
254
255     f_rna = open("rna_api.py", 'w')
256     f_txt = open(basename + '_work.txt', 'w')
257     f_py = open(basename + '_work.py', 'w')
258
259     # reminder: props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
260     # [comment *] ToolSettings.snap_align_rotation -> use_snap_align_rotation:    boolean    [Align rotation with the snapping target]
261     rna = py = txt = ''
262     props_list = [['NOTE', 'CHANGED', 'CLASS', 'FROM', 'TO', 'KEYWORD-CHECK', 'TYPE', 'DESCRIPTION']] + props_list
263     for props in props_list:
264         # txt
265
266         # quick way we can tell if it changed
267         if props[3] == props[4]:
268             txt += "#"
269         else:
270             txt += " "
271
272         if props[0] != '':
273             txt += '%s * ' % props[0]   # comment
274         txt += '%s.%s -> %s:   %s  "%s"\n' % tuple(props[2:5] + props[6:])   # skipping keyword-check
275         # rna_api
276         if props[0] == 'NOTE':
277             indent = '#   '
278         else:
279             indent = '    '
280         # Description is already string formatted.
281         rna += indent + '("%s", "%s", "%s", "%s", "%s"),\n' % tuple(props[2:5] + props[6:])
282         # py
283         blanks = [' ' * (x[0] - x[1]) for x in zip(props_length_max, list(map(len, props)))]
284         props = [('"%s"%s' if props[-1] != x[0] else "%s%s") % (x[0], x[1]) for x in zip(props, blanks)]
285         py += indent + '(%s, %s, %s, %s, %s, %s, %s, "%s"),\n' % tuple(props)
286
287     f_txt.write(txt)
288     f_py.write("rna_api = [\n%s]\n" % py)
289     f_rna.write("rna_api = [\n%s]\n" % rna)
290
291     # write useful py script, won't hurt
292     f_py.write("\n'''\n")
293     f_py.write("for p_note, p_changed, p_class, p_from, p_to, p_check, p_type, p_desc in rna_api:\n")
294     f_py.write("    print(p_to)\n")
295     f_py.write("\n'''\n")
296
297     f_txt.close()
298     f_py.close()
299     f_rna.close()
300
301     print('\nSaved %s, %s and %s.\n' % (font_bold(f_txt.name), font_bold(f_py.name), font_bold(f_rna.name)))
302
303
304 def main():
305
306     global sort_choices, default_sort_choice
307     global kw_prefixes, kw
308
309     sort_choices = ['note', 'changed', 'class', 'from', 'to', 'kw', 'class.to']
310     default_sort_choice = sort_choices[-1]
311     kw_prefixes = ['active', 'apply', 'bl', 'exclude', 'has', 'invert', 'is', 'lock',
312                    'pressed', 'show', 'show_only', 'use', 'use_only', 'layers', 'states', 'select']
313     kw = ['active', 'hide', 'invert', 'select', 'layers', 'mute', 'states', 'use', 'lock']
314
315     input_filename, sort_priority = check_commandline()
316     props_list, props_length_max = get_props(input_filename)
317     props_list = sort(props_list, sort_priority)
318
319     output_basename = file_basename(input_filename)
320     write_files(output_basename, props_list, props_length_max)
321
322
323 if __name__ == '__main__':
324     import sys
325     if sys.version_info.major < 3:
326         print("Incorrect python version, use Python 3 or newer!")
327     else:
328         main()