From 3e6064b7ef631fea6c6802cedb132b3477c3a08a Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:18:47 +0800 Subject: [PATCH] fix: raise ValueError instead of UnboundLocalError in _get_indexed_match on zero matches `for i, result in enumerate(re.finditer(pattern, text))` never binds `i` when the pattern matches zero times, so the "not found" branch's error message (which references `i`) raises UnboundLocalError instead of the intended ValueError -- breaking extract_text_before()'s and extract_text_after()'s documented "pattern not found" contract. Initialize `i = -1` before the loop so the existing ValueError branch can safely report a sensible "largest index" even with zero matches. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + test_unstructured/cleaners/test_extract.py | 10 ++++++++++ unstructured/cleaners/extract.py | 1 + 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1d11f8f6..cfcaad1c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixes - **Update README.md**: readme-only changes; added a link to Unstructured Pipelines to the README. No library behavior changes. +- **`_get_indexed_match` raises `ValueError` instead of `UnboundLocalError` on zero matches**: `extract_text_before()`/`extract_text_after()`'s underlying helper looped `for i, result in enumerate(re.finditer(pattern, text))` and referenced `i` in its "not found" error message; when the pattern matched zero times, the loop body never ran, so `i` was never bound, raising `UnboundLocalError` instead of the intended `ValueError`. `i` is now initialized to `-1` before the loop. ## 0.25.0 diff --git a/test_unstructured/cleaners/test_extract.py b/test_unstructured/cleaners/test_extract.py index 86ac0e8487..7e29ba5e3c 100644 --- a/test_unstructured/cleaners/test_extract.py +++ b/test_unstructured/cleaners/test_extract.py @@ -19,6 +19,16 @@ def test_get_indexed_match_raises_with_index_too_high(): extract._get_indexed_match("BLAH BLAH BLAH", "BLAH", 4) +def test_get_indexed_match_raises_value_error_with_zero_matches(): + """Regression test: when the pattern has zero matches at all (not just an + out-of-range index on an otherwise-matching pattern), the "not found" branch + referenced the loop variable `i`, which Python never binds when a for-loop's + iterable is empty -- raising UnboundLocalError instead of the intended + ValueError.""" + with pytest.raises(ValueError): + extract._get_indexed_match("no digits here", r"[0-9]+") + + def test_extract_text_before(): text = "Teacher: BLAH BLAH BLAH; Student: BLAH BLAH BLAH!" assert extract.extract_text_before(text, "BLAH", 1) == "Teacher: BLAH" diff --git a/unstructured/cleaners/extract.py b/unstructured/cleaners/extract.py index b576f9ccc6..30f9a9d050 100644 --- a/unstructured/cleaners/extract.py +++ b/unstructured/cleaners/extract.py @@ -18,6 +18,7 @@ def _get_indexed_match(text: str, pattern: str, index: int = 0) -> re.Match: raise ValueError(f"The index is {index}. Index must be a non-negative integer.") regex_match = None + i = -1 for i, result in enumerate(re.finditer(pattern, text)): if i == index: regex_match = result