mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab3f8fb1a4 | ||
|
|
6edca79b79 | ||
|
|
2a888970e8 | ||
|
|
73fdfc8059 | ||
|
|
8dc11417d2 | ||
|
|
b02855fb93 | ||
|
|
af255a46b4 | ||
|
|
2748492f6b | ||
|
|
c69d3188a6 | ||
|
|
9b9c04e633 | ||
|
|
350e0fb1f7 | ||
|
|
f14358bc72 | ||
|
|
705bc6ec82 | ||
|
|
61a02e6f56 | ||
|
|
8e5226ece0 | ||
|
|
2a1af0b880 | ||
|
|
014d149512 | ||
|
|
77ae4aa92e | ||
|
|
588471a811 | ||
|
|
e59c19e752 | ||
|
|
b0a92414fa | ||
|
|
dae500d7c7 | ||
|
|
a01bc2918e | ||
|
|
a33bbc97d5 | ||
|
|
ba2e8bdac7 | ||
|
|
4d6e045989 | ||
|
|
00eea4560e | ||
|
|
6e02dddabd | ||
|
|
b550c5e625 | ||
|
|
b36861ada9 | ||
|
|
d482e26c99 | ||
|
|
a8ad59ae4e | ||
|
|
76cc649f5e | ||
|
|
d52e1aa17b | ||
|
|
65f61acffa | ||
|
|
fdc689954f | ||
|
|
076c48532d | ||
|
|
e4ea463c73 | ||
|
|
87d8269365 | ||
|
|
a753ee215f | ||
|
|
014a38fae2 | ||
|
|
8913f5a6fe | ||
|
|
4ce9825c2b | ||
|
|
e9c5b5fa84 | ||
|
|
fa54c1af7c | ||
|
|
9fbbfd07a6 | ||
|
|
929de7c651 | ||
|
|
49af6e1435 | ||
|
|
a6bfc7db4e | ||
|
|
872a0d663e | ||
|
|
c01583f3dd | ||
|
|
e0d41c7012 | ||
|
|
628b992442 | ||
|
|
99e7f52bbd | ||
|
|
b2a275ee9e | ||
|
|
7e423b668f | ||
|
|
2c3956bdb0 | ||
|
|
a48a19ce31 | ||
|
|
1de983f0de | ||
|
|
eb20a8e049 | ||
|
|
78e25747a4 | ||
|
|
240f0ebdc3 | ||
|
|
69f78d3bb6 | ||
|
|
740fe504c9 | ||
|
|
7bb84286f5 | ||
|
|
3891e81bdd | ||
|
|
bb62833897 | ||
|
|
fb703150ce | ||
|
|
33d4ead8b6 | ||
|
|
76b1f8c907 | ||
|
|
d7152e360e | ||
|
|
ab69b88a21 | ||
|
|
43a8eab2e0 | ||
|
|
15f35e827e | ||
|
|
4d17c171f1 | ||
|
|
c27005a8b7 | ||
|
|
ecd371f8ae | ||
|
|
bda1388bff | ||
|
|
c02bd7f318 | ||
|
|
fe1d805c80 | ||
|
|
f9b0de883f | ||
|
|
1cad8ea7fd | ||
|
|
08a643e005 |
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.11"
|
||||
- uses: iterative/dvc-bench@main
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '32 19 * * 2'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['python']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
|
||||
@@ -46,13 +46,15 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11"]
|
||||
pytest-filter:
|
||||
- "import or plot or live or experiment"
|
||||
- "not (import or plot or live or experiment)"
|
||||
include:
|
||||
- {os: ubuntu-latest, pyv: "3.11"}
|
||||
- {os: macos-latest, pyv: "3.8"}
|
||||
- {os: macos-latest, pyv: "3.9"}
|
||||
- {os: macos-latest, pyv: "3.10"}
|
||||
- {os: macos-latest, pyv: "3.11"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
+6
-2
@@ -13,7 +13,7 @@
|
||||
|
||||
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|
||||
|
||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|PyPI| |PyPI Downloads| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|
||||
|
|
||||
|
||||
@@ -276,6 +276,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI
|
||||
|
||||
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/dvc.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI Downloads
|
||||
|
||||
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
|
||||
:target: https://github.com/iterative/dvc/releases/latest
|
||||
:alt: deb|pkg|rpm|exe
|
||||
@@ -292,6 +296,6 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://github.com/iterative/dvc/graphs/contributors
|
||||
:alt: Contributors
|
||||
|
||||
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
|
||||
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
|
||||
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
|
||||
:alt: VS Code Extension
|
||||
|
||||
+3
-16
@@ -2,8 +2,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
add,
|
||||
cache,
|
||||
@@ -128,17 +128,6 @@ class DvcParser(argparse.ArgumentParser):
|
||||
return args
|
||||
|
||||
|
||||
class VersionAction(argparse.Action): # pragma: no cover
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Shows DVC version and exits."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from dvc import __version__
|
||||
|
||||
print(__version__)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_parent_parser():
|
||||
"""Create instances of a parser containing common arguments shared among
|
||||
all the commands.
|
||||
@@ -186,13 +175,11 @@ def get_main_parser():
|
||||
help="Show this help message and exit.",
|
||||
)
|
||||
|
||||
# NOTE: On some python versions action='version' prints to stderr
|
||||
# instead of stdout https://bugs.python.org/issue18920
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
action=VersionAction,
|
||||
nargs=0,
|
||||
action="version",
|
||||
version=__version__,
|
||||
help="Show program's version.",
|
||||
)
|
||||
|
||||
|
||||
@@ -18,15 +18,13 @@ class CmdExperimentsList(CmdBase):
|
||||
)
|
||||
tags = self.repo.scm.describe(exps)
|
||||
remained = {baseline for baseline, tag in tags.items() if tag is None}
|
||||
base = "refs/heads/"
|
||||
base = "refs/heads"
|
||||
ref_heads = self.repo.scm.describe(remained, base=base)
|
||||
|
||||
for baseline in exps:
|
||||
name = (
|
||||
tags[baseline]
|
||||
or ref_heads[baseline][len(base) :]
|
||||
or baseline[:7]
|
||||
)
|
||||
name = baseline[:7]
|
||||
if tags[baseline] or ref_heads[baseline]:
|
||||
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
|
||||
if not name_only:
|
||||
print(f"{name}:")
|
||||
for exp_name in exps[baseline]:
|
||||
|
||||
@@ -238,26 +238,6 @@ def _add_common_args(parser):
|
||||
help="Declare output plot file (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live", help="Declare output as dvclive.", metavar="<path>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-cache",
|
||||
help="Declare output as dvclive (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-summary",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-html",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Signal dvclive logger to not produce training report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--wdir",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Mapping
|
||||
|
||||
from dvc.output import ARTIFACT_SCHEMA, Output
|
||||
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
|
||||
|
||||
from .base import Dependency
|
||||
from .param import ParamsDependency
|
||||
@@ -15,10 +15,11 @@ SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
|
||||
def _get(stage, p, info, fs_config=None):
|
||||
def _get(stage, p, info, **kwargs):
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
@@ -27,14 +28,15 @@ def _get(stage, p, info, fs_config=None):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
return Dependency(stage, p, info, fs_config=fs_config)
|
||||
return Dependency(stage, p, info, **kwargs)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
ret = []
|
||||
for d in d_list:
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
ret.append(_get(stage, p, d))
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(_get(stage, p, d, files=files))
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@@ -62,3 +62,8 @@ class Dependency(Output):
|
||||
self.fs_path = self.fs.path.version_path(
|
||||
self.fs_path, self.meta.version_id
|
||||
)
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
if self.fs.version_aware:
|
||||
kwargs["with_files"] = True
|
||||
return super().dumpd(**kwargs)
|
||||
|
||||
@@ -2,9 +2,6 @@ DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_PATH = "DVCLIVE_PATH"
|
||||
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
|
||||
DVCLIVE_HTML = "DVCLIVE_HTML"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
|
||||
+10
-3
@@ -132,10 +132,17 @@ class NotDvcRepoError(DvcException):
|
||||
class CyclicGraphError(DvcException):
|
||||
def __init__(self, stages):
|
||||
assert isinstance(stages, list)
|
||||
msg = "Pipeline has a cycle involving: {}.".format(
|
||||
", ".join(s.addressing for s in stages)
|
||||
stage_part = "stage" if len(stages) == 1 else "stages"
|
||||
msg = (
|
||||
"Same item(s) are defined as both a dependency and an output "
|
||||
"in {stage_part}: {stage}."
|
||||
)
|
||||
super().__init__(
|
||||
msg.format(
|
||||
stage_part=stage_part,
|
||||
stage=", ".join(s.addressing for s in stages),
|
||||
)
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ConfirmRemoveError(DvcException):
|
||||
|
||||
+101
-63
@@ -87,10 +87,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)
|
||||
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)
|
||||
push = d.pop(Output.PARAM_PUSH, True)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -101,10 +101,10 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
live=live,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
push=push,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -118,8 +118,8 @@ def loads_from(
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
remote=None,
|
||||
push=True,
|
||||
):
|
||||
return [
|
||||
_get(
|
||||
@@ -131,8 +131,8 @@ def loads_from(
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
live=live,
|
||||
remote=remote,
|
||||
push=push,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
@@ -166,29 +166,21 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
stage.PARAM_OUTS,
|
||||
stage.PARAM_METRICS,
|
||||
stage.PARAM_PLOTS,
|
||||
stage.PARAM_LIVE,
|
||||
):
|
||||
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
|
||||
|
||||
metric = typ == stage.PARAM_METRICS
|
||||
plot = typ == stage.PARAM_PLOTS
|
||||
live = typ == stage.PARAM_LIVE
|
||||
if live:
|
||||
# `live` is single object
|
||||
data = [data]
|
||||
|
||||
d = _merge_data(data)
|
||||
|
||||
for path, flags in d.items():
|
||||
plt_d, live_d = {}, {}
|
||||
plt_d = {}
|
||||
if plot:
|
||||
from dvc.schema import PLOT_PROPS
|
||||
|
||||
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
|
||||
if live:
|
||||
from dvc.schema import LIVE_PROPS
|
||||
|
||||
live_d, flags = _split_dict(flags, keys=LIVE_PROPS.keys())
|
||||
extra = project(
|
||||
flags,
|
||||
[
|
||||
@@ -196,6 +188,7 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
Output.PARAM_PUSH,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
@@ -206,7 +199,6 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
info={},
|
||||
plot=plt_d or plot,
|
||||
metric=metric,
|
||||
live=live_d or live,
|
||||
**extra,
|
||||
)
|
||||
|
||||
@@ -263,10 +255,8 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
PARAM_REMOTE = "remote"
|
||||
PARAM_PUSH = "push"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
None,
|
||||
@@ -292,7 +282,6 @@ class Output:
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
@@ -301,6 +290,7 @@ class Output:
|
||||
repo=None,
|
||||
fs_config=None,
|
||||
files: List[Dict[str, Any]] = None,
|
||||
push: bool = True,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
@@ -355,7 +345,7 @@ class Output:
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
@@ -377,8 +367,12 @@ class Output:
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
if self.hash_info and self.hash_info.isdir:
|
||||
if self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
if not self.hash_info and self.hash_name != "md5":
|
||||
md5 = getattr(self.meta, "md5", None)
|
||||
if md5:
|
||||
self.hash_info = HashInfo("md5", md5)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
parsed = urlparse(self.def_path)
|
||||
@@ -523,7 +517,10 @@ class Output:
|
||||
except NoRemoteError:
|
||||
remote = None
|
||||
|
||||
return DataIndexEntry(
|
||||
if self.files and not self.obj:
|
||||
self.obj = self.get_obj()
|
||||
|
||||
entry = DataIndexEntry(
|
||||
meta=self.meta,
|
||||
obj=self.obj,
|
||||
hash_info=self.hash_info,
|
||||
@@ -531,6 +528,16 @@ class Output:
|
||||
cache=self.odb,
|
||||
remote=remote,
|
||||
)
|
||||
if self.stage.is_import and not self.stage.is_repo_import:
|
||||
dep = self.stage.deps[0]
|
||||
entry.fs = dep.fs
|
||||
entry.path = dep.fs_path
|
||||
entry.meta = dep.meta
|
||||
if not entry.obj:
|
||||
if not dep.obj and dep.files:
|
||||
dep.obj = dep.get_obj()
|
||||
entry.obj = dep.obj
|
||||
return entry
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
@@ -631,8 +638,16 @@ class Output:
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
_, self.meta, obj = build(
|
||||
if self.use_cache:
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
else:
|
||||
_, self.meta, self.obj = build(
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
@@ -640,23 +655,11 @@ class Output:
|
||||
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
|
||||
)
|
||||
return
|
||||
|
||||
assert not self.IS_DEPENDENCY
|
||||
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
@@ -754,40 +757,40 @@ class Output:
|
||||
|
||||
ret[self.PARAM_PATH] = path
|
||||
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
if not self.IS_DEPENDENCY:
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
if isinstance(self.metric, dict):
|
||||
if (
|
||||
self.PARAM_METRIC_XPATH in self.metric
|
||||
and not self.metric[self.PARAM_METRIC_XPATH]
|
||||
):
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
|
||||
if isinstance(self.metric, dict):
|
||||
if (
|
||||
self.PARAM_METRIC_XPATH in self.metric
|
||||
and not self.metric[self.PARAM_METRIC_XPATH]
|
||||
):
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if self.live:
|
||||
ret[self.PARAM_LIVE] = self.live
|
||||
if not self.can_push:
|
||||
ret[self.PARAM_PUSH] = self.can_push
|
||||
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if self.hash_info.isdir and (
|
||||
kwargs.get("with_files") or self.files is not None
|
||||
if (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
and self.hash_info.isdir
|
||||
and (kwargs.get("with_files") or self.files is not None)
|
||||
):
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
@@ -1040,9 +1043,15 @@ class Output:
|
||||
if not self.use_cache:
|
||||
return {}
|
||||
|
||||
push: bool = kwargs.pop("push", False)
|
||||
if self.stage.is_repo_import:
|
||||
if push:
|
||||
return {}
|
||||
return self.get_used_external(**kwargs)
|
||||
|
||||
if push and not self.can_push:
|
||||
return {}
|
||||
|
||||
if not self.hash_info:
|
||||
msg = (
|
||||
"Output '{}'({}) is missing version info. "
|
||||
@@ -1182,11 +1191,39 @@ class Output:
|
||||
|
||||
@property
|
||||
def is_metric(self) -> bool:
|
||||
return bool(self.metric) or bool(self.live)
|
||||
return bool(self.metric)
|
||||
|
||||
@property
|
||||
def is_plot(self) -> bool:
|
||||
return bool(self.plot) or bool(self.live)
|
||||
return bool(self.plot)
|
||||
|
||||
def restore_fields(self, other: "Output"):
|
||||
"""Restore attributes that need to be preserved when serialized."""
|
||||
self.annot = other.annot
|
||||
self.remote = other.remote
|
||||
self.can_push = other.can_push
|
||||
|
||||
def merge_version_meta(self, other: "Output"):
|
||||
"""Merge version meta for files which are unchanged from other."""
|
||||
if not self.hash_info:
|
||||
return
|
||||
if self.hash_info.isdir:
|
||||
return self._merge_dir_version_meta(other)
|
||||
if self.hash_info != other.hash_info:
|
||||
return
|
||||
self.meta = other.meta
|
||||
|
||||
def _merge_dir_version_meta(self, other: "Output"):
|
||||
from dvc_data.hashfile.tree import update_meta
|
||||
|
||||
if not self.obj or not other.hash_info.isdir:
|
||||
return
|
||||
other_obj = other.obj if other.obj is not None else other.get_obj()
|
||||
assert isinstance(self.obj, Tree) and isinstance(other_obj, Tree)
|
||||
updated = update_meta(self.obj, other_obj)
|
||||
assert updated.hash_info == self.obj.hash_info
|
||||
self.obj = updated
|
||||
self.files = updated.as_list(with_meta=True)
|
||||
|
||||
|
||||
META_SCHEMA = {
|
||||
@@ -1217,5 +1254,6 @@ SCHEMA = {
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_PUSH: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Iterable, Mapping
|
||||
@@ -70,6 +71,16 @@ def embrace(s: str):
|
||||
return BRACE_OPEN + s + BRACE_CLOSE
|
||||
|
||||
|
||||
def escape_str(value):
|
||||
if os.name == "nt":
|
||||
from subprocess import list2cmdline
|
||||
|
||||
return list2cmdline([value])
|
||||
from shlex import quote
|
||||
|
||||
return quote(value)
|
||||
|
||||
|
||||
@singledispatch
|
||||
def to_str(obj) -> str:
|
||||
return str(obj)
|
||||
@@ -97,12 +108,12 @@ def _(obj: dict):
|
||||
result += f"--no-{k} "
|
||||
|
||||
elif isinstance(v, str):
|
||||
result += f"--{k} '{v}' "
|
||||
result += f"--{k} {escape_str(v)} "
|
||||
|
||||
elif isinstance(v, Iterable):
|
||||
for n, i in enumerate(v):
|
||||
if isinstance(i, str):
|
||||
i = f"'{i}'"
|
||||
i = escape_str(i)
|
||||
elif isinstance(i, Iterable):
|
||||
raise ParseError(
|
||||
f"Cannot interpolate nested iterable in '{k}'"
|
||||
|
||||
@@ -14,14 +14,14 @@ from dvc.render.converter.vega import VegaConverter
|
||||
|
||||
|
||||
def _get_converter(
|
||||
renderer_class, props
|
||||
renderer_class, renderer_id, props, data
|
||||
) -> Union[VegaConverter, ImageConverter]:
|
||||
from dvc_render import ImageRenderer, VegaRenderer
|
||||
|
||||
if renderer_class.TYPE == VegaRenderer.TYPE:
|
||||
return VegaConverter(props)
|
||||
return VegaConverter(renderer_id, data, props)
|
||||
if renderer_class.TYPE == ImageRenderer.TYPE:
|
||||
return ImageConverter(props)
|
||||
return ImageConverter(renderer_id, data, props)
|
||||
|
||||
raise ValueError(f"Invalid renderer class {renderer_class}")
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
class Converter:
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
self.plot_properties = plot_properties or {}
|
||||
def __init__(
|
||||
self,
|
||||
plot_id: str,
|
||||
data: Dict[str, Any] = None,
|
||||
properties: Optional[Dict] = None,
|
||||
):
|
||||
self.plot_id = plot_id
|
||||
self.properties = properties or {}
|
||||
self.data = data or {}
|
||||
|
||||
def convert(self, data, revision: str, filename: str, **kwargs):
|
||||
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
||||
|
||||
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
|
||||
|
||||
@@ -34,25 +34,33 @@ class ImageConverter(Converter):
|
||||
base64_str = base64.b64encode(image_data).decode()
|
||||
return f"data:image;base64,{base64_str}"
|
||||
|
||||
def convert(
|
||||
self, data, revision: str, filename: str, **kwargs
|
||||
) -> Tuple[List[Dict], Dict]:
|
||||
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
|
||||
datas = []
|
||||
for filename, image_data in self.data.items():
|
||||
datas.append((filename, "", image_data))
|
||||
return datas, self.properties
|
||||
|
||||
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
|
||||
"""
|
||||
Convert the DVC Plots content to DVC Render datapoints.
|
||||
Return both generated datapoints and updated properties.
|
||||
"""
|
||||
path = self.plot_properties.get("out")
|
||||
if path:
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
src = self._write_image(
|
||||
os.path.abspath(path), revision, filename, data
|
||||
)
|
||||
else:
|
||||
src = self._encode_image(data)
|
||||
datapoint = {
|
||||
REVISION_FIELD: revision,
|
||||
FILENAME_FIELD: filename,
|
||||
SRC_FIELD: src,
|
||||
}
|
||||
return [datapoint], self.plot_properties
|
||||
path = self.properties.get("out")
|
||||
datapoints = []
|
||||
datas, properties = self.convert()
|
||||
for filename, _, image_data in datas:
|
||||
if path:
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
src = self._write_image(
|
||||
os.path.abspath(path), revision, filename, image_data
|
||||
)
|
||||
else:
|
||||
src = self._encode_image(image_data)
|
||||
datapoint = {
|
||||
REVISION_FIELD: revision,
|
||||
FILENAME_FIELD: filename,
|
||||
SRC_FIELD: src,
|
||||
}
|
||||
datapoints.append(datapoint)
|
||||
return datapoints, properties
|
||||
|
||||
+263
-265
@@ -1,90 +1,101 @@
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import Dict, Iterable, List, Optional, Set, Union
|
||||
from typing import Any, Dict, Iterable, List, Tuple, Union
|
||||
|
||||
from funcy import first, project
|
||||
from funcy import first, last
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import (
|
||||
FILENAME_FIELD,
|
||||
INDEX_FIELD,
|
||||
REVISION_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
from dvc.render import FILENAME_FIELD, INDEX_FIELD, VERSION_FIELD
|
||||
|
||||
from . import Converter
|
||||
|
||||
|
||||
class FieldsNotFoundError(DvcException):
|
||||
def __init__(self, expected_fields, found_fields):
|
||||
expected_str = ", ".join(expected_fields)
|
||||
class FieldNotFoundError(DvcException):
|
||||
def __init__(self, expected_field, found_fields):
|
||||
found_str = ", ".join(found_fields)
|
||||
super().__init__(
|
||||
f"Could not find all provided fields ('{expected_str}') "
|
||||
f"Could not find provided field ('{expected_field}') "
|
||||
f"in data fields ('{found_str}')."
|
||||
)
|
||||
|
||||
|
||||
class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
def _lists(blob: Union[Dict, List]) -> Iterable[List]:
|
||||
if isinstance(blob, list):
|
||||
yield blob
|
||||
else:
|
||||
for _, value in blob.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
|
||||
|
||||
def _filter_fields(
|
||||
datapoints: List[Dict], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
if not fields:
|
||||
return datapoints
|
||||
assert isinstance(fields, set)
|
||||
|
||||
new_data = []
|
||||
for data_point in datapoints:
|
||||
keys = set(data_point.keys())
|
||||
if not fields <= keys:
|
||||
raise FieldsNotFoundError(fields, keys)
|
||||
|
||||
new_data.append(project(data_point, fields))
|
||||
|
||||
return new_data
|
||||
def _file_field(*args):
|
||||
for axis_def in args:
|
||||
if axis_def is not None:
|
||||
for file, val in axis_def.items():
|
||||
if isinstance(val, str):
|
||||
yield file, val
|
||||
elif isinstance(val, list):
|
||||
for field in val:
|
||||
yield file, field
|
||||
|
||||
|
||||
def _lists(dictionary: Dict):
|
||||
for _, value in dictionary.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
def _find(
|
||||
filename: str,
|
||||
field: str,
|
||||
data_series: List[Tuple[str, str, Any]],
|
||||
):
|
||||
for data_file, data_field, data in data_series:
|
||||
if data_file == filename and data_field == field:
|
||||
return data_file, data_field, data
|
||||
return None
|
||||
|
||||
|
||||
def _find_first_list(
|
||||
data: Union[Dict, List], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
fields = fields or set()
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
for lst in _lists(data):
|
||||
if (
|
||||
all(isinstance(dp, dict) for dp in lst)
|
||||
# if fields is empty, it will match any set
|
||||
and set(first(lst).keys()) & fields == fields
|
||||
):
|
||||
return lst
|
||||
|
||||
raise PlotDataStructureError()
|
||||
def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
|
||||
if filename in file2datapoints:
|
||||
datapoint = first(file2datapoints[filename])
|
||||
if field not in datapoint:
|
||||
raise FieldNotFoundError(field, datapoint.keys())
|
||||
return
|
||||
|
||||
|
||||
def _append_index(datapoints: List[Dict], **kwargs) -> List[Dict]:
|
||||
if INDEX_FIELD in first(datapoints).keys():
|
||||
return datapoints
|
||||
def _get_x(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
||||
x = properties.get("x", None)
|
||||
if x is not None and isinstance(x, dict):
|
||||
filename, field = first(_file_field(x))
|
||||
_verify_field(file2datapoints, filename, field)
|
||||
return filename, field
|
||||
return None
|
||||
|
||||
for index, data_point in enumerate(datapoints):
|
||||
data_point[INDEX_FIELD] = index
|
||||
return datapoints
|
||||
|
||||
def _get_ys(properties, file2datapoints: Dict[str, List[Dict]]):
|
||||
y = properties.get("y", None)
|
||||
if y is not None:
|
||||
for filename, field in _file_field(y):
|
||||
_verify_field(file2datapoints, filename, field)
|
||||
yield filename, field
|
||||
|
||||
|
||||
def _is_datapoints(lst: List[Dict]):
|
||||
"""
|
||||
check if dict keys match, datapoints with different keys mgiht lead
|
||||
to unexpected behavior
|
||||
"""
|
||||
|
||||
return all(isinstance(item, dict) for item in lst) and set(
|
||||
first(lst).keys()
|
||||
) == {key for keys in lst for key in keys}
|
||||
|
||||
|
||||
def get_datapoints(file_content: Dict):
|
||||
result: List[Dict[str, Any]] = []
|
||||
for lst in _lists(file_content):
|
||||
if _is_datapoints(lst):
|
||||
for index, datapoint in enumerate(lst):
|
||||
if len(result) <= index:
|
||||
result.append({})
|
||||
result[index].update(datapoint)
|
||||
return result
|
||||
|
||||
|
||||
class VegaConverter(Converter):
|
||||
@@ -95,228 +106,215 @@ class VegaConverter(Converter):
|
||||
('x', 'y') it will attempt to fill in the blanks.
|
||||
"""
|
||||
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
super().__init__(plot_properties)
|
||||
def __init__(
|
||||
self, plot_id: str, data: Dict = None, properties: Dict = None
|
||||
):
|
||||
super().__init__(plot_id, data, properties)
|
||||
self.plot_id = plot_id
|
||||
self.inferred_properties: Dict = {}
|
||||
|
||||
self.steps = []
|
||||
# TODO we should be handling that in `convert`,
|
||||
# to avoid stateful `self.inferred_properties`
|
||||
self._infer_x_y()
|
||||
|
||||
self._infer_x()
|
||||
self._infer_fields()
|
||||
def _infer_y_from_data(self):
|
||||
if self.plot_id in self.data:
|
||||
for lst in _lists(self.data[self.plot_id]):
|
||||
if all(isinstance(item, dict) for item in lst):
|
||||
datapoint = first(lst)
|
||||
field = last(datapoint.keys())
|
||||
self.inferred_properties["y"] = {self.plot_id: field}
|
||||
break
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"find_data",
|
||||
partial(
|
||||
_find_first_list,
|
||||
fields=self.inferred_properties.get("fields", set())
|
||||
- {INDEX_FIELD},
|
||||
),
|
||||
)
|
||||
)
|
||||
def _infer_x_y(self):
|
||||
def _infer_files(from_name, to_name):
|
||||
from_value = self.properties.get(from_name, None)
|
||||
to_value = self.properties.get(to_name, None)
|
||||
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.steps.append(("append_index", partial(_append_index)))
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"filter_fields",
|
||||
partial(
|
||||
_filter_fields,
|
||||
fields=self.inferred_properties.get("fields", set()),
|
||||
),
|
||||
)
|
||||
)
|
||||
self.steps.append(
|
||||
(
|
||||
"infer_y",
|
||||
partial(
|
||||
self._infer_y,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"generate_y",
|
||||
partial(
|
||||
self._generate_y_values,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _infer_x(self):
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.inferred_properties["x"] = INDEX_FIELD
|
||||
|
||||
def skip_step(self, name: str):
|
||||
self.steps = [(_name, fn) for _name, fn in self.steps if _name != name]
|
||||
|
||||
def _infer_fields(self):
|
||||
fields = self.plot_properties.get("fields", set())
|
||||
if fields:
|
||||
fields = {
|
||||
*fields,
|
||||
self.plot_properties.get("x", None),
|
||||
self.plot_properties.get("y", None),
|
||||
self.inferred_properties.get("x", None),
|
||||
} - {None}
|
||||
self.inferred_properties["fields"] = fields
|
||||
|
||||
def _infer_y(self, datapoints: List[Dict], **kwargs):
|
||||
if "y" not in self.plot_properties:
|
||||
data_fields = list(first(datapoints))
|
||||
skip = (
|
||||
REVISION_FIELD,
|
||||
self.plot_properties.get("x", None)
|
||||
or self.inferred_properties.get("x"),
|
||||
FILENAME_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
inferred_y = first(
|
||||
f for f in reversed(data_fields) if f not in skip
|
||||
)
|
||||
if "y" in self.inferred_properties:
|
||||
previous_y = self.inferred_properties["y"]
|
||||
if previous_y != inferred_y:
|
||||
raise DvcException(
|
||||
f"Inferred y ('{inferred_y}' value does not match"
|
||||
f"previously matched one ('f{previous_y}')."
|
||||
)
|
||||
if isinstance(to_value, str):
|
||||
self.inferred_properties[to_name] = {}
|
||||
if isinstance(from_value, dict):
|
||||
for file in from_value.keys():
|
||||
self.inferred_properties[to_name][file] = to_value
|
||||
else:
|
||||
self.inferred_properties[to_name][self.plot_id] = to_value
|
||||
else:
|
||||
self.inferred_properties["y"] = inferred_y
|
||||
return datapoints
|
||||
self.inferred_properties[to_name] = to_value
|
||||
|
||||
_infer_files("y", "x")
|
||||
_infer_files("x", "y")
|
||||
|
||||
if self.inferred_properties.get("y", None) is None:
|
||||
self._infer_y_from_data()
|
||||
|
||||
def _find_datapoints(self):
|
||||
result = {}
|
||||
for file, content in self.data.items():
|
||||
result[file] = get_datapoints(content)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def infer_y_label(properties):
|
||||
y_label = properties.get("y_label", None)
|
||||
if y_label is None:
|
||||
y = properties.get("y", None)
|
||||
if isinstance(y, dict):
|
||||
fields = {field for _, field in _file_field(y)}
|
||||
if len(fields) == 1:
|
||||
y_label = first(fields)
|
||||
else:
|
||||
y_label = "y"
|
||||
elif isinstance(y, list):
|
||||
y_label = "y"
|
||||
elif isinstance(y, str):
|
||||
y_label = y
|
||||
|
||||
return y_label
|
||||
|
||||
@staticmethod
|
||||
def infer_x_label(properties):
|
||||
x_label = properties.get("x_label", None)
|
||||
if x_label is None:
|
||||
x = properties.get("x", None)
|
||||
if isinstance(x, dict):
|
||||
fields = {field for _, field in _file_field(x)}
|
||||
if len(fields) == 1:
|
||||
x_label = first(fields)
|
||||
else:
|
||||
x_label = "x"
|
||||
else:
|
||||
x_label = INDEX_FIELD
|
||||
return x_label
|
||||
|
||||
def flat_datapoints(self, revision):
|
||||
|
||||
file2datapoints, properties = self.convert()
|
||||
|
||||
props_update = {}
|
||||
|
||||
x = _get_x(properties, file2datapoints)
|
||||
|
||||
# assign "step" if no x provided
|
||||
if not x:
|
||||
x_file, x_field = (
|
||||
None,
|
||||
INDEX_FIELD,
|
||||
)
|
||||
else:
|
||||
x_file, x_field = x
|
||||
props_update["x"] = x_field
|
||||
|
||||
ys = list(_get_ys(properties, file2datapoints))
|
||||
|
||||
all_datapoints = []
|
||||
all_y_fields = {y_field for _, y_field in ys}
|
||||
|
||||
# override to unified y field name if there are different y fields
|
||||
if len(all_y_fields) > 1:
|
||||
props_update["y"] = "dvc_inferred_y_value"
|
||||
else:
|
||||
props_update["y"] = first(all_y_fields)
|
||||
|
||||
for y_file, y_field in ys:
|
||||
datapoints = deepcopy(file2datapoints.get(y_file, []))
|
||||
|
||||
if props_update.get("y", None) == "dvc_inferred_y_value":
|
||||
_update_from_field(
|
||||
datapoints,
|
||||
field="dvc_inferred_y_value",
|
||||
source_field=y_field,
|
||||
)
|
||||
|
||||
if x_field == INDEX_FIELD and x_file is None:
|
||||
_update_from_index(datapoints, INDEX_FIELD)
|
||||
else:
|
||||
x_datapoints = file2datapoints.get(x_file, [])
|
||||
try:
|
||||
_update_from_field(
|
||||
datapoints,
|
||||
field=x_field,
|
||||
source_datapoints=x_datapoints,
|
||||
)
|
||||
except IndexError:
|
||||
raise DvcException(
|
||||
f"Cannot join '{x_field}' from '{x_file}' and "
|
||||
"'{y_field}' from '{y_file}'. "
|
||||
"They have to have same length."
|
||||
)
|
||||
|
||||
_update_all(
|
||||
datapoints,
|
||||
update_dict={
|
||||
VERSION_FIELD: {
|
||||
"revision": revision,
|
||||
FILENAME_FIELD: y_file,
|
||||
"field": y_field,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
all_datapoints.extend(datapoints)
|
||||
|
||||
if not all_datapoints:
|
||||
return [], {}
|
||||
|
||||
properties = {**properties, **props_update}
|
||||
|
||||
return all_datapoints, properties
|
||||
|
||||
def convert(
|
||||
self,
|
||||
data,
|
||||
revision: str,
|
||||
filename: str,
|
||||
skip: List = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Convert the data. Fill necessary fields ('x', 'y') and return both
|
||||
generated datapoints and updated properties.
|
||||
generated datapoints and updated properties. If `x` is not provided,
|
||||
leave it as None, fronteds should handle it.
|
||||
|
||||
NOTE: Studio uses this method.
|
||||
The only thing studio FE handles is filling `x` and `y`.
|
||||
`x/y_label` should be filled here.
|
||||
|
||||
Datapoints are not stripped according to config, because users
|
||||
might be utilizing other fields in their custom plots.
|
||||
"""
|
||||
if not skip:
|
||||
skip = []
|
||||
datapoints = self._find_datapoints()
|
||||
properties = {**self.properties, **self.inferred_properties}
|
||||
|
||||
processed = deepcopy(data)
|
||||
properties["y_label"] = self.infer_y_label(properties)
|
||||
properties["x_label"] = self.infer_x_label(properties)
|
||||
|
||||
for step_name, step in self.steps:
|
||||
if step_name not in skip:
|
||||
processed = step( # type: ignore
|
||||
processed,
|
||||
revision=revision,
|
||||
filename=filename,
|
||||
)
|
||||
return datapoints, properties
|
||||
|
||||
return processed, {**self.plot_properties, **self.inferred_properties}
|
||||
|
||||
def _generate_y_values( # noqa: C901
|
||||
self,
|
||||
datapoints: List[Dict],
|
||||
revision: str,
|
||||
filename: str,
|
||||
**kwargs,
|
||||
) -> List[Dict]:
|
||||
def _update_from_field(
|
||||
target_datapoints: List[Dict],
|
||||
field: str,
|
||||
source_datapoints: List[Dict] = None,
|
||||
source_field: str = None,
|
||||
):
|
||||
if source_datapoints is None:
|
||||
source_datapoints = target_datapoints
|
||||
if source_field is None:
|
||||
source_field = field
|
||||
|
||||
y_values = self.plot_properties.get(
|
||||
"y", None
|
||||
) or self.inferred_properties.get("y", None)
|
||||
if len(source_datapoints) != len(target_datapoints):
|
||||
raise IndexError(
|
||||
"Source and target datapoints must have the same length"
|
||||
)
|
||||
|
||||
assert y_values is not None
|
||||
for index, datapoint in enumerate(target_datapoints):
|
||||
source_datapoint = source_datapoints[index]
|
||||
if source_field in source_datapoint:
|
||||
datapoint[field] = source_datapoint[source_field]
|
||||
|
||||
result = []
|
||||
properties_update = {}
|
||||
|
||||
def _add_version_info(datapoint, version_info):
|
||||
tmp = datapoint.copy()
|
||||
tmp[VERSION_FIELD] = version_info
|
||||
return tmp
|
||||
def _update_from_index(datapoints: List[Dict], new_field: str):
|
||||
for index, datapoint in enumerate(datapoints):
|
||||
datapoint[new_field] = index
|
||||
|
||||
def _version_info(revision, filename=None, field=None):
|
||||
res = {"revision": revision}
|
||||
if filename is not None:
|
||||
res["filename"] = filename
|
||||
if field is not None:
|
||||
res["field"] = field
|
||||
return res
|
||||
|
||||
def _generate_y(datapoint, field):
|
||||
tmp = datapoint.copy()
|
||||
tmp["dvc_inferred_y_value"] = datapoint[field]
|
||||
tmp = _add_version_info(
|
||||
tmp, _version_info(revision, filename, field)
|
||||
)
|
||||
if (
|
||||
"y_label" not in properties_update
|
||||
and "y_label" not in self.plot_properties
|
||||
):
|
||||
properties_update["y_label"] = "y"
|
||||
def _update_all(datapoints: List[Dict], update_dict: Dict):
|
||||
|
||||
properties_update["y"] = "dvc_inferred_y_value"
|
||||
|
||||
return tmp
|
||||
|
||||
if isinstance(y_values, str):
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename, y_values)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(y_values, list):
|
||||
for datapoint in datapoints:
|
||||
for y_val in y_values:
|
||||
if y_val in datapoint:
|
||||
result.append(_generate_y(datapoint, y_val))
|
||||
|
||||
if isinstance(y_values, dict):
|
||||
|
||||
def _to_set(values: Iterable):
|
||||
result = set()
|
||||
for val in values:
|
||||
if isinstance(val, list):
|
||||
for elem in val:
|
||||
result.add(elem)
|
||||
else:
|
||||
result.add(val)
|
||||
|
||||
return result
|
||||
|
||||
all_fields = _to_set(y_values.values())
|
||||
if (
|
||||
all([isinstance(field, str) for field in all_fields])
|
||||
and len(all_fields) == 1
|
||||
):
|
||||
# if we use the same field from all files,
|
||||
# we dont have to generate it
|
||||
y_field = all_fields.pop()
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint,
|
||||
_version_info(revision, filename, y_field),
|
||||
)
|
||||
)
|
||||
properties_update.update({"y": y_field})
|
||||
else:
|
||||
for def_filename, val in y_values.items():
|
||||
if isinstance(val, str):
|
||||
fields = [val]
|
||||
if isinstance(val, list):
|
||||
fields = val
|
||||
for datapoint in datapoints:
|
||||
for field in fields:
|
||||
if field in datapoint and def_filename in filename:
|
||||
result.append(_generate_y(datapoint, field))
|
||||
|
||||
self.inferred_properties = {
|
||||
**self.inferred_properties,
|
||||
**properties_update,
|
||||
}
|
||||
|
||||
return result
|
||||
for datapoint in datapoints:
|
||||
datapoint.update(update_dict)
|
||||
|
||||
+9
-10
@@ -42,7 +42,7 @@ class PlotsData:
|
||||
return dict(groups)
|
||||
|
||||
def get_definition_data(self, target_files, rev):
|
||||
result = []
|
||||
result = {}
|
||||
for file in target_files:
|
||||
file_content = (
|
||||
self.data.get(rev, {})
|
||||
@@ -52,7 +52,7 @@ class PlotsData:
|
||||
.get("data", {})
|
||||
)
|
||||
if file_content:
|
||||
result.append((file, file_content))
|
||||
result[file] = file_content
|
||||
|
||||
return result
|
||||
|
||||
@@ -91,15 +91,14 @@ def match_defs_renderers(
|
||||
renderer_cls = VegaRenderer
|
||||
renderer_id = plot_id
|
||||
|
||||
converter = _get_converter(renderer_cls, props)
|
||||
converter = _get_converter(
|
||||
renderer_cls, inner_id, props, definitions_data
|
||||
)
|
||||
|
||||
for filename, plot_data in definitions_data:
|
||||
dps, final_props = converter.convert(
|
||||
revision=rev,
|
||||
filename=filename,
|
||||
data=plot_data,
|
||||
)
|
||||
plot_datapoints.extend(dps)
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
if not final_props and rev_props:
|
||||
final_props = rev_props
|
||||
plot_datapoints.extend(dps)
|
||||
|
||||
if "title" not in final_props:
|
||||
final_props["title"] = renderer_id
|
||||
|
||||
@@ -269,7 +269,7 @@ class Repo:
|
||||
except SCMError:
|
||||
if self._uninitialized:
|
||||
# might not be a git/dvc repo at all
|
||||
# used in `params/metrics/plots/live` targets
|
||||
# used in `params/metrics/plots` targets
|
||||
return SCM(self.root_dir, no_scm=True)
|
||||
raise
|
||||
|
||||
@@ -401,6 +401,7 @@ class Repo:
|
||||
used_run_cache=None,
|
||||
revs=None,
|
||||
num=1,
|
||||
push: bool = False,
|
||||
):
|
||||
"""Get the stages related to the given target and collect
|
||||
the `info` of its outputs.
|
||||
@@ -435,6 +436,7 @@ class Repo:
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ def add( # noqa: C901
|
||||
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
|
||||
else:
|
||||
try:
|
||||
stage.save()
|
||||
stage.save(merge_versioned=True)
|
||||
if not no_commit:
|
||||
stage.commit()
|
||||
except CacheLinkError:
|
||||
|
||||
+5
-3
@@ -12,6 +12,7 @@ from typing import (
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
@@ -19,10 +20,9 @@ from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.base import Base
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
@@ -140,7 +140,9 @@ class GitInfo(TypedDict, total=False):
|
||||
is_dirty: bool
|
||||
|
||||
|
||||
def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
def _git_info(
|
||||
scm: Union["Git", "NoSCM"], untracked_files: str = "all"
|
||||
) -> GitInfo:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
import time
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
from funcy import cached_property, chain, first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -171,7 +171,18 @@ class Experiments:
|
||||
) -> Dict[str, str]:
|
||||
results: Dict[str, str] = {}
|
||||
if entries is None:
|
||||
entries = list(self.celery_queue.iter_queued())
|
||||
entries = list(
|
||||
chain(
|
||||
self.celery_queue.iter_active(),
|
||||
self.celery_queue.iter_queued(),
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"reproduce all these entries '%s'",
|
||||
entries,
|
||||
)
|
||||
|
||||
if not entries:
|
||||
return results
|
||||
|
||||
@@ -189,7 +200,10 @@ class Experiments:
|
||||
time.sleep(1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
result = self.celery_queue.get_result(entry)
|
||||
try:
|
||||
result = self.celery_queue.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
result = None
|
||||
if result is None or result.exp_hash is None:
|
||||
name = entry.name or entry.stash_rev[:7]
|
||||
failed.append(name)
|
||||
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
from dvc.repo.experiments.show import _collect_experiment_commit
|
||||
from dvc.repo.experiments.show import collect_experiment_commit
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if repo.scm.no_commits:
|
||||
@@ -15,15 +15,15 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
|
||||
if a_rev:
|
||||
rev = resolve_rev(repo.scm, a_rev)
|
||||
old = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
old = _collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
|
||||
if b_rev:
|
||||
rev = resolve_rev(repo.scm, b_rev)
|
||||
new = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
new = _collect_experiment_commit(
|
||||
new = collect_experiment_commit(
|
||||
repo, "workspace", param_deps=param_deps
|
||||
)
|
||||
|
||||
|
||||
@@ -602,29 +602,22 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
finally:
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
super().cleanup(infofile)
|
||||
|
||||
def collect_cache(
|
||||
self, repo: "Repo", exp_ref: "ExpRefInfo", run_cache: bool = True
|
||||
|
||||
@@ -248,11 +248,6 @@ def init(
|
||||
except MissingParamsFile:
|
||||
pass
|
||||
|
||||
models = context.get("models")
|
||||
live_path = context.pop("live", None)
|
||||
live_metrics = f"{live_path}.json" if live_path else None
|
||||
live_plots = os.path.join(live_path, "scalars") if live_path else None
|
||||
|
||||
if type == "checkpoint":
|
||||
outs_key = "checkpoints"
|
||||
metrics_key = "metrics_persist_no_cache"
|
||||
@@ -262,6 +257,13 @@ def init(
|
||||
metrics_key = "metrics_no_cache"
|
||||
plots_key = "plots_no_cache"
|
||||
|
||||
models = [context.get("models")]
|
||||
metrics = [context.get("metrics")]
|
||||
plots = [context.get("plots")]
|
||||
if live_path := context.pop("live", None):
|
||||
metrics.append(os.path.join(live_path, "metrics.json"))
|
||||
plots.append(os.path.join(live_path, "plots"))
|
||||
|
||||
stage = repo.stage.create(
|
||||
name=name,
|
||||
cmd=context["cmd"],
|
||||
@@ -269,9 +271,9 @@ def init(
|
||||
params=[{params: None}] if params else None,
|
||||
force=force,
|
||||
**{
|
||||
outs_key: compact([models]),
|
||||
metrics_key: compact([context.get("metrics"), live_metrics]),
|
||||
plots_key: compact([context.get("plots"), live_plots]),
|
||||
outs_key: compact(models),
|
||||
metrics_key: compact(metrics),
|
||||
plots_key: compact(plots),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import (
|
||||
@@ -526,16 +525,9 @@ class BaseStashQueue(ABC):
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
|
||||
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
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from funcy import cached_property
|
||||
from funcy import cached_property, first
|
||||
from kombu.message import Message
|
||||
|
||||
from dvc.daemon import daemonize
|
||||
@@ -23,13 +23,9 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import UnresolvedQueueExpNamesError
|
||||
from ..executor.base import (
|
||||
EXEC_TMP_DIR,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
from .exceptions import CannotKillTasksError
|
||||
from .tasks import run_exp
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
|
||||
@@ -187,6 +183,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
continue
|
||||
args, kwargs, _embed = msg.decode()
|
||||
entry_dict = kwargs.get("entry_dict", args[0])
|
||||
logger.debug("Found queued task %s", entry_dict["stash_rev"])
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
|
||||
@@ -203,6 +200,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
@@ -211,6 +209,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -243,67 +242,115 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_result(
|
||||
def _load_info(self, rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _get_done_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional[ExecutorResult]:
|
||||
) -> Optional["ExecutorResult"]:
|
||||
from celery.exceptions import TimeoutError as _CeleryTimeout
|
||||
|
||||
def _load_info(rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _load_collected(rev: str) -> Optional[ExecutorResult]:
|
||||
executor_info = _load_info(rev)
|
||||
if executor_info.status > TaskStatus.SUCCESS:
|
||||
for msg, processed_entry in self._iter_processed():
|
||||
if entry.stash_rev == processed_entry.stash_rev:
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = self._load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
raise FileNotFoundError
|
||||
raise FileNotFoundError
|
||||
|
||||
def get_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional["ExecutorResult"]:
|
||||
|
||||
try:
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
except FileNotFoundError:
|
||||
# Infofile will not be created until execution begins
|
||||
pass
|
||||
|
||||
for queue_entry in self.iter_queued():
|
||||
if entry.stash_rev == queue_entry.stash_rev:
|
||||
raise DvcException("Experiment has not been started.")
|
||||
for result, active_entry in self._iter_active_tasks():
|
||||
if entry.stash_rev == active_entry.stash_rev:
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = _load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
|
||||
# NOTE: It's possible for an exp to complete while iterating through
|
||||
# other queued and active tasks, in which case the exp will get moved
|
||||
# out of the active task list, and needs to be loaded here.
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
def _get_running_task_ids(self) -> Set[str]:
|
||||
running_task_ids: Set[str] = set()
|
||||
active_workers = self.worker_status()
|
||||
for _, tasks in active_workers.items():
|
||||
task = first(tasks)
|
||||
if task:
|
||||
running_task_ids.add(task["id"])
|
||||
return running_task_ids
|
||||
|
||||
def _try_to_kill_tasks(
|
||||
self, to_kill: Dict[QueueEntry, str]
|
||||
) -> Dict[QueueEntry, str]:
|
||||
fail_to_kill_entries: Dict[QueueEntry, str] = {}
|
||||
for queue_entry, rev in to_kill.items():
|
||||
try:
|
||||
self.proc.kill(queue_entry.stash_rev)
|
||||
logger.debug(f"Task {rev} had been killed.")
|
||||
except ProcessLookupError:
|
||||
fail_to_kill_entries[queue_entry] = rev
|
||||
return fail_to_kill_entries
|
||||
|
||||
def _mark_inactive_tasks_failure(self, remained_entries):
|
||||
remained_revs: List[str] = []
|
||||
running_ids = self._get_running_task_ids()
|
||||
logger.debug(f"Current running tasks ids: {running_ids}.")
|
||||
for msg, entry in self._iter_processed():
|
||||
if entry not in remained_entries:
|
||||
continue
|
||||
task_id = msg.headers["id"]
|
||||
if task_id in running_ids:
|
||||
remained_revs.append(remained_entries[entry])
|
||||
else:
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug(
|
||||
f"Task id {task_id} rev {remained_entries[entry]} "
|
||||
"marked as failure."
|
||||
)
|
||||
self.celery.backend.mark_as_failure(task_id, None)
|
||||
|
||||
if remained_revs:
|
||||
raise CannotKillTasksError(remained_revs)
|
||||
|
||||
def kill(self, revs: Collection[str]) -> None:
|
||||
to_kill: Set[QueueEntry] = set()
|
||||
name_dict: Dict[
|
||||
str, Optional[QueueEntry]
|
||||
] = self.match_queue_entry_by_name(set(revs), self.iter_active())
|
||||
|
||||
missing_rev: List[str] = []
|
||||
to_kill: Dict[QueueEntry, str] = {}
|
||||
missing_revs: List[str] = []
|
||||
for rev, queue_entry in name_dict.items():
|
||||
if queue_entry is None:
|
||||
missing_rev.append(rev)
|
||||
missing_revs.append(rev)
|
||||
else:
|
||||
to_kill.add(queue_entry)
|
||||
to_kill[queue_entry] = rev
|
||||
if missing_revs:
|
||||
raise UnresolvedQueueExpNamesError(missing_revs)
|
||||
|
||||
if missing_rev:
|
||||
raise UnresolvedQueueExpNamesError(missing_rev)
|
||||
fail_to_kill_entries: Dict[QueueEntry, str] = self._try_to_kill_tasks(
|
||||
to_kill
|
||||
)
|
||||
|
||||
for queue_entry in to_kill:
|
||||
self.proc.kill(queue_entry.stash_rev)
|
||||
if fail_to_kill_entries:
|
||||
self._mark_inactive_tasks_failure(fail_to_kill_entries)
|
||||
|
||||
def shutdown(self, kill: bool = False):
|
||||
self.celery.control.shutdown()
|
||||
@@ -359,10 +406,11 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
) as fobj:
|
||||
ui.write(fobj.read())
|
||||
|
||||
def worker_status(self) -> Dict:
|
||||
def worker_status(self) -> Dict[str, List[Dict]]:
|
||||
"""Return the current active celery worker"""
|
||||
status = self.celery.control.inspect().active() or {}
|
||||
logger.debug(f"Worker status: {status}")
|
||||
|
||||
return status
|
||||
|
||||
def clear(self, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import Collection
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class CannotKillTasksError(DvcException):
|
||||
def __init__(self, revs: Collection[str]):
|
||||
rev_str = ",".join(revs)
|
||||
super().__init__(
|
||||
f"Task '{rev_str}' is initializing, please wait a few seconds "
|
||||
"until the experiments start running to retry the kill operation."
|
||||
)
|
||||
@@ -77,9 +77,8 @@ class TempDirQueue(WorkspaceQueue):
|
||||
for stash_rev in self.proc:
|
||||
infofile = self.get_infofile_path(stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
if (
|
||||
not executor_info.status <= TaskStatus.SUCCESS
|
||||
and os.path.exists(executor_info.root_dir)
|
||||
if executor_info.status <= TaskStatus.SUCCESS and os.path.exists(
|
||||
executor_info.root_dir
|
||||
):
|
||||
yield QueueEntry(
|
||||
self.repo.root_dir,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from ..executor.base import ExecutorInfo, TaskStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,7 +41,7 @@ def fetch_running_exp_from_temp_dir(
|
||||
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return False
|
||||
return True
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
@@ -52,7 +54,7 @@ def fetch_running_exp_from_temp_dir(
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
except (InvalidRemoteSCMRepo, SCMError):
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
return result
|
||||
|
||||
+19
-22
@@ -1,9 +1,8 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from dvc.dependency.param import ParamsDependency
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo import locked
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.cli_parse import to_path_overrides
|
||||
@@ -29,19 +28,24 @@ def run(
|
||||
Returns a dict mapping new experiment SHAs to the results
|
||||
of `repro` for that experiment.
|
||||
"""
|
||||
try:
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
except ValueError:
|
||||
if sys.version_info >= (3, 11):
|
||||
raise DvcException("exp run is not supported in Python >= 3.11")
|
||||
raise
|
||||
|
||||
if run_all:
|
||||
entries = list(repo.experiments.celery_queue.iter_queued())
|
||||
return repo.experiments.reproduce_celery(entries, jobs=jobs)
|
||||
return repo.experiments.reproduce_celery(jobs=jobs)
|
||||
|
||||
hydra_sweep = None
|
||||
if params:
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
path_overrides = to_path_overrides(params)
|
||||
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`"
|
||||
)
|
||||
else:
|
||||
path_overrides = {}
|
||||
|
||||
@@ -51,23 +55,16 @@ def run(
|
||||
# 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:
|
||||
if kwargs.get("name"):
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used alongside `--name`"
|
||||
)
|
||||
from dvc.utils.hydra import get_hydra_sweeps
|
||||
|
||||
sweeps = get_hydra_sweeps(path_overrides)
|
||||
|
||||
+208
-100
@@ -3,16 +3,32 @@ from collections import OrderedDict, defaultdict
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from scmrepo.exceptions import SCMError as InnerScmError
|
||||
|
||||
from dvc.repo.metrics.show import _gather_metrics
|
||||
from dvc.repo.params.show import _gather_params
|
||||
from dvc.scm import iter_revs
|
||||
from dvc.scm import SCMError, iter_revs, resolve_rev
|
||||
from dvc.utils import error_handler, onerror_collect, relpath
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
from scmrepo.git.objects import GitCommit
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,34 +41,49 @@ class ExpStatus(Enum):
|
||||
Failed = 3
|
||||
|
||||
|
||||
def _is_scm_error(collected_exp: Dict[str, Any]) -> bool:
|
||||
if "error" in collected_exp and (
|
||||
isinstance(collected_exp["error"], SCMError)
|
||||
or isinstance(collected_exp["error"], InnerScmError)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _show_onerror_collect(result: Dict, exception: Exception, *args, **kwargs):
|
||||
onerror_collect(result, exception, *args, **kwargs)
|
||||
result["data"] = {}
|
||||
|
||||
|
||||
@error_handler
|
||||
def _collect_experiment_commit(
|
||||
def collect_experiment_commit(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
param_deps=False,
|
||||
running=None,
|
||||
running: Optional[Dict[str, Any]] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
):
|
||||
) -> Dict[str, Any]:
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
|
||||
res: Dict[str, Optional[Any]] = defaultdict(dict)
|
||||
result: Dict[str, Any] = defaultdict(dict)
|
||||
running = running or {}
|
||||
for rev in repo.brancher(revs=[exp_rev]):
|
||||
if rev == "workspace":
|
||||
if exp_rev != "workspace":
|
||||
continue
|
||||
res["timestamp"] = None
|
||||
result["timestamp"] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
res["timestamp"] = datetime.fromtimestamp(commit.commit_time)
|
||||
result["timestamp"] = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
params = _gather_params(
|
||||
repo, rev=rev, targets=None, deps=param_deps, onerror=onerror
|
||||
)
|
||||
if params:
|
||||
res["params"] = params
|
||||
result["params"] = params
|
||||
|
||||
res["deps"] = {
|
||||
result["deps"] = {
|
||||
relpath(dep.fs_path, repo.root_dir): {
|
||||
"hash": dep.hash_info.value,
|
||||
"size": dep.meta.size,
|
||||
@@ -61,7 +92,7 @@ def _collect_experiment_commit(
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
}
|
||||
res["outs"] = {
|
||||
result["outs"] = {
|
||||
relpath(out.fs_path, repo.root_dir): {
|
||||
"hash": out.hash_info.value,
|
||||
"size": out.meta.size,
|
||||
@@ -73,14 +104,14 @@ def _collect_experiment_commit(
|
||||
if not (out.is_metric or out.is_plot)
|
||||
}
|
||||
|
||||
res["status"] = status.name
|
||||
result["status"] = status.name
|
||||
if status == ExpStatus.Running:
|
||||
res["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
result["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
else:
|
||||
res["executor"] = None
|
||||
result["executor"] = None
|
||||
|
||||
if status == ExpStatus.Failed:
|
||||
res["error"] = {
|
||||
result["error"] = {
|
||||
"msg": "Experiment run failed.",
|
||||
"type": "",
|
||||
}
|
||||
@@ -89,96 +120,157 @@ def _collect_experiment_commit(
|
||||
vals = _gather_metrics(
|
||||
repo, targets=None, rev=rev, recursive=False, onerror=onerror
|
||||
)
|
||||
res["metrics"] = vals
|
||||
result["metrics"] = vals
|
||||
|
||||
return res
|
||||
return result
|
||||
|
||||
|
||||
def _collect_experiment_branch(
|
||||
res,
|
||||
repo,
|
||||
branch,
|
||||
baseline,
|
||||
onerror: Optional[Callable] = None,
|
||||
running=None,
|
||||
def _collect_complete_experiment(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
exp_rev: str,
|
||||
running: Dict[str, Any],
|
||||
revs: List[str],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.scm import resolve_rev
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
||||
|
||||
checkpoint: bool = True if len(revs) > 1 else False
|
||||
prev = ""
|
||||
|
||||
exp_rev = resolve_rev(repo.scm, branch)
|
||||
prev = None
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
for rev in revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
collected_exp = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
rev,
|
||||
onerror=onerror,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if len(revs) > 1:
|
||||
if _is_scm_error(collected_exp):
|
||||
return {}
|
||||
if checkpoint:
|
||||
exp = {"checkpoint_tip": exp_rev}
|
||||
if prev:
|
||||
res[prev]["data"][ # type: ignore[unreachable]
|
||||
results[prev]["data"][ # type: ignore[unreachable]
|
||||
"checkpoint_parent"
|
||||
] = rev
|
||||
if rev in res:
|
||||
res[rev]["data"].update(exp)
|
||||
res.move_to_end(rev)
|
||||
if rev in results:
|
||||
results[rev]["data"].update(exp)
|
||||
results.move_to_end(rev)
|
||||
else:
|
||||
exp.update(collected_exp["data"])
|
||||
else:
|
||||
exp = collected_exp["data"]
|
||||
if rev not in res:
|
||||
res[rev] = {"data": exp}
|
||||
if rev not in results:
|
||||
results[rev] = {"data": exp}
|
||||
prev = rev
|
||||
if len(revs) > 1:
|
||||
res[prev]["data"]["checkpoint_parent"] = baseline
|
||||
return res
|
||||
if checkpoint and prev:
|
||||
results[prev]["data"]["checkpoint_parent"] = baseline
|
||||
return results
|
||||
|
||||
|
||||
def get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
|
||||
def _collect_branch(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: Dict[str, Dict[str, Any]] = OrderedDict()
|
||||
status = ExpStatus.Running if baseline in running else ExpStatus.Success
|
||||
results["baseline"] = collect_experiment_commit(
|
||||
repo,
|
||||
baseline,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if baseline == "workspace" or _is_scm_error(results["baseline"]):
|
||||
return results
|
||||
|
||||
ref_info = ExpRefInfo(baseline_sha=baseline)
|
||||
commits: List[Tuple[str, "GitCommit", str, List[str]]] = []
|
||||
|
||||
for ref in repo.scm.iter_refs(base=str(ref_info)):
|
||||
try:
|
||||
commit = repo.scm.resolve_commit(ref)
|
||||
exp_rev = resolve_rev(repo.scm, ref)
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
except (SCMError, InnerScmError):
|
||||
continue
|
||||
commits.append((ref, commit, exp_rev, revs))
|
||||
|
||||
for exp_ref, _, exp_rev, revs in sorted(
|
||||
commits, key=lambda x: x[1].commit_time, reverse=True
|
||||
):
|
||||
ref_info = ExpRefInfo.from_ref(exp_ref)
|
||||
assert ref_info.baseline_sha == baseline
|
||||
collected_exp = _collect_complete_experiment(
|
||||
repo,
|
||||
baseline=baseline,
|
||||
exp_rev=exp_rev,
|
||||
running=running,
|
||||
revs=revs,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
results.update(collected_exp)
|
||||
return results
|
||||
|
||||
|
||||
def get_branch_names(
|
||||
scm: "Git", baseline_set: Iterable[str]
|
||||
) -> Dict[str, Optional[str]]:
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for base in [
|
||||
f"refs/exps/{baseline[:2]}/{baseline[2:]}/"
|
||||
for baseline in baseline_set
|
||||
] + ["refs/heads/", "refs/tags/"]:
|
||||
for ref in scm.iter_refs(base=base):
|
||||
if ref:
|
||||
try:
|
||||
rev = scm.get_ref(ref)
|
||||
names[rev] = ref[len(base) :]
|
||||
except KeyError:
|
||||
logger.debug("unreosolved ref %s", ref)
|
||||
logger.debug("found refs for revs: %s", names)
|
||||
return names
|
||||
|
||||
|
||||
def update_names(
|
||||
repo: "Repo",
|
||||
branch_names: Dict[str, Optional[str]],
|
||||
result: Dict[str, Dict[str, Any]],
|
||||
):
|
||||
|
||||
rev_set = set()
|
||||
baseline_set = set()
|
||||
for baseline in result:
|
||||
for rev in result[baseline]:
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
baseline_set.add(rev)
|
||||
if rev != "workspace":
|
||||
rev_set.add(rev)
|
||||
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for base in ("refs/tags/", "refs/heads/"):
|
||||
if rev_set:
|
||||
names.update(
|
||||
(rev, ref[len(base) :])
|
||||
for rev, ref in repo.scm.describe(
|
||||
baseline_set, base=base
|
||||
).items()
|
||||
if ref is not None
|
||||
)
|
||||
rev_set.difference_update(names.keys())
|
||||
if rev_set:
|
||||
rev_set.difference_update(branch_names.keys())
|
||||
|
||||
exact_name = repo.experiments.get_exact_name(rev_set)
|
||||
|
||||
for baseline, baseline_results in result.items():
|
||||
name_set: Set[str] = set()
|
||||
for rev, rev_result in baseline_results.items():
|
||||
name: Optional[str] = None
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
if rev == "workspace":
|
||||
continue
|
||||
name = names.get(rev, None)
|
||||
name = name or exact_name[rev]
|
||||
if name:
|
||||
name = branch_names.get(rev, None) or exact_name[rev]
|
||||
if name and name not in name_set:
|
||||
name_set.add(name)
|
||||
rev_result["data"]["name"] = name
|
||||
|
||||
|
||||
# flake8: noqa: C901
|
||||
def _collect_active_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
@@ -194,13 +286,16 @@ def _collect_active_experiment(
|
||||
if entry.baseline_rev in found_revs and (
|
||||
stash_rev not in running or not running[stash_rev].get("last")
|
||||
):
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Running,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -214,13 +309,16 @@ def _collect_queued_experiment(
|
||||
for entry in repo.experiments.celery_queue.iter_queued():
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Queued,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -235,14 +333,16 @@ def _collect_failed_experiment(
|
||||
entry = queue_done_result.entry
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
experiment = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Failed,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
result[entry.baseline_rev][stash_rev] = experiment
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -254,6 +354,40 @@ def update_new(
|
||||
to_dict[baseline][rev] = to_dict[baseline].get(rev, experiment)
|
||||
|
||||
|
||||
def move_properties_to_head(result: Dict[str, Dict[str, Dict[str, Any]]]):
|
||||
for _, baseline_results in result.items():
|
||||
checkpoint: bool = False
|
||||
head: Dict[str, Any] = {}
|
||||
for rev, rev_data in baseline_results.items():
|
||||
if (
|
||||
"data" not in rev_data
|
||||
or rev_data["data"].get("checkpoint_tip", None) is None
|
||||
):
|
||||
checkpoint = False
|
||||
head = {}
|
||||
continue
|
||||
|
||||
rev_result: Dict[str, Any] = rev_data["data"]
|
||||
if (
|
||||
checkpoint is True
|
||||
and rev_result["checkpoint_tip"] == head["checkpoint_tip"]
|
||||
):
|
||||
if "name" in rev_result and "name" not in head:
|
||||
head["name"] = rev_result["name"]
|
||||
del rev_result["name"]
|
||||
if rev_result["executor"]:
|
||||
if not head["executor"]:
|
||||
head["executor"] = rev_result["executor"]
|
||||
rev_result["executor"] = None
|
||||
if rev_result["status"] == ExpStatus.Running.name:
|
||||
head["status"] = ExpStatus.Running.name
|
||||
rev_result["status"] = ExpStatus.Success.name
|
||||
else:
|
||||
if rev_result["checkpoint_tip"] == rev:
|
||||
head = rev_result
|
||||
checkpoint = True
|
||||
|
||||
|
||||
def show(
|
||||
repo: "Repo",
|
||||
all_branches=False,
|
||||
@@ -272,8 +406,7 @@ def show(
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
onerror = onerror or _show_onerror_collect
|
||||
|
||||
res: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
|
||||
@@ -286,6 +419,7 @@ def show(
|
||||
found_revs.update(
|
||||
iter_revs(repo.scm, revs, num, all_branches, all_tags, all_commits)
|
||||
)
|
||||
branch_names = get_branch_names(repo.scm, found_revs)
|
||||
|
||||
running: Dict[str, Dict] = repo.experiments.get_running_exps(
|
||||
fetch_refs=fetch_running
|
||||
@@ -316,7 +450,6 @@ def show(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
sha_only=sha_only,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
@@ -324,49 +457,24 @@ def show(
|
||||
else {}
|
||||
)
|
||||
|
||||
for rev in found_revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
res[rev]["baseline"] = _collect_experiment_commit(
|
||||
for baseline in found_revs:
|
||||
res[baseline] = _collect_branch(
|
||||
repo,
|
||||
rev,
|
||||
status=status,
|
||||
param_deps=param_deps,
|
||||
baseline,
|
||||
running=running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
if rev == "workspace":
|
||||
continue
|
||||
|
||||
ref_info = ExpRefInfo(baseline_sha=rev)
|
||||
commits = [
|
||||
(ref, repo.scm.resolve_commit(ref))
|
||||
for ref in repo.scm.iter_refs(base=str(ref_info))
|
||||
]
|
||||
for exp_ref, _ in sorted(
|
||||
commits, key=lambda x: x[1].commit_time, reverse=True
|
||||
):
|
||||
ref_info = ExpRefInfo.from_ref(exp_ref)
|
||||
assert ref_info.baseline_sha == rev
|
||||
_collect_experiment_branch(
|
||||
res[rev],
|
||||
repo,
|
||||
exp_ref,
|
||||
rev,
|
||||
param_deps=param_deps,
|
||||
running=running,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
if not hide_failed:
|
||||
update_new(res, failed_experiments)
|
||||
update_new(res, failed_experiments)
|
||||
|
||||
update_new(res, active_experiment)
|
||||
|
||||
if not hide_queued:
|
||||
update_new(res, queued_experiment)
|
||||
update_new(res, queued_experiment)
|
||||
|
||||
if not sha_only:
|
||||
get_names(repo, res)
|
||||
update_names(repo, branch_names, res)
|
||||
|
||||
move_properties_to_head(res)
|
||||
|
||||
return res
|
||||
|
||||
+2
-15
@@ -17,20 +17,6 @@ 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)
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def fetch(
|
||||
self,
|
||||
@@ -59,6 +45,7 @@ def fetch(
|
||||
remote is configured
|
||||
"""
|
||||
from dvc.repo.imports import save_imports
|
||||
from dvc.repo.worktree import fetch_worktree
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
if isinstance(targets, str):
|
||||
@@ -67,7 +54,7 @@ def fetch(
|
||||
with suppress(NoRemoteError):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _fetch_worktree(self, _remote)
|
||||
return fetch_worktree(self, _remote)
|
||||
|
||||
failed_count = 0
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ def imp_url(
|
||||
else:
|
||||
stage.run(jobs=jobs, no_download=no_download)
|
||||
|
||||
if not no_exec and stage.deps[0].fs.version_aware:
|
||||
stage.outs[0].can_push = False
|
||||
|
||||
stage.frozen = frozen
|
||||
|
||||
dvcfile.dump(stage)
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ def unfetched_view(
|
||||
|
||||
return True
|
||||
|
||||
unfetched = index.targets_view(targets, filter_fn=need_fetch, **kwargs)
|
||||
unfetched = index.targets_view(targets, stage_filter=need_fetch, **kwargs)
|
||||
return unfetched, changed_deps
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def partial_view(
|
||||
) -> "IndexView":
|
||||
return index.targets_view(
|
||||
targets,
|
||||
filter_fn=lambda s: s.is_partial_import,
|
||||
stage_filter=lambda s: s.is_partial_import,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
+59
-36
@@ -7,8 +7,10 @@ from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from funcy import cached_property, nullcontext
|
||||
@@ -150,25 +152,35 @@ class Index:
|
||||
|
||||
def targets_view(
|
||||
self,
|
||||
targets: "TargetType",
|
||||
filter_fn: Optional[Callable[["Stage"], bool]] = None,
|
||||
targets: Optional["TargetType"],
|
||||
stage_filter: Optional[Callable[["Stage"], bool]] = None,
|
||||
outs_filter: Optional[Callable[["Output"], bool]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "IndexView":
|
||||
"""Return read-only view of index for the specified targets.
|
||||
|
||||
Args:
|
||||
targets: Targets to collect
|
||||
filter_fn: Optional stage filter to be applied after collecting
|
||||
stage_filter: Optional stage filter to be applied after collecting
|
||||
targets.
|
||||
outs_filter: Optional output filter to be applied after collecting
|
||||
targets.
|
||||
|
||||
Additional kwargs will be passed into the stage collector.
|
||||
|
||||
Note:
|
||||
If both stage_filter and outs_filter are provided, stage_filter
|
||||
will be applied first, and the resulting view will only contain
|
||||
outputs from stages that matched stage_filter. Outputs from stages
|
||||
that did not match will be excluded from the view (whether or not
|
||||
the output would have matched outs_filter).
|
||||
"""
|
||||
stage_infos = [
|
||||
stage_info
|
||||
for stage_info in self._collect_targets(targets, **kwargs)
|
||||
if not filter_fn or filter_fn(stage_info.stage)
|
||||
if not stage_filter or stage_filter(stage_info.stage)
|
||||
]
|
||||
return IndexView(self, stage_infos)
|
||||
return IndexView(self, stage_infos, outs_filter=outs_filter)
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
@@ -242,15 +254,8 @@ class Index:
|
||||
workspace, key = out.index_key
|
||||
data_index = by_workspace[workspace]
|
||||
|
||||
if out.files:
|
||||
out.obj = out.get_obj()
|
||||
|
||||
entry = out.get_entry()
|
||||
entry.key = key
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
entry.fs = out.stage.deps[0].fs
|
||||
entry.path = out.stage.deps[0].fs_path
|
||||
entry.meta = out.stage.deps[0].meta
|
||||
data_index.add(entry)
|
||||
|
||||
return dict(by_workspace)
|
||||
@@ -263,6 +268,7 @@ class Index:
|
||||
force: bool = False,
|
||||
recursive: bool = False,
|
||||
jobs: int = None,
|
||||
push: bool = False,
|
||||
) -> "ObjectContainer":
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -276,6 +282,7 @@ class Index:
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
filter_info=filter_info,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
return used
|
||||
@@ -345,17 +352,26 @@ class Index:
|
||||
return dict_md5(self.dumpd())
|
||||
|
||||
|
||||
class _DataPrefixes(NamedTuple):
|
||||
explicit: Set["DataIndexKey"]
|
||||
recursive: Set["DataIndexKey"]
|
||||
|
||||
|
||||
class IndexView:
|
||||
"""Read-only view of Index.data using filtered stages."""
|
||||
|
||||
def __init__( # pylint: disable=redefined-outer-name
|
||||
self, index: Index, stage_infos: Iterable["StageInfo"]
|
||||
self,
|
||||
index: Index,
|
||||
stage_infos: Iterable["StageInfo"],
|
||||
outs_filter: Optional[Callable[["Output"], bool]],
|
||||
):
|
||||
self._index = index
|
||||
self._stage_infos = stage_infos
|
||||
# NOTE: stage_infos might have the same stage multiple times but with
|
||||
# different filter_info
|
||||
self._stages = list({stage for stage, _ in stage_infos})
|
||||
self._outs_filter = outs_filter
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._stages)
|
||||
@@ -384,30 +400,32 @@ class IndexView:
|
||||
yield from stage.deps
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
outs = set()
|
||||
def _filtered_outs(self) -> Iterator[Tuple["Output", Optional[str]]]:
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
outs.add(out)
|
||||
yield from outs
|
||||
if not self._outs_filter or self._outs_filter(out):
|
||||
yield out, filter_info
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
yield from {out for (out, _) in self._filtered_outs}
|
||||
|
||||
@cached_property
|
||||
def _data_prefixes(self) -> Dict[str, Set["DataIndexKey"]]:
|
||||
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
|
||||
from collections import defaultdict
|
||||
|
||||
prefixes: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = (
|
||||
*key,
|
||||
out.fs.path.relparts(filter_info, out.fs_path),
|
||||
)
|
||||
prefixes[workspace].add(key)
|
||||
prefixes[workspace].update(
|
||||
key[:i] for i in range(len(key), 0, -1)
|
||||
)
|
||||
prefixes: Dict[str, "_DataPrefixes"] = defaultdict(
|
||||
lambda: _DataPrefixes(set(), set())
|
||||
)
|
||||
for out, filter_info in self._filtered_outs:
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.path.relparts(filter_info, out.fs_path)
|
||||
if out.meta.isdir:
|
||||
prefixes[workspace].recursive.add(key)
|
||||
prefixes[workspace].explicit.update(
|
||||
key[:i] for i in range(len(key), 0, -1)
|
||||
)
|
||||
return prefixes
|
||||
|
||||
@cached_property
|
||||
@@ -418,14 +436,19 @@ class IndexView:
|
||||
|
||||
def key_filter(workspace: str, key: "DataIndexKey"):
|
||||
try:
|
||||
return key in self._data_prefixes[workspace]
|
||||
prefixes = self._data_prefixes[workspace]
|
||||
return key in prefixes.explicit or any(
|
||||
key[: len(prefix)] == prefix
|
||||
for prefix in prefixes.recursive
|
||||
)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return {
|
||||
workspace: view(index, partial(key_filter, workspace))
|
||||
for workspace, index in self._index.data.items()
|
||||
}
|
||||
data = {}
|
||||
for workspace, data_index in self._index.data.items():
|
||||
data_index.load()
|
||||
data[workspace] = view(data_index, partial(key_filter, workspace))
|
||||
return data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
|
||||
|
||||
def summary_fs_path(out: "Output") -> Optional[str]:
|
||||
from dvc.output import Output
|
||||
|
||||
assert out.live
|
||||
has_summary = True
|
||||
if isinstance(out.live, dict):
|
||||
has_summary = out.live.get(Output.PARAM_LIVE_SUMMARY, True)
|
||||
if has_summary:
|
||||
return out.fs.path.with_suffix(out.fs_path, ".json")
|
||||
return None
|
||||
@@ -8,7 +8,6 @@ from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.collect import StrPaths, collect
|
||||
from dvc.repo.live import summary_fs_path
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.utils import error_handler, errored_revisions, onerror_collect
|
||||
from dvc.utils.collections import ensure_list
|
||||
@@ -18,7 +17,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_metric(out: Output) -> bool:
|
||||
return bool(out.metric) or bool(out.live)
|
||||
return bool(out.metric)
|
||||
|
||||
|
||||
def _to_fs_paths(metrics: List[Output]) -> StrPaths:
|
||||
@@ -26,10 +25,6 @@ def _to_fs_paths(metrics: List[Output]) -> StrPaths:
|
||||
for out in metrics:
|
||||
if out.metric:
|
||||
result.append(out.repo.dvcfs.from_os_path(out.fs_path))
|
||||
elif out.live:
|
||||
fs_path = summary_fs_path(out)
|
||||
if fs_path:
|
||||
result.append(out.repo.dvcfs.from_os_path(fs_path))
|
||||
return result
|
||||
|
||||
|
||||
|
||||
+21
-12
@@ -14,11 +14,12 @@ from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
)
|
||||
|
||||
import dpath.options
|
||||
import dpath.util
|
||||
from funcy import cached_property, first, project
|
||||
from funcy import cached_property, distinct, first, project
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import error_handler, errored_revisions, onerror_collect
|
||||
@@ -265,7 +266,7 @@ class Plots:
|
||||
|
||||
|
||||
def _is_plot(out: "Output") -> bool:
|
||||
return bool(out.plot) or bool(out.live)
|
||||
return bool(out.plot)
|
||||
|
||||
|
||||
def _resolve_data_sources(plots_data: Dict):
|
||||
@@ -313,16 +314,18 @@ def _get_data_targets(definitions: Dict):
|
||||
|
||||
|
||||
def infer_data_sources(plot_id, config=None):
|
||||
def _deduplicate(lst: List):
|
||||
return list({elem: None for elem in lst}.keys())
|
||||
|
||||
y = config.get("y", None)
|
||||
|
||||
if isinstance(y, dict):
|
||||
sources = list(y.keys())
|
||||
else:
|
||||
sources = [plot_id]
|
||||
|
||||
return _deduplicate(source for source in sources)
|
||||
x = config.get("x", None)
|
||||
if isinstance(x, dict):
|
||||
sources.append(first(x.keys()))
|
||||
|
||||
return distinct(source for source in sources)
|
||||
|
||||
|
||||
def _matches(targets, config_file, plot_id):
|
||||
@@ -444,16 +447,22 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
|
||||
if isinstance(dvcfile, PipelineFile):
|
||||
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
|
||||
dvcfile_defs = dvcfile.load().get("plots", {})
|
||||
dvcfile_defs_dict: Dict[str, Union[Dict, None]] = {}
|
||||
if isinstance(dvcfile_defs, list):
|
||||
dvcfile_defs = {
|
||||
k: v for elem in dvcfile_defs for k, v in elem.items()
|
||||
}
|
||||
for elem in dvcfile_defs:
|
||||
if isinstance(elem, str):
|
||||
dvcfile_defs_dict[elem] = None
|
||||
else:
|
||||
k, v = list(elem.items())[0]
|
||||
dvcfile_defs_dict[k] = v
|
||||
else:
|
||||
dvcfile_defs_dict = dvcfile_defs
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
dvcfile_path,
|
||||
dvcfile_defs,
|
||||
dvcfile_defs_dict,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
@@ -546,7 +555,7 @@ def parse(fs, path, props=None, **kwargs):
|
||||
return _load_sv(path=path, fs=fs, delimiter="\t", header=header)
|
||||
if extension in LOADERS or extension in (".yml", ".yaml"):
|
||||
return LOADERS[extension](path=path, fs=fs)
|
||||
if extension in (".jpeg", ".jpg", ".gif", ".png"):
|
||||
if extension in (".jpeg", ".jpg", ".gif", ".png", ".svg"):
|
||||
with fs.open(path, "rb") as fd:
|
||||
return fd.read()
|
||||
raise PlotMetricTypeError(path)
|
||||
@@ -555,7 +564,7 @@ def parse(fs, path, props=None, **kwargs):
|
||||
def _plot_props(out: "Output") -> Dict:
|
||||
from dvc.schema import PLOT_PROPS
|
||||
|
||||
if not (out.plot or out.live):
|
||||
if not (out.plot):
|
||||
raise NotAPlotError(out)
|
||||
if isinstance(out.plot, list):
|
||||
raise DvcException("Multiple plots per data file not supported.")
|
||||
|
||||
+4
-43
@@ -9,48 +9,6 @@ if TYPE_CHECKING:
|
||||
from dvc_objects.db.base import ObjectDB
|
||||
|
||||
|
||||
def _push_worktree(repo, remote):
|
||||
from dvc_data.index import checkout
|
||||
from dvc_data.index.save import build_tree
|
||||
|
||||
index = repo.index.data["repo"]
|
||||
checkout(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 = build_tree(index, key)
|
||||
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, update_pipeline=False)
|
||||
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def push(
|
||||
self,
|
||||
@@ -68,9 +26,11 @@ def push(
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
include_imports=False,
|
||||
):
|
||||
from dvc.repo.worktree import push_worktree
|
||||
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _push_worktree(self, _remote)
|
||||
return push_worktree(self, _remote)
|
||||
|
||||
used_run_cache = (
|
||||
self.stage_cache.push(remote, odb=odb) if run_cache else []
|
||||
@@ -93,6 +53,7 @@ def push(
|
||||
recursive=recursive,
|
||||
used_run_cache=used_run_cache,
|
||||
revs=revs,
|
||||
push=True,
|
||||
)
|
||||
|
||||
pushed = len(used_run_cache)
|
||||
|
||||
@@ -72,12 +72,6 @@ def _get_stage_files(stage: "Stage") -> typing.Iterator[str]:
|
||||
for out in stage.outs:
|
||||
if not out.use_scm_ignore and out.is_in_repo:
|
||||
yield out.fs_path
|
||||
if out.live:
|
||||
from dvc.repo.live import summary_fs_path
|
||||
|
||||
summary = summary_fs_path(out)
|
||||
if summary:
|
||||
yield summary
|
||||
|
||||
|
||||
def _track_stage(stage: "Stage") -> None:
|
||||
|
||||
@@ -89,6 +89,7 @@ def _cloud_status(
|
||||
remote=remote,
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
push=True,
|
||||
)
|
||||
|
||||
ret = {}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.fs.callbacks import Callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.cloud import Remote
|
||||
from dvc.index import Index, IndexView
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import TargetType
|
||||
|
||||
|
||||
def worktree_view(
|
||||
index: "Index",
|
||||
targets: Optional["TargetType"] = None,
|
||||
push: bool = False,
|
||||
**kwargs,
|
||||
) -> "IndexView":
|
||||
"""Return view of data that can be stored in worktree remotes.
|
||||
|
||||
Args:
|
||||
targets: Optional targets.
|
||||
push: Whether the view should be restricted to pushable data only.
|
||||
|
||||
Additional kwargs will be passed into target collection.
|
||||
"""
|
||||
|
||||
def stage_filter(stage: "Stage") -> bool:
|
||||
if push and stage.is_repo_import:
|
||||
return False
|
||||
return True
|
||||
|
||||
def outs_filter(out: "Output") -> bool:
|
||||
if (
|
||||
not out.is_in_repo
|
||||
or not out.use_cache
|
||||
or (push and not out.can_push)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
return index.targets_view(
|
||||
targets,
|
||||
stage_filter=stage_filter,
|
||||
outs_filter=outs_filter,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def fetch_worktree(repo: "Repo", remote: "Remote") -> int:
|
||||
from dvc_data.index import save
|
||||
|
||||
view = worktree_view(repo.index)
|
||||
index = view.data["repo"]
|
||||
for key, entry in index.iteritems():
|
||||
entry.fs = remote.fs
|
||||
entry.path = remote.fs.path.join(
|
||||
remote.path,
|
||||
*key,
|
||||
)
|
||||
total = len(index)
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file", desc="Fetch", disable=total == 0
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
return save(index, callback=cb)
|
||||
|
||||
|
||||
def push_worktree(repo: "Repo", remote: "Remote") -> int:
|
||||
from dvc_data.index import checkout
|
||||
from dvc_data.index.save import build_tree
|
||||
|
||||
view = worktree_view(repo.index, push=True)
|
||||
index = view.data["repo"]
|
||||
total = len(index)
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file", desc="Push", disable=total == 0
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
pushed = checkout(
|
||||
index,
|
||||
remote.path,
|
||||
remote.fs,
|
||||
latest_only=False,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
for stage in view.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 = build_tree(index, key)
|
||||
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, update_pipeline=False)
|
||||
|
||||
return pushed
|
||||
+3
-10
@@ -51,6 +51,7 @@ OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_PUSH: bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +68,6 @@ PLOT_PROPS = {
|
||||
PLOT_PROPS_SCHEMA = {**OUT_PSTAGE_DETAILED_SCHEMA[str], **PLOT_PROPS}
|
||||
PLOT_PSTAGE_SCHEMA = {str: Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])}
|
||||
|
||||
LIVE_PROPS = {
|
||||
Output.PARAM_LIVE_SUMMARY: bool,
|
||||
Output.PARAM_LIVE_HTML: bool,
|
||||
}
|
||||
LIVE_PROPS_SCHEMA = {**PLOT_PROPS_SCHEMA, **LIVE_PROPS}
|
||||
LIVE_PSTAGE_SCHEMA = {str: LIVE_PROPS_SCHEMA}
|
||||
|
||||
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
|
||||
|
||||
VARS_SCHEMA = [str, dict]
|
||||
@@ -93,7 +87,6 @@ STAGE_DEFINITION = {
|
||||
Any(str, OUT_PSTAGE_DETAILED_SCHEMA)
|
||||
],
|
||||
Optional(StageParams.PARAM_PLOTS): [Any(str, PLOT_PSTAGE_SCHEMA)],
|
||||
Optional(StageParams.PARAM_LIVE): Any(str, LIVE_PSTAGE_SCHEMA),
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +104,7 @@ def either_or(primary, fallback, fallback_includes=None):
|
||||
|
||||
|
||||
PLOT_DEFINITION = {
|
||||
Output.PARAM_PLOT_X: str,
|
||||
Output.PARAM_PLOT_X: Any(str, {str: str}),
|
||||
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
|
||||
Output.PARAM_PLOT_X_LABEL: str,
|
||||
Output.PARAM_PLOT_Y_LABEL: str,
|
||||
@@ -127,7 +120,7 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
|
||||
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
|
||||
}
|
||||
MULTI_STAGE_SCHEMA = {
|
||||
PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]),
|
||||
PLOTS: Any(SINGLE_PLOT_SCHEMA, [Any(str, SINGLE_PLOT_SCHEMA)]),
|
||||
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
|
||||
VARS_KWD: VARS_SCHEMA,
|
||||
}
|
||||
|
||||
+36
-23
@@ -113,10 +113,11 @@ def restore_fields(stage):
|
||||
stage.meta = old.meta
|
||||
stage.desc = old.desc
|
||||
|
||||
old_fields = {out.def_path: (out.annot, out.remote) for out in old.outs}
|
||||
old_outs = {out.def_path: out for out in old.outs}
|
||||
for out in stage.outs:
|
||||
if out_fields := old_fields.get(out.def_path, None):
|
||||
out.annot, out.remote = out_fields
|
||||
old_out = old_outs.get(out.def_path, None)
|
||||
if old_out is not None:
|
||||
out.restore_fields(old_out)
|
||||
|
||||
|
||||
class Stage(params.StageParams):
|
||||
@@ -280,22 +281,7 @@ class Stage(params.StageParams):
|
||||
|
||||
def _read_env(self, out, checkpoint_func=None) -> Env:
|
||||
env: Env = {}
|
||||
if out.live:
|
||||
from dvc.env import DVCLIVE_HTML, DVCLIVE_PATH, DVCLIVE_SUMMARY
|
||||
from dvc.output import Output
|
||||
from dvc.schema import LIVE_PROPS
|
||||
|
||||
env[DVCLIVE_PATH] = relpath(out.fs_path, self.wdir)
|
||||
if isinstance(out.live, dict):
|
||||
config = project(out.live, LIVE_PROPS)
|
||||
|
||||
env[DVCLIVE_SUMMARY] = str(
|
||||
int(config.get(Output.PARAM_LIVE_SUMMARY, True))
|
||||
)
|
||||
env[DVCLIVE_HTML] = str(
|
||||
int(config.get(Output.PARAM_LIVE_HTML, True))
|
||||
)
|
||||
elif out.checkpoint and checkpoint_func:
|
||||
if out.checkpoint and checkpoint_func:
|
||||
from dvc.env import DVC_CHECKPOINT
|
||||
|
||||
env.update({DVC_CHECKPOINT: "1"})
|
||||
@@ -379,7 +365,7 @@ class Stage(params.StageParams):
|
||||
def remove_outs(self, ignore_remove=False, force=False):
|
||||
"""Used mainly for `dvc remove --outs` and :func:`Stage.reproduce`."""
|
||||
for out in self.outs:
|
||||
if (out.persist or out.checkpoint or out.live) and not force:
|
||||
if (out.persist or out.checkpoint) and not force:
|
||||
out.unprotect()
|
||||
continue
|
||||
|
||||
@@ -477,10 +463,12 @@ class Stage(params.StageParams):
|
||||
logger.debug("Computed %s md5: '%s'", self, m)
|
||||
return m
|
||||
|
||||
def save(self, allow_missing=False):
|
||||
def save(self, allow_missing: bool = False, merge_versioned: bool = False):
|
||||
self.save_deps(allow_missing=allow_missing)
|
||||
|
||||
self.save_outs(allow_missing=allow_missing)
|
||||
self.save_outs(
|
||||
allow_missing=allow_missing, merge_versioned=merge_versioned
|
||||
)
|
||||
self.md5 = self.compute_md5()
|
||||
|
||||
self.repo.stage_cache.save(self)
|
||||
@@ -495,15 +483,40 @@ class Stage(params.StageParams):
|
||||
if not allow_missing:
|
||||
raise
|
||||
|
||||
def save_outs(self, allow_missing=False):
|
||||
def save_outs(
|
||||
self, allow_missing: bool = False, merge_versioned: bool = False
|
||||
):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
from .exceptions import StageFileDoesNotExistError, StageNotFound
|
||||
|
||||
if merge_versioned:
|
||||
try:
|
||||
old = self.reload()
|
||||
old_outs = {out.def_path: out for out in old.outs}
|
||||
merge_versioned = any(
|
||||
(
|
||||
out.files is not None
|
||||
or (
|
||||
out.meta is not None
|
||||
and out.meta.version_id is not None
|
||||
)
|
||||
)
|
||||
for out in old_outs.values()
|
||||
)
|
||||
except (StageFileDoesNotExistError, StageNotFound):
|
||||
merge_versioned = False
|
||||
|
||||
for out in self.outs:
|
||||
try:
|
||||
out.save()
|
||||
except OutputDoesNotExistError:
|
||||
if not (allow_missing or out.checkpoint):
|
||||
raise
|
||||
if merge_versioned:
|
||||
old_out = old_outs.get(out.def_path)
|
||||
if old_out is not None:
|
||||
out.merge_version_meta(old_out)
|
||||
|
||||
def ignore_outs(self):
|
||||
for out in self.outs:
|
||||
|
||||
+6
-4
@@ -180,7 +180,7 @@ class StageCache:
|
||||
dump_yaml(tmp, cache)
|
||||
self.repo.odb.local.move(tmp, path)
|
||||
|
||||
def restore(self, stage, run_cache=True, pull=False):
|
||||
def restore(self, stage, run_cache=True, pull=False, dry=False):
|
||||
from .serialize import to_single_stage_lockfile
|
||||
|
||||
if not _can_hash(stage):
|
||||
@@ -195,14 +195,15 @@ class StageCache:
|
||||
else:
|
||||
if not run_cache: # backward compatibility
|
||||
raise RunCacheNotFoundError(stage)
|
||||
stage.save_deps()
|
||||
if not dry:
|
||||
stage.save_deps()
|
||||
cache = self._load(stage)
|
||||
if not cache:
|
||||
raise RunCacheNotFoundError(stage)
|
||||
|
||||
cached_stage = self._create_stage(cache, wdir=stage.wdir)
|
||||
|
||||
if pull:
|
||||
if pull and not dry:
|
||||
for objs in cached_stage.get_used_objs().values():
|
||||
self.repo.cloud.pull(objs)
|
||||
|
||||
@@ -213,7 +214,8 @@ class StageCache:
|
||||
"Stage '%s' is cached - skipping run, checking out outputs",
|
||||
stage.addressing,
|
||||
)
|
||||
cached_stage.checkout()
|
||||
if not dry:
|
||||
cached_stage.checkout()
|
||||
|
||||
def transfer(self, from_odb, to_odb):
|
||||
from dvc.fs import HTTPFileSystem, LocalFileSystem
|
||||
|
||||
@@ -97,7 +97,6 @@ class StageLoader(Mapping):
|
||||
stage.PARAM_OUTS,
|
||||
stage.PARAM_METRICS,
|
||||
stage.PARAM_PLOTS,
|
||||
stage.PARAM_LIVE,
|
||||
],
|
||||
)
|
||||
stage.outs = lcat(
|
||||
|
||||
@@ -20,10 +20,6 @@ class CheckpointKilledError(StageCmdFailedError):
|
||||
pass
|
||||
|
||||
|
||||
class LiveKilledError(StageCmdFailedError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorTask:
|
||||
stage: "Stage"
|
||||
|
||||
@@ -12,4 +12,3 @@ class StageParams:
|
||||
PARAM_METRICS = "metrics"
|
||||
PARAM_PLOTS = "plots"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
|
||||
+6
-4
@@ -139,14 +139,16 @@ def cmd_run(stage, dry=False, checkpoint_func=None, run_env=None):
|
||||
def run_stage(
|
||||
stage, dry=False, force=False, checkpoint_func=None, run_env=None, **kwargs
|
||||
):
|
||||
if not (dry or force or checkpoint_func):
|
||||
if not (force or checkpoint_func):
|
||||
from .cache import RunCacheNotFoundError
|
||||
|
||||
try:
|
||||
stage.repo.stage_cache.restore(stage, **kwargs)
|
||||
return
|
||||
stage.repo.stage_cache.restore(stage, dry=dry, **kwargs)
|
||||
if not dry:
|
||||
return
|
||||
except RunCacheNotFoundError:
|
||||
stage.save_deps()
|
||||
if not dry:
|
||||
stage.save_deps()
|
||||
|
||||
run = cmd_run if dry else unlocked_repo(cmd_run)
|
||||
run(stage, dry=dry, checkpoint_func=checkpoint_func, run_env=run_env)
|
||||
|
||||
+6
-10
@@ -29,6 +29,7 @@ PARAM_PERSIST = Output.PARAM_PERSIST
|
||||
PARAM_CHECKPOINT = Output.PARAM_CHECKPOINT
|
||||
PARAM_DESC = Annotation.PARAM_DESC
|
||||
PARAM_REMOTE = Output.PARAM_REMOTE
|
||||
PARAM_PUSH = Output.PARAM_PUSH
|
||||
|
||||
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
|
||||
@@ -52,10 +53,10 @@ def _get_flags(out):
|
||||
# `out.plot` is in the same order as is in the file when read
|
||||
# and, should be dumped as-is without any sorting
|
||||
yield from out.plot.items()
|
||||
if out.live and isinstance(out.live, dict):
|
||||
yield from out.live.items()
|
||||
if out.remote:
|
||||
yield PARAM_REMOTE, out.remote
|
||||
if not out.can_push:
|
||||
yield PARAM_PUSH, False
|
||||
|
||||
|
||||
def _serialize_out(out):
|
||||
@@ -65,19 +66,15 @@ def _serialize_out(out):
|
||||
|
||||
@no_type_check
|
||||
def _serialize_outs(outputs: List[Output]):
|
||||
outs, metrics, plots, live = [], [], [], None
|
||||
outs, metrics, plots = [], [], []
|
||||
for out in sort_by_path(outputs):
|
||||
bucket = outs
|
||||
if out.plot:
|
||||
bucket = plots
|
||||
elif out.metric:
|
||||
bucket = metrics
|
||||
elif out.live:
|
||||
assert live is None
|
||||
live = _serialize_out(out)
|
||||
continue
|
||||
bucket.append(_serialize_out(out))
|
||||
return outs, metrics, plots, live
|
||||
return outs, metrics, plots
|
||||
|
||||
|
||||
def _serialize_params_keys(params):
|
||||
@@ -126,7 +123,7 @@ def to_pipeline_file(stage: "PipelineStage"):
|
||||
deps = sorted(d.def_path for d in deps)
|
||||
params = _serialize_params_keys(params)
|
||||
|
||||
outs, metrics, plots, live = _serialize_outs(stage.outs)
|
||||
outs, metrics, plots = _serialize_outs(stage.outs)
|
||||
|
||||
cmd = stage.cmd
|
||||
assert cmd, (
|
||||
@@ -142,7 +139,6 @@ def to_pipeline_file(stage: "PipelineStage"):
|
||||
(stage.PARAM_OUTS, outs),
|
||||
(stage.PARAM_METRICS, metrics),
|
||||
(stage.PARAM_PLOTS, plots),
|
||||
(stage.PARAM_LIVE, live),
|
||||
(stage.PARAM_FROZEN, stage.frozen),
|
||||
(stage.PARAM_ALWAYS_CHANGED, stage.always_changed),
|
||||
(stage.PARAM_META, stage.meta),
|
||||
|
||||
+1
-43
@@ -2,7 +2,7 @@ import os
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from funcy import concat, first, lsplit, rpartial, without
|
||||
from funcy import concat, first, lsplit, rpartial
|
||||
|
||||
from dvc.annotations import ANNOTATION_FIELDS
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
@@ -66,8 +66,6 @@ def fill_stage_outputs(stage, **kwargs):
|
||||
|
||||
stage.outs = []
|
||||
|
||||
stage.outs += _load_live_output(stage, **kwargs)
|
||||
|
||||
for key in keys:
|
||||
stage.outs += loads_from(
|
||||
stage,
|
||||
@@ -80,34 +78,6 @@ def fill_stage_outputs(stage, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
def _load_live_output(
|
||||
stage,
|
||||
live=None,
|
||||
live_no_cache=None,
|
||||
live_summary=False,
|
||||
live_html=False,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.output import Output, loads_from
|
||||
|
||||
outs = []
|
||||
if live or live_no_cache:
|
||||
assert bool(live) != bool(live_no_cache)
|
||||
|
||||
path = live or live_no_cache
|
||||
outs += loads_from(
|
||||
stage,
|
||||
[path],
|
||||
use_cache=not bool(live_no_cache),
|
||||
live={
|
||||
Output.PARAM_LIVE_SUMMARY: live_summary,
|
||||
Output.PARAM_LIVE_HTML: live_html,
|
||||
},
|
||||
)
|
||||
|
||||
return outs
|
||||
|
||||
|
||||
def fill_stage_dependencies(
|
||||
stage, deps=None, erepo=None, params=None, fs_config=None
|
||||
):
|
||||
@@ -274,7 +244,6 @@ def prepare_file_path(kwargs):
|
||||
kwargs.get("outs_persist", []),
|
||||
kwargs.get("outs_persist_no_cache", []),
|
||||
kwargs.get("checkpoints", []),
|
||||
without([kwargs.get("live", None)], None),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -332,15 +301,4 @@ def validate_kwargs(single_stage: bool = False, fname: str = None, **kwargs):
|
||||
if single_stage:
|
||||
kwargs.pop("name", None)
|
||||
|
||||
if kwargs.get("live") and kwargs.get("live_no_cache"):
|
||||
raise InvalidArgumentError(
|
||||
"cannot specify both `--live` and `--live-no-cache`"
|
||||
)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"live_summary": not kwargs.pop("live_no_summary", False),
|
||||
"live_html": not kwargs.pop("live_no_html", False),
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
@@ -13,14 +13,22 @@ class TestAPI:
|
||||
assert api.get_url("foo") == expected_url
|
||||
|
||||
def test_open(self, tmp_dir, dvc, remote):
|
||||
tmp_dir.dvc_gen("foo", "foo-text")
|
||||
tmp_dir.dvc_gen(
|
||||
{
|
||||
"foo": "foo-text",
|
||||
"dir": {"bar": "bar-text"},
|
||||
}
|
||||
)
|
||||
dvc.push()
|
||||
|
||||
# Remove cache to force download
|
||||
remove(dvc.odb.local.path)
|
||||
|
||||
with api.open("foo") as fd:
|
||||
assert fd.read() == "foo-text"
|
||||
with api.open("foo") as fobj:
|
||||
assert fobj.read() == "foo-text"
|
||||
|
||||
with api.open("dir/bar") as fobj:
|
||||
assert fobj.read() == "bar-text"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"clear_cache", [True, False], ids=["cache", "no_cache"]
|
||||
|
||||
+3
-1
@@ -270,7 +270,9 @@ class Console:
|
||||
|
||||
path = Path(file).resolve()
|
||||
url = (
|
||||
relpath(path) if "Microsoft" in uname().release else path.as_uri()
|
||||
relpath(path)
|
||||
if "microsoft" in uname().release.lower()
|
||||
else path.as_uri()
|
||||
)
|
||||
|
||||
opened = webbrowser.open(url)
|
||||
|
||||
+43
-10
@@ -1,15 +1,24 @@
|
||||
import sys
|
||||
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 omegaconf import OmegaConf
|
||||
try:
|
||||
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 omegaconf import OmegaConf
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
hydra_compatible = True
|
||||
except ValueError:
|
||||
if sys.version_info >= (3, 11):
|
||||
hydra_compatible = False
|
||||
else:
|
||||
raise
|
||||
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
|
||||
from .serialize import DUMPERS, MODIFIERS
|
||||
@@ -37,11 +46,20 @@ def compose_and_dump(
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
if not hydra_compatible:
|
||||
raise DvcException(
|
||||
"hydra functionality is not supported in Python >= 3.11"
|
||||
)
|
||||
|
||||
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))
|
||||
suffix = Path(output_file).suffix.lower()
|
||||
if suffix not in [".yml", ".yaml"]:
|
||||
dumper = DUMPERS[suffix]
|
||||
dumper(output_file, OmegaConf.to_object(cfg))
|
||||
else:
|
||||
Path(output_file).write_text(OmegaConf.to_yaml(cfg), encoding="utf-8")
|
||||
|
||||
|
||||
def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
@@ -53,6 +71,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
"""
|
||||
if not hydra_compatible:
|
||||
raise DvcException(
|
||||
"hydra functionality is not supported in Python >= 3.11"
|
||||
)
|
||||
|
||||
suffix = Path(path).suffix.lower()
|
||||
|
||||
hydra_errors = (ConfigCompositionException, OverrideParseException)
|
||||
@@ -78,6 +101,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
|
||||
|
||||
|
||||
def to_hydra_overrides(path_overrides):
|
||||
if not hydra_compatible:
|
||||
raise DvcException(
|
||||
"hydra functionality is not supported in Python >= 3.11"
|
||||
)
|
||||
|
||||
parser = OverridesParser.create()
|
||||
return parser.parse_overrides(overrides=path_overrides)
|
||||
|
||||
@@ -89,6 +117,11 @@ def dict_product(dicts):
|
||||
|
||||
|
||||
def get_hydra_sweeps(path_overrides):
|
||||
if not hydra_compatible:
|
||||
raise DvcException(
|
||||
"hydra functionality is not supported in Python >= 3.11"
|
||||
)
|
||||
|
||||
path_sweeps = {}
|
||||
for path, overrides in path_overrides.items():
|
||||
overrides = to_hydra_overrides(overrides)
|
||||
|
||||
+6
-2
@@ -1,2 +1,6 @@
|
||||
def get_plot_id(plot_id: str, config_file_path: str = ""):
|
||||
return f"{config_file_path}::{plot_id}" if config_file_path else plot_id
|
||||
def get_plot_id(config_plot_id: str, config_file_path: str = ""):
|
||||
return (
|
||||
f"{config_file_path}::{config_plot_id}"
|
||||
if config_file_path
|
||||
else config_plot_id
|
||||
)
|
||||
|
||||
+13
-11
@@ -51,13 +51,13 @@ dependencies = [
|
||||
"pyparsing>=2.4.7",
|
||||
"typing-extensions>=3.7.4",
|
||||
"scmrepo==0.1.3",
|
||||
"dvc-render==0.0.12",
|
||||
"dvc-task==0.1.4",
|
||||
"dvclive>=0.10.0",
|
||||
"dvc-data==0.25.2",
|
||||
"dvc-render==0.0.14",
|
||||
"dvc-task==0.1.6",
|
||||
"dvclive>=1.0",
|
||||
"dvc-data==0.28.0",
|
||||
"dvc-http==2.27.2",
|
||||
"hydra-core>=1.1.0",
|
||||
"iterative-telemetry==0.0.5",
|
||||
"iterative-telemetry==0.0.6",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
classifiers = [
|
||||
@@ -70,12 +70,12 @@ classifiers = [
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
azure = ["dvc-azure==2.20.4"]
|
||||
gdrive = ["dvc-gdrive==2.19.0"]
|
||||
gs = ["dvc-gs==2.19.1"]
|
||||
azure = ["dvc-azure==2.20.5"]
|
||||
gdrive = ["dvc-gdrive==2.19.1"]
|
||||
gs = ["dvc-gs==2.20.0"]
|
||||
hdfs = ["dvc-hdfs==2.19.0; python_version < '3.11'"]
|
||||
oss = ["dvc-oss==2.19.0"]
|
||||
s3 = ["dvc-s3==2.20.1"]
|
||||
s3 = ["dvc-s3==2.21.0"]
|
||||
ssh = ["dvc-ssh==2.20.0"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]==2.20.0"]
|
||||
webdav = ["dvc-webdav==2.19.0"]
|
||||
@@ -99,8 +99,8 @@ tests = [
|
||||
"pytest-timeout==2.1.0",
|
||||
"filelock==3.8.0",
|
||||
# optional dependencies
|
||||
"dvclive>=1.0",
|
||||
"pywin32>=225; sys_platform == 'win32'",
|
||||
"dvclive[image]>=0.7.3",
|
||||
"beautifulsoup4==4.11.1",
|
||||
# pylint requirements
|
||||
"pylint==2.15.5",
|
||||
@@ -110,7 +110,7 @@ tests = [
|
||||
# and, protected method calls in our tests
|
||||
"pylint-plugin-utils==0.7",
|
||||
# type-checking
|
||||
"mypy==0.982",
|
||||
"mypy==0.990",
|
||||
"types-requests>=2.27.15",
|
||||
"types-tabulate>=0.8.6",
|
||||
"types-toml>=0.10.4",
|
||||
@@ -171,6 +171,7 @@ markers = [
|
||||
]
|
||||
xfail_strict = true
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = ["error::pytest_mock.PytestMockWarning"]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
@@ -200,6 +201,7 @@ show_traceback = true
|
||||
pretty = true
|
||||
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
|
||||
ignore_missing_imports = true
|
||||
implicit_optional = true
|
||||
check_untyped_defs = false
|
||||
# Warnings
|
||||
warn_no_return = true
|
||||
|
||||
@@ -44,6 +44,10 @@ if args.pkg == "osxpkg":
|
||||
]
|
||||
)
|
||||
else:
|
||||
if args.pkg == "rpm":
|
||||
# https://github.com/jordansissel/fpm/issues/1503
|
||||
flags.extend(["--rpm-rpmbuild-define", "_build_id_links none"])
|
||||
|
||||
bash_dir = build / "etc" / "bash_completion.d"
|
||||
dirs = ["usr", "etc"]
|
||||
flags.extend(["--depends", "git >= 1.7.0"]) # needed for gitpython
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
hiddenimports = ["win32timezone"]
|
||||
@@ -3,4 +3,3 @@ from PyInstaller.utils.hooks import ( # pylint:disable=import-error
|
||||
)
|
||||
|
||||
datas = copy_metadata("pydrive2")
|
||||
datas += copy_metadata("google-api-python-client")
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from unittest import TestCase
|
||||
|
||||
import pytest
|
||||
import shortuuid
|
||||
|
||||
from dvc.repo import Repo as DvcRepo
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
logger = logging.getLogger("dvc")
|
||||
|
||||
|
||||
class TestDirFixture:
|
||||
DATA_DIR = "data_dir"
|
||||
DATA_SUB_DIR = os.path.join(DATA_DIR, "data_sub_dir")
|
||||
DATA = os.path.join(DATA_DIR, "data")
|
||||
DATA_SUB = os.path.join(DATA_SUB_DIR, "data_sub")
|
||||
DATA_CONTENTS = DATA
|
||||
DATA_SUB_CONTENTS = DATA_SUB
|
||||
FOO = "foo"
|
||||
FOO_CONTENTS = FOO
|
||||
BAR = "bar"
|
||||
# NOTE: len(FOO_CONTENTS) must be != len(BAR_CONTENTS)
|
||||
#
|
||||
# Our state database relies on file mtime and file size to determine
|
||||
# that a file has changed. Usually, mtime is enough by itself but on
|
||||
# some filesystems like APFS on macOS mtime resolution is so bad,
|
||||
# that our tests can overwrite a file in that time window without dvc
|
||||
# being able to detect that, thus causing tests to fail. Usually,
|
||||
# in tests, we replace foo with bar, so we need to make sure that when we
|
||||
# modify a file in our tests, its content length changes.
|
||||
BAR_CONTENTS = BAR + "r"
|
||||
CODE = "code.py"
|
||||
CODE_CONTENTS = (
|
||||
"import sys\nimport shutil\n"
|
||||
"shutil.copyfile(sys.argv[1], sys.argv[2])"
|
||||
)
|
||||
UNICODE = "тест"
|
||||
UNICODE_CONTENTS = "проверка"
|
||||
|
||||
def __init__(self):
|
||||
root_dir = self.mkdtemp()
|
||||
self._root_dir = os.path.realpath(root_dir)
|
||||
self._saved_dir = None
|
||||
|
||||
@property
|
||||
def root_dir(self):
|
||||
return self._root_dir
|
||||
|
||||
def _pushd(self, d):
|
||||
if not self._saved_dir:
|
||||
self._saved_dir = os.path.realpath(os.curdir)
|
||||
os.chdir(d)
|
||||
|
||||
def _popd(self):
|
||||
os.chdir(self._saved_dir)
|
||||
self._saved_dir = None
|
||||
|
||||
def create(self, name, contents):
|
||||
dname = os.path.dirname(name)
|
||||
if len(dname) > 0 and not os.path.isdir(dname):
|
||||
os.makedirs(dname)
|
||||
|
||||
with open(name, "a", encoding="utf-8") as f:
|
||||
f.write(
|
||||
contents
|
||||
if isinstance(contents, str)
|
||||
else contents.decode("utf-8")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mkdtemp(base_directory=None):
|
||||
prefix = f"dvc-test.{os.getpid()}."
|
||||
suffix = f".{shortuuid.uuid()}"
|
||||
return tempfile.mkdtemp(
|
||||
prefix=prefix, suffix=suffix, dir=base_directory
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self._pushd(self._root_dir)
|
||||
self.create(self.FOO, self.FOO_CONTENTS)
|
||||
self.create(self.BAR, self.BAR_CONTENTS)
|
||||
self.create(self.CODE, self.CODE_CONTENTS)
|
||||
os.mkdir(self.DATA_DIR)
|
||||
os.mkdir(self.DATA_SUB_DIR)
|
||||
self.create(self.DATA, self.DATA_CONTENTS)
|
||||
self.create(self.DATA_SUB, self.DATA_SUB_CONTENTS)
|
||||
self.create(self.UNICODE, self.UNICODE_CONTENTS)
|
||||
|
||||
def tearDown(self):
|
||||
self._popd()
|
||||
try:
|
||||
remove(self._root_dir)
|
||||
except OSError as exc: # pragma: no cover
|
||||
# pylint: disable=no-member
|
||||
# We ignore this under Windows with a warning because it happened
|
||||
# to be really hard to trace all not properly closed files.
|
||||
#
|
||||
# Best guess so far is that gitpython is the culprit:
|
||||
# it opens files and uses __del__ to close them, which can happen
|
||||
# late in current pythons. TestGitFixture and TestDvcFixture try
|
||||
# to close that and it works on most of the tests, but not all.
|
||||
# Repos and thus git repos are created all over the dvc ;)
|
||||
if os.name == "nt" and exc.winerror == 32:
|
||||
warnings.warn("Failed to remove test dir: " + str(exc))
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class TestGitFixture(TestDirFixture):
|
||||
N_RETRIES = 5
|
||||
|
||||
def setUp(self):
|
||||
from scmrepo.git import Git
|
||||
|
||||
super().setUp()
|
||||
self.git = Git.init(".")
|
||||
self.git.add_commit(self.CODE, message="add code")
|
||||
|
||||
def tearDown(self):
|
||||
self.git.close()
|
||||
super().tearDown()
|
||||
|
||||
|
||||
class TestDvcFixture(TestDirFixture):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dvc = DvcRepo.init(self.root_dir, no_scm=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.dvc.close()
|
||||
super().tearDown()
|
||||
|
||||
|
||||
class TestDvcGitFixture(TestGitFixture):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dvc = DvcRepo.init(self.root_dir)
|
||||
self.dvc.scm.commit("init dvc")
|
||||
|
||||
def tearDown(self):
|
||||
self.dvc.close()
|
||||
super().tearDown()
|
||||
|
||||
|
||||
# NOTE: Inheritance order in the classes below is important.
|
||||
|
||||
|
||||
class TestDvc(TestDvcFixture, TestCase):
|
||||
def __init__(self, methodName):
|
||||
TestDvcFixture.__init__(self)
|
||||
TestCase.__init__(self, methodName)
|
||||
self._caplog = None
|
||||
self._capsys = None
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def inject_fixtures(self, caplog, capsys):
|
||||
self._caplog = caplog
|
||||
self._capsys = capsys
|
||||
|
||||
|
||||
class TestDvcGit(TestDvcGitFixture, TestCase):
|
||||
def __init__(self, methodName):
|
||||
TestDvcGitFixture.__init__(self)
|
||||
TestCase.__init__(self, methodName)
|
||||
self._caplog = None
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def inject_fixtures(self, caplog):
|
||||
self._caplog = caplog
|
||||
+19
-5
@@ -1,3 +1,4 @@
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -9,7 +10,7 @@ from dvc.testing.fixtures import * # noqa, pylint: disable=wildcard-import
|
||||
|
||||
from .dir_helpers import * # noqa, pylint: disable=wildcard-import
|
||||
from .remotes import * # noqa, pylint: disable=wildcard-import
|
||||
from .utils.scriptify import scriptify
|
||||
from .scripts import * # noqa, pylint: disable=wildcard-import
|
||||
|
||||
# Prevent updater and analytics from running their processes
|
||||
os.environ["DVC_TEST"] = "true"
|
||||
@@ -172,9 +173,6 @@ def custom_template(tmp_dir, dvc):
|
||||
return template
|
||||
|
||||
|
||||
scriptify_fixture = pytest.fixture(lambda: scriptify, name="scriptify")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mocked_webbrowser_open(mocker):
|
||||
mocker.patch("webbrowser.open")
|
||||
@@ -218,7 +216,7 @@ defaultBranch=master
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_copy_metrics(tmp_dir, run_copy):
|
||||
def run_copy_metrics(tmp_dir, copy_script):
|
||||
def run(
|
||||
file1,
|
||||
file2,
|
||||
@@ -262,3 +260,19 @@ def mock_hydra_conf(mocker):
|
||||
# NOTE: using sentinel here so that any imports from `hydra.conf`
|
||||
# return a mock.
|
||||
sys.modules["hydra.conf"] = mocker.sentinel
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def gc_collect_on_dvc_close_on_win_311(mocker):
|
||||
if sys.version_info < (3, 11) and os.name != "nt":
|
||||
return
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
close = Repo.close
|
||||
|
||||
def wrapped(repo):
|
||||
close(repo)
|
||||
gc.collect()
|
||||
|
||||
mocker.patch("dvc.repo.Repo.close", wrapped)
|
||||
|
||||
+2
-29
@@ -47,7 +47,6 @@ from global repo template to creating everything inplace, which:
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -67,17 +66,7 @@ disable_other_loggers()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_copy(tmp_dir, dvc):
|
||||
tmp_dir.gen(
|
||||
"copy.py",
|
||||
(
|
||||
"import sys, shutil, os\n"
|
||||
"shutil.copyfile(sys.argv[1], sys.argv[2]) "
|
||||
"if os.path.isfile(sys.argv[1]) "
|
||||
"else shutil.copytree(sys.argv[1], sys.argv[2])"
|
||||
),
|
||||
)
|
||||
|
||||
def run_copy(tmp_dir, copy_script, dvc):
|
||||
def run_copy(src, dst, **run_kwargs):
|
||||
wdir = pathlib.Path(run_kwargs.get("wdir", "."))
|
||||
wdir = pathlib.Path("../" * len(wdir.parts))
|
||||
@@ -94,23 +83,7 @@ def run_copy(tmp_dir, dvc):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_head(tmp_dir, dvc):
|
||||
"""Output first line of each file to different file with '-1' appended.
|
||||
Useful for tracking multiple outputs/dependencies which are not a copy
|
||||
of each others.
|
||||
"""
|
||||
tmp_dir.gen(
|
||||
{
|
||||
"head.py": dedent(
|
||||
"""
|
||||
import sys
|
||||
for file in sys.argv[1:]:
|
||||
with open(file) as f, open(file +"-1","w+") as w:
|
||||
w.write(f.readline())
|
||||
"""
|
||||
)
|
||||
}
|
||||
)
|
||||
def run_head(tmp_dir, head_script, dvc):
|
||||
script = os.path.abspath(tmp_dir / "head.py")
|
||||
|
||||
def run(*args, **run_kwargs):
|
||||
|
||||
@@ -13,7 +13,7 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.scm import resolve_rev
|
||||
from dvc.stage.exceptions import StageFileDoesNotExistError
|
||||
from dvc.utils.serialize import PythonFileCorruptedError
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
from tests.scripts import COPY_SCRIPT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [None, "foo"])
|
||||
@@ -209,8 +209,7 @@ def test_get_baseline(tmp_dir, scm, dvc, exp_stage):
|
||||
assert dvc.experiments.get_baseline(f"{CELERY_STASH}@{{1}}") == init_rev
|
||||
|
||||
|
||||
def test_update_py_params(tmp_dir, scm, dvc, test_queue):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_update_py_params(tmp_dir, scm, dvc, test_queue, copy_script):
|
||||
tmp_dir.gen("params.py", "INT = 1\n")
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py params.py metrics.py",
|
||||
@@ -371,8 +370,7 @@ def test_no_scm(tmp_dir):
|
||||
getattr(dvc.experiments, cmd)()
|
||||
|
||||
|
||||
def test_untracked(tmp_dir, scm, dvc, caplog, workspace):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_untracked(tmp_dir, scm, dvc, caplog, workspace, copy_script):
|
||||
tmp_dir.scm_gen("params.yaml", "foo: 1", commit="track params")
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py params.yaml metrics.yaml",
|
||||
@@ -419,7 +417,7 @@ def test_packed_args_exists(tmp_dir, scm, dvc, exp_stage, caplog):
|
||||
assert "Temporary DVC file" in caplog.text
|
||||
|
||||
|
||||
def test_list(tmp_dir, scm, dvc, exp_stage):
|
||||
def _prepare_experiments(tmp_dir, scm, dvc, exp_stage):
|
||||
baseline_a = scm.get_rev()
|
||||
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp_a = first(results)
|
||||
@@ -435,6 +433,18 @@ def test_list(tmp_dir, scm, dvc, exp_stage):
|
||||
exp_c = first(results)
|
||||
ref_info_c = first(exp_refs_by_rev(scm, exp_c))
|
||||
|
||||
return baseline_a, baseline_c, ref_info_a, ref_info_b, ref_info_c
|
||||
|
||||
|
||||
def test_list(tmp_dir, scm, dvc, exp_stage):
|
||||
(
|
||||
baseline_a,
|
||||
baseline_c,
|
||||
ref_info_a,
|
||||
ref_info_b,
|
||||
ref_info_c,
|
||||
) = _prepare_experiments(tmp_dir, scm, dvc, exp_stage)
|
||||
|
||||
assert dvc.experiments.ls() == {baseline_c: [ref_info_c.name]}
|
||||
|
||||
exp_list = dvc.experiments.ls(rev=ref_info_a.baseline_sha)
|
||||
@@ -449,6 +459,25 @@ def test_list(tmp_dir, scm, dvc, exp_stage):
|
||||
}
|
||||
|
||||
|
||||
def test_list_cli(tmp_dir, scm, dvc, capsys, exp_stage):
|
||||
from dvc.cli import main
|
||||
|
||||
baseline_a, _, ref_info_a, ref_info_b, ref_info_c = _prepare_experiments(
|
||||
tmp_dir, scm, dvc, exp_stage
|
||||
)
|
||||
|
||||
# Make sure that we prioritize the current branch name
|
||||
scm.checkout("branch", True)
|
||||
|
||||
capsys.readouterr()
|
||||
assert main(["exp", "list", "-A"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert set(cap.out.split()) == set(
|
||||
["branch:", baseline_a[:7] + ":"]
|
||||
+ [ref_info_a.name, ref_info_b.name, ref_info_c.name]
|
||||
)
|
||||
|
||||
|
||||
def test_subdir(tmp_dir, scm, dvc, workspace):
|
||||
subdir = tmp_dir / "dir"
|
||||
subdir.gen("copy.py", COPY_SCRIPT)
|
||||
@@ -538,7 +567,7 @@ def test_run_celery(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
repro_spy = mocker.spy(dvc.experiments, "reproduce_celery")
|
||||
results = dvc.experiments.run(run_all=True)
|
||||
assert len(results) == 2
|
||||
repro_spy.assert_called_once_with(entries=mocker.ANY, jobs=1)
|
||||
repro_spy.assert_called_once_with(jobs=1)
|
||||
|
||||
expected = {"foo: 2", "foo: 3"}
|
||||
metrics = set()
|
||||
@@ -600,9 +629,10 @@ def test_fix_exp_head(tmp_dir, scm, tail):
|
||||
"params, target",
|
||||
itertools.product(("foo: 1", "foo: 2"), (True, False)),
|
||||
)
|
||||
def test_modified_data_dep(tmp_dir, scm, dvc, workspace, params, target):
|
||||
def test_modified_data_dep(
|
||||
tmp_dir, scm, dvc, workspace, params, target, copy_script
|
||||
):
|
||||
tmp_dir.dvc_gen("data", "data")
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
exp_stage = dvc.run(
|
||||
cmd="python copy.py params.yaml metrics.yaml",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import pytest
|
||||
from funcy import first
|
||||
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 1)])
|
||||
def test_workspace(tmp_dir, scm, dvc, queued, expected):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_workspace(tmp_dir, scm, dvc, queued, expected, copy_script):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
stage = dvc.run(
|
||||
@@ -26,8 +23,7 @@ def test_workspace(tmp_dir, scm, dvc, queued, expected):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 2)])
|
||||
def test_all_commits(tmp_dir, scm, dvc, queued, expected):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_all_commits(tmp_dir, scm, dvc, queued, expected, copy_script):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
stage = dvc.run(
|
||||
@@ -55,8 +51,7 @@ def test_all_commits(tmp_dir, scm, dvc, queued, expected):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
|
||||
def test_all_branches(tmp_dir, scm, dvc, queued, expected):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_all_branches(tmp_dir, scm, dvc, queued, expected, copy_script):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
stage = dvc.run(
|
||||
@@ -91,8 +86,7 @@ def test_all_branches(tmp_dir, scm, dvc, queued, expected):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
|
||||
def test_all_tags(tmp_dir, scm, dvc, queued, expected):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_all_tags(tmp_dir, scm, dvc, queued, expected, copy_script):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
stage = dvc.run(
|
||||
|
||||
@@ -345,11 +345,13 @@ def test_init_interactive_live(
|
||||
"train": {
|
||||
"cmd": "python script.py",
|
||||
"deps": ["data", "script.py"],
|
||||
"metrics": [{"dvclive.json": {"cache": False}}],
|
||||
"metrics": [
|
||||
{os.path.join("dvclive", "metrics.json"): {"cache": False}}
|
||||
],
|
||||
"outs": ["models"],
|
||||
"params": [{"params.yaml": None}],
|
||||
"plots": [
|
||||
{os.path.join("dvclive", "scalars"): {"cache": False}}
|
||||
{os.path.join("dvclive", "plots"): {"cache": False}}
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -434,12 +436,12 @@ def test_init_with_type_default_and_live_provided(
|
||||
"cmd": "cmd",
|
||||
"deps": ["data", "src"],
|
||||
"metrics": [
|
||||
{"live.json": {"cache": False}},
|
||||
{os.path.join("live", "metrics.json"): {"cache": False}},
|
||||
],
|
||||
"outs": ["models"],
|
||||
"params": [{"params.yaml": None}],
|
||||
"plots": [
|
||||
{os.path.join("live", "scalars"): {"cache": False}},
|
||||
{os.path.join("live", "plots"): {"cache": False}},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -477,13 +479,13 @@ def test_init_with_live_and_metrics_plots_provided(
|
||||
"cmd": "cmd",
|
||||
"deps": ["data", "src"],
|
||||
"metrics": [
|
||||
{"live.json": {"cache": False}},
|
||||
{os.path.join("live", "metrics.json"): {"cache": False}},
|
||||
{"metrics.json": {"cache": False}},
|
||||
],
|
||||
"outs": ["models"],
|
||||
"params": [{"params.yaml": None}],
|
||||
"plots": [
|
||||
{os.path.join("live", "scalars"): {"cache": False}},
|
||||
{os.path.join("live", "plots"): {"cache": False}},
|
||||
{"plots": {"cache": False}},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -11,64 +11,54 @@ def to_dict(tasks):
|
||||
return status_dict
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def success_tasks(tmp_dir, dvc, scm, test_queue, exp_stage):
|
||||
queue_length = 3
|
||||
name_list = []
|
||||
for i in range(queue_length):
|
||||
name = f"success{i}"
|
||||
name_list.append(name)
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
return name_list
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
|
||||
queue_length = 3
|
||||
name_list = []
|
||||
for i in range(queue_length):
|
||||
name = f"failed{i}"
|
||||
name_list.append(name)
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing,
|
||||
params=[f"foo={i+queue_length}"],
|
||||
queue=True,
|
||||
name=name,
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
return name_list
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
@pytest.mark.parametrize("follow", [True, False])
|
||||
def test_celery_logs(
|
||||
tmp_dir,
|
||||
scm,
|
||||
dvc,
|
||||
failed_exp_stage,
|
||||
test_queue,
|
||||
follow,
|
||||
capsys,
|
||||
):
|
||||
celery_queue = dvc.experiments.celery_queue
|
||||
dvc.experiments.run(failed_exp_stage.addressing, queue=True)
|
||||
dvc.experiments.run(run_all=True)
|
||||
|
||||
queue = dvc.experiments.celery_queue
|
||||
done_result = first(queue.iter_done())
|
||||
done_result = first(celery_queue.iter_done())
|
||||
|
||||
name = done_result.entry.stash_rev
|
||||
captured = capsys.readouterr()
|
||||
queue.logs(name, follow=follow)
|
||||
celery_queue.logs(name, follow=follow)
|
||||
captured = capsys.readouterr()
|
||||
assert "failed to reproduce 'failed-copy-file'" in captured.out
|
||||
|
||||
|
||||
@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())
|
||||
def test_queue_remove_done(
|
||||
dvc,
|
||||
exp_stage,
|
||||
failed_exp_stage,
|
||||
):
|
||||
queue_length = 3
|
||||
success_tasks = []
|
||||
failed_tasks = []
|
||||
celery_queue = dvc.experiments.celery_queue
|
||||
for i in range(queue_length):
|
||||
name = f"success{i}"
|
||||
success_tasks.append(name)
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
|
||||
)
|
||||
name_fail = f"failed{i}"
|
||||
failed_tasks.append(name_fail)
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing,
|
||||
params=[f"foo={i+queue_length}"],
|
||||
queue=True,
|
||||
name=name_fail,
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
assert len(celery_queue.failed_stash) == 3
|
||||
status = to_dict(celery_queue.status())
|
||||
assert len(status) == 6
|
||||
for name in failed_tasks:
|
||||
assert status[name] == "Failed"
|
||||
@@ -76,27 +66,21 @@ def test_queue_remove_done(dvc, failed_tasks, success_tasks):
|
||||
assert status[name] == "Success"
|
||||
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.celery_queue.remove(failed_tasks[:2] + ["non-exist"])
|
||||
assert len(dvc.experiments.celery_queue.status()) == 6
|
||||
celery_queue.remove(failed_tasks[:2] + ["non-exist"])
|
||||
assert len(celery_queue.status()) == 6
|
||||
|
||||
to_remove = [failed_tasks[0], success_tasks[2]]
|
||||
assert set(dvc.experiments.celery_queue.remove(to_remove)) == set(
|
||||
to_remove
|
||||
)
|
||||
assert set(celery_queue.remove(to_remove)) == set(to_remove)
|
||||
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 2
|
||||
status = to_dict(dvc.experiments.celery_queue.status())
|
||||
assert len(celery_queue.failed_stash) == 2
|
||||
status = to_dict(celery_queue.status())
|
||||
assert set(status) == set(failed_tasks[1:] + success_tasks[:2])
|
||||
|
||||
assert dvc.experiments.celery_queue.clear(failed=True) == failed_tasks[1:]
|
||||
assert set(celery_queue.clear(failed=True)) == set(failed_tasks[1:])
|
||||
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 0
|
||||
assert set(to_dict(dvc.experiments.celery_queue.status())) == set(
|
||||
success_tasks[:2]
|
||||
)
|
||||
assert len(celery_queue.failed_stash) == 0
|
||||
assert set(to_dict(celery_queue.status())) == set(success_tasks[:2])
|
||||
|
||||
assert (
|
||||
dvc.experiments.celery_queue.clear(success=True) == success_tasks[:2]
|
||||
)
|
||||
assert set(celery_queue.clear(success=True)) == set(success_tasks[:2])
|
||||
|
||||
assert dvc.experiments.celery_queue.status() == []
|
||||
assert celery_queue.status() == []
|
||||
|
||||
@@ -110,6 +110,11 @@ def test_hydra_compose_and_dump(
|
||||
["foo=bar,baz"],
|
||||
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
|
||||
),
|
||||
(
|
||||
False,
|
||||
[],
|
||||
[{}],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hydra_sweep(
|
||||
@@ -139,5 +144,18 @@ def test_hydra_sweep(
|
||||
|
||||
|
||||
def test_hydra_sweep_requires_queue(params_repo, dvc):
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
with pytest.raises(
|
||||
InvalidArgumentError,
|
||||
match="Sweep overrides can't be used without `--queue`",
|
||||
):
|
||||
dvc.experiments.run(params=["db=mysql,postgresql"])
|
||||
|
||||
|
||||
def test_hydra_sweep_cant_use_name(tmp_dir, params_repo, dvc):
|
||||
with pytest.raises(
|
||||
InvalidArgumentError,
|
||||
match="Sweep overrides can't be used alongside `--name`",
|
||||
):
|
||||
dvc.experiments.run(
|
||||
params=["db=mysql,postgresql"], queue=True, name="foo"
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
from funcy import first, get_in
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.cli import main
|
||||
from dvc.repo.experiments.executor.base import (
|
||||
@@ -20,7 +22,6 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import makedirs
|
||||
from dvc.utils.serialize import YAMLFileCorruptedError
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
|
||||
LOCK_CONTENTS = {
|
||||
"read": {
|
||||
@@ -173,7 +174,6 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage):
|
||||
baseline_rev = scm.get_rev()
|
||||
timestamp = datetime.fromtimestamp(
|
||||
@@ -313,10 +313,10 @@ def test_show_filter(
|
||||
scm,
|
||||
dvc,
|
||||
capsys,
|
||||
copy_script,
|
||||
):
|
||||
capsys.readouterr()
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -646,6 +646,9 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
rev1 = first(result1)
|
||||
ref_info1 = first(exp_refs_by_rev(scm, rev1))
|
||||
|
||||
# at least 1 second gap between these experiments to make sure
|
||||
# the previous experiment to be regarded as branch_base
|
||||
time.sleep(1)
|
||||
result2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
rev2 = first(result2)
|
||||
@@ -679,8 +682,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_show_only_changed(tmp_dir, dvc, scm, capsys):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_show_only_changed(tmp_dir, dvc, scm, capsys, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -726,13 +728,14 @@ def test_show_only_changed(tmp_dir, dvc, scm, capsys):
|
||||
assert "metrics.yaml:bar" in cap.out
|
||||
|
||||
|
||||
def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
|
||||
def test_show_parallel_coordinates(
|
||||
tmp_dir, dvc, scm, mocker, capsys, copy_script
|
||||
):
|
||||
from dvc.commands.experiments import show
|
||||
|
||||
webbroser_open = mocker.patch("webbrowser.open")
|
||||
show_experiments = mocker.spy(show, "show_experiments")
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -807,8 +810,7 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_show_outs(tmp_dir, dvc, scm, erepo_dir, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -889,8 +891,7 @@ def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
|
||||
}
|
||||
|
||||
|
||||
def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_metrics_renaming(tmp_dir, dvc, scm, capsys, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -975,3 +976,122 @@ def test_show_sorted_deps(tmp_dir, dvc, scm, capsys):
|
||||
assert main(["exp", "show", "--csv"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert "a,b,c,z" in cap.out
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_queued_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=["foo=2"], queue=True, name="test_name"
|
||||
)
|
||||
exp_rev_2 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
|
||||
dvc.experiments.run(exp_stage.addressing, params=["foo=3"], queue=True)
|
||||
exp_rev_3 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == exp_rev_3:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == 2
|
||||
exp_2 = results[exp_rev_2]["data"]
|
||||
assert exp_2["status"] == "Queued"
|
||||
assert exp_2["params"]["params.yaml"] == {"data": {"foo": 2}}
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_completed_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
|
||||
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp_rev_2 = first(result_2)
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
result_3 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
exp_rev_3 = first(result_3)
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == exp_rev_3:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
experiments = dvc.experiments.show()[baseline_rev]
|
||||
assert len(experiments) == 2
|
||||
assert exp_rev_2 in experiments
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_checkpoint_error(tmp_dir, scm, dvc, checkpoint_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
results = dvc.experiments.run(
|
||||
checkpoint_stage.addressing, params=["foo=2"]
|
||||
)
|
||||
exp_rev = first(results)
|
||||
exp_ref = str(first(exp_refs_by_rev(scm, exp_rev)))
|
||||
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == checkpoint_stage.iterations + 1
|
||||
|
||||
checkpoints = {}
|
||||
for rev in results:
|
||||
if rev != "baseline":
|
||||
checkpoints[rev] = scm.resolve_commit(rev)
|
||||
checkpoints[exp_ref] = scm.resolve_commit(exp_ref)
|
||||
checkpoints[baseline_rev] = scm.resolve_commit(baseline_rev)
|
||||
|
||||
failed_rev = random.choice(list(checkpoints.keys()))
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == failed_rev:
|
||||
raise SCMError
|
||||
return checkpoints[rev]
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == 1
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_baseline_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
branch = scm.active_branch()
|
||||
|
||||
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp_rev_2 = first(result_2)
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == baseline_rev:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
experiments = dvc.experiments.show()[baseline_rev]
|
||||
assert len(experiments) == 1
|
||||
assert experiments["baseline"]["data"] == {"name": branch}
|
||||
assert isinstance(experiments["baseline"]["error"], SCMError)
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from dvc.dependency import _merge_params
|
||||
from dvc.parsing import DEFAULT_PARAMS_FILE, DataResolver
|
||||
from dvc.parsing.context import recurse_not_a_node
|
||||
from dvc.parsing.interpolate import escape_str
|
||||
|
||||
from . import (
|
||||
CONTEXT_DATA,
|
||||
@@ -272,14 +273,17 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
|
||||
if list_config:
|
||||
conf["parsing"]["list"] = list_config
|
||||
|
||||
string = "spaced string"
|
||||
mixed_quote_string = "quote\"'d"
|
||||
data = {
|
||||
"dict": {
|
||||
"foo": "foo",
|
||||
"bar": 2,
|
||||
"string": "spaced string",
|
||||
"string": string,
|
||||
"mixed_quote_string": mixed_quote_string,
|
||||
"bool": True,
|
||||
"bool-false": False,
|
||||
"list": [1, 2, "foo"],
|
||||
"list": [1, 2, "foo", mixed_quote_string],
|
||||
"nested": {"foo": "foo"},
|
||||
}
|
||||
}
|
||||
@@ -296,9 +300,10 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
|
||||
bool_resolved = " --bool --no-bool-false"
|
||||
|
||||
if list_config is None or list_config == "nargs":
|
||||
list_resolved = " --list 1 2 'foo'"
|
||||
list_resolved = f" --list 1 2 foo {escape_str(mixed_quote_string)}"
|
||||
else:
|
||||
list_resolved = " --list 1 --list 2 --list 'foo'"
|
||||
list_resolved = " --list 1 --list 2 --list foo"
|
||||
list_resolved += f" --list {escape_str(mixed_quote_string)}"
|
||||
|
||||
assert_stage_equal(
|
||||
resolver.resolve(),
|
||||
@@ -306,11 +311,12 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
|
||||
"stages": {
|
||||
"stage1": {
|
||||
"cmd": "python script.py"
|
||||
" --foo 'foo' --bar 2"
|
||||
" --string 'spaced string'"
|
||||
" --foo foo --bar 2"
|
||||
f" --string {escape_str(string)}"
|
||||
f" --mixed_quote_string {escape_str(mixed_quote_string)}"
|
||||
f"{bool_resolved}"
|
||||
f"{list_resolved}"
|
||||
" --nested.foo 'foo'"
|
||||
" --nested.foo foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -257,16 +257,21 @@ def test_log_errors(
|
||||
)
|
||||
|
||||
|
||||
def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
|
||||
with open("image.jpg", "wb") as fd:
|
||||
@pytest.mark.parametrize("ext", ["jpg", "svg"])
|
||||
def test_plots_binary(
|
||||
tmp_dir, scm, dvc, run_copy_metrics, custom_template, ext
|
||||
):
|
||||
file1 = f"image.{ext}"
|
||||
file2 = f"plot.{ext}"
|
||||
with open(file1, "wb") as fd:
|
||||
fd.write(b"content")
|
||||
|
||||
dvc.add(["image.jpg"])
|
||||
dvc.add([file1])
|
||||
run_copy_metrics(
|
||||
"image.jpg",
|
||||
"plot.jpg",
|
||||
file1,
|
||||
file2,
|
||||
commit="run training",
|
||||
plots=["plot.jpg"],
|
||||
plots=[file2],
|
||||
name="s2",
|
||||
single_stage=False,
|
||||
)
|
||||
@@ -276,12 +281,12 @@ def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
|
||||
|
||||
scm.tag("v1")
|
||||
|
||||
with open("plot.jpg", "wb") as fd:
|
||||
with open(file2, "wb") as fd:
|
||||
fd.write(b"content2")
|
||||
|
||||
result = dvc.plots.show(revs=["v1", "workspace"])
|
||||
assert get_plot(result, "v1", file="plot.jpg") == b"content"
|
||||
assert get_plot(result, "workspace", file="plot.jpg") == b"content2"
|
||||
assert get_plot(result, "v1", file=file2) == b"content"
|
||||
assert get_plot(result, "workspace", file=file2) == b"content2"
|
||||
|
||||
|
||||
def test_collect_non_existing_dir(tmp_dir, dvc, run_copy_metrics):
|
||||
@@ -331,16 +336,15 @@ def test_collect_non_existing_dir(tmp_dir, dvc, run_copy_metrics):
|
||||
@pytest.mark.parametrize(
|
||||
"plot_config,expected_datafiles",
|
||||
[
|
||||
# TODO - enable providing data files for x
|
||||
# (
|
||||
# {
|
||||
# "comparison": {
|
||||
# "x": {"data1.json": "a"},
|
||||
# "y": {"sub/dir/data2.json": "b"},
|
||||
# }
|
||||
# },
|
||||
# ["data1.json", os.path.join("sub", "dir", "data2.json")],
|
||||
# ),
|
||||
(
|
||||
{
|
||||
"comparison": {
|
||||
"x": {"data1.json": "a"},
|
||||
"y": {"sub/dir/data2.json": "b"},
|
||||
}
|
||||
},
|
||||
["data1.json", os.path.join("sub", "dir", "data2.json")],
|
||||
),
|
||||
(
|
||||
{"data1.json": {"x": "c", "y": "a", "title": "File as key test"}},
|
||||
["data1.json"],
|
||||
|
||||
+145
-194
@@ -4,8 +4,7 @@ import os
|
||||
import shutil
|
||||
import stat
|
||||
import textwrap
|
||||
import time
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import call
|
||||
|
||||
import colorama
|
||||
import pytest
|
||||
@@ -35,12 +34,11 @@ from dvc.stage.exceptions import (
|
||||
StagePathNotFoundError,
|
||||
)
|
||||
from dvc.testing.workspace_tests import TestAdd
|
||||
from dvc.utils import LARGE_DIR_SIZE, relpath
|
||||
from dvc.utils import LARGE_DIR_SIZE
|
||||
from dvc.utils.fs import path_isin
|
||||
from dvc.utils.serialize import YAMLFileCorruptedError, load_yaml
|
||||
from dvc.utils.serialize import YAMLFileCorruptedError
|
||||
from dvc_data.hashfile.hash import file_md5
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.utils import get_gitignore_content
|
||||
|
||||
|
||||
@@ -120,49 +118,38 @@ def test_add_directory(tmp_dir, dvc):
|
||||
assert "\\" not in part
|
||||
|
||||
|
||||
class TestAddDirectoryRecursive(TestDvc):
|
||||
def test(self):
|
||||
stages = self.dvc.add(self.DATA_DIR, recursive=True)
|
||||
self.assertEqual(len(stages), 2)
|
||||
def test_add_directory_recursive(tmp_dir, dvc):
|
||||
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
|
||||
stages = dvc.add("data", recursive=True)
|
||||
assert len(stages) == 2
|
||||
|
||||
|
||||
class TestAddCmdDirectoryRecursive(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", "--recursive", self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_warn_about_large_directories(self):
|
||||
warning = (
|
||||
"You are adding a large directory 'large-dir' recursively."
|
||||
"\nConsider tracking it as a whole instead with "
|
||||
"`{cyan}dvc add large-dir{nc}`"
|
||||
).format(
|
||||
cyan=colorama.Fore.CYAN,
|
||||
nc=colorama.Style.RESET_ALL,
|
||||
)
|
||||
|
||||
os.mkdir("large-dir")
|
||||
|
||||
# Create a lot of files
|
||||
for iteration in range(LARGE_DIR_SIZE + 1):
|
||||
path = os.path.join("large-dir", str(iteration))
|
||||
with open(path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(path)
|
||||
|
||||
assert main(["add", "--recursive", "large-dir"]) == 0
|
||||
assert warning in self._capsys.readouterr()[1]
|
||||
def test_add_cmd_directory_recursive(tmp_dir, dvc):
|
||||
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
|
||||
assert main(["add", "--recursive", "data"]) == 0
|
||||
|
||||
|
||||
class TestAddDirectoryWithForwardSlash(TestDvc):
|
||||
def test(self):
|
||||
dname = "directory/"
|
||||
os.mkdir(dname)
|
||||
self.create(os.path.join(dname, "file"), "file")
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
self.assertEqual(os.path.abspath("directory.dvc"), stage.path)
|
||||
def test_warn_about_large_directories_recursive_add(tmp_dir, dvc, capsys):
|
||||
warning = (
|
||||
"You are adding a large directory 'large-dir' recursively."
|
||||
"\nConsider tracking it as a whole instead with "
|
||||
"`{cyan}dvc add large-dir{nc}`"
|
||||
).format(
|
||||
cyan=colorama.Fore.CYAN,
|
||||
nc=colorama.Style.RESET_ALL,
|
||||
)
|
||||
|
||||
tmp_dir.gen(
|
||||
"large-dir", {f"{i}": f"{i}" for i in range(LARGE_DIR_SIZE + 1)}
|
||||
)
|
||||
assert main(["add", "--recursive", "large-dir"]) == 0
|
||||
assert warning in capsys.readouterr()[1]
|
||||
|
||||
|
||||
def test_add_directory_with_forward_slash(tmp_dir, dvc):
|
||||
tmp_dir.gen("directory", {"file": "file"})
|
||||
(stage,) = dvc.add("directory/")
|
||||
assert stage.relpath == "directory.dvc"
|
||||
|
||||
|
||||
def test_add_tracked_file(tmp_dir, scm, dvc):
|
||||
@@ -178,33 +165,23 @@ def test_add_tracked_file(tmp_dir, scm, dvc):
|
||||
dvc.add(path)
|
||||
|
||||
|
||||
class TestAddDirWithExistingCache(TestDvc):
|
||||
def test(self):
|
||||
dname = "a"
|
||||
fname = os.path.join(dname, "b")
|
||||
os.mkdir(dname)
|
||||
shutil.copyfile(self.FOO, fname)
|
||||
def test_add_dir_with_existing_cache(tmp_dir, dvc):
|
||||
tmp_dir.gen({"foo": "foo", "dir": {"file": "foo"}})
|
||||
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
(stage,) = dvc.add("foo")
|
||||
assert stage is not None
|
||||
(stage,) = dvc.add("dir")
|
||||
assert stage is not None
|
||||
|
||||
|
||||
class TestAddModifiedDir(TestDvc):
|
||||
def test(self):
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
os.unlink(self.DATA)
|
||||
def test_add_modified_dir(tmp_dir, dvc):
|
||||
tmp_dir.gen("data", {"foo": "foo", "sub": {"bar": "bar"}})
|
||||
(stage,) = dvc.add("data")
|
||||
assert stage is not None
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
(tmp_dir / "data" / "foo").unlink()
|
||||
(stage,) = dvc.add("data")
|
||||
assert stage is not None
|
||||
|
||||
|
||||
def test_add_file_in_dir(tmp_dir, dvc):
|
||||
@@ -326,57 +303,47 @@ def test_add_external_relpath(tmp_dir, dvc, local_cloud):
|
||||
assert dvc.status() == {}
|
||||
|
||||
|
||||
class TestAddLocalRemoteFile(TestDvc):
|
||||
def test(self):
|
||||
"""
|
||||
Making sure that 'remote' syntax is handled properly for local outs.
|
||||
"""
|
||||
cwd = os.getcwd()
|
||||
remote = "myremote"
|
||||
def test_add_local_remote_file(tmp_dir, dvc):
|
||||
"""
|
||||
Making sure that 'remote' syntax is handled properly for local outs.
|
||||
"""
|
||||
tmp_dir.gen({"foo": "foo", "bar": "bar"})
|
||||
tmp_dir.add_remote(url=tmp_dir.fs_path, name="myremote")
|
||||
|
||||
ret = main(["remote", "add", remote, cwd])
|
||||
self.assertEqual(ret, 0)
|
||||
assert main(["add", "remote://myremote/foo"]) == 0
|
||||
d = (tmp_dir / "foo.dvc").load_yaml()
|
||||
assert d["outs"][0]["path"] == "remote://myremote/foo"
|
||||
|
||||
self.dvc.config.load()
|
||||
|
||||
foo = f"remote://{remote}/{self.FOO}"
|
||||
ret = main(["add", foo])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
d = load_yaml("foo.dvc")
|
||||
self.assertEqual(d["outs"][0]["path"], foo)
|
||||
|
||||
bar = os.path.join(cwd, self.BAR)
|
||||
ret = main(["add", bar])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
d = load_yaml("bar.dvc")
|
||||
self.assertEqual(d["outs"][0]["path"], self.BAR)
|
||||
assert main(["add", (tmp_dir / "bar").fs_path]) == 0
|
||||
d = (tmp_dir / "bar.dvc").load_yaml()
|
||||
assert d["outs"][0]["path"] == "bar"
|
||||
|
||||
|
||||
class TestCmdAdd(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_cmd_add(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", "non-existing-file"])
|
||||
self.assertNotEqual(ret, 0)
|
||||
ret = main(["add", "non-existing-file"])
|
||||
assert ret != 0
|
||||
|
||||
|
||||
class TestDoubleAddUnchanged(TestDvc):
|
||||
def test_file(self):
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_double_add_unchanged_file(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
def test_dir(self):
|
||||
ret = main(["add", self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(["add", self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_double_add_unchanged_dir(tmp_dir, dvc):
|
||||
tmp_dir.gen("data", {"foo": "foo"})
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
|
||||
|
||||
def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
|
||||
@@ -443,19 +410,17 @@ def test_should_update_state_entry_for_directory_after_add(
|
||||
assert file_md5_counter.mock.call_count == 10
|
||||
|
||||
|
||||
class TestAddCommit(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.FOO, "--no-commit"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.FOO))
|
||||
self.assertFalse(os.path.exists(self.dvc.odb.local.path))
|
||||
def test_add_commit(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["add", "foo", "--no-commit"])
|
||||
assert ret == 0
|
||||
assert os.path.isfile("foo")
|
||||
assert not os.path.exists(dvc.odb.local.path)
|
||||
|
||||
ret = main(["commit", self.FOO + ".dvc"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.FOO))
|
||||
self.assertTrue(
|
||||
self.dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
|
||||
)
|
||||
ret = main(["commit", "foo" + ".dvc"])
|
||||
assert ret == 0
|
||||
assert os.path.isfile("foo")
|
||||
assert dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
|
||||
|
||||
|
||||
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
@@ -474,80 +439,66 @@ def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
assert counter.mock.call_count == 5
|
||||
|
||||
|
||||
class TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink(TestDvc):
|
||||
def test(self):
|
||||
def is_symlink_true_below_dvc_root(path):
|
||||
if path == os.path.dirname(self.dvc.root_dir):
|
||||
return True
|
||||
return False
|
||||
def test_should_place_stage_in_data_dir_if_repository_below_symlink(
|
||||
mocker, tmp_dir, dvc
|
||||
):
|
||||
def is_symlink_true_below_dvc_root(path):
|
||||
return path == os.path.dirname(dvc.root_dir)
|
||||
|
||||
with patch.object(
|
||||
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
|
||||
):
|
||||
tmp_dir.gen({"data": {"foo": "foo"}})
|
||||
mocker.patch.object(
|
||||
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
|
||||
)
|
||||
ret = main(["add", os.path.join("data", "foo")])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", self.DATA])
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
stage_file_path_on_data_below_symlink = (
|
||||
os.path.basename(self.DATA) + DVC_FILE_SUFFIX
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.exists(stage_file_path_on_data_below_symlink)
|
||||
)
|
||||
|
||||
stage_file_path = self.DATA + DVC_FILE_SUFFIX
|
||||
self.assertTrue(os.path.exists(stage_file_path))
|
||||
assert not (tmp_dir / "foo.dvc").exists()
|
||||
assert (tmp_dir / "data" / "foo.dvc").exists()
|
||||
|
||||
|
||||
class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.FOO])
|
||||
assert 0 == ret
|
||||
def test_should_throw_proper_exception_on_corrupted_stage_file(
|
||||
caplog, tmp_dir, dvc
|
||||
):
|
||||
tmp_dir.gen({"foo": "foo", "bar": " bar"})
|
||||
assert main(["add", "foo"]) == 0
|
||||
|
||||
foo_stage = relpath(self.FOO + DVC_FILE_SUFFIX)
|
||||
with (tmp_dir / "foo.dvc").open("a+") as f:
|
||||
f.write("this will break yaml file structure")
|
||||
|
||||
# corrupt stage file
|
||||
with open(foo_stage, "a+", encoding="utf-8") as file:
|
||||
file.write("this will break yaml file structure")
|
||||
|
||||
self._caplog.clear()
|
||||
|
||||
ret = main(["add", self.BAR])
|
||||
assert 1 == ret
|
||||
|
||||
expected_error = (
|
||||
f"unable to read: '{foo_stage}', YAML file structure is corrupted"
|
||||
)
|
||||
|
||||
assert expected_error in self._caplog.text
|
||||
caplog.clear()
|
||||
assert main(["add", "bar"]) == 1
|
||||
expected_error = (
|
||||
"unable to read: 'foo.dvc', YAML file structure is corrupted"
|
||||
)
|
||||
assert expected_error in caplog.text
|
||||
|
||||
|
||||
class TestAddFilename(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.FOO, self.BAR, "--file", "error.dvc"])
|
||||
self.assertNotEqual(0, ret)
|
||||
def test_add_filename(tmp_dir, dvc):
|
||||
tmp_dir.gen({"foo": "foo", "bar": "bar", "data": {"file": "file"}})
|
||||
ret = main(["add", "foo", "bar", "--file", "error.dvc"])
|
||||
assert ret != 0
|
||||
|
||||
ret = main(["add", "-R", self.DATA_DIR, "--file", "error.dvc"])
|
||||
self.assertNotEqual(0, ret)
|
||||
ret = main(["add", "-R", "data", "--file", "error.dvc"])
|
||||
assert ret != 0
|
||||
|
||||
with self.assertRaises(RecursiveAddingWhileUsingFilename):
|
||||
self.dvc.add(self.DATA_DIR, recursive=True, fname="error.dvc")
|
||||
with pytest.raises(RecursiveAddingWhileUsingFilename):
|
||||
dvc.add("data", recursive=True, fname="error.dvc")
|
||||
|
||||
ret = main(["add", self.DATA_DIR, "--file", "data_directory.dvc"])
|
||||
self.assertEqual(0, ret)
|
||||
self.assertTrue(os.path.exists("data_directory.dvc"))
|
||||
ret = main(["add", "data", "--file", "data_directory.dvc"])
|
||||
assert ret == 0
|
||||
assert (tmp_dir / "data_directory.dvc").exists()
|
||||
|
||||
ret = main(["add", self.FOO, "--file", "bar.dvc"])
|
||||
self.assertEqual(0, ret)
|
||||
self.assertTrue(os.path.exists("bar.dvc"))
|
||||
self.assertFalse(os.path.exists("foo.dvc"))
|
||||
ret = main(["add", "foo", "--file", "bar.dvc"])
|
||||
assert ret == 0
|
||||
assert (tmp_dir / "bar.dvc").exists()
|
||||
assert not (tmp_dir / "foo.dvc").exists()
|
||||
|
||||
os.remove("bar.dvc")
|
||||
(tmp_dir / "bar.dvc").unlink()
|
||||
|
||||
ret = main(["add", self.FOO, "--file", "bar.dvc"])
|
||||
self.assertEqual(0, ret)
|
||||
self.assertTrue(os.path.exists("bar.dvc"))
|
||||
self.assertFalse(os.path.exists("foo.dvc"))
|
||||
ret = main(["add", "foo", "--file", "bar.dvc"])
|
||||
assert ret == 0
|
||||
assert (tmp_dir / "bar.dvc").exists()
|
||||
assert not (tmp_dir / "foo.dvc").exists()
|
||||
|
||||
|
||||
def test_failed_add_cleanup(tmp_dir, scm, dvc):
|
||||
@@ -577,25 +528,25 @@ def test_should_not_track_git_internal_files(mocker, dvc, tmp_dir):
|
||||
assert ".git" not in fname
|
||||
|
||||
|
||||
class TestAddUnprotected(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["config", "cache.type", "hardlink"])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_add_unprotected(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["config", "cache.type", "hardlink"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
self.assertFalse(os.access(self.FOO, os.W_OK))
|
||||
self.assertTrue(system.is_hardlink(self.FOO))
|
||||
assert not os.access("foo", os.W_OK)
|
||||
assert system.is_hardlink("foo")
|
||||
|
||||
ret = main(["unprotect", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["unprotect", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
self.assertFalse(os.access(self.FOO, os.W_OK))
|
||||
self.assertTrue(system.is_hardlink(self.FOO))
|
||||
assert not os.access("foo", os.W_OK)
|
||||
assert system.is_hardlink("foo")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -12,7 +12,7 @@ from tests.utils import ANY
|
||||
def test_daemon_analytics(mocker, tmp_path):
|
||||
mock_send = mocker.patch("dvc.analytics.send")
|
||||
report = os.fspath(tmp_path)
|
||||
assert 0 == main(["daemon", "analytics", report])
|
||||
assert main(["daemon", "analytics", report]) == 0
|
||||
|
||||
mock_send.assert_called_with(report)
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_main_analytics(mocker, tmp_dir, dvc):
|
||||
mock_is_enabled = mocker.patch("dvc.analytics.collect_and_send_report")
|
||||
mock_report = mocker.patch("dvc.analytics.is_enabled", return_value=True)
|
||||
tmp_dir.gen("foo", "text")
|
||||
assert 0 == main(["add", "foo"])
|
||||
assert main(["add", "foo"]) == 0
|
||||
assert mock_is_enabled.called
|
||||
assert mock_report.called
|
||||
|
||||
|
||||
+305
-409
@@ -1,16 +1,13 @@
|
||||
import collections
|
||||
import filecmp
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import textwrap
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.cli import main
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX, PIPELINE_FILE, Dvcfile
|
||||
from dvc.dvcfile import PIPELINE_FILE, Dvcfile
|
||||
from dvc.exceptions import (
|
||||
CheckoutError,
|
||||
CheckoutErrorSuggestGit,
|
||||
@@ -24,8 +21,7 @@ from dvc.stage.exceptions import StageFileDoesNotExistError
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.serialize import dump_yaml, load_yaml
|
||||
from tests.basic_env import TestDvc, TestDvcGit
|
||||
from tests.func.test_repro import TestRepro
|
||||
from tests.utils import get_gitignore_content
|
||||
|
||||
logger = logging.getLogger("dvc")
|
||||
|
||||
@@ -36,465 +32,365 @@ def walk_files(directory):
|
||||
yield os.path.join(root, f)
|
||||
|
||||
|
||||
class TestCheckout(TestRepro):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
def test_checkout(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
|
||||
dvc.run(
|
||||
fname="file1.dvc",
|
||||
outs=["file1"],
|
||||
deps=["foo", "copy.py"],
|
||||
cmd="python copy.py foo file1",
|
||||
single_stage=True,
|
||||
)
|
||||
remove(tmp_dir / "foo")
|
||||
remove("data")
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.data_dir_stage = stages[0]
|
||||
self.assertTrue(self.data_dir_stage is not None)
|
||||
|
||||
self.orig = "orig"
|
||||
shutil.copy(self.FOO, self.orig)
|
||||
os.unlink(self.FOO)
|
||||
|
||||
self.orig_dir = "orig_dir"
|
||||
shutil.copytree(self.DATA_DIR, self.orig_dir)
|
||||
shutil.rmtree(self.DATA_DIR)
|
||||
|
||||
def test(self):
|
||||
self.dvc.checkout(force=True)
|
||||
self._test_checkout()
|
||||
|
||||
def _test_checkout(self):
|
||||
self.assertTrue(os.path.isfile(self.FOO))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, self.orig, shallow=False))
|
||||
dvc.checkout(force=True)
|
||||
assert (tmp_dir / "foo").read_text() == "foo"
|
||||
assert (tmp_dir / "data").read_text() == {"file": "file"}
|
||||
|
||||
|
||||
class TestCheckoutSingleStage(TestCheckout):
|
||||
def test(self):
|
||||
ret = main(["checkout", "--force", self.foo_stage.path])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_checkout_cli(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
|
||||
dvc.run(
|
||||
fname="file1.dvc",
|
||||
outs=["file1"],
|
||||
deps=["foo", "copy.py"],
|
||||
cmd="python copy.py foo file1",
|
||||
single_stage=True,
|
||||
)
|
||||
remove(tmp_dir / "foo")
|
||||
remove("data")
|
||||
|
||||
ret = main(["checkout", "--force", self.data_dir_stage.path])
|
||||
self.assertEqual(ret, 0)
|
||||
assert main(["checkout", "--force"]) == 0
|
||||
assert (tmp_dir / "foo").read_text() == "foo"
|
||||
assert (tmp_dir / "data").read_text() == {"file": "file"}
|
||||
|
||||
self._test_checkout()
|
||||
remove(tmp_dir / "foo")
|
||||
remove("data")
|
||||
|
||||
assert main(["checkout", "--force", "foo.dvc"]) == 0
|
||||
assert main(["checkout", "--force", "data.dvc"]) == 0
|
||||
assert (tmp_dir / "foo").read_text() == "foo"
|
||||
assert (tmp_dir / "data").read_text() == {"file": "file"}
|
||||
|
||||
|
||||
class TestCheckoutCorruptedCacheFile(TestRepro):
|
||||
def test(self):
|
||||
cache = self.foo_stage.outs[0].cache_path
|
||||
def test_checkout_corrupted_cache_file(tmp_dir, dvc):
|
||||
(foo_stage,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
cache = foo_stage.outs[0].cache_path
|
||||
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "a", encoding="utf-8") as fd:
|
||||
fd.write("1")
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "a", encoding="utf-8") as fd:
|
||||
fd.write("1")
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
self.dvc.checkout(force=True)
|
||||
with pytest.raises(CheckoutError):
|
||||
dvc.checkout(force=True)
|
||||
|
||||
self.assertFalse(os.path.isfile(self.FOO))
|
||||
self.assertFalse(os.path.isfile(cache))
|
||||
assert not os.path.isfile("foo")
|
||||
assert not os.path.isfile(cache)
|
||||
|
||||
|
||||
class TestCheckoutCorruptedCacheDir(TestDvc):
|
||||
def test(self):
|
||||
from dvc_data.hashfile import load
|
||||
def test_checkout_corrupted_cache_dir(tmp_dir, dvc):
|
||||
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"])
|
||||
self.assertEqual(ret, 0)
|
||||
tmp_dir.gen("data", {"foo": "foo", "bar": "bar"})
|
||||
# NOTE: using 'copy' so that cache and link don't have same inode
|
||||
ret = main(["config", "cache.type", "copy"])
|
||||
assert ret == 0
|
||||
|
||||
self.dvc.config.load()
|
||||
dvc.config.load()
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertEqual(len(stages[0].outs), 1)
|
||||
out = stages[0].outs[0]
|
||||
stages = dvc.add("data")
|
||||
assert len(stages) == 1
|
||||
assert len(stages[0].outs) == 1
|
||||
out = stages[0].outs[0]
|
||||
|
||||
# NOTE: modifying cache file for one of the files inside the directory
|
||||
# to check if dvc will detect that the cache is corrupted.
|
||||
obj = load(self.dvc.odb.local, out.hash_info)
|
||||
_, _, entry_oid = list(obj)[0]
|
||||
cache = self.dvc.odb.local.oid_to_path(entry_oid.value)
|
||||
# NOTE: modifying cache file for one of the files inside the directory
|
||||
# to check if dvc will detect that the cache is corrupted.
|
||||
obj = load(dvc.odb.local, out.hash_info)
|
||||
_, _, entry_oid = list(obj)[0]
|
||||
cache = dvc.odb.local.oid_to_path(entry_oid.value)
|
||||
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("1")
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("1")
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
self.dvc.checkout(force=True)
|
||||
with pytest.raises(CheckoutError):
|
||||
dvc.checkout(force=True)
|
||||
|
||||
self.assertFalse(os.path.exists(cache))
|
||||
assert not os.path.exists(cache)
|
||||
|
||||
|
||||
class TestCmdCheckout(TestCheckout):
|
||||
def test(self):
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
self._test_checkout()
|
||||
def test_remove_files_when_checkout(tmp_dir, dvc, scm):
|
||||
# add the file into a separate branch
|
||||
scm.checkout("branch", True)
|
||||
ret = main(["checkout", "--force"])
|
||||
assert ret == 0
|
||||
tmp_dir.dvc_gen("file_in_a_branch", "random text", commit="add file")
|
||||
|
||||
# Checkout back in master
|
||||
scm.checkout("master")
|
||||
assert os.path.exists("file_in_a_branch")
|
||||
|
||||
# Make sure `dvc checkout` removes the file
|
||||
# self.dvc.checkout()
|
||||
ret = main(["checkout", "--force"])
|
||||
assert ret == 0
|
||||
assert not os.path.exists("file_in_a_branch")
|
||||
|
||||
|
||||
class CheckoutBase(TestDvcGit):
|
||||
GIT_IGNORE = ".gitignore"
|
||||
|
||||
def commit_data_file(self, fname, content="random text"):
|
||||
with open(fname, "w", encoding="utf-8") as fd:
|
||||
fd.write(content)
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
self.dvc.scm.add([fname + ".dvc", ".gitignore"])
|
||||
self.dvc.scm.commit("adding " + fname)
|
||||
|
||||
def read_ignored(self):
|
||||
with open(self.GIT_IGNORE, encoding="utf-8") as f:
|
||||
return [s.strip("\n") for s in f.readlines()]
|
||||
|
||||
def outs_info(self, stage):
|
||||
FileInfo = collections.namedtuple("FileInfo", "path inode")
|
||||
|
||||
paths = [
|
||||
path
|
||||
for output in stage["outs"]
|
||||
for path in self.dvc.fs.find(output["path"])
|
||||
]
|
||||
|
||||
return [
|
||||
FileInfo(path=path, inode=system.inode(path)) for path in paths
|
||||
]
|
||||
|
||||
|
||||
class TestRemoveFilesWhenCheckout(CheckoutBase):
|
||||
def test(self):
|
||||
fname = "file_in_a_branch"
|
||||
branch_master = "master"
|
||||
branch_1 = "b1"
|
||||
|
||||
self.dvc.scm.add(self.dvc.scm.untracked_files())
|
||||
self.dvc.scm.commit("add all files")
|
||||
|
||||
# add the file into a separate branch
|
||||
self.dvc.scm.checkout(branch_1, True)
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.commit_data_file(fname)
|
||||
|
||||
# Checkout back in master
|
||||
self.dvc.scm.checkout(branch_master)
|
||||
self.assertTrue(os.path.exists(fname))
|
||||
|
||||
# Make sure `dvc checkout` removes the file
|
||||
# self.dvc.checkout()
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(os.path.exists(fname))
|
||||
|
||||
|
||||
class TestCheckoutCleanWorkingDir(CheckoutBase):
|
||||
@patch("dvc.prompt.confirm", return_value=True)
|
||||
def test(self, mock_prompt):
|
||||
mock_prompt.return_value = True
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
stage = stages[0]
|
||||
|
||||
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
|
||||
with open(working_dir_change, "w", encoding="utf-8") as f:
|
||||
f.write("not_cached")
|
||||
|
||||
ret = main(["checkout", stage.relpath])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(os.path.exists(working_dir_change))
|
||||
|
||||
@patch("dvc.prompt.confirm", return_value=False)
|
||||
def test_force(self, mock_prompt):
|
||||
mock_prompt.return_value = False
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
|
||||
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
|
||||
with open(working_dir_change, "w", encoding="utf-8") as f:
|
||||
f.write("not_cached")
|
||||
|
||||
ret = main(["checkout", stage.relpath])
|
||||
self.assertNotEqual(ret, 0)
|
||||
class TestCheckoutCleanWorkingDir:
|
||||
def test(self, mocker, tmp_dir, dvc): # pylint: disable=unused-argument
|
||||
mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=True)
|
||||
(stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"})
|
||||
|
||||
# change working directory
|
||||
(tmp_dir / "data").gen("not_cached.txt", "not_cached")
|
||||
assert main(["checkout", stage.relpath]) == 0
|
||||
mock_prompt.assert_called()
|
||||
self.assertNotEqual(ret, 0)
|
||||
self.assertRaises(DvcException)
|
||||
assert not (tmp_dir / "data" / "not_cached.txt").exists()
|
||||
|
||||
def test_force(self, mocker, tmp_dir, dvc):
|
||||
mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=False)
|
||||
(stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"})
|
||||
|
||||
# change working directory
|
||||
(tmp_dir / "data").gen("not_cached.txt", "not_cached")
|
||||
assert main(["checkout", stage.relpath]) != 0
|
||||
mock_prompt.assert_called()
|
||||
assert (tmp_dir / "data" / "not_cached.txt").exists()
|
||||
|
||||
|
||||
def test_checkout_selective_remove(tmp_dir, dvc):
|
||||
# Use copy to test for changes in the inodes
|
||||
dvc.odb.local.cache_types = ["copy"]
|
||||
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
|
||||
|
||||
foo_inode = system.inode(os.path.join("data", "foo"))
|
||||
bar_inode = system.inode(os.path.join("data", "bar"))
|
||||
# move instead of remove, to lock inode assigned to stage_files[0].path
|
||||
# if we were to use remove, we might end up with same inode assigned to
|
||||
# newly checked out file
|
||||
shutil.move(os.path.join("data", "foo"), "random_name")
|
||||
|
||||
assert main(["checkout", "--force", "data.dvc"]) == 0
|
||||
assert (tmp_dir / "data").read_text() == {"foo": "foo", "bar": "bar"}
|
||||
assert system.inode(os.path.join("data", "foo")) != foo_inode
|
||||
assert system.inode(os.path.join("data", "bar")) == bar_inode
|
||||
|
||||
|
||||
def test_gitignore_basic(tmp_dir, dvc, scm):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
assert not os.path.exists(scm.GITIGNORE)
|
||||
|
||||
tmp_dir.dvc_gen("file1", "random text1", commit="add file1")
|
||||
tmp_dir.dvc_gen("file2", "random text2", commit="add file2")
|
||||
dvc.run(
|
||||
single_stage=True,
|
||||
cmd="cp foo file3",
|
||||
deps=["foo"],
|
||||
outs_no_cache=["file3"],
|
||||
)
|
||||
assert get_gitignore_content() == ["/file1", "/file2"]
|
||||
|
||||
|
||||
def test_gitignore_when_checkout(tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen("file_in_a_master", "master", commit="master")
|
||||
|
||||
class TestCheckoutSelectiveRemove(CheckoutBase):
|
||||
def test(self):
|
||||
# Use copy to test for changes in the inodes
|
||||
ret = main(["config", "cache.type", "copy"])
|
||||
self.assertEqual(ret, 0)
|
||||
scm.checkout("branch", True)
|
||||
ret = main(["checkout", "--force"])
|
||||
assert ret == 0
|
||||
tmp_dir.dvc_gen("file_in_a_branch", "branch", commit="branch")
|
||||
|
||||
scm.checkout("master")
|
||||
ret = main(["checkout", "--force"])
|
||||
assert ret == 0
|
||||
|
||||
ignored = get_gitignore_content()
|
||||
|
||||
assert len(ignored) == 1
|
||||
assert "/file_in_a_master" in ignored
|
||||
|
||||
ret = main(["add", self.DATA_DIR])
|
||||
self.assertEqual(0, ret)
|
||||
scm.checkout("branch")
|
||||
ret = main(["checkout", "--force"])
|
||||
assert ret == 0
|
||||
ignored = get_gitignore_content()
|
||||
assert "/file_in_a_branch" in ignored
|
||||
|
||||
|
||||
def test_checkout_missing_md5_in_stage_file(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
|
||||
stage = dvc.run(
|
||||
fname="file1.dvc",
|
||||
outs=["file1"],
|
||||
deps=["foo", "copy.py"],
|
||||
cmd="python copy.py foo file1",
|
||||
single_stage=True,
|
||||
)
|
||||
d = load_yaml(stage.relpath)
|
||||
del d[Stage.PARAM_OUTS][0][LocalFileSystem.PARAM_CHECKSUM]
|
||||
del d[Stage.PARAM_DEPS][0][LocalFileSystem.PARAM_CHECKSUM]
|
||||
dump_yaml(stage.relpath, d)
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
dvc.checkout(force=True)
|
||||
|
||||
|
||||
def test_checkout_empty_dir(tmp_dir, dvc):
|
||||
empty_dir = tmp_dir / "empty_dir"
|
||||
empty_dir.mkdir()
|
||||
(stage,) = dvc.add("empty_dir")
|
||||
|
||||
stage.outs[0].remove()
|
||||
assert not empty_dir.exists()
|
||||
|
||||
stats = dvc.checkout(force=True)
|
||||
assert stats["added"] == [os.path.join("empty_dir", "")]
|
||||
assert empty_dir.is_dir()
|
||||
assert not list(empty_dir.iterdir())
|
||||
|
||||
|
||||
def test_checkout_not_cached_file(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
dvc.run(
|
||||
cmd="cp foo bar",
|
||||
deps=["foo"],
|
||||
outs_no_cache=["bar"],
|
||||
single_stage=True,
|
||||
)
|
||||
stats = dvc.checkout(force=True)
|
||||
assert not any(stats.values())
|
||||
|
||||
|
||||
def test_checkout_with_deps_cli(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
|
||||
dvc.run(
|
||||
fname="file1.dvc",
|
||||
outs=["file1"],
|
||||
deps=["foo", "copy.py"],
|
||||
cmd="python copy.py foo file1",
|
||||
single_stage=True,
|
||||
)
|
||||
remove("foo")
|
||||
remove("file1")
|
||||
|
||||
assert not os.path.exists("foo")
|
||||
assert not os.path.exists("file1")
|
||||
|
||||
stage_path = self.DATA_DIR + DVC_FILE_SUFFIX
|
||||
stage = load_yaml(stage_path)
|
||||
staged_files = self.outs_info(stage)
|
||||
ret = main(["checkout", "--force", "file1.dvc", "--with-deps"])
|
||||
assert ret == 0
|
||||
|
||||
assert os.path.exists("foo")
|
||||
assert os.path.exists("file1")
|
||||
|
||||
# move instead of remove, to lock inode assigned to stage_files[0].path
|
||||
# if we were to use remove, we might end up with same inode assigned to
|
||||
# newly checked out file
|
||||
shutil.move(staged_files[0].path, "random_name")
|
||||
|
||||
ret = main(["checkout", "--force", stage_path])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_checkout_directory(tmp_dir, dvc):
|
||||
(stage,) = tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
|
||||
|
||||
remove("data")
|
||||
assert not os.path.exists("data")
|
||||
|
||||
checkedout_files = self.outs_info(stage)
|
||||
ret = main(["checkout", stage.path])
|
||||
assert ret == 0
|
||||
|
||||
self.assertEqual(len(staged_files), len(checkedout_files))
|
||||
self.assertEqual(staged_files[0].path, checkedout_files[0].path)
|
||||
self.assertNotEqual(staged_files[0].inode, checkedout_files[0].inode)
|
||||
self.assertEqual(staged_files[1].inode, checkedout_files[1].inode)
|
||||
assert os.path.exists("data")
|
||||
|
||||
|
||||
class TestGitIgnoreBasic(CheckoutBase):
|
||||
def test(self):
|
||||
fname1 = "file_1"
|
||||
fname2 = "file_2"
|
||||
fname3 = "file_3"
|
||||
def test_checkout_hook(mocker, tmp_dir, dvc):
|
||||
"""Test that dvc checkout handles EOFError gracefully, which is what
|
||||
it will experience when running in a git hook.
|
||||
"""
|
||||
tmp_dir.dvc_gen({"data": {"foo": "foo"}})
|
||||
mocker.patch("sys.stdout.isatty", return_value=True)
|
||||
mocker.patch("dvc.prompt.input", side_effect=EOFError)
|
||||
|
||||
self.dvc.scm.add(self.dvc.scm.untracked_files())
|
||||
self.dvc.scm.commit("add all files")
|
||||
(tmp_dir / "data").gen("test", "test")
|
||||
with pytest.raises(ConfirmRemoveError):
|
||||
dvc.checkout()
|
||||
|
||||
self.assertFalse(os.path.exists(self.GIT_IGNORE))
|
||||
|
||||
self.commit_data_file(fname1)
|
||||
self.commit_data_file(fname2)
|
||||
self.dvc.run(
|
||||
single_stage=True,
|
||||
cmd=f"python {self.CODE} {self.FOO} {fname3}",
|
||||
deps=[self.CODE, self.FOO],
|
||||
outs_no_cache=[fname3],
|
||||
)
|
||||
def test_checkout_suggest_git(tmp_dir, dvc, scm):
|
||||
# pylint: disable=no-member
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
try:
|
||||
dvc.checkout(targets="gitbranch")
|
||||
except DvcException as exc:
|
||||
assert isinstance(exc, CheckoutErrorSuggestGit)
|
||||
assert isinstance(exc.__cause__, NoOutputOrStageError)
|
||||
assert isinstance(exc.__cause__.__cause__, StageFileDoesNotExistError)
|
||||
|
||||
try:
|
||||
dvc.checkout(targets="foo")
|
||||
except DvcException as exc:
|
||||
assert isinstance(exc, CheckoutErrorSuggestGit)
|
||||
assert isinstance(exc.__cause__, NoOutputOrStageError)
|
||||
assert exc.__cause__.__cause__ is None
|
||||
|
||||
try:
|
||||
dvc.checkout(targets="looks-like-dvcfile.dvc")
|
||||
except DvcException as exc:
|
||||
assert isinstance(exc, CheckoutErrorSuggestGit)
|
||||
assert isinstance(exc.__cause__, StageFileDoesNotExistError)
|
||||
assert exc.__cause__.__cause__ is None
|
||||
|
||||
self.assertTrue(os.path.exists(self.GIT_IGNORE))
|
||||
|
||||
ignored = self.read_ignored()
|
||||
def test_checkout_target_recursive_should_not_remove_other_used_files(
|
||||
tmp_dir, dvc
|
||||
):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "bar": "bar", "data": {"file": "file"}})
|
||||
assert main(["checkout", "-R", "data"]) == 0
|
||||
assert (tmp_dir / "foo").exists()
|
||||
assert (tmp_dir / "bar").exists()
|
||||
|
||||
self.assertEqual(len(ignored), 2)
|
||||
|
||||
self.assertIn("/" + fname1, ignored)
|
||||
self.assertIn("/" + fname2, ignored)
|
||||
def test_checkout_recursive_not_directory(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
stats = dvc.checkout(targets=["foo" + ".dvc"], recursive=True)
|
||||
assert stats == {"added": [], "modified": [], "deleted": []}
|
||||
|
||||
class TestGitIgnoreWhenCheckout(CheckoutBase):
|
||||
def test(self):
|
||||
fname_master = "file_in_a_master"
|
||||
branch_master = "master"
|
||||
fname_branch = "file_in_a_branch"
|
||||
branch_1 = "b1"
|
||||
|
||||
self.dvc.scm.add(self.dvc.scm.untracked_files())
|
||||
self.dvc.scm.commit("add all files")
|
||||
self.commit_data_file(fname_master)
|
||||
def test_checkout_moved_cache_dir_with_symlinks(tmp_dir, dvc):
|
||||
tmp_dir.gen({"foo": "foo", "data": {"file": "file"}})
|
||||
ret = main(["config", "cache.type", "symlink"])
|
||||
assert ret == 0
|
||||
|
||||
self.dvc.scm.checkout(branch_1, True)
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.commit_data_file(fname_branch)
|
||||
ret = main(["add", "foo"])
|
||||
assert ret == 0
|
||||
|
||||
self.dvc.scm.checkout(branch_master)
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
|
||||
ignored = self.read_ignored()
|
||||
assert system.is_symlink("foo")
|
||||
old_foo_link = os.path.realpath("foo")
|
||||
|
||||
self.assertEqual(len(ignored), 1)
|
||||
self.assertIn("/" + fname_master, ignored)
|
||||
assert system.is_symlink(os.path.join("data", "file"))
|
||||
old_data_link = os.path.realpath(os.path.join("data", "file"))
|
||||
|
||||
self.dvc.scm.checkout(branch_1)
|
||||
ret = main(["checkout", "--force"])
|
||||
self.assertEqual(ret, 0)
|
||||
ignored = self.read_ignored()
|
||||
self.assertIn("/" + fname_branch, ignored)
|
||||
old_cache_dir = dvc.odb.local.path
|
||||
new_cache_dir = old_cache_dir + "_new"
|
||||
os.rename(old_cache_dir, new_cache_dir)
|
||||
|
||||
ret = main(["cache", "dir", new_cache_dir])
|
||||
assert ret == 0
|
||||
|
||||
class TestCheckoutMissingMd5InStageFile(TestRepro):
|
||||
def test(self):
|
||||
d = load_yaml(self.file1_stage)
|
||||
del d[Stage.PARAM_OUTS][0][LocalFileSystem.PARAM_CHECKSUM]
|
||||
del d[Stage.PARAM_DEPS][0][LocalFileSystem.PARAM_CHECKSUM]
|
||||
dump_yaml(self.file1_stage, d)
|
||||
ret = main(["checkout", "-f"])
|
||||
assert ret == 0
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
self.dvc.checkout(force=True)
|
||||
assert system.is_symlink("foo")
|
||||
new_foo_link = os.path.realpath("foo")
|
||||
|
||||
assert system.is_symlink(os.path.join("data", "file"))
|
||||
new_data_link = os.path.realpath(os.path.join("data", "file"))
|
||||
|
||||
class TestCheckoutEmptyDir(TestDvc):
|
||||
def test(self):
|
||||
dname = "empty_dir"
|
||||
os.mkdir(dname)
|
||||
assert relpath(old_foo_link, old_cache_dir) == relpath(
|
||||
new_foo_link, new_cache_dir
|
||||
)
|
||||
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
self.assertEqual(len(stage.outs), 1)
|
||||
|
||||
stage.outs[0].remove()
|
||||
self.assertFalse(os.path.exists(dname))
|
||||
|
||||
stats = self.dvc.checkout(force=True)
|
||||
assert stats["added"] == [dname + os.sep]
|
||||
|
||||
self.assertTrue(os.path.isdir(dname))
|
||||
self.assertEqual(len(os.listdir(dname)), 0)
|
||||
|
||||
|
||||
class TestCheckoutNotCachedFile(TestDvc):
|
||||
def test(self):
|
||||
cmd = "python {} {} {}".format(self.CODE, self.FOO, "out")
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
stage = self.dvc.run(
|
||||
cmd=cmd,
|
||||
deps=[self.FOO, self.CODE],
|
||||
outs_no_cache=["out"],
|
||||
single_stage=True,
|
||||
)
|
||||
self.assertTrue(stage is not None)
|
||||
|
||||
stats = self.dvc.checkout(force=True)
|
||||
assert not any(stats.values())
|
||||
|
||||
|
||||
class TestCheckoutWithDeps(TestRepro):
|
||||
def test(self):
|
||||
os.unlink(self.FOO)
|
||||
os.unlink(self.file1)
|
||||
|
||||
self.assertFalse(os.path.exists(self.FOO))
|
||||
self.assertFalse(os.path.exists(self.file1))
|
||||
|
||||
ret = main(["checkout", "--force", self.file1_stage, "--with-deps"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(os.path.exists(self.FOO))
|
||||
self.assertTrue(os.path.exists(self.file1))
|
||||
|
||||
|
||||
class TestCheckoutDirectory(TestRepro):
|
||||
def test(self):
|
||||
stage = self.dvc.add(self.DATA_DIR)[0]
|
||||
|
||||
shutil.rmtree(self.DATA_DIR)
|
||||
self.assertFalse(os.path.exists(self.DATA_DIR))
|
||||
|
||||
ret = main(["checkout", stage.path])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(os.path.exists(self.DATA_DIR))
|
||||
|
||||
|
||||
class TestCheckoutHook(TestDvc):
|
||||
@patch("sys.stdout.isatty", return_value=True)
|
||||
@patch("dvc.prompt.input", side_effect=EOFError)
|
||||
def test(self, _mock_input, _mock_isatty):
|
||||
"""Test that dvc checkout handles EOFError gracefully, which is what
|
||||
it will experience when running in a git hook.
|
||||
"""
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertNotEqual(stage, None)
|
||||
|
||||
self.create(os.path.join(self.DATA_DIR, "test"), "test")
|
||||
|
||||
with self.assertRaises(ConfirmRemoveError):
|
||||
self.dvc.checkout()
|
||||
|
||||
|
||||
class TestCheckoutSuggestGit(TestRepro):
|
||||
def test(self):
|
||||
# pylint: disable=no-member
|
||||
|
||||
try:
|
||||
self.dvc.checkout(targets="gitbranch")
|
||||
except DvcException as exc:
|
||||
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
|
||||
self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
|
||||
self.assertIsInstance(
|
||||
exc.__cause__.__cause__, StageFileDoesNotExistError
|
||||
)
|
||||
|
||||
try:
|
||||
self.dvc.checkout(targets=self.FOO)
|
||||
except DvcException as exc:
|
||||
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
|
||||
self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
|
||||
self.assertIsNone(exc.__cause__.__cause__)
|
||||
|
||||
try:
|
||||
self.dvc.checkout(targets="looks-like-dvcfile.dvc")
|
||||
except DvcException as exc:
|
||||
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
|
||||
self.assertIsInstance(exc.__cause__, StageFileDoesNotExistError)
|
||||
self.assertIsNone(exc.__cause__.__cause__)
|
||||
|
||||
|
||||
class TestCheckoutTargetRecursiveShouldNotRemoveOtherUsedFiles(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.DATA_DIR, self.FOO, self.BAR])
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
ret = main(["checkout", "-R", self.DATA_DIR])
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
self.assertTrue(os.path.exists(self.FOO))
|
||||
self.assertTrue(os.path.exists(self.BAR))
|
||||
|
||||
|
||||
class TestCheckoutRecursiveNotDirectory(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(0, ret)
|
||||
|
||||
stats = self.dvc.checkout(targets=[self.FOO + ".dvc"], recursive=True)
|
||||
assert stats == {"added": [], "modified": [], "deleted": []}
|
||||
|
||||
|
||||
class TestCheckoutMovedCacheDirWithSymlinks(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["config", "cache.type", "symlink"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(["add", self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(system.is_symlink(self.FOO))
|
||||
old_foo_link = os.path.realpath(self.FOO)
|
||||
|
||||
self.assertTrue(system.is_symlink(self.DATA))
|
||||
old_data_link = os.path.realpath(self.DATA)
|
||||
|
||||
old_cache_dir = self.dvc.odb.local.path
|
||||
new_cache_dir = old_cache_dir + "_new"
|
||||
os.rename(old_cache_dir, new_cache_dir)
|
||||
|
||||
ret = main(["cache", "dir", new_cache_dir])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(["checkout", "-f"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(system.is_symlink(self.FOO))
|
||||
new_foo_link = os.path.realpath(self.FOO)
|
||||
|
||||
self.assertTrue(system.is_symlink(self.DATA))
|
||||
new_data_link = os.path.realpath(self.DATA)
|
||||
|
||||
self.assertEqual(
|
||||
relpath(old_foo_link, old_cache_dir),
|
||||
relpath(new_foo_link, new_cache_dir),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
relpath(old_data_link, old_cache_dir),
|
||||
relpath(new_data_link, new_cache_dir),
|
||||
)
|
||||
assert relpath(old_data_link, old_cache_dir) == relpath(
|
||||
new_data_link, new_cache_dir
|
||||
)
|
||||
|
||||
|
||||
def test_checkout_no_checksum(tmp_dir, dvc):
|
||||
|
||||
+137
-147
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.cli import DvcParserError, parse_args
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.commands.add import CmdAdd
|
||||
@@ -12,200 +14,188 @@ from dvc.commands.repro import CmdRepro
|
||||
from dvc.commands.run import CmdRun
|
||||
from dvc.commands.status import CmdDataStatus
|
||||
from dvc.exceptions import DvcException
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestArgParse(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args(["init"])
|
||||
self.assertIsInstance(args.func(args), CmdInit)
|
||||
def test_argparse(dvc):
|
||||
args = parse_args(["init"])
|
||||
assert isinstance(args.func(args), CmdInit)
|
||||
|
||||
|
||||
class TestRun(TestDvc):
|
||||
def test(self):
|
||||
dep1 = "dep1"
|
||||
dep2 = "dep2"
|
||||
def test_run(dvc):
|
||||
dep1 = "dep1"
|
||||
dep2 = "dep2"
|
||||
|
||||
out1 = "out1"
|
||||
out2 = "out2"
|
||||
out1 = "out1"
|
||||
out2 = "out2"
|
||||
|
||||
out_no_cache1 = "out_no_cache1"
|
||||
out_no_cache2 = "out_no_cache2"
|
||||
out_no_cache1 = "out_no_cache1"
|
||||
out_no_cache2 = "out_no_cache2"
|
||||
|
||||
fname = "dvc.dvc"
|
||||
cmd = "cmd"
|
||||
arg1 = "arg1"
|
||||
arg2 = "arg2"
|
||||
fname = "dvc.dvc"
|
||||
cmd = "cmd"
|
||||
arg1 = "arg1"
|
||||
arg2 = "arg2"
|
||||
|
||||
args = parse_args(
|
||||
[
|
||||
"run",
|
||||
"-d",
|
||||
dep1,
|
||||
"--deps",
|
||||
dep2,
|
||||
"-o",
|
||||
out1,
|
||||
"--outs",
|
||||
out2,
|
||||
"-O",
|
||||
out_no_cache1,
|
||||
"--outs-no-cache",
|
||||
out_no_cache2,
|
||||
"--file",
|
||||
fname,
|
||||
cmd,
|
||||
arg1,
|
||||
arg2,
|
||||
]
|
||||
)
|
||||
cmd_cls = args.func(args)
|
||||
self.assertIsInstance(cmd_cls, CmdRun)
|
||||
self.assertEqual(args.deps, [dep1, dep2])
|
||||
self.assertEqual(args.outs, [out1, out2])
|
||||
self.assertEqual(args.outs_no_cache, [out_no_cache1, out_no_cache2])
|
||||
self.assertEqual(args.file, fname)
|
||||
self.assertEqual(args.command, [cmd, arg1, arg2])
|
||||
args = parse_args(
|
||||
[
|
||||
"run",
|
||||
"-d",
|
||||
dep1,
|
||||
"--deps",
|
||||
dep2,
|
||||
"-o",
|
||||
out1,
|
||||
"--outs",
|
||||
out2,
|
||||
"-O",
|
||||
out_no_cache1,
|
||||
"--outs-no-cache",
|
||||
out_no_cache2,
|
||||
"--file",
|
||||
fname,
|
||||
cmd,
|
||||
arg1,
|
||||
arg2,
|
||||
]
|
||||
)
|
||||
cmd_cls = args.func(args)
|
||||
assert isinstance(cmd_cls, CmdRun)
|
||||
assert args.deps == [dep1, dep2]
|
||||
assert args.outs == [out1, out2]
|
||||
assert args.outs_no_cache == [out_no_cache1, out_no_cache2]
|
||||
assert args.file == fname
|
||||
assert args.command == [cmd, arg1, arg2]
|
||||
|
||||
cmd_cls.repo.close()
|
||||
cmd_cls.repo.close()
|
||||
|
||||
|
||||
class TestPull(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args(["pull"])
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdDataPull)
|
||||
def test_pull(dvc):
|
||||
args = parse_args(["pull"])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdDataPull)
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestPush(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args(["push"])
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdDataPush)
|
||||
def test_push(dvc):
|
||||
args = parse_args(["push"])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdDataPush)
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestStatus(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args(["status"])
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdDataStatus)
|
||||
def test_status(dvc):
|
||||
args = parse_args(["status"])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdDataStatus)
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestRepro(TestDvc):
|
||||
def test(self):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
def test_repro(dvc):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
|
||||
args = parse_args(
|
||||
["repro", target1, target2, "-f", "--force", "-s", "--single-item"]
|
||||
)
|
||||
args = parse_args(
|
||||
["repro", target1, target2, "-f", "--force", "-s", "--single-item"]
|
||||
)
|
||||
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdRepro)
|
||||
self.assertEqual(args.targets, [target1, target2])
|
||||
self.assertEqual(args.force, True)
|
||||
self.assertEqual(args.single_item, True)
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdRepro)
|
||||
assert args.targets == [target1, target2]
|
||||
assert args.force
|
||||
assert args.single_item
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestRemove(TestDvc):
|
||||
def test(self):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
def test_remove(dvc):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
|
||||
args = parse_args(["remove", target1, target2])
|
||||
args = parse_args(["remove", target1, target2])
|
||||
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdRemove)
|
||||
self.assertEqual(args.targets, [target1, target2])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdRemove)
|
||||
assert args.targets == [target1, target2]
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestAdd(TestDvc):
|
||||
def test(self):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
def test_add(dvc):
|
||||
target1 = "1"
|
||||
target2 = "2"
|
||||
|
||||
args = parse_args(["add", target1, target2])
|
||||
args = parse_args(["add", target1, target2])
|
||||
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdAdd)
|
||||
self.assertEqual(args.targets, [target1, target2])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdAdd)
|
||||
assert args.targets == [target1, target2]
|
||||
|
||||
cmd.repo.close()
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestConfig(TestDvc):
|
||||
def test(self):
|
||||
name = "section.option"
|
||||
value = "1"
|
||||
def test_config_unset(dvc):
|
||||
name = "section.option"
|
||||
value = "1"
|
||||
|
||||
args = parse_args(["config", "-u", "--unset", name, value])
|
||||
args = parse_args(["config", "-u", "--unset", name, value])
|
||||
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdConfig)
|
||||
self.assertEqual(args.unset, True)
|
||||
self.assertEqual(args.name, (False, "section", "option"))
|
||||
self.assertEqual(args.value, value)
|
||||
|
||||
def test_config_list(self):
|
||||
args = parse_args(["config", "--list"])
|
||||
|
||||
self.assertTrue(args.list)
|
||||
self.assertIsNone(args.name)
|
||||
self.assertIsNone(args.value)
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdConfig)
|
||||
assert args.unset
|
||||
assert args.name == (False, "section", "option")
|
||||
assert args.value == value
|
||||
|
||||
|
||||
class TestCheckout(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args(["checkout"])
|
||||
cmd = args.func(args)
|
||||
self.assertIsInstance(cmd, CmdCheckout)
|
||||
def test_config_list():
|
||||
args = parse_args(["config", "--list"])
|
||||
|
||||
cmd.repo.close()
|
||||
assert args.list
|
||||
assert args.name is None
|
||||
assert args.value is None
|
||||
|
||||
|
||||
class TestFindRoot(TestDvc):
|
||||
def test(self):
|
||||
class Cmd(CmdBase):
|
||||
def run(self):
|
||||
pass
|
||||
def test_checkout(dvc):
|
||||
args = parse_args(["checkout"])
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdCheckout)
|
||||
|
||||
class A:
|
||||
quiet = False
|
||||
verbose = True
|
||||
cd = os.path.pardir
|
||||
|
||||
args = A()
|
||||
with self.assertRaises(DvcException):
|
||||
Cmd(args)
|
||||
cmd.repo.close()
|
||||
|
||||
|
||||
class TestCd(TestDvc):
|
||||
def test(self):
|
||||
class Cmd(CmdBase):
|
||||
def run(self):
|
||||
pass
|
||||
def test_find_root(dvc):
|
||||
class Cmd(CmdBase):
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
class A:
|
||||
quiet = False
|
||||
verbose = True
|
||||
cd = os.path.pardir
|
||||
class A:
|
||||
quiet = False
|
||||
verbose = True
|
||||
cd = os.path.pardir
|
||||
|
||||
parent_dir = os.path.realpath(os.path.pardir)
|
||||
args = A()
|
||||
with self.assertRaises(DvcException):
|
||||
Cmd(args)
|
||||
current_dir = os.path.realpath(os.path.curdir)
|
||||
self.assertEqual(parent_dir, current_dir)
|
||||
args = A()
|
||||
with pytest.raises(DvcException):
|
||||
Cmd(args)
|
||||
|
||||
|
||||
def test_cd(dvc):
|
||||
class Cmd(CmdBase):
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
class A:
|
||||
quiet = False
|
||||
verbose = True
|
||||
cd = os.path.pardir
|
||||
|
||||
parent_dir = os.path.realpath(os.path.pardir)
|
||||
args = A()
|
||||
with pytest.raises(DvcException):
|
||||
Cmd(args)
|
||||
current_dir = os.path.realpath(os.path.curdir)
|
||||
assert parent_dir == current_dir
|
||||
|
||||
|
||||
def test_unknown_command_help(capsys):
|
||||
|
||||
@@ -418,6 +418,7 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
|
||||
"plots": {
|
||||
"path/to/plot": {"x": "value", "y": "value"},
|
||||
"path/to/another/plot": {"x": "value", "y": "value"},
|
||||
"path/to/empty/plot": None,
|
||||
},
|
||||
"stages": STAGE_EXAMPLE,
|
||||
},
|
||||
@@ -425,6 +426,8 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
|
||||
"plots": [
|
||||
{"path/to/plot": {"x": "value", "y": "value"}},
|
||||
{"path/to/another/plot": {"x": "value", "y": "value"}},
|
||||
{"path/to/empty/plot": None},
|
||||
"path/to/plot/str",
|
||||
],
|
||||
"stages": STAGE_EXAMPLE,
|
||||
},
|
||||
|
||||
+56
-150
@@ -2,186 +2,92 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import configobj
|
||||
import pytest
|
||||
from git import Repo
|
||||
|
||||
from dvc.cli import main
|
||||
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.hashfile.db.local import LocalHashFileDB
|
||||
from tests.basic_env import TestDvcGit
|
||||
|
||||
|
||||
class TestGC(TestDvcGit):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@pytest.fixture
|
||||
def good_and_bad_cache(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
(stage,) = tmp_dir.dvc_gen(
|
||||
"data",
|
||||
{"sub": {"data_sub": "data_sub", "data": "data", "тест": "проверка"}},
|
||||
)
|
||||
raw_dir_hash = stage.outs[0].hash_info.as_raw().value
|
||||
odb = dvc.odb.local
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
raw_dir_hash = stages[0].outs[0].hash_info.as_raw().value
|
||||
bad_cache = {raw_dir_hash}
|
||||
for i in ["123", "234", "345"]:
|
||||
odb.add_bytes(i, i.encode("utf8"))
|
||||
bad_cache.add(i)
|
||||
|
||||
self.good_cache = [
|
||||
self.dvc.odb.local.oid_to_path(md5)
|
||||
for md5 in self.dvc.odb.local.all()
|
||||
if md5 != raw_dir_hash
|
||||
]
|
||||
|
||||
self.bad_cache = [self.dvc.odb.local.oid_to_path(raw_dir_hash)]
|
||||
for i in ["123", "234", "345"]:
|
||||
path = os.path.join(self.dvc.odb.local.path, i[0:2], i[2:])
|
||||
self.create(path, i)
|
||||
self.bad_cache.append(path)
|
||||
|
||||
def test_api(self):
|
||||
self.dvc.gc(workspace=True)
|
||||
self._test_gc()
|
||||
|
||||
def test_cli(self):
|
||||
ret = main(["gc", "-wf"])
|
||||
self.assertEqual(ret, 0)
|
||||
self._test_gc()
|
||||
|
||||
def _test_gc(self):
|
||||
self.assertTrue(os.path.isdir(self.dvc.odb.local.path))
|
||||
for c in self.bad_cache:
|
||||
self.assertFalse(os.path.exists(c))
|
||||
|
||||
for c in self.good_cache:
|
||||
self.assertTrue(os.path.exists(c))
|
||||
good_cache = {md5 for md5 in odb.all() if md5 not in bad_cache}
|
||||
return good_cache, bad_cache
|
||||
|
||||
|
||||
class TestGCBranchesTags(TestDvcGit):
|
||||
def _check_cache(self, num):
|
||||
total = 0
|
||||
for _, _, files in os.walk(os.path.join(".dvc", "cache")):
|
||||
total += len(files)
|
||||
self.assertEqual(total, num)
|
||||
|
||||
def test(self):
|
||||
fname = "file"
|
||||
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("v1.0")
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add([".gitignore", stages[0].relpath])
|
||||
self.dvc.scm.commit("v1.0")
|
||||
self.dvc.scm.tag("v1.0")
|
||||
|
||||
self.dvc.scm.checkout("test", create_new=True)
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("test")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add([".gitignore", stages[0].relpath])
|
||||
self.dvc.scm.commit("test")
|
||||
|
||||
self.dvc.scm.checkout("master")
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("trash")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add([".gitignore", stages[0].relpath])
|
||||
self.dvc.scm.commit("trash")
|
||||
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("master")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add([".gitignore", stages[0].relpath])
|
||||
self.dvc.scm.commit("master")
|
||||
|
||||
self._check_cache(4)
|
||||
|
||||
self.dvc.gc(all_tags=True, all_branches=True)
|
||||
|
||||
self._check_cache(3)
|
||||
|
||||
self.dvc.gc(all_tags=False, all_branches=True)
|
||||
|
||||
self._check_cache(2)
|
||||
|
||||
self.dvc.gc(all_tags=True, all_branches=False)
|
||||
|
||||
self._check_cache(1)
|
||||
def test_gc_api(dvc, good_and_bad_cache):
|
||||
dvc.gc(workspace=True)
|
||||
odb = dvc.odb.local
|
||||
good_cache, bad_cache = good_and_bad_cache
|
||||
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
|
||||
|
||||
|
||||
class TestGCMultipleDvcRepos(TestDvcGit):
|
||||
def _check_cache(self, num):
|
||||
total = 0
|
||||
for _, _, files in os.walk(os.path.join(".dvc", "cache")):
|
||||
total += len(files)
|
||||
self.assertEqual(total, num)
|
||||
def test_gc_cli(dvc, good_and_bad_cache):
|
||||
assert main(["gc", "-wf"]) == 0
|
||||
odb = dvc.odb.local
|
||||
good_cache, bad_cache = good_and_bad_cache
|
||||
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.additional_path = self.mkdtemp()
|
||||
self.additional_git = Repo.init(self.additional_path)
|
||||
self.additional_dvc = DvcRepo.init(self.additional_path)
|
||||
|
||||
cache_path = os.path.join(self._root_dir, ".dvc", "cache")
|
||||
config_path = os.path.join(
|
||||
self.additional_path, ".dvc", "config.local"
|
||||
)
|
||||
cfg = configobj.ConfigObj()
|
||||
cfg.filename = config_path
|
||||
cfg["cache"] = {"dir": cache_path}
|
||||
cfg.write()
|
||||
def test_gc_branches_tags(tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen("file", "v1.0", commit="v1.0")
|
||||
scm.tag("v1.0")
|
||||
|
||||
self.additional_dvc = DvcRepo(self.additional_path)
|
||||
with tmp_dir.branch("test", new=True):
|
||||
dvc.remove("file.dvc")
|
||||
tmp_dir.dvc_gen("file", "test", commit="test")
|
||||
|
||||
def test(self):
|
||||
dvc.remove("file.dvc")
|
||||
tmp_dir.dvc_gen("file", "trash", commit="trash")
|
||||
|
||||
# ADD FILE ONLY IN MAIN PROJECT
|
||||
fname = "only_in_first"
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("only in main repo")
|
||||
dvc.remove("file.dvc")
|
||||
tmp_dir.dvc_gen("file", "master", commit="trash")
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
odb = dvc.odb.local
|
||||
assert len(list(odb.all())) == 4
|
||||
|
||||
# ADD FILE IN MAIN PROJECT THAT IS ALSO IN SECOND PROJECT
|
||||
fname = "in_both"
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("in both repos")
|
||||
dvc.gc(all_tags=True, all_branches=True)
|
||||
assert len(list(odb.all())) == 3
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
dvc.gc(all_tags=False, all_branches=True)
|
||||
assert len(list(odb.all())) == 2
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(self.additional_path)
|
||||
# ADD FILE ONLY IN SECOND PROJECT
|
||||
fname = "only_in_second"
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("only in additional repo")
|
||||
dvc.gc(all_tags=True, all_branches=False)
|
||||
assert len(list(odb.all())) == 1
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
# ADD FILE IN SECOND PROJECT THAT IS ALSO IN MAIN PROJECT
|
||||
fname = "in_both"
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("in both repos")
|
||||
def test_gc_multiple_dvc_repos(tmp_dir, scm, dvc, erepo_dir):
|
||||
tmp_dir.dvc_gen("only_in_first", "only in main repo")
|
||||
tmp_dir.dvc_gen("in_both", "in both repos")
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
erepo_dir.dvc.odb.local.path = dvc.odb.local.path
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen("in_both", "in both repos")
|
||||
erepo_dir.dvc_gen("only_in_second", "only in additional repo")
|
||||
|
||||
os.chdir(cwd)
|
||||
odb = dvc.odb.local
|
||||
assert len(list(odb.all())) == 3
|
||||
|
||||
self._check_cache(3)
|
||||
dvc.gc(repos=[erepo_dir], workspace=True)
|
||||
assert len(list(odb.all())) == 3
|
||||
|
||||
self.dvc.gc(repos=[self.additional_path], workspace=True)
|
||||
self._check_cache(3)
|
||||
|
||||
self.dvc.gc(workspace=True)
|
||||
self._check_cache(2)
|
||||
dvc.gc(workspace=True)
|
||||
assert len(list(odb.all())) == 2
|
||||
|
||||
|
||||
def test_all_commits(tmp_dir, scm, dvc):
|
||||
|
||||
+13
-14
@@ -1,6 +1,5 @@
|
||||
import filecmp
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from funcy import first
|
||||
@@ -49,7 +48,7 @@ def test_import_git_file(tmp_dir, scm, dvc, git_dir, src_is_dvc):
|
||||
}
|
||||
|
||||
|
||||
def test_import_cached_file(erepo_dir, tmp_dir, dvc, scm, monkeypatch):
|
||||
def test_import_cached_file(mocker, erepo_dir, tmp_dir, dvc, scm, monkeypatch):
|
||||
src = "some_file"
|
||||
dst = "some_file_imported"
|
||||
|
||||
@@ -60,10 +59,10 @@ def test_import_cached_file(erepo_dir, tmp_dir, dvc, scm, monkeypatch):
|
||||
(tmp_dir / dst).unlink()
|
||||
|
||||
remote_exception = NoRemoteError("dvc import")
|
||||
with patch.object(
|
||||
mocker.patch.object(
|
||||
dvc.cloud, "get_remote_odb", side_effect=remote_exception
|
||||
):
|
||||
tmp_dir.dvc.imp(os.fspath(erepo_dir), src, dst)
|
||||
)
|
||||
tmp_dir.dvc.imp(os.fspath(erepo_dir), src, dst)
|
||||
|
||||
assert (tmp_dir / dst).is_file()
|
||||
assert filecmp.cmp(erepo_dir / src, tmp_dir / dst, shallow=False)
|
||||
@@ -305,7 +304,7 @@ def test_pull_imported_directory_stage(tmp_dir, dvc, erepo_dir):
|
||||
dvc.imp(os.fspath(erepo_dir), "dir", "dir_imported")
|
||||
|
||||
remove("dir_imported")
|
||||
remove(dvc.odb.local.path)
|
||||
dvc.odb.local.clear()
|
||||
|
||||
dvc.pull(["dir_imported.dvc"])
|
||||
|
||||
@@ -321,7 +320,7 @@ def test_pull_wildcard_imported_directory_stage(tmp_dir, dvc, erepo_dir):
|
||||
dvc.imp(os.fspath(erepo_dir), "dir123", "dir_imported123")
|
||||
|
||||
remove("dir_imported123")
|
||||
remove(dvc.odb.local.path)
|
||||
dvc.odb.local.clear()
|
||||
|
||||
dvc.pull(["dir_imported*.dvc"], glob=True)
|
||||
|
||||
@@ -357,7 +356,9 @@ def test_push_wildcard_from_bare_git_repo(
|
||||
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dir123")
|
||||
|
||||
|
||||
def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
|
||||
def test_download_error_pulling_imported_stage(
|
||||
mocker, tmp_dir, dvc, erepo_dir
|
||||
):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
|
||||
dvc.imp(os.fspath(erepo_dir), "foo", "foo_imported")
|
||||
@@ -368,9 +369,8 @@ def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
|
||||
remove("foo_imported")
|
||||
remove(dst_cache)
|
||||
|
||||
with patch(
|
||||
"dvc_objects.fs.generic.transfer", side_effect=Exception
|
||||
), pytest.raises(DownloadError):
|
||||
mocker.patch("dvc_objects.fs.generic.transfer", side_effect=Exception)
|
||||
with pytest.raises(DownloadError):
|
||||
dvc.pull(["foo_imported.dvc"])
|
||||
|
||||
|
||||
@@ -546,9 +546,8 @@ def test_pull_imported_stage_from_subrepos(
|
||||
dvc.imp(os.fspath(erepo_dir), path, out="out")
|
||||
|
||||
# clean everything
|
||||
remove(dvc.odb.local.path)
|
||||
dvc.odb.local.clear()
|
||||
remove("out")
|
||||
makedirs(dvc.odb.local.path)
|
||||
|
||||
stats = dvc.pull(["out.dvc"])
|
||||
|
||||
@@ -633,7 +632,7 @@ def test_chained_import(tmp_dir, dvc, make_tmp_dir, erepo_dir, local_cloud):
|
||||
assert (dst / "foo").read_text() == "foo"
|
||||
assert (dst / "bar").read_text() == "bar"
|
||||
|
||||
remove(dvc.odb.local.path)
|
||||
dvc.odb.local.clear()
|
||||
remove("dir_imported")
|
||||
|
||||
# pulled objects should come from the original upstream repo's remote,
|
||||
|
||||
@@ -13,37 +13,32 @@ from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.stage import Stage
|
||||
from dvc.testing.workspace_tests import TestImport as _TestImport
|
||||
from dvc.utils.fs import makedirs
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestCmdImport(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["import-url", self.FOO, "import"])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.exists("import.dvc"))
|
||||
def test_cmd_import(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
ret = main(["import-url", "foo", "import"])
|
||||
assert ret == 0
|
||||
assert os.path.exists("import.dvc")
|
||||
|
||||
ret = main(["import-url", "non-existing-file", "import"])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
def test_unsupported(self):
|
||||
ret = main(["import-url", "unsupported://path", "import_unsupported"])
|
||||
self.assertNotEqual(ret, 0)
|
||||
ret = main(["import-url", "non-existing-file", "import"])
|
||||
assert ret != 0
|
||||
|
||||
|
||||
class TestDefaultOutput(TestDvc):
|
||||
def test(self):
|
||||
tmpdir = self.mkdtemp()
|
||||
filename = str(uuid4())
|
||||
tmpfile = os.path.join(tmpdir, filename)
|
||||
def test_cmd_unsupported_scheme(dvc):
|
||||
ret = main(["import-url", "unsupported://path", "import_unsupported"])
|
||||
assert ret != 0
|
||||
|
||||
with open(tmpfile, "w", encoding="utf-8") as fd:
|
||||
fd.write("content")
|
||||
|
||||
ret = main(["import-url", tmpfile])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.exists(filename))
|
||||
with open(filename, encoding="utf-8") as fd:
|
||||
self.assertEqual(fd.read(), "content")
|
||||
def test_default_output(tmp_dir, dvc, cloud):
|
||||
filename = str(uuid4())
|
||||
tmpfile = cloud / filename
|
||||
tmpfile.write_bytes(b"content")
|
||||
cloud.gen(filename, "content")
|
||||
|
||||
ret = main(["import-url", tmpfile.fs_path])
|
||||
assert ret == 0
|
||||
assert (tmp_dir / filename).read_bytes() == b"content"
|
||||
|
||||
|
||||
def test_should_remove_outs_before_import(tmp_dir, dvc, mocker, erepo_dir):
|
||||
@@ -56,26 +51,22 @@ def test_should_remove_outs_before_import(tmp_dir, dvc, mocker, erepo_dir):
|
||||
assert remove_outs_call_counter.mock.call_count == 1
|
||||
|
||||
|
||||
class TestImportFilename(TestDvc):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
tmp_dir = self.mkdtemp()
|
||||
self.external_source = os.path.join(tmp_dir, "file")
|
||||
with open(self.external_source, "w", encoding="utf-8") as fobj:
|
||||
fobj.write("content")
|
||||
def test_import_filename(tmp_dir, dvc, cloud):
|
||||
external_source = cloud / "file"
|
||||
(cloud / "file").write_text("content", encoding="utf-8")
|
||||
ret = main(["import-url", "--file", "bar.dvc", external_source.fs_path])
|
||||
assert ret == 0
|
||||
assert (tmp_dir / "bar.dvc").exists()
|
||||
|
||||
def test(self):
|
||||
ret = main(["import-url", "--file", "bar.dvc", self.external_source])
|
||||
self.assertEqual(0, ret)
|
||||
self.assertTrue(os.path.exists("bar.dvc"))
|
||||
(tmp_dir / "bar.dvc").unlink()
|
||||
(tmp_dir / "sub").mkdir()
|
||||
|
||||
os.remove("bar.dvc")
|
||||
os.mkdir("sub")
|
||||
|
||||
path = os.path.join("sub", "bar.dvc")
|
||||
ret = main(["import-url", "--file", path, self.external_source, "out"])
|
||||
self.assertEqual(0, ret)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
path = tmp_dir / "sub" / "bar.dvc"
|
||||
ret = main(
|
||||
["import-url", "--file", path.fs_path, external_source.fs_path, "out"]
|
||||
)
|
||||
assert ret == 0
|
||||
assert path.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dname", [".", "dir", "dir/subdir"])
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_init_quiet_should_not_display_welcome_screen(tmp_dir, scm, caplog):
|
||||
with caplog.at_level(logging.INFO, logger="dvc"):
|
||||
ret = main(["init", "--quiet"])
|
||||
|
||||
assert 0 == ret
|
||||
assert ret == 0
|
||||
assert "" == caplog.text
|
||||
|
||||
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
from copy import deepcopy
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from funcy import first
|
||||
|
||||
from dvc import stage as stage_module
|
||||
|
||||
pytest.importorskip("dvclive", reason="no dvclive")
|
||||
|
||||
|
||||
LIVE_SCRIPT = dedent(
|
||||
"""
|
||||
from dvclive import Live
|
||||
from PIL import Image
|
||||
|
||||
import sys
|
||||
r = 2
|
||||
metrics_logger = Live()
|
||||
for i in range(r):
|
||||
metrics_logger.log("loss", 1-i/r)
|
||||
metrics_logger.log("accuracy", i/r)
|
||||
image = Image.new("RGB", (10,10), (i,i,i))
|
||||
metrics_logger.log_image("image.jpg", image)
|
||||
metrics_logger.next_step()"""
|
||||
)
|
||||
|
||||
LIVE_CHECKPOINT_SCRIPT = dedent(
|
||||
"""
|
||||
import os
|
||||
from dvclive import Live
|
||||
|
||||
def read(path):
|
||||
value=0
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as fobj:
|
||||
try:
|
||||
value = int(fobj.read())
|
||||
except ValueError:
|
||||
pass
|
||||
return value
|
||||
|
||||
def dump(value, path):
|
||||
with open(path, "w") as fobj:
|
||||
fobj.write(str(value))
|
||||
|
||||
r = 3
|
||||
checkpoint_file = "checkpoint"
|
||||
metrics_logger = Live()
|
||||
|
||||
value = read(checkpoint_file)
|
||||
for i in range(1,r):
|
||||
m = i + value
|
||||
dump(m, checkpoint_file)
|
||||
|
||||
metrics_logger.log("metric1", m)
|
||||
metrics_logger.log("metric2", m * 2)
|
||||
metrics_logger.next_step()"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def live_stage(tmp_dir, scm, dvc, mocker):
|
||||
mocker.patch("dvc.stage.run.Monitor.AWAIT", 0.01)
|
||||
|
||||
def make(
|
||||
html=True,
|
||||
live=None,
|
||||
live_no_cache=None,
|
||||
code=LIVE_SCRIPT,
|
||||
):
|
||||
assert bool(live) != bool(live_no_cache)
|
||||
tmp_dir.gen("train.py", code)
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
stage = dvc.run(
|
||||
cmd="python train.py",
|
||||
params=["foo"],
|
||||
deps=["train.py"],
|
||||
name="live_stage",
|
||||
live=live,
|
||||
live_no_cache=live_no_cache,
|
||||
live_no_html=not html,
|
||||
)
|
||||
|
||||
scm.add(["dvc.yaml", "dvc.lock", "train.py", "params.yaml"])
|
||||
scm.commit("initial: live_stage")
|
||||
return stage
|
||||
|
||||
yield make
|
||||
|
||||
|
||||
@pytest.mark.parametrize("html", (True, False))
|
||||
def test_export_config(tmp_dir, dvc, mocker, live_stage, html):
|
||||
run_spy = mocker.spy(stage_module.run, "_run")
|
||||
live_stage(html=html, live="logs")
|
||||
|
||||
assert run_spy.call_count == 1
|
||||
_, kwargs = run_spy.call_args
|
||||
|
||||
assert "DVCLIVE_PATH" in kwargs["env"]
|
||||
assert kwargs["env"]["DVCLIVE_PATH"] == "logs"
|
||||
|
||||
assert "DVCLIVE_HTML" in kwargs["env"]
|
||||
assert kwargs["env"]["DVCLIVE_HTML"] == str(int(html))
|
||||
|
||||
|
||||
def test_live_provides_metrics(tmp_dir, dvc, live_stage):
|
||||
live_stage(live="logs")
|
||||
|
||||
assert (tmp_dir / "logs.json").is_file()
|
||||
assert dvc.metrics.show() == {
|
||||
"": {
|
||||
"data": {
|
||||
"logs.json": {
|
||||
"data": {"accuracy": 0.5, "loss": 0.5, "step": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert (tmp_dir / "logs").is_dir()
|
||||
plots_data = dvc.plots.show()
|
||||
files = list(plots_data["workspace"]["sources"]["data"])
|
||||
assert "logs/scalars/accuracy.tsv" in files
|
||||
assert "logs/scalars/loss.tsv" in files
|
||||
assert "logs/images/0/image.jpg" in files
|
||||
assert "logs/images/1/image.jpg" in files
|
||||
|
||||
|
||||
@pytest.mark.parametrize("typ", ("live", "live_no_cache"))
|
||||
def test_experiments_track_summary(tmp_dir, scm, dvc, live_stage, typ):
|
||||
live_stage(**{typ: "logs"})
|
||||
baseline_rev = scm.get_rev()
|
||||
|
||||
experiments = dvc.experiments.run(targets=["live_stage"], params=["foo=2"])
|
||||
assert len(experiments) == 1
|
||||
((exp_rev, _),) = experiments.items()
|
||||
|
||||
res = dvc.experiments.show()
|
||||
assert "logs.json" in res[baseline_rev][exp_rev]["data"]["metrics"].keys()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("html", [True, False])
|
||||
def test_live_html(tmp_dir, dvc, live_stage, html):
|
||||
live_stage(live="logs", html=html)
|
||||
|
||||
assert (tmp_dir / "logs_dvc_plots" / "index.html").is_file() == html
|
||||
if html:
|
||||
html_text = (tmp_dir / "logs_dvc_plots" / "index.html").read_text()
|
||||
assert 'http-equiv="refresh"' in html_text
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def live_checkpoint_stage(tmp_dir, scm, dvc, mocker):
|
||||
mocker.patch("dvc.stage.run.Monitor.AWAIT", 0.01)
|
||||
|
||||
def make(live=None, live_no_cache=None):
|
||||
assert bool(live) != bool(live_no_cache)
|
||||
|
||||
tmp_dir.gen("train.py", LIVE_CHECKPOINT_SCRIPT)
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
stage = dvc.run(
|
||||
cmd="python train.py",
|
||||
params=["foo"],
|
||||
deps=["train.py"],
|
||||
name="live_stage",
|
||||
live=live,
|
||||
live_no_cache=live_no_cache,
|
||||
checkpoints=["checkpoint"],
|
||||
no_exec=True,
|
||||
)
|
||||
|
||||
scm.add(["dvc.yaml", "train.py", "params.yaml", ".gitignore"])
|
||||
scm.commit("initial: live_stage")
|
||||
return stage
|
||||
|
||||
yield make
|
||||
|
||||
|
||||
def checkpoints_metric(show_results, metric_file, metric_name):
|
||||
tmp = deepcopy(show_results)
|
||||
tmp.pop("workspace")
|
||||
tmp = first(tmp.values())
|
||||
tmp.pop("baseline")
|
||||
return [
|
||||
exp["data"]["metrics"][metric_file]["data"][metric_name]
|
||||
for exp in tmp.values()
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("typ", ("live", "live_no_cache"))
|
||||
def test_live_checkpoints_resume(
|
||||
tmp_dir, scm, dvc, live_checkpoint_stage, typ
|
||||
):
|
||||
stage = live_checkpoint_stage(**{typ: "logs"})
|
||||
results = dvc.experiments.run(
|
||||
stage.addressing, params=["foo=2"], tmp_dir=False
|
||||
)
|
||||
|
||||
checkpoint_resume = first(results)
|
||||
|
||||
dvc.experiments.run(
|
||||
stage.addressing, checkpoint_resume=checkpoint_resume, tmp_dir=False
|
||||
)
|
||||
|
||||
results = dvc.experiments.show()
|
||||
assert checkpoints_metric(results, "logs.json", "step") == [3, 2, 1, 0]
|
||||
assert checkpoints_metric(results, "logs.json", "metric1") == [4, 3, 2, 1]
|
||||
assert checkpoints_metric(results, "logs.json", "metric2") == [8, 6, 4, 2]
|
||||
|
||||
|
||||
def test_dvclive_stage_with_different_wdir(tmp_dir, scm, dvc):
|
||||
(tmp_dir / "subdir").gen("train.py", LIVE_SCRIPT)
|
||||
(tmp_dir / "subdir" / "params.yaml").dump({"foo": 1})
|
||||
dvc.stage.add(
|
||||
cmd="python train.py",
|
||||
params=["foo"],
|
||||
deps=["train.py"],
|
||||
name="live_stage",
|
||||
live="dvclive",
|
||||
live_no_html=True,
|
||||
wdir="subdir",
|
||||
)
|
||||
|
||||
dvc.reproduce()
|
||||
|
||||
assert (tmp_dir / "subdir" / "dvclive").is_dir()
|
||||
assert (tmp_dir / "subdir" / "dvclive.json").is_file()
|
||||
+87
-156
@@ -4,198 +4,129 @@ import textwrap
|
||||
import pytest
|
||||
|
||||
from dvc.cli import main
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.exceptions import DvcException, MoveNotDataSourceError
|
||||
from dvc.utils.serialize import load_yaml
|
||||
from tests.basic_env import TestDvc, TestDvcGit
|
||||
from tests.func.test_repro import TestRepro
|
||||
from tests.utils import cd
|
||||
|
||||
|
||||
class TestMove(TestDvc):
|
||||
def test(self):
|
||||
dst = self.FOO + "1"
|
||||
self.dvc.add(self.FOO)
|
||||
self.dvc.move(self.FOO, dst)
|
||||
def test_move(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
dvc.move("foo", "foo1")
|
||||
|
||||
self.assertFalse(os.path.isfile(self.FOO))
|
||||
self.assertTrue(os.path.isfile(dst))
|
||||
assert not (tmp_dir / "foo").is_file()
|
||||
assert (tmp_dir / "foo1").is_file()
|
||||
|
||||
|
||||
class TestMoveNonExistentFile(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(DvcException):
|
||||
self.dvc.move("non_existent_file", "dst")
|
||||
def test_move_non_existent_file(dvc):
|
||||
with pytest.raises(DvcException):
|
||||
dvc.move("non_existent_file", "dst")
|
||||
|
||||
|
||||
class TestMoveDirectory(TestDvc):
|
||||
def test(self):
|
||||
dst = "dst"
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
self.dvc.move(self.DATA_DIR, dst)
|
||||
self.assertFalse(os.path.exists(self.DATA_DIR))
|
||||
self.assertTrue(os.path.exists(dst))
|
||||
def test_move_directory(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("data", {"foo": "foo", "bar": "bar"})
|
||||
dvc.move("data", "dst")
|
||||
assert not (tmp_dir / "data").is_dir()
|
||||
assert (tmp_dir / "dst").is_dir()
|
||||
|
||||
|
||||
class TestCmdMove(TestDvc):
|
||||
def test(self):
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
|
||||
ret = main(["move", self.FOO, self.FOO + "1"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(["move", "non-existing-file", "dst"])
|
||||
self.assertNotEqual(ret, 0)
|
||||
def test_cmd_move(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
assert main(["move", "foo", "foo" + "1"]) == 0
|
||||
assert main(["move", "non-existing-file", "dst"]) != 0
|
||||
|
||||
|
||||
class TestMoveNotDataSource(TestRepro):
|
||||
def test(self):
|
||||
from dvc.repo import Repo as DvcRepo
|
||||
def test_move_not_data_source(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
dvc.run(
|
||||
cmd="cp foo file1",
|
||||
outs=["file1"],
|
||||
deps=["foo"],
|
||||
single_stage=True,
|
||||
)
|
||||
|
||||
self.dvc = DvcRepo(self._root_dir)
|
||||
with self.assertRaises(MoveNotDataSourceError):
|
||||
self.dvc.move(self.file1, "dst")
|
||||
with pytest.raises(MoveNotDataSourceError):
|
||||
dvc.move("file1", "dst")
|
||||
|
||||
ret = main(["move", self.file1, "dst"])
|
||||
self.assertNotEqual(ret, 0)
|
||||
assert main(["move", "file1", "dst"]) != 0
|
||||
assert (tmp_dir / "file1").exists()
|
||||
|
||||
|
||||
class TestMoveFileWithExtension(TestDvc):
|
||||
def test(self):
|
||||
with open(
|
||||
os.path.join(self.dvc.root_dir, "file.csv"), "w", encoding="utf-8"
|
||||
) as fd:
|
||||
fd.write("1,2,3\n")
|
||||
def test_move_file_with_extension(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("file.csv", "1,2,3\n")
|
||||
|
||||
self.dvc.add("file.csv")
|
||||
|
||||
self.assertTrue(os.path.exists("file.csv"))
|
||||
self.assertTrue(os.path.exists("file.csv.dvc"))
|
||||
|
||||
ret = main(["move", "file.csv", "other_name.csv"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertFalse(os.path.exists("file.csv"))
|
||||
self.assertFalse(os.path.exists("file.csv.dvc"))
|
||||
self.assertTrue(os.path.exists("other_name.csv"))
|
||||
self.assertTrue(os.path.exists("other_name.csv.dvc"))
|
||||
assert main(["move", "file.csv", "other_name.csv"]) == 0
|
||||
assert not (tmp_dir / "file.csv").exists()
|
||||
assert not (tmp_dir / "file.csv.dvc").exists()
|
||||
assert (tmp_dir / "other_name.csv").exists()
|
||||
assert (tmp_dir / "other_name.csv.dvc").exists()
|
||||
|
||||
|
||||
class TestMoveFileToDirectory(TestDvc):
|
||||
def test(self):
|
||||
foo_dvc_file = self.FOO + DVC_FILE_SUFFIX
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.exists(foo_dvc_file))
|
||||
def test_move_file_to_directory(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
tmp_dir.gen({"data": {"bar": "bar"}})
|
||||
|
||||
new_foo_path = os.path.join(self.DATA_DIR, self.FOO)
|
||||
new_foo_dvc_path = new_foo_path + DVC_FILE_SUFFIX
|
||||
ret = main(["move", self.FOO, new_foo_path])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertFalse(os.path.exists(self.FOO))
|
||||
self.assertFalse(os.path.exists(foo_dvc_file))
|
||||
self.assertTrue(os.path.exists(new_foo_path))
|
||||
self.assertTrue(os.path.exists(new_foo_dvc_path))
|
||||
assert main(["move", "foo", os.path.join("data", "foo")]) == 0
|
||||
assert not (tmp_dir / "foo").exists()
|
||||
assert not (tmp_dir / "foo.dvc").exists()
|
||||
assert (tmp_dir / "data" / "foo").exists()
|
||||
assert (tmp_dir / "data" / "foo.dvc").exists()
|
||||
|
||||
|
||||
class TestMoveFileToDirectoryWithoutSpecifiedTargetName(TestDvc):
|
||||
def test(self):
|
||||
foo_stage_file_path = self.FOO + DVC_FILE_SUFFIX
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.exists(foo_stage_file_path))
|
||||
def test_move_file_to_directory_without_specified_target_name(tmp_dir, dvc):
|
||||
tmp_dir.dvc_gen("foo", "foo")
|
||||
tmp_dir.gen({"data": {"bar": "bar"}})
|
||||
|
||||
target_foo_path = os.path.join(self.DATA_DIR, self.FOO)
|
||||
target_foo_stage_file_path = target_foo_path + DVC_FILE_SUFFIX
|
||||
assert main(["move", "foo", "data"]) == 0
|
||||
assert not (tmp_dir / "foo").exists()
|
||||
assert not (tmp_dir / "foo.dvc").exists()
|
||||
assert (tmp_dir / "data" / "foo").exists()
|
||||
assert (tmp_dir / "data" / "foo.dvc").exists()
|
||||
|
||||
ret = main(["move", self.FOO, self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertFalse(os.path.exists(self.FOO))
|
||||
self.assertFalse(os.path.exists(foo_stage_file_path))
|
||||
|
||||
self.assertTrue(os.path.exists(target_foo_path))
|
||||
self.assertTrue(os.path.exists(target_foo_stage_file_path))
|
||||
|
||||
new_stage = load_yaml(target_foo_stage_file_path)
|
||||
self.assertEqual(self.FOO, new_stage["outs"][0]["path"])
|
||||
new_stage = (tmp_dir / "data" / "foo.dvc").load_yaml()
|
||||
assert new_stage["outs"][0]["path"] == "foo"
|
||||
|
||||
|
||||
class TestMoveDirectoryShouldNotOverwriteExisting(TestDvcGit):
|
||||
def test(self):
|
||||
dir_name = "dir"
|
||||
orig_listdir = set(os.listdir(self.DATA_DIR))
|
||||
def test_move_directory_should_not_overwrite_existing(tmp_dir, dvc, scm):
|
||||
tmp_dir.dvc_gen({"data": {"foo": "foo"}})
|
||||
new_dir = tmp_dir / "dir"
|
||||
new_dir.mkdir()
|
||||
|
||||
self.dvc.add(self.DATA_DIR)
|
||||
|
||||
os.mkdir(dir_name)
|
||||
new_dir_name = os.path.join(dir_name, self.DATA_DIR)
|
||||
|
||||
self.dvc.move(self.DATA_DIR, dir_name)
|
||||
|
||||
data_dir_stage = self.DATA_DIR + DVC_FILE_SUFFIX
|
||||
self.assertFalse(os.path.exists(self.DATA_DIR))
|
||||
self.assertFalse(os.path.exists(data_dir_stage))
|
||||
|
||||
self.assertTrue(os.path.exists(dir_name))
|
||||
self.assertEqual(
|
||||
set(os.listdir(dir_name)),
|
||||
{".gitignore", data_dir_stage, self.DATA_DIR},
|
||||
)
|
||||
|
||||
self.assertTrue(os.path.exists(new_dir_name))
|
||||
self.assertTrue(os.path.isfile(new_dir_name + DVC_FILE_SUFFIX))
|
||||
self.assertEqual(set(os.listdir(new_dir_name)), orig_listdir)
|
||||
dvc.move("data", "dir")
|
||||
assert not (tmp_dir / "data").exists()
|
||||
assert not (tmp_dir / "data.dvc").exists()
|
||||
assert set(new_dir.iterdir()) == {
|
||||
new_dir / ".gitignore",
|
||||
new_dir / "data.dvc",
|
||||
new_dir / "data",
|
||||
}
|
||||
assert set((new_dir / "data").iterdir()) == {new_dir / "data" / "foo"}
|
||||
|
||||
|
||||
class TestMoveFileBetweenDirectories(TestDvc):
|
||||
def test(self):
|
||||
data_stage_file = self.DATA + DVC_FILE_SUFFIX
|
||||
ret = main(["add", self.DATA])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.exists(data_stage_file))
|
||||
def test_move_file_between_directories(tmp_dir, dvc):
|
||||
tmp_dir.gen({"data": {"foo": "foo"}})
|
||||
dvc.add(os.path.join("data", "foo"))
|
||||
|
||||
new_data_dir = "data_dir2"
|
||||
os.makedirs(new_data_dir)
|
||||
(tmp_dir / "data2").mkdir()
|
||||
|
||||
ret = main(["move", self.DATA, new_data_dir])
|
||||
self.assertEqual(ret, 0)
|
||||
assert main(["move", os.path.join("data", "foo"), "data2"]) == 0
|
||||
assert not (tmp_dir / "data" / "foo").exists()
|
||||
assert not (tmp_dir / "data" / "foo.dvc").exists()
|
||||
assert (tmp_dir / "data2" / "foo").exists()
|
||||
assert (tmp_dir / "data2" / "foo.dvc").exists()
|
||||
|
||||
new_data_path = os.path.join(new_data_dir, os.path.basename(self.DATA))
|
||||
new_data_stage_file = new_data_path + DVC_FILE_SUFFIX
|
||||
|
||||
self.assertFalse(os.path.exists(self.DATA))
|
||||
self.assertFalse(os.path.exists(data_stage_file))
|
||||
|
||||
self.assertTrue(os.path.exists(new_data_path))
|
||||
self.assertTrue(os.path.exists(new_data_stage_file))
|
||||
|
||||
new_stage_file = load_yaml(new_data_stage_file)
|
||||
self.assertEqual(
|
||||
os.path.basename(self.DATA), new_stage_file["outs"][0]["path"]
|
||||
)
|
||||
d = (tmp_dir / "data2" / "foo.dvc").load_yaml()
|
||||
assert d["outs"][0]["path"] == "foo"
|
||||
|
||||
|
||||
class TestMoveFileInsideDirectory(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["add", self.DATA])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_move_file_inside_directory(tmp_dir, dvc):
|
||||
tmp_dir.gen({"data": {"foo": "foo"}})
|
||||
file = tmp_dir / "data" / "foo"
|
||||
dvc.add(file.fs_path)
|
||||
|
||||
with cd(self.DATA_DIR):
|
||||
ret = main(["move", os.path.basename(self.DATA), "data.txt"])
|
||||
self.assertEqual(ret, 0)
|
||||
with (tmp_dir / "data").chdir():
|
||||
assert main(["move", "foo", "data.txt"]) == 0
|
||||
|
||||
self.assertFalse(os.path.exists(self.DATA))
|
||||
|
||||
data_fullpath = os.path.join(self.DATA_DIR, "data.txt")
|
||||
dvc_fullpath = os.path.join(self.DATA_DIR, "data.txt.dvc")
|
||||
self.assertTrue(os.path.exists(data_fullpath))
|
||||
self.assertTrue(os.path.exists(dvc_fullpath))
|
||||
assert not file.exists()
|
||||
assert (tmp_dir / "data" / "data.txt").exists()
|
||||
assert (tmp_dir / "data" / "data.txt.dvc").exists()
|
||||
|
||||
|
||||
def test_move_should_save_stage_info(tmp_dir, dvc):
|
||||
|
||||
+56
-65
@@ -10,58 +10,51 @@ from dvc.cli import main
|
||||
from dvc.config import Config
|
||||
from dvc.exceptions import DownloadError, RemoteCacheRequiredError, UploadError
|
||||
from dvc.utils.fs import remove
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestRemote(TestDvc):
|
||||
def test(self):
|
||||
remotes = ["a", "b", "c"]
|
||||
def test_remote(dvc):
|
||||
remotes = ["a", "b", "c"]
|
||||
|
||||
self.assertEqual(main(["remote", "list"]), 0)
|
||||
self.assertNotEqual(main(["remote", "remove", remotes[0]]), 0)
|
||||
assert main(["remote", "list"]) == 0
|
||||
assert main(["remote", "remove", remotes[0]]) != 0
|
||||
|
||||
for r in remotes:
|
||||
self.assertEqual(
|
||||
main(["remote", "add", "--default", r, "s3://bucket/name"]), 0
|
||||
)
|
||||
for r in remotes:
|
||||
assert main(["remote", "add", "--default", r, "s3://bucket/name"]) == 0
|
||||
|
||||
self.assertEqual(main(["remote", "list"]), 0)
|
||||
assert main(["remote", "list"]) == 0
|
||||
|
||||
self.assertEqual(
|
||||
main(["remote", "modify", remotes[0], "checksum_jobs", "1"]), 0
|
||||
)
|
||||
self.assertEqual(main(["remote", "remove", remotes[0]]), 0)
|
||||
assert main(["remote", "modify", remotes[0], "checksum_jobs", "1"]) == 0
|
||||
assert main(["remote", "remove", remotes[0]]) == 0
|
||||
|
||||
self.assertEqual(main(["remote", "list"]), 0)
|
||||
assert main(["remote", "list"]) == 0
|
||||
|
||||
def test_relative_path(self):
|
||||
dname = os.path.join("..", "path", "to", "dir")
|
||||
ret = main(["remote", "add", "mylocal", dname])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
# NOTE: we are in the repo's root and config is in .dvc/, so
|
||||
# dir path written to config should be just one level above.
|
||||
rel = os.path.join("..", dname)
|
||||
config = configobj.ConfigObj(self.dvc.config.files["repo"])
|
||||
self.assertEqual(
|
||||
config['remote "mylocal"']["url"], rel.replace("\\", "/")
|
||||
)
|
||||
def test_remote_add_relative_path(dvc):
|
||||
dname = os.path.join("..", "path", "to", "dir")
|
||||
ret = main(["remote", "add", "mylocal", dname])
|
||||
assert ret == 0
|
||||
|
||||
def test_overwrite(self):
|
||||
remote_name = "a"
|
||||
remote_url = "s3://bucket/name"
|
||||
self.assertEqual(main(["remote", "add", remote_name, remote_url]), 0)
|
||||
self.assertEqual(main(["remote", "add", remote_name, remote_url]), 251)
|
||||
self.assertEqual(
|
||||
main(["remote", "add", "-f", remote_name, remote_url]), 0
|
||||
)
|
||||
# NOTE: we are in the repo's root and config is in .dvc/, so
|
||||
# dir path written to config should be just one level above.
|
||||
rel = os.path.join("..", dname)
|
||||
config = configobj.ConfigObj(dvc.config.files["repo"])
|
||||
assert config['remote "mylocal"']["url"] == rel.replace("\\", "/")
|
||||
|
||||
def test_referencing_other_remotes(self):
|
||||
assert main(["remote", "add", "foo", "ssh://localhost/"]) == 0
|
||||
assert main(["remote", "add", "bar", "remote://foo/dvc-storage"]) == 0
|
||||
|
||||
config = configobj.ConfigObj(self.dvc.config.files["repo"])
|
||||
assert config['remote "bar"']["url"] == "remote://foo/dvc-storage"
|
||||
def test_remote_overwrite(dvc):
|
||||
remote_name = "a"
|
||||
remote_url = "s3://bucket/name"
|
||||
assert main(["remote", "add", remote_name, remote_url]) == 0
|
||||
assert main(["remote", "add", remote_name, remote_url]) == 251
|
||||
assert main(["remote", "add", "-f", remote_name, remote_url]) == 0
|
||||
|
||||
|
||||
def test_referencing_other_remotes(dvc):
|
||||
assert main(["remote", "add", "foo", "ssh://localhost/"]) == 0
|
||||
assert main(["remote", "add", "bar", "remote://foo/dvc-storage"]) == 0
|
||||
|
||||
config = configobj.ConfigObj(dvc.config.files["repo"])
|
||||
assert config['remote "bar"']["url"] == "remote://foo/dvc-storage"
|
||||
|
||||
|
||||
def test_remove_default(tmp_dir, dvc):
|
||||
@@ -85,37 +78,35 @@ def test_remove_default(tmp_dir, dvc):
|
||||
assert local_config.get("core", {}).get("remote") is None
|
||||
|
||||
|
||||
class TestRemoteRemove(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["config", "core.checksum_jobs", "1"])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_remote_remove(dvc):
|
||||
ret = main(["config", "core.checksum_jobs", "1"])
|
||||
assert ret == 0
|
||||
|
||||
remote = "mys3"
|
||||
ret = main(["remote", "add", remote, "s3://bucket/name"])
|
||||
self.assertEqual(ret, 0)
|
||||
remote = "mys3"
|
||||
ret = main(["remote", "add", remote, "s3://bucket/name"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["remote", "remove", remote])
|
||||
self.assertEqual(ret, 0)
|
||||
ret = main(["remote", "remove", remote])
|
||||
assert ret == 0
|
||||
|
||||
|
||||
class TestRemoteDefault(TestDvc):
|
||||
def test(self):
|
||||
remote = "mys3"
|
||||
ret = main(["remote", "add", "mys3", "s3://bucket/path"])
|
||||
self.assertEqual(ret, 0)
|
||||
def test_remote_default_cmd(dvc):
|
||||
remote = "mys3"
|
||||
ret = main(["remote", "add", "mys3", "s3://bucket/path"])
|
||||
assert ret == 0
|
||||
|
||||
ret = main(["remote", "default", "mys3"])
|
||||
self.assertEqual(ret, 0)
|
||||
config_file = os.path.join(self.dvc.dvc_dir, Config.CONFIG)
|
||||
config = configobj.ConfigObj(config_file)
|
||||
default = config["core"]["remote"]
|
||||
self.assertEqual(default, remote)
|
||||
ret = main(["remote", "default", "mys3"])
|
||||
assert ret == 0
|
||||
config_file = os.path.join(dvc.dvc_dir, Config.CONFIG)
|
||||
config = configobj.ConfigObj(config_file)
|
||||
default = config["core"]["remote"]
|
||||
assert default == remote
|
||||
|
||||
ret = main(["remote", "default", "--unset"])
|
||||
self.assertEqual(ret, 0)
|
||||
config = configobj.ConfigObj(config_file)
|
||||
default = config.get("core", {}).get("remote")
|
||||
self.assertEqual(default, None)
|
||||
ret = main(["remote", "default", "--unset"])
|
||||
assert ret == 0
|
||||
config = configobj.ConfigObj(config_file)
|
||||
default = config.get("core", {}).get("remote")
|
||||
assert default is None
|
||||
|
||||
|
||||
def test_show_default(dvc, capsys):
|
||||
|
||||
@@ -220,8 +220,8 @@ def test_unique_identifier(tmp_dir, dvc, scm, run_copy):
|
||||
# we get "workspace" as well from the brancher by default
|
||||
assert len(revs) + 1 == len(ids)
|
||||
possible_ids = {
|
||||
True: "2ba7c7c5b395d4211348d6274b869fc7",
|
||||
False: "8406970ad2fcafaa84d9310330a67576",
|
||||
True: "dd9cf3a3dc2caac5835e64f258ac2d7b",
|
||||
False: "b446f3cdc35539884dcc474d90ac6286",
|
||||
}
|
||||
assert set(ids) == {possible_ids[os.name == "posix"]}
|
||||
|
||||
@@ -303,3 +303,85 @@ def test_getitem(tmp_dir, dvc, run_copy):
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_ = index["no-valid-stage-name"]
|
||||
|
||||
|
||||
def test_view_granular_dir(tmp_dir, scm, dvc, run_copy):
|
||||
tmp_dir.dvc_gen(
|
||||
{"dir": {"subdir": {"in-subdir": "in-subdir"}, "in-dir": "in-dir"}},
|
||||
commit="init",
|
||||
)
|
||||
index = Index(dvc)
|
||||
|
||||
# view should include the specific target, parent dirs, and children
|
||||
# view should exclude any siblings of the target
|
||||
view = index.targets_view("dir/subdir")
|
||||
data_index = view.data["repo"]
|
||||
assert ("dir",) in data_index
|
||||
assert (
|
||||
"dir",
|
||||
"subdir",
|
||||
) in data_index
|
||||
assert ("dir", "subdir", "in-subdir") in data_index
|
||||
assert (
|
||||
"dir",
|
||||
"in-dir",
|
||||
) not in data_index
|
||||
|
||||
|
||||
def test_view_stage_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(None)
|
||||
assert set(view.stages) == {stage1, stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in (stage1.outs + stage2.outs)
|
||||
}
|
||||
|
||||
view = index.targets_view(
|
||||
None, stage_filter=lambda s: getattr(s, "name", "").startswith("copy")
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage2.outs
|
||||
}
|
||||
|
||||
|
||||
def test_view_outs_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(None, outs_filter=lambda o: o.def_path == "foo")
|
||||
assert set(view.stages) == {stage1, stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage1.outs
|
||||
}
|
||||
|
||||
|
||||
def test_view_combined_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(
|
||||
None,
|
||||
stage_filter=lambda s: getattr(s, "name", "").startswith("copy"),
|
||||
outs_filter=lambda o: o.def_path == "foo",
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert set(view.outs) == set()
|
||||
|
||||
view = index.targets_view(
|
||||
None,
|
||||
stage_filter=lambda s: getattr(s, "name", "").startswith("copy"),
|
||||
outs_filter=lambda o: o.def_path == "bar",
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage2.outs
|
||||
}
|
||||
|
||||
+731
-740
File diff suppressed because it is too large
Load Diff
@@ -9,149 +9,6 @@ from dvc.cli import main
|
||||
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
|
||||
from dvc.exceptions import CyclicGraphError, ReproductionError
|
||||
from dvc.stage import PipelineStage
|
||||
from tests.func import test_repro
|
||||
from tests.utils.scriptify import scriptify
|
||||
|
||||
|
||||
def copy_script(src, dest):
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(src, dest)
|
||||
|
||||
|
||||
COPY_SCRIPT, _ = scriptify(copy_script)
|
||||
|
||||
|
||||
class MultiStageRun:
|
||||
def _run(self, **kwargs):
|
||||
assert kwargs.get("name")
|
||||
kwargs.pop("fname", None)
|
||||
# ignore fname for now
|
||||
return self.dvc.run(**kwargs) # noqa, pylint: disable=no-member
|
||||
|
||||
@staticmethod
|
||||
def _get_stage_target(stage):
|
||||
return stage.addressing
|
||||
|
||||
|
||||
class TestReproFailMultiStage(MultiStageRun, test_repro.TestReproFail):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproCyclicGraphMultiStage(
|
||||
MultiStageRun, test_repro.TestReproCyclicGraph
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproUnderDirMultiStage(
|
||||
MultiStageRun, test_repro.TestReproDepUnderDir
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproDepDirWithOutputsUnderItMultiStage(
|
||||
MultiStageRun, test_repro.TestReproDepDirWithOutputsUnderIt
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproForceMultiStage(MultiStageRun, test_repro.TestReproForce):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproChangedCodeMultiStage(
|
||||
MultiStageRun, test_repro.TestReproChangedCode
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproChangedDataMultiStage(
|
||||
MultiStageRun, test_repro.TestReproChangedData
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproDry(MultiStageRun, test_repro.TestReproDry):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproUpToDateMultiStage(MultiStageRun, test_repro.TestReproUpToDate):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproChangedDeepDataMultiStage(
|
||||
MultiStageRun, test_repro.TestReproChangedDeepData
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproPipelineMultiStage(MultiStageRun, test_repro.TestReproPipeline):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproPipelinesMultiStage(
|
||||
MultiStageRun, test_repro.TestReproPipelines
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproFrozenMultiStage(MultiStageRun, test_repro.TestReproFrozen):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproFrozenCallbackMultiStage(
|
||||
MultiStageRun, test_repro.TestReproFrozenCallback
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproFrozenUnchangedMultiStage(
|
||||
MultiStageRun, test_repro.TestReproFrozenUnchanged
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproPhonyMultiStage(MultiStageRun, test_repro.TestReproPhony):
|
||||
pass
|
||||
|
||||
|
||||
class TestCmdReproMultiStage(MultiStageRun, test_repro.TestCmdRepro):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproAllPipelinesMultiStage(
|
||||
MultiStageRun, test_repro.TestReproAllPipelines
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproNoCommit(MultiStageRun, test_repro.TestReproNoCommit):
|
||||
pass
|
||||
|
||||
|
||||
class TestNonExistingOutputMultiStage(
|
||||
MultiStageRun, test_repro.TestNonExistingOutput
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproAlreadyCachedMultiStage(
|
||||
MultiStageRun, test_repro.TestReproAlreadyCached
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproChangedDirMultiStage(
|
||||
MultiStageRun, test_repro.TestReproChangedDir
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestReproChangedDirDataMultiStage(
|
||||
MultiStageRun, test_repro.TestReproChangedDirData
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def test_non_existing_stage_name(tmp_dir, dvc, run_copy):
|
||||
@@ -300,10 +157,11 @@ def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy, mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_repro_when_new_deps_is_added_in_dvcfile(tmp_dir, dvc, run_copy):
|
||||
def test_repro_when_new_deps_is_added_in_dvcfile(
|
||||
tmp_dir, dvc, run_copy, copy_script
|
||||
):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen({"foo": "foo", "bar": "bar"})
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py {} {}".format("foo", "foobar"),
|
||||
@@ -322,10 +180,9 @@ def test_repro_when_new_deps_is_added_in_dvcfile(tmp_dir, dvc, run_copy):
|
||||
assert dvc.reproduce(target)[0] == stage
|
||||
|
||||
|
||||
def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc):
|
||||
def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc, copy_script):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen({"foo": "foo", "bar": "bar"})
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py {} {}".format("foo", "foobar"),
|
||||
@@ -344,10 +201,9 @@ def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc):
|
||||
assert dvc.reproduce(target)[0] == stage
|
||||
|
||||
|
||||
def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
|
||||
def test_repro_when_new_deps_is_moved(tmp_dir, dvc, copy_script):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen({"foo": "foo", "bar": "foo"})
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py {} {}".format("foo", "foobar"),
|
||||
@@ -360,7 +216,12 @@ def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
|
||||
|
||||
# hardcode values in source code, ignore sys.argv
|
||||
tmp_dir.gen(
|
||||
"copy.py", COPY_SCRIPT.replace("func(*argv)", 'func("bar", "foobar")')
|
||||
"copy.py",
|
||||
"""
|
||||
import shutil
|
||||
|
||||
shutil.copyfile('bar', 'foobar')
|
||||
""",
|
||||
)
|
||||
from shutil import move
|
||||
|
||||
@@ -394,8 +255,7 @@ def test_repro_when_new_out_overlaps_others_stage_outs(tmp_dir, dvc):
|
||||
dvc.reproduce(":run-copy")
|
||||
|
||||
|
||||
def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
(tmp_dir / PIPELINE_FILE).dump(
|
||||
{
|
||||
@@ -412,8 +272,7 @@ def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
|
||||
dvc.reproduce(":run-copy")
|
||||
|
||||
|
||||
def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
(tmp_dir / PIPELINE_FILE).dump(
|
||||
{
|
||||
@@ -430,8 +289,7 @@ def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
|
||||
dvc.reproduce(":run-copy")
|
||||
|
||||
|
||||
def test_repro_when_lockfile_gets_deleted(tmp_dir, dvc):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_repro_when_lockfile_gets_deleted(tmp_dir, dvc, copy_script):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
(tmp_dir / PIPELINE_FILE).dump(
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_restore(tmp_dir, dvc, run_copy, mocker):
|
||||
|
||||
(stage,) = dvc.reproduce("copy-foo-bar")
|
||||
|
||||
mock_restore.assert_called_once_with(stage)
|
||||
mock_restore.assert_called_once_with(stage, dry=False)
|
||||
mock_run.assert_not_called()
|
||||
assert (tmp_dir / "bar").exists() and not (tmp_dir / "foo").unlink()
|
||||
assert (tmp_dir / PIPELINE_LOCK).exists()
|
||||
@@ -100,7 +100,7 @@ def test_memory_for_multiple_runs_of_same_stage(
|
||||
assert (tmp_dir / PIPELINE_LOCK).exists()
|
||||
assert (tmp_dir / "bar").read_text() == "foobar"
|
||||
mock_run.assert_not_called()
|
||||
mock_restore.assert_called_once_with(stage)
|
||||
mock_restore.assert_called_once_with(stage, dry=False)
|
||||
mock_restore.reset_mock()
|
||||
|
||||
(tmp_dir / PIPELINE_LOCK).unlink()
|
||||
@@ -109,7 +109,7 @@ def test_memory_for_multiple_runs_of_same_stage(
|
||||
|
||||
assert (tmp_dir / "bar").read_text() == "foo"
|
||||
mock_run.assert_not_called()
|
||||
mock_restore.assert_called_once_with(stage)
|
||||
mock_restore.assert_called_once_with(stage, dry=False)
|
||||
assert (tmp_dir / "bar").exists() and not (tmp_dir / "foo").unlink()
|
||||
assert (tmp_dir / PIPELINE_LOCK).exists()
|
||||
|
||||
@@ -138,7 +138,7 @@ def test_memory_runs_of_multiple_stages(tmp_dir, dvc, run_copy, mocker):
|
||||
assert (tmp_dir / "foo.bak").read_text() == "foo"
|
||||
assert (tmp_dir / PIPELINE_LOCK).exists()
|
||||
mock_run.assert_not_called()
|
||||
mock_restore.assert_called_once_with(stage)
|
||||
mock_restore.assert_called_once_with(stage, dry=False)
|
||||
mock_restore.reset_mock()
|
||||
|
||||
(stage,) = dvc.reproduce("backup-bar")
|
||||
@@ -146,7 +146,7 @@ def test_memory_runs_of_multiple_stages(tmp_dir, dvc, run_copy, mocker):
|
||||
assert (tmp_dir / "bar.bak").read_text() == "bar"
|
||||
assert (tmp_dir / PIPELINE_LOCK).exists()
|
||||
mock_run.assert_not_called()
|
||||
mock_restore.assert_called_once_with(stage)
|
||||
mock_restore.assert_called_once_with(stage, dry=False)
|
||||
|
||||
|
||||
def test_restore_pull(tmp_dir, dvc, run_copy, mocker, local_remote):
|
||||
@@ -168,7 +168,7 @@ def test_restore_pull(tmp_dir, dvc, run_copy, mocker, local_remote):
|
||||
|
||||
(stage,) = dvc.reproduce("copy-foo-bar", pull=True)
|
||||
|
||||
mock_restore.assert_called_once_with(stage, pull=True)
|
||||
mock_restore.assert_called_once_with(stage, pull=True, dry=False)
|
||||
mock_run.assert_not_called()
|
||||
assert mock_checkout.call_count == 2
|
||||
assert (tmp_dir / "bar").exists() and not (tmp_dir / "foo").unlink()
|
||||
|
||||
+498
-605
File diff suppressed because it is too large
Load Diff
+53
-72
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.annotations import Annotation
|
||||
from dvc.cli import main
|
||||
from dvc.dvcfile import SingleStageFile
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.output import Output
|
||||
@@ -15,7 +13,6 @@ from dvc.stage.utils import compute_md5
|
||||
from dvc.utils import dict_md5
|
||||
from dvc.utils.serialize import dump_yaml, load_yaml
|
||||
from dvc.utils.strictyaml import YAMLValidationError
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
def test_cmd_obj():
|
||||
@@ -71,90 +68,74 @@ def test_list():
|
||||
SingleStageFile.validate(d)
|
||||
|
||||
|
||||
class TestReload(TestDvc):
|
||||
def test(self):
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
def test_reload(tmp_dir, dvc):
|
||||
(stage,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
d = load_yaml(stage.relpath)
|
||||
|
||||
d = load_yaml(stage.relpath)
|
||||
# NOTE: checking that reloaded stage didn't change its checksum
|
||||
md5 = "11111111111111111111111111111111"
|
||||
d[stage.PARAM_MD5] = md5
|
||||
dump_yaml(stage.relpath, d)
|
||||
|
||||
# NOTE: checking that reloaded stage didn't change its checksum
|
||||
md5 = "11111111111111111111111111111111"
|
||||
d[stage.PARAM_MD5] = md5
|
||||
dump_yaml(stage.relpath, d)
|
||||
dvcfile = SingleStageFile(dvc, stage.relpath)
|
||||
stage = dvcfile.stage
|
||||
|
||||
dvcfile = SingleStageFile(self.dvc, stage.relpath)
|
||||
stage = dvcfile.stage
|
||||
assert stage is not None
|
||||
dvcfile.dump(stage)
|
||||
|
||||
self.assertTrue(stage is not None)
|
||||
dvcfile.dump(stage)
|
||||
|
||||
d = load_yaml(stage.relpath)
|
||||
self.assertEqual(d[stage.PARAM_MD5], md5)
|
||||
d = load_yaml(stage.relpath)
|
||||
assert d[stage.PARAM_MD5] == md5
|
||||
|
||||
|
||||
class TestDefaultWorkingDirectory(TestDvc):
|
||||
def test_ignored_in_checksum(self):
|
||||
stage = self.dvc.run(
|
||||
cmd=f"echo test > {self.FOO}",
|
||||
deps=[self.BAR],
|
||||
outs=[self.FOO],
|
||||
single_stage=True,
|
||||
)
|
||||
def test_default_wdir_ignored_in_checksum(tmp_dir, dvc):
|
||||
tmp_dir.gen("bar", "bar")
|
||||
stage = dvc.run(
|
||||
cmd="cp bar foo",
|
||||
deps=["bar"],
|
||||
outs=["foo"],
|
||||
single_stage=True,
|
||||
)
|
||||
|
||||
d = stage.dumpd()
|
||||
self.assertNotIn(Stage.PARAM_WDIR, d.keys())
|
||||
d = stage.dumpd()
|
||||
assert Stage.PARAM_WDIR not in d.keys()
|
||||
|
||||
d = load_yaml(stage.relpath)
|
||||
self.assertNotIn(Stage.PARAM_WDIR, d.keys())
|
||||
d = load_yaml(stage.relpath)
|
||||
assert Stage.PARAM_WDIR not in d.keys()
|
||||
|
||||
with self.dvc.lock:
|
||||
stage = SingleStageFile(self.dvc, stage.relpath).stage
|
||||
self.assertFalse(stage.changed())
|
||||
with dvc.lock:
|
||||
stage = SingleStageFile(dvc, stage.relpath).stage
|
||||
assert not stage.changed()
|
||||
|
||||
|
||||
class TestExternalRemoteResolution(TestDvc):
|
||||
def test_remote_output(self):
|
||||
tmp_path = tempfile.mkdtemp()
|
||||
storage = os.path.join(tmp_path, "storage")
|
||||
file_path = os.path.join(storage, "file")
|
||||
def test_external_remote_output_resolution(tmp_dir, dvc, make_remote):
|
||||
tmp_path = make_remote("tmp", default=False)
|
||||
tmp_dir.add_remote(
|
||||
url="remote://tmp/storage", name="storage", default=False
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
storage.mkdir()
|
||||
file_path = storage / "file"
|
||||
|
||||
os.makedirs(storage)
|
||||
dvc.run(
|
||||
cmd=f"echo file > {file_path}",
|
||||
outs_no_cache=["remote://storage/file"],
|
||||
single_stage=True,
|
||||
)
|
||||
assert os.path.exists(file_path)
|
||||
|
||||
assert main(["remote", "add", "tmp", tmp_path]) == 0
|
||||
assert main(["remote", "add", "storage", "remote://tmp/storage"]) == 0
|
||||
assert (
|
||||
main(
|
||||
[
|
||||
"run",
|
||||
"--single-stage",
|
||||
"-O",
|
||||
"remote://storage/file",
|
||||
f"echo file > {file_path}",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
assert os.path.exists(file_path)
|
||||
def test_external_remote_dependency_resolution(tmp_dir, dvc, make_remote):
|
||||
tmp_path = make_remote("tmp", default=False)
|
||||
tmp_dir.add_remote(
|
||||
url="remote://tmp/storage", name="storage", default=False
|
||||
)
|
||||
storage = tmp_path / "storage"
|
||||
storage.mkdir()
|
||||
file_path = storage / "file"
|
||||
file_path.write_text("Isle of Dogs", encoding="utf-8")
|
||||
|
||||
def test_remote_dependency(self):
|
||||
tmp_path = tempfile.mkdtemp()
|
||||
storage = os.path.join(tmp_path, "storage")
|
||||
file_path = os.path.join(storage, "file")
|
||||
|
||||
os.makedirs(storage)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write("Isle of Dogs")
|
||||
|
||||
assert main(["remote", "add", "tmp", tmp_path]) == 0
|
||||
assert main(["remote", "add", "storage", "remote://tmp/storage"]) == 0
|
||||
assert main(["import-url", "remote://storage/file", "movie.txt"]) == 0
|
||||
|
||||
assert os.path.exists("movie.txt")
|
||||
dvc.imp_url("remote://storage/file", "movie.txt")
|
||||
assert (tmp_dir / "movie.txt").read_text() == "Isle of Dogs"
|
||||
|
||||
|
||||
def test_md5_ignores_comments(tmp_dir, dvc):
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import os
|
||||
|
||||
from dvc.cli import main
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
def test_unprotect(tmp_dir, dvc):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
|
||||
class TestUnprotect(TestDvc):
|
||||
def test(self):
|
||||
ret = main(["config", "cache.type", "hardlink"])
|
||||
self.assertEqual(ret, 0)
|
||||
dvc.odb.local.cache_types = ["hardlink"]
|
||||
dvc.add("foo")
|
||||
cache = os.path.join(
|
||||
".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
|
||||
)
|
||||
assert not os.access("foo", os.W_OK)
|
||||
assert not os.access(cache, os.W_OK)
|
||||
|
||||
ret = main(["add", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
dvc.unprotect("foo")
|
||||
assert os.access("foo", os.W_OK)
|
||||
|
||||
cache = os.path.join(
|
||||
".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
|
||||
)
|
||||
if os.name == "nt":
|
||||
# NOTE: cache is now unprotected, because NTFS doesn't allow
|
||||
# deleting read-only files, so we have to try to set write perms
|
||||
# on files that we try to delete, which propagates to the cache
|
||||
# file. But it should be restored after the next cache check, hence
|
||||
# why we call `dvc status` here.
|
||||
assert os.access(cache, os.W_OK)
|
||||
dvc.status()
|
||||
|
||||
self.assertFalse(os.access(self.FOO, os.W_OK))
|
||||
self.assertFalse(os.access(cache, os.W_OK))
|
||||
|
||||
ret = main(["unprotect", self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(os.access(self.FOO, os.W_OK))
|
||||
|
||||
if os.name == "nt":
|
||||
# NOTE: cache is now unprotected, because NTFS doesn't allow
|
||||
# deleting read-only files, so we have to try to set write perms
|
||||
# on files that we try to delete, which propagates to the cache
|
||||
# file. But it should be restored after the next cache check, hence
|
||||
# why we call `dvc status` here.
|
||||
self.assertTrue(os.access(cache, os.W_OK))
|
||||
ret = main(["status"])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertFalse(os.access(cache, os.W_OK))
|
||||
assert not os.access(cache, os.W_OK)
|
||||
|
||||
@@ -51,3 +51,16 @@ def test_from_gitfs_when_pwd_not_in_root(
|
||||
|
||||
with wdir.chdir():
|
||||
assert dvc.used_objs([target], revs=[scm.get_rev()])
|
||||
|
||||
|
||||
def test_used_objs_push(tmp_dir, scm, dvc):
|
||||
stage = tmp_dir.dvc_gen("foo", "foo")[0]
|
||||
hash_info = stage.outs[0].hash_info
|
||||
|
||||
stage.outs[0].can_push = True
|
||||
assert stage.get_used_objs(push=False) == {None: {hash_info}}
|
||||
assert stage.get_used_objs(push=True) == {None: {hash_info}}
|
||||
|
||||
stage.outs[0].can_push = False
|
||||
assert stage.get_used_objs(push=False) == {None: {hash_info}}
|
||||
assert stage.get_used_objs(push=True) == {}
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_(tmp_dir, dvc, scm, capsys):
|
||||
assert re.search(rf"DVC version: {DVC_VERSION_REGEX}", out)
|
||||
assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", out)
|
||||
for subproject in SUBPROJECTS:
|
||||
assert re.search(rf"{subproject} = {DVC_VERSION_REGEX}", out)
|
||||
assert re.search(rf"{subproject} = .*", out)
|
||||
|
||||
assert find_supported_remotes(out)
|
||||
assert re.search(r"Cache types: .*", out)
|
||||
|
||||
@@ -174,12 +174,25 @@ def test_compose_and_dump(tmp_dir, suffix, overrides, expected):
|
||||
from dvc.utils.hydra import compose_and_dump
|
||||
|
||||
config_name = "config"
|
||||
config_dir = hydra_setup(tmp_dir, "conf", "config")
|
||||
output_file = tmp_dir / f"params.{suffix}"
|
||||
config_dir = hydra_setup(tmp_dir, "conf", "config")
|
||||
compose_and_dump(output_file, config_dir, config_name, overrides)
|
||||
assert output_file.parse() == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="unsupported on 3.11")
|
||||
def test_compose_and_dump_yaml_handles_string(tmp_dir):
|
||||
"""Regression test for 8583"""
|
||||
from dvc.utils.hydra import compose_and_dump
|
||||
|
||||
config = tmp_dir / "conf" / "config.yaml"
|
||||
config.parent.mkdir()
|
||||
config.write_text("foo: 'no'\n")
|
||||
output_file = tmp_dir / "params.yaml"
|
||||
compose_and_dump(output_file, str(config.parent), "config", [])
|
||||
assert output_file.read_text() == "foo: 'no'\n"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides, expected",
|
||||
[
|
||||
|
||||
@@ -456,9 +456,7 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
|
||||
"dvc.yaml::confusion_train_vs_test",
|
||||
],
|
||||
)
|
||||
assert html_result["dvc.yaml::linear_train_vs_test"]["data"][
|
||||
"values"
|
||||
] == _update_datapoints(
|
||||
ble = _update_datapoints(
|
||||
plots["data"]["linear_train.json"],
|
||||
{
|
||||
REVISION_FIELD: "linear_train.json",
|
||||
@@ -470,4 +468,7 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
|
||||
},
|
||||
)
|
||||
|
||||
assert (
|
||||
html_result["dvc.yaml::linear_train_vs_test"]["data"]["values"] == ble
|
||||
)
|
||||
# TODO check json results once vscode is able to handle flexible plots
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import pytest
|
||||
|
||||
COPY_SCRIPT = """
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if os.path.isfile(sys.argv[1]):
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
else:
|
||||
shutil.copytree(sys.argv[1], sys.argv[2])
|
||||
"""
|
||||
|
||||
|
||||
def _add_script(tmp_dir, path, contents=""):
|
||||
script, *_ = tmp_dir.gen(path, contents)
|
||||
if hasattr(tmp_dir, "scm"):
|
||||
tmp_dir.scm_add(path, commit=f"add {path}")
|
||||
return script.fs_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def append_foo_script(tmp_dir):
|
||||
return _add_script(
|
||||
tmp_dir,
|
||||
"append_foo.py",
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
with Path(sys.argv[1]).open("a+", encoding="utf-8") as f:
|
||||
f.write("foo")
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def copy_script(
|
||||
tmp_dir,
|
||||
):
|
||||
return _add_script(tmp_dir, "copy.py", COPY_SCRIPT)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def head_script(tmp_dir):
|
||||
"""Output first line of each file to different file with '-1' appended.
|
||||
Useful for tracking multiple outputs/dependencies which are not a copy
|
||||
of each others.
|
||||
"""
|
||||
return _add_script(
|
||||
tmp_dir,
|
||||
"head.py",
|
||||
"""
|
||||
import sys
|
||||
for file in sys.argv[1:]:
|
||||
with open(file) as f, open(file +"-1","w+") as w:
|
||||
w.write(f.readline())
|
||||
""",
|
||||
)
|
||||
@@ -39,7 +39,7 @@ def test_default(mocker, capsys):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
assert (
|
||||
"Added:\n"
|
||||
" file\n"
|
||||
@@ -77,7 +77,7 @@ def test_show_hash(mocker, capsys):
|
||||
"not in cache": [],
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
@@ -115,7 +115,7 @@ def test_show_json(mocker, capsys):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
|
||||
assert '"deleted": []' in out
|
||||
@@ -145,7 +145,7 @@ def test_show_json_and_hash(mocker, capsys):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
'"added": [{"path": "file1", "hash": "11111111"}, '
|
||||
@@ -180,7 +180,7 @@ def test_show_json_hide_missing(mocker, capsys):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
|
||||
assert '"deleted": []' in out
|
||||
@@ -200,7 +200,7 @@ def test_diff_show_markdown_and_hash(mocker, show_hash):
|
||||
mock_show_markdown = mocker.patch("dvc.commands.diff._show_markdown")
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff.copy())
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
mock_show_markdown.assert_called_once_with(diff, show_hash, False)
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ def test_no_changes(mocker, capsys, opts, show, expected):
|
||||
cmd = args.func(args)
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value={})
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert expected == out.strip()
|
||||
|
||||
@@ -349,7 +349,7 @@ def test_hide_missing(mocker, capsys):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"Added:\n"
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_plots_diff_open(tmp_dir, dvc, mocker, capsys, plots_data, auto_open):
|
||||
def test_plots_diff_open_WSL(tmp_dir, dvc, mocker, plots_data):
|
||||
mocked_open = mocker.patch("webbrowser.open", return_value=True)
|
||||
mocked_uname_result = mocker.MagicMock()
|
||||
mocked_uname_result.release = "Microsoft"
|
||||
mocked_uname_result.release = "microsoft"
|
||||
mocker.patch("platform.uname", return_value=mocked_uname_result)
|
||||
|
||||
cli_args = parse_args(
|
||||
|
||||
@@ -23,12 +23,6 @@ def test_run(mocker, dvc):
|
||||
"plots",
|
||||
"--plots-no-cache",
|
||||
"plots-no-cache",
|
||||
"--live",
|
||||
"live",
|
||||
"--live-no-cache",
|
||||
"live-no-cache",
|
||||
"--live-no-summary",
|
||||
"--live-no-html",
|
||||
"--file",
|
||||
"file",
|
||||
"--wdir",
|
||||
@@ -74,10 +68,6 @@ def test_run(mocker, dvc):
|
||||
outs_persist_no_cache=["outs-persist-no-cache"],
|
||||
checkpoints=["checkpoints"],
|
||||
params=["file:param1,param2", "param3"],
|
||||
live="live",
|
||||
live_no_cache="live-no-cache",
|
||||
live_no_summary=True,
|
||||
live_no_html=True,
|
||||
fname="file",
|
||||
wdir="wdir",
|
||||
no_exec=True,
|
||||
@@ -107,10 +97,6 @@ def test_run_args_from_cli(mocker, dvc):
|
||||
metrics_no_cache=[],
|
||||
plots=[],
|
||||
plots_no_cache=[],
|
||||
live=None,
|
||||
live_no_cache=None,
|
||||
live_no_summary=False,
|
||||
live_no_html=False,
|
||||
outs_persist=[],
|
||||
outs_persist_no_cache=[],
|
||||
checkpoints=[],
|
||||
@@ -144,10 +130,6 @@ def test_run_args_with_spaces(mocker, dvc):
|
||||
metrics_no_cache=[],
|
||||
plots=[],
|
||||
plots_no_cache=[],
|
||||
live=None,
|
||||
live_no_cache=None,
|
||||
live_no_summary=False,
|
||||
live_no_html=False,
|
||||
outs_persist=[],
|
||||
outs_persist_no_cache=[],
|
||||
checkpoints=[],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user