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