From cd4cdc69e622c1e9f07a709cc7180a2ee6b6fd2a Mon Sep 17 00:00:00 2001 From: Mubashir Rahim Date: Thu, 4 Jun 2026 14:53:10 +0500 Subject: [PATCH] fix: prevent IndexError in adaptive relocation with auto_save When `css()`/`xpath()` are called with both `adaptive=True` and `auto_save=True`, the relocation branch guarded the re-save with `if elements is not None`. However `relocate()` returns an empty list (never `None`) when no candidate clears the `percentage` threshold, so the guard always passed and `self.save(elements[0], ...)` raised `IndexError: list index out of range`. This crashes exactly when adaptive resilience is needed most: the page structure changed enough that nothing matches above the threshold. Fix: use a truthiness check (`if elements and auto_save`) so the re-save is skipped when relocation yields nothing. The successful relocation path (which re-saves the relocated element) is unchanged. Added a regression test that fails before the fix (IndexError) and passes after. Co-Authored-By: Claude Opus 4.8 --- scrapling/parser.py | 2 +- tests/parser/test_adaptive.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scrapling/parser.py b/scrapling/parser.py index 2b88af3..200cb43 100644 --- a/scrapling/parser.py +++ b/scrapling/parser.py @@ -671,7 +671,7 @@ class Selector(SelectorsGeneration): element_data = self.retrieve(identifier or selector) if element_data: elements = self.relocate(element_data, percentage) - if elements is not None and auto_save: + if elements and auto_save: self.save(elements[0], identifier or selector) return self.__handle_elements(elements) diff --git a/tests/parser/test_adaptive.py b/tests/parser/test_adaptive.py index 8d40f77..85881d1 100644 --- a/tests/parser/test_adaptive.py +++ b/tests/parser/test_adaptive.py @@ -56,6 +56,32 @@ class TestParserAdaptive: assert relocated[0].has_class("new-class") assert relocated[0].css(".new-description")[0].text == "Description 1" + def test_relocation_auto_save_no_match_above_threshold(self): + """Adaptive relocation with `auto_save=True` must not crash when no element + clears the `percentage` threshold (relocate() returns an empty list).""" + original_html = """ +
+
+

Widget

+

A widget

+
+
+ """ + # Unrelated structure so nothing can match a high threshold + changed_html = "totally unrelated content" + + old_page = Selector(original_html, url="example.com", adaptive=True) + new_page = Selector(changed_html, url="example.com", adaptive=True) + + old_page.css("#target", identifier="target", auto_save=True) + + # Before the fix this raised `IndexError: list index out of range` because the + # guard checked `elements is not None` but relocate() returns [] (never None). + result = new_page.css( + "#target", identifier="target", adaptive=True, auto_save=True, percentage=95 + ) + assert list(result) == [] + @pytest.mark.asyncio async def test_element_relocation_async(self): """Test relocating element after structure change in async mode"""