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