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