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