57 lines
1.9 KiB
Python
57 lines
1.9 KiB
Python
import codecs
|
|
import os
|
|
import re
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
import pathspec
|
|
|
|
from tclint.config import ExcludePattern
|
|
|
|
|
|
def register_codec_warning(name):
|
|
def replace_with_warning_handler(e):
|
|
# TODO: formal warning mechanism, include path
|
|
print("Warning: non-unicode characters in file, replacing with �")
|
|
return codecs.replace_errors(e)
|
|
|
|
codecs.register_error(name, replace_with_warning_handler)
|
|
|
|
|
|
def make_exclude_filter(
|
|
exclude_patterns: list[ExcludePattern],
|
|
) -> Callable[[Path], bool]:
|
|
# Transform patterns into a data structure keyed on root.
|
|
patterns_by_root = defaultdict(list)
|
|
for pattern, root in exclude_patterns:
|
|
# I think this is escaping #, which would otherwise be treated like a comment.
|
|
# Not 100% sure though, I originally wrote this a while ago.
|
|
pattern = re.sub(r"^\s*#", r"\#", pattern)
|
|
patterns_by_root[root.resolve()].append(pattern)
|
|
|
|
compiled_patterns = {}
|
|
for root in patterns_by_root.keys():
|
|
patterns = patterns_by_root[root]
|
|
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
compiled_patterns[root] = spec
|
|
|
|
def is_excluded(path: Path) -> bool:
|
|
abspath = path.resolve()
|
|
|
|
for root, exclude_spec in compiled_patterns.items():
|
|
try:
|
|
relpath = Path(os.path.relpath(abspath, start=root))
|
|
except ValueError:
|
|
# We get here if path and exclude_root are on different drives (on
|
|
# Windows).Things should still behave roughly as expected without
|
|
# using a relative path. See
|
|
# test_cli_utils.py::test_exclude_filter_windows for test cases.
|
|
relpath = abspath
|
|
|
|
if exclude_spec.match_file(relpath):
|
|
return True
|
|
return False
|
|
|
|
return is_excluded
|