from __future__ import annotations from typing import TYPE_CHECKING import pytest from scrapy.http import Request, Response, TextResponse, XmlResponse from scrapy.spiders import CSVFeedSpider, Spider, XMLFeedSpider from tests import get_testdata from tests.spiders import RawResponseSpider from tests.utils.bases.spider import TestSpiderBase from tests.utils.crawl import crawl_items from tests.utils.decorators import coroutine_test if TYPE_CHECKING: from tests.mockserver.http import MockServer class RawFeedSpider(RawResponseSpider): content_type = "text/xml" async def start(self): yield Request(self.raw_url) class TestSpider(TestSpiderBase): spider_class = Spider class TestXMLFeedSpider(TestSpiderBase): spider_class = XMLFeedSpider def test_register_namespace(self): body = b""" http://www.example.com/Special-Offers.html2009-08-16 http://www.example.com/2009-08-16 """ response = XmlResponse(url="http://example.com/sitemap.xml", body=body) class _XMLSpider(self.spider_class): itertag = "url" namespaces = ( ("a", "http://www.google.com/schemas/sitemap/0.84"), ("b", "http://www.example.com/schemas/extras/1.0"), ) def parse_node(self, response, selector): yield { "loc": selector.xpath("a:loc/text()").getall(), "updated": selector.xpath("b:updated/text()").getall(), "other": selector.xpath("other/@value").getall(), "custom": selector.xpath("other/@b:custom").getall(), } for iterator in ("iternodes", "xml"): spider = _XMLSpider("example", iterator=iterator) output = list(spider._parse(response)) assert len(output) == 2, iterator assert output == [ { "loc": ["http://www.example.com/Special-Offers.html"], "updated": ["2009-08-16"], "custom": ["fuu"], "other": ["bar"], }, { "loc": [], "updated": ["2009-08-16"], "other": ["foo"], "custom": [], }, ], iterator @coroutine_test async def test_parse_node_uses_parse_item(self, mockserver: MockServer): # parse_node falls back to parse_item for backward compatibility. class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] itertag = "item" def raw_body(self): return "1" def parse_item(self, response, selector): return {"id": selector.xpath("id/text()").get()} items, _ = await crawl_items(_Spider, mockserver) assert items == [{"id": "1"}] @coroutine_test async def test_parse_node_not_defined(self, mockserver: MockServer): class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] itertag = "item" def raw_body(self): return "1" items, crawler = await crawl_items(_Spider, mockserver) assert items == [] assert crawler.stats assert crawler.stats.get_value("spider_exceptions/NotImplementedError") == 1 @coroutine_test async def test_html_iterator(self, mockserver: MockServer): class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] iterator = "html" itertag = "item" content_type = "text/html" def raw_body(self): return ( "1" "2" ) def parse_node(self, response, selector): return {"id": selector.xpath("id/text()").get()} items, _ = await crawl_items(_Spider, mockserver) assert items == [{"id": "1"}, {"id": "2"}] @coroutine_test async def test_unsupported_iterator(self, mockserver: MockServer): class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] iterator = "unsupported" def raw_body(self): return "" def parse_node(self, response, selector): return {} items, crawler = await crawl_items(_Spider, mockserver) assert items == [] assert crawler.stats assert crawler.stats.get_value("spider_exceptions/NotSupported") == 1 @pytest.mark.parametrize("feed_iterator", ["xml", "html"]) @coroutine_test async def test_non_text_response(self, feed_iterator: str, mockserver: MockServer): # The xml and html iterators require a text response. class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] content_type = "application/octet-stream" iterator = feed_iterator def raw_body(self): # A binary (non-text) body, so the response is a plain Response. return "\x00\x01\x02\x03" def parse_node(self, response, selector): return {} items, crawler = await crawl_items(_Spider, mockserver) assert items == [] assert crawler.stats assert crawler.stats.get_value("spider_exceptions/ValueError") == 1 class TestCSVFeedSpider(TestSpiderBase): spider_class = CSVFeedSpider def test_parse_rows(self): body = get_testdata("feeds", "feed-sample6.csv") response = Response("http://example.org/dummy.csv", body=body) class _CrawlSpider(self.spider_class): name = "test" delimiter = "," quotechar = "'" def parse_row(self, response, row): return row spider = _CrawlSpider() rows = list(spider.parse_rows(response)) assert rows[0] == {"id": "1", "name": "alpha", "value": "foobar"} assert len(rows) == 4 @coroutine_test async def test_parse(self, mockserver: MockServer): class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] content_type = "text/csv" delimiter = "," quotechar = "'" def raw_body(self): return get_testdata("feeds", "feed-sample6.csv").decode() def parse_row(self, response, row): return row items, _ = await crawl_items(_Spider, mockserver) assert items[0] == {"id": "1", "name": "alpha", "value": "foobar"} assert len(items) == 4 @coroutine_test async def test_parse_row_not_defined(self, mockserver: MockServer): class _Spider(RawFeedSpider, self.spider_class): # type: ignore[name-defined,misc] content_type = "text/csv" def raw_body(self): return "id\n1\n" items, crawler = await crawl_items(_Spider, mockserver) assert items == [] assert crawler.stats assert crawler.stats.get_value("spider_exceptions/NotImplementedError") == 1 class TestNoParseMethodSpider: spider_class = Spider def test_undefined_parse_method(self): spider = self.spider_class("example.com") text = b"Random text" resp = TextResponse(url="http://www.example.com/random_url", body=text) exc_msg = "Spider.parse callback is not defined" with pytest.raises(NotImplementedError, match=exc_msg): spider.parse(resp)