Skip to content

Commit 0c9aa6f

Browse files
authored
bpo-30264: ExpatParser closes the source on error (#1451) (#1475)
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. (cherry picked from commit ef9c0e7)
1 parent ee22948 commit 0c9aa6f

File tree

2 files changed

+40
-17
lines changed

2 files changed

+40
-17
lines changed

Lib/test/test_sax.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from xml.sax import make_parser, ContentHandler, \
55
SAXException, SAXReaderNotAvailable, SAXParseException
66
import unittest
7+
from unittest import mock
78
try:
89
make_parser()
910
except SAXReaderNotAvailable:
@@ -175,12 +176,8 @@ def test_parse_bytes(self):
175176
with self.assertRaises(SAXException):
176177
self.check_parse(BytesIO(xml_bytes(self.data, 'iso-8859-1', None)))
177178
make_xml_file(self.data, 'iso-8859-1', None)
178-
with support.check_warnings(('unclosed file', ResourceWarning)):
179-
# XXX Failed parser leaks an opened file.
180-
with self.assertRaises(SAXException):
181-
self.check_parse(TESTFN)
182-
# Collect leaked file.
183-
gc.collect()
179+
with self.assertRaises(SAXException):
180+
self.check_parse(TESTFN)
184181
with open(TESTFN, 'rb') as f:
185182
with self.assertRaises(SAXException):
186183
self.check_parse(f)
@@ -194,6 +191,21 @@ def test_parse_InputSource(self):
194191
input.setEncoding('iso-8859-1')
195192
self.check_parse(input)
196193

194+
def test_parse_close_source(self):
195+
builtin_open = open
196+
fileobj = None
197+
198+
def mock_open(*args):
199+
nonlocal fileobj
200+
fileobj = builtin_open(*args)
201+
return fileobj
202+
203+
with mock.patch('xml.sax.saxutils.open', side_effect=mock_open):
204+
make_xml_file(self.data, 'iso-8859-1', None)
205+
with self.assertRaises(SAXException):
206+
self.check_parse(TESTFN)
207+
self.assertTrue(fileobj.closed)
208+
197209
def check_parseString(self, s):
198210
from xml.sax import parseString
199211
result = StringIO()

Lib/xml/sax/expatreader.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,16 @@ def parse(self, source):
105105
source = saxutils.prepare_input_source(source)
106106

107107
self._source = source
108-
self.reset()
109-
self._cont_handler.setDocumentLocator(ExpatLocator(self))
110-
xmlreader.IncrementalParser.parse(self, source)
108+
try:
109+
self.reset()
110+
self._cont_handler.setDocumentLocator(ExpatLocator(self))
111+
xmlreader.IncrementalParser.parse(self, source)
112+
except:
113+
# bpo-30264: Close the source on error to not leak resources:
114+
# xml.sax.parse() doesn't give access to the underlying parser
115+
# to the caller
116+
self._close_source()
117+
raise
111118

112119
def prepareParser(self, source):
113120
if source.getSystemId() is not None:
@@ -213,6 +220,17 @@ def feed(self, data, isFinal = 0):
213220
# FIXME: when to invoke error()?
214221
self._err_handler.fatalError(exc)
215222

223+
def _close_source(self):
224+
source = self._source
225+
try:
226+
file = source.getCharacterStream()
227+
if file is not None:
228+
file.close()
229+
finally:
230+
file = source.getByteStream()
231+
if file is not None:
232+
file.close()
233+
216234
def close(self):
217235
if (self._entity_stack or self._parser is None or
218236
isinstance(self._parser, _ClosedParser)):
@@ -232,14 +250,7 @@ def close(self):
232250
parser.ErrorColumnNumber = self._parser.ErrorColumnNumber
233251
parser.ErrorLineNumber = self._parser.ErrorLineNumber
234252
self._parser = parser
235-
try:
236-
file = self._source.getCharacterStream()
237-
if file is not None:
238-
file.close()
239-
finally:
240-
file = self._source.getByteStream()
241-
if file is not None:
242-
file.close()
253+
self._close_source()
243254

244255
def _reset_cont_handler(self):
245256
self._parser.ProcessingInstructionHandler = \

0 commit comments

Comments
 (0)