sphinx_changelog_gen.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. # <pep8 compliant>
  19. """
  20. Dump the python API into a text file so we can generate changelogs.
  21. output from this tool should be added into "doc/python_api/rst/change_log.rst"
  22. # dump api blender_version.py in CWD
  23. blender --background --python doc/python_api/sphinx_changelog_gen.py -- --dump
  24. # create changelog
  25. blender --background --factory-startup --python doc/python_api/sphinx_changelog_gen.py -- \
  26. --api_from blender_2_63_0.py \
  27. --api_to blender_2_64_0.py \
  28. --api_out changes.rst
  29. # Api comparison can also run without blender
  30. python doc/python_api/sphinx_changelog_gen.py -- \
  31. --api_from blender_api_2_63_0.py \
  32. --api_to blender_api_2_64_0.py \
  33. --api_out changes.rst
  34. # Save the latest API dump in this folder, renaming it with its revision.
  35. # This way the next person updating it doesn't need to build an old Blender only for that
  36. """
  37. # format
  38. '''
  39. {"module.name":
  40. {"parent.class":
  41. {"basic_type", "member_name":
  42. ("Name", type, range, length, default, descr, f_args, f_arg_types, f_ret_types)}, ...
  43. }, ...
  44. }
  45. '''
  46. api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types"
  47. API_BASIC_TYPE = 0
  48. API_F_ARGS = 7
  49. def api_dunp_fname():
  50. import bpy
  51. return "blender_api_%s.py" % "_".join([str(i) for i in bpy.app.version])
  52. def api_dump():
  53. dump = {}
  54. dump_module = dump["bpy.types"] = {}
  55. import rna_info
  56. import inspect
  57. struct = rna_info.BuildRNAInfo()[0]
  58. for struct_id, struct_info in sorted(struct.items()):
  59. struct_id_str = struct_info.identifier
  60. if rna_info.rna_id_ignore(struct_id_str):
  61. continue
  62. for base in struct_info.get_bases():
  63. struct_id_str = base.identifier + "." + struct_id_str
  64. dump_class = dump_module[struct_id_str] = {}
  65. props = [(prop.identifier, prop) for prop in struct_info.properties]
  66. for prop_id, prop in sorted(props):
  67. # if prop.type == 'boolean':
  68. # continue
  69. prop_type = prop.type
  70. prop_length = prop.array_length
  71. prop_range = round(prop.min, 4), round(prop.max, 4)
  72. prop_default = prop.default
  73. if type(prop_default) is float:
  74. prop_default = round(prop_default, 4)
  75. if prop_range[0] == -1 and prop_range[1] == -1:
  76. prop_range = None
  77. dump_class[prop_id] = (
  78. "prop_rna", # basic_type
  79. prop.name, # name
  80. prop_type, # type
  81. prop_range, # range
  82. prop_length, # length
  83. prop.default, # default
  84. prop.description, # descr
  85. Ellipsis, # f_args
  86. Ellipsis, # f_arg_types
  87. Ellipsis, # f_ret_types
  88. )
  89. del props
  90. # python props, tricky since we don't know much about them.
  91. for prop_id, attr in struct_info.get_py_properties():
  92. dump_class[prop_id] = (
  93. "prop_py", # basic_type
  94. Ellipsis, # name
  95. Ellipsis, # type
  96. Ellipsis, # range
  97. Ellipsis, # length
  98. Ellipsis, # default
  99. attr.__doc__, # descr
  100. Ellipsis, # f_args
  101. Ellipsis, # f_arg_types
  102. Ellipsis, # f_ret_types
  103. )
  104. # kludge func -> props
  105. funcs = [(func.identifier, func) for func in struct_info.functions]
  106. for func_id, func in funcs:
  107. func_ret_types = tuple([prop.type for prop in func.return_values])
  108. func_args_ids = tuple([prop.identifier for prop in func.args])
  109. func_args_type = tuple([prop.type for prop in func.args])
  110. dump_class[func_id] = (
  111. "func_rna", # basic_type
  112. Ellipsis, # name
  113. Ellipsis, # type
  114. Ellipsis, # range
  115. Ellipsis, # length
  116. Ellipsis, # default
  117. func.description, # descr
  118. func_args_ids, # f_args
  119. func_args_type, # f_arg_types
  120. func_ret_types, # f_ret_types
  121. )
  122. del funcs
  123. # kludge func -> props
  124. funcs = struct_info.get_py_functions()
  125. for func_id, attr in funcs:
  126. # arg_str = inspect.formatargspec(*inspect.getargspec(py_func))
  127. sig = inspect.signature(attr)
  128. func_args_ids = [k for k, v in sig.parameters.items()]
  129. dump_class[func_id] = (
  130. "func_py", # basic_type
  131. Ellipsis, # name
  132. Ellipsis, # type
  133. Ellipsis, # range
  134. Ellipsis, # length
  135. Ellipsis, # default
  136. attr.__doc__, # descr
  137. func_args_ids, # f_args
  138. Ellipsis, # f_arg_types
  139. Ellipsis, # f_ret_types
  140. )
  141. del funcs
  142. import pprint
  143. filename = api_dunp_fname()
  144. filehandle = open(filename, 'w', encoding='utf-8')
  145. tot = filehandle.write(pprint.pformat(dump, width=1))
  146. filehandle.close()
  147. print("%s, %d bytes written" % (filename, tot))
  148. def compare_props(a, b, fuzz=0.75):
  149. # must be same basic_type, function != property
  150. if a[0] != b[0]:
  151. return False
  152. tot = 0
  153. totlen = 0
  154. for i in range(1, len(a)):
  155. if not (Ellipsis is a[i] is b[i]):
  156. tot += (a[i] == b[i])
  157. totlen += 1
  158. return ((tot / totlen) >= fuzz)
  159. def api_changelog(api_from, api_to, api_out):
  160. file_handle = open(api_from, 'r', encoding='utf-8')
  161. dict_from = eval(file_handle.read())
  162. file_handle.close()
  163. file_handle = open(api_to, 'r', encoding='utf-8')
  164. dict_to = eval(file_handle.read())
  165. file_handle.close()
  166. api_changes = []
  167. # first work out what moved
  168. for mod_id, mod_data in dict_to.items():
  169. mod_data_other = dict_from[mod_id]
  170. for class_id, class_data in mod_data.items():
  171. class_data_other = mod_data_other.get(class_id)
  172. if class_data_other is None:
  173. # TODO, document new structs
  174. continue
  175. # find the props which are not in either
  176. set_props_new = set(class_data.keys())
  177. set_props_other = set(class_data_other.keys())
  178. set_props_shared = set_props_new & set_props_other
  179. props_moved = []
  180. props_new = []
  181. props_old = []
  182. func_args = []
  183. set_props_old = set_props_other - set_props_shared
  184. set_props_new = set_props_new - set_props_shared
  185. # first find settings which have been moved old -> new
  186. for prop_id_old in set_props_old.copy():
  187. prop_data_other = class_data_other[prop_id_old]
  188. for prop_id_new in set_props_new.copy():
  189. prop_data = class_data[prop_id_new]
  190. if compare_props(prop_data_other, prop_data):
  191. props_moved.append((prop_id_old, prop_id_new))
  192. # remove
  193. if prop_id_old in set_props_old:
  194. set_props_old.remove(prop_id_old)
  195. set_props_new.remove(prop_id_new)
  196. # func args
  197. for prop_id in set_props_shared:
  198. prop_data = class_data[prop_id]
  199. prop_data_other = class_data_other[prop_id]
  200. if prop_data[API_BASIC_TYPE] == prop_data_other[API_BASIC_TYPE]:
  201. if prop_data[API_BASIC_TYPE].startswith("func"):
  202. args_new = prop_data[API_F_ARGS]
  203. args_old = prop_data_other[API_F_ARGS]
  204. if args_new != args_old:
  205. func_args.append((prop_id, args_old, args_new))
  206. if props_moved or set_props_new or set_props_old or func_args:
  207. props_moved.sort()
  208. props_new[:] = sorted(set_props_new)
  209. props_old[:] = sorted(set_props_old)
  210. func_args.sort()
  211. api_changes.append((mod_id, class_id, props_moved, props_new, props_old, func_args))
  212. # also document function argument changes
  213. fout = open(api_out, 'w', encoding='utf-8')
  214. fw = fout.write
  215. # print(api_changes)
  216. # :class:`bpy_struct.id_data`
  217. def write_title(title, title_char):
  218. fw("%s\n%s\n\n" % (title, title_char * len(title)))
  219. for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes:
  220. class_name = class_id.split(".")[-1]
  221. title = mod_id + "." + class_name
  222. write_title(title, "-")
  223. if props_new:
  224. write_title("Added", "^")
  225. for prop_id in props_new:
  226. fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id))
  227. fw("\n")
  228. if props_old:
  229. write_title("Removed", "^")
  230. for prop_id in props_old:
  231. fw("* **%s**\n" % prop_id) # can't link to removed docs
  232. fw("\n")
  233. if props_moved:
  234. write_title("Renamed", "^")
  235. for prop_id_old, prop_id in props_moved:
  236. fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id))
  237. fw("\n")
  238. if func_args:
  239. write_title("Function Arguments", "^")
  240. for func_id, args_old, args_new in func_args:
  241. args_new = ", ".join(args_new)
  242. args_old = ", ".join(args_old)
  243. fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, func_id, args_new, args_old))
  244. fw("\n")
  245. fout.close()
  246. print("Written: %r" % api_out)
  247. def main():
  248. import sys
  249. import os
  250. try:
  251. import argparse
  252. except ImportError:
  253. print("Old Blender, just dumping")
  254. api_dump()
  255. return
  256. argv = sys.argv
  257. if "--" not in argv:
  258. argv = [] # as if no args are passed
  259. else:
  260. argv = argv[argv.index("--") + 1:] # get all args after "--"
  261. # When --help or no args are given, print this help
  262. usage_text = "Run blender in background mode with this script: "
  263. "blender --background --factory-startup --python %s -- [options]" % os.path.basename(__file__)
  264. epilog = "Run this before releases"
  265. parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
  266. parser.add_argument(
  267. "--dump", dest="dump", action='store_true',
  268. help="When set the api will be dumped into blender_version.py")
  269. parser.add_argument(
  270. "--api_from", dest="api_from", metavar='FILE',
  271. help="File to compare from (previous version)")
  272. parser.add_argument(
  273. "--api_to", dest="api_to", metavar='FILE',
  274. help="File to compare from (current)")
  275. parser.add_argument(
  276. "--api_out", dest="api_out", metavar='FILE',
  277. help="Output sphinx changelog")
  278. args = parser.parse_args(argv) # In this example we won't use the args
  279. if not argv:
  280. print("No args given!")
  281. parser.print_help()
  282. return
  283. if args.dump:
  284. api_dump()
  285. else:
  286. if args.api_from and args.api_to and args.api_out:
  287. api_changelog(args.api_from, args.api_to, args.api_out)
  288. else:
  289. print("Error: --api_from/api_to/api_out args needed")
  290. parser.print_help()
  291. return
  292. print("batch job finished, exiting")
  293. if __name__ == "__main__":
  294. main()