Skip to content

Commit d81f9e2

Browse files
authored
bpo-30264: ExpatParser now closes the source (#1476)
ExpatParser.parse() of xml.sax.xmlreader now closes the source: close the file object or the urllib object if source is a string (not an open file-like object). Add test_parse_close_source() unit test.
1 parent fd6094c commit d81f9e2

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

Lib/test/test_sax.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# $Id$
33

44
from xml.sax import make_parser, ContentHandler, \
5-
SAXException, SAXReaderNotAvailable, SAXParseException
5+
SAXException, SAXReaderNotAvailable, SAXParseException, \
6+
saxutils
67
try:
78
make_parser()
89
except SAXReaderNotAvailable:
@@ -173,6 +174,21 @@ def test_parse_InputSource(self):
173174
input.setEncoding('iso-8859-1')
174175
self.check_parse(input)
175176

177+
def test_parse_close_source(self):
178+
builtin_open = open
179+
non_local = {'fileobj': None}
180+
181+
def mock_open(*args):
182+
fileobj = builtin_open(*args)
183+
non_local['fileobj'] = fileobj
184+
return fileobj
185+
186+
with support.swap_attr(saxutils, 'open', mock_open):
187+
make_xml_file(self.data, 'iso-8859-1', None)
188+
with self.assertRaises(SAXException):
189+
self.check_parse(TESTFN)
190+
self.assertTrue(non_local['fileobj'].closed)
191+
176192
def check_parseString(self, s):
177193
from xml.sax import parseString
178194
result = StringIO()

Lib/xml/sax/expatreader.py

Lines changed: 22 additions & 3 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:
@@ -216,6 +223,17 @@ def feed(self, data, isFinal = 0):
216223
# FIXME: when to invoke error()?
217224
self._err_handler.fatalError(exc)
218225

226+
def _close_source(self):
227+
source = self._source
228+
try:
229+
file = source.getCharacterStream()
230+
if file is not None:
231+
file.close()
232+
finally:
233+
file = source.getByteStream()
234+
if file is not None:
235+
file.close()
236+
219237
def close(self):
220238
if (self._entity_stack or self._parser is None or
221239
isinstance(self._parser, _ClosedParser)):
@@ -235,6 +253,7 @@ def close(self):
235253
parser.ErrorColumnNumber = self._parser.ErrorColumnNumber
236254
parser.ErrorLineNumber = self._parser.ErrorLineNumber
237255
self._parser = parser
256+
self._close_source()
238257

239258
def _reset_cont_handler(self):
240259
self._parser.ProcessingInstructionHandler = \

0 commit comments

Comments
 (0)