Skip to content

Commit 597825d

Browse files
committed
docstring fixes and consolidation of duplicated ignore code
1 parent 2a7fc17 commit 597825d

1 file changed

Lines changed: 158 additions & 53 deletions

File tree

src/runloop_api_client/lib/_ignore.py

Lines changed: 158 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,84 @@
2424
class 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

4590
def _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

121162
def 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

197252
def _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

219282
def 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)
308404
class 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

Comments
 (0)