Skip to content

API reference

Complete reference for all public classes and functions in the searchpath library. This reference is auto-generated from source docstrings using mkdocstrings.

Contents


Module-level functions

Convenience functions for one-shot searches without creating a SearchPath instance.

Functions:

Name Description
first

Find the first matching path across directories.

match

Find the first matching path with provenance information.

all

Find all matching paths across directories.

matches

Find all matching paths with provenance information.

searchpath.first

first(
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> Path | None

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
*entries Entry

Directory entries to search, in priority order.

()
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestors.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestors.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
Path | None

The first matching Path, or None if not found.

Source code in src/searchpath/_functions.py
def first(  # noqa: PLR0913
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> "Path | None":
    """Find the first matching path across directories.

    Convenience wrapper for SearchPath(*entries).first(pattern, ...).

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        *entries: Directory entries to search, in priority order.
        kind: What to match: "files", "dirs", or "both".
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from ancestors.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from ancestors.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        The first matching Path, or None if not found.

    Example:
        ```python
        import tempfile
        from pathlib import Path
        import searchpath

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "config.toml").touch()
            config = searchpath.first("config.toml", d)
            config is not None  # True
        ```
    """
    return SearchPath(*entries).first(
        pattern,
        kind=kind,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )

searchpath.match

match(
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> Match | None

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
*entries Entry

Directory entries to search, in priority order.

()
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestors.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestors.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
Match | None

The first Match object, or None if not found.

Source code in src/searchpath/_functions.py
def match(  # noqa: PLR0913
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> "Match | None":
    """Find the first matching path with provenance information.

    Convenience wrapper for SearchPath(*entries).match(pattern, ...).

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        *entries: Directory entries to search, in priority order.
        kind: What to match: "files", "dirs", or "both".
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from ancestors.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from ancestors.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        The first Match object, or None if not found.

    Example:
        ```python
        import tempfile
        from pathlib import Path
        import searchpath

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "config.toml").touch()
            m = searchpath.match("*.toml", ("project", d))
            m.scope if m else None  # 'project'
        ```
    """
    return SearchPath(*entries).match(
        pattern,
        kind=kind,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )

searchpath.all

all(
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> list[Path]

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
*entries Entry

Directory entries to search, in priority order.

()
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
dedupe bool

If True, keep only first occurrence per relative path.

True
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestors.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestors.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
list[Path]

List of matching Path objects.

Source code in src/searchpath/_functions.py
def all(  # noqa: A001, PLR0913
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> list["Path"]:
    """Find all matching paths across directories.

    Convenience wrapper for SearchPath(*entries).all(pattern, ...).

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        *entries: Directory entries to search, in priority order.
        kind: What to match: "files", "dirs", or "both".
        dedupe: If True, keep only first occurrence per relative path.
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from ancestors.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from ancestors.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        List of matching Path objects.

    Example:
        ```python
        import tempfile
        from pathlib import Path
        import searchpath

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "main.py").touch()
            (d / "test_main.py").touch()
            files = searchpath.all("*.py", d)
            len(files) >= 2  # True
        ```
    """
    return SearchPath(*entries).all(
        pattern,
        kind=kind,
        dedupe=dedupe,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )

searchpath.matches

matches(
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> list[Match]

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
*entries Entry

Directory entries to search, in priority order.

()
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
dedupe bool

If True, keep only first occurrence per relative path.

True
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestors.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestors.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
list[Match]

List of Match objects.

Source code in src/searchpath/_functions.py
def matches(  # noqa: PLR0913
    pattern: str = "**",
    *entries: Entry,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> list["Match"]:
    """Find all matching paths with provenance information.

    Convenience wrapper for SearchPath(*entries).matches(pattern, ...).

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        *entries: Directory entries to search, in priority order.
        kind: What to match: "files", "dirs", or "both".
        dedupe: If True, keep only first occurrence per relative path.
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from ancestors.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from ancestors.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        List of Match objects.

    Example:
        ```python
        import tempfile
        from pathlib import Path
        import searchpath

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "config.toml").touch()
            results = searchpath.matches("*.toml", ("proj", d))
            len(results) >= 1 and results[0].scope == "proj"  # True
        ```
    """
    return SearchPath(*entries).matches(
        pattern,
        kind=kind,
        dedupe=dedupe,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )

SearchPath class

The core class representing an ordered list of directories to search.

searchpath.SearchPath

An ordered list of directories to search.

SearchPath represents an ordered sequence of directories that can be searched for files. Each directory is associated with a scope name that identifies where matches came from (e.g., "user", "project").

Entries can be specified as: - A tuple of (scope_name, path): Explicit scope naming - A bare Path or str: Auto-named as "dir0", "dir1", etc. - None: Silently ignored (useful for optional directories)

Attributes:

Name Type Description
dirs list[Path]

List of directories in the search path.

scopes list[str]

List of scope names corresponding to each directory.

Example
from pathlib import Path

sp = SearchPath(
    ("project", Path("/project/.config")),
    ("user", Path.home() / ".config"),
)
len(sp)  # 2
list(sp)  # [PosixPath('/project/.config'), PosixPath('/home/user/.config')]

Methods:

Name Description
__init__

Initialize a SearchPath with the given entries.

__add__

Concatenate two search paths.

__bool__

Return True if the search path has any directories.

__iter__

Iterate over directories in the search path.

__len__

Return the number of directories in the search path.

__repr__

Return a detailed string representation.

__str__

Return a human-readable string representation.

with_suffix

Create a new SearchPath with path components appended.

filter

Create a new SearchPath with entries filtered by a predicate.

existing

Create a new SearchPath with only existing directories.

items

Iterate over (scope, path) pairs in the search path.

first

Find the first matching path in the search path.

match

Find the first matching path with provenance information.

all

Find all matching paths in the search path.

matches

Find all matching paths with provenance information.

Source code in src/searchpath/_searchpath.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
@final
class SearchPath:
    """An ordered list of directories to search.

    SearchPath represents an ordered sequence of directories that can be
    searched for files. Each directory is associated with a scope name
    that identifies where matches came from (e.g., "user", "project").

    Entries can be specified as:
    - A tuple of (scope_name, path): Explicit scope naming
    - A bare Path or str: Auto-named as "dir0", "dir1", etc.
    - None: Silently ignored (useful for optional directories)

    Attributes:
        dirs: List of directories in the search path.
        scopes: List of scope names corresponding to each directory.

    Example:
        ```python
        from pathlib import Path

        sp = SearchPath(
            ("project", Path("/project/.config")),
            ("user", Path.home() / ".config"),
        )
        len(sp)  # 2
        list(sp)  # [PosixPath('/project/.config'), PosixPath('/home/user/.config')]
        ```
    """

    __slots__ = ("_entries",)

    def __init__(
        self,
        *entries: tuple[str, Path | str | None] | Path | str | None,
    ) -> None:
        """Initialize a SearchPath with the given entries.

        Args:
            *entries: Directory entries to include in the search path.
                Each entry can be:
                - A tuple of (scope_name, path): Explicit scope naming
                - A bare Path or str: Auto-named as "dir0", "dir1", etc.
                - None: Silently ignored
        """
        bare_paths = [e for e in entries if e is not None and not isinstance(e, tuple)]
        auto_names = {id(p): f"dir{i}" for i, p in enumerate(bare_paths)}

        self._entries: list[tuple[str, Path]] = []
        for entry in entries:
            parsed = self._parse_entry(entry, auto_names)
            if parsed is not None:
                self._entries.append(parsed)

    def __add__(self, other: object) -> "Self":
        """Concatenate two search paths.

        Creates a new SearchPath containing all entries from this search
        path followed by all entries from the other search path.

        Args:
            other: Another SearchPath to concatenate.

        Returns:
            A new SearchPath with entries from both.

        Raises:
            TypeError: If other is not a SearchPath.

        Example:
            ```python
            sp1 = SearchPath(("a", Path("/a")))
            sp2 = SearchPath(("b", Path("/b")))
            combined = sp1 + sp2
            list(combined.scopes)  # ['a', 'b']
            ```
        """
        if not isinstance(other, SearchPath):
            return NotImplemented  # type: ignore[return-value]
        new_entries = self._entries + other._entries
        return self._from_entries(new_entries)

    def __bool__(self) -> bool:
        """Return True if the search path has any directories."""
        return len(self._entries) > 0

    def __iter__(self) -> "Iterator[Path]":
        """Iterate over directories in the search path.

        Yields:
            Each directory Path in order.
        """
        for _, path in self._entries:
            yield path

    def __len__(self) -> int:
        """Return the number of directories in the search path."""
        return len(self._entries)

    @override
    def __repr__(self) -> str:
        """Return a detailed string representation.

        Returns:
            A string showing the SearchPath constructor call.
        """
        if not self._entries:
            return "SearchPath()"
        entries_repr = ", ".join(
            f"({scope!r}, {path!r})" for scope, path in self._entries
        )
        return f"SearchPath({entries_repr})"

    @override
    def __str__(self) -> str:
        """Return a human-readable string representation.

        Format suitable for error messages showing scope: path pairs.

        Returns:
            A string like "project: /project/.config, user: ~/.config"
        """
        if not self._entries:
            return "(empty)"
        return ", ".join(f"{scope}: {path}" for scope, path in self._entries)

    @classmethod
    def _from_entries(cls, entries: list[tuple[str, Path]]) -> "Self":
        """Create a SearchPath from a pre-built entry list.

        This is an internal constructor used by methods that need to
        create SearchPath instances without re-parsing entries.

        Args:
            entries: List of (scope, path) tuples.

        Returns:
            A new SearchPath instance.
        """
        instance = cls.__new__(cls)
        instance._entries = entries  # noqa: SLF001
        return instance

    @staticmethod
    def _parse_entry(
        entry: tuple[str, Path | str | None] | Path | str | None,
        auto_names: dict[int, str],
    ) -> tuple[str, Path] | None:
        """Parse a single entry into (scope, path) or None.

        Args:
            entry: The entry to parse.
            auto_names: Mapping from bare path id to auto-generated scope name.

        Returns:
            A tuple of (scope, path) or None if entry should be skipped.
        """
        if entry is None:
            return None

        if isinstance(entry, tuple):
            scope, path = entry
            if path is None:
                return None
            resolved_path = Path(path) if isinstance(path, str) else path
            return (scope, resolved_path)

        resolved_path = Path(entry) if isinstance(entry, str) else entry
        return (auto_names[id(entry)], resolved_path)

    @property
    def dirs(self) -> list[Path]:
        """List of directories in the search path.

        Returns:
            A list of directory Paths in order.
        """
        return [path for _, path in self._entries]

    @property
    def scopes(self) -> list[str]:
        """List of scope names in the search path.

        Returns:
            A list of scope name strings in order.
        """
        return [scope for scope, _ in self._entries]

    def with_suffix(self, *parts: str) -> "Self":
        """Create a new SearchPath with path components appended.

        Appends the given path components to each directory in this
        search path, creating a new SearchPath.

        Args:
            *parts: Path components to append to each directory.

        Returns:
            A new SearchPath with the path components appended.

        Example:
            ```python
            sp = SearchPath(("user", Path("/home/user")))
            sp2 = sp.with_suffix(".config", "myapp")
            list(sp2)  # [PosixPath('/home/user/.config/myapp')]
            ```
        """
        new_entries = [(scope, path.joinpath(*parts)) for scope, path in self._entries]
        return self._from_entries(new_entries)

    def filter(self, predicate: "Callable[[Path], bool]") -> "Self":
        """Create a new SearchPath with entries filtered by a predicate.

        Args:
            predicate: A function that takes a Path and returns True to
                keep the entry or False to exclude it.

        Returns:
            A new SearchPath containing only entries for which the
            predicate returned True.

        Example:
            ```python
            sp = SearchPath(("a", Path("/exists")), ("b", Path("/missing")))
            filtered = sp.filter(lambda p: p.exists())
            ```
        """
        new_entries = [
            (scope, path) for scope, path in self._entries if predicate(path)
        ]
        return self._from_entries(new_entries)

    def existing(self) -> "Self":
        """Create a new SearchPath with only existing directories.

        This is a shorthand for `filter(lambda p: p.exists())`.

        Returns:
            A new SearchPath containing only directories that exist.

        Example:
            ```python
            import tempfile
            from pathlib import Path

            with tempfile.TemporaryDirectory() as tmp:
                sp = SearchPath(
                    ("exists", Path(tmp)),
                    ("missing", Path("/no/such/dir")),
                )
                existing_sp = sp.existing()
                len(existing_sp)  # 1
            ```
        """
        return self.filter(lambda p: p.exists())

    def items(self) -> "Iterator[tuple[str, Path]]":
        """Iterate over (scope, path) pairs in the search path.

        Yields:
            Tuples of (scope_name, directory_path) in order.

        Example:
            ```python
            sp = SearchPath(("user", Path("/user")), ("system", Path("/sys")))
            list(sp.items())
            # [('user', PosixPath('/user')), ('system', PosixPath('/sys'))]
            ```
        """
        yield from self._entries

    @staticmethod
    def _normalize_pattern_arg(
        patterns: "str | Sequence[str] | None",
    ) -> "Sequence[str]":
        """Normalize pattern argument to a sequence of strings.

        Args:
            patterns: A single pattern string, sequence of patterns, or None.

        Returns:
            A sequence of pattern strings (empty if None).
        """
        if patterns is None:
            return ()
        if isinstance(patterns, str):
            return (patterns,)
        return patterns

    @staticmethod
    def _normalize_path_arg(
        paths: "Path | str | Sequence[Path | str] | None",
    ) -> "Sequence[Path]":
        """Normalize path argument to a sequence of Paths.

        Args:
            paths: A single path, sequence of paths, or None.

        Returns:
            A sequence of Path objects (empty if None).
        """
        if paths is None:
            return ()
        if isinstance(paths, (str, Path)):
            return (Path(paths),)
        return tuple(Path(p) for p in paths)

    @staticmethod
    def _load_pattern_files(paths: "Sequence[Path]") -> list[str]:
        """Load patterns from multiple pattern files.

        Args:
            paths: Sequence of paths to pattern files.

        Returns:
            Combined list of patterns from all files.

        Raises:
            PatternFileError: If any file cannot be read.
        """
        patterns: list[str] = []
        for path in paths:
            patterns.extend(load_patterns(path))
        return patterns

    def _build_patterns(
        self,
        include: "str | Sequence[str] | None",
        include_from: "Path | str | Sequence[Path | str] | None",
        exclude: "str | Sequence[str] | None",
        exclude_from: "Path | str | Sequence[Path | str] | None",
    ) -> tuple[list[str], list[str]]:
        """Build effective include and exclude pattern lists from all sources.

        Args:
            include: Inline include patterns.
            include_from: Path(s) to load additional include patterns from.
            exclude: Inline exclude patterns.
            exclude_from: Path(s) to load additional exclude patterns from.

        Returns:
            Tuple of (include_patterns, exclude_patterns).

        Raises:
            PatternFileError: If any pattern file cannot be read.
        """
        include_patterns = list(self._normalize_pattern_arg(include))
        include_paths = self._normalize_path_arg(include_from)
        if include_paths:
            include_patterns.extend(self._load_pattern_files(include_paths))

        exclude_patterns = list(self._normalize_pattern_arg(exclude))
        exclude_paths = self._normalize_path_arg(exclude_from)
        if exclude_paths:
            exclude_patterns.extend(self._load_pattern_files(exclude_paths))

        return (include_patterns, exclude_patterns)

    @staticmethod
    def _dedupe_matches(matches: "Iterable[Match]") -> "Iterator[Match]":
        """Deduplicate matches by relative path, keeping first occurrence.

        Args:
            matches: Iterable of Match objects.

        Yields:
            Match objects with unique relative paths.
        """
        seen: set[str] = set()
        for match in matches:
            key = match.relative.as_posix()
            if key not in seen:
                seen.add(key)
                yield match

    def _should_include_with_ancestors(  # noqa: PLR0913
        self,
        path: Path,
        source_resolved: Path,
        ancestors: AncestorPatterns,
        include: "Sequence[str]",
        exclude: "Sequence[str]",
        matcher: "PathMatcher",
    ) -> bool:
        """Check if a path should be included when using ancestor patterns.

        Returns True if the path passes the merged ancestor + inline patterns.
        """
        rel_path = path.relative_to(source_resolved).as_posix()
        is_dir = path.is_dir()

        merged_include = merge_patterns(ancestors.include, include)
        merged_exclude = merge_patterns(ancestors.exclude, exclude)

        if not merged_include and not merged_exclude:
            return True

        return matcher.matches(
            rel_path, is_dir=is_dir, include=merged_include, exclude=merged_exclude
        )

    def _iter_matches(  # noqa: PLR0913
        self,
        pattern: str,
        *,
        kind: TraversalKind,
        include: "Sequence[str]",
        exclude: "Sequence[str]",
        include_from_ancestors: str | None,
        exclude_from_ancestors: str | None,
        matcher: "PathMatcher | None",
        follow_symlinks: bool,
    ) -> "Iterator[Match]":
        """Core iteration logic yielding Match objects for all matching paths.

        Args:
            pattern: Glob pattern for matching paths.
            kind: What to yield: "files", "dirs", or "both".
            include: Additional patterns paths must match.
            exclude: Patterns that reject paths.
            include_from_ancestors: Filename to load include patterns from
                ancestor directories.
            exclude_from_ancestors: Filename to load exclude patterns from
                ancestor directories.
            matcher: PathMatcher implementation.
            follow_symlinks: Whether to follow symbolic links.

        Yields:
            Match objects for all matching paths across all entries.
        """
        use_ancestors = (
            include_from_ancestors is not None or exclude_from_ancestors is not None
        )

        if not use_ancestors:
            yield from self._iter_matches_simple(
                pattern,
                kind,
                include,
                exclude,
                matcher,
                follow_symlinks=follow_symlinks,
            )
            return

        yield from self._iter_matches_with_ancestors(
            pattern,
            kind,
            include,
            exclude,
            include_from_ancestors,
            exclude_from_ancestors,
            matcher,
            follow_symlinks=follow_symlinks,
        )

    def _iter_matches_simple(  # noqa: PLR0913
        self,
        pattern: str,
        kind: TraversalKind,
        include: "Sequence[str]",
        exclude: "Sequence[str]",
        matcher: "PathMatcher | None",
        *,
        follow_symlinks: bool,
    ) -> "Iterator[Match]":
        """Iterate matches without ancestor pattern handling."""
        for scope, source in self._entries:
            for path in traverse(
                source,
                pattern=pattern,
                kind=kind,
                include=include,
                exclude=exclude,
                matcher=matcher,
                follow_symlinks=follow_symlinks,
            ):
                yield Match(path=path, scope=scope, source=source)

    def _iter_matches_with_ancestors(  # noqa: PLR0913
        self,
        pattern: str,
        kind: TraversalKind,
        include: "Sequence[str]",
        exclude: "Sequence[str]",
        include_from_ancestors: str | None,
        exclude_from_ancestors: str | None,
        matcher: "PathMatcher | None",
        *,
        follow_symlinks: bool,
    ) -> "Iterator[Match]":
        """Iterate matches with ancestor pattern handling."""
        ancestor_cache: dict[Path, list[str]] = {}
        resolved_matcher = matcher if matcher is not None else GlobMatcher()
        traverse_include = () if include_from_ancestors is not None else include

        for scope, source in self._entries:
            source_resolved = source.resolve()
            for path in traverse(
                source,
                pattern=pattern,
                kind=kind,
                include=traverse_include,
                exclude=exclude,
                matcher=matcher,
                follow_symlinks=follow_symlinks,
            ):
                ancestors = collect_ancestor_patterns(
                    file_path=path,
                    entry_root=source_resolved,
                    include_filename=include_from_ancestors,
                    exclude_filename=exclude_from_ancestors,
                    cache=ancestor_cache,
                )

                if self._should_include_with_ancestors(
                    path,
                    source_resolved,
                    ancestors,
                    include,
                    exclude,
                    resolved_matcher,
                ):
                    yield Match(path=path, scope=scope, source=source)

    def first(  # noqa: PLR0913
        self,
        pattern: str = "**",
        *,
        kind: Literal["files", "dirs", "both"] = "files",
        include: "str | Sequence[str] | None" = None,
        include_from: "Path | str | Sequence[Path | str] | None" = None,
        include_from_ancestors: str | None = None,
        exclude: "str | Sequence[str] | None" = None,
        exclude_from: "Path | str | Sequence[Path | str] | None" = None,
        exclude_from_ancestors: str | None = None,
        matcher: "PathMatcher | None" = None,
        follow_symlinks: bool = True,
    ) -> Path | None:
        """Find the first matching path in the search path.

        Searches directories in order and returns the first path that matches
        the pattern and filter criteria. Returns None if no match is found.

        Args:
            pattern: Glob pattern for matching paths. Defaults to "**" (all).
            kind: What to match: "files", "dirs", or "both".
            include: Additional patterns paths must match.
            include_from: Path(s) to files containing include patterns.
            include_from_ancestors: Filename to load include patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            exclude: Patterns that reject paths.
            exclude_from: Path(s) to files containing exclude patterns.
            exclude_from_ancestors: Filename to load exclude patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            matcher: PathMatcher implementation. Defaults to GlobMatcher.
            follow_symlinks: Whether to follow symbolic links.

        Returns:
            The first matching Path, or None if not found.

        Example:
            ```python
            import tempfile
            from pathlib import Path

            with tempfile.TemporaryDirectory() as tmp:
                d = Path(tmp)
                (d / "config.toml").touch()
                sp = SearchPath(("dir", d))
                result = sp.first("*.toml")
                result is not None  # True
            ```
        """
        result = self.match(
            pattern,
            kind=kind,
            include=include,
            include_from=include_from,
            include_from_ancestors=include_from_ancestors,
            exclude=exclude,
            exclude_from=exclude_from,
            exclude_from_ancestors=exclude_from_ancestors,
            matcher=matcher,
            follow_symlinks=follow_symlinks,
        )
        return result.path if result is not None else None

    def match(  # noqa: PLR0913
        self,
        pattern: str = "**",
        *,
        kind: Literal["files", "dirs", "both"] = "files",
        include: "str | Sequence[str] | None" = None,
        include_from: "Path | str | Sequence[Path | str] | None" = None,
        include_from_ancestors: str | None = None,
        exclude: "str | Sequence[str] | None" = None,
        exclude_from: "Path | str | Sequence[Path | str] | None" = None,
        exclude_from_ancestors: str | None = None,
        matcher: "PathMatcher | None" = None,
        follow_symlinks: bool = True,
    ) -> Match | None:
        """Find the first matching path with provenance information.

        Like first(), but returns a Match object containing the path along
        with the scope name and source directory. Returns None if no match.

        Args:
            pattern: Glob pattern for matching paths. Defaults to "**" (all).
            kind: What to match: "files", "dirs", or "both".
            include: Additional patterns paths must match.
            include_from: Path(s) to files containing include patterns.
            include_from_ancestors: Filename to load include patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            exclude: Patterns that reject paths.
            exclude_from: Path(s) to files containing exclude patterns.
            exclude_from_ancestors: Filename to load exclude patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            matcher: PathMatcher implementation. Defaults to GlobMatcher.
            follow_symlinks: Whether to follow symbolic links.

        Returns:
            The first Match object, or None if not found.

        Example:
            ```python
            import tempfile
            from pathlib import Path

            with tempfile.TemporaryDirectory() as tmp:
                d = Path(tmp)
                (d / "config.toml").touch()
                sp = SearchPath(("project", d))
                m = sp.match("*.toml")
                m is not None and m.scope == "project"  # True
            ```
        """
        include_patterns, exclude_patterns = self._build_patterns(
            include, include_from, exclude, exclude_from
        )

        for m in self._iter_matches(
            pattern,
            kind=kind,
            include=include_patterns,
            exclude=exclude_patterns,
            include_from_ancestors=include_from_ancestors,
            exclude_from_ancestors=exclude_from_ancestors,
            matcher=matcher,
            follow_symlinks=follow_symlinks,
        ):
            return m

        return None

    def all(  # noqa: PLR0913
        self,
        pattern: str = "**",
        *,
        kind: Literal["files", "dirs", "both"] = "files",
        dedupe: bool = True,
        include: "str | Sequence[str] | None" = None,
        include_from: "Path | str | Sequence[Path | str] | None" = None,
        include_from_ancestors: str | None = None,
        exclude: "str | Sequence[str] | None" = None,
        exclude_from: "Path | str | Sequence[Path | str] | None" = None,
        exclude_from_ancestors: str | None = None,
        matcher: "PathMatcher | None" = None,
        follow_symlinks: bool = True,
    ) -> list[Path]:
        """Find all matching paths in the search path.

        Searches all directories and returns paths matching the pattern and
        filter criteria. By default, deduplicates by relative path, keeping
        the first occurrence (from higher priority directories).

        Args:
            pattern: Glob pattern for matching paths. Defaults to "**" (all).
            kind: What to match: "files", "dirs", or "both".
            dedupe: If True, keep only first occurrence per relative path.
            include: Additional patterns paths must match.
            include_from: Path(s) to files containing include patterns.
            include_from_ancestors: Filename to load include patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            exclude: Patterns that reject paths.
            exclude_from: Path(s) to files containing exclude patterns.
            exclude_from_ancestors: Filename to load exclude patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            matcher: PathMatcher implementation. Defaults to GlobMatcher.
            follow_symlinks: Whether to follow symbolic links.

        Returns:
            List of matching Path objects.

        Example:
            ```python
            import tempfile
            from pathlib import Path

            with tempfile.TemporaryDirectory() as tmp:
                d = Path(tmp).resolve()
                (d / "a.py").touch()
                (d / "b.py").touch()
                sp = SearchPath(("dir", d))
                len(sp.all("*.py")) >= 2  # True
            ```
        """
        match_list = self.matches(
            pattern,
            kind=kind,
            dedupe=dedupe,
            include=include,
            include_from=include_from,
            include_from_ancestors=include_from_ancestors,
            exclude=exclude,
            exclude_from=exclude_from,
            exclude_from_ancestors=exclude_from_ancestors,
            matcher=matcher,
            follow_symlinks=follow_symlinks,
        )
        return [m.path for m in match_list]

    def matches(  # noqa: PLR0913
        self,
        pattern: str = "**",
        *,
        kind: Literal["files", "dirs", "both"] = "files",
        dedupe: bool = True,
        include: "str | Sequence[str] | None" = None,
        include_from: "Path | str | Sequence[Path | str] | None" = None,
        include_from_ancestors: str | None = None,
        exclude: "str | Sequence[str] | None" = None,
        exclude_from: "Path | str | Sequence[Path | str] | None" = None,
        exclude_from_ancestors: str | None = None,
        matcher: "PathMatcher | None" = None,
        follow_symlinks: bool = True,
    ) -> list[Match]:
        """Find all matching paths with provenance information.

        Like all(), but returns Match objects containing paths along with
        scope names and source directories.

        Args:
            pattern: Glob pattern for matching paths. Defaults to "**" (all).
            kind: What to match: "files", "dirs", or "both".
            dedupe: If True, keep only first occurrence per relative path.
            include: Additional patterns paths must match.
            include_from: Path(s) to files containing include patterns.
            include_from_ancestors: Filename to load include patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            exclude: Patterns that reject paths.
            exclude_from: Path(s) to files containing exclude patterns.
            exclude_from_ancestors: Filename to load exclude patterns from
                ancestor directories. Patterns are collected from the search
                entry root toward each file's parent directory.
            matcher: PathMatcher implementation. Defaults to GlobMatcher.
            follow_symlinks: Whether to follow symbolic links.

        Returns:
            List of Match objects.

        Example:
            ```python
            import tempfile
            from pathlib import Path

            with tempfile.TemporaryDirectory() as tmp:
                d = Path(tmp).resolve()
                (d / "config.toml").touch()
                sp = SearchPath(("project", d))
                matches = sp.matches("*.toml")
                len(matches) >= 1 and matches[0].scope == "project"  # True
            ```
        """
        include_patterns, exclude_patterns = self._build_patterns(
            include, include_from, exclude, exclude_from
        )

        all_matches = self._iter_matches(
            pattern,
            kind=kind,
            include=include_patterns,
            exclude=exclude_patterns,
            include_from_ancestors=include_from_ancestors,
            exclude_from_ancestors=exclude_from_ancestors,
            matcher=matcher,
            follow_symlinks=follow_symlinks,
        )

        if dedupe:
            return list(self._dedupe_matches(all_matches))
        return list(all_matches)

searchpath.SearchPath.dirs property

dirs: list[Path]

List of directories in the search path.

Returns:

Type Description
list[Path]

A list of directory Paths in order.

searchpath.SearchPath.scopes property

scopes: list[str]

List of scope names in the search path.

Returns:

Type Description
list[str]

A list of scope name strings in order.

searchpath.SearchPath.__init__

__init__(*entries: tuple[str, Path | str | None] | Path | str | None) -> None

Parameters:

Name Type Description Default
*entries tuple[str, Path | str | None] | Path | str | None

Directory entries to include in the search path. Each entry can be: - A tuple of (scope_name, path): Explicit scope naming - A bare Path or str: Auto-named as "dir0", "dir1", etc. - None: Silently ignored

()
Source code in src/searchpath/_searchpath.py
def __init__(
    self,
    *entries: tuple[str, Path | str | None] | Path | str | None,
) -> None:
    """Initialize a SearchPath with the given entries.

    Args:
        *entries: Directory entries to include in the search path.
            Each entry can be:
            - A tuple of (scope_name, path): Explicit scope naming
            - A bare Path or str: Auto-named as "dir0", "dir1", etc.
            - None: Silently ignored
    """
    bare_paths = [e for e in entries if e is not None and not isinstance(e, tuple)]
    auto_names = {id(p): f"dir{i}" for i, p in enumerate(bare_paths)}

    self._entries: list[tuple[str, Path]] = []
    for entry in entries:
        parsed = self._parse_entry(entry, auto_names)
        if parsed is not None:
            self._entries.append(parsed)

searchpath.SearchPath.__add__

__add__(other: object) -> Self

Concatenate two search paths.

Creates a new SearchPath containing all entries from this search path followed by all entries from the other search path.

Parameters:

Name Type Description Default
other object

Another SearchPath to concatenate.

required

Returns:

Type Description
Self

A new SearchPath with entries from both.

Raises:

Type Description
TypeError

If other is not a SearchPath.

Example
sp1 = SearchPath(("a", Path("/a")))
sp2 = SearchPath(("b", Path("/b")))
combined = sp1 + sp2
list(combined.scopes)  # ['a', 'b']
Source code in src/searchpath/_searchpath.py
def __add__(self, other: object) -> "Self":
    """Concatenate two search paths.

    Creates a new SearchPath containing all entries from this search
    path followed by all entries from the other search path.

    Args:
        other: Another SearchPath to concatenate.

    Returns:
        A new SearchPath with entries from both.

    Raises:
        TypeError: If other is not a SearchPath.

    Example:
        ```python
        sp1 = SearchPath(("a", Path("/a")))
        sp2 = SearchPath(("b", Path("/b")))
        combined = sp1 + sp2
        list(combined.scopes)  # ['a', 'b']
        ```
    """
    if not isinstance(other, SearchPath):
        return NotImplemented  # type: ignore[return-value]
    new_entries = self._entries + other._entries
    return self._from_entries(new_entries)

searchpath.SearchPath.__bool__

__bool__() -> bool

Return True if the search path has any directories.

Source code in src/searchpath/_searchpath.py
def __bool__(self) -> bool:
    """Return True if the search path has any directories."""
    return len(self._entries) > 0

searchpath.SearchPath.__iter__

__iter__() -> Iterator[Path]

Iterate over directories in the search path.

Yields:

Type Description
Path

Each directory Path in order.

Source code in src/searchpath/_searchpath.py
def __iter__(self) -> "Iterator[Path]":
    """Iterate over directories in the search path.

    Yields:
        Each directory Path in order.
    """
    for _, path in self._entries:
        yield path

searchpath.SearchPath.__len__

__len__() -> int

Return the number of directories in the search path.

Source code in src/searchpath/_searchpath.py
def __len__(self) -> int:
    """Return the number of directories in the search path."""
    return len(self._entries)

searchpath.SearchPath.__repr__

__repr__() -> str

Return a detailed string representation.

Returns:

Type Description
str

A string showing the SearchPath constructor call.

Source code in src/searchpath/_searchpath.py
@override
def __repr__(self) -> str:
    """Return a detailed string representation.

    Returns:
        A string showing the SearchPath constructor call.
    """
    if not self._entries:
        return "SearchPath()"
    entries_repr = ", ".join(
        f"({scope!r}, {path!r})" for scope, path in self._entries
    )
    return f"SearchPath({entries_repr})"

searchpath.SearchPath.__str__

__str__() -> str

Return a human-readable string representation.

Format suitable for error messages showing scope: path pairs.

Returns:

Type Description
str

A string like "project: /project/.config, user: ~/.config"

Source code in src/searchpath/_searchpath.py
@override
def __str__(self) -> str:
    """Return a human-readable string representation.

    Format suitable for error messages showing scope: path pairs.

    Returns:
        A string like "project: /project/.config, user: ~/.config"
    """
    if not self._entries:
        return "(empty)"
    return ", ".join(f"{scope}: {path}" for scope, path in self._entries)

searchpath.SearchPath.with_suffix

with_suffix(*parts: str) -> Self

Create a new SearchPath with path components appended.

Appends the given path components to each directory in this search path, creating a new SearchPath.

Parameters:

Name Type Description Default
*parts str

Path components to append to each directory.

()

Returns:

Type Description
Self

A new SearchPath with the path components appended.

Example
sp = SearchPath(("user", Path("/home/user")))
sp2 = sp.with_suffix(".config", "myapp")
list(sp2)  # [PosixPath('/home/user/.config/myapp')]
Source code in src/searchpath/_searchpath.py
def with_suffix(self, *parts: str) -> "Self":
    """Create a new SearchPath with path components appended.

    Appends the given path components to each directory in this
    search path, creating a new SearchPath.

    Args:
        *parts: Path components to append to each directory.

    Returns:
        A new SearchPath with the path components appended.

    Example:
        ```python
        sp = SearchPath(("user", Path("/home/user")))
        sp2 = sp.with_suffix(".config", "myapp")
        list(sp2)  # [PosixPath('/home/user/.config/myapp')]
        ```
    """
    new_entries = [(scope, path.joinpath(*parts)) for scope, path in self._entries]
    return self._from_entries(new_entries)

searchpath.SearchPath.filter

filter(predicate: Callable[[Path], bool]) -> Self

Create a new SearchPath with entries filtered by a predicate.

Parameters:

Name Type Description Default
predicate Callable[[Path], bool]

A function that takes a Path and returns True to keep the entry or False to exclude it.

required

Returns:

Type Description
Self

A new SearchPath containing only entries for which the

Self

predicate returned True.

Example
sp = SearchPath(("a", Path("/exists")), ("b", Path("/missing")))
filtered = sp.filter(lambda p: p.exists())
Source code in src/searchpath/_searchpath.py
def filter(self, predicate: "Callable[[Path], bool]") -> "Self":
    """Create a new SearchPath with entries filtered by a predicate.

    Args:
        predicate: A function that takes a Path and returns True to
            keep the entry or False to exclude it.

    Returns:
        A new SearchPath containing only entries for which the
        predicate returned True.

    Example:
        ```python
        sp = SearchPath(("a", Path("/exists")), ("b", Path("/missing")))
        filtered = sp.filter(lambda p: p.exists())
        ```
    """
    new_entries = [
        (scope, path) for scope, path in self._entries if predicate(path)
    ]
    return self._from_entries(new_entries)

searchpath.SearchPath.existing

existing() -> Self

Create a new SearchPath with only existing directories.

This is a shorthand for filter(lambda p: p.exists()).

Returns:

Type Description
Self

A new SearchPath containing only directories that exist.

Example
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    sp = SearchPath(
        ("exists", Path(tmp)),
        ("missing", Path("/no/such/dir")),
    )
    existing_sp = sp.existing()
    len(existing_sp)  # 1
Source code in src/searchpath/_searchpath.py
def existing(self) -> "Self":
    """Create a new SearchPath with only existing directories.

    This is a shorthand for `filter(lambda p: p.exists())`.

    Returns:
        A new SearchPath containing only directories that exist.

    Example:
        ```python
        import tempfile
        from pathlib import Path

        with tempfile.TemporaryDirectory() as tmp:
            sp = SearchPath(
                ("exists", Path(tmp)),
                ("missing", Path("/no/such/dir")),
            )
            existing_sp = sp.existing()
            len(existing_sp)  # 1
        ```
    """
    return self.filter(lambda p: p.exists())

searchpath.SearchPath.items

items() -> Iterator[tuple[str, Path]]

Iterate over (scope, path) pairs in the search path.

Yields:

Type Description
tuple[str, Path]

Tuples of (scope_name, directory_path) in order.

Example
sp = SearchPath(("user", Path("/user")), ("system", Path("/sys")))
list(sp.items())
# [('user', PosixPath('/user')), ('system', PosixPath('/sys'))]
Source code in src/searchpath/_searchpath.py
def items(self) -> "Iterator[tuple[str, Path]]":
    """Iterate over (scope, path) pairs in the search path.

    Yields:
        Tuples of (scope_name, directory_path) in order.

    Example:
        ```python
        sp = SearchPath(("user", Path("/user")), ("system", Path("/sys")))
        list(sp.items())
        # [('user', PosixPath('/user')), ('system', PosixPath('/sys'))]
        ```
    """
    yield from self._entries

searchpath.SearchPath.first

first(
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> Path | None

Find the first matching path in the search path.

Searches directories in order and returns the first path that matches the pattern and filter criteria. Returns None if no match is found.

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
Path | None

The first matching Path, or None if not found.

Example
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    d = Path(tmp)
    (d / "config.toml").touch()
    sp = SearchPath(("dir", d))
    result = sp.first("*.toml")
    result is not None  # True
Source code in src/searchpath/_searchpath.py
def first(  # noqa: PLR0913
    self,
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> Path | None:
    """Find the first matching path in the search path.

    Searches directories in order and returns the first path that matches
    the pattern and filter criteria. Returns None if no match is found.

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        kind: What to match: "files", "dirs", or "both".
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        The first matching Path, or None if not found.

    Example:
        ```python
        import tempfile
        from pathlib import Path

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp)
            (d / "config.toml").touch()
            sp = SearchPath(("dir", d))
            result = sp.first("*.toml")
            result is not None  # True
        ```
    """
    result = self.match(
        pattern,
        kind=kind,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )
    return result.path if result is not None else None

searchpath.SearchPath.match

match(
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> Match | None

Find the first matching path with provenance information.

Like first(), but returns a Match object containing the path along with the scope name and source directory. Returns None if no match.

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
Match | None

The first Match object, or None if not found.

Example
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    d = Path(tmp)
    (d / "config.toml").touch()
    sp = SearchPath(("project", d))
    m = sp.match("*.toml")
    m is not None and m.scope == "project"  # True
Source code in src/searchpath/_searchpath.py
def match(  # noqa: PLR0913
    self,
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> Match | None:
    """Find the first matching path with provenance information.

    Like first(), but returns a Match object containing the path along
    with the scope name and source directory. Returns None if no match.

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        kind: What to match: "files", "dirs", or "both".
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        The first Match object, or None if not found.

    Example:
        ```python
        import tempfile
        from pathlib import Path

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp)
            (d / "config.toml").touch()
            sp = SearchPath(("project", d))
            m = sp.match("*.toml")
            m is not None and m.scope == "project"  # True
        ```
    """
    include_patterns, exclude_patterns = self._build_patterns(
        include, include_from, exclude, exclude_from
    )

    for m in self._iter_matches(
        pattern,
        kind=kind,
        include=include_patterns,
        exclude=exclude_patterns,
        include_from_ancestors=include_from_ancestors,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    ):
        return m

    return None

searchpath.SearchPath.all

all(
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> list[Path]

Find all matching paths in the search path.

Searches all directories and returns paths matching the pattern and filter criteria. By default, deduplicates by relative path, keeping the first occurrence (from higher priority directories).

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
dedupe bool

If True, keep only first occurrence per relative path.

True
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
list[Path]

List of matching Path objects.

Example
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    d = Path(tmp).resolve()
    (d / "a.py").touch()
    (d / "b.py").touch()
    sp = SearchPath(("dir", d))
    len(sp.all("*.py")) >= 2  # True
Source code in src/searchpath/_searchpath.py
def all(  # noqa: PLR0913
    self,
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> list[Path]:
    """Find all matching paths in the search path.

    Searches all directories and returns paths matching the pattern and
    filter criteria. By default, deduplicates by relative path, keeping
    the first occurrence (from higher priority directories).

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        kind: What to match: "files", "dirs", or "both".
        dedupe: If True, keep only first occurrence per relative path.
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        List of matching Path objects.

    Example:
        ```python
        import tempfile
        from pathlib import Path

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "a.py").touch()
            (d / "b.py").touch()
            sp = SearchPath(("dir", d))
            len(sp.all("*.py")) >= 2  # True
        ```
    """
    match_list = self.matches(
        pattern,
        kind=kind,
        dedupe=dedupe,
        include=include,
        include_from=include_from,
        include_from_ancestors=include_from_ancestors,
        exclude=exclude,
        exclude_from=exclude_from,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )
    return [m.path for m in match_list]

searchpath.SearchPath.matches

matches(
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: str | Sequence[str] | None = None,
    include_from: Path | str | Sequence[Path | str] | None = None,
    include_from_ancestors: str | None = None,
    exclude: str | Sequence[str] | None = None,
    exclude_from: Path | str | Sequence[Path | str] | None = None,
    exclude_from_ancestors: str | None = None,
    matcher: PathMatcher | None = None,
    follow_symlinks: bool = True
) -> list[Match]

Find all matching paths with provenance information.

Like all(), but returns Match objects containing paths along with scope names and source directories.

Parameters:

Name Type Description Default
pattern str

Glob pattern for matching paths. Defaults to "**" (all).

'**'
kind Literal['files', 'dirs', 'both']

What to match: "files", "dirs", or "both".

'files'
dedupe bool

If True, keep only first occurrence per relative path.

True
include str | Sequence[str] | None

Additional patterns paths must match.

None
include_from Path | str | Sequence[Path | str] | None

Path(s) to files containing include patterns.

None
include_from_ancestors str | None

Filename to load include patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
exclude str | Sequence[str] | None

Patterns that reject paths.

None
exclude_from Path | str | Sequence[Path | str] | None

Path(s) to files containing exclude patterns.

None
exclude_from_ancestors str | None

Filename to load exclude patterns from ancestor directories. Patterns are collected from the search entry root toward each file's parent directory.

None
matcher PathMatcher | None

PathMatcher implementation. Defaults to GlobMatcher.

None
follow_symlinks bool

Whether to follow symbolic links.

True

Returns:

Type Description
list[Match]

List of Match objects.

Example
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    d = Path(tmp).resolve()
    (d / "config.toml").touch()
    sp = SearchPath(("project", d))
    matches = sp.matches("*.toml")
    len(matches) >= 1 and matches[0].scope == "project"  # True
Source code in src/searchpath/_searchpath.py
def matches(  # noqa: PLR0913
    self,
    pattern: str = "**",
    *,
    kind: Literal["files", "dirs", "both"] = "files",
    dedupe: bool = True,
    include: "str | Sequence[str] | None" = None,
    include_from: "Path | str | Sequence[Path | str] | None" = None,
    include_from_ancestors: str | None = None,
    exclude: "str | Sequence[str] | None" = None,
    exclude_from: "Path | str | Sequence[Path | str] | None" = None,
    exclude_from_ancestors: str | None = None,
    matcher: "PathMatcher | None" = None,
    follow_symlinks: bool = True,
) -> list[Match]:
    """Find all matching paths with provenance information.

    Like all(), but returns Match objects containing paths along with
    scope names and source directories.

    Args:
        pattern: Glob pattern for matching paths. Defaults to "**" (all).
        kind: What to match: "files", "dirs", or "both".
        dedupe: If True, keep only first occurrence per relative path.
        include: Additional patterns paths must match.
        include_from: Path(s) to files containing include patterns.
        include_from_ancestors: Filename to load include patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        exclude: Patterns that reject paths.
        exclude_from: Path(s) to files containing exclude patterns.
        exclude_from_ancestors: Filename to load exclude patterns from
            ancestor directories. Patterns are collected from the search
            entry root toward each file's parent directory.
        matcher: PathMatcher implementation. Defaults to GlobMatcher.
        follow_symlinks: Whether to follow symbolic links.

    Returns:
        List of Match objects.

    Example:
        ```python
        import tempfile
        from pathlib import Path

        with tempfile.TemporaryDirectory() as tmp:
            d = Path(tmp).resolve()
            (d / "config.toml").touch()
            sp = SearchPath(("project", d))
            matches = sp.matches("*.toml")
            len(matches) >= 1 and matches[0].scope == "project"  # True
        ```
    """
    include_patterns, exclude_patterns = self._build_patterns(
        include, include_from, exclude, exclude_from
    )

    all_matches = self._iter_matches(
        pattern,
        kind=kind,
        include=include_patterns,
        exclude=exclude_patterns,
        include_from_ancestors=include_from_ancestors,
        exclude_from_ancestors=exclude_from_ancestors,
        matcher=matcher,
        follow_symlinks=follow_symlinks,
    )

    if dedupe:
        return list(self._dedupe_matches(all_matches))
    return list(all_matches)

Match class

Result object containing a matched path with provenance information.

searchpath.Match dataclass

Result of a search path lookup.

A Match represents a file or directory found during a search path lookup. It includes the absolute path to the match, the scope name identifying which search path entry it came from, and the source directory.

Attributes:

Name Type Description
path Path

Absolute path to the matched file or directory.

scope str

Scope name of the search path entry (e.g., "user", "project").

source Path

The search path directory this match came from.

Example
from pathlib import Path

match = Match(
    path=Path("/home/user/.config/myapp/config.toml"),
    scope="user",
    source=Path("/home/user/.config/myapp"),
)
match.relative  # PosixPath('config.toml')
Source code in src/searchpath/_match.py
@dataclass(frozen=True, slots=True)
class Match:
    """Result of a search path lookup.

    A Match represents a file or directory found during a search path lookup.
    It includes the absolute path to the match, the scope name identifying
    which search path entry it came from, and the source directory.

    Attributes:
        path: Absolute path to the matched file or directory.
        scope: Scope name of the search path entry (e.g., "user", "project").
        source: The search path directory this match came from.

    Example:
        ```python
        from pathlib import Path

        match = Match(
            path=Path("/home/user/.config/myapp/config.toml"),
            scope="user",
            source=Path("/home/user/.config/myapp"),
        )
        match.relative  # PosixPath('config.toml')
        ```
    """

    path: "Path"
    """Absolute path to the matched file or directory."""

    scope: str
    """Scope name of the search path entry (e.g., "user", "project")."""

    source: "Path"
    """The search path directory this match came from."""

    @property
    def relative(self) -> "Path":
        """Path relative to the source directory.

        Returns:
            The path of this match relative to its source directory.
        """
        return self.path.relative_to(self.source)

searchpath.Match.path instance-attribute

path: Path

Absolute path to the matched file or directory.

searchpath.Match.scope instance-attribute

scope: str

Scope name of the search path entry (e.g., "user", "project").

searchpath.Match.source instance-attribute

source: Path

The search path directory this match came from.

searchpath.Match.relative property

relative: Path

Path relative to the source directory.

Returns:

Type Description
Path

The path of this match relative to its source directory.


Entry type

Type alias for SearchPath entry arguments.

searchpath.Entry module-attribute

Entry: TypeAlias = 'tuple[str, Path | str | None] | Path | str | None'

Type alias for SearchPath entry arguments.


Pattern matchers

PathMatcher protocol

Protocol defining the interface for pattern matching implementations.

searchpath.PathMatcher

Bases: Protocol

Protocol for pattern matching implementations.

Path matchers check if paths match include/exclude pattern lists. Each matcher handles pattern compilation internally.

Example
matcher = GlobMatcher()
matcher.matches("src/main.py", include=["**/*.py"])  # True
matcher.matches("src/main.py", exclude=["**/test_*"])  # True

Methods:

Name Description
matches

Check if path matches the include/exclude patterns.

Attributes:

Name Type Description
supports_negation bool

Whether this matcher supports negation patterns (e.g., !pattern).

supports_dir_only bool

Whether this matcher supports directory-only patterns (e.g., pattern/).

Source code in src/searchpath/_matchers.py
class PathMatcher(Protocol):  # pragma: no cover
    """Protocol for pattern matching implementations.

    Path matchers check if paths match include/exclude pattern lists.
    Each matcher handles pattern compilation internally.

    Example:
        ```python
        matcher = GlobMatcher()
        matcher.matches("src/main.py", include=["**/*.py"])  # True
        matcher.matches("src/main.py", exclude=["**/test_*"])  # True
        ```
    """

    @property
    def supports_negation(self) -> bool:
        """Whether this matcher supports negation patterns (e.g., !pattern)."""
        ...

    @property
    def supports_dir_only(self) -> bool:
        """Whether this matcher supports directory-only patterns (e.g., pattern/)."""
        ...

    def matches(
        self,
        path: str,
        *,
        is_dir: bool = False,
        include: "Sequence[str]" = (),
        exclude: "Sequence[str]" = (),
    ) -> bool:
        """Check if path matches the include/exclude patterns.

        A path matches if:
        - It matches at least one include pattern (or include is empty), AND
        - It does not match any exclude pattern

        Args:
            path: Relative path from search root (forward slashes).
            is_dir: Whether the path represents a directory.
            include: Patterns the path must match (empty = match all).
            exclude: Patterns that reject the path.

        Returns:
            True if the path should be included in results.
        """
        ...
searchpath.PathMatcher.supports_negation property
supports_negation: bool

Whether this matcher supports negation patterns (e.g., !pattern).

searchpath.PathMatcher.supports_dir_only property
supports_dir_only: bool

Whether this matcher supports directory-only patterns (e.g., pattern/).

searchpath.PathMatcher.matches
matches(
    path: str,
    *,
    is_dir: bool = False,
    include: Sequence[str] = (),
    exclude: Sequence[str] = ()
) -> bool

Check if path matches the include/exclude patterns.

A path matches if: - It matches at least one include pattern (or include is empty), AND - It does not match any exclude pattern

Parameters:

Name Type Description Default
path str

Relative path from search root (forward slashes).

required
is_dir bool

Whether the path represents a directory.

False
include Sequence[str]

Patterns the path must match (empty = match all).

()
exclude Sequence[str]

Patterns that reject the path.

()

Returns:

Type Description
bool

True if the path should be included in results.

Source code in src/searchpath/_matchers.py
def matches(
    self,
    path: str,
    *,
    is_dir: bool = False,
    include: "Sequence[str]" = (),
    exclude: "Sequence[str]" = (),
) -> bool:
    """Check if path matches the include/exclude patterns.

    A path matches if:
    - It matches at least one include pattern (or include is empty), AND
    - It does not match any exclude pattern

    Args:
        path: Relative path from search root (forward slashes).
        is_dir: Whether the path represents a directory.
        include: Patterns the path must match (empty = match all).
        exclude: Patterns that reject the path.

    Returns:
        True if the path should be included in results.
    """
    ...

GlobMatcher

Default pattern matcher using glob-style patterns.

searchpath.GlobMatcher

Path matcher using glob-style patterns.

Patterns are translated to regular expressions internally. The ** pattern is only treated as recursive when it appears as a complete path component (e.g., /, foo//bar), matching gitignore semantics.

Supports
  • *: Matches any characters except /
  • **: Matches any characters including / (when complete component)
  • ?: Matches any single character except /
  • [abc]: Matches any character in the set
  • [!abc] or [^abc]: Matches any character not in the set
  • [a-z]: Matches any character in the range
Does not support
  • Negation patterns (!pattern)
  • Directory-only patterns (pattern/)
  • Anchored patterns (/pattern)
Example
matcher = GlobMatcher()
matcher.matches("src/main.py", include=["**/*.py"])  # True
matcher.matches("test_main.py", exclude=["test_*"])  # False

Methods:

Name Description
__init__

Initialize the matcher with an empty pattern cache.

matches

Check if path matches the include/exclude patterns.

Attributes:

Name Type Description
supports_negation bool

Whether this matcher supports negation patterns.

supports_dir_only bool

Whether this matcher supports directory-only patterns.

Source code in src/searchpath/_matchers.py
@final
class GlobMatcher:
    """Path matcher using glob-style patterns.

    Patterns are translated to regular expressions internally. The **
    pattern is only treated as recursive when it appears as a complete
    path component (e.g., **/, foo/**/bar), matching gitignore semantics.

    Supports:
        - ``*``: Matches any characters except ``/``
        - ``**``: Matches any characters including ``/`` (when complete component)
        - ``?``: Matches any single character except ``/``
        - ``[abc]``: Matches any character in the set
        - ``[!abc]`` or ``[^abc]``: Matches any character not in the set
        - ``[a-z]``: Matches any character in the range

    Does not support:
        - Negation patterns (``!pattern``)
        - Directory-only patterns (``pattern/``)
        - Anchored patterns (``/pattern``)

    Example:
        ```python
        matcher = GlobMatcher()
        matcher.matches("src/main.py", include=["**/*.py"])  # True
        matcher.matches("test_main.py", exclude=["test_*"])  # False
        ```
    """

    __slots__ = ("_cache",)

    def __init__(self) -> None:
        """Initialize the matcher with an empty pattern cache."""
        self._cache: dict[str, _CompiledPattern] = {}

    @property
    def supports_negation(self) -> bool:
        """Whether this matcher supports negation patterns.

        Returns:
            Always False for GlobMatcher.
        """
        return False

    @property
    def supports_dir_only(self) -> bool:
        """Whether this matcher supports directory-only patterns.

        Returns:
            Always False for GlobMatcher.
        """
        return False

    def matches(
        self,
        path: str,
        *,
        is_dir: bool = False,
        include: "Sequence[str]" = (),
        exclude: "Sequence[str]" = (),
    ) -> bool:
        """Check if path matches the include/exclude patterns.

        A path matches if:
        - It matches at least one include pattern (or include is empty), AND
        - It does not match any exclude pattern

        Args:
            path: Relative path from search root (forward slashes).
            is_dir: Whether the path represents a directory (ignored by GlobMatcher).
            include: Patterns the path must match (empty = match all).
            exclude: Patterns that reject the path.

        Returns:
            True if the path should be included in results.

        Raises:
            PatternSyntaxError: If any pattern has invalid syntax.

        Example:
            ```python
            matcher = GlobMatcher()
            matcher.matches("main.py", include=["*.py"])  # True
            matcher.matches("main.py", exclude=["main.*"])  # False
            ```
        """
        del is_dir  # Unused by GlobMatcher (no dir_only support)

        # Check include patterns (empty = match all)
        if include:
            included = any(self._match_pattern(path, p) for p in include)
            if not included:
                return False

        # Check exclude patterns
        if exclude:
            excluded = any(self._match_pattern(path, p) for p in exclude)
            if excluded:
                return False

        return True

    def _match_pattern(self, path: str, pattern: str) -> bool:
        """Test if a path matches a single pattern.

        Args:
            path: Relative path from search root.
            pattern: Glob pattern to match against.

        Returns:
            True if the path matches the pattern.
        """
        compiled = self._compile(pattern)
        return compiled.regex.fullmatch(path) is not None

    def _compile(self, pattern: str) -> _CompiledPattern:
        """Compile a glob pattern (with caching).

        Args:
            pattern: The glob pattern string to compile.

        Returns:
            A compiled pattern ready for matching.

        Raises:
            PatternSyntaxError: If the pattern is empty or has unclosed brackets.
        """
        if pattern in self._cache:
            return self._cache[pattern]

        if not pattern:
            raise PatternSyntaxError(pattern, "empty pattern")

        regex_str = self._glob_to_regex(pattern)
        regex = re.compile(regex_str)

        compiled = _CompiledPattern(regex=regex, pattern=pattern)
        self._cache[pattern] = compiled
        return compiled

    def _glob_to_regex(self, pattern: str) -> str:
        """Translate a glob pattern to a regex string.

        Args:
            pattern: The glob pattern to translate.

        Returns:
            A regex string equivalent to the glob pattern.

        Raises:
            PatternSyntaxError: If the pattern has unclosed brackets.
        """
        result: list[str] = []
        i = 0
        n = len(pattern)

        while i < n:
            c = pattern[i]

            if c == "*":
                i = self._translate_star(pattern, i, n, result)
            elif c == "?":
                result.append("[^/]")
                i += 1
            elif c == "[":
                i = self._translate_bracket(pattern, i, n, result)
            else:
                self._translate_literal(c, result)
                i += 1

        return "".join(result)

    def _translate_star(self, pattern: str, i: int, n: int, result: list[str]) -> int:
        """Translate * or ** glob pattern to regex.

        Gitignore-style semantics: ** is only recursive when it's a complete
        path component (bounded by / or string start/end). Otherwise ** is
        treated as a single * (matches anything except /).
        """
        # Single star - matches anything except /
        if i + 1 >= n or pattern[i + 1] != "*":
            result.append("[^/]*")
            return i + 1

        # Double star - check if it's a complete path component
        return self._translate_double_star(pattern, i, n, result)

    def _translate_double_star(
        self, pattern: str, i: int, n: int, result: list[str]
    ) -> int:
        """Translate ** pattern, checking if it's a complete path component."""
        next_pos = i + 2
        at_start = i == 0
        at_end = next_pos >= n
        after_slash = i > 0 and pattern[i - 1] == "/"
        before_slash = next_pos < n and pattern[next_pos] == "/"

        is_component = (at_start or after_slash) and (at_end or before_slash)

        if not is_component:
            # ** not a complete component (e.g., a**b) - treat as single *
            result.append("[^/]*")
            return next_pos

        # ** as complete path component - recursive match
        if before_slash:
            # **/ - match zero or more path segments including trailing slash
            result.append("(?:.*/)?")
            return next_pos + 1
        # ** at end - match anything (zero or more of any char)
        result.append(".*")
        return next_pos

    def _translate_literal(self, c: str, result: list[str]) -> None:
        """Translate a literal character, escaping regex metacharacters."""
        if c in r"\.+^${}()|":
            result.append("\\")
        result.append(c)

    def _translate_bracket(
        self, pattern: str, i: int, n: int, result: list[str]
    ) -> int:
        """Translate a character class [...] to regex.

        Args:
            pattern: The full pattern string.
            i: Current position (pointing to '[').
            n: Length of pattern.
            result: List to append regex parts to.

        Returns:
            New position after the closing ']'.

        Raises:
            PatternSyntaxError: If the bracket is unclosed.
        """
        bracket_start = i
        i += 1

        if i >= n:
            raise PatternSyntaxError(
                pattern, "unclosed bracket", position=bracket_start
            )

        # Check for negation at start
        # Gitignore-style: negated classes should also exclude /
        if pattern[i] in "!^":
            result.append("[^/")
            i += 1
        else:
            result.append("[")

        if i >= n:
            raise PatternSyntaxError(
                pattern, "unclosed bracket", position=bracket_start
            )

        # Handle ] as first character in class (literal ])
        if pattern[i] == "]":
            result.append("]")
            i += 1

        # Collect characters until closing ]
        i = self._collect_bracket_contents(pattern, i, n, result)

        if i >= n:
            raise PatternSyntaxError(
                pattern, "unclosed bracket", position=bracket_start
            )

        result.append("]")
        return i + 1

    def _collect_bracket_contents(
        self, pattern: str, i: int, n: int, result: list[str]
    ) -> int:
        """Collect contents of a character class until closing ']'."""
        while i < n and pattern[i] != "]":
            char = pattern[i]
            if char == "\\":
                i = self._handle_bracket_escape(pattern, i, n, result)
            elif char == "-":
                result.append("-")
                i += 1
            else:
                self._handle_bracket_char(char, result)
                i += 1
        return i

    def _handle_bracket_escape(
        self, pattern: str, i: int, n: int, result: list[str]
    ) -> int:
        """Handle backslash escape inside bracket expression."""
        result.append("\\")
        i += 1
        if i < n:
            result.append(pattern[i])
            i += 1
        return i

    def _handle_bracket_char(self, char: str, result: list[str]) -> None:
        """Handle a regular character inside bracket expression."""
        if char in r"\^-]":
            result.append("\\")
        result.append(char)
searchpath.GlobMatcher.supports_negation property
supports_negation: bool

Whether this matcher supports negation patterns.

Returns:

Type Description
bool

Always False for GlobMatcher.

searchpath.GlobMatcher.supports_dir_only property
supports_dir_only: bool

Whether this matcher supports directory-only patterns.

Returns:

Type Description
bool

Always False for GlobMatcher.

searchpath.GlobMatcher.__init__
__init__() -> None
Source code in src/searchpath/_matchers.py
def __init__(self) -> None:
    """Initialize the matcher with an empty pattern cache."""
    self._cache: dict[str, _CompiledPattern] = {}
searchpath.GlobMatcher.matches
matches(
    path: str,
    *,
    is_dir: bool = False,
    include: Sequence[str] = (),
    exclude: Sequence[str] = ()
) -> bool

Check if path matches the include/exclude patterns.

A path matches if: - It matches at least one include pattern (or include is empty), AND - It does not match any exclude pattern

Parameters:

Name Type Description Default
path str

Relative path from search root (forward slashes).

required
is_dir bool

Whether the path represents a directory (ignored by GlobMatcher).

False
include Sequence[str]

Patterns the path must match (empty = match all).

()
exclude Sequence[str]

Patterns that reject the path.

()

Returns:

Type Description
bool

True if the path should be included in results.

Raises:

Type Description
PatternSyntaxError

If any pattern has invalid syntax.

Example
matcher = GlobMatcher()
matcher.matches("main.py", include=["*.py"])  # True
matcher.matches("main.py", exclude=["main.*"])  # False
Source code in src/searchpath/_matchers.py
def matches(
    self,
    path: str,
    *,
    is_dir: bool = False,
    include: "Sequence[str]" = (),
    exclude: "Sequence[str]" = (),
) -> bool:
    """Check if path matches the include/exclude patterns.

    A path matches if:
    - It matches at least one include pattern (or include is empty), AND
    - It does not match any exclude pattern

    Args:
        path: Relative path from search root (forward slashes).
        is_dir: Whether the path represents a directory (ignored by GlobMatcher).
        include: Patterns the path must match (empty = match all).
        exclude: Patterns that reject the path.

    Returns:
        True if the path should be included in results.

    Raises:
        PatternSyntaxError: If any pattern has invalid syntax.

    Example:
        ```python
        matcher = GlobMatcher()
        matcher.matches("main.py", include=["*.py"])  # True
        matcher.matches("main.py", exclude=["main.*"])  # False
        ```
    """
    del is_dir  # Unused by GlobMatcher (no dir_only support)

    # Check include patterns (empty = match all)
    if include:
        included = any(self._match_pattern(path, p) for p in include)
        if not included:
            return False

    # Check exclude patterns
    if exclude:
        excluded = any(self._match_pattern(path, p) for p in exclude)
        if excluded:
            return False

    return True

RegexMatcher

Pattern matcher using Python regular expressions.

searchpath.RegexMatcher

Path matcher using Python regular expressions.

Uses the re module for full regex syntax. Patterns are matched against the entire path using fullmatch() for consistency with GlobMatcher.

Supports
  • Full Python regex syntax (re module)
  • Character classes, quantifiers, alternation, groups
Does not support
  • Negation patterns (!pattern)
  • Directory-only patterns (pattern/)
Example
matcher = RegexMatcher()
matcher.matches("src/main.py", include=[r".*\.py"])  # True
matcher.matches("test_main.py", exclude=[r"test_.*"])  # False

Methods:

Name Description
__init__

Initialize the matcher with an empty pattern cache.

matches

Check if path matches the include/exclude patterns.

Attributes:

Name Type Description
supports_negation bool

Whether this matcher supports negation patterns.

supports_dir_only bool

Whether this matcher supports directory-only patterns.

Source code in src/searchpath/_matchers.py
@final
class RegexMatcher:
    r"""Path matcher using Python regular expressions.

    Uses the re module for full regex syntax. Patterns are matched against
    the entire path using fullmatch() for consistency with GlobMatcher.

    Supports:
        - Full Python regex syntax (re module)
        - Character classes, quantifiers, alternation, groups

    Does not support:
        - Negation patterns (``!pattern``)
        - Directory-only patterns (``pattern/``)

    Example:
        ```python
        matcher = RegexMatcher()
        matcher.matches("src/main.py", include=[r".*\.py"])  # True
        matcher.matches("test_main.py", exclude=[r"test_.*"])  # False
        ```
    """

    __slots__ = ("_cache",)

    def __init__(self) -> None:
        """Initialize the matcher with an empty pattern cache."""
        self._cache: dict[str, _CompiledPattern] = {}

    @property
    def supports_negation(self) -> bool:
        """Whether this matcher supports negation patterns.

        Returns:
            Always False for RegexMatcher.
        """
        return False

    @property
    def supports_dir_only(self) -> bool:
        """Whether this matcher supports directory-only patterns.

        Returns:
            Always False for RegexMatcher.
        """
        return False

    def matches(
        self,
        path: str,
        *,
        is_dir: bool = False,
        include: "Sequence[str]" = (),
        exclude: "Sequence[str]" = (),
    ) -> bool:
        """Check if path matches the include/exclude patterns.

        A path matches if:
        - It matches at least one include pattern (or include is empty), AND
        - It does not match any exclude pattern

        Args:
            path: Relative path from search root (forward slashes).
            is_dir: Whether the path represents a directory (ignored by RegexMatcher).
            include: Patterns the path must match (empty = match all).
            exclude: Patterns that reject the path.

        Returns:
            True if the path should be included in results.

        Raises:
            PatternSyntaxError: If any pattern has invalid regex syntax.
        """
        del is_dir  # Unused by RegexMatcher (no dir_only support)

        # Check include patterns (empty = match all)
        if include:
            included = any(self._match_pattern(path, p) for p in include)
            if not included:
                return False

        # Check exclude patterns
        if exclude:
            excluded = any(self._match_pattern(path, p) for p in exclude)
            if excluded:
                return False

        return True

    def _match_pattern(self, path: str, pattern: str) -> bool:
        """Test if a path matches a single pattern.

        Args:
            path: Relative path from search root.
            pattern: Regex pattern to match against.

        Returns:
            True if the path matches the pattern.
        """
        compiled = self._compile(pattern)
        return compiled.regex.fullmatch(path) is not None

    def _compile(self, pattern: str) -> _CompiledPattern:
        """Compile a regex pattern (with caching).

        Args:
            pattern: The regex pattern string to compile.

        Returns:
            A compiled pattern ready for matching.

        Raises:
            PatternSyntaxError: If the pattern is empty or has invalid syntax.
        """
        if pattern in self._cache:
            return self._cache[pattern]

        if not pattern:
            raise PatternSyntaxError(pattern, "empty pattern")

        try:
            regex = re.compile(pattern)
        except re.error as e:
            raise PatternSyntaxError(pattern, str(e)) from e

        compiled = _CompiledPattern(regex=regex, pattern=pattern)
        self._cache[pattern] = compiled
        return compiled
searchpath.RegexMatcher.supports_negation property
supports_negation: bool

Whether this matcher supports negation patterns.

Returns:

Type Description
bool

Always False for RegexMatcher.

searchpath.RegexMatcher.supports_dir_only property
supports_dir_only: bool

Whether this matcher supports directory-only patterns.

Returns:

Type Description
bool

Always False for RegexMatcher.

searchpath.RegexMatcher.__init__
__init__() -> None
Source code in src/searchpath/_matchers.py
def __init__(self) -> None:
    """Initialize the matcher with an empty pattern cache."""
    self._cache: dict[str, _CompiledPattern] = {}
searchpath.RegexMatcher.matches
matches(
    path: str,
    *,
    is_dir: bool = False,
    include: Sequence[str] = (),
    exclude: Sequence[str] = ()
) -> bool

Check if path matches the include/exclude patterns.

A path matches if: - It matches at least one include pattern (or include is empty), AND - It does not match any exclude pattern

Parameters:

Name Type Description Default
path str

Relative path from search root (forward slashes).

required
is_dir bool

Whether the path represents a directory (ignored by RegexMatcher).

False
include Sequence[str]

Patterns the path must match (empty = match all).

()
exclude Sequence[str]

Patterns that reject the path.

()

Returns:

Type Description
bool

True if the path should be included in results.

Raises:

Type Description
PatternSyntaxError

If any pattern has invalid regex syntax.

Source code in src/searchpath/_matchers.py
def matches(
    self,
    path: str,
    *,
    is_dir: bool = False,
    include: "Sequence[str]" = (),
    exclude: "Sequence[str]" = (),
) -> bool:
    """Check if path matches the include/exclude patterns.

    A path matches if:
    - It matches at least one include pattern (or include is empty), AND
    - It does not match any exclude pattern

    Args:
        path: Relative path from search root (forward slashes).
        is_dir: Whether the path represents a directory (ignored by RegexMatcher).
        include: Patterns the path must match (empty = match all).
        exclude: Patterns that reject the path.

    Returns:
        True if the path should be included in results.

    Raises:
        PatternSyntaxError: If any pattern has invalid regex syntax.
    """
    del is_dir  # Unused by RegexMatcher (no dir_only support)

    # Check include patterns (empty = match all)
    if include:
        included = any(self._match_pattern(path, p) for p in include)
        if not included:
            return False

    # Check exclude patterns
    if exclude:
        excluded = any(self._match_pattern(path, p) for p in exclude)
        if excluded:
            return False

    return True

GitignoreMatcher

Pattern matcher using gitignore-style patterns via the pathspec library.

searchpath.GitignoreMatcher

Path matcher using gitignore-style patterns via pathspec library.

Provides full gitignore compatibility including
  • *: Matches any characters except /
  • **: Recursive directory matching
  • ?: Matches any single character except /
  • [abc]: Character classes
  • !pattern: Negation (un-ignores previously matched paths)
  • pattern/: Directory-only patterns
  • /pattern: Anchored patterns (match from root only)

Requires the optional pathspec package. Install with::

pip install searchpath[gitignore]
Example
matcher = GitignoreMatcher()
matcher.matches("src/main.py", include=["*.py"])  # True
matcher.matches("test_main.py", exclude=["test_*"])  # False

Methods:

Name Description
__init__

Initialize the matcher, checking for pathspec availability.

matches

Check if path matches the include/exclude patterns.

Attributes:

Name Type Description
supports_negation bool

Whether this matcher supports negation patterns.

supports_dir_only bool

Whether this matcher supports directory-only patterns.

Source code in src/searchpath/_matchers.py
@final
class GitignoreMatcher:
    """Path matcher using gitignore-style patterns via pathspec library.

    Provides full gitignore compatibility including:
        - ``*``: Matches any characters except ``/``
        - ``**``: Recursive directory matching
        - ``?``: Matches any single character except ``/``
        - ``[abc]``: Character classes
        - ``!pattern``: Negation (un-ignores previously matched paths)
        - ``pattern/``: Directory-only patterns
        - ``/pattern``: Anchored patterns (match from root only)

    Requires the optional ``pathspec`` package. Install with::

        pip install searchpath[gitignore]

    Example:
        ```python
        matcher = GitignoreMatcher()
        matcher.matches("src/main.py", include=["*.py"])  # True
        matcher.matches("test_main.py", exclude=["test_*"])  # False
        ```
    """

    __slots__ = ("_spec_cache",)

    def __init__(self) -> None:
        """Initialize the matcher, checking for pathspec availability."""
        try:
            import pathspec  # noqa: F401, PLC0415  # pyright: ignore[reportUnusedImport]
        except ImportError as e:  # pragma: no cover
            msg = (
                "GitignoreMatcher requires the 'pathspec' package. "
                "Install it with: pip install searchpath[gitignore]"
            )
            raise ImportError(msg) from e
        self._spec_cache: dict[tuple[str, ...], GitIgnoreSpec] = {}

    @property
    def supports_negation(self) -> bool:
        """Whether this matcher supports negation patterns.

        Returns:
            Always True for GitignoreMatcher.
        """
        return True

    @property
    def supports_dir_only(self) -> bool:
        """Whether this matcher supports directory-only patterns.

        Returns:
            Always True for GitignoreMatcher.
        """
        return True

    def matches(
        self,
        path: str,
        *,
        is_dir: bool = False,
        include: "Sequence[str]" = (),
        exclude: "Sequence[str]" = (),
    ) -> bool:
        """Check if path matches the include/exclude patterns.

        Uses gitignore semantics where patterns are evaluated in order and
        negation patterns (!pattern) can re-include previously excluded paths.

        Args:
            path: Relative path from search root (forward slashes).
            is_dir: Whether the path represents a directory.
            include: Patterns the path must match (empty = match all).
            exclude: Patterns that reject the path.

        Returns:
            True if the path should be included in results.

        Raises:
            PatternSyntaxError: If any pattern has invalid syntax.
        """
        del is_dir  # Unused by GitignoreMatcher currently

        # Check include patterns (empty = match all)
        if include and not self._matches_spec(path, include):
            return False

        # Check exclude patterns - return False if matches, True otherwise
        return not (exclude and self._matches_spec(path, exclude))

    def _matches_spec(self, path: str, patterns: "Sequence[str]") -> bool:
        """Check if path matches gitignore spec built from patterns.

        Args:
            path: Relative path from search root.
            patterns: Gitignore-style patterns.

        Returns:
            True if the path matches the pattern spec.

        Raises:
            PatternSyntaxError: If any pattern is empty or invalid.
        """
        self._validate_patterns(patterns)
        spec = self._build_spec(patterns)
        return spec.match_file(path)

    def _validate_patterns(self, patterns: "Sequence[str]") -> None:
        """Validate that all patterns are non-empty.

        Args:
            patterns: Patterns to validate.

        Raises:
            PatternSyntaxError: If any pattern is empty.
        """
        for p in patterns:
            if not p:
                raise PatternSyntaxError(p, "empty pattern")

    def _build_spec(self, patterns: "Sequence[str]") -> "GitIgnoreSpec":
        """Build a GitIgnoreSpec from patterns.

        Args:
            patterns: Gitignore-style patterns.

        Returns:
            Compiled GitIgnoreSpec.

        Raises:
            PatternSyntaxError: If patterns have invalid syntax.
        """
        cache_key = tuple(patterns)
        if cache_key in self._spec_cache:
            return self._spec_cache[cache_key]

        from pathspec import GitIgnoreSpec  # noqa: PLC0415

        try:
            spec = GitIgnoreSpec.from_lines(patterns)
        except Exception as e:
            raise PatternSyntaxError(str(patterns), str(e)) from e

        self._spec_cache[cache_key] = spec
        return spec
searchpath.GitignoreMatcher.supports_negation property
supports_negation: bool

Whether this matcher supports negation patterns.

Returns:

Type Description
bool

Always True for GitignoreMatcher.

searchpath.GitignoreMatcher.supports_dir_only property
supports_dir_only: bool

Whether this matcher supports directory-only patterns.

Returns:

Type Description
bool

Always True for GitignoreMatcher.

searchpath.GitignoreMatcher.__init__
__init__() -> None
Source code in src/searchpath/_matchers.py
def __init__(self) -> None:
    """Initialize the matcher, checking for pathspec availability."""
    try:
        import pathspec  # noqa: F401, PLC0415  # pyright: ignore[reportUnusedImport]
    except ImportError as e:  # pragma: no cover
        msg = (
            "GitignoreMatcher requires the 'pathspec' package. "
            "Install it with: pip install searchpath[gitignore]"
        )
        raise ImportError(msg) from e
    self._spec_cache: dict[tuple[str, ...], GitIgnoreSpec] = {}
searchpath.GitignoreMatcher.matches
matches(
    path: str,
    *,
    is_dir: bool = False,
    include: Sequence[str] = (),
    exclude: Sequence[str] = ()
) -> bool

Check if path matches the include/exclude patterns.

Uses gitignore semantics where patterns are evaluated in order and negation patterns (!pattern) can re-include previously excluded paths.

Parameters:

Name Type Description Default
path str

Relative path from search root (forward slashes).

required
is_dir bool

Whether the path represents a directory.

False
include Sequence[str]

Patterns the path must match (empty = match all).

()
exclude Sequence[str]

Patterns that reject the path.

()

Returns:

Type Description
bool

True if the path should be included in results.

Raises:

Type Description
PatternSyntaxError

If any pattern has invalid syntax.

Source code in src/searchpath/_matchers.py
def matches(
    self,
    path: str,
    *,
    is_dir: bool = False,
    include: "Sequence[str]" = (),
    exclude: "Sequence[str]" = (),
) -> bool:
    """Check if path matches the include/exclude patterns.

    Uses gitignore semantics where patterns are evaluated in order and
    negation patterns (!pattern) can re-include previously excluded paths.

    Args:
        path: Relative path from search root (forward slashes).
        is_dir: Whether the path represents a directory.
        include: Patterns the path must match (empty = match all).
        exclude: Patterns that reject the path.

    Returns:
        True if the path should be included in results.

    Raises:
        PatternSyntaxError: If any pattern has invalid syntax.
    """
    del is_dir  # Unused by GitignoreMatcher currently

    # Check include patterns (empty = match all)
    if include and not self._matches_spec(path, include):
        return False

    # Check exclude patterns - return False if matches, True otherwise
    return not (exclude and self._matches_spec(path, exclude))

Exceptions

Base exception

searchpath.SearchPathError

Bases: Exception

Base exception for all searchpath errors.

Source code in src/searchpath/_exceptions.py
class SearchPathError(Exception):
    """Base exception for all searchpath errors."""

Pattern exceptions

searchpath.PatternError

Bases: SearchPathError

Base exception for pattern-related errors.

Source code in src/searchpath/_exceptions.py
class PatternError(SearchPathError):
    """Base exception for pattern-related errors."""

searchpath.PatternSyntaxError

Bases: PatternError

Raised when a pattern has invalid syntax.

Attributes:

Name Type Description
pattern str

The pattern that failed to parse.

message str

Description of the syntax error.

position int | None

Character position where the error occurred, or None.

Methods:

Name Description
__init__

Initialize a PatternSyntaxError.

Source code in src/searchpath/_exceptions.py
class PatternSyntaxError(PatternError):
    """Raised when a pattern has invalid syntax.

    Attributes:
        pattern: The pattern that failed to parse.
        message: Description of the syntax error.
        position: Character position where the error occurred, or None.
    """

    pattern: str
    message: str
    position: int | None

    def __init__(
        self,
        pattern: str,
        message: str,
        position: int | None = None,
    ) -> None:
        """Initialize a PatternSyntaxError.

        Args:
            pattern: The pattern that failed to parse.
            message: Description of the syntax error.
            position: Character position where the error occurred, or None.
        """
        self.pattern = pattern
        self.message = message
        self.position = position

        if position is not None:
            error_msg = f"Invalid pattern {pattern!r} at position {position}: {message}"
        else:
            error_msg = f"Invalid pattern {pattern!r}: {message}"

        super().__init__(error_msg)
searchpath.PatternSyntaxError.__init__
__init__(pattern: str, message: str, position: int | None = None) -> None

Parameters:

Name Type Description Default
pattern str

The pattern that failed to parse.

required
message str

Description of the syntax error.

required
position int | None

Character position where the error occurred, or None.

None
Source code in src/searchpath/_exceptions.py
def __init__(
    self,
    pattern: str,
    message: str,
    position: int | None = None,
) -> None:
    """Initialize a PatternSyntaxError.

    Args:
        pattern: The pattern that failed to parse.
        message: Description of the syntax error.
        position: Character position where the error occurred, or None.
    """
    self.pattern = pattern
    self.message = message
    self.position = position

    if position is not None:
        error_msg = f"Invalid pattern {pattern!r} at position {position}: {message}"
    else:
        error_msg = f"Invalid pattern {pattern!r}: {message}"

    super().__init__(error_msg)

searchpath.PatternFileError

Bases: PatternError

Raised when a pattern file cannot be read or parsed.

Attributes:

Name Type Description
path Path

Path to the pattern file.

message str

Description of the error.

line_number int | None

Line number where the error occurred, or None.

Methods:

Name Description
__init__

Initialize a PatternFileError.

Source code in src/searchpath/_exceptions.py
class PatternFileError(PatternError):
    """Raised when a pattern file cannot be read or parsed.

    Attributes:
        path: Path to the pattern file.
        message: Description of the error.
        line_number: Line number where the error occurred, or None.
    """

    path: "Path"
    message: str
    line_number: int | None

    def __init__(
        self,
        path: "Path",
        message: str,
        line_number: int | None = None,
    ) -> None:
        """Initialize a PatternFileError.

        Args:
            path: Path to the pattern file.
            message: Description of the error.
            line_number: Line number where the error occurred, or None.
        """
        self.path = path
        self.message = message
        self.line_number = line_number

        if line_number is not None:
            error_msg = f"Error in pattern file {path}:{line_number}: {message}"
        else:
            error_msg = f"Error in pattern file {path}: {message}"

        super().__init__(error_msg)
searchpath.PatternFileError.__init__
__init__(path: Path, message: str, line_number: int | None = None) -> None

Parameters:

Name Type Description Default
path Path

Path to the pattern file.

required
message str

Description of the error.

required
line_number int | None

Line number where the error occurred, or None.

None
Source code in src/searchpath/_exceptions.py
def __init__(
    self,
    path: "Path",
    message: str,
    line_number: int | None = None,
) -> None:
    """Initialize a PatternFileError.

    Args:
        path: Path to the pattern file.
        message: Description of the error.
        line_number: Line number where the error occurred, or None.
    """
    self.path = path
    self.message = message
    self.line_number = line_number

    if line_number is not None:
        error_msg = f"Error in pattern file {path}:{line_number}: {message}"
    else:
        error_msg = f"Error in pattern file {path}: {message}"

    super().__init__(error_msg)

Configuration exceptions

searchpath.ConfigurationError

Bases: SearchPathError

Raised when SearchPath configuration is invalid.

Source code in src/searchpath/_exceptions.py
class ConfigurationError(SearchPathError):
    """Raised when SearchPath configuration is invalid."""