bpo-30264: ExpatParser closes the source on error (#1451)
ExpatParser.parse() of xml.sax.xmlreader now always closes the source: close the file object or the urllib object if source is a string (not an open file-like object). The change fixes a ResourceWarning on parsing error. Add test_parse_close_source() unit test.
This commit is contained in:
parent
7186cc29be
commit
ef9c0e732f
|
@ -4,6 +4,7 @@
|
|||
from xml.sax import make_parser, ContentHandler, \
|
||||
SAXException, SAXReaderNotAvailable, SAXParseException
|
||||
import unittest
|
||||
from unittest import mock
|
||||
try:
|
||||
make_parser()
|
||||
except SAXReaderNotAvailable:
|
||||
|
@ -175,12 +176,8 @@ class ParseTest(unittest.TestCase):
|
|||
with self.assertRaises(SAXException):
|
||||
self.check_parse(BytesIO(xml_bytes(self.data, 'iso-8859-1', None)))
|
||||
make_xml_file(self.data, 'iso-8859-1', None)
|
||||
with support.check_warnings(('unclosed file', ResourceWarning)):
|
||||
# XXX Failed parser leaks an opened file.
|
||||
with self.assertRaises(SAXException):
|
||||
self.check_parse(TESTFN)
|
||||
# Collect leaked file.
|
||||
gc.collect()
|
||||
with self.assertRaises(SAXException):
|
||||
self.check_parse(TESTFN)
|
||||
with open(TESTFN, 'rb') as f:
|
||||
with self.assertRaises(SAXException):
|
||||
self.check_parse(f)
|
||||
|
@ -194,6 +191,21 @@ class ParseTest(unittest.TestCase):
|
|||
input.setEncoding('iso-8859-1')
|
||||
self.check_parse(input)
|
||||
|
||||
def test_parse_close_source(self):
|
||||
builtin_open = open
|
||||
fileobj = None
|
||||
|
||||
def mock_open(*args):
|
||||
nonlocal fileobj
|
||||
fileobj = builtin_open(*args)
|
||||
return fileobj
|
||||
|
||||
with mock.patch('xml.sax.saxutils.open', side_effect=mock_open):
|
||||
make_xml_file(self.data, 'iso-8859-1', None)
|
||||
with self.assertRaises(SAXException):
|
||||
self.check_parse(TESTFN)
|
||||
self.assertTrue(fileobj.closed)
|
||||
|
||||
def check_parseString(self, s):
|
||||
from xml.sax import parseString
|
||||
result = StringIO()
|
||||
|
|
|
@ -105,9 +105,16 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
|
|||
source = saxutils.prepare_input_source(source)
|
||||
|
||||
self._source = source
|
||||
self.reset()
|
||||
self._cont_handler.setDocumentLocator(ExpatLocator(self))
|
||||
xmlreader.IncrementalParser.parse(self, source)
|
||||
try:
|
||||
self.reset()
|
||||
self._cont_handler.setDocumentLocator(ExpatLocator(self))
|
||||
xmlreader.IncrementalParser.parse(self, source)
|
||||
except:
|
||||
# bpo-30264: Close the source on error to not leak resources:
|
||||
# xml.sax.parse() doesn't give access to the underlying parser
|
||||
# to the caller
|
||||
self._close_source()
|
||||
raise
|
||||
|
||||
def prepareParser(self, source):
|
||||
if source.getSystemId() is not None:
|
||||
|
@ -213,6 +220,17 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
|
|||
# FIXME: when to invoke error()?
|
||||
self._err_handler.fatalError(exc)
|
||||
|
||||
def _close_source(self):
|
||||
source = self._source
|
||||
try:
|
||||
file = source.getCharacterStream()
|
||||
if file is not None:
|
||||
file.close()
|
||||
finally:
|
||||
file = source.getByteStream()
|
||||
if file is not None:
|
||||
file.close()
|
||||
|
||||
def close(self):
|
||||
if (self._entity_stack or self._parser is None or
|
||||
isinstance(self._parser, _ClosedParser)):
|
||||
|
@ -232,14 +250,7 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
|
|||
parser.ErrorColumnNumber = self._parser.ErrorColumnNumber
|
||||
parser.ErrorLineNumber = self._parser.ErrorLineNumber
|
||||
self._parser = parser
|
||||
try:
|
||||
file = self._source.getCharacterStream()
|
||||
if file is not None:
|
||||
file.close()
|
||||
finally:
|
||||
file = self._source.getByteStream()
|
||||
if file is not None:
|
||||
file.close()
|
||||
self._close_source()
|
||||
|
||||
def _reset_cont_handler(self):
|
||||
self._parser.ProcessingInstructionHandler = \
|
||||
|
|
Loading…
Reference in New Issue