summaryrefslogtreecommitdiff
path: root/docs/sphinx/hieroglyph
diff options
context:
space:
mode:
Diffstat (limited to 'docs/sphinx/hieroglyph')
-rw-r--r--docs/sphinx/hieroglyph/LICENSE.txt26
-rw-r--r--docs/sphinx/hieroglyph/README.txt10
-rw-r--r--docs/sphinx/hieroglyph/__init__.py6
-rw-r--r--docs/sphinx/hieroglyph/errors.py10
-rw-r--r--docs/sphinx/hieroglyph/hieroglyph.py404
-rw-r--r--docs/sphinx/hieroglyph/nodes.py267
-rw-r--r--docs/sphinx/hieroglyph/test/__init__.py2
-rw-r--r--docs/sphinx/hieroglyph/test/test_comments.py586
-rw-r--r--docs/sphinx/hieroglyph/test/test_hierglyph.py264
-rw-r--r--docs/sphinx/hieroglyph/test/test_nodes.py386
-rw-r--r--docs/sphinx/hieroglyph/version.py3
11 files changed, 1964 insertions, 0 deletions
diff --git a/docs/sphinx/hieroglyph/LICENSE.txt b/docs/sphinx/hieroglyph/LICENSE.txt
new file mode 100644
index 000000000..3f7a63830
--- /dev/null
+++ b/docs/sphinx/hieroglyph/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2011, Robert Smallshire
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Robert Smallshire nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/sphinx/hieroglyph/README.txt b/docs/sphinx/hieroglyph/README.txt
new file mode 100644
index 000000000..c26409d89
--- /dev/null
+++ b/docs/sphinx/hieroglyph/README.txt
@@ -0,0 +1,10 @@
+Sphinx is a popular tool for documenting Python APIs which uses reStructuredText
+as a its lightweight markup language. Sphinx extends restructured text with
+semantic markup elements for documenting Python APIs but once these are used the
+ratio of markup to content becomes too high and readability is compromised
+enough that the docstring becomes unsuitable for use with standard Python
+introspection mechanisms like help() or IDEs.
+
+Hieroglyph is an a Sphinx extension which automatically converts a highly
+readable docstring format suitable for use with help() and IDEs to the
+reStructuredText hieroglyphics required by Sphinx. \ No newline at end of file
diff --git a/docs/sphinx/hieroglyph/__init__.py b/docs/sphinx/hieroglyph/__init__.py
new file mode 100644
index 000000000..25dea27fb
--- /dev/null
+++ b/docs/sphinx/hieroglyph/__init__.py
@@ -0,0 +1,6 @@
+# We only need to expose the setup function to Sphinx
+
+from .hieroglyph import setup
+from .version import __version__
+
+__author__ = 'Robert Smallshire' \ No newline at end of file
diff --git a/docs/sphinx/hieroglyph/errors.py b/docs/sphinx/hieroglyph/errors.py
new file mode 100644
index 000000000..334b097d8
--- /dev/null
+++ b/docs/sphinx/hieroglyph/errors.py
@@ -0,0 +1,10 @@
+
+from sphinx.errors import ExtensionError
+
+__author__ = 'rjs'
+
+class HieroglyphError(ExtensionError):
+ '''
+ An exception type specific to the Hieroglyph Sphinx extension.
+ '''
+ pass \ No newline at end of file
diff --git a/docs/sphinx/hieroglyph/hieroglyph.py b/docs/sphinx/hieroglyph/hieroglyph.py
new file mode 100644
index 000000000..0056d9ab8
--- /dev/null
+++ b/docs/sphinx/hieroglyph/hieroglyph.py
@@ -0,0 +1,404 @@
+from __future__ import print_function
+
+import re
+
+from errors import HieroglyphError
+from nodes import (Node, Raises, Except, Note, Warning, Returns, Arg,
+ ensure_terminal_blank)
+
+__author__ = 'Robert Smallshire'
+
+def parse_hieroglyph_text(lines):
+ '''Parse text in hieroglyph format and return a reStructuredText equivalent
+
+ Args:
+ lines: A sequence of strings representing the lines of a single
+ docstring as read from the source by Sphinx. This string should be
+ in a format that can be parsed by hieroglyph.
+
+ Returns:
+ A list of lines containing the transformed docstring as
+ reStructuredText as produced by hieroglyph.
+
+ Raises:
+ RuntimeError: If the docstring cannot be parsed.
+ '''
+ indent_lines = unindent(lines)
+ indent_lines = pad_blank_lines(indent_lines)
+ indent_lines = first_paragraph_indent(indent_lines)
+ indent_paragraphs = gather_lines(indent_lines)
+ parse_tree = group_paragraphs(indent_paragraphs)
+ syntax_tree = extract_structure(parse_tree)
+ result = syntax_tree.render_rst()
+ ensure_terminal_blank(result)
+ return result
+
+
+def unindent(lines):
+ '''Convert an iterable of indented lines into a sequence of tuples.
+
+ The first element of each tuple is the indent in number of characters, and
+ the second element is the unindented string.
+
+ Args:
+ lines: A sequence of strings representing the lines of text in a docstring.
+
+ Returns:
+ A list of tuples where each tuple corresponds to one line of the input
+ list. Each tuple has two entries - the first is an integer giving the
+ size of the indent in characters, the second is the unindented text.
+ '''
+ unindented_lines = []
+ for line in lines:
+ unindented_line = line.lstrip()
+ indent = len(line) - len(unindented_line)
+ unindented_lines.append((indent, unindented_line))
+ return unindented_lines
+
+
+def pad_blank_lines(indent_texts):
+ '''Give blank (empty) lines the same indent level as the preceding line.
+
+ Args:
+ indent_texts: An iterable of tuples each containing an integer in the
+ first element and a string in the second element.
+
+ Returns:
+ A list of tuples each containing an integer in the first element and a
+ string in the second element.
+ '''
+ current_indent = 0
+ result = []
+ for indent, text in indent_texts:
+ if len(text) > 0:
+ current_indent = indent
+ result.append((current_indent, text))
+ return result
+
+
+def extract_structure(parse_tree):
+ '''Create an Abstract Syntax Tree representing the semantics of a parse tree.
+
+ Args:
+ parse_tree: TODO
+
+ Returns:
+ A Node with is the result of an Abstract Syntax Tree representing the
+ docstring.
+
+ Raises:
+ HieroglyphError: In the event that the parse tree cannot be understood.
+ '''
+ return convert_node(parse_tree)
+
+
+def convert_node(node):
+ if node.indent == 0 and len(node.lines) == 0:
+ return convert_children(node)
+ if node.lines[0].startswith('Args:'):
+ return convert_args(node)
+ if node.lines[0].startswith('Returns:'):
+ return convert_returns(node)
+ if node.lines[0].startswith('Raises:'):
+ return convert_raises(node)
+ if node.lines[0].startswith('Note:'):
+ return convert_note(node)
+ if node.lines[0].startswith('Warning:'):
+ return convert_warning(node)
+ result = convert_children(node)
+ result.lines = node.lines
+ result.indent = node.indent
+ return result
+
+
+def convert_children(node):
+ converted_children = [convert_node(child) for child in node.children]
+ result = Node()
+ result.children = converted_children
+ return result
+
+
+ARG_REGEX = re.compile(r'(\*{0,2}\w+)(\s+\((\w+)\))?\s*:\s*(.*)')
+
+def append_child_to_args_group_node(child, group_node, indent):
+ arg = None
+ non_empty_lines = (line for line in child.lines if line)
+ for line in non_empty_lines:
+ m = ARG_REGEX.match(line)
+ if m is None:
+ raise HieroglyphError("Invalid hieroglyph argument syntax: {0}".format(line))
+ param_name = m.group(1)
+ param_type = m.group(3)
+ param_text = m.group(4)
+
+ arg = Arg(indent, child.indent, param_name)
+ group_node.children.append(arg)
+ arg.type = param_type
+
+ if param_text is not None:
+ arg.children.append(Node(indent, [param_text], arg))
+ if arg is not None:
+ last_child = arg.children[-1] if len(arg.children) != 0 else arg
+ for grandchild in child.children:
+ last_child.children.append(grandchild)
+
+
+def convert_args(node):
+ assert node.lines[0].startswith('Args:')
+ group_node = Node()
+ for child in node.children:
+ append_child_to_args_group_node(child, group_node, node.indent)
+ return group_node
+
+
+def convert_returns(node):
+ assert node.lines[0].startswith('Returns:')
+ returns = Returns(node.indent)
+ returns.line = node.lines[0][8:].strip()
+ returns.children = node.children
+ return returns
+
+
+def convert_note(node):
+ assert node.lines[0].startswith('Note:')
+ note = Note(node.indent)
+ note.line = node.lines[0][5:].strip()
+ note.children = node.children
+ return note
+
+
+def convert_warning(node):
+ assert node.lines[0].startswith('Warning:')
+ warning = Warning(node.indent)
+ warning.line = node.lines[0][8:].strip()
+ warning.children = node.children
+ return warning
+
+
+def convert_raises(node):
+ assert node.lines[0].startswith('Raises:')
+ group_node = Raises(node.indent)
+ for child in node.children:
+ append_child_to_raise_node(child, group_node)
+ return group_node
+
+
+RAISE_REGEX = re.compile(r'(\w+)\s*:\s*(.*)')
+
+def extract_exception_type_and_text(line):
+ m = RAISE_REGEX.match(line)
+ if m is None:
+ raise HieroglyphError("Invalid hieroglyph exception syntax: {0}".format(line))
+ return (m.group(2), m.group(1))
+
+
+def append_child_to_raise_node(child, group_node):
+ exception = None
+ non_empty_lines = (line for line in child.lines if line)
+ for line in non_empty_lines:
+ exception_text, exception_type = extract_exception_type_and_text(line)
+
+ exception = Except(child.indent, exception_type)
+ group_node.children.append(exception) # TODO: Could use parent here.
+
+ if exception_text is not None:
+ exception.children.append( Node(child.indent,
+ [exception_text], exception))
+ if exception is not None:
+ last_child = exception.children[-1] if len(exception.children) != 0 else exception
+ for grandchild in child.children:
+ last_child.children.append(grandchild)
+
+
+def group_paragraphs(indent_paragraphs):
+ '''
+ Group paragraphs so that more indented paragraphs become children of less
+ indented paragraphs.
+ '''
+ # The tree consists of tuples of the form (indent, [children]) where the
+ # children may be strings or other tuples
+
+ root = Node(0, [], None)
+ current_node = root
+
+ previous_indent = -1
+ for indent, lines in indent_paragraphs:
+ if indent > previous_indent:
+ current_node = create_child_node(current_node, indent, lines)
+ elif indent == previous_indent:
+ current_node = create_sibling_node(current_node, indent, lines)
+ elif indent < previous_indent:
+ current_node = create_uncle_node(current_node, indent, lines)
+ previous_indent = indent
+ return root
+
+
+def create_sibling_node(current_node, indent, lines):
+ sibling = Node(indent, lines, current_node.parent)
+ current_node.parent.add_child(sibling)
+ current_node = sibling
+ return current_node
+
+
+def create_child_node(current_node, indent, lines):
+ child = Node(indent, lines, current_node)
+ current_node.add_child(child)
+ current_node = child
+ return current_node
+
+
+def create_uncle_node(current_node, indent, lines):
+ ancestor = current_node
+ while ancestor.indent >= indent:
+ if ancestor.parent is None:
+ break
+ ancestor = ancestor.parent
+ uncle = Node(indent, lines, ancestor)
+ ancestor.add_child(uncle)
+ current_node = uncle
+ return current_node
+
+
+def gather_lines(indent_lines):
+ '''Split the list of (int, str) tuples into a list of (int, [str]) tuples
+ to group the lines into paragraphs of consistent indent.
+ '''
+ return remove_empty_paragraphs(split_separated_lines(gather_lines_by_indent(indent_lines)))
+
+def gather_lines_by_indent(indent_lines):
+ result = []
+ previous_indent = -1
+ for indent, line in indent_lines:
+ if indent != previous_indent:
+ paragraph = (indent, [])
+ result.append(paragraph)
+ else:
+ paragraph = result[-1]
+ paragraph[1].append(line)
+ previous_indent = indent
+ return result
+
+def split_separated_lines(indent_paragraphs):
+ result = []
+ for indent, paragraph in indent_paragraphs:
+ result.append((indent, []))
+
+ if len(paragraph) > 0:
+ result[-1][1].append(paragraph[0])
+
+ if len(paragraph) > 2:
+ for line in paragraph[1: -1]:
+ result[-1][1].append(line)
+ if len(line) == 0:
+ result.append((indent, []))
+
+ if len(paragraph) > 1:
+ result[-1][1].append(paragraph[-1])
+
+ return result
+
+def remove_empty_paragraphs(indent_paragraphs):
+ return [(indent, paragraph) for indent, paragraph in indent_paragraphs if len(paragraph)]
+
+def first_paragraph_indent(indent_texts):
+ '''Fix the indentation on the first paragraph.
+
+ This occurs because the first line of a multi-line docstring following the
+ opening quote usually has no indent.
+
+ Args:
+ indent_texts: The lines of the docstring as an iterable over 2-tuples
+ each containing an integer indent level as the first element and
+ the text as the second element.
+
+ Return:
+ A list of 2-tuples, each containing an integer indent level as the
+ first element and the text as the second element.
+ '''
+ opening_indent = determine_opening_indent(indent_texts)
+
+ result = []
+ input = iter(indent_texts)
+ for indent, text in input:
+ if indent == 0:
+ result.append((opening_indent, text))
+ else:
+ result.append((indent, text))
+ break
+
+ for indent, text in input:
+ result.append((indent, text))
+
+ return result
+
+
+def determine_opening_indent(indent_texts):
+ '''Determine the opening indent level for a docstring.
+
+ The opening indent level is the indent level is the first non-zero indent
+ level of a non-empty line in the docstring.
+
+ Args:
+ indent_texts: The lines of the docstring as an iterable over 2-tuples
+ each containing an integer indent level as the first element and
+ the text as the second element.
+
+ Returns:
+ The opening indent level as an integer.
+ '''
+ num_lines = len(indent_texts)
+
+ if num_lines < 1:
+ return 0
+
+ assert num_lines >= 1
+
+ first_line_indent = indent_texts[0][0]
+
+ if num_lines == 1:
+ return first_line_indent
+
+ assert num_lines >= 2
+
+ second_line_indent = indent_texts[1][0]
+ second_line_text = indent_texts[1][1]
+
+ if len(second_line_text) == 0:
+ return first_line_indent
+
+ return second_line_indent
+
+
+
+def rewrite_autodoc(app, what, name, obj, options, lines):
+ '''Convert lines from Hieroglyph to Sphinx format.
+
+ The function to be called by the Sphinx autodoc extension when autodoc
+ has read and processed a docstring. This function modified its
+ ``lines`` argument *in place* replacing Hieroglyph syntax input into
+ Sphinx reStructuredText output.
+
+ Args:
+ apps: The Sphinx application object.
+
+ what: The type of object which the docstring belongs to. One of
+ 'module', 'class', 'exception', 'function', 'method', 'attribute'
+
+ name: The fully qualified name of the object.
+
+ obj: The object itself.
+
+ options: The options given to the directive. An object with attributes
+ ``inherited_members``, ``undoc_members``, ``show_inheritance`` and
+ ``noindex`` that are ``True`` if the flag option of the same name
+ was given to the auto directive.
+
+ lines: The lines of the docstring. Will be modified *in place*.
+ '''
+ lines[:] = parse_hieroglyph_text(lines)
+
+
+def setup(app):
+ app.connect('autodoc-process-docstring', rewrite_autodoc)
+
+
diff --git a/docs/sphinx/hieroglyph/nodes.py b/docs/sphinx/hieroglyph/nodes.py
new file mode 100644
index 000000000..e583ce04d
--- /dev/null
+++ b/docs/sphinx/hieroglyph/nodes.py
@@ -0,0 +1,267 @@
+__author__ = 'Robert Smallshire'
+
+class Node(object):
+
+ def __init__(self, indent=None, lines=None, parent=None):
+ if indent is not None:
+ self.indent = indent
+ else:
+ self.indent = 0
+
+ if lines is not None:
+ self.lines = lines
+ else:
+ self.lines = []
+
+ self._parent = parent
+
+ self.children = []
+
+ parent = property(lambda self: self._parent)
+
+ def add_child(self, child):
+ assert(child.parent is self)
+ self.children.append(child)
+
+
+ def __repr__(self):
+ return "Node(" + repr(self.indent) + ", " + repr(self.lines) + ", children=" + repr(self.children) + ")"
+
+
+ def render_rst(self, *args, **kwargs):
+ result = []
+ prefix = ' ' * self.indent
+ result.extend(prefix + line for line in self.lines)
+ for child in self.children:
+ result.extend(child.render_rst())
+ return result
+
+
+
+class Arg(Node):
+
+ def __init__(self, indent, child_indent, name):
+ super(Arg, self).__init__(indent)
+ self.child_indent = child_indent
+ self.name = name
+ self.type = None
+
+
+ def __repr__(self):
+ return "Arg(" + repr(self.name) + ", " + repr(self.type) + ", children=" + repr(self.children) + ")"
+
+
+ def render_rst(self, *args, **kwargs):
+ result = []
+ indent = ' ' * self.indent
+
+ # Render the param description
+ description = []
+ for child in self.children:
+ child_lines = child.render_rst()
+ description.extend(child_lines)
+
+ dedent = self.child_indent - self.indent
+
+ name = self.name.replace('*', r'\*')
+
+ first_description = description[0].lstrip() if len(description) else ''
+ if not first_description:
+ # TODO: Emit a warning about a missing argument description
+ pass
+
+ result.append("{indent}:param {name}: {first_description}".format(indent=indent, name=name,
+ first_description=first_description))
+
+ dedented_body = [line[dedent:] for line in description[1:]]
+
+ result.extend(dedented_body)
+
+ # If a type was specified render the type
+ if self.type is not None:
+ result.append("{indent}:type {name}: {type}".format(indent=indent, name=self.name, type=self.type))
+ result.append('')
+
+ ensure_terminal_blank(result)
+
+ return result
+
+
+
+class Raises(Node):
+
+ def __init__(self, indent=None):
+ super(Raises, self).__init__(indent=indent)
+
+ def __repr__(self):
+ return "Raises(" + repr(self.indent) + ", children=" + repr(self.children) + ")"
+
+
+ def render_rst(self, *args, **kwargs):
+ result = []
+ indent = ' ' * self.indent
+ result.append(indent + ':raises:')
+ for child in self.children:
+ result.extend(child.render_rst(only_child=len(self.children) == 1))
+
+ ensure_terminal_blank(result)
+
+ return result
+
+
+class Except(Node):
+
+ def __init__(self, indent, type):
+ super(Except, self).__init__(indent=indent)
+ #self.child_indent = child_indent
+ self.type = type
+
+
+ def __repr__(self):
+ return "Except(" + repr(self.type) + ", children=" + repr(self.children) + ")"
+
+
+ def render_rst(self, only_child=False, *args, **kwargs):
+ result = []
+ indent = ' ' * self.indent
+
+ # Render the param description
+ description = []
+ for child in self.children:
+ child_lines = child.render_rst()
+ description.extend(child_lines)
+
+ #dedent = self.child_indent - self.indent
+ bullet = '* ' if not only_child else ''
+
+ first_description = description[0].lstrip() if len(description) else ''
+ result.append("{indent}{bullet}{type} - {first_description}".format(indent=indent,
+ bullet=bullet, type=self.type,
+ first_description=first_description))
+
+ #dedented_body = [' ' * len(bullet) + line[dedent:] for line in description[1:]]
+ #result.extend(dedented_body)
+ result.extend(description[1:])
+ ensure_terminal_blank(result)
+
+ return result
+
+
+
+class Returns(Node):
+
+ def __init__(self, indent):
+ super(Returns, self).__init__(indent=indent)
+ self.title = 'Returns'
+ self.line = ''
+
+
+ def __repr__(self):
+ return "Returns(" + str(self.indent) + ", children=" + str(self.children) + ")"
+
+
+ def render_rst(self, *args, **kwargs):
+ result = []
+ indent = ' ' * self.indent
+
+ # Render the param description
+ description = [self.line] if self.line else []
+ for child in self.children:
+ child_lines = child.render_rst()
+ description.extend(child_lines)
+
+ self.render_title(description, indent, result)
+
+ result.extend(description[1:])
+
+ ensure_terminal_blank(result)
+ return result
+
+
+ def render_title(self, description, indent, result):
+ result.append(
+ "{indent}:returns: {first_description}".format(indent=indent,
+ first_description=description[0].lstrip()))
+
+
+
+class Warning(Node):
+
+ def __init__(self, indent):
+ super(Warning, self).__init__(indent=indent)
+
+ def __repr__(self):
+ return "Warning(" + repr(self.indent) + ", children=" + str(self.children) + ")"
+
+ def render_rst(self, *args, **kwargs):
+ # TODO: Factor out the commonality between this and Note below
+ result = []
+ indent = ' ' * self.indent
+
+ # Render the param description
+ description = [self.line] if self.line else []
+ for child in self.children:
+ child_lines = child.render_rst()
+ description.extend(child_lines)
+
+ # Fix the indent on the first line
+ if len(description) > 1 and len(description[1].strip()) != 0:
+ body_indent = len(description[1]) - len(description[1].strip())
+ else:
+ body_indent = self.indent + 4
+
+ if len(description) > 0:
+ description[0] = ' ' * body_indent + description[0]
+
+ result.append(indent + ".. warning::")
+ result.append(indent + '')
+ result.extend(description)
+
+ ensure_terminal_blank(result)
+ return result
+
+
+class Note(Node):
+
+ def __init__(self, indent):
+ super(Note, self).__init__(indent=indent)
+ self.line = ''
+
+
+ def __repr__(self):
+ return "Note(" + repr(self.indent) + ", children=" + str(self.children) + ")"
+
+
+ def render_rst(self, *args, **kwargs):
+ # TODO: Factor out the commonality between this and Warning above
+ result = []
+ indent = ' ' * self.indent
+
+ # Render the param description
+ description = [self.line] if self.line else []
+ for child in self.children:
+ child_lines = child.render_rst()
+ description.extend(child_lines)
+
+ # Fix the indent on the first line
+ if len(description) > 1 and len(description[1].strip()) != 0:
+ body_indent = len(description[1]) - len(description[1].strip())
+ else:
+ body_indent = self.indent + 4
+
+ if len(description) > 0:
+ description[0] = ' ' * body_indent + description[0]
+
+ result.append(indent + ".. note::")
+ result.append(indent + '')
+ result.extend(description)
+
+ ensure_terminal_blank(result)
+ return result
+
+
+def ensure_terminal_blank(result):
+ '''If the description didn't end with a blank line add one here.'''
+ if len(result) > 0:
+ if len(result[-1].strip()) != 0:
+ result.append('')
diff --git a/docs/sphinx/hieroglyph/test/__init__.py b/docs/sphinx/hieroglyph/test/__init__.py
new file mode 100644
index 000000000..fd249423f
--- /dev/null
+++ b/docs/sphinx/hieroglyph/test/__init__.py
@@ -0,0 +1,2 @@
+__author__ = 'rjs'
+ \ No newline at end of file
diff --git a/docs/sphinx/hieroglyph/test/test_comments.py b/docs/sphinx/hieroglyph/test/test_comments.py
new file mode 100644
index 000000000..d1a1453ee
--- /dev/null
+++ b/docs/sphinx/hieroglyph/test/test_comments.py
@@ -0,0 +1,586 @@
+import unittest
+
+from hieroglyph.hieroglyph import parse_hieroglyph_text
+from hieroglyph.errors import HieroglyphError
+
+class CommentTests(unittest.TestCase):
+
+ def test_comment1(self):
+ source = """Fetches rows from a Bigtable.
+ This is a continuation of the opening paragraph.
+
+ Retrieves rows pertaining to the given keys from the Table instance
+ represented by big_table. Silly things may happen if
+ other_silly_variable is not None.
+
+ Args:
+ big_table: An open Bigtable Table instance.
+ keys: A sequence of strings representing the key of each table row
+ to fetch.
+ other_silly_variable (str): Another optional variable, that has a much
+ longer name than the other args, and which does nothing.
+
+ Returns:
+ A dict mapping keys to the corresponding table row data
+ fetched. Each row is represented as a tuple of strings. For
+ example:
+
+ {'Serak': ('Rigel VII', 'Preparer'),
+ 'Zim': ('Irk', 'Invader'),
+ 'Lrrr': ('Omicron Persei 8', 'Emperor')}
+
+ If a key from the keys argument is missing from the dictionary,
+ then that row was not found in the table.
+
+ Raises:
+ IOError: An error occurred accessing the bigtable.Table object.
+ """
+
+ expected = """ Fetches rows from a Bigtable.
+ This is a continuation of the opening paragraph.
+
+ Retrieves rows pertaining to the given keys from the Table instance
+ represented by big_table. Silly things may happen if
+ other_silly_variable is not None.
+
+ :param big_table: An open Bigtable Table instance.
+
+ :param keys: A sequence of strings representing the key of each table row
+ to fetch.
+
+ :param other_silly_variable: Another optional variable, that has a much
+ longer name than the other args, and which does nothing.
+
+ :type other_silly_variable: str
+
+ :returns: A dict mapping keys to the corresponding table row data
+ fetched. Each row is represented as a tuple of strings. For
+ example:
+
+ {'Serak': ('Rigel VII', 'Preparer'),
+ 'Zim': ('Irk', 'Invader'),
+ 'Lrrr': ('Omicron Persei 8', 'Emperor')}
+
+ If a key from the keys argument is missing from the dictionary,
+ then that row was not found in the table.
+
+ :raises:
+ IOError - An error occurred accessing the bigtable.Table object.
+ """
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment2(self):
+ source = """Determine if all elements in the source sequence satisfy a condition.
+
+ All of the source sequence will be consumed.
+
+ Note: This method uses immediate execution.
+
+ Args:
+ predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+ Returns:
+ True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+ Raises:
+ ValueError: If the Queryable is closed()
+ TypeError: If predicate is not callable.
+ """
+
+ expected = """Determine if all elements in the source sequence satisfy a condition.
+
+ All of the source sequence will be consumed.
+
+ .. note::
+
+ This method uses immediate execution.
+
+ :param predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+ :returns: True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+ :raises:
+ * ValueError - If the Queryable is closed()
+
+ * TypeError - If predicate is not callable.
+ """
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment3(self):
+ source = """Determine if all elements in the source sequence satisfy a condition.
+
+ All of the source sequence will be consumed.
+
+ Note: This method uses immediate execution.
+
+ Args:
+ predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+ Returns:
+ True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+ Raises:
+ ValueError: If the Queryable is closed()
+ TypeError: If predicate is not callable.
+ """
+
+ expected = """Determine if all elements in the source sequence satisfy a condition.
+
+ All of the source sequence will be consumed.
+
+ .. note::
+
+ This method uses immediate execution.
+
+ :param predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+ :returns: True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+ :raises:
+ * ValueError - If the Queryable is closed()
+
+ * TypeError - If predicate is not callable.
+ """
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment4(self):
+ source_lines = [u'Determine if all elements in the source sequence satisfy a condition.',
+ u'',
+ u'All of the source sequence will be consumed.',
+ u'',
+ u'Note: This method uses immediate execution.',
+ u'',
+ u'Args:',
+ u' predicate: An optional single argument function used to test each',
+ u' elements. If omitted, the bool() function is used resulting in',
+ u' the elements being tested directly.',
+ u'',
+ u'Returns:',
+ u' True if all elements in the sequence meet the predicate condition,',
+ u' otherwise False.',
+ u'',
+ u'Raises:',
+ u' ValueError: If the Queryable is closed()',
+ u' TypeError: If predicate is not callable.',
+ u'']
+
+ expected = """Determine if all elements in the source sequence satisfy a condition.
+
+All of the source sequence will be consumed.
+
+.. note::
+
+ This method uses immediate execution.
+
+:param predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+:returns: True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+:raises:
+ * ValueError - If the Queryable is closed()
+
+ * TypeError - If predicate is not callable.
+
+"""
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment5(self):
+ source_lines = [u'An empty Queryable.',
+ u'',
+ u'Note: The same empty instance will be returned each time.',
+ u'',
+ u'Returns: A Queryable over an empty sequence.',
+ u'']
+
+ expected = """An empty Queryable.
+
+.. note::
+
+ The same empty instance will be returned each time.
+
+:returns: A Queryable over an empty sequence.
+
+"""
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment6(self):
+ source_lines = [u'A convenience factory for creating Records.',
+ u'',
+ u'Args:',
+ u' **kwargs: Each keyword argument will be used to initialise an',
+ u' attribute with the same name as the argument and the given',
+ u' value.',
+ u'',
+ u'Returns:',
+ u' A Record which has a named attribute for each of the keyword arguments.',
+ u'']
+
+ expected = """A convenience factory for creating Records.
+
+:param \*\*kwargs: Each keyword argument will be used to initialise an
+ attribute with the same name as the argument and the given
+ value.
+
+:returns: A Record which has a named attribute for each of the keyword arguments.
+
+"""
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment7(self):
+ source = """Projects each element of a sequence to an intermediate new sequence,
+ flattens the resulting sequences into one sequence and optionally
+ transforms the flattened sequence using a selector function.
+
+ Note: This method uses deferred execution.
+
+ Args:
+ collection_selector: A unary function mapping each element of the
+ source iterable into an intermediate sequence. The single
+ argument of the collection_selector is the value of an element
+ from the source sequence. The return value should be an
+ iterable derived from that element value. The default
+ collection_selector, which is the identity function, assumes
+ that each element of the source sequence is itself iterable.
+
+ result_selector: An optional unary function mapping the elements in
+ the flattened intermediate sequence to corresponding elements
+ of the result sequence. The single argument of the
+ result_selector is the value of an element from the flattened
+ intermediate sequence. The return value should be the
+ corresponding value in the result sequence. The default
+ result_selector is the identity function.
+
+ Returns:
+ A Queryable over a generated sequence whose elements are the result
+ of applying the one-to-many collection_selector to each element of
+ the source sequence, concatenating the results into an intermediate
+ sequence, and then mapping each of those elements through the
+ result_selector into the result sequence.
+
+ Raises:
+ ValueError: If this Queryable has been closed.
+ TypeError: If either collection_selector or result_selector are not
+ callable.
+ """
+
+ expected = """ Projects each element of a sequence to an intermediate new sequence,
+ flattens the resulting sequences into one sequence and optionally
+ transforms the flattened sequence using a selector function.
+
+ .. note::
+
+ This method uses deferred execution.
+
+ :param collection_selector: A unary function mapping each element of the
+ source iterable into an intermediate sequence. The single
+ argument of the collection_selector is the value of an element
+ from the source sequence. The return value should be an
+ iterable derived from that element value. The default
+ collection_selector, which is the identity function, assumes
+ that each element of the source sequence is itself iterable.
+
+ :param result_selector: An optional unary function mapping the elements in
+ the flattened intermediate sequence to corresponding elements
+ of the result sequence. The single argument of the
+ result_selector is the value of an element from the flattened
+ intermediate sequence. The return value should be the
+ corresponding value in the result sequence. The default
+ result_selector is the identity function.
+
+ :returns: A Queryable over a generated sequence whose elements are the result
+ of applying the one-to-many collection_selector to each element of
+ the source sequence, concatenating the results into an intermediate
+ sequence, and then mapping each of those elements through the
+ result_selector into the result sequence.
+
+ :raises:
+ * ValueError - If this Queryable has been closed.
+
+ * TypeError - If either collection_selector or result_selector are not
+ callable.
+ """
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment8(self):
+ source = """A convenience factory for creating Records.
+
+ Args:
+ **kwargs: Each keyword argument will be used to initialise an
+ attribute with the same name as the argument and the given
+ value.
+
+ Returns:
+ A Record which has a named attribute for each of the keyword arguments.
+ """
+
+ expected = """A convenience factory for creating Records.
+
+ :param \*\*kwargs: Each keyword argument will be used to initialise an
+ attribute with the same name as the argument and the given
+ value.
+
+ :returns: A Record which has a named attribute for each of the keyword arguments.
+
+"""
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment9(self):
+ source_lines = [u'Parse a single line of a tree to determine depth and node.',
+ u'',
+ u'Args:',
+ u' This line is missing an argument name.',
+ u' ',
+ u'Returns:',
+ u' A 2-tuple containing the tree 0 based tree depth as the first',
+ u' element and the node description as the second element.',
+ u'',
+ u'Raises:',
+ u' ValueError: If line does not have the expected form.',
+ u'']
+
+ self.assertRaises(HieroglyphError, lambda: parse_hieroglyph_text(source_lines))
+
+ def test_comment10(self):
+ source = """
+ Execute the command described by concatenating the string function arguments
+ with the p4 -s global scripting flag and return the results in a dictionary.
+
+ For example, to run the command::
+
+ p4 -s fstat -T depotFile foo.h
+
+ call::
+
+ p4('fstat', '-T', 'depotFile', 'foo.h')
+
+ Args:
+ args: The arguments to the p4 command as a list of objects which will
+ be converted to strings.
+
+ Returns:
+ A dictionary of lists where each key in the dictionary is the field name
+ from the command output, and each value is a list of output lines in
+ order.
+
+ Raises:
+ PerforceError: If the command could not be run or if the command
+ reported an error.
+ """
+
+ expected = """
+ Execute the command described by concatenating the string function arguments
+ with the p4 -s global scripting flag and return the results in a dictionary.
+
+ For example, to run the command::
+
+ p4 -s fstat -T depotFile foo.h
+
+ call::
+
+ p4('fstat', '-T', 'depotFile', 'foo.h')
+
+ :param args: The arguments to the p4 command as a list of objects which will
+ be converted to strings.
+
+ :returns: A dictionary of lists where each key in the dictionary is the field name
+ from the command output, and each value is a list of output lines in
+ order.
+
+ :raises:
+ PerforceError - If the command could not be run or if the command
+ reported an error.
+
+"""
+
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment11(self):
+ source = """Projects each element of a sequence to an intermediate new sequence,
+ flattens the resulting sequences into one sequence and optionally
+ transforms the flattened sequence using a selector function.
+
+ Warning: This method may explode at short notice.
+
+ Args:
+ collection_selector: A unary function mapping each element of the
+ source iterable into an intermediate sequence. The single
+ argument of the collection_selector is the value of an element
+ from the source sequence. The return value should be an
+ iterable derived from that element value. The default
+ collection_selector, which is the identity function, assumes
+ that each element of the source sequence is itself iterable.
+
+ result_selector: An optional unary function mapping the elements in
+ the flattened intermediate sequence to corresponding elements
+ of the result sequence. The single argument of the
+ result_selector is the value of an element from the flattened
+ intermediate sequence. The return value should be the
+ corresponding value in the result sequence. The default
+ result_selector is the identity function.
+
+ Returns:
+ A Queryable over a generated sequence whose elements are the result
+ of applying the one-to-many collection_selector to each element of
+ the source sequence, concatenating the results into an intermediate
+ sequence, and then mapping each of those elements through the
+ result_selector into the result sequence.
+
+ Raises:
+ ValueError: If this Queryable has been closed.
+ TypeError: If either collection_selector or result_selector are not
+ callable.
+ """
+
+ expected = """ Projects each element of a sequence to an intermediate new sequence,
+ flattens the resulting sequences into one sequence and optionally
+ transforms the flattened sequence using a selector function.
+
+ .. warning::
+
+ This method may explode at short notice.
+
+ :param collection_selector: A unary function mapping each element of the
+ source iterable into an intermediate sequence. The single
+ argument of the collection_selector is the value of an element
+ from the source sequence. The return value should be an
+ iterable derived from that element value. The default
+ collection_selector, which is the identity function, assumes
+ that each element of the source sequence is itself iterable.
+
+ :param result_selector: An optional unary function mapping the elements in
+ the flattened intermediate sequence to corresponding elements
+ of the result sequence. The single argument of the
+ result_selector is the value of an element from the flattened
+ intermediate sequence. The return value should be the
+ corresponding value in the result sequence. The default
+ result_selector is the identity function.
+
+ :returns: A Queryable over a generated sequence whose elements are the result
+ of applying the one-to-many collection_selector to each element of
+ the source sequence, concatenating the results into an intermediate
+ sequence, and then mapping each of those elements through the
+ result_selector into the result sequence.
+
+ :raises:
+ * ValueError - If this Queryable has been closed.
+
+ * TypeError - If either collection_selector or result_selector are not
+ callable.
+ """
+ source_lines = source.splitlines()
+ actual_lines = parse_hieroglyph_text(source_lines)
+ expected_lines = expected.splitlines()
+ self.assertEqual(len(actual_lines), len(expected_lines))
+ for actual_line, result_line in zip(actual_lines, expected_lines):
+ if len(actual_line.strip()) == 0:
+ self.assertTrue(len(result_line.strip()) == 0)
+ else:
+ self.assertEqual(actual_line, result_line)
+
+ def test_comment12(self):
+ source = """Determine if all elements in the source sequence satisfy a condition.
+
+ All of the source sequence will be consumed.
+
+ Note: This method uses immediate execution.
+
+ Args:
+ predicate: An optional single argument function used to test each
+ elements. If omitted, the bool() function is used resulting in
+ the elements being tested directly.
+
+ Returns:
+ True if all elements in the sequence meet the predicate condition,
+ otherwise False.
+
+ Raises:
+ This is not a proper exception description
+ """
+
+ source_lines = source.splitlines()
+ self.assertRaises(HieroglyphError, lambda: parse_hieroglyph_text(source_lines))
+
diff --git a/docs/sphinx/hieroglyph/test/test_hierglyph.py b/docs/sphinx/hieroglyph/test/test_hierglyph.py
new file mode 100644
index 000000000..42947cb0c
--- /dev/null
+++ b/docs/sphinx/hieroglyph/test/test_hierglyph.py
@@ -0,0 +1,264 @@
+import unittest
+from hieroglyph.hieroglyph import first_paragraph_indent, gather_lines, unindent
+
+__author__ = 'Robert Smallshire'
+
+class UnindentTests(unittest.TestCase):
+
+ def test_zero_lines(self):
+ source = []
+ expected = []
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+ def test_one_zero_indent_line(self):
+ source = ["First line"]
+ expected = [(0, "First line")]
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+ def test_two_zero_indent_lines(self):
+ source = ["First line",
+ "Second line"]
+ expected = [(0, "First line"),
+ (0, "Second line")]
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+ def test_two_indented_lines(self):
+ source = [" First line",
+ " Second line"]
+ expected = [(4, "First line"),
+ (6, "Second line")]
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+ def test_whitespace_line(self):
+ source = [" "]
+ expected = [(4, "")]
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+ def test_tab_line(self):
+ source = ["\tHello"]
+ expected = [(1, "Hello")]
+ actual = unindent(source)
+ self.assertEqual(actual, expected)
+
+
+class FirstParagraphIndentTests(unittest.TestCase):
+
+ def test_zero_lines(self):
+ source = []
+ expected = []
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_single_line_non_indented_comment(self):
+ source = [(0, "A single line comment")]
+ expected = [(0, "A single line comment")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_single_line_indented_comment(self):
+ source = [(4, "A single line comment")]
+ expected = [(4, "A single line comment")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_double_line_non_indented_comment(self):
+ source = [(0, "The first line"),
+ (0, "The second line")]
+ expected = [(0, "The first line"),
+ (0, "The second line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_double_line_indented_comment(self):
+ source = [(4, "The first line"),
+ (4, "The second line")]
+ expected = [(4, "The first line"),
+ (4, "The second line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_first_line_indent(self):
+ source = [(4, "The first line"),
+ (0, "The second line")]
+ expected = [(4, "The first line"),
+ (0, "The second line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_first_line_non_indent(self):
+ source = [(0, "The first line"),
+ (4, "The second line")]
+ expected = [(4, "The first line"),
+ (4, "The second line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_increasing_indent(self):
+ source = [(0, "The first line"),
+ (4, "The second line"),
+ (8, "The third line")]
+ expected = [(4, "The first line"),
+ (4, "The second line"),
+ (8, "The third line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_separate_paragraphs(self):
+ source = [(0, "This is the first paragraph"),
+ (0, ""),
+ (4, "This is the second paragraph")]
+ expected = [(0, "This is the first paragraph"),
+ (0, ""),
+ (4, "This is the second paragraph")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_separate_paragraphs_indented(self):
+ source = [(4, "This is the first paragraph"),
+ (4, ""),
+ (8, "This is the second paragraph")]
+ expected = [(4, "This is the first paragraph"),
+ (4, ""),
+ (8, "This is the second paragraph")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_separated_lines_first_line_non_indented(self):
+ source = [(0, "The first line"),
+ (0, ""),
+ (4, "The third line")]
+ expected = [(0, "The first line"),
+ (0, ""),
+ (4, "The third line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+ def test_separated_lines_first_line_indented(self):
+ source = [(4, "The first line"),
+ (4, ""),
+ (4, "The third line")]
+ expected = [(4, "The first line"),
+ (4, ""),
+ (4, "The third line")]
+ actual = first_paragraph_indent(source)
+ self.assertEqual(actual, expected)
+
+class GatherLinesTests(unittest.TestCase):
+
+ def test_empty(self):
+ source = []
+ expected = []
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_one_liner(self):
+ source = [(0, 'One liner')]
+ expected = [(0, ['One liner'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_two_liner(self):
+ source = [(0, 'First line'),
+ (0, 'Second line')]
+ expected = [(0, ['First line',
+ 'Second line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_separated_lines(self):
+ source = [(0, 'First line'),
+ (0, ''),
+ (0, 'Third line')]
+ expected = [(0, ['First line',
+ '']),
+ (0, ['Third line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_separated_multi_lines(self):
+ source = [(0, 'First line'),
+ (0, 'Second line'),
+ (0, ''),
+ (0, 'Fourth line'),
+ (0, 'Fifth line')]
+ expected = [(0, ['First line',
+ 'Second line',
+ '']),
+ (0, ['Fourth line',
+ 'Fifth line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+
+ def test_indented_lines(self):
+ source = [(0, 'First line'),
+ (4, 'Second line')]
+ expected = [(0, ['First line']),
+ (4, ['Second line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_dedented_lines(self):
+ source = [(4, 'First line'),
+ (0, 'Second line')]
+ expected = [(4, ['First line']),
+ (0, ['Second line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_indented_multi_lines(self):
+ source = [(0, 'First line'),
+ (0, 'Second line'),
+ (4, 'Third line'),
+ (4, 'Fourth line')]
+ expected = [(0, ['First line',
+ 'Second line']),
+ (4, ['Third line',
+ 'Fourth line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_dedented_multi_lines(self):
+ source = [(4, 'First line'),
+ (4, 'Second line'),
+ (0, 'Third line'),
+ (0, 'Fourth line')]
+ expected = [(4, ['First line',
+ 'Second line']),
+ (0, ['Third line',
+ 'Fourth line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_indented_separated_multi_lines(self):
+ source = [(0, 'First line'),
+ (0, 'Second line'),
+ (0, ''),
+ (4, 'Fourth line'),
+ (4, 'Fifth line')]
+ expected = [(0, ['First line',
+ 'Second line',
+ '']),
+ (4, ['Fourth line',
+ 'Fifth line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
+
+ def test_dedented_separated_multi_lines(self):
+ source = [(4, 'First line'),
+ (4, 'Second line'),
+ (4, ''),
+ (0, 'Fourth line'),
+ (0, 'Fifth line')]
+ expected = [(4, ['First line',
+ 'Second line',
+ '']),
+ (0, ['Fourth line',
+ 'Fifth line'])]
+ actual = gather_lines(source)
+ self.assertEqual(actual, expected)
diff --git a/docs/sphinx/hieroglyph/test/test_nodes.py b/docs/sphinx/hieroglyph/test/test_nodes.py
new file mode 100644
index 000000000..4cc17b477
--- /dev/null
+++ b/docs/sphinx/hieroglyph/test/test_nodes.py
@@ -0,0 +1,386 @@
+import unittest
+from hieroglyph.nodes import Node, Arg, Raises, Except, Returns, Warning, Note
+
+__author__ = 'Robert Smallshire'
+
+class NodeTests(unittest.TestCase):
+
+ def test_create_default_node(self):
+ node = Node()
+ self.assertEqual(node.indent, 0)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_create_with_indent(self):
+ node = Node(indent=4)
+ self.assertEqual(node.indent, 4)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_create_with_lines(self):
+ node = Node(lines= ['First', 'Second', 'Third'])
+ self.assertEqual(node.indent, 0)
+ self.assertEqual(node.lines, ['First', 'Second', 'Third'])
+ self.assertIsNone(node.parent)
+
+ def test_repr(self):
+ node = Node(5, ['One', 'Two', 'Three'])
+ actual = repr(node)
+ expected = "Node(5, ['One', 'Two', 'Three'], children=[])"
+ self.assertEqual(expected, actual)
+
+ def test_add_one_child(self):
+ node = Node()
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Node()
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_render_rst_empty(self):
+ node = Node()
+ rst = node.render_rst()
+ self.assertEqual(len(rst), 0)
+
+ def test_render_rst_indent(self):
+ node = Node(indent=4)
+ rst = node.render_rst()
+ self.assertEqual(len(rst), 0)
+
+ def test_render_rst_lines(self):
+ node = Node(lines= ['First',
+ 'Second',
+ 'Third'])
+ rst = node.render_rst()
+ self.assertEqual(rst, ['First',
+ 'Second',
+ 'Third'])
+
+ def test_render_rst_indented_lines(self):
+ node = Node(indent=3, lines= ['First',
+ 'Second',
+ 'Third'])
+ rst = node.render_rst()
+ self.assertEqual(rst, [' First',
+ ' Second',
+ ' Third'])
+
+ def test_render_rst_with_child(self):
+ node = Node(indent=4, lines=["Parent"])
+ child = Node(indent=8, lines=["Child"], parent=node)
+ node.add_child(child)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' Parent',
+ ' Child'])
+
+ def test_render_rst_with_children(self):
+ node = Node(indent=4, lines=["Parent"])
+ child_a = Node(indent=8, lines=["ChildA"], parent=node)
+ node.add_child(child_a)
+ child_b = Node(indent=6, lines=["ChildB"], parent=node)
+ node.add_child(child_b)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' Parent',
+ ' ChildA',
+ ' ChildB'])
+
+
+class ArgTests(unittest.TestCase):
+
+ def test_create(self):
+ node = Arg(5, 10, 'foo')
+ self.assertEqual(node.indent, 5)
+ self.assertEqual(node.child_indent, 10)
+ self.assertEqual(node.name, 'foo')
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_set_type(self):
+ node = Arg(5, 10, 'foo')
+ node.type = 'str'
+ self.assertEqual(node.type, 'str')
+
+ def test_add_one_child(self):
+ node = Arg(5, 10, 'foo')
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Arg(5, 10, 'foo')
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_repr(self):
+ node = Arg(5, 10, 'foo')
+ actual = repr(node)
+ expected = "Arg('foo', None, children=[])"
+ self.assertEqual(expected, actual)
+
+ def test_render_rst_empty(self):
+ node = Arg(5, 10, 'bar')
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :param bar: ',
+ ''])
+
+ def test_render_rst_with_child(self):
+ node = Arg(5, 10, 'bar')
+ child = Node(indent=10, lines=["Description"], parent=node)
+ node.add_child(child)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :param bar: Description',
+ ''])
+
+ def test_render_rst_with_children(self):
+ node = Arg(5, 10, 'bar')
+ child_a = Node(indent=10, lines=["ChildA"], parent=node)
+ node.add_child(child_a)
+ child_b = Node(indent=10, lines=["ChildB"], parent=node)
+ node.add_child(child_b)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :param bar: ChildA',
+ ' ChildB',
+ ''])
+
+ def test_render_rst_with_type(self):
+ node = Arg(5, 10, 'bar')
+ node.type = 'str'
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :param bar: ',
+ ' :type bar: str',
+ ''])
+
+
+class RaisesTests(unittest.TestCase):
+
+ def test_create_default_node(self):
+ node = Raises()
+ self.assertEqual(node.indent, 0)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_create_with_indent(self):
+ node = Raises(indent=4)
+ self.assertEqual(node.indent, 4)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_repr(self):
+ node = Raises(5)
+ actual = repr(node)
+ expected = "Raises(5, children=[])"
+ self.assertEqual(expected, actual)
+
+ def test_add_one_child(self):
+ node = Raises()
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Raises()
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_render_rst_empty(self):
+ node = Raises()
+ rst = node.render_rst()
+ self.assertEqual(rst, [':raises:',
+ ''])
+
+ def test_render_rst_indent(self):
+ node = Raises(indent=5)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :raises:',
+ ''])
+
+ def test_render_rst_with_child(self):
+ node = Raises(5)
+ child = Node(indent=10, lines=["Description"], parent=node)
+ node.add_child(child)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :raises:',
+ ' Description',
+ ''])
+
+ def test_render_rst_with_children(self):
+ node = Raises(5)
+ child_a = Node(indent=10, lines=["ChildA"], parent=node)
+ node.add_child(child_a)
+ child_b = Node(indent=10, lines=["ChildB"], parent=node)
+ node.add_child(child_b)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' :raises:',
+ ' ChildA',
+ ' ChildB',
+ ''])
+
+
+class ExceptTests(unittest.TestCase):
+
+ def test_create(self):
+ node = Except(5, 'FooError')
+ self.assertEqual(node.indent, 5)
+ self.assertEqual(node.type, 'FooError')
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_add_one_child(self):
+ node = Except(5, 'FooError')
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Except(5, 'FooError')
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_repr(self):
+ node = Except(5,'FooError')
+ actual = repr(node)
+ expected = "Except('FooError', children=[])"
+ self.assertEqual(expected, actual)
+
+ def test_render_rst_empty(self):
+ node = Except(5, 'FooError')
+ rst = node.render_rst()
+ self.assertEqual(rst, [' * FooError - ',
+ ''])
+
+ def test_render_rst_indent(self):
+ node = Except(5, 'FooError')
+ rst = node.render_rst()
+ self.assertEqual(rst, [' * FooError - ',
+ ''])
+
+ def test_render_rst_with_child(self):
+ node = Except(5, 'FooError')
+ child = Node(indent=10, lines=["Description"], parent=node)
+ node.add_child(child)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' * FooError - Description',
+ ''])
+
+ def test_render_rst_with_children(self):
+ node = Except(5, 'FooError')
+ child_a = Node(indent=10, lines=["ChildA"], parent=node)
+ node.add_child(child_a)
+ child_b = Node(indent=10, lines=["ChildB"], parent=node)
+ node.add_child(child_b)
+ rst = node.render_rst()
+ self.assertEqual(rst, [' * FooError - ChildA',
+ ' ChildB',
+ ''])
+
+class ReturnsTests(unittest.TestCase):
+
+ def test_create(self):
+ node = Returns(5)
+ self.assertEqual(node.indent, 5)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_add_one_child(self):
+ node = Returns(5)
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Returns(5)
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_repr(self):
+ node = Returns(5)
+ actual = repr(node)
+ expected = "Returns(5, children=[])"
+ self.assertEqual(expected, actual)
+
+ # TODO test_render_rst
+
+class WarningTests(unittest.TestCase):
+
+ def test_create(self):
+ node = Warning(5)
+ self.assertEqual(node.indent, 5)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_add_one_child(self):
+ node = Warning(5)
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Warning(5)
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_repr(self):
+ node = Warning(5)
+ actual = repr(node)
+ expected = "Warning(5, children=[])"
+ self.assertEqual(expected, actual)
+
+ # TODO test_render_rst
+
+class NoteTests(unittest.TestCase):
+
+ def test_create(self):
+ node = Note(5)
+ self.assertEqual(node.indent, 5)
+ self.assertEqual(node.lines, [])
+ self.assertIsNone(node.parent)
+
+ def test_add_one_child(self):
+ node = Note(5)
+ child = Node(parent=node)
+ node.add_child(child)
+ self.assertIs(node.children[0], child)
+
+ def test_add_two_children(self):
+ node = Note(5)
+ child0 = Node(parent=node)
+ child1 = Node(parent=node)
+ node.add_child(child0)
+ node.add_child(child1)
+ self.assertIs(node.children[0], child0)
+ self.assertIs(node.children[1], child1)
+
+ def test_repr(self):
+ node = Note(5)
+ actual = repr(node)
+ expected = "Note(5, children=[])"
+ self.assertEqual(expected, actual)
+
+ # TODO test_render_rst
diff --git a/docs/sphinx/hieroglyph/version.py b/docs/sphinx/hieroglyph/version.py
new file mode 100644
index 000000000..d060125c0
--- /dev/null
+++ b/docs/sphinx/hieroglyph/version.py
@@ -0,0 +1,3 @@
+'''Specification of the hieroglyph version'''
+
+__version__ = '0.6'