From 7c140644c4baca468c42c2f4987d24ab97ca1d27 Mon Sep 17 00:00:00 2001 From: Larry Gao <75778943+Larry-Labs@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:27:14 +0800 Subject: [PATCH] Preserve PathSpec state when backend reconstruction fails --- pathspec/pathspec.py | 6 ++++-- tests/test_05_pathspec.py | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index 6db7fff..fa11546 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -141,8 +141,10 @@ def __iadd__(self: Self, other: PathSpec) -> Self: # type: ignore[misc] (:class:`PathSpec`) to this instance. """ if isinstance(other, PathSpec): - self.patterns = [*self.patterns, *other.patterns] - self._backend = self._make_backend(self._backend_name, self.patterns) + patterns = [*self.patterns, *other.patterns] + backend = self._make_backend(self._backend_name, patterns) + self.patterns = patterns + self._backend = backend return self else: return NotImplemented diff --git a/tests/test_05_pathspec.py b/tests/test_05_pathspec.py index c55ac66..97771a0 100644 --- a/tests/test_05_pathspec.py +++ b/tests/test_05_pathspec.py @@ -28,8 +28,11 @@ from unittest import ( SkipTest, TestCase) +from unittest.mock import ( + patch) from pathspec import ( + GitIgnoreSpec, PathSpec, RegexPattern) from pathspec.backend import ( @@ -784,6 +787,43 @@ def test_03_iadd_2_mixed(self): 'test.txt', }, debug) + def test_03_iadd_backend_failure(self): + """ + A failed backend rebuild must leave the original spec usable and unchanged. + """ + for spec_class in (PathSpec, GitIgnoreSpec): + with self.subTest(spec_class=spec_class): + spec = spec_class.from_lines('gitignore', ['*.txt'], backend='simple') + original_patterns = spec.patterns + other = spec_class.from_lines('gitignore', ['!keep.txt'], backend='simple') + with patch.object(spec_class, '_make_backend', side_effect=ValueError('Cannot compile patterns')): + with self.assertRaisesRegex(ValueError, 'Cannot compile patterns'): + spec += other + + self.assertIs(spec.patterns, original_patterns) + self.assertEqual(len(spec), 1) + self.assertTrue(spec.match_file('keep.txt')) + spec += other + self.assertEqual(len(spec), 2) + self.assertFalse(spec.match_file('keep.txt')) + self.assertTrue(spec.match_file('other.txt')) + + def test_03_iadd_incompatible_re2_pattern(self): + """An unsupported regex must not become part of the RE2 spec.""" + require_backend('re2') + import re2 + spec = PathSpec([RegexPattern(re.compile(r'.*\.txt$'), include=True)], backend='re2') + other = PathSpec([RegexPattern(re.compile(r'(?=keep).*'), include=False)], backend='simple') + original_patterns = spec.patterns + with self.assertRaises(re2.error): + spec += other + self.assertIs(spec.patterns, original_patterns) + self.assertTrue(spec.match_file('keep.txt')) + spec += PathSpec.from_lines('gitignore', ['!keep.txt'], backend='simple') + self.assertEqual(len(spec), 2) + self.assertFalse(spec.match_file('keep.txt')) + self.assertTrue(spec.match_file('other.txt')) + def test_04_len(self): """ Test spec length.