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