如何使用 Python 将更改应用于源文件?
How can I apply changes to source files with Python?
我正在使用 Clang 编译器 (cindex) 的 Python 绑定重构 C++ 代码。使用它,我分析 AST 并准备更改。我最终得到类似于以下的操作列表:
DELETE line 10
INSERT line 5 column 32: << "tofu"
REPLACE from line 31 colum 6 to line 33 column 82 with: std::cout << "Thanks SO"
...
我的问题是如何将这些变成实际的文件更改。
直接用 python 做似乎很乏味:补丁需要以正确的顺序应用并检查一致性。看起来挺难的,而且容易出错。
我也找不到一个好的库来提供帮助(clang 确实有一个叫做 Rewriter 的东西,但它没有包含在 Python 中。如果可能的话,我真的很想避免使用 C++ 进行重构).
也许一个想法是生成补丁并用 git 应用它们,也许吧?但这似乎有点乏味。
有什么想法吗?
所以我推出了自己的。该代码几乎可以肯定是错误的并且不是很漂亮,但我将其发布是希望它能对某人有所帮助,直到找到更好的解决方案。
class PatchRecord(object):
""" Record patches, validate them, order them, and apply them """
def __init__(self):
# output of readlines for each patched file
self.lines = {}
# list of patches for each patched file
self.patches = {}
class Patch(object):
""" Abstract base class for editing operations """
def __init__(self, filename, start, end):
self.filename = filename
self.start = start
self.end = end
def __repr__(self):
return "{op}: {filename} {start}/{end} {what}".format(
op=self.__class__.__name__.upper(),
filename=self.filename,
start=format_place(self.start),
end=format_place(self.end),
what=getattr(self, "what", ""))
def apply(self, lines):
print "Warning: applying no-op patch"
class Delete(Patch):
def __init__(self, filename, extent):
super(PatchRecord.Delete, self).__init__(
filename, extent.start, extent.end)
print "DELETE: {file} {extent}".format(file=self.filename,
extent=format_extent(extent))
def apply(self, lines):
lines[self.start.line - 1:self.end.line] = [
lines[self.start.line - 1][:self.start.column - 1] +
lines[self.end.line - 1][self.end.column:]]
class Insert(Patch):
def __init__(self, filename, start, what):
super(PatchRecord.Insert, self).__init__(filename, start, start)
self.what = what
print "INSERT {where} {what}".format(what=what, where=format_place(self.start))
def apply(self, lines):
line = lines[self.start.line - 1]
lines[self.start.line - 1] = "%s%s%s" % (
line[:self.start.column],
self.what,
line[self.start.column:])
class Replace(Patch):
def __init__(self, filename, extent, what):
super(PatchRecord.Replace, self).__init__(
filename, extent.start, extent.end)
self.what = what
print "REPLACE: {where} {what}".format(what=what,
where=format_extent(extent))
def apply(self, lines):
lines[self.start.line - 1:self.end.line] = [
lines[self.start.line - 1][:self.start.column - 1] +
self.what +
lines[self.end.line - 1][self.end.column - 1:]]
# Convenience functions for creating patches
def delete(self, filename, extent):
self.patches[filename] = self.patches.get(
filename, []) + [self.Delete(filename, extent)]
def insert(self, filename, where, what):
self.patches[filename] = self.patches.get(
filename, []) + [self.Insert(filename, where, what)]
def replace(self, filename, extent, what):
self.patches[filename] = self.patches.get(
filename, []) + [self.Replace(filename, extent, what)]
def _pos_to_tuple(self, position):
""" Convert a source location to a tuple for use as a sorting key """
return (position.line, position.column)
def sort(self, filename):
""" Sort patches by extent start """
self.patches[filename].sort(key=lambda p: self._pos_to_tuple(p.start))
def validate(self, filename):
"""Try to insure patches are consistent"""
print "Checking patches for %s" % filename
self.sort(filename)
previous = self.patches[filename][0]
for p in self.patches[filename][1:]:
assert(self._pos_to_tuple(p.start) >
self._pos_to_tuple(previous.start))
def _apply(self, filename):
self.sort(filename)
lines = self._getlines(filename)
for p in reversed(self.patches[filename]):
print p
p.apply(lines)
def _getlines(self, filename):
""" Get source file lines for editing """
if not filename in self.lines:
with open(filename) as f:
self.lines[filename] = f.readlines()
return self.lines[filename]
def apply(self):
for filename in self.patches:
self.validate(filename)
self._apply(filename)
# with open(filename+".patched","w") as output:
with open(filename, "w") as output:
output.write("".join(self._getlines(filename)))
只需创建一个 PatchRecord
对象,使用 create
、replace
和 delete
方法添加更改,并在您使用 apply
时应用它们准备好了。
我正在使用 Clang 编译器 (cindex) 的 Python 绑定重构 C++ 代码。使用它,我分析 AST 并准备更改。我最终得到类似于以下的操作列表:
DELETE line 10
INSERT line 5 column 32: << "tofu"
REPLACE from line 31 colum 6 to line 33 column 82 with: std::cout << "Thanks SO"
...
我的问题是如何将这些变成实际的文件更改。
直接用 python 做似乎很乏味:补丁需要以正确的顺序应用并检查一致性。看起来挺难的,而且容易出错。
我也找不到一个好的库来提供帮助(clang 确实有一个叫做 Rewriter 的东西,但它没有包含在 Python 中。如果可能的话,我真的很想避免使用 C++ 进行重构).
也许一个想法是生成补丁并用 git 应用它们,也许吧?但这似乎有点乏味。
有什么想法吗?
所以我推出了自己的。该代码几乎可以肯定是错误的并且不是很漂亮,但我将其发布是希望它能对某人有所帮助,直到找到更好的解决方案。
class PatchRecord(object):
""" Record patches, validate them, order them, and apply them """
def __init__(self):
# output of readlines for each patched file
self.lines = {}
# list of patches for each patched file
self.patches = {}
class Patch(object):
""" Abstract base class for editing operations """
def __init__(self, filename, start, end):
self.filename = filename
self.start = start
self.end = end
def __repr__(self):
return "{op}: {filename} {start}/{end} {what}".format(
op=self.__class__.__name__.upper(),
filename=self.filename,
start=format_place(self.start),
end=format_place(self.end),
what=getattr(self, "what", ""))
def apply(self, lines):
print "Warning: applying no-op patch"
class Delete(Patch):
def __init__(self, filename, extent):
super(PatchRecord.Delete, self).__init__(
filename, extent.start, extent.end)
print "DELETE: {file} {extent}".format(file=self.filename,
extent=format_extent(extent))
def apply(self, lines):
lines[self.start.line - 1:self.end.line] = [
lines[self.start.line - 1][:self.start.column - 1] +
lines[self.end.line - 1][self.end.column:]]
class Insert(Patch):
def __init__(self, filename, start, what):
super(PatchRecord.Insert, self).__init__(filename, start, start)
self.what = what
print "INSERT {where} {what}".format(what=what, where=format_place(self.start))
def apply(self, lines):
line = lines[self.start.line - 1]
lines[self.start.line - 1] = "%s%s%s" % (
line[:self.start.column],
self.what,
line[self.start.column:])
class Replace(Patch):
def __init__(self, filename, extent, what):
super(PatchRecord.Replace, self).__init__(
filename, extent.start, extent.end)
self.what = what
print "REPLACE: {where} {what}".format(what=what,
where=format_extent(extent))
def apply(self, lines):
lines[self.start.line - 1:self.end.line] = [
lines[self.start.line - 1][:self.start.column - 1] +
self.what +
lines[self.end.line - 1][self.end.column - 1:]]
# Convenience functions for creating patches
def delete(self, filename, extent):
self.patches[filename] = self.patches.get(
filename, []) + [self.Delete(filename, extent)]
def insert(self, filename, where, what):
self.patches[filename] = self.patches.get(
filename, []) + [self.Insert(filename, where, what)]
def replace(self, filename, extent, what):
self.patches[filename] = self.patches.get(
filename, []) + [self.Replace(filename, extent, what)]
def _pos_to_tuple(self, position):
""" Convert a source location to a tuple for use as a sorting key """
return (position.line, position.column)
def sort(self, filename):
""" Sort patches by extent start """
self.patches[filename].sort(key=lambda p: self._pos_to_tuple(p.start))
def validate(self, filename):
"""Try to insure patches are consistent"""
print "Checking patches for %s" % filename
self.sort(filename)
previous = self.patches[filename][0]
for p in self.patches[filename][1:]:
assert(self._pos_to_tuple(p.start) >
self._pos_to_tuple(previous.start))
def _apply(self, filename):
self.sort(filename)
lines = self._getlines(filename)
for p in reversed(self.patches[filename]):
print p
p.apply(lines)
def _getlines(self, filename):
""" Get source file lines for editing """
if not filename in self.lines:
with open(filename) as f:
self.lines[filename] = f.readlines()
return self.lines[filename]
def apply(self):
for filename in self.patches:
self.validate(filename)
self._apply(filename)
# with open(filename+".patched","w") as output:
with open(filename, "w") as output:
output.write("".join(self._getlines(filename)))
只需创建一个 PatchRecord
对象,使用 create
、replace
和 delete
方法添加更改,并在您使用 apply
时应用它们准备好了。