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 21SCRIPT_HELP_MSG = """ 22 23API dump in RST files 24--------------------- 25 Run this script from Blender's root path once you have compiled Blender 26 27 blender --background --factory-startup -noaudio --python doc/python_api/sphinx_doc_gen.py 28 29 This will generate python files in doc/python_api/sphinx-in/ 30 providing ./blender is or links to the blender executable 31 32 To choose sphinx-in directory: 33 blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --output ../python_api 34 35 For quick builds: 36 blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --partial bmesh.* 37 38 39Sphinx: HTML generation 40----------------------- 41 After you have built doc/python_api/sphinx-in (see above), 42 generate html docs by running: 43 44 sphinx-build doc/python_api/sphinx-in doc/python_api/sphinx-out 45 46 47Sphinx: PDF generation 48---------------------- 49 After you have built doc/python_api/sphinx-in (see above), 50 generate the pdf doc by running: 51 52 sphinx-build -b latex doc/python_api/sphinx-in doc/python_api/sphinx-out 53 cd doc/python_api/sphinx-out 54 make 55 56""" 57 58try: 59 import bpy # Blender module 60except ImportError: 61 print("\nERROR: this script must run from inside Blender") 62 print(SCRIPT_HELP_MSG) 63 import sys 64 sys.exit() 65 66import rna_info # Blender module 67 68 69def rna_info_BuildRNAInfo_cache(): 70 if rna_info_BuildRNAInfo_cache.ret is None: 71 rna_info_BuildRNAInfo_cache.ret = rna_info.BuildRNAInfo() 72 return rna_info_BuildRNAInfo_cache.ret 73 74 75rna_info_BuildRNAInfo_cache.ret = None 76# --- end rna_info cache 77 78# import rpdb2; rpdb2.start_embedded_debugger('test') 79import os 80import sys 81import inspect 82import shutil 83import logging 84 85from textwrap import indent 86 87from platform import platform 88PLATFORM = platform().split('-')[0].lower() # 'linux', 'darwin', 'windows' 89 90SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) 91 92# For now, ignore add-ons and internal subclasses of 'bpy.types.PropertyGroup'. 93# 94# Besides disabling this line, the main change will be to add a 95# 'toctree' to 'write_rst_index' which contains the generated rst files. 96# This 'toctree' can be generated automatically. 97# 98# See: D6261 for reference. 99USE_ONLY_BUILTIN_RNA_TYPES = True 100 101 102def handle_args(): 103 ''' 104 Parse the args passed to Blender after "--", ignored by Blender 105 ''' 106 import argparse 107 108 # When --help is given, print the usage text 109 parser = argparse.ArgumentParser( 110 formatter_class=argparse.RawTextHelpFormatter, 111 usage=SCRIPT_HELP_MSG 112 ) 113 114 # optional arguments 115 parser.add_argument("-p", "--partial", 116 dest="partial", 117 type=str, 118 default="", 119 help="Use a wildcard to only build specific module(s)\n" 120 "Example: --partial bmesh*\n", 121 required=False) 122 123 parser.add_argument("-f", "--fullrebuild", 124 dest="full_rebuild", 125 default=False, 126 action='store_true', 127 help="Rewrite all rst files in sphinx-in/ " 128 "(default=False)", 129 required=False) 130 131 parser.add_argument("-b", "--bpy", 132 dest="bpy", 133 default=False, 134 action='store_true', 135 help="Write the rst file of the bpy module " 136 "(default=False)", 137 required=False) 138 139 parser.add_argument("-o", "--output", 140 dest="output_dir", 141 type=str, 142 default=SCRIPT_DIR, 143 help="Path of the API docs (default=<script dir>)", 144 required=False) 145 146 parser.add_argument("-B", "--sphinx-build", 147 dest="sphinx_build", 148 default=False, 149 action='store_true', 150 help="Build the html docs by running:\n" 151 "sphinx-build SPHINX_IN SPHINX_OUT\n" 152 "(default=False; does not depend on -P)", 153 required=False) 154 155 parser.add_argument("-P", "--sphinx-build-pdf", 156 dest="sphinx_build_pdf", 157 default=False, 158 action='store_true', 159 help="Build the pdf by running:\n" 160 "sphinx-build -b latex SPHINX_IN SPHINX_OUT_PDF\n" 161 "(default=False; does not depend on -B)", 162 required=False) 163 164 parser.add_argument("-R", "--pack-reference", 165 dest="pack_reference", 166 default=False, 167 action='store_true', 168 help="Pack all necessary files in the deployed dir.\n" 169 "(default=False; use with -B and -P)", 170 required=False) 171 172 parser.add_argument("-l", "--log", 173 dest="log", 174 default=False, 175 action='store_true', 176 help="Log the output of the API dump and sphinx|latex " 177 "warnings and errors (default=False).\n" 178 "If given, save logs in:\n" 179 "* OUTPUT_DIR/.bpy.log\n" 180 "* OUTPUT_DIR/.sphinx-build.log\n" 181 "* OUTPUT_DIR/.sphinx-build_pdf.log\n" 182 "* OUTPUT_DIR/.latex_make.log", 183 required=False) 184 185 # parse only the args passed after '--' 186 argv = [] 187 if "--" in sys.argv: 188 argv = sys.argv[sys.argv.index("--") + 1:] # get all args after "--" 189 190 return parser.parse_args(argv) 191 192 193ARGS = handle_args() 194 195# ----------------------------------BPY----------------------------------------- 196 197BPY_LOGGER = logging.getLogger('bpy') 198BPY_LOGGER.setLevel(logging.DEBUG) 199 200""" 201# for quick rebuilds 202rm -rf /b/doc/python_api/sphinx-* && \ 203./blender -b -noaudio --factory-startup -P doc/python_api/sphinx_doc_gen.py && \ 204sphinx-build doc/python_api/sphinx-in doc/python_api/sphinx-out 205 206or 207 208./blender -b -noaudio --factory-startup -P doc/python_api/sphinx_doc_gen.py -- -f -B 209""" 210 211# Switch for quick testing so doc-builds don't take so long 212if not ARGS.partial: 213 # full build 214 FILTER_BPY_OPS = None 215 FILTER_BPY_TYPES = None 216 EXCLUDE_INFO_DOCS = False 217 EXCLUDE_MODULES = [] 218 219else: 220 # can manually edit this too: 221 # FILTER_BPY_OPS = ("import.scene", ) # allow 222 # FILTER_BPY_TYPES = ("bpy_struct", "Operator", "ID") # allow 223 EXCLUDE_INFO_DOCS = True 224 EXCLUDE_MODULES = [ 225 "aud", 226 "bgl", 227 "blf", 228 "bl_math", 229 "imbuf", 230 "bmesh", 231 "bmesh.ops", 232 "bmesh.types", 233 "bmesh.utils", 234 "bmesh.geometry", 235 "bpy.app", 236 "bpy.app.handlers", 237 "bpy.app.timers", 238 "bpy.app.translations", 239 "bpy.context", 240 "bpy.data", 241 "bpy.ops", # supports filtering 242 "bpy.path", 243 "bpy.props", 244 "bpy.types", # supports filtering 245 "bpy.utils", 246 "bpy.utils.previews", 247 "bpy.utils.units", 248 "bpy_extras", 249 "gpu", 250 "gpu.types", 251 "gpu.matrix", 252 "gpu.select", 253 "gpu_extras", 254 "idprop.types", 255 "mathutils", 256 "mathutils.bvhtree", 257 "mathutils.geometry", 258 "mathutils.interpolate", 259 "mathutils.kdtree", 260 "mathutils.noise", 261 "freestyle", 262 "freestyle.chainingiterators", 263 "freestyle.functions", 264 "freestyle.predicates", 265 "freestyle.shaders", 266 "freestyle.types", 267 "freestyle.utils", 268 ] 269 270 # ------ 271 # Filter 272 # 273 # TODO, support bpy.ops and bpy.types filtering 274 import fnmatch 275 m = None 276 EXCLUDE_MODULES = [m for m in EXCLUDE_MODULES if not fnmatch.fnmatchcase(m, ARGS.partial)] 277 278 # special support for bpy.types.XXX 279 FILTER_BPY_OPS = tuple([m[8:] for m in ARGS.partial.split(":") if m.startswith("bpy.ops.")]) 280 if FILTER_BPY_OPS: 281 EXCLUDE_MODULES.remove("bpy.ops") 282 283 FILTER_BPY_TYPES = tuple([m[10:] for m in ARGS.partial.split(":") if m.startswith("bpy.types.")]) 284 if FILTER_BPY_TYPES: 285 EXCLUDE_MODULES.remove("bpy.types") 286 287 print(FILTER_BPY_TYPES) 288 289 EXCLUDE_INFO_DOCS = (not fnmatch.fnmatchcase("info", ARGS.partial)) 290 291 del m 292 del fnmatch 293 294 BPY_LOGGER.debug( 295 "Partial Doc Build, Skipping: %s\n" % 296 "\n ".join(sorted(EXCLUDE_MODULES))) 297 298 # 299 # done filtering 300 # -------------- 301 302try: 303 __import__("aud") 304except ImportError: 305 BPY_LOGGER.debug("Warning: Built without 'aud' module, docs incomplete...") 306 EXCLUDE_MODULES.append("aud") 307 308try: 309 __import__("freestyle") 310except ImportError: 311 BPY_LOGGER.debug("Warning: Built without 'freestyle' module, docs incomplete...") 312 EXCLUDE_MODULES.extend([ 313 "freestyle", 314 "freestyle.chainingiterators", 315 "freestyle.functions", 316 "freestyle.predicates", 317 "freestyle.shaders", 318 "freestyle.types", 319 "freestyle.utils", 320 ]) 321 322# Source files we use, and need to copy to the OUTPUT_DIR 323# to have working out-of-source builds. 324# Note that ".." is replaced by "__" in the RST files, 325# to avoid having to match Blender's source tree. 326EXTRA_SOURCE_FILES = ( 327 "../../../release/scripts/templates_py/bmesh_simple.py", 328 "../../../release/scripts/templates_py/gizmo_operator.py", 329 "../../../release/scripts/templates_py/gizmo_operator_target.py", 330 "../../../release/scripts/templates_py/gizmo_simple.py", 331 "../../../release/scripts/templates_py/operator_simple.py", 332 "../../../release/scripts/templates_py/ui_panel_simple.py", 333 "../../../release/scripts/templates_py/ui_previews_custom_icon.py", 334 "../examples/bmesh.ops.1.py", 335 "../examples/bpy.app.translations.py", 336) 337 338 339# examples 340EXAMPLES_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "examples")) 341EXAMPLE_SET = set() 342for f in os.listdir(EXAMPLES_DIR): 343 if f.endswith(".py"): 344 EXAMPLE_SET.add(os.path.splitext(f)[0]) 345EXAMPLE_SET_USED = set() 346 347# rst files dir 348RST_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "rst")) 349 350# extra info, not api reference docs 351# stored in ./rst/info_* 352INFO_DOCS = ( 353 ("info_quickstart.rst", 354 "Quickstart: New to Blender or scripting and want to get your feet wet?"), 355 ("info_overview.rst", 356 "API Overview: A more complete explanation of Python integration"), 357 ("info_api_reference.rst", 358 "API Reference Usage: examples of how to use the API reference docs"), 359 ("info_best_practice.rst", 360 "Best Practice: Conventions to follow for writing good scripts"), 361 ("info_tips_and_tricks.rst", 362 "Tips and Tricks: Hints to help you while writing scripts for Blender"), 363 ("info_gotcha.rst", 364 "Gotcha's: Some of the problems you may encounter when writing scripts"), 365 ("change_log.rst", "Change Log: List of changes since last Blender release"), 366) 367 368# only support for properties atm. 369RNA_BLACKLIST = { 370 # XXX messes up PDF!, really a bug but for now just workaround. 371 "PreferencesSystem": {"language", } 372} 373 374MODULE_GROUPING = { 375 "bmesh.types": ( 376 ("Base Mesh Type", '-'), 377 "BMesh", 378 ("Mesh Elements", '-'), 379 "BMVert", 380 "BMEdge", 381 "BMFace", 382 "BMLoop", 383 ("Sequence Accessors", '-'), 384 "BMElemSeq", 385 "BMVertSeq", 386 "BMEdgeSeq", 387 "BMFaceSeq", 388 "BMLoopSeq", 389 "BMIter", 390 ("Selection History", '-'), 391 "BMEditSelSeq", 392 "BMEditSelIter", 393 ("Custom-Data Layer Access", '-'), 394 "BMLayerAccessVert", 395 "BMLayerAccessEdge", 396 "BMLayerAccessFace", 397 "BMLayerAccessLoop", 398 "BMLayerCollection", 399 "BMLayerItem", 400 ("Custom-Data Layer Types", '-'), 401 "BMLoopUV", 402 "BMDeformVert" 403 ) 404} 405 406# --------------------configure compile time options---------------------------- 407 408# -------------------------------BLENDER---------------------------------------- 409 410# converting bytes to strings, due to T30154 411BLENDER_REVISION = str(bpy.app.build_hash, 'utf_8') 412 413# '2.83.0 Beta' or '2.83.0' or '2.83.1' 414BLENDER_VERSION_DOTS = bpy.app.version_string 415 416if BLENDER_REVISION != "Unknown": 417 # SHA1 Git hash 418 BLENDER_VERSION_HASH = BLENDER_REVISION 419else: 420 # Fallback: Should not be used 421 BLENDER_VERSION_HASH = "Hash Unknown" 422 423# '2_83' 424BLENDER_VERSION_PATH = "%d_%d" % (bpy.app.version[0], bpy.app.version[1]) 425 426# --------------------------DOWNLOADABLE FILES---------------------------------- 427 428REFERENCE_NAME = "blender_python_reference_%s" % BLENDER_VERSION_PATH 429REFERENCE_PATH = os.path.join(ARGS.output_dir, REFERENCE_NAME) 430BLENDER_PDF_FILENAME = "%s.pdf" % REFERENCE_NAME 431BLENDER_ZIP_FILENAME = "%s.zip" % REFERENCE_NAME 432 433# -------------------------------SPHINX----------------------------------------- 434 435SPHINX_IN = os.path.join(ARGS.output_dir, "sphinx-in") 436SPHINX_IN_TMP = SPHINX_IN + "-tmp" 437SPHINX_OUT = os.path.join(ARGS.output_dir, "sphinx-out") 438 439# html build 440if ARGS.sphinx_build: 441 SPHINX_BUILD = ["sphinx-build", SPHINX_IN, SPHINX_OUT] 442 443 if ARGS.log: 444 SPHINX_BUILD_LOG = os.path.join(ARGS.output_dir, ".sphinx-build.log") 445 SPHINX_BUILD = [ 446 "sphinx-build", 447 "-w", SPHINX_BUILD_LOG, 448 SPHINX_IN, SPHINX_OUT, 449 ] 450 451# pdf build 452if ARGS.sphinx_build_pdf: 453 SPHINX_OUT_PDF = os.path.join(ARGS.output_dir, "sphinx-out_pdf") 454 SPHINX_BUILD_PDF = [ 455 "sphinx-build", 456 "-b", "latex", 457 SPHINX_IN, SPHINX_OUT_PDF, 458 ] 459 SPHINX_MAKE_PDF = ["make", "-C", SPHINX_OUT_PDF] 460 SPHINX_MAKE_PDF_STDOUT = None 461 462 if ARGS.log: 463 SPHINX_BUILD_PDF_LOG = os.path.join(ARGS.output_dir, ".sphinx-build_pdf.log") 464 SPHINX_BUILD_PDF = [ 465 "sphinx-build", "-b", "latex", 466 "-w", SPHINX_BUILD_PDF_LOG, 467 SPHINX_IN, SPHINX_OUT_PDF, 468 ] 469 sphinx_make_pdf_log = os.path.join(ARGS.output_dir, ".latex_make.log") 470 SPHINX_MAKE_PDF_STDOUT = open(sphinx_make_pdf_log, "w", encoding="utf-8") 471 472# --------------------------------API DUMP-------------------------------------- 473 474# lame, python won't give some access 475ClassMethodDescriptorType = type(dict.__dict__['fromkeys']) 476MethodDescriptorType = type(dict.get) 477GetSetDescriptorType = type(int.real) 478StaticMethodType = type(staticmethod(lambda: None)) 479from types import ( 480 MemberDescriptorType, 481 MethodType, 482 FunctionType, 483) 484 485_BPY_STRUCT_FAKE = "bpy_struct" 486_BPY_PROP_COLLECTION_FAKE = "bpy_prop_collection" 487 488if _BPY_PROP_COLLECTION_FAKE: 489 _BPY_PROP_COLLECTION_ID = ":class:`%s`" % _BPY_PROP_COLLECTION_FAKE 490else: 491 _BPY_PROP_COLLECTION_ID = "collection" 492 493if _BPY_STRUCT_FAKE: 494 bpy_struct = bpy.types.bpy_struct 495else: 496 bpy_struct = None 497 498 499def import_value_from_module(module_name, import_name): 500 ns = {} 501 exec_str = "from %s import %s as value" % (module_name, import_name) 502 exec(exec_str, ns, ns) 503 return ns["value"] 504 505 506def escape_rst(text): 507 """ Escape plain text which may contain characters used by RST. 508 """ 509 return text.translate(escape_rst.trans) 510 511 512escape_rst.trans = str.maketrans({ 513 "`": "\\`", 514 "|": "\\|", 515 "*": "\\*", 516 "\\": "\\\\", 517}) 518 519 520def is_struct_seq(value): 521 return isinstance(value, tuple) and type(tuple) != tuple and hasattr(value, "n_fields") 522 523 524def undocumented_message(module_name, type_name, identifier): 525 return "Undocumented, consider `contributing <https://developer.blender.org/T51061>`__." 526 527 528def range_str(val): 529 ''' 530 Converts values to strings for the range directive. 531 (unused function it seems) 532 ''' 533 if val < -10000000: 534 return '-inf' 535 elif val > 10000000: 536 return 'inf' 537 elif type(val) == float: 538 return '%g' % val 539 else: 540 return str(val) 541 542 543def example_extract_docstring(filepath): 544 file = open(filepath, "r", encoding="utf-8") 545 line = file.readline() 546 line_no = 0 547 text = [] 548 if line.startswith('"""'): # assume nothing here 549 line_no += 1 550 else: 551 file.close() 552 return "", 0 553 554 for line in file.readlines(): 555 line_no += 1 556 if line.startswith('"""'): 557 break 558 else: 559 text.append(line.rstrip()) 560 561 line_no += 1 562 file.close() 563 return "\n".join(text), line_no 564 565 566def title_string(text, heading_char, double=False): 567 filler = len(text) * heading_char 568 569 if double: 570 return "%s\n%s\n%s\n\n" % (filler, text, filler) 571 else: 572 return "%s\n%s\n\n" % (text, filler) 573 574 575def write_example_ref(ident, fw, example_id, ext="py"): 576 if example_id in EXAMPLE_SET: 577 578 # extract the comment 579 filepath = os.path.join("..", "examples", "%s.%s" % (example_id, ext)) 580 filepath_full = os.path.join(os.path.dirname(fw.__self__.name), filepath) 581 582 text, line_no = example_extract_docstring(filepath_full) 583 584 for line in text.split("\n"): 585 fw("%s\n" % (ident + line).rstrip()) 586 fw("\n") 587 588 fw("%s.. literalinclude:: %s\n" % (ident, filepath)) 589 if line_no > 0: 590 fw("%s :lines: %d-\n" % (ident, line_no)) 591 fw("\n") 592 EXAMPLE_SET_USED.add(example_id) 593 else: 594 if bpy.app.debug: 595 BPY_LOGGER.debug("\tskipping example: " + example_id) 596 597 # Support for numbered files bpy.types.Operator -> bpy.types.Operator.1.py 598 i = 1 599 while True: 600 example_id_num = "%s.%d" % (example_id, i) 601 if example_id_num in EXAMPLE_SET: 602 write_example_ref(ident, fw, example_id_num, ext) 603 i += 1 604 else: 605 break 606 607 608def write_indented_lines(ident, fn, text, strip=True): 609 ''' 610 Apply same indentation to all lines in a multilines text. 611 ''' 612 if text is None: 613 return 614 615 lines = text.split("\n") 616 617 # strip empty lines from the start/end 618 while lines and not lines[0].strip(): 619 del lines[0] 620 while lines and not lines[-1].strip(): 621 del lines[-1] 622 623 if strip: 624 # set indentation to <indent> 625 ident_strip = 1000 626 for l in lines: 627 if l.strip(): 628 ident_strip = min(ident_strip, len(l) - len(l.lstrip())) 629 for l in lines: 630 fn(ident + l[ident_strip:] + "\n") 631 else: 632 # add <indent> number of blanks to the current indentation 633 for l in lines: 634 fn(ident + l + "\n") 635 636 637def pymethod2sphinx(ident, fw, identifier, py_func): 638 ''' 639 class method to sphinx 640 ''' 641 arg_str = inspect.formatargspec(*inspect.getargspec(py_func)) 642 if arg_str.startswith("(self, "): 643 arg_str = "(" + arg_str[7:] 644 func_type = "method" 645 elif arg_str.startswith("(cls, "): 646 arg_str = "(" + arg_str[6:] 647 func_type = "classmethod" 648 else: 649 func_type = "staticmethod" 650 651 fw(ident + ".. %s:: %s%s\n\n" % (func_type, identifier, arg_str)) 652 if py_func.__doc__: 653 write_indented_lines(ident + " ", fw, py_func.__doc__) 654 fw("\n") 655 656 657def pyfunc2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_class=True): 658 ''' 659 function or class method to sphinx 660 ''' 661 662 if type(py_func) == MethodType: 663 return 664 665 arg_str = str(inspect.signature(py_func)) 666 667 if not is_class: 668 func_type = "function" 669 670 # the rest are class methods 671 elif arg_str.startswith("(self, ") or arg_str == "(self)": 672 arg_str = "()" if (arg_str == "(self)") else ("(" + arg_str[7:]) 673 func_type = "method" 674 elif arg_str.startswith("(cls, "): 675 arg_str = "()" if (arg_str == "(cls)") else ("(" + arg_str[6:]) 676 func_type = "classmethod" 677 else: 678 func_type = "staticmethod" 679 680 doc = py_func.__doc__ 681 if (not doc) or (not doc.startswith(".. %s:: " % func_type)): 682 fw(ident + ".. %s:: %s%s\n\n" % (func_type, identifier, arg_str)) 683 ident_temp = ident + " " 684 else: 685 ident_temp = ident 686 687 if doc: 688 write_indented_lines(ident_temp, fw, doc) 689 fw("\n") 690 del doc, ident_temp 691 692 if is_class: 693 write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier) 694 else: 695 write_example_ref(ident + " ", fw, module_name + "." + identifier) 696 697 698def py_descr2sphinx(ident, fw, descr, module_name, type_name, identifier): 699 if identifier.startswith("_"): 700 return 701 702 doc = descr.__doc__ 703 if not doc: 704 doc = undocumented_message(module_name, type_name, identifier) 705 706 if type(descr) == GetSetDescriptorType: 707 fw(ident + ".. attribute:: %s\n\n" % identifier) 708 write_indented_lines(ident + " ", fw, doc, False) 709 fw("\n") 710 elif type(descr) == MemberDescriptorType: # same as above but use 'data' 711 fw(ident + ".. data:: %s\n\n" % identifier) 712 write_indented_lines(ident + " ", fw, doc, False) 713 fw("\n") 714 elif type(descr) in {MethodDescriptorType, ClassMethodDescriptorType}: 715 write_indented_lines(ident, fw, doc, False) 716 fw("\n") 717 else: 718 raise TypeError("type was not GetSetDescriptorType, MethodDescriptorType or ClassMethodDescriptorType") 719 720 write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier) 721 fw("\n") 722 723 724def py_c_func2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_class=True): 725 ''' 726 c defined function to sphinx. 727 ''' 728 729 # dump the docstring, assume its formatted correctly 730 if py_func.__doc__: 731 write_indented_lines(ident, fw, py_func.__doc__, False) 732 fw("\n") 733 else: 734 fw(ident + ".. function:: %s()\n\n" % identifier) 735 fw(ident + " " + undocumented_message(module_name, type_name, identifier)) 736 737 if is_class: 738 write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier) 739 else: 740 write_example_ref(ident + " ", fw, module_name + "." + identifier) 741 742 fw("\n") 743 744 745def pyprop2sphinx(ident, fw, identifier, py_prop): 746 ''' 747 Python property to sphinx 748 ''' 749 # readonly properties use "data" directive, variables use "attribute" directive 750 if py_prop.fset is None: 751 fw(ident + ".. data:: %s\n\n" % identifier) 752 else: 753 fw(ident + ".. attribute:: %s\n\n" % identifier) 754 write_indented_lines(ident + " ", fw, py_prop.__doc__) 755 fw("\n") 756 if py_prop.fset is None: 757 fw(ident + " (readonly)\n\n") 758 759 760def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): 761 import types 762 attribute_set = set() 763 filepath = os.path.join(basepath, module_name + ".rst") 764 765 module_all = getattr(module, "__all__", None) 766 module_dir = sorted(dir(module)) 767 768 if module_all: 769 module_dir = module_all 770 771 # TODO - currently only used for classes 772 # grouping support 773 module_grouping = MODULE_GROUPING.get(module_name) 774 775 def module_grouping_index(name): 776 if module_grouping is not None: 777 try: 778 return module_grouping.index(name) 779 except ValueError: 780 pass 781 return -1 782 783 def module_grouping_heading(name): 784 if module_grouping is not None: 785 i = module_grouping_index(name) - 1 786 if i >= 0 and type(module_grouping[i]) == tuple: 787 return module_grouping[i] 788 return None, None 789 790 def module_grouping_sort_key(name): 791 return module_grouping_index(name) 792 # done grouping support 793 794 file = open(filepath, "w", encoding="utf-8") 795 796 fw = file.write 797 798 fw(title_string("%s (%s)" % (title, module_name), "=")) 799 800 fw(".. module:: %s\n\n" % module_name) 801 802 if module.__doc__: 803 # Note, may contain sphinx syntax, don't mangle! 804 fw(module.__doc__.strip()) 805 fw("\n\n") 806 807 # write submodules 808 # we could also scan files but this ensures __all__ is used correctly 809 if module_all or module_all_extra: 810 submod_name = None 811 submod = None 812 submod_ls = [] 813 for submod_name in (module_all or ()): 814 submod = import_value_from_module(module_name, submod_name) 815 if type(submod) == types.ModuleType: 816 submod_ls.append((submod_name, submod)) 817 818 for submod_name in module_all_extra: 819 if submod_name in attribute_set: 820 continue 821 submod = import_value_from_module(module_name, submod_name) 822 # No type checks, since there are non-module types we treat as modules 823 # such as `bpy.app.translations` & `bpy.app.handlers`. 824 submod_ls.append((submod_name, submod)) 825 826 del submod_name 827 del submod 828 829 if submod_ls: 830 fw(".. toctree::\n") 831 fw(" :maxdepth: 1\n") 832 fw(" :caption: Submodules\n\n") 833 834 for submod_name, submod in submod_ls: 835 submod_name_full = "%s.%s" % (module_name, submod_name) 836 fw(" %s.rst\n" % submod_name_full) 837 838 pymodule2sphinx(basepath, submod_name_full, submod, "%s submodule" % module_name, ()) 839 fw("\n") 840 del submod_ls 841 # done writing submodules! 842 843 write_example_ref("", fw, module_name) 844 845 # write members of the module 846 # only tested with PyStructs which are not exactly modules 847 for key, descr in sorted(type(module).__dict__.items()): 848 if key.startswith("__"): 849 continue 850 if key in module_all_extra: 851 continue 852 # naughty, we also add getset's into PyStructs, this is not typical py but also not incorrect. 853 854 # type_name is only used for examples and messages 855 # "<class 'bpy.app.handlers'>" --> bpy.app.handlers 856 type_name = str(type(module)).strip("<>").split(" ", 1)[-1][1:-1] 857 if type(descr) == types.GetSetDescriptorType: 858 py_descr2sphinx("", fw, descr, module_name, type_name, key) 859 attribute_set.add(key) 860 descr_sorted = [] 861 for key, descr in sorted(type(module).__dict__.items()): 862 if key.startswith("__"): 863 continue 864 865 if type(descr) == MemberDescriptorType: 866 if descr.__doc__: 867 value = getattr(module, key, None) 868 869 value_type = type(value) 870 descr_sorted.append((key, descr, value, type(value))) 871 # sort by the valye type 872 descr_sorted.sort(key=lambda descr_data: str(descr_data[3])) 873 for key, descr, value, value_type in descr_sorted: 874 if key in module_all_extra: 875 continue 876 877 # must be documented as a submodule 878 if is_struct_seq(value): 879 continue 880 881 type_name = value_type.__name__ 882 py_descr2sphinx("", fw, descr, module_name, type_name, key) 883 884 attribute_set.add(key) 885 886 del key, descr, descr_sorted 887 888 classes = [] 889 submodules = [] 890 891 # use this list so we can sort by type 892 module_dir_value_type = [] 893 894 for attribute in module_dir: 895 if attribute.startswith("_"): 896 continue 897 898 if attribute in attribute_set: 899 continue 900 901 if attribute.startswith("n_"): # annoying exception, needed for bpy.app 902 continue 903 904 # workaround for bpy.app documenting .index() and .count() 905 if isinstance(module, tuple) and hasattr(tuple, attribute): 906 continue 907 908 value = getattr(module, attribute) 909 910 module_dir_value_type.append((attribute, value, type(value))) 911 912 # sort by str of each type 913 # this way lists, functions etc are grouped. 914 module_dir_value_type.sort(key=lambda triple: str(triple[2])) 915 916 for attribute, value, value_type in module_dir_value_type: 917 if attribute in module_all_extra: 918 continue 919 920 if value_type == FunctionType: 921 pyfunc2sphinx("", fw, module_name, None, attribute, value, is_class=False) 922 # both the same at the moment but to be future proof 923 elif value_type in {types.BuiltinMethodType, types.BuiltinFunctionType}: 924 # note: can't get args from these, so dump the string as is 925 # this means any module used like this must have fully formatted docstrings. 926 py_c_func2sphinx("", fw, module_name, None, attribute, value, is_class=False) 927 elif value_type == type: 928 classes.append((attribute, value)) 929 elif issubclass(value_type, types.ModuleType): 930 submodules.append((attribute, value)) 931 elif issubclass(value_type, (bool, int, float, str, tuple)): 932 # constant, not much fun we can do here except to list it. 933 # TODO, figure out some way to document these! 934 fw(".. data:: %s\n\n" % attribute) 935 write_indented_lines(" ", fw, "constant value %s" % repr(value), False) 936 fw("\n") 937 else: 938 BPY_LOGGER.debug("\tnot documenting %s.%s of %r type" % (module_name, attribute, value_type.__name__)) 939 continue 940 941 attribute_set.add(attribute) 942 # TODO, more types... 943 del module_dir_value_type 944 945 # TODO, bpy_extras does this already, mathutils not. 946 ''' 947 if submodules: 948 fw("\n" 949 "**********\n" 950 "Submodules\n" 951 "**********\n" 952 "\n" 953 ) 954 for attribute, submod in submodules: 955 fw("* :mod:`%s.%s`\n" % (module_name, attribute)) 956 fw("\n") 957 ''' 958 959 if module_grouping is not None: 960 classes.sort(key=lambda pair: module_grouping_sort_key(pair[0])) 961 962 # write collected classes now 963 for (type_name, value) in classes: 964 965 if module_grouping is not None: 966 heading, heading_char = module_grouping_heading(type_name) 967 if heading: 968 fw(title_string(heading, heading_char)) 969 970 # May need to be its own function 971 if value.__doc__: 972 if value.__doc__.startswith(".. class::"): 973 fw(value.__doc__) 974 else: 975 fw(".. class:: %s\n\n" % type_name) 976 write_indented_lines(" ", fw, value.__doc__, True) 977 else: 978 fw(".. class:: %s\n\n" % type_name) 979 fw("\n") 980 981 write_example_ref(" ", fw, module_name + "." + type_name) 982 983 descr_items = [(key, descr) for key, descr in sorted(value.__dict__.items()) if not key.startswith("_")] 984 985 for key, descr in descr_items: 986 if type(descr) == ClassMethodDescriptorType: 987 py_descr2sphinx(" ", fw, descr, module_name, type_name, key) 988 989 # needed for pure Python classes 990 for key, descr in descr_items: 991 if type(descr) == FunctionType: 992 pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, is_class=True) 993 994 for key, descr in descr_items: 995 if type(descr) == MethodDescriptorType: 996 py_descr2sphinx(" ", fw, descr, module_name, type_name, key) 997 998 for key, descr in descr_items: 999 if type(descr) == GetSetDescriptorType: 1000 py_descr2sphinx(" ", fw, descr, module_name, type_name, key) 1001 1002 for key, descr in descr_items: 1003 if type(descr) == StaticMethodType: 1004 descr = getattr(value, key) 1005 write_indented_lines(" ", fw, descr.__doc__ or "Undocumented", False) 1006 fw("\n") 1007 1008 fw("\n\n") 1009 1010 file.close() 1011 1012 1013# Changes in Blender will force errors here 1014context_type_map = { 1015 # context_member: (RNA type, is_collection) 1016 "active_annotation_layer": ("GPencilLayer", False), 1017 "active_base": ("ObjectBase", False), 1018 "active_bone": ("EditBone", False), 1019 "active_gpencil_frame": ("GreasePencilLayer", True), 1020 "active_gpencil_layer": ("GPencilLayer", True), 1021 "active_node": ("Node", False), 1022 "active_object": ("Object", False), 1023 "active_operator": ("Operator", False), 1024 "active_pose_bone": ("PoseBone", False), 1025 "active_editable_fcurve": ("FCurve", False), 1026 "annotation_data": ("GreasePencil", False), 1027 "annotation_data_owner": ("ID", False), 1028 "armature": ("Armature", False), 1029 "bone": ("Bone", False), 1030 "brush": ("Brush", False), 1031 "camera": ("Camera", False), 1032 "cloth": ("ClothModifier", False), 1033 "collection": ("LayerCollection", False), 1034 "collision": ("CollisionModifier", False), 1035 "curve": ("Curve", False), 1036 "dynamic_paint": ("DynamicPaintModifier", False), 1037 "edit_bone": ("EditBone", False), 1038 "edit_image": ("Image", False), 1039 "edit_mask": ("Mask", False), 1040 "edit_movieclip": ("MovieClip", False), 1041 "edit_object": ("Object", False), 1042 "edit_text": ("Text", False), 1043 "editable_bones": ("EditBone", True), 1044 "editable_gpencil_layers": ("GPencilLayer", True), 1045 "editable_gpencil_strokes": ("GPencilStroke", True), 1046 "editable_objects": ("Object", True), 1047 "editable_fcurves": ("FCurve", True), 1048 "fluid": ("FluidSimulationModifier", False), 1049 "gpencil": ("GreasePencil", False), 1050 "gpencil_data": ("GreasePencil", False), 1051 "gpencil_data_owner": ("ID", False), 1052 "hair": ("Hair", False), 1053 "image_paint_object": ("Object", False), 1054 "lattice": ("Lattice", False), 1055 "light": ("Light", False), 1056 "lightprobe": ("LightProbe", False), 1057 "line_style": ("FreestyleLineStyle", False), 1058 "material": ("Material", False), 1059 "material_slot": ("MaterialSlot", False), 1060 "mesh": ("Mesh", False), 1061 "meta_ball": ("MetaBall", False), 1062 "object": ("Object", False), 1063 "objects_in_mode": ("Object", True), 1064 "objects_in_mode_unique_data": ("Object", True), 1065 "particle_edit_object": ("Object", False), 1066 "particle_settings": ("ParticleSettings", False), 1067 "particle_system": ("ParticleSystem", False), 1068 "particle_system_editable": ("ParticleSystem", False), 1069 "pointcloud": ("PointCloud", False), 1070 "pose_bone": ("PoseBone", False), 1071 "pose_object": ("Object", False), 1072 "scene": ("Scene", False), 1073 "sculpt_object": ("Object", False), 1074 "selectable_objects": ("Object", True), 1075 "selected_bones": ("EditBone", True), 1076 "selected_editable_bones": ("EditBone", True), 1077 "selected_editable_fcurves": ("FCurve", True), 1078 "selected_editable_keyframes": ("Keyframe", True), 1079 "selected_editable_objects": ("Object", True), 1080 "selected_editable_sequences": ("Sequence", True), 1081 "selected_nla_strips": ("NlaStrip", True), 1082 "selected_nodes": ("Node", True), 1083 "selected_objects": ("Object", True), 1084 "selected_pose_bones": ("PoseBone", True), 1085 "selected_pose_bones_from_active_object": ("PoseBone", True), 1086 "selected_sequences": ("Sequence", True), 1087 "selected_visible_fcurves": ("FCurve", True), 1088 "sequences": ("Sequence", True), 1089 "soft_body": ("SoftBodyModifier", False), 1090 "speaker": ("Speaker", False), 1091 "texture": ("Texture", False), 1092 "texture_slot": ("MaterialTextureSlot", False), 1093 "texture_user": ("ID", False), 1094 "texture_user_property": ("Property", False), 1095 "vertex_paint_object": ("Object", False), 1096 "view_layer": ("ViewLayer", False), 1097 "visible_bones": ("EditBone", True), 1098 "visible_gpencil_layers": ("GPencilLayer", True), 1099 "visible_objects": ("Object", True), 1100 "visible_pose_bones": ("PoseBone", True), 1101 "visible_fcurves": ("FCurve", True), 1102 "weight_paint_object": ("Object", False), 1103 "volume": ("Volume", False), 1104 "world": ("World", False), 1105} 1106 1107 1108def pycontext2sphinx(basepath): 1109 # Only use once. very irregular 1110 1111 filepath = os.path.join(basepath, "bpy.context.rst") 1112 file = open(filepath, "w", encoding="utf-8") 1113 fw = file.write 1114 fw(title_string("Context Access (bpy.context)", "=")) 1115 fw(".. module:: bpy.context\n") 1116 fw("\n") 1117 fw("The context members available depend on the area of Blender which is currently being accessed.\n") 1118 fw("\n") 1119 fw("Note that all context values are readonly,\n") 1120 fw("but may be modified through the data API or by running operators\n\n") 1121 1122 def write_contex_cls(): 1123 1124 fw(title_string("Global Context", "-")) 1125 fw("These properties are available in any contexts.\n\n") 1126 1127 # very silly. could make these global and only access once. 1128 # structs, funcs, ops, props = rna_info.BuildRNAInfo() 1129 structs, funcs, ops, props = rna_info_BuildRNAInfo_cache() 1130 struct = structs[("", "Context")] 1131 struct_blacklist = RNA_BLACKLIST.get(struct.identifier, ()) 1132 del structs, funcs, ops, props 1133 1134 sorted_struct_properties = struct.properties[:] 1135 sorted_struct_properties.sort(key=lambda prop: prop.identifier) 1136 1137 # First write RNA 1138 for prop in sorted_struct_properties: 1139 # support blacklisting props 1140 if prop.identifier in struct_blacklist: 1141 continue 1142 1143 type_descr = prop.get_type_description( 1144 class_fmt=":class:`bpy.types.%s`", collection_id=_BPY_PROP_COLLECTION_ID) 1145 fw(".. data:: %s\n\n" % prop.identifier) 1146 if prop.description: 1147 fw(" %s\n\n" % prop.description) 1148 1149 # special exception, can't use generic code here for enums 1150 if prop.type == "enum": 1151 enum_text = pyrna_enum2sphinx(prop) 1152 if enum_text: 1153 write_indented_lines(" ", fw, enum_text) 1154 fw("\n") 1155 del enum_text 1156 # end enum exception 1157 1158 fw(" :type: %s\n\n" % type_descr) 1159 1160 write_contex_cls() 1161 del write_contex_cls 1162 # end 1163 1164 # nasty, get strings directly from Blender because there is no other way to get it 1165 import ctypes 1166 1167 context_strings = ( 1168 "screen_context_dir", 1169 "view3d_context_dir", 1170 "buttons_context_dir", 1171 "image_context_dir", 1172 "node_context_dir", 1173 "text_context_dir", 1174 "clip_context_dir", 1175 "sequencer_context_dir", 1176 ) 1177 1178 unique = set() 1179 blend_cdll = ctypes.CDLL("") 1180 for ctx_str in context_strings: 1181 subsection = "%s Context" % ctx_str.split("_")[0].title() 1182 fw("\n%s\n%s\n\n" % (subsection, (len(subsection) * '-'))) 1183 1184 attr = ctypes.addressof(getattr(blend_cdll, ctx_str)) 1185 c_char_p_p = ctypes.POINTER(ctypes.c_char_p) 1186 char_array = c_char_p_p.from_address(attr) 1187 i = 0 1188 while char_array[i] is not None: 1189 member = ctypes.string_at(char_array[i]).decode(encoding="ascii") 1190 fw(".. data:: %s\n\n" % member) 1191 member_type, is_seq = context_type_map[member] 1192 fw(" :type: %s :class:`bpy.types.%s`\n\n" % ("sequence of " if is_seq else "", member_type)) 1193 unique.add(member) 1194 i += 1 1195 1196 # generate typemap... 1197 # for member in sorted(unique): 1198 # print(' "%s": ("", False),' % member) 1199 if len(context_type_map) > len(unique): 1200 raise Exception( 1201 "Some types are not used: %s" % 1202 str([member for member in context_type_map if member not in unique])) 1203 else: 1204 pass # will have raised an error above 1205 1206 file.close() 1207 1208 1209def pyrna_enum2sphinx(prop, use_empty_descriptions=False): 1210 """ write a bullet point list of enum + descriptions 1211 """ 1212 1213 if use_empty_descriptions: 1214 ok = True 1215 else: 1216 ok = False 1217 for identifier, name, description in prop.enum_items: 1218 if description: 1219 ok = True 1220 break 1221 1222 if ok: 1223 return "".join([ 1224 "* ``%s``\n" 1225 "%s.\n" % ( 1226 identifier, 1227 # Account for multi-line enum descriptions, allowing this to be a block of text. 1228 indent(", ".join(escape_rst(val) for val in (name, description) if val) or "Undocumented", " "), 1229 ) 1230 for identifier, name, description in prop.enum_items 1231 ]) 1232 else: 1233 return "" 1234 1235 1236def pyrna2sphinx(basepath): 1237 """ bpy.types and bpy.ops 1238 """ 1239 # structs, funcs, ops, props = rna_info.BuildRNAInfo() 1240 structs, funcs, ops, props = rna_info_BuildRNAInfo_cache() 1241 1242 if USE_ONLY_BUILTIN_RNA_TYPES: 1243 # Ignore properties that use non 'bpy.types' properties. 1244 structs_blacklist = { 1245 v.identifier for v in structs.values() 1246 if v.module_name != "bpy.types" 1247 } 1248 for k, v in structs.items(): 1249 for p in v.properties: 1250 for identifier in ( 1251 getattr(p.srna, "identifier", None), 1252 getattr(p.fixed_type, "identifier", None), 1253 ): 1254 if identifier is not None: 1255 if identifier in structs_blacklist: 1256 RNA_BLACKLIST.setdefault(k, set()).add(identifier) 1257 del structs_blacklist 1258 1259 structs = { 1260 k: v for k, v in structs.items() 1261 if v.module_name == "bpy.types" 1262 } 1263 1264 if FILTER_BPY_TYPES is not None: 1265 structs = { 1266 k: v for k, v in structs.items() 1267 if k[1] in FILTER_BPY_TYPES 1268 if v.module_name == "bpy.types" 1269 } 1270 1271 if FILTER_BPY_OPS is not None: 1272 ops = {k: v for k, v in ops.items() if v.module_name in FILTER_BPY_OPS} 1273 1274 def write_param(ident, fw, prop, is_return=False): 1275 if is_return: 1276 id_name = "return" 1277 id_type = "rtype" 1278 kwargs = {"as_ret": True} 1279 identifier = "" 1280 else: 1281 id_name = "arg" 1282 id_type = "type" 1283 kwargs = {"as_arg": True} 1284 identifier = " %s" % prop.identifier 1285 1286 kwargs["class_fmt"] = ":class:`%s`" 1287 1288 kwargs["collection_id"] = _BPY_PROP_COLLECTION_ID 1289 1290 type_descr = prop.get_type_description(**kwargs) 1291 1292 enum_text = pyrna_enum2sphinx(prop) 1293 1294 if prop.name or prop.description or enum_text: 1295 fw(ident + ":%s%s:\n\n" % (id_name, identifier)) 1296 1297 if prop.name or prop.description: 1298 fw(indent(", ".join(val for val in (prop.name, prop.description) if val), ident + " ") + "\n\n") 1299 1300 # special exception, can't use generic code here for enums 1301 if enum_text: 1302 write_indented_lines(ident + " ", fw, enum_text) 1303 fw("\n") 1304 del enum_text 1305 # end enum exception 1306 1307 fw(ident + ":%s%s: %s\n" % (id_type, identifier, type_descr)) 1308 1309 def write_struct(struct): 1310 # if not struct.identifier.startswith("Sc") and not struct.identifier.startswith("I"): 1311 # return 1312 1313 # if not struct.identifier == "Object": 1314 # return 1315 1316 struct_module_name = struct.module_name 1317 if USE_ONLY_BUILTIN_RNA_TYPES: 1318 assert(struct_module_name == "bpy.types") 1319 filepath = os.path.join(basepath, "%s.%s.rst" % (struct_module_name, struct.identifier)) 1320 file = open(filepath, "w", encoding="utf-8") 1321 fw = file.write 1322 1323 base_id = getattr(struct.base, "identifier", "") 1324 struct_id = struct.identifier 1325 1326 if _BPY_STRUCT_FAKE: 1327 if not base_id: 1328 base_id = _BPY_STRUCT_FAKE 1329 1330 if base_id: 1331 title = "%s(%s)" % (struct_id, base_id) 1332 else: 1333 title = struct_id 1334 1335 fw(title_string(title, "=")) 1336 1337 fw(".. currentmodule:: %s\n\n" % struct_module_name) 1338 1339 # docs first?, ok 1340 write_example_ref("", fw, "%s.%s" % (struct_module_name, struct_id)) 1341 1342 base_ids = [base.identifier for base in struct.get_bases()] 1343 1344 if _BPY_STRUCT_FAKE: 1345 base_ids.append(_BPY_STRUCT_FAKE) 1346 1347 base_ids.reverse() 1348 1349 if base_ids: 1350 if len(base_ids) > 1: 1351 fw("base classes --- ") 1352 else: 1353 fw("base class --- ") 1354 1355 fw(", ".join((":class:`%s`" % base_id) for base_id in base_ids)) 1356 fw("\n\n") 1357 1358 subclass_ids = [ 1359 s.identifier for s in structs.values() 1360 if s.base is struct 1361 if not rna_info.rna_id_ignore(s.identifier) 1362 ] 1363 subclass_ids.sort() 1364 if subclass_ids: 1365 fw("subclasses --- \n" + ", ".join((":class:`%s`" % s) for s in subclass_ids) + "\n\n") 1366 1367 base_id = getattr(struct.base, "identifier", "") 1368 1369 if _BPY_STRUCT_FAKE: 1370 if not base_id: 1371 base_id = _BPY_STRUCT_FAKE 1372 1373 if base_id: 1374 fw(".. class:: %s(%s)\n\n" % (struct_id, base_id)) 1375 else: 1376 fw(".. class:: %s\n\n" % struct_id) 1377 1378 fw(" %s\n\n" % struct.description) 1379 1380 # properties sorted in alphabetical order 1381 sorted_struct_properties = struct.properties[:] 1382 sorted_struct_properties.sort(key=lambda prop: prop.identifier) 1383 1384 # support blacklisting props 1385 struct_blacklist = RNA_BLACKLIST.get(struct_id, ()) 1386 1387 for prop in sorted_struct_properties: 1388 1389 # support blacklisting props 1390 if prop.identifier in struct_blacklist: 1391 continue 1392 1393 type_descr = prop.get_type_description(class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID) 1394 # readonly properties use "data" directive, variables properties use "attribute" directive 1395 if 'readonly' in type_descr: 1396 fw(" .. data:: %s\n\n" % prop.identifier) 1397 else: 1398 fw(" .. attribute:: %s\n\n" % prop.identifier) 1399 if prop.description: 1400 fw(" %s\n\n" % prop.description) 1401 1402 # special exception, can't use generic code here for enums 1403 if prop.type == "enum": 1404 enum_text = pyrna_enum2sphinx(prop) 1405 if enum_text: 1406 write_indented_lines(" ", fw, enum_text) 1407 fw("\n") 1408 del enum_text 1409 # end enum exception 1410 1411 fw(" :type: %s\n\n" % type_descr) 1412 1413 # Python attributes 1414 py_properties = struct.get_py_properties() 1415 py_prop = None 1416 for identifier, py_prop in py_properties: 1417 pyprop2sphinx(" ", fw, identifier, py_prop) 1418 del py_properties, py_prop 1419 1420 for func in struct.functions: 1421 args_str = ", ".join(prop.get_arg_default(force=False) for prop in func.args) 1422 1423 fw(" .. %s:: %s(%s)\n\n" % 1424 ("classmethod" if func.is_classmethod else "method", func.identifier, args_str)) 1425 fw(" %s\n\n" % func.description) 1426 1427 for prop in func.args: 1428 write_param(" ", fw, prop) 1429 1430 if len(func.return_values) == 1: 1431 write_param(" ", fw, func.return_values[0], is_return=True) 1432 elif func.return_values: # multiple return values 1433 fw(" :return (%s):\n" % ", ".join(prop.identifier for prop in func.return_values)) 1434 for prop in func.return_values: 1435 # TODO, pyrna_enum2sphinx for multiple return values... actually don't 1436 # think we even use this but still!!! 1437 type_descr = prop.get_type_description( 1438 as_ret=True, class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID) 1439 descr = prop.description 1440 if not descr: 1441 descr = prop.name 1442 # In rare cases descr may be empty 1443 fw(" `%s`, %s\n\n" % 1444 (prop.identifier, 1445 ", ".join((val for val in (descr, type_descr) if val)))) 1446 1447 write_example_ref(" ", fw, struct_module_name + "." + struct_id + "." + func.identifier) 1448 1449 fw("\n") 1450 1451 # Python methods 1452 py_funcs = struct.get_py_functions() 1453 py_func = None 1454 1455 for identifier, py_func in py_funcs: 1456 pyfunc2sphinx(" ", fw, "bpy.types", struct_id, identifier, py_func, is_class=True) 1457 del py_funcs, py_func 1458 1459 py_funcs = struct.get_py_c_functions() 1460 py_func = None 1461 1462 for identifier, py_func in py_funcs: 1463 py_c_func2sphinx(" ", fw, "bpy.types", struct_id, identifier, py_func, is_class=True) 1464 1465 lines = [] 1466 1467 if struct.base or _BPY_STRUCT_FAKE: 1468 bases = list(reversed(struct.get_bases())) 1469 1470 # props 1471 del lines[:] 1472 1473 if _BPY_STRUCT_FAKE: 1474 descr_items = [ 1475 (key, descr) for key, descr in sorted(bpy_struct.__dict__.items()) 1476 if not key.startswith("__") 1477 ] 1478 1479 if _BPY_STRUCT_FAKE: 1480 for key, descr in descr_items: 1481 if type(descr) == GetSetDescriptorType: 1482 lines.append(" * :class:`%s.%s`\n" % (_BPY_STRUCT_FAKE, key)) 1483 1484 for base in bases: 1485 for prop in base.properties: 1486 lines.append(" * :class:`%s.%s`\n" % (base.identifier, prop.identifier)) 1487 1488 for identifier, py_prop in base.get_py_properties(): 1489 lines.append(" * :class:`%s.%s`\n" % (base.identifier, identifier)) 1490 1491 if lines: 1492 fw(".. rubric:: Inherited Properties\n\n") 1493 1494 fw(".. hlist::\n") 1495 fw(" :columns: 2\n\n") 1496 1497 for line in lines: 1498 fw(line) 1499 fw("\n") 1500 1501 # funcs 1502 del lines[:] 1503 1504 if _BPY_STRUCT_FAKE: 1505 for key, descr in descr_items: 1506 if type(descr) == MethodDescriptorType: 1507 lines.append(" * :class:`%s.%s`\n" % (_BPY_STRUCT_FAKE, key)) 1508 1509 for base in bases: 1510 for func in base.functions: 1511 lines.append(" * :class:`%s.%s`\n" % (base.identifier, func.identifier)) 1512 for identifier, py_func in base.get_py_functions(): 1513 lines.append(" * :class:`%s.%s`\n" % (base.identifier, identifier)) 1514 for identifier, py_func in base.get_py_c_functions(): 1515 lines.append(" * :class:`%s.%s`\n" % (base.identifier, identifier)) 1516 1517 if lines: 1518 fw(".. rubric:: Inherited Functions\n\n") 1519 1520 fw(".. hlist::\n") 1521 fw(" :columns: 2\n\n") 1522 1523 for line in lines: 1524 fw(line) 1525 fw("\n") 1526 1527 del lines[:] 1528 1529 if struct.references: 1530 # use this otherwise it gets in the index for a normal heading. 1531 fw(".. rubric:: References\n\n") 1532 1533 fw(".. hlist::\n") 1534 fw(" :columns: 2\n\n") 1535 1536 # context does its own thing 1537 # "active_base": ("ObjectBase", False), 1538 for ref_attr, (ref_type, ref_is_seq) in sorted(context_type_map.items()): 1539 if ref_type == struct_id: 1540 fw(" * :mod:`bpy.context.%s`\n" % ref_attr) 1541 del ref_attr, ref_type, ref_is_seq 1542 1543 for ref in struct.references: 1544 ref_split = ref.split(".") 1545 if len(ref_split) > 2: 1546 ref = ref_split[-2] + "." + ref_split[-1] 1547 fw(" * :class:`%s`\n" % ref) 1548 fw("\n") 1549 1550 # docs last?, disable for now 1551 # write_example_ref("", fw, "bpy.types.%s" % struct_id) 1552 file.close() 1553 1554 if "bpy.types" not in EXCLUDE_MODULES: 1555 for struct in structs.values(): 1556 # TODO, rna_info should filter these out! 1557 if "_OT_" in struct.identifier: 1558 continue 1559 write_struct(struct) 1560 1561 def fake_bpy_type(class_module_name, class_value, class_name, descr_str, use_subclasses=True): 1562 filepath = os.path.join(basepath, "%s.%s.rst" % (class_module_name, class_name)) 1563 file = open(filepath, "w", encoding="utf-8") 1564 fw = file.write 1565 1566 fw(title_string(class_name, "=")) 1567 1568 fw(".. currentmodule:: %s\n\n" % class_module_name) 1569 1570 if use_subclasses: 1571 subclass_ids = [ 1572 s.identifier for s in structs.values() 1573 if s.base is None 1574 if not rna_info.rna_id_ignore(s.identifier) 1575 ] 1576 if subclass_ids: 1577 fw("subclasses --- \n" + ", ".join((":class:`%s`" % s) for s in sorted(subclass_ids)) + "\n\n") 1578 1579 fw(".. class:: %s\n\n" % class_name) 1580 fw(" %s\n\n" % descr_str) 1581 fw(" .. note::\n\n") 1582 fw(" Note that :class:`%s.%s` is not actually available from within Blender,\n" 1583 " it only exists for the purpose of documentation.\n\n" % (class_module_name, class_name)) 1584 1585 descr_items = [ 1586 (key, descr) for key, descr in sorted(class_value.__dict__.items()) 1587 if not key.startswith("__") 1588 ] 1589 1590 for key, descr in descr_items: 1591 # GetSetDescriptorType, GetSetDescriptorType's are not documented yet 1592 if type(descr) == MethodDescriptorType: 1593 py_descr2sphinx(" ", fw, descr, "bpy.types", class_name, key) 1594 1595 for key, descr in descr_items: 1596 if type(descr) == GetSetDescriptorType: 1597 py_descr2sphinx(" ", fw, descr, "bpy.types", class_name, key) 1598 file.close() 1599 1600 # write fake classes 1601 if _BPY_STRUCT_FAKE: 1602 class_value = bpy_struct 1603 fake_bpy_type( 1604 "bpy.types", class_value, _BPY_STRUCT_FAKE, 1605 "built-in base class for all classes in bpy.types.", use_subclasses=True, 1606 ) 1607 1608 if _BPY_PROP_COLLECTION_FAKE: 1609 class_value = bpy.data.objects.__class__ 1610 fake_bpy_type( 1611 "bpy.types", class_value, _BPY_PROP_COLLECTION_FAKE, 1612 "built-in class used for all collections.", use_subclasses=False, 1613 ) 1614 1615 # operators 1616 def write_ops(): 1617 API_BASEURL = "https://developer.blender.org/diffusion/B/browse/master/release/scripts" 1618 API_BASEURL_ADDON = "https://developer.blender.org/diffusion/BA" 1619 API_BASEURL_ADDON_CONTRIB = "https://developer.blender.org/diffusion/BAC" 1620 1621 op_modules = {} 1622 for op in ops.values(): 1623 op_modules.setdefault(op.module_name, []).append(op) 1624 del op 1625 1626 for op_module_name, ops_mod in op_modules.items(): 1627 filepath = os.path.join(basepath, "bpy.ops.%s.rst" % op_module_name) 1628 file = open(filepath, "w", encoding="utf-8") 1629 fw = file.write 1630 1631 title = "%s Operators" % op_module_name.replace("_", " ").title() 1632 1633 fw(title_string(title, "=")) 1634 1635 fw(".. module:: bpy.ops.%s\n\n" % op_module_name) 1636 1637 ops_mod.sort(key=lambda op: op.func_name) 1638 1639 for op in ops_mod: 1640 args_str = ", ".join(prop.get_arg_default(force=True) for prop in op.args) 1641 fw(".. function:: %s(%s)\n\n" % (op.func_name, args_str)) 1642 1643 # if the description isn't valid, we output the standard warning 1644 # with a link to the wiki so that people can help 1645 if not op.description or op.description == "(undocumented operator)": 1646 operator_description = undocumented_message('bpy.ops', op.module_name, op.func_name) 1647 else: 1648 operator_description = op.description 1649 1650 fw(" %s\n\n" % operator_description) 1651 for prop in op.args: 1652 write_param(" ", fw, prop) 1653 if op.args: 1654 fw("\n") 1655 1656 location = op.get_location() 1657 if location != (None, None): 1658 if location[0].startswith("addons_contrib" + os.sep): 1659 url_base = API_BASEURL_ADDON_CONTRIB 1660 elif location[0].startswith("addons" + os.sep): 1661 url_base = API_BASEURL_ADDON 1662 else: 1663 url_base = API_BASEURL 1664 1665 fw(" :file: `%s\\:%d <%s/%s$%d>`_\n\n" % 1666 (location[0], location[1], url_base, location[0], location[1])) 1667 1668 file.close() 1669 1670 if "bpy.ops" not in EXCLUDE_MODULES: 1671 write_ops() 1672 1673 1674def write_sphinx_conf_py(basepath): 1675 ''' 1676 Write sphinx's conf.py 1677 ''' 1678 filepath = os.path.join(basepath, "conf.py") 1679 file = open(filepath, "w", encoding="utf-8") 1680 fw = file.write 1681 1682 fw("import sys, os\n\n") 1683 fw("extensions = ['sphinx.ext.intersphinx']\n\n") 1684 fw("intersphinx_mapping = {'blender_manual': ('https://docs.blender.org/manual/en/dev/', None)}\n\n") 1685 fw("project = 'Blender %s Python API'\n" % BLENDER_VERSION_DOTS) 1686 fw("master_doc = 'index'\n") 1687 fw("copyright = u'Blender Foundation'\n") 1688 fw("version = '%s'\n" % BLENDER_VERSION_HASH) 1689 fw("release = '%s'\n" % BLENDER_VERSION_HASH) 1690 1691 # Quiet file not in table-of-contents warnings. 1692 fw("exclude_patterns = [\n") 1693 fw(" 'include__bmesh.rst',\n") 1694 fw("]\n\n") 1695 1696 fw("html_title = 'Blender Python API'\n") 1697 1698 fw("html_theme = 'default'\n") 1699 # The theme 'sphinx_rtd_theme' is no longer distributed with sphinx by default, only use when available. 1700 fw(r""" 1701try: 1702 __import__('sphinx_rtd_theme') 1703 html_theme = 'sphinx_rtd_theme' 1704except ModuleNotFoundError: 1705 pass 1706""") 1707 1708 fw("if html_theme == 'sphinx_rtd_theme':\n") 1709 fw(" html_theme_options = {\n") 1710 fw(" 'canonical_url': 'https://docs.blender.org/api/current/',\n") 1711 # fw(" 'analytics_id': '',\n") 1712 # fw(" 'collapse_navigation': True,\n") 1713 fw(" 'sticky_navigation': False,\n") 1714 fw(" 'navigation_depth': 1,\n") 1715 # fw(" 'includehidden': True,\n") 1716 # fw(" 'titles_only': False\n") 1717 fw(" }\n\n") 1718 1719 # not helpful since the source is generated, adds to upload size. 1720 fw("html_copy_source = False\n") 1721 fw("html_show_sphinx = False\n") 1722 fw("html_use_opensearch = 'https://docs.blender.org/api/current'\n") 1723 fw("html_split_index = True\n") 1724 fw("html_static_path = ['static']\n") 1725 fw("html_extra_path = ['static/favicon.ico', 'static/blender_logo.svg']\n") 1726 fw("html_favicon = 'static/favicon.ico'\n") 1727 fw("html_logo = 'static/blender_logo.svg'\n") 1728 fw("html_last_updated_fmt = '%m/%d/%Y'\n\n") 1729 1730 # needed for latex, pdf gen 1731 fw("latex_elements = {\n") 1732 fw(" 'papersize': 'a4paper',\n") 1733 fw("}\n\n") 1734 1735 fw("latex_documents = [ ('contents', 'contents.tex', 'Blender Index', 'Blender Foundation', 'manual'), ]\n") 1736 1737 # Workaround for useless links leading to compile errors 1738 # See https://github.com/sphinx-doc/sphinx/issues/3866 1739 fw(r""" 1740from sphinx.domains.python import PythonDomain 1741 1742class PatchedPythonDomain(PythonDomain): 1743 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): 1744 if 'refspecific' in node: 1745 del node['refspecific'] 1746 return super(PatchedPythonDomain, self).resolve_xref( 1747 env, fromdocname, builder, typ, target, node, contnode) 1748""") 1749 # end workaround 1750 1751 fw("def setup(app):\n") 1752 fw(" app.add_css_file('css/theme_overrides.css')\n") 1753 fw(" app.add_domain(PatchedPythonDomain, override=True)\n\n") 1754 1755 file.close() 1756 1757 1758def execfile(filepath): 1759 global_namespace = {"__file__": filepath, "__name__": "__main__"} 1760 file_handle = open(filepath) 1761 exec(compile(file_handle.read(), filepath, 'exec'), global_namespace) 1762 file_handle.close() 1763 1764 1765def write_rst_index(basepath): 1766 ''' 1767 Write the rst file of the main page, needed for sphinx (index.html) 1768 ''' 1769 filepath = os.path.join(basepath, "index.rst") 1770 file = open(filepath, "w", encoding="utf-8") 1771 fw = file.write 1772 1773 fw(title_string("Blender %s Python API Documentation" % BLENDER_VERSION_DOTS, "%", double=True)) 1774 fw("\n") 1775 fw("Welcome to the Python API documentation for `Blender <https://www.blender.org>`__, ") 1776 fw("the free and open source 3D creation suite.\n") 1777 fw("\n") 1778 1779 # fw("`A PDF version of this document is also available <%s>`_\n" % BLENDER_PDF_FILENAME) 1780 fw("This site can be used offline: `Download the full documentation (zipped HTML files) <%s>`__\n" % 1781 BLENDER_ZIP_FILENAME) 1782 fw("\n") 1783 1784 if not EXCLUDE_INFO_DOCS: 1785 fw(".. toctree::\n") 1786 fw(" :maxdepth: 1\n") 1787 fw(" :caption: Documentation\n\n") 1788 for info, info_desc in INFO_DOCS: 1789 fw(" %s <%s>\n" % (info_desc, info)) 1790 fw("\n") 1791 1792 fw(".. toctree::\n") 1793 fw(" :maxdepth: 1\n") 1794 fw(" :caption: Application Modules\n\n") 1795 1796 app_modules = ( 1797 "bpy.context", # note: not actually a module 1798 "bpy.data", # note: not actually a module 1799 "bpy.msgbus", # note: not actually a module 1800 "bpy.ops", 1801 "bpy.types", 1802 1803 # py modules 1804 "bpy.utils", 1805 "bpy.path", 1806 "bpy.app", 1807 1808 # C modules 1809 "bpy.props", 1810 ) 1811 1812 for mod in app_modules: 1813 if mod not in EXCLUDE_MODULES: 1814 fw(" %s\n" % mod) 1815 fw("\n") 1816 1817 fw(".. toctree::\n") 1818 fw(" :maxdepth: 1\n") 1819 fw(" :caption: Standalone Modules\n\n") 1820 1821 standalone_modules = ( 1822 # submodules are added in parent page 1823 "aud", 1824 "bgl", 1825 "bl_math", 1826 "blf", 1827 "bmesh", 1828 "bpy_extras", 1829 "freestyle", 1830 "gpu", 1831 "gpu_extras", 1832 "idprop.types", 1833 "imbuf", 1834 "mathutils", 1835 ) 1836 1837 for mod in standalone_modules: 1838 if mod not in EXCLUDE_MODULES: 1839 fw(" %s\n" % mod) 1840 fw("\n") 1841 1842 fw(title_string("Indices", "=")) 1843 fw("* :ref:`genindex`\n") 1844 fw("* :ref:`modindex`\n\n") 1845 1846 # special case, this 'bmesh.ops.rst' is extracted from C source 1847 if "bmesh.ops" not in EXCLUDE_MODULES: 1848 execfile(os.path.join(SCRIPT_DIR, "rst_from_bmesh_opdefines.py")) 1849 1850 file.close() 1851 1852 1853def write_rst_bpy(basepath): 1854 ''' 1855 Write rst file of bpy module (disabled by default) 1856 ''' 1857 if ARGS.bpy: 1858 filepath = os.path.join(basepath, "bpy.rst") 1859 file = open(filepath, "w", encoding="utf-8") 1860 fw = file.write 1861 1862 fw("\n") 1863 1864 title = ":mod:`bpy` --- Blender Python Module" 1865 1866 fw(title_string(title, "=")) 1867 1868 fw(".. module:: bpy.types\n\n") 1869 file.close() 1870 1871 1872def write_rst_types_index(basepath): 1873 ''' 1874 Write the rst file of bpy.types module (index) 1875 ''' 1876 if "bpy.types" not in EXCLUDE_MODULES: 1877 filepath = os.path.join(basepath, "bpy.types.rst") 1878 file = open(filepath, "w", encoding="utf-8") 1879 fw = file.write 1880 fw(title_string("Types (bpy.types)", "=")) 1881 fw(".. module:: bpy.types\n\n") 1882 fw(".. toctree::\n") 1883 fw(" :glob:\n\n") 1884 fw(" bpy.types.*\n\n") 1885 file.close() 1886 1887 1888def write_rst_ops_index(basepath): 1889 ''' 1890 Write the rst file of bpy.ops module (index) 1891 ''' 1892 if "bpy.ops" not in EXCLUDE_MODULES: 1893 filepath = os.path.join(basepath, "bpy.ops.rst") 1894 file = open(filepath, "w", encoding="utf-8") 1895 fw = file.write 1896 fw(title_string("Operators (bpy.ops)", "=")) 1897 fw(".. module:: bpy.ops\n\n") 1898 write_example_ref("", fw, "bpy.ops") 1899 fw(".. toctree::\n") 1900 fw(" :caption: Submodules\n") 1901 fw(" :glob:\n\n") 1902 fw(" bpy.ops.*\n\n") 1903 file.close() 1904 1905 1906def write_rst_msgbus(basepath): 1907 """ 1908 Write the rst files of bpy.msgbus module 1909 """ 1910 if 'bpy.msgbus' in EXCLUDE_MODULES: 1911 return 1912 1913 # Write the index. 1914 filepath = os.path.join(basepath, "bpy.msgbus.rst") 1915 file = open(filepath, "w", encoding="utf-8") 1916 fw = file.write 1917 fw(title_string("Message Bus (bpy.msgbus)", "=")) 1918 write_example_ref("", fw, "bpy.msgbus") 1919 fw(".. toctree::\n") 1920 fw(" :glob:\n\n") 1921 fw(" bpy.msgbus.*\n\n") 1922 file.close() 1923 1924 # Write the contents. 1925 pymodule2sphinx(basepath, 'bpy.msgbus', bpy.msgbus, 'Message Bus', ()) 1926 EXAMPLE_SET_USED.add("bpy.msgbus") 1927 1928 1929def write_rst_data(basepath): 1930 ''' 1931 Write the rst file of bpy.data module 1932 ''' 1933 if "bpy.data" not in EXCLUDE_MODULES: 1934 # not actually a module, only write this file so we 1935 # can reference in the TOC 1936 filepath = os.path.join(basepath, "bpy.data.rst") 1937 file = open(filepath, "w", encoding="utf-8") 1938 fw = file.write 1939 fw(title_string("Data Access (bpy.data)", "=")) 1940 fw(".. module:: bpy.data\n") 1941 fw("\n") 1942 fw("This module is used for all Blender/Python access.\n") 1943 fw("\n") 1944 fw(".. data:: data\n") 1945 fw("\n") 1946 fw(" Access to Blender's internal data\n") 1947 fw("\n") 1948 fw(" :type: :class:`bpy.types.BlendData`\n") 1949 fw("\n") 1950 fw(".. literalinclude:: ../examples/bpy.data.py\n") 1951 file.close() 1952 1953 EXAMPLE_SET_USED.add("bpy.data") 1954 1955 1956def write_rst_importable_modules(basepath): 1957 ''' 1958 Write the rst files of importable modules 1959 ''' 1960 importable_modules = { 1961 # Python_modules 1962 "bpy.path": "Path Utilities", 1963 "bpy.utils": "Utilities", 1964 "bpy_extras": "Extra Utilities", 1965 "gpu_extras": "GPU Utilities", 1966 1967 # C_modules 1968 "aud": "Audio System", 1969 "blf": "Font Drawing", 1970 "imbuf": "Image Buffer", 1971 "gpu": "GPU Shader Module", 1972 "gpu.types": "GPU Types", 1973 "gpu.matrix": "GPU Matrix", 1974 "gpu.select": "GPU Select", 1975 "gpu.shader": "GPU Shader", 1976 "bmesh": "BMesh Module", 1977 "bmesh.ops": "BMesh Operators", 1978 "bmesh.types": "BMesh Types", 1979 "bmesh.utils": "BMesh Utilities", 1980 "bmesh.geometry": "BMesh Geometry Utilities", 1981 "bpy.app": "Application Data", 1982 "bpy.app.handlers": "Application Handlers", 1983 "bpy.app.translations": "Application Translations", 1984 "bpy.app.icons": "Application Icons", 1985 "bpy.app.timers": "Application Timers", 1986 "bpy.props": "Property Definitions", 1987 "idprop.types": "ID Property Access", 1988 "mathutils": "Math Types & Utilities", 1989 "mathutils.geometry": "Geometry Utilities", 1990 "mathutils.bvhtree": "BVHTree Utilities", 1991 "mathutils.kdtree": "KDTree Utilities", 1992 "mathutils.interpolate": "Interpolation Utilities", 1993 "mathutils.noise": "Noise Utilities", 1994 "bl_math": "Additional Math Functions", 1995 "freestyle": "Freestyle Module", 1996 "freestyle.types": "Freestyle Types", 1997 "freestyle.predicates": "Freestyle Predicates", 1998 "freestyle.functions": "Freestyle Functions", 1999 "freestyle.chainingiterators": "Freestyle Chaining Iterators", 2000 "freestyle.shaders": "Freestyle Shaders", 2001 "freestyle.utils": "Freestyle Utilities", 2002 } 2003 2004 # This is needed since some of the sub-modules listed above are not actual modules. 2005 # Examples include `bpy.app.translations` & `bpy.app.handlers`. 2006 # 2007 # Most of these are `PyStructSequence` internally, 2008 # however we don't want to document all of these as modules since some only contain 2009 # a few values (version number for e.g). 2010 # 2011 # If we remove this logic and document all `PyStructSequence` as sub-modules it means 2012 # `bpy.app.timers` for example would be presented on the same level as library information 2013 # access such as `bpy.app.sdl` which doesn't seem useful since it hides more useful 2014 # module-like objects among library data access. 2015 importable_modules_parent_map = {} 2016 for mod_name in importable_modules.keys(): 2017 if mod_name in EXCLUDE_MODULES: 2018 continue 2019 if "." in mod_name: 2020 mod_name, submod_name = mod_name.rsplit(".", 1) 2021 importable_modules_parent_map.setdefault(mod_name, []).append(submod_name) 2022 2023 for mod_name, mod_descr in importable_modules.items(): 2024 if mod_name in EXCLUDE_MODULES: 2025 continue 2026 module_all_extra = importable_modules_parent_map.get(mod_name, ()) 2027 module = __import__(mod_name, fromlist=[mod_name.rsplit(".", 1)[-1]]) 2028 pymodule2sphinx(basepath, mod_name, module, mod_descr, module_all_extra) 2029 2030 2031def copy_handwritten_rsts(basepath): 2032 2033 # info docs 2034 if not EXCLUDE_INFO_DOCS: 2035 for info, info_desc in INFO_DOCS: 2036 shutil.copy2(os.path.join(RST_DIR, info), basepath) 2037 2038 # TODO put this docs in Blender's code and use import as per modules above 2039 handwritten_modules = [ 2040 "bgl", # "Blender OpenGl wrapper" 2041 "bmesh.ops", # generated by rst_from_bmesh_opdefines.py 2042 2043 # includes... 2044 "include__bmesh", 2045 ] 2046 2047 for mod_name in handwritten_modules: 2048 if mod_name not in EXCLUDE_MODULES: 2049 # copy2 keeps time/date stamps 2050 shutil.copy2(os.path.join(RST_DIR, "%s.rst" % mod_name), basepath) 2051 2052 # changelog 2053 shutil.copy2(os.path.join(RST_DIR, "change_log.rst"), basepath) 2054 2055 # copy images, could be smarter but just glob for now. 2056 for f in os.listdir(RST_DIR): 2057 if f.endswith(".png"): 2058 shutil.copy2(os.path.join(RST_DIR, f), basepath) 2059 2060 2061def copy_handwritten_extra(basepath): 2062 for f_src in EXTRA_SOURCE_FILES: 2063 if os.sep != "/": 2064 f_src = os.sep.join(f_src.split("/")) 2065 2066 f_dst = f_src.replace("..", "__") 2067 2068 f_src = os.path.join(RST_DIR, f_src) 2069 f_dst = os.path.join(basepath, f_dst) 2070 2071 os.makedirs(os.path.dirname(f_dst), exist_ok=True) 2072 2073 shutil.copy2(f_src, f_dst) 2074 2075 2076def copy_theme_assets(basepath): 2077 shutil.copytree(os.path.join(SCRIPT_DIR, "static"), 2078 os.path.join(basepath, "static"), 2079 copy_function=shutil.copy) 2080 2081 2082def rna2sphinx(basepath): 2083 2084 try: 2085 os.mkdir(basepath) 2086 except: 2087 pass 2088 2089 # sphinx setup 2090 write_sphinx_conf_py(basepath) 2091 2092 # main page 2093 write_rst_index(basepath) 2094 2095 # context 2096 if "bpy.context" not in EXCLUDE_MODULES: 2097 # one of a kind, context doc (uses ctypes to extract info!) 2098 # doesn't work on mac and windows 2099 if PLATFORM not in {"darwin", "windows"}: 2100 pycontext2sphinx(basepath) 2101 2102 # internal modules 2103 write_rst_bpy(basepath) # bpy, disabled by default 2104 write_rst_types_index(basepath) # bpy.types 2105 write_rst_ops_index(basepath) # bpy.ops 2106 write_rst_msgbus(basepath) # bpy.msgbus 2107 pyrna2sphinx(basepath) # bpy.types.* and bpy.ops.* 2108 write_rst_data(basepath) # bpy.data 2109 write_rst_importable_modules(basepath) 2110 2111 # copy the other rsts 2112 copy_handwritten_rsts(basepath) 2113 2114 # copy source files referenced 2115 copy_handwritten_extra(basepath) 2116 2117 # copy extra files needed for theme 2118 copy_theme_assets(basepath) 2119 2120 2121def align_sphinx_in_to_sphinx_in_tmp(dir_src, dir_dst): 2122 ''' 2123 Move changed files from SPHINX_IN_TMP to SPHINX_IN 2124 ''' 2125 import filecmp 2126 2127 # possible the dir doesn't exist when running recursively 2128 os.makedirs(dir_dst, exist_ok=True) 2129 2130 sphinx_dst_files = set(os.listdir(dir_dst)) 2131 sphinx_src_files = set(os.listdir(dir_src)) 2132 2133 # remove deprecated files that have been removed 2134 for f in sorted(sphinx_dst_files): 2135 if f not in sphinx_src_files: 2136 BPY_LOGGER.debug("\tdeprecated: %s" % f) 2137 f_dst = os.path.join(dir_dst, f) 2138 if os.path.isdir(f_dst): 2139 shutil.rmtree(f_dst, True) 2140 else: 2141 os.remove(f_dst) 2142 2143 # freshen with new files. 2144 for f in sorted(sphinx_src_files): 2145 f_src = os.path.join(dir_src, f) 2146 f_dst = os.path.join(dir_dst, f) 2147 2148 if os.path.isdir(f_src): 2149 align_sphinx_in_to_sphinx_in_tmp(f_src, f_dst) 2150 else: 2151 do_copy = True 2152 if f in sphinx_dst_files: 2153 if filecmp.cmp(f_src, f_dst): 2154 do_copy = False 2155 2156 if do_copy: 2157 BPY_LOGGER.debug("\tupdating: %s" % f) 2158 shutil.copy(f_src, f_dst) 2159 2160 2161def refactor_sphinx_log(sphinx_logfile): 2162 refactored_log = [] 2163 with open(sphinx_logfile, "r", encoding="utf-8") as original_logfile: 2164 lines = set(original_logfile.readlines()) 2165 for line in lines: 2166 if 'warning' in line.lower() or 'error' in line.lower(): 2167 line = line.strip().split(None, 2) 2168 if len(line) == 3: 2169 location, kind, msg = line 2170 location = os.path.relpath(location, start=SPHINX_IN) 2171 refactored_log.append((kind, location, msg)) 2172 with open(sphinx_logfile, "w", encoding="utf-8") as refactored_logfile: 2173 for log in sorted(refactored_log): 2174 refactored_logfile.write("%-12s %s\n %s\n" % log) 2175 2176 2177def setup_monkey_patch(): 2178 filepath = os.path.join(SCRIPT_DIR, "sphinx_doc_gen_monkeypatch.py") 2179 global_namespace = {"__file__": filepath, "__name__": "__main__"} 2180 file = open(filepath, 'rb') 2181 exec(compile(file.read(), filepath, 'exec'), global_namespace) 2182 file.close() 2183 2184 2185# Avoid adding too many changes here. 2186def setup_blender(): 2187 import bpy 2188 2189 # Remove handlers since the functions get included 2190 # in the doc-string and don't have meaningful names. 2191 for ls in bpy.app.handlers: 2192 if isinstance(ls, list): 2193 ls.clear() 2194 2195 2196def main(): 2197 2198 # First monkey patch to load in fake members. 2199 setup_monkey_patch() 2200 2201 # Perform changes to Blender it's self. 2202 setup_blender() 2203 2204 # eventually, create the dirs 2205 for dir_path in [ARGS.output_dir, SPHINX_IN]: 2206 if not os.path.exists(dir_path): 2207 os.mkdir(dir_path) 2208 2209 # eventually, log in files 2210 if ARGS.log: 2211 bpy_logfile = os.path.join(ARGS.output_dir, ".bpy.log") 2212 bpy_logfilehandler = logging.FileHandler(bpy_logfile, mode="w") 2213 bpy_logfilehandler.setLevel(logging.DEBUG) 2214 BPY_LOGGER.addHandler(bpy_logfilehandler) 2215 2216 # using a FileHandler seems to disable the stdout, so we add a StreamHandler 2217 bpy_log_stdout_handler = logging.StreamHandler(stream=sys.stdout) 2218 bpy_log_stdout_handler.setLevel(logging.DEBUG) 2219 BPY_LOGGER.addHandler(bpy_log_stdout_handler) 2220 2221 # in case of out-of-source build, copy the needed dirs 2222 if ARGS.output_dir != SCRIPT_DIR: 2223 # examples dir 2224 examples_dir_copy = os.path.join(ARGS.output_dir, "examples") 2225 if os.path.exists(examples_dir_copy): 2226 shutil.rmtree(examples_dir_copy, True) 2227 shutil.copytree(EXAMPLES_DIR, 2228 examples_dir_copy, 2229 ignore=shutil.ignore_patterns(*(".svn",)), 2230 copy_function=shutil.copy) 2231 2232 # dump the api in rst files 2233 if os.path.exists(SPHINX_IN_TMP): 2234 shutil.rmtree(SPHINX_IN_TMP, True) 2235 2236 rna2sphinx(SPHINX_IN_TMP) 2237 2238 if ARGS.full_rebuild: 2239 # only for full updates 2240 shutil.rmtree(SPHINX_IN, True) 2241 shutil.copytree(SPHINX_IN_TMP, 2242 SPHINX_IN, 2243 copy_function=shutil.copy) 2244 if ARGS.sphinx_build and os.path.exists(SPHINX_OUT): 2245 shutil.rmtree(SPHINX_OUT, True) 2246 if ARGS.sphinx_build_pdf and os.path.exists(SPHINX_OUT_PDF): 2247 shutil.rmtree(SPHINX_OUT_PDF, True) 2248 else: 2249 # move changed files in SPHINX_IN 2250 align_sphinx_in_to_sphinx_in_tmp(SPHINX_IN_TMP, SPHINX_IN) 2251 2252 # report which example files weren't used 2253 EXAMPLE_SET_UNUSED = EXAMPLE_SET - EXAMPLE_SET_USED 2254 if EXAMPLE_SET_UNUSED: 2255 BPY_LOGGER.debug("\nUnused examples found in '%s'..." % EXAMPLES_DIR) 2256 for f in sorted(EXAMPLE_SET_UNUSED): 2257 BPY_LOGGER.debug(" %s.py" % f) 2258 BPY_LOGGER.debug(" %d total\n" % len(EXAMPLE_SET_UNUSED)) 2259 2260 # eventually, build the html docs 2261 if ARGS.sphinx_build: 2262 import subprocess 2263 subprocess.call(SPHINX_BUILD) 2264 2265 # sphinx-build log cleanup+sort 2266 if ARGS.log: 2267 if os.stat(SPHINX_BUILD_LOG).st_size: 2268 refactor_sphinx_log(SPHINX_BUILD_LOG) 2269 2270 # eventually, build the pdf docs 2271 if ARGS.sphinx_build_pdf: 2272 import subprocess 2273 subprocess.call(SPHINX_BUILD_PDF) 2274 subprocess.call(SPHINX_MAKE_PDF, stdout=SPHINX_MAKE_PDF_STDOUT) 2275 2276 # sphinx-build log cleanup+sort 2277 if ARGS.log: 2278 if os.stat(SPHINX_BUILD_PDF_LOG).st_size: 2279 refactor_sphinx_log(SPHINX_BUILD_PDF_LOG) 2280 2281 # eventually, prepare the dir to be deployed online (REFERENCE_PATH) 2282 if ARGS.pack_reference: 2283 2284 if ARGS.sphinx_build: 2285 # delete REFERENCE_PATH 2286 if os.path.exists(REFERENCE_PATH): 2287 shutil.rmtree(REFERENCE_PATH, True) 2288 2289 # copy SPHINX_OUT to the REFERENCE_PATH 2290 ignores = ('.doctrees', '.buildinfo') 2291 shutil.copytree(SPHINX_OUT, 2292 REFERENCE_PATH, 2293 ignore=shutil.ignore_patterns(*ignores)) 2294 2295 # zip REFERENCE_PATH 2296 basename = os.path.join(ARGS.output_dir, REFERENCE_NAME) 2297 tmp_path = shutil.make_archive(basename, 'zip', 2298 root_dir=ARGS.output_dir, 2299 base_dir=REFERENCE_NAME) 2300 final_path = os.path.join(REFERENCE_PATH, BLENDER_ZIP_FILENAME) 2301 os.rename(tmp_path, final_path) 2302 2303 if ARGS.sphinx_build_pdf: 2304 # copy the pdf to REFERENCE_PATH 2305 shutil.copy(os.path.join(SPHINX_OUT_PDF, "contents.pdf"), 2306 os.path.join(REFERENCE_PATH, BLENDER_PDF_FILENAME)) 2307 2308 sys.exit() 2309 2310 2311if __name__ == '__main__': 2312 main() 2313