1# encoding: utf-8
2"""
3This module provides an object oriented interface for pattern matching
4of files.
5"""
6
7try:
8	from typing import (
9		Any,
10		AnyStr,
11		Callable,
12		Iterable,
13		Iterator,
14		Optional,
15		Text,
16		Union)
17except ImportError:
18	pass
19
20try:
21	# Python 3.6+ type hints.
22	from os import PathLike
23	from typing import Collection
24except ImportError:
25	pass
26
27from . import util
28from .compat import (
29	CollectionType,
30	iterkeys,
31	izip_longest,
32	string_types)
33from .pattern import Pattern
34from .util import TreeEntry
35
36
37class PathSpec(object):
38	"""
39	The :class:`PathSpec` class is a wrapper around a list of compiled
40	:class:`.Pattern` instances.
41	"""
42
43	def __init__(self, patterns):
44		# type: (Iterable[Pattern]) -> None
45		"""
46		Initializes the :class:`PathSpec` instance.
47
48		*patterns* (:class:`~collections.abc.Collection` or :class:`~collections.abc.Iterable`)
49		yields each compiled pattern (:class:`.Pattern`).
50		"""
51
52		self.patterns = patterns if isinstance(patterns, CollectionType) else list(patterns)
53		"""
54		*patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`)
55		contains the compiled patterns.
56		"""
57
58	def __eq__(self, other):
59		# type: (PathSpec) -> bool
60		"""
61		Tests the equality of this path-spec with *other* (:class:`PathSpec`)
62		by comparing their :attr:`~PathSpec.patterns` attributes.
63		"""
64		if isinstance(other, PathSpec):
65			paired_patterns = izip_longest(self.patterns, other.patterns)
66			return all(a == b for a, b in paired_patterns)
67		else:
68			return NotImplemented
69
70	def __len__(self):
71		"""
72		Returns the number of compiled patterns this path-spec contains
73		(:class:`int`).
74		"""
75		return len(self.patterns)
76
77	def __add__(self, other):
78		# type: (PathSpec) -> PathSpec
79		"""
80		Combines the :attr:`Pathspec.patterns` patterns from two
81		:class:`PathSpec` instances.
82		"""
83		if isinstance(other, PathSpec):
84			return PathSpec(self.patterns + other.patterns)
85		else:
86			return NotImplemented
87
88	def __iadd__(self, other):
89		# type: (PathSpec) -> PathSpec
90		"""
91		Adds the :attr:`Pathspec.patterns` patterns from one :class:`PathSpec`
92		instance to this instance.
93		"""
94		if isinstance(other, PathSpec):
95			self.patterns += other.patterns
96			return self
97		else:
98			return NotImplemented
99
100	@classmethod
101	def from_lines(cls, pattern_factory, lines):
102		# type: (Union[Text, Callable[[AnyStr], Pattern]], Iterable[AnyStr]) -> PathSpec
103		"""
104		Compiles the pattern lines.
105
106		*pattern_factory* can be either the name of a registered pattern
107		factory (:class:`str`), or a :class:`~collections.abc.Callable` used
108		to compile patterns. It must accept an uncompiled pattern (:class:`str`)
109		and return the compiled pattern (:class:`.Pattern`).
110
111		*lines* (:class:`~collections.abc.Iterable`) yields each uncompiled
112		pattern (:class:`str`). This simply has to yield each line so it can
113		be a :class:`file` (e.g., from :func:`open` or :class:`io.StringIO`)
114		or the result from :meth:`str.splitlines`.
115
116		Returns the :class:`PathSpec` instance.
117		"""
118		if isinstance(pattern_factory, string_types):
119			pattern_factory = util.lookup_pattern(pattern_factory)
120		if not callable(pattern_factory):
121			raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory))
122
123		if not util._is_iterable(lines):
124			raise TypeError("lines:{!r} is not an iterable.".format(lines))
125
126		patterns = [pattern_factory(line) for line in lines if line]
127		return cls(patterns)
128
129	def match_file(self, file, separators=None):
130		# type: (Union[Text, PathLike], Optional[Collection[Text]]) -> bool
131		"""
132		Matches the file to this path-spec.
133
134		*file* (:class:`str` or :class:`~pathlib.PurePath`) is the file path
135		to be matched against :attr:`self.patterns <PathSpec.patterns>`.
136
137		*separators* (:class:`~collections.abc.Collection` of :class:`str`)
138		optionally contains the path separators to normalize. See
139		:func:`~pathspec.util.normalize_file` for more information.
140
141		Returns :data:`True` if *file* matched; otherwise, :data:`False`.
142		"""
143		norm_file = util.normalize_file(file, separators=separators)
144		return util.match_file(self.patterns, norm_file)
145
146	def match_entries(self, entries, separators=None):
147		# type: (Iterable[TreeEntry], Optional[Collection[Text]]) -> Iterator[TreeEntry]
148		"""
149		Matches the entries to this path-spec.
150
151		*entries* (:class:`~collections.abc.Iterable` of :class:`~util.TreeEntry`)
152		contains the entries to be matched against :attr:`self.patterns <PathSpec.patterns>`.
153
154		*separators* (:class:`~collections.abc.Collection` of :class:`str`;
155		or :data:`None`) optionally contains the path separators to
156		normalize. See :func:`~pathspec.util.normalize_file` for more
157		information.
158
159		Returns the matched entries (:class:`~collections.abc.Iterator` of
160		:class:`~util.TreeEntry`).
161		"""
162		if not util._is_iterable(entries):
163			raise TypeError("entries:{!r} is not an iterable.".format(entries))
164
165		entry_map = util._normalize_entries(entries, separators=separators)
166		match_paths = util.match_files(self.patterns, iterkeys(entry_map))
167		for path in match_paths:
168			yield entry_map[path]
169
170	def match_files(self, files, separators=None):
171		# type: (Iterable[Union[Text, PathLike]], Optional[Collection[Text]]) -> Iterator[Union[Text, PathLike]]
172		"""
173		Matches the files to this path-spec.
174
175		*files* (:class:`~collections.abc.Iterable` of :class:`str; or
176		:class:`pathlib.PurePath`) contains the file paths to be matched
177		against :attr:`self.patterns <PathSpec.patterns>`.
178
179		*separators* (:class:`~collections.abc.Collection` of :class:`str`;
180		or :data:`None`) optionally contains the path separators to
181		normalize. See :func:`~pathspec.util.normalize_file` for more
182		information.
183
184		Returns the matched files (:class:`~collections.abc.Iterator` of
185		:class:`str` or :class:`pathlib.PurePath`).
186		"""
187		if not util._is_iterable(files):
188			raise TypeError("files:{!r} is not an iterable.".format(files))
189
190		file_map = util.normalize_files(files, separators=separators)
191		matched_files = util.match_files(self.patterns, iterkeys(file_map))
192		for norm_file in matched_files:
193			for orig_file in file_map[norm_file]:
194				yield orig_file
195
196	def match_tree_entries(self, root, on_error=None, follow_links=None):
197		# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[TreeEntry]
198		"""
199		Walks the specified root path for all files and matches them to this
200		path-spec.
201
202		*root* (:class:`str`; or :class:`pathlib.PurePath`) is the root
203		directory to search.
204
205		*on_error* (:class:`~collections.abc.Callable` or :data:`None`)
206		optionally is the error handler for file-system exceptions. See
207		:func:`~pathspec.util.iter_tree_entries` for more information.
208
209		*follow_links* (:class:`bool` or :data:`None`) optionally is whether
210		to walk symbolic links that resolve to directories. See
211		:func:`~pathspec.util.iter_tree_files` for more information.
212
213		Returns the matched files (:class:`~collections.abc.Iterator` of
214		:class:`.TreeEntry`).
215		"""
216		entries = util.iter_tree_entries(root, on_error=on_error, follow_links=follow_links)
217		return self.match_entries(entries)
218
219	def match_tree_files(self, root, on_error=None, follow_links=None):
220		# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text]
221		"""
222		Walks the specified root path for all files and matches them to this
223		path-spec.
224
225		*root* (:class:`str`; or :class:`pathlib.PurePath`) is the root
226		directory to search for files.
227
228		*on_error* (:class:`~collections.abc.Callable` or :data:`None`)
229		optionally is the error handler for file-system exceptions. See
230		:func:`~pathspec.util.iter_tree_files` for more information.
231
232		*follow_links* (:class:`bool` or :data:`None`) optionally is whether
233		to walk symbolic links that resolve to directories. See
234		:func:`~pathspec.util.iter_tree_files` for more information.
235
236		Returns the matched files (:class:`~collections.abc.Iterable` of
237		:class:`str`).
238		"""
239		files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links)
240		return self.match_files(files)
241
242	# Alias `match_tree_files()` as `match_tree()`.
243	match_tree = match_tree_files
244