编程语言
首页 > 编程语言> > 如何在Python标准库中关闭不正确的文件对象后进行清理(出现异常后)

如何在Python标准库中关闭不正确的文件对象后进行清理(出现异常后)

作者:互联网

TL; DR:引发异常时,标准库无法关闭文件.我正在寻找处理这种情况的最佳方法.随意阅读从“仔细检查CPython的源代码”开始的段落.也向下滚动到问题的末尾,以获取一个独立的脚本,该脚本在Windows上重现此问题.

我正在编写一个Python软件包,其中使用STL的ConfigParser(2.x)或configparser(3.x)来解析用户配置文件(由于问题主要出在2.x实现中,因此我将两者都称为ConfigParser) .从现在开始,将在适当的时候链接我在GitHub上的相关代码行.当配置文件格式错误时,ConfigParser.ConfigParser.read(filenames)(在我的代码here中使用)引发ConfigParser.Error异常.我在测试套件中使用unittest.TestCase.assertRaises(ConfigParser.Error)针对这种情况设置了some code.格式错误的配置文件是带有tempfile.mkstemp的properly generated(返回的fd先由os.close关闭),而我尝试使用os.remove到remove the temp file.

os.remove是麻烦开始的地方.我的测试在Windows(同时在OS X和Ubuntu上同时运行)和Python 2.7上失败(请参见this AppVeyor build):

Traceback (most recent call last):
  File "C:\projects\storyboard\tests\test_util.py", line 235, in test_option_reader
    os.remove(malformed_conf_file)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\\users\\appveyor\\appdata\\local\\temp\\1\\storyboard-test-3clysg.conf'

请注意,如上所述,malformed_conf_file由tempfile.mkstemp生成,并由os.close立即关闭,因此打开的唯一时间是当我在unittest.TestCase.assertRaises(ConfigParser.Error) context内部调用ConfigParser.ConfigParser.read([malformed_conf_file])here时.罪魁祸首似乎是STL,而不是我自己的代码.

在仔细检查CPython的源代码后,我发现ConfigParser.ConfigPaser.read确实在引发异常时无法正确关闭文件. 2.7(here on CPython’s Mercurial)中的read方法具有以下几行:

for filename in filenames:
    try:
        fp = open(filename)
    except IOError:
        continue
    self._read(fp, filename)
    fp.close()
    read_ok.append(filename)

self._read(fp,filename)引发异常(如果有),但是如您所见,如果self._read引发,则不会关闭fp,因为fp.close()仅在self之后调用. _read返回.

同时,从3.4(here)开始的读取方法不会遇到相同的问题,因为这一次它们正确地在上下文中嵌入了文件处理:

for filename in filenames:
    try:
        with open(filename, encoding=encoding) as fp:
            self._read(fp, filename)
    except OSError:
        continue
    read_ok.append(filename)

因此,我认为问题很明显是2.7的STL存在缺陷.而处理这种情况的最佳方法是什么?特别:

>我有什么办法可以关闭该文件?
>是否值得向bugs.python.org报告?

现在,我想我只想在os.remove上添加一个尝试..除了OSError ..(有什么建议吗?).

更新:可用于在Windows上重现此问题的自包含脚本:

#!/usr/bin/env python2.7
import ConfigParser
import os
import tempfile

def main():
    fd, path = tempfile.mkstemp()
    os.close(fd)
    with open(path, 'w') as f:
        f.write("malformed\n")
    config = ConfigParser.ConfigParser()
    try:
        config.read(path)
    except ConfigParser.Error:
        pass
    os.remove(path)

if __name__ == '__main__':
    main()

当我使用Python 2.7解释器运行它时:

Traceback (most recent call last):
  File ".\example.py", line 19, in <module>
    main()
  File ".\example.py", line 16, in main
    os.remove(path)
WindowsError: [Error 32] The process cannot access the file because it is being used by another process: 'c:\\users\\redacted\\appdata\\local\\temp\\tmp07yq2v'

解决方法:

这是一个有趣的问题.正如Lukas Graf在评论中指出的那样,问题似乎在于异常回溯对象持有对引发异常的调用帧的引用.该调用框架包括当时存在的局部变量,其中之一是对打开文件的引用.因此,该文件对象仍然具有对它的引用,并且没有正确关闭.

对于您的独立示例,只需删除try / except ConfigParser.Error“ works”:未捕获有关格式错误的配置文件的异常并停止程序.但是,在您的实际应用程序中,assertRaises正在捕获异常,以检查它是否是您要测试的异常.我不确定100%为什么即使在使用assertRaises块之后,回溯仍然存在,但显然可以.

对于您的示例,另一个更有希望的修复方法是将您的except子句中的传递更改为sys.exc_clear():

try:
    config.read(path)
except ConfigParser.Error:
    sys.exc_clear()

这将摆脱讨厌的追溯对象,并允许关闭文件.

但是,尚不清楚在实际的应用程序中该怎么做,因为令人讨厌的except子句位于unittest内部.我认为最简单的事情可能是不直接使用assertRaises.而是编写一个辅助函数来进行测试,检查所需的异常,使用sys.exc_clear()技巧进行清理,然后引发另一个自定义异常.然后在assertRaises中包装对该辅助方法的调用.这样,您就可以控制ConfigParser引发的有问题的异常,并可以正确清除它(哪个单元测试没有执行).

这是我的意思的草图:

# in your test method
assertRaises(CleanedUpConfigError, helperMethod, conf_file, malformed_conf_file)

# helper method that you add to your test case class
def helperMethod(self, conf_file, malformed_conf_file):
     gotRightError = False
     try:
          or6 = OptionReader(
               config_files=[conf_file, malformed_conf_file],
               section='sec',
          )
     except ConfigParser.Error:
          gotRightError = True
          sys.exc_clear()
     if gotRightError:
          raise CleanedUpConfigError("Config error was raised and cleaned up!")

当然,我实际上并未对此进行测试,因为我没有在您的代码中设置整个unittest.您可能需要稍微调整一下. (想一想,如果执行此操作,您甚至可能甚至不需要exc_clear(),因为既然异常处理程序现在位于单独的函数中,则应该在helperMethod退出时正确清除回溯.)但是,我认为这种想法可能带你去某个地方.基本上,您需要确保捕获到了此特定ConfigParser.Error的except子句是您编写的,以便可以在尝试删除测试文件之前对其进行清理.

附录:似乎如果上下文管理器处理异常,则回溯实际上将被存储,直到包含with块的函数结束为止,如以下示例所示:

class CM(object):
    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, tb):
        return True

def foo():
    with CM():
        raise ValueError
    print(sys.exc_info())

即使with块在打印发生时已经结束,所以应该完成异常处理,但是sys.exc_info仍会返回异常信息,就好像存在活动异常一样.这也是代码中发生的事情:with assertRaises块导致回溯一直持续到该函数的结尾,从而干扰了os.remove.这似乎是错误的行为,并且我注意到它在Python 3中不再能以这种方式工作(打印输出(无,无,无)),因此我想这是在Python 3中修复的疣.

基于此,我怀疑仅在os.remove之前(在with assertRaises块之后)插入sys.exc_clear()就足够了.

标签:python-2-7,exception-handling,python
来源: https://codeday.me/bug/20191120/2043832.html