fix for doc building after pepper merge, also WIP tips/tricks.
[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 Usage:
25
26 For HTML generation
27 -------------------
28 - Run this script from blenders root path once you have compiled blender
29
30     ./blender.bin --background --python doc/python_api/sphinx_doc_gen.py
31
32   This will generate python files in doc/python_api/sphinx-in/
33   providing ./blender.bin is or links to the blender executable
34
35 - Generate html docs by running...
36
37     cd doc/python_api
38     sphinx-build sphinx-in sphinx-out
39
40   This requires sphinx 1.0.7 to be installed.
41
42 For PDF generation
43 ------------------
44 - After you have built doc/python_api/sphinx-in (see above), run:
45
46     sphinx-build -b latex doc/python_api/sphinx-in doc/python_api/sphinx-out
47     cd doc/python_api/sphinx-out
48     make
49 '''
50
51 # Check we're running in blender
52 if __import__("sys").modules.get("bpy") is None:
53     print("\nError, this script must run from inside blender2.5")
54     print(script_help_msg)
55
56     import sys
57     sys.exit()
58
59
60 # Switch for quick testing
61 if 1:
62     # full build
63     EXCLUDE_INFO_DOCS = False
64     EXCLUDE_MODULES = ()
65     FILTER_BPY_TYPES = None
66     FILTER_BPY_OPS = None
67
68 else:
69     EXCLUDE_INFO_DOCS = False
70     # for testing so doc-builds dont take so long.
71     EXCLUDE_MODULES = (
72         "bpy.context",
73         "bpy.app",
74         "bpy.path",
75         "bpy.data",
76         "bpy.props",
77         "bpy.utils",
78         "bpy.context",
79         "bpy.types",  # supports filtering
80         "bpy.ops",  # supports filtering
81         "bpy_extras",
82         "bge",
83         "aud",
84         "bgl",
85         "blf",
86         "mathutils",
87         "mathutils.geometry",
88     )
89
90     FILTER_BPY_TYPES = ("bpy_struct", "Panel", "ID")  # allow
91     FILTER_BPY_OPS = ("import.scene", )  # allow
92
93     # for quick rebuilds
94     """
95 rm -rf /b/doc/python_api/sphinx-* && \
96 ./blender.bin --background --factory-startup --python  doc/python_api/sphinx_doc_gen.py && \
97 sphinx-build doc/python_api/sphinx-in doc/python_api/sphinx-out
98
99     """
100
101 # extra info, not api reference docs
102 # stored in ./rst/info/
103 INFO_DOCS = (
104     ("info_quickstart.rst", "Blender/Python Quickstart: new to blender/scripting and want to get you're feet wet?"),
105     ("info_overview.rst", "Blender/Python API Overview: a more complete explanation of python integration"),
106     ("info_gotcha.rst", "Gotcha's: some of the problems you may come up against when writing scripts"),
107     )
108
109 # import rpdb2; rpdb2.start_embedded_debugger('test')
110
111 import os
112 import inspect
113 import bpy
114 import rna_info
115
116 # lame, python wont give some access
117 ClassMethodDescriptorType = type(dict.__dict__['fromkeys'])
118 MethodDescriptorType = type(dict.get)
119 GetSetDescriptorType = type(int.real)
120
121 EXAMPLE_SET = set()
122 EXAMPLE_SET_USED = set()
123
124 _BPY_STRUCT_FAKE = "bpy_struct"
125 _BPY_PROP_COLLECTION_FAKE = "bpy_prop_collection"
126 _BPY_FULL_REBUILD = False
127
128 if _BPY_PROP_COLLECTION_FAKE:
129     _BPY_PROP_COLLECTION_ID = ":class:`%s`" % _BPY_PROP_COLLECTION_FAKE
130 else:
131     _BPY_PROP_COLLECTION_ID = "collection"
132
133
134 def undocumented_message(module_name, type_name, identifier):
135     if str(type_name).startswith('<module'):
136         preloadtitle = '%s.%s' % (module_name, identifier)
137     else:
138         preloadtitle = '%s.%s.%s' % (module_name, type_name, identifier)
139     message = "Undocumented (`contribute "\
140         "<http://wiki.blender.org/index.php/Dev:2.5/Py/API/Documentation/Contribute"\
141         "?action=edit&section=new&preload=Dev:2.5/Py/API/Documentation/Contribute/Howto-message"\
142         "&preloadtitle=%s>`_)\n\n" % preloadtitle
143     return message
144
145
146 def range_str(val):
147     '''
148     Converts values to strings for the range directive.
149     (unused function it seems)
150     '''
151     if val < -10000000:
152         return '-inf'
153     elif val > 10000000:
154         return 'inf'
155     elif type(val) == float:
156         return '%g' % val
157     else:
158         return str(val)
159
160
161 def example_extract_docstring(filepath):
162     file = open(filepath, 'r')
163     line = file.readline()
164     line_no = 0
165     text = []
166     if line.startswith('"""'):  # assume nothing here
167         line_no += 1
168     else:
169         file.close()
170         return "", 0
171
172     for line in file.readlines():
173         line_no += 1
174         if line.startswith('"""'):
175             break
176         else:
177             text.append(line.rstrip())
178
179     line_no += 1
180     file.close()
181     return "\n".join(text), line_no
182
183
184 def write_title(fw, text, heading_char):
185     fw("%s\n%s\n\n" % (text, len(text) * heading_char))
186
187
188 def write_example_ref(ident, fw, example_id, ext="py"):
189     if example_id in EXAMPLE_SET:
190
191         # extract the comment
192         filepath = "../examples/%s.%s" % (example_id, ext)
193         filepath_full = os.path.join(os.path.dirname(fw.__self__.name), filepath)
194
195         text, line_no = example_extract_docstring(filepath_full)
196
197         for line in text.split("\n"):
198             fw("%s\n" % (ident + line).rstrip())
199         fw("\n")
200
201         fw("%s.. literalinclude:: %s\n" % (ident, filepath))
202         if line_no > 0:
203             fw("%s   :lines: %d-\n" % (ident, line_no))
204         fw("\n")
205         EXAMPLE_SET_USED.add(example_id)
206     else:
207         if bpy.app.debug:
208             print("\tskipping example:", example_id)
209
210     # Support for numbered files bpy.types.Operator -> bpy.types.Operator.1.py
211     i = 1
212     while True:
213         example_id_num = "%s.%d" % (example_id, i)
214         if example_id_num in EXAMPLE_SET:
215             write_example_ref(ident, fw, example_id_num, ext)
216             i += 1
217         else:
218             break
219
220
221 def write_indented_lines(ident, fn, text, strip=True):
222     '''
223     Apply same indentation to all lines in a multilines text.
224     '''
225     if text is None:
226         return
227
228     lines = text.split("\n")
229
230     # strip empty lines from the start/end
231     while lines and not lines[0].strip():
232         del lines[0]
233     while lines and not lines[-1].strip():
234         del lines[-1]
235
236     if strip:
237         ident_strip = 1000
238         for l in lines:
239             if l.strip():
240                 ident_strip = min(ident_strip, len(l) - len(l.lstrip()))
241         for l in lines:
242             fn(ident + l[ident_strip:] + "\n")
243     else:
244         for l in lines:
245             fn(ident + l + "\n")
246
247
248 def pymethod2sphinx(ident, fw, identifier, py_func):
249     '''
250     class method to sphinx
251     '''
252     arg_str = inspect.formatargspec(*inspect.getargspec(py_func))
253     if arg_str.startswith("(self, "):
254         arg_str = "(" + arg_str[7:]
255         func_type = "method"
256     elif arg_str.startswith("(cls, "):
257         arg_str = "(" + arg_str[6:]
258         func_type = "classmethod"
259     else:
260         func_type = "staticmethod"
261
262     fw(ident + ".. %s:: %s%s\n\n" % (func_type, identifier, arg_str))
263     if py_func.__doc__:
264         write_indented_lines(ident + "   ", fw, py_func.__doc__)
265         fw("\n")
266
267
268 def pyfunc2sphinx(ident, fw, identifier, py_func, is_class=True):
269     '''
270     function or class method to sphinx
271     '''
272     arg_str = inspect.formatargspec(*inspect.getargspec(py_func))
273
274     if not is_class:
275         func_type = "function"
276
277         # ther rest are class methods
278     elif arg_str.startswith("(self, "):
279         arg_str = "(" + arg_str[7:]
280         func_type = "method"
281     elif arg_str.startswith("(cls, "):
282         arg_str = "(" + arg_str[6:]
283         func_type = "classmethod"
284     else:
285         func_type = "staticmethod"
286
287     fw(ident + ".. %s:: %s%s\n\n" % (func_type, identifier, arg_str))
288     if py_func.__doc__:
289         write_indented_lines(ident + "   ", fw, py_func.__doc__)
290         fw("\n")
291
292
293 def py_descr2sphinx(ident, fw, descr, module_name, type_name, identifier):
294     if identifier.startswith("_"):
295         return
296
297     doc = descr.__doc__
298     if not doc:
299         doc = undocumented_message(module_name, type_name, identifier)
300
301     if type(descr) == GetSetDescriptorType:
302         fw(ident + ".. attribute:: %s\n\n" % identifier)
303         write_indented_lines(ident + "   ", fw, doc, False)
304         fw("\n")
305     elif type(descr) in (MethodDescriptorType, ClassMethodDescriptorType):
306         write_indented_lines(ident, fw, doc, False)
307         fw("\n")
308     else:
309         raise TypeError("type was not GetSetDescriptorType, MethodDescriptorType or ClassMethodDescriptorType")
310
311     write_example_ref(ident + "   ", fw, module_name + "." + type_name + "." + identifier)
312     fw("\n")
313
314
315 def py_c_func2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_class=True):
316     '''
317     c defined function to sphinx.
318     '''
319
320     # dump the docstring, assume its formatted correctly
321     if py_func.__doc__:
322         write_indented_lines(ident, fw, py_func.__doc__, False)
323         fw("\n")
324     else:
325         fw(ident + ".. function:: %s()\n\n" % identifier)
326         fw(ident + "   " + undocumented_message(module_name, type_name, identifier))
327
328     if is_class:
329         write_example_ref(ident + "   ", fw, module_name + "." + type_name + "." + identifier)
330     else:
331         write_example_ref(ident + "   ", fw, module_name + "." + identifier)
332
333     fw("\n")
334
335
336 def pyprop2sphinx(ident, fw, identifier, py_prop):
337     '''
338     python property to sphinx
339     '''
340     # readonly properties use "data" directive, variables use "attribute" directive
341     if py_prop.fset is None:
342         fw(ident + ".. data:: %s\n\n" % identifier)
343     else:
344         fw(ident + ".. attribute:: %s\n\n" % identifier)
345     write_indented_lines(ident + "   ", fw, py_prop.__doc__)
346     if py_prop.fset is None:
347         fw(ident + "   (readonly)\n\n")
348
349
350 def pymodule2sphinx(BASEPATH, module_name, module, title):
351     import types
352     attribute_set = set()
353     filepath = os.path.join(BASEPATH, module_name + ".rst")
354
355     module_all = getattr(module, "__all__", None)
356     module_dir = sorted(dir(module))
357
358     if module_all:
359         module_dir = module_all
360
361     file = open(filepath, "w")
362
363     fw = file.write
364
365     write_title(fw, "%s (%s)" % (title, module_name), "=")
366
367     fw(".. module:: %s\n\n" % module_name)
368
369     if module.__doc__:
370         # Note, may contain sphinx syntax, dont mangle!
371         fw(module.__doc__.strip())
372         fw("\n\n")
373
374     write_example_ref("", fw, module_name)
375
376     # write submodules
377     # we could also scan files but this ensures __all__ is used correctly
378     if module_all is not None:
379         submod_name = None
380         submod = None
381         submod_ls = []
382         for submod_name in module_all:
383             ns = {}
384             exec_str = "from %s import %s as submod" % (module.__name__, submod_name)
385             exec(exec_str, ns, ns)
386             submod = ns["submod"]
387             if type(submod) == types.ModuleType:
388                 submod_ls.append((submod_name, submod))
389
390         del submod_name
391         del submod
392
393         if submod_ls:
394             fw(".. toctree::\n")
395             fw("   :maxdepth: 1\n\n")
396
397             for submod_name, submod in submod_ls:
398                 submod_name_full = "%s.%s" % (module_name, submod_name)
399                 fw("   %s.rst\n\n" % submod_name_full)
400
401                 pymodule2sphinx(BASEPATH, submod_name_full, submod, "%s submodule" % module_name)
402         del submod_ls
403     # done writing submodules!
404
405     # write members of the module
406     # only tested with PyStructs which are not exactly modules
407     for key, descr in sorted(type(module).__dict__.items()):
408         if key.startswith("__"):
409             continue
410         # naughty, we also add getset's into PyStructs, this is not typical py but also not incorrect.
411         if type(descr) == types.GetSetDescriptorType:  # 'bpy_app_type' name is only used for examples and messages
412             py_descr2sphinx("", fw, descr, module_name, "bpy_app_type", key)
413             attribute_set.add(key)
414     for key, descr in sorted(type(module).__dict__.items()):
415         if key.startswith("__"):
416             continue
417
418         if type(descr) == types.MemberDescriptorType:
419             if descr.__doc__:
420                 fw(".. data:: %s\n\n" % key)
421                 write_indented_lines("   ", fw, descr.__doc__, False)
422                 fw("\n")
423                 attribute_set.add(key)
424
425     del key, descr
426
427     classes = []
428     submodules = []
429
430     for attribute in module_dir:
431         if not attribute.startswith("_"):
432             if attribute in attribute_set:
433                 continue
434
435             if attribute.startswith("n_"):  # annoying exception, needed for bpy.app
436                 continue
437
438             value = getattr(module, attribute)
439
440             value_type = type(value)
441
442             if value_type == types.FunctionType:
443                 pyfunc2sphinx("", fw, attribute, value, is_class=False)
444             elif value_type in (types.BuiltinMethodType, types.BuiltinFunctionType):  # both the same at the moment but to be future proof
445                 # note: can't get args from these, so dump the string as is
446                 # this means any module used like this must have fully formatted docstrings.
447                 py_c_func2sphinx("", fw, module_name, None, attribute, value, is_class=False)
448             elif value_type == type:
449                 classes.append((attribute, value))
450             elif issubclass(value_type, types.ModuleType):
451                 submodules.append((attribute, value))
452             elif value_type in (bool, int, float, str, tuple):
453                 # constant, not much fun we can do here except to list it.
454                 # TODO, figure out some way to document these!
455                 fw(".. data:: %s\n\n" % attribute)
456                 write_indented_lines("   ", fw, "constant value %s" % repr(value), False)
457                 fw("\n")
458             else:
459                 print("\tnot documenting %s.%s of %r type" % (module_name, attribute, value_type.__name__))
460                 continue
461
462             attribute_set.add(attribute)
463             # TODO, more types...
464
465     # TODO, bpy_extras does this already, mathutils not.
466     """
467     if submodules:
468         fw("\n"
469            "**********\n"
470            "Submodules\n"
471            "**********\n"
472            "\n"
473            )
474         for attribute, submod in submodules:
475             fw("* :mod:`%s.%s`\n" % (module_name, attribute))
476         fw("\n")
477     """
478
479     # write collected classes now
480     for (type_name, value) in classes:
481         # May need to be its own function
482         fw(".. class:: %s\n\n" % type_name)
483         if value.__doc__:
484             write_indented_lines("   ", fw, value.__doc__, False)
485             fw("\n")
486         write_example_ref("   ", fw, module_name + "." + type_name)
487
488         descr_items = [(key, descr) for key, descr in sorted(value.__dict__.items()) if not key.startswith("__")]
489
490         for key, descr in descr_items:
491             if type(descr) == ClassMethodDescriptorType:
492                 py_descr2sphinx("   ", fw, descr, module_name, type_name, key)
493
494         for key, descr in descr_items:
495             if type(descr) == MethodDescriptorType:
496                 py_descr2sphinx("   ", fw, descr, module_name, type_name, key)
497
498         for key, descr in descr_items:
499             if type(descr) == GetSetDescriptorType:
500                 py_descr2sphinx("   ", fw, descr, module_name, type_name, key)
501
502         fw("\n\n")
503
504     file.close()
505
506
507 def pycontext2sphinx(BASEPATH):
508     # Only use once. very irregular
509
510     filepath = os.path.join(BASEPATH, "bpy.context.rst")
511     file = open(filepath, "w")
512     fw = file.write
513     fw("Context Access (bpy.context)\n")
514     fw("============================\n\n")
515     fw(".. module:: bpy.context\n")
516     fw("\n")
517     fw("The context members available depend on the area of blender which is currently being accessed.\n")
518     fw("\n")
519     fw("Note that all context values are readonly, but may be modified through the data api or by running operators\n\n")
520
521     # nasty, get strings directly from blender because there is no other way to get it
522     import ctypes
523
524     context_strings = (
525         "screen_context_dir",
526         "view3d_context_dir",
527         "buttons_context_dir",
528         "image_context_dir",
529         "node_context_dir",
530         "text_context_dir",
531     )
532
533     # Changes in blender will force errors here
534     type_map = {
535         "active_base": ("ObjectBase", False),
536         "active_bone": ("Bone", False),
537         "active_object": ("Object", False),
538         "active_pose_bone": ("PoseBone", False),
539         "armature": ("Armature", False),
540         "bone": ("Bone", False),
541         "brush": ("Brush", False),
542         "camera": ("Camera", False),
543         "cloth": ("ClothModifier", False),
544         "collision": ("CollisionModifier", False),
545         "curve": ("Curve", False),
546         "edit_bone": ("EditBone", False),
547         "edit_image": ("Image", False),
548         "edit_object": ("Object", False),
549         "edit_text": ("Text", False),
550         "editable_bones": ("EditBone", True),
551         "fluid": ("FluidSimulationModifier", False),
552         "image_paint_object": ("Object", False),
553         "lamp": ("Lamp", False),
554         "lattice": ("Lattice", False),
555         "material": ("Material", False),
556         "material_slot": ("MaterialSlot", False),
557         "mesh": ("Mesh", False),
558         "meta_ball": ("MetaBall", False),
559         "object": ("Object", False),
560         "particle_edit_object": ("Object", False),
561         "particle_system": ("ParticleSystem", False),
562         "particle_system_editable": ("ParticleSystem", False),
563         "pose_bone": ("PoseBone", False),
564         "scene": ("Scene", False),
565         "sculpt_object": ("Object", False),
566         "selectable_bases": ("ObjectBase", True),
567         "selectable_objects": ("Object", True),
568         "selected_bases": ("ObjectBase", True),
569         "selected_bones": ("Bone", True),
570         "selected_editable_bases": ("ObjectBase", True),
571         "selected_editable_bones": ("Bone", True),
572         "selected_editable_objects": ("Object", True),
573         "selected_editable_sequences": ("Sequence", True),
574         "selected_nodes": ("Node", True),
575         "selected_objects": ("Object", True),
576         "selected_pose_bones": ("PoseBone", True),
577         "selected_sequences": ("Sequence", True),
578         "sequences": ("Sequence", True),
579         "smoke": ("SmokeModifier", False),
580         "soft_body": ("SoftBodyModifier", False),
581         "speaker": ("Speaker", False),
582         "texture": ("Texture", False),
583         "texture_slot": ("MaterialTextureSlot", False),
584         "vertex_paint_object": ("Object", False),
585         "visible_bases": ("ObjectBase", True),
586         "visible_bones": ("Object", True),
587         "visible_objects": ("Object", True),
588         "visible_pose_bones": ("PoseBone", True),
589         "weight_paint_object": ("Object", False),
590         "world": ("World", False),
591     }
592
593     unique = set()
594     blend_cdll = ctypes.CDLL("")
595     for ctx_str in context_strings:
596         subsection = "%s Context" % ctx_str.split("_")[0].title()
597         fw("\n%s\n%s\n\n" % (subsection, (len(subsection) * '-')))
598
599         attr = ctypes.addressof(getattr(blend_cdll, ctx_str))
600         c_char_p_p = ctypes.POINTER(ctypes.c_char_p)
601         char_array = c_char_p_p.from_address(attr)
602         i = 0
603         while char_array[i] is not None:
604             member = ctypes.string_at(char_array[i]).decode()
605             fw(".. data:: %s\n\n" % member)
606             member_type, is_seq = type_map[member]
607             fw("   :type: %s :class:`bpy.types.%s`\n\n" % ("sequence of " if is_seq else "", member_type))
608             unique.add(member)
609             i += 1
610
611     # generate typemap...
612     # for member in sorted(unique):
613     #     print('        "%s": ("", False),' % member)
614     if len(type_map) > len(unique):
615         raise Exception("Some types are not used: %s" % str([member for member in type_map if member not in unique]))
616     else:
617         pass  # will have raised an error above
618
619     file.close()
620
621
622 def pyrna2sphinx(BASEPATH):
623     """ bpy.types and bpy.ops
624     """
625     structs, funcs, ops, props = rna_info.BuildRNAInfo()
626     if FILTER_BPY_TYPES is not None:
627         structs = {k: v for k, v in structs.items() if k[1] in FILTER_BPY_TYPES}
628
629     if FILTER_BPY_OPS is not None:
630         ops = {k: v for k, v in ops.items() if v.module_name in FILTER_BPY_OPS}
631
632     def write_param(ident, fw, prop, is_return=False):
633         if is_return:
634             id_name = "return"
635             id_type = "rtype"
636             kwargs = {"as_ret": True}
637             identifier = ""
638         else:
639             id_name = "arg"
640             id_type = "type"
641             kwargs = {"as_arg": True}
642             identifier = " %s" % prop.identifier
643
644         kwargs["class_fmt"] = ":class:`%s`"
645
646         kwargs["collection_id"] = _BPY_PROP_COLLECTION_ID
647
648         type_descr = prop.get_type_description(**kwargs)
649         if prop.name or prop.description:
650             fw(ident + ":%s%s: %s\n" % (id_name, identifier, ", ".join(val for val in (prop.name, prop.description) if val)))
651         fw(ident + ":%s%s: %s\n" % (id_type, identifier, type_descr))
652
653     def write_struct(struct):
654         #if not struct.identifier.startswith("Sc") and not struct.identifier.startswith("I"):
655         #    return
656
657         #if not struct.identifier == "Object":
658         #    return
659
660         filepath = os.path.join(BASEPATH, "bpy.types.%s.rst" % struct.identifier)
661         file = open(filepath, "w")
662         fw = file.write
663
664         base_id = getattr(struct.base, "identifier", "")
665
666         if _BPY_STRUCT_FAKE:
667             if not base_id:
668                 base_id = _BPY_STRUCT_FAKE
669
670         if base_id:
671             title = "%s(%s)" % (struct.identifier, base_id)
672         else:
673             title = struct.identifier
674
675         write_title(fw, title, "=")
676
677         fw(".. module:: bpy.types\n\n")
678
679         # docs first?, ok
680         write_example_ref("", fw, "bpy.types.%s" % struct.identifier)
681
682         base_ids = [base.identifier for base in struct.get_bases()]
683
684         if _BPY_STRUCT_FAKE:
685             base_ids.append(_BPY_STRUCT_FAKE)
686
687         base_ids.reverse()
688
689         if base_ids:
690             if len(base_ids) > 1:
691                 fw("base classes --- ")
692             else:
693                 fw("base class --- ")
694
695             fw(", ".join((":class:`%s`" % base_id) for base_id in base_ids))
696             fw("\n\n")
697
698         subclass_ids = [s.identifier for s in structs.values() if s.base is struct if not rna_info.rna_id_ignore(s.identifier)]
699         if subclass_ids:
700             fw("subclasses --- \n" + ", ".join((":class:`%s`" % s) for s in subclass_ids) + "\n\n")
701
702         base_id = getattr(struct.base, "identifier", "")
703
704         if _BPY_STRUCT_FAKE:
705             if not base_id:
706                 base_id = _BPY_STRUCT_FAKE
707
708         if base_id:
709             fw(".. class:: %s(%s)\n\n" % (struct.identifier, base_id))
710         else:
711             fw(".. class:: %s\n\n" % struct.identifier)
712
713         fw("   %s\n\n" % struct.description)
714
715         # properties sorted in alphabetical order
716         sorted_struct_properties = struct.properties[:]
717         sorted_struct_properties.sort(key=lambda prop: prop.identifier)
718
719         for prop in sorted_struct_properties:
720             type_descr = prop.get_type_description(class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID)
721             # readonly properties use "data" directive, variables properties use "attribute" directive
722             if 'readonly' in type_descr:
723                 fw("   .. data:: %s\n\n" % prop.identifier)
724             else:
725                 fw("   .. attribute:: %s\n\n" % prop.identifier)
726             if prop.description:
727                 fw("      %s\n\n" % prop.description)
728             fw("      :type: %s\n\n" % type_descr)
729
730         # python attributes
731         py_properties = struct.get_py_properties()
732         py_prop = None
733         for identifier, py_prop in py_properties:
734             pyprop2sphinx("   ", fw, identifier, py_prop)
735         del py_properties, py_prop
736
737         for func in struct.functions:
738             args_str = ", ".join(prop.get_arg_default(force=False) for prop in func.args)
739
740             fw("   .. %s:: %s(%s)\n\n" % ("classmethod" if func.is_classmethod else "method", func.identifier, args_str))
741             fw("      %s\n\n" % func.description)
742
743             for prop in func.args:
744                 write_param("      ", fw, prop)
745
746             if len(func.return_values) == 1:
747                 write_param("      ", fw, func.return_values[0], is_return=True)
748             elif func.return_values:  # multiple return values
749                 fw("      :return (%s):\n" % ", ".join(prop.identifier for prop in func.return_values))
750                 for prop in func.return_values:
751                     type_descr = prop.get_type_description(as_ret=True, class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID)
752                     descr = prop.description
753                     if not descr:
754                         descr = prop.name
755                     fw("         `%s`, %s, %s\n\n" % (prop.identifier, descr, type_descr))
756
757             write_example_ref("      ", fw, "bpy.types." + struct.identifier + "." + func.identifier)
758
759             fw("\n")
760
761         # python methods
762         py_funcs = struct.get_py_functions()
763         py_func = None
764
765         for identifier, py_func in py_funcs:
766             pyfunc2sphinx("   ", fw, identifier, py_func, is_class=True)
767         del py_funcs, py_func
768
769         py_funcs = struct.get_py_c_functions()
770         py_func = None
771
772         for identifier, py_func in py_funcs:
773             py_c_func2sphinx("   ", fw, "bpy.types", struct.identifier, identifier, py_func, is_class=True)
774
775         lines = []
776
777         if struct.base or _BPY_STRUCT_FAKE:
778             bases = list(reversed(struct.get_bases()))
779
780             # props
781             lines[:] = []
782
783             if _BPY_STRUCT_FAKE:
784                 descr_items = [(key, descr) for key, descr in sorted(bpy.types.Struct.__bases__[0].__dict__.items()) if not key.startswith("__")]
785
786             if _BPY_STRUCT_FAKE:
787                 for key, descr in descr_items:
788                     if type(descr) == GetSetDescriptorType:
789                         lines.append("   * :class:`%s.%s`\n" % (_BPY_STRUCT_FAKE, key))
790
791             for base in bases:
792                 for prop in base.properties:
793                     lines.append("   * :class:`%s.%s`\n" % (base.identifier, prop.identifier))
794
795                 for identifier, py_prop in base.get_py_properties():
796                     lines.append("   * :class:`%s.%s`\n" % (base.identifier, identifier))
797
798                 for identifier, py_prop in base.get_py_properties():
799                     lines.append("   * :class:`%s.%s`\n" % (base.identifier, identifier))
800
801             if lines:
802                 fw(".. rubric:: Inherited Properties\n\n")
803
804                 fw(".. hlist::\n")
805                 fw("   :columns: 2\n\n")
806
807                 for line in lines:
808                     fw(line)
809                 fw("\n")
810
811             # funcs
812             lines[:] = []
813
814             if _BPY_STRUCT_FAKE:
815                 for key, descr in descr_items:
816                     if type(descr) == MethodDescriptorType:
817                         lines.append("   * :class:`%s.%s`\n" % (_BPY_STRUCT_FAKE, key))
818
819             for base in bases:
820                 for func in base.functions:
821                     lines.append("   * :class:`%s.%s`\n" % (base.identifier, func.identifier))
822                 for identifier, py_func in base.get_py_functions():
823                     lines.append("   * :class:`%s.%s`\n" % (base.identifier, identifier))
824
825             if lines:
826                 fw(".. rubric:: Inherited Functions\n\n")
827
828                 fw(".. hlist::\n")
829                 fw("   :columns: 2\n\n")
830
831                 for line in lines:
832                     fw(line)
833                 fw("\n")
834
835             lines[:] = []
836
837         if struct.references:
838             # use this otherwise it gets in the index for a normal heading.
839             fw(".. rubric:: References\n\n")
840
841             fw(".. hlist::\n")
842             fw("   :columns: 2\n\n")
843
844             for ref in struct.references:
845                 ref_split = ref.split(".")
846                 if len(ref_split) > 2:
847                     ref = ref_split[-2] + "." + ref_split[-1]
848                 fw("   * :class:`%s`\n" % ref)
849             fw("\n")
850
851         # docs last?, disable for now
852         # write_example_ref("", fw, "bpy.types.%s" % struct.identifier)
853         file.close()
854
855     if "bpy.types" not in EXCLUDE_MODULES:
856         for struct in structs.values():
857             # TODO, rna_info should filter these out!
858             if "_OT_" in struct.identifier:
859                 continue
860             write_struct(struct)
861
862         def fake_bpy_type(class_value, class_name, descr_str, use_subclasses=True):
863             filepath = os.path.join(BASEPATH, "bpy.types.%s.rst" % class_name)
864             file = open(filepath, "w")
865             fw = file.write
866
867             write_title(fw, class_name, "=")
868
869             fw(".. module:: bpy.types\n")
870             fw("\n")
871
872             if use_subclasses:
873                 subclass_ids = [s.identifier for s in structs.values() if s.base is None if not rna_info.rna_id_ignore(s.identifier)]
874                 if subclass_ids:
875                     fw("subclasses --- \n" + ", ".join((":class:`%s`" % s) for s in sorted(subclass_ids)) + "\n\n")
876
877             fw(".. class:: %s\n\n" % class_name)
878             fw("   %s\n\n" % descr_str)
879             fw("   .. note::\n\n")
880             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)
881
882             descr_items = [(key, descr) for key, descr in sorted(class_value.__dict__.items()) if not key.startswith("__")]
883
884             for key, descr in descr_items:
885                 if type(descr) == MethodDescriptorType:  # GetSetDescriptorType, GetSetDescriptorType's are not documented yet
886                     py_descr2sphinx("   ", fw, descr, "bpy.types", class_name, key)
887
888             for key, descr in descr_items:
889                 if type(descr) == GetSetDescriptorType:
890                     py_descr2sphinx("   ", fw, descr, "bpy.types", class_name, key)
891             file.close()
892
893         # write fake classes
894         if _BPY_STRUCT_FAKE:
895             class_value = bpy.types.Struct.__bases__[0]
896             fake_bpy_type(class_value, _BPY_STRUCT_FAKE, "built-in base class for all classes in bpy.types.", use_subclasses=True)
897
898         if _BPY_PROP_COLLECTION_FAKE:
899             class_value = bpy.data.objects.__class__
900             fake_bpy_type(class_value, _BPY_PROP_COLLECTION_FAKE, "built-in class used for all collections.", use_subclasses=False)
901
902     # operators
903     def write_ops():
904         API_BASEURL = "http://svn.blender.org/svnroot/bf-blender/trunk/blender/release/scripts"
905         API_BASEURL_ADDON = "http://svn.blender.org/svnroot/bf-extensions/trunk/py/scripts"
906         API_BASEURL_ADDON_CONTRIB = "http://svn.blender.org/svnroot/bf-extensions/contrib/py/scripts"
907
908         op_modules = {}
909         for op in ops.values():
910             op_modules.setdefault(op.module_name, []).append(op)
911         del op
912
913         for op_module_name, ops_mod in op_modules.items():
914             filepath = os.path.join(BASEPATH, "bpy.ops.%s.rst" % op_module_name)
915             file = open(filepath, "w")
916             fw = file.write
917
918             title = "%s Operators" % op_module_name.replace("_", " ").title()
919
920             write_title(fw, title, "=")
921
922             fw(".. module:: bpy.ops.%s\n\n" % op_module_name)
923
924             ops_mod.sort(key=lambda op: op.func_name)
925
926             for op in ops_mod:
927                 args_str = ", ".join(prop.get_arg_default(force=True) for prop in op.args)
928                 fw(".. function:: %s(%s)\n\n" % (op.func_name, args_str))
929
930                 # if the description isn't valid, we output the standard warning
931                 # with a link to the wiki so that people can help
932                 if not op.description or op.description == "(undocumented operator)":
933                     operator_description = undocumented_message('bpy.ops', op.module_name, op.func_name)
934                 else:
935                     operator_description = op.description
936
937                 fw("   %s\n\n" % operator_description)
938                 for prop in op.args:
939                     write_param("   ", fw, prop)
940                 if op.args:
941                     fw("\n")
942
943                 location = op.get_location()
944                 if location != (None, None):
945                     if location[0].startswith("addons_contrib" + os.sep):
946                         url_base = API_BASEURL_ADDON_CONTRIB
947                     elif location[0].startswith("addons" + os.sep):
948                         url_base = API_BASEURL_ADDON
949                     else:
950                         url_base = API_BASEURL
951
952                     fw("   :file: `%s <%s/%s>`_:%d\n\n" % (location[0], url_base, location[0], location[1]))
953
954             file.close()
955
956     if "bpy.ops" not in EXCLUDE_MODULES:
957         write_ops()
958
959
960 def rna2sphinx(BASEPATH):
961
962     try:
963         os.mkdir(BASEPATH)
964     except:
965         pass
966
967     # conf.py - empty for now
968     filepath = os.path.join(BASEPATH, "conf.py")
969     file = open(filepath, "w")
970     fw = file.write
971
972     version_string = ".".join(str(v) for v in bpy.app.version)
973     if bpy.app.build_revision != "Unknown":
974         version_string = version_string + " r" + bpy.app.build_revision
975
976     version_string_fp = "_".join(str(v) for v in bpy.app.version)
977
978     if bpy.app.version_cycle == "release":
979         version_string_pdf = "%s%s_release" % ("_".join(str(v) for v in bpy.app.version[:2]), bpy.app.version_char)
980     else:
981         version_string_pdf = version_string_fp
982
983     fw("project = 'Blender'\n")
984     # fw("master_doc = 'index'\n")
985     fw("copyright = u'Blender Foundation'\n")
986     fw("version = '%s - API'\n" % version_string)
987     fw("release = '%s - API'\n" % version_string)
988     fw("html_theme = 'blender-org'\n")
989     fw("html_theme_path = ['../']\n")
990     fw("html_favicon = 'favicon.ico'\n")
991     # not helpful since the source us generated, adds to upload size.
992     fw("html_copy_source = False\n")
993     fw("\n")
994     # needed for latex, pdf gen
995     fw("latex_documents = [ ('contents', 'contents.tex', 'Blender Index', 'Blender Foundation', 'manual'), ]\n")
996     fw("latex_paper_size = 'a4paper'\n")
997     file.close()
998
999     # main page needed for sphinx (index.html)
1000     filepath = os.path.join(BASEPATH, "contents.rst")
1001     file = open(filepath, "w")
1002     fw = file.write
1003
1004     fw("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n")
1005     fw(" Blender Documentation contents\n")
1006     fw("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n")
1007     fw("\n")
1008     fw("Welcome, this document is an API reference for Blender %s. built %s.\n" % (version_string, bpy.app.build_date))
1009     fw("\n")
1010     fw("`A PDF version of this document is also available <blender_python_reference_%s.pdf>`_\n" % version_string_pdf)
1011
1012     fw("\n")
1013
1014     if not EXCLUDE_INFO_DOCS:
1015         fw("============================\n")
1016         fw("Blender/Python Documentation\n")
1017         fw("============================\n")
1018         fw("\n")
1019         fw("\n")
1020         fw(".. toctree::\n")
1021         fw("   :maxdepth: 1\n\n")
1022         for info, info_desc in INFO_DOCS:
1023             fw("   %s <%s>\n\n" % (info_desc, info))
1024         fw("\n")
1025
1026     fw("===================\n")
1027     fw("Application Modules\n")
1028     fw("===================\n")
1029     fw("\n")
1030     fw(".. toctree::\n")
1031     fw("   :maxdepth: 1\n\n")
1032     if "bpy.context" not in EXCLUDE_MODULES:
1033         fw("   bpy.context.rst\n\n")  # note: not actually a module
1034     if "bpy.data" not in EXCLUDE_MODULES:
1035         fw("   bpy.data.rst\n\n")  # note: not actually a module
1036     if "bpy.ops" not in EXCLUDE_MODULES:
1037         fw("   bpy.ops.rst\n\n")
1038     if "bpy.types" not in EXCLUDE_MODULES:
1039         fw("   bpy.types.rst\n\n")
1040
1041     # py modules
1042     if "bpy.utils" not in EXCLUDE_MODULES:
1043         fw("   bpy.utils.rst\n\n")
1044     if "bpy.path" not in EXCLUDE_MODULES:
1045         fw("   bpy.path.rst\n\n")
1046     if "bpy.app" not in EXCLUDE_MODULES:
1047         fw("   bpy.app.rst\n\n")
1048
1049     # C modules
1050     if "bpy.props" not in EXCLUDE_MODULES:
1051         fw("   bpy.props.rst\n\n")
1052
1053     fw("==================\n")
1054     fw("Standalone Modules\n")
1055     fw("==================\n")
1056     fw("\n")
1057     fw(".. toctree::\n")
1058     fw("   :maxdepth: 1\n\n")
1059
1060     if "mathutils" not in EXCLUDE_MODULES:
1061         fw("   mathutils.rst\n\n")
1062     if "mathutils.geometry" not in EXCLUDE_MODULES:
1063         fw("   mathutils.geometry.rst\n\n")
1064     if "bgl" not in EXCLUDE_MODULES:
1065         fw("   bgl.rst\n\n")
1066     if "blf" not in EXCLUDE_MODULES:
1067         fw("   blf.rst\n\n")
1068     if "aud" not in EXCLUDE_MODULES:
1069         fw("   aud.rst\n\n")
1070     if "bpy_extras" not in EXCLUDE_MODULES:
1071         fw("   bpy_extras.rst\n\n")
1072
1073     # game engine
1074     if "bge" not in EXCLUDE_MODULES:
1075         fw("===================\n")
1076         fw("Game Engine Modules\n")
1077         fw("===================\n")
1078         fw("\n")
1079         fw(".. toctree::\n")
1080         fw("   :maxdepth: 1\n\n")
1081         fw("   bge.types.rst\n\n")
1082         fw("   bge.logic.rst\n\n")
1083         fw("   bge.render.rst\n\n")
1084         fw("   bge.texture.rst\n\n")
1085         fw("   bge.events.rst\n\n")
1086         fw("   bge.constraints.rst\n\n")
1087
1088     # rna generated change log
1089     fw("========\n")
1090     fw("API Info\n")
1091     fw("========\n")
1092     fw("\n")
1093     fw(".. toctree::\n")
1094     fw("   :maxdepth: 1\n\n")
1095     fw("   change_log.rst\n\n")
1096
1097     fw("\n")
1098     fw("\n")
1099     fw(".. note:: The Blender Python API has areas which are still in development.\n")
1100     fw("   \n")
1101     fw("   The following areas are subject to change.\n")
1102     fw("      * operator behavior, names and arguments\n")
1103     fw("      * mesh creation and editing functions\n")
1104     fw("   \n")
1105     fw("   These parts of the API are relatively stable and are unlikely to change significantly\n")
1106     fw("      * data API, access to attributes of blender data such as mesh verts, material color, timeline frames and scene objects\n")
1107     fw("      * user interface functions for defining buttons, creation of menus, headers, panels\n")
1108     fw("      * render engine integration\n")
1109     fw("      * modules: bgl, mathutils & game engine.\n")
1110     fw("\n")
1111
1112     file.close()
1113
1114     # internal modules
1115     if "bpy.ops" not in EXCLUDE_MODULES:
1116         filepath = os.path.join(BASEPATH, "bpy.ops.rst")
1117         file = open(filepath, "w")
1118         fw = file.write
1119         fw("Operators (bpy.ops)\n")
1120         fw("===================\n\n")
1121         write_example_ref("", fw, "bpy.ops")
1122         fw(".. toctree::\n")
1123         fw("   :glob:\n\n")
1124         fw("   bpy.ops.*\n\n")
1125         file.close()
1126
1127     if "bpy.types" not in EXCLUDE_MODULES:
1128         filepath = os.path.join(BASEPATH, "bpy.types.rst")
1129         file = open(filepath, "w")
1130         fw = file.write
1131         fw("Types (bpy.types)\n")
1132         fw("=================\n\n")
1133         fw(".. toctree::\n")
1134         fw("   :glob:\n\n")
1135         fw("   bpy.types.*\n\n")
1136         file.close()
1137
1138     if "bpy.data" not in EXCLUDE_MODULES:
1139         # not actually a module, only write this file so we
1140         # can reference in the TOC
1141         filepath = os.path.join(BASEPATH, "bpy.data.rst")
1142         file = open(filepath, "w")
1143         fw = file.write
1144         fw("Data Access (bpy.data)\n")
1145         fw("======================\n\n")
1146         fw(".. module:: bpy\n")
1147         fw("\n")
1148         fw("This module is used for all blender/python access.\n")
1149         fw("\n")
1150         fw(".. data:: data\n")
1151         fw("\n")
1152         fw("   Access to blenders internal data\n")
1153         fw("\n")
1154         fw("   :type: :class:`bpy.types.BlendData`\n")
1155         fw("\n")
1156         fw(".. literalinclude:: ../examples/bpy.data.py\n")
1157         file.close()
1158
1159     EXAMPLE_SET_USED.add("bpy.data")
1160
1161     module = None
1162
1163     if "bpy.context" not in EXCLUDE_MODULES:
1164         # one of a kind, context doc (uses ctypes to extract info!)
1165         pycontext2sphinx(BASEPATH)
1166
1167     # python modules
1168     if "bpy.utils" not in EXCLUDE_MODULES:
1169         from bpy import utils as module
1170         pymodule2sphinx(BASEPATH, "bpy.utils", module, "Utilities")
1171
1172     if "bpy.path" not in EXCLUDE_MODULES:
1173         from bpy import path as module
1174         pymodule2sphinx(BASEPATH, "bpy.path", module, "Path Utilities")
1175
1176     if "bpy_extras" not in EXCLUDE_MODULES:
1177         import bpy_extras as module
1178         pymodule2sphinx(BASEPATH, "bpy_extras", module, "Extra Utilities")
1179
1180     # C modules
1181     if "bpy.app" not in EXCLUDE_MODULES:
1182         from bpy import app as module
1183         pymodule2sphinx(BASEPATH, "bpy.app", module, "Application Data")
1184
1185     if "bpy.props" not in EXCLUDE_MODULES:
1186         from bpy import props as module
1187         pymodule2sphinx(BASEPATH, "bpy.props", module, "Property Definitions")
1188
1189     if "mathutils" not in EXCLUDE_MODULES:
1190         import mathutils as module
1191         pymodule2sphinx(BASEPATH, "mathutils", module, "Math Types & Utilities")
1192
1193     if "mathutils.geometry" not in EXCLUDE_MODULES:
1194         import mathutils.geometry as module
1195         pymodule2sphinx(BASEPATH, "mathutils.geometry", module, "Geometry Utilities")
1196
1197     if "blf" not in EXCLUDE_MODULES:
1198         import blf as module
1199         pymodule2sphinx(BASEPATH, "blf", module, "Font Drawing")
1200
1201     if "bgl" not in EXCLUDE_MODULES:
1202         #import bgl as module
1203         #pymodule2sphinx(BASEPATH, "bgl", module, "Blender OpenGl wrapper")
1204         #del module
1205         import shutil
1206         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bgl.rst"), BASEPATH)
1207
1208     if "aud" not in EXCLUDE_MODULES:
1209         import aud as module
1210         pymodule2sphinx(BASEPATH, "aud", module, "Audio System")
1211     del module
1212
1213     ## game engine
1214     import shutil
1215     # copy2 keeps time/date stamps
1216     if "bge" not in EXCLUDE_MODULES:
1217         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.types.rst"), BASEPATH)
1218         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.logic.rst"), BASEPATH)
1219         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.render.rst"), BASEPATH)
1220         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.texture.rst"), BASEPATH)
1221         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.events.rst"), BASEPATH)
1222         shutil.copy2(os.path.join(BASEPATH, "..", "rst", "bge.constraints.rst"), BASEPATH)
1223
1224     shutil.copy2(os.path.join(BASEPATH, "..", "rst", "change_log.rst"), BASEPATH)
1225
1226     if not EXCLUDE_INFO_DOCS:
1227         for info, info_desc in INFO_DOCS:
1228             shutil.copy2(os.path.join(BASEPATH, "..", "rst", info), BASEPATH)
1229
1230     if 0:
1231         filepath = os.path.join(BASEPATH, "bpy.rst")
1232         file = open(filepath, "w")
1233         fw = file.write
1234
1235         fw("\n")
1236
1237         title = ":mod:`bpy` --- Blender Python Module"
1238
1239         write_title(fw, title, "=")
1240
1241         fw(".. module:: bpy.types\n\n")
1242         file.close()
1243
1244     # bpy.types and bpy.ops
1245     pyrna2sphinx(BASEPATH)
1246
1247     file.close()
1248
1249
1250 def main():
1251     import shutil
1252
1253     script_dir = os.path.dirname(__file__)
1254     path_in = os.path.join(script_dir, "sphinx-in")
1255     path_out = os.path.join(script_dir, "sphinx-out")
1256     path_examples = os.path.join(script_dir, "examples")
1257     # only for partial updates
1258     path_in_tmp = path_in + "-tmp"
1259
1260     if not os.path.exists(path_in):
1261         os.mkdir(path_in)
1262
1263     for f in os.listdir(path_examples):
1264         if f.endswith(".py"):
1265             EXAMPLE_SET.add(os.path.splitext(f)[0])
1266
1267     # only for full updates
1268     if _BPY_FULL_REBUILD:
1269         shutil.rmtree(path_in, True)
1270         shutil.rmtree(path_out, True)
1271     else:
1272         # write here, then move
1273         shutil.rmtree(path_in_tmp, True)
1274
1275     rna2sphinx(path_in_tmp)
1276
1277     if not _BPY_FULL_REBUILD:
1278         import filecmp
1279
1280         # now move changed files from 'path_in_tmp' --> 'path_in'
1281         file_list_path_in = set(os.listdir(path_in))
1282         file_list_path_in_tmp = set(os.listdir(path_in_tmp))
1283
1284         # remove deprecated files that have been removed.
1285         for f in sorted(file_list_path_in):
1286             if f not in file_list_path_in_tmp:
1287                 print("\tdeprecated: %s" % f)
1288                 os.remove(os.path.join(path_in, f))
1289
1290         # freshen with new files.
1291         for f in sorted(file_list_path_in_tmp):
1292             f_from = os.path.join(path_in_tmp, f)
1293             f_to = os.path.join(path_in, f)
1294
1295             do_copy = True
1296             if f in file_list_path_in:
1297                 if filecmp.cmp(f_from, f_to):
1298                     do_copy = False
1299
1300             if do_copy:
1301                 print("\tupdating: %s" % f)
1302                 shutil.copy(f_from, f_to)
1303             '''else:
1304                 print("\tkeeping: %s" % f) # eh, not that useful'''
1305
1306     EXAMPLE_SET_UNUSED = EXAMPLE_SET - EXAMPLE_SET_USED
1307     if EXAMPLE_SET_UNUSED:
1308         print("\nUnused examples found in '%s'..." % path_examples)
1309         for f in EXAMPLE_SET_UNUSED:
1310             print("    %s.py" % f)
1311         print("  %d total\n" % len(EXAMPLE_SET_UNUSED))
1312
1313     import sys
1314     sys.exit()
1315
1316 if __name__ == '__main__':
1317     main()