Repository: boto/botocore
Pull request #566 (2015-06-17 19:55:47)

Port bcdoc
This pull request pulls bcdoc into botocore. I also removed bcdoc as a dependency. I installed botocore in a clean virtualenv and the test pass fine. I will also have subsequent pull requests for Boto3 and the CLI.

cc @jamesls @mtdowling 


Patch:
diff --git a/botocore/docs/bcdoc/__init__.py b/botocore/docs/bcdoc/__init__.py
new file mode 100644
index 000000000..b687f69da
--- /dev/null
+++ b/botocore/docs/bcdoc/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+__version__ = '0.16.0'
diff --git a/botocore/docs/bcdoc/docevents.py b/botocore/docs/bcdoc/docevents.py
new file mode 100644
index 000000000..f5c6c8493
--- /dev/null
+++ b/botocore/docs/bcdoc/docevents.py
@@ -0,0 +1,103 @@
+# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+
+
+DOC_EVENTS = {
+    'doc-breadcrumbs': '.%s',
+    'doc-title': '.%s',
+    'doc-description': '.%s',
+    'doc-synopsis-start': '.%s',
+    'doc-synopsis-option': '.%s.%s',
+    'doc-synopsis-end': '.%s',
+    'doc-options-start': '.%s',
+    'doc-option': '.%s.%s',
+    'doc-option-example': '.%s.%s',
+    'doc-options-end': '.%s',
+    'doc-examples': '.%s',
+    'doc-output': '.%s',
+    'doc-subitems-start': '.%s',
+    'doc-subitem': '.%s.%s',
+    'doc-subitems-end': '.%s',
+    'doc-relateditems-start': '.%s',
+    'doc-relateditem': '.%s.%s',
+    'doc-relateditems-end': '.%s'
+    }
+
+
+def generate_events(session, help_command):
+    # Now generate the documentation events
+    session.emit('doc-breadcrumbs.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-title.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-description.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-synopsis-start.%s' % help_command.event_class,
+                 help_command=help_command)
+    if help_command.arg_table:
+        for arg_name in help_command.arg_table:
+            # An argument can set an '_UNDOCUMENTED' attribute
+            # to True to indicate a parameter that exists
+            # but shouldn't be documented.  This can be used
+            # for backwards compatibility of deprecated arguments.
+            if getattr(help_command.arg_table[arg_name],
+                       '_UNDOCUMENTED', False):
+                continue
+            session.emit(
+                'doc-synopsis-option.%s.%s' % (help_command.event_class,
+                                               arg_name),
+                arg_name=arg_name, help_command=help_command)
+    session.emit('doc-synopsis-end.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-options-start.%s' % help_command.event_class,
+                 help_command=help_command)
+    if help_command.arg_table:
+        for arg_name in help_command.arg_table:
+            if getattr(help_command.arg_table[arg_name],
+                       '_UNDOCUMENTED', False):
+                continue
+            session.emit('doc-option.%s.%s' % (help_command.event_class,
+                                               arg_name),
+                         arg_name=arg_name, help_command=help_command)
+            session.emit('doc-option-example.%s.%s' %
+                         (help_command.event_class, arg_name),
+                         arg_name=arg_name, help_command=help_command)
+    session.emit('doc-options-end.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-subitems-start.%s' % help_command.event_class,
+                 help_command=help_command)
+    if help_command.command_table:
+        for command_name in sorted(help_command.command_table.keys()):
+            if hasattr(help_command.command_table[command_name],
+                       '_UNDOCUMENTED'):
+                continue
+            session.emit('doc-subitem.%s.%s'
+                         % (help_command.event_class, command_name),
+                         command_name=command_name,
+                         help_command=help_command)
+    session.emit('doc-subitems-end.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-examples.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-output.%s' % help_command.event_class,
+                 help_command=help_command)
+    session.emit('doc-relateditems-start.%s' % help_command.event_class,
+                 help_command=help_command)
+    if help_command.related_items:
+        for related_item in sorted(help_command.related_items):
+            session.emit('doc-relateditem.%s.%s'
+                         % (help_command.event_class, related_item),
+                         help_command=help_command,
+                         related_item=related_item)
+    session.emit('doc-relateditems-end.%s' % help_command.event_class,
+                 help_command=help_command)
diff --git a/botocore/docs/bcdoc/docstringparser.py b/botocore/docs/bcdoc/docstringparser.py
new file mode 100644
index 000000000..d00471817
--- /dev/null
+++ b/botocore/docs/bcdoc/docstringparser.py
@@ -0,0 +1,51 @@
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.compat import six
+from six.moves import html_parser
+
+
+class DocStringParser(html_parser.HTMLParser):
+    """
+    A simple HTML parser.  Focused on converting the subset of HTML
+    that appears in the documentation strings of the JSON models into
+    simple ReST format.
+    """
+
+    def __init__(self, doc):
+        html_parser.HTMLParser.__init__(self)
+        self.doc = doc
+        self.unhandled_tags = []
+
+    def handle_starttag(self, tag, attrs):
+        handler_name = 'start_%s' % tag
+        if hasattr(self.doc.style, handler_name):
+            getattr(self.doc.style, handler_name)(attrs)
+        else:
+            self.unhandled_tags.append(tag)
+
+    def handle_endtag(self, tag):
+        handler_name = 'end_%s' % tag
+        if hasattr(self.doc.style, handler_name):
+            getattr(self.doc.style, handler_name)()
+
+    def handle_data(self, data):
+        if data.isspace():
+            data = ' '
+        else:
+            end_space = data[-1].isspace()
+            words = data.split()
+            words = self.doc.translate_words(words)
+            data = ' '.join(words)
+            if end_space:
+                data += ' '
+        self.doc.handle_data(data)
diff --git a/botocore/docs/bcdoc/restdoc.py b/botocore/docs/bcdoc/restdoc.py
new file mode 100644
index 000000000..96566a990
--- /dev/null
+++ b/botocore/docs/bcdoc/restdoc.py
@@ -0,0 +1,199 @@
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+import logging
+
+from botocore.compat import OrderedDict
+from botocore.docs.bcdoc.docstringparser import DocStringParser
+from botocore.docs.bcdoc.style import ReSTStyle
+
+LOG = logging.getLogger('bcdocs')
+
+
+class ReSTDocument(object):
+
+    def __init__(self, target='man'):
+        self.style = ReSTStyle(self)
+        self.target = target
+        self.parser = DocStringParser(self)
+        self.keep_data = True
+        self.do_translation = False
+        self.translation_map = {}
+        self.hrefs = {}
+        self._writes = []
+        self._last_doc_string = None
+
+    def _write(self, s):
+        if self.keep_data and s is not None:
+            self._writes.append(s)
+
+    def write(self, content):
+        """
+        Write content into the document.
+        """
+        self._write(content)
+
+    def writeln(self, content):
+        """
+        Write content on a newline.
+        """
+        self._write('%s%s\n' % (self.style.spaces(), content))
+
+    def peek_write(self):
+        """
+        Returns the last content written to the document without
+        removing it from the stack.
+        """
+        return self._writes[-1]
+
+    def pop_write(self):
+        """
+        Removes and returns the last content written to the stack.
+        """
+        return self._writes.pop()
+
+    def push_write(self, s):
+        """
+        Places new content on the stack.
+        """
+        self._writes.append(s)
+
+    def getvalue(self):
+        """
+        Returns the current content of the document as a string.
+        """
+        if self.hrefs:
+            self.style.new_paragraph()
+            for refname, link in self.hrefs.items():
+                self.style.link_target_definition(refname, link)
+        return ''.join(self._writes).encode('utf-8')
+
+    def translate_words(self, words):
+        return [self.translation_map.get(w, w) for w in words]
+
+    def handle_data(self, data):
+        if data and self.keep_data:
+            self._write(data)
+
+    def include_doc_string(self, doc_string):
+        if doc_string:
+            try:
+                start = len(self._writes)
+                self.parser.feed(doc_string)
+                end = len(self._writes)
+                self._last_doc_string = (start, end)
+            except Exception:
+                LOG.debug('Error parsing doc string', exc_info=True)
+                LOG.debug(doc_string)
+
+    def remove_last_doc_string(self):
+        # Removes all writes inserted by last doc string
+        if self._last_doc_string is not None:
+            start, end = self._last_doc_string
+            del self._writes[start:end]
+
+
+class DocumentStructure(ReSTDocument):
+    def __init__(self, name, section_names=None, target='man'):
+        """Provides a Hierarichial structure to a ReSTDocument
+
+        You can write to it similiar to as you can to a ReSTDocument but
+        has an innate structure for more orginaztion and abstraction.
+
+        :param name: The name of the document
+        :param section_names: A list of sections to be included
+            in the document.
+        :parma target: The target documentation of the Document structure
+        """
+        super(DocumentStructure, self).__init__(target=target)
+        self._name = name
+        self._structure = OrderedDict()
+        self._path = [self._name]
+        if section_names is not None:
+            self._generate_structure(section_names)
+
+    @property
+    def name(self):
+        """The name of the document structure"""
+        return self._name
+
+    @property
+    def path(self):
+        """
+        A list of where to find a particular document structure in the
+        overlying document structure.
+        """
+        return self._path
+
+    @path.setter
+    def path(self, value):
+        self._path = value
+
+    @property
+    def available_sections(self):
+        return list(self._structure)
+
+    def _generate_structure(self, section_names):
+        for section_name in section_names:
+            self.add_new_section(section_name)
+
+    def add_new_section(self, name):
+        """Adds a new section to the current document structure
+
+        This document structure will be considered a section to the
+        current document structure but will in itself be an entirely
+        new document structure that can be written to and have sections
+        as well
+
+        :param name: The name of the section.
+        :rtype: DocumentStructure
+        :returns: A new document structure to add to but lives as a section
+            to the document structure it was instantiated from.
+        """
+        # Add a new section
+        section = self.__class__(name=name, target=self.target)
+        section.path = self.path + [name]
+        # Indent the section apporpriately as well
+        section.style.indentation = self.style.indentation
+        section.translation_map = self.translation_map
+        section.hrefs = self.hrefs
+        self._structure[name] = section
+        return section
+
+    def get_section(self, name):
+        """Retrieve a section"""
+        return self._structure[name]
+
+    def delete_section(self, name):
+        """Delete a section"""
+        del self._structure[name]
+
+    def flush_structure(self):
+        """Flushes a doc structure to a ReSTructed string
+
+        The document is flushed out in a DFS style where sections and their
+        subsections' values are added to the string as they are visited.
+        """
+        # We are at the root flush the links at the beginning of the
+        # document
+        if len(self.path) == 1:
+            if self.hrefs:
+                self.style.new_paragraph()
+                for refname, link in self.hrefs.items():
+                    self.style.link_target_definition(refname, link)
+        value = self.getvalue()
+        for name, section in self._structure.items():
+            value += section.flush_structure()
+        return value
+
+    def getvalue(self):
+        return ''.join(self._writes).encode('utf-8')
diff --git a/botocore/docs/bcdoc/style.py b/botocore/docs/bcdoc/style.py
new file mode 100644
index 000000000..6e30f0007
--- /dev/null
+++ b/botocore/docs/bcdoc/style.py
@@ -0,0 +1,368 @@
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+
+import logging
+
+logger = logging.getLogger('bcdocs')
+
+
+class BaseStyle(object):
+
+    def __init__(self, doc, indent_width=2):
+        self.doc = doc
+        self.indent_width = indent_width
+        self._indent = 0
+        self.keep_data = True
+
+    @property
+    def indentation(self):
+        return self._indent
+
+    @indentation.setter
+    def indentation(self, value):
+        self._indent = value
+
+    def new_paragraph(self):
+        return '\n%s' % self.spaces()
+
+    def indent(self):
+        self._indent += 1
+
+    def dedent(self):
+        if self._indent > 0:
+            self._indent -= 1
+
+    def spaces(self):
+        return ' ' * (self._indent * self.indent_width)
+
+    def bold(self, s):
+        return s
+
+    def ref(self, link, title=None):
+        return link
+
+    def h2(self, s):
+        return s
+
+    def h3(self, s):
+        return s
+
+    def underline(self, s):
+        return s
+
+    def italics(self, s):
+        return s
+
+
+class ReSTStyle(BaseStyle):
+
+    def __init__(self, doc, indent_width=2):
+        BaseStyle.__init__(self, doc, indent_width)
+        self.do_p = True
+        self.a_href = None
+
+    def new_paragraph(self):
+        if self.do_p:
+            self.doc.write('\n\n%s' % self.spaces())
+
+    def new_line(self):
+        if self.do_p:
+            self.doc.write('\n%s' % self.spaces())
+
+    def _start_inline(self, markup):
+        self.doc.write(markup)
+
+    def _end_inline(self, markup):
+        # Sometimes the HTML markup has whitespace between the end
+        # of the text inside the inline markup and the closing element
+        # (e.g. <b>foobar </b>).  This trailing space will cause
+        # problems in the ReST inline markup so we remove it here
+        # by popping the last item written off the stack, striping
+        # the whitespace and then pushing it back on the stack.
+        last_write = self.doc.pop_write()
+        self.doc.push_write(last_write.rstrip(' '))
+        self.doc.write(markup + ' ')
+
+    def start_bold(self, attrs=None):
+        self._start_inline('**')
+
+    def end_bold(self):
+        self._end_inline('**')
+
+    def start_b(self, attrs=None):
+        self.doc.do_translation = True
+        self.start_bold(attrs)
+
+    def end_b(self):
+        self.doc.do_translation = False
+        self.end_bold()
+
+    def bold(self, s):
+        if s:
+            self.start_bold()
+            self.doc.write(s)
+            self.end_bold()
+
+    def ref(self, title, link=None):
+        if link is None:
+            link = title
+        self.doc.write(':doc:`%s <%s>`' % (title, link))
+
+    def _heading(self, s, border_char):
+        border = border_char * len(s)
+        self.new_paragraph()
+        self.doc.write('%s\n%s\n%s' % (border, s, border))
+        self.new_paragraph()
+
+    def h1(self, s):
+        self._heading(s, '*')
+
+    def h2(self, s):
+        self._heading(s, '=')
+
+    def h3(self, s):
+        self._heading(s, '-')
+
+    def start_italics(self, attrs=None):
+        self._start_inline('*')
+
+    def end_italics(self):
+        self._end_inline('*')
+
+    def italics(self, s):
+        if s:
+            self.start_italics()
+            self.doc.write(s)
+            self.end_italics()
+
+    def start_p(self, attrs=None):
+        if self.do_p:
+            self.doc.write('\n\n%s' % self.spaces())
+
+    def end_p(self):
+        if self.do_p:
+            self.doc.write('\n\n')
+
+    def start_code(self, attrs=None):
+        self.doc.do_translation = True
+        self._start_inline('``')
+
+    def end_code(self):
+        self.doc.do_translation = False
+        self._end_inline('``')
+
+    def code(self, s):
+        if s:
+            self.start_code()
+            self.doc.write(s)
+            self.end_code()
+
+    def start_note(self, attrs=None):
+        self.new_paragraph()
+        self.doc.write('.. note::')
+        self.indent()
+        self.new_paragraph()
+
+    def end_note(self):
+        self.dedent()
+        self.new_paragraph()
+
+    def start_important(self, attrs=None):
+        self.new_paragraph()
+        self.doc.write('.. warning::')
+        self.indent()
+        self.new_paragraph()
+
+    def end_important(self):
+        self.dedent()
+        self.new_paragraph()
+
+    def start_a(self, attrs=None):
+        if attrs:
+            for attr_key, attr_value in attrs:
+                if attr_key == 'href':
+                    self.a_href = attr_value
+                    self.doc.write('`')
+        else:
+            # There are some model documentation that
+            # looks like this: <a>DescribeInstances</a>.
+            # In this case we just write out an empty
+            # string.
+            self.doc.write(' ')
+        self.doc.do_translation = True
+
+    def link_target_definition(self, refname, link):
+        self.doc.writeln('.. _%s: %s' % (refname, link))
+
+    def sphinx_reference_label(self, label, text=None):
+        if text is None:
+            text = label
+        if self.doc.target == 'html':
+            self.doc.write(':ref:`%s <%s>`' % (text, label))
+        else:
+            self.doc.write(text)
+
+    def end_a(self):
+        self.doc.do_translation = False
+        if self.a_href:
+            last_write = self.doc.pop_write()
+            last_write = last_write.rstrip(' ')
+            if last_write and last_write != '`':
+                if ':' in last_write:
+                    last_write = last_write.replace(':', r'\:')
+                self.doc.push_write(last_write)
+                self.doc.hrefs[last_write] = self.a_href
+                self.doc.write('`_')
+            elif last_write == '`':
+                # Look at start_a().  It will do a self.doc.write('`')
+                # which is the start of the link title.  If that is the
+                # case then there was no link text.  We should just
+                # use an inline link.  The syntax of this is
+                # `<http://url>`_
+                self.doc.push_write('`<%s>`_' % self.a_href)
+            else:
+                self.doc.push_write(self.a_href)
+                self.doc.hrefs[self.a_href] = self.a_href
+                self.doc.write('`_')
+            self.a_href = None
+        self.doc.write(' ')
+
+    def start_i(self, attrs=None):
+        self.doc.do_translation = True
+        self.start_italics()
+
+    def end_i(self):
+        self.doc.do_translation = False
+        self.end_italics()
+
+    def start_li(self, attrs=None):
+        self.new_line()
+        self.do_p = False
+        self.doc.write('* ')
+
+    def end_li(self):
+        self.do_p = True
+        self.new_line()
+
+    def li(self, s):
+        if s:
+            self.start_li()
+            self.doc.writeln(s)
+            self.end_li()
+
+    def start_ul(self, attrs=None):
+        self.new_paragraph()
+
+    def end_ul(self):
+        self.new_paragraph()
+
+    def start_ol(self, attrs=None):
+        # TODO: Need to control the bullets used for LI items
+        self.new_paragraph()
+
+    def end_ol(self):
+        self.new_paragraph()
+
+    def start_examples(self, attrs=None):
+        self.doc.keep_data = False
+
+    def end_examples(self):
+        self.doc.keep_data = True
+
+    def start_fullname(self, attrs=None):
+        self.doc.keep_data = False
+
+    def end_fullname(self):
+        self.doc.keep_data = True
+
+    def start_codeblock(self, attrs=None):
+        self.doc.write('::')
+        self.indent()
+        self.new_paragraph()
+
+    def end_codeblock(self):
+        self.dedent()
+        self.new_paragraph()
+
+    def codeblock(self, code):
+        """
+        Literal code blocks are introduced by ending a paragraph with
+        the special marker ::.  The literal block must be indented
+        (and, like all paragraphs, separated from the surrounding
+        ones by blank lines).
+        """
+        self.start_codeblock()
+        self.doc.writeln(code)
+        self.end_codeblock()
+
+    def toctree(self):
+        if self.doc.target == 'html':
+            self.doc.write('\n.. toctree::\n')
+            self.doc.write('  :maxdepth: 1\n')
+            self.doc.write('  :titlesonly:\n\n')
+        else:
+            self.start_ul()
+
+    def tocitem(self, item, file_name=None):
+        if self.doc.target == 'man':
+            self.li(item)
+        else:
+            if file_name:
+                self.doc.writeln('  %s' % file_name)
+            else:
+                self.doc.writeln('  %s' % item)
+
+    def hidden_toctree(self):
+        if self.doc.target == 'html':
+            self.doc.write('\n.. toctree::\n')
+            self.doc.write('  :maxdepth: 1\n')
+            self.doc.write('  :hidden:\n\n')
+
+    def hidden_tocitem(self, item):
+        if self.doc.target == 'html':
+            self.tocitem(item)
+
+    def table_of_contents(self, title=None, depth=None):
+        self.doc.write('.. contents:: ')
+        if title is not None:
+            self.doc.writeln(title)
+        if depth is not None:
+            self.doc.writeln('   :depth: %s' % depth)
+
+    def start_sphinx_py_class(self, class_name):
+        self.new_paragraph()
+        self.doc.write('.. py:class:: %s' % class_name)
+        self.indent()
+        self.new_paragraph()
+
+    def end_sphinx_py_class(self):
+        self.dedent()
+        self.new_paragraph()
+
+    def start_sphinx_py_method(self, method_name, parameters=None):
+        self.new_paragraph()
+        content = '.. py:method:: %s' % method_name
+        if parameters is not None:
+            content += '(%s)' % parameters
+        self.doc.write(content)
+        self.indent()
+        self.new_paragraph()
+
+    def end_sphinx_py_method(self):
+        self.dedent()
+        self.new_paragraph()
+
+    def write_py_doc_string(self, docstring):
+        docstring_lines = docstring.splitlines()
+        for docstring_line in docstring_lines:
+            self.doc.writeln(docstring_line)
diff --git a/botocore/docs/bcdoc/textwriter.py b/botocore/docs/bcdoc/textwriter.py
new file mode 100644
index 000000000..82f1d51e3
--- /dev/null
+++ b/botocore/docs/bcdoc/textwriter.py
@@ -0,0 +1,727 @@
+# -*- coding: utf-8 -*-
+"""
+
+    Custom docutils writer for plain text.
+    Based heavily on the Sphinx text writer.  See copyright below.
+
+    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+
+"""
+import os
+import re
+import textwrap
+
+from docutils import nodes, writers
+
+
+class TextWrapper(textwrap.TextWrapper):
+    """Custom subclass that uses a different word separator regex."""
+
+    wordsep_re = re.compile(
+        r'(\s+|'                                  # any whitespace
+        r'(?<=\s)(?::[a-z-]+:)?`\S+|'             # interpreted text start
+        r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|'   # hyphenated words
+        r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))')   # em-dash
+
+
+MAXWIDTH = 70
+STDINDENT = 3
+
+
+def my_wrap(text, width=MAXWIDTH, **kwargs):
+    w = TextWrapper(width=width, **kwargs)
+    return w.wrap(text)
+
+
+class TextWriter(writers.Writer):
+    supported = ('text',)
+    settings_spec = ('No options here.', '', ())
+    settings_defaults = {}
+
+    output = None
+
+    def __init__(self):
+        writers.Writer.__init__(self)
+
+    def translate(self):
+        visitor = TextTranslator(self.document)
+        self.document.walkabout(visitor)
+        self.output = visitor.body
+
+
+class TextTranslator(nodes.NodeVisitor):
+    sectionchars = '*=-~"+`'
+
+    def __init__(self, document):
+        nodes.NodeVisitor.__init__(self, document)
+
+        self.nl = os.linesep
+        self.states = [[]]
+        self.stateindent = [0]
+        self.list_counter = []
+        self.sectionlevel = 0
+        self.table = None
+
+    def add_text(self, text):
+        self.states[-1].append((-1, text))
+
+    def new_state(self, indent=STDINDENT):
+        self.states.append([])
+        self.stateindent.append(indent)
+
+    def end_state(self, wrap=True, end=[''], first=None):
+        content = self.states.pop()
+        maxindent = sum(self.stateindent)
+        indent = self.stateindent.pop()
+        result = []
+        toformat = []
+        def do_format():
+            if not toformat:
+                return
+            if wrap:
+                res = my_wrap(''.join(toformat), width=MAXWIDTH-maxindent)
+            else:
+                res = ''.join(toformat).splitlines()
+            if end:
+                res += end
+            result.append((indent, res))
+        for itemindent, item in content:
+            if itemindent == -1:
+                toformat.append(item)
+            else:
+                do_format()
+                result.append((indent + itemindent, item))
+                toformat = []
+        do_format()
+        if first is not None and result:
+            itemindent, item = result[0]
+            if item:
+                result.insert(0, (itemindent - indent, [first + item[0]]))
+                result[1] = (itemindent, item[1:])
+        self.states[-1].extend(result)
+
+    def visit_document(self, node):
+        self.new_state(0)
+    def depart_document(self, node):
+        self.end_state()
+        self.body = self.nl.join(line and (' '*indent + line)
+                                 for indent, lines in self.states[0]
+                                 for line in lines)
+        # XXX header/footer?
+
+    def visit_highlightlang(self, node):
+        raise nodes.SkipNode
+
+    def visit_section(self, node):
+        self._title_char = self.sectionchars[self.sectionlevel]
+        self.sectionlevel += 1
+    def depart_section(self, node):
+        self.sectionlevel -= 1
+
+    def visit_topic(self, node):
+        self.new_state(0)
+    def depart_topic(self, node):
+        self.end_state()
+
+    visit_sidebar = visit_topic
+    depart_sidebar = depart_topic
+
+    def visit_rubric(self, node):
+        self.new_state(0)
+        self.add_text('-[ ')
+
+    def depart_rubric(self, node):
+        self.add_text(' ]-')
+        self.end_state()
+
+    def visit_compound(self, node):
+        pass
+
+    def depart_compound(self, node):
+        pass
+
+    def visit_glossary(self, node):
+        pass
+    def depart_glossary(self, node):
+        pass
+
+    def visit_title(self, node):
+        if isinstance(node.parent, nodes.Admonition):
+            self.add_text(node.astext()+': ')
+            raise nodes.SkipNode
+        self.new_state(0)
+    def depart_title(self, node):
+        if isinstance(node.parent, nodes.section):
+            char = self._title_char
+        else:
+            char = '^'
+        text = ''.join(x[1] for x in self.states.pop() if x[0] == -1)
+        self.stateindent.pop()
+        self.states[-1].append((0, ['', text, '%s' % (char * len(text)), '']))
+
+    def visit_subtitle(self, node):
+        pass
+    def depart_subtitle(self, node):
+        pass
+
+    def visit_attribution(self, node):
+        self.add_text('-- ')
+    def depart_attribution(self, node):
+        pass
+
+    def visit_desc(self, node):
+        pass
+    def depart_desc(self, node):
+        pass
+
+    def visit_desc_signature(self, node):
+        self.new_state(0)
+        if node.parent['objtype'] in ('class', 'exception'):
+            self.add_text('%s ' % node.parent['objtype'])
+    def depart_desc_signature(self, node):
+        # XXX: wrap signatures in a way that makes sense
+        self.end_state(wrap=False, end=None)
+
+    def visit_desc_name(self, node):
+        pass
+    def depart_desc_name(self, node):
+        pass
+
+    def visit_desc_addname(self, node):
+        pass
+    def depart_desc_addname(self, node):
+        pass
+
+    def visit_desc_type(self, node):
+        pass
+    def depart_desc_type(self, node):
+        pass
+
+    def visit_desc_returns(self, node):
+        self.add_text(' -> ')
+    def depart_desc_returns(self, node):
+        pass
+
+    def visit_desc_parameterlist(self, node):
+        self.add_text('(')
+        self.first_param = 1
+    def depart_desc_parameterlist(self, node):
+        self.add_text(')')
+
+    def visit_desc_parameter(self, node):
+        if not self.first_param:
+            self.add_text(', ')
+        else:
+            self.first_param = 0
+        self.add_text(node.astext())
+        raise nodes.SkipNode
+
+    def visit_desc_optional(self, node):
+        self.add_text('[')
+    def depart_desc_optional(self, node):
+        self.add_text(']')
+
+    def visit_desc_annotation(self, node):
+        pass
+    def depart_desc_annotation(self, node):
+        pass
+
+    def visit_refcount(self, node):
+        pass
+    def depart_refcount(self, node):
+        pass
+
+    def visit_desc_content(self, node):
+        self.new_state()
+        self.add_text(self.nl)
+    def depart_desc_content(self, node):
+        self.end_state()
+
+    def visit_figure(self, node):
+        self.new_state()
+    def depart_figure(self, node):
+        self.end_state()
+
+    def visit_caption(self, node):
+        pass
+    def depart_caption(self, node):
+        pass
+
+    def visit_productionlist(self, node):
+        self.new_state()
+        names = []
+        for production in node:
+            names.append(production['tokenname'])
+        maxlen = max(len(name) for name in names)
+        for production in node:
+            if production['tokenname']:
+                self.add_text(production['tokenname'].ljust(maxlen) + ' ::=')
+                lastname = production['tokenname']
+            else:
+                self.add_text('%s    ' % (' '*len(lastname)))
+            self.add_text(production.astext() + self.nl)
+        self.end_state(wrap=False)
+        raise nodes.SkipNode
+
+    def visit_seealso(self, node):
+        self.new_state()
+    def depart_seealso(self, node):
+        self.end_state(first='')
+
+    def visit_footnote(self, node):
+        self._footnote = node.children[0].astext().strip()
+        self.new_state(len(self._footnote) + 3)
+    def depart_footnote(self, node):
+        self.end_state(first='[%s] ' % self._footnote)
+
+    def visit_citation(self, node):
+        if len(node) and isinstance(node[0], nodes.label):
+            self._citlabel = node[0].astext()
+        else:
+            self._citlabel = ''
+        self.new_state(len(self._citlabel) + 3)
+    def depart_citation(self, node):
+        self.end_state(first='[%s] ' % self._citlabel)
+
+    def visit_label(self, node):
+        raise nodes.SkipNode
+
+    # XXX: option list could use some better styling
+
+    def visit_option_list(self, node):
+        pass
+    def depart_option_list(self, node):
+        pass
+
+    def visit_option_list_item(self, node):
+        self.new_state(0)
+    def depart_option_list_item(self, node):
+        self.end_state()
+
+    def visit_option_group(self, node):
+        self._firstoption = True
+    def depart_option_group(self, node):
+        self.add_text('     ')
+
+    def visit_option(self, node):
+        if self._firstoption:
+            self._firstoption = False
+        else:
+            self.add_text(', ')
+    def depart_option(self, node):
+        pass
+
+    def visit_option_string(self, node):
+        pass
+    def depart_option_string(self, node):
+        pass
+
+    def visit_option_argument(self, node):
+        self.add_text(node['delimiter'])
+    def depart_option_argument(self, node):
+        pass
+
+    def visit_description(self, node):
+        pass
+    def depart_description(self, node):
+        pass
+
+    def visit_tabular_col_spec(self, node):
+        raise nodes.SkipNode
+
+    def visit_colspec(self, node):
+        self.table[0].append(node['colwidth'])
+        raise nodes.SkipNode
+
+    def visit_tgroup(self, node):
+        pass
+    def depart_tgroup(self, node):
+        pass
+
+    def visit_thead(self, node):
+        pass
+    def depart_thead(self, node):
+        pass
+
+    def visit_tbody(self, node):
+        self.table.append('sep')
+    def depart_tbody(self, node):
+        pass
+
+    def visit_row(self, node):
+        self.table.append([])
+    def depart_row(self, node):
+        pass
+
+    def visit_entry(self, node):
+        if node.has_key('morerows') or node.has_key('morecols'):
+            raise NotImplementedError('Column or row spanning cells are '
+                                      'not implemented.')
+        self.new_state(0)
+    def depart_entry(self, node):
+        text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop())
+        self.stateindent.pop()
+        self.table[-1].append(text)
+
+    def visit_table(self, node):
+        if self.table:
+            raise NotImplementedError('Nested tables are not supported.')
+        self.new_state(0)
+        self.table = [[]]
+    def depart_table(self, node):
+        lines = self.table[1:]
+        fmted_rows = []
+        colwidths = self.table[0]
+        realwidths = colwidths[:]
+        separator = 0
+        # don't allow paragraphs in table cells for now
+        for line in lines:
+            if line == 'sep':
+                separator = len(fmted_rows)
+            else:
+                cells = []
+                for i, cell in enumerate(line):
+                    par = my_wrap(cell, width=colwidths[i])
+                    if par:
+                        maxwidth = max(map(len, par))
+                    else:
+                        maxwidth = 0
+                    realwidths[i] = max(realwidths[i], maxwidth)
+                    cells.append(par)
+                fmted_rows.append(cells)
+
+        def writesep(char='-'):
+            out = ['+']
+            for width in realwidths:
+                out.append(char * (width+2))
+                out.append('+')
+            self.add_text(''.join(out) + self.nl)
+
+        def writerow(row):
+            lines = zip(*row)
+            for line in lines:
+                out = ['|']
+                for i, cell in enumerate(line):
+                    if cell:
+                        out.append(' ' + cell.ljust(realwidths[i]+1))
+                    else:
+                        out.append(' ' * (realwidths[i] + 2))
+                    out.append('|')
+                self.add_text(''.join(out) + self.nl)
+
+        for i, row in enumerate(fmted_rows):
+            if separator and i == separator:
+                writesep('=')
+            else:
+                writesep('-')
+            writerow(row)
+        writesep('-')
+        self.table = None
+        self.end_state(wrap=False)
+
+    def visit_acks(self, node):
+        self.new_state(0)
+        self.add_text(', '.join(n.astext() for n in node.children[0].children)
+                      + '.')
+        self.end_state()
+        raise nodes.SkipNode
+
+    def visit_image(self, node):
+        if 'alt' in node.attributes:
+            self.add_text(_('[image: %s]') % node['alt'])
+        self.add_text(_('[image]'))
+        raise nodes.SkipNode
+
+    def visit_transition(self, node):
+        indent = sum(self.stateindent)
+        self.new_state(0)
+        self.add_text('=' * (MAXWIDTH - indent))
+        self.end_state()
+        raise nodes.SkipNode
+
+    def visit_bullet_list(self, node):
+        self.list_counter.append(-1)
+    def depart_bullet_list(self, node):
+        self.list_counter.pop()
+
+    def visit_enumerated_list(self, node):
+        self.list_counter.append(0)
+    def depart_enumerated_list(self, node):
+        self.list_counter.pop()
+
+    def visit_definition_list(self, node):
+        self.list_counter.append(-2)
+    def depart_definition_list(self, node):
+        self.list_counter.pop()
+
+    def visit_list_item(self, node):
+        if self.list_counter[-1] == -1:
+            # bullet list
+            self.new_state(2)
+        elif self.list_counter[-1] == -2:
+            # definition list
+            pass
+        else:
+            # enumerated list
+            self.list_counter[-1] += 1
+            self.new_state(len(str(self.list_counter[-1])) + 2)
+    def depart_list_item(self, node):
+        if self.list_counter[-1] == -1:
+            self.end_state(first='* ', end=None)
+        elif self.list_counter[-1] == -2:
+            pass
+        else:
+            self.end_state(first='%s. ' % self.list_counter[-1], end=None)
+
+    def visit_definition_list_item(self, node):
+        self._li_has_classifier = len(node) >= 2 and \
+                                  isinstance(node[1], nodes.classifier)
+    def depart_definition_list_item(self, node):
+        pass
+
+    def visit_term(self, node):
+        self.new_state(0)
+    def depart_term(self, node):
+        if not self._li_has_classifier:
+            self.end_state(end=None)
+
+    def visit_termsep(self, node):
+        self.add_text(', ')
+        raise nodes.SkipNode
+
+    def visit_classifier(self, node):
+        self.add_text(' : ')
+    def depart_classifier(self, node):
+        self.end_state(end=None)
+
+    def visit_definition(self, node):
+        self.new_state()
+    def depart_definition(self, node):
+        self.end_state()
+
+    def visit_field_list(self, node):
+        pass
+    def depart_field_list(self, node):
+        pass
+
+    def visit_field(self, node):
+        pass
+    def depart_field(self, node):
+        pass
+
+    def visit_field_name(self, node):
+        self.new_state(0)
+    def depart_field_name(self, node):
+        self.add_text(':')
+        self.end_state(end=None)
+
+    def visit_field_body(self, node):
+        self.new_state()
+    def depart_field_body(self, node):
+        self.end_state()
+
+    def visit_centered(self, node):
+        pass
+    def depart_centered(self, node):
+        pass
+
+    def visit_hlist(self, node):
+        pass
+    def depart_hlist(self, node):
+        pass
+
+    def visit_hlistcol(self, node):
+        pass
+    def depart_hlistcol(self, node):
+        pass
+
+    def visit_admonition(self, node):
+        self.new_state(0)
+    def depart_admonition(self, node):
+        self.end_state()
+
+    def visit_versionmodified(self, node):
+        self.new_state(0)
+
+    def depart_versionmodified(self, node):
+        self.end_state()
+
+    def visit_literal_block(self, node):
+        self.new_state()
+    def depart_literal_block(self, node):
+        self.end_state(wrap=False)
+
+    def visit_doctest_block(self, node):
+        self.new_state(0)
+    def depart_doctest_block(self, node):
+        self.end_state(wrap=False)
+
+    def visit_line_block(self, node):
+        self.new_state(0)
+    def depart_line_block(self, node):
+        self.end_state(wrap=False)
+
+    def visit_line(self, node):
+        pass
+    def depart_line(self, node):
+        pass
+
+    def visit_block_quote(self, node):
+        self.new_state()
+    def depart_block_quote(self, node):
+        self.end_state()
+
+    def visit_compact_paragraph(self, node):
+        pass
+    def depart_compact_paragraph(self, node):
+        pass
+
+    def visit_paragraph(self, node):
+        self.new_state(0)
+
+    def depart_paragraph(self, node):
+        self.end_state()
+
+    def visit_target(self, node):
+        raise nodes.SkipNode
+
+    def visit_index(self, node):
+        raise nodes.SkipNode
+
+    def visit_substitution_definition(self, node):
+        raise nodes.SkipNode
+
+    def visit_pending_xref(self, node):
+        pass
+    def depart_pending_xref(self, node):
+        pass
+
+    def visit_reference(self, node):
+        pass
+    def depart_reference(self, node):
+        pass
+
+    def visit_download_reference(self, node):
+        pass
+    def depart_download_reference(self, node):
+        pass
+
+    def visit_emphasis(self, node):
+        self.add_text('*')
+    def depart_emphasis(self, node):
+        self.add_text('*')
+
+    def visit_literal_emphasis(self, node):
+        self.add_text('*')
+    def depart_literal_emphasis(self, node):
+        self.add_text('*')
+
+    def visit_strong(self, node):
+        self.add_text('**')
+    def depart_strong(self, node):
+        self.add_text('**')
+
+    def visit_abbreviation(self, node):
+        self.add_text('')
+    def depart_abbreviation(self, node):
+        if node.hasattr('explanation'):
+            self.add_text(' (%s)' % node['explanation'])
+
+    def visit_title_reference(self, node):
+        self.add_text('*')
+    def depart_title_reference(self, node):
+        self.add_text('*')
+
+    def visit_literal(self, node):
+        self.add_text('"')
+    def depart_literal(self, node):
+        self.add_text('"')
+
+    def visit_subscript(self, node):
+        self.add_text('_')
+    def depart_subscript(self, node):
+        pass
+
+    def visit_superscript(self, node):
+        self.add_text('^')
+    def depart_superscript(self, node):
+        pass
+
+    def visit_footnote_reference(self, node):
+        self.add_text('[%s]' % node.astext())
+        raise nodes.SkipNode
+
+    def visit_citation_reference(self, node):
+        self.add_text('[%s]' % node.astext())
+        raise nodes.SkipNode
+
+    def visit_Text(self, node):
+        self.add_text(node.astext())
+    def depart_Text(self, node):
+        pass
+
+    def visit_generated(self, node):
+        pass
+
+    def depart_generated(self, node):
+        pass
+
+    def visit_inline(self, node):
+        pass
+
+    def depart_inline(self, node):
+        pass
+
+    def visit_problematic(self, node):
+        self.add_text('>>')
+
+    def depart_problematic(self, node):
+        self.add_text('<<')
+
+    def visit_system_message(self, node):
+        self.new_state(0)
+        self.add_text('<SYSTEM MESSAGE: %s>' % node.astext())
+        self.end_state()
+        raise nodes.SkipNode
+
+    def visit_comment(self, node):
+        raise nodes.SkipNode
+
+    def visit_meta(self, node):
+        # only valid for HTML
+        raise nodes.SkipNode
+
+    def visit_raw(self, node):
+        if 'text' in node.get('format', '').split():
+            self.body.append(node.astext())
+        raise nodes.SkipNode
+
+    def _visit_admonition(self, node):
+        self.new_state(2)
+    def _make_depart_admonition(name):
+        def depart_admonition(self, node):
+            self.end_state(first=name.capitalize() + ': ')
+        return depart_admonition
+
+    visit_attention = _visit_admonition
+    depart_attention = _make_depart_admonition('attention')
+    visit_caution = _visit_admonition
+    depart_caution = _make_depart_admonition('caution')
+    visit_danger = _visit_admonition
+    depart_danger = _make_depart_admonition('danger')
+    visit_error = _visit_admonition
+    depart_error = _make_depart_admonition('error')
+    visit_hint = _visit_admonition
+    depart_hint = _make_depart_admonition('hint')
+    visit_important = _visit_admonition
+    depart_important = _make_depart_admonition('important')
+    visit_note = _visit_admonition
+    depart_note = _make_depart_admonition('note')
+    visit_tip = _visit_admonition
+    depart_tip = _make_depart_admonition('tip')
+    visit_warning = _visit_admonition
+    depart_warning = _make_depart_admonition('warning')
+
+    def unknown_visit(self, node):
+        raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
diff --git a/botocore/docs/service.py b/botocore/docs/service.py
index 3b0304b8a..c992e2d16 100644
--- a/botocore/docs/service.py
+++ b/botocore/docs/service.py
@@ -10,14 +10,13 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
-from bcdoc.restdoc import DocumentStructure
-
 import botocore.session
 from botocore.exceptions import DataNotFoundError
 from botocore.docs.utils import get_official_service_name
 from botocore.docs.client import ClientDocumenter
 from botocore.docs.waiter import WaiterDocumenter
 from botocore.docs.paginator import PaginatorDocumenter
+from botocore.docs.bcdoc.restdoc import DocumentStructure
 
 
 class ServiceDocumenter(object):
diff --git a/requirements.txt b/requirements.txt
index 8b8006077..23b83b17a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,5 @@ python-dateutil>=2.1,<3.0.0
 nose==1.3.0
 mock==1.0.1
 wheel==0.24.0
+docutils>=0.10
 -e git://github.com/boto/jmespath.git@develop#egg=jmespath
--e git://github.com/boto/bcdoc.git@develop#egg=bcdoc
diff --git a/setup.py b/setup.py
index 3783228cd..9232efa14 100644
--- a/setup.py
+++ b/setup.py
@@ -6,8 +6,8 @@ from setuptools import setup, find_packages
 
 
 requires = ['jmespath==0.7.1',
-            'bcdoc>=0.16.0,<0.17.0',
-            'python-dateutil>=2.1,<3.0.0']
+            'python-dateutil>=2.1,<3.0.0',
+            'docutils>=0.10']
 
 
 if sys.version_info[:2] == (2, 6):


Test patch:
diff --git a/tests/unit/docs/__init__.py b/tests/unit/docs/__init__.py
index b86121baf..761a93410 100644
--- a/tests/unit/docs/__init__.py
+++ b/tests/unit/docs/__init__.py
@@ -15,7 +15,7 @@ import json
 import tempfile
 import shutil
 
-from bcdoc.restdoc import DocumentStructure
+from botocore.docs.bcdoc.restdoc import DocumentStructure
 import mock
 
 from tests import unittest
diff --git a/tests/unit/docs/bcdoc/__init__.py b/tests/unit/docs/bcdoc/__init__.py
new file mode 100644
index 000000000..a4018327e
--- /dev/null
+++ b/tests/unit/docs/bcdoc/__init__.py
@@ -0,0 +1,12 @@
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
diff --git a/tests/unit/docs/bcdoc/test_document.py b/tests/unit/docs/bcdoc/test_document.py
new file mode 100644
index 000000000..64dfb9a49
--- /dev/null
+++ b/tests/unit/docs/bcdoc/test_document.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+from tests import unittest
+from botocore.compat import six
+from botocore.docs.bcdoc.restdoc import ReSTDocument, DocumentStructure
+
+
+class TestReSTDocument(unittest.TestCase):
+
+    def test_write(self):
+        doc = ReSTDocument()
+        doc.write('foo')
+        self.assertEqual(doc.getvalue(), six.b('foo'))
+
+    def test_writeln(self):
+        doc = ReSTDocument()
+        doc.writeln('foo')
+        self.assertEqual(doc.getvalue(), six.b('foo\n'))
+
+    def test_include_doc_string(self):
+        doc = ReSTDocument()
+        doc.include_doc_string('<p>this is a <code>test</code></p>')
+        self.assertEqual(doc.getvalue(), six.b('\n\nthis is a ``test`` \n\n'))
+        
+    def test_remove_doc_string(self):
+        doc = ReSTDocument()
+        doc.writeln('foo')
+        doc.include_doc_string('<p>this is a <code>test</code></p>')
+        doc.remove_last_doc_string()
+        self.assertEqual(doc.getvalue(), six.b('foo\n'))
+
+
+class TestDocumentStructure(unittest.TestCase):
+    def setUp(self):
+        self.name = 'mydoc'
+        self.doc_structure = DocumentStructure(self.name)
+
+    def test_name(self):
+        self.assertEqual(self.doc_structure.name, self.name)
+
+    def test_path(self):
+        self.assertEqual(self.doc_structure.path, [self.name])
+        self.doc_structure.path = ['foo']
+        self.assertEqual(self.doc_structure.path, ['foo'])
+
+    def test_add_new_section(self):
+        section = self.doc_structure.add_new_section('mysection')
+
+        # Ensure the name of the section is correct
+        self.assertEqual(section.name, 'mysection')
+
+        # Ensure we can get the section.
+        self.assertEqual(
+            self.doc_structure.get_section('mysection'), section)
+
+        # Ensure the path is correct
+        self.assertEqual(section.path, ['mydoc', 'mysection'])
+
+        # Ensure some of the necessary attributes are passed to the
+        # the section.
+        self.assertEqual(section.style.indentation,
+                         self.doc_structure.style.indentation)
+        self.assertEqual(section.translation_map,
+                         self.doc_structure.translation_map)
+        self.assertEqual(section.hrefs,
+                         self.doc_structure.hrefs)
+
+    def test_delete_section(self):
+        section = self.doc_structure.add_new_section('mysection')
+        self.assertEqual(
+            self.doc_structure.get_section('mysection'), section)
+        self.doc_structure.delete_section('mysection')
+        with self.assertRaises(KeyError):
+            section.get_section('mysection')
+
+    def test_create_sections_at_instantiation(self):
+        sections = ['intro', 'middle', 'end']
+        self.doc_structure = DocumentStructure(
+            self.name, section_names=sections)
+        # Ensure the sections are attached to the new document structure.
+        for section_name in sections:
+            section = self.doc_structure.get_section(section_name)
+            self.assertEqual(section.name, section_name)
+
+    def test_flush_structure(self):
+        section = self.doc_structure.add_new_section('mysection')
+        subsection = section.add_new_section('mysubsection')
+        self.doc_structure.writeln('1')
+        section.writeln('2')
+        subsection.writeln('3')
+        second_section = self.doc_structure.add_new_section('mysection2')
+        second_section.writeln('4')
+        contents = self.doc_structure.flush_structure()
+
+        # Ensure the contents were flushed out correctly
+        self.assertEqual(contents, six.b('1\n2\n3\n4\n'))
+
+    def test_flush_structure_hrefs(self):
+        section = self.doc_structure.add_new_section('mysection')
+        section.writeln('section contents')
+        self.doc_structure.hrefs['foo'] = 'www.foo.com'
+        section.hrefs['bar'] = 'www.bar.com'
+        contents = self.doc_structure.flush_structure()
+        self.assertIn(six.b('.. _foo: www.foo.com'), contents)
+        self.assertIn(six.b('.. _bar: www.bar.com'), contents)
+
+    def test_available_sections(self):
+        self.doc_structure.add_new_section('mysection')
+        self.doc_structure.add_new_section('mysection2')
+        self.assertEqual(
+            self.doc_structure.available_sections,
+            ['mysection', 'mysection2']
+        )
diff --git a/tests/unit/docs/bcdoc/test_style.py b/tests/unit/docs/bcdoc/test_style.py
new file mode 100644
index 000000000..5c75d8703
--- /dev/null
+++ b/tests/unit/docs/bcdoc/test_style.py
@@ -0,0 +1,229 @@
+# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+from tests import unittest
+from botocore.compat import six
+from botocore.docs.bcdoc.style import ReSTStyle
+from botocore.docs.bcdoc.restdoc import ReSTDocument
+
+
+class TestStyle(unittest.TestCase):
+
+    def test_spaces(self):
+        style = ReSTStyle(None, 4)
+        self.assertEqual(style.spaces(), '')
+        style.indent()
+        self.assertEqual(style.spaces(), '    ')
+        style.indent()
+        self.assertEqual(style.spaces(), '        ')
+        style.dedent()
+        self.assertEqual(style.spaces(), '    ')
+        style.dedent()
+        self.assertEqual(style.spaces(), '')
+        style.dedent()
+        self.assertEqual(style.spaces(), '')
+
+    def test_bold(self):
+        style = ReSTStyle(ReSTDocument())
+        style.bold('foobar')
+        self.assertEqual(style.doc.getvalue(), six.b('**foobar** '))
+
+    def test_italics(self):
+        style = ReSTStyle(ReSTDocument())
+        style.italics('foobar')
+        self.assertEqual(style.doc.getvalue(), six.b('*foobar* '))
+
+    def test_code(self):
+        style = ReSTStyle(ReSTDocument())
+        style.code('foobar')
+        self.assertEqual(style.doc.getvalue(), six.b('``foobar`` '))
+
+    def test_h1(self):
+        style = ReSTStyle(ReSTDocument())
+        style.h1('foobar fiebaz')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n\n*************\nfoobar fiebaz\n*************\n\n'))
+
+    def test_h2(self):
+        style = ReSTStyle(ReSTDocument())
+        style.h2('foobar fiebaz')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n\n=============\nfoobar fiebaz\n=============\n\n'))
+
+    def test_h3(self):
+        style = ReSTStyle(ReSTDocument())
+        style.h3('foobar fiebaz')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n\n-------------\nfoobar fiebaz\n-------------\n\n'))
+
+    def test_ref(self):
+        style = ReSTStyle(ReSTDocument())
+        style.ref('foobar', 'http://foo.bar.com')
+        self.assertEqual(style.doc.getvalue(),
+                         six.b(':doc:`foobar <http://foo.bar.com>`'))
+
+    def test_examples(self):
+        style = ReSTStyle(ReSTDocument())
+        self.assertTrue(style.doc.keep_data)
+        style.start_examples()
+        self.assertFalse(style.doc.keep_data)
+        style.end_examples()
+        self.assertTrue(style.doc.keep_data)
+
+    def test_codeblock(self):
+        style = ReSTStyle(ReSTDocument())
+        style.codeblock('foobar')
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('::\n\n  foobar\n\n\n'))
+
+    def test_toctree_html(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'html'
+        style.toctree()
+        style.tocitem('foo')
+        style.tocitem('bar')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n.. toctree::\n  :maxdepth: 1'
+                  '\n  :titlesonly:\n\n  foo\n  bar\n'))
+
+    def test_toctree_man(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'man'
+        style.toctree()
+        style.tocitem('foo')
+        style.tocitem('bar')
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('\n\n\n* foo\n\n\n* bar\n\n'))
+
+    def test_hidden_toctree_html(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'html'
+        style.hidden_toctree()
+        style.hidden_tocitem('foo')
+        style.hidden_tocitem('bar')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n.. toctree::\n  :maxdepth: 1'
+                  '\n  :hidden:\n\n  foo\n  bar\n'))
+
+    def test_hidden_toctree_non_html(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'man'
+        style.hidden_toctree()
+        style.hidden_tocitem('foo')
+        style.hidden_tocitem('bar')
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b(''))
+
+    def test_escape_href_link(self):
+        style = ReSTStyle(ReSTDocument())
+        style.start_a(attrs=[('href', 'http://example.org')])
+        style.doc.write('foo: the next bar')
+        style.end_a()
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('`foo\\: the next bar`_ \n\n.. _foo\\: the next '
+                  'bar: http://example.org\n'))
+
+    def test_handle_no_text_hrefs(self):
+        style = ReSTStyle(ReSTDocument())
+        style.start_a(attrs=[('href', 'http://example.org')])
+        style.end_a()
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('`<http://example.org>`_ '))
+
+    def test_sphinx_reference_label_html(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'html'
+        style.sphinx_reference_label('foo', 'bar')
+        self.assertEqual(style.doc.getvalue(), six.b(':ref:`bar <foo>`'))
+
+    def test_sphinx_reference_label_html_no_text(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'html'
+        style.sphinx_reference_label('foo')
+        self.assertEqual(style.doc.getvalue(), six.b(':ref:`foo <foo>`'))
+
+    def test_sphinx_reference_label_non_html(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'man'
+        style.sphinx_reference_label('foo', 'bar')
+        self.assertEqual(style.doc.getvalue(), six.b('bar'))
+
+    def test_sphinx_reference_label_non_html_no_text(self):
+        style = ReSTStyle(ReSTDocument())
+        style.doc.target = 'man'
+        style.sphinx_reference_label('foo')
+        self.assertEqual(style.doc.getvalue(), six.b('foo'))
+
+    def test_table_of_contents(self):
+        style = ReSTStyle(ReSTDocument())
+        style.table_of_contents()
+        self.assertEqual(style.doc.getvalue(), six.b('.. contents:: '))
+
+    def test_table_of_contents_with_title(self):
+        style = ReSTStyle(ReSTDocument())
+        style.table_of_contents(title='Foo')
+        self.assertEqual(style.doc.getvalue(), six.b('.. contents:: Foo\n'))
+
+    def test_table_of_contents_with_title_and_depth(self):
+        style = ReSTStyle(ReSTDocument())
+        style.table_of_contents(title='Foo', depth=2)
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('.. contents:: Foo\n   :depth: 2\n'))
+
+    def test_sphinx_py_class(self):
+        style = ReSTStyle(ReSTDocument())
+        style.start_sphinx_py_class('FooClass')
+        style.end_sphinx_py_class()
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('\n\n.. py:class:: FooClass\n\n  \n\n'))
+
+    def test_sphinx_py_method(self):
+        style = ReSTStyle(ReSTDocument())
+        style.start_sphinx_py_method('method')
+        style.end_sphinx_py_method()
+        self.assertEqual(style.doc.getvalue(),
+                         six.b('\n\n.. py:method:: method\n\n  \n\n'))
+
+    def test_sphinx_py_method_with_params(self):
+        style = ReSTStyle(ReSTDocument())
+        style.start_sphinx_py_method('method', 'foo=None')
+        style.end_sphinx_py_method()
+        self.assertEqual(
+            style.doc.getvalue(),
+            six.b('\n\n.. py:method:: method(foo=None)\n\n  \n\n'))
+
+    def test_write_py_doc_string(self):
+        style = ReSTStyle(ReSTDocument())
+        docstring = (
+            'This describes a function\n'
+            ':param foo: Describes foo\n'
+            'returns: None'
+        )
+        style.write_py_doc_string(docstring)
+        self.assertEqual(style.doc.getvalue(), six.b(docstring + '\n'))