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
1730     fw(title_string("Application Modules", "=", double=True))
1731     fw(".. toctree::\n")
1732     fw("   :maxdepth: 1\n\n")
1733
1734     app_modules = (
1735         "bpy.context",  # note: not actually a module
1736         "bpy.data",     # note: not actually a module
1737         "bpy.ops",
1738         "bpy.types",
1739
1740         # py modules
1741         "bpy.utils",
1742         "bpy.utils.previews",
1743         "bpy.path",
1744         "bpy.app",
1745
1746         # C modules
1747         "bpy.props",
1748     )
1749
1750     for mod in app_modules:
1751         if mod not in EXCLUDE_MODULES:
1752             fw("   %s\n\n" % mod)
1753
1754     fw(title_string("Standalone Modules", "=", double=True))
1755     fw(".. toctree::\n")
1756     fw("   :maxdepth: 1\n\n")
1757
1758     standalone_modules = (
1759         # submodules are added in parent page
1760         "mathutils", "freestyle", "bgl", "blf", "gpu",
1761         "aud", "bpy_extras", "idprop.types", "bmesh",
1762     )
1763
1764     for mod in standalone_modules:
1765         if mod not in EXCLUDE_MODULES:
1766             fw("   %s\n\n" % mod)
1767
1768     # special case, this 'bmesh.ops.rst' is extracted from C source
1769     if "bmesh.ops" not in EXCLUDE_MODULES:
1770         execfile(os.path.join(SCRIPT_DIR, "rst_from_bmesh_opdefines.py"))
1771
1772     # game engine
1773     if "bge" not in EXCLUDE_MODULES:
1774         fw(title_string("Game Engine Modules", "=", double=True))
1775         fw(".. toctree::\n")
1776         fw("   :maxdepth: 1\n\n")
1777         fw("   bge.types.rst\n\n")
1778         fw("   bge.logic.rst\n\n")
1779         fw("   bge.render.rst\n\n")
1780         fw("   bge.texture.rst\n\n")
1781         fw("   bge.events.rst\n\n")
1782         fw("   bge.constraints.rst\n\n")
1783         fw("   bge.app.rst\n\n")
1784
1785     # rna generated change log
1786     fw(title_string("API Info", "=", double=True))
1787     fw(".. toctree::\n")
1788     fw("   :maxdepth: 1\n\n")
1789     fw("   change_log.rst\n\n")
1790
1791     fw("\n")
1792     fw("\n")
1793     fw(".. note:: The Blender Python API has areas which are still in development.\n")
1794     fw("   \n")
1795     fw("   The following areas are subject to change.\n")
1796     fw("      * operator behavior, names and arguments\n")
1797     fw("      * mesh creation and editing functions\n")
1798     fw("   \n")
1799     fw("   These parts of the API are relatively stable and are unlikely to change significantly\n")
1800     fw("      * data API, access to attributes of Blender data such as mesh verts, material color,\n")
1801     fw("        timeline frames and scene objects\n")
1802     fw("      * user interface functions for defining buttons, creation of menus, headers, panels\n")
1803     fw("      * render engine integration\n")
1804     fw("      * modules: bgl, mathutils & game engine.\n")
1805     fw("\n")
1806
1807     file.close()
1808
1809
1810 def write_rst_bpy(basepath):
1811     '''
1812     Write rst file of bpy module (disabled by default)
1813     '''
1814     if ARGS.bpy:
1815         filepath = os.path.join(basepath, "bpy.rst")
1816         file = open(filepath, "w", encoding="utf-8")
1817         fw = file.write
1818
1819         fw("\n")
1820
1821         title = ":mod:`bpy` --- Blender Python Module"
1822
1823         fw(title_string(title, "="))
1824
1825         fw(".. module:: bpy.types\n\n")
1826         file.close()
1827
1828
1829 def write_rst_types_index(basepath):
1830     '''
1831     Write the rst file of bpy.types module (index)
1832     '''
1833     if "bpy.types" not in EXCLUDE_MODULES:
1834         filepath = os.path.join(basepath, "bpy.types.rst")
1835         file = open(filepath, "w", encoding="utf-8")
1836         fw = file.write
1837         fw(title_string("Types (bpy.types)", "="))
1838         fw(".. toctree::\n")
1839         fw("   :glob:\n\n")
1840         fw("   bpy.types.*\n\n")
1841         file.close()
1842
1843
1844 def write_rst_ops_index(basepath):
1845     '''
1846     Write the rst file of bpy.ops module (index)
1847     '''
1848     if "bpy.ops" not in EXCLUDE_MODULES:
1849         filepath = os.path.join(basepath, "bpy.ops.rst")
1850         file = open(filepath, "w", encoding="utf-8")
1851         fw = file.write
1852         fw(title_string("Operators (bpy.ops)", "="))
1853         write_example_ref("", fw, "bpy.ops")
1854         fw(".. toctree::\n")
1855         fw("   :glob:\n\n")
1856         fw("   bpy.ops.*\n\n")
1857         file.close()
1858
1859
1860 def write_rst_data(basepath):
1861     '''
1862     Write the rst file of bpy.data module
1863     '''
1864     if "bpy.data" not in EXCLUDE_MODULES:
1865         # not actually a module, only write this file so we
1866         # can reference in the TOC
1867         filepath = os.path.join(basepath, "bpy.data.rst")
1868         file = open(filepath, "w", encoding="utf-8")
1869         fw = file.write
1870         fw(title_string("Data Access (bpy.data)", "="))
1871         fw(".. module:: bpy\n")
1872         fw("\n")
1873         fw("This module is used for all Blender/Python access.\n")
1874         fw("\n")
1875         fw(".. data:: data\n")
1876         fw("\n")
1877         fw("   Access to Blender's internal data\n")
1878         fw("\n")
1879         fw("   :type: :class:`bpy.types.BlendData`\n")
1880         fw("\n")
1881         fw(".. literalinclude:: ../examples/bpy.data.py\n")
1882         file.close()
1883
1884         EXAMPLE_SET_USED.add("bpy.data")
1885
1886
1887 def write_rst_importable_modules(basepath):
1888     '''
1889     Write the rst files of importable modules
1890     '''
1891     importable_modules = {
1892         # Python_modules
1893         "bpy.path": "Path Utilities",
1894         "bpy.utils": "Utilities",
1895         "bpy_extras": "Extra Utilities",
1896
1897         # C_modules
1898         "aud": "Audio System",
1899         "blf": "Font Drawing",
1900         "gpu.offscreen": "GPU Off-Screen Buffer",
1901         "bmesh": "BMesh Module",
1902         "bmesh.types": "BMesh Types",
1903         "bmesh.utils": "BMesh Utilities",
1904         "bmesh.geometry": "BMesh Geometry Utilities",
1905         "bpy.app": "Application Data",
1906         "bpy.app.handlers": "Application Handlers",
1907         "bpy.app.translations": "Application Translations",
1908         "bpy.props": "Property Definitions",
1909         "idprop.types": "ID Property Access",
1910         "mathutils": "Math Types & Utilities",
1911         "mathutils.geometry": "Geometry Utilities",
1912         "mathutils.bvhtree": "BVHTree Utilities",
1913         "mathutils.kdtree": "KDTree Utilities",
1914         "mathutils.interpolate": "Interpolation Utilities",
1915         "mathutils.noise": "Noise Utilities",
1916         "freestyle": "Freestyle Module",
1917         "freestyle.types": "Freestyle Types",
1918         "freestyle.predicates": "Freestyle Predicates",
1919         "freestyle.functions": "Freestyle Functions",
1920         "freestyle.chainingiterators": "Freestyle Chaining Iterators",
1921         "freestyle.shaders": "Freestyle Shaders",
1922         "freestyle.utils": "Freestyle Utilities",
1923     }
1924     for mod_name, mod_descr in importable_modules.items():
1925         if mod_name not in EXCLUDE_MODULES:
1926             module = __import__(mod_name,
1927                                 fromlist=[mod_name.rsplit(".", 1)[-1]])
1928             pymodule2sphinx(basepath, mod_name, module, mod_descr)
1929
1930
1931 def copy_handwritten_rsts(basepath):
1932
1933     # info docs
1934     if not EXCLUDE_INFO_DOCS:
1935         for info, info_desc in INFO_DOCS:
1936             shutil.copy2(os.path.join(RST_DIR, info), basepath)
1937
1938     # TODO put this docs in Blender's code and use import as per modules above
1939     handwritten_modules = [
1940         "bge.logic",
1941         "bge.render",
1942         "bge.texture",
1943         "bge.events",
1944         "bge.constraints",
1945         "bge.app",
1946         "bgl",  # "Blender OpenGl wrapper"
1947         "gpu",  # "GPU Shader Module"
1948
1949         "bmesh.ops",  # generated by rst_from_bmesh_opdefines.py
1950
1951         # includes...
1952         "include__bmesh",
1953     ]
1954     for mod_name in handwritten_modules:
1955         if mod_name not in EXCLUDE_MODULES:
1956             # copy2 keeps time/date stamps
1957             shutil.copy2(os.path.join(RST_DIR, "%s.rst" % mod_name), basepath)
1958
1959     if "bge.types" not in EXCLUDE_MODULES:
1960         shutil.copy2(os.path.join(RST_DIR, "bge.types.rst"), basepath)
1961
1962         bge_types_dir = os.path.join(RST_DIR, "bge_types")
1963
1964         for i in os.listdir(bge_types_dir):
1965             if i.startswith("."):
1966                 # Avoid things like .svn dir...
1967                 continue
1968             shutil.copy2(os.path.join(bge_types_dir, i), basepath)
1969
1970     # changelog
1971     shutil.copy2(os.path.join(RST_DIR, "change_log.rst"), basepath)
1972
1973     # copy images, could be smarter but just glob for now.
1974     for f in os.listdir(RST_DIR):
1975         if f.endswith(".png"):
1976             shutil.copy2(os.path.join(RST_DIR, f), basepath)
1977
1978
1979 def copy_handwritten_extra(basepath):
1980     for f_src in EXTRA_SOURCE_FILES:
1981         if os.sep != "/":
1982             f_src = os.sep.join(f_src.split("/"))
1983
1984         f_dst = f_src.replace("..", "__")
1985
1986         f_src = os.path.join(RST_DIR, f_src)
1987         f_dst = os.path.join(basepath, f_dst)
1988
1989         os.makedirs(os.path.dirname(f_dst), exist_ok=True)
1990
1991         shutil.copy2(f_src, f_dst)
1992
1993
1994 def rna2sphinx(basepath):
1995
1996     try:
1997         os.mkdir(basepath)
1998     except:
1999         pass
2000
2001     # sphinx setup
2002     write_sphinx_conf_py(basepath)
2003
2004     # main page
2005     write_rst_contents(basepath)
2006
2007     # context
2008     if "bpy.context" not in EXCLUDE_MODULES:
2009         # one of a kind, context doc (uses ctypes to extract info!)
2010         # doesn't work on mac and windows
2011         if PLATFORM not in {"darwin", "windows"}:
2012             pycontext2sphinx(basepath)
2013
2014     # internal modules
2015     write_rst_bpy(basepath)                 # bpy, disabled by default
2016     write_rst_types_index(basepath)         # bpy.types
2017     write_rst_ops_index(basepath)           # bpy.ops
2018     pyrna2sphinx(basepath)                  # bpy.types.* and bpy.ops.*
2019     write_rst_data(basepath)                # bpy.data
2020     write_rst_importable_modules(basepath)
2021
2022     # copy the other rsts
2023     copy_handwritten_rsts(basepath)
2024
2025     # copy source files referenced
2026     copy_handwritten_extra(basepath)
2027
2028
2029 def align_sphinx_in_to_sphinx_in_tmp(dir_src, dir_dst):
2030     '''
2031     Move changed files from SPHINX_IN_TMP to SPHINX_IN
2032     '''
2033     import filecmp
2034
2035     # possible the dir doesn't exist when running recursively
2036     os.makedirs(dir_dst, exist_ok=True)
2037
2038     sphinx_dst_files = set(os.listdir(dir_dst))
2039     sphinx_src_files = set(os.listdir(dir_src))
2040
2041     # remove deprecated files that have been removed
2042     for f in sorted(sphinx_dst_files):
2043         if f not in sphinx_src_files:
2044             BPY_LOGGER.debug("\tdeprecated: %s" % f)
2045             f_dst = os.path.join(dir_dst, f)
2046             if os.path.isdir(f_dst):
2047                 shutil.rmtree(f_dst, True)
2048             else:
2049                 os.remove(f_dst)
2050
2051     # freshen with new files.
2052     for f in sorted(sphinx_src_files):
2053         f_src = os.path.join(dir_src, f)
2054         f_dst = os.path.join(dir_dst, f)
2055
2056         if os.path.isdir(f_src):
2057             align_sphinx_in_to_sphinx_in_tmp(f_src, f_dst)
2058         else:
2059             do_copy = True
2060             if f in sphinx_dst_files:
2061                 if filecmp.cmp(f_src, f_dst):
2062                     do_copy = False
2063
2064             if do_copy:
2065                 BPY_LOGGER.debug("\tupdating: %s" % f)
2066                 shutil.copy(f_src, f_dst)
2067
2068
2069 def refactor_sphinx_log(sphinx_logfile):
2070     refactored_log = []
2071     with open(sphinx_logfile, "r", encoding="utf-8") as original_logfile:
2072         lines = set(original_logfile.readlines())
2073         for line in lines:
2074             if 'warning' in line.lower() or 'error' in line.lower():
2075                 line = line.strip().split(None, 2)
2076                 if len(line) == 3:
2077                     location, kind, msg = line
2078                     location = os.path.relpath(location, start=SPHINX_IN)
2079                     refactored_log.append((kind, location, msg))
2080     with open(sphinx_logfile, "w", encoding="utf-8") as refactored_logfile:
2081         for log in sorted(refactored_log):
2082             refactored_logfile.write("%-12s %s\n             %s\n" % log)
2083
2084
2085 def monkey_patch():
2086     filepath = os.path.join(SCRIPT_DIR, "sphinx_doc_gen_monkeypatch.py")
2087     global_namespace = {"__file__": filepath, "__name__": "__main__"}
2088     file = open(filepath, 'rb')
2089     exec(compile(file.read(), filepath, 'exec'), global_namespace)
2090     file.close()
2091
2092
2093 def main():
2094
2095     # first monkey patch to load in fake members
2096     monkey_patch()
2097
2098     # eventually, create the dirs
2099     for dir_path in [ARGS.output_dir, SPHINX_IN]:
2100         if not os.path.exists(dir_path):
2101             os.mkdir(dir_path)
2102
2103     # eventually, log in files
2104     if ARGS.log:
2105         bpy_logfile = os.path.join(ARGS.output_dir, ".bpy.log")
2106         bpy_logfilehandler = logging.FileHandler(bpy_logfile, mode="w")
2107         bpy_logfilehandler.setLevel(logging.DEBUG)
2108         BPY_LOGGER.addHandler(bpy_logfilehandler)
2109
2110         # using a FileHandler seems to disable the stdout, so we add a StreamHandler
2111         bpy_log_stdout_handler = logging.StreamHandler(stream=sys.stdout)
2112         bpy_log_stdout_handler.setLevel(logging.DEBUG)
2113         BPY_LOGGER.addHandler(bpy_log_stdout_handler)
2114
2115     # in case of out-of-source build, copy the needed dirs
2116     if ARGS.output_dir != SCRIPT_DIR:
2117         # examples dir
2118         examples_dir_copy = os.path.join(ARGS.output_dir, "examples")
2119         if os.path.exists(examples_dir_copy):
2120             shutil.rmtree(examples_dir_copy, True)
2121         shutil.copytree(EXAMPLES_DIR,
2122                         examples_dir_copy,
2123                         ignore=shutil.ignore_patterns(*(".svn",)),
2124                         copy_function=shutil.copy)
2125
2126         # eventually, copy the theme dir
2127         if ARGS.sphinx_theme == "blender-org":
2128             if os.path.exists(SPHINX_THEME_DIR):
2129                 shutil.rmtree(SPHINX_THEME_DIR, True)
2130             shutil.copytree(SPHINX_THEME_SVN_DIR,
2131                             SPHINX_THEME_DIR,
2132                             ignore=shutil.ignore_patterns(*(".svn",)),
2133                             copy_function=shutil.copy)
2134
2135     # dump the api in rst files
2136     if os.path.exists(SPHINX_IN_TMP):
2137         shutil.rmtree(SPHINX_IN_TMP, True)
2138
2139     rna2sphinx(SPHINX_IN_TMP)
2140
2141     if ARGS.full_rebuild:
2142         # only for full updates
2143         shutil.rmtree(SPHINX_IN, True)
2144         shutil.copytree(SPHINX_IN_TMP,
2145                         SPHINX_IN,
2146                         copy_function=shutil.copy)
2147         if ARGS.sphinx_build and os.path.exists(SPHINX_OUT):
2148             shutil.rmtree(SPHINX_OUT, True)
2149         if ARGS.sphinx_build_pdf and os.path.exists(SPHINX_OUT_PDF):
2150             shutil.rmtree(SPHINX_OUT_PDF, True)
2151     else:
2152         # move changed files in SPHINX_IN
2153         align_sphinx_in_to_sphinx_in_tmp(SPHINX_IN_TMP, SPHINX_IN)
2154
2155     # report which example files weren't used
2156     EXAMPLE_SET_UNUSED = EXAMPLE_SET - EXAMPLE_SET_USED
2157     if EXAMPLE_SET_UNUSED:
2158         BPY_LOGGER.debug("\nUnused examples found in '%s'..." % EXAMPLES_DIR)
2159         for f in sorted(EXAMPLE_SET_UNUSED):
2160             BPY_LOGGER.debug("    %s.py" % f)
2161         BPY_LOGGER.debug("  %d total\n" % len(EXAMPLE_SET_UNUSED))
2162
2163     # eventually, build the html docs
2164     if ARGS.sphinx_build:
2165         import subprocess
2166         subprocess.call(SPHINX_BUILD)
2167
2168         # sphinx-build log cleanup+sort
2169         if ARGS.log:
2170             if os.stat(SPHINX_BUILD_LOG).st_size:
2171                 refactor_sphinx_log(SPHINX_BUILD_LOG)
2172
2173     # eventually, build the pdf docs
2174     if ARGS.sphinx_build_pdf:
2175         import subprocess
2176         subprocess.call(SPHINX_BUILD_PDF)
2177         subprocess.call(SPHINX_MAKE_PDF, stdout=SPHINX_MAKE_PDF_STDOUT)
2178
2179         # sphinx-build log cleanup+sort
2180         if ARGS.log:
2181             if os.stat(SPHINX_BUILD_PDF_LOG).st_size:
2182                 refactor_sphinx_log(SPHINX_BUILD_PDF_LOG)
2183
2184     # eventually, prepare the dir to be deployed online (REFERENCE_PATH)
2185     if ARGS.pack_reference:
2186
2187         if ARGS.sphinx_build:
2188             # delete REFERENCE_PATH
2189             if os.path.exists(REFERENCE_PATH):
2190                 shutil.rmtree(REFERENCE_PATH, True)
2191
2192             # copy SPHINX_OUT to the REFERENCE_PATH
2193             ignores = ('.doctrees', 'objects.inv', '.buildinfo')
2194             shutil.copytree(SPHINX_OUT,
2195                             REFERENCE_PATH,
2196                             ignore=shutil.ignore_patterns(*ignores))
2197             shutil.copy(os.path.join(REFERENCE_PATH, "contents.html"),
2198                         os.path.join(REFERENCE_PATH, "index.html"))
2199
2200             # zip REFERENCE_PATH
2201             basename = os.path.join(ARGS.output_dir, REFERENCE_NAME)
2202             tmp_path = shutil.make_archive(basename, 'zip',
2203                                            root_dir=ARGS.output_dir,
2204                                            base_dir=REFERENCE_NAME)
2205             final_path = os.path.join(REFERENCE_PATH, BLENDER_ZIP_FILENAME)
2206             os.rename(tmp_path, final_path)
2207
2208         if ARGS.sphinx_build_pdf:
2209             # copy the pdf to REFERENCE_PATH
2210             shutil.copy(os.path.join(SPHINX_OUT_PDF, "contents.pdf"),
2211                         os.path.join(REFERENCE_PATH, BLENDER_PDF_FILENAME))
2212
2213     sys.exit()
2214
2215
2216 if __name__ == '__main__':
2217     main()