update automatic rna changelog
[blender.git] / doc / python_api / sphinx_changelog_gen.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 compliant>
20
21 """
22 Dump the python API into a text file so we can generate changelogs.
23
24 output from this tool should be added into "doc/python_api/rst/change_log.rst"
25
26 # dump api blender_version.py in CWD
27 blender --background --python doc/python_api/sphinx_changelog_gen.py -- --dump
28
29 # create changelog
30 blender --background --python doc/python_api/sphinx_changelog_gen.py -- \
31         --api_from blender_2_56_1.py \
32         --api_to blender_2_57_0.py \
33         --api_out changes.rst
34
35
36 # Api comparison can also run without blender
37 python doc/python_api/sphinx_changelog_gen.py \
38         --api_from blender_api_2_56_6.py \
39         --api_to blender_api_2_57.py \
40         --api_out changes.rst
41
42 """
43
44 # format
45 '''
46 {"module.name":
47     {"parent.class":
48         {"basic_type", "member_name": ("Name", type, range, length, default, descr, f_args, f_arg_types, f_ret_types)}, ...
49     }, ...
50 }
51 '''
52
53 api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types"
54
55 API_BASIC_TYPE = 0
56 API_F_ARGS = 7
57
58
59 def api_dunp_fname():
60     import bpy
61     return "blender_api_%s.py" % "_".join([str(i) for i in bpy.app.version])
62
63
64 def api_dump():
65     dump = {}
66     dump_module = dump["bpy.types"] = {}
67
68     import rna_info
69     import inspect
70
71     struct = rna_info.BuildRNAInfo()[0]
72     for struct_id, strict_info in sorted(struct.items()):
73
74         struct_id_str = strict_info.identifier
75
76         if rna_info.rna_id_ignore(struct_id_str):
77             continue
78
79         for base in strict_info.get_bases():
80             struct_id_str = base.identifier + "." + struct_id_str
81
82         dump_class = dump_module[struct_id_str] = {}
83
84         props = [(prop.identifier, prop) for prop in strict_info.properties]
85         for prop_id, prop in sorted(props):
86             # if prop.type == 'boolean':
87             #     continue
88             prop_type = prop.type
89             prop_length = prop.array_length
90             prop_range = round(prop.min, 4), round(prop.max, 4)
91             prop_default = prop.default
92             if type(prop_default) is float:
93                 prop_default = round(prop_default, 4)
94
95             if prop_range[0] == -1 and prop_range[1] == -1:
96                 prop_range = None
97
98             dump_class[prop_id] = (
99                     "prop_rna",                 # basic_type
100                     prop.name,                  # name
101                     prop_type,                  # type
102                     prop_range,                 # range
103                     prop_length,                # length
104                     prop.default,               # default
105                     prop.description,           # descr
106                     Ellipsis,                   # f_args
107                     Ellipsis,                   # f_arg_types
108                     Ellipsis,                   # f_ret_types
109                     )
110         del props
111
112         # python props, tricky since we dont know much about them.
113         for prop_id, attr in strict_info.get_py_properties():
114
115             dump_class[prop_id] = (
116                     "prop_py",                  # basic_type
117                     Ellipsis,                   # name
118                     Ellipsis,                   # type
119                     Ellipsis,                   # range
120                     Ellipsis,                   # length
121                     Ellipsis,                   # default
122                     attr.__doc__,               # descr
123                     Ellipsis,                   # f_args
124                     Ellipsis,                   # f_arg_types
125                     Ellipsis,                   # f_ret_types
126                     )
127
128         # kludge func -> props
129         funcs = [(func.identifier, func) for func in strict_info.functions]
130         for func_id, func in funcs:
131
132             func_ret_types = tuple([prop.type for prop in func.return_values])
133             func_args_ids = tuple([prop.identifier for prop in func.args])
134             func_args_type = tuple([prop.type for prop in func.args])
135
136             dump_class[func_id] = (
137                     "func_rna",                 # basic_type
138                     Ellipsis,                   # name
139                     Ellipsis,                   # type
140                     Ellipsis,                   # range
141                     Ellipsis,                   # length
142                     Ellipsis,                   # default
143                     func.description,           # descr
144                     func_args_ids,              # f_args
145                     func_args_type,             # f_arg_types
146                     func_ret_types,             # f_ret_types
147                     )
148         del funcs
149
150         # kludge func -> props
151         funcs = strict_info.get_py_functions()
152         for func_id, attr in funcs:
153             # arg_str = inspect.formatargspec(*inspect.getargspec(py_func))
154
155             func_args_ids = tuple(inspect.getargspec(attr).args)
156
157             dump_class[func_id] = (
158                     "func_py",                  # basic_type
159                     Ellipsis,                   # name
160                     Ellipsis,                   # type
161                     Ellipsis,                   # range
162                     Ellipsis,                   # length
163                     Ellipsis,                   # default
164                     attr.__doc__,               # descr
165                     func_args_ids,              # f_args
166                     Ellipsis,                   # f_arg_types
167                     Ellipsis,                   # f_ret_types
168                     )
169         del funcs
170
171     import pprint
172
173     filename = api_dunp_fname()
174     filehandle = open(filename, 'w')
175     tot = filehandle.write(pprint.pformat(dump, width=1))
176     filehandle.close()
177     print("%s, %d bytes written" % (filename, tot))
178
179
180 def compare_props(a, b, fuzz=0.75):
181
182     # must be same basic_type, function != property
183     if a[0] != b[0]:
184         return False
185
186     tot = 0
187     totlen = 0
188     for i in range(1, len(a)):
189         if not (Ellipsis is a[i] is b[i]):
190             tot += (a[i] == b[i])
191             totlen += 1
192
193     return ((tot / totlen) >= fuzz)
194
195
196 def api_changelog(api_from, api_to, api_out):
197
198     file_handle = open(api_from, 'r')
199     dict_from = eval(file_handle.read())
200     file_handle.close()
201
202     file_handle = open(api_to, 'r')
203     dict_to = eval(file_handle.read())
204     file_handle.close()
205
206     api_changes = []
207
208     # first work out what moved
209     for mod_id, mod_data in dict_to.items():
210         mod_data_other = dict_from[mod_id]
211         for class_id, class_data in mod_data.items():
212             class_data_other = mod_data_other.get(class_id)
213             if class_data_other is None:
214                 # TODO, document new structs
215                 continue
216
217             # find the props which are not in either
218             set_props_new = set(class_data.keys())
219             set_props_other = set(class_data_other.keys())
220             set_props_shared = set_props_new & set_props_other
221
222             props_moved = []
223             props_new = []
224             props_old = []
225             func_args = []
226
227             set_props_old = set_props_other - set_props_shared
228             set_props_new = set_props_new - set_props_shared
229
230             # first find settings which have been moved old -> new
231             for prop_id_old in set_props_old.copy():
232                 prop_data_other = class_data_other[prop_id_old]
233                 for prop_id_new in set_props_new.copy():
234                     prop_data = class_data[prop_id_new]
235                     if compare_props(prop_data_other, prop_data):
236                         props_moved.append((prop_id_old, prop_id_new))
237
238                         # remove
239                         if prop_id_old in set_props_old:
240                             set_props_old.remove(prop_id_old)
241                         set_props_new.remove(prop_id_new)
242
243             # func args
244             for prop_id in set_props_shared:
245                 prop_data = class_data[prop_id]
246                 prop_data_other = class_data_other[prop_id]
247                 if prop_data[API_BASIC_TYPE] == prop_data_other[API_BASIC_TYPE]:
248                     if prop_data[API_BASIC_TYPE].startswith("func"):
249                         args_new = prop_data[API_F_ARGS]
250                         args_old = prop_data_other[API_F_ARGS]
251
252                         if args_new != args_old:
253                             func_args.append((prop_id, args_old, args_new))
254
255             if props_moved or set_props_new or set_props_old or func_args:
256                 props_moved.sort()
257                 props_new[:] = sorted(set_props_new)
258                 props_old[:] = sorted(set_props_old)
259                 func_args.sort()
260
261                 api_changes.append((mod_id, class_id, props_moved, props_new, props_old, func_args))
262
263     # also document function argument changes
264
265     fout = open(api_out, 'w')
266     fw = fout.write
267     # print(api_changes)
268
269     # :class:`bpy_struct.id_data`
270
271     def write_title(title, title_char):
272         fw("%s\n%s\n\n" % (title, title_char * len(title)))
273
274     for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes:
275         class_name = class_id.split(".")[-1]
276         title = mod_id + "." + class_name
277         write_title(title, "-")
278
279         if props_new:
280             write_title("Added", "^")
281             for prop_id in props_new:
282                 fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id))
283             fw("\n")
284
285         if props_old:
286             write_title("Removed", "^")
287             for prop_id in props_old:
288                 fw("* **%s**\n" % prop_id)  # cant link to remvoed docs
289             fw("\n")
290
291         if props_moved:
292             write_title("Renamed", "^")
293             for prop_id_old, prop_id in props_moved:
294                 fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id))
295             fw("\n")
296
297         if func_args:
298             write_title("Function Arguments", "^")
299             for func_id, args_old, args_new in func_args:
300                 args_new = ", ".join(args_new)
301                 args_old = ", ".join(args_old)
302                 fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, prop_id, args_new, args_old))
303             fw("\n")
304
305     fout.close()
306
307
308 def main():
309     import sys
310     import os
311
312     try:
313         import argparse
314     except:
315         print("Old Blender, just dumping")
316         api_dump()
317         return
318
319     argv = sys.argv
320
321     if "--" not in argv:
322         argv = []  # as if no args are passed
323     else:
324         argv = argv[argv.index("--") + 1:]  # get all args after "--"
325
326     # When --help or no args are given, print this help
327     usage_text = "Run blender in background mode with this script: "
328     "blender --background --python %s -- [options]" % os.path.basename(__file__)
329
330     epilog = "Run this before releases"
331
332     parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
333
334     parser.add_argument("--dump", dest="dump", action='store_true',
335             help="When set the api will be dumped into blender_version.py")
336
337     parser.add_argument("--api_from", dest="api_from", metavar='FILE',
338             help="File to compare from (previous version)")
339     parser.add_argument("--api_to", dest="api_to", metavar='FILE',
340             help="File to compare from (current)")
341     parser.add_argument("--api_out", dest="api_out", metavar='FILE',
342             help="Output sphinx changelog")
343
344     args = parser.parse_args(argv)  # In this example we wont use the args
345
346     if not argv:
347         parser.print_help()
348         return
349
350     if args.dump:
351         api_dump()
352     else:
353         if args.api_from and args.api_to and args.api_out:
354             api_changelog(args.api_from, args.api_to, args.api_out)
355         else:
356             print("Error: --api_from/api_to/api_out args needed")
357             parser.print_help()
358             return
359
360     print("batch job finished, exiting")
361
362
363 if __name__ == "__main__":
364     main()