== rna cleanup ==
[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
11
12 def font_bold(mystring):
13     """
14     Formats the string as bold, to be used in printouts.
15     """
16     font_bold = "\033[1m"
17     font_reset = "\033[0;0m"
18     return font_bold + mystring + font_reset
19     
20
21 def usage():
22     """
23     Prints script usage.
24     """
25     import sys
26     scriptname = sys.argv[0]
27     sort_choices_string = '|'.join(sort_choices)
28     message = "\nUSAGE:"
29     message += "\n%s input-file (.txt|.py) order-priority (%s).\n" % (font_bold(scriptname), sort_choices_string)
30     message += "%s -h for help\n" % font_bold(scriptname)
31     print(message)
32     exit()
33
34
35 def help():
36     """
37     Prints script' help.
38     """
39     message = '\nHELP:'
40     message += '\nRun this script to re-format the edits you make in the input file.\n'
41     message += 'Do quick modification to important fields like \'to\' and don\'t care about fields like \'changed\' or \'description\' and save.\n'
42     message += 'The script outputs 3 files:\n'
43     message += '   1) *_clean.txt: is formatted same as the .txt input, can be edited by user.\n'
44     message += '   2) *_clean.py: is formatted same as the .py input, can be edited by user.\n'
45     message += '   3) rna_api.py is not formatted for readability and go under complete check. Can be used for rna cleanup.\n'
46     print(message)
47     usage()
48
49
50 def check_commandline():
51     """
52     Takes parameters from the commandline.
53     """
54     import sys
55     # Usage
56     if len(sys.argv)==1 or len(sys.argv)>3:
57         usage()
58     if sys.argv[1]!= '-h':
59         input_filename = sys.argv[1]
60     else:
61         help()
62     if not (input_filename[-4:] == '.txt' or input_filename[-3:] == '.py'):
63         print ('\nBad input file extension... exiting.')
64         usage()
65     if len(sys.argv)==2:
66         order_priority = default_sort_choice
67         print ('\nSecond parameter missing: choosing to order by %s.' % font_bold(order_priority))
68     elif len(sys.argv)==3:
69         order_priority = sys.argv[2]
70         if order_priority not in sort_choices:
71             print('\nWrong order_priority... exiting.')
72             usage()
73     return (input_filename, order_priority)
74
75
76 def check_prefix(prop):
77     # reminder: props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
78     if '_' in prop:
79         prefix = prop.split('_')[0]
80         if prefix not in kw_prefixes:
81             return 'BAD-PREFIX: ' + prefix
82         else:
83             return prefix + '_'
84     elif prop in kw:
85         return 'SPECIAL-KEYWORD: ' + prop
86     else:
87         return 'BAD-KEYWORD: ' + prop
88
89
90 def check_if_changed(a,b):
91     if a != b: return 'changed'
92     else: return 'same'
93
94
95 def get_props_from_txt(input_filename):
96     """
97     If the file is *.txt, the script assumes it is formatted as outlined in this script docstring
98     """
99     
100     file=open(input_filename,'r')
101     file_lines=file.readlines()
102     file.close()
103
104     props_list=[]
105     props_length_max=[0,0,0,0,0,0,0,0]
106     for line in file_lines:
107         
108         # debug
109         #print(line)
110         
111         # empty line or comment
112         if not line.strip() or line.startswith('#'):
113             continue
114
115         # class
116         [bclass, tail] = [x.strip() for x in line.split('.', 1)]
117
118         # comment
119         if '*' in bclass:
120             [comment, bclass] = [x.strip() for x in bclass.split('*', 1)]
121         else:
122             comment= ''
123
124         # skipping the header if we have one.
125         # the header is assumed to be "NOTE * CLASS.FROM -> TO:   TYPE  DESCRIPTION"
126         if comment == 'NOTE' and bclass == 'CLASS':
127             continue
128
129         # from
130         [bfrom, tail] = [x.strip() for x in tail.split('->', 1)]
131
132         # to
133         [bto, tail] = [x.strip() for x in tail.split(':', 1)]
134
135         # type, description
136         try:
137             [btype, description] = tail.split(None, 1)
138             if '"' in description:
139                 description.replace('"', "'")
140         except ValueError:
141             [btype, description] = [tail,'NO DESCRIPTION']
142
143         # keyword-check
144         kwcheck = check_prefix(bto)
145
146         # changed
147         changed = check_if_changed(bfrom, bto)
148         
149         # lists formatting
150         props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
151         props_list.append(props)
152         props_length_max=list(map(max,zip(props_length_max,list(map(len,props)))))
153         
154     return (props_list,props_length_max)
155
156
157 def get_props_from_py(input_filename):
158     """
159     If the file is *.py, the script assumes it contains a python list (as "rna_api=[...]")
160     This means that this script executes the text in the py file with an exec(text).
161     """    
162     # adds the list "rna_api" to this function's scope
163     rna_api = __import__(input_filename[:-3]).rna_api
164
165     props_length_max = [0 for i in rna_api[0]] # this way if the vector will take more elements we are safe
166     for props in rna_api:
167         [comment, changed, bclass, bfrom, bto, kwcheck, btype, description] = props
168         kwcheck = check_prefix(bto)   # keyword-check
169         changed = check_if_changed(bfrom, bto)  # changed?
170         props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
171         props_length = list(map(len,props)) # lengths
172         props_length_max = list(map(max,zip(props_length_max,props_length)))    # max lengths
173     return (rna_api,props_length_max)
174
175
176 def get_props(input_filename):
177     if input_filename[-4:] == '.txt':
178         props_list,props_length_max = get_props_from_txt(input_filename)
179     elif input_filename[-3:] == '.py':
180         props_list,props_length_max = get_props_from_py(input_filename)
181     return (props_list,props_length_max)
182
183         
184 def sort(props_list, sort_priority):
185     """
186     reminder
187     props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
188     """
189
190     # order based on the i-th element in lists
191     i = sort_choices.index(sort_priority)
192     if i == 0:
193         props_list = sorted(props_list, key=lambda p: p[i], reverse=True)
194     else:
195         props_list = sorted(props_list, key=lambda p: p[i])
196         
197     print ('\nSorted by %s.' % font_bold(sort_priority))
198     return props_list
199
200
201 def write_files(props_list, props_length_max):
202     """
203     Writes in 3 files:
204       * output_filename_txt: formatted as txt input file
205       * output_filename_py:  formatted for readability (could be worked on)
206       * rna_api.py: unformatted, just as final output
207     """
208
209     # if needed will use os.path
210     if input_filename[-4:] == '.txt':
211         if input_filename[-9:] == '_work.txt':
212             base_filename = input_filename[:-9]
213         else:
214             base_filename = input_filename[:-4]
215     elif input_filename[-3:] == '.py':
216         if input_filename[-8:] == '_work.py':
217             base_filename = input_filename[:-8]
218         else:
219             base_filename = input_filename[:-3]
220
221     f_rna = open("rna_api.py",'w')
222     f_txt = open(base_filename+'_work.txt','w')
223     f_py = open(base_filename+'_work.py','w')
224
225     # reminder: props=[comment, changed, bclass, bfrom, bto, kwcheck, btype, description]
226     # [comment *] ToolSettings.snap_align_rotation -> use_snap_align_rotation:    boolean    [Align rotation with the snapping target]
227     rna = py = txt = ''
228     props_list = [['NOTE', 'CHANGED', 'CLASS', 'FROM', 'TO', 'KEYWORD-CHECK', 'TYPE', 'DESCRIPTION']] + props_list
229     for props in props_list:
230         #txt
231         if props[0] != '': txt +=  '%s * ' % props[0]   # comment
232         txt +=  '%s.%s -> %s:   %s  %s\n' % tuple(props[2:5] + props[6:])   # skipping keyword-check
233         # rna_api
234         if props[0] == 'NOTE': indent = '#   '
235         else: indent = '    '
236         rna += indent + '("%s", "%s", "%s", "%s", "%s"),\n' % tuple(props[2:5] + props[6:])    
237         # py
238         if props[0] == 'NOTE': indent = '#   '
239         else: indent = '    '
240         blanks = [' '* (x[0]-x[1]) for x in zip(props_length_max,list(map(len,props)))]
241         props = ['"%s"%s'%(x[0],x[1]) for x in zip(props,blanks)]
242         py += indent + '(%s, %s, %s, %s, %s, %s, %s, %s),\n' % tuple(props)
243     f_txt.write(txt)
244     f_py.write("rna_api = [\n%s]\n" % py)
245     f_rna.write("rna_api = [\n%s]\n" % rna)
246
247     print ('\nSaved %s, %s and %s.\n' % (font_bold(f_txt.name), font_bold(f_py.name), font_bold(f_rna.name) ) )
248
249
250 def main():
251
252     global input_filename
253     global sort_choices, default_sort_choice
254     global kw_prefixes, kw
255
256     sort_choices = ['note','changed','class','from','to','kw']
257     default_sort_choice = sort_choices[0]
258     kw_prefixes = ['invert','is','lock','show','showonly','use','useonly']
259     kw = ['hidden','selected','layer','state']
260
261     input_filename, sort_priority = check_commandline()
262     props_list,props_length_max = get_props(input_filename)
263     props_list = sort(props_list,sort_priority)
264     write_files(props_list,props_length_max)
265
266
267 if __name__=='__main__':
268     main()
269