Repository: valohai/gitignorant (language: python)

Issue:
Can't ignore folders
Folder ignoring doesn’t work with custom files like `.vhignore`.

But you can use `.gitignore` to ignore folders (e.g. a line with `directory/`).
Can't ignore folders
Folder ignoring doesn’t work with custom files like `.vhignore`.

But you can use `.gitignore` to ignore folders (e.g. a line with `directory/`).


Discussion / hints:

Fix patch:
diff --git a/gitignorant/__init__.py b/gitignorant/__init__.py
index b0b4f06..22da95f 100644
--- a/gitignorant/__init__.py
+++ b/gitignorant/__init__.py
@@ -1,11 +1,14 @@
+import os
 import re
 from functools import lru_cache
-from typing import Iterable, TextIO, Optional, Union, List
+from typing import Callable, Iterable, List, Optional, TextIO, Tuple, Union
 
 __all__ = [
     "Rule",
     "check_match",
+    "check_path_match",
     "parse_gitignore_file",
+    "try_parse_rule",
 ]
 
 # TODO: this may not correctly support \] within ]
@@ -71,7 +74,7 @@ def __init__(self, *, negative: bool, content: str) -> None:
         self.content = str(content)
 
     def __repr__(self) -> str:
-        return f'<Rule {self.content!r}{ "(negative)" if self.negative else ""}>'
+        return f'<Rule {self.content!r}{" (negative)" if self.negative else ""}>'
 
     def matches(self, path: str, is_dir: bool = False) -> bool:
         pat = self.content
@@ -132,13 +135,57 @@ def try_parse_rule(line: str) -> Optional["Rule"]:
     return Rule(negative=negative, content=line)
 
 
-def check_match(rules: List[Rule], path: str, is_dir: bool = False) -> bool:
+def _find_match(rules: List[Rule], path: str, is_dir: bool = False) -> Optional[bool]:
+    """
+    Internal helper function for check_match() and check_path_match().
+
+    Returns True or False if the path matches any of the rules
+    (where True is returned for positive rules and False for negative rules).
+
+    Returns None if the path matches no rules.
+    """
+
     # Algorithm: Find the last matching rule in the list and
     #            figure out whether it was not negative.
     for rule in reversed(rules):
         if rule.matches(path, is_dir):
             return not rule.negative
-    return False
+    return None
+
+
+def check_match(rules: List[Rule], path: str, is_dir: bool = False) -> bool:
+    """
+    Check whether the given string (likely a path) matches any of the given rules,
+    but without any splitting of the path into components.
+
+    See check_path_match() for a version that splits the path into components.
+    """
+    return bool(_find_match(rules, path, is_dir))
+
+
+def check_path_match(
+    rules: List[Rule],
+    path: str,
+    split_path: Callable[[str], Tuple[str, str]] = os.path.split,
+) -> bool:
+    """
+    Check whether the given path matches any of the rules.
+
+    In other words,
+
+    * Split `path` into directory and file parts using the `split_path` function.
+    * Check whether the directory part matches any directory rule,
+      and if it does, consider that the result.
+    * Check whether the full path matches any file rule.
+    """
+
+    dirname, basename = split_path(path)
+
+    if dirname:
+        dir_match = _find_match(rules, dirname, is_dir=True)
+        if dir_match is not None:
+            return dir_match
+    return check_match(rules, path, is_dir=False)
 
 
 def parse_gitignore_file(f: Union[TextIO, Iterable[str]]) -> Iterable[Rule]:


Test patch:
diff --git a/test_gitignorant.py b/test_gitignorant.py
index a6b03f2..ca3b96b 100644
--- a/test_gitignorant.py
+++ b/test_gitignorant.py
@@ -2,7 +2,13 @@
 
 import pytest
 
-from gitignorant import parse_gitignore_file, check_match, Rule, try_parse_rule
+from gitignorant import (
+    Rule,
+    check_match,
+    check_path_match,
+    parse_gitignore_file,
+    try_parse_rule,
+)
 
 GITIGNORE_STRING = r"""
 # Hello! this line is ignored.
@@ -34,37 +40,48 @@ def rules() -> List[Rule]:
     return list(parse_gitignore_file(GITIGNORE_STRING.strip().splitlines()))
 
 
-@pytest.mark.parametrize(
-    "expected,path",
-    [
-        (False, "hello"),
-        (True, "hello.peasoup"),
-        (False, "hello.peasoupiness"),
-        (True, "ballo/allo.peasoup"),
-        (False, "allo.peasoup"),
-        (False, "cullo.peasoup"),
-        (True, "bazze/allo.peasoup"),
-        (False, "booze/allo.peasoup"),
-        (True, "booze/scaffolding/allo.peasoup"),
-        (False, "scaffolding/allo.peasoup"),
-        (True, "asdf/ab.john"),
-        (False, "asdf/aba.john"),
-        (True, "ab.john"),
-        (False, "asdf/cb.john"),
-        (True, "!important_1*"),
-        (True, "spaced_out "),
-        (False, "spaced_out"),
-        (True, "zztop"),
-        (False, "jazztop"),
-        (False, "# comment"),
-        (False, "#comment"),
-        (False, "comment"),
-        (False, "comment_after.file"),
-        (True, "comment_after.file # a comment must be on its own line"),
-    ],
-)
-def test_gitignorant_files(rules: List[Rule], path: str, expected: bool) -> None:
-    assert check_match(rules, path, is_dir=False) == expected
+TEST_CASES = [
+    (False, "hello"),
+    (True, "hello.peasoup"),
+    (False, "hello.peasoupiness"),
+    (True, "ballo/allo.peasoup"),
+    (False, "allo.peasoup"),
+    (False, "cullo.peasoup"),
+    (True, "bazze/allo.peasoup"),
+    (False, "booze/allo.peasoup"),
+    (True, "booze/scaffolding/allo.peasoup"),
+    (False, "scaffolding/allo.peasoup"),
+    (True, "asdf/ab.john"),
+    (False, "asdf/aba.john"),
+    (True, "ab.john"),
+    (False, "asdf/cb.john"),
+    (True, "!important_1*"),
+    (True, "spaced_out "),
+    (False, "spaced_out"),
+    (True, "zztop"),
+    (False, "jazztop"),
+    (False, "# comment"),
+    (False, "#comment"),
+    (False, "comment"),
+    (False, "comment_after.file"),
+    (True, "comment_after.file # a comment must be on its own line"),
+]
+
+
+@pytest.mark.parametrize(["expected", "path"], TEST_CASES)
+@pytest.mark.parametrize("func", ["check_match", "check_path_match"])
+def test_gitignorant_files(
+    rules: List[Rule],
+    path: str,
+    expected: bool,
+    func: str,
+) -> None:
+    if func == "check_match":
+        assert check_match(rules, path, is_dir=False) == expected
+    elif func == "check_path_match":
+        assert check_path_match(rules, path) == expected
+    else:
+        raise NotImplementedError("...")
 
 
 @pytest.mark.parametrize(
@@ -154,3 +171,20 @@ def test_unfinished_group_parsing() -> None:
     assert r1.matches("unfinished/symph", is_dir=False)
     assert r1.matches("unfinished/sympy", is_dir=False)
     assert not r1.matches("unfinished/sympathy", is_dir=False)
+
+
+CHECK_PATH_MATCH_CASES = [
+    # These should match since `recipes/` is in the list,
+    # and it's not anchored to the root
+    ("recipes/zep", True),
+    ("splop/recipes/zep", True),
+    # This should not match, since `/other/recipes/` is explicitly negated
+    ("other/recipes/zep", False),
+    # This too should match, since it's trying to ignore the whole folder
+    ("recipes/", True),
+]
+
+
+@pytest.mark.parametrize(["path", "expected"], CHECK_PATH_MATCH_CASES)
+def test_check_path_match(rules: List[Rule], path: str, expected: bool) -> None:
+    assert check_path_match(rules, path) == expected