Diff python 文件,忽略行结束样式、缩进样式和尾随空格

Diff python files, ignoring line ending styles, indentation styles and trailing spaces

TL-DR获取两个 python 文件的 'functionally' 差异

我正在编写一个插件框架,它将在 unix、mac 和 windows 上 运行。 一方面,我需要检查两个文件夹中的文件在功能上是否相同 python 代码,以便删除冗余。 现在我知道“将文件 a 运行 与文件 b 的结果相同”是一个既棘手又愚蠢的问题。 我想要的是检查文件 a 和文件 b 是否包含相同的代码,同时忽略:

如果可能的话:

如果返回差异表示,我会更喜欢它,但是“不匹配”信息和第一个不匹配的行号就足够了。如果使用外部实用程序,它们需要在各自的系统上是标准的,或者是免费的、轻量级的和可移植的(这样我就可以将它们包含在我的可移植框架中)。

目前我 运行在 python 3:

##  test if two files are the same (spare for line-endings)
def cmp_lines(path_1, path_2, skip_blanklines=True, skip_trailing=True, skip_leading=False, spaces_per_tab=4, comp_indent=False):
    l1 = l2 = True
    with open(path_1, 'rU') as f1, open(path_2, 'rU') as f2:
        ind1, ind2 = [0],[0]
        while l1 and l2:
            l1 = f1.readline()
            l2 = f2.readline()
            # ueberarbeiten: trailing whitespaces entfernen.
            if skip_trailing: l1, l2 = l1.rstrip(), l2.rstrip()
            # indentation testen (entfernt auch leading-whitespaces)
            #-  hier werden unter-indentierungen zb in mehrzeiligen listen
            #-  als normale indentierungen behandelt
            if comp_indent:
                l1b, l2b = l1.lstrip(), l2.lstrip()
                i1, i2 = l1[:len(l1)-len(l1b)], l2[:len(l2)-len(l2b)]
                ind1b = len(i1)*[1, spaces_per_tab][i1=="\t"*len(i1)]
                ind2b = len(i2)*[1, spaces_per_tab][i2=="\t"*len(i2)]
                while ind1b < ind1[-1]: ind1.pop()
                while ind2b < ind2[-1]: ind2.pop()
                if ind1[-1]<ind1b: ind1.append(ind1b)
                if ind2[-1]<ind2b: ind2.append(ind2b)
                if len(ind1)!=len(ind2): print("indentation missmatch")
                l1, l2 = l1b, l2b
            # ueberarbeiten: leading whitespaces entfernen.
            elif skip_leading: l1, l2 = l1.lstrip(), l2.lstrip()
            if l1 != l2:
                #print('a',l1,'-a',l1=='',l1=='\n',l1=='\r\n',l1=='\r')
                #print('b',l2,'-b',l2=='',l2=='\n',l2=='\r\n',l2=='\r')
                if skip_blanklines: # ueberarbeiten. kann bisher nur einen skip
                    if l1 == '\n':
                        l1b=f1.readline()
                        if l1b==l2: continue
                    if l2 == '\n':
                        l2b=f2.readline()
                        if l2b==l1: continue
                return False
    return True

这两个代码应该相等(\t 代表制表符,\r 代表 CR,\n 代表 LF)

if True:  \r\n
    \n
    print('HI')\r
if True:\n
\tprint('HI')\n

Get 'functionally' diff of two python files

据我所知,除了解析 python 之外没有其他方法可以做到这一点。 原因是有时空间很重要,有时却不重要。看上下文,不解析就不知道上下文

例如,以下之间存在“功能”差异:

a_string = """foo
bar"""

a_string = """foo
     bar"""

尽管这只是缩进差异

解析应该不是你自己做的。您可能想要嵌入一个已经存在的 python 解析器。但这可能需要很多工作。

如果解析不适合您,您可能希望使用完全不关心空格的降级比较版本(类似于 diff -w 的东西)。这是我的尝试:

from collections import OrderedDict

class no_space_file_reader :
    def __init__(self, filepath):
        self.file = open(filepath)

    def all_chars(self):
        last_is_space = False
        for line in self.file.readlines():
            for char in line:
                if char in " \t\r\n":
                    if last_is_space :
                        continue
                    else:
                        last_is_space = True
                else :
                    last_is_space = False
                    yield char

a = no_space_file_reader("a.txt")
b = no_space_file_reader("b.txt")
for c_a,c_b in zip(a.all_chars(), b.all_chars()):
    if c_a != c_b:
       print("diff")

当然看不出有什么区别

a_string = m("")

a_string = m(" ")

这很不酷。但这是不解析的代价。

I would prefer it if a diff-representation would be returned

这也很棘手。但至少可行。这是我的全部尝试:

from collections import OrderedDict

class no_space_file_reader :
    def __init__(self, filepath):
        self.file = open(filepath)
        self.context_size = 2
        self.context = OrderedDict()

    def all_chars(self):
        last_was_space = False
        for lineid,line in enumerate(self.file.readlines()):
            self.context[lineid] = line.strip()
            if len(self.context) > self.context_size:
                self.context.popitem(last=False)
            for char in line:
                if char in " \t\r\n":
                    if last_was_space :
                        continue
                    else:
                        last_was_space = True
                else :
                    last_was_space = False
                    yield char

class diff_agglomerator :
    def __init__(self):
        self.diff = [{},{}]
        self.context_size = 2

    def append(self, contexts):
        self.diff[0].update(contexts[0])
        self.diff[1].update(contexts[1])

    def pop_and_format_diff_if_ended(self, current_lines):
        if self.is_empty():
            return ""
        last_lines = [max(self.diff[0].keys()), max(self.diff[1].keys())]
        toReturn =""
        if last_lines[0] < current_lines[0] - self.context_size and \
           last_lines[1] < current_lines[1] - self.context_size:
              toReturn = self.pop_and_format_diff()
        return toReturn

    def format_line(self, a_dict):
        return "\n".join(["{} :{}".format(k,v) for k,v in a_dict])

    def pop_and_format_diff(self):
        toReturn = ">"*5 + "\n"
        toReturn += self.format_line(self.diff[0].items()) + "\n"
        toReturn += "="*5 + "\n"
        toReturn += self.format_line(self.diff[1].items()) + "\n"
        toReturn += "<"*5 + "\n"
        self.diff = [{},{}]
        return toReturn

    def is_empty(self):
        return len(self.diff[0]) == 0 and len (self.diff[1]) == 0

def print_if_non_empty(a_string):
    if len(a_string)>0:
        print(a_string)
        
a = no_space_file_reader("a.txt")
b = no_space_file_reader("b.txt")

diff = diff_agglomerator()
for c_a,c_b in zip(a.all_chars(), b.all_chars()):
    print(c_a,c_b)
    if c_a != c_b:
        diff.append([a.context, b.context])
    else:
        first_context_lines = [min(a.context.keys()), min(b.context.keys())]
        print_if_non_empty(diff.pop_and_format_diff_if_ended(first_context_lines))
print_if_non_empty(diff.pop_and_format_diff())

它会产生那种结果:

>>>>>
4 :
5 :foo
6 :bar
=====
3 :
4 :bar  foo
<<<<<

>>>>>
18 :
19 :foo
20 :bar
=====
16 :
17 :bar foo
<<<<<