tmp
/
pip-install-ghxuqwgs
/numpy_78e94bf2b6094bf9a1f3d92042f9bf46
/doc
/sphinxext
/numpydoc
/comment_eater.py
| from __future__ import division, absolute_import, print_function | |
| import sys | |
| if sys.version_info[0] >= 3: | |
| from io import StringIO | |
| else: | |
| from io 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 | |