368 lines
13 KiB
Python
368 lines
13 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# <pep8 compliant>
|
|
|
|
"""
|
|
Dump the python API into a text file so we can generate changelogs.
|
|
|
|
output from this tool should be added into "doc/python_api/rst/change_log.rst"
|
|
|
|
# dump api blender_version.py in CWD
|
|
blender --background --python doc/python_api/sphinx_changelog_gen.py -- --dump
|
|
|
|
# create changelog
|
|
blender --background --python doc/python_api/sphinx_changelog_gen.py -- \
|
|
--api_from blender_2_56_1.py \
|
|
--api_to blender_2_57_0.py \
|
|
--api_out changes.rst
|
|
|
|
|
|
# Api comparison can also run without blender
|
|
python doc/python_api/sphinx_changelog_gen.py \
|
|
--api_from blender_api_2_56_6.py \
|
|
--api_to blender_api_2_57.py \
|
|
--api_out changes.rst
|
|
|
|
# Save the latest API dump in this folder, renaming it with its revision.
|
|
# This way the next person updating it doesn't need to build an old Blender only for that
|
|
|
|
"""
|
|
|
|
# format
|
|
'''
|
|
{"module.name":
|
|
{"parent.class":
|
|
{"basic_type", "member_name": ("Name", type, range, length, default, descr, f_args, f_arg_types, f_ret_types)}, ...
|
|
}, ...
|
|
}
|
|
'''
|
|
|
|
api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types"
|
|
|
|
API_BASIC_TYPE = 0
|
|
API_F_ARGS = 7
|
|
|
|
|
|
def api_dunp_fname():
|
|
import bpy
|
|
return "blender_api_%s.py" % "_".join([str(i) for i in bpy.app.version])
|
|
|
|
|
|
def api_dump():
|
|
dump = {}
|
|
dump_module = dump["bpy.types"] = {}
|
|
|
|
import rna_info
|
|
import inspect
|
|
|
|
struct = rna_info.BuildRNAInfo()[0]
|
|
for struct_id, strict_info in sorted(struct.items()):
|
|
|
|
struct_id_str = strict_info.identifier
|
|
|
|
if rna_info.rna_id_ignore(struct_id_str):
|
|
continue
|
|
|
|
for base in strict_info.get_bases():
|
|
struct_id_str = base.identifier + "." + struct_id_str
|
|
|
|
dump_class = dump_module[struct_id_str] = {}
|
|
|
|
props = [(prop.identifier, prop) for prop in strict_info.properties]
|
|
for prop_id, prop in sorted(props):
|
|
# if prop.type == 'boolean':
|
|
# continue
|
|
prop_type = prop.type
|
|
prop_length = prop.array_length
|
|
prop_range = round(prop.min, 4), round(prop.max, 4)
|
|
prop_default = prop.default
|
|
if type(prop_default) is float:
|
|
prop_default = round(prop_default, 4)
|
|
|
|
if prop_range[0] == -1 and prop_range[1] == -1:
|
|
prop_range = None
|
|
|
|
dump_class[prop_id] = (
|
|
"prop_rna", # basic_type
|
|
prop.name, # name
|
|
prop_type, # type
|
|
prop_range, # range
|
|
prop_length, # length
|
|
prop.default, # default
|
|
prop.description, # descr
|
|
Ellipsis, # f_args
|
|
Ellipsis, # f_arg_types
|
|
Ellipsis, # f_ret_types
|
|
)
|
|
del props
|
|
|
|
# python props, tricky since we dont know much about them.
|
|
for prop_id, attr in strict_info.get_py_properties():
|
|
|
|
dump_class[prop_id] = (
|
|
"prop_py", # basic_type
|
|
Ellipsis, # name
|
|
Ellipsis, # type
|
|
Ellipsis, # range
|
|
Ellipsis, # length
|
|
Ellipsis, # default
|
|
attr.__doc__, # descr
|
|
Ellipsis, # f_args
|
|
Ellipsis, # f_arg_types
|
|
Ellipsis, # f_ret_types
|
|
)
|
|
|
|
# kludge func -> props
|
|
funcs = [(func.identifier, func) for func in strict_info.functions]
|
|
for func_id, func in funcs:
|
|
|
|
func_ret_types = tuple([prop.type for prop in func.return_values])
|
|
func_args_ids = tuple([prop.identifier for prop in func.args])
|
|
func_args_type = tuple([prop.type for prop in func.args])
|
|
|
|
dump_class[func_id] = (
|
|
"func_rna", # basic_type
|
|
Ellipsis, # name
|
|
Ellipsis, # type
|
|
Ellipsis, # range
|
|
Ellipsis, # length
|
|
Ellipsis, # default
|
|
func.description, # descr
|
|
func_args_ids, # f_args
|
|
func_args_type, # f_arg_types
|
|
func_ret_types, # f_ret_types
|
|
)
|
|
del funcs
|
|
|
|
# kludge func -> props
|
|
funcs = strict_info.get_py_functions()
|
|
for func_id, attr in funcs:
|
|
# arg_str = inspect.formatargspec(*inspect.getargspec(py_func))
|
|
|
|
func_args_ids = tuple(inspect.getargspec(attr).args)
|
|
|
|
dump_class[func_id] = (
|
|
"func_py", # basic_type
|
|
Ellipsis, # name
|
|
Ellipsis, # type
|
|
Ellipsis, # range
|
|
Ellipsis, # length
|
|
Ellipsis, # default
|
|
attr.__doc__, # descr
|
|
func_args_ids, # f_args
|
|
Ellipsis, # f_arg_types
|
|
Ellipsis, # f_ret_types
|
|
)
|
|
del funcs
|
|
|
|
import pprint
|
|
|
|
filename = api_dunp_fname()
|
|
filehandle = open(filename, 'w')
|
|
tot = filehandle.write(pprint.pformat(dump, width=1))
|
|
filehandle.close()
|
|
print("%s, %d bytes written" % (filename, tot))
|
|
|
|
|
|
def compare_props(a, b, fuzz=0.75):
|
|
|
|
# must be same basic_type, function != property
|
|
if a[0] != b[0]:
|
|
return False
|
|
|
|
tot = 0
|
|
totlen = 0
|
|
for i in range(1, len(a)):
|
|
if not (Ellipsis is a[i] is b[i]):
|
|
tot += (a[i] == b[i])
|
|
totlen += 1
|
|
|
|
return ((tot / totlen) >= fuzz)
|
|
|
|
|
|
def api_changelog(api_from, api_to, api_out):
|
|
|
|
file_handle = open(api_from, 'r')
|
|
dict_from = eval(file_handle.read())
|
|
file_handle.close()
|
|
|
|
file_handle = open(api_to, 'r')
|
|
dict_to = eval(file_handle.read())
|
|
file_handle.close()
|
|
|
|
api_changes = []
|
|
|
|
# first work out what moved
|
|
for mod_id, mod_data in dict_to.items():
|
|
mod_data_other = dict_from[mod_id]
|
|
for class_id, class_data in mod_data.items():
|
|
class_data_other = mod_data_other.get(class_id)
|
|
if class_data_other is None:
|
|
# TODO, document new structs
|
|
continue
|
|
|
|
# find the props which are not in either
|
|
set_props_new = set(class_data.keys())
|
|
set_props_other = set(class_data_other.keys())
|
|
set_props_shared = set_props_new & set_props_other
|
|
|
|
props_moved = []
|
|
props_new = []
|
|
props_old = []
|
|
func_args = []
|
|
|
|
set_props_old = set_props_other - set_props_shared
|
|
set_props_new = set_props_new - set_props_shared
|
|
|
|
# first find settings which have been moved old -> new
|
|
for prop_id_old in set_props_old.copy():
|
|
prop_data_other = class_data_other[prop_id_old]
|
|
for prop_id_new in set_props_new.copy():
|
|
prop_data = class_data[prop_id_new]
|
|
if compare_props(prop_data_other, prop_data):
|
|
props_moved.append((prop_id_old, prop_id_new))
|
|
|
|
# remove
|
|
if prop_id_old in set_props_old:
|
|
set_props_old.remove(prop_id_old)
|
|
set_props_new.remove(prop_id_new)
|
|
|
|
# func args
|
|
for prop_id in set_props_shared:
|
|
prop_data = class_data[prop_id]
|
|
prop_data_other = class_data_other[prop_id]
|
|
if prop_data[API_BASIC_TYPE] == prop_data_other[API_BASIC_TYPE]:
|
|
if prop_data[API_BASIC_TYPE].startswith("func"):
|
|
args_new = prop_data[API_F_ARGS]
|
|
args_old = prop_data_other[API_F_ARGS]
|
|
|
|
if args_new != args_old:
|
|
func_args.append((prop_id, args_old, args_new))
|
|
|
|
if props_moved or set_props_new or set_props_old or func_args:
|
|
props_moved.sort()
|
|
props_new[:] = sorted(set_props_new)
|
|
props_old[:] = sorted(set_props_old)
|
|
func_args.sort()
|
|
|
|
api_changes.append((mod_id, class_id, props_moved, props_new, props_old, func_args))
|
|
|
|
# also document function argument changes
|
|
|
|
fout = open(api_out, 'w')
|
|
fw = fout.write
|
|
# print(api_changes)
|
|
|
|
# :class:`bpy_struct.id_data`
|
|
|
|
def write_title(title, title_char):
|
|
fw("%s\n%s\n\n" % (title, title_char * len(title)))
|
|
|
|
for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes:
|
|
class_name = class_id.split(".")[-1]
|
|
title = mod_id + "." + class_name
|
|
write_title(title, "-")
|
|
|
|
if props_new:
|
|
write_title("Added", "^")
|
|
for prop_id in props_new:
|
|
fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id))
|
|
fw("\n")
|
|
|
|
if props_old:
|
|
write_title("Removed", "^")
|
|
for prop_id in props_old:
|
|
fw("* **%s**\n" % prop_id) # cant link to remvoed docs
|
|
fw("\n")
|
|
|
|
if props_moved:
|
|
write_title("Renamed", "^")
|
|
for prop_id_old, prop_id in props_moved:
|
|
fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id))
|
|
fw("\n")
|
|
|
|
if func_args:
|
|
write_title("Function Arguments", "^")
|
|
for func_id, args_old, args_new in func_args:
|
|
args_new = ", ".join(args_new)
|
|
args_old = ", ".join(args_old)
|
|
fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, func_id, args_new, args_old))
|
|
fw("\n")
|
|
|
|
fout.close()
|
|
|
|
|
|
def main():
|
|
import sys
|
|
import os
|
|
|
|
try:
|
|
import argparse
|
|
except:
|
|
print("Old Blender, just dumping")
|
|
api_dump()
|
|
return
|
|
|
|
argv = sys.argv
|
|
|
|
if "--" not in argv:
|
|
argv = [] # as if no args are passed
|
|
else:
|
|
argv = argv[argv.index("--") + 1:] # get all args after "--"
|
|
|
|
# When --help or no args are given, print this help
|
|
usage_text = "Run blender in background mode with this script: "
|
|
"blender --background --python %s -- [options]" % os.path.basename(__file__)
|
|
|
|
epilog = "Run this before releases"
|
|
|
|
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
|
|
|
parser.add_argument("--dump", dest="dump", action='store_true',
|
|
help="When set the api will be dumped into blender_version.py")
|
|
|
|
parser.add_argument("--api_from", dest="api_from", metavar='FILE',
|
|
help="File to compare from (previous version)")
|
|
parser.add_argument("--api_to", dest="api_to", metavar='FILE',
|
|
help="File to compare from (current)")
|
|
parser.add_argument("--api_out", dest="api_out", metavar='FILE',
|
|
help="Output sphinx changelog")
|
|
|
|
args = parser.parse_args(argv) # In this example we wont use the args
|
|
|
|
if not argv:
|
|
parser.print_help()
|
|
return
|
|
|
|
if args.dump:
|
|
api_dump()
|
|
else:
|
|
if args.api_from and args.api_to and args.api_out:
|
|
api_changelog(args.api_from, args.api_to, args.api_out)
|
|
else:
|
|
print("Error: --api_from/api_to/api_out args needed")
|
|
parser.print_help()
|
|
return
|
|
|
|
print("batch job finished, exiting")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|