TL;DR: The Standard Library fails to close a file when an exception is raised. I'm looking for the best way to handle this situation. Feel free to read from the paragraph beginning with "Upon closer inspection of CPython's source code". Also scroll down to the end of the question to grab a self-contained script that reproduces this issue on Windows.
I'm writing a Python package in which I use STL's ConfigParser
(2.x) or configparser
(3.x) to parse user config file (I will refer to both as ConfigParser
since the problem mainly lies in the 2.x implementation). From now on my relevant lines of code on GitHub will be linked when appropriate throughout. ConfigParser.ConfigParser.read(filenames)
(used in my code here) raises a ConfigParser.Error
exception when the config file is malformed. I have some code in my test suite targeting this situation, using unittest.TestCase.assertRaises(ConfigParser.Error)
. The malformed config file is properly generated with tempfile.mkstemp
(the returned fd is closed with os.close
before anything) and I attempt to remove the temp file with os.remove
.
The os.remove
is where trouble begins. My tests fail on Windows (while working on both OS X and Ubuntu) with Python 2.7 (see 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'
Note that as I said above, malformed_conf_file
is generated with tempfile.mkstemp
and immediately closed with os.close
, so the only time it is opened is when I call ConfigParser.ConfigParser.read([malformed_conf_file])
here inside the unittest.TestCase.assertRaises(ConfigParser.Error)
context. So the culprit seems to be the STL rather than my own code.
Upon closer inspection of CPython's source code, I found that ConfigParser.ConfigPaser.read
indeed doesn't close the file properly when an exception is raised. The read
method from 2.7 (here on CPython's Mercurial) has the following lines:
for filename in filenames:
try:
fp = open(filename)
except IOError:
continue
self._read(fp, filename)
fp.close()
read_ok.append(filename)
The exception (if any) is raised by self._read(fp, filename)
, but as you can see, if self._read
raises, then fp
won't be closed, since fp.close()
is only called after self._read
returns.
Meanwhile, the read
method from 3.4 (here) does not suffer from the same problem since this time they properly embedded file handling in a context:
for filename in filenames:
try:
with open(filename, encoding=encoding) as fp:
self._read(fp, filename)
except OSError:
continue
read_ok.append(filename)
So I think it's pretty clear that the problem is a defect in 2.7's STL. And what's the best way to handle this situation? Specifically:
- Is there anything I can do from my side to close that file?
- Is it worth reporting to bugs.python.org?
For now I guess I'll just add a try .. except OSError ..
to that os.remove
(any suggestions?).
Update: A self-contained script that can be used to reproduce this issue on 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()
When I run it with Python 2.7 interpreter:
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'