From 51e0be89c8f94ddc1bb023a4fb7ce4161e42df98 Mon Sep 17 00:00:00 2001
From: Cyrille Bagard <nocbos@gmail.com>
Date: Tue, 27 Oct 2015 16:49:10 +0000
Subject: Added a tool to update Python documentation online.

git-svn-id: svn://svn.gna.org/svn/chrysalide/trunk@601 abbe820e-26c8-41b2-8c08-b7b2b41f8b0a
---
 ChangeLog                            |  10 ++
 tools/gendocs/exporter.py            |  95 ++++++++++++++++
 tools/gendocs/exporters/html.py      | 201 ++++++++++++++++++++++++++++++++++
 tools/gendocs/exporters/mediawiki.py | 156 ++++++++++++++++++++++++++
 tools/gendocs/gendoc.py              |  36 ++++++
 tools/gendocs/source.py              |  54 +++++++++
 tools/gendocs/sources/python.py      | 207 +++++++++++++++++++++++++++++++++++
 7 files changed, 759 insertions(+)
 create mode 100644 tools/gendocs/exporter.py
 create mode 100644 tools/gendocs/exporters/html.py
 create mode 100644 tools/gendocs/exporters/mediawiki.py
 create mode 100755 tools/gendocs/gendoc.py
 create mode 100644 tools/gendocs/source.py
 create mode 100644 tools/gendocs/sources/python.py

diff --git a/ChangeLog b/ChangeLog
index 962bae3..6b748c9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+15-10-27  Cyrille Bagard <nocbos@gmail.com>
+
+	* tools/gendocs/exporter.py:
+	* tools/gendocs/exporters/html.py:
+	* tools/gendocs/exporters/mediawiki.py:
+	* tools/gendocs/gendoc.py:
+	* tools/gendocs/source.py:
+	* tools/gendocs/sources/python.py:
+	Add a tool to update Python documentation online.
+
 15-10-18  Cyrille Bagard <nocbos@gmail.com>
 
 	* po/fr.po:
diff --git a/tools/gendocs/exporter.py b/tools/gendocs/exporter.py
new file mode 100644
index 0000000..c34c468
--- /dev/null
+++ b/tools/gendocs/exporter.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+
+class DocExporter:
+    """Exporte une documentation d'une façon donnée."""
+
+
+    def open(self, name, fullname):
+        """Initialise les propriétés utiles."""
+
+        pass
+
+
+    def close(self):
+        """Termine l'édition de la documentation."""
+
+        pass
+
+
+    def show_list_sub_modules(self, gparent, parent, others, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        pass
+
+
+    def show_list_sub_classes(self, parent, classes, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        pass
+
+
+    def describe_module(self, desc):
+        """Affiche la description du module courant."""
+
+        pass
+
+
+    def start_main_section(self, title):
+        """Affiche un en-tête pour la zone des classes."""
+
+        pass
+
+
+    def show_class_info(self, name, desc):
+        """Affiche les informations générales d'une classe."""
+
+        pass
+
+
+    def show_info_section(self, title):
+        """Affiche une section d'informations particulières."""
+
+        pass
+
+
+    def begin_hierarchy_level(self):
+        """Démarre une arborescence hiérarchique."""
+
+        pass
+
+
+    def terminate_hierarchy_level(self):
+        """Arrête une arborescence hiérarchique."""
+
+        pass
+
+
+    def print_hierarchy_level(self, fullname, name, page, level):
+        """Affiche un élément hiérarchique."""
+
+        pass
+
+
+    def show_constant_info(self, name):
+        """Affiche un élément de type 'constant'."""
+
+        pass
+
+
+    def show_data_info(self, name, value):
+        """Affiche un élément de type 'donnée'."""
+
+        pass
+
+    def show_attribute_info(self, name, desc):
+        """Affiche un élément de type 'attribut'."""
+
+        pass
+
+
+    def show_callable_info(self, ret, name, args, desc):
+        """Affiche un élément de type 'routine'."""
+
+        pass
diff --git a/tools/gendocs/exporters/html.py b/tools/gendocs/exporters/html.py
new file mode 100644
index 0000000..72eda46
--- /dev/null
+++ b/tools/gendocs/exporters/html.py
@@ -0,0 +1,201 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+from exporter import DocExporter
+
+
+class HtmlExporter(DocExporter):
+    """Exporte une documentation sous forme HTML."""
+
+
+    def open(self, name, fullname):
+        """Initialise les propriétés utiles."""
+
+        self._filename = fullname + '.html'
+        self._fd = open(self._filename, 'w')
+
+        self._fd.write("""\
+<HTML>
+<HEAD>
+    <META http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
+    <TITLE>Documentation</TITLE>
+    <STYLE>
+    <!--
+    H1 {
+        background-color: rgb(218, 218, 218);
+        border: 1px solid rgb(184, 184, 184);
+        border-radius: 3px 3px 3px 3px;
+        padding: 8px;
+    }
+    H2 {
+        background-color: rgb(238, 238, 238);
+        border: 1px solid rgb(204, 204, 204);
+        border-radius: 3px 3px 3px 3px;
+        padding: 4px;
+    }
+    .info {
+        background-color: #efc;
+        border: 1px solid #ac9;
+        border-radius: 3px;
+        margin-left: 40px;
+        padding: 8px;
+    }
+    .steps {
+        font-family: monospace;
+    }
+    .constant_name {
+        font-weight: bold;
+    }
+    .data_name { 
+        font-weight: bold;
+    }
+    .data_value {
+    }
+    .property_name { 
+        font-weight: bold;
+    }
+    .property_desc {
+        padding-left: 40px;
+    }
+    .callable_name {
+        font-weight: bold;
+    }
+    .callable_desc {
+        padding-left: 40px;
+    }
+    -->
+    </STYLE>
+<BODY>
+        """)
+
+
+    def close(self):
+        """Termine l'édition de la documentation."""
+
+        self._fd.write("""
+</BODY>
+</HTML>
+        """)
+
+        self._fd.close()
+
+
+    def _normalize_desc(self, desc):
+        """S'assure d'un bon rendu HTML d'un commentaire prévu pour."""
+
+        return desc.replace(" ", "&nbsp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<BR>")
+
+
+
+    def show_list_sub_modules(self, gparent, parent, others, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        if len(others) > 0 or gparent != None:
+
+            self._fd.write('<H2>Sub modules</H2>')
+
+            self._fd.write('<UL>')
+
+            if gparent != None:
+                self._fd.write('<LI><A href="%s.html">..</A></LI>' % gparent)
+
+            for o in others:
+                fullname = builder(parent, o._name)
+                self._fd.write('<LI><A href="%s.html">%s</A></LI>' % (fullname, fullname))
+
+            self._fd.write('</UL>')
+
+
+    def show_list_sub_classes(self, parent, classes, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        if len(classes) > 0:
+
+            self._fd.write('<H2>Classes</H2>')
+
+            self._fd.write('<UL>')
+
+            for cls in classes:
+                self._fd.write('<LI><A href="%s.html#%s">%s</A></LI>'
+                               % (parent, cls, builder(parent, cls)))
+
+            self._fd.write('</UL>')
+
+
+    def describe_module(self, desc):
+        """Affiche la description du module courant."""
+
+        self._fd.write('<H2>Description</H2>')
+
+        self._fd.write('<P class="info">%s</P>' % self._normalize_desc(desc))
+
+
+    def start_main_section(self, title):
+        """Affiche un en-tête pour la zone des classes."""
+
+        self._fd.write('<H1>%s</H1>' % title)
+
+
+    def show_class_info(self, name, desc):
+        """Affiche les informations générales d'une classe."""
+
+        self._fd.write('<A name="%s"><H2>%s</H2></A>' % (name, name))
+
+        self._fd.write('<P class="info">%s</P>' % self._normalize_desc(desc))
+
+
+    def show_info_section(self, title):
+        """Affiche une section d'informations particulières."""
+
+        self._fd.write('<H3>%s</H3>' % title)
+
+
+    def print_hierarchy_level(self, fullname, name, page, level):
+        """Affiche un élément hiérarchique."""
+
+        if level > 0:
+
+            self._fd.write('<SPAN class="steps">')
+
+            for i in range(level - 1):
+                self._fd.write('&nbsp;' * 5)
+
+            self._fd.write('&nbsp;╰──&nbsp;')
+
+            self._fd.write('</SPAN>')
+
+        if page != None:
+            self._fd.write('<A href="%s.html#%s">' % (page, name))
+
+        self._fd.write(fullname)
+
+        if page != None:
+            self._fd.write('</A>')
+
+        self._fd.write('<BR>')
+
+
+    def show_constant_info(self, name):
+        """Affiche un élément de type 'constant'."""
+
+        self._fd.write('<P><SPAN class="constant_name">%s</SPAN></P>' % name)
+
+
+    def show_data_info(self, name, value):
+        """Affiche un élément de type 'donnée'."""
+
+        self._fd.write('<P><SPAN class="data_name">%s</SPAN> = <SPAN class="data_value">%s</SPAN></P>' % (name, self._normalize_desc(value)))
+
+    def show_attribute_info(self, name, desc):
+        """Affiche un élément de type 'attribut'."""
+
+        self._fd.write('<P><SPAN class="property_name">%s</SPAN><BR><DIV class="property_desc">%s</DIV></P>' % (name, self._normalize_desc(desc)))
+
+
+    def show_callable_info(self, ret, name, args, desc):
+        """Affiche un élément de type 'routine'."""
+
+        if args == None:
+            args = '()'
+
+        self._fd.write('<P><SPAN class="callable_name">%s</SPAN>(%s)<BR><DIV class="callable_desc">%s</DIV></P>' % (name, args, self._normalize_desc(desc)))
diff --git a/tools/gendocs/exporters/mediawiki.py b/tools/gendocs/exporters/mediawiki.py
new file mode 100644
index 0000000..d994dae
--- /dev/null
+++ b/tools/gendocs/exporters/mediawiki.py
@@ -0,0 +1,156 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+import pywikibot
+from exporter import DocExporter
+
+
+class MWExporter(DocExporter):
+    """Exporte une documentation vers un site Mediawiki."""
+
+
+    def open(self, name, fullname):
+        """Initialise les propriétés utiles."""
+
+        self._site = pywikibot.Site()
+        self._page = pywikibot.Page(self._site, self._build_page_name(fullname))
+
+        self._content = '__NOTOC__'
+        self._content += '\n'
+
+
+    def close(self):
+        """Termine l'édition de la documentation."""
+
+        if self._content != self._page.text:
+            self._page.text = self._content
+            self._page.save('API update')
+
+
+    def _build_page_name(self, orig):
+        """Définit le nom final d'une page faisant référence à un module."""
+
+        return orig + ' Python Module'
+
+
+    def show_list_sub_modules(self, gparent, parent, others, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        if len(others) > 0 or gparent != None:
+
+            self._content += '<div class="h2">\n==Sub modules==\n</div>\n'
+
+            if gparent != None:
+                self._content += '* [[%s|..]]\n' % self._build_page_name(gparent)
+
+            for o in others:
+                fullname = builder(parent, o._name)
+                self._content += '* [[%s|%s]]\n' % (self._build_page_name(fullname), fullname)
+
+
+    def show_list_sub_classes(self, parent, classes, builder):
+        """Affiche des renvois vers les sous-parties présentes."""
+
+        if len(classes) > 0:
+
+            self._content += '<div class="h2">\n==Classes==\n</div>\n'
+
+            for cls in classes:
+                self._content += '* [[%s#%s|%s]]\n' % (self._build_page_name(parent), cls, builder(parent, cls))
+
+
+    def describe_module(self, desc):
+        """Affiche la description du module courant."""
+
+        self._content += '<div class="h2">\n==Description==\n</div>\n'
+
+        self._content += '<div class="fakepre info">%s</div>\n' % desc
+
+
+    def start_main_section(self, title):
+        """Affiche un en-tête pour la zone des classes."""
+
+        self._content += '<div class="h1">\n=%s=\n</div>\n' % title
+
+
+    def show_class_info(self, name, desc):
+        """Affiche les informations générales d'une classe."""
+
+        self._content += '<div id="%s" class="h2">\n==%s==\n</div>\n' % (name, name)
+
+        self._content += '<pre class="fakepre info">%s</pre>\n' % desc
+
+
+    def show_info_section(self, title):
+        """Affiche une section d'informations particulières."""
+
+        self._content += '<div class="h3">\n===%s===\n</div>\n' % title
+
+
+    def begin_hierarchy_level(self):
+        """Démarre une arborescence hiérarchique."""
+
+        self._content += '<div class="fakepre treeclasses">\n'
+
+
+    def terminate_hierarchy_level(self):
+        """Arrête une arborescence hiérarchique."""
+
+        self._content += '</div>\n\n'
+
+
+    def print_hierarchy_level(self, fullname, name, page, level):
+        """Affiche un élément hiérarchique."""
+
+        if level > 0:
+
+            for i in range(level - 1):
+                self._content += ' ' * 5
+
+            self._content += ' ╰── '
+
+        if page != None:
+            self._content += '[[%s#%s|' % (self._build_page_name(page), name)
+
+        self._content += fullname
+
+        if page != None:
+            self._content += ']]'
+
+        self._content += '\n'
+
+
+    def show_constant_info(self, name):
+        """Affiche un élément de type 'constant'."""
+
+        self._content += '\n'
+        self._content += '<span class="constant_name">%s</span>\n' % name
+
+
+    def show_data_info(self, name, value):
+        """Affiche un élément de type 'donnée'."""
+
+        self._content += '\n'
+        self._content += '<span class="data_name">%s</span> = ' % name
+        self._content += '<span class="data_value">%s</span>\n' % value
+
+
+    def show_attribute_info(self, name, desc):
+        """Affiche un élément de type 'attribut'."""
+
+        self._content += '\n'
+        self._content += '<span class="property_name">%s</span>\n' % name
+        self._content += '\n'
+        self._content += '<span class="property_desc">%s</span>\n' % desc
+
+
+    def show_callable_info(self, ret, name, args, desc):
+        """Affiche un élément de type 'routine'."""
+
+        if args == None:
+            args = '()'
+
+        self._content += '\n'
+        self._content += '<span class="callable_name">%s</span>(%s)\n' % (name, args)
+        self._content += '\n'
+        self._content += '<pre class="fakepre callable_desc">%s</pre>\n' % desc
diff --git a/tools/gendocs/gendoc.py b/tools/gendocs/gendoc.py
new file mode 100755
index 0000000..7c29b2f
--- /dev/null
+++ b/tools/gendocs/gendoc.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+
+def validate_coverage(target):
+
+    result = False
+
+    for mod in sys.argv[1:]:
+        result = target.startswith(mod)
+        if result:
+            break
+
+    return result
+
+
+if __name__ == '__main__':
+
+    if os.environ.get('PYWIKIBOT2_DIR') is None:
+        print('Environment variable "KEY_THAT_MIGHT_EXIST" is not set!')
+        sys.exit(1)
+
+    if len(sys.argv) == 1:
+        print('Usage: %s <module>' % sys.argv[0])
+        sys.exit(1)
+
+    from exporters.html import HtmlExporter
+    from exporters.mediawiki import MWExporter
+    from sources.python import PythonReader
+
+    for mod in sys.argv[1:]:
+        reader = PythonReader(None, mod, MWExporter)
+        reader.build(validate_coverage)
diff --git a/tools/gendocs/source.py b/tools/gendocs/source.py
new file mode 100644
index 0000000..3de4874
--- /dev/null
+++ b/tools/gendocs/source.py
@@ -0,0 +1,54 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+
+class SourceReader(object):
+    """Lecteur attentif de code source."""
+
+
+    def __init__(self, parent, name, output):
+        """Initialise l'identité d'une partie de documentation."""
+
+        self._parent = parent
+        self._name = name
+        self._output_cls = output
+        self._output = output()
+
+
+    def build(self, validation):
+        """Construit de façon générique une documentation complète."""
+
+        self._output.open(self._name, self._fullname)
+
+        others, mod_desc = self.prepare_module()
+        classes = self.list_all_classes()
+
+        # Contenu du module
+
+        self._output.start_main_section('Content')
+
+        self._output.show_list_sub_modules(self._parent, self._fullname, others, self.make_path)
+
+        self._output.show_list_sub_classes(self._fullname, classes, self.make_path)
+
+        self._output.describe_module(mod_desc)
+
+        # Description des classes
+
+        if len(classes) > 0:
+
+            self._output.start_main_section('Classes')
+
+            for cls in classes:
+                self.describe_class(cls, validation)
+
+        # Eléments propres au module
+
+        self.describe_module_items()
+
+        # Fermeture et suite
+
+        self._output.close()
+
+        for o in others:
+            o.build(validation)
diff --git a/tools/gendocs/sources/python.py b/tools/gendocs/sources/python.py
new file mode 100644
index 0000000..aa7d32e
--- /dev/null
+++ b/tools/gendocs/sources/python.py
@@ -0,0 +1,207 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+from collections import deque
+from source import SourceReader
+import importlib
+import inspect
+
+
+is_callable = lambda t: inspect.isroutine(t[2]) \
+              or isinstance(t[2], staticmethod) \
+              or isinstance(t[2], classmethod)
+
+is_attribute = lambda t: not(is_callable(t)) and (inspect.isgetsetdescriptor(t[2]) or isinstance(t[2], property))
+
+is_constant = lambda t: not(is_attribute(t) or is_callable(t)) and t[0].isupper()
+
+is_data = lambda t: not(is_constant(t) or is_attribute(t) or is_callable(t))
+
+
+class PythonReader(SourceReader):
+    """Lecteur attentif de code Python."""
+
+
+    def __init__(self, parent, name, output):
+        """Initialise l'identité d'une partie de documentation."""
+
+        super(PythonReader, self).__init__(parent, name, output)
+
+        self._fullname = self.make_path(parent, name)
+
+
+    def make_path(self, parent, target):
+        """Construit le chemin complet d'un élément d'un module donné."""
+
+        return parent + '.' + target if parent != None else target
+
+
+    def prepare_module(self):
+        """Charge le module courant et liste tous ses sous-modules."""
+
+        result = [ ]
+
+        self._module = importlib.import_module(self._fullname)
+
+        submodules = inspect.getmembers(self._module, inspect.ismodule)
+
+        for name, inst in submodules:
+            other = PythonReader(self._fullname, name, self._output_cls)
+            result.append(other)
+
+        return result, self._module.__doc__
+
+
+    def list_all_classes(self):
+        """Liste toutes les classes présentes dans le module courant."""
+
+        result = [ ]
+
+        classes = inspect.getmembers(self._module, inspect.isclass)
+
+        for name, inst in classes:
+            result.append(name)
+
+        return result
+
+
+    def is_visible(self, name):
+        """Sélectionne les éléments à faire apparaître dans une documenation."""
+
+        # On évite les noms spéciaux internes et redondants
+        if name in {'__author__', '__builtins__', '__cached__', '__credits__',
+                    '__date__', '__doc__', '__file__', '__spec__',
+                    '__loader__', '__module__', '__name__', '__package__',
+                    '__path__', '__qualname__', '__slots__', '__version__'}:
+            return False
+
+        # Les noms spéciaux restants ne sont pas invisibles
+        if name.startswith('__') and name.endswith('__'):
+            return True
+
+        return not name.startswith('_')
+
+
+    def describe_module_items(self):
+        """Décrit tous les éléments du module qui n'ont pas déjà été présentés."""
+
+        processed = lambda v: inspect.ismodule(v) or inspect.isclass(v)
+
+        attrs = [(key, self._module, self._module.__dict__[key])
+                 for key in dir(self._module)
+                 if self.is_visible(key) and not(processed(self._module.__dict__[key]))
+        ]
+
+        self.describe_attribs_list(attrs)
+
+
+    def describe_class(self, name, validation):
+
+        cls = getattr(self._module, name)
+
+        self._output.show_class_info(cls.__name__, cls.__doc__)
+
+        self._output.show_info_section('Class Hierarchy')
+
+        self._output.begin_hierarchy_level()
+
+        mro = deque(inspect.getmro(cls))
+        mro.reverse()
+        level = 0
+
+        for base in mro:
+
+            if (level + 1) == len(mro) or not validation(base.__module__):
+                page = None
+            else:
+                page = base.__module__
+
+            fullname = self.make_path(base.__module__, base.__name__)
+
+            self._output.print_hierarchy_level(fullname, base.__name__, page, level)
+
+            level = level + 1
+
+        self._output.terminate_hierarchy_level()
+
+        attrs = [(name, owner, value)
+                 for (name, kind, owner, value) in inspect.classify_class_attrs(cls)
+                 if owner == cls and self.is_visible(name)
+        ]
+
+        self.describe_attribs_list(attrs)
+
+
+    def describe_attribs_list(self, attrs):
+        """Describe some module/class attributes in a given order."""
+
+        def filter_attrs(lst, predicate, title):
+
+            remaining = []
+
+            first = True
+
+            for a in lst:
+                if predicate(a):
+                    if first:
+                        self._output.show_info_section(title)
+                        first = False
+                    self.describe_item(a)
+                else:
+                    remaining.append(a)
+
+            return remaining
+
+        attrs = filter_attrs(attrs, is_constant, 'Constants')
+        attrs = filter_attrs(attrs, is_data, 'Data')
+        attrs = filter_attrs(attrs, is_attribute, 'Attributes')
+        attrs = filter_attrs(attrs, is_callable, 'Methods')
+
+        assert(len(attrs) == 0)
+
+
+    def describe_item(self, item):
+        """Describe one module/class item."""
+
+        name, homecls, value = item
+
+        if is_callable(item):
+
+            value = getattr(homecls, name)
+
+            if hasattr(value, '__text_signature__') and value.__text_signature__ != None:
+
+                args = ''
+
+                for p in inspect.signature(value).parameters.values():
+                    if len(args) > 0:
+                        args = args + ', '
+                    args = args + p.name
+
+            else:
+
+                args = '...'
+
+            # method_descriptor -> value.__doc__
+
+            doc = getattr(homecls, name).__doc__
+
+            self._output.show_callable_info('', name, args, doc)
+
+
+        elif is_attribute(item):
+
+            self._output.show_attribute_info(name, value.__doc__)
+
+        elif is_constant(item):
+
+            self._output.show_constant_info(name)
+
+        elif is_data(item):
+
+            self._output.show_data_info(name, value)
+
+
+        else:
+
+            assert(False)
-- 
cgit v0.11.2-87-g4458