1
from cStringIO import StringIO
7
from compiler_unparse import unparse
10
class Comment(object):
14
def __init__(self, start_lineno, end_lineno, text):
15
# int : The first line number in the block. 1-indexed.
16
self.start_lineno = start_lineno
17
# int : The last line number. Inclusive!
18
self.end_lineno = end_lineno
19
# str : The text block including '#' character but not any leading spaces.
22
def add(self, string, start, end, line):
23
""" Add a new comment line.
25
self.start_lineno = min(self.start_lineno, start[0])
26
self.end_lineno = max(self.end_lineno, end[0])
30
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.start_lineno,
31
self.end_lineno, self.text)
34
class NonComment(object):
35
""" A non-comment block of code.
38
def __init__(self, start_lineno, end_lineno):
39
self.start_lineno = start_lineno
40
self.end_lineno = end_lineno
42
def add(self, string, start, end, line):
43
""" Add lines to the block.
46
# Only add if not entirely whitespace.
47
self.start_lineno = min(self.start_lineno, start[0])
48
self.end_lineno = max(self.end_lineno, end[0])
51
return '%s(%r, %r)' % (self.__class__.__name__, self.start_lineno,
55
class CommentBlocker(object):
56
""" Pull out contiguous comment blocks.
60
self.current_block = NonComment(0, 0)
62
# All of the blocks seen so far.
65
# The index mapping lines of code to their associated comment blocks.
68
def process_file(self, file):
69
""" Process a file object.
71
for token in tokenize.generate_tokens(file.next):
72
self.process_token(*token)
75
def process_token(self, kind, string, start, end, line):
76
""" Process a single token.
78
if self.current_block.is_comment:
79
if kind == tokenize.COMMENT:
80
self.current_block.add(string, start, end, line)
82
self.new_noncomment(start[0], end[0])
84
if kind == tokenize.COMMENT:
85
self.new_comment(string, start, end, line)
87
self.current_block.add(string, start, end, line)
89
def new_noncomment(self, start_lineno, end_lineno):
90
""" We are transitioning from a noncomment to a comment.
92
block = NonComment(start_lineno, end_lineno)
93
self.blocks.append(block)
94
self.current_block = block
96
def new_comment(self, string, start, end, line):
97
""" Possibly add a new comment.
99
Only adds a new comment if this comment is the only thing on the line.
100
Otherwise, it extends the noncomment block.
102
prefix = line[:start[1]]
104
# Oops! Trailing comment, not a comment block.
105
self.current_block.add(string, start, end, line)
108
block = Comment(start[0], end[0], string)
109
self.blocks.append(block)
110
self.current_block = block
112
def make_index(self):
113
""" Make the index mapping lines of actual code to their associated
116
for prev, block in zip(self.blocks[:-1], self.blocks[1:]):
117
if not block.is_comment:
118
self.index[block.start_lineno] = prev
120
def search_for_comment(self, lineno, default=None):
121
""" Find the comment block just before the given line number.
123
Returns None (or the specified default) if there is no such block.
127
block = self.index.get(lineno, None)
128
text = getattr(block, 'text', default)
132
def strip_comment_marker(text):
133
""" Strip # markers at the front of a block of comment text.
136
for line in text.splitlines():
137
lines.append(line.lstrip('#'))
138
text = textwrap.dedent('\n'.join(lines))
142
def get_class_traits(klass):
143
""" Yield all of the documentation for trait definitions on a class object.
145
# FIXME: gracefully handle errors here or in the caller?
146
source = inspect.getsource(klass)
147
cb = CommentBlocker()
148
cb.process_file(StringIO(source))
149
mod_ast = compiler.parse(source)
150
class_ast = mod_ast.node.nodes[0]
151
for node in class_ast.code.nodes:
152
# FIXME: handle other kinds of assignments?
153
if isinstance(node, compiler.ast.Assign):
154
name = node.nodes[0].name
155
rhs = unparse(node.expr).strip()
156
doc = strip_comment_marker(cb.search_for_comment(node.lineno, default=''))