Sphinx RNA API changelog generator.
[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 intern/tools/rna_api_dump.py -- --dump
28
29 # create changelog
30 blender --background --python intern/tools/rna_api_dump.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 intern/tools/rna_api_dump.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
256             if props_moved or set_props_new or set_props_old or func_args:
257                 props_moved.sort()
258                 props_new[:] = sorted(set_props_new)
259                 props_old[:] = sorted(set_props_old)
260                 func_args.sort()
261
262                 api_changes.append((mod_id, class_id, props_moved, props_new, props_old, func_args))
263
264     # also document function argument changes
265     
266
267
268
269
270     fout = open(api_out, 'w')
271     fw = fout.write
272     # print(api_changes)
273
274     # :class:`bpy_struct.id_data`
275
276     def write_title(title, title_char):
277         fw("%s\n%s\n\n" % (title, title_char * len(title)))
278
279     for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes:
280         class_name = class_id.split(".")[-1]
281         title = mod_id + "." + class_name
282         write_title(title, "-")
283
284         if props_new:
285             write_title("Added", "^")
286             for prop_id in props_new:
287                 fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id))
288             fw("\n")
289
290         if props_old:
291             write_title("Removed", "^")
292             for prop_id in props_old:
293                 fw("* **%s**\n" % prop_id)  # cant link to remvoed docs
294             fw("\n")
295
296         if props_moved:
297             write_title("Renamed", "^")
298             for prop_id_old, prop_id in props_moved:
299                 fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id))
300             fw("\n")
301             
302         if func_args:
303             write_title("Function Arguments", "^")
304             for func_id, args_old, args_new in func_args:
305                 args_new = ", ".join(args_new)
306                 args_old = ", ".join(args_old)
307                 fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, prop_id, args_new, args_old))
308             fw("\n")
309
310     fout.close()
311
312
313 def main():
314     import sys
315     import os
316     
317     try:
318         import argparse
319     except:
320         print("Old Blender, just dumping")
321         api_dump()
322         return
323         
324
325     argv = sys.argv
326
327     if "--" not in argv:
328         argv = []  # as if no args are passed
329     else:
330         argv = argv[argv.index("--") + 1:]  # get all args after "--"
331
332     # When --help or no args are given, print this help
333     usage_text = "Run blender in background mode with this script: "
334     "blender --background --python %s -- [options]" % os.path.basename(__file__)
335
336     epilog = "Run this before releases"
337
338     parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
339
340     parser.add_argument("--dump", dest="dump", action='store_true',
341             help="When set the api will be dumped into blender_version.py")
342
343     parser.add_argument("--api_from", dest="api_from", metavar='FILE',
344             help="File to compare from (previous version)")
345     parser.add_argument("--api_to", dest="api_to", metavar='FILE',
346             help="File to compare from (current)")
347     parser.add_argument("--api_out", dest="api_out", metavar='FILE',
348             help="Output sphinx changelog")
349
350     args = parser.parse_args(argv)  # In this example we wont use the args
351
352     if not argv:
353         parser.print_help()
354         return
355
356     if args.dump:
357         api_dump()
358     else:
359         if args.api_from and args.api_to and args.api_out:
360             api_changelog(args.api_from, args.api_to, args.api_out)
361         else:
362             print("Error: --api_from/api_to/api_out args needed")
363             parser.print_help()
364             return
365
366     print("batch job finished, exiting")
367
368
369 if __name__ == "__main__":
370     main()