mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cbf319e0d | ||
|
|
477a6e177f | ||
|
|
f854fc6365 | ||
|
|
1b90683e02 | ||
|
|
d762372d55 | ||
|
|
f3d304580a | ||
|
|
0e0d46e232 | ||
|
|
95ae018ed8 | ||
|
|
c29e721823 | ||
|
|
a9c7032d50 | ||
|
|
ecb767d622 | ||
|
|
fa45a9bbba | ||
|
|
f50ae589ba | ||
|
|
611c114f01 | ||
|
|
fdc18d5483 | ||
|
|
3b532b7d59 | ||
|
|
e78c4455de | ||
|
|
8e5f4a8f35 | ||
|
|
cb3159e586 | ||
|
|
7b301b09a1 | ||
|
|
55b16166c8 | ||
|
|
c70d0bd14d | ||
|
|
b99a0cd6ca | ||
|
|
d4170b2ec0 | ||
|
|
a3181bb509 | ||
|
|
4d6b90502b | ||
|
|
947427f4d9 | ||
|
|
0482a1a88b | ||
|
|
b470459987 | ||
|
|
81a010674e | ||
|
|
d946a1328e | ||
|
|
ad7d39da20 | ||
|
|
8432de5980 | ||
|
|
c73764156e | ||
|
|
df60df5242 | ||
|
|
504485ed6e | ||
|
|
1e989bde2c | ||
|
|
7e2248bbe5 | ||
|
|
a2d80b2403 | ||
|
|
76e7c76ae5 | ||
|
|
6cada6dc12 | ||
|
|
89d4461908 | ||
|
|
a124d7e866 | ||
|
|
628be2ae49 | ||
|
|
7a71e06946 | ||
|
|
91e54d8c27 | ||
|
|
071b2b8f81 | ||
|
|
0b2b59a601 | ||
|
|
65dc1cd08f | ||
|
|
1b103297b7 | ||
|
|
887c73b3c6 | ||
|
|
660c17f654 | ||
|
|
49da2eef02 | ||
|
|
aff65d42bb | ||
|
|
8968405770 | ||
|
|
b6e8ba6758 | ||
|
|
e307a62dc0 | ||
|
|
639eeed43b | ||
|
|
91f12395f1 | ||
|
|
e7daeb1c3e | ||
|
|
d3569035c8 | ||
|
|
daee52e176 | ||
|
|
a8415b01d0 | ||
|
|
77f56090ff | ||
|
|
1d3102f29b | ||
|
|
a0cbcaa3c8 | ||
|
|
ab780879d1 | ||
|
|
bfad82a2e3 | ||
|
|
e5eec59cb8 | ||
|
|
2a4b83060e | ||
|
|
9e2ebe9fcc | ||
|
|
2cef130191 | ||
|
|
17b9b1571e | ||
|
|
37c8ef3406 | ||
|
|
b7aea414e6 | ||
|
|
126a64618b | ||
|
|
8bdf9b1138 | ||
|
|
9cb242c8a5 | ||
|
|
11802426de | ||
|
|
b376044c45 | ||
|
|
7b3b4b373b | ||
|
|
512bfc4305 | ||
|
|
54ee8c0d77 | ||
|
|
063eb6904d | ||
|
|
3e183d62ed | ||
|
|
e815bbe211 | ||
|
|
2374c72105 | ||
|
|
5cf1421c61 | ||
|
|
a6a2feea62 | ||
|
|
6c1d00c2d9 | ||
|
|
f72456f67c | ||
|
|
2c556f58d3 | ||
|
|
460d08057c | ||
|
|
cc6deb24c8 | ||
|
|
e681030c72 | ||
|
|
7a51548214 | ||
|
|
8e99fd0091 | ||
|
|
7e5a586f85 | ||
|
|
c76a84b7b1 | ||
|
|
26008f1559 | ||
|
|
92d680ff94 | ||
|
|
c69c46d08c | ||
|
|
6477ade557 | ||
|
|
78dd045d29 | ||
|
|
2ddbbdf5a4 | ||
|
|
17a565023b | ||
|
|
b5d5728bf8 | ||
|
|
055641a51e | ||
|
|
fc509d4e4a | ||
|
|
a67fea2235 | ||
|
|
f062b52e05 | ||
|
|
25166f3e02 | ||
|
|
a79ea15827 | ||
|
|
8db9a2f70f | ||
|
|
ff2f2e433f | ||
|
|
3fc9f323a7 | ||
|
|
bf0aa5e8aa | ||
|
|
3b3866323e |
@@ -1 +0,0 @@
|
||||
* @iterative/dvc
|
||||
+50
-110
@@ -6,152 +6,92 @@ on:
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-18.04
|
||||
binary:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
setup.cfg
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build deb
|
||||
run: python scripts/build.py deb
|
||||
- name: Publish deb
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_content_type: binary/octet-stream
|
||||
rpm:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build rpm
|
||||
run: python scripts/build.py rpm
|
||||
- name: Publish rpm
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_content_type: binary/octet-stream
|
||||
osxpkg:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
# available lxml (webdav dependency) wheels are built with an old
|
||||
# SDK, which breaks osxpkg notarization. Building from scratch
|
||||
# instead.
|
||||
pip install .[all] --no-binary lxml
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build
|
||||
run: python scripts/build.py osxpkg
|
||||
- name: Publish
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_content_type: binary/octet-stream
|
||||
exe:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install -U wheel pip setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
dvc pull
|
||||
- name: Build
|
||||
run: python scripts\build.py exe
|
||||
shell: cmd
|
||||
- name: Publish
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
|
||||
- name: Install
|
||||
run: pip install ".[all,tests]"
|
||||
run: |
|
||||
pip install -U wheel pip build setuptools
|
||||
pip install ".[all,tests]"
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
@@ -43,6 +43,12 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
pytest-filter:
|
||||
- "import or plot or live or experiment"
|
||||
- "not (import or plot or live or experiment)"
|
||||
include:
|
||||
- {os: ubuntu-latest, pyv: "3.11-dev"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -68,12 +74,11 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install -e ".[dev]"
|
||||
env:
|
||||
AIOHTTP_NO_EXTENSIONS: ${{ matrix.pyv == '3.11-dev' && '1' }}
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: >-
|
||||
python -m tests -n=auto
|
||||
--cov-report=xml --cov-report=term
|
||||
${{ env.extra_test_args }}
|
||||
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
||||
+5
-1
@@ -11,7 +11,7 @@
|
||||
• `Contributing`_
|
||||
• `Community and Support`_
|
||||
|
||||
|CI| |Maintainability| |Coverage| |VS Code| |DOI|
|
||||
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|
||||
|
||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|
||||
@@ -248,6 +248,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: Python Version
|
||||
|
||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_DESC: str,
|
||||
Annotation.PARAM_TYPE: str,
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
+12
-6
@@ -1,9 +1,15 @@
|
||||
from .data import ( # noqa, pylint: disable=redefined-builtin
|
||||
get_url,
|
||||
open,
|
||||
read,
|
||||
)
|
||||
from dvc.fs.dvc import _DvcFileSystem as DvcFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
|
||||
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
|
||||
__all__ = [
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"read",
|
||||
"DvcFileSystem",
|
||||
]
|
||||
|
||||
@@ -37,6 +37,10 @@ def params_show(
|
||||
stages to retrieve parameters from.
|
||||
Defaults to `None`.
|
||||
If `None`, all parameters from all stages will be retrieved.
|
||||
If this method is called from a different location to the one where
|
||||
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
|
||||
be provided as a prefix with the syntax `{relpath}:{stage}`.
|
||||
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
|
||||
from.
|
||||
Defaults to `None`.
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
)
|
||||
return None
|
||||
|
||||
from dvc_data.build import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
||||
|
||||
if isinstance(exc, IgnoreInCollectedDirError):
|
||||
logger.exception("")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
+36
-9
@@ -2,12 +2,43 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.exceptions import (
|
||||
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
+132
-20
@@ -1,9 +1,12 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.ui import ui
|
||||
@@ -31,15 +34,22 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_cache": 'use "dvc pull <file>..." '
|
||||
"to update your local storage",
|
||||
"committed": "git commit the corresponding dvc files "
|
||||
"to update the repo",
|
||||
"uncommitted": 'use "dvc commit <file>..." to track changes',
|
||||
"untracked": 'use "git add <file> ..." or '
|
||||
'dvc add <file>..." to commit to git or to dvc',
|
||||
"git_dirty": "there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": (
|
||||
"git commit the corresponding dvc files to update the repo",
|
||||
),
|
||||
"uncommitted": (
|
||||
'use "dvc commit <file>..." to track changes',
|
||||
'use "dvc checkout <file>..." to discard changes',
|
||||
),
|
||||
"untracked": (
|
||||
'use "git add <file> ..." or '
|
||||
'"dvc add <file>..." to commit to git or to dvc',
|
||||
),
|
||||
"git_dirty": (
|
||||
"there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -76,8 +86,9 @@ class CmdDataStatus(CmdBase):
|
||||
color = cls.COLORS.get(stage, None)
|
||||
|
||||
ui.write(header)
|
||||
if hint := cls.HINTS.get(stage):
|
||||
ui.write(f" ({hint})")
|
||||
if hints := cls.HINTS.get(stage):
|
||||
for hint in hints:
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
@@ -92,9 +103,10 @@ class CmdDataStatus(CmdBase):
|
||||
out = "\n".join(tabs + item for item in chunk)
|
||||
ui.write(colorize(out, color))
|
||||
|
||||
if (hint := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
for hint in hints:
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
return 0
|
||||
|
||||
def run(self) -> int:
|
||||
@@ -102,7 +114,6 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
with_dirs=self.args.with_dirs,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -116,6 +127,65 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(
|
||||
self.repo, targets=self.args.targets, recursive=self.args.recursive
|
||||
)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -172,10 +242,52 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--with-dirs",
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=DATA_LS_HELP,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
|
||||
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
self.log_summary(getattr(exc, "stats", {}))
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
elif diff:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
precision = self.args.precision or DEFAULT_PRECISION
|
||||
|
||||
+17
-10
@@ -19,7 +19,11 @@ class CmdImport(CmdBase):
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -33,6 +37,8 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -69,20 +75,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-j",
|
||||
@@ -94,4 +99,6 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+17
-11
@@ -17,9 +17,13 @@ class CmdImportUrl(CmdBase):
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -34,6 +38,8 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download or copy file from URL and take it under DVC control."
|
||||
)
|
||||
@@ -66,11 +72,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
@@ -94,13 +108,5 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
+24
-32
@@ -155,11 +155,6 @@ class CmdPlots(CmdBase):
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
return 0
|
||||
if self.args.json:
|
||||
renderers = _filter_unhandled_renderers(renderers)
|
||||
_show_json(renderers, self.args.split)
|
||||
@@ -167,6 +162,12 @@ class CmdPlots(CmdBase):
|
||||
|
||||
_adjust_vega_renderers(renderers)
|
||||
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
return 0
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
|
||||
if renderers:
|
||||
@@ -222,31 +223,22 @@ class CmdPlotsModify(CmdPlots):
|
||||
|
||||
|
||||
class CmdPlotsTemplates(CmdBase):
|
||||
TEMPLATES_CHOICES = [
|
||||
"simple",
|
||||
"linear",
|
||||
"confusion",
|
||||
"confusion_normalized",
|
||||
"scatter",
|
||||
"smooth",
|
||||
"bar_horizontal_sorted",
|
||||
"bar_horizontal",
|
||||
]
|
||||
|
||||
def run(self):
|
||||
from dvc_render.vega_templates import dump_templates
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc_render.vega_templates import TEMPLATES
|
||||
|
||||
try:
|
||||
out = (
|
||||
os.path.join(os.getcwd(), self.args.out)
|
||||
if self.args.out
|
||||
else self.repo.plots.templates_dir
|
||||
)
|
||||
target = self.args.template
|
||||
if target:
|
||||
for template in TEMPLATES:
|
||||
if target == template.DEFAULT_NAME:
|
||||
ui.write_json(template.DEFAULT_CONTENT)
|
||||
return 0
|
||||
raise InvalidArgumentError(f"Unexpected template: {target}.")
|
||||
|
||||
targets = [self.args.target] if self.args.target else None
|
||||
dump_templates(output=out, targets=targets)
|
||||
templates_path = os.path.relpath(out, os.getcwd())
|
||||
ui.write(f"Templates have been written into '{templates_path}'.")
|
||||
else:
|
||||
for template in TEMPLATES:
|
||||
ui.write(template.DEFAULT_NAME)
|
||||
|
||||
return 0
|
||||
except DvcException:
|
||||
@@ -356,8 +348,7 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
TEMPLATES_HELP = (
|
||||
"Write built-in plots templates to a directory "
|
||||
"(.dvc/plots by default)."
|
||||
"List built-in plots templates or show JSON specification for one."
|
||||
)
|
||||
plots_templates_parser = plots_subparsers.add_parser(
|
||||
"templates",
|
||||
@@ -367,13 +358,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_templates_parser.add_argument(
|
||||
"target",
|
||||
"template",
|
||||
default=None,
|
||||
nargs="?",
|
||||
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
|
||||
help="Template to write. Writes all templates by default.",
|
||||
help=(
|
||||
"Template for which to show JSON specification. "
|
||||
"List all template names by default."
|
||||
),
|
||||
)
|
||||
_add_output_argument(plots_templates_parser, typ="templates")
|
||||
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
|
||||
# removing duplicates while maintaining order
|
||||
collected = chain.from_iterable(
|
||||
self.repo.stage.collect(
|
||||
target=target, recursive=self.args.recursive, accept_group=True
|
||||
target=target, recursive=self.args.recursive
|
||||
)
|
||||
for target in self.args.targets
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
to_remote=self.args.to_remote,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
@@ -55,6 +56,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Update all stages in the specified directory.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update .dvc file hash value(s)"
|
||||
" but do not download the file(s).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
|
||||
@@ -78,7 +78,9 @@ REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
"jobs": All(Coerce(int), Range(1)),
|
||||
Optional("worktree"): Bool,
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
@@ -289,4 +291,9 @@ SCHEMA = {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
"list": All(Lower, Choices("nargs", "append")),
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
}
|
||||
|
||||
+40
-14
@@ -3,16 +3,37 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from dvc_data.db import get_index
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc_data.hashfile.db import get_index
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.status import CompareStatusResult
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, path, fs, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
|
||||
self.worktree = config.pop("worktree", False)
|
||||
self.config = config
|
||||
|
||||
@cached_property
|
||||
def odb(self):
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
"""Class that manages dvc remotes.
|
||||
|
||||
@@ -27,18 +48,23 @@ class DataCloud:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
def get_remote_odb(
|
||||
def get_remote(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
if name:
|
||||
return self._init_odb(name)
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(fs_path, fs, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
@@ -55,13 +81,13 @@ class DataCloud:
|
||||
|
||||
raise NoRemoteError(error_msg)
|
||||
|
||||
def _init_odb(self, name):
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc_data.db import get_odb
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), fs_path, **config)
|
||||
def get_remote_odb(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
if status.missing:
|
||||
@@ -82,7 +108,7 @@ class DataCloud:
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.exceptions import FileTransferError
|
||||
from dvc_data.transfer import TransferError, transfer
|
||||
from dvc_data.hashfile.transfer import TransferError, transfer
|
||||
|
||||
try:
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
@@ -164,7 +190,7 @@ class DataCloud:
|
||||
log_missing: log warning messages if file doesn't exist
|
||||
neither in cache, neither in cloud.
|
||||
"""
|
||||
from dvc_data.status import compare_status
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
|
||||
@@ -18,7 +18,7 @@ SCHEMA: Mapping[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
def _get(stage, p, info):
|
||||
def _get(stage, p, info, fs_config=None):
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
@@ -27,7 +27,7 @@ def _get(stage, p, info):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
return Dependency(stage, p, info)
|
||||
return Dependency(stage, p, info, fs_config=fs_config)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
@@ -38,10 +38,18 @@ def loadd_from(stage, d_list):
|
||||
return ret
|
||||
|
||||
|
||||
def loads_from(stage, s_list, erepo=None):
|
||||
def loads_from(stage, s_list, erepo=None, fs_config=None):
|
||||
assert isinstance(s_list, list)
|
||||
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
|
||||
return [_get(stage, s, info.copy()) for s in s_list]
|
||||
return [
|
||||
_get(
|
||||
stage,
|
||||
s,
|
||||
info.copy(),
|
||||
fs_config=fs_config,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
|
||||
|
||||
def _merge_params(s_list):
|
||||
|
||||
@@ -39,14 +39,15 @@ class ParamsDependency(Dependency):
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
self.params = list(params) if params else []
|
||||
info = (
|
||||
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
|
||||
)
|
||||
hash_info = HashInfo()
|
||||
if isinstance(params, dict):
|
||||
hash_info = HashInfo(self.PARAM_PARAMS, params)
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, info=info, repo=repo)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
ret = super().dumpd()
|
||||
if not self.hash_info:
|
||||
ret[self.PARAM_PARAMS] = self.params or {}
|
||||
|
||||
@@ -59,11 +59,11 @@ class RepoDependency(Dependency):
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
@@ -103,8 +103,8 @@ class RepoDependency(Dependency):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.build import build
|
||||
from dvc_data.objects.tree import Tree, TreeError
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
@@ -153,8 +153,8 @@ class RepoDependency(Dependency):
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc_data.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceHashFileDB):
|
||||
return
|
||||
|
||||
+6
-3
@@ -198,7 +198,7 @@ class SingleStageFile(FileMixin):
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug("Saving information to '%s'.", relpath(self.path))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage): # pylint: disable=unused-argument
|
||||
@@ -243,9 +243,11 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.dump(stage)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage):
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
|
||||
raise ParametrizedDumpError(
|
||||
f"cannot {action} a parametrized {stage}"
|
||||
)
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.remove()
|
||||
|
||||
def remove_stage(self, stage):
|
||||
self._check_if_parametrized(stage, "remove")
|
||||
self._lockfile.remove_stage(stage)
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
+17
-25
@@ -10,6 +10,8 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
Schemes,
|
||||
generic,
|
||||
get_fs_cls,
|
||||
known_implementations,
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
)
|
||||
@@ -19,33 +21,26 @@ from dvc_objects.fs.errors import ( # noqa: F401
|
||||
ConfigError,
|
||||
RemoteMissingDepsError,
|
||||
)
|
||||
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
|
||||
from dvc_objects.fs.implementations.azure import AzureFileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.gdrive import ( # noqa: F401
|
||||
GDriveFileSystem,
|
||||
)
|
||||
from dvc_objects.fs.implementations.gs import GSFileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.hdfs import HDFSFileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.local import localfs # noqa: F401
|
||||
from dvc_objects.fs.implementations.oss import OSSFileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.s3 import S3FileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.ssh import ( # noqa: F401
|
||||
DEFAULT_PORT as DEFAULT_SSH_PORT,
|
||||
)
|
||||
from dvc_objects.fs.implementations.ssh import SSHFileSystem # noqa: F401
|
||||
from dvc_objects.fs.implementations.webdav import ( # noqa: F401
|
||||
WebDAVFileSystem,
|
||||
WebDAVSFileSystem,
|
||||
)
|
||||
from dvc_objects.fs.implementations.webhdfs import ( # noqa: F401
|
||||
WebHDFSFileSystem,
|
||||
)
|
||||
from dvc_objects.fs.path import Path # noqa: F401
|
||||
|
||||
from .data import DataFileSystem # noqa: F401
|
||||
from .dvc import DvcFileSystem # noqa: F401
|
||||
from .git import GitFileSystem # noqa: F401
|
||||
|
||||
known_implementations.update(
|
||||
{
|
||||
"dvc": {
|
||||
"class": "dvc.fs.dvc.DvcFileSystem",
|
||||
"err": "dvc is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
"git": {
|
||||
"class": "dvc.fs.git.GitFileSystem",
|
||||
"err": "git is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
@@ -116,11 +111,8 @@ def get_cloud_fs(repo, **kwargs):
|
||||
|
||||
cls = get_fs_cls(remote_conf)
|
||||
|
||||
if cls == GDriveFileSystem and repo:
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
url = remote_conf.pop("url")
|
||||
if issubclass(cls, WebDAVFileSystem):
|
||||
if cls.protocol in ["webdav", "webdavs"]:
|
||||
# For WebDAVFileSystem, provided url is the base path itself, so it
|
||||
# should be treated as being a root path.
|
||||
fs_path = cls.root_marker
|
||||
|
||||
+119
-203
@@ -4,12 +4,12 @@ import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from itertools import takewhile
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from funcy import cached_property, wrap_prop, wrap_with
|
||||
|
||||
from dvc.utils.fs import makedirs
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||
from dvc_objects.fs.path import Path
|
||||
@@ -22,29 +22,19 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||
Key = Tuple[str, ...]
|
||||
|
||||
|
||||
def as_posix(path: str) -> str:
|
||||
return path.replace(ntpath.sep, posixpath.sep)
|
||||
|
||||
|
||||
def _wrap_walk(dvc_fs, *args, **kwargs):
|
||||
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
|
||||
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
|
||||
# NOT the same as dvc.dvcfile.is_dvc_file()!
|
||||
def _is_dvc_file(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
|
||||
def _ls(fs, path):
|
||||
dnames = []
|
||||
fnames = []
|
||||
|
||||
for entry in fs.ls(path, detail=True):
|
||||
name = fs.path.name(entry["name"])
|
||||
if entry["type"] == "directory":
|
||||
dnames.append(name)
|
||||
else:
|
||||
fnames.append(name)
|
||||
|
||||
return dnames, fnames
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
@@ -70,6 +60,10 @@ def _merge_info(repo, fs_info, dvc_info):
|
||||
return ret
|
||||
|
||||
|
||||
def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
|
||||
|
||||
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
@@ -82,28 +76,25 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
root_marker = "/"
|
||||
|
||||
PARAM_REPO_URL = "repo_url"
|
||||
PARAM_REPO_ROOT = "repo_root"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_CACHE_DIR = "cache_dir"
|
||||
PARAM_CACHE_TYPES = "cache_types"
|
||||
PARAM_SUBREPOS = "subrepos"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos=False,
|
||||
repo_factory: RepoFactory = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
**repo_kwargs: Any,
|
||||
) -> None:
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
if repo is None:
|
||||
repo, repo_factory = self._repo_from_fs_config(
|
||||
subrepos=subrepos, **kwargs
|
||||
repo = self._make_repo(
|
||||
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
|
||||
)
|
||||
assert repo
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -137,78 +128,35 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path) -> Key:
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
return parts
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
return parts
|
||||
|
||||
def _from_key(self, parts: Key) -> str:
|
||||
return self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
if self.repo is None:
|
||||
return None
|
||||
return self.repo.url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
self.PARAM_REPO_URL: self.repo_url,
|
||||
self.PARAM_REPO_ROOT: self.repo.root_dir,
|
||||
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
|
||||
self.PARAM_CACHE_DIR: os.path.abspath(self.repo.odb.local.path),
|
||||
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
|
||||
self.PARAM_SUBREPOS: self._traverse_subrepos,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _repo_from_fs_config(
|
||||
cls, **config
|
||||
) -> Tuple["Repo", Optional["RepoFactory"]]:
|
||||
from dvc.external_repo import erepo_factory, external_repo
|
||||
def _make_repo(cls, **kwargs) -> "Repo":
|
||||
from dvc.repo import Repo
|
||||
|
||||
url = config.get(cls.PARAM_REPO_URL)
|
||||
root = config.get(cls.PARAM_REPO_ROOT)
|
||||
assert url or root
|
||||
with Repo.open(uninitialized=True, **kwargs) as repo:
|
||||
return repo
|
||||
|
||||
def _open(*args, **kwargs):
|
||||
# NOTE: if original repo was an erepo (and has a URL),
|
||||
# we cannot use Repo.open() since it will skip erepo
|
||||
# cache/remote setup for local URLs
|
||||
if url is None:
|
||||
return Repo.open(*args, **kwargs)
|
||||
return external_repo(*args, **kwargs)
|
||||
|
||||
cache_dir = config.get(cls.PARAM_CACHE_DIR)
|
||||
cache_config = (
|
||||
{}
|
||||
if not cache_dir
|
||||
else {
|
||||
"cache": {
|
||||
"dir": cache_dir,
|
||||
"type": config.get(cls.PARAM_CACHE_TYPES),
|
||||
}
|
||||
}
|
||||
)
|
||||
repo_kwargs: dict = {
|
||||
"rev": config.get(cls.PARAM_REV),
|
||||
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
|
||||
"uninitialized": True,
|
||||
}
|
||||
factory: Optional["RepoFactory"] = None
|
||||
if url is None:
|
||||
repo_kwargs["config"] = cache_config
|
||||
else:
|
||||
repo_kwargs["cache_dir"] = cache_dir
|
||||
factory = erepo_factory(url, root, cache_config)
|
||||
|
||||
with _open(
|
||||
url if url else root,
|
||||
**repo_kwargs,
|
||||
) as repo:
|
||||
return repo, factory
|
||||
|
||||
def _get_repo(self, path: str) -> "Repo":
|
||||
def _get_repo(self, key: Key) -> "Repo":
|
||||
"""Returns repo that the path falls in, using prefix.
|
||||
|
||||
If the path is already tracked/collected, it just returns the repo.
|
||||
@@ -216,33 +164,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
Otherwise, it collects the repos that might be in the path's parents
|
||||
and then returns the appropriate one.
|
||||
"""
|
||||
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
||||
# outside of repo
|
||||
return self.repo
|
||||
|
||||
key = self._get_key(path)
|
||||
repo = self._subrepos_trie.get(key)
|
||||
if repo:
|
||||
return repo
|
||||
|
||||
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
|
||||
prefix = self.repo.fs.path.join(
|
||||
self.repo.root_dir,
|
||||
*prefix_key, # pylint: disable=not-an-iterable
|
||||
)
|
||||
|
||||
parents = (parent for parent in self.repo.fs.path.parents(path))
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
|
||||
self._update(dir_keys, starting_repo=repo)
|
||||
return self._subrepos_trie.get(key) or self.repo
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _update(self, dirs, starting_repo):
|
||||
def _update(self, dir_keys, starting_repo):
|
||||
"""Checks for subrepo in directories and updates them."""
|
||||
repo = starting_repo
|
||||
for d in dirs:
|
||||
key = self._get_key(d)
|
||||
for key in dir_keys:
|
||||
d = self._from_key(key)
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(
|
||||
d,
|
||||
@@ -265,79 +201,64 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self.repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
) -> Tuple[
|
||||
Optional["Repo"],
|
||||
Optional[FileSystem],
|
||||
Optional[str],
|
||||
Optional[DataFileSystem],
|
||||
Optional[str],
|
||||
]:
|
||||
def _get_subrepo_info(
|
||||
self, key: Key
|
||||
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
|
||||
"""
|
||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||
Returns information about the subrepo the key is part of.
|
||||
"""
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
|
||||
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
repo = self._get_repo(fs_path)
|
||||
fs = repo.fs
|
||||
|
||||
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
|
||||
if repo_parts[0] == os.curdir:
|
||||
repo_parts = repo_parts[1:]
|
||||
|
||||
dvc_parts = parts[len(repo_parts) :]
|
||||
if dvc_parts and dvc_parts[0] == os.curdir:
|
||||
dvc_parts = dvc_parts[1:]
|
||||
|
||||
key = self._get_key(repo.root_dir)
|
||||
dvc_fs = self._datafss.get(key)
|
||||
if dvc_fs:
|
||||
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
|
||||
repo = self._get_repo(key)
|
||||
repo_key: Key
|
||||
if repo is self.repo:
|
||||
repo_key = ()
|
||||
subkey = key
|
||||
else:
|
||||
dvc_path = None
|
||||
repo_key = self._get_key(repo.root_dir)
|
||||
subkey = key[len(repo_key) :]
|
||||
|
||||
return repo, fs, fs_path, dvc_fs, dvc_path
|
||||
dvc_fs = self._datafss.get(repo_key)
|
||||
return repo, dvc_fs, subkey
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding="utf-8", **kwargs
|
||||
def _open(
|
||||
self, path, mode="rb", **kwargs
|
||||
): # pylint: disable=arguments-renamed, arguments-differ
|
||||
if "b" in mode:
|
||||
encoding = None
|
||||
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return fs.open(fs_path, mode=mode, encoding=encoding)
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
_, _, _, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
names = set()
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for entry in dvc_fs.ls(dvc_path, detail=False):
|
||||
names.add(dvc_fs.path.name(entry))
|
||||
|
||||
if not dvc_only and fs:
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
for entry in dvcignore.ls(
|
||||
for entry in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
names.add(fs.path.name(entry))
|
||||
@@ -345,26 +266,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
def _func(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
if dvcfiles:
|
||||
return True
|
||||
|
||||
return not (
|
||||
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
)
|
||||
|
||||
names = filter(_func, names)
|
||||
if not dvcfiles:
|
||||
names = (name for name in names if not _is_dvc_file(name))
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
for name in names:
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_key = key + (name,)
|
||||
try:
|
||||
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
|
||||
info = self._info(
|
||||
entry_key,
|
||||
entry_path,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
check_ignored=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
infos.append(info)
|
||||
@@ -378,45 +294,57 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
fs = self.repo.fs
|
||||
|
||||
if fs:
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
if self.isdir(rpath):
|
||||
makedirs(lpath, exist_ok=True)
|
||||
return None
|
||||
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
key = self._get_key_from_relative(path)
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
if dvc_fs:
|
||||
try:
|
||||
dvc_info = dvc_fs.info(dvc_path)
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
if fs:
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if check_ignored and repo.dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
|
||||
# NOTE: if some parent in fs_path turns out to be a file, it means
|
||||
# that the whole repofs branch doesn't exist.
|
||||
if fs and not fs_info and dvc_info:
|
||||
if dvc_info and not fs_info:
|
||||
for parent in fs.path.parents(fs_path):
|
||||
try:
|
||||
if fs.info(parent)["type"] != "directory":
|
||||
@@ -432,14 +360,6 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def checksum(self, path):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.checksum(fs_path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.checksum(dvc_path)
|
||||
|
||||
|
||||
class DvcFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
@@ -468,10 +388,6 @@ class DvcFileSystem(FileSystem):
|
||||
def repo_url(self):
|
||||
return self.fs.repo_url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.fs.config
|
||||
|
||||
def from_os_path(self, path):
|
||||
if os.path.isabs(path):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+27
-2
@@ -14,6 +14,14 @@ from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
@@ -23,6 +31,7 @@ def get_dvc_info():
|
||||
"---------------------------------",
|
||||
f"Platform: Python {platform.python_version()} on "
|
||||
f"{platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
]
|
||||
|
||||
@@ -88,13 +97,29 @@ def _get_linktype_support_info(repo):
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
def _get_subprojects():
|
||||
subprojects = []
|
||||
for subproject in SUBPROJECTS:
|
||||
try:
|
||||
version = importlib_metadata.version(subproject)
|
||||
subprojects.append(f"{subproject} = {version}")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return "\n\t" + "\n\t".join(subprojects)
|
||||
|
||||
|
||||
def _get_supported_remotes():
|
||||
supported_remotes = []
|
||||
for scheme in registry:
|
||||
if scheme in [Schemes.LOCAL, Schemes.MEMORY]:
|
||||
if scheme in [Schemes.LOCAL, Schemes.MEMORY, "dvc", "git"]:
|
||||
continue
|
||||
|
||||
try:
|
||||
fs_cls = registry[scheme]
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
fs_cls = registry[scheme]
|
||||
if not fs_cls.get_missing_deps():
|
||||
dependencies = []
|
||||
for requirement in fs_cls.REQUIRES:
|
||||
|
||||
@@ -3,7 +3,8 @@ from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import SSHFileSystem
|
||||
from dvc_ssh import SSHFileSystem
|
||||
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ from contextlib import contextmanager
|
||||
from functools import partial, partialmethod
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .base import BaseMachineBackend
|
||||
@@ -68,7 +69,7 @@ class TerraformBackend(BaseMachineBackend):
|
||||
resource = tpi.default_resource(name)
|
||||
return {
|
||||
"host": resource["instance_ip"],
|
||||
"port": DEFAULT_SSH_PORT,
|
||||
"port": DEFAULT_PORT,
|
||||
"username": "ubuntu",
|
||||
"fs_factory": partial(_sshfs, dict(resource)),
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.db import get_odb
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
|
||||
+155
-54
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, project
|
||||
from funcy import cached_property, collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc import prompt
|
||||
@@ -17,29 +17,25 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc_data import Tree
|
||||
from dvc_data import check as ocheck
|
||||
from dvc_data import load as oload
|
||||
from dvc_data.build import build
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .fs import (
|
||||
HDFSFileSystem,
|
||||
LocalFileSystem,
|
||||
RemoteMissingDepsError,
|
||||
S3FileSystem,
|
||||
Schemes,
|
||||
get_cloud_fs,
|
||||
)
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,10 +61,12 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
#
|
||||
# so when a few types of outputs share the same name, we only need
|
||||
# specify it once.
|
||||
HDFS_PARAM_CHECKSUM = "checksum"
|
||||
S3_PARAM_CHECKSUM = "etag"
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +83,10 @@ def loadd_from(stage, d_list):
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
desc = d.pop(Output.PARAM_DESC, False)
|
||||
live = d.pop(Output.PARAM_LIVE, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -98,9 +97,10 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
desc=desc,
|
||||
live=live,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -192,6 +192,7 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -245,6 +246,7 @@ class Output:
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_CHECKPOINT = "checkpoint"
|
||||
PARAM_FILES = "files"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
@@ -257,7 +259,6 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
@@ -289,11 +290,29 @@ class Output:
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
remote=None,
|
||||
repo=None,
|
||||
fs_config=None,
|
||||
files: List[Dict[str, Any]] = None,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
)
|
||||
self.repo = stage.repo if not repo and stage else repo
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
|
||||
meta = Meta.from_dict(info or {})
|
||||
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
||||
# set based on whether or not path is versioned
|
||||
fs_kwargs = {"version_aware": True} if meta.version_id else {}
|
||||
|
||||
if fs_config is not None:
|
||||
fs_kwargs.update(**fs_config)
|
||||
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(
|
||||
self.repo, url=path, **fs_kwargs
|
||||
)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -325,21 +344,38 @@ class Output:
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.meta = meta
|
||||
self.files = files
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.desc = desc
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj = None
|
||||
self.obj: Optional["HashFile"] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
if self.fs.version_aware:
|
||||
_, version_id = self.fs.path.coalesce_version(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
|
||||
if self.is_in_repo:
|
||||
self.hash_name = "md5"
|
||||
else:
|
||||
self.hash_name = self.fs.PARAM_CHECKSUM
|
||||
|
||||
self.hash_info = HashInfo(
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
if self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
parsed = urlparse(self.def_path)
|
||||
if (
|
||||
@@ -380,6 +416,12 @@ class Output:
|
||||
|
||||
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
def clear(self):
|
||||
self.hash_info = HashInfo.from_dict({})
|
||||
self.meta = Meta.from_dict({})
|
||||
self.obj = None
|
||||
self.files = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.fs.protocol
|
||||
@@ -419,21 +461,27 @@ class Output:
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
_, hash_info = self._get_hash_meta()
|
||||
return hash_info
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.odb
|
||||
name = self.odb.fs.PARAM_CHECKSUM
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, _, obj = build(
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
name,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
return obj.hash_info
|
||||
return meta, obj.hash_info
|
||||
|
||||
def get_meta(self) -> Meta:
|
||||
meta, _ = self._get_hash_meta()
|
||||
return meta
|
||||
|
||||
@property
|
||||
def is_dir_checksum(self):
|
||||
@@ -452,6 +500,17 @@ class Output:
|
||||
|
||||
return self.fs.exists(self.fs_path)
|
||||
|
||||
@cached_property
|
||||
def index_key(self) -> Tuple[str, "DataIndexKey"]:
|
||||
if self.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
|
||||
else:
|
||||
workspace = self.fs.protocol
|
||||
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
|
||||
key = self.fs.path.parts(no_drive)[1:]
|
||||
return workspace, key
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
|
||||
@@ -469,6 +528,11 @@ class Output:
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return True
|
||||
|
||||
def changed_meta(self) -> bool:
|
||||
if self.fs.version_aware and self.meta.version_id:
|
||||
return self.meta.version_id == self.get_meta().version_id
|
||||
return False
|
||||
|
||||
def workspace_status(self):
|
||||
if not self.exists:
|
||||
return {str(self): "deleted"}
|
||||
@@ -551,11 +615,12 @@ class Output:
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.debug(
|
||||
"Output '%s' doesn't use cache. Skipping saving.", self
|
||||
@@ -568,18 +633,19 @@ class Output:
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
def set_exec(self):
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.odb.set_exec(self.fs_path)
|
||||
|
||||
def _checkout(self, *args, **kwargs):
|
||||
from dvc_data.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.checkout import LinkError, PromptError
|
||||
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
try:
|
||||
@@ -610,7 +676,7 @@ class Output:
|
||||
self.odb,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
otransfer(
|
||||
@@ -639,7 +705,7 @@ class Output:
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
@@ -653,8 +719,10 @@ class Output:
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self):
|
||||
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
|
||||
def dumpd(self, **kwargs):
|
||||
meta = self.meta.to_dict()
|
||||
meta.pop("isdir", None)
|
||||
ret = {**self.hash_info.to_dict(), **meta}
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.fs.path.as_posix(
|
||||
@@ -668,9 +736,7 @@ class Output:
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
|
||||
if self.desc:
|
||||
ret[self.PARAM_DESC] = self.desc
|
||||
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
@@ -699,6 +765,18 @@ class Output:
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if (
|
||||
self.use_cache
|
||||
and self.is_in_repo
|
||||
and self.hash_info.isdir
|
||||
and (kwargs.get("with_files") or self.files is not None)
|
||||
):
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
else:
|
||||
obj = self.get_obj()
|
||||
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
|
||||
|
||||
return ret
|
||||
|
||||
def verify_metric(self):
|
||||
@@ -731,14 +809,22 @@ class Output:
|
||||
) as cb:
|
||||
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.hash_info:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if self.files:
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest()
|
||||
obj = tree
|
||||
else:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -858,6 +944,7 @@ class Output:
|
||||
)
|
||||
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
return obj
|
||||
|
||||
def get_files_number(self, filter_info=None):
|
||||
@@ -928,6 +1015,7 @@ class Output:
|
||||
|
||||
obj = self.get_obj()
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
@@ -1020,7 +1108,7 @@ class Output:
|
||||
other = out.dumpd()
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
@@ -1040,8 +1128,8 @@ class Output:
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc_data.objects.tree import MergeError as TreeMergeError
|
||||
from dvc_data.objects.tree import du, merge
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1064,6 +1152,7 @@ class Output:
|
||||
self.odb.add(merged.path, merged.fs, merged.oid)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.files = None
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
nfiles=len(merged),
|
||||
@@ -1086,21 +1175,33 @@ class Output:
|
||||
return bool(self.plot) or bool(self.live)
|
||||
|
||||
|
||||
META_SCHEMA = {
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Meta.PARAM_VERSION_ID: str,
|
||||
}
|
||||
|
||||
ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
DIR_FILES_SCHEMA: Dict[str, Any] = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Tree.PARAM_RELPATH): str,
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC):
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
|
||||
+40
-1
@@ -3,7 +3,7 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.repo.scm_context import SCMContext
|
||||
from dvc.scm import Base
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -444,6 +445,44 @@ class Repo:
|
||||
|
||||
return used
|
||||
|
||||
def partial_imports(
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
commit_date: Optional[str] = None,
|
||||
recursive=False,
|
||||
revs=None,
|
||||
num=1,
|
||||
) -> List["Stage"]:
|
||||
"""Get the stages related to the given target and collect dependencies
|
||||
which are missing outputs.
|
||||
|
||||
This is useful to retrieve files which have been imported to the repo
|
||||
using --no-download.
|
||||
|
||||
Returns:
|
||||
A list of partially imported stages
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
partial_imports = chain.from_iterable(
|
||||
self.index.partial_imports(targets, recursive=recursive)
|
||||
for _ in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
)
|
||||
)
|
||||
|
||||
return list(partial_imports)
|
||||
|
||||
@property
|
||||
def stages(self): # obsolete, only for backward-compatibility
|
||||
return self.index.stages
|
||||
|
||||
+3
-2
@@ -264,6 +264,7 @@ def create_stages(
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
if kwargs.get("desc"):
|
||||
stage.outs[0].desc = kwargs["desc"]
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
|
||||
+136
-79
@@ -1,7 +1,21 @@
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, cast
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
cast,
|
||||
)
|
||||
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -10,19 +24,20 @@ if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
|
||||
|
||||
def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
if not obj:
|
||||
return False
|
||||
if not obj.hash_info.value:
|
||||
return False
|
||||
|
||||
assert obj.value
|
||||
try:
|
||||
cache.check(obj.hash_info.value)
|
||||
cache.check(obj.value)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
@@ -30,31 +45,30 @@ def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
|
||||
def _shallow_diff(
|
||||
root: str,
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
old: Optional["HashInfo"],
|
||||
new: Optional["HashInfo"],
|
||||
cache: "HashFileDB",
|
||||
) -> Dict[str, List[str]]:
|
||||
# TODO: add support for shallow diff in dvc-data
|
||||
# TODO: we may want to recursively do in_cache check
|
||||
d = {}
|
||||
|
||||
from dvc_data.objects.tree import Tree
|
||||
old_root = new_root = root
|
||||
if old and old.isdir:
|
||||
old_root = os.path.sep.join([root, ""])
|
||||
if new and new.isdir:
|
||||
new_root = os.path.sep.join([root, ""])
|
||||
|
||||
if isinstance(new_obj, Tree):
|
||||
root = os.path.sep.join([root, ""])
|
||||
if old and not _in_cache(old, cache):
|
||||
d["not_in_cache"] = [old_root]
|
||||
|
||||
if not _in_cache(old_obj, cache):
|
||||
d["not_in_cache"] = [root]
|
||||
|
||||
if old_obj is None and new_obj is None:
|
||||
if not old and not new:
|
||||
return d
|
||||
if old_obj is None:
|
||||
return {"added": [root], **d}
|
||||
if new_obj is None:
|
||||
return {"deleted": [root], **d}
|
||||
if old_obj.hash_info != new_obj.hash_info:
|
||||
return {"modified": [root], **d}
|
||||
return {"unchanged": [root], **d}
|
||||
if not new:
|
||||
return {"deleted": [old_root], **d}
|
||||
if not old:
|
||||
return {"added": [new_root], **d}
|
||||
if old != new:
|
||||
return {"modified": [new_root], **d}
|
||||
return {"unchanged": [new_root], **d}
|
||||
|
||||
|
||||
def _granular_diff(
|
||||
@@ -62,19 +76,12 @@ def _granular_diff(
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.diff import ROOT
|
||||
from dvc_data.diff import diff as odiff
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc_data.hashfile.diff import ROOT
|
||||
from dvc_data.hashfile.diff import diff as odiff
|
||||
|
||||
drop_root = False
|
||||
trees = isinstance(old_obj, Tree) or isinstance(new_obj, Tree)
|
||||
if trees:
|
||||
drop_root = not with_dirs
|
||||
|
||||
def path_join(root: str, *paths: str) -> str:
|
||||
if not trees and paths == ROOT:
|
||||
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
|
||||
if not isdir and paths == ROOT:
|
||||
return root
|
||||
return os.path.sep.join([root, *paths])
|
||||
|
||||
@@ -83,38 +90,46 @@ def _granular_diff(
|
||||
output: Dict[str, List[str]] = defaultdict(list)
|
||||
for state in ("added", "deleted", "modified", "unchanged"):
|
||||
items = getattr(diff_data, state)
|
||||
output[state].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items
|
||||
if not (drop_root and item.new.key == ROOT)
|
||||
)
|
||||
# TODO: PERF: diff is checking not_in_cache for each even if we only
|
||||
# need it for the index.
|
||||
# BUG: not_in_cache file also shows up as modified in staged and
|
||||
# unstaged. We currently don't know if it is really modified.
|
||||
output["not_in_cache"].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items
|
||||
if not item.old.in_cache
|
||||
and not (drop_root and item.new.key == ROOT)
|
||||
and state != "added"
|
||||
)
|
||||
return output
|
||||
for item in items: # pylint: disable=not-an-iterable
|
||||
entry = item.old if state == "deleted" else item.new
|
||||
isdir = entry.oid.isdir if entry.oid else False
|
||||
|
||||
path = path_join(root, *entry.key, isdir=isdir)
|
||||
output[state].append(path)
|
||||
if not item.old.in_cache and state != "added":
|
||||
output["not_in_cache"].append(path)
|
||||
return dict(output)
|
||||
|
||||
|
||||
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
|
||||
if not obj:
|
||||
return []
|
||||
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
sep = os.path.sep
|
||||
if isinstance(obj, Tree):
|
||||
return [sep.join([root, *key]) for key, _, _ in obj]
|
||||
return [root]
|
||||
|
||||
|
||||
def _diff(
|
||||
root: str,
|
||||
old_oid: Optional["HashInfo"],
|
||||
old_obj: Optional["HashFile"],
|
||||
new_oid: Optional["HashInfo"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
odb: "HashFileDB",
|
||||
granular: bool = False,
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
if granular:
|
||||
return _granular_diff(
|
||||
root, old_obj, new_obj, cache, with_dirs=with_dirs
|
||||
)
|
||||
return _shallow_diff(root, old_obj, new_obj, cache)
|
||||
if not granular:
|
||||
return _shallow_diff(root, old_oid, new_oid, odb)
|
||||
if (old_oid and not old_obj) or (new_oid and not new_obj):
|
||||
# we don't have enough information to give full details
|
||||
unknown = _get_obj_items(root, new_obj)
|
||||
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
|
||||
return {**shallow_diff, "unknown": unknown}
|
||||
return _granular_diff(root, old_obj, new_obj, odb)
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
@@ -141,6 +156,8 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
empty_repo = False
|
||||
|
||||
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
|
||||
if os.name == "nt":
|
||||
untracked = [posixpath_to_os_path(path) for path in untracked]
|
||||
# NOTE: order is important here.
|
||||
return GitInfo(
|
||||
staged=staged,
|
||||
@@ -152,7 +169,7 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
|
||||
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
unstaged_diff = defaultdict(list)
|
||||
for out in repo.index.outs:
|
||||
@@ -175,8 +192,18 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
cache = repo.odb.repo
|
||||
root = str(out)
|
||||
old = out.get_obj()
|
||||
|
||||
with ui.status(f"Calculating diff for {root} between index/workspace"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root,
|
||||
out.hash_info,
|
||||
old,
|
||||
new.hash_info if new else None,
|
||||
new,
|
||||
cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -188,7 +215,9 @@ def _diff_head_to_index(
|
||||
repo: "Repo", head: str = "HEAD", **kwargs: Any
|
||||
) -> Dict[str, List[str]]:
|
||||
# we need to store objects from index and the HEAD to diff later
|
||||
objs: Dict[str, Dict[str, "HashFile"]] = defaultdict(dict)
|
||||
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
|
||||
objs = defaultdict(dict)
|
||||
|
||||
staged_diff = defaultdict(list)
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
for out in repo.index.outs:
|
||||
@@ -197,15 +226,20 @@ def _diff_head_to_index(
|
||||
continue
|
||||
|
||||
root = str(out)
|
||||
if isinstance(out.fs, GitFileSystem):
|
||||
root = posixpath_to_os_path(root)
|
||||
typ = "index" if rev == "workspace" else head
|
||||
objs[root][typ] = out.get_obj()
|
||||
objs[root][typ] = (out.get_obj(), out.hash_info)
|
||||
|
||||
cache = repo.odb.repo
|
||||
for root, obj_d in objs.items():
|
||||
old = obj_d.get(head, None)
|
||||
new = obj_d.get("index", None)
|
||||
old_obj, old_oid = obj_d.get(head, (None, None))
|
||||
new_obj, new_oid = obj_d.get("index", (None, None))
|
||||
with ui.status(f"Calculating diff for {root} between head/index"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -216,28 +250,36 @@ def _diff_head_to_index(
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
committed: Dict[str, Any]
|
||||
uncommitted: Dict[str, Any]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
unchanged: List[str]
|
||||
git: GitInfo
|
||||
|
||||
|
||||
def _transform_git_paths_to_dvc(repo: "Repo", files: List[str]):
|
||||
def _transform_git_paths_to_dvc(
|
||||
repo: "Repo", files: Iterable[str]
|
||||
) -> List[str]:
|
||||
"""Transform files rel. to Git root to DVC root, and drop outside files."""
|
||||
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
if rel in (os.curdir, ""):
|
||||
return files
|
||||
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
return [file[length:] for file in files if file.startswith(prefix)]
|
||||
# if we have repo root in a different location than scm's root,
|
||||
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
|
||||
# repo root and anything outside need to be filtered out.
|
||||
if rel not in (os.curdir, ""):
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
files = (file[length:] for file in files if file.startswith(prefix))
|
||||
|
||||
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
|
||||
if start in (os.curdir, ""):
|
||||
return list(files)
|
||||
# we need to convert repo relative paths to curdir relative.
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
@@ -265,3 +307,18 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
unchanged=list(unchanged),
|
||||
git=git_info,
|
||||
)
|
||||
|
||||
|
||||
def ls(
|
||||
repo: "Repo",
|
||||
targets: List[Optional[str]] = None,
|
||||
recursive: bool = False,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
repo.stage.collect_granular(target, recursive=recursive)
|
||||
for target in targets
|
||||
)
|
||||
for stage, filter_info in pairs:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
yield {"path": str(out), **out.annot.to_dict()}
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ def _paths_checksums(repo, targets):
|
||||
|
||||
def _output_paths(repo, targets):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseExecutorManager(ABC, Mapping):
|
||||
class BaseExecutorManager(ABC, Mapping): # noqa: B024
|
||||
"""Manages executors for a collection of experiments to be run."""
|
||||
|
||||
EXECUTOR_CLS: Type = BaseExecutor
|
||||
|
||||
@@ -5,10 +5,9 @@ import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional
|
||||
|
||||
from dvc_ssh import SSHFileSystem
|
||||
from funcy import first
|
||||
|
||||
from dvc.fs import SSHFileSystem
|
||||
|
||||
from ..refs import (
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import (
|
||||
@@ -19,6 +20,7 @@ from typing import (
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -520,14 +522,36 @@ class BaseStashQueue(ABC):
|
||||
provided via `exp run --set-param`.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
from dvc.utils.hydra import apply_overrides
|
||||
try:
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
except ValueError:
|
||||
if sys.version_info >= (3, 11):
|
||||
raise DvcException(
|
||||
"--set-param is not supported in Python >= 3.11"
|
||||
)
|
||||
raise
|
||||
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
for path, overrides in params.items():
|
||||
apply_overrides(path, overrides)
|
||||
if hydra_enabled and path == hydra_output_file:
|
||||
config_dir = os.path.join(
|
||||
self.repo.root_dir, hydra_config.get("config_dir", "conf")
|
||||
)
|
||||
config_name = hydra_config.get("config_name", "config")
|
||||
compose_and_dump(
|
||||
path,
|
||||
config_dir,
|
||||
config_name,
|
||||
overrides,
|
||||
)
|
||||
else:
|
||||
apply_overrides(path, overrides)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from dvc.dependency.param import ParamsDependency
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo import locked
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.cli_parse import to_path_overrides
|
||||
@@ -26,26 +28,59 @@ def run(
|
||||
Returns a dict mapping new experiment SHAs to the results
|
||||
of `repro` for that experiment.
|
||||
"""
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
if run_all:
|
||||
entries = list(repo.experiments.celery_queue.iter_queued())
|
||||
return repo.experiments.reproduce_celery(entries, jobs=jobs)
|
||||
|
||||
if params:
|
||||
params = to_path_overrides(params)
|
||||
path_overrides = to_path_overrides(params)
|
||||
else:
|
||||
path_overrides = {}
|
||||
|
||||
if queue:
|
||||
if not kwargs.get("checkpoint_resume", None):
|
||||
kwargs["reset"] = True
|
||||
hydra_enabled = repo.config.get("hydra", {}).get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
if hydra_enabled and hydra_output_file not in path_overrides:
|
||||
# Force `_update_params` even if `--set-param` was not used
|
||||
path_overrides[hydra_output_file] = []
|
||||
|
||||
hydra_sweep = any(
|
||||
x.is_sweep_override()
|
||||
for param_file in path_overrides
|
||||
for x in to_hydra_overrides(path_overrides[param_file])
|
||||
)
|
||||
|
||||
if hydra_sweep and not queue:
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used without `--queue`"
|
||||
)
|
||||
|
||||
if not queue:
|
||||
return repo.experiments.reproduce_one(
|
||||
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
|
||||
)
|
||||
|
||||
if hydra_sweep:
|
||||
from dvc.utils.hydra import get_hydra_sweeps
|
||||
|
||||
sweeps = get_hydra_sweeps(path_overrides)
|
||||
else:
|
||||
sweeps = [path_overrides]
|
||||
|
||||
if not kwargs.get("checkpoint_resume", None):
|
||||
kwargs["reset"] = True
|
||||
|
||||
for sweep_overrides in sweeps:
|
||||
queue_entry = repo.experiments.queue_one(
|
||||
repo.experiments.celery_queue,
|
||||
targets=targets,
|
||||
params=params,
|
||||
params=sweep_overrides,
|
||||
**kwargs,
|
||||
)
|
||||
if sweep_overrides:
|
||||
ui.write(f"Queueing with overrides '{sweep_overrides}'.")
|
||||
name = queue_entry.name or queue_entry.stash_rev[:7]
|
||||
ui.write(f"Queued experiment '{name}' for future execution.")
|
||||
return {}
|
||||
|
||||
return repo.experiments.reproduce_one(
|
||||
targets=targets, params=params, tmp_dir=tmp_dir, **kwargs
|
||||
)
|
||||
return {}
|
||||
|
||||
@@ -142,6 +142,9 @@ def show(
|
||||
fetch_running: bool = True,
|
||||
):
|
||||
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import DownloadError, FileTransferError
|
||||
from dvc.fs import Schemes
|
||||
|
||||
@@ -12,6 +14,36 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _fetch_worktree(repo, remote):
|
||||
from dvc_data.index import save
|
||||
|
||||
index = repo.index.data["repo"]
|
||||
for key, entry in index.iteritems():
|
||||
entry.fs = remote.fs
|
||||
entry.path = remote.fs.path.join(
|
||||
remote.path,
|
||||
*key,
|
||||
)
|
||||
save(index)
|
||||
|
||||
for stage in repo.index.stages:
|
||||
for out in stage.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
if not out.is_in_repo:
|
||||
continue
|
||||
|
||||
workspace, key = out.index_key
|
||||
entry = repo.index.data[workspace][key]
|
||||
out.hash_info = entry.hash_info
|
||||
out.meta = entry.meta
|
||||
|
||||
stage.dvcfile.dump(stage)
|
||||
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def fetch(
|
||||
self,
|
||||
@@ -42,6 +74,11 @@ def fetch(
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
with suppress(NoRemoteError):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _fetch_worktree(self, _remote)
|
||||
|
||||
used = self.used_objs(
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
@@ -93,6 +130,18 @@ def fetch(
|
||||
downloaded += d
|
||||
failed += f
|
||||
|
||||
d, f = _fetch_partial_imports(
|
||||
self,
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
recursive=recursive,
|
||||
revs=revs,
|
||||
)
|
||||
downloaded += d
|
||||
failed += f
|
||||
|
||||
if failed:
|
||||
raise DownloadError(failed)
|
||||
|
||||
@@ -107,3 +156,33 @@ def _fetch(repo, obj_ids, **kwargs):
|
||||
except FileTransferError as exc:
|
||||
failed += exc.amount
|
||||
return downloaded, failed
|
||||
|
||||
|
||||
def _fetch_partial_imports(repo, targets, **kwargs):
|
||||
from dvc.stage.exceptions import DataSourceChanged
|
||||
|
||||
downloaded = 0
|
||||
failed = 0
|
||||
for stage in repo.partial_imports(targets, **kwargs):
|
||||
dep = stage.deps[0]
|
||||
out = stage.outs[0]
|
||||
try:
|
||||
if dep.changed_checksum():
|
||||
raise DataSourceChanged(f"{stage} ({dep})")
|
||||
|
||||
logger.info("Importing '%s' -> '%s'", dep, out)
|
||||
out.transfer(
|
||||
dep.def_path, jobs=kwargs.get("jobs"), odb=repo.odb.local
|
||||
)
|
||||
except DataSourceChanged as exc:
|
||||
logger.warning(f"{exc}")
|
||||
failed += 1
|
||||
continue
|
||||
if not any(
|
||||
kwargs.get(kw, None)
|
||||
for kw in ("all_branches", "all_tags", "all_commits", "revs")
|
||||
):
|
||||
stage.dump()
|
||||
|
||||
downloaded += 1
|
||||
return downloaded, failed
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ def gc(
|
||||
from contextlib import ExitStack
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.db import get_index
|
||||
from dvc_data.gc import gc as ogc
|
||||
from dvc_data.hashfile.db import get_index
|
||||
from dvc_data.hashfile.gc import gc as ogc
|
||||
|
||||
if not repos:
|
||||
repos = []
|
||||
|
||||
+11
-6
@@ -17,11 +17,16 @@ def imp_url(
|
||||
fname=None,
|
||||
erepo=None,
|
||||
frozen=True,
|
||||
no_download=False,
|
||||
no_exec=False,
|
||||
remote=None,
|
||||
to_remote=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
jobs=None,
|
||||
fs_config=None,
|
||||
):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
from dvc.stage import Stage, create_stage, restore_fields
|
||||
@@ -31,9 +36,9 @@ def imp_url(
|
||||
self, out, always_local=to_remote and not out
|
||||
)
|
||||
|
||||
if to_remote and no_exec:
|
||||
if to_remote and (no_exec or no_download):
|
||||
raise InvalidArgumentError(
|
||||
"--no-exec can't be combined with --to-remote"
|
||||
"--no-exec/--no-download cannot be combined with --to-remote"
|
||||
)
|
||||
|
||||
if not to_remote and remote:
|
||||
@@ -57,12 +62,12 @@ def imp_url(
|
||||
deps=[url],
|
||||
outs=[out],
|
||||
erepo=erepo,
|
||||
fs_config=fs_config,
|
||||
)
|
||||
restore_fields(stage)
|
||||
|
||||
if desc:
|
||||
stage.outs[0].desc = desc
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
|
||||
dvcfile = Dvcfile(self, stage.path)
|
||||
dvcfile.remove()
|
||||
|
||||
@@ -80,7 +85,7 @@ def imp_url(
|
||||
stage.save_deps()
|
||||
stage.md5 = stage.compute_md5()
|
||||
else:
|
||||
stage.run(jobs=jobs)
|
||||
stage.run(jobs=jobs, no_download=no_download)
|
||||
|
||||
stage.frozen = frozen
|
||||
|
||||
|
||||
+25
-10
@@ -183,15 +183,7 @@ class Index:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
if out.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(
|
||||
out.fs_path, self.repo.root_dir
|
||||
)
|
||||
else:
|
||||
workspace = out.fs.protocol
|
||||
no_drive = out.fs.path.flavour.splitdrive(out.fs_path)[1]
|
||||
key = out.fs.path.parts(no_drive)[1:]
|
||||
workspace, key = out.index_key
|
||||
|
||||
try:
|
||||
remote = self.repo.cloud.get_remote_odb(out.remote)
|
||||
@@ -202,9 +194,10 @@ class Index:
|
||||
|
||||
data_index[key] = DataIndexEntry(
|
||||
meta=out.meta,
|
||||
obj=out.obj,
|
||||
obj=out.get_obj() if out.files else out.obj,
|
||||
hash_info=out.hash_info,
|
||||
odb=out.odb,
|
||||
cache=out.odb,
|
||||
remote=remote,
|
||||
)
|
||||
|
||||
@@ -246,6 +239,28 @@ class Index:
|
||||
used[odb].update(objs)
|
||||
return used
|
||||
|
||||
def partial_imports(
|
||||
self,
|
||||
targets: "TargetType" = None,
|
||||
recursive: bool = False,
|
||||
) -> List["Stage"]:
|
||||
from itertools import chain
|
||||
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
collect_targets: Sequence[Optional[str]] = (None,)
|
||||
if targets:
|
||||
collect_targets = ensure_list(targets)
|
||||
|
||||
pairs = chain.from_iterable(
|
||||
self.stage_collector.collect_granular(
|
||||
target, recursive=recursive, with_deps=True
|
||||
)
|
||||
for target in collect_targets
|
||||
)
|
||||
|
||||
return [stage for stage, _ in pairs if stage.is_partial_import]
|
||||
|
||||
# Following methods help us treat the collection as a set-like structure
|
||||
# and provides faux-immutability.
|
||||
# These methods do not preserve stages order.
|
||||
|
||||
+11
-12
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
@@ -66,15 +65,16 @@ def _ls(
|
||||
|
||||
infos = {}
|
||||
for root, dirs, files in fs.walk(
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
|
||||
):
|
||||
entries = chain(files, dirs) if not recursive else files
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
for entry in entries:
|
||||
entry_fs_path = fs.path.join(root, entry)
|
||||
for name, entry in files.items():
|
||||
entry_fs_path = fs.path.join(root, name)
|
||||
relparts = fs.path.relparts(entry_fs_path, fs_path)
|
||||
name = os.path.join(*relparts)
|
||||
infos[name] = fs.info(entry_fs_path)
|
||||
infos[name] = entry
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
@@ -85,11 +85,10 @@ def _ls(
|
||||
ret = {}
|
||||
for name, info in infos.items():
|
||||
dvc_info = info.get("dvc_info", {})
|
||||
if fs.isdvc(info["name"], recursive=True) or not dvc_only:
|
||||
ret[name] = {
|
||||
"isout": dvc_info.get("isout", False),
|
||||
"isdir": info["type"] == "directory",
|
||||
"isexec": info.get("isexec", False),
|
||||
}
|
||||
ret[name] = {
|
||||
"isout": dvc_info.get("isout", False),
|
||||
"isdir": info["type"] == "directory",
|
||||
"isexec": info.get("isexec", False),
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
@@ -85,12 +85,14 @@ def _read_metrics(repo, metrics, rev, onerror=None):
|
||||
|
||||
res = {}
|
||||
for metric in metrics:
|
||||
rel_metric_path = os.path.join(relpath, *fs.path.parts(metric))
|
||||
if not fs.isfile(metric):
|
||||
continue
|
||||
if fs.isfile(rel_metric_path):
|
||||
metric = rel_metric_path
|
||||
else:
|
||||
continue
|
||||
|
||||
res[os.path.join(relpath, *fs.path.parts(metric))] = _read_metric(
|
||||
metric, fs, rev, onerror=onerror
|
||||
)
|
||||
res[rel_metric_path] = _read_metric(metric, fs, rev, onerror=onerror)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ def _collect_configs(
|
||||
default_params
|
||||
):
|
||||
fs_paths.append(default_params)
|
||||
|
||||
if targets and (deps or stages) and not params:
|
||||
# A target has been provided but it is not used in the stages
|
||||
fs_paths = []
|
||||
return params, fs_paths
|
||||
|
||||
|
||||
|
||||
+73
-38
@@ -386,30 +386,56 @@ def _collect_output_plots(
|
||||
return result
|
||||
|
||||
|
||||
def _adjust_definitions_to_cwd(fs, config_relpath, plots_definitions):
|
||||
result = defaultdict(dict)
|
||||
def _id_is_path(plot_props=None):
|
||||
if not plot_props:
|
||||
return True
|
||||
|
||||
config_dirname = fs.path.dirname(config_relpath)
|
||||
y_def = plot_props.get("y")
|
||||
return not isinstance(y_def, dict)
|
||||
|
||||
for plot_id, plot_def in plots_definitions.items():
|
||||
|
||||
y_def = plot_def.get("y", None) if plot_def else None
|
||||
if y_def is None or not isinstance(y_def, dict):
|
||||
# plot_id is filename
|
||||
new_plot_id = _normpath(fs.path.join(config_dirname, plot_id))
|
||||
result[new_plot_id] = plot_def or {}
|
||||
def _adjust_sources(fs, plot_props, config_dir):
|
||||
new_plot_props = deepcopy(plot_props)
|
||||
old_y = new_plot_props.pop("y", {})
|
||||
new_y = {}
|
||||
for filepath, val in old_y.items():
|
||||
new_y[_normpath(fs.path.join(config_dir, filepath))] = val
|
||||
new_plot_props["y"] = new_y
|
||||
return new_plot_props
|
||||
|
||||
|
||||
def _resolve_definitions(
|
||||
fs, targets, props, config_path, definitions, onerror=None
|
||||
):
|
||||
config_dir = fs.path.dirname(config_path)
|
||||
result: Dict[str, Dict] = {}
|
||||
for plot_id, plot_props in definitions.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
if _id_is_path(plot_props):
|
||||
data_path = _normpath(fs.path.join(config_dir, plot_id))
|
||||
if _matches(targets, config_path, plot_id):
|
||||
unpacked = unpack_if_dir(
|
||||
fs,
|
||||
data_path,
|
||||
props={**plot_props, **props},
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
unpacked,
|
||||
)
|
||||
else:
|
||||
new_plot_def = deepcopy(plot_def)
|
||||
old_y = new_plot_def.pop("y")
|
||||
new_y = {}
|
||||
for filepath, val in old_y.items():
|
||||
new_y[_normpath(fs.path.join(config_dirname, filepath))] = val
|
||||
new_plot_def["y"] = new_y
|
||||
result[plot_id] = new_plot_def
|
||||
return dict(result)
|
||||
if _matches(targets, config_path, plot_id):
|
||||
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
|
||||
dpath.util.merge(
|
||||
result, {"data": {plot_id: {**adjusted_props, **props}}}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _collect_pipeline_files(repo, targets: List[str], props):
|
||||
def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
|
||||
from dvc.dvcfile import PipelineFile
|
||||
|
||||
result: Dict[str, Dict] = {}
|
||||
@@ -417,21 +443,19 @@ def _collect_pipeline_files(repo, targets: List[str], props):
|
||||
for dvcfile in dvcfiles:
|
||||
if isinstance(dvcfile, PipelineFile):
|
||||
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
|
||||
dvcfile_defs = _adjust_definitions_to_cwd(
|
||||
repo.fs, dvcfile_path, dvcfile.load().get("plots", {})
|
||||
dvcfile_defs = dvcfile.load().get("plots", {})
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
dvcfile_path,
|
||||
dvcfile_defs,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{dvcfile_path: resolved},
|
||||
)
|
||||
for plot_id, plot_props in dvcfile_defs.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
if _matches(targets, dvcfile_path, plot_id):
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{
|
||||
dvcfile_path: {
|
||||
"data": {plot_id: {**plot_props, **props}}
|
||||
}
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -453,7 +477,10 @@ def _collect_definitions(
|
||||
fs = DvcFileSystem(repo=repo)
|
||||
|
||||
if not config_files:
|
||||
dpath.util.merge(result, _collect_pipeline_files(repo, targets, props))
|
||||
dpath.util.merge(
|
||||
result,
|
||||
_collect_pipeline_files(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
|
||||
if targets or (not targets and not config_files):
|
||||
dpath.util.merge(
|
||||
@@ -463,12 +490,20 @@ def _collect_definitions(
|
||||
|
||||
if config_files:
|
||||
for path in config_files:
|
||||
definitions = parse(fs, path)
|
||||
definitions = _adjust_definitions_to_cwd(
|
||||
repo.fs, path, definitions
|
||||
)
|
||||
definitions = parse(fs, path).get("data", {})
|
||||
if definitions:
|
||||
dpath.util.merge(result, {path: definitions})
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
path,
|
||||
definitions,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{path: resolved},
|
||||
)
|
||||
|
||||
for target in targets:
|
||||
if not result or fs.exists(target):
|
||||
|
||||
@@ -12,6 +12,8 @@ def _revisions(repo, revs, experiment):
|
||||
|
||||
|
||||
def diff(repo, *args, revs=None, experiment=False, **kwargs):
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
return repo.plots.show(
|
||||
*args, revs=_revisions(repo, revs, experiment), **kwargs
|
||||
)
|
||||
|
||||
@@ -9,6 +9,52 @@ if TYPE_CHECKING:
|
||||
from dvc_objects.db.base import ObjectDB
|
||||
|
||||
|
||||
def _push_worktree(repo, remote):
|
||||
from dvc_data.hashfile.tree import tree_from_index
|
||||
from dvc_data.index import build, checkout, collect
|
||||
|
||||
index = repo.index.data["repo"]
|
||||
checkout(index, remote.path, remote.fs, force=True)
|
||||
build(index, remote.path, remote.fs)
|
||||
if any(out.isdir() for out in repo.index.outs):
|
||||
collect(index, remote.path, remote.fs)
|
||||
|
||||
for stage in repo.index.stages:
|
||||
for out in stage.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
if not out.is_in_repo:
|
||||
continue
|
||||
|
||||
workspace, key = out.index_key
|
||||
index = repo.index.data[workspace]
|
||||
entry = index[key]
|
||||
if out.isdir():
|
||||
old_tree = out.get_obj()
|
||||
entry.hash_info = old_tree.hash_info
|
||||
entry.meta = out.meta
|
||||
for subkey, entry in index.iteritems(key):
|
||||
if entry.meta.isdir:
|
||||
continue
|
||||
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
|
||||
_, hash_info = old_tree.get(
|
||||
repo.fs.path.relparts(fs_path, out.fs_path)
|
||||
)
|
||||
entry.hash_info = hash_info
|
||||
tree_meta, new_tree = tree_from_index(index, key)
|
||||
new_tree.digest()
|
||||
out.obj = new_tree
|
||||
out.hash_info = new_tree.hash_info
|
||||
out.meta = tree_meta
|
||||
else:
|
||||
out.hash_info = entry.hash_info
|
||||
out.meta = entry.meta
|
||||
stage.dvcfile.dump(stage, with_files=True)
|
||||
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def push(
|
||||
self,
|
||||
@@ -26,6 +72,10 @@ def push(
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
include_imports=False,
|
||||
):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _push_worktree(self, _remote)
|
||||
|
||||
used_run_cache = (
|
||||
self.stage_cache.push(remote, odb=odb) if run_cache else []
|
||||
)
|
||||
|
||||
+17
-1
@@ -1,6 +1,13 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileDoesNotExistError,
|
||||
StageFileIsNotDvcFileError,
|
||||
StageNotFound,
|
||||
)
|
||||
|
||||
from . import locked
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -11,7 +18,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
def remove(self: "Repo", target: str, outs: bool = False):
|
||||
stages = self.stage.from_target(target)
|
||||
try:
|
||||
stages = self.stage.from_target(target, accept_group=False)
|
||||
except (StageNotFound, StageFileDoesNotExistError) as e:
|
||||
# If the user specified a tracked file as a target instead of a stage,
|
||||
# e.g. `data.csv` instead of `data.csv.dvc`,
|
||||
# give a more helpful error message.
|
||||
if self.fs.exists(target + DVC_FILE_SUFFIX):
|
||||
raise StageFileIsNotDvcFileError(target) from e
|
||||
else:
|
||||
raise
|
||||
|
||||
for stage in stages:
|
||||
stage.remove(remove_outs=outs, force=outs)
|
||||
|
||||
@@ -102,7 +102,6 @@ def reproduce(
|
||||
from .graph import get_pipeline, get_pipelines
|
||||
|
||||
glob = kwargs.pop("glob", False)
|
||||
accept_group = not glob
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -137,7 +136,6 @@ def reproduce(
|
||||
self.stage.collect(
|
||||
target,
|
||||
recursive=recursive,
|
||||
accept_group=accept_group,
|
||||
glob=glob,
|
||||
)
|
||||
)
|
||||
|
||||
+11
-22
@@ -82,7 +82,6 @@ def _collect_specific_target(
|
||||
target: str,
|
||||
with_deps: bool,
|
||||
recursive: bool,
|
||||
accept_group: bool,
|
||||
) -> Tuple[StageIter, "OptStr", "OptStr"]:
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
|
||||
@@ -98,13 +97,11 @@ def _collect_specific_target(
|
||||
msg = "Checking if stage '%s' is in '%s'"
|
||||
logger.debug(msg, target, PIPELINE_FILE)
|
||||
if not (recursive and loader.fs.isdir(target)):
|
||||
stages = _maybe_collect_from_dvc_yaml(
|
||||
loader, target, with_deps, accept_group=accept_group
|
||||
)
|
||||
stages = _maybe_collect_from_dvc_yaml(loader, target, with_deps)
|
||||
if stages:
|
||||
return stages, file, name
|
||||
elif not with_deps and is_valid_filename(file):
|
||||
stages = loader.load_all(file, name, accept_group=accept_group)
|
||||
stages = loader.load_all(file, name)
|
||||
return stages, file, name
|
||||
return [], file, name
|
||||
|
||||
@@ -209,7 +206,7 @@ class StageLoad:
|
||||
return stage
|
||||
|
||||
def from_target(
|
||||
self, target: str, accept_group: bool = False, glob: bool = False
|
||||
self, target: str, accept_group: bool = True, glob: bool = False
|
||||
) -> StageList:
|
||||
"""
|
||||
Returns a list of stage from the provided target.
|
||||
@@ -250,15 +247,12 @@ class StageLoad:
|
||||
self,
|
||||
stages: "StageLoader",
|
||||
name: str = None,
|
||||
accept_group: bool = False,
|
||||
accept_group: bool = True,
|
||||
glob: bool = False,
|
||||
) -> Iterable[str]:
|
||||
|
||||
assert not (accept_group and glob)
|
||||
|
||||
if not name:
|
||||
return stages.keys()
|
||||
|
||||
if accept_group and stages.is_foreach_generated(name):
|
||||
return self._get_group_keys(stages, name)
|
||||
if glob:
|
||||
@@ -269,7 +263,7 @@ class StageLoad:
|
||||
self,
|
||||
path: str = None,
|
||||
name: str = None,
|
||||
accept_group: bool = False,
|
||||
accept_group: bool = True,
|
||||
glob: bool = False,
|
||||
) -> StageList:
|
||||
"""Load a list of stages from a file.
|
||||
@@ -338,7 +332,6 @@ class StageLoad:
|
||||
with_deps: bool = False,
|
||||
recursive: bool = False,
|
||||
graph: "DiGraph" = None,
|
||||
accept_group: bool = False,
|
||||
glob: bool = False,
|
||||
) -> StageIter:
|
||||
"""Collect list of stages from the provided target.
|
||||
@@ -347,15 +340,14 @@ class StageLoad:
|
||||
target: if not provided, all of the stages in the graph are
|
||||
returned.
|
||||
Target can be:
|
||||
- a stage name in the `dvc.yaml` file.
|
||||
- a foreach group name or a stage name in the `dvc.yaml` file.
|
||||
- a generated stage name from a foreach group.
|
||||
- a path to `dvc.yaml` or `.dvc` file.
|
||||
- in case of a stage to a dvc.yaml file in a different
|
||||
directory than current working directory, it can be a path
|
||||
to dvc.yaml file, followed by a colon `:`, followed by stage
|
||||
name (eg: `../dvc.yaml:build`).
|
||||
- in case of `recursive`, it can be a path to a directory.
|
||||
- in case of `accept_group`, it can be a group name of
|
||||
`foreach` generated stage.
|
||||
- in case of `glob`, it can be a wildcard pattern to match
|
||||
stages. Example: `build*` for stages in `dvc.yaml` file, or
|
||||
`../dvc.yaml:build*` for stages in dvc.yaml in a different
|
||||
@@ -367,8 +359,6 @@ class StageLoad:
|
||||
recursive: if true and if `target` is a directory, all of the
|
||||
stages inside that directory is returned.
|
||||
graph: graph to use. Defaults to `repo.graph`.
|
||||
accept_group: if true, all of the `foreach` generated stages of
|
||||
the specified target is returned.
|
||||
glob: Use `target` as a pattern to match stages in a file.
|
||||
"""
|
||||
if not target:
|
||||
@@ -380,7 +370,7 @@ class StageLoad:
|
||||
path = self.fs.path.abspath(target)
|
||||
return collect_inside_path(path, graph or self.graph)
|
||||
|
||||
stages = self.from_target(target, accept_group=accept_group, glob=glob)
|
||||
stages = self.from_target(target, glob=glob)
|
||||
if not with_deps:
|
||||
return stages
|
||||
|
||||
@@ -392,14 +382,14 @@ class StageLoad:
|
||||
with_deps: bool = False,
|
||||
recursive: bool = False,
|
||||
graph: "DiGraph" = None,
|
||||
accept_group: bool = False,
|
||||
) -> List[StageInfo]:
|
||||
"""Collects a list of (stage, filter_info) from the given target.
|
||||
|
||||
Priority is in the order of following in case of ambiguity:
|
||||
- .dvc file or .yaml file
|
||||
- dir if recursive and directory exists
|
||||
- stage_name
|
||||
- foreach_group_name or stage_name
|
||||
- generated stage name from a foreach group
|
||||
- output file
|
||||
|
||||
Args:
|
||||
@@ -418,7 +408,7 @@ class StageLoad:
|
||||
target = as_posix(target)
|
||||
|
||||
stages, file, _ = _collect_specific_target(
|
||||
self, target, with_deps, recursive, accept_group
|
||||
self, target, with_deps, recursive
|
||||
)
|
||||
if not stages:
|
||||
if not (recursive and self.fs.isdir(target)):
|
||||
@@ -440,7 +430,6 @@ class StageLoad:
|
||||
with_deps,
|
||||
recursive,
|
||||
graph,
|
||||
accept_group=accept_group,
|
||||
)
|
||||
except StageFileDoesNotExistError as exc:
|
||||
# collect() might try to use `target` as a stage name
|
||||
|
||||
+13
-1
@@ -10,6 +10,7 @@ def update(
|
||||
rev=None,
|
||||
recursive=False,
|
||||
to_remote=False,
|
||||
no_download=False,
|
||||
remote=None,
|
||||
jobs=None,
|
||||
):
|
||||
@@ -21,6 +22,11 @@ def update(
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
if to_remote and no_download:
|
||||
raise InvalidArgumentError(
|
||||
"--to-remote can't be used with --no-download"
|
||||
)
|
||||
|
||||
if not to_remote and remote:
|
||||
raise InvalidArgumentError(
|
||||
"--remote can't be used without --to-remote"
|
||||
@@ -31,7 +37,13 @@ def update(
|
||||
stages.update(self.stage.collect(target, recursive=recursive))
|
||||
|
||||
for stage in stages:
|
||||
stage.update(rev, to_remote=to_remote, remote=remote, jobs=jobs)
|
||||
stage.update(
|
||||
rev,
|
||||
to_remote=to_remote,
|
||||
remote=remote,
|
||||
no_download=no_download,
|
||||
jobs=jobs,
|
||||
)
|
||||
dvcfile = Dvcfile(self, stage.path)
|
||||
dvcfile.dump(stage)
|
||||
|
||||
|
||||
+14
-7
@@ -3,11 +3,11 @@ from collections.abc import Mapping
|
||||
from voluptuous import Any, Optional, Required, Schema
|
||||
|
||||
from dvc import dependency, output
|
||||
from dvc.output import CHECKSUMS_SCHEMA, Output
|
||||
from dvc.annotations import ANNOTATION_SCHEMA
|
||||
from dvc.output import CHECKSUMS_SCHEMA, DIR_FILES_SCHEMA, META_SCHEMA, Output
|
||||
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
|
||||
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
|
||||
from dvc.stage.params import StageParams
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
STAGES = "stages"
|
||||
SINGLE_STAGE_SCHEMA = {
|
||||
@@ -25,10 +25,9 @@ SINGLE_STAGE_SCHEMA = {
|
||||
|
||||
DATA_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required("path"): str,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
LOCK_FILE_STAGE_SCHEMA = {
|
||||
Required(StageParams.PARAM_CMD): Any(str, list),
|
||||
@@ -47,16 +46,15 @@ LOCKFILE_V2_SCHEMA = {
|
||||
|
||||
OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||
str: {
|
||||
**ANNOTATION_SCHEMA, # type: ignore
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
}
|
||||
}
|
||||
|
||||
PLOTS = "plots"
|
||||
PLOTS_SCHEMA = dict
|
||||
PLOT_PROPS = {
|
||||
Output.PARAM_PLOT_TEMPLATE: str,
|
||||
Output.PARAM_PLOT_X: str,
|
||||
@@ -112,6 +110,15 @@ def either_or(primary, fallback, fallback_includes=None):
|
||||
return validator
|
||||
|
||||
|
||||
PLOT_DEFINITION = {
|
||||
Output.PARAM_PLOT_X: str,
|
||||
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
|
||||
Output.PARAM_PLOT_X_LABEL: str,
|
||||
Output.PARAM_PLOT_Y_LABEL: str,
|
||||
Output.PARAM_PLOT_TITLE: str,
|
||||
Output.PARAM_PLOT_TEMPLATE: str,
|
||||
}
|
||||
PLOTS_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
|
||||
FOREACH_IN = {
|
||||
Required(FOREACH_KWD): Any(dict, list, str),
|
||||
Required(DO_KWD): STAGE_DEFINITION,
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
except InternalRevError as exc:
|
||||
# `scm` will only resolve git branch and tag names,
|
||||
# if rev is not a sha it may be an abbreviated experiment name
|
||||
if not rev.startswith("refs/"):
|
||||
if not (rev == "HEAD" or rev.startswith("refs/")):
|
||||
from dvc.repo.experiments.utils import (
|
||||
AmbiguousExpRefInfo,
|
||||
resolve_name,
|
||||
|
||||
+43
-17
@@ -78,6 +78,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
|
||||
|
||||
wdir = os.path.abspath(kwargs.get("wdir", None) or os.curdir)
|
||||
path = os.path.abspath(path)
|
||||
|
||||
check_dvcfile_path(repo, path)
|
||||
check_stage_path(repo, wdir, is_wdir=kwargs.get("wdir"))
|
||||
check_stage_path(repo, os.path.dirname(path))
|
||||
@@ -87,7 +88,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
|
||||
if not external:
|
||||
check_no_externals(stage)
|
||||
fill_stage_dependencies(
|
||||
stage, **project(kwargs, ["deps", "erepo", "params"])
|
||||
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config"])
|
||||
)
|
||||
check_circular_dependency(stage)
|
||||
check_duplicated_arguments(stage)
|
||||
@@ -109,16 +110,13 @@ def restore_fields(stage):
|
||||
# will be used to restore comments later
|
||||
# noqa, pylint: disable=protected-access
|
||||
stage._stage_text = old._stage_text
|
||||
|
||||
stage.meta = old.meta
|
||||
stage.desc = old.desc
|
||||
|
||||
old_fields = {out.def_path: (out.desc, out.remote) for out in old.outs}
|
||||
|
||||
old_fields = {out.def_path: (out.annot, out.remote) for out in old.outs}
|
||||
for out in stage.outs:
|
||||
out_fields = old_fields.get(out.def_path, None)
|
||||
if out_fields:
|
||||
out.desc, out.remote = out_fields
|
||||
if out_fields := old_fields.get(out.def_path, None):
|
||||
out.annot, out.remote = out_fields
|
||||
|
||||
|
||||
class Stage(params.StageParams):
|
||||
@@ -242,6 +240,14 @@ class Stage(params.StageParams):
|
||||
"""Whether the DVC file was created with `dvc import`."""
|
||||
return not self.cmd and len(self.deps) == 1 and len(self.outs) == 1
|
||||
|
||||
@property
|
||||
def is_partial_import(self) -> bool:
|
||||
"""
|
||||
Whether the DVC file was created using `dvc import --no-download`
|
||||
or `dvc import-url --no-download`.
|
||||
"""
|
||||
return self.is_import and (not self.outs[0].hash_info)
|
||||
|
||||
@property
|
||||
def is_repo_import(self):
|
||||
if not self.is_import:
|
||||
@@ -433,18 +439,30 @@ class Stage(params.StageParams):
|
||||
|
||||
return self
|
||||
|
||||
def update(self, rev=None, to_remote=False, remote=None, jobs=None):
|
||||
def update(
|
||||
self,
|
||||
rev=None,
|
||||
to_remote=False,
|
||||
remote=None,
|
||||
no_download=None,
|
||||
jobs=None,
|
||||
):
|
||||
if not (self.is_repo_import or self.is_import):
|
||||
raise StageUpdateError(self.relpath)
|
||||
update_import(
|
||||
self, rev=rev, to_remote=to_remote, remote=remote, jobs=jobs
|
||||
self,
|
||||
rev=rev,
|
||||
to_remote=to_remote,
|
||||
remote=remote,
|
||||
no_download=no_download,
|
||||
jobs=jobs,
|
||||
)
|
||||
|
||||
def reload(self):
|
||||
return self.dvcfile.stage
|
||||
|
||||
def dumpd(self):
|
||||
return get_dump(self)
|
||||
def dumpd(self, **kwargs):
|
||||
return get_dump(self, **kwargs)
|
||||
|
||||
def compute_md5(self):
|
||||
# `dvc add`ed files don't need stage md5
|
||||
@@ -457,6 +475,7 @@ class Stage(params.StageParams):
|
||||
|
||||
def save(self, allow_missing=False):
|
||||
self.save_deps(allow_missing=allow_missing)
|
||||
|
||||
self.save_outs(allow_missing=allow_missing)
|
||||
self.md5 = self.compute_md5()
|
||||
|
||||
@@ -525,14 +544,16 @@ class Stage(params.StageParams):
|
||||
no_commit=False,
|
||||
force=False,
|
||||
allow_missing=False,
|
||||
no_download=False,
|
||||
**kwargs,
|
||||
):
|
||||
if (self.cmd or self.is_import) and not self.frozen and not dry:
|
||||
self.remove_outs(ignore_remove=False, force=False)
|
||||
|
||||
if not self.frozen and self.is_import:
|
||||
jobs = kwargs.get("jobs", None)
|
||||
self._sync_import(dry, force, jobs)
|
||||
if (not self.frozen and self.is_import) or self.is_partial_import:
|
||||
self._sync_import(
|
||||
dry, force, kwargs.get("jobs", None), no_download
|
||||
)
|
||||
elif not self.frozen and self.cmd:
|
||||
self._run_stage(dry, force, **kwargs)
|
||||
else:
|
||||
@@ -544,7 +565,7 @@ class Stage(params.StageParams):
|
||||
self._check_missing_outputs()
|
||||
|
||||
if not dry:
|
||||
if kwargs.get("checkpoint_func", None):
|
||||
if kwargs.get("checkpoint_func", None) or no_download:
|
||||
allow_missing = True
|
||||
self.save(allow_missing=allow_missing)
|
||||
if not no_commit:
|
||||
@@ -555,8 +576,8 @@ class Stage(params.StageParams):
|
||||
return run_stage(self, dry, force, **kwargs)
|
||||
|
||||
@rwlocked(read=["deps"], write=["outs"])
|
||||
def _sync_import(self, dry, force, jobs):
|
||||
sync_import(self, dry, force, jobs)
|
||||
def _sync_import(self, dry, force, jobs, no_download):
|
||||
sync_import(self, dry, force, jobs, no_download)
|
||||
|
||||
@rwlocked(read=["outs"])
|
||||
def _check_missing_outputs(self):
|
||||
@@ -571,6 +592,8 @@ class Stage(params.StageParams):
|
||||
@rwlocked(write=["outs"])
|
||||
def checkout(self, allow_missing=False, **kwargs):
|
||||
stats = defaultdict(list)
|
||||
if self.is_partial_import:
|
||||
return {}
|
||||
for out in self.filter_outs(kwargs.get("filter_info")):
|
||||
key, outs = self._checkout(
|
||||
out,
|
||||
@@ -658,6 +681,9 @@ class Stage(params.StageParams):
|
||||
self, *args, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
"""Return set of object IDs used by this stage."""
|
||||
if self.is_partial_import:
|
||||
return {}
|
||||
|
||||
used_objs = defaultdict(set)
|
||||
for out in self.filter_outs(kwargs.get("filter_info")):
|
||||
for odb, objs in out.get_used_objs(*args, **kwargs).items():
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ from funcy import cached_property, first
|
||||
from dvc import fs
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import dict_sha256, relpath
|
||||
from dvc_data.transfer import _log_exceptions
|
||||
from dvc_data.hashfile.transfer import _log_exceptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
@@ -31,7 +31,7 @@ class StageFileIsNotDvcFileError(DvcException):
|
||||
|
||||
sname = fname + DVC_FILE_SUFFIX
|
||||
if is_dvc_file(sname):
|
||||
msg += f" Do you mean '{sname}'?"
|
||||
msg += f". Do you mean '{sname}'?"
|
||||
|
||||
super().__init__(msg)
|
||||
|
||||
@@ -80,6 +80,11 @@ class MissingDataSource(DvcException):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class DataSourceChanged(DvcException):
|
||||
def __init__(self, path: str):
|
||||
super().__init__(f"data source changed: {path}")
|
||||
|
||||
|
||||
class StageNotFound(DvcException, KeyError):
|
||||
def __init__(self, file, name):
|
||||
self.file = file.relpath
|
||||
|
||||
+24
-4
@@ -17,20 +17,36 @@ def _update_import_on_remote(stage, remote, jobs):
|
||||
stage.outs[0].transfer(url, odb=odb, jobs=jobs, update=True)
|
||||
|
||||
|
||||
def update_import(stage, rev=None, to_remote=False, remote=None, jobs=None):
|
||||
def update_import(
|
||||
stage, rev=None, to_remote=False, remote=None, no_download=None, jobs=None
|
||||
):
|
||||
stage.deps[0].update(rev=rev)
|
||||
outs = stage.outs
|
||||
deps = stage.deps
|
||||
|
||||
frozen = stage.frozen
|
||||
stage.frozen = False
|
||||
|
||||
if stage.outs:
|
||||
stage.outs[0].clear()
|
||||
try:
|
||||
if to_remote:
|
||||
_update_import_on_remote(stage, remote, jobs)
|
||||
else:
|
||||
stage.reproduce(jobs=jobs)
|
||||
stage.reproduce(no_download=no_download, jobs=jobs)
|
||||
finally:
|
||||
if deps == stage.deps:
|
||||
stage.outs = outs
|
||||
stage.frozen = frozen
|
||||
|
||||
|
||||
def sync_import(stage, dry=False, force=False, jobs=None):
|
||||
def sync_import(
|
||||
stage,
|
||||
dry=False,
|
||||
force=False,
|
||||
jobs=None,
|
||||
no_download=False,
|
||||
):
|
||||
"""Synchronize import's outs to the workspace."""
|
||||
logger.info("Importing '%s' -> '%s'", stage.deps[0], stage.outs[0])
|
||||
if dry:
|
||||
@@ -40,4 +56,8 @@ def sync_import(stage, dry=False, force=False, jobs=None):
|
||||
stage.outs[0].checkout()
|
||||
else:
|
||||
stage.save_deps()
|
||||
stage.deps[0].download(stage.outs[0], jobs=jobs)
|
||||
if not no_download:
|
||||
stage.deps[0].download(
|
||||
stage.outs[0],
|
||||
jobs=jobs,
|
||||
)
|
||||
|
||||
+4
-1
@@ -69,7 +69,10 @@ class StageLoader(Mapping):
|
||||
info = info.copy()
|
||||
info.pop("path", None)
|
||||
item.meta = Meta.from_dict(info)
|
||||
item.hash_info = HashInfo.from_dict(info)
|
||||
hash_value = getattr(item.meta, item.hash_name, None)
|
||||
item.hash_info = HashInfo(item.hash_name, hash_value)
|
||||
if item.hash_info and item.hash_info.isdir:
|
||||
item.meta.isdir = True
|
||||
|
||||
@classmethod
|
||||
def load_stage(cls, dvcfile, name, stage_data, lock_data=None):
|
||||
|
||||
+10
-7
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, List, no_type_check
|
||||
from funcy import post_processing
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.output import Output
|
||||
from dvc.output import Annotation, Output
|
||||
from dvc.utils.collections import apply_diff
|
||||
from dvc.utils.serialize import parse_yaml_for_update
|
||||
|
||||
@@ -27,7 +27,7 @@ PARAM_METRIC = Output.PARAM_METRIC
|
||||
PARAM_PLOT = Output.PARAM_PLOT
|
||||
PARAM_PERSIST = Output.PARAM_PERSIST
|
||||
PARAM_CHECKPOINT = Output.PARAM_CHECKPOINT
|
||||
PARAM_DESC = Output.PARAM_DESC
|
||||
PARAM_DESC = Annotation.PARAM_DESC
|
||||
PARAM_REMOTE = Output.PARAM_REMOTE
|
||||
|
||||
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
@@ -38,8 +38,9 @@ sort_by_path = partial(sorted, key=attrgetter("def_path"))
|
||||
|
||||
@post_processing(OrderedDict)
|
||||
def _get_flags(out):
|
||||
if out.desc:
|
||||
yield PARAM_DESC, out.desc
|
||||
annot = out.annot.to_dict()
|
||||
yield from annot.items()
|
||||
|
||||
if not out.use_cache:
|
||||
yield PARAM_CACHE, False
|
||||
if out.checkpoint:
|
||||
@@ -155,10 +156,12 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
|
||||
assert stage.cmd
|
||||
|
||||
def _dumpd(item):
|
||||
meta_d = item.meta.to_dict()
|
||||
meta_d.pop("isdir", None)
|
||||
ret = [
|
||||
(item.PARAM_PATH, item.def_path),
|
||||
*item.hash_info.to_dict().items(),
|
||||
*item.meta.to_dict().items(),
|
||||
*meta_d.items(),
|
||||
]
|
||||
|
||||
return OrderedDict(ret)
|
||||
@@ -185,8 +188,8 @@ def to_lockfile(stage: "PipelineStage") -> dict:
|
||||
return {stage.name: to_single_stage_lockfile(stage)}
|
||||
|
||||
|
||||
def to_single_stage_file(stage: "Stage"):
|
||||
state = stage.dumpd()
|
||||
def to_single_stage_file(stage: "Stage", **kwargs):
|
||||
state = stage.dumpd(**kwargs)
|
||||
|
||||
# When we load a stage we parse yaml with a fast parser, which strips
|
||||
# off all the comments and formatting. To retain those on update we do
|
||||
|
||||
+11
-6
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Union
|
||||
|
||||
from funcy import concat, first, lsplit, rpartial, without
|
||||
|
||||
from dvc.annotations import ANNOTATION_FIELDS
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
@@ -107,12 +108,16 @@ def _load_live_output(
|
||||
return outs
|
||||
|
||||
|
||||
def fill_stage_dependencies(stage, deps=None, erepo=None, params=None):
|
||||
def fill_stage_dependencies(
|
||||
stage, deps=None, erepo=None, params=None, fs_config=None
|
||||
):
|
||||
from dvc.dependency import loads_from, loads_params
|
||||
|
||||
assert not stage.deps
|
||||
stage.deps = []
|
||||
stage.deps += loads_from(stage, deps or [], erepo=erepo)
|
||||
stage.deps += loads_from(
|
||||
stage, deps or [], erepo=erepo, fs_config=fs_config
|
||||
)
|
||||
stage.deps += loads_params(stage, params or [])
|
||||
|
||||
|
||||
@@ -194,9 +199,9 @@ def compute_md5(stage):
|
||||
return dict_md5(
|
||||
d,
|
||||
exclude=[
|
||||
*ANNOTATION_FIELDS,
|
||||
stage.PARAM_LOCKED, # backward compatibility
|
||||
stage.PARAM_FROZEN,
|
||||
Output.PARAM_DESC,
|
||||
Output.PARAM_METRIC,
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
@@ -221,7 +226,7 @@ def resolve_paths(fs, path, wdir=None):
|
||||
return path, wdir
|
||||
|
||||
|
||||
def get_dump(stage):
|
||||
def get_dump(stage, **kwargs):
|
||||
return {
|
||||
key: value
|
||||
for key, value in {
|
||||
@@ -230,8 +235,8 @@ def get_dump(stage):
|
||||
stage.PARAM_CMD: stage.cmd,
|
||||
stage.PARAM_WDIR: resolve_wdir(stage.wdir, stage.path),
|
||||
stage.PARAM_FROZEN: stage.frozen,
|
||||
stage.PARAM_DEPS: [d.dumpd() for d in stage.deps],
|
||||
stage.PARAM_OUTS: [o.dumpd() for o in stage.outs],
|
||||
stage.PARAM_DEPS: [d.dumpd(**kwargs) for d in stage.deps],
|
||||
stage.PARAM_OUTS: [o.dumpd(**kwargs) for o in stage.outs],
|
||||
stage.PARAM_ALWAYS_CHANGED: stage.always_changed,
|
||||
stage.PARAM_META: stage.meta,
|
||||
}.items()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from dvc import api
|
||||
from dvc.api import DvcFileSystem
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
|
||||
@@ -18,3 +21,103 @@ class TestAPI:
|
||||
|
||||
with api.open("foo") as fd:
|
||||
assert fd.read() == "foo-text"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"clear_cache", [True, False], ids=["cache", "no_cache"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"fs_kwargs",
|
||||
[
|
||||
{},
|
||||
{"url": "{path}"},
|
||||
{"url": "{path}", "rev": "master"},
|
||||
{"url": "file://{posixpath}"},
|
||||
{"url": "file://{posixpath}", "rev": "master"},
|
||||
],
|
||||
ids=["current", "local", "local_rev", "git", "git_rev"],
|
||||
)
|
||||
def test_filesystem(
|
||||
self,
|
||||
M,
|
||||
tmp_dir,
|
||||
make_tmp_dir,
|
||||
scm,
|
||||
dvc,
|
||||
remote,
|
||||
fs_kwargs,
|
||||
clear_cache,
|
||||
):
|
||||
tmp_dir.scm_gen({"scripts": {"script1": "script1"}}, commit="scripts")
|
||||
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}}, commit="data")
|
||||
dvc.push()
|
||||
|
||||
if clear_cache:
|
||||
remove(dvc.odb.repo.path)
|
||||
|
||||
if url := fs_kwargs.get("url"):
|
||||
fs_kwargs["url"] = url.format(
|
||||
path=tmp_dir, posixpath=tmp_dir.as_posix()
|
||||
)
|
||||
|
||||
fs = DvcFileSystem(**fs_kwargs)
|
||||
|
||||
assert fs.ls("/", detail=False) == M.unordered(
|
||||
"/.gitignore", "/scripts", "/data"
|
||||
)
|
||||
assert fs.ls("scripts", detail=False) == ["scripts/script1"]
|
||||
assert fs.ls("data", detail=False) == M.unordered(
|
||||
"data/foo", "data/bar"
|
||||
)
|
||||
|
||||
data_info = M.dict(
|
||||
name="/data",
|
||||
type="directory",
|
||||
dvc_info=M.dict(isdvc=True, isout=True),
|
||||
)
|
||||
scripts_info = M.dict(name="/scripts", type="directory", isexec=False)
|
||||
|
||||
assert sorted(fs.ls("/"), key=lambda i: i["name"]) == [
|
||||
M.dict(name="/.gitignore", type="file", isexec=False),
|
||||
data_info,
|
||||
scripts_info,
|
||||
]
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
fs.info("/not-existing-path")
|
||||
|
||||
assert fs.info("/") == M.dict(name="/", isexec=False, type="directory")
|
||||
assert fs.info("/data") == data_info
|
||||
assert fs.info("/scripts") == scripts_info
|
||||
assert fs.info("/data/foo") == M.dict(name="/data/foo", type="file")
|
||||
assert fs.info("/scripts/script1") == M.dict(
|
||||
name="/scripts/script1", type="file"
|
||||
)
|
||||
|
||||
assert not fs.isdvc("/")
|
||||
assert fs.isdvc("/data")
|
||||
assert fs.isdvc("/data/foo")
|
||||
assert not fs.isdvc("/scripts")
|
||||
assert not fs.isdvc("/scripts/script1")
|
||||
|
||||
with pytest.raises((IsADirectoryError, PermissionError)):
|
||||
fs.open("data")
|
||||
with pytest.raises((IsADirectoryError, PermissionError)):
|
||||
fs.open("scripts")
|
||||
with fs.open("/data/foo") as fobj:
|
||||
assert fobj.read() == b"foo"
|
||||
with fs.open("/scripts/script1") as fobj:
|
||||
assert fobj.read() == b"script1"
|
||||
|
||||
tmp = make_tmp_dir("temp-download")
|
||||
fs.get_file("data/foo", tmp / "foo")
|
||||
assert (tmp / "foo").read_text() == "foo"
|
||||
|
||||
fs.get_file("scripts/script1", tmp / "script1")
|
||||
assert (tmp / "script1").read_text() == "script1"
|
||||
|
||||
fs.get("/", (tmp / "all").fs_path, recursive=True)
|
||||
assert (tmp / "all").read_text() == {
|
||||
".gitignore": "/data\n",
|
||||
"data": {"bar": "bar", "foo": "foo"},
|
||||
"scripts": {"script1": "script1"},
|
||||
}
|
||||
|
||||
+63
-7
@@ -1,21 +1,50 @@
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from hydra import compose, initialize_config_dir
|
||||
from hydra._internal.config_loader_impl import ConfigLoaderImpl
|
||||
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
|
||||
from hydra.core.override_parser.overrides_parser import OverridesParser
|
||||
from hydra.core.override_parser.types import ValueType
|
||||
from hydra.errors import ConfigCompositionException, OverrideParseException
|
||||
from hydra.types import RunMode
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
|
||||
from .serialize import MODIFIERS
|
||||
from .serialize import DUMPERS, MODIFIERS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
def compose_and_dump(
|
||||
output_file: "StrPath",
|
||||
config_dir: str,
|
||||
config_name: str,
|
||||
overrides: List[str],
|
||||
) -> None:
|
||||
"""Compose Hydra config and dumpt it to `output_file`.
|
||||
|
||||
Args:
|
||||
output_file: File where the composed config will be dumped.
|
||||
config_dir: Folder containing the Hydra config files.
|
||||
Must be absolute file system path.
|
||||
config_name: Name of the config file containing defaults,
|
||||
without the .yaml extension.
|
||||
overrides: List of `Hydra Override`_ patterns.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
with initialize_config_dir(config_dir, version_base=None):
|
||||
cfg = compose(config_name=config_name, overrides=overrides)
|
||||
|
||||
dumper = DUMPERS[Path(output_file).suffix.lower()]
|
||||
dumper(output_file, OmegaConf.to_object(cfg))
|
||||
|
||||
|
||||
def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
"""Update `path` params with the provided `Hydra Override`_ patterns.
|
||||
|
||||
@@ -32,11 +61,7 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
modify_data = MODIFIERS[suffix]
|
||||
with modify_data(path) as original_data:
|
||||
try:
|
||||
parser = OverridesParser.create()
|
||||
parsed = parser.parse_overrides(overrides=overrides)
|
||||
ConfigLoaderImpl.validate_sweep_overrides_legal(
|
||||
parsed, run_mode=RunMode.RUN, from_shell=True
|
||||
)
|
||||
parsed = to_hydra_overrides(overrides)
|
||||
|
||||
new_data = OmegaConf.create(
|
||||
to_omegaconf(original_data),
|
||||
@@ -51,3 +76,34 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
|
||||
merge_dicts(original_data, new_data)
|
||||
remove_missing_keys(original_data, new_data)
|
||||
|
||||
|
||||
def to_hydra_overrides(path_overrides):
|
||||
parser = OverridesParser.create()
|
||||
return parser.parse_overrides(overrides=path_overrides)
|
||||
|
||||
|
||||
def get_hydra_sweeps(path_overrides):
|
||||
merged_overrides = []
|
||||
for path, overrides in path_overrides.items():
|
||||
merged_overrides.extend(
|
||||
[f"{path}___{override}" for override in overrides]
|
||||
)
|
||||
|
||||
hydra_overrides = to_hydra_overrides(merged_overrides)
|
||||
for hydra_override in hydra_overrides:
|
||||
if hydra_override.value_type == ValueType.GLOB_CHOICE_SWEEP:
|
||||
raise InvalidArgumentError(
|
||||
f"Glob override '{hydra_override.input_line}' "
|
||||
"is not supported."
|
||||
)
|
||||
|
||||
splits = BasicSweeper.split_arguments(hydra_overrides, None)[0]
|
||||
sweeps = []
|
||||
for split in splits:
|
||||
sweep_overrides = defaultdict(list)
|
||||
for merged_override in split:
|
||||
path, override = merged_override.split("___")
|
||||
sweep_overrides[path].append(override)
|
||||
sweeps.append(dict(sweep_overrides))
|
||||
return sweeps
|
||||
|
||||
+3
-3
@@ -30,11 +30,11 @@ line_length = 79
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_level = "debug"
|
||||
addopts = "-ra"
|
||||
addopts = "-ra -p no:hydra_pytest"
|
||||
markers = [
|
||||
"needs_internet: Might need network access for the tests",
|
||||
"vscode: Tests verifying contract between DVC and VSCode plugin",
|
||||
"studio: Tests verifying contract between DVC and Studio"
|
||||
"vscode: Tests verifying contract between DVC and VSCode plugin",
|
||||
"studio: Tests verifying contract between DVC and Studio",
|
||||
]
|
||||
xfail_strict = true
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# NOTE: using strict version due to
|
||||
# https://github.com/iterative/dvc/issues/7949
|
||||
PyInstaller==5.2
|
||||
PyInstaller==5.0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import textwrap
|
||||
from subprocess import STDOUT, check_call
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import pathlib
|
||||
from subprocess import STDOUT, check_call
|
||||
from subprocess import STDOUT, check_call, check_output
|
||||
|
||||
path = pathlib.Path(__file__).parent.absolute()
|
||||
hooks = path / "hooks"
|
||||
@@ -21,10 +21,27 @@ check_call(
|
||||
stderr=STDOUT,
|
||||
)
|
||||
|
||||
check_call(
|
||||
out = check_output(
|
||||
[
|
||||
path / "dist" / "dvc" / "dvc",
|
||||
"doctor",
|
||||
],
|
||||
stderr=STDOUT,
|
||||
)
|
||||
).decode()
|
||||
|
||||
remotes = [
|
||||
"s3",
|
||||
"oss",
|
||||
"gdrive",
|
||||
"gs",
|
||||
"hdfs",
|
||||
"http",
|
||||
"webhdfs",
|
||||
"azure",
|
||||
"ssh",
|
||||
"webdav",
|
||||
]
|
||||
|
||||
print(out)
|
||||
for remote in remotes:
|
||||
assert f"\t{remote}" in out, f"Missing support for {remote}"
|
||||
|
||||
@@ -16,5 +16,15 @@ datas += copy_metadata("webdav4")
|
||||
datas += copy_metadata("aiohttp")
|
||||
datas += copy_metadata("aiohttp_retry")
|
||||
|
||||
# https://github.com/pypa/setuptools/issues/1963
|
||||
hiddenimports = ["pkg_resources.py2_warn"]
|
||||
hiddenimports = [
|
||||
"dvc_azure",
|
||||
"dvc_gdrive",
|
||||
"dvc_gs",
|
||||
"dvc_hdfs",
|
||||
"dvc_oss",
|
||||
"dvc_s3",
|
||||
"dvc_webdav",
|
||||
"dvc_webhdfs",
|
||||
# https://github.com/pypa/setuptools/issues/1963
|
||||
"pkg_resources.py2_warn",
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
hiddenimports = ["fsspec.implementations.memory"]
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Temporary fix for https://github.com/iterative/dvc/issues/5618."""
|
||||
from PyInstaller.utils.hooks import ( # pylint:disable=import-error
|
||||
collect_data_files,
|
||||
copy_metadata,
|
||||
)
|
||||
|
||||
datas = collect_data_files("googleapiclient.discovery")
|
||||
datas += copy_metadata("google_api_python_client")
|
||||
@@ -62,14 +62,12 @@ install_requires =
|
||||
rich>=10.13.0
|
||||
pyparsing>=2.4.7
|
||||
typing-extensions>=3.7.4
|
||||
fsspec[http]>=2021.10.1
|
||||
aiohttp-retry>=2.4.5
|
||||
scmrepo==0.0.25
|
||||
dvc-render==0.0.9
|
||||
scmrepo==0.1.1
|
||||
dvc-render==0.0.10
|
||||
dvc-task==0.1.2
|
||||
dvclive>=0.10.0
|
||||
dvc-data==0.1.17
|
||||
dvc-http==0.0.2
|
||||
dvc-data==0.8.0
|
||||
dvc-http==2.19.1
|
||||
hydra-core>=1.1.0
|
||||
|
||||
[options.extras_require]
|
||||
@@ -82,46 +80,34 @@ all =
|
||||
%(s3)s
|
||||
%(ssh)s
|
||||
%(webdav)s
|
||||
%(webhdfs)s
|
||||
dev =
|
||||
%(all)s
|
||||
%(tests)s
|
||||
dvc-data[cli]
|
||||
# Extra dependencies for remote integrations
|
||||
azure =
|
||||
adlfs>=2021.10.0
|
||||
azure-identity>=1.4.0
|
||||
knack
|
||||
gdrive = pydrive2[fsspec]>=1.10.1
|
||||
gs = gcsfs>=2021.11.1
|
||||
azure = dvc-azure==2.20.2
|
||||
gdrive = dvc-gdrive==2.19.0
|
||||
gs = dvc-gs==2.19.1
|
||||
hdfs =
|
||||
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
|
||||
# explicitly specify pyarrow here
|
||||
pyarrow>=1
|
||||
fsspec[arrow]
|
||||
oss = ossfs>=2021.8.0
|
||||
s3 =
|
||||
s3fs[boto3]>=2022.02.0
|
||||
aiobotocore[boto3]>2
|
||||
ssh =
|
||||
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
|
||||
# explicitly specify bcrypt here
|
||||
bcrypt
|
||||
sshfs[bcrypt]>=2022.6.0
|
||||
dvc-hdfs==2.19.0; python_version < '3.11'
|
||||
oss = dvc-oss==2.19.0
|
||||
s3 = dvc-s3==2.20.0
|
||||
ssh = dvc-ssh==2.19.0
|
||||
# gssapi should not be included in all_remotes, because it doesn't have wheels
|
||||
# for Linux, so it will fail to compile if user doesn't have all the
|
||||
# requirements, including kerberos itself.
|
||||
ssh_gssapi = sshfs[gssapi]>=2022.6.0
|
||||
webdav = webdav4>=0.9.3
|
||||
# not to break `dvc[webhdfs]`
|
||||
webhdfs =
|
||||
ssh_gssapi = dvc-ssh[gssapi]==2.19.0
|
||||
webdav = dvc-webdav==2.19.0
|
||||
webhdfs = dvc-webhdfs==2.19.0
|
||||
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
|
||||
webhdfs_kerberos = requests-kerberos==0.14.0
|
||||
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
|
||||
terraform = tpi[ssh]>=2.1.0
|
||||
tests =
|
||||
%(terraform)s
|
||||
dvc_ssh==0.0.1a0
|
||||
dvc-ssh==2.19.0
|
||||
# Test requirements
|
||||
pytest==7.1.2
|
||||
pytest==7.1.3
|
||||
pytest-cov==3.0.0
|
||||
pytest-xdist==2.5.0
|
||||
pytest-mock==3.8.2
|
||||
@@ -130,11 +116,10 @@ tests =
|
||||
# https://github.com/docker/docker-py/issues/2902
|
||||
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
|
||||
flaky==3.7.0
|
||||
mock==4.0.3
|
||||
pytest-timeout==2.1.0
|
||||
filelock==3.8.0
|
||||
# pylint requirements
|
||||
pylint==2.14.5
|
||||
pylint==2.15.2
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
pylint-pytest==1.1.2
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
@@ -160,6 +145,8 @@ exclude =
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
dvc = dvc.cli:main
|
||||
fsspec.specs =
|
||||
dvc = dvc.api:DvcFileSystem
|
||||
|
||||
[flake8]
|
||||
ignore=
|
||||
|
||||
@@ -250,3 +250,15 @@ def run_copy_metrics(tmp_dir, run_copy):
|
||||
return stage
|
||||
|
||||
return run
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_hydra_conf(mocker):
|
||||
if sys.version_info < (3, 11):
|
||||
return
|
||||
|
||||
# `hydra.conf` fails to import in 3.11, it raises ValueError due to changes
|
||||
# in dataclasses. See https://github.com/python/cpython/pull/29867.
|
||||
# NOTE: using sentinel here so that any imports from `hydra.conf`
|
||||
# return a mock.
|
||||
sys.modules["hydra.conf"] = mocker.sentinel
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
@@ -67,6 +68,9 @@ def test_params_show_targets(params_repo):
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
}
|
||||
assert api.params_show("params.yaml", stages="stage-1") == {
|
||||
"foo": 5,
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_deps(params_repo):
|
||||
@@ -92,6 +96,27 @@ def test_params_show_stages(params_repo):
|
||||
api.params_show(stages="stage-0")
|
||||
|
||||
|
||||
def test_params_show_stage_addressing(tmp_dir, dvc):
|
||||
for subdir in {"subdir1", "subdir2"}:
|
||||
subdir = tmp_dir / subdir
|
||||
subdir.mkdir()
|
||||
with subdir.chdir():
|
||||
subdir.gen("params.yaml", "foo: 1")
|
||||
|
||||
dvc.run(name="stage-0", cmd="echo stage-0", params=["foo"])
|
||||
|
||||
for s in {"subdir1", "subdir2"}:
|
||||
dvcyaml = os.path.join(s, "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
with subdir.chdir():
|
||||
nested = subdir / "nested"
|
||||
nested.mkdir()
|
||||
with nested.chdir():
|
||||
dvcyaml = os.path.join("..", "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
|
||||
def test_params_show_revs(params_repo):
|
||||
assert api.params_show(rev="HEAD~1") == {
|
||||
"params.yaml:foo": 1,
|
||||
@@ -168,3 +193,15 @@ def test_params_show_stage_without_params(tmp_dir, dvc):
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show(deps=True)
|
||||
|
||||
|
||||
def test_params_show_untracked_target(params_repo, tmp_dir):
|
||||
tmp_dir.gen("params_foo.yaml", "foo: 1")
|
||||
|
||||
assert api.params_show("params_foo.yaml") == {"foo": 1}
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show("params_foo.yaml", stages="stage-0")
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show("params_foo.yaml", deps=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from dvc.exceptions import DownloadError, UploadError
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.db import get_index
|
||||
from dvc_data.hashfile.db import get_index
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -3,7 +3,9 @@ from textwrap import dedent
|
||||
import pytest
|
||||
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
from tests.unit.repo.experiments.conftest import test_queue # noqa
|
||||
from tests.unit.repo.experiments.conftest import ( # noqa pylint disable=unused-argument
|
||||
test_queue,
|
||||
)
|
||||
|
||||
DEFAULT_ITERATIONS = 2
|
||||
CHECKPOINT_SCRIPT_FORMAT = dedent(
|
||||
|
||||
@@ -4,9 +4,9 @@ from functools import partial
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
from dvc_ssh import SSHFileSystem
|
||||
from dvc_ssh.tests.cloud import TEST_SSH_KEY_PATH, TEST_SSH_USER
|
||||
|
||||
from dvc.fs import SSHFileSystem
|
||||
from dvc.repo.experiments.executor.base import ExecutorInfo, ExecutorResult
|
||||
from dvc.repo.experiments.executor.ssh import SSHExecutor
|
||||
from dvc.repo.experiments.refs import EXEC_HEAD, EXEC_MERGE
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
from funcy import first
|
||||
|
||||
|
||||
def test_diff_no_commits(tmp_dir):
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
git = Git.init(tmp_dir.fs_path)
|
||||
assert git.no_commits
|
||||
|
||||
assert Repo.init().experiments.diff() == {}
|
||||
|
||||
|
||||
def test_diff_empty(tmp_dir, scm, dvc, exp_stage):
|
||||
assert dvc.experiments.diff() == {"params": {}, "metrics": {}}
|
||||
|
||||
|
||||
@@ -104,20 +104,6 @@ def test_failed_exp_workspace(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes, expected",
|
||||
[
|
||||
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
],
|
||||
)
|
||||
def test_modify_params(params_repo, dvc, changes, expected):
|
||||
dvc.experiments.run(params=changes)
|
||||
# pylint: disable=unspecified-encoding
|
||||
with open("params.yaml", mode="r") as fobj:
|
||||
assert fobj.read().strip() == expected
|
||||
|
||||
|
||||
def test_apply(tmp_dir, scm, dvc, exp_stage):
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo.experiments.exceptions import ApplyConflictError
|
||||
|
||||
@@ -42,6 +42,7 @@ def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
|
||||
return name_list
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
@pytest.mark.parametrize("follow", [True, False])
|
||||
def test_celery_logs(
|
||||
tmp_dir,
|
||||
@@ -64,7 +65,7 @@ def test_celery_logs(
|
||||
assert "failed to reproduce 'failed-copy-file'" in captured.out
|
||||
|
||||
|
||||
@pytest.mark.flaky(max_runs=3, min_passes=1)
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
def test_queue_remove_done(dvc, failed_tasks, success_tasks):
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 3
|
||||
status = to_dict(dvc.experiments.celery_queue.status())
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
from ..utils.test_hydra import hydra_setup
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes, expected",
|
||||
[
|
||||
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
],
|
||||
)
|
||||
def test_modify_params(params_repo, dvc, changes, expected):
|
||||
dvc.experiments.run(params=changes)
|
||||
# pylint: disable=unspecified-encoding
|
||||
with open("params.yaml", mode="r") as fobj:
|
||||
assert fobj.read().strip() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hydra_enabled",
|
||||
[
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 11), reason="unsupported on 3.11"
|
||||
),
|
||||
),
|
||||
False,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"config_dir,config_name",
|
||||
[
|
||||
(None, None),
|
||||
(None, "bar"),
|
||||
("conf", "bar"),
|
||||
],
|
||||
)
|
||||
def test_hydra_compose_and_dump(
|
||||
tmp_dir, params_repo, dvc, hydra_enabled, config_dir, config_name
|
||||
):
|
||||
hydra_setup(
|
||||
tmp_dir,
|
||||
config_dir=config_dir or "conf",
|
||||
config_name=config_name or "config",
|
||||
)
|
||||
|
||||
dvc.experiments.run()
|
||||
assert (tmp_dir / "params.yaml").parse() == {
|
||||
"foo": [{"bar": 1}, {"baz": 2}],
|
||||
"goo": {"bag": 3.0},
|
||||
"lorem": False,
|
||||
}
|
||||
|
||||
with dvc.config.edit() as conf:
|
||||
if hydra_enabled:
|
||||
conf["hydra"]["enabled"] = True
|
||||
if config_dir is not None:
|
||||
conf["hydra"]["config_dir"] = config_dir
|
||||
if config_name is not None:
|
||||
conf["hydra"]["config_name"] = config_name
|
||||
|
||||
dvc.experiments.run()
|
||||
|
||||
if hydra_enabled:
|
||||
assert (tmp_dir / "params.yaml").parse() == {
|
||||
"db": {"driver": "mysql", "user": "omry", "pass": "secret"},
|
||||
}
|
||||
|
||||
dvc.experiments.run(params=["db=postgresql"])
|
||||
assert (tmp_dir / "params.yaml").parse() == {
|
||||
"db": {
|
||||
"driver": "postgresql",
|
||||
"user": "foo",
|
||||
"pass": "bar",
|
||||
"timeout": 10,
|
||||
}
|
||||
}
|
||||
else:
|
||||
assert (tmp_dir / "params.yaml").parse() == {
|
||||
"foo": [{"bar": 1}, {"baz": 2}],
|
||||
"goo": {"bag": 3.0},
|
||||
"lorem": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hydra_enabled,overrides,expected",
|
||||
[
|
||||
(
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.xfail(
|
||||
sys.version_info >= (3, 11), reason="unsupported on 3.11"
|
||||
),
|
||||
),
|
||||
["db=mysql,postgresql"],
|
||||
[
|
||||
{"params.yaml": ["db=mysql"]},
|
||||
{"params.yaml": ["db=postgresql"]},
|
||||
],
|
||||
),
|
||||
(
|
||||
False,
|
||||
["foo=bar,baz"],
|
||||
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hydra_sweep(
|
||||
tmp_dir, params_repo, dvc, mocker, hydra_enabled, overrides, expected
|
||||
):
|
||||
patched = mocker.patch.object(dvc.experiments, "queue_one")
|
||||
|
||||
if hydra_enabled:
|
||||
hydra_setup(
|
||||
tmp_dir,
|
||||
config_dir="conf",
|
||||
config_name="config",
|
||||
)
|
||||
with dvc.config.edit() as conf:
|
||||
conf["hydra"]["enabled"] = True
|
||||
|
||||
dvc.experiments.run(params=overrides, queue=True)
|
||||
|
||||
assert patched.call_count == len(expected)
|
||||
for e in expected:
|
||||
patched.assert_any_call(
|
||||
mocker.ANY,
|
||||
params=e,
|
||||
reset=True,
|
||||
targets=None,
|
||||
)
|
||||
|
||||
|
||||
def test_hydra_sweep_requires_queue(params_repo, dvc):
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.run(params=["db=mysql,postgresql"])
|
||||
@@ -35,6 +35,18 @@ def make_executor_info(**kwargs):
|
||||
return ExecutorInfo(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_no_commits(tmp_dir):
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
git = Git.init(tmp_dir.fs_path)
|
||||
assert git.no_commits
|
||||
|
||||
assert Repo.init().experiments.show() == {}
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_simple(tmp_dir, scm, dvc, exp_stage):
|
||||
assert dvc.experiments.show()["workspace"] == {
|
||||
|
||||
@@ -21,6 +21,32 @@ def test_show_simple(tmp_dir, dvc, run_copy_metrics):
|
||||
}
|
||||
|
||||
|
||||
def test_show_simple_from_subdir(tmp_dir, dvc, run_copy_metrics):
|
||||
subdir = tmp_dir / "subdir"
|
||||
subdir.mkdir()
|
||||
tmp_dir.gen("metrics_t.yaml", "1.1")
|
||||
run_copy_metrics(
|
||||
"metrics_t.yaml",
|
||||
"subdir/metrics.yaml",
|
||||
metrics=["subdir/metrics.yaml"],
|
||||
)
|
||||
|
||||
expected_path = os.path.join("subdir", "metrics.yaml")
|
||||
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
|
||||
|
||||
expected_path = os.path.join("..", "subdir", "metrics.yaml")
|
||||
with subdir.chdir():
|
||||
assert dvc.metrics.show() == {
|
||||
"": {"data": {expected_path: {"data": 1.1}}}
|
||||
}
|
||||
subdir2 = tmp_dir / "subdir2"
|
||||
subdir2.mkdir()
|
||||
with subdir2.chdir():
|
||||
assert dvc.metrics.show() == {
|
||||
"": {"data": {expected_path: {"data": 1.1}}}
|
||||
}
|
||||
|
||||
|
||||
def test_show(tmp_dir, dvc, run_copy_metrics):
|
||||
tmp_dir.gen("metrics_t.yaml", "foo: 1.1")
|
||||
run_copy_metrics(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from tests.utils.plots import get_plot
|
||||
|
||||
|
||||
@@ -46,3 +48,15 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
assert get_plot(
|
||||
diff_result, "workspace", "definitions", file="", endkey="data"
|
||||
) == {"metric.json": props}
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_no_commits(tmp_dir):
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
git = Git.init(tmp_dir.fs_path)
|
||||
assert git.no_commits
|
||||
|
||||
assert Repo.init().plots.diff() == {}
|
||||
|
||||
+32
-3
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
import dvc as dvc_module
|
||||
import dvc_data
|
||||
from dvc.annotations import Annotation
|
||||
from dvc.cli import main
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.exceptions import (
|
||||
@@ -103,7 +104,7 @@ def test_add_unsupported_file(dvc):
|
||||
|
||||
|
||||
def test_add_directory(tmp_dir, dvc):
|
||||
from dvc_data import load
|
||||
from dvc_data.hashfile import load
|
||||
|
||||
(stage,) = tmp_dir.dvc_gen({"dir": {"file": "file"}})
|
||||
|
||||
@@ -457,7 +458,7 @@ class TestAddCommit(TestDvc):
|
||||
|
||||
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
tmp_dir.gen({"data/data": "foo"})
|
||||
counter = mocker.spy(dvc_data.build, "_build_tree")
|
||||
counter = mocker.spy(dvc_data.hashfile.build, "_build_tree")
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 3
|
||||
@@ -881,7 +882,7 @@ def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
|
||||
mocker.patch(
|
||||
"dvc_data.checkout.test_links",
|
||||
"dvc_data.hashfile.checkout.test_links",
|
||||
return_value=[],
|
||||
)
|
||||
dvc.add("foo")
|
||||
@@ -903,6 +904,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: foo # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
remote: testremote
|
||||
meta: some metadata
|
||||
"""
|
||||
@@ -916,6 +921,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: foo # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
remote: testremote
|
||||
md5: acbd18db4cc2f85cedef654fccc4a4d8
|
||||
size: 3
|
||||
@@ -1161,3 +1170,23 @@ def test_add_ignore_duplicated_targets(tmp_dir, dvc, capsys):
|
||||
_, err = capsys.readouterr()
|
||||
assert len(stages) == 3
|
||||
assert "ignoring duplicated targets: foo, bar" in err
|
||||
|
||||
|
||||
def test_add_with_annotations(M, tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
|
||||
annot = {
|
||||
"desc": "foo desc",
|
||||
"labels": ["l1", "l2"],
|
||||
"type": "t1",
|
||||
"meta": {"key": "value"},
|
||||
}
|
||||
(stage,) = dvc.add("foo", **annot)
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
# try to selectively update/overwrite some annotations
|
||||
annot = {**annot, "type": "t2"}
|
||||
(stage,) = dvc.add("foo", type="t2")
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
@@ -90,7 +90,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
|
||||
|
||||
class TestCheckoutCorruptedCacheDir(TestDvc):
|
||||
def test(self):
|
||||
from dvc_data import load
|
||||
from dvc_data.hashfile import load
|
||||
|
||||
# NOTE: using 'copy' so that cache and link don't have same inode
|
||||
ret = main(["config", "cache.type", "copy"])
|
||||
|
||||
@@ -47,6 +47,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: foo # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
meta:
|
||||
key1: value1
|
||||
key2: value2
|
||||
remote: testremote
|
||||
meta: some metadata
|
||||
"""
|
||||
@@ -61,6 +68,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: foo # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
meta:
|
||||
key1: value1
|
||||
key2: value2
|
||||
remote: testremote
|
||||
md5: acbd18db4cc2f85cedef654fccc4a4d8
|
||||
size: 3
|
||||
|
||||
@@ -13,8 +13,8 @@ from dvc.testing.test_remote import ( # noqa, pylint: disable=unused-import
|
||||
TestRemote,
|
||||
)
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.db.local import LocalHashFileDB
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.db.local import LocalHashFileDB
|
||||
|
||||
|
||||
def test_cloud_cli(tmp_dir, dvc, remote, mocker):
|
||||
@@ -273,6 +273,20 @@ def test_pull_external_dvc_imports(tmp_dir, dvc, scm, erepo_dir):
|
||||
assert (tmp_dir / "new_dir" / "bar").read_text() == "bar"
|
||||
|
||||
|
||||
def test_pull_partial_import(tmp_dir, dvc, local_workspace):
|
||||
local_workspace.gen("file", "file content")
|
||||
dst = tmp_dir / "file"
|
||||
stage = dvc.imp_url(
|
||||
"remote://workspace/file", os.fspath(dst), no_download=True
|
||||
)
|
||||
|
||||
result = dvc.pull("file")
|
||||
assert result["fetched"] == 1
|
||||
assert dst.exists()
|
||||
|
||||
assert stage.outs[0].get_hash().value == "d10b4c3ff123b26dc068d43a8bef2d23"
|
||||
|
||||
|
||||
def test_pull_external_dvc_imports_mixed(
|
||||
tmp_dir, dvc, scm, erepo_dir, local_remote
|
||||
):
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from operator import itemgetter
|
||||
|
||||
from dvc.repo.data import ls
|
||||
|
||||
|
||||
def test_data_ls(tmp_dir, dvc):
|
||||
assert not list(ls(dvc))
|
||||
|
||||
tmp_dir.dvc_gen("bar", "bar")
|
||||
tmp_dir.gen("foo", "foo")
|
||||
dvc.add(
|
||||
"foo",
|
||||
meta={"key": "value"},
|
||||
labels=["l1", "l2"],
|
||||
type="t1",
|
||||
desc="foo",
|
||||
)
|
||||
|
||||
foo_entry = {
|
||||
"path": "foo",
|
||||
"desc": "foo",
|
||||
"type": "t1",
|
||||
"labels": ["l1", "l2"],
|
||||
"meta": {"key": "value"},
|
||||
}
|
||||
assert sorted(ls(dvc), key=itemgetter("path")) == [
|
||||
{"path": "bar"},
|
||||
foo_entry,
|
||||
]
|
||||
assert list(ls(dvc, targets=["foo"])) == [foo_entry]
|
||||
assert list(ls(dvc, targets=["foo"], recursive=True)) == [foo_entry]
|
||||
+322
-53
@@ -1,6 +1,10 @@
|
||||
from os import fspath
|
||||
from os.path import join
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.data import _transform_git_paths_to_dvc, posixpath_to_os_path
|
||||
from dvc.testing.tmp_dir import make_subrepo
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
@@ -14,24 +18,55 @@ EMPTY_STATUS = {
|
||||
}
|
||||
|
||||
|
||||
def test_file(M, tmp_dir, scm, dvc):
|
||||
@pytest.mark.parametrize("path", [None, ("sub", "repo")])
|
||||
def test_git_to_dvc_path_wdir_transformation(tmp_dir, scm, path):
|
||||
struct = {"dir": {"foo": "foo", "bar": "bar"}, "file": "file", "dir2": {}}
|
||||
tmp_dir.gen(struct)
|
||||
|
||||
subdir = tmp_dir.joinpath(*path) if path else tmp_dir
|
||||
make_subrepo(subdir, scm)
|
||||
dvc = subdir.dvc
|
||||
|
||||
with subdir.chdir():
|
||||
subdir.gen(struct)
|
||||
_, _, untracked = scm.status(untracked_files="all")
|
||||
# make order independent of the platforms for easier test assertions
|
||||
untracked = sorted(map(posixpath_to_os_path, untracked), reverse=True)
|
||||
assert _transform_git_paths_to_dvc(dvc, untracked) == [
|
||||
"file",
|
||||
join("dir", "foo"),
|
||||
join("dir", "bar"),
|
||||
]
|
||||
with (subdir / "dir").chdir():
|
||||
assert _transform_git_paths_to_dvc(dvc, untracked) == [
|
||||
join("..", "file"),
|
||||
"foo",
|
||||
"bar",
|
||||
]
|
||||
with (subdir / "dir2").chdir():
|
||||
assert _transform_git_paths_to_dvc(dvc, untracked) == [
|
||||
join("..", "file"),
|
||||
join("..", "dir", "foo"),
|
||||
join("..", "dir", "bar"),
|
||||
]
|
||||
|
||||
|
||||
def test_file(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen("foo", "foo", commit="add foo")
|
||||
tmp_dir.dvc_gen("foo", "foobar")
|
||||
remove(tmp_dir / "foo")
|
||||
|
||||
expected = {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"modified": ["foo"]},
|
||||
"uncommitted": {"deleted": ["foo"]},
|
||||
"git": M.instance_of(dict),
|
||||
"not_in_cache": [],
|
||||
"unchanged": [],
|
||||
"untracked": [],
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status() == expected
|
||||
assert dvc.data_status(granular=True) == expected
|
||||
|
||||
|
||||
def test_directory(M, tmp_dir, scm, dvc):
|
||||
def test_directory(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar", "foobar": "foobar"}})
|
||||
remove(tmp_dir / "dir")
|
||||
@@ -39,12 +74,10 @@ def test_directory(M, tmp_dir, scm, dvc):
|
||||
tmp_dir.gen("untracked", "untracked")
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"modified": [join("dir", "")]},
|
||||
"uncommitted": {"modified": [join("dir", "")]},
|
||||
"git": M.instance_of(dict),
|
||||
"not_in_cache": [],
|
||||
"unchanged": [],
|
||||
"untracked": [],
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
@@ -52,20 +85,52 @@ def test_directory(M, tmp_dir, scm, dvc):
|
||||
"added": M.unordered(
|
||||
join("dir", "bar"),
|
||||
join("dir", "foobar"),
|
||||
)
|
||||
),
|
||||
"modified": [join("dir", "")],
|
||||
},
|
||||
"uncommitted": {
|
||||
"added": [join("dir", "baz")],
|
||||
"modified": [join("dir", "bar")],
|
||||
"modified": M.unordered(join("dir", ""), join("dir", "bar")),
|
||||
"deleted": [join("dir", "foobar")],
|
||||
},
|
||||
"git": M.instance_of(dict),
|
||||
"git": M.dict(),
|
||||
"not_in_cache": [],
|
||||
"unchanged": [join("dir", "foo")],
|
||||
"untracked": ["untracked"],
|
||||
}
|
||||
|
||||
|
||||
def test_tracked_directory_deep(M, tmp_dir, dvc, scm):
|
||||
"""Test for a directory not in cwd, but nested inside other directories."""
|
||||
(tmp_dir / "sub").gen({"dir": {"foo": "foo"}})
|
||||
dvc.add(fspath(tmp_dir / "sub" / "dir"))
|
||||
scm.add_commit(["sub/dir.dvc", "sub/.gitignore"], message="add sub/dir")
|
||||
|
||||
(tmp_dir / "sub" / "dir").gen("bar", "bar")
|
||||
dvc.commit(None, force=True)
|
||||
(tmp_dir / "sub" / "dir").gen("foobar", "foobar")
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"modified": [join("sub", "dir", "")]},
|
||||
"uncommitted": {"modified": [join("sub", "dir", "")]},
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {
|
||||
"added": [join("sub", "dir", "bar")],
|
||||
"modified": [join("sub", "dir", "")],
|
||||
},
|
||||
"uncommitted": {
|
||||
"added": [join("sub", "dir", "foobar")],
|
||||
"modified": [join("sub", "dir", "")],
|
||||
},
|
||||
"git": M.dict(),
|
||||
"unchanged": [join("sub", "dir", "foo")],
|
||||
}
|
||||
|
||||
|
||||
def test_new_empty_git_repo(M, tmp_dir, scm):
|
||||
dvc = Repo.init()
|
||||
assert dvc.data_status() == {
|
||||
@@ -81,52 +146,19 @@ def test_noscm_repo(dvc):
|
||||
assert dvc.data_status() == EMPTY_STATUS
|
||||
|
||||
|
||||
def test_unchanged(M, tmp_dir, scm, dvc):
|
||||
def test_unchanged(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"git": M.instance_of(dict),
|
||||
"git": M.dict(),
|
||||
"unchanged": M.unordered("bar", join("dir", "")),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"git": M.instance_of(dict),
|
||||
"unchanged": M.unordered("bar", join("dir", "foo")),
|
||||
}
|
||||
|
||||
|
||||
def test_not_in_cache(M, tmp_dir, scm, dvc):
|
||||
# TODO: investigation required, might return wrong results
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
|
||||
remove(dvc.odb.local.path)
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"not_in_cache": M.unordered("bar", join("dir", "")),
|
||||
"git": M.instance_of(dict),
|
||||
"unchanged": ["bar"],
|
||||
"uncommitted": {"added": [join("dir", "")]},
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"not_in_cache": M.unordered("bar"),
|
||||
"git": M.instance_of(dict),
|
||||
"unchanged": [],
|
||||
"committed": {"modified": ["bar"]},
|
||||
"uncommitted": {"modified": ["bar"], "added": [join("dir", "foo")]},
|
||||
}
|
||||
|
||||
|
||||
def test_withdirs(M, tmp_dir, scm, dvc):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
|
||||
assert dvc.data_status(granular=True, with_dirs=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"git": M.instance_of(dict),
|
||||
"unchanged": M.unordered("bar", join("dir", "foo"), join("dir", "")),
|
||||
"git": M.dict(),
|
||||
"unchanged": M.unordered("bar", join("dir", ""), join("dir", "foo")),
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +178,8 @@ def test_skip_uncached_pipeline_outputs(tmp_dir, dvc, run_copy_metrics):
|
||||
|
||||
def test_output_with_newly_added_stage(tmp_dir, dvc):
|
||||
dvc.stage.add(deps=["bar"], outs=["foo"], name="copy", cmd="cp foo bar")
|
||||
assert dvc.data_status() == {**EMPTY_STATUS, "not_in_cache": ["foo"]}
|
||||
assert dvc.data_status() == EMPTY_STATUS
|
||||
assert dvc.data_status(granular=True) == EMPTY_STATUS
|
||||
|
||||
|
||||
def test_subdir(M, tmp_dir, scm):
|
||||
@@ -161,7 +194,243 @@ def test_subdir(M, tmp_dir, scm):
|
||||
dvc = subrepo.dvc
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"git": M.instance_of(dict),
|
||||
"unchanged": M.unordered("bar", join("dir", "foo")),
|
||||
"git": M.dict(),
|
||||
"unchanged": M.unordered(
|
||||
"bar", join("dir", ""), join("dir", "foo")
|
||||
),
|
||||
"untracked": ["untracked"],
|
||||
}
|
||||
|
||||
|
||||
def test_untracked_newly_added_files(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}})
|
||||
tmp_dir.gen("foobar", "foobar")
|
||||
|
||||
expected = {
|
||||
**EMPTY_STATUS,
|
||||
"untracked": M.unordered(
|
||||
join("dir", "foo"), join("dir", "bar"), "foobar"
|
||||
),
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(untracked_files="all") == expected
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == expected
|
||||
|
||||
|
||||
def test_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
|
||||
tmp_dir.dvc_gen("foobar", "foobar")
|
||||
remove(dvc.odb.repo.path)
|
||||
|
||||
assert dvc.data_status(untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
|
||||
"committed": {"added": M.unordered("foobar", join("dir", ""))},
|
||||
"not_in_cache": M.unordered("foobar", join("dir", "")),
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
|
||||
"committed": {"added": M.unordered("foobar", join("dir", ""))},
|
||||
"uncommitted": {
|
||||
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
|
||||
},
|
||||
"not_in_cache": M.unordered(
|
||||
"foobar",
|
||||
join("dir", ""),
|
||||
),
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
|
||||
tmp_dir.dvc_gen("foobar", "foobar")
|
||||
for path in [dvc.odb.repo.path, "dir", "foobar"]:
|
||||
remove(path)
|
||||
|
||||
assert dvc.data_status(untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
|
||||
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
|
||||
"committed": {"added": M.unordered("foobar", join("dir", ""))},
|
||||
"not_in_cache": M.unordered("foobar", join("dir", "")),
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
|
||||
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
|
||||
"committed": {"added": M.unordered("foobar", join("dir", ""))},
|
||||
"not_in_cache": M.unordered("foobar", join("dir", "")),
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_git_committed_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
|
||||
remove(dvc.odb.local.path)
|
||||
|
||||
assert dvc.data_status(untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"not_in_cache": M.unordered("foobar", join("dir", "")),
|
||||
"git": M.dict(),
|
||||
"unchanged": M.unordered("foobar", join("dir", "")),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"not_in_cache": M.unordered(
|
||||
"foobar",
|
||||
join("dir", ""),
|
||||
),
|
||||
"uncommitted": {
|
||||
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
|
||||
},
|
||||
"git": M.dict(),
|
||||
"unchanged": M.unordered("foobar", join("dir", "")),
|
||||
}
|
||||
|
||||
|
||||
def test_git_committed_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
|
||||
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
|
||||
for path in [dvc.odb.repo.path, "dir", "foobar"]:
|
||||
remove(path)
|
||||
|
||||
assert dvc.data_status(untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
|
||||
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True, untracked_files="all") == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {},
|
||||
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
|
||||
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_partial_missing_cache(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
|
||||
|
||||
# remove "foo" from cache
|
||||
odb = dvc.odb.repo
|
||||
odb.fs.rm(odb.oid_to_path("acbd18db4cc2f85cedef654fccc4a4d8"))
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"added": [join("dir", "")]},
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {
|
||||
"added": M.unordered(
|
||||
join("dir", ""), join("dir", "foo"), join("dir", "bar")
|
||||
)
|
||||
},
|
||||
"not_in_cache": [join("dir", "foo")],
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_missing_dir_object_from_head(M, tmp_dir, dvc, scm):
|
||||
(stage,) = tmp_dir.dvc_gen(
|
||||
{"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir"
|
||||
)
|
||||
remove("dir")
|
||||
tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
|
||||
odb = dvc.odb.repo
|
||||
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"modified": [join("dir", "")]},
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {
|
||||
"modified": [join("dir", "")],
|
||||
"unknown": [join("dir", "foobar")],
|
||||
},
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_missing_dir_object_from_index(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
|
||||
remove("dir")
|
||||
(stage,) = tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
|
||||
odb = dvc.odb.repo
|
||||
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"modified": [join("dir", "")]},
|
||||
"not_in_cache": [join("dir", "")],
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {
|
||||
"modified": [join("dir", "")],
|
||||
},
|
||||
"uncommitted": {"unknown": [join("dir", "foobar")]},
|
||||
"not_in_cache": [join("dir", "")],
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_root_from_dir_to_file(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
|
||||
remove("data")
|
||||
tmp_dir.gen("data", "file")
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"added": [join("data", "")]},
|
||||
"uncommitted": {"modified": ["data"]},
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {
|
||||
"added": M.unordered(
|
||||
join("data", ""), join("data", "foo"), join("data", "bar")
|
||||
)
|
||||
},
|
||||
"uncommitted": {
|
||||
"deleted": M.unordered(join("data", "foo"), join("data", "bar")),
|
||||
"modified": ["data"],
|
||||
},
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
|
||||
def test_root_from_file_to_dir(M, tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen("data", "file")
|
||||
remove("data")
|
||||
tmp_dir.gen({"data": {"foo": "foo", "bar": "bar"}})
|
||||
|
||||
assert dvc.data_status() == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"added": ["data"]},
|
||||
"uncommitted": {"modified": [join("data", "")]},
|
||||
"git": M.dict(),
|
||||
}
|
||||
assert dvc.data_status(granular=True) == {
|
||||
**EMPTY_STATUS,
|
||||
"committed": {"added": ["data"]},
|
||||
"uncommitted": {
|
||||
"modified": [join("data", "")],
|
||||
"added": M.unordered(join("data", "foo"), join("data", "bar")),
|
||||
},
|
||||
"git": M.dict(),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.annotations import Annotation
|
||||
from dvc.dvcfile import (
|
||||
PIPELINE_FILE,
|
||||
PIPELINE_LOCK,
|
||||
@@ -335,7 +336,7 @@ def test_dvcfile_dump_preserves_desc(tmp_dir, dvc, run_copy):
|
||||
(tmp_dir / dvcfile.path).dump(data)
|
||||
|
||||
assert stage.desc == stage_desc
|
||||
stage.outs[0].desc = out_desc
|
||||
stage.outs[0].annot.desc = out_desc
|
||||
dvcfile.dump(stage)
|
||||
loaded = dvcfile._load()[0]
|
||||
assert loaded == data
|
||||
@@ -382,3 +383,31 @@ def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, name):
|
||||
dvcfile.dump(stage)
|
||||
|
||||
assert str(exc.value) == f"cannot dump a parametrized stage: '{name}'"
|
||||
|
||||
|
||||
def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
|
||||
data = {
|
||||
"stages": {
|
||||
"stage1": {
|
||||
"cmd": "cp foo bar",
|
||||
"desc": "stage desc",
|
||||
"meta": {"key1": "value1", "key2": "value2"},
|
||||
"deps": ["foo"],
|
||||
"outs": [
|
||||
{"bar": {"desc": "bar desc", "meta": {"key": "value"}}}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
(tmp_dir / "dvc.yaml").dump(data)
|
||||
|
||||
stage = dvc.stage.load_one(name="stage1")
|
||||
assert stage.meta == {"key1": "value1", "key2": "value2"}
|
||||
assert stage.desc == "stage desc"
|
||||
assert stage.outs[0].annot == Annotation(
|
||||
desc="bar desc", meta={"key": "value"}
|
||||
)
|
||||
|
||||
# sanity check
|
||||
stage.dump()
|
||||
assert (tmp_dir / "dvc.yaml").parse() == data
|
||||
|
||||
@@ -7,8 +7,8 @@ from dvc.external_repo import CLONES, external_repo
|
||||
from dvc.testing.tmp_dir import make_subrepo
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import makedirs, remove
|
||||
from dvc_data.build import build
|
||||
from dvc_data.transfer import transfer
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.transfer import transfer
|
||||
|
||||
|
||||
def test_external_repo(erepo_dir, mocker):
|
||||
|
||||
@@ -11,7 +11,7 @@ from dvc.exceptions import CollectCacheError
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.repo import Repo as DvcRepo
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.db.local import LocalHashFileDB
|
||||
from dvc_data.hashfile.db.local import LocalHashFileDB
|
||||
from tests.basic_env import TestDir, TestDvcGit
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
from dvc.repo import Repo
|
||||
from dvc.testing.tmp_dir import TmpDir
|
||||
from dvc.types import List
|
||||
from dvc_data.build import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.utils import get_mtime_and_size
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from funcy import first
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.annotations import Annotation
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.dvcfile import Dvcfile
|
||||
from dvc.exceptions import DownloadError, PathMissingError
|
||||
@@ -527,7 +528,7 @@ def test_import_with_no_exec(tmp_dir, dvc, erepo_dir):
|
||||
|
||||
|
||||
def test_import_with_jobs(mocker, dvc, erepo_dir):
|
||||
import dvc_data.transfer as otransfer
|
||||
import dvc_data.hashfile.transfer as otransfer
|
||||
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen(
|
||||
@@ -627,3 +628,26 @@ def test_parameterized_repo(tmp_dir, dvc, scm, erepo_dir, paths):
|
||||
"url": os.fspath(erepo_dir),
|
||||
"rev_lock": erepo_dir.scm.get_rev(),
|
||||
}
|
||||
|
||||
|
||||
def test_import_with_annotations(M, tmp_dir, scm, dvc, erepo_dir):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
|
||||
|
||||
annot = {
|
||||
"desc": "foo desc",
|
||||
"labels": ["l1", "l2"],
|
||||
"type": "t1",
|
||||
"meta": {"key": "value"},
|
||||
}
|
||||
stage = dvc.imp(os.fspath(erepo_dir), "foo", "foo", no_exec=True, **annot)
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
# try to selectively update/overwrite some annotations
|
||||
annot = {**annot, "type": "t2"}
|
||||
stage = dvc.imp(
|
||||
os.fspath(erepo_dir), "foo", "foo", no_exec=True, type="t2"
|
||||
)
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
@@ -5,8 +5,9 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.annotations import Annotation
|
||||
from dvc.cli import main
|
||||
from dvc.dependency.base import DependencyDoesNotExistError
|
||||
from dvc.dependency.base import Dependency, DependencyDoesNotExistError
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.stage import Stage
|
||||
from dvc.testing.test_workspace import TestImport as _TestImport
|
||||
@@ -132,7 +133,7 @@ class TestImport(_TestImport):
|
||||
return False
|
||||
|
||||
|
||||
def test_import_url_preserve_meta(tmp_dir, dvc):
|
||||
def test_import_url_preserve_fields(tmp_dir, dvc):
|
||||
text = textwrap.dedent(
|
||||
"""\
|
||||
# top comment
|
||||
@@ -142,6 +143,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: bar # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
meta:
|
||||
key: value
|
||||
meta: some metadata
|
||||
"""
|
||||
)
|
||||
@@ -160,6 +167,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
|
||||
outs:
|
||||
- path: bar # out comment
|
||||
desc: out desc
|
||||
type: mytype
|
||||
labels:
|
||||
- label1
|
||||
- label2
|
||||
meta:
|
||||
key: value
|
||||
md5: acbd18db4cc2f85cedef654fccc4a4d8
|
||||
size: 3
|
||||
meta: some metadata
|
||||
@@ -267,3 +280,71 @@ def test_import_url_to_remote_status(tmp_dir, dvc, local_cloud, local_remote):
|
||||
|
||||
status = dvc.status()
|
||||
assert len(status) == 0
|
||||
|
||||
|
||||
def test_import_url_no_download(tmp_dir, dvc, local_workspace):
|
||||
local_workspace.gen("file", "file content")
|
||||
dst = tmp_dir / "file"
|
||||
stage = dvc.imp_url(
|
||||
"remote://workspace/file", os.fspath(dst), no_download=True
|
||||
)
|
||||
|
||||
assert stage.deps[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
|
||||
|
||||
assert not dst.exists()
|
||||
|
||||
out = stage.outs[0]
|
||||
assert not out.hash_info
|
||||
assert out.meta.size is None
|
||||
|
||||
status = dvc.status()
|
||||
assert status["file.dvc"] == [
|
||||
{"changed outs": {"file": "deleted"}},
|
||||
]
|
||||
|
||||
|
||||
def test_imp_url_with_annotations(M, tmp_dir, dvc, local_workspace):
|
||||
local_workspace.gen("foo", "foo")
|
||||
annot = {
|
||||
"desc": "foo desc",
|
||||
"labels": ["l1", "l2"],
|
||||
"type": "t1",
|
||||
"meta": {"key": "value"},
|
||||
}
|
||||
stage = dvc.imp_url(
|
||||
"remote://workspace/foo",
|
||||
os.fspath(tmp_dir / "foo"),
|
||||
no_exec=True,
|
||||
**annot,
|
||||
)
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
# try to selectively update/overwrite some annotations
|
||||
annot = {**annot, "type": "t2"}
|
||||
stage = dvc.imp_url(
|
||||
"remote://workspace/foo",
|
||||
os.fspath(tmp_dir / "foo"),
|
||||
no_exec=True,
|
||||
type="t2",
|
||||
)
|
||||
assert stage.outs[0].annot == Annotation(**annot)
|
||||
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
|
||||
|
||||
|
||||
def test_import_url_fs_config(tmp_dir, dvc, workspace, mocker):
|
||||
import dvc.fs as dvc_fs
|
||||
|
||||
workspace.gen("foo", "foo")
|
||||
|
||||
url = "remote://workspace/foo"
|
||||
get_fs_config = mocker.spy(dvc_fs, "get_fs_config")
|
||||
dep_init = mocker.spy(Dependency, "__init__")
|
||||
dvc.imp_url(url, fs_config={"jobs": 42})
|
||||
|
||||
dep_init_kwargs = dep_init.call_args[1]
|
||||
assert dep_init_kwargs.get("fs_config") == {"jobs": 42}
|
||||
|
||||
assert get_fs_config.call_args_list[0][1] == {"url": "foo"}
|
||||
assert get_fs_config.call_args_list[1][1] == {"url": url, "jobs": 42}
|
||||
assert get_fs_config.call_args_list[2][1] == {"name": "workspace"}
|
||||
|
||||
@@ -43,7 +43,7 @@ def test_cache(tmp_dir, dvc):
|
||||
|
||||
|
||||
def test_cache_load_bad_dir_cache(tmp_dir, dvc):
|
||||
from dvc_data import load
|
||||
from dvc_data.hashfile import load
|
||||
|
||||
dir_hash = "123.dir"
|
||||
fname = os.fspath(dvc.odb.local.oid_to_path(dir_hash))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user