gitignore.py 3.8 KB
Newer Older
SupunGurusinghe's avatar
SupunGurusinghe committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
"""
This module provides :class:`.GitIgnoreSpec` which replicates
*.gitignore* behavior.
"""

from typing import (
	AnyStr,
	Callable,
	Collection,
	Iterable,
	Type,
	TypeVar,
	Union)

from .pathspec import (
	PathSpec)
from .pattern import (
	Pattern)
from .patterns.gitwildmatch import (
	GitWildMatchPattern,
	GitWildMatchPatternError,
	_DIR_MARK)
from .util import (
	_is_iterable)

Self = TypeVar("Self", bound="GitIgnoreSpec")
"""
:class:`GitIgnoreSpec` self type hint to support Python v<3.11 using PEP
673 recommendation.
"""


class GitIgnoreSpec(PathSpec):
	"""
	The :class:`GitIgnoreSpec` class extends :class:`PathSpec` to
	replicate *.gitignore* behavior.
	"""

	def __eq__(self, other: object) -> bool:
		"""
		Tests the equality of this gitignore-spec with *other*
		(:class:`GitIgnoreSpec`) by comparing their :attr:`~PathSpec.patterns`
		attributes. A non-:class:`GitIgnoreSpec` will not compare equal.
		"""
		if isinstance(other, GitIgnoreSpec):
			return super().__eq__(other)
		elif isinstance(other, PathSpec):
			return False
		else:
			return NotImplemented

	@classmethod
	def from_lines(
		cls: Type[Self],
		lines: Iterable[AnyStr],
		pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None,
	) -> Self:
		"""
		Compiles the pattern lines.

		*lines* (:class:`~collections.abc.Iterable`) yields each uncompiled
		pattern (:class:`str`). This simply has to yield each line so it can
		be a :class:`io.TextIOBase` (e.g., from :func:`open` or
		:class:`io.StringIO`) or the result from :meth:`str.splitlines`.

		*pattern_factory* can be :data:`None`, the name of a registered
		pattern factory (:class:`str`), or a :class:`~collections.abc.Callable`
		used to compile patterns. The callable must accept an uncompiled
		pattern (:class:`str`) and return the compiled pattern (:class:`.Pattern`).
		Default is :data:`None` for :class:`.GitWildMatchPattern`).

		Returns the :class:`GitIgnoreSpec` instance.
		"""
		if pattern_factory is None:
			pattern_factory = GitWildMatchPattern

		elif (isinstance(lines, str) or callable(lines)) and _is_iterable(pattern_factory):
			# Support reversed order of arguments from PathSpec.
			pattern_factory, lines = lines, pattern_factory

		self = super().from_lines(pattern_factory, lines)
		return self  # type: ignore

	@staticmethod
	def _match_file(
		patterns: Collection[GitWildMatchPattern],
		file: str,
	) -> bool:
		"""
		Matches the file to the patterns.

		.. NOTE:: Subclasses of :class:`.PathSpec` may override this
		   method as an instance method. It does not have to be a static
		   method.

		*patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
		contains the patterns to use.

		*file* (:class:`str`) is the normalized file path to be matched
		against *patterns*.

		Returns :data:`True` if *file* matched; otherwise, :data:`False`.
		"""
		out_matched = False
		out_priority = 0
		for pattern in patterns:
			if pattern.include is not None:
				match = pattern.match_file(file)
				if match is not None:
					# Pattern matched.

					# Check for directory marker.
					try:
						dir_mark = match.match.group(_DIR_MARK)
					except IndexError as e:
						# NOTICE: The exact content of this error message is subject
						# to change.
						raise GitWildMatchPatternError((
							f"Invalid git pattern: directory marker regex group is missing. "
							f"Debug: file={file!r} regex={pattern.regex!r} "
							f"group={_DIR_MARK!r} match={match.match!r}."
						)) from e

					if dir_mark:
						# Pattern matched by a directory pattern.
						priority = 1
					else:
						# Pattern matched by a file pattern.
						priority = 2

					if pattern.include and dir_mark:
						out_matched = pattern.include
						out_priority = priority
					elif priority >= out_priority:
						out_matched = pattern.include
						out_priority = priority

		return out_matched