Skip to content

Initial implementation of allow patterns #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/orgecc/filematcher/core/file_matcher_base.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
from typing import override
from functools import lru_cache

from ..file_matcher_api import FileMatcher, FileMatcherFactory, DenyPatternSource
from ..file_matcher_api import FileMatcher, FileMatcherFactory, DenyPatternSource, AllowPatternSource


class FileMatcherFactoryBase(FileMatcherFactory):

def _new_matcher(self, patterns: tuple[str, ...]) -> FileMatcher: ...
def _new_matcher(
self, deny_patterns: tuple[str, ...],
allow_patterns: tuple[str, ...] = tuple()
) -> FileMatcher: ...

@lru_cache(maxsize=128)
def _cached_pattern2matcher(self, patterns: tuple[str, ...]) -> FileMatcher:
return self._new_matcher(patterns)
def _cached_pattern2matcher(
self, deny_patterns: tuple[str, ...],
allow_patterns: tuple[str, ...] = tuple()
) -> FileMatcher:
return self._new_matcher(deny_patterns, allow_patterns)

@override
def pattern2matcher(
self,
deny_source: DenyPatternSource
deny_source: DenyPatternSource,
allow_source: AllowPatternSource | None = None
) -> FileMatcher:
return self._cached_pattern2matcher(deny_source.deny_patterns)
return self._cached_pattern2matcher(
deny_source.deny_patterns,
allow_source.allow_patterns if allow_source is not None else tuple()
)
9 changes: 5 additions & 4 deletions src/orgecc/filematcher/core/file_matcher_ext_gitignorefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class ExtLibGitignorefileMatcherFactory(FileMatcherFactoryBase):
"""
This factory creates matchers that delegate pattern matching to the
external library 'gitignorefile'.
external library 'gitignorefile'
"""
def __enter__(self):
"""Context manager entry point."""
Expand All @@ -19,17 +19,18 @@ def __exit__(self, exc_type, exc_val, exc_tb):
pass

@override
def _new_matcher(self, patterns: tuple[str, ...]) -> FileMatcher:
def _new_matcher(self, deny_patterns: tuple[str, ...], allow_patterns=tuple()) -> FileMatcher:
"""
Create a new matcher instance for the given patterns.

Args:
patterns: A tuple of gitignore pattern strings.
deny_patterns: A tuple of gitignore pattern strings.

Returns:
A FileMatcher instance configured with the given patterns.
:param allow_patterns:
"""
return _ExtLibGitignorefileMatcher(patterns)
return _ExtLibGitignorefileMatcher(deny_patterns)


class _ExtLibGitignorefileMatcher(FileMatcher):
Expand Down
7 changes: 4 additions & 3 deletions src/orgecc/filematcher/core/file_matcher_ext_pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ def __exit__(self, exc_type, exc_val, exc_tb):
pass

@override
def _new_matcher(self, patterns: tuple[str, ...]) -> FileMatcher:
def _new_matcher(self, deny_patterns: tuple[str, ...], allow_patterns=tuple()) -> FileMatcher:
"""
Create a new matcher instance for the given patterns.

Args:
patterns: A tuple of gitignore pattern strings.
deny_patterns: A tuple of gitignore pattern strings.

Returns:
A FileMatcher instance configured with the given patterns.
:param allow_patterns:
"""
return _ExtLibPathspecMatcher(patterns)
return _ExtLibPathspecMatcher(deny_patterns)


class _ExtLibPathspecMatcher(FileMatcher):
Expand Down
4 changes: 2 additions & 2 deletions src/orgecc/filematcher/core/file_matcher_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
pass

@override
def _new_matcher(self, patterns: tuple[str, ...]) -> FileMatcher:
def _new_matcher(self, deny_patterns: tuple[str, ...], allow_patterns=tuple()) -> FileMatcher:
with self._lock:
self._instance_counter += 1
instance_id = self._instance_counter
return _GitIgnoreNativeMatcher(patterns, instance_id, self)
return _GitIgnoreNativeMatcher(deny_patterns, instance_id, self)

def cleanup_matcher(self, instance_id: int) -> None:
if self._temp_dir:
Expand Down
52 changes: 40 additions & 12 deletions src/orgecc/filematcher/core/file_matcher_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ def __exit__(self, exc_type, exc_val, exc_tb):
pass

@override
def _new_matcher(self, patterns: tuple[str, ...]) -> FileMatcher:
def _new_matcher(self, deny_patterns: tuple[str, ...], allow_patterns: tuple[str, ...] = tuple()) -> FileMatcher:
"""
Create a new matcher instance for the given patterns.

Args:
patterns: A tuple of gitignore pattern strings.
deny_patterns: A tuple of gitignore pattern strings.

Returns:
A FileMatcher instance configured with the given patterns.
:param allow_patterns:
"""
return _GitIgnorePythonMatcher(patterns)
return _GitIgnorePythonMatcher(deny_patterns, allow_patterns)

@lru_cache(maxsize=512)
def gitignore_syntax_2_fnmatch(
Expand Down Expand Up @@ -318,25 +319,38 @@ class _GitIgnorePythonMatcher(FileMatcher):
behavior of .gitignore files.
"""

__slots__ = ('patterns', 'base_path')
__slots__ = ('deny_patterns', 'allow_patterns', 'base_path')

def __init__(self, patterns: tuple[str, ...], base_path: str = "."):
def __init__(
self,
deny_patterns: tuple[str, ...],
allow_patterns: tuple[str, ...] = tuple(),
base_path: str = "."
):
"""
Initialize GitIgnoreParser with a list of patterns.

Args:
patterns: list of gitignore pattern strings.
deny_patterns: list of gitignore pattern strings.
base_path: Base directory for relative patterns.
"""
self.patterns: list[FilePattern] = []
self.deny_patterns: list[FilePattern] = []
self.allow_patterns: list[FilePattern] = []
self.base_path = Path(base_path).resolve()

for pattern_str in patterns:
for pattern_str in deny_patterns:
parsed = FilePattern.from_line(pattern_str)
# Debug: Log the result of parsing each pattern
logging.debug("[_parse_pattern] '%s' -> %s", pattern_str, parsed)
logging.debug("[_parse_pattern deny] '%s' -> %s", pattern_str, parsed)
if parsed is not None:
self.patterns.append(parsed)
self.deny_patterns.append(parsed)

for pattern_str in allow_patterns:
parsed = FilePattern.from_line(pattern_str)
# Debug: Log the result of parsing each pattern
logging.debug("[_parse_pattern allow] '%s' -> %s", pattern_str, parsed)
if parsed is not None:
self.allow_patterns.append(parsed)

@override
def match(self, path: str, is_dir: bool=False) -> FileMatchResult:
Expand All @@ -355,11 +369,25 @@ def match(self, path: str, is_dir: bool=False) -> FileMatchResult:

# Last match wins
_match = None
for file_pattern in self.patterns:
for file_pattern in self.deny_patterns:
result = file_pattern.match(path, path_is_dir)
if result.matches:
_match = result._replace(matches=not file_pattern.is_negative)
if _match.matches and _match.by_dir:
_match = _match._replace(description=f"{_match.description} (early stop)")
break
result_before_allow = _match or FileMatchResult(False)
if result_before_allow.matches or not self.allow_patterns:
return result_before_allow

# matches = False

_match = FileMatchResult(False)
for file_pattern in self.allow_patterns:
result = file_pattern.match(path, path_is_dir)
if result.matches:
_match = result._replace(matches=not file_pattern.is_negative)
if _match.matches and _match.by_dir:
_match = _match._replace(description=f"{_match.description} (early stop)")
break
return _match or FileMatchResult(False)
return result_before_allow._replace(matches=not _match.matches)
13 changes: 11 additions & 2 deletions src/orgecc/filematcher/file_matcher_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,23 @@
from typing import Protocol, Iterable
from collections import namedtuple


class DenyPatternSource(Iterable[str]):
@property
def deny_patterns(self) -> tuple[str, ...]: ...

def __iter__(self) -> Iterable[str]:
return iter(self.deny_patterns)


class AllowPatternSource(Iterable[str]):
@property
def allow_patterns(self) -> set[str]: ...

def __iter__(self) -> Iterable[str]:
return iter(self.allow_patterns)


FileMatchResult = namedtuple('FileMatchResult', ['matches', 'description', 'by_dir'], defaults=[None, False])
"""
Represents the result of a file matching operation.
Expand All @@ -68,7 +71,7 @@ class FileMatcher(Protocol):
while maintaining a consistent interface.
"""

def match(self, path: str, is_dir: bool=False) -> FileMatchResult:
def match(self, path: str, is_dir: bool = False) -> FileMatchResult:
"""
Check if a given path matches the configured patterns.

Expand All @@ -81,6 +84,7 @@ def match(self, path: str, is_dir: bool=False) -> FileMatchResult:
"""
...


class FileMatcherFactory(Protocol):
"""
Protocol defining the interface for creating file matcher instances.
Expand All @@ -89,7 +93,11 @@ class FileMatcherFactory(Protocol):
while maintaining a consistent way to create matcher instances.
"""

def pattern2matcher(self, deny_source: DenyPatternSource) -> FileMatcher:
def pattern2matcher(
self,
deny_source: DenyPatternSource,
allow_source: AllowPatternSource | None = None
) -> FileMatcher:
"""
Create a new matcher instance from patterns or pattern files.

Expand All @@ -102,4 +110,5 @@ def pattern2matcher(self, deny_source: DenyPatternSource) -> FileMatcher:
...

def __enter__(self): ...

def __exit__(self, exc_type, exc_val, exc_tb): ...
5 changes: 3 additions & 2 deletions src/orgecc/filematcher/patterns/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from importlib.resources.abc import Traversable
from pathlib import PurePath
from typing import Iterable
from pathlib import Path, PurePath
from ..file_matcher_api import DenyPatternSource

from .pattern_kit import DenyPatternSourceImpl, DenyPatternSourceGroup
from ..file_matcher_api import DenyPatternSource

__all__ = ('new_deny_pattern_source', 'merge_deny_pattern_sources')

Expand Down