a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Shared ignore-file handling for local source filtering."""
|
|
|
|
import pathlib
|
|
from dataclasses import dataclass
|
|
|
|
import pathspec
|
|
|
|
_ALWAYS_EXCLUDE = [
|
|
"__pycache__/",
|
|
".git/",
|
|
".venv/",
|
|
"venv/",
|
|
"node_modules/",
|
|
".tox/",
|
|
".mypy_cache/",
|
|
]
|
|
_ALWAYS_EXCLUDE_NAMES = frozenset(
|
|
pattern.rstrip("/").split("/")[-1] for pattern in _ALWAYS_EXCLUDE
|
|
)
|
|
_GLOB_CHARS = frozenset("*?[")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _NegatedDockerignoreHints:
|
|
exact_dirs: frozenset[pathlib.PurePosixPath] = frozenset()
|
|
wildcard_prefixes: frozenset[pathlib.PurePosixPath] = frozenset()
|
|
recurse_all: bool = False
|
|
|
|
def requires_dir_walk(self, path: pathlib.PurePosixPath) -> bool:
|
|
if self.recurse_all or path in self.exact_dirs:
|
|
return True
|
|
return any(
|
|
path == prefix or path in prefix.parents or prefix in path.parents
|
|
for prefix in self.wildcard_prefixes
|
|
)
|
|
|
|
|
|
def _build_ignore_spec(
|
|
directory: pathlib.Path, *, include_gitignore: bool = True
|
|
) -> pathspec.PathSpec:
|
|
"""Build a PathSpec combining built-in exclusions with ignore files.
|
|
|
|
Always excludes common non-source directories (`_ALWAYS_EXCLUDE`). On top
|
|
of that, patterns from `.dockerignore` are merged in. `.gitignore` patterns
|
|
are optional because some callers need Docker build-context semantics,
|
|
while archive creation wants both files.
|
|
"""
|
|
lines: list[str] = list(_ALWAYS_EXCLUDE)
|
|
ignore_files = [".dockerignore"]
|
|
if include_gitignore:
|
|
ignore_files.append(".gitignore")
|
|
for name in ignore_files:
|
|
ignore_file = directory / name
|
|
if ignore_file.is_file():
|
|
lines.extend(ignore_file.read_text(encoding="utf-8").splitlines())
|
|
return pathspec.PathSpec.from_lines("gitwildmatch", lines)
|
|
|
|
|
|
def _is_always_excluded(path: pathlib.PurePosixPath, *, is_dir: bool) -> bool:
|
|
"""Whether `path` lives inside a built-in excluded directory."""
|
|
parent_parts = path.parts if is_dir else path.parts[:-1]
|
|
return any(part in _ALWAYS_EXCLUDE_NAMES for part in parent_parts)
|
|
|
|
|
|
def _build_dockerignore_negation_hints(
|
|
directory: pathlib.Path,
|
|
) -> _NegatedDockerignoreHints:
|
|
"""Summarize which ignored directories must still be traversed.
|
|
|
|
Most negations only require walking a small, concrete chain of parent
|
|
directories (for example `!assets/keep.txt` requires entering `assets/`).
|
|
Broader glob negations may force a wider walk.
|
|
"""
|
|
ignore_file = directory / ".dockerignore"
|
|
if not ignore_file.is_file():
|
|
return _NegatedDockerignoreHints()
|
|
|
|
exact_dirs: set[pathlib.PurePosixPath] = set()
|
|
wildcard_prefixes: set[pathlib.PurePosixPath] = set()
|
|
recurse_all = False
|
|
|
|
for raw_line in ignore_file.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#") or line.startswith("\\!"):
|
|
continue
|
|
if line.startswith("\\#"):
|
|
line = line[1:]
|
|
if not line.startswith("!"):
|
|
continue
|
|
|
|
pattern = line[1:].lstrip("/")
|
|
while pattern.startswith("./"):
|
|
pattern = pattern[2:]
|
|
pattern = pattern.rstrip("/")
|
|
parts = [part for part in pattern.split("/") if part and part != "."]
|
|
if not parts:
|
|
recurse_all = True
|
|
continue
|
|
|
|
wildcard_index = next(
|
|
(
|
|
idx
|
|
for idx, part in enumerate(parts)
|
|
if any(char in part for char in _GLOB_CHARS)
|
|
),
|
|
None,
|
|
)
|
|
if wildcard_index is not None:
|
|
literal_parts = parts[:wildcard_index]
|
|
if not literal_parts:
|
|
recurse_all = True
|
|
continue
|
|
wildcard_prefixes.add(pathlib.PurePosixPath(*literal_parts))
|
|
continue
|
|
|
|
parent_parts = parts[:-1]
|
|
for idx in range(1, len(parent_parts) + 1):
|
|
exact_dirs.add(pathlib.PurePosixPath(*parent_parts[:idx]))
|
|
|
|
return _NegatedDockerignoreHints(
|
|
exact_dirs=frozenset(exact_dirs),
|
|
wildcard_prefixes=frozenset(wildcard_prefixes),
|
|
recurse_all=recurse_all,
|
|
)
|