|
@@ -0,0 +1,680 @@
|
|
|
+#!@PYTHON@
|
|
|
+
|
|
|
+# Copyright (C) 2011 Internet Systems Consortium.
|
|
|
+#
|
|
|
+# Permission to use, copy, modify, and distribute this software for any
|
|
|
+# purpose with or without fee is hereby granted, provided that the above
|
|
|
+# copyright notice and this permission notice appear in all copies.
|
|
|
+#
|
|
|
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
|
|
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
|
|
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
|
|
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
|
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
|
|
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
|
|
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
|
|
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
+
|
|
|
+r"""
|
|
|
+A helper to semi-auto generate Python docstring text from C++ Doxygen
|
|
|
+documentation.
|
|
|
+
|
|
|
+This script converts an XML-format doxygen documentation for C++ library
|
|
|
+into a template Python docstring for the corresponding Python version
|
|
|
+of the library. While it's not perfect and you'll still need to edit the
|
|
|
+output by hand, but past experiments showed the script produces a pretty
|
|
|
+good template. It will help provide more compatible documentation for
|
|
|
+both C++ and Python versions of library from a unified source (C++ Doxygen
|
|
|
+documentation) with minimizing error-prone and boring manual conversion.
|
|
|
+
|
|
|
+HOW TO USE IT
|
|
|
+
|
|
|
+1. Generate XML output by doxygen. Use bind10/doc/Doxyfile-xml:
|
|
|
+
|
|
|
+ % cd bind10/doc
|
|
|
+ % doxygen Doxyfile-xml
|
|
|
+ (XML files will be generated under bind10/doc/html/xml)
|
|
|
+
|
|
|
+2. Identify the xml file of the conversion target (C++ class, function, etc)
|
|
|
+
|
|
|
+ This is a bit tricky. You'll probably need to do manual search.
|
|
|
+ For example, to identify the xml file for a C++ class
|
|
|
+ isc::datasrc::memory::ZoneWriter, you might do:
|
|
|
+
|
|
|
+ % cd bind10/doc/html/xml
|
|
|
+ % grep ZoneWriter *.xml | grep 'kind="class"'
|
|
|
+ index.xml: <compound refid="d4/d3c/classisc_1_1datasrc_1_1memory_1_1ZoneWriter" kind="class"><name>isc::datasrc::memory::ZoneWriter</name>
|
|
|
+
|
|
|
+ In this case the file under the d4/d3c directory (with .xml suffix) would
|
|
|
+ be the file you're looking for.
|
|
|
+
|
|
|
+3. Run this script for the xml file:
|
|
|
+
|
|
|
+ % python3 doxygen2pydoc.py <top_srcdir>/doc/html/xml/d4/d3c/classisc_1_1datasrc_1_1memory_1_1ZoneWriter.xml > output.cc
|
|
|
+
|
|
|
+ The template content is dumped to standard out (redirected to file
|
|
|
+ "output.cc" in this example).
|
|
|
+
|
|
|
+ Sometimes the script produces additional output to standard error,
|
|
|
+ like this:
|
|
|
+
|
|
|
+ Replaced camelCased terms:
|
|
|
+ resetMemorySegment => reset_memory_segment
|
|
|
+ getConfiguration => get_configuration
|
|
|
+
|
|
|
+ In BIND 10 naming convention for methods is different for C++ and
|
|
|
+ Python. This script uses some heuristic guess to convert the
|
|
|
+ C++-style method names to likely Python-style ones, and the converted
|
|
|
+ method names are used in the dumped template. In many cases the guessed
|
|
|
+ names are correct, but you should check this list and make adjustments
|
|
|
+ by hand if necessary.
|
|
|
+
|
|
|
+ If there's no standard error output, this type of conversion didn't
|
|
|
+ happen.
|
|
|
+
|
|
|
+4. Edit and copy the template
|
|
|
+
|
|
|
+ The dumped template has the following organization:
|
|
|
+
|
|
|
+ namespace {
|
|
|
+ #ifdef COPY_THIS_TO_MAIN_CC
|
|
|
+ { "cleanup", ZoneWriter_cleanup, METH_NOARGS,
|
|
|
+ ZoneWriter_cleanup_doc },
|
|
|
+ { "install", ZoneWriter_install, METH_NOARGS,
|
|
|
+ ZoneWriter_install_doc },
|
|
|
+ { "load", ZoneWriter_load, METH_VARARGS,
|
|
|
+ ZoneWriter_load_doc },
|
|
|
+ #endif // COPY_THIS_TO_MAIN_CC
|
|
|
+
|
|
|
+ const char* const ZoneWriter_doc = "\
|
|
|
+ ...
|
|
|
+ ";
|
|
|
+
|
|
|
+ const char* const ZoneWriter_install_doc = "\
|
|
|
+ ...
|
|
|
+ ";
|
|
|
+
|
|
|
+ ...
|
|
|
+ }
|
|
|
+
|
|
|
+ The ifdef-ed block is a template for class methods information
|
|
|
+ to be added to the corresponding PyMethodDef structure array
|
|
|
+ (your wrapper C++ source would have something like ZoneWriter_methods
|
|
|
+ of this type). These lines should be copied there. As long as
|
|
|
+ the method names and corresponding wrapper function (such as
|
|
|
+ ZoneWriter_cleanup) are correct you shouldn't have to edit this part
|
|
|
+ (and they would be normally correct, unless the guessed method name
|
|
|
+ conversion was needed).
|
|
|
+
|
|
|
+ The rest of the content is a sequence of constant C-string variables.
|
|
|
+ Usually the first variable corresponds to the class description, and
|
|
|
+ the rest are method descriptions (note that ZoneWriter_install_doc
|
|
|
+ is referenced from the ifdef-ed block). The content of this part
|
|
|
+ would generally make sense, but you'll often need to make some
|
|
|
+ adjsutments by hand. A common examples of such adjustment is to
|
|
|
+ replace "NULL" with "None". Also, it's not uncommon that some part
|
|
|
+ of the description simply doesn't apply to the Python version or
|
|
|
+ that Python specific notes are needed. So go through the description
|
|
|
+ carefully and make necessary changes. A common practice is to add
|
|
|
+ comments for a summary of adjustments like this:
|
|
|
+
|
|
|
+ // Modifications:
|
|
|
+ // NULL->None
|
|
|
+ // - removed notes about derived classes (which doesn't apply for python)
|
|
|
+ const char* const ZoneWriter_doc = "\
|
|
|
+ ...
|
|
|
+ ";
|
|
|
+ This note will help next time you need to auto-generate and edit the
|
|
|
+ template (probably because the original C++ document is updated).
|
|
|
+
|
|
|
+ You can simply copy this part to the main C++ wrapper file, but since
|
|
|
+ it's relatively large a common practice is to maintain it in a separate
|
|
|
+ file that is exclusively included from the main file: if the name of
|
|
|
+ the main file is zonewriter_python.cc, the pydoc strings would be copied
|
|
|
+ in zonewriter_python_inc.cc, and the main file would have this line:
|
|
|
+
|
|
|
+ #include "zonewriter_inc.cc"
|
|
|
+
|
|
|
+ (In case you are C++ language police: it's okay to use the unnamed
|
|
|
+ name space for a file to be included because it's essentially a part
|
|
|
+ of the single .cc file, not expected to be included by others).
|
|
|
+
|
|
|
+ In either case, the ifdef-ed part should be removed.
|
|
|
+
|
|
|
+ADVANCED FEATURES
|
|
|
+
|
|
|
+You can use a special "xmlonly" doxygen command in C++ doxygent document
|
|
|
+in order to include Python code excerpt (while hiding it from the doxygen
|
|
|
+output for the C++ version). This command will be converted to
|
|
|
+a special XML tag in the XML output.
|
|
|
+
|
|
|
+The block enclosed by \xmlonly and \endxmlonly should contain
|
|
|
+a verbatim XML tag named "pythonlisting", in which the python code should
|
|
|
+be placed.
|
|
|
+/// \code
|
|
|
+/// Name name("example.com");
|
|
|
+/// std::cout << name.toText() << std::endl;
|
|
|
+/// \endcode
|
|
|
+///
|
|
|
+/// \xmlonly <pythonlisting>
|
|
|
+/// name = Name("example.com")
|
|
|
+/// print(name.to_text())
|
|
|
+/// </pythonlisting> \endxmlonly
|
|
|
+
|
|
|
+Note that there must be a blank line between \endcode and \xmlonly.
|
|
|
+doxygen2pydoc assume the pythonlisting tag is in a separate <para> node.
|
|
|
+This blank ensures doxygen will produce the XML file that meets the
|
|
|
+assumption.
|
|
|
+
|
|
|
+INTERNAL MEMO (incomplete, and not very unredable yet)
|
|
|
+
|
|
|
+This simplified utility assumes the following structure:
|
|
|
+...
|
|
|
+ <compounddef ...>
|
|
|
+ <compoundname>isc::dns::TSIGError</compoundname>
|
|
|
+ <sectiondef kind="user-defined">
|
|
|
+ constructor, destructor
|
|
|
+ </sectiondef>
|
|
|
+ <sectiondef kind="public-type">
|
|
|
+ ..
|
|
|
+ </sectiondef>
|
|
|
+ <sectiondef kind="public-func">
|
|
|
+ <memberdef kind="function"...>
|
|
|
+ <type>return type (if any)</type>
|
|
|
+ <argsstring>(...) [const]</argsstring>
|
|
|
+ <name>method name</name>
|
|
|
+ <briefdescription>method's brief description</briefdescription>
|
|
|
+ <detaileddescription>
|
|
|
+ <para>...</para>...
|
|
|
+ <para>
|
|
|
+ <parameterlist kind="exception">
|
|
|
+ <parameteritem>
|
|
|
+ <parameternamelist>
|
|
|
+ <parametername>Exception name</parametername>
|
|
|
+ </parameternamelist>
|
|
|
+ <parameterdescription>
|
|
|
+ <para>exception desc</para>
|
|
|
+ </parameterdescription>
|
|
|
+ </parameteritem>
|
|
|
+ </parameterlist>
|
|
|
+ <parameterlist kind="param">
|
|
|
+ <parameteritem>
|
|
|
+ <parameternamelist>
|
|
|
+ <parametername>param name</parametername>
|
|
|
+ </parameternamelist>
|
|
|
+ <parameterdescription>
|
|
|
+ <para>param desc</para>
|
|
|
+ </parameterdescription>
|
|
|
+ </parameteritem>
|
|
|
+ ...
|
|
|
+ </parameterlist>
|
|
|
+ <simplesect kind="return">Return value</simplesect>
|
|
|
+ </para>
|
|
|
+ </detaileddescription>
|
|
|
+ </memberdef>
|
|
|
+ </sectiondef>
|
|
|
+ <sectiondef kind="public-static-attrib|user-defined">
|
|
|
+ <memberdef kind="variable"...>
|
|
|
+ <name>class-specific-constant</name>
|
|
|
+ <initializer>value</initializer>
|
|
|
+ <brief|detaileddescription>paragraph(s)</brief|detaileddescription>
|
|
|
+ </sectiondef>
|
|
|
+ <briefdescription>
|
|
|
+ class's brief description
|
|
|
+ </briefdescription>
|
|
|
+ <detaileddescription>
|
|
|
+ class's detailed description
|
|
|
+ </detaileddescription>
|
|
|
+ </compounddef>
|
|
|
+"""
|
|
|
+
|
|
|
+import re, string, sys, textwrap
|
|
|
+from xml.dom.minidom import parse
|
|
|
+from textwrap import fill, dedent, TextWrapper
|
|
|
+
|
|
|
+camel_replacements = {}
|
|
|
+member_functions = []
|
|
|
+constructors = []
|
|
|
+class_variables = []
|
|
|
+
|
|
|
+RE_CAMELTERM = re.compile('([\s\.]|^)[a-z]+[A-Z]\S*')
|
|
|
+RE_SIMPLECAMEL = re.compile("([a-z])([A-Z])")
|
|
|
+RE_CAMELAFTERUPPER = re.compile("([A-Z])([A-Z])([a-z])")
|
|
|
+
|
|
|
+class Paragraph:
|
|
|
+ TEXT = 0
|
|
|
+ ITEMIZEDLIST = 1
|
|
|
+ CPPLISTING = 2
|
|
|
+ PYLISTING = 3
|
|
|
+ VERBATIM = 4
|
|
|
+
|
|
|
+ def __init__(self, xml_node):
|
|
|
+ if len(xml_node.getElementsByTagName("pythonlisting")) > 0:
|
|
|
+ self.type = self.PYLISTING
|
|
|
+ self.text = re.sub("///", "", get_text(xml_node))
|
|
|
+ elif len(xml_node.getElementsByTagName("verbatim")) > 0:
|
|
|
+ self.type = self.VERBATIM
|
|
|
+ self.text = get_text(xml_node)
|
|
|
+ elif len(xml_node.getElementsByTagName("programlisting")) > 0:
|
|
|
+ # We ignore node containing a "programlisting" tag.
|
|
|
+ # They are C++ example code, and we are not interested in them
|
|
|
+ # in pydoc.
|
|
|
+ self.type = self.CPPLISTING
|
|
|
+ elif len(xml_node.getElementsByTagName("itemizedlist")) > 0:
|
|
|
+ self.type = self.ITEMIZEDLIST
|
|
|
+ self.items = []
|
|
|
+ for item in xml_node.getElementsByTagName("listitem"):
|
|
|
+ self.items.append(get_text(item))
|
|
|
+ else:
|
|
|
+ self.type = self.TEXT
|
|
|
+
|
|
|
+ # A single textual paragraph could have multiple simple sections
|
|
|
+ # if it contains notes.
|
|
|
+
|
|
|
+ self.texts = []
|
|
|
+ subnodes = []
|
|
|
+ for child in xml_node.childNodes:
|
|
|
+ if child.nodeType == child.ELEMENT_NODE and \
|
|
|
+ child.nodeName == 'simplesect' and \
|
|
|
+ child.getAttribute('kind') == 'note':
|
|
|
+ if len(subnodes) > 0:
|
|
|
+ self.texts.append(get_text_fromnodelist(subnodes))
|
|
|
+ subnodes = []
|
|
|
+ subtext = 'Note: '
|
|
|
+ for t in child.childNodes:
|
|
|
+ subtext += get_text(t)
|
|
|
+ self.texts.append(subtext)
|
|
|
+ else:
|
|
|
+ subnodes.append(child)
|
|
|
+ if len(subnodes) > 0:
|
|
|
+ self.texts.append(get_text_fromnodelist(subnodes))
|
|
|
+
|
|
|
+ def dump(self, f, wrapper):
|
|
|
+ if self.type == self.CPPLISTING:
|
|
|
+ return
|
|
|
+ elif self.type == self.ITEMIZEDLIST:
|
|
|
+ for item in self.items:
|
|
|
+ item_wrapper = TextWrapper(\
|
|
|
+ initial_indent=wrapper.initial_indent + "- ",
|
|
|
+ subsequent_indent=wrapper.subsequent_indent + " ")
|
|
|
+ dump_filled_text(f, item_wrapper, item)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ elif self.type == self.TEXT:
|
|
|
+ for text in self.texts:
|
|
|
+ if text != self.texts[0]:
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ dump_filled_text(f, wrapper, text)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ else:
|
|
|
+ dump_filled_text(f, None, self.text)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ f.write("\\n\\\n")
|
|
|
+
|
|
|
+class NamedItem:
|
|
|
+ def __init__(self, name, desc):
|
|
|
+ self.name = name
|
|
|
+ self.desc = desc
|
|
|
+
|
|
|
+ def dump(self, f, wrapper):
|
|
|
+ # we use deeper indent inside the item list.
|
|
|
+ new_initial_indent = wrapper.initial_indent + " " * 2
|
|
|
+ new_subsequent_indent = wrapper.subsequent_indent + " " * (2 + 11)
|
|
|
+ local_wrapper = TextWrapper(initial_indent=new_initial_indent,
|
|
|
+ subsequent_indent=new_subsequent_indent)
|
|
|
+
|
|
|
+ # concatenate name and description with a fixed width (up to 10 chars)
|
|
|
+ # for the name, and wrap the entire text, then dump it to file.
|
|
|
+ dump_filled_text(f, local_wrapper, "%-10s %s" % (self.name, self.desc))
|
|
|
+ f.write("\\n\\\n")
|
|
|
+
|
|
|
+class FunctionDefinition:
|
|
|
+ # function types
|
|
|
+ CONSTRUCTOR = 0
|
|
|
+ COPY_CONSTRUCTOR = 1
|
|
|
+ DESTRUCTOR = 2
|
|
|
+ ASSIGNMENT_OP = 3
|
|
|
+ OTHER = 4
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.type = self.OTHER
|
|
|
+ self.name = None
|
|
|
+ self.pyname = None
|
|
|
+ self.args = ""
|
|
|
+ self.ret_type = None
|
|
|
+ self.brief_desc = None
|
|
|
+ self.detailed_desc = []
|
|
|
+ self.exceptions = []
|
|
|
+ self.parameters = []
|
|
|
+ self.returns = None
|
|
|
+ self.have_param = False
|
|
|
+
|
|
|
+ def dump_doc(self, f, wrapper=TextWrapper()):
|
|
|
+ f.write(self.pyname + "(" + self.args + ")")
|
|
|
+ if self.ret_type is not None:
|
|
|
+ f.write(" -> " + self.ret_type)
|
|
|
+ f.write("\\n\\\n\\n\\\n")
|
|
|
+
|
|
|
+ if self.brief_desc is not None:
|
|
|
+ dump_filled_text(f, wrapper, self.brief_desc)
|
|
|
+ f.write("\\n\\\n\\n\\\n")
|
|
|
+
|
|
|
+ for para in self.detailed_desc:
|
|
|
+ para.dump(f, wrapper)
|
|
|
+
|
|
|
+ if len(self.exceptions) > 0:
|
|
|
+ f.write(wrapper.fill("Exceptions:") + "\\n\\\n")
|
|
|
+ for ex_desc in self.exceptions:
|
|
|
+ ex_desc.dump(f, wrapper)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ if len(self.parameters) > 0:
|
|
|
+ f.write(wrapper.fill("Parameters:") + "\\n\\\n")
|
|
|
+ for param_desc in self.parameters:
|
|
|
+ param_desc.dump(f, wrapper)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ if self.returns is not None:
|
|
|
+ dump_filled_text(f, wrapper, "Return Value(s): " + self.returns)
|
|
|
+ f.write("\\n\\\n")
|
|
|
+
|
|
|
+ def dump_pymethod_def(self, f, class_name):
|
|
|
+ f.write(' { "' + self.pyname + '", ')
|
|
|
+ f.write(class_name + '_' + self.name + ', ')
|
|
|
+ if len(self.parameters) == 0:
|
|
|
+ f.write('METH_NOARGS,\n')
|
|
|
+ else:
|
|
|
+ f.write('METH_VARARGS,\n')
|
|
|
+ f.write(' ' + class_name + '_' + self.name + '_doc },\n')
|
|
|
+
|
|
|
+class VariableDefinition:
|
|
|
+ def __init__(self, nodelist):
|
|
|
+ self.value = None
|
|
|
+ self.brief_desc = None
|
|
|
+ self.detailed_desc = []
|
|
|
+
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeName == "name":
|
|
|
+ self.name = get_text(node)
|
|
|
+ elif node.nodeName == "initializer":
|
|
|
+ self.value = get_text(node)
|
|
|
+ elif node.nodeName == "briefdescription":
|
|
|
+ self.brief_desc = get_text(node)
|
|
|
+ elif node.nodeName == "detaileddescription":
|
|
|
+ for para in node.childNodes:
|
|
|
+ if para.nodeName != "para":
|
|
|
+ # ignore surrounding empty nodes
|
|
|
+ continue
|
|
|
+ self.detailed_desc.append(Paragraph(para))
|
|
|
+
|
|
|
+ def dump_doc(self, f, wrapper=TextWrapper()):
|
|
|
+ name_value = self.name
|
|
|
+ if self.value is not None:
|
|
|
+ name_value += ' = ' + self.value
|
|
|
+ dump_filled_text(f, wrapper, name_value)
|
|
|
+ f.write('\\n\\\n')
|
|
|
+
|
|
|
+ desc_initial_indent = wrapper.initial_indent + " "
|
|
|
+ desc_subsequent_indent = wrapper.subsequent_indent + " "
|
|
|
+ desc_wrapper = TextWrapper(initial_indent=desc_initial_indent,
|
|
|
+ subsequent_indent=desc_subsequent_indent)
|
|
|
+ if self.brief_desc is not None:
|
|
|
+ dump_filled_text(f, desc_wrapper, self.brief_desc)
|
|
|
+ f.write("\\n\\\n\\n\\\n")
|
|
|
+
|
|
|
+ for para in self.detailed_desc:
|
|
|
+ para.dump(f, desc_wrapper)
|
|
|
+
|
|
|
+def dump_filled_text(f, wrapper, text):
|
|
|
+ """Fill given text using wrapper, and dump it to the given file
|
|
|
+ appending an escaped CR at each end of line.
|
|
|
+ """
|
|
|
+ filled_text = wrapper.fill(text) if wrapper is not None else text
|
|
|
+ f.write("".join(re.sub("\n", r"\\n\\\n", filled_text)))
|
|
|
+
|
|
|
+def camel_to_lowerscores(matchobj):
|
|
|
+ oldtext = matchobj.group(0)
|
|
|
+ newtext = re.sub(RE_SIMPLECAMEL, r"\1_\2", oldtext)
|
|
|
+ newtext = re.sub(RE_CAMELAFTERUPPER, r"\1_\2\3", newtext)
|
|
|
+ newtext = newtext.lower()
|
|
|
+ camel_replacements[oldtext] = newtext
|
|
|
+ return newtext.lower()
|
|
|
+
|
|
|
+def cpp_to_python(text):
|
|
|
+ text = text.replace("::", ".")
|
|
|
+ text = text.replace('"', '\\"')
|
|
|
+
|
|
|
+ # convert camelCase to "_"-concatenated format
|
|
|
+ # (e.g. getLength -> get_length)
|
|
|
+ return re.sub(RE_CAMELTERM, camel_to_lowerscores, text)
|
|
|
+
|
|
|
+def convert_type_name(type_name):
|
|
|
+ """Convert C++ type name to python type name using common conventions"""
|
|
|
+ # strip off leading 'const' and trailing '&/*'
|
|
|
+ type_name = re.sub("^const\S*", "", type_name)
|
|
|
+ type_name = re.sub("\S*[&\*]$", "", type_name)
|
|
|
+
|
|
|
+ # We often typedef smart pointers as [Const]TypePtr. Convert them to
|
|
|
+ # just "Type"
|
|
|
+ type_name = re.sub("^Const", "", type_name)
|
|
|
+ type_name = re.sub("Ptr$", "", type_name)
|
|
|
+
|
|
|
+ if type_name == "std::string":
|
|
|
+ return "string"
|
|
|
+ if re.search(r"(int\d+_t|size_t)", type_name):
|
|
|
+ return "integer"
|
|
|
+ return type_name
|
|
|
+
|
|
|
+def get_text(root, do_convert=True):
|
|
|
+ """Recursively extract bare text inside the specified node (root),
|
|
|
+ concatenate all extracted text and return the result.
|
|
|
+ """
|
|
|
+ nodelist = root.childNodes
|
|
|
+ rc = []
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeType == node.TEXT_NODE:
|
|
|
+ if do_convert:
|
|
|
+ rc.append(cpp_to_python(node.data))
|
|
|
+ else:
|
|
|
+ rc.append(node.data)
|
|
|
+ elif node.nodeType == node.ELEMENT_NODE:
|
|
|
+ rc.append(get_text(node))
|
|
|
+ # return the result, removing any leading newlines (that often happens for
|
|
|
+ # brief descriptions, which will cause lines not well aligned)
|
|
|
+ return re.sub("^(\n*)", "", ''.join(rc))
|
|
|
+
|
|
|
+def get_text_fromnodelist(nodelist, do_convert=True):
|
|
|
+ """Recursively extract bare text inside the specified node (root),
|
|
|
+ concatenate all extracted text and return the result.
|
|
|
+ """
|
|
|
+ rc = []
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeType == node.TEXT_NODE:
|
|
|
+ if do_convert:
|
|
|
+ rc.append(cpp_to_python(node.data))
|
|
|
+ else:
|
|
|
+ rc.append(node.data)
|
|
|
+ elif node.nodeType == node.ELEMENT_NODE:
|
|
|
+ rc.append(get_text(node))
|
|
|
+ # return the result, removing any leading newlines (that often happens for
|
|
|
+ # brief descriptions, which will cause lines not well aligned)
|
|
|
+ return re.sub("^(\n*)", "", ''.join(rc))
|
|
|
+
|
|
|
+def parse_parameters(nodelist):
|
|
|
+ rc = []
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeName != "parameteritem":
|
|
|
+ continue
|
|
|
+ # for simplicity, we assume one parametername and one
|
|
|
+ # parameterdescription for each parameter.
|
|
|
+ name = get_text(node.getElementsByTagName("parametername")[0])
|
|
|
+ desc = get_text(node.getElementsByTagName("parameterdescription")[0])
|
|
|
+ rc.append(NamedItem(name, desc))
|
|
|
+ return rc
|
|
|
+
|
|
|
+def parse_function_description(func_def, nodelist):
|
|
|
+ for node in nodelist:
|
|
|
+ # nodelist contains beginning and ending empty text nodes.
|
|
|
+ # ignore them (otherwise they cause disruption below).
|
|
|
+ if node.nodeName != "para":
|
|
|
+ continue
|
|
|
+
|
|
|
+ if node.getElementsByTagName("parameterlist"):
|
|
|
+ # within this node there may be exception list, parameter list,
|
|
|
+ # and description for return value. parse and store them
|
|
|
+ # seprately.
|
|
|
+ for paramlist in node.getElementsByTagName("parameterlist"):
|
|
|
+ if paramlist.getAttribute("kind") == "exception":
|
|
|
+ func_def.exceptions = \
|
|
|
+ parse_parameters(paramlist.childNodes)
|
|
|
+ elif paramlist.getAttribute("kind") == "param":
|
|
|
+ func_def.parameters = \
|
|
|
+ parse_parameters(paramlist.childNodes)
|
|
|
+ if node.getElementsByTagName("simplesect"):
|
|
|
+ simplesect = node.getElementsByTagName("simplesect")[0]
|
|
|
+ if simplesect.getAttribute("kind") == "return":
|
|
|
+ func_def.returns = get_text(simplesect)
|
|
|
+ else:
|
|
|
+ # for normal text, python listing and itemized list, append them
|
|
|
+ # to the list of paragraphs
|
|
|
+ func_def.detailed_desc.append(Paragraph(node))
|
|
|
+
|
|
|
+def parse_function(func_def, class_name, nodelist):
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeName == "name":
|
|
|
+ func_def.name = get_text(node, False)
|
|
|
+ func_def.pyname = cpp_to_python(func_def.name)
|
|
|
+ elif node.nodeName == "argsstring":
|
|
|
+ # extract parameter names only, assuming they immediately follow
|
|
|
+ # their type name + space, and are immeidatelly followed by
|
|
|
+ # either "," or ")". If it's a pointer or reference, */& is
|
|
|
+ # prepended to the parameter name without a space:
|
|
|
+ # e.g. (int var1, char *var2, Foo &var3)
|
|
|
+ args = get_text(node, False)
|
|
|
+ # extract parameter names, possibly with */&
|
|
|
+ func_def.args = ', '.join(re.findall(r"\s(\S+)[,)]", args))
|
|
|
+ # then remove any */& symbols
|
|
|
+ func_def.args = re.sub("[\*&]", "", func_def.args)
|
|
|
+ elif node.nodeName == "type" and node.hasChildNodes():
|
|
|
+ func_def.ret_type = convert_type_name(get_text(node, False))
|
|
|
+ elif node.nodeName == "param":
|
|
|
+ func_def.have_param = True
|
|
|
+ elif node.nodeName == "briefdescription":
|
|
|
+ func_def.brief_desc = get_text(node)
|
|
|
+ elif node.nodeName == "detaileddescription":
|
|
|
+ parse_function_description(func_def, node.childNodes)
|
|
|
+ # identify the type of function using the name and arg
|
|
|
+ if func_def.name == class_name and \
|
|
|
+ re.search("^\(const " + class_name + " &[^,]*$", args):
|
|
|
+ # This function is ClassName(const ClassName& param), which is
|
|
|
+ # the copy constructor.
|
|
|
+ func_def.type = func_def.COPY_CONSTRUCTOR
|
|
|
+ elif func_def.name == class_name:
|
|
|
+ # if it's not the copy ctor but the function name == class name,
|
|
|
+ # it's a constructor.
|
|
|
+ func_def.type = func_def.CONSTRUCTOR
|
|
|
+ elif func_def.name == "~" + class_name:
|
|
|
+ func_def.type = func_def.DESTRUCTOR
|
|
|
+ elif func_def.name == "operator=":
|
|
|
+ func_def.type = func_def.ASSIGNMENT_OP
|
|
|
+
|
|
|
+ # register the definition to the approriate list
|
|
|
+ if func_def.type == func_def.CONSTRUCTOR:
|
|
|
+ constructors.append(func_def)
|
|
|
+ elif func_def.type == func_def.OTHER:
|
|
|
+ member_functions.append(func_def)
|
|
|
+
|
|
|
+def parse_functions(class_name, nodelist):
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeName == "memberdef" and \
|
|
|
+ node.getAttribute("kind") == "function":
|
|
|
+ func_def = FunctionDefinition()
|
|
|
+ parse_function(func_def, class_name, node.childNodes)
|
|
|
+
|
|
|
+def parse_class_variables(class_name, nodelist):
|
|
|
+ for node in nodelist:
|
|
|
+ if node.nodeName == "memberdef" and \
|
|
|
+ node.getAttribute("kind") == "variable":
|
|
|
+ class_variables.append(VariableDefinition(node.childNodes))
|
|
|
+
|
|
|
+def dump(f, class_name, class_brief_doc, class_detailed_doc):
|
|
|
+ f.write("namespace {\n")
|
|
|
+
|
|
|
+ f.write('#ifdef COPY_THIS_TO_MAIN_CC\n')
|
|
|
+ for func in member_functions:
|
|
|
+ func.dump_pymethod_def(f, class_name)
|
|
|
+ f.write('#endif // COPY_THIS_TO_MAIN_CC\n\n')
|
|
|
+
|
|
|
+ f.write("const char* const " + class_name + '_doc = "\\\n')
|
|
|
+ if class_brief_doc is not None:
|
|
|
+ f.write("".join(re.sub("\n", r"\\n\\\n", fill(class_brief_doc))))
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ f.write("\\n\\\n")
|
|
|
+ if len(class_detailed_doc) > 0:
|
|
|
+ for para in class_detailed_doc:
|
|
|
+ para.dump(f, wrapper=TextWrapper())
|
|
|
+
|
|
|
+ # dump constructors
|
|
|
+ for func in constructors:
|
|
|
+ indent = " " * 4
|
|
|
+ func.dump_doc(f, wrapper=TextWrapper(initial_indent=indent,
|
|
|
+ subsequent_indent=indent))
|
|
|
+
|
|
|
+ # dump class variables
|
|
|
+ if len(class_variables) > 0:
|
|
|
+ f.write("Class constant data:\\n\\\n")
|
|
|
+ for var in class_variables:
|
|
|
+ var.dump_doc(f)
|
|
|
+
|
|
|
+ f.write("\";\n")
|
|
|
+
|
|
|
+ for func in member_functions:
|
|
|
+ f.write("\n")
|
|
|
+ f.write("const char* const " + class_name + "_" + func.name + \
|
|
|
+ "_doc = \"\\\n");
|
|
|
+ func.dump_doc(f)
|
|
|
+ f.write("\";\n")
|
|
|
+
|
|
|
+ f.write("} // unnamed namespace") # close namespace
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ dom = parse(sys.argv[1])
|
|
|
+ class_elements = dom.getElementsByTagName("compounddef")[0].childNodes
|
|
|
+ class_brief_doc = None
|
|
|
+ class_detailed_doc = []
|
|
|
+ for node in class_elements:
|
|
|
+ if node.nodeName == "compoundname":
|
|
|
+ # class name is the last portion of the period-separated fully
|
|
|
+ # qualified class name. (this should exist)
|
|
|
+ class_name = re.split("\.", get_text(node))[-1]
|
|
|
+ if node.nodeName == "briefdescription":
|
|
|
+ # we assume a brief description consists at most one para
|
|
|
+ class_brief_doc = get_text(node)
|
|
|
+ elif node.nodeName == "detaileddescription":
|
|
|
+ # a detaild description consists of one or more paragraphs
|
|
|
+ for para in node.childNodes:
|
|
|
+ if para.nodeName != "para": # ignore surrounding empty nodes
|
|
|
+ continue
|
|
|
+ class_detailed_doc.append(Paragraph(para))
|
|
|
+ elif node.nodeName == "sectiondef" and \
|
|
|
+ node.getAttribute("kind") == "public-func":
|
|
|
+ parse_functions(class_name, node.childNodes)
|
|
|
+ elif node.nodeName == "sectiondef" and \
|
|
|
+ node.getAttribute("kind") == "public-static-attrib":
|
|
|
+ parse_class_variables(class_name, node.childNodes)
|
|
|
+ elif node.nodeName == "sectiondef" and \
|
|
|
+ node.getAttribute("kind") == "user-defined":
|
|
|
+ # there are two possiblities: functions and variables
|
|
|
+ for child in node.childNodes:
|
|
|
+ if child.nodeName != "memberdef":
|
|
|
+ continue
|
|
|
+ if child.getAttribute("kind") == "function":
|
|
|
+ parse_function(FunctionDefinition(), class_name,
|
|
|
+ child.childNodes)
|
|
|
+ elif child.getAttribute("kind") == "variable":
|
|
|
+ class_variables.append(VariableDefinition(child.childNodes))
|
|
|
+
|
|
|
+ dump(sys.stdout, class_name, class_brief_doc, class_detailed_doc)
|
|
|
+
|
|
|
+ if len(camel_replacements) > 0:
|
|
|
+ sys.stderr.write("Replaced camelCased terms:\n")
|
|
|
+ for oldterm in camel_replacements.keys():
|
|
|
+ sys.stderr.write("%s => %s\n" % (oldterm,
|
|
|
+ camel_replacements[oldterm]))
|