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