Combine multiple docstring formatting helper scripts into one
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Mon, 8 Aug 2016 14:57:08 +0000 (17:57 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Thu, 11 Aug 2016 15:04:52 +0000 (18:04 +0300)
python/helpers/epydoc_formatter.py [deleted file]
python/helpers/google_formatter.py [deleted file]
python/helpers/numpy_formatter.py [deleted file]
python/helpers/rest_formatter.py
python/src/com/jetbrains/python/PythonHelper.java
python/src/com/jetbrains/python/documentation/docstrings/DocStringFormat.java
python/src/com/jetbrains/python/documentation/docstrings/PyStructuredDocstringFormatter.java

diff --git a/python/helpers/epydoc_formatter.py b/python/helpers/epydoc_formatter.py
deleted file mode 100644 (file)
index 0521b13..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-import sys
-
-import epydoc.markup.epytext
-from epydoc.markup import DocstringLinker
-from epydoc.markup.epytext import parse_docstring, ParseError, _colorize
-from rest_formatter import read_safe, print_safe
-
-
-def _add_para(doc, para_token, stack, indent_stack, errors):
-    """Colorize the given paragraph, and add it to the DOM tree."""
-    para = _colorize(doc, para_token, errors)
-    if para_token.inline:
-        para.attribs['inline'] = True
-    stack[-1].children.append(para)
-
-
-epydoc.markup.epytext._add_para = _add_para
-ParseError.is_fatal = lambda self: False
-
-src = read_safe()
-errors = []
-
-
-class EmptyLinker(DocstringLinker):
-    def translate_indexterm(self, indexterm):
-        return ""
-
-    def translate_identifier_xref(self, identifier, label=None):
-        return identifier
-
-
-docstring = parse_docstring(src, errors)
-docstring, fields = docstring.split_fields()
-html = docstring.to_html(EmptyLinker())
-
-if errors and not html:
-    print_safe(u'Error parsing docstring:\n', error=True)
-    for error in errors:
-        # This script is run only with Python 2 interpreter
-        print_safe(unicode(error) + "\n", error=True)
-    sys.exit(1)
-
-print_safe(html)
diff --git a/python/helpers/google_formatter.py b/python/helpers/google_formatter.py
deleted file mode 100644 (file)
index 6888b31..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import textwrap
-
-import rest_formatter
-from sphinxcontrib.napoleon.docstring import GoogleDocstring
-
-
-def main(text=None):
-    src = rest_formatter.read_safe() if text is None else text
-    rest_formatter.main(str(GoogleDocstring(textwrap.dedent(src))))
-
-
-if __name__ == '__main__':
-    main()
diff --git a/python/helpers/numpy_formatter.py b/python/helpers/numpy_formatter.py
deleted file mode 100644 (file)
index 1de7b83..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import textwrap
-
-import rest_formatter
-from sphinxcontrib.napoleon.docstring import NumpyDocstring
-
-
-def main(text=None):
-    src = rest_formatter.read_safe() if text is None else text
-    rest_formatter.main(str(NumpyDocstring(textwrap.dedent(src))))
-
-
-if __name__ == '__main__':
-    main()
index f8c692debbb2cd599faf074554fdfc19aaaef98e..f0de7b3165910d66c024ecb1845d8c1cf9f6d71e 100644 (file)
@@ -1,15 +1,10 @@
 import os
 import re
 import sys
+import textwrap
 
-from docutils import nodes
-from docutils.core import publish_string
-from docutils.frontend import OptionParser
-from docutils.nodes import Text, field_body, field_name, rubric
-from docutils.parsers.rst import directives
-from docutils.parsers.rst.directives.admonitions import BaseAdmonition
-from docutils.writers.html4css1 import HTMLTranslator, Writer as HTMLWriter
-from docutils.writers import Writer
+import six
+from six import text_type, u
 
 ENCODING = 'utf-8'
 _stdin = os.fdopen(sys.stdin.fileno(), 'rb')
@@ -27,277 +22,282 @@ def print_safe(s, error=False):
     stream.flush()
 
 
-# Copied from the Sphinx' sources. Docutils doesn't handle "seealso" directives by default.
-class seealso(nodes.Admonition, nodes.Element):
-    """Custom "see also" admonition."""
-
-
-class SeeAlso(BaseAdmonition):
-    """
-    An admonition mentioning things to look at as reference.
-    """
-    node_class = seealso
-
-
-directives.register_directive('seealso', SeeAlso)
-
-
-class RestHTMLTranslator(HTMLTranslator):
-    settings = None
-
-    def __init__(self, document):
-        # Copied from epydoc.markup.restructuredtext._EpydocHTMLTranslator
-        if self.settings is None:
-            settings = OptionParser([HTMLWriter()]).get_default_values()
-            self.__class__.settings = settings
-        document.settings = self.settings
-
-        HTMLTranslator.__init__(self, document)
-
-    def visit_document(self, node):
-        pass
-
-    def depart_document(self, node):
-        pass
-
-    def visit_docinfo(self, node):
-        pass
-
-    def depart_docinfo(self, node):
-        pass
-
-    def unimplemented_visit(self, node):
-        pass
-
-    def visit_field_name(self, node):
-        atts = {}
-        if self.in_docinfo:
-            atts['class'] = 'docinfo-name'
-        else:
-            atts['class'] = 'field-name'
-
-        self.context.append('')
-        atts['align'] = "right"
-        self.body.append(self.starttag(node, 'th', '', **atts))
-
-    def visit_field_body(self, node):
-        self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
-        parent_text = node.parent[0][0].astext()
-        if hasattr(node.parent, "type"):
-            self.body.append("(")
-            self.body.append(self.starttag(node, 'a', '',
-                                           href='psi_element://#typename#' + node.parent.type))
-            self.body.append(node.parent.type)
-            self.body.append("</a>")
-            self.body.append(") ")
-        elif parent_text.startswith("type "):
-            index = parent_text.index("type ")
-            type_string = parent_text[index + len("type ")]
-            self.body.append(self.starttag(node, 'a', '',
-                                           href='psi_element://#typename#' + type_string))
-        elif parent_text.startswith("rtype"):
-            type_string = node.children[0][0].astext()
-            self.body.append(self.starttag(node, 'a', '',
-                                           href='psi_element://#typename#' + type_string))
-
-        self.set_class_on_child(node, 'first', 0)
-        field = node.parent
-        if (self.compact_field_list or
-                isinstance(field.parent, nodes.docinfo) or
-                    field.parent.index(field) == len(field.parent) - 1):
-            # If we are in a compact list, the docinfo, or if this is
-            # the last field of the field list, do not add vertical
-            # space after last element.
-            self.set_class_on_child(node, 'last', -1)
-
-    def depart_field_body(self, node):
-        if node.parent[0][0].astext().startswith("type "):
-            self.body.append("</a>")
-        HTMLTranslator.depart_field_body(self, node)
-
-    def visit_reference(self, node):
-        atts = {}
-        if 'refuri' in node:
-            atts['href'] = node['refuri']
-            if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
-                atts['href'] = self.cloak_mailto(atts['href'])
-                self.in_mailto = True
-                # atts['class'] += ' external'
-        else:
-            assert 'refid' in node, 'References must have "refuri" or "refid" attribute.'
-            atts['href'] = '#' + node['refid']
-            atts['class'] += ' internal'
-        if not isinstance(node.parent, nodes.TextElement):
-            assert len(node) == 1 and isinstance(node[0], nodes.image)
-            atts['class'] += ' image-reference'
-        self.body.append(self.starttag(node, 'a', '', **atts))
-
-    def starttag(self, node, tagname, suffix='\n', **attributes):
-        attr_dicts = [attributes]
-        if isinstance(node, nodes.Node):
-            attr_dicts.append(node.attributes)
-        if isinstance(node, dict):
-            attr_dicts.append(node)
-        # Munge each attribute dictionary.  Unfortunately, we need to
-        # iterate through attributes one at a time because some
-        # versions of docutils don't case-normalize attributes.
-        for attr_dict in attr_dicts:
-            # For some reason additional classes in bullet list make it render poorly.
-            # Such lists are used to render multiple return values in Numpy docstrings by Napoleon.
-            if tagname == 'ul' and isinstance(node.parent, field_body):
-                attr_dict.pop('class', None)
-                attr_dict.pop('classes', None)
-                continue
-
-            for (key, val) in attr_dict.items():
-                # Prefix all CSS classes with "rst-"; and prefix all
-                # names with "rst-" to avoid conflicts.
-                if key.lower() in ('class', 'id', 'name'):
-                    attr_dict[key] = 'rst-%s' % val
-                elif key.lower() in ('classes', 'ids', 'names'):
-                    attr_dict[key] = ['rst-%s' % cls for cls in val]
-                elif key.lower() == 'href':
-                    if attr_dict[key][:1] == '#':
-                        attr_dict[key] = '#rst-%s' % attr_dict[key][1:]
-
-        if tagname == 'th' and isinstance(node, field_name):
-            attributes['valign'] = 'top'
-
-        # For headings, use class="heading"
-        if re.match(r'^h\d+$', tagname):
-            attributes['class'] = ' '.join([attributes.get('class', ''), 'heading']).strip()
-        return HTMLTranslator.starttag(self, node, tagname, suffix, **attributes)
-
-    def visit_rubric(self, node):
-        self.body.append(self.starttag(node, 'h1', '', CLASS='rubric'))
-
-    def depart_rubric(self, node):
-        self.body.append('</h1>\n')
-
-    def visit_note(self, node):
-        self.body.append('<h1 class="heading">Note</h1>\n')
-
-    def depart_note(self, node):
-        pass
-
-    def visit_seealso(self, node):
-        self.body.append('<h1 class="heading">See Also</h1>\n')
-
-    def depart_seealso(self, node):
-        pass
-
-    def visit_field_list(self, node):
-        fields = {}
-        for n in node.children:
-            if not n.children:
-                continue
-            child = n.children[0]
-            rawsource = child.rawsource
-            if rawsource.startswith("param "):
-                index = rawsource.index("param ")
-                if not child.children:
-                    continue
-                param_name = rawsource[index + len("param "):]
-                param_type = None
-                parts = param_name.rsplit(None, 1)
-                if len(parts) == 2:
-                    param_type, param_name = parts
-                # Strip leading escaped asterisks for vararg parameters in Google code style docstrings
-                param_name = re.sub(r'\\\*', '*', param_name)
-                child.children[0] = Text(param_name)
-                fields[param_name] = n
-                if param_type:
-                    n.type = param_type
-            if rawsource == "return":
-                fields["return"] = n
-
-        for n in node.children:
-            if len(n.children) < 2:
-                continue
-            field_name, field_body = n.children[0], n.children[1]
-            rawsource = field_name.rawsource
-            if rawsource.startswith("type "):
-                index = rawsource.index("type ")
-                name = re.sub(r'\\\*', '*', rawsource[index + len("type "):])
-                if name in fields:
-                    fields[name].type = self._strip_markup(field_body.astext())[1]
-                    node.children.remove(n)
-            if rawsource == "rtype":
-                if "return" in fields:
-                    fields["return"].type = self._strip_markup(field_body.astext())[1]
-                    node.children.remove(n)
-
-        HTMLTranslator.visit_field_list(self, node)
-
-    def unknown_visit(self, node):
-        """ Ignore unknown nodes """
-
-    def unknown_departure(self, node):
-        """ Ignore unknown nodes """
-
-    def visit_problematic(self, node):
-        # Don't insert hyperlinks to nowhere for e.g. unclosed asterisks
-        if not self._is_text_wrapper(node):
-            return HTMLTranslator.visit_problematic(self, node)
-
-        directive, text = self._strip_markup(node.astext())
-        if directive and directive[1:-1] in ('exc', 'class'):
-            self.body.append(self.starttag(node, 'a', '', href='psi_element://#typename#' + text))
-            self.body.append(text)
-            self.body.append('</a>')
-        else:
-            self.body.append(text)
-        raise nodes.SkipNode
-
-    @staticmethod
-    def _strip_markup(text):
-        m = re.match(r'(:\w+)?(:\S+:)?`(.+?)`', text)
-        if m:
-            _, directive, trimmed = m.groups('')
-            return directive, trimmed
-        return None, text
-
-    def depart_problematic(self, node):
-        if not self._is_text_wrapper(node):
-            return HTMLTranslator.depart_problematic(self, node)
-
-    def visit_Text(self, node):
-        text = node.astext()
-        encoded = self.encode(text)
-        if not isinstance(node.parent, (nodes.literal, nodes.literal_block)):
-            encoded = encoded.replace('---', '&mdash;').replace('--', '&ndash;')
-        if self.in_mailto and self.settings.cloak_email_addresses:
-            encoded = self.cloak_email(encoded)
-        self.body.append(encoded)
-
-    def _is_text_wrapper(self, node):
-        return len(node.children) == 1 and isinstance(node.children[0], Text)
-
-    def visit_block_quote(self, node):
-        self.body.append(self.emptytag(node, "br"))
-
-    def depart_block_quote(self, node):
-        pass
-
-    def visit_literal(self, node):
-        """Process text to prevent tokens from wrapping."""
-        self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal'))
-        text = node.astext()
-        for token in self.words_and_spaces.findall(text):
-            if token.strip():
-                self.body.append('<code>%s</code>'
-                                 % self.encode(token))
-            elif token in ('\n', ' '):
-                # Allow breaks at whitespace:
-                self.body.append(token)
+def format_rest(docstring):
+    from docutils import nodes
+    from docutils.core import publish_string
+    from docutils.frontend import OptionParser
+    from docutils.nodes import Text, field_body, field_name
+    from docutils.parsers.rst import directives
+    from docutils.parsers.rst.directives.admonitions import BaseAdmonition
+    from docutils.writers import Writer
+    from docutils.writers.html4css1 import HTMLTranslator, Writer as HTMLWriter
+
+    # Copied from the Sphinx' sources. Docutils doesn't handle "seealso" directives by default.
+    class seealso(nodes.Admonition, nodes.Element):
+        """Custom "see also" admonition."""
+
+    class SeeAlso(BaseAdmonition):
+        """
+        An admonition mentioning things to look at as reference.
+        """
+        node_class = seealso
+
+    directives.register_directive('seealso', SeeAlso)
+
+    class RestHTMLTranslator(HTMLTranslator):
+        settings = None
+
+        def __init__(self, document):
+            # Copied from epydoc.markup.restructuredtext._EpydocHTMLTranslator
+            if self.settings is None:
+                settings = OptionParser([HTMLWriter()]).get_default_values()
+                self.__class__.settings = settings
+            document.settings = self.settings
+
+            HTMLTranslator.__init__(self, document)
+
+        def visit_document(self, node):
+            pass
+
+        def depart_document(self, node):
+            pass
+
+        def visit_docinfo(self, node):
+            pass
+
+        def depart_docinfo(self, node):
+            pass
+
+        def unimplemented_visit(self, node):
+            pass
+
+        def visit_field_name(self, node):
+            atts = {}
+            if self.in_docinfo:
+                atts['class'] = 'docinfo-name'
+            else:
+                atts['class'] = 'field-name'
+
+            self.context.append('')
+            atts['align'] = "right"
+            self.body.append(self.starttag(node, 'th', '', **atts))
+
+        def visit_field_body(self, node):
+            self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
+            parent_text = node.parent[0][0].astext()
+            if hasattr(node.parent, "type"):
+                self.body.append("(")
+                self.body.append(self.starttag(node, 'a', '',
+                                               href='psi_element://#typename#' + node.parent.type))
+                self.body.append(node.parent.type)
+                self.body.append("</a>")
+                self.body.append(") ")
+            elif parent_text.startswith("type "):
+                index = parent_text.index("type ")
+                type_string = parent_text[index + len("type ")]
+                self.body.append(self.starttag(node, 'a', '',
+                                               href='psi_element://#typename#' + type_string))
+            elif parent_text.startswith("rtype"):
+                type_string = node.children[0][0].astext()
+                self.body.append(self.starttag(node, 'a', '',
+                                               href='psi_element://#typename#' + type_string))
+
+            self.set_class_on_child(node, 'first', 0)
+            field = node.parent
+            if (self.compact_field_list or
+                    isinstance(field.parent, nodes.docinfo) or
+                        field.parent.index(field) == len(field.parent) - 1):
+                # If we are in a compact list, the docinfo, or if this is
+                # the last field of the field list, do not add vertical
+                # space after last element.
+                self.set_class_on_child(node, 'last', -1)
+
+        def depart_field_body(self, node):
+            if node.parent[0][0].astext().startswith("type "):
+                self.body.append("</a>")
+            HTMLTranslator.depart_field_body(self, node)
+
+        def visit_reference(self, node):
+            atts = {}
+            if 'refuri' in node:
+                atts['href'] = node['refuri']
+                if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
+                    atts['href'] = self.cloak_mailto(atts['href'])
+                    self.in_mailto = True
+                    # atts['class'] += ' external'
             else:
-                # Protect runs of multiple spaces; the last space can wrap:
-                self.body.append('&nbsp;' * (len(token) - 1) + ' ')
-        self.body.append('</tt>')
-        raise nodes.SkipNode
+                assert 'refid' in node, 'References must have "refuri" or "refid" attribute.'
+                atts['href'] = '#' + node['refid']
+                atts['class'] += ' internal'
+            if not isinstance(node.parent, nodes.TextElement):
+                assert len(node) == 1 and isinstance(node[0], nodes.image)
+                atts['class'] += ' image-reference'
+            self.body.append(self.starttag(node, 'a', '', **atts))
+
+        def starttag(self, node, tagname, suffix='\n', **attributes):
+            attr_dicts = [attributes]
+            if isinstance(node, nodes.Node):
+                attr_dicts.append(node.attributes)
+            if isinstance(node, dict):
+                attr_dicts.append(node)
+            # Munge each attribute dictionary.  Unfortunately, we need to
+            # iterate through attributes one at a time because some
+            # versions of docutils don't case-normalize attributes.
+            for attr_dict in attr_dicts:
+                # For some reason additional classes in bullet list make it render poorly.
+                # Such lists are used to render multiple return values in Numpy docstrings by Napoleon.
+                if tagname == 'ul' and isinstance(node.parent, field_body):
+                    attr_dict.pop('class', None)
+                    attr_dict.pop('classes', None)
+                    continue
+
+                for (key, val) in attr_dict.items():
+                    # Prefix all CSS classes with "rst-"; and prefix all
+                    # names with "rst-" to avoid conflicts.
+                    if key.lower() in ('class', 'id', 'name'):
+                        attr_dict[key] = 'rst-%s' % val
+                    elif key.lower() in ('classes', 'ids', 'names'):
+                        attr_dict[key] = ['rst-%s' % cls for cls in val]
+                    elif key.lower() == 'href':
+                        if attr_dict[key][:1] == '#':
+                            attr_dict[key] = '#rst-%s' % attr_dict[key][1:]
+
+            if tagname == 'th' and isinstance(node, field_name):
+                attributes['valign'] = 'top'
+
+            # For headings, use class="heading"
+            if re.match(r'^h\d+$', tagname):
+                attributes['class'] = ' '.join([attributes.get('class', ''), 'heading']).strip()
+            return HTMLTranslator.starttag(self, node, tagname, suffix, **attributes)
+
+        def visit_rubric(self, node):
+            self.body.append(self.starttag(node, 'h1', '', CLASS='rubric'))
+
+        def depart_rubric(self, node):
+            self.body.append('</h1>\n')
+
+        def visit_note(self, node):
+            self.body.append('<h1 class="heading">Note</h1>\n')
+
+        def depart_note(self, node):
+            pass
+
+        def visit_seealso(self, node):
+            self.body.append('<h1 class="heading">See Also</h1>\n')
 
+        def depart_seealso(self, node):
+            pass
+
+        def visit_field_list(self, node):
+            fields = {}
+            for n in node.children:
+                if not n.children:
+                    continue
+                child = n.children[0]
+                rawsource = child.rawsource
+                if rawsource.startswith("param "):
+                    index = rawsource.index("param ")
+                    if not child.children:
+                        continue
+                    param_name = rawsource[index + len("param "):]
+                    param_type = None
+                    parts = param_name.rsplit(None, 1)
+                    if len(parts) == 2:
+                        param_type, param_name = parts
+                    # Strip leading escaped asterisks for vararg parameters in Google code style docstrings
+                    param_name = re.sub(r'\\\*', '*', param_name)
+                    child.children[0] = Text(param_name)
+                    fields[param_name] = n
+                    if param_type:
+                        n.type = param_type
+                if rawsource == "return":
+                    fields["return"] = n
+
+            for n in node.children:
+                if len(n.children) < 2:
+                    continue
+                field_name, field_body = n.children[0], n.children[1]
+                rawsource = field_name.rawsource
+                if rawsource.startswith("type "):
+                    index = rawsource.index("type ")
+                    name = re.sub(r'\\\*', '*', rawsource[index + len("type "):])
+                    if name in fields:
+                        fields[name].type = self._strip_markup(field_body.astext())[1]
+                        node.children.remove(n)
+                if rawsource == "rtype":
+                    if "return" in fields:
+                        fields["return"].type = self._strip_markup(field_body.astext())[1]
+                        node.children.remove(n)
+
+            HTMLTranslator.visit_field_list(self, node)
+
+        def unknown_visit(self, node):
+            """ Ignore unknown nodes """
+
+        def unknown_departure(self, node):
+            """ Ignore unknown nodes """
+
+        def visit_problematic(self, node):
+            # Don't insert hyperlinks to nowhere for e.g. unclosed asterisks
+            if not self._is_text_wrapper(node):
+                return HTMLTranslator.visit_problematic(self, node)
+
+            directive, text = self._strip_markup(node.astext())
+            if directive and directive[1:-1] in ('exc', 'class'):
+                self.body.append(self.starttag(node, 'a', '', href='psi_element://#typename#' + text))
+                self.body.append(text)
+                self.body.append('</a>')
+            else:
+                self.body.append(text)
+            raise nodes.SkipNode
+
+        @staticmethod
+        def _strip_markup(text):
+            m = re.match(r'(:\w+)?(:\S+:)?`(.+?)`', text)
+            if m:
+                _, directive, trimmed = m.groups('')
+                return directive, trimmed
+            return None, text
+
+        def depart_problematic(self, node):
+            if not self._is_text_wrapper(node):
+                return HTMLTranslator.depart_problematic(self, node)
+
+        def visit_Text(self, node):
+            text = node.astext()
+            encoded = self.encode(text)
+            if not isinstance(node.parent, (nodes.literal, nodes.literal_block)):
+                encoded = encoded.replace('---', '&mdash;').replace('--', '&ndash;')
+            if self.in_mailto and self.settings.cloak_email_addresses:
+                encoded = self.cloak_email(encoded)
+            self.body.append(encoded)
+
+        def _is_text_wrapper(self, node):
+            return len(node.children) == 1 and isinstance(node.children[0], Text)
+
+        def visit_block_quote(self, node):
+            self.body.append(self.emptytag(node, "br"))
+
+        def depart_block_quote(self, node):
+            pass
+
+        def visit_literal(self, node):
+            """Process text to prevent tokens from wrapping."""
+            self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal'))
+            text = node.astext()
+            for token in self.words_and_spaces.findall(text):
+                if token.strip():
+                    self.body.append('<code>%s</code>'
+                                     % self.encode(token))
+                elif token in ('\n', ' '):
+                    # Allow breaks at whitespace:
+                    self.body.append(token)
+                else:
+                    # Protect runs of multiple spaces; the last space can wrap:
+                    self.body.append('&nbsp;' * (len(token) - 1) + ' ')
+            self.body.append('</tt>')
+            raise nodes.SkipNode
 
-def format_docstring(docstring):
     class _DocumentPseudoWriter(Writer):
         def __init__(self):
             self.document = None
@@ -315,12 +315,81 @@ def format_docstring(docstring):
     document.settings.xml_declaration = None
     visitor = RestHTMLTranslator(document)
     document.walkabout(visitor)
-    return ''.join(visitor.body)
+    return u('').join(visitor.body)
+
+
+def format_google(docstring):
+    from sphinxcontrib.napoleon import GoogleDocstring
+    transformed = text_type(GoogleDocstring(textwrap.dedent(docstring)))
+    return format_rest(transformed)
+
+
+def format_numpy(docstring):
+    from sphinxcontrib.napoleon import NumpyDocstring
+    transformed = text_type(NumpyDocstring(textwrap.dedent(docstring)))
+    return format_rest(transformed)
+
+
+def format_epytext(docstring):
+    if six.PY3:
+        return u('Epydoc is not compatible with Python 3 interpreter')
+
+    import epydoc.markup.epytext
+    from epydoc.markup import DocstringLinker
+    from epydoc.markup.epytext import parse_docstring, ParseError, _colorize
+
+    def _add_para(doc, para_token, stack, indent_stack, errors):
+        """Colorize the given paragraph, and add it to the DOM tree."""
+        para = _colorize(doc, para_token, errors)
+        if para_token.inline:
+            para.attribs['inline'] = True
+        stack[-1].children.append(para)
+
+    epydoc.markup.epytext._add_para = _add_para
+    ParseError.is_fatal = lambda self: False
+
+    errors = []
+
+    class EmptyLinker(DocstringLinker):
+        def translate_indexterm(self, indexterm):
+            return ""
+
+        def translate_identifier_xref(self, identifier, label=None):
+            return identifier
+
+    docstring = parse_docstring(docstring, errors)
+    docstring, fields = docstring.split_fields()
+    html = docstring.to_html(EmptyLinker())
+
+    if errors and not html:
+        # It's not possible to recover original stacktraces of the errors
+        error_lines = '\n'.join(text_type(e) for e in errors)
+        raise Exception('Error parsing docstring. Probable causes:\n' + error_lines)
+
+    return html
+
+
+def main():
+    args = sys.argv[1:]
+
+    docstring_format = args[0] if args else 'rest'
+    if len(args) > 1:
+        try:
+            f = open(args[1], 'rb')
+            text = f.read().decode('utf-8')
+        finally:
+            f.close()
+    else:
+        text = read_safe()
 
+    formatter = {
+        'rest': format_rest,
+        'google': format_google,
+        'numpy': format_numpy,
+        'epytext': format_epytext
+    }.get(docstring_format, format_rest)
 
-def main(text=None):
-    src = read_safe() if text is None else text
-    html = format_docstring(src)
+    html = formatter(text)
     print_safe(html)
 
 
index 91867b915b1a38d316fbd16b70d5ea93751047d0..03cc8c926cedc5c73a09d5ee7ecfaf6ccaeccad4 100644 (file)
@@ -68,10 +68,7 @@ public enum PythonHelper implements HelperPackage {
 
   BUILDOUT_ENGULFER("pycharm", "buildout_engulfer"),
 
-  EPYDOC_FORMATTER("epydoc_formatter.py"),
-  REST_FORMATTER("rest_formatter.py"),
-  GOOGLE_FORMATTER("google_formatter.py"),
-  NUMPY_FORMATTER("numpy_formatter.py"),
+  DOCSTRING_FORMATTER("rest_formatter.py"),
 
   EXTRA_SYSPATH("extra_syspath.py"),
   SYSPATH("syspath.py"),
index caa5ec15a0a1ac757a1b89a7fbeaab7bc1bf90c1..43f32c57f3ab51d58269a8a84c49457ce2731a8e 100644 (file)
@@ -16,7 +16,6 @@
 package com.jetbrains.python.documentation.docstrings;
 
 import com.intellij.psi.PsiElement;
-import com.intellij.util.Function;
 import com.intellij.util.ObjectUtils;
 import com.intellij.util.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
@@ -32,11 +31,11 @@ public enum DocStringFormat {
   /**
    * @see DocStringUtil#ensureNotPlainDocstringFormat(PsiElement)
    */
-  PLAIN("Plain"),
-  EPYTEXT("Epytext"),
-  REST("reStructuredText"),
-  NUMPY("NumPy"),
-  GOOGLE("Google");
+  PLAIN("Plain", ""),
+  EPYTEXT("Epytext", "epytext"),
+  REST("reStructuredText", "rest"),
+  NUMPY("NumPy", "numpy"),
+  GOOGLE("Google", "google");
 
   public static final List<String> ALL_NAMES = getAllNames();
 
@@ -67,10 +66,12 @@ public enum DocStringFormat {
     return ObjectUtils.notNull(fromName(name), PLAIN);
   }
 
-  String myName;
+  private final String myName;
+  private final String myFormatterCommand;
 
-  DocStringFormat(@NotNull String name) {
+  DocStringFormat(@NotNull String name, @NotNull String formatterCommand) {
     myName = name;
+    myFormatterCommand = formatterCommand;
   }
 
   @NotNull
@@ -78,4 +79,8 @@ public enum DocStringFormat {
     return myName;
   }
 
+  @NotNull
+  public String getFormatterCommand() {
+    return myFormatterCommand;
+  }
 }
index a24d45d9ee2176fe54a839b0864915ca52397ca8..95bc1d2ed2d855e3fcc986492a022b67c438165c 100644 (file)
@@ -25,7 +25,6 @@ import com.intellij.openapi.module.ModuleUtilCore;
 import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.vfs.CharsetToolkit;
 import com.intellij.psi.PsiElement;
-import com.jetbrains.python.HelperPackage;
 import com.jetbrains.python.PyBundle;
 import com.jetbrains.python.PythonHelper;
 import com.jetbrains.python.psi.PyIndentUtil;
@@ -70,37 +69,24 @@ public class PyStructuredDocstringFormatter {
 
     final String preparedDocstring = PyIndentUtil.removeCommonIndent(docstring, true).trim();
 
-    final HelperPackage formatter;
-    final StructuredDocString structuredDocString;
     final DocStringFormat format = DocStringUtil.guessDocStringFormat(preparedDocstring, element);
-    if (format == DocStringFormat.GOOGLE) {
-      formatter = PythonHelper.GOOGLE_FORMATTER;
-      structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.GOOGLE, preparedDocstring);
-    }
-    else if (format == DocStringFormat.NUMPY) {
-      formatter = PythonHelper.NUMPY_FORMATTER;
-      structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.NUMPY, preparedDocstring);
-    }
-    else if (format == DocStringFormat.EPYTEXT) {
-      formatter = PythonHelper.EPYDOC_FORMATTER;
-      structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.EPYTEXT, preparedDocstring);
-      result.add(formatStructuredDocString(structuredDocString));
-    }
-    else if (format == DocStringFormat.REST) {
-      formatter = PythonHelper.REST_FORMATTER;
-      structuredDocString = DocStringUtil.parseDocStringContent(DocStringFormat.REST, preparedDocstring);
-    }
-
-    else {
+    if (format == DocStringFormat.PLAIN) {
       return null;
     }
 
-    final String output = runExternalTool(module, formatter, preparedDocstring);
+    final StructuredDocString structuredDocString = DocStringUtil.parseDocStringContent(format, preparedDocstring);
+
+    final String output = runExternalTool(module, format, preparedDocstring);
     if (output != null) {
-      result.add(0, output);
+      result.add(output);
     }
     else {
-      result.add(0, structuredDocString.getDescription());
+      result.add(structuredDocString.getDescription());
+    }
+
+    // Information about parameters in Epytext-style docstrings are formatter on our side
+    if (format == DocStringFormat.EPYTEXT) {
+      result.add(formatStructuredDocString(structuredDocString));
     }
 
     return result;
@@ -108,11 +94,11 @@ public class PyStructuredDocstringFormatter {
 
   @Nullable
   private static String runExternalTool(@NotNull final Module module,
-                                        @NotNull final HelperPackage formatter,
+                                        @NotNull final DocStringFormat format,
                                         @NotNull final String docstring) {
     final Sdk sdk;
     final String missingInterpreterMessage;
-    if (formatter == PythonHelper.EPYDOC_FORMATTER) {
+    if (format == DocStringFormat.EPYTEXT) {
       sdk = PythonSdkType.findPython2Sdk(module);
       missingInterpreterMessage = PyBundle.message("QDOC.epydoc.python2.sdk.not.found");
     }
@@ -121,7 +107,7 @@ public class PyStructuredDocstringFormatter {
       missingInterpreterMessage = PyBundle.message("QDOC.sdk.not.found");
     }
     if (sdk == null) {
-      LOG.warn("Python SDK for docstring formatter " + formatter +  " is not found");
+      LOG.warn("Python SDK for docstring formatter " + format +  " is not found");
       return "<p color=\"red\">" + missingInterpreterMessage + "</p>";
     }
 
@@ -132,9 +118,10 @@ public class PyStructuredDocstringFormatter {
     final byte[] data = new byte[encoded.limit()];
     encoded.get(data);
 
-    final GeneralCommandLine commandLine = formatter.newCommandLine(sdk, Lists.newArrayList());
+    final ArrayList<String> arguments = Lists.newArrayList(format.getFormatterCommand());
+    final GeneralCommandLine commandLine = PythonHelper.DOCSTRING_FORMATTER.newCommandLine(sdk, arguments);
     commandLine.setCharset(DEFAULT_CHARSET);
-    
+
     LOG.debug("Command for launching docstring formatter: " + commandLine.getCommandLineString());
     
     final ProcessOutput output = PySdkUtil.getProcessOutput(commandLine, new File(sdkHome).getParent(), null, 5000, data, false);