diff --git a/CHANGES.rst b/CHANGES.rst index b11b4a2..bbae426 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,18 @@ Change History ============== + +0.12.1 (TBD) +------------------- + +- Bug fixes: + +- `Issue #84`_: PathSpec.match_file() returns None since 0.12.0. + + +.. _`Issue #84`: https://github.com/cpburnz/python-pathspec/issues/84 + + 0.12.0 (2023-12-09) ------------------- @@ -12,7 +24,7 @@ Major changes: API changes: -- Signature of protected method `pathspec.pathspec.PathSpec._match_file()` has been changed from `def _match_file(patterns: Iterable[Pattern], file: str) -> bool` to `def _match_file(patterns: Iterable[Tuple[int, Pattern]], file: str) -> Tuple[Optional[bool], Optional[int]]`. +- Signature of protected method `pathspec.pathspec.PathSpec._match_file()` (with a leading underscore) has been changed from `def _match_file(patterns: Iterable[Pattern], file: str) -> bool` to `def _match_file(patterns: Iterable[Tuple[int, Pattern]], file: str) -> Tuple[Optional[bool], Optional[int]]`. New features: diff --git a/Makefile b/Makefile index f88a705..559e09d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,6 @@ # # This Makefile is used to manage development and distribution. # -# Created: 2022-08-11 -# Updated: 2022-09-01 -# .PHONY: build create-venv help prebuild publish test test-all test-docs update-venv diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 8483e71..173c4a4 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -55,4 +55,4 @@ "kurtmckee ", ] __license__ = "MPL 2.0" -__version__ = "0.12.0" +__version__ = "0.12.1.dev1" diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index 377f159..bdfaccd 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -277,7 +277,7 @@ def match_file( """ norm_file = normalize_file(file, separators) include, _index = self._match_file(enumerate(self.patterns), norm_file) - return include + return bool(include) def match_files( self, diff --git a/tests/test_01_util.py b/tests/test_01_util.py index 67c9b41..1385fe7 100644 --- a/tests/test_01_util.py +++ b/tests/test_01_util.py @@ -12,14 +12,15 @@ from functools import ( partial) from typing import ( - Iterable, - Optional, - Tuple) + Iterable, # Replaced by `collections.abc.Iterable` in 3.9. + Optional, # Replaced by `X | None` in 3.10. + Tuple) # Replaced by `tuple` in 3.9. from pathspec.patterns.gitwildmatch import ( GitWildMatchPattern) from pathspec.util import ( RecursionError, + check_match_file, iter_tree_entries, iter_tree_files, match_file, @@ -32,6 +33,82 @@ ospath) +class CheckMatchFileTest(unittest.TestCase): + """ + The :class:`CheckMatchFileTest` class tests the :meth:`.check_match_file` + function. + """ + + def test_01_single_1_include(self): + """ + Test checking a single file that is included. + """ + patterns = list(enumerate(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ]))) + + include_index = check_match_file(patterns, "include.txt") + + self.assertEqual(include_index, (True, 0)) + + def test_01_single_2_exclude(self): + """ + Test checking a single file that is excluded. + """ + patterns = list(enumerate(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ]))) + + include_index = check_match_file(patterns, "test/exclude.txt") + + self.assertEqual(include_index, (False, 1)) + + def test_01_single_3_unmatch(self): + """ + Test checking a single file that is ignored. + """ + patterns = list(enumerate(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ]))) + + include_index = check_match_file(patterns, "unmatch.bin") + + self.assertEqual(include_index, (None, None)) + + def test_02_many(self): + """ + Test matching files individually. + """ + patterns = list(enumerate(map(GitWildMatchPattern, [ + '*.txt', + '!b.txt', + ]))) + files = { + 'X/a.txt', + 'X/b.txt', + 'X/Z/c.txt', + 'Y/a.txt', + 'Y/b.txt', + 'Y/Z/c.txt', + } + + includes = { + __file + for __file in files + if check_match_file(patterns, __file)[0] + } + + self.assertEqual(includes, { + 'X/a.txt', + 'X/Z/c.txt', + 'Y/a.txt', + 'Y/Z/c.txt', + }) + + class IterTreeTest(unittest.TestCase): """ The :class:`IterTreeTest` class tests :meth:`.iter_tree_entries` and @@ -345,7 +422,46 @@ class MatchFileTest(unittest.TestCase): function. """ - def test_01_match_file(self): + def test_01_single_1_include(self): + """ + Test checking a single file that is included. + """ + patterns = list(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ])) + + include = match_file(patterns, "include.txt") + + self.assertIs(include, True) + + def test_01_single_2_exclude(self): + """ + Test checking a single file that is excluded. + """ + patterns = list(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ])) + + include = match_file(patterns, "test/exclude.txt") + + self.assertIs(include, False) + + def test_01_single_3_unmatch(self): + """ + Test checking a single file that is ignored. + """ + patterns = list(map(GitWildMatchPattern, [ + "*.txt", + "!test/", + ])) + + include = match_file(patterns, "unmatch.bin") + + self.assertIs(include, False) + + def test_02_many(self): """ Test matching files individually. """ @@ -353,15 +469,18 @@ def test_01_match_file(self): '*.txt', '!b.txt', ])) - results = set(filter(partial(match_file, patterns), [ + files = { 'X/a.txt', 'X/b.txt', 'X/Z/c.txt', 'Y/a.txt', 'Y/b.txt', 'Y/Z/c.txt', - ])) - self.assertEqual(results, { + } + + includes = set(filter(partial(match_file, patterns), files)) + + self.assertEqual(includes, { 'X/a.txt', 'X/Z/c.txt', 'Y/a.txt', diff --git a/tests/test_03_pathspec.py b/tests/test_03_pathspec.py index f77d18d..d1b18cd 100644 --- a/tests/test_03_pathspec.py +++ b/tests/test_03_pathspec.py @@ -17,6 +17,7 @@ iter_tree_entries) from .util import ( + CheckResult, debug_results, get_includes, make_dirs, @@ -109,22 +110,61 @@ def test_01_absolute_dir_paths_2(self): 'foo/a.py', }, debug) - def test_01_check_files(self): + def test_01_check_file_1_include(self): + """ + Test checking a single file that is included. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + result = spec.check_file("include.txt") + + self.assertEqual(result, CheckResult("include.txt", True, 0)) + + def test_01_check_file_2_exclude(self): + """ + Test checking a single file that is excluded. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + result = spec.check_file("test/exclude.txt") + + self.assertEqual(result, CheckResult("test/exclude.txt", False, 1)) + + def test_01_check_file_3_unmatch(self): + """ + Test checking a single file that is unmatched. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + result = spec.check_file("unmatch.bin") + + self.assertEqual(result, CheckResult("unmatch.bin", None, None)) + + def test_01_check_file_4_many(self): """ Test that checking files one at a time yields the same results as checking multiples files at once. """ spec = PathSpec.from_lines('gitwildmatch', [ '*.txt', - '!test1/**', + '!test1/', ]) files = { - 'src/test1/a.txt', - 'src/test1/b.txt', - 'src/test1/c/c.txt', - 'src/test2/a.txt', - 'src/test2/b.txt', - 'src/test2/c/c.txt', + 'test1/a.txt', + 'test1/b.txt', + 'test1/c/c.txt', + 'test2/a.txt', + 'test2/b.txt', + 'test2/c/c.txt', } single_results = set(map(spec.check_file, files)) @@ -218,6 +258,45 @@ def test_01_empty_path_2(self): '\\ ', ]) + def test_01_match_file_1_include(self): + """ + Test matching a single file that is included. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + include = spec.match_file("include.txt") + + self.assertIs(include, True) + + def test_01_match_file_2_exclude(self): + """ + Test matching a single file that is excluded. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + include = spec.match_file("test/exclude.txt") + + self.assertIs(include, False) + + def test_01_match_file_3_unmatch(self): + """ + Test match a single file that is unmatched. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + "*.txt", + "!test/", + ]) + + include = spec.match_file("unmatch.bin") + + self.assertIs(include, False) + def test_01_match_files(self): """ Test that matching files one at a time yields the same results as matching @@ -225,15 +304,15 @@ def test_01_match_files(self): """ spec = PathSpec.from_lines('gitwildmatch', [ '*.txt', - '!test1/**', + '!test1/', ]) files = { - 'src/test1/a.txt', - 'src/test1/b.txt', - 'src/test1/c/c.txt', - 'src/test2/a.txt', - 'src/test2/b.txt', - 'src/test2/c/c.txt', + 'test1/a.txt', + 'test1/b.txt', + 'test1/c/c.txt', + 'test2/a.txt', + 'test2/b.txt', + 'test2/c/c.txt', } single_files = set(filter(spec.match_file, files)) @@ -251,12 +330,12 @@ def test_01_windows_current_dir_paths(self): '!test1/', ]) files = { - '.\\src\\test1\\a.txt', - '.\\src\\test1\\b.txt', - '.\\src\\test1\\c\\c.txt', - '.\\src\\test2\\a.txt', - '.\\src\\test2\\b.txt', - '.\\src\\test2\\c\\c.txt', + '.\\test1\\a.txt', + '.\\test1\\b.txt', + '.\\test1\\c\\c.txt', + '.\\test2\\a.txt', + '.\\test2\\b.txt', + '.\\test2\\c\\c.txt', } results = list(spec.check_files(files, separators=['\\'])) @@ -264,9 +343,9 @@ def test_01_windows_current_dir_paths(self): debug = debug_results(spec, results) self.assertEqual(includes, { - '.\\src\\test2\\a.txt', - '.\\src\\test2\\b.txt', - '.\\src\\test2\\c\\c.txt', + '.\\test2\\a.txt', + '.\\test2\\b.txt', + '.\\test2\\c\\c.txt', }, debug) def test_01_windows_paths(self): @@ -278,12 +357,12 @@ def test_01_windows_paths(self): '!test1/', ]) files = { - 'src\\test1\\a.txt', - 'src\\test1\\b.txt', - 'src\\test1\\c\\c.txt', - 'src\\test2\\a.txt', - 'src\\test2\\b.txt', - 'src\\test2\\c\\c.txt', + 'test1\\a.txt', + 'test1\\b.txt', + 'test1\\c\\c.txt', + 'test2\\a.txt', + 'test2\\b.txt', + 'test2\\c\\c.txt', } results = list(spec.check_files(files, separators=['\\'])) @@ -291,9 +370,9 @@ def test_01_windows_paths(self): debug = debug_results(spec, results) self.assertEqual(includes, { - 'src\\test2\\a.txt', - 'src\\test2\\b.txt', - 'src\\test2\\c\\c.txt', + 'test2\\a.txt', + 'test2\\b.txt', + 'test2\\c\\c.txt', }, debug) def test_02_eq(self):