Этот вопрос естественным образом возникает при написании любого вида кода Python beautifier, pep-8 checker и т. Д. В таких случаях вы делаете преобразования источник-источник, вы делаете ожидают, что ввод будет написан человеком, и не только хотят, чтобы вывод был удобочитаемым, но, кроме того, ожидают, что он:
- будет включать все комментарии, именно там, где они появляются в оригинале.
- выводит точное написание строк, включая строки документов, как в оригинале.
Это далеко не просто сделать с модулем ast.Вы могли бы назвать это дырой в API, но, похоже, не существует простого способа расширить API, чтобы легко выполнять 1 и 2.
Предложение Андрея использовать одновременно ast и tokenize - блестящий обходной путь.Идея пришла мне в голову и при написании Python в Coffeescript конвертер , но код далеко не тривиален.
Класс TokenSync
(ts), начинающийся со строки 1305 в py2cs.py координирует связь между данными на основе токенов и астрономическим обходом.Учитывая исходную строку s, класс TokenSync
маркирует s и вставляет внутренние структуры данных, которые поддерживают несколько методов интерфейса:
ts.leading_lines(node)
: возвращает список предыдущего комментария и пустые строки.
ts.trailing_comment(node)
: вернуть строку, содержащую завершающий комментарий для узла, если таковой имеется.
ts.sync_string(node)
: вернуть написание строки в данном узле.
Это просто,но просто немного неуклюже, чтобы посетители могли использовать эти методы.Вот несколько примеров из класса CoffeeScriptTraverser
(cst) в py2cs.py:
def do_Str(self, node):
'''A string constant, including docstrings.'''
if hasattr(node, 'lineno'):
return self.sync_string(node)
Это работает при условии, что узлы ast.Str посещаются в порядке их появления в источниках.Это происходит естественным образом при большинстве прохождений.
Вот аст. Если посетитель.В нем показано, как использовать ts.leading_lines
и ts.trailing_comment
:
def do_If(self, node):
result = self.leading_lines(node)
tail = self.trailing_comment(node)
s = 'if %s:%s' % (self.visit(node.test), tail)
result.append(self.indent(s))
for z in node.body:
self.level += 1
result.append(self.visit(z))
self.level -= 1
if node.orelse:
tail = self.tail_after_body(node.body, node.orelse, result)
result.append(self.indent('else:' + tail))
for z in node.orelse:
self.level += 1
result.append(self.visit(z))
self.level -= 1
return ''.join(result)
Метод ts.tail_after_body
компенсирует тот факт, что отсутствуют узлы ast, представляющие предложения else.Это не ракетостроение, но это не красиво:
def tail_after_body(self, body, aList, result):
'''
Return the tail of the 'else' or 'finally' statement following the given body.
aList is the node.orelse or node.finalbody list.
'''
node = self.last_node(body)
if node:
max_n = node.lineno
leading = self.leading_lines(aList[0])
if leading:
result.extend(leading)
max_n += len(leading)
tail = self.trailing_comment_at_lineno(max_n + 1)
else:
tail = '\n'
return tail
Обратите внимание, что cst.tail_after_body
просто звонит ts.tail_after_body
.
Резюме
Класс TokenSync инкапсулирует большинство сложностей, связанных с обеспечением доступности токен-ориентированных данных для кода обхода ast.Использовать класс TokenSync несложно, но посетители ast для всех операторов Python (и ast.Str) должны включать вызовы ts.leading_lines
, ts.trailing_comment
и ts.sync_string
.Более того, взлом ts.tail_after_body
необходим для обработки «отсутствующих» аст-узлов.
Короче говоря, код работает хорошо, но немного неуклюже.
@ Андрей: ваш короткий ответМожно предположить, что вы знаете более элегантный способ.Если это так, я хотел бы видеть это.
Эдвард К. Реам