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