Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.5] bpo-30264: ExpatParser closes the source on error (#1451) #1475

Merged
merged 1 commit into from May 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 18 additions & 6 deletions Lib/test/test_sax.py
Expand Up @@ -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:
Expand Down Expand Up @@ -175,12 +176,8 @@ def test_parse_bytes(self):
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)
Expand All @@ -194,6 +191,21 @@ def test_parse_InputSource(self):
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()
Expand Down
33 changes: 22 additions & 11 deletions Lib/xml/sax/expatreader.py
Expand Up @@ -105,9 +105,16 @@ def parse(self, source):
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:
Expand Down Expand Up @@ -213,6 +220,17 @@ def feed(self, data, isFinal = 0):
# 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)):
Expand All @@ -232,14 +250,7 @@ def close(self):
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 = \
Expand Down