diff --git a/python/pyarrow/_compute.pyx b/python/pyarrow/_compute.pyx index 1c0c779f603..9074946ffdd 100644 --- a/python/pyarrow/_compute.pyx +++ b/python/pyarrow/_compute.pyx @@ -1221,13 +1221,19 @@ class ReplaceSubstringOptions(_ReplaceSubstringOptions): replacement : str What to replace the pattern with. max_replacements : int or None, default None - The maximum number of strings to replace in each - input value (unlimited if None). + The maximum number of replacements to make in each + input value (unlimited if None or -1). If specified, + must be >= 0. """ def __init__(self, pattern, replacement, *, max_replacements=None): if max_replacements is None: max_replacements = -1 + if max_replacements < -1: + raise ValueError( + f"max_replacements must be >= -1, got {max_replacements}. " + "Use None or -1 for unlimited replacements." + ) self._set_options(pattern, replacement, max_replacements) diff --git a/python/pyarrow/tests/test_compute.py b/python/pyarrow/tests/test_compute.py index d4d44e9347e..4f1e81616da 100644 --- a/python/pyarrow/tests/test_compute.py +++ b/python/pyarrow/tests/test_compute.py @@ -1244,6 +1244,30 @@ def test_replace_plain(): assert ar.tolist() == ['barzfoo', 'bard', None] +def test_replace_substring_negative_max_replacements(): + # GH-39149: negative max_replacements should raise ValueError, + # not cause an indefinite hang. + with pytest.raises(ValueError, match="max_replacements must be >= -1"): + pc.ReplaceSubstringOptions(pattern='a', replacement='b', + max_replacements=-3) + with pytest.raises(ValueError, match="max_replacements must be >= -1"): + pc.ReplaceSubstringOptions(pattern='a', replacement='b', + max_replacements=-2) + # -1 and None are valid (unlimited) + opts = pc.ReplaceSubstringOptions(pattern='a', replacement='b', + max_replacements=-1) + assert opts.max_replacements == -1 + opts = pc.ReplaceSubstringOptions(pattern='a', replacement='b') + assert opts.max_replacements == -1 + # Non-negative values are valid + opts = pc.ReplaceSubstringOptions(pattern='a', replacement='b', + max_replacements=0) + assert opts.max_replacements == 0 + opts = pc.ReplaceSubstringOptions(pattern='a', replacement='b', + max_replacements=5) + assert opts.max_replacements == 5 + + def test_replace_regex(): data = pa.array(['foo', 'mood', None]) expected = ['f00', 'm00d', None]