From 5fa5c4545b069f3f41f4a8e5f8277344df44c7db Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Wed, 22 Aug 2012 13:46:50 -0300 Subject: [PATCH] use ScrapyJSONEncoder JsonItemExporter & JsonLinesItemExporter, to support nested items properly --- docs/news.rst | 1 + scrapy/contrib/exporter/__init__.py | 5 +++-- scrapy/tests/test_contrib_exporter.py | 25 +++++++++++++++++++++++++ scrapy/utils/serialize.py | 3 +++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index 8b4fdc1b0..41e87a326 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -11,6 +11,7 @@ Scrapy changes: - removed modules: ``scrapy.xlib.BeautifulSoup`` and ``scrapy.xlib.ClientForm`` - SitemapSpider: added support for sitemap urls ending in .xml and .xml.gz, even if they advertise a wrong content type (:commit:`10ed28b`) - StackTraceDump extension: also dump trackref live references (:commit:`fe2ce93`) +- nested items now fully supported in JSON and JSONLines exporters - added :reqmeta:`cookiejar` Request meta key to support multiple cookie sessions per spider - decoupled encoding detection code to `w3lib.encoding`_, and ported Scrapy code to use that mdule - dropped support for Python 2.5. See http://blog.scrapy.org/scrapy-dropping-support-for-python-25 diff --git a/scrapy/contrib/exporter/__init__.py b/scrapy/contrib/exporter/__init__.py index 1419d76f6..685ab35da 100644 --- a/scrapy/contrib/exporter/__init__.py +++ b/scrapy/contrib/exporter/__init__.py @@ -8,6 +8,7 @@ import marshal import json import cPickle as pickle from xml.sax.saxutils import XMLGenerator +from scrapy.utils.serialize import ScrapyJSONEncoder __all__ = ['BaseItemExporter', 'PprintItemExporter', 'PickleItemExporter', \ @@ -79,7 +80,7 @@ class JsonLinesItemExporter(BaseItemExporter): def __init__(self, file, **kwargs): self._configure(kwargs) self.file = file - self.encoder = json.JSONEncoder(**kwargs) + self.encoder = ScrapyJSONEncoder(**kwargs) def export_item(self, item): itemdict = dict(self._get_serialized_fields(item)) @@ -91,7 +92,7 @@ class JsonItemExporter(JsonLinesItemExporter): def __init__(self, file, **kwargs): self._configure(kwargs) self.file = file - self.encoder = json.JSONEncoder(**kwargs) + self.encoder = ScrapyJSONEncoder(**kwargs) self.first_item = True def start_exporting(self): diff --git a/scrapy/tests/test_contrib_exporter.py b/scrapy/tests/test_contrib_exporter.py index 4d6b10524..b3fb1fc47 100644 --- a/scrapy/tests/test_contrib_exporter.py +++ b/scrapy/tests/test_contrib_exporter.py @@ -173,6 +173,8 @@ class XmlItemExporterTest(BaseItemExporterTest): class JsonLinesItemExporterTest(BaseItemExporterTest): + _expected_nested = {'name': u'Jesus', 'age': {'name': 'Maria', 'age': {'name': 'Joseph', 'age': '22'}}} + def _get_exporter(self, **kwargs): return JsonLinesItemExporter(self.output, **kwargs) @@ -180,8 +182,20 @@ class JsonLinesItemExporterTest(BaseItemExporterTest): exported = json.loads(self.output.getvalue().strip()) self.assertEqual(exported, dict(self.i)) + def test_nested_item(self): + i1 = TestItem(name=u'Joseph\xa3', age='22') + i2 = TestItem(name=u'Maria', age=i1) + i3 = TestItem(name=u'Jesus', age=i2) + self.ie.start_exporting() + self.ie.export_item(i3) + self.ie.finish_exporting() + exported = json.loads(self.output.getvalue()) + self.assertEqual(exported, self._expected_nested) + class JsonItemExporterTest(JsonLinesItemExporterTest): + _expected_nested = [JsonLinesItemExporterTest._expected_nested] + def _get_exporter(self, **kwargs): return JsonItemExporter(self.output, **kwargs) @@ -197,6 +211,17 @@ class JsonItemExporterTest(JsonLinesItemExporterTest): exported = json.loads(self.output.getvalue()) self.assertEqual(exported, [dict(self.i), dict(self.i)]) + def test_nested_item(self): + i1 = TestItem(name=u'Joseph\xa3', age='22') + i2 = TestItem(name=u'Maria', age=i1) + i3 = TestItem(name=u'Jesus', age=i2) + self.ie.start_exporting() + self.ie.export_item(i3) + self.ie.finish_exporting() + exported = json.loads(self.output.getvalue()) + expected = {'name': u'Jesus', 'age': {'name': 'Maria', 'age': dict(i1)}} + self.assertEqual(exported, [expected]) + class CustomItemExporterTest(unittest.TestCase): def test_exporter_custom_serializer(self): diff --git a/scrapy/utils/serialize.py b/scrapy/utils/serialize.py index 41ee88860..c60221e4c 100644 --- a/scrapy/utils/serialize.py +++ b/scrapy/utils/serialize.py @@ -7,6 +7,7 @@ from twisted.internet import defer from scrapy.spider import BaseSpider from scrapy.http import Request, Response +from scrapy.item import BaseItem class SpiderReferencer(object): @@ -98,6 +99,8 @@ class ScrapyJSONEncoder(json.JSONEncoder): return str(o) elif isinstance(o, defer.Deferred): return str(o) + elif isinstance(o, BaseItem): + return dict(o) elif isinstance(o, Request): return "<%s %s %s>" % (type(o).__name__, o.method, o.url) elif isinstance(o, Response):