Skip to content

Commit

Permalink
Implement issue 80
Browse files Browse the repository at this point in the history
  • Loading branch information
cpburnz committed Jul 29, 2023
1 parent 933dd7d commit fb2246c
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 43 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Change History
0.11.2 (TBD)
------------

New features:

- `Issue #80`_: match_files with negated path spec. `pathspec.PathSpec.match_*()` now have a `negate` parameter to make using *.gitignore* logic easier and more efficient.

Bug fixes:

- `Pull #76`_: Add edge case: patterns that end with an escaped space
Expand All @@ -15,6 +19,7 @@ Bug fixes:
.. _`Pull #76`: https://github.com/cpburnz/python-pathspec/pull/76
.. _`Issue #77`: https://github.com/cpburnz/python-pathspec/issues/77
.. _`Pull #78`: https://github.com/cpburnz/python-pathspec/pull/78/
.. _`Issue #80`: https://github.com/cpburnz/python-pathspec/issues/80


0.11.1 (2023-03-14)
Expand Down
1 change: 1 addition & 0 deletions pathspec/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"yschroeder <https://github.com/yschroeder>",
"axesider <https://github.com/axesider>",
"tomruk <https://github.com/tomruk>",
"oprypin <https://github.com/oprypin>",
]
__license__ = "MPL 2.0"
__version__ = "0.11.2.dev1"
116 changes: 74 additions & 42 deletions pathspec/pathspec.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
"""
This module provides an object oriented interface for pattern matching
of files.
This module provides an object oriented interface for pattern matching of files.
"""

import sys
from collections.abc import (
Collection as CollectionType)
from itertools import (
zip_longest)
from os import (
PathLike)
from typing import (
AnyStr,
Callable,
Expand Down Expand Up @@ -107,15 +103,15 @@ def from_lines(
"""
Compiles the pattern lines.
*pattern_factory* can be either the name of a registered pattern
factory (:class:`str`), or a :class:`~collections.abc.Callable` used
to compile patterns. It must accept an uncompiled pattern (:class:`str`)
and return the compiled pattern (:class:`.Pattern`).
*pattern_factory* can be either the name of a registered pattern factory
(:class:`str`), or a :class:`~collections.abc.Callable` used to compile
patterns. It must accept an uncompiled pattern (:class:`str`) and return the
compiled pattern (:class:`.Pattern`).
*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`.
*lines* (:class:`~collections.abc.Iterable`) yields each uncompiled pattern
(:class:`str`). This simply has to yield each line so that it can be a
:class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or
the result from :meth:`str.splitlines`.
Returns the :class:`PathSpec` instance.
"""
Expand All @@ -135,17 +131,23 @@ def match_entries(
self,
entries: Iterable[TreeEntry],
separators: Optional[Collection[str]] = None,
*,
negate: Optional[bool] = None,
) -> Iterator[TreeEntry]:
"""
Matches the entries to this path-spec.
*entries* (:class:`~collections.abc.Iterable` of :class:`~util.TreeEntry`)
contains the entries to be matched against :attr:`self.patterns <PathSpec.patterns>`.
*separators* (:class:`~collections.abc.Collection` of :class:`str`;
or :data:`None`) optionally contains the path separators to
normalize. See :func:`~pathspec.util.normalize_file` for more
information.
*separators* (:class:`~collections.abc.Collection` of :class:`str`; or
:data:`None`) optionally contains the path separators to normalize. See
:func:`~pathspec.util.normalize_file` for more information.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
Returns the matched entries (:class:`~collections.abc.Iterator` of
:class:`~util.TreeEntry`).
Expand All @@ -156,12 +158,17 @@ def match_entries(
use_patterns = _filter_patterns(self.patterns)
for entry in entries:
norm_file = normalize_file(entry.path, separators)
if self._match_file(use_patterns, norm_file):
is_match = self._match_file(use_patterns, norm_file)

if negate:
is_match = not is_match

if is_match:
yield entry

# Match files using the `match_file()` utility function. Subclasses
# may override this method as an instance method. It does not have to
# be a static method.
# Match files using the `match_file()` utility function. Subclasses may
# override this method as an instance method. It does not have to be a static
# method.
_match_file = staticmethod(match_file)

def match_file(
Expand All @@ -188,6 +195,8 @@ def match_files(
self,
files: Iterable[StrPath],
separators: Optional[Collection[str]] = None,
*,
negate: Optional[bool] = None,
) -> Iterator[StrPath]:
"""
Matches the files to this path-spec.
Expand All @@ -196,10 +205,14 @@ def match_files(
:class:`os.PathLike[str]`) contains the file paths to be matched against
:attr:`self.patterns <PathSpec.patterns>`.
*separators* (:class:`~collections.abc.Collection` of :class:`str`;
or :data:`None`) optionally contains the path separators to
normalize. See :func:`~pathspec.util.normalize_file` for more
information.
*separators* (:class:`~collections.abc.Collection` of :class:`str`; or
:data:`None`) optionally contains the path separators to normalize. See
:func:`~pathspec.util.normalize_file` for more information.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
Returns the matched files (:class:`~collections.abc.Iterator` of
:class:`str` or :class:`os.PathLike[str]`).
Expand All @@ -210,63 +223,82 @@ def match_files(
use_patterns = _filter_patterns(self.patterns)
for orig_file in files:
norm_file = normalize_file(orig_file, separators)
if self._match_file(use_patterns, norm_file):
is_match = self._match_file(use_patterns, norm_file)

if negate:
is_match = not is_match

if is_match:
yield orig_file

def match_tree_entries(
self,
root: StrPath,
on_error: Optional[Callable] = None,
follow_links: Optional[bool] = None,
*,
negate: Optional[bool] = None,
) -> Iterator[TreeEntry]:
"""
Walks the specified root path for all files and matches them to this
path-spec.
*root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory
to search.
*root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to
search.
*on_error* (:class:`~collections.abc.Callable` or :data:`None`)
optionally is the error handler for file-system exceptions. See
*on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally
is the error handler for file-system exceptions. See
:func:`~pathspec.util.iter_tree_entries` for more information.
*follow_links* (:class:`bool` or :data:`None`) optionally is whether
to walk symbolic links that resolve to directories. See
*follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk
symbolic links that resolve to directories. See
:func:`~pathspec.util.iter_tree_files` for more information.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
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)
yield from self.match_entries(entries)
yield from self.match_entries(entries, negate=negate)

def match_tree_files(
self,
root: StrPath,
on_error: Optional[Callable] = None,
follow_links: Optional[bool] = None,
*,
negate: Optional[bool] = None,
) -> Iterator[str]:
"""
Walks the specified root path for all files and matches them to this
path-spec.
*root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory
to search for files.
*root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to
search for files.
*on_error* (:class:`~collections.abc.Callable` or :data:`None`)
optionally is the error handler for file-system exceptions. See
*on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally
is the error handler for file-system exceptions. See
:func:`~pathspec.util.iter_tree_files` for more information.
*follow_links* (:class:`bool` or :data:`None`) optionally is whether
to walk symbolic links that resolve to directories. See
*follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk
symbolic links that resolve to directories. See
:func:`~pathspec.util.iter_tree_files` for more information.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
Returns the matched files (:class:`~collections.abc.Iterable` of
:class:`str`).
"""
files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links)
yield from self.match_files(files)
yield from self.match_files(files, negate=negate)

# Alias `match_tree_files()` as `match_tree()` for backward
# compatibility before v0.3.2.
# Alias `match_tree_files()` as `match_tree()` for backward compatibility
# before v0.3.2.
match_tree = match_tree_files
50 changes: 50 additions & 0 deletions tests/test_pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,53 @@ def test_08_issue_39(self):
'important/d.log',
'important/e.txt',
})

def test_09_issue_80_a(self):
"""
Test negating patterns.
"""
spec = PathSpec.from_lines('gitwildmatch', [
'build',
'*.log',
'.*',
'!.gitignore',
])
files = {
'.c-tmp',
'.gitignore',
'a.log',
'b.txt',
'build/d.log',
'build/trace.bin',
'trace.c',
}
keeps = set(spec.match_files(files, negate=True))
self.assertEqual(keeps, {
'.gitignore',
'b.txt',
'trace.c',
})

def test_09_issue_80_b(self):
"""
Test negating patterns.
"""
spec = PathSpec.from_lines('gitwildmatch', [
'build',
'*.log',
'.*',
'!.gitignore',
])
files = {
'.c-tmp',
'.gitignore',
'a.log',
'b.txt',
'build/d.log',
'build/trace.bin',
'trace.c',
}
keeps = set(spec.match_files(files, negate=True))
ignores = set(spec.match_files(files))
self.assertEqual(files - ignores, keeps)
self.assertEqual(files - keeps, ignores)
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py37, py38, py39, py310, py311, pypy3
envlist = py37, py38, py39, py310, py311, py312, pypy3
isolated_build = True

[testenv]
Expand Down

0 comments on commit fb2246c

Please sign in to comment.