Skip to content

Commit

Permalink
Add type hints
Browse files Browse the repository at this point in the history
  • Loading branch information
cpburnz committed Jun 12, 2021
1 parent 19daf50 commit c00b332
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Change History
- `Issue #45`_: Fix for duplicate leading double-asterisk, and edge cases.
- `Issue #46`_: Fix matching absolute paths.
- API change: `util.normalize_files()` now returns a `Dict[str, List[pathlike]]` instead of a `Dict[str, pathlike]`.
- Added type hinting.

.. _`Issue #45`: https://github.com/cpburnz/python-path-specification/pull/45
.. _`Issue #46`: https://github.com/cpburnz/python-path-specification/issues/46
Expand Down
3 changes: 3 additions & 0 deletions pathspec/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ def iterkeys(mapping):
except ImportError:
# Python 2.7 - 3.5.
from collections import Container as Collection

CollectionType = Collection
IterableType = Iterable
54 changes: 45 additions & 9 deletions pathspec/pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,34 @@
of files.
"""

try:
from typing import (
Any,
AnyStr,
Callable,
Iterable,
Iterator,
Optional,
Text,
Union)
except ImportError:
pass

try:
# Python 3.6+ type hints.
from os import PathLike
from typing import Collection
except ImportError:
pass

from . import util
from .compat import Collection, iterkeys, izip_longest, string_types, unicode
from .compat import (
CollectionType,
iterkeys,
izip_longest,
string_types)
from .pattern import Pattern
from .util import TreeEntry


class PathSpec(object):
Expand All @@ -15,20 +41,22 @@ class PathSpec(object):
"""

def __init__(self, patterns):
# type: (Iterable[Pattern]) -> None
"""
Initializes the :class:`PathSpec` instance.
*patterns* (:class:`~collections.abc.Collection` or :class:`~collections.abc.Iterable`)
yields each compiled pattern (:class:`.Pattern`).
"""

self.patterns = patterns if isinstance(patterns, Collection) else list(patterns)
self.patterns = patterns if isinstance(patterns, CollectionType) else list(patterns)
"""
*patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`)
contains the compiled patterns.
"""

def __eq__(self, other):
# type: (PathSpec) -> bool
"""
Tests the equality of this path-spec with *other* (:class:`PathSpec`)
by comparing their :attr:`~PathSpec.patterns` attributes.
Expand All @@ -47,6 +75,7 @@ def __len__(self):
return len(self.patterns)

def __add__(self, other):
# type: (PathSpec) -> PathSpec
"""
Combines the :attr:`Pathspec.patterns` patterns from two
:class:`PathSpec` instances.
Expand All @@ -57,6 +86,7 @@ def __add__(self, other):
return NotImplemented

def __iadd__(self, other):
# type: (PathSpec) -> PathSpec
"""
Adds the :attr:`Pathspec.patterns` patterns from one :class:`PathSpec`
instance to this instance.
Expand All @@ -69,6 +99,7 @@ def __iadd__(self, other):

@classmethod
def from_lines(cls, pattern_factory, lines):
# type: (Union[Text, Callable[[AnyStr], Pattern]], Iterable[AnyStr]) -> PathSpec
"""
Compiles the pattern lines.
Expand All @@ -92,10 +123,11 @@ def from_lines(cls, pattern_factory, lines):
if not util._is_iterable(lines):
raise TypeError("lines:{!r} is not an iterable.".format(lines))

lines = [pattern_factory(line) for line in lines if line]
return cls(lines)
patterns = [pattern_factory(line) for line in lines if line]
return cls(patterns)

def match_file(self, file, separators=None):
# type: (Union[Text, PathLike], Optional[Collection[Text]]) -> bool
"""
Matches the file to this path-spec.
Expand All @@ -112,6 +144,7 @@ def match_file(self, file, separators=None):
return util.match_file(self.patterns, norm_file)

def match_entries(self, entries, separators=None):
# type: (Iterable[TreeEntry], Optional[Collection[Text]]) -> Iterator[TreeEntry]
"""
Matches the entries to this path-spec.
Expand All @@ -123,7 +156,7 @@ def match_entries(self, entries, separators=None):
normalize. See :func:`~pathspec.util.normalize_file` for more
information.
Returns the matched entries (:class:`~collections.abc.Iterable` of
Returns the matched entries (:class:`~collections.abc.Iterator` of
:class:`~util.TreeEntry`).
"""
if not util._is_iterable(entries):
Expand All @@ -135,6 +168,7 @@ def match_entries(self, entries, separators=None):
yield entry_map[path]

def match_files(self, files, separators=None):
# type: (Iterable[Union[Text, PathLike]], Optional[Collection[Text]]) -> Iterator[Union[Text, PathLike]]
"""
Matches the files to this path-spec.
Expand All @@ -147,8 +181,8 @@ def match_files(self, files, separators=None):
normalize. See :func:`~pathspec.util.normalize_file` for more
information.
Returns the matched files (:class:`~collections.abc.Iterable` of
:class:`str`).
Returns the matched files (:class:`~collections.abc.Iterator` of
:class:`str` or :class:`pathlib.PurePath`).
"""
if not util._is_iterable(files):
raise TypeError("files:{!r} is not an iterable.".format(files))
Expand All @@ -160,6 +194,7 @@ def match_files(self, files, separators=None):
yield orig_file

def match_tree_entries(self, root, on_error=None, follow_links=None):
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[TreeEntry]
"""
Walks the specified root path for all files and matches them to this
path-spec.
Expand All @@ -175,13 +210,14 @@ def match_tree_entries(self, root, on_error=None, follow_links=None):
to walk symbolic links that resolve to directories. See
:func:`~pathspec.util.iter_tree_files` for more information.
Returns the matched files (:class:`~collections.abc.Iterable` of
:class:`str`).
Returns the matched files (:class:`~collections.abc.Iterator` of
:class:`.TreeEntry`).
"""
entries = util.iter_tree_entries(root, on_error=on_error, follow_links=follow_links)
return self.match_entries(entries)

def match_tree_files(self, root, on_error=None, follow_links=None):
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text]
"""
Walks the specified root path for all files and matches them to this
path-spec.
Expand Down
18 changes: 18 additions & 0 deletions pathspec/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
"""

import re
try:
from typing import (
AnyStr,
Iterable,
Iterator,
Optional,
Pattern as RegexHint,
Text,
Tuple,
Union)
except ImportError:
pass

from .compat import unicode

Expand All @@ -17,6 +29,7 @@ class Pattern(object):
__slots__ = ('include',)

def __init__(self, include):
# type: (Optional[bool]) -> None
"""
Initializes the :class:`Pattern` instance.
Expand All @@ -33,6 +46,7 @@ def __init__(self, include):
"""

def match(self, files):
# type: (Iterable[Text]) -> Iterator[Text]
"""
Matches this pattern against the specified files.
Expand All @@ -55,6 +69,7 @@ class RegexPattern(Pattern):
__slots__ = ('regex',)

def __init__(self, pattern, include=None):
# type: (Union[AnyStr, RegexHint], Optional[bool]) -> None
"""
Initializes the :class:`RegexPattern` instance.
Expand Down Expand Up @@ -103,6 +118,7 @@ def __init__(self, pattern, include=None):
self.regex = regex

def __eq__(self, other):
# type: (RegexPattern) -> bool
"""
Tests the equality of this regex pattern with *other* (:class:`RegexPattern`)
by comparing their :attr:`~Pattern.include` and :attr:`~RegexPattern.regex`
Expand All @@ -114,6 +130,7 @@ def __eq__(self, other):
return NotImplemented

def match(self, files):
# type: (Iterable[Text]) -> Iterable[Text]
"""
Matches this pattern against the specified files.
Expand All @@ -130,6 +147,7 @@ def match(self, files):

@classmethod
def pattern_to_regex(cls, pattern):
# type: (Text) -> Tuple[Text, bool]
"""
Convert the pattern into an uncompiled regular expression.
Expand Down
61 changes: 43 additions & 18 deletions pathspec/patterns/gitwildmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

import re
import warnings
try:
from typing import (
AnyStr,
Optional,
Text,
Tuple)
except ImportError:
pass

from .. import util
from ..compat import unicode
Expand All @@ -28,6 +36,7 @@ class GitWildMatchPattern(RegexPattern):

@classmethod
def pattern_to_regex(cls, pattern):
# type: (AnyStr) -> Tuple[Optional[AnyStr], Optional[bool]]
"""
Convert the pattern into a regular expression.
Expand Down Expand Up @@ -205,6 +214,7 @@ def pattern_to_regex(cls, pattern):

@staticmethod
def _translate_segment_glob(pattern):
# type: (Text) -> Text
"""
Translates the glob pattern to a regular expression. This is used in
the constructor to translate a path segment glob pattern to its
Expand Down Expand Up @@ -245,28 +255,28 @@ def _translate_segment_glob(pattern):
regex += '[^/]'

elif char == '[':
# Braket expression wildcard. Except for the beginning
# exclamation mark, the whole braket expression can be used
# Bracket expression wildcard. Except for the beginning
# exclamation mark, the whole bracket expression can be used
# directly as regex but we have to find where the expression
# ends.
# - "[][!]" matchs ']', '[' and '!'.
# - "[]-]" matchs ']' and '-'.
# - "[!]a-]" matchs any character except ']', 'a' and '-'.
# - "[][!]" matches ']', '[' and '!'.
# - "[]-]" matches ']' and '-'.
# - "[!]a-]" matches any character except ']', 'a' and '-'.
j = i
# Pass brack expression negation.
if j < end and pattern[j] == '!':
j += 1
# Pass first closing braket if it is at the beginning of the
# Pass first closing bracket if it is at the beginning of the
# expression.
if j < end and pattern[j] == ']':
j += 1
# Find closing braket. Stop once we reach the end or find it.
# Find closing bracket. Stop once we reach the end or find it.
while j < end and pattern[j] != ']':
j += 1

if j < end:
# Found end of braket expression. Increment j to be one past
# the closing braket:
# Found end of bracket expression. Increment j to be one past
# the closing bracket:
#
# [...]
# ^ ^
Expand All @@ -280,27 +290,27 @@ def _translate_segment_glob(pattern):
expr += '^'
i += 1
elif pattern[i] == '^':
# POSIX declares that the regex braket expression negation
# POSIX declares that the regex bracket expression negation
# "[^...]" is undefined in a glob pattern. Python's
# `fnmatch.translate()` escapes the caret ('^') as a
# literal. To maintain consistency with undefined behavior,
# I am escaping the '^' as well.
expr += '\\^'
i += 1

# Build regex braket expression. Escape slashes so they are
# Build regex bracket expression. Escape slashes so they are
# treated as literal slashes by regex as defined by POSIX.
expr += pattern[i:j].replace('\\', '\\\\')

# Add regex braket expression to regex result.
# Add regex bracket expression to regex result.
regex += expr

# Set i to one past the closing braket.
# Set i to one past the closing bracket.
i = j

else:
# Failed to find closing braket, treat opening braket as a
# braket literal instead of as an expression.
# Failed to find closing bracket, treat opening bracket as a
# bracket literal instead of as an expression.
regex += '\\['

else:
Expand All @@ -311,18 +321,33 @@ def _translate_segment_glob(pattern):

@staticmethod
def escape(s):
# type: (AnyStr) -> AnyStr
"""
Escape special characters in the given string.
*s* (:class:`unicode` or :class:`bytes`) a filename or a string
that you want to escape, usually before adding it to a `.gitignore`
Returns the escaped string (:class:`unicode`, :class:`bytes`)
Returns the escaped string (:class:`unicode` or :class:`bytes`)
"""
if isinstance(s, unicode):
return_type = unicode
string = s
elif isinstance(s, bytes):
return_type = bytes
string = s.decode(_BYTES_ENCODING)
else:
raise TypeError("s:{!r} is not a unicode or byte string.".format(s))

# Reference: https://git-scm.com/docs/gitignore#_pattern_format
meta_characters = r"[]!*#?"

return "".join("\\" + x if x in meta_characters else x for x in s)
out_string = "".join("\\" + x if x in meta_characters else x for x in string)

if return_type is bytes:
return out_string.encode(_BYTES_ENCODING)
else:
return out_string

util.register_pattern('gitwildmatch', GitWildMatchPattern)

Expand All @@ -338,7 +363,7 @@ def __init__(self, *args, **kw):
Warn about deprecation.
"""
self._deprecated()
return super(GitIgnorePattern, self).__init__(*args, **kw)
super(GitIgnorePattern, self).__init__(*args, **kw)

@staticmethod
def _deprecated():
Expand Down

0 comments on commit c00b332

Please sign in to comment.