使用 M2Crypto 的代码在检查时出现 _shutdown AttributeError(已忽略)

9 投票
4 回答
840 浏览
提问于 2025-04-17 01:26

我正在这样运行代码检查工具:

$ python -m pylint.lint m2test.py

使用的代码是:

import M2Crypto
def f():
    M2Crypto.RSA.new_pub_key("").as_pem(cipher=None).split("\n")

代码检查的输出结果是:

Exception AttributeError: '_shutdown' in <module 'threading' from '/usr/lib/python2.7/site-packages/M2Crypto-0.21.1-py2.7-linux-x86_64.egg/M2Crypto/threading.pyc'> ignored

这段代码运行得很好(上面的例子其实是个简单的测试;但完整版本也能正常工作)。虽然有个异常被忽略了,但Bitten认为这是个错误,所以在这一步就停止了。

我尝试在函数定义的周围加上'M2Crypto.threading.init()'和'M2Crypto.threading.cleanup()',但这样并没有解决问题。

我该如何避免这个问题呢?

我使用的是M2Crypto 0.21.1,pylint 0.24和Python 2.7(也试过2.7.2),在Debian Lenny x86_64上运行。

4 个回答

1

这看起来更像是一种小技巧,不过我觉得这样做是有效的。就是把“as_pem()”的结果复制下来,然后进行分割。

import M2Crypto
def f():
    M2Crypto.RSA.new_pub_key("").as_pem(cipher=None)[:].split("\n")

我使用的是Python 2.6.7,M2Crypto 0.21.1,pylint 0.23

3

非常感谢Brandon Craig Rhodes找到这个问题,并写了这么详细的帖子。

我已经从astng中删除了有问题的那一行代码,这个代码可以在hg仓库里找到,直到logilab-astng 0.23.0发布为止。我可以确认,这样做解决了原作者的问题。

16

你看到的这个错误是因为 astng 这个包里的一个bug造成的(可能是“抽象语法树,下一代”?),这个包是 pylint 依赖的工具包,也是由同一组人开发的。顺便提一下,我总是鼓励大家尽量使用 pyflakes,因为它快速、简单、效率高且结果可预测,而 pylint 则尝试做一些复杂的事情,不仅慢,而且容易出问题。:)

以下是这两个包在PyPI上的链接:

http://pypi.python.org/pypi/pylint

http://pypi.python.org/pypi/astng

需要注意的是,这个问题一定是 pylint 的bug,而不是你代码的问题,因为 pylint 在生成报告时并不会运行你的代码——想象一下如果它真的运行了会造成多大的混乱(因为被检查的代码可能会删除文件等等)!由于你的代码并没有被执行,所以无论你多么小心,比如用线程的 init()cleanup() 函数来保护调用,都无法避免这个错误——除非这些代码片段因为其他原因改变了我们即将调查的行为。

接下来,我们来看看你遇到的具体异常。

我之前从未听说过 _shutdown!快速搜索了一下Python标准库,发现它在 threading.py 中有定义,但没有从任何地方调用这个函数;只有通过搜索Python的C源代码,我才发现它在 pythonrun.c 中的解释器关闭时被调用:

static void
wait_for_thread_shutdown(void)
{
    ...
    PyObject *threading = PyMapping_GetItemString(tstate->interp->modules,
                                                  "threading");
    if (threading == NULL) {
        /* threading not imported */
        PyErr_Clear();
        return;
    }
    result = PyObject_CallMethod(threading, "_shutdown", "");
    if (result == NULL) {
        PyErr_WriteUnraisable(threading);
    }
    ...
}

显然,这是一个清理函数,threading标准库模块需要它,他们特别处理了Python解释器本身,以确保它被调用。

从上面的代码可以看出,Python在程序运行期间安静地处理了 threading 模块未被导入的情况。但如果 threading 被导入,并且在关闭时仍然存在,那么解释器会查找 _shutdown 函数,如果找不到就会打印错误信息——并返回一个非零的退出状态,这就是你遇到问题的原因。

所以我们需要找出为什么在 pylint 完成检查你的程序并且Python即将退出时,threading 模块存在但没有 _shutdown 方法。我们需要进行一些调查。我们能否在 pylint 退出时打印出模块的样子?可以的!pylint/lint.py 模块在最后几行运行它的“主程序”,通过实例化一个它定义的 Run 类:

if __name__ == '__main__':
    Run(sys.argv[1:])

于是我在编辑器中打开了 lint.py——在Python虚拟环境中安装每个小项目的一个好处就是我可以随意编辑第三方代码进行快速实验——并在 Run 类的 __init__() 方法的底部添加了以下 print 语句:

    sys.path.pop(0)
    print "*****", sys.modules['threading'].__file__  # added by me!
    if exit:
        sys.exit(self.linter.msg_status)

我重新运行了命令:

python -m pylint.lint m2test.py

结果打印出了 threading 模块的 __file__ 字符串:

***** /home/brandon/venv/lib/python2.7/site-packages/M2Crypto/threading.pyc

哎呀,看看这个。

这就是问题所在!

根据这个路径,实际上存在一个 M2Crypto/threading.py 模块,在所有正常情况下,它应该被称为 M2Crypto.threading,因此在 sys.modules 字典中以这个名字存在:

sys.modules['M2Crypto.threading']

但不知怎么的,这个文件也被加载为主Python threading 模块,覆盖了标准库中的官方 threading 模块。因此,Python退出逻辑正确地抱怨缺少标准库的 _shutdown() 函数。

这怎么会发生呢?顶级模块只能出现在 sys.path 中明确列出的路径中,而不能在它们下面的子目录中。这引出了一个新问题:在 pylint 运行期间,…/M2Crypto/ 目录是否被放在 sys.path 中,就好像它包含顶级模块一样?让我们看看!

我们需要更多的调查:我们需要让Python在 sys.path 中出现带有 M2Crypto 名称的目录时告诉我们。这会让事情变得很慢,但让我们在 pylint__init__.py 中添加一个跟踪函数——因为这是运行 -m pylint.lint 时第一个被导入的模块——它会写入一个输出文件,告诉我们每执行一行代码时,sys.path 是否有任何不好的值:

def install_tracer():
    import sys
    output = open('mytracer.out', 'w')
    def mytracer(frame, event, arg):
        broken = any(p.endswith('M2Crypto') for p in sys.path)
        output.write('{} {}:{} {}\n'.format(
                broken, frame.f_code.co_filename, frame.f_lineno, event))
        return mytracer
    sys.settrace(mytracer)

install_tracer()
del install_tracer

注意我在这里是多么小心:我只在模块的命名空间中定义了一个名称,然后在让 pylint 继续加载之前小心地删除它,以清理自己!而且跟踪函数本身需要的所有资源——即 sys 模块和 output 打开的文件——都在 install_tracer() 闭包中,这样,从外部看,pylint 看起来和往常一样。以防有人试图检查它,就像 pylint 可能会做的那样!

这生成了一个大约800k行的文件 mytracer.out,每行看起来像这样:

False /home/brandon/venv/lib/python2.7/posixpath.py:118 call

其中 False 表示 sys.path 看起来是干净的,文件名和行号是正在执行的代码行,而 call 表示解释器当前的执行阶段。

那么 sys.path 是否被污染了呢?让我们看看每行的第一个 TrueFalse,看看每个值开头的连续行有多少:

$ awk '{print$1}' mytracer.out | uniq -c
 607997 False
   3173 True
   4558 False
  33217 True
   4304 False
  41699 True
   2953 False
 110503 True
  52575 False

哇!这可真是个问题!在几千行的运行中,我们的测试案例是 True,这意味着解释器在运行时,…/M2Crypto/——或者包含 M2Crypto 的某个路径变体——在路径中,而它不应该在;只有包含 …/M2Crypto 的目录应该在路径中。寻找文件中第一个 FalseTrue 的转换,我看到了这个:

False /home/brandon/venv/lib/python2.7/site-packages/logilab/astng/builder.py:132 line
False /home/brandon/venv/lib/python2.7/posixpath.py:118 call
...
False /home/brandon/venv/lib/python2.7/posixpath.py:124 line
False /home/brandon/venv/lib/python2.7/posixpath.py:124 return
True /home/brandon/venv/lib/python2.7/site-packages/logilab/astng/builder.py:133 line

查看 builder.py 文件的132和133行,揭示了我们的罪魁祸首:

130    # build astng representation
131    try:
132        sys.path.insert(0, dirname(path)) # XXX (syt) iirk
133        node = self.string_build(data, modname, path)
134    finally:
135        sys.path.pop(0)

注意这个注释,它是原始代码的一部分,而不是我自己的添加!显然,XXX (syt) iirk 是这个程序员奇怪母语中的感叹词,意思是“把这个模块的父目录放到 sys.path 中,这样每当有人强迫 pylint 检查一个有 threading 子模块的包时,pylint 就会神秘地崩溃。”显然,这是一个非常简洁的母语。 :)

如果你调整跟踪模块来监视 sys.modulesthreading 的实际导入——这个练习我留给读者——你会发现它发生在 SocketServer 被其他标准库模块导入时,它又试图无辜地导入 threading

那么我们来回顾一下发生了什么:

  1. pylint 是一种危险的魔法。
  2. 作为它魔法的一部分,如果它看到你 import foo,它就会去磁盘上寻找 foo.py,解析它,并预测你是否从它的命名空间中加载了有效或无效的名称。
  3. [见我下面的评论。] 因为你对 RSA.as_pem() 的返回值调用了 .split()pylint 尝试检查 as_pem() 方法,而这个方法又使用了 M2Crypto.BIO 模块,这又导致 pylint 导入了 threading
  4. 作为加载任何模块 foo.py 的一部分,pylint 会将包含 foo.py 的目录放入 sys.path即使该目录在一个包内部,因此在分析期间给该目录中的模块覆盖标准库中同名模块的特权。
  5. 当Python退出时,它对 M2Crypto.threading 库坐在 threading 应该在的位置感到不满,因为它想运行 threading_shutdown() 方法。

你应该把这个报告作为bug提交给 pylint / astng 的开发者,告诉他们是我推荐的你。

如果你决定在遇到这个问题后继续使用 pylint,那么在这种情况下似乎有两个解决方案:要么不检查调用 M2Crypto 的代码,要么在 pylint 导入过程中导入 threading——例如通过将 import threading 放入 pylint/__init__.py 中——这样模块就有机会在 pylint 兴奋地尝试让 M2Crypto/threading.py 占据位置之前,先抓住 sys.modules['threading'] 的位置。

最后,我觉得 astng 的作者说得最好:XXX (syt) iirk。确实如此。

撰写回答