如何在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