2424class IgnorePattern :
2525 """Single parsed ignore pattern.
2626
27- Follows Docker-style .dockerignore semantics, supports other ignore use cases following same approach.
28-
29- Details:
30- - ``pattern``: The normalized pattern text with leading/trailing ``/`` removed.
31- Always uses POSIX ``'/'`` separators.
32- - ``negated``: True if this is a negation pattern starting with ``!``.
33- - ``directory_only``: True if the original pattern ended with ``/`` and should
34- apply only to directories and their descendants.
35- - ``anchored``: True if the pattern contains a path separator and should be
36- matched relative to the root path rather than at any depth.
27+ Follows Docker-style ``.dockerignore`` semantics and supports other ignore
28+ use cases following the same approach.
3729 """
3830
3931 pattern : str
32+ """The normalized pattern text with leading and trailing ``/`` removed.
33+
34+ Always uses POSIX ``'/'`` separators.
35+ """
36+
4037 negated : bool
38+ """Whether this is a negation pattern starting with ``!``."""
39+
4140 directory_only : bool
41+ """Whether the original pattern ended with ``/`` and should apply only to
42+ directories and their descendants.
43+ """
44+
4245 anchored : bool
46+ """Whether the pattern contains a path separator and should be matched
47+ relative to the root path rather than at any depth.
48+ """
49+
50+
51+ def _normalize_pattern_string (raw : str ) -> str :
52+ """Normalize a single ignore pattern string.
53+
54+ Shared helper for patterns coming from both ignorefiles and inline pattern
55+ lists. Handles:
56+
57+ - Optional leading ``!`` negation marker (with surrounding whitespace
58+ trimmed).
59+ - ``os.path.normpath`` cleanup.
60+ - Normalising path separators to POSIX ``'/'``.
61+ - Stripping a single leading ``/`` so absolute-style patterns behave like
62+ relative ones.
63+
64+ Comment / blank-line handling is deliberately *not* included here; callers
65+ are responsible for that.
66+ """
67+
68+ if not raw :
69+ return raw
70+
71+ invert = raw [0 ] == "!"
72+ pattern = raw [1 :].strip () if invert else raw .strip ()
73+
74+ if pattern :
75+ # filepath.Clean equivalent
76+ pattern = os .path .normpath (pattern )
77+ # filepath.ToSlash equivalent
78+ pattern = pattern .replace (os .sep , "/" )
79+ # Leading forward-slashes are removed so "/some/path" and "some/path"
80+ # are considered equivalent.
81+ if len (pattern ) > 1 and pattern [0 ] == "/" :
82+ pattern = pattern [1 :]
83+
84+ if invert :
85+ pattern = "!" + pattern
86+
87+ return pattern
4388
4489
4590def _normalize_pattern_line (raw : bytes , * , is_first_line : bool ) -> Optional [str ]:
4691 """Normalize a single ignorefile line, mirroring moby's ignorefile.ReadAll.
4792
4893 Behavior is based on:
4994 https://github.com/moby/patternmatcher/blob/main/ignorefile/ignorefile.go
95+
96+ :param raw: Raw line bytes from the ignore file, including any newline
97+ characters.
98+ :type raw: bytes
99+ :param is_first_line: Whether this is the first line in the file (used to
100+ detect and strip a UTF-8 BOM).
101+ :type is_first_line: bool
102+ :return: Normalized pattern string, or ``None`` if the line should be
103+ ignored (empty or comment).
104+ :rtype: Optional[str]
50105 """
51106
52107 # Strip UTF-8 BOM from the first line if present
@@ -67,40 +122,26 @@ def _normalize_pattern_line(raw: bytes, *, is_first_line: bool) -> Optional[str]
67122 if not pattern :
68123 return None
69124
70- # Normalize absolute paths to paths relative to the context (taking care of '!' prefix)
71- invert = pattern [0 ] == "!"
72- if invert :
73- pattern = pattern [1 :].strip ()
74-
75- if pattern :
76- # filepath.Clean equivalent
77- pattern = os .path .normpath (pattern )
78- # filepath.ToSlash equivalent
79- pattern = pattern .replace (os .sep , "/" )
80- # Leading forward-slashes are removed so "/some/path" and "some/path"
81- # are considered equivalent.
82- if len (pattern ) > 1 and pattern [0 ] == "/" :
83- pattern = pattern [1 :]
125+ normalized = _normalize_pattern_string (pattern )
126+ return normalized or None
84127
85- if invert :
86- pattern = "!" + pattern
87-
88- return pattern
89128
90-
91- def read_ignorefile (path : Optional [Path ]) -> list [str ]:
129+ def read_ignorefile (path : Path ) -> list [str ]:
92130 """Read an ignore file and return a list of normalized pattern strings.
93131
94132 This mirrors the behavior of moby's ``ignorefile.ReadAll``:
95133
96134 - UTF-8 BOM on the first line is stripped.
97135 - Lines starting with ``#`` are treated as comments and skipped.
98136 - Remaining lines are trimmed, optionally negated with ``!``, cleaned,
99- have path separators normalized to ``/``, and leading ``/`` removed.
100- """
137+ have path separators normalized to ``/``, and leading and trailing ``/`` removed.
101138
102- if path is None :
103- return []
139+ :param path: Filesystem path to the ignore file to read.
140+ :type path: Path
141+ :return: List of normalized pattern strings in the order they appear in
142+ the ignore file.
143+ :rtype: list[str]
144+ """
104145
105146 if not path .exists ():
106147 return []
@@ -119,7 +160,13 @@ def read_ignorefile(path: Optional[Path]) -> list[str]:
119160
120161
121162def compile_ignore (patterns : Sequence [str ]) -> list [IgnorePattern ]:
122- """Compile raw pattern strings into :class:`IgnorePattern` objects."""
163+ """Compile raw pattern strings into :class:`IgnorePattern` objects.
164+
165+ :param patterns: Raw pattern strings following Docker-style semantics.
166+ :type patterns: Sequence[str]
167+ :return: Compiled ignore patterns.
168+ :rtype: list[IgnorePattern]
169+ """
123170
124171 compiled : list [IgnorePattern ] = []
125172
@@ -160,9 +207,17 @@ def _segment_match(pattern_segment: str, path_segment: str) -> bool:
160207 """Match a single path segment against a glob pattern segment.
161208
162209 Supports:
210+
163211 - ``*``: any sequence of characters except ``/``.
164212 - ``?``: any single character except ``/``.
165213 - ``[]``: character classes, excluding ``/``.
214+
215+ :param pattern_segment: Glob-style pattern segment.
216+ :type pattern_segment: str
217+ :param path_segment: Path segment (no ``/``) to match against.
218+ :type path_segment: str
219+ :return: ``True`` if the path segment matches the pattern segment.
220+ :rtype: bool
166221 """
167222
168223 import re
@@ -195,7 +250,15 @@ def _segment_match(pattern_segment: str, path_segment: str) -> bool:
195250
196251
197252def _match_parts_recursive (pattern_parts : list [str ], path_parts : list [str ]) -> bool :
198- """Recursive helper implementing ``**`` segment semantics."""
253+ """Recursive helper implementing ``**`` segment semantics.
254+
255+ :param pattern_parts: Pattern split into POSIX path segments.
256+ :type pattern_parts: list[str]
257+ :param path_parts: Path split into POSIX path segments.
258+ :type path_parts: list[str]
259+ :return: ``True`` if the pattern parts match the path parts.
260+ :rtype: bool
261+ """
199262
200263 if not pattern_parts :
201264 return not path_parts
@@ -217,7 +280,17 @@ def _match_parts_recursive(pattern_parts: list[str], path_parts: list[str]) -> b
217280
218281
219282def path_match (pattern : IgnorePattern , relpath : str , * , is_dir : bool ) -> bool :
220- """Return True if ``relpath`` matches a compiled ignore pattern."""
283+ """Return ``True`` if ``relpath`` matches a compiled ignore pattern.
284+
285+ :param pattern: Compiled ignore pattern to test.
286+ :type pattern: IgnorePattern
287+ :param relpath: Path to test, relative to the ignore root.
288+ :type relpath: str
289+ :param is_dir: Whether ``relpath`` refers to a directory.
290+ :type is_dir: bool
291+ :return: ``True`` if the path is matched by the pattern.
292+ :rtype: bool
293+ """
221294
222295 relpath_posix = PurePosixPath (relpath ).as_posix ()
223296 path_parts = PurePosixPath (relpath_posix ).parts
@@ -247,6 +320,15 @@ def is_ignored(relpath: str, *, is_dir: bool, patterns: Sequence[IgnorePattern])
247320
248321 excludes all ``.log`` files except ``important.log``. Patterns are applied
249322 in order, and the last matching pattern determines inclusion.
323+
324+ :param relpath: Path to evaluate, relative to the ignore root.
325+ :type relpath: str
326+ :param is_dir: Whether ``relpath`` refers to a directory.
327+ :type is_dir: bool
328+ :param patterns: Compiled ignore patterns to apply in order.
329+ :type patterns: Sequence[IgnorePattern]
330+ :return: ``True`` if the path should be treated as ignored.
331+ :rtype: bool
250332 """
251333
252334 included = True # include by default
@@ -264,7 +346,15 @@ def iter_included_files(
264346 """Yield all files under ``root`` that are not ignored.
265347
266348 This performs directory pruning so that ignored directories are never
267- traversed, mirroring Docker's behavior for .dockerignore.
349+ traversed, mirroring Docker's behavior for ``.dockerignore``.
350+
351+ :param root: Root directory to walk.
352+ :type root: Path
353+ :param patterns: Compiled ignore patterns controlling which files and
354+ directories are included.
355+ :type patterns: Sequence[IgnorePattern]
356+ :return: Iterator over non-ignored file paths under ``root``.
357+ :rtype: Iterable[Path]
268358 """
269359
270360 if not root .is_dir ():
@@ -301,15 +391,22 @@ class IgnoreMatcher(ABC):
301391
302392 @abstractmethod
303393 def iter_paths (self , root : Path ) -> Iterable [Path ]:
304- """Yield filesystem paths to include under ``root``."""
394+ """Yield filesystem paths to include under ``root``.
395+
396+ :param root: Root directory to scan for files.
397+ :type root: Path
398+ :return: Iterator over filesystem paths that should be included.
399+ :rtype: Iterable[Path]
400+ """
305401
306402
307403@dataclass (frozen = True )
308404class DockerIgnoreMatcher (IgnoreMatcher ):
309405 """Ignore matcher that mirrors Docker's .dockerignore semantics.
310406
311407 This matcher:
312- - Closely follows Docker's .dockerignore semantics.
408+
409+ - Closely follows Docker's ``.dockerignore`` semantics.
313410 - Always loads patterns from ``.dockerignore`` in the provided context
314411 root, if present.
315412 - Optionally loads additional patterns from an extra ignorefile.
@@ -319,11 +416,22 @@ class DockerIgnoreMatcher(IgnoreMatcher):
319416 """
320417
321418 extra_ignorefile : str | Path | None = None
419+ """Optional path to an additional ignorefile whose patterns are appended
420+ after the default ``.dockerignore``.
421+ """
422+
322423 patterns : Sequence [str ] | None = None
424+ """Optional inline pattern strings appended after any ignorefiles."""
323425
324426 @override
325427 def iter_paths (self , root : Path ) -> Iterable [Path ]:
326- """Yield non-ignored files under ``root`` honoring Docker-style patterns."""
428+ """Yield non-ignored files under ``root`` honoring Docker-style patterns.
429+
430+ :param root: Context directory whose contents should be filtered.
431+ :type root: Path
432+ :return: Iterator over non-ignored file paths under ``root``.
433+ :rtype: Iterable[Path]
434+ """
327435 root = root .resolve ()
328436
329437 all_patterns : list [str ] = []
@@ -345,17 +453,7 @@ def iter_paths(self, root: Path) -> Iterable[Path]:
345453 for raw in self .patterns :
346454 if not raw :
347455 continue
348-
349- invert = raw [0 ] == "!"
350- pattern = raw [1 :].strip () if invert else raw .strip ()
351-
352- if pattern :
353- pattern = os .path .normpath (pattern )
354- pattern = pattern .replace (os .sep , "/" )
355- if len (pattern ) > 1 and pattern [0 ] == "/" :
356- pattern = pattern [1 :]
357-
358- normalized = f"!{ pattern } " if invert else pattern
456+ normalized = _normalize_pattern_string (raw )
359457 if normalized :
360458 all_patterns .append (normalized )
361459
@@ -376,6 +474,7 @@ class FilePatternMatcher(IgnoreMatcher):
376474 """
377475
378476 patterns : Sequence [str ] | str
477+ """Pattern or patterns to apply as ignore rules when matching files."""
379478
380479 def __post_init__ (self ) -> None :
381480 # Normalise a single pattern string into a list for downstream helpers.
@@ -384,7 +483,13 @@ def __post_init__(self) -> None:
384483
385484 @override
386485 def iter_paths (self , root : Path ) -> Iterable [Path ]:
387- """Yield non-ignored files under ``root`` based only on ``patterns``."""
486+ """Yield non-ignored files under ``root`` based only on ``patterns``.
487+
488+ :param root: Root directory whose contents should be filtered.
489+ :type root: Path
490+ :return: Iterator over non-ignored file paths under ``root``.
491+ :rtype: Iterable[Path]
492+ """
388493
389494 root = root .resolve ()
390495 compiled : list [IgnorePattern ] = compile_ignore (self .patterns ) # type: ignore[arg-type]
0 commit comments