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