from __future__ import annotations import dataclasses import attr import pytest from itemadapter import ItemAdapter from itemloaders.processors import Compose, Identity, MapCompose, TakeFirst from scrapy.http import HtmlResponse, Response from scrapy.item import Field, Item from scrapy.loader import ItemLoader from scrapy.selector import Selector # test items class NameItem(Item): name = Field() class SummaryItem(NameItem): url = Field() summary = Field() class NestedItem(Item): name = Field() name_div = Field() name_value = Field() url = Field() image = Field() @attr.s class AttrsNameItem: name = attr.ib(default="") @dataclasses.dataclass class NameDataClass: name: list[str] = dataclasses.field(default_factory=list) # test item loaders class NameItemLoader(ItemLoader): default_item_class = SummaryItem class NestedItemLoader(ItemLoader): default_item_class = NestedItem class ProcessorItemLoader(NameItemLoader): name_in = MapCompose(lambda v: v.title()) class TestBasicItemLoader: def test_add_value_on_unknown_field(self): il = ProcessorItemLoader() with pytest.raises(KeyError): il.add_value("wrong_field", ["lala", "lolo"]) def test_load_item_using_default_loader(self): i = SummaryItem() i["summary"] = "lala" il = ItemLoader(item=i) il.add_value("name", "marta") item = il.load_item() assert item is i assert item["summary"] == ["lala"] assert item["name"] == ["marta"] def test_load_item_using_custom_loader(self): il = ProcessorItemLoader() il.add_value("name", "marta") item = il.load_item() assert item["name"] == ["Marta"] class InitializationTestMixin: item_class: type | None = None def test_keep_single_value(self): """Loaded item should contain values from the initial item""" input_item = self.item_class(name="foo") il = ItemLoader(item=input_item) loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo"]} def test_keep_list(self): """Loaded item should contain values from the initial item""" input_item = self.item_class(name=["foo", "bar"]) il = ItemLoader(item=input_item) loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo", "bar"]} def test_add_value_singlevalue_singlevalue(self): """Values added after initialization should be appended""" input_item = self.item_class(name="foo") il = ItemLoader(item=input_item) il.add_value("name", "bar") loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo", "bar"]} def test_add_value_singlevalue_list(self): """Values added after initialization should be appended""" input_item = self.item_class(name="foo") il = ItemLoader(item=input_item) il.add_value("name", ["item", "loader"]) loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo", "item", "loader"]} def test_add_value_list_singlevalue(self): """Values added after initialization should be appended""" input_item = self.item_class(name=["foo", "bar"]) il = ItemLoader(item=input_item) il.add_value("name", "qwerty") loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo", "bar", "qwerty"]} def test_add_value_list_list(self): """Values added after initialization should be appended""" input_item = self.item_class(name=["foo", "bar"]) il = ItemLoader(item=input_item) il.add_value("name", ["item", "loader"]) loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == { "name": ["foo", "bar", "item", "loader"] } def test_get_output_value_singlevalue(self): """Getting output value must not remove value from item""" input_item = self.item_class(name="foo") il = ItemLoader(item=input_item) assert il.get_output_value("name") == ["foo"] loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo"]} def test_get_output_value_list(self): """Getting output value must not remove value from item""" input_item = self.item_class(name=["foo", "bar"]) il = ItemLoader(item=input_item) assert il.get_output_value("name") == ["foo", "bar"] loaded_item = il.load_item() assert isinstance(loaded_item, self.item_class) assert ItemAdapter(loaded_item).asdict() == {"name": ["foo", "bar"]} def test_values_single(self): """Values from initial item must be added to loader._values""" input_item = self.item_class(name="foo") il = ItemLoader(item=input_item) assert il._values.get("name") == ["foo"] def test_values_list(self): """Values from initial item must be added to loader._values""" input_item = self.item_class(name=["foo", "bar"]) il = ItemLoader(item=input_item) assert il._values.get("name") == ["foo", "bar"] class TestInitializationFromDict(InitializationTestMixin): item_class = dict class TestInitializationFromItem(InitializationTestMixin): item_class = NameItem class TestInitializationFromAttrsItem(InitializationTestMixin): item_class = AttrsNameItem class TestInitializationFromDataClass(InitializationTestMixin): item_class = NameDataClass class BaseNoInputReprocessingLoader(ItemLoader): title_in = MapCompose(str.upper) title_out = TakeFirst() class NoInputReprocessingItem(Item): title = Field() class NoInputReprocessingItemLoader(BaseNoInputReprocessingLoader): default_item_class = NoInputReprocessingItem class TestNoInputReprocessingFromItem: """ Loaders initialized from loaded items must not reprocess fields (Item instances) """ def test_avoid_reprocessing_with_initial_values_single(self): il = NoInputReprocessingItemLoader(item=NoInputReprocessingItem(title="foo")) il_loaded = il.load_item() assert il_loaded == {"title": "foo"} assert NoInputReprocessingItemLoader(item=il_loaded).load_item() == { "title": "foo" } def test_avoid_reprocessing_with_initial_values_list(self): il = NoInputReprocessingItemLoader( item=NoInputReprocessingItem(title=["foo", "bar"]) ) il_loaded = il.load_item() assert il_loaded == {"title": "foo"} assert NoInputReprocessingItemLoader(item=il_loaded).load_item() == { "title": "foo" } def test_avoid_reprocessing_without_initial_values_single(self): il = NoInputReprocessingItemLoader() il.add_value("title", "FOO") il_loaded = il.load_item() assert il_loaded == {"title": "FOO"} assert NoInputReprocessingItemLoader(item=il_loaded).load_item() == { "title": "FOO" } def test_avoid_reprocessing_without_initial_values_list(self): il = NoInputReprocessingItemLoader() il.add_value("title", ["foo", "bar"]) il_loaded = il.load_item() assert il_loaded == {"title": "FOO"} assert NoInputReprocessingItemLoader(item=il_loaded).load_item() == { "title": "FOO" } class TestOutputProcessorItem: def test_output_processor(self): class TempItem(Item): temp = Field() def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) self.setdefault("temp", 0.3) class TempLoader(ItemLoader): default_item_class = TempItem default_input_processor = Identity() default_output_processor = Compose(TakeFirst()) loader = TempLoader() item = loader.load_item() assert isinstance(item, TempItem) assert dict(item) == {"temp": 0.3} class TestSelectortemLoader: response = HtmlResponse( url="", encoding="utf-8", body=b"""
paragraph
homepage
""",
)
def test_init_method(self):
l = ProcessorItemLoader()
assert l.selector is None
def test_init_method_errors(self):
l = ProcessorItemLoader()
with pytest.raises(RuntimeError):
l.add_xpath("url", "//a/@href")
with pytest.raises(RuntimeError):
l.replace_xpath("url", "//a/@href")
with pytest.raises(RuntimeError):
l.get_xpath("//a/@href")
with pytest.raises(RuntimeError):
l.add_css("name", "#name::text")
with pytest.raises(RuntimeError):
l.replace_css("name", "#name::text")
with pytest.raises(RuntimeError):
l.get_css("#name::text")
def test_init_method_with_selector(self):
sel = Selector(text="paragraph