Skip to content

Commit dedb622

Browse files
committed
Finish invalid range notation support
1 parent 1794eba commit dedb622

5 files changed

Lines changed: 55 additions & 36 deletions

File tree

CHANGES_1.in.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
1.0.5 (TBD)
33
-----------
44

5-
TODO: Review #104.
6-
TODO: Review #105.
75
TODO: Tests for #106.
86

97
Bug fixes:
108

9+
- `Issue #93`_: Git discards invalid range notation. `GitIgnoreSpecPattern` now discards patterns with invalid range notation like Git.
1110
- `Pull #106`_: Fix escape() not escaping backslash characters.
1211

1312
Improvements:
@@ -22,6 +21,8 @@ Improvements:
2221
1.0.4 (2026-01-26)
2322
------------------
2423

24+
Bug fixes:
25+
2526
- `Issue #103`_: Using re2 fails if pyre2 is also installed.
2627

2728
.. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103

pathspec/patterns/gitignore/basic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]:
293293

294294
else:
295295
# Match segment glob pattern.
296+
# - EDGE CASE: The gitignore docs defer to *fnmatch(3)* which treats
297+
# invalid range notation as a literal.
296298
out_parts.append(cls._translate_segment_glob(seg, 'literal'))
297299

298300
if i == end:

pathspec/patterns/gitignore/spec.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
This module provides :class:`GitIgnoreSpecPattern` which implements Git's
33
`gitignore`_ patterns, and handles edge-cases where Git's behavior differs from
44
what's documented. Git allows including files from excluded directories which
5-
appears to contradict the documentation. This is used by
6-
:class:`~pathspec.gitignore.GitIgnoreSpec` to fully replicate Git's handling.
5+
appears to contradict the documentation. Git discards patterns with invalid
6+
range notation. This is used by :class:`~pathspec.gitignore.GitIgnoreSpec` to
7+
fully replicate Git's handling.
78
89
.. _`gitignore`: https://git-scm.com/docs/gitignore
910
"""
@@ -245,7 +246,7 @@ def pattern_to_regex(
245246
try:
246247
regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs)
247248
except _RangeError:
248-
# EDGE CASE: Git discards patterns with range notation errors.
249+
# EDGE CASE: Git discards patterns with invalid range notation.
249250
return (None, None)
250251
except ValueError as e:
251252
raise GitIgnorePatternError((
@@ -328,6 +329,7 @@ def __translate_segments(
328329

329330
else:
330331
# Match segment glob pattern.
332+
# - EDGE CASE: Git discards patterns with invalid range notation.
331333
out_parts.append(cls._translate_segment_glob(seg, 'raise'))
332334

333335
if i == end:

tests/test_02_gitignore_basic.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -907,10 +907,14 @@ def test_15_issue_93_b_2_double(self):
907907

908908
def test_15_issue_93_c_1_valid(self):
909909
"""
910-
Test patterns with valid range notations.
910+
Test patterns with valid range notation.
911911
"""
912912
for raw_pattern, regex in [
913+
('[!a-z]', f'^(?:.+/)?[^a-z]{_DIR_OPT}'),
914+
('[^a-z]', f'^(?:.+/)?[^a-z]{_DIR_OPT}'),
913915
('[a-z]', f'^(?:.+/)?[a-z]{_DIR_OPT}'),
916+
('a[!a-z]', f'^(?:.+/)?a[^a-z]{_DIR_OPT}'),
917+
('a[^a-z]', f'^(?:.+/)?a[^a-z]{_DIR_OPT}'),
914918
('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_OPT}'),
915919
]:
916920
with self.subTest(f"p={raw_pattern!r}"):
@@ -920,22 +924,21 @@ def test_15_issue_93_c_1_valid(self):
920924

921925
def test_15_issue_93_c_2_invalid(self):
922926
"""
923-
Test patterns with invalid range notations.
927+
Test patterns with invalid range notation.
924928
"""
925-
# TODO BUG: These tests need to pass.
926-
# - See <https://github.com/cpburnz/python-pathspec/issues/93>.
927-
for raw_pattern in [
928-
'[!]',
929-
'a[!]',
929+
# The basic pattern treats invalid range notation as a literal.
930+
for raw_pattern, regex in [
931+
('[!]', f'^(?:.+/)?\\[!\\]{_DIR_OPT}'),
932+
('[^]', f'^(?:.+/)?\\[\\^\\]{_DIR_OPT}'),
933+
('a[!]', f'^(?:.+/)?a\\[!\\]{_DIR_OPT}'),
934+
('a[^]', f'^(?:.+/)?a\\[\\^\\]{_DIR_OPT}'),
930935
]:
931936
with self.subTest(f"p={raw_pattern!r}"):
932937
pattern = GitIgnoreBasicPattern(raw_pattern)
933-
self.assertIs(pattern.include, None)
934-
self.assertIs(pattern.regex, None)
938+
self.assertIs(pattern.include, True)
939+
self.assertEqual(pattern.regex.pattern, regex)
935940

936941
# The `re` module fails to compile these.
937-
# - NOTE: Technically, these should result in null patterns rather than
938-
# exceptions to fully replicate Git's behavior.
939942
for raw_pattern in [
940943
'[z-a]',
941944
'a[z-a]',
@@ -946,25 +949,29 @@ def test_15_issue_93_c_2_invalid(self):
946949

947950
def test_15_issue_93_c_3_unclosed(self):
948951
"""
949-
Test patterns with unclosed range notations.
952+
Test patterns with unclosed range notation.
950953
"""
951954
# TODO BUG: These tests need to pass.
952955
# - See <https://github.com/cpburnz/python-pathspec/issues/93>.
953-
for raw_pattern in [
954-
'[!',
955-
'[-',
956-
'[a',
957-
'[a-',
958-
'[a-z',
959-
'a[',
960-
'a[-',
961-
'a[a-',
962-
'a[a-z',
956+
for raw_pattern, regex in [
957+
('[!', f'^(?:.+/)?\\[!{_DIR_OPT}'),
958+
('[', f'^(?:.+/)?\\[{_DIR_OPT}'),
959+
('[-', f'^(?:.+/)?\\[\\-{_DIR_OPT}'),
960+
('[^', f'^(?:.+/)?\\[\\^{_DIR_OPT}'),
961+
('[a', f'^(?:.+/)?\\[a{_DIR_OPT}'),
962+
('[a-', f'^(?:.+/)?\\[a\\-{_DIR_OPT}'),
963+
('[a-z', f'^(?:.+/)?\\[a\\-z{_DIR_OPT}'),
964+
('a[!', f'^(?:.+/)?a\\[!{_DIR_OPT}'),
965+
('a[', f'^(?:.+/)?a\\[{_DIR_OPT}'),
966+
('a[-', f'^(?:.+/)?a\\[\\-{_DIR_OPT}'),
967+
('a[^', f'^(?:.+/)?a\\[\\^{_DIR_OPT}'),
968+
('a[a-', f'^(?:.+/)?a\\[a\\-{_DIR_OPT}'),
969+
('a[a-z', f'^(?:.+/)?a\\[a\\-z{_DIR_OPT}'),
963970
]:
964971
with self.subTest(f"p={raw_pattern!r}"):
965972
pattern = GitIgnoreBasicPattern(raw_pattern)
966-
self.assertIs(pattern.include, None)
967-
self.assertIs(pattern.regex, None)
973+
self.assertIs(pattern.include, True)
974+
self.assertEqual(pattern.regex.pattern, regex)
968975

969976
def test_16_repr_str(self):
970977
"""

tests/test_03_gitignore_spec.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,14 @@ def test_15_issue_93_b_2_double(self):
909909

910910
def test_15_issue_93_c_1_valid(self):
911911
"""
912-
Test patterns with valid range notations.
912+
Test patterns with valid range notation.
913913
"""
914914
for raw_pattern, regex in [
915+
('[!a-z]', f'^(?:.+/)?[^a-z]{_DIR_MARK_OPT}'),
916+
('[^a-z]', f'^(?:.+/)?[^a-z]{_DIR_MARK_OPT}'),
915917
('[a-z]', f'^(?:.+/)?[a-z]{_DIR_MARK_OPT}'),
918+
('a[!a-z]', f'^(?:.+/)?a[^a-z]{_DIR_MARK_OPT}'),
919+
('a[^a-z]', f'^(?:.+/)?a[^a-z]{_DIR_MARK_OPT}'),
916920
('a[a-z]', f'^(?:.+/)?a[a-z]{_DIR_MARK_OPT}'),
917921
]:
918922
with self.subTest(f"p={raw_pattern!r}"):
@@ -922,13 +926,14 @@ def test_15_issue_93_c_1_valid(self):
922926

923927
def test_15_issue_93_c_2_invalid(self):
924928
"""
925-
Test patterns with invalid range notations.
929+
Test patterns with invalid range notation.
926930
"""
927-
# TODO BUG: These tests need to pass.
928-
# - See <https://github.com/cpburnz/python-pathspec/issues/93>.
931+
# The spec pattern discards patterns with invalid range notation.
929932
for raw_pattern in [
930933
'[!]',
934+
'[^]',
931935
'a[!]',
936+
'a[^]',
932937
]:
933938
with self.subTest(f"p={raw_pattern!r}"):
934939
pattern = GitIgnoreSpecPattern(raw_pattern)
@@ -948,18 +953,20 @@ def test_15_issue_93_c_2_invalid(self):
948953

949954
def test_15_issue_93_c_3_unclosed(self):
950955
"""
951-
Test patterns with unclosed range notations.
956+
Test patterns with unclosed range notation.
952957
"""
953-
# TODO BUG: These tests need to pass.
954-
# - See <https://github.com/cpburnz/python-pathspec/issues/93>.
955958
for raw_pattern in [
956959
'[!',
960+
'[',
957961
'[-',
962+
'[^',
958963
'[a',
959964
'[a-',
960965
'[a-z',
966+
'a[!',
961967
'a[',
962968
'a[-',
969+
'a[^',
963970
'a[a-',
964971
'a[a-z',
965972
]:

0 commit comments

Comments
 (0)