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