]> granicus.if.org Git - python/commitdiff
bpo-30264: ExpatParser closes the source on error (#1451) (#1474)
authorVictor Stinner <victor.stinner@gmail.com>
Fri, 5 May 2017 08:04:57 +0000 (10:04 +0200)
committerGitHub <noreply@github.com>
Fri, 5 May 2017 08:04:57 +0000 (10:04 +0200)
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 ef9c0e732fc50aefbdd7c5a80e04e14b31684e66)

Lib/test/test_sax.py
Lib/xml/sax/expatreader.py

index 2411895d9d12b1ce07f1cc27895e85fb5117d151..2eb62905ffa882ba04c181af54a51424e3132d4e 100644 (file)
@@ -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()
index 98b5ca953983c707f5497de896399a7e004931e8..421358fa5bc7f02023dd33c5ce1147f7b3a5e114 100644 (file)
@@ -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 = \