1"""
2This module provides :class:`.GitIgnoreSpec` which replicates
3*.gitignore* behavior.
4"""
5
6from typing import (
7 AnyStr,
8 Callable, # Replaced by `collections.abc.Callable` in 3.9.
9 Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
10 Optional, # Replaced by `X | None` in 3.10.
11 Tuple, # Replaced by `tuple` in 3.9.
12 Type, # Replaced by `type` in 3.9.
13 TypeVar,
14 Union, # Replaced by `X | Y` in 3.10.
15 cast,
16 overload)
17
18from .pathspec import (
19 PathSpec)
20from .pattern import (
21 Pattern)
22from .patterns.gitwildmatch import (
23 GitWildMatchPattern,
24 _DIR_MARK)
25from .util import (
26 _is_iterable)
27
28Self = TypeVar("Self", bound="GitIgnoreSpec")
29"""
30:class:`GitIgnoreSpec` self type hint to support Python v<3.11 using PEP
31673 recommendation.
32"""
33
34
35class GitIgnoreSpec(PathSpec):
36 """
37 The :class:`GitIgnoreSpec` class extends :class:`pathspec.pathspec.PathSpec` to
38 replicate *.gitignore* behavior.
39 """
40
41 def __eq__(self, other: object) -> bool:
42 """
43 Tests the equality of this gitignore-spec with *other* (:class:`GitIgnoreSpec`)
44 by comparing their :attr:`~pathspec.pattern.Pattern`
45 attributes. A non-:class:`GitIgnoreSpec` will not compare equal.
46 """
47 if isinstance(other, GitIgnoreSpec):
48 return super().__eq__(other)
49 elif isinstance(other, PathSpec):
50 return False
51 else:
52 return NotImplemented
53
54 # Support reversed order of arguments from PathSpec.
55 @overload
56 @classmethod
57 def from_lines(
58 cls: Type[Self],
59 pattern_factory: Union[str, Callable[[AnyStr], Pattern]],
60 lines: Iterable[AnyStr],
61 ) -> Self:
62 ...
63
64 @overload
65 @classmethod
66 def from_lines(
67 cls: Type[Self],
68 lines: Iterable[AnyStr],
69 pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None,
70 ) -> Self:
71 ...
72
73 @classmethod
74 def from_lines(
75 cls: Type[Self],
76 lines: Iterable[AnyStr],
77 pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None,
78 ) -> Self:
79 """
80 Compiles the pattern lines.
81
82 *lines* (:class:`~collections.abc.Iterable`) yields each uncompiled
83 pattern (:class:`str`). This simply has to yield each line so it can
84 be a :class:`io.TextIOBase` (e.g., from :func:`open` or
85 :class:`io.StringIO`) or the result from :meth:`str.splitlines`.
86
87 *pattern_factory* can be :data:`None`, the name of a registered
88 pattern factory (:class:`str`), or a :class:`~collections.abc.Callable`
89 used to compile patterns. The callable must accept an uncompiled
90 pattern (:class:`str`) and return the compiled pattern
91 (:class:`pathspec.pattern.Pattern`).
92 Default is :data:`None` for :class:`.GitWildMatchPattern`).
93
94 Returns the :class:`GitIgnoreSpec` instance.
95 """
96 if pattern_factory is None:
97 pattern_factory = GitWildMatchPattern
98
99 elif (isinstance(lines, (str, bytes)) or callable(lines)) and _is_iterable(pattern_factory):
100 # Support reversed order of arguments from PathSpec.
101 pattern_factory, lines = lines, pattern_factory
102
103 self = super().from_lines(pattern_factory, lines)
104 return cast(Self, self)
105
106 @staticmethod
107 def _match_file(
108 patterns: Iterable[Tuple[int, GitWildMatchPattern]],
109 file: str,
110 ) -> Tuple[Optional[bool], Optional[int]]:
111 """
112 Check the file against the patterns.
113
114 .. NOTE:: Subclasses of :class:`~pathspec.pathspec.PathSpec` may override
115 this method as an instance method. It does not have to be a static
116 method. The signature for this method is subject to change.
117
118 *patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern
119 (:class:`tuple`) which contains the pattern index (:class:`int`) and actual
120 pattern (:class:`~pathspec.pattern.Pattern`).
121
122 *file* (:class:`str`) is the normalized file path to be matched against
123 *patterns*.
124
125 Returns a :class:`tuple` containing whether to include *file* (:class:`bool`
126 or :data:`None`), and the index of the last matched pattern (:class:`int` or
127 :data:`None`).
128 """
129 out_include: Optional[bool] = None
130 out_index: Optional[int] = None
131 out_priority = 0
132 for index, pattern in patterns:
133 if pattern.include is not None:
134 match = pattern.match_file(file)
135 if match is not None:
136 # Pattern matched.
137
138 # Check for directory marker.
139 dir_mark = match.match.groupdict().get(_DIR_MARK)
140
141 if dir_mark:
142 # Pattern matched by a directory pattern.
143 priority = 1
144 else:
145 # Pattern matched by a file pattern.
146 priority = 2
147
148 if pattern.include and dir_mark:
149 out_include = pattern.include
150 out_index = index
151 out_priority = priority
152 elif priority >= out_priority:
153 out_include = pattern.include
154 out_index = index
155 out_priority = priority
156
157 return out_include, out_index