Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathspec/_backends/simple/gitignore.py: 25%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

55 statements  

1""" 

2This module provides the simple backend for :class:`~pathspec.gitignore.GitIgnoreSpec`. 

3 

4WARNING: The *pathspec._backends.simple* package is not part of the public API. 

5Its contents and structure are likely to change. 

6""" 

7 

8from collections.abc import ( 

9 Sequence) 

10from typing import ( 

11 Optional) # Replaced by `X | None` in 3.10. 

12 

13from pathspec.pattern import ( 

14 RegexPattern) 

15from pathspec.patterns.gitignore.spec import ( 

16 _DIR_MARK) 

17from pathspec._typing import ( 

18 override) # Added in 3.12. 

19 

20from .pathspec import ( 

21 SimplePsBackend) 

22 

23 

24class SimpleGiBackend(SimplePsBackend): 

25 """ 

26 The :class:`SimpleGiBackend` class is the default (or simple) implementation 

27 used by :class:`~pathspec.gitignore.GitIgnoreSpec` for matching files. 

28 """ 

29 

30 # Change type hint. 

31 _patterns: list[tuple[int, RegexPattern]] # type: ignore[assignment] 

32 

33 def __init__( 

34 self, 

35 patterns: Sequence[RegexPattern], 

36 *, 

37 no_filter: Optional[bool] = None, 

38 no_reverse: Optional[bool] = None, 

39 ) -> None: 

40 """ 

41 Initialize the :class:`SimpleGiBackend` instance. 

42 

43 *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the 

44 compiled patterns. 

45 

46 *no_filter* (:class:`bool`) is whether to keep no-op patterns (:data:`True`), 

47 or remove them (:data:`False`). 

48 

49 *no_reverse* (:class:`bool`) is whether to keep the pattern order 

50 (:data:`True`), or reverse the order (:data:`True`). 

51 """ 

52 super().__init__(patterns, no_filter=no_filter, no_reverse=no_reverse) 

53 

54 @override 

55 def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: 

56 """ 

57 Check the file against the patterns. 

58 

59 *file* (:class:`str`) is the normalized file path to check. 

60 

61 Returns a :class:`tuple` containing whether to include *file* (:class:`bool` 

62 or :data:`None`), and the index of the last matched pattern (:class:`int` or 

63 :data:`None`). 

64 """ 

65 return self._match(file, check_ancestors=True) 

66 

67 def _match( 

68 self, file: str, check_ancestors: bool, 

69 ) -> tuple[Optional[bool], Optional[int]]: 

70 """ 

71 Implements :meth:`match_file`. *check_ancestors* (:class:`bool`) is 

72 whether to ask if an ancestor directory of *file* is excluded; see 

73 :meth:`_match_ancestor` for why it can be skipped. 

74 """ 

75 is_reversed = self._is_reversed 

76 

77 # Resolve the ancestor directory and the file separately: a file negation 

78 # only applies while no ancestor directory is excluded. 

79 dir_include: Optional[bool] = None 

80 dir_index: Optional[int] = None 

81 file_include: Optional[bool] = None 

82 file_index: Optional[int] = None 

83 

84 for index, pattern in self._patterns: 

85 if ( 

86 (include := pattern.include) is not None 

87 and (match := pattern.match_file(file)) is not None 

88 ): 

89 # Pattern matched. 

90 if match.match.groupdict().get(_DIR_MARK): 

91 # A pattern can match both a strict ancestor of the file and the 

92 # file itself. For anchored patterns there is only one match, but 

93 # `**/` compiles to the unanchored `(?P<ps_d>/)`: on "a/b/" it 

94 # matches at "a/" and at "b/", and `match` only returns the first. 

95 is_ancestor = is_self = False 

96 assert pattern.regex is not None, pattern 

97 for dir_match in pattern.regex.finditer(file): 

98 if dir_match.end(_DIR_MARK) < len(file): 

99 is_ancestor = True 

100 else: 

101 is_self = True 

102 

103 if is_ancestor and (dir_include is None or not is_reversed): 

104 dir_include = include 

105 dir_index = index 

106 

107 if is_self and (file_include is None or not is_reversed): 

108 file_include = include 

109 file_index = index 

110 elif file_include is None or not is_reversed: 

111 # Pattern matched by a file pattern. 

112 file_include = include 

113 file_index = index 

114 

115 if check_ancestors and (dir_include is not None or file_include is not None): 

116 # Neither file nor directory negations override an excluded ancestor. 

117 # Some patterns (e.g. "a/**") match without a directory marker. 

118 ancestor_index = self._match_ancestor(file) 

119 if ancestor_index is not None: 

120 return (True, ancestor_index) 

121 

122 if file_include is not None: 

123 return (file_include, file_index) 

124 elif dir_include: 

125 # An ancestor matched an exclude pattern, but the spec as a whole 

126 # re-includes that ancestor, so the rule does not apply. 

127 return (None, None) 

128 else: 

129 return (dir_include, dir_index) 

130 

131 def _match_ancestor(self, file: str) -> Optional[int]: 

132 """ 

133 Return the excluding pattern index for a strict ancestor, or :data:`None`. 

134 Git stops descending at the first excluded directory, so the ancestors are 

135 asked outermost first, each as a directory query (trailing slash included). 

136 By the time an ancestor is asked, every ancestor above it is known not to 

137 be excluded, so it is matched without checking its own ancestors again; 

138 otherwise each level re-asks all the levels above it and the work grows 

139 exponentially with depth. 

140 """ 

141 index = file.find('/') 

142 while index != -1 and index + 1 < len(file): 

143 ancestor_include, ancestor_index = self._match( 

144 file[:index + 1], check_ancestors=False, 

145 ) 

146 if ancestor_include: 

147 return ancestor_index 

148 index = file.find('/', index + 1) 

149 

150 return None 

151