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