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"""
22Dump the python API into a text file so we can generate changelogs.
23
24output from this tool should be added into "doc/python_api/rst/change_log.rst"
25
26# dump api blender_version.py in CWD
27blender --background --python doc/python_api/sphinx_changelog_gen.py -- --dump
28
29# create changelog
30blender --background --factory-startup --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
37python 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
57api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types"
58
59API_BASIC_TYPE = 0
60API_F_ARGS = 7
61
62
63def api_dunp_fname():
64    import bpy
65    return "blender_api_%s.py" % "_".join([str(i) for i in bpy.app.version])
66
67
68def 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 don't 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            sig = inspect.signature(attr)
160            func_args_ids = [k for k, v in sig.parameters.items()]
161
162            dump_class[func_id] = (
163                "func_py",                  # basic_type
164                Ellipsis,                   # name
165                Ellipsis,                   # type
166                Ellipsis,                   # range
167                Ellipsis,                   # length
168                Ellipsis,                   # default
169                attr.__doc__,               # descr
170                func_args_ids,              # f_args
171                Ellipsis,                   # f_arg_types
172                Ellipsis,                   # f_ret_types
173            )
174        del funcs
175
176    import pprint
177
178    filename = api_dunp_fname()
179    filehandle = open(filename, 'w', encoding='utf-8')
180    tot = filehandle.write(pprint.pformat(dump, width=1))
181    filehandle.close()
182    print("%s, %d bytes written" % (filename, tot))
183
184
185def compare_props(a, b, fuzz=0.75):
186
187    # must be same basic_type, function != property
188    if a[0] != b[0]:
189        return False
190
191    tot = 0
192    totlen = 0
193    for i in range(1, len(a)):
194        if not (Ellipsis is a[i] is b[i]):
195            tot += (a[i] == b[i])
196            totlen += 1
197
198    return ((tot / totlen) >= fuzz)
199
200
201def api_changelog(api_from, api_to, api_out):
202
203    file_handle = open(api_from, 'r', encoding='utf-8')
204    dict_from = eval(file_handle.read())
205    file_handle.close()
206
207    file_handle = open(api_to, 'r', encoding='utf-8')
208    dict_to = eval(file_handle.read())
209    file_handle.close()
210
211    api_changes = []
212
213    # first work out what moved
214    for mod_id, mod_data in dict_to.items():
215        mod_data_other = dict_from[mod_id]
216        for class_id, class_data in mod_data.items():
217            class_data_other = mod_data_other.get(class_id)
218            if class_data_other is None:
219                # TODO, document new structs
220                continue
221
222            # find the props which are not in either
223            set_props_new = set(class_data.keys())
224            set_props_other = set(class_data_other.keys())
225            set_props_shared = set_props_new & set_props_other
226
227            props_moved = []
228            props_new = []
229            props_old = []
230            func_args = []
231
232            set_props_old = set_props_other - set_props_shared
233            set_props_new = set_props_new - set_props_shared
234
235            # first find settings which have been moved old -> new
236            for prop_id_old in set_props_old.copy():
237                prop_data_other = class_data_other[prop_id_old]
238                for prop_id_new in set_props_new.copy():
239                    prop_data = class_data[prop_id_new]
240                    if compare_props(prop_data_other, prop_data):
241                        props_moved.append((prop_id_old, prop_id_new))
242
243                        # remove
244                        if prop_id_old in set_props_old:
245                            set_props_old.remove(prop_id_old)
246                        set_props_new.remove(prop_id_new)
247
248            # func args
249            for prop_id in set_props_shared:
250                prop_data = class_data[prop_id]
251                prop_data_other = class_data_other[prop_id]
252                if prop_data[API_BASIC_TYPE] == prop_data_other[API_BASIC_TYPE]:
253                    if prop_data[API_BASIC_TYPE].startswith("func"):
254                        args_new = prop_data[API_F_ARGS]
255                        args_old = prop_data_other[API_F_ARGS]
256
257                        if args_new != args_old:
258                            func_args.append((prop_id, args_old, args_new))
259
260            if props_moved or set_props_new or set_props_old or func_args:
261                props_moved.sort()
262                props_new[:] = sorted(set_props_new)
263                props_old[:] = sorted(set_props_old)
264                func_args.sort()
265
266                api_changes.append((mod_id, class_id, props_moved, props_new, props_old, func_args))
267
268    # also document function argument changes
269
270    fout = open(api_out, 'w', encoding='utf-8')
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)  # can't link to removed 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, func_id, args_new, args_old))
308            fw("\n")
309
310    fout.close()
311
312    print("Written: %r" % api_out)
313
314
315def main():
316    import sys
317    import os
318
319    try:
320        import argparse
321    except ImportError:
322        print("Old Blender, just dumping")
323        api_dump()
324        return
325
326    argv = sys.argv
327
328    if "--" not in argv:
329        argv = []  # as if no args are passed
330    else:
331        argv = argv[argv.index("--") + 1:]  # get all args after "--"
332
333    # When --help or no args are given, print this help
334    usage_text = "Run blender in background mode with this script: "
335    "blender --background --factory-startup --python %s -- [options]" % os.path.basename(__file__)
336
337    epilog = "Run this before releases"
338
339    parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
340
341    parser.add_argument(
342        "--dump", dest="dump", action='store_true',
343        help="When set the api will be dumped into blender_version.py")
344
345    parser.add_argument(
346        "--api_from", dest="api_from", metavar='FILE',
347        help="File to compare from (previous version)")
348    parser.add_argument(
349        "--api_to", dest="api_to", metavar='FILE',
350        help="File to compare from (current)")
351    parser.add_argument(
352        "--api_out", dest="api_out", metavar='FILE',
353        help="Output sphinx changelog")
354
355    args = parser.parse_args(argv)  # In this example we won't use the args
356
357    if not argv:
358        print("No args given!")
359        parser.print_help()
360        return
361
362    if args.dump:
363        api_dump()
364    else:
365        if args.api_from and args.api_to and args.api_out:
366            api_changelog(args.api_from, args.api_to, args.api_out)
367        else:
368            print("Error: --api_from/api_to/api_out args needed")
369            parser.print_help()
370            return
371
372    print("batch job finished, exiting")
373
374
375if __name__ == "__main__":
376    main()
377