mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-04 07:18:27 +01:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7594d2f2ed | ||
|
|
56e5982951 | ||
|
|
defe4062aa | ||
|
|
f74c1c0e70 | ||
|
|
b5dede14f2 | ||
|
|
8d37f966c3 | ||
|
|
95165cc39a | ||
|
|
ccb72126ea | ||
|
|
d5315c3827 | ||
|
|
8131c32c33 | ||
|
|
0013af99d4 | ||
|
|
06ff81cd7f | ||
|
|
e19b0874c0 | ||
|
|
b290eaea43 | ||
|
|
2a2b543dd6 |
@@ -14,11 +14,11 @@ jobs:
|
||||
name: run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: run benchmarks on base branch
|
||||
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
name: Build & Verify package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@v4.37.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-extended
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v4.37.4
|
||||
|
||||
@@ -23,18 +23,18 @@ jobs:
|
||||
plugin: ["dvc-s3"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: dvc
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
repository: treeverse/${{ matrix.plugin }}
|
||||
ref: main
|
||||
path: ${{ matrix.plugin }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.14'
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||
|
||||
- name: Cache mypy
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
|
||||
@@ -130,11 +130,11 @@ jobs:
|
||||
pyv: "3.14"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
|
||||
--cov --cov-report=xml --cov-report=term --durations-path=./.github/.test_durations
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
check:
|
||||
if: always()
|
||||
needs: [tests]
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2.3.3
|
||||
uses: rtCamp/action-slack-notify@v2.4.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
|
||||
@@ -22,7 +22,7 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.15.8'
|
||||
rev: 'v0.15.20'
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
+48
-37
@@ -60,13 +60,16 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
self.pattern_list: list[PatternInfo] = []
|
||||
self.dirname = dirname
|
||||
self.find_matching_pattern = functools.cache(self._find_matching_pattern)
|
||||
self._ignore_parents = functools.cache(self._ignore_parents)
|
||||
|
||||
regex_pattern_list: list[tuple[str, bool, bool, PatternInfo]] = []
|
||||
for count, pattern_info in enumerate(pattern_infos):
|
||||
for pattern_info in pattern_infos:
|
||||
regex, ignore = GitIgnoreSpecPattern.pattern_to_regex(pattern_info.patterns)
|
||||
if regex is not None and ignore is not None:
|
||||
self.pattern_list.append(pattern_info)
|
||||
regex = regex.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||
# DVC resolves directory matches itself; only the outer rule
|
||||
# group is needed to identify a match in the combined regex.
|
||||
regex = regex.replace(f"(?P<{_DIR_MARK}>", "(?:")
|
||||
regex_pattern_list.append(
|
||||
(regex, ignore, pattern_info.patterns.endswith("/"), pattern_info)
|
||||
)
|
||||
@@ -126,8 +129,20 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
files = [f for f in files if not self.matches(root, f)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||
# Entries are basenames from one directory listing, so normalize the
|
||||
# shared prefix and check its ancestors only once.
|
||||
prefix = self._get_normalize_path(root, "")
|
||||
if prefix is None:
|
||||
return dirs.copy(), files.copy()
|
||||
if prefix and self._ignore_parents(prefix[:-1])[0]:
|
||||
return [], []
|
||||
|
||||
files = [
|
||||
f for f in files if not self.find_matching_pattern(f"{prefix}{f}", False)[0]
|
||||
]
|
||||
dirs = [
|
||||
d for d in dirs if not self.find_matching_pattern(f"{prefix}{d}", True)[0]
|
||||
]
|
||||
|
||||
return dirs, files
|
||||
|
||||
@@ -189,55 +204,51 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
_match: list[PatternInfo] = []
|
||||
if path:
|
||||
result, _match = self._ignore(path, is_dir)
|
||||
return (result, _match) if details else result
|
||||
return (result, _match.copy()) if details else result
|
||||
|
||||
def _find_matching_pattern(
|
||||
self, path: str, is_dir: bool
|
||||
) -> tuple[bool, list[PatternInfo]]:
|
||||
paths = [path]
|
||||
if is_dir and not path.endswith("/"):
|
||||
paths.append(f"{path}/")
|
||||
|
||||
for pattern, ignore, dir_only_pattern, pattern_map in reversed(
|
||||
self.ignore_spec
|
||||
):
|
||||
if dir_only_pattern and not is_dir:
|
||||
continue
|
||||
for p in paths:
|
||||
match = pattern.match(p)
|
||||
if not match:
|
||||
continue
|
||||
if ignore:
|
||||
group_name, _match = next(
|
||||
(
|
||||
(name, _match)
|
||||
for name, _match in match.groupdict().items()
|
||||
if name.startswith("rule_") and _match is not None
|
||||
)
|
||||
)
|
||||
else:
|
||||
# unignored patterns are not combined with `|`,
|
||||
# so there are no groups.
|
||||
group_name = None
|
||||
_regex, pattern_info = pattern_map[group_name]
|
||||
return ignore, [pattern_info]
|
||||
match = pattern.match(path)
|
||||
# Only directory-only patterns need the trailing slash. Probing it
|
||||
# for other patterns would make X/** exclude X itself.
|
||||
if not match and dir_only_pattern and not path.endswith("/"):
|
||||
match = pattern.match(f"{path}/")
|
||||
if not match:
|
||||
continue
|
||||
if ignore:
|
||||
group_name = match.lastgroup
|
||||
else:
|
||||
# unignored patterns are not combined with `|`,
|
||||
# so there are no groups.
|
||||
group_name = None
|
||||
_regex, pattern_info = pattern_map[group_name]
|
||||
return ignore, [pattern_info]
|
||||
return False, []
|
||||
|
||||
def _ignore(self, path: str, is_dir: bool) -> tuple[bool, list[PatternInfo]]:
|
||||
parent, sep, _ = path.rpartition("/")
|
||||
if sep:
|
||||
result = self._ignore_parents(parent)
|
||||
if result[0]:
|
||||
return result
|
||||
return self.find_matching_pattern(path, is_dir)
|
||||
|
||||
def _ignore_parents(self, path: str) -> tuple[bool, list[PatternInfo]]:
|
||||
# Files in the same directory share the ancestor verdict, not just
|
||||
# individual regex matches. Cache it to avoid rebuilding every prefix.
|
||||
parts = path.split("/")
|
||||
result = False
|
||||
matches: list[PatternInfo] = []
|
||||
for i in range(1, len(parts) + 1):
|
||||
rel_path = "/".join(parts[:i])
|
||||
result, _matches = self.find_matching_pattern(
|
||||
rel_path, is_dir or i < len(parts)
|
||||
)
|
||||
if i < len(parts) and not result:
|
||||
continue
|
||||
matches.extend(_matches)
|
||||
result, matches = self.find_matching_pattern(rel_path, True)
|
||||
if result:
|
||||
break
|
||||
return result, matches
|
||||
return result, matches
|
||||
return False, []
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.dirname + ":" + str(self.pattern_list))
|
||||
|
||||
@@ -90,7 +90,7 @@ class BaseStashQueue(ABC):
|
||||
"""Construct a queue.
|
||||
|
||||
Arguments:
|
||||
scm: Git SCM instance for this queue.
|
||||
repo: DVC repo instance for this queue.
|
||||
ref: Git stash ref for this queue.
|
||||
failed_ref: Failed run Git stash ref for this queue.
|
||||
"""
|
||||
|
||||
@@ -84,8 +84,8 @@ def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
|
||||
"""Cleanup after an experiment.
|
||||
|
||||
Arguments:
|
||||
tmp_dir: Temp directory to be removed.
|
||||
entry_dict: Serialized QueueEntry for this experiment.
|
||||
executor: Executor to clean up.
|
||||
infofile: Path to the executor info file.
|
||||
"""
|
||||
executor.cleanup(infofile)
|
||||
|
||||
|
||||
@@ -19,6 +19,36 @@ def test_check_ignore(tmp_dir, dvc, file, ret, output, caplog, capsys):
|
||||
assert "Having any troubles?" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern, exception, ignored",
|
||||
[
|
||||
("volumes/functions/**", "volumes/functions/deno.json", False),
|
||||
("volumes/functions/**", "volumes/functions/deno.json*", False),
|
||||
("volumes/functions/", "volumes/functions/deno.json", True),
|
||||
("ignore.txt", "no-ignore.txt", False),
|
||||
("*ignore.txt", "no-ignore.txt", False),
|
||||
],
|
||||
)
|
||||
def test_check_ignore_reinclude(tmp_dir, dvc, pattern, exception, ignored):
|
||||
tmp_dir.gen(
|
||||
{
|
||||
".dvcignore": f"{pattern}\n!{exception}\n",
|
||||
"volumes": {"functions": {"deno.json": "", "other.py": ""}},
|
||||
"ignore.txt": "",
|
||||
"no-ignore.txt": "",
|
||||
}
|
||||
)
|
||||
file = exception.rstrip("*").replace("/", os.sep)
|
||||
sibling = (
|
||||
os.path.join("volumes", "functions", "other.py")
|
||||
if pattern.startswith("volumes/")
|
||||
else "ignore.txt"
|
||||
)
|
||||
|
||||
assert main(["check-ignore", "-q", file]) == (0 if ignored else 1)
|
||||
assert main(["check-ignore", "-q", sibling]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file,ret,output",
|
||||
[
|
||||
|
||||
@@ -31,6 +31,50 @@ def test_ignore(tmp_dir, dvc, filename):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("suffix, ignored", [("/**", False), ("/", True), ("", True)])
|
||||
@pytest.mark.parametrize("exception", ["deno.json", "deno.json*"])
|
||||
def test_reinclude_directory_contents(tmp_dir, dvc, suffix, ignored, exception):
|
||||
tmp_dir.gen(
|
||||
{
|
||||
".dvcignore": (
|
||||
f"volumes/functions{suffix}\n!volumes/functions/{exception}\n"
|
||||
),
|
||||
"volumes": {"functions": {"deno.json": "", "other.py": ""}},
|
||||
}
|
||||
)
|
||||
dvc._reset()
|
||||
|
||||
directory = tmp_dir / "volumes" / "functions"
|
||||
assert dvc.dvcignore.is_ignored_dir(str(directory)) is ignored
|
||||
assert dvc.dvcignore.is_ignored_file(str(directory / "deno.json")) is ignored
|
||||
assert dvc.dvcignore.is_ignored_file(str(directory / "other.py"))
|
||||
|
||||
files = {
|
||||
os.path.join(root, name)
|
||||
for root, _, names in dvc.dvcignore.walk(dvc.fs, tmp_dir)
|
||||
for name in names
|
||||
}
|
||||
expected = {str(tmp_dir / ".dvcignore")}
|
||||
if not ignored:
|
||||
expected.add(str(directory / "deno.json"))
|
||||
assert files == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pattern", ["ignore.txt", "*ignore.txt"])
|
||||
def test_reinclude_root_file(tmp_dir, dvc, pattern):
|
||||
tmp_dir.gen(
|
||||
{
|
||||
".dvcignore": f"{pattern}\n!no-ignore.txt\n",
|
||||
"ignore.txt": "",
|
||||
"no-ignore.txt": "",
|
||||
}
|
||||
)
|
||||
dvc._reset()
|
||||
|
||||
assert dvc.dvcignore.is_ignored_file(str(tmp_dir / "ignore.txt"))
|
||||
assert not dvc.dvcignore.is_ignored_file(str(tmp_dir / "no-ignore.txt"))
|
||||
|
||||
|
||||
def test_walk(tmp_dir, dvc):
|
||||
tmp_dir.gen(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,64 @@ from os.path import join
|
||||
import pytest
|
||||
|
||||
from dvc.ignore import DvcIgnorePatterns
|
||||
from dvc.pathspec_math import PatternInfo
|
||||
|
||||
|
||||
def test_match_details_combined_patterns():
|
||||
patterns = [
|
||||
PatternInfo("*.tmp", ".dvcignore:1:*.tmp"),
|
||||
PatternInfo("*.log", ".dvcignore:2:*.log"),
|
||||
PatternInfo("build/", ".dvcignore:3:build/"),
|
||||
PatternInfo("cache/", ".dvcignore:4:cache/"),
|
||||
PatternInfo("!keep.log", ".dvcignore:5:!keep.log"),
|
||||
]
|
||||
ignore = DvcIgnorePatterns(patterns, "/repo", "/")
|
||||
for _ in range(2):
|
||||
assert ignore.matches("/repo", "data/error.log", details=True) == (
|
||||
True,
|
||||
[patterns[1]],
|
||||
)
|
||||
assert ignore.matches("/repo", "cache/keep.log", details=True) == (
|
||||
True,
|
||||
[patterns[3]],
|
||||
)
|
||||
assert ignore.matches("/repo", "data/keep.log", details=True) == (
|
||||
False,
|
||||
[patterns[4]],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["data/file.tmp", "build/file.txt"])
|
||||
def test_match_details_mutation(path):
|
||||
ignore = DvcIgnorePatterns(["*.tmp", "build/"], "/repo", "/")
|
||||
result, details = ignore.matches("/repo", path, details=True)
|
||||
expected = details.copy()
|
||||
details.clear()
|
||||
|
||||
assert result
|
||||
assert expected
|
||||
assert ignore.matches("/repo", path, details=True) == (True, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"root, expected_dirs, expected_files",
|
||||
[
|
||||
("/repo", ["child"], ["keep.txt", "logs"]),
|
||||
("/repo/dir", [], ["keep.txt"]),
|
||||
("/repo/ignored/dir", [], []),
|
||||
("/repo2", ["logs", "ignored", "child"], ["keep.txt", "other.tmp", "logs"]),
|
||||
],
|
||||
)
|
||||
def test_filter_directory_listing(root, expected_dirs, expected_files):
|
||||
ignore = DvcIgnorePatterns(
|
||||
["ignored/", "dir/**", "!dir/keep.txt", "logs/", "*.tmp"], "/repo", "/"
|
||||
)
|
||||
dirs = ["logs", "ignored", "child"]
|
||||
files = ["keep.txt", "other.tmp", "logs"]
|
||||
for _ in range(2):
|
||||
assert ignore(root, dirs, files) == (expected_dirs, expected_files)
|
||||
assert dirs == ["logs", "ignored", "child"]
|
||||
assert files == ["keep.txt", "other.tmp", "logs"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user