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