1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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
|
import logging
import os
import re
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Sequence, Union
__all__ = 'BaseFilter', 'DefaultFilter', 'PythonFilter'
logger = logging.getLogger('watchfiles.watcher')
if TYPE_CHECKING:
from .main import Change
class BaseFilter:
"""
Useful base class for creating filters. `BaseFilter` should be inherited and configured, rather than used
directly.
The class supports ignoring files in 3 ways:
"""
__slots__ = '_ignore_dirs', '_ignore_entity_regexes', '_ignore_paths'
ignore_dirs: Sequence[str] = ()
"""Full names of directories to ignore, an obvious example would be `.git`."""
ignore_entity_patterns: Sequence[str] = ()
"""
Patterns of files or directories to ignore, these are compiled into regexes.
"entity" here refers to the specific file or directory - basically the result of `path.split(os.sep)[-1]`,
an obvious example would be `r'\\.py[cod]$'`.
"""
ignore_paths: Sequence[Union[str, Path]] = ()
"""
Full paths to ignore, e.g. `/home/users/.cache` or `C:\\Users\\user\\.cache`.
"""
def __init__(self) -> None:
self._ignore_dirs = set(self.ignore_dirs)
self._ignore_entity_regexes = tuple(re.compile(r) for r in self.ignore_entity_patterns)
self._ignore_paths = tuple(map(str, self.ignore_paths))
def __call__(self, change: 'Change', path: str) -> bool:
"""
Instances of `BaseFilter` subclasses can be used as callables.
Args:
change: The type of change that occurred, see [`Change`][watchfiles.Change].
path: the raw path of the file or directory that changed.
Returns:
True if the file should be included in changes, False if it should be ignored.
"""
parts = path.lstrip(os.sep).split(os.sep)
if any(p in self._ignore_dirs for p in parts):
return False
entity_name = parts[-1]
if any(r.search(entity_name) for r in self._ignore_entity_regexes):
return False
elif self._ignore_paths and path.startswith(self._ignore_paths):
return False
else:
return True
def __repr__(self) -> str:
args = ', '.join(f'{k}={getattr(self, k, None)!r}' for k in self.__slots__)
return f'{self.__class__.__name__}({args})'
class DefaultFilter(BaseFilter):
"""
The default filter, which ignores files and directories that you might commonly want to ignore.
"""
ignore_dirs: Sequence[str] = (
'__pycache__',
'.git',
'.hg',
'.svn',
'.tox',
'.venv',
'.idea',
'node_modules',
'.mypy_cache',
'.pytest_cache',
'.hypothesis',
)
"""Directory names to ignore."""
ignore_entity_patterns: Sequence[str] = (
r'\.py[cod]$',
r'\.___jb_...___$',
r'\.sw.$',
'~$',
r'^\.\#',
r'^\.DS_Store$',
r'^flycheck_',
)
"""File/Directory name patterns to ignore."""
def __init__(
self,
*,
ignore_dirs: Optional[Sequence[str]] = None,
ignore_entity_patterns: Optional[Sequence[str]] = None,
ignore_paths: Optional[Sequence[Union[str, Path]]] = None,
) -> None:
"""
Args:
ignore_dirs: if not `None`, overrides the `ignore_dirs` value set on the class.
ignore_entity_patterns: if not `None`, overrides the `ignore_entity_patterns` value set on the class.
ignore_paths: if not `None`, overrides the `ignore_paths` value set on the class.
"""
if ignore_dirs is not None:
self.ignore_dirs = ignore_dirs
if ignore_entity_patterns is not None:
self.ignore_entity_patterns = ignore_entity_patterns
if ignore_paths is not None:
self.ignore_paths = ignore_paths
super().__init__()
class PythonFilter(DefaultFilter):
"""
A filter for Python files, since this class inherits from [`DefaultFilter`][watchfiles.DefaultFilter]
it will ignore files and directories that you might commonly want to ignore as well as filtering out
all changes except in Python files (files with extensions `('.py', '.pyx', '.pyd')`).
"""
def __init__(
self,
*,
ignore_paths: Optional[Sequence[Union[str, Path]]] = None,
extra_extensions: Sequence[str] = (),
) -> None:
"""
Args:
ignore_paths: The paths to ignore, see [`BaseFilter`][watchfiles.BaseFilter].
extra_extensions: extra extensions to ignore.
`ignore_paths` and `extra_extensions` can be passed as arguments partly to support [CLI](../cli.md) usage where
`--ignore-paths` and `--extensions` can be passed as arguments.
"""
self.extensions = ('.py', '.pyx', '.pyd') + tuple(extra_extensions)
super().__init__(ignore_paths=ignore_paths)
def __call__(self, change: 'Change', path: str) -> bool:
return path.endswith(self.extensions) and super().__call__(change, path)
|