From 03ab0772491c61cb7f2198b2c019c7eb7672a331 Mon Sep 17 00:00:00 2001 From: Paul Tremberth Date: Sat, 17 Sep 2016 01:36:56 +0200 Subject: [PATCH] Feed exporter: start exporting only on first item Fixes GH-872 --- scrapy/extensions/feedexport.py | 12 ++++++-- tests/test_feedexport.py | 49 ++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/scrapy/extensions/feedexport.py b/scrapy/extensions/feedexport.py index c3fc66de5..85d328528 100644 --- a/scrapy/extensions/feedexport.py +++ b/scrapy/extensions/feedexport.py @@ -170,6 +170,7 @@ class FeedExporter(object): if not self._exporter_supported(self.format): raise NotConfigured self.store_empty = settings.getbool('FEED_STORE_EMPTY') + self._exporting = False self.export_fields = settings.getlist('FEED_EXPORT_FIELDS') or None uripar = settings['FEED_URI_PARAMS'] self._uripar = load_object(uripar) if uripar else lambda x, y: None @@ -188,14 +189,18 @@ class FeedExporter(object): file = storage.open(spider) exporter = self._get_exporter(file, fields_to_export=self.export_fields, encoding=self.export_encoding) - exporter.start_exporting() + if self.store_empty: + exporter.start_exporting() + self._exporting = True self.slot = SpiderSlot(file, exporter, storage, uri) def close_spider(self, spider): slot = self.slot if not slot.itemcount and not self.store_empty: return - slot.exporter.finish_exporting() + if self._exporting: + slot.exporter.finish_exporting() + self._exporting = False logfmt = "%s %%(format)s feed (%%(itemcount)d items) in: %%(uri)s" log_args = {'format': self.format, 'itemcount': slot.itemcount, @@ -210,6 +215,9 @@ class FeedExporter(object): def item_scraped(self, item, spider): slot = self.slot + if not self._exporting: + slot.exporter.start_exporting() + self._exporting = True slot.exporter.export_item(item) slot.itemcount += 1 return item diff --git a/tests/test_feedexport.py b/tests/test_feedexport.py index 353b21927..e93d2bafb 100644 --- a/tests/test_feedexport.py +++ b/tests/test_feedexport.py @@ -197,6 +197,21 @@ class FeedExportTest(unittest.TestCase): data = yield self.run_and_export(TestSpider, settings) defer.returnValue(data) + @defer.inlineCallbacks + def exported_no_data(self, settings): + """ + Return exported data which a spider yielding no ``items`` would return. + """ + class TestSpider(scrapy.Spider): + name = 'testspider' + start_urls = ['http://localhost:8998/'] + + def parse(self, response): + pass + + data = yield self.run_and_export(TestSpider, settings) + defer.returnValue(data) + @defer.inlineCallbacks def assertExportedCsv(self, items, header, rows, settings=None, ordered=True): settings = settings or {} @@ -283,6 +298,32 @@ class FeedExportTest(unittest.TestCase): header = self.MyItem.fields.keys() yield self.assertExported(items, header, rows, ordered=False) + @defer.inlineCallbacks + def test_export_no_items_not_store_empty(self): + formats = ('json', + 'jsonlines', + 'xml', + 'csv',) + + for fmt in formats: + settings = {'FEED_FORMAT': fmt} + data = yield self.exported_no_data(settings) + self.assertEqual(data, b'') + + @defer.inlineCallbacks + def test_export_no_items_store_empty(self): + formats = ( + ('json', b'[\n\n]'), + ('jsonlines', b''), + ('xml', b'\n'), + ('csv', b''), + ) + + for fmt, expctd in formats: + settings = {'FEED_FORMAT': fmt, 'FEED_STORE_EMPTY': True} + data = yield self.exported_no_data(settings) + self.assertEqual(data, expctd) + @defer.inlineCallbacks def test_export_multiple_item_classes(self): @@ -376,26 +417,26 @@ class FeedExportTest(unittest.TestCase): def test_export_encoding(self): items = [dict({'foo': u'Test\xd6'})] header = ['foo'] - + formats = { 'json': u'[\n{"foo": "Test\\u00d6"}\n]'.encode('utf-8'), 'jsonlines': u'{"foo": "Test\\u00d6"}\n'.encode('utf-8'), 'xml': u'\nTest\xd6'.encode('utf-8'), 'csv': u'foo\r\nTest\xd6\r\n'.encode('utf-8'), } - + for format in formats: settings = {'FEED_FORMAT': format} data = yield self.exported_data(items, settings) self.assertEqual(formats[format], data) - + formats = { 'json': u'[\n{"foo": "Test\xd6"}\n]'.encode('latin-1'), 'jsonlines': u'{"foo": "Test\xd6"}\n'.encode('latin-1'), 'xml': u'\nTest\xd6'.encode('latin-1'), 'csv': u'foo\r\nTest\xd6\r\n'.encode('latin-1'), } - + for format in formats: settings = {'FEED_FORMAT': format, 'FEED_EXPORT_ENCODING': 'latin-1'} data = yield self.exported_data(items, settings)