summaryrefslogtreecommitdiff
path: root/doc/sphinxext/numpydoc/comment_eater.py
diff options
context:
space:
mode:
Diffstat (limited to 'doc/sphinxext/numpydoc/comment_eater.py')
-rw-r--r--doc/sphinxext/numpydoc/comment_eater.py167
1 files changed, 167 insertions, 0 deletions
diff --git a/doc/sphinxext/numpydoc/comment_eater.py b/doc/sphinxext/numpydoc/comment_eater.py
new file mode 100644
index 000000000..f84ca6934
--- /dev/null
+++ b/doc/sphinxext/numpydoc/comment_eater.py
@@ -0,0 +1,167 @@
+import sys
+if sys.version_info[0] >= 3:
+ from io import StringIO
+else:
+ from cStringIO import StringIO
+
+import compiler
+import inspect
+import textwrap
+import tokenize
+
+from .compiler_unparse import unparse
+
+
+class Comment(object):
+ """ A comment block.
+ """
+ is_comment = True
+ def __init__(self, start_lineno, end_lineno, text):
+ # int : The first line number in the block. 1-indexed.
+ self.start_lineno = start_lineno
+ # int : The last line number. Inclusive!
+ self.end_lineno = end_lineno
+ # str : The text block including '#' character but not any leading spaces.
+ self.text = text
+
+ def add(self, string, start, end, line):
+ """ Add a new comment line.
+ """
+ self.start_lineno = min(self.start_lineno, start[0])
+ self.end_lineno = max(self.end_lineno, end[0])
+ self.text += string
+
+ def __repr__(self):
+ return '%s(%r, %r, %r)' % (self.__class__.__name__, self.start_lineno,
+ self.end_lineno, self.text)
+
+
+class NonComment(object):
+ """ A non-comment block of code.
+ """
+ is_comment = False
+ def __init__(self, start_lineno, end_lineno):
+ self.start_lineno = start_lineno
+ self.end_lineno = end_lineno
+
+ def add(self, string, start, end, line):
+ """ Add lines to the block.
+ """
+ if string.strip():
+ # Only add if not entirely whitespace.
+ self.start_lineno = min(self.start_lineno, start[0])
+ self.end_lineno = max(self.end_lineno, end[0])
+
+ def __repr__(self):
+ return '%s(%r, %r)' % (self.__class__.__name__, self.start_lineno,
+ self.end_lineno)
+
+
+class CommentBlocker(object):
+ """ Pull out contiguous comment blocks.
+ """
+ def __init__(self):
+ # Start with a dummy.
+ self.current_block = NonComment(0, 0)
+
+ # All of the blocks seen so far.
+ self.blocks = []
+
+ # The index mapping lines of code to their associated comment blocks.
+ self.index = {}
+
+ def process_file(self, file):
+ """ Process a file object.
+ """
+ if sys.version_info[0] >= 3:
+ nxt = file.__next__
+ else:
+ nxt = file.next
+ for token in tokenize.generate_tokens(nxt):
+ self.process_token(*token)
+ self.make_index()
+
+ def process_token(self, kind, string, start, end, line):
+ """ Process a single token.
+ """
+ if self.current_block.is_comment:
+ if kind == tokenize.COMMENT:
+ self.current_block.add(string, start, end, line)
+ else:
+ self.new_noncomment(start[0], end[0])
+ else:
+ if kind == tokenize.COMMENT:
+ self.new_comment(string, start, end, line)
+ else:
+ self.current_block.add(string, start, end, line)
+
+ def new_noncomment(self, start_lineno, end_lineno):
+ """ We are transitioning from a noncomment to a comment.
+ """
+ block = NonComment(start_lineno, end_lineno)
+ self.blocks.append(block)
+ self.current_block = block
+
+ def new_comment(self, string, start, end, line):
+ """ Possibly add a new comment.
+
+ Only adds a new comment if this comment is the only thing on the line.
+ Otherwise, it extends the noncomment block.
+ """
+ prefix = line[:start[1]]
+ if prefix.strip():
+ # Oops! Trailing comment, not a comment block.
+ self.current_block.add(string, start, end, line)
+ else:
+ # A comment block.
+ block = Comment(start[0], end[0], string)
+ self.blocks.append(block)
+ self.current_block = block
+
+ def make_index(self):
+ """ Make the index mapping lines of actual code to their associated
+ prefix comments.
+ """
+ for prev, block in zip(self.blocks[:-1], self.blocks[1:]):
+ if not block.is_comment:
+ self.index[block.start_lineno] = prev
+
+ def search_for_comment(self, lineno, default=None):
+ """ Find the comment block just before the given line number.
+
+ Returns None (or the specified default) if there is no such block.
+ """
+ if not self.index:
+ self.make_index()
+ block = self.index.get(lineno, None)
+ text = getattr(block, 'text', default)
+ return text
+
+
+def strip_comment_marker(text):
+ """ Strip # markers at the front of a block of comment text.
+ """
+ lines = []
+ for line in text.splitlines():
+ lines.append(line.lstrip('#'))
+ text = textwrap.dedent('\n'.join(lines))
+ return text
+
+
+def get_class_traits(klass):
+ """ Yield all of the documentation for trait definitions on a class object.
+ """
+ # FIXME: gracefully handle errors here or in the caller?
+ source = inspect.getsource(klass)
+ cb = CommentBlocker()
+ cb.process_file(StringIO(source))
+ mod_ast = compiler.parse(source)
+ class_ast = mod_ast.node.nodes[0]
+ for node in class_ast.code.nodes:
+ # FIXME: handle other kinds of assignments?
+ if isinstance(node, compiler.ast.Assign):
+ name = node.nodes[0].name
+ rhs = unparse(node.expr).strip()
+ doc = strip_comment_marker(cb.search_for_comment(node.lineno, default=''))
+ yield name, rhs, doc
+