mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-03 15:28:29 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec3d48731 | ||
|
|
5417ccd7b6 | ||
|
|
f2b7b00af5 | ||
|
|
4f3fb15f2e | ||
|
|
016f285fe3 | ||
|
|
f6dd641855 | ||
|
|
3f2584d155 | ||
|
|
705b7bbea6 | ||
|
|
26ad01bec0 | ||
|
|
e76aaeac57 | ||
|
|
6f24d04b5e | ||
|
|
2dcaea8119 | ||
|
|
81db9b4572 | ||
|
|
98dde6cd27 | ||
|
|
e42d094310 | ||
|
|
38ee0a01f7 | ||
|
|
d494126cc5 | ||
|
|
fd3b43df05 | ||
|
|
b9bcb45aaa | ||
|
|
7bca71db2e | ||
|
|
52405a0e10 | ||
|
|
c514b0ea31 | ||
|
|
853116a0e3 | ||
|
|
5e4daf6cfb | ||
|
|
991a816995 | ||
|
|
9735b5aaa9 | ||
|
|
fa317d5b1d | ||
|
|
1b0fb6487b | ||
|
|
43e6229308 | ||
|
|
1bef0da4fb | ||
|
|
284d83f390 | ||
|
|
417e6d4b76 | ||
|
|
248ba3b6b6 | ||
|
|
8a91c2b303 | ||
|
|
544dad75e9 | ||
|
|
8f50f2509f | ||
|
|
cdc1857559 | ||
|
|
358dd163da | ||
|
|
aae1a93cb5 | ||
|
|
c9b7218659 | ||
|
|
88c4b9525d | ||
|
|
69d36eeb06 | ||
|
|
3201794553 | ||
|
|
4464f8b4b9 | ||
|
|
efc7e05893 | ||
|
|
a555c3d29a | ||
|
|
676645b1ca | ||
|
|
93c3100bad | ||
|
|
c74156288d |
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: Epic/Story Issue Template
|
||||
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
|
||||
title: 'Epic: New Feature'
|
||||
labels: epic
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
## Summary / Background
|
||||
What do you want to achieve, why? business context
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
What will be impacted and what won't be?
|
||||
What needs implementation and what is invariant?
|
||||
e.g.
|
||||
- user should be able to run workflow X from UI
|
||||
- enable workflow Y, Z from CLI
|
||||
|
||||
## Assumptions
|
||||
Product / UX assumptions as well as technical assumptions / limitations
|
||||
e.g.
|
||||
* Support only Python Runtime
|
||||
* Focus on DVC experiments only
|
||||
* Deployment environments don't change often and can be picked up from shared configuration
|
||||
|
||||
## Open Questions
|
||||
e.g.
|
||||
- How should access control work for shared artifacts (workflow X)
|
||||
- Python runtime assumption - is it really valid? in light of <...>
|
||||
|
||||
## Blockers / Dependencies
|
||||
List issues or other conditions / blockers
|
||||
|
||||
## General Approach
|
||||
Invocation example:
|
||||
```shell
|
||||
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Must have (p1)
|
||||
- [ ] subissue2
|
||||
- [ ] step 2
|
||||
- info
|
||||
- info
|
||||
|
||||
### Optional / followup (p2)
|
||||
- [ ] ⌛ step 3 wip
|
||||
- [ ] step 4
|
||||
|
||||
## Timelines
|
||||
|
||||
Put your estimations here. Update once certainty changes
|
||||
- end of week (Feb 3) for prototype with workflows X, Y
|
||||
- Feb 15 - MVP in production
|
||||
- Low priority followups can be done later
|
||||
@@ -5,6 +5,10 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: "1"
|
||||
PY_COLORS: "1"
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: run benchmarks
|
||||
@@ -12,7 +16,25 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: iterative/dvc-bench@main
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: hynek/setup-cached-uv@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- run: uv pip install '.[tests]' --system
|
||||
- name: run benchmarks on base branch
|
||||
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- run: uv pip install '.[tests]' --system
|
||||
- name: run benchmarks for PR
|
||||
run: >
|
||||
pytest --benchmark-compare --benchmark-compare-fail=min:5%
|
||||
--benchmark-group-by name
|
||||
dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
@@ -27,14 +27,15 @@ jobs:
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- run: python -m pip install --upgrade pip setuptools_scm build twine
|
||||
- uses: hynek/setup-cached-uv@v2
|
||||
- run: uv pip install --upgrade setuptools_scm build twine --system
|
||||
|
||||
- name: Force version for Test PyPI upload
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- run: echo 'PKG = "pip"'>dvc/_build.py
|
||||
- run: python -m build
|
||||
- run: python -m build --installer=uv
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
|
||||
- run: twine check --strict dist/*
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
pyv: ["3.10"]
|
||||
os: [ubuntu-latest]
|
||||
pyv: ["3.12"]
|
||||
plugin: ["dvc-s3"]
|
||||
|
||||
steps:
|
||||
@@ -37,16 +37,17 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
|
||||
- uses: hynek/setup-cached-uv@v2
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
dvc/pyproject.toml
|
||||
${{ matrix.plugin }}/setup.cfg
|
||||
${{ matrix.plugin }}/pyproject.toml
|
||||
|
||||
- name: Install plugin + DVC@PR
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install "./dvc[testing]"
|
||||
pip install -e "./${{ matrix.plugin }}[tests]"
|
||||
uv pip install "./dvc[testing]" --system
|
||||
uv pip install -e "./${{ matrix.plugin }}[tests]" --system
|
||||
|
||||
- name: Run plugin tests
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -16,15 +16,68 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
timeout-minutes: 50
|
||||
lint:
|
||||
timeout-minutes: 10
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.9", "3.10", "3.11", "3.12"]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: hynek/setup-cached-uv@v2
|
||||
with:
|
||||
cache-dependency-path: pyproject.toml
|
||||
- run: uv pip install -e ".[dev]" --system
|
||||
env:
|
||||
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||
|
||||
- name: Cache mypy
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
|
||||
- uses: pre-commit/[email protected]
|
||||
|
||||
tests:
|
||||
timeout-minutes: 20
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
pyv: ["3.9", "3.10", "3.11", "3.12"]
|
||||
include:
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -33,20 +86,21 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
- uses: hynek/setup-cached-uv@v2
|
||||
with:
|
||||
cache-suffix: ${{ matrix.pyv }}
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: install
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
- run: uv pip install -e ".[dev]" pytest-split --system
|
||||
env:
|
||||
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
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@v3
|
||||
with:
|
||||
|
||||
@@ -22,13 +22,13 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.4.4'
|
||||
rev: 'v0.6.2'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
|Banner|
|
||||
🚀 Check out our new product `DataChain <https://github.com/iterative/datachain>`_ (and give it a ⭐!) if you need to version and process a large number of files. Contact us at [email protected] to discuss commercial solutions and support for AI reproducibility and data management scenarios.
|
||||
|
||||
--------------------------
|
||||
|
||||
`Website <https://dvc.org>`_
|
||||
• `Docs <https://dvc.org/doc>`_
|
||||
|
||||
+1
-1
@@ -285,7 +285,7 @@ def _open(
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
fs: Union[FileSystem, DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ class CmdBase(ABC):
|
||||
|
||||
os.chdir(args.cd)
|
||||
|
||||
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config: "Config" = self.repo.config
|
||||
self.repo: Repo = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config: Config = self.repo.config
|
||||
self.args = args
|
||||
|
||||
def do_run(self):
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ def _show_ascii(graph: "DiGraph"):
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
ret.append(draw(pipeline.nodes, pipeline.edges))
|
||||
ret.append(draw(pipeline.nodes, pipeline.edges)) # noqa: PERF401
|
||||
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
@@ -263,7 +263,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
push_parser.add_argument(
|
||||
@@ -325,7 +325,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
fetch_parser.add_argument(
|
||||
"--run-cache",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
|
||||
@@ -42,7 +42,7 @@ class CmdDatasetAdd(CmdBase):
|
||||
if rev := dataset.lock.rev:
|
||||
ver = rev
|
||||
|
||||
ver_part: Optional["Text"] = None
|
||||
ver_part: Optional[Text] = None
|
||||
if ver:
|
||||
ver_part = ui.rich_text.assemble(" @ ", (ver, "repr.number"))
|
||||
text = ui.rich_text.assemble("(", (url, "repr.url"), ver_part or "", ")")
|
||||
|
||||
@@ -84,7 +84,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
experiments_pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
default=False,
|
||||
help="Pull run history for all stages.",
|
||||
)
|
||||
experiments_pull_parser.add_argument(
|
||||
|
||||
@@ -122,7 +122,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
experiments_push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
experiments_push_parser.add_argument(
|
||||
|
||||
@@ -72,7 +72,7 @@ class CmdPlots(CmdBase):
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
def run(self) -> int: # noqa: C901, PLR0911
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
|
||||
@@ -68,7 +68,7 @@ def prepare_stages_data(
|
||||
class CmdStageList(CmdBase):
|
||||
def _get_stages(self) -> Iterable["Stage"]:
|
||||
if self.args.all:
|
||||
stages: list["Stage"] = self.repo.index.stages
|
||||
stages: list[Stage] = self.repo.index.stages
|
||||
logger.trace("%d no. of stages found", len(stages))
|
||||
return stages
|
||||
|
||||
|
||||
@@ -18,11 +18,16 @@ class CmdStudioLogin(CmdConfig):
|
||||
from dvc.utils.studio import STUDIO_URL
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
studio = self.config.get("studio", {})
|
||||
name = self.args.name
|
||||
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
|
||||
hostname = (
|
||||
self.args.hostname
|
||||
or os.environ.get(DVC_STUDIO_URL)
|
||||
or studio.get("url")
|
||||
or STUDIO_URL
|
||||
)
|
||||
scopes = self.args.scopes
|
||||
|
||||
studio = self.config.get("studio", {})
|
||||
if studio.get("url", hostname) == hostname and "token" in studio:
|
||||
raise DvcException(
|
||||
"Token already exists. "
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
if key not in keys:
|
||||
self.add_column(key)
|
||||
|
||||
row: list["CellT"] = [
|
||||
row: list[CellT] = [
|
||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||
]
|
||||
self.append(row)
|
||||
|
||||
+3
-3
@@ -44,10 +44,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
||||
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
popen = subprocess.Popen(
|
||||
popen = subprocess.Popen( # noqa: S603
|
||||
args,
|
||||
close_fds=True,
|
||||
shell=False, # noqa: S603
|
||||
shell=False,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
**kwargs,
|
||||
@@ -149,7 +149,7 @@ def _spawn(
|
||||
env: Optional[Mapping[str, str]] = None,
|
||||
output_file: Optional[str] = None,
|
||||
) -> int:
|
||||
file: "AbstractContextManager[Any]" = nullcontext()
|
||||
file: AbstractContextManager[Any] = nullcontext()
|
||||
kwargs = {}
|
||||
if output_file:
|
||||
file = open(output_file, "ab") # noqa: SIM115
|
||||
|
||||
@@ -45,7 +45,7 @@ class Dependency(Output):
|
||||
self.fs_path = self.fs.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
return fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
|
||||
@@ -37,13 +37,17 @@ class AbstractDependency(Dependency):
|
||||
"""Dependency without workspace/fs/fs_path"""
|
||||
|
||||
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
|
||||
self.repo: "Repo" = stage.repo
|
||||
self.repo: Repo = stage.repo
|
||||
self.stage = stage
|
||||
self.fs = None
|
||||
self.fs_path = None
|
||||
self.def_path = None # type: ignore[assignment]
|
||||
self.info = info or {}
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
return False
|
||||
|
||||
|
||||
class DbDependency(AbstractDependency):
|
||||
PARAM_CONNECTION = "connection"
|
||||
@@ -70,8 +74,8 @@ class DbDependency(AbstractDependency):
|
||||
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}:{}".format(
|
||||
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, "".join(f"{k}={v}" for k, v in self.info.items())
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -9,7 +9,9 @@ from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import DVCFileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.stage import Stage
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
@@ -91,6 +93,27 @@ class RepoDependency(Dependency):
|
||||
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
|
||||
}
|
||||
|
||||
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
files = super().download(to=to, jobs=jobs)
|
||||
if not isinstance(to.fs, LocalFileSystem):
|
||||
return
|
||||
|
||||
hashes: list[tuple[str, HashInfo, dict[str, Any]]] = []
|
||||
for src_path, dest_path, maybe_info in files:
|
||||
try:
|
||||
info = maybe_info or self.fs.info(src_path)
|
||||
hash_info = info["dvc_info"]["entry"].hash_info
|
||||
dest_info = to.fs.info(dest_path)
|
||||
except (KeyError, AttributeError):
|
||||
# If no hash info found, just keep going and output will be hashed later
|
||||
continue
|
||||
if hash_info:
|
||||
hashes.append((dest_path, hash_info, dest_info))
|
||||
cache = to.cache if to.use_cache else to.local_cache
|
||||
cache.state.save_many(hashes, to.fs)
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
|
||||
+22
-18
@@ -1,4 +1,5 @@
|
||||
import glob
|
||||
from itertools import repeat
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -47,28 +48,12 @@ known_implementations.update(
|
||||
|
||||
def download(
|
||||
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
|
||||
) -> int:
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
from dvc.scm import lfs_prefetch
|
||||
|
||||
from .callbacks import TqdmCallback
|
||||
|
||||
with TqdmCallback(desc=f"Downloading {fs.name(fs_path)}", unit="files") as cb:
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
from_infos = [
|
||||
path for path in fs.find(fs_path) if not path.endswith(fs.flavour.sep)
|
||||
]
|
||||
if not from_infos:
|
||||
localfs.makedirs(to, exist_ok=True)
|
||||
return 0
|
||||
to_infos = [
|
||||
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
||||
]
|
||||
else:
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
if isinstance(fs, DVCFileSystem):
|
||||
lfs_prefetch(
|
||||
fs,
|
||||
@@ -78,10 +63,29 @@ def download(
|
||||
else glob.escape(fs_path)
|
||||
],
|
||||
)
|
||||
if not glob.has_magic(fs_path):
|
||||
return fs._get(fs_path, to, batch_size=jobs, callback=cb)
|
||||
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
from_infos = [
|
||||
path for path in fs.find(fs_path) if not path.endswith(fs.flavour.sep)
|
||||
]
|
||||
if not from_infos:
|
||||
localfs.makedirs(to, exist_ok=True)
|
||||
return []
|
||||
to_infos = [
|
||||
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
||||
]
|
||||
else:
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
|
||||
return len(to_infos)
|
||||
return list(zip(from_infos, to_infos, repeat(None)))
|
||||
|
||||
|
||||
def parse_external_url(url, fs_config=None, config=None):
|
||||
|
||||
+169
-6
@@ -4,22 +4,28 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from collections import deque
|
||||
from contextlib import ExitStack, suppress
|
||||
from collections import defaultdict, deque
|
||||
from contextlib import ExitStack, nullcontext, suppress
|
||||
from glob import has_magic
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from fsspec.spec import DEFAULT_CALLBACK, AbstractFileSystem
|
||||
from funcy import wrap_with
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc.utils.threadpool import ThreadPoolExecutor
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem
|
||||
|
||||
from .data import DataFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import DictStrAny, StrPath
|
||||
|
||||
from .callbacks import Callback
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[..., "Repo"], type["Repo"]]
|
||||
@@ -429,7 +435,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info( # noqa: C901, PLR0912
|
||||
def _info( # noqa: C901
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
@@ -474,9 +480,127 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def get(
|
||||
self,
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=False,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
maxdepth=None,
|
||||
batch_size=None,
|
||||
**kwargs,
|
||||
):
|
||||
self._get(
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=recursive,
|
||||
callback=callback,
|
||||
maxdepth=maxdepth,
|
||||
batch_size=batch_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _get( # noqa: C901, PLR0912
|
||||
self,
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=False,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
maxdepth=None,
|
||||
batch_size=None,
|
||||
**kwargs,
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
if (
|
||||
isinstance(rpath, list)
|
||||
or isinstance(lpath, list)
|
||||
or has_magic(rpath)
|
||||
or not self.exists(rpath)
|
||||
or not recursive
|
||||
):
|
||||
super().get(
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=recursive,
|
||||
callback=callback,
|
||||
maxdepth=maxdepth,
|
||||
**kwargs,
|
||||
)
|
||||
return []
|
||||
|
||||
if os.path.isdir(lpath) or lpath.endswith(os.path.sep):
|
||||
lpath = self.join(lpath, os.path.basename(rpath))
|
||||
|
||||
if self.isfile(rpath):
|
||||
with callback.branched(rpath, lpath) as child:
|
||||
self.get_file(rpath, lpath, callback=child, **kwargs)
|
||||
return [(rpath, lpath, None)]
|
||||
|
||||
result: list[tuple[str, str, Optional[dict]]] = []
|
||||
_dirs: list[str] = []
|
||||
_files: dict[FileSystem, list[tuple[str, str, Optional[dict]]]]
|
||||
_files = defaultdict(list)
|
||||
|
||||
for root, dirs, files in self.walk(rpath, maxdepth=maxdepth, detail=True):
|
||||
if files:
|
||||
callback.set_size((callback.size or 0) + len(files))
|
||||
|
||||
parts = self.relparts(root, rpath)
|
||||
if parts in ((os.curdir,), ("",)):
|
||||
parts = ()
|
||||
dest_root = os.path.join(lpath, *parts)
|
||||
if not maxdepth or len(parts) < maxdepth - 1:
|
||||
_dirs.extend(f"{dest_root}{os.path.sep}{d}" for d in dirs)
|
||||
|
||||
key = self._get_key_from_relative(root)
|
||||
_, dvc_fs, _ = self._get_subrepo_info(key)
|
||||
|
||||
for name, info in files.items():
|
||||
dvc_info = info.get("dvc_info")
|
||||
fs_info = info.get("fs_info")
|
||||
if dvc_fs and dvc_info and not fs_info:
|
||||
fs = dvc_fs
|
||||
fs_path = dvc_info["name"]
|
||||
else:
|
||||
fs = self.repo.fs
|
||||
fs_path = fs_info["name"]
|
||||
|
||||
src_path = f"{root}{self.sep}{name}"
|
||||
dest_path = f"{dest_root}{os.path.sep}{name}"
|
||||
_files[fs].append((fs_path, dest_path, dvc_info))
|
||||
result.append((src_path, dest_path, info))
|
||||
|
||||
os.makedirs(lpath, exist_ok=True)
|
||||
for d in _dirs:
|
||||
os.mkdir(d)
|
||||
|
||||
def get_file(arg: tuple[FileSystem, tuple[str, str, Optional[dict]]]):
|
||||
fs, (src, dest, info) = arg
|
||||
kw = kwargs
|
||||
if isinstance(fs, DataFileSystem):
|
||||
kw = kw | {"info": info}
|
||||
return fs.get_file(src, dest, callback=callback, **kw)
|
||||
|
||||
if batch_size == 1:
|
||||
ctx: AbstractContextManager = nullcontext()
|
||||
map_fn: Callable = map
|
||||
else:
|
||||
ctx = ThreadPoolExecutor(max_workers=batch_size)
|
||||
map_fn = ctx.imap_unordered
|
||||
|
||||
with ctx:
|
||||
it = ((fs, f) for fs, files in _files.items() for f in files)
|
||||
deque(map_fn(get_file, it), maxlen=0)
|
||||
return result
|
||||
|
||||
def get_file(self, rpath, lpath, **kwargs):
|
||||
dvc_info = kwargs.pop("info", {}).pop("dvc_info", None)
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
dirpath = os.path.dirname(lpath)
|
||||
if dirpath:
|
||||
# makedirs raises error if the string is empty
|
||||
os.makedirs(dirpath, exist_ok=True)
|
||||
|
||||
try:
|
||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||
except FileNotFoundError:
|
||||
@@ -485,7 +609,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
return dvc_fs.get_file(dvc_path, lpath, info=dvc_info, **kwargs)
|
||||
|
||||
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
|
||||
if maxdepth is not None:
|
||||
@@ -553,6 +677,45 @@ class DVCFileSystem(FileSystem):
|
||||
def getcwd(self):
|
||||
return self.fs.getcwd()
|
||||
|
||||
def _get(
|
||||
self,
|
||||
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
recursive: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
# FileSystem.get is non-recursive by default if arguments are lists
|
||||
# otherwise, it's recursive.
|
||||
recursive = not (isinstance(from_info, list) and isinstance(to_info, list))
|
||||
return self.fs._get(
|
||||
from_info,
|
||||
to_info,
|
||||
callback=callback,
|
||||
recursive=recursive,
|
||||
batch_size=batch_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
recursive: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._get(
|
||||
from_info,
|
||||
to_info,
|
||||
callback=callback,
|
||||
batch_size=batch_size,
|
||||
recursive=recursive,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def fsid(self) -> str:
|
||||
return self.fs.fsid
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ def _get_supported_remotes():
|
||||
if not fs_cls.get_missing_deps():
|
||||
dependencies = []
|
||||
for requirement in fs_cls.REQUIRES:
|
||||
dependencies.append(
|
||||
dependencies.append( # noqa: PERF401
|
||||
f"{requirement} = {importlib_metadata.version(requirement)}"
|
||||
)
|
||||
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import hashlib
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Union
|
||||
|
||||
import flufl.lock
|
||||
import zc.lockfile
|
||||
@@ -174,9 +175,9 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
self._owned = True
|
||||
self._retry_errnos = []
|
||||
|
||||
def lock(self):
|
||||
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
|
||||
try:
|
||||
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
|
||||
super().lock(timeout or timedelta(seconds=DEFAULT_TIMEOUT))
|
||||
except flufl.lock.TimeOutError:
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
||||
|
||||
|
||||
+13
-11
@@ -390,7 +390,7 @@ class Output:
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
self.obj: Optional[HashFile] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
@@ -542,7 +542,9 @@ class Output:
|
||||
desc=f"Collecting files and computing hashes in {self}",
|
||||
disable=no_progress_bar,
|
||||
) as pb:
|
||||
return build(*args, callback=pb.as_callback(), **kwargs)
|
||||
kwargs["callback"] = pb.as_callback()
|
||||
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||
return build(*args, **kwargs)
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
@@ -730,6 +732,7 @@ class Output:
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||
try:
|
||||
return checkout(*args, **kwargs)
|
||||
except PromptError as exc:
|
||||
@@ -749,11 +752,8 @@ class Output:
|
||||
granular = (
|
||||
self.is_dir_checksum and filter_info and filter_info != self.fs_path
|
||||
)
|
||||
# NOTE: trying to use hardlink during transfer only if we will be
|
||||
# relinking later
|
||||
hardlink = relink
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
||||
obj = self._commit_granular_dir(filter_info, hardlink=False)
|
||||
else:
|
||||
staging, _, obj = self._build(
|
||||
self.cache,
|
||||
@@ -771,7 +771,7 @@ class Output:
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
hardlink=hardlink,
|
||||
hardlink=False,
|
||||
callback=cb,
|
||||
)
|
||||
if relink:
|
||||
@@ -786,6 +786,7 @@ class Output:
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=cb,
|
||||
old=obj,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
@@ -902,7 +903,7 @@ class Output:
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
obj: Optional[HashFile] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.files:
|
||||
@@ -1158,7 +1159,7 @@ class Output:
|
||||
logger.warning(msg)
|
||||
return {}
|
||||
|
||||
obj: Optional["HashFile"]
|
||||
obj: Optional[HashFile]
|
||||
if self.is_dir_checksum:
|
||||
obj = self._collect_used_dir_cache(**kwargs)
|
||||
else:
|
||||
@@ -1353,7 +1354,7 @@ class Output:
|
||||
cache = self.cache if self.use_cache else self.local_cache
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
new: HashFile
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = self._build(
|
||||
@@ -1403,7 +1404,7 @@ class Output:
|
||||
staging,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
hardlink=relink,
|
||||
hardlink=False,
|
||||
shallow=False,
|
||||
callback=cb,
|
||||
)
|
||||
@@ -1421,6 +1422,7 @@ class Output:
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=callback,
|
||||
old=obj,
|
||||
)
|
||||
self.set_exec()
|
||||
return obj
|
||||
|
||||
@@ -563,10 +563,10 @@ class MatrixDefinition:
|
||||
iterable = self.resolved_iterable
|
||||
assert isinstance(iterable, Mapping)
|
||||
|
||||
ret: dict[str, "DictStrAny"] = {}
|
||||
ret: dict[str, DictStrAny] = {}
|
||||
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
||||
for combination in product(*matrix.values()):
|
||||
d: "DictStrAny" = {}
|
||||
d: DictStrAny = {}
|
||||
fragments: list[str] = []
|
||||
for k, (i, v) in zip(matrix.keys(), combination):
|
||||
d[k] = v
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class Tqdm(tqdm):
|
||||
"miniters": 1,
|
||||
}
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
iterable=None,
|
||||
disable=None,
|
||||
|
||||
@@ -23,7 +23,7 @@ def _lists(blob: Union[dict, list]) -> Iterable[list]:
|
||||
if isinstance(blob, list):
|
||||
yield blob
|
||||
else:
|
||||
for _, value in blob.items():
|
||||
for value in blob.values():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
|
||||
+11
-6
@@ -147,7 +147,7 @@ class Repo:
|
||||
):
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.repo.datasets import Datasets
|
||||
@@ -161,7 +161,7 @@ class Repo:
|
||||
|
||||
self.url = url
|
||||
self._fs_conf = {"repo_factory": repo_factory}
|
||||
self._fs = fs or localfs
|
||||
self._fs = fs or LocalFileSystem()
|
||||
self._scm = scm
|
||||
self._config = config
|
||||
self._remote = remote
|
||||
@@ -184,12 +184,12 @@ class Repo:
|
||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||
self.subrepos = subrepos
|
||||
|
||||
self.cloud: "DataCloud" = DataCloud(self)
|
||||
self.stage: "StageLoad" = StageLoad(self)
|
||||
self.cloud: DataCloud = DataCloud(self)
|
||||
self.stage: StageLoad = StageLoad(self)
|
||||
|
||||
self.lock: "LockBase"
|
||||
self.lock: LockBase
|
||||
self.cache: CacheManager
|
||||
self.state: "StateBase"
|
||||
self.state: StateBase
|
||||
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
@@ -206,6 +206,11 @@ class Repo:
|
||||
friendly=True,
|
||||
)
|
||||
os.makedirs(self.site_cache_dir, exist_ok=True)
|
||||
if not fs and (
|
||||
checksum_jobs := self.config["core"].get("checksum_jobs")
|
||||
):
|
||||
self.fs.hash_jobs = checksum_jobs
|
||||
|
||||
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
||||
else:
|
||||
self.lock = LockNoop()
|
||||
|
||||
@@ -137,14 +137,12 @@ class Artifacts:
|
||||
|
||||
assert not (version and stage)
|
||||
name = _reformat_name(name)
|
||||
tags: list["GitTag"] = find(
|
||||
name=name, version=version, stage=stage, scm=self.scm
|
||||
)
|
||||
tags: list[GitTag] = find(name=name, version=version, stage=stage, scm=self.scm)
|
||||
if not tags:
|
||||
raise ArtifactNotFoundError(name, version=version, stage=stage)
|
||||
if version or stage:
|
||||
return tags[-1].target
|
||||
gto_tags: list["GTOTag"] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
gto_tags: list[GTOTag] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
return gto_tags[0].tag.target
|
||||
|
||||
@classmethod
|
||||
@@ -222,7 +220,7 @@ class Artifacts:
|
||||
|
||||
out = resolve_output(path, out, force=force)
|
||||
fs = self.repo.dvcfs
|
||||
count = fs_download(fs, path, os.path.abspath(out), jobs=jobs)
|
||||
count = len(fs_download(fs, path, os.path.abspath(out), jobs=jobs))
|
||||
return count, out
|
||||
|
||||
@staticmethod
|
||||
@@ -285,7 +283,7 @@ class Artifacts:
|
||||
return len(to_infos), relpath(localfs.commonpath(to_infos))
|
||||
|
||||
@classmethod
|
||||
def get( # noqa: PLR0913
|
||||
def get(
|
||||
cls,
|
||||
url: str,
|
||||
name: str,
|
||||
|
||||
@@ -125,7 +125,7 @@ def checkout( # noqa: C901
|
||||
(StageFileDoesNotExistError, StageFileBadNameError, NoOutputOrStageError),
|
||||
):
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
raise
|
||||
raise # noqa: PLE0704
|
||||
|
||||
view = self.index.targets_view(
|
||||
targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
|
||||
@@ -76,7 +76,7 @@ class SerDe:
|
||||
return asdict(self, filter=filter_defaults, value_serializer=value_serializer)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type["Self"], d: dict[str, Any]) -> "Self":
|
||||
def from_dict(cls, d: dict[str, Any]) -> "Self":
|
||||
_fields = fields(cast("type[AttrsInstance]", cls))
|
||||
kwargs = {f.name: d[f.name] for f in _fields if f.name in d}
|
||||
return cls(**kwargs)
|
||||
@@ -242,7 +242,7 @@ class DatasetNotFoundError(DvcException, KeyError):
|
||||
|
||||
class Datasets(Mapping[str, Dataset]):
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo: "Repo" = repo
|
||||
self.repo: Repo = repo
|
||||
|
||||
def __repr__(self):
|
||||
return repr(dict(self))
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ def _diff(old, new, data_keys, with_missing=False):
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
|
||||
from dvc_data.index.diff import diff as idiff
|
||||
|
||||
ret: "dict[str, list[dict]]" = {
|
||||
ret: dict[str, list[dict]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
|
||||
@@ -119,7 +119,7 @@ class Experiments:
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
exp_queue: "BaseStashQueue" = (
|
||||
exp_queue: BaseStashQueue = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
@@ -249,7 +249,7 @@ class Experiments:
|
||||
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
|
||||
|
||||
results: dict[str, str] = {}
|
||||
for _, exp_result in exec_results.items():
|
||||
for exp_result in exec_results.values():
|
||||
results.update(exp_result)
|
||||
return results
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import RevError, resolve_rev
|
||||
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
is_stash: bool = False
|
||||
|
||||
@@ -57,7 +57,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
|
||||
|
||||
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
with exps.apply_stash.preserve_workspace(rev, name=name):
|
||||
with repo.scm.detach_head(rev, force=True):
|
||||
|
||||
@@ -69,11 +69,11 @@ def collect_rev(
|
||||
|
||||
def _collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
revision: str,
|
||||
param_deps: bool = False,
|
||||
**kwargs,
|
||||
) -> SerializableExp:
|
||||
with repo.switch(rev) as rev:
|
||||
with repo.switch(revision) as rev:
|
||||
if rev == "workspace":
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
@@ -174,7 +174,7 @@ def collect_active(
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
result: dict[str, list["ExpRange"]] = {}
|
||||
result: dict[str, list[ExpRange]] = {}
|
||||
exps = repo.experiments
|
||||
for queue in (exps.workspace_queue, exps.tempdir_queue, exps.celery_queue):
|
||||
for baseline, active_exps in queue.collect_active_data(
|
||||
@@ -220,7 +220,7 @@ def collect_successful(
|
||||
Returns:
|
||||
Dict mapping baseline revision to successful experiments.
|
||||
"""
|
||||
result: dict[str, list["ExpRange"]] = {}
|
||||
result: dict[str, list[ExpRange]] = {}
|
||||
for baseline_rev in baseline_revs:
|
||||
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
|
||||
return result
|
||||
@@ -304,7 +304,7 @@ def collect(
|
||||
)
|
||||
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result: list["ExpState"] = [workspace_data]
|
||||
result: list[ExpState] = [workspace_data]
|
||||
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
|
||||
active = collect_active(repo, baseline_revs, **kwargs)
|
||||
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
|
||||
|
||||
@@ -199,7 +199,7 @@ class BaseExecutor(ABC):
|
||||
@classmethod
|
||||
def from_info(cls, info: "ExecutorInfo") -> "Self":
|
||||
if info.result_hash:
|
||||
result: Optional["ExecutorResult"] = ExecutorResult(
|
||||
result: Optional[ExecutorResult] = ExecutorResult(
|
||||
info.result_hash,
|
||||
(ExpRefInfo.from_ref(info.result_ref) if info.result_ref else None),
|
||||
info.result_force,
|
||||
@@ -288,7 +288,7 @@ class BaseExecutor(ABC):
|
||||
stages = []
|
||||
if targets:
|
||||
for target in targets:
|
||||
stages.append(
|
||||
stages.append( # noqa: PERF401
|
||||
dvc.commit(
|
||||
target, recursive=recursive, force=True, relink=False
|
||||
)
|
||||
@@ -470,7 +470,7 @@ class BaseExecutor(ABC):
|
||||
cls._set_log_level(log_level)
|
||||
|
||||
exp_hash: Optional[str] = None
|
||||
exp_ref: Optional["ExpRefInfo"] = None
|
||||
exp_ref: Optional[ExpRefInfo] = None
|
||||
repro_force: bool = False
|
||||
|
||||
if info.name:
|
||||
@@ -559,7 +559,7 @@ class BaseExecutor(ABC):
|
||||
)
|
||||
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref: Optional["ExpRefInfo"] = ExpRefInfo.from_ref(ref) if ref else None
|
||||
exp_ref: Optional[ExpRefInfo] = ExpRefInfo.from_ref(ref) if ref else None
|
||||
if cls.WARN_UNTRACKED:
|
||||
untracked = dvc.scm.untracked_files()
|
||||
if untracked:
|
||||
|
||||
@@ -201,7 +201,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
@classmethod
|
||||
def from_stash_entry(cls, repo: "Repo", entry: "ExpStashEntry", **kwargs):
|
||||
root_dir = repo.scm.root_dir
|
||||
executor: "WorkspaceExecutor" = cls._from_stash_entry(
|
||||
executor: WorkspaceExecutor = cls._from_stash_entry(
|
||||
repo, entry, root_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init workspace executor in '%s'", root_dir)
|
||||
|
||||
@@ -30,7 +30,7 @@ def pull( # noqa: C901
|
||||
pull_cache: bool = False,
|
||||
**kwargs,
|
||||
) -> Iterable[str]:
|
||||
exp_ref_set: set["ExpRefInfo"] = set()
|
||||
exp_ref_set: set[ExpRefInfo] = set()
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm, git_remote))
|
||||
elif exp_names:
|
||||
@@ -55,7 +55,7 @@ def pull( # noqa: C901
|
||||
rev_dict = iter_revs(repo.scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
for ref_info_list in ref_info_dict.values():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
|
||||
pull_result = _pull(repo, git_remote, exp_ref_set, force)
|
||||
@@ -97,7 +97,7 @@ def _pull(
|
||||
def group_result(refspec):
|
||||
return results[str(refspec)]
|
||||
|
||||
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
|
||||
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
|
||||
|
||||
return pull_result
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ def exp_refs_from_rev(scm: "Git", rev: list[str], num: int = 1) -> set["ExpRefIn
|
||||
rev_dict = iter_revs(scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(scm, rev_set)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
for ref_info_list in ref_info_dict.values():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
return exp_ref_set
|
||||
|
||||
@@ -96,7 +96,7 @@ def push(
|
||||
push_cache: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
exp_ref_set: set["ExpRefInfo"] = set()
|
||||
exp_ref_set: set[ExpRefInfo] = set()
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
@@ -162,7 +162,7 @@ def _push(
|
||||
def group_result(refspec):
|
||||
return results[str(refspec)]
|
||||
|
||||
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
|
||||
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
|
||||
|
||||
return pull_result
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
def _get_running_task_ids(self) -> set[str]:
|
||||
running_task_ids: set[str] = set()
|
||||
active_workers = self.worker_status()
|
||||
for _, tasks in active_workers.items():
|
||||
for tasks in active_workers.values():
|
||||
task = first(tasks)
|
||||
if task:
|
||||
running_task_ids.add(task["id"])
|
||||
@@ -470,11 +470,11 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
exp_names = [exp_names]
|
||||
results: dict[str, ExpRefAndQueueEntry] = {}
|
||||
|
||||
exp_ref_match: dict[str, Optional["ExpRefInfo"]] = resolve_name(
|
||||
exp_ref_match: dict[str, Optional[ExpRefInfo]] = resolve_name(
|
||||
self.scm, exp_names, git_remote
|
||||
)
|
||||
if not git_remote:
|
||||
queue_entry_match: dict[str, Optional["QueueEntry"]] = (
|
||||
queue_entry_match: dict[str, Optional[QueueEntry]] = (
|
||||
self.match_queue_entry_by_name(
|
||||
exp_names, self.iter_queued(), self.iter_done()
|
||||
)
|
||||
@@ -613,7 +613,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
"""Map exp refs to any available successful executors."""
|
||||
from dvc.repo.experiments.serialize import ExpExecutor, LocalExpExecutor
|
||||
|
||||
result: dict[str, "ExpExecutor"] = {}
|
||||
result: dict[str, ExpExecutor] = {}
|
||||
for entry, exec_result in self.iter_success():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
|
||||
@@ -21,11 +21,11 @@ def remove_tasks( # noqa: C901, PLR0912
|
||||
"""
|
||||
from celery.result import AsyncResult
|
||||
|
||||
stash_revs: dict[str, "ExpStashEntry"] = {}
|
||||
failed_stash_revs: list["ExpStashEntry"] = []
|
||||
done_entry_set: set["QueueEntry"] = set()
|
||||
stash_revs: dict[str, ExpStashEntry] = {}
|
||||
failed_stash_revs: list[ExpStashEntry] = []
|
||||
done_entry_set: set[QueueEntry] = set()
|
||||
stash_rev_all = celery_queue.stash.stash_revs
|
||||
failed_rev_all: dict[str, "ExpStashEntry"] = {}
|
||||
failed_rev_all: dict[str, ExpStashEntry] = {}
|
||||
if celery_queue.failed_stash:
|
||||
failed_rev_all = celery_queue.failed_stash.stash_revs
|
||||
for entry in queue_entries:
|
||||
@@ -90,17 +90,17 @@ def celery_clear(
|
||||
"""
|
||||
|
||||
removed: list[str] = []
|
||||
entry_list: list["QueueEntry"] = []
|
||||
entry_list: list[QueueEntry] = []
|
||||
if queued:
|
||||
queue_entries: list["QueueEntry"] = list(self.iter_queued())
|
||||
queue_entries: list[QueueEntry] = list(self.iter_queued())
|
||||
entry_list.extend(queue_entries)
|
||||
removed.extend(_get_names(queue_entries))
|
||||
if failed:
|
||||
failed_tasks: list["QueueDoneResult"] = list(self.iter_failed())
|
||||
failed_tasks: list[QueueDoneResult] = list(self.iter_failed())
|
||||
entry_list.extend([result.entry for result in failed_tasks])
|
||||
removed.extend(_get_names(failed_tasks))
|
||||
if success:
|
||||
success_tasks: list["QueueDoneResult"] = list(self.iter_success())
|
||||
success_tasks: list[QueueDoneResult] = list(self.iter_success())
|
||||
entry_list.extend([result.entry for result in success_tasks])
|
||||
removed.extend(_get_names(success_tasks))
|
||||
|
||||
@@ -125,7 +125,7 @@ def celery_remove(self: "LocalCeleryQueue", revs: Collection[str]) -> list[str]:
|
||||
|
||||
remained: list[str] = []
|
||||
removed: list[str] = []
|
||||
entry_to_remove: list["QueueEntry"] = []
|
||||
entry_to_remove: list[QueueEntry] = []
|
||||
for name, entry in match_results.items():
|
||||
if entry:
|
||||
entry_to_remove.append(entry)
|
||||
|
||||
@@ -25,7 +25,7 @@ def get_remote_executor_refs(scm: "Git", remote_url: str) -> list[str]:
|
||||
refs = []
|
||||
for ref in iter_remote_refs(scm, remote_url, base=EXPS_NAMESPACE):
|
||||
if not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
|
||||
refs.append(ref)
|
||||
refs.append(ref) # noqa: PERF401
|
||||
return refs
|
||||
|
||||
|
||||
|
||||
@@ -34,17 +34,17 @@ def remove( # noqa: C901, PLR0912
|
||||
removed: list[str] = []
|
||||
if not any([exp_names, queue, all_commits, rev]):
|
||||
return removed
|
||||
celery_queue: "LocalCeleryQueue" = repo.experiments.celery_queue
|
||||
celery_queue: LocalCeleryQueue = repo.experiments.celery_queue
|
||||
|
||||
if queue:
|
||||
removed.extend(celery_queue.clear(queued=True))
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
exp_ref_list: list["ExpRefInfo"] = []
|
||||
queue_entry_list: list["QueueEntry"] = []
|
||||
exp_ref_list: list[ExpRefInfo] = []
|
||||
queue_entry_list: list[QueueEntry] = []
|
||||
if exp_names:
|
||||
results: dict[str, "ExpRefAndQueueEntry"] = (
|
||||
results: dict[str, ExpRefAndQueueEntry] = (
|
||||
celery_queue.get_ref_and_entry_by_names(exp_names, git_remote)
|
||||
)
|
||||
remained: list[str] = []
|
||||
@@ -94,11 +94,11 @@ def _resolve_exp_by_baseline(
|
||||
) -> dict[str, "ExpRefInfo"]:
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
commit_ref_dict: dict[str, "ExpRefInfo"] = {}
|
||||
commit_ref_dict: dict[str, ExpRefInfo] = {}
|
||||
rev_dict = iter_revs(repo.scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
for ref_info_list in ref_info_dict.values():
|
||||
for ref_info in ref_info_list:
|
||||
commit_ref_dict[ref_info.name] = ref_info
|
||||
return commit_ref_dict
|
||||
|
||||
@@ -50,7 +50,12 @@ class SerializableExp:
|
||||
Params, metrics, deps, outs are filled via repo fs/index, all other fields
|
||||
should be passed via kwargs.
|
||||
"""
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
from dvc.dependency import (
|
||||
DatasetDependency,
|
||||
DbDependency,
|
||||
ParamsDependency,
|
||||
RepoDependency,
|
||||
)
|
||||
|
||||
rev = rev or repo.get_rev()
|
||||
assert rev
|
||||
@@ -68,7 +73,10 @@ class SerializableExp:
|
||||
nfiles=dep.meta.nfiles if dep.meta else None,
|
||||
)
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
if not isinstance(
|
||||
dep,
|
||||
(ParamsDependency, RepoDependency, DatasetDependency, DbDependency),
|
||||
)
|
||||
},
|
||||
outs={
|
||||
relpath(out.fs_path, repo.root_dir): ExpOut(
|
||||
|
||||
@@ -117,7 +117,7 @@ def _build_rows(
|
||||
**kwargs,
|
||||
) -> Iterator[tuple["CellT", ...]]:
|
||||
for baseline in baseline_states:
|
||||
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
|
||||
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
|
||||
row["Experiment"] = ""
|
||||
if baseline.name:
|
||||
row["rev"] = baseline.name
|
||||
@@ -232,7 +232,7 @@ def _exp_range_rows(
|
||||
logger.debug("Returning tip commit for legacy checkpoint exp")
|
||||
exp = first(exp_range.revs)
|
||||
if exp:
|
||||
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
|
||||
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
|
||||
row["Experiment"] = exp.name or ""
|
||||
row["rev"] = exp.rev[:7] if Git.is_sha(exp.rev) else exp.rev
|
||||
row["typ"] = "branch_base" if is_base else "branch_commit"
|
||||
|
||||
@@ -199,7 +199,7 @@ def exp_commits(
|
||||
scm: "Git", ref_infos: Optional[Iterable[ExpRefInfo]] = None
|
||||
) -> Iterable[str]:
|
||||
"""Iterate over all experiment commits."""
|
||||
shas: set["str"] = set()
|
||||
shas: set[str] = set()
|
||||
refs = ref_infos if ref_infos else exp_refs(scm)
|
||||
for ref_info in refs:
|
||||
shas.update(scm.branch_revs(str(ref_info), ref_info.baseline_sha))
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ def fetch( # noqa: PLR0913
|
||||
|
||||
|
||||
def _log_unversioned(data: list["DataIndex"]) -> tuple[list["DataIndex"], int]:
|
||||
ret: list["DataIndex"] = []
|
||||
ret: list[DataIndex] = []
|
||||
unversioned: list[str] = []
|
||||
for fs_index in data:
|
||||
remote = fs_index.storage_map[()].remote
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ def gc( # noqa: C901, PLR0912, PLR0913
|
||||
repos = []
|
||||
all_repos = [Repo(path) for path in repos]
|
||||
|
||||
odb_to_obj_ids: "ObjectContainer" = {}
|
||||
odb_to_obj_ids: ObjectContainer = {}
|
||||
with ExitStack() as stack:
|
||||
for repo in all_repos:
|
||||
stack.enter_context(repo.lock)
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ def get(
|
||||
from dvc.fs import download
|
||||
from dvc.fs.data import DataFileSystem
|
||||
|
||||
fs: Union[DataFileSystem, "DVCFileSystem"]
|
||||
fs: Union[DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=repo.index.data["local"])
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ def check_acyclic(graph: "DiGraph") -> None:
|
||||
except nx.NetworkXNoCycle:
|
||||
return
|
||||
|
||||
stages: set["Stage"] = set()
|
||||
stages: set[Stage] = set()
|
||||
for from_node, to_node, _ in edges:
|
||||
stages.add(from_node)
|
||||
stages.add(to_node)
|
||||
|
||||
@@ -4,7 +4,6 @@ from funcy import compact
|
||||
|
||||
from dvc.exceptions import OutputDuplicationError
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import resolve_output, resolve_paths
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -25,11 +24,6 @@ def imp_db(
|
||||
force: bool = False,
|
||||
connection: Optional[str] = None,
|
||||
):
|
||||
ui.warn("WARNING: import-db is an experimental feature.")
|
||||
ui.warn(
|
||||
"The functionality may change or break without notice, "
|
||||
"which could lead to unexpected behavior."
|
||||
)
|
||||
assert sql or table
|
||||
assert output_format in ("csv", "json")
|
||||
|
||||
|
||||
+13
-13
@@ -300,7 +300,7 @@ class Index:
|
||||
self._artifacts = artifacts or {}
|
||||
self._datasets: dict[str, list[dict[str, Any]]] = datasets or {}
|
||||
self._datasets_lock: dict[str, list[dict[str, Any]]] = datasets_lock or {}
|
||||
self._collected_targets: dict[int, list["StageInfo"]] = {}
|
||||
self._collected_targets: dict[int, list[StageInfo]] = {}
|
||||
|
||||
@cached_property
|
||||
def rev(self) -> Optional[str]:
|
||||
@@ -416,7 +416,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -470,7 +470,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -488,7 +488,7 @@ class Index:
|
||||
def metric_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
from .metrics.show import _collect_top_level_metrics
|
||||
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
@@ -509,7 +509,7 @@ class Index:
|
||||
def param_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
from .params.show import _collect_top_level_params
|
||||
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
param_paths = _collect_top_level_params(self.repo)
|
||||
@@ -525,7 +525,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def plot_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
@@ -548,7 +548,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "dict[str, DataIndex]":
|
||||
prefix: "DataIndexKey"
|
||||
prefix: DataIndexKey
|
||||
loaded = False
|
||||
|
||||
index = self.repo.data_index
|
||||
@@ -600,7 +600,7 @@ class Index:
|
||||
if not onerror:
|
||||
|
||||
def onerror(_target, _exc):
|
||||
raise
|
||||
raise # noqa: PLE0704
|
||||
|
||||
targets = ensure_list(targets)
|
||||
if not targets:
|
||||
@@ -627,7 +627,7 @@ class Index:
|
||||
jobs: Optional[int] = None,
|
||||
push: bool = False,
|
||||
) -> "ObjectContainer":
|
||||
used: "ObjectContainer" = defaultdict(set)
|
||||
used: ObjectContainer = defaultdict(set)
|
||||
pairs = self.collect_targets(targets, recursive=recursive, with_deps=with_deps)
|
||||
for stage, filter_info in pairs:
|
||||
for odb, objs in stage.get_used_objs(
|
||||
@@ -748,7 +748,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -764,7 +764,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def _data_prefixes(self) -> dict[str, "_DataPrefixes"]:
|
||||
prefixes: dict[str, "_DataPrefixes"] = defaultdict(
|
||||
prefixes: dict[str, _DataPrefixes] = defaultdict(
|
||||
lambda: _DataPrefixes(set(), set())
|
||||
)
|
||||
for out, filter_info in self._filtered_outs:
|
||||
@@ -781,7 +781,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
ret: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
ret: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
for out, filter_info in self._filtered_outs:
|
||||
if not out.use_cache:
|
||||
@@ -811,7 +811,7 @@ class IndexView:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
data: dict[str, Union["DataIndex", "DataIndexView"]] = {}
|
||||
data: dict[str, Union[DataIndex, DataIndexView]] = {}
|
||||
for workspace, data_index in self._index.data.items():
|
||||
if self.stages:
|
||||
data[workspace] = view(data_index, partial(key_filter, workspace))
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ def _ls(
|
||||
recursive: Optional[bool] = None,
|
||||
dvc_only: bool = False,
|
||||
):
|
||||
fs: "DVCFileSystem" = repo.dvcfs
|
||||
fs: DVCFileSystem = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
fs_path = fs.info(fs_path)["name"]
|
||||
|
||||
@@ -146,7 +146,7 @@ def _cached_clone(url, rev):
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _clone_default_branch(url, rev): # noqa: PLR0912
|
||||
def _clone_default_branch(url, rev):
|
||||
"""Get or create a clean clone of the url.
|
||||
|
||||
The cloned is reactualized with git pull unless rev is a known sha.
|
||||
|
||||
@@ -29,7 +29,7 @@ def collect_stages(
|
||||
recursive: bool = False,
|
||||
glob: bool = False,
|
||||
) -> list["Stage"]:
|
||||
stages: list["Stage"] = []
|
||||
stages: list[Stage] = []
|
||||
for target in targets:
|
||||
stages.extend(repo.stage.collect(target, recursive=recursive, glob=glob))
|
||||
return ldistinct(stages)
|
||||
@@ -166,10 +166,10 @@ def _reproduce(
|
||||
) -> list["Stage"]:
|
||||
assert on_error in ("fail", "keep-going", "ignore")
|
||||
|
||||
result: list["Stage"] = []
|
||||
failed: list["Stage"] = []
|
||||
to_skip: dict["Stage", "Stage"] = {}
|
||||
ret: Optional["Stage"] = None
|
||||
result: list[Stage] = []
|
||||
failed: list[Stage] = []
|
||||
to_skip: dict[Stage, Stage] = {}
|
||||
ret: Optional[Stage] = None
|
||||
|
||||
force_state = dict.fromkeys(stages, force)
|
||||
|
||||
@@ -228,7 +228,7 @@ def reproduce(
|
||||
if not kwargs.get("interactive", False):
|
||||
kwargs["interactive"] = self.config["core"].get("interactive", False)
|
||||
|
||||
stages: list["Stage"] = []
|
||||
stages: list[Stage] = []
|
||||
if not all_pipelines:
|
||||
targets_list = ensure_list(targets or PROJECT_FILE)
|
||||
stages = collect_stages(self, targets_list, recursive=recursive, glob=glob)
|
||||
|
||||
@@ -21,7 +21,7 @@ class SCMContext:
|
||||
def __init__(self, scm: "Base", config: Optional[dict[str, Any]] = None) -> None:
|
||||
from funcy import get_in
|
||||
|
||||
self.scm: "Base" = scm
|
||||
self.scm: Base = scm
|
||||
self.autostage: bool = get_in(
|
||||
config or {}, ["core", "autostage"], default=False
|
||||
)
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ def locked(f):
|
||||
|
||||
class StageLoad:
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo: "Repo" = repo
|
||||
self.repo: Repo = repo
|
||||
|
||||
@property
|
||||
def fs(self):
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ def update( # noqa: C901
|
||||
raise InvalidArgumentError("--remote can't be used without --to-remote")
|
||||
|
||||
import_stages = set()
|
||||
other_stage_infos: list["StageInfo"] = []
|
||||
other_stage_infos: list[StageInfo] = []
|
||||
|
||||
for stage_info in self.index.collect_targets(targets, recursive=recursive):
|
||||
if stage_info.stage.is_import:
|
||||
|
||||
@@ -166,7 +166,7 @@ def update_worktree_stages(repo: "Repo", stage_infos: Iterable["StageInfo"]):
|
||||
|
||||
view = IndexView(repo.index, stage_infos, outs_filter=outs_filter)
|
||||
local_index = view.data["repo"]
|
||||
remote_indexes: dict[str, tuple["Remote", "DataIndex"]] = {}
|
||||
remote_indexes: dict[str, tuple[Remote, DataIndex]] = {}
|
||||
for stage in view.stages:
|
||||
for out in stage.outs:
|
||||
_update_worktree_out(repo, out, local_index, remote_indexes)
|
||||
|
||||
@@ -290,7 +290,7 @@ class Stage(params.StageParams):
|
||||
)
|
||||
|
||||
def short_description(self) -> Optional["str"]:
|
||||
desc: Optional["str"] = None
|
||||
desc: Optional[str] = None
|
||||
if self.desc:
|
||||
with suppress(ValueError):
|
||||
# try to use first non-empty line as a description
|
||||
@@ -658,7 +658,7 @@ class Stage(params.StageParams):
|
||||
def checkout(
|
||||
self, allow_missing: bool = False, **kwargs
|
||||
) -> dict[str, list["StrPath"]]:
|
||||
stats: dict[str, list["StrPath"]] = defaultdict(list)
|
||||
stats: dict[str, list[StrPath]] = defaultdict(list)
|
||||
if self.is_partial_import:
|
||||
return stats
|
||||
|
||||
|
||||
+3
-2
@@ -229,7 +229,7 @@ class StageCache:
|
||||
if not dry:
|
||||
cached_stage.checkout()
|
||||
|
||||
def transfer(self, from_odb, to_odb):
|
||||
def transfer(self, from_odb, to_odb, force=True):
|
||||
from dvc.fs import HTTPFileSystem, LocalFileSystem
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
@@ -258,10 +258,11 @@ class StageCache:
|
||||
|
||||
dst = to_fs.join(to_odb.path, rel)
|
||||
key = to_fs.parent(dst)
|
||||
|
||||
# check if any build cache already exists for this key
|
||||
# TODO: check if MaxKeys=1 or something like that applies
|
||||
# or otherwise this will take a lot of time!
|
||||
if to_fs.exists(key) and first(to_fs.find(key)):
|
||||
if not force and to_fs.exists(key) and first(to_fs.find(key)):
|
||||
continue
|
||||
|
||||
src_name = from_fs.name(src)
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class StageLoader(Mapping):
|
||||
from dvc.output import Output, merge_file_meta_from_cloud
|
||||
|
||||
assert isinstance(lock_data, dict)
|
||||
items: Iterable[tuple[str, "Output"]] = chain(
|
||||
items: Iterable[tuple[str, Output]] = chain(
|
||||
((StageParams.PARAM_DEPS, dep) for dep in stage.deps),
|
||||
((StageParams.PARAM_OUTS, out) for out in stage.outs),
|
||||
)
|
||||
|
||||
+6
-2
@@ -27,8 +27,8 @@ def _fish_supports_no_config(executable) -> bool:
|
||||
bool: True if the fish version is greater than 3.3.0, False otherwise.
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[executable, "--version"], # noqa: S603
|
||||
output = subprocess.check_output( # noqa: S603
|
||||
[executable, "--version"],
|
||||
text=True,
|
||||
)
|
||||
version = Version(output.split(" ")[-1].strip())
|
||||
@@ -154,7 +154,11 @@ def cmd_run(stage, dry=False, run_env=None):
|
||||
|
||||
|
||||
def _pull_missing_deps(stage):
|
||||
from dvc.dependency import DatasetDependency, DbDependency
|
||||
|
||||
for dep in stage.deps:
|
||||
if isinstance(dep, (DatasetDependency, DbDependency)):
|
||||
continue
|
||||
if not dep.exists:
|
||||
stage.repo.pull(dep.def_path)
|
||||
|
||||
|
||||
+2
-2
@@ -302,9 +302,9 @@ def _get_stage_files(stage: "Stage") -> list[str]:
|
||||
and dep.is_in_repo
|
||||
and not stage.repo.dvcfs.isdvc(stage.repo.dvcfs.from_os_path(str(dep)))
|
||||
):
|
||||
ret.append(relpath(dep.fs_path))
|
||||
ret.append(relpath(dep.fs_path)) # noqa: PERF401
|
||||
|
||||
for out in stage.outs:
|
||||
if not out.use_scm_ignore and out.is_in_repo:
|
||||
ret.append(relpath(out.fs_path))
|
||||
ret.append(relpath(out.fs_path)) # noqa: PERF401
|
||||
return ret
|
||||
|
||||
@@ -1,2 +1,19 @@
|
||||
def test_add(bench_dvc, tmp_dir, dvc, dataset):
|
||||
bench_dvc("add", dataset)
|
||||
from .test_checkout import _skip_unsupported_link
|
||||
|
||||
|
||||
def generate_test(*, link_type="copy"):
|
||||
def _test_add(bench_dvc, tmp_dir, dvc, dataset):
|
||||
_skip_unsupported_link((tmp_dir / ".dvc" / "cache"), tmp_dir, link_type)
|
||||
|
||||
with dvc.config.edit() as conf:
|
||||
conf["cache"]["type"] = link_type
|
||||
|
||||
bench_dvc("add", dataset)
|
||||
bench_dvc("add", dataset, name="noop")
|
||||
|
||||
return _test_add
|
||||
|
||||
|
||||
test_add_copy = generate_test(link_type="copy")
|
||||
test_add_symlink = generate_test(link_type="symlink")
|
||||
test_add_hardlink = generate_test(link_type="hardlink")
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
from shutil import rmtree
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.requires(minversion=(2, 15, 0), reason="new command")
|
||||
|
||||
|
||||
def test_data_status(bench_dvc, tmp_dir, scm, dvc, make_dataset):
|
||||
args = ("data", "status")
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.requires(minversion=(2, 28, 0))
|
||||
def test_exp_show(make_project, monkeypatch, bench_dvc, dvc_bin):
|
||||
url = "https://github.com/iterative/example-get-started"
|
||||
rev = "main"
|
||||
|
||||
@@ -2,6 +2,12 @@ import pytest
|
||||
|
||||
|
||||
@pytest.mark.flaky(reruns=3)
|
||||
@pytest.mark.requires(
|
||||
"!=3.53.*,!=3.54.0",
|
||||
reason="Takes 10 mins to run. Regression in 3.53.0, fixed in 3.54.1",
|
||||
)
|
||||
# Introduced in https://github.com/iterative/dvc/pull/10388.
|
||||
# Fixed in https://github.com/iterative/dvc/pull/10531.
|
||||
def test_import(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.testing.benchmarks.fixtures import _pull
|
||||
|
||||
|
||||
@pytest.mark.requires(minversion=(2, 34, 0), reason="top-level plots not supported")
|
||||
def test_plots(project, bench_dvc):
|
||||
with Repo() as dvc:
|
||||
_pull(dvc)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import shutil
|
||||
|
||||
|
||||
def test_sharing(bench_dvc, tmp_dir, dvc, dataset, remote):
|
||||
bench_dvc("add", dataset)
|
||||
bench_dvc("add", dataset, name="noop")
|
||||
def test_sharing(bench_dvc, tmp_dir, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(cache=True, dvcfile=True)
|
||||
|
||||
bench_dvc("push")
|
||||
bench_dvc("push", name="noop")
|
||||
@@ -13,6 +12,3 @@ def test_sharing(bench_dvc, tmp_dir, dvc, dataset, remote):
|
||||
|
||||
bench_dvc("fetch")
|
||||
bench_dvc("fetch", name="noop")
|
||||
|
||||
bench_dvc("checkout")
|
||||
bench_dvc("checkout", name="noop")
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from .fixtures import * # noqa: F403
|
||||
|
||||
pytest_plugins = ["dvc.testing.plugin"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import check_output
|
||||
from subprocess import check_call, check_output
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
@@ -23,13 +24,14 @@ class VirtualEnv:
|
||||
self.bin = self.path / ("Scripts" if os.name == "nt" else "bin")
|
||||
|
||||
def create(self) -> None:
|
||||
import virtualenv
|
||||
check_call([sys.executable, "-m", "uv", "venv", self.path]) # noqa:S603
|
||||
|
||||
virtualenv.cli_run([os.fspath(self.path)])
|
||||
def install(self, *packages: str) -> None:
|
||||
check_call([sys.executable, "-m", "uv", "pip", "install", *packages]) # noqa: S603
|
||||
|
||||
def run(self, cmd: str, *args: str, env: Optional[dict[str, str]] = None) -> None:
|
||||
exe = self.which(cmd)
|
||||
check_output([exe, *args], env=env) # noqa: S603
|
||||
check_call([exe, *args], env=env) # noqa: S603
|
||||
|
||||
def which(self, cmd: str) -> str:
|
||||
assert self.bin.exists()
|
||||
@@ -58,8 +60,8 @@ def dvc_venvs():
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dvc_git_repo(tmp_path_factory, bench_config):
|
||||
url = bench_config.dvc_git_repo
|
||||
def dvc_repo(tmp_path_factory, bench_config):
|
||||
url = bench_config.dvc_repo
|
||||
|
||||
if os.path.isdir(url):
|
||||
return url
|
||||
@@ -71,8 +73,10 @@ def dvc_git_repo(tmp_path_factory, bench_config):
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dvc_bench_git_repo(tmp_path_factory, bench_config):
|
||||
url = bench_config.dvc_bench_git_repo
|
||||
def dvc_bench_repo(tmp_path_factory, bench_config):
|
||||
url = bench_config.dvc_bench_repo
|
||||
if url is None:
|
||||
pytest.skip("--dvc-bench-repo is not set")
|
||||
|
||||
if os.path.isdir(url):
|
||||
return Path(url)
|
||||
@@ -88,61 +92,55 @@ def make_dvc_bin(
|
||||
dvc_rev,
|
||||
dvc_venvs,
|
||||
make_dvc_venv,
|
||||
dvc_git_repo,
|
||||
dvc_repo,
|
||||
bench_config,
|
||||
request,
|
||||
):
|
||||
if dvc_rev:
|
||||
venv = dvc_venvs.get(dvc_rev)
|
||||
venv: VirtualEnv = dvc_venvs.get(dvc_rev)
|
||||
if not venv:
|
||||
venv = make_dvc_venv(dvc_rev)
|
||||
venv.run("pip", "install", "-U", "pip")
|
||||
if bench_config.dvc_install_deps:
|
||||
egg = f"dvc[{bench_config.dvc_install_deps}]"
|
||||
pkg = f"dvc[{bench_config.dvc_install_deps}]"
|
||||
else:
|
||||
egg = "dvc"
|
||||
venv.run("pip", "install", f"git+file://{dvc_git_repo}@{dvc_rev}#egg={egg}")
|
||||
if dvc_rev in ["2.18.1", "2.11.0", "2.6.3"]:
|
||||
venv.run("pip", "install", "fsspec==2022.11.0")
|
||||
pkg = "dvc"
|
||||
packages = [f"{pkg} @ git+file://{dvc_repo}@{dvc_rev}"]
|
||||
try:
|
||||
if version.Version(dvc_rev) < version.Version("3.50.3"):
|
||||
venv.run("pip", "install", "pygit2==1.14.1")
|
||||
packages.append("pygit2==1.14.1")
|
||||
except version.InvalidVersion:
|
||||
pass
|
||||
venv.install(*packages)
|
||||
|
||||
dvc_venvs[dvc_rev] = venv
|
||||
dvc_bin = venv.which("dvc")
|
||||
else:
|
||||
dvc_bin = bench_config.dvc_bin
|
||||
|
||||
def _dvc_bin(*args):
|
||||
return check_output([dvc_bin, *args], text=True) # noqa: S603
|
||||
check_call([dvc_bin, *args]) # noqa: S603
|
||||
|
||||
_dvc_bin.version = parse_tuple(_dvc_bin("--version")) # type: ignore[attr-defined]
|
||||
_dvc_bin.version = check_output([dvc_bin, "--version"], text=True) # type: ignore[attr-defined] # noqa: S603
|
||||
return _dvc_bin
|
||||
|
||||
|
||||
def parse_tuple(version_string):
|
||||
from packaging.version import Version
|
||||
|
||||
parsed = version.parse(version_string)
|
||||
assert isinstance(parsed, Version)
|
||||
return (parsed.major, parsed.minor, parsed.micro)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dvc_bin(request, make_dvc_bin):
|
||||
if marker := request.node.get_closest_marker("requires"):
|
||||
minversion = marker.kwargs.get("minversion") or first(marker.args)
|
||||
assert minversion, (
|
||||
"'minversion' needs to be specified as"
|
||||
" a positional or a keyword argument"
|
||||
)
|
||||
reason = marker.kwargs.get("reason", "")
|
||||
if isinstance(minversion, str):
|
||||
minversion = parse_tuple(minversion)
|
||||
if make_dvc_bin.version < minversion:
|
||||
version_repr = ".".join(map(str, minversion))
|
||||
pytest.skip(f"requires dvc>={version_repr}: {reason}")
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version, parse
|
||||
|
||||
spec = first(marker.args)
|
||||
assert spec is not None
|
||||
spec = SpecifierSet(spec) if isinstance(spec, str) else spec
|
||||
reason = marker.kwargs["reason"]
|
||||
dvc_version = make_dvc_bin.version
|
||||
version = Version(parse(dvc_version).base_version)
|
||||
if version not in spec:
|
||||
pytest.skip(
|
||||
f"Version {dvc_version} "
|
||||
f"does not satisfy requirement {spec!r}: {reason}"
|
||||
)
|
||||
return make_dvc_bin
|
||||
|
||||
|
||||
@@ -169,11 +167,19 @@ def make_bench(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bench_dvc(dvc_bin, make_bench):
|
||||
def bench_dvc(request, dvc_bin, make_bench):
|
||||
def _bench_dvc(*args, **kwargs):
|
||||
name = kwargs.pop("name", None)
|
||||
name = f"-{name}" if name else ""
|
||||
bench = make_bench(args[0] + name)
|
||||
if request.config.getoption("--benchmark-cprofile-dump") or kwargs.pop(
|
||||
"cprofile", False
|
||||
):
|
||||
cprofile_results = request.config.invocation_params.dir / "prof"
|
||||
cprofile_results.mkdir(exist_ok=True)
|
||||
stats_file = cprofile_results / f"{bench.name}.prof"
|
||||
args = (*args, "--cprofile-dump", stats_file)
|
||||
|
||||
return bench.pedantic(dvc_bin, args=args, **kwargs)
|
||||
|
||||
return _bench_dvc
|
||||
@@ -190,14 +196,14 @@ def _pull(repo, *args):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_dataset(request, bench_config, tmp_dir, dvc_bench_git_repo):
|
||||
def make_dataset(request, bench_config, tmp_dir, dvc_bench_repo):
|
||||
def _make_dataset(
|
||||
dvcfile=False, files=True, cache=False, commit=False, remote=False
|
||||
):
|
||||
from dvc.repo import Repo
|
||||
|
||||
path = tmp_dir / "dataset"
|
||||
root = dvc_bench_git_repo
|
||||
root = dvc_bench_repo
|
||||
src = root / "data" / bench_config.dataset / "dataset"
|
||||
src_dvc = src.with_suffix(".dvc")
|
||||
|
||||
@@ -256,7 +262,7 @@ def make_project(tmp_path_factory):
|
||||
@pytest.fixture
|
||||
def project(bench_config, monkeypatch, make_project):
|
||||
rev = bench_config.project_rev
|
||||
url = bench_config.project_git_repo
|
||||
url = bench_config.project_repo
|
||||
|
||||
if os.path.isdir(url):
|
||||
path = url
|
||||
|
||||
@@ -1,57 +1,84 @@
|
||||
import os
|
||||
from dataclasses import dataclass, fields
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
DEFAULT_DVC_BIN = "dvc"
|
||||
DEFAULT_DVC_GIT_REPO = "https://github.com/iterative/dvc"
|
||||
DEFAULT_DVC_BENCH_GIT_REPO = "https://github.com/iterative/dvc-bench"
|
||||
DEFAULT_PROJECT_GIT_REPO = "https://github.com/iterative/example-get-started"
|
||||
DEFAULT_DVC_REPO = os.fspath(Path(__file__).parents[3])
|
||||
DEFAULT_PROJECT_REPO = "https://github.com/iterative/example-get-started"
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
bconf = config.bench_config
|
||||
return f"dvc-bench: (dataset: {bconf.dataset!r}, revs: {bconf.dvc_revs!r})"
|
||||
return f"dvc-bench: {bconf}"
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
str_revs = metafunc.config.getoption("--dvc-revs")
|
||||
revs = str_revs.split(",") if str_revs else [None]
|
||||
revs = metafunc.config.getoption("--dvc-revs")
|
||||
if not revs:
|
||||
revs = [None]
|
||||
if "dvc_rev" in metafunc.fixturenames:
|
||||
metafunc.parametrize("dvc_rev", revs, scope="session")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DVCBenchConfig:
|
||||
def __init__(self):
|
||||
self.dataset = "small"
|
||||
self.dvc_bin = DEFAULT_DVC_BIN
|
||||
self.dvc_revs = None
|
||||
self.dvc_git_repo = DEFAULT_DVC_GIT_REPO
|
||||
self.dvc_install_deps = None
|
||||
self.dvc_bench_git_repo = DEFAULT_DVC_BENCH_GIT_REPO
|
||||
self.project_rev = None
|
||||
self.project_git_repo = DEFAULT_PROJECT_GIT_REPO
|
||||
dataset: str = "tiny"
|
||||
dvc_repo: str = DEFAULT_DVC_REPO
|
||||
dvc_bench_repo: Optional[str] = None
|
||||
project_repo: str = DEFAULT_PROJECT_REPO
|
||||
project_rev: Optional[str] = None
|
||||
dvc_bin: str = DEFAULT_DVC_BIN
|
||||
dvc_revs: Optional[list[str]] = None
|
||||
dvc_install_deps: Optional[str] = None
|
||||
|
||||
def __repr__(self):
|
||||
args = ", ".join(
|
||||
f"{f.name}={val!r}"
|
||||
for f in fields(self)
|
||||
if (val := getattr(self, f.name)) != f.default
|
||||
)
|
||||
return f"{self.__class__.__name__}({args})"
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "requires(minversion): mark a test as requiring minimum DVC version"
|
||||
"markers",
|
||||
"requires(spec): mark a test to run only on versions that satisfy the spec",
|
||||
)
|
||||
config.bench_config = DVCBenchConfig(
|
||||
dataset=config.getoption("--dataset"),
|
||||
dvc_repo=config.getoption("--dvc-repo"),
|
||||
dvc_bench_repo=config.getoption("--dvc-bench-repo"),
|
||||
project_repo=config.getoption("--project-repo"),
|
||||
project_rev=config.getoption("--project-rev"),
|
||||
dvc_bin=config.getoption("--dvc-bin"),
|
||||
dvc_revs=config.getoption("--dvc-revs"),
|
||||
dvc_install_deps=config.getoption("--dvc-install-deps"),
|
||||
)
|
||||
|
||||
config.bench_config = DVCBenchConfig()
|
||||
config.bench_config.dataset = config.getoption("--dataset")
|
||||
config.bench_config.dvc_bin = config.getoption("--dvc-bin")
|
||||
config.bench_config.dvc_revs = config.getoption("--dvc-revs")
|
||||
config.bench_config.dvc_git_repo = config.getoption("--dvc-git-repo")
|
||||
config.bench_config.dvc_install_deps = config.getoption("--dvc-install-deps")
|
||||
config.bench_config.dvc_bench_git_repo = config.getoption("--dvc-bench-git-repo")
|
||||
config.bench_config.project_rev = config.getoption("--project-rev")
|
||||
config.bench_config.project_git_repo = config.getoption("--project-git-repo")
|
||||
|
||||
def resolve_path(path):
|
||||
if os.path.isdir(path):
|
||||
return os.path.abspath(path)
|
||||
return path
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="small",
|
||||
default="tiny",
|
||||
help="Dataset name to use in tests (e.g. tiny/small/large/mnist/etc)",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--benchmark-cprofile-dump",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Save cprofile results",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--dvc-bin",
|
||||
type=str,
|
||||
@@ -61,36 +88,34 @@ def pytest_addoption(parser):
|
||||
|
||||
parser.addoption(
|
||||
"--dvc-revs",
|
||||
type=str,
|
||||
type=lambda revs: revs.split(","),
|
||||
help=("Comma-separated list of DVC revisions to test (overrides `--dvc-bin`)"),
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--dvc-git-repo",
|
||||
type=str,
|
||||
default=DEFAULT_DVC_GIT_REPO,
|
||||
"--dvc-repo",
|
||||
type=resolve_path,
|
||||
default=DEFAULT_DVC_REPO,
|
||||
help="Path or url to dvc git repo",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--dvc-install-deps",
|
||||
type=str,
|
||||
default="",
|
||||
help="Comma-separated list of DVC installation packages",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--dvc-bench-git-repo",
|
||||
type=str,
|
||||
default=DEFAULT_DVC_BENCH_GIT_REPO,
|
||||
"--dvc-bench-repo",
|
||||
type=resolve_path,
|
||||
default=None,
|
||||
help="Path or url to dvc-bench git repo (for loading benchmark datasets)",
|
||||
)
|
||||
|
||||
parser.addoption("--project-rev", type=str, help="Project revision to test")
|
||||
|
||||
parser.addoption(
|
||||
"--project-git-repo",
|
||||
type=str,
|
||||
default=DEFAULT_PROJECT_GIT_REPO,
|
||||
"--project-repo",
|
||||
type=resolve_path,
|
||||
default=DEFAULT_PROJECT_REPO,
|
||||
help="Path or url to dvc project",
|
||||
)
|
||||
|
||||
@@ -228,10 +228,10 @@ def docker_services(tmp_path_factory, request):
|
||||
pytest.skip("disabled for Windows on CI")
|
||||
|
||||
try:
|
||||
subprocess.check_output(
|
||||
subprocess.check_output( # noqa: S602
|
||||
"docker ps", # noqa: S607
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=True, # noqa: S602
|
||||
shell=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as err:
|
||||
out = (err.output or b"").decode("utf-8")
|
||||
|
||||
@@ -32,7 +32,7 @@ class PathInfo(pathlib.PurePath, _BasePath):
|
||||
|
||||
def __new__(cls, *args):
|
||||
if cls is PathInfo:
|
||||
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo
|
||||
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo # noqa: PLW0642
|
||||
|
||||
return cls._from_parts(args) # type: ignore[attr-defined]
|
||||
|
||||
@@ -79,13 +79,13 @@ class PathInfo(pathlib.PurePath, _BasePath):
|
||||
and self._cparts[:n] == other._cparts # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
def relative_to(self, other):
|
||||
def relative_to(self, other, *args, **kwargs):
|
||||
# pathlib relative_to raises exception when one path is not a direct
|
||||
# descendant of the other when os.path.relpath would return abspath.
|
||||
# For DVC PathInfo we only need the relpath behavior.
|
||||
# See: https://bugs.python.org/issue40358
|
||||
try:
|
||||
path = super().relative_to(other)
|
||||
path = super().relative_to(other, *args, **kwargs)
|
||||
except ValueError:
|
||||
path = relpath(self, other)
|
||||
return self.__class__(path)
|
||||
|
||||
@@ -18,3 +18,9 @@ def pytest_configure(config):
|
||||
from .benchmarks.plugin import pytest_configure as bench_configure
|
||||
|
||||
bench_configure(config)
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
from .benchmarks.plugin import pytest_report_header as bench_report_header
|
||||
|
||||
return bench_report_header(config)
|
||||
|
||||
@@ -3,7 +3,6 @@ import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.stage.cache import RunCacheNotSupported
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
@@ -97,8 +96,10 @@ class TestRemote:
|
||||
status_dir = dvc.cloud.status(dir_hashes)
|
||||
_check_status(status_dir, ok=dir_hashes)
|
||||
|
||||
@pytest.mark.xfail(raises=RunCacheNotSupported, strict=False)
|
||||
def test_stage_cache_push_pull(self, tmp_dir, dvc, remote):
|
||||
if remote.scheme in ("http", "https"):
|
||||
pytest.skip("HTTP remote does not support stage cache")
|
||||
|
||||
tmp_dir.gen("foo", "foo")
|
||||
stage = dvc.stage.add(
|
||||
deps=["foo"], outs=["bar"], name="copy-foo-bar", cmd="cp foo bar"
|
||||
|
||||
@@ -71,7 +71,7 @@ class TmpDir(pathlib.Path):
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is TmpDir:
|
||||
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir
|
||||
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir # noqa: PLW0642
|
||||
|
||||
# init parameter and `_init` method has been removed in Python 3.10.
|
||||
kw = {"init": False} if sys.version_info < (3, 10) else {}
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ class Console:
|
||||
styled=styled,
|
||||
)
|
||||
|
||||
def write_json( # noqa: PLR0913
|
||||
def write_json(
|
||||
self,
|
||||
data: Any,
|
||||
indent: Optional[int] = None,
|
||||
|
||||
@@ -243,7 +243,7 @@ def env2bool(var, undefined=False):
|
||||
var = os.getenv(var, None)
|
||||
if var is None:
|
||||
return undefined
|
||||
return bool(re.search("1|y|yes|true", var, flags=re.I))
|
||||
return bool(re.search("1|y|yes|true", var, flags=re.IGNORECASE))
|
||||
|
||||
|
||||
def resolve_output(inp: str, out: Optional[str], force=False) -> str:
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
STUDIO_URL = "https://studio.iterative.ai"
|
||||
STUDIO_URL = "https://studio.dvc.ai"
|
||||
|
||||
|
||||
def post(
|
||||
|
||||
+13
-14
@@ -38,15 +38,15 @@ dependencies = [
|
||||
"distro>=1.3",
|
||||
"dpath<3,>=2.1.0",
|
||||
"dulwich",
|
||||
"dvc-data>=3.15,<3.16",
|
||||
"dvc-data>=3.16.2,<3.17",
|
||||
"dvc-http>=2.29.0",
|
||||
"dvc-objects",
|
||||
"dvc-render>=1.0.1,<2",
|
||||
"dvc-studio-client>=0.20,<1",
|
||||
"dvc-studio-client>=0.21,<1",
|
||||
"dvc-task>=0.3.0,<1",
|
||||
"flatten_dict<1,>=0.4.1",
|
||||
"flufl.lock>=5,<8", # https://github.com/iterative/dvc/issues/9654
|
||||
"fsspec",
|
||||
"flufl.lock>=8.1.0,<9",
|
||||
"fsspec>=2024.2.0",
|
||||
"funcy>=1.14",
|
||||
"grandalf<1,>=0.7",
|
||||
"gto>=1.6.0,<2",
|
||||
@@ -83,7 +83,7 @@ gdrive = ["dvc-gdrive>=3,<4"]
|
||||
gs = ["dvc-gs>=3,<4"]
|
||||
hdfs = ["dvc-hdfs>=3,<4"]
|
||||
lint = [
|
||||
"mypy==1.10.0",
|
||||
"mypy==1.11.2",
|
||||
"pandas-stubs",
|
||||
"types-colorama",
|
||||
"types-psutil",
|
||||
@@ -101,7 +101,7 @@ ssh_gssapi = ["dvc-ssh[gssapi]>=4,<5"]
|
||||
testing = [
|
||||
"pytest-benchmark[histogram]",
|
||||
"pytest-test-utils",
|
||||
"virtualenv",
|
||||
"uv",
|
||||
]
|
||||
tests = [
|
||||
"dvc[testing]",
|
||||
@@ -159,7 +159,7 @@ namespaces = false
|
||||
write_to = "dvc/_dvc_version.py"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-ra --cov-config pyproject.toml --dist worksteal --benchmark-skip"
|
||||
addopts = "-ra --cov-config pyproject.toml"
|
||||
filterwarnings = [
|
||||
"error::ResourceWarning",
|
||||
"error::pytest.PytestUnraisableExceptionWarning",
|
||||
@@ -256,6 +256,7 @@ module = [
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
|
||||
skip = "CODE_OF_CONDUCT.md"
|
||||
|
||||
[tool.ruff]
|
||||
output-format = "full"
|
||||
@@ -263,21 +264,19 @@ show-fixes = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = [
|
||||
"N818", "S101", "ISC001", "PT004", "PT007", "RET502", "RET503", "SIM105", "SIM108", "SIM117",
|
||||
"TRY003", "TRY300", "PLR2004", "PLW2901", "LOG007",
|
||||
"N818", "S101", "ISC001", "PT007", "RET502", "RET503", "SIM105", "SIM108", "SIM117",
|
||||
"TRY003", "TRY300", "PERF203", "PLR2004", "PLW2901", "LOG007",
|
||||
]
|
||||
select = [
|
||||
"F", "E", "W", "C90", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "B", "A", "C4", "DTZ", "T10",
|
||||
"EXE", "ISC", "ICN", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET",
|
||||
"EXE", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET",
|
||||
"SLOT", "SIM", "TID", "TCH", "ARG", "PGH", "PLC", "PLE", "PLR", "PLW", "TRY",
|
||||
"FLY", "PERF101", "LOG", "RUF", "RUF022", "RUF023", "RUF024", "RUF025", "RUF026", "RUF027"
|
||||
"FLY", "PERF", "FURB", "RUF", "RUF022", "RUF023", "RUF024", "RUF026", "RUF027", "RUF029", "RUF030",
|
||||
]
|
||||
preview = true
|
||||
explicit-preview-rules = true
|
||||
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
parametrize-names-type = "csv"
|
||||
raises-extend-require-match-for = ["dvc.exceptions.DvcException", "dvc.scm.SCMError", "scmrepo.exceptions.SCMError"]
|
||||
|
||||
@@ -304,4 +303,4 @@ max-args = 10
|
||||
"dvc/commands/**" = ["N806"]
|
||||
"dvc/testing/**" = ["ARG002"]
|
||||
"dvc/testing/benchmarks/**" = ["ARG001"]
|
||||
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301"]
|
||||
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301", "PERF"]
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ if __name__ == "__main__":
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-n=logical", *sys.argv[1:]]))
|
||||
sys.exit(pytest.main(["-v", "-n=logical", "--dist=worksteal", *sys.argv[1:]]))
|
||||
|
||||
@@ -305,20 +305,20 @@ def test_should_update_state_entry_for_directory_after_add(mocker, dvc, tmp_dir)
|
||||
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 5
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
os.rename("data", "data.back")
|
||||
ret = main(["checkout"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 6
|
||||
assert file_md5_counter.mock.call_count == 5
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 7
|
||||
assert file_md5_counter.mock.call_count == 6
|
||||
|
||||
|
||||
def test_add_commit(tmp_dir, dvc):
|
||||
@@ -339,16 +339,16 @@ def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
counter = mocker.spy(dvc_data.hashfile.build, "_build_tree")
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 1
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 2
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 3
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 4
|
||||
|
||||
|
||||
def test_should_place_stage_in_data_dir_if_repository_below_symlink(
|
||||
mocker, tmp_dir, dvc
|
||||
@@ -754,6 +754,7 @@ def test_add_file_in_symlink_dir(make_tmp_dir, tmp_dir, dvc):
|
||||
def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
|
||||
dvc.cache.local.cache_types = ["symlink", "hardlink"]
|
||||
mocker.patch("dvc_data.hashfile.checkout.test_links", return_value=[])
|
||||
dvc.add("foo")
|
||||
err = capsys.readouterr()[1]
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager, suppress
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import ClassVar
|
||||
@@ -41,7 +41,7 @@ UPDATER_INFO_STR = json.dumps(UPDATER_INFO).encode("utf8")
|
||||
|
||||
|
||||
def make_request_handler():
|
||||
class RequestHandler(SimpleHTTPRequestHandler):
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
# save requests count for each method
|
||||
hits: ClassVar[dict[str, int]] = defaultdict(int)
|
||||
|
||||
@@ -80,7 +80,10 @@ def server():
|
||||
thread = Thread(target=httpd.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
yield httpd
|
||||
try:
|
||||
yield httpd
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
def test_analytics(tmp_path, server):
|
||||
@@ -94,6 +97,11 @@ def test_analytics(tmp_path, server):
|
||||
}
|
||||
env.pop("DVC_TEST", None)
|
||||
env.pop("DVC_NO_ANALYTICS", None)
|
||||
# The `iterative-telemetry` package calls `gh api` to generate a CI id.
|
||||
# This might hang especially on Windows,
|
||||
# possibly due to system load from the running tests.
|
||||
# Removing the GITHUB_ACTIONS env var avoids calling `gh api`.
|
||||
env.pop("GITHUB_ACTIONS", None)
|
||||
|
||||
output = subprocess.check_output(
|
||||
[*_get_dvc_args(), "config", "-l", "-vv"],
|
||||
@@ -101,18 +109,24 @@ def test_analytics(tmp_path, server):
|
||||
text=True,
|
||||
)
|
||||
|
||||
match = re.search(r".*Saving analytics report to (.*)", output, flags=re.M)
|
||||
match = re.search(r".*Saving analytics report to (.*)", output, flags=re.MULTILINE)
|
||||
assert match, "no match for the report file"
|
||||
report_file = match.group(1).strip()
|
||||
|
||||
match = re.search(r".*Spawned .*analytics.* with pid (.*)", output, flags=re.M)
|
||||
match = re.search(
|
||||
r".*Spawned .*analytics.* with pid (.*)", output, flags=re.MULTILINE
|
||||
)
|
||||
assert match, "no match for the pid"
|
||||
pid = int(match.group(1).strip())
|
||||
|
||||
with suppress(psutil.NoSuchProcess):
|
||||
psutil.Process(pid).wait(timeout=10)
|
||||
|
||||
log_contents = logfile.read_text(encoding="utf8")
|
||||
expected_line = (f"Process {pid} " if os.name != "nt" else "") + "exiting with 0"
|
||||
assert expected_line in log_contents
|
||||
|
||||
assert not os.path.exists(report_file)
|
||||
assert f"Process {pid} exiting with 0" in logfile.read_text(encoding="utf8")
|
||||
assert server.RequestHandlerClass.hits == {"POST": 1}
|
||||
|
||||
|
||||
@@ -136,13 +150,19 @@ def test_updater(tmp_dir, dvc, server):
|
||||
text=True,
|
||||
)
|
||||
|
||||
match = re.search(r".*Spawned .*updater.* with pid (.*)", output, flags=re.M)
|
||||
match = re.search(
|
||||
r".*Spawned .*updater.* with pid (.*)", output, flags=re.MULTILINE
|
||||
)
|
||||
assert match, "no match for the pid"
|
||||
pid = int(match.group(1).strip())
|
||||
|
||||
with suppress(psutil.NoSuchProcess):
|
||||
psutil.Process(pid).wait(timeout=10)
|
||||
assert f"Process {pid} exiting with 0" in logfile.read_text(encoding="utf8")
|
||||
|
||||
log_contents = logfile.read_text(encoding="utf8")
|
||||
expected_line = (f"Process {pid} " if os.name != "nt" else "") + "exiting with 0"
|
||||
assert expected_line in log_contents
|
||||
|
||||
assert server.RequestHandlerClass.hits == {"GET": 1}
|
||||
# check that the file is saved correctly
|
||||
updater_file = Path(dvc.tmp_dir) / Updater.UPDATER_FILE
|
||||
|
||||
@@ -12,7 +12,8 @@ from dvc.scm import Git
|
||||
from dvc.stage.exceptions import StagePathNotFoundError
|
||||
from dvc.testing.tmp_dir import make_subrepo
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.index.index import DataIndexDirError
|
||||
from dvc_data.hashfile import hash
|
||||
from dvc_data.index.index import DataIndex, DataIndexDirError
|
||||
|
||||
|
||||
def test_import(tmp_dir, scm, dvc, erepo_dir):
|
||||
@@ -722,3 +723,43 @@ def test_import_invalid_configs(tmp_dir, scm, dvc, erepo_dir):
|
||||
remote="myremote",
|
||||
remote_config={"key": "value"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files,expected_info_calls",
|
||||
[
|
||||
({"foo": "foo"}, {("foo",)}),
|
||||
(
|
||||
{
|
||||
"dir": {
|
||||
"bar": "bar",
|
||||
"subdir": {"lorem": "ipsum", "nested": {"lorem": "lorem"}},
|
||||
}
|
||||
},
|
||||
# info calls should be made for only directories
|
||||
{("dir",), ("dir", "subdir"), ("dir", "subdir", "nested")},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_import_no_hash(
|
||||
tmp_dir, scm, dvc, erepo_dir, mocker, files, expected_info_calls
|
||||
):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen(files, commit="create foo")
|
||||
|
||||
file_md5_spy = mocker.spy(hash, "file_md5")
|
||||
index_info_spy = mocker.spy(DataIndex, "info")
|
||||
name = next(iter(files))
|
||||
|
||||
dvc.imp(os.fspath(erepo_dir), name, "out")
|
||||
|
||||
local_hashes = [
|
||||
call.args[0]
|
||||
for call in file_md5_spy.call_args_list
|
||||
if call.args[1].protocol == "local"
|
||||
]
|
||||
# no files should be hashed, should use existing metadata
|
||||
assert not local_hashes
|
||||
assert {
|
||||
call.args[1] for call in index_info_spy.call_args_list
|
||||
} == expected_info_calls
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
import os
|
||||
import posixpath
|
||||
from hashlib import md5
|
||||
from itertools import product
|
||||
|
||||
import pytest
|
||||
from fsspec.implementations.local import LocalFileSystem, make_path_posix
|
||||
from fsspec.tests.abstract.common import GLOB_EDGE_CASES_TESTS
|
||||
|
||||
from dvc.api import DVCFileSystem
|
||||
|
||||
|
||||
class DVCFixtures:
|
||||
"""The fixtures imitate the fsspec.tests.abstract.AbstractFixtures.
|
||||
|
||||
This has been modified to use dvc's fixture, as DVCFileSystem is a read-only
|
||||
filesystem, and cannot be used to create directories or files.
|
||||
|
||||
The `Output.ignore()` is mocked to avoid `.gitignore` files in the directories,
|
||||
as we can reuse the tests from fsspec with minimal modifications.
|
||||
`.gitignore` file is manually created with required patterns at the root of the
|
||||
repository.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def fs_bulk_operations_scenario_0(self, tmp_dir):
|
||||
"""
|
||||
Scenario that is used for many cp/get/put tests. Creates the following
|
||||
directory and file structure:
|
||||
|
||||
📁 source
|
||||
├── 📄 file1
|
||||
├── 📄 file2
|
||||
└── 📁 subdir
|
||||
├── 📄 subfile1
|
||||
├── 📄 subfile2
|
||||
└── 📁 nesteddir
|
||||
└── 📄 nestedfile
|
||||
"""
|
||||
source = tmp_dir / "source"
|
||||
source.mkdir()
|
||||
tmp_dir.scm_gen(
|
||||
".gitignore", "/source/file2/\nsource/subdir", commit="add .gitignore"
|
||||
)
|
||||
|
||||
tmp_dir.scm_gen("source/file1", "file1", commit="add file1")
|
||||
tmp_dir.dvc_gen("source/file2", "file2", commit="add file2")
|
||||
tmp_dir.dvc_gen(
|
||||
{
|
||||
"source/subdir": {
|
||||
"subfile1": "subfile1",
|
||||
"subfile2": "subfile2",
|
||||
"nesteddir": {"nestedfile": "nestedfile"},
|
||||
}
|
||||
},
|
||||
commit="add subdir",
|
||||
)
|
||||
return "/source"
|
||||
|
||||
@pytest.fixture
|
||||
def fs_10_files_with_hashed_names(self, tmp_dir, local_fs, local_join, local_path):
|
||||
"""
|
||||
Scenario that is used to check cp/get/put files order when source and
|
||||
destination are lists. Creates the following directory and file structure:
|
||||
|
||||
📁 source
|
||||
└── 📄 {hashed([0-9])}.txt
|
||||
"""
|
||||
dir_contents = {
|
||||
md5(str(i).encode("utf-8")).hexdigest() + ".txt": str(i) for i in range(10)
|
||||
}
|
||||
tmp_dir.dvc_gen({"source": dir_contents}, commit="add source")
|
||||
tmp_dir.scm_gen(".gitignore", "/source", commit="add .gitignore")
|
||||
return "/source"
|
||||
|
||||
@pytest.fixture
|
||||
def src_directory(self, tmp_dir):
|
||||
# https://github.com/fsspec/filesystem_spec/issues/1062
|
||||
# Recursive cp/get/put of source directory into non-existent target directory.
|
||||
tmp_dir.dvc_gen({"src": {"file": "file"}}, commit="add source")
|
||||
return "/src"
|
||||
|
||||
@pytest.fixture
|
||||
def fs_dir_and_file_with_same_name_prefix(self, tmp_dir):
|
||||
"""
|
||||
Scenario that is used to check cp/get/put on directory and file with
|
||||
the same name prefixes. Creates the following directory and file structure:
|
||||
|
||||
📁 source
|
||||
├── 📄 subdir.txt
|
||||
└── 📁 subdir
|
||||
└── 📄 subfile.txt
|
||||
"""
|
||||
source = tmp_dir / "source"
|
||||
source.mkdir()
|
||||
|
||||
tmp_dir.scm_gen(".gitignore", "/source/subdir", commit="add .gitignore")
|
||||
tmp_dir.scm_gen("source/subdir.txt", "subdir.txt", commit="add subdir.txt")
|
||||
tmp_dir.dvc_gen(
|
||||
{"source/subdir": {"subfile.txt": "subfile.txt"}}, commit="add subdir"
|
||||
)
|
||||
return "/source"
|
||||
|
||||
@pytest.fixture
|
||||
def fs_glob_edge_cases_files(self, tmp_dir):
|
||||
"""
|
||||
Scenario that is used for glob edge cases cp/get/put tests.
|
||||
Creates the following directory and file structure:
|
||||
|
||||
📁 source
|
||||
├── 📄 file1
|
||||
├── 📄 file2
|
||||
├── 📁 subdir0
|
||||
│ ├── 📄 subfile1
|
||||
│ ├── 📄 subfile2
|
||||
│ └── 📁 nesteddir
|
||||
│ └── 📄 nestedfile
|
||||
└── 📁 subdir1
|
||||
├── 📄 subfile1
|
||||
├── 📄 subfile2
|
||||
└── 📁 nesteddir
|
||||
└── 📄 nestedfile
|
||||
"""
|
||||
source = tmp_dir / "source"
|
||||
source.mkdir()
|
||||
|
||||
tmp_dir.scm_gen(
|
||||
".gitignore", "/source/file1\n/source/subdir1", commit="add .gitignore"
|
||||
)
|
||||
tmp_dir.scm_gen("source/file1", "file1", commit="add file1")
|
||||
tmp_dir.dvc_gen("source/file2", "file2", commit="add file2")
|
||||
|
||||
dir_contents = {
|
||||
"subfile1": "subfile1",
|
||||
"subfile2": "subfile2",
|
||||
"nesteddir": {"nestedfile": "nestedfile"},
|
||||
}
|
||||
tmp_dir.scm_gen({"source/subdir0": dir_contents}, commit="add subdir0")
|
||||
tmp_dir.dvc_gen({"source/subdir1": dir_contents}, commit="add subdir1")
|
||||
return "/source"
|
||||
|
||||
@pytest.fixture(params=[{"rev": "HEAD"}, {}])
|
||||
def fs(self, request, tmp_dir, dvc, scm):
|
||||
return DVCFileSystem(tmp_dir, **request.param)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_ignore(self, mocker):
|
||||
mocker.patch("dvc.output.Output.ignore")
|
||||
|
||||
@pytest.fixture
|
||||
def fs_join(self):
|
||||
return posixpath.join
|
||||
|
||||
@pytest.fixture
|
||||
def fs_path(self, fs):
|
||||
return fs.root_marker
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def local_fs(self):
|
||||
# Maybe need an option for auto_mkdir=False? This is only relevant
|
||||
# for certain implementations.
|
||||
return LocalFileSystem(auto_mkdir=True)
|
||||
|
||||
@pytest.fixture
|
||||
def local_join(self):
|
||||
"""
|
||||
Return a function that joins its arguments together into a path, on
|
||||
the local filesystem.
|
||||
"""
|
||||
return os.path.join
|
||||
|
||||
@pytest.fixture
|
||||
def local_path(self, tmpdir):
|
||||
return tmpdir
|
||||
|
||||
@pytest.fixture
|
||||
def local_target(self, local_fs, local_join, local_path):
|
||||
"""
|
||||
Return name of local directory that does not yet exist to copy into.
|
||||
|
||||
Cleans up at the end of each test it which it is used.
|
||||
"""
|
||||
target = local_join(local_path, "target")
|
||||
yield target
|
||||
if local_fs.exists(target):
|
||||
local_fs.rm(target, recursive=True)
|
||||
|
||||
|
||||
class TestDVCFileSystemGet(DVCFixtures):
|
||||
"""
|
||||
This test is adapted from `fsspec.tests.abstract.get.AbstractGetTests`
|
||||
with minor modifications to work with DVCFixtures and DVCFileSystem.
|
||||
"""
|
||||
|
||||
def test_get_file_to_existing_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1a
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
assert local_fs.isdir(target)
|
||||
|
||||
target_file2 = local_join(target, "file2")
|
||||
target_subfile1 = local_join(target, "subfile1")
|
||||
|
||||
# Copy from source directory
|
||||
fs.get(fs_join(source, "file2"), target)
|
||||
assert local_fs.isfile(target_file2)
|
||||
|
||||
# Copy from sub directory
|
||||
fs.get(fs_join(source, "subdir", "subfile1"), target)
|
||||
assert local_fs.isfile(target_subfile1)
|
||||
|
||||
# Remove copied files
|
||||
local_fs.rm([target_file2, target_subfile1])
|
||||
assert not local_fs.exists(target_file2)
|
||||
assert not local_fs.exists(target_subfile1)
|
||||
|
||||
# Repeat with trailing slash on target
|
||||
fs.get(fs_join(source, "file2"), target + "/")
|
||||
assert local_fs.isdir(target)
|
||||
assert local_fs.isfile(target_file2)
|
||||
|
||||
fs.get(fs_join(source, "subdir", "subfile1"), target + "/")
|
||||
assert local_fs.isfile(target_subfile1)
|
||||
|
||||
def test_get_file_to_new_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1b
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
fs.get(
|
||||
fs_join(source, "subdir", "subfile1"), local_join(target, "newdir/")
|
||||
) # Note trailing slash
|
||||
|
||||
assert local_fs.isdir(target)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
|
||||
def test_get_file_to_file_in_existing_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1c
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
fs.get(fs_join(source, "subdir", "subfile1"), local_join(target, "newfile"))
|
||||
assert local_fs.isfile(local_join(target, "newfile"))
|
||||
|
||||
def test_get_file_to_file_in_new_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1d
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
fs.get(
|
||||
fs_join(source, "subdir", "subfile1"),
|
||||
local_join(target, "newdir", "newfile"),
|
||||
)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "newfile"))
|
||||
|
||||
def test_get_directory_to_existing_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1e
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
assert local_fs.isdir(target)
|
||||
|
||||
for source_slash, target_slash in zip([False, True], [False, True]):
|
||||
s = fs_join(source, "subdir")
|
||||
if source_slash:
|
||||
s += "/"
|
||||
t = target + "/" if target_slash else target
|
||||
|
||||
# Without recursive does nothing
|
||||
fs.get(s, t)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# With recursive
|
||||
fs.get(s, t, recursive=True)
|
||||
if source_slash:
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subfile2"))
|
||||
assert local_fs.isdir(local_join(target, "nesteddir"))
|
||||
assert local_fs.isfile(local_join(target, "nesteddir", "nestedfile"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "subfile1"),
|
||||
local_join(target, "subfile2"),
|
||||
local_join(target, "nesteddir"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
else:
|
||||
assert local_fs.isdir(local_join(target, "subdir"))
|
||||
assert local_fs.isfile(local_join(target, "subdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subdir", "subfile2"))
|
||||
assert local_fs.isdir(local_join(target, "subdir", "nesteddir"))
|
||||
assert local_fs.isfile(
|
||||
local_join(target, "subdir", "nesteddir", "nestedfile")
|
||||
)
|
||||
|
||||
local_fs.rm(local_join(target, "subdir"), recursive=True)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# Limit recursive by maxdepth
|
||||
fs.get(s, t, recursive=True, maxdepth=1)
|
||||
if source_slash:
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "nesteddir"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "subfile1"),
|
||||
local_join(target, "subfile2"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
else:
|
||||
assert local_fs.isdir(local_join(target, "subdir"))
|
||||
assert local_fs.isfile(local_join(target, "subdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subdir", "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "subdir", "nesteddir"))
|
||||
|
||||
local_fs.rm(local_join(target, "subdir"), recursive=True)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
def test_get_directory_to_new_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1f
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
for source_slash, target_slash in zip([False, True], [False, True]):
|
||||
s = fs_join(source, "subdir")
|
||||
if source_slash:
|
||||
s += "/"
|
||||
t = local_join(target, "newdir")
|
||||
if target_slash:
|
||||
t += "/"
|
||||
|
||||
# Without recursive does nothing
|
||||
fs.get(s, t)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# With recursive
|
||||
fs.get(s, t, recursive=True)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile2"))
|
||||
assert local_fs.isdir(local_join(target, "newdir", "nesteddir"))
|
||||
assert local_fs.isfile(
|
||||
local_join(target, "newdir", "nesteddir", "nestedfile")
|
||||
)
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(local_join(target, "newdir"), recursive=True)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# Limit recursive by maxdepth
|
||||
fs.get(s, t, recursive=True, maxdepth=1)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "nesteddir"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(local_join(target, "newdir"), recursive=True)
|
||||
assert not local_fs.exists(local_join(target, "newdir"))
|
||||
|
||||
def test_get_glob_to_existing_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1g
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
for target_slash in [False, True]:
|
||||
t = target + "/" if target_slash else target
|
||||
|
||||
# Without recursive
|
||||
fs.get(fs_join(source, "subdir", "*"), t)
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subfile2"))
|
||||
assert not local_fs.isdir(local_join(target, "nesteddir"))
|
||||
assert not local_fs.exists(local_join(target, "nesteddir", "nestedfile"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "subfile1"),
|
||||
local_join(target, "subfile2"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# With recursive
|
||||
for glob, recursive in zip(["*", "**"], [True, False]):
|
||||
fs.get(fs_join(source, "subdir", glob), t, recursive=recursive)
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subfile2"))
|
||||
assert local_fs.isdir(local_join(target, "nesteddir"))
|
||||
assert local_fs.isfile(local_join(target, "nesteddir", "nestedfile"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "subfile1"),
|
||||
local_join(target, "subfile2"),
|
||||
local_join(target, "nesteddir"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# Limit recursive by maxdepth
|
||||
fs.get(
|
||||
fs_join(source, "subdir", glob), t, recursive=recursive, maxdepth=1
|
||||
)
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "nesteddir"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "subfile1"),
|
||||
local_join(target, "subfile2"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
def test_get_glob_to_new_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1h
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
for target_slash in [False, True]:
|
||||
t = fs_join(target, "newdir")
|
||||
if target_slash:
|
||||
t += "/"
|
||||
|
||||
# Without recursive
|
||||
fs.get(fs_join(source, "subdir", "*"), t)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "nesteddir"))
|
||||
assert not local_fs.exists(
|
||||
local_join(target, "newdir", "nesteddir", "nestedfile")
|
||||
)
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "subdir"))
|
||||
|
||||
local_fs.rm(local_join(target, "newdir"), recursive=True)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# With recursive
|
||||
for glob, recursive in zip(["*", "**"], [True, False]):
|
||||
fs.get(fs_join(source, "subdir", glob), t, recursive=recursive)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile2"))
|
||||
assert local_fs.isdir(local_join(target, "newdir", "nesteddir"))
|
||||
assert local_fs.isfile(
|
||||
local_join(target, "newdir", "nesteddir", "nestedfile")
|
||||
)
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "subdir"))
|
||||
|
||||
local_fs.rm(local_join(target, "newdir"), recursive=True)
|
||||
assert not local_fs.exists(local_join(target, "newdir"))
|
||||
|
||||
# Limit recursive by maxdepth
|
||||
fs.get(
|
||||
fs_join(source, "subdir", glob), t, recursive=recursive, maxdepth=1
|
||||
)
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile2"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "nesteddir"))
|
||||
assert not local_fs.exists(local_join(target, "subdir"))
|
||||
assert not local_fs.exists(local_join(target, "newdir", "subdir"))
|
||||
|
||||
local_fs.rm(local_fs.ls(target, detail=False), recursive=True)
|
||||
assert not local_fs.exists(local_join(target, "newdir"))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
GLOB_EDGE_CASES_TESTS["argnames"],
|
||||
GLOB_EDGE_CASES_TESTS["argvalues"],
|
||||
)
|
||||
def test_get_glob_edge_cases(
|
||||
self,
|
||||
path,
|
||||
recursive,
|
||||
maxdepth,
|
||||
expected,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_glob_edge_cases_files,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 1g
|
||||
source = fs_glob_edge_cases_files
|
||||
|
||||
target = local_target
|
||||
|
||||
for new_dir, target_slash in product([True, False], [True, False]):
|
||||
local_fs.mkdir(target)
|
||||
|
||||
t = local_join(target, "newdir") if new_dir else target
|
||||
t = t + "/" if target_slash else t
|
||||
|
||||
fs.get(fs_join(source, path), t, recursive=recursive, maxdepth=maxdepth)
|
||||
|
||||
output = local_fs.find(target)
|
||||
if new_dir:
|
||||
prefixed_expected = [
|
||||
make_path_posix(local_join(target, "newdir", p)) for p in expected
|
||||
]
|
||||
else:
|
||||
prefixed_expected = [
|
||||
make_path_posix(local_join(target, p)) for p in expected
|
||||
]
|
||||
assert sorted(output) == sorted(prefixed_expected)
|
||||
|
||||
try:
|
||||
local_fs.rm(target, recursive=True)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_get_list_of_files_to_existing_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 2a
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
source_files = [
|
||||
fs_join(source, "file1"),
|
||||
fs_join(source, "file2"),
|
||||
fs_join(source, "subdir", "subfile1"),
|
||||
]
|
||||
|
||||
for target_slash in [False, True]:
|
||||
t = target + "/" if target_slash else target
|
||||
|
||||
fs.get(source_files, t)
|
||||
assert local_fs.isfile(local_join(target, "file1"))
|
||||
assert local_fs.isfile(local_join(target, "file2"))
|
||||
assert local_fs.isfile(local_join(target, "subfile1"))
|
||||
|
||||
local_fs.rm(
|
||||
[
|
||||
local_join(target, "file1"),
|
||||
local_join(target, "file2"),
|
||||
local_join(target, "subfile1"),
|
||||
],
|
||||
recursive=True,
|
||||
)
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
def test_get_list_of_files_to_new_directory(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
fs_bulk_operations_scenario_0,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
):
|
||||
# Copy scenario 2b
|
||||
source = fs_bulk_operations_scenario_0
|
||||
|
||||
target = local_target
|
||||
local_fs.mkdir(target)
|
||||
|
||||
source_files = [
|
||||
fs_join(source, "file1"),
|
||||
fs_join(source, "file2"),
|
||||
fs_join(source, "subdir", "subfile1"),
|
||||
]
|
||||
|
||||
fs.get(source_files, local_join(target, "newdir") + "/") # Note trailing slash
|
||||
assert local_fs.isdir(local_join(target, "newdir"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "file1"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "file2"))
|
||||
assert local_fs.isfile(local_join(target, "newdir", "subfile1"))
|
||||
|
||||
def test_get_directory_recursive(
|
||||
self, src_directory, fs, fs_join, fs_path, local_fs, local_join, local_target
|
||||
):
|
||||
target = local_target
|
||||
src = src_directory
|
||||
|
||||
# get without slash
|
||||
assert not local_fs.exists(target)
|
||||
for loop in range(2):
|
||||
fs.get(src, target, recursive=True)
|
||||
assert local_fs.isdir(target)
|
||||
|
||||
if loop == 0:
|
||||
assert local_fs.isfile(local_join(target, "file"))
|
||||
assert not local_fs.exists(local_join(target, "src"))
|
||||
else:
|
||||
assert local_fs.isfile(local_join(target, "file"))
|
||||
assert local_fs.isdir(local_join(target, "src"))
|
||||
assert local_fs.isfile(local_join(target, "src", "file"))
|
||||
|
||||
local_fs.rm(target, recursive=True)
|
||||
|
||||
# get with slash
|
||||
assert not local_fs.exists(target)
|
||||
for _ in range(2):
|
||||
fs.get(src + "/", target, recursive=True)
|
||||
assert local_fs.isdir(target)
|
||||
assert local_fs.isfile(local_join(target, "file"))
|
||||
assert not local_fs.exists(local_join(target, "src"))
|
||||
|
||||
def test_get_directory_without_files_with_same_name_prefix(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
fs_dir_and_file_with_same_name_prefix,
|
||||
):
|
||||
# Create the test dirs
|
||||
source = fs_dir_and_file_with_same_name_prefix
|
||||
target = local_target
|
||||
|
||||
# Test without glob
|
||||
fs.get(fs_join(source, "subdir"), target, recursive=True)
|
||||
|
||||
assert local_fs.isfile(local_join(target, "subfile.txt"))
|
||||
assert not local_fs.isfile(local_join(target, "subdir.txt"))
|
||||
|
||||
local_fs.rm([local_join(target, "subfile.txt")])
|
||||
assert local_fs.ls(target) == []
|
||||
|
||||
# Test with glob
|
||||
fs.get(fs_join(source, "subdir*"), target, recursive=True)
|
||||
|
||||
assert local_fs.isdir(local_join(target, "subdir"))
|
||||
assert local_fs.isfile(local_join(target, "subdir", "subfile.txt"))
|
||||
assert local_fs.isfile(local_join(target, "subdir.txt"))
|
||||
|
||||
def test_get_with_source_and_destination_as_list(
|
||||
self,
|
||||
fs,
|
||||
fs_join,
|
||||
local_fs,
|
||||
local_join,
|
||||
local_target,
|
||||
fs_10_files_with_hashed_names,
|
||||
):
|
||||
# Create the test dir
|
||||
source = fs_10_files_with_hashed_names
|
||||
target = local_target
|
||||
|
||||
# Create list of files for source and destination
|
||||
source_files = []
|
||||
destination_files = []
|
||||
for i in range(10):
|
||||
hashed_i = md5(str(i).encode("utf-8")).hexdigest()
|
||||
source_files.append(fs_join(source, f"{hashed_i}.txt"))
|
||||
destination_files.append(
|
||||
make_path_posix(local_join(target, f"{hashed_i}.txt"))
|
||||
)
|
||||
|
||||
# Copy and assert order was kept
|
||||
fs.get(rpath=source_files, lpath=destination_files)
|
||||
|
||||
for i in range(10):
|
||||
file_content = local_fs.cat(destination_files[i]).decode("utf-8")
|
||||
assert file_content == str(i)
|
||||
|
||||
|
||||
def test_maxdepth(tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen(
|
||||
{
|
||||
"dir": {
|
||||
"file1": "file1",
|
||||
"subdir": {
|
||||
"file2": "file2",
|
||||
"subdir2": {"file3": "file3", "subdir3": {"file4": "file4"}},
|
||||
},
|
||||
}
|
||||
},
|
||||
commit="add dir",
|
||||
)
|
||||
|
||||
fs = DVCFileSystem(url=tmp_dir)
|
||||
fs.get("dir", "dir1", recursive=True, maxdepth=1)
|
||||
assert (tmp_dir / "dir1").read_text() == {"file1": "file1"}
|
||||
|
||||
fs.get("dir", "dir2", recursive=True, maxdepth=2)
|
||||
assert (tmp_dir / "dir2").read_text() == {
|
||||
"file1": "file1",
|
||||
"subdir": {"file2": "file2"},
|
||||
}
|
||||
|
||||
fs.get("dir", "dir3", recursive=True, maxdepth=3)
|
||||
assert (tmp_dir / "dir3").read_text() == {
|
||||
"file1": "file1",
|
||||
"subdir": {"file2": "file2", "subdir2": {"file3": "file3"}},
|
||||
}
|
||||
|
||||
fs.get("dir", "dir4", recursive=True, maxdepth=4)
|
||||
assert (tmp_dir / "dir4").read_text() == {
|
||||
"file1": "file1",
|
||||
"subdir": {
|
||||
"file2": "file2",
|
||||
"subdir2": {"file3": "file3", "subdir3": {"file4": "file4"}},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user