Pythonで別のスクリプトを操作するスクリプトを作成しようとしていますが、変更するスクリプトの構造は次のとおりです。
class SomethingRecord(Record):
description = 'This records something'
author = 'john smith'
ast
を使用してdescription
行番号を検索し、コードを使用して、行番号に基づいた新しい説明文字列で元のファイルを変更します。ここまでは順調ですね。
現在、唯一の問題はdescription
であり、複数行の文字列である場合があります。
description = ('line 1'
'line 2'
'line 3')
または
description = 'line 1' \
'line 2' \
'line 3'
そして、私は最初の行の行番号だけを持っており、次の行は持っていません。だから私の1行の代替品は
description = 'new value'
'line 2' \
'line 3'
そしてコードは壊れています。 description
割り当ての開始行番号と終了行数/行数の両方がわかっていれば、そのような状況を処理するためにコードを修復できると思いました。 Python標準ライブラリでそのような情報を取得するにはどうすればよいですか?
私は他の答えを見ました。あなたの本当の問題がコードの変更の1つであるとき、人々は行番号の計算の問題を回避するためにバックフリップをしているようです。これは、ベースライン機構が本当に必要な方法を支援していないことを示しています。
プログラム変換システム(PTS) を使用すると、このナンセンスの多くを回避できます。
優れたPTSは、ソースコードをASTに解析し、ソースレベルの書き換えルールを適用してASTを変更し、最後に変更されたASTをソーステキストに変換します。一般的にPTS基本的にこの形式の変換ルールを受け入れます。
if you see *this*, replace it by *that*
[ASTはPTSではありません。このようなルールは許可されていません。アドホックコードを記述してツリーをハッキングすることはできますが、通常はかなり厄介です。実行しないでください。 ASTソーステキストの再生成を行います。]
(私のPTS、バイオを参照、呼ばれる)DMSはこれを達成できるPTSです。 OPの特定の例は、次の書き換えルールを使用して簡単に実行できます。
source domain Python; -- tell DMS the syntax of pattern left hand sides
target domain Python; -- tell DMS the syntax of pattern right hand sides
rule replace_description(e: expression): statement -> statement =
" description = \e "
->
" description = ('line 1'
'line 2'
'line 3')";
1つの変換ルールには、定義する可能性のある他のすべてのルールと区別するために、名前replace_descriptionが付けられています。ルールパラメータ(e:式)は、パターンがソース言語で定義された任意の式を許可することを示します。 statement-> statementは、ルールがソース言語のステートメントをターゲット言語のステートメントにマップすることを意味します。 DMSに提供されているPython文法)の他の構文カテゴリを使用できます。ここで使用されている "はmetaquote、ルール言語の構文をサブジェクト言語の構文と区別するために使用されます.2番目の->ソースパターンthisをターゲットパターンthatから分離します。
行番号を記載する必要がないことに気付くでしょう。 PTSは、ソースファイルの解析に使用されたのと同じパーサーでパターンを実際に解析することにより、ルールサーフェス構文を対応するASTに変換します。パターン用に生成されたASTは、パターンの一致/置換を行うために使用されます。これはASTから駆動されるため、元のコードの実際のレイアウト(間隔、改行、コメント)は、DMSの一致または置換の機能に影響を与えません。コメントはツリーノードではなくツリーノードに添付されるため、マッチングの問題にはなりません。それらは変換されたプログラムに保存されます。 DMSは、すべてのツリー要素の行と正確な列情報をキャプチャします。変換を実装する必要はありません。コードレイアウトも、その行/列情報を使用して、DMSによる出力に保持されます。
他のPTSは、一般的に同様の機能を提供します。
回避策として、以下を変更できます。
description = 'line 1' \
'line 2' \
'line 3'
に:
description = 'new value'; tmp = 'line 1' \
'line 2' \
'line 3'
等.
これは単純な変更ですが、実際には醜いコードが生成されます。
実際、必要な情報はast
に保存されていません。必要なものの詳細はわかりませんが、標準ライブラリのtokenize
モジュールを使用できるようです。アイデアは、すべての論理PythonステートメントがNEWLINE
トークンで終了するということです(セミコロンの場合もありますが、私が理解しているように、そうではありません)。これをテストしました。そのようなファイルでのアプローチ:
# first comment
class SomethingRecord:
description = ('line 1'
'line 2'
'line 3')
class SomethingRecord2:
description = ('line 1',
'line 2',
# comment in the middle
'line 3')
class SomethingRecord3:
description = 'line 1' \
'line 2' \
'line 3'
whatever = 'line'
class SomethingRecord3:
description = 'line 1', \
'line 2', \
'line 3'
# last comment
そして、これが私が提案することです:
import tokenize
from io import BytesIO
from collections import defaultdict
with tokenize.open('testmod.py') as f:
code = f.read()
enc = f.encoding
rl = BytesIO(code.encode(enc)).readline
tokens = list(tokenize.tokenize(rl))
token_table = defaultdict(list) # mapping line numbers to token numbers
for i, tok in enumerate(tokens):
token_table[tok.start[0]].append(i)
def find_end(start):
i = token_table[start][-1] # last token number on the start line
while tokens[i].exact_type != tokenize.NEWLINE:
i += 1
return tokens[i].start[0]
print(find_end(3))
print(find_end(8))
print(find_end(15))
print(find_end(21))
これは印刷されます:
5
12
17
23
これは正しいようです。正確に必要なものに応じて、このアプローチを調整できます。 tokenize
はast
よりも冗長ですが、柔軟性もあります。もちろん、最善のアプローチは、タスクのさまざまな部分に両方を使用することです。
EDIT: Python 3.4でこれを試しましたが、他のバージョンでも機能するはずです。
私の解決策は別の道をたどります:別のファイルのコードを変更する必要があるとき、ファイルを開いてその行を見つけ、最初の行よりも深いインデントを持つ次のすべての行を取得し、最初の行の行番号を返します。より深く。探していたテキストが見つからなかった場合は、None、Noneを返します。もちろんこれは不完全ですが、私はあなたを通り抜けるのに十分だと思います:)
def get_all_indented(text_lines, text_in_first_line):
first_line = None
indent = None
for line_num in range(len(text_lines)):
if indent is not None and first_line is not None:
if not text_lines[line_num].startswith(indent):
return first_line, line_num # First and last lines
if text_in_first_line in text_lines[line_num]:
first_line = line_num
indent = text_lines[line_num][:text_lines[line_num].index(text_in_first_line)] + ' ' # At least 1 more space.
return None, None
これにうまく対処する新しいasttokens
ライブラリがあります: https://github.com/gristlabs/asttokens
import ast, asttokens
code = '''
class SomethingRecord(object):
desc1 = 'This records something'
desc2 = ('line 1'
'line 2'
'line 3')
desc3 = 'line 1' \
'line 2' \
'line 3'
author = 'john smith'
'''
atok = asttokens.ASTTokens(code, parse=True)
assign_values = [n.value for n in ast.walk(atok.tree) if isinstance(n, ast.Assign)]
replacements = [atok.get_text_range(n) + ("'new value'",) for n in assign_values]
print(asttokens.util.replace(atok.text, replacements))
を生成します
class SomethingRecord(object):
desc1 = 'new value'
desc2 = ('new value')
desc3 = 'new value'
author = 'new value'