1""":module: watchdog.utils.patterns
2:synopsis: Common wildcard searching/filtering functionality for files.
3:author: [email protected] (Boris Staletic)
4:author: [email protected] (Yesudeep Mangalapilly)
5:author: [email protected] (Mickaël Schoentgen)
6"""
7
8from __future__ import annotations
9
10# Non-pure path objects are only allowed on their respective OS's.
11# Thus, these utilities require "pure" path objects that don't access the filesystem.
12# Since pathlib doesn't have a `case_sensitive` parameter, we have to approximate it
13# by converting input paths to `PureWindowsPath` and `PurePosixPath` where:
14#   - `PureWindowsPath` is always case-insensitive.
15#   - `PurePosixPath` is always case-sensitive.
16# Reference: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match
17from pathlib import PurePosixPath, PureWindowsPath
18from typing import TYPE_CHECKING
19
20if TYPE_CHECKING:
21    from collections.abc import Iterator
22
23
24def _match_path(
25    raw_path: str,
26    included_patterns: set[str],
27    excluded_patterns: set[str],
28    *,
29    case_sensitive: bool,
30) -> bool:
31    """Internal function same as :func:`match_path` but does not check arguments."""
32    path: PurePosixPath | PureWindowsPath
33    if case_sensitive:
34        path = PurePosixPath(raw_path)
35    else:
36        included_patterns = {pattern.lower() for pattern in included_patterns}
37        excluded_patterns = {pattern.lower() for pattern in excluded_patterns}
38        path = PureWindowsPath(raw_path)
39
40    common_patterns = included_patterns & excluded_patterns
41    if common_patterns:
42        error = f"conflicting patterns `{common_patterns}` included and excluded"
43        raise ValueError(error)
44
45    return any(path.match(p) for p in included_patterns) and not any(path.match(p) for p in excluded_patterns)
46
47
48def filter_paths(
49    paths: list[str],
50    *,
51    included_patterns: list[str] | None = None,
52    excluded_patterns: list[str] | None = None,
53    case_sensitive: bool = True,
54) -> Iterator[str]:
55    """Filters from a set of paths based on acceptable patterns and
56    ignorable patterns.
57    :param paths:
58        A list of path names that will be filtered based on matching and
59        ignored patterns.
60    :param included_patterns:
61        Allow filenames matching wildcard patterns specified in this list.
62        If no pattern list is specified, ["*"] is used as the default pattern,
63        which matches all files.
64    :param excluded_patterns:
65        Ignores filenames matching wildcard patterns specified in this list.
66        If no pattern list is specified, no files are ignored.
67    :param case_sensitive:
68        ``True`` if matching should be case-sensitive; ``False`` otherwise.
69    :returns:
70        A list of pathnames that matched the allowable patterns and passed
71        through the ignored patterns.
72    """
73    included = set(["*"] if included_patterns is None else included_patterns)
74    excluded = set([] if excluded_patterns is None else excluded_patterns)
75
76    for path in paths:
77        if _match_path(path, included, excluded, case_sensitive=case_sensitive):
78            yield path
79
80
81def match_any_paths(
82    paths: list[str],
83    *,
84    included_patterns: list[str] | None = None,
85    excluded_patterns: list[str] | None = None,
86    case_sensitive: bool = True,
87) -> bool:
88    """Matches from a set of paths based on acceptable patterns and
89    ignorable patterns.
90    See ``filter_paths()`` for signature details.
91    """
92    return any(
93        filter_paths(
94            paths,
95            included_patterns=included_patterns,
96            excluded_patterns=excluded_patterns,
97            case_sensitive=case_sensitive,
98        ),
99    )
100