experiments: add dvc exp push/pull/list (#5042)

* git: add support for on_diverged in push_refspec

* experiments: update executor for push-refspec changes

* experiments: implement push()

* tests: add tests for experiments.push

* git: add iter_remote_refs

* roughly equivalent to `git ls-remote --refs`

* experiments: add experiments.list for listing local and remote
experiments

* refactor git upstream/downstream in tests

* experiments: add experiments.pull

* experiments: add CLI commands for push/pull/list

* exp list: drop short options

* experiments: s/list/ls/

* dulwich: catch not repo errors when creating remote client

* fix remote rev resolution for list

* fix command test

* add tests for push/pull with ambiguous experiment names
This commit is contained in:
Peter Rowlands (변기호)
2020-12-08 21:20:03 +09:00
committed by GitHub
parent a41e23a453
commit 51fa0f5c38
15 changed files with 830 additions and 44 deletions
+144
View File
@@ -524,6 +524,68 @@ class CmdExperimentsBranch(CmdBase):
return 0
class CmdExperimentsList(CmdBase):
def run(self):
exps = self.repo.experiments.ls(
rev=self.args.rev,
git_remote=self.args.git_remote,
all_=self.args.all,
)
for baseline in exps:
tag = self.repo.scm.describe(baseline)
if not tag:
branch = self.repo.scm.describe(baseline, base="refs/heads")
if branch:
tag = branch.split("/")[-1]
name = tag if tag else baseline[:7]
logger.info(f"{name}:")
for exp_name in exps[baseline]:
logger.info(f"\t{exp_name}")
return 0
class CmdExperimentsPush(CmdBase):
def run(self):
self.repo.experiments.push(
self.args.git_remote, self.args.experiment, force=self.args.force
)
logger.info(
(
"Pushed experiment '%s' to Git remote '%s'. "
"To push cache for this experiment to a DVC remote run:\n\n"
"\tdvc push ..."
),
self.args.experiment,
self.args.git_remote,
)
return 0
class CmdExperimentsPull(CmdBase):
def run(self):
self.repo.experiments.pull(
self.args.git_remote, self.args.experiment, force=self.args.force
)
logger.info(
(
"Pulled experiment '%s' from Git remote '%s'. "
"To pull cache for this experiment from a DVC remote run:\n\n"
"\tdvc pull ..."
),
self.args.experiment,
self.args.git_remote,
)
return 0
def add_parser(subparsers, parent_parser):
EXPERIMENTS_HELP = "Commands to run and compare experiments."
@@ -842,6 +904,88 @@ def add_parser(subparsers, parent_parser):
)
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
EXPERIMENTS_LIST_HELP = "List local and remote experiments."
experiments_list_parser = experiments_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_LIST_HELP, "experiments/list"),
help=EXPERIMENTS_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_list_parser.add_argument(
"--rev",
type=str,
default=None,
help=(
"List experiments derived from the specified revision. "
"Defaults to HEAD if neither `--rev` nor `--all` are specified."
),
metavar="<rev>",
)
experiments_list_parser.add_argument(
"--all", action="store_true", help="List all experiments.",
)
experiments_list_parser.add_argument(
"git_remote",
nargs="?",
default=None,
help=(
"Optional Git remote name or Git URL. If provided, experiments "
"from the specified Git repository will be listed instead of "
"local experiments."
),
metavar="[<git_remote>]",
)
experiments_list_parser.set_defaults(func=CmdExperimentsList)
EXPERIMENTS_PUSH_HELP = "Push a local experiment to a Git remote."
experiments_push_parser = experiments_subparsers.add_parser(
"push",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_PUSH_HELP, "experiments/push"),
help=EXPERIMENTS_PUSH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_push_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Replace experiment in the remote if it already exists.",
)
experiments_push_parser.add_argument(
"git_remote",
help="Git remote name or Git URL.",
metavar="<git_remote>",
)
experiments_push_parser.add_argument(
"experiment", help="Experiment to push.", metavar="<experiment>",
)
experiments_push_parser.set_defaults(func=CmdExperimentsPush)
EXPERIMENTS_PULL_HELP = "Pull an experiment from a Git remote."
experiments_pull_parser = experiments_subparsers.add_parser(
"pull",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_PULL_HELP, "experiments/pull"),
help=EXPERIMENTS_PULL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_pull_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Replace local experiment already exists.",
)
experiments_pull_parser.add_argument(
"git_remote",
help="Git remote name or Git URL.",
metavar="<git_remote>",
)
experiments_pull_parser.add_argument(
"experiment", help="Experiment to pull.", metavar="<experiment>",
)
experiments_pull_parser.set_defaults(func=CmdExperimentsPull)
def _add_run_common(parser):
"""Add common args for 'exp run' and 'exp resume'."""
+15
View File
@@ -652,3 +652,18 @@ class Experiments:
from dvc.repo.experiments.gc import gc
return gc(self.repo, *args, **kwargs)
def push(self, *args, **kwargs):
from dvc.repo.experiments.push import push
return push(self.repo, *args, **kwargs)
def pull(self, *args, **kwargs):
from dvc.repo.experiments.pull import pull
return pull(self.repo, *args, **kwargs)
def ls(self, *args, **kwargs):
from dvc.repo.experiments.ls import ls
return ls(self.repo, *args, **kwargs)
+3 -6
View File
@@ -152,21 +152,18 @@ class BaseExecutor:
if not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
refs.append(ref)
def on_diverged_ref(orig_ref, new_rev):
def on_diverged_ref(orig_ref: str, new_rev: str):
orig_rev = dest_scm.get_ref(orig_ref)
if dest_scm.diff(orig_rev, new_rev):
if force:
logger.debug(
"Replacing existing experiment '%s'",
os.fsdecode(orig_ref),
"Replacing existing experiment '%s'", orig_ref,
)
return True
if on_diverged:
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT) is not None
on_diverged(orig_ref, checkpoint)
logger.debug(
"Reproduced existing experiment '%s'", os.fsdecode(orig_ref)
)
logger.debug("Reproduced existing experiment '%s'", orig_ref)
return False
# fetch experiments
+50
View File
@@ -0,0 +1,50 @@
import logging
from collections import defaultdict
from dvc.repo import locked
from dvc.repo.scm_context import scm_context
from dvc.scm.base import RevError
from .utils import (
exp_refs,
exp_refs_by_baseline,
remote_exp_refs,
remote_exp_refs_by_baseline,
)
logger = logging.getLogger(__name__)
@locked
@scm_context
def ls(repo, *args, rev=None, git_remote=None, all_=False, **kwargs):
from dvc.scm.git import Git
if rev:
try:
rev = repo.scm.resolve_rev(rev)
except RevError:
if not (git_remote and Git.is_sha(rev)):
# This could be a remote rev that has not been fetched yet
raise
elif not all_:
rev = repo.scm.get_rev()
results = defaultdict(list)
if rev:
if git_remote:
gen = remote_exp_refs_by_baseline(repo.scm, git_remote, rev)
else:
gen = exp_refs_by_baseline(repo.scm, rev)
for info in gen:
results[rev].append(info.name)
elif all_:
if git_remote:
gen = remote_exp_refs(repo.scm, git_remote)
else:
gen = exp_refs(repo.scm)
for info in gen:
results[info.baseline_sha].append(info.name)
return results
+57
View File
@@ -0,0 +1,57 @@
import logging
from dvc.exceptions import DvcException, InvalidArgumentError
from dvc.repo import locked
from dvc.repo.scm_context import scm_context
from .utils import remote_exp_refs_by_name
logger = logging.getLogger(__name__)
@locked
@scm_context
def pull(repo, git_remote, exp_name, *args, force=False, **kwargs):
exp_ref = _get_exp_ref(repo, git_remote, exp_name)
def on_diverged(refname: str, rev: str) -> bool:
if repo.scm.get_ref(refname) == rev:
return True
raise DvcException(
f"Local experiment '{exp_name}' has diverged from remote "
"experiment with the same name. To override the local experiment "
"re-run with '--force'."
)
refspec = f"{exp_ref}:{exp_ref}"
logger.debug("Pull '%s' -> '%s'", git_remote, refspec)
repo.scm.fetch_refspecs(
git_remote, [refspec], force=force, on_diverged=on_diverged
)
def _get_exp_ref(repo, git_remote, exp_name):
if exp_name.startswith("refs/"):
return exp_name
exp_refs = list(remote_exp_refs_by_name(repo.scm, git_remote, exp_name))
if not exp_refs:
raise InvalidArgumentError(
f"Experiment '{exp_name}' does not exist in '{git_remote}'"
)
if len(exp_refs) > 1:
cur_rev = repo.scm.get_rev()
for info in exp_refs:
if info.baseline_sha == cur_rev:
return info
msg = [
(
f"Ambiguous name '{exp_name}' refers to multiple "
"experiments in '{git_remote}'. Use full refname to pull one "
"of the following:"
),
"",
]
msg.extend([f"\t{info}" for info in exp_refs])
raise InvalidArgumentError("\n".join(msg))
return exp_refs[0]
+57
View File
@@ -0,0 +1,57 @@
import logging
from dvc.exceptions import DvcException, InvalidArgumentError
from dvc.repo import locked
from dvc.repo.scm_context import scm_context
from .utils import exp_refs_by_name
logger = logging.getLogger(__name__)
@locked
@scm_context
def push(repo, git_remote, exp_name, *args, force=False, **kwargs):
exp_ref = _get_exp_ref(repo, exp_name)
def on_diverged(refname: str, rev: str) -> bool:
if repo.scm.get_ref(refname) == rev:
return True
raise DvcException(
f"Local experiment '{exp_name}' has diverged from remote "
"experiment with the same name. To override the remote experiment "
"re-run with '--force'."
)
refname = str(exp_ref)
logger.debug("Push '%s' -> '%s'", exp_ref, git_remote)
repo.scm.push_refspec(
git_remote, refname, refname, force=force, on_diverged=on_diverged
)
def _get_exp_ref(repo, exp_name):
if exp_name.startswith("refs/"):
return exp_name
exp_refs = list(exp_refs_by_name(repo.scm, exp_name))
if not exp_refs:
raise InvalidArgumentError(
f"'{exp_name}' is not a valid experiment name"
)
if len(exp_refs) > 1:
cur_rev = repo.scm.get_rev()
for info in exp_refs:
if info.baseline_sha == cur_rev:
return info
msg = [
(
f"Ambiguous name '{exp_name}' refers to multiple "
"experiments. Use full refname to push one of the "
"following:"
),
"",
]
msg.extend([f"\t{info}" for info in exp_refs])
raise InvalidArgumentError("\n".join(msg))
return exp_refs[0]
+41
View File
@@ -30,3 +30,44 @@ def exp_refs_by_name(
for ref_info in exp_refs(scm):
if ref_info.name == name:
yield ref_info
def exp_refs_by_baseline(
scm: "Git", rev: str
) -> Generator["ExpRefInfo", None, None]:
"""Iterate over all experiment refs with the specified baseline."""
ref_info = ExpRefInfo(baseline_sha=rev)
for ref in scm.iter_refs(base=str(ref_info)):
if ref.startswith(EXEC_NAMESPACE) or ref == EXPS_STASH:
continue
yield ExpRefInfo.from_ref(ref)
def remote_exp_refs(
scm: "Git", url: str
) -> Generator["ExpRefInfo", None, None]:
"""Iterate over all remote experiment refs."""
for ref in scm.iter_remote_refs(url, base=EXPS_NAMESPACE):
if ref.startswith(EXEC_NAMESPACE) or ref == EXPS_STASH:
continue
yield ExpRefInfo.from_ref(ref)
def remote_exp_refs_by_name(
scm: "Git", url: str, name: str
) -> Generator["ExpRefInfo", None, None]:
"""Iterate over all remote experiment refs matching the specified name."""
for ref_info in remote_exp_refs(scm, url):
if ref_info.name == name:
yield ref_info
def remote_exp_refs_by_baseline(
scm: "Git", url: str, rev: str
) -> Generator["ExpRefInfo", None, None]:
"""Iterate over all remote experiment refs with the specified baseline."""
ref_info = ExpRefInfo(baseline_sha=rev)
for ref in scm.iter_remote_refs(url, base=str(ref_info)):
if ref.startswith(EXEC_NAMESPACE) or ref == EXPS_STASH:
continue
yield ExpRefInfo.from_ref(ref)
+1
View File
@@ -321,6 +321,7 @@ class Git(Base):
get_ref = partialmethod(_backend_func, "get_ref")
remove_ref = partialmethod(_backend_func, "remove_ref")
iter_refs = partialmethod(_backend_func, "iter_refs")
iter_remote_refs = partialmethod(_backend_func, "iter_remote_refs")
get_refs_containing = partialmethod(_backend_func, "get_refs_containing")
push_refspec = partialmethod(_backend_func, "push_refspec")
fetch_refspecs = partialmethod(_backend_func, "fetch_refspecs")
+26 -4
View File
@@ -174,22 +174,42 @@ class BaseGitBackend(ABC):
If base is specified, only refs which begin with base will be yielded.
"""
@abstractmethod
def iter_remote_refs(self, url: str, base: Optional[str] = None):
"""Iterate over all refs in the specified remote Git repo.
If base is specified, only refs which begin with base will be yielded.
URL can be a named Git remote or URL.
"""
@abstractmethod
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
"""Iterate over all git refs containing the specfied revision."""
@abstractmethod
def push_refspec(self, url: str, src: Optional[str], dest: str):
def push_refspec(
self,
url: str,
src: Optional[str],
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
"""Push refspec to a remote Git repo.
Args:
url: Remote repo Git URL (Note this must be a Git URL and not
a remote name).
url: Git remote name or absolute Git URL.
src: Local refspec. If src ends with "/" it will be treated as a
prefix, and all refs inside src will be pushed using dest
as destination refspec prefix. If src is None, dest will be
deleted from the remote.
dest: Remote refspec.
force: If True, remote refs will be overwritten.
on_diverged: Callback function which will be called if local ref
and remote have diverged and force is False. If the callback
returns True the remote ref will be overwritten.
Callback will be of the form:
on_diverged(local_refname, remote_sha)
"""
@abstractmethod
@@ -198,7 +218,7 @@ class BaseGitBackend(ABC):
url: str,
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[bytes, bytes], bool]] = None,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
"""Fetch refspecs from a remote Git repo.
@@ -211,6 +231,8 @@ class BaseGitBackend(ABC):
on_diverged: Callback function which will be called if local ref
and remote have diverged and force is False. If the callback
returns True the local ref will be overwritten.
Callback will be of the form:
on_diverged(local_refname, remote_sha)
"""
@abstractmethod
+94 -26
View File
@@ -185,11 +185,92 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
else:
yield os.fsdecode(key)
def iter_remote_refs(self, url: str, base: Optional[str] = None):
from dulwich.client import get_transport_and_path
from dulwich.porcelain import get_remote_repo
try:
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
except Exception as exc:
raise SCMError(
f"'{url}' is not a valid Git remote or URL"
) from exc
if base:
yield from (
os.fsdecode(ref)
for ref in client.get_refs(path)
if ref.startswith(os.fsencode(base))
)
else:
yield from (os.fsdecode(ref) for ref in client.get_refs(path))
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
raise NotImplementedError
def push_refspec(self, url: str, src: Optional[str], dest: str):
def push_refspec(
self,
url: str,
src: Optional[str],
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
from dulwich.client import get_transport_and_path
from dulwich.errors import NotGitRepository, SendPackError
from dulwich.porcelain import (
DivergedBranches,
check_diverged,
get_remote_repo,
)
dest_refs, values = self._push_dest_refs(src, dest)
try:
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
except Exception as exc:
raise SCMError(
f"'{url}' is not a valid Git remote or URL"
) from exc
def update_refs(refs):
new_refs = {}
for ref, value in zip(dest_refs, values):
if ref in refs:
local_sha = self.repo.refs[ref]
remote_sha = refs[ref]
try:
check_diverged(self.repo, remote_sha, local_sha)
except DivergedBranches:
if not force:
overwrite = False
if on_diverged:
overwrite = on_diverged(
os.fsdecode(ref), os.fsdecode(remote_sha),
)
if not overwrite:
continue
new_refs[ref] = value
return new_refs
def progress(msg):
logger.trace("git send_pack: %s", msg)
try:
client.send_pack(
path,
update_refs,
self.repo.object_store.generate_pack_data,
progress=progress,
)
except (NotGitRepository, SendPackError) as exc:
raise SCMError("Git failed to push '{src}' to '{url}'") from exc
def _push_dest_refs(
self, src: str, dest: str
) -> Tuple[Iterable[bytes], Iterable[bytes]]:
from dulwich.objects import ZERO_SHA
if src is not None and src.endswith("/"):
@@ -203,37 +284,22 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
else:
values = [self.repo.refs[os.fsencode(src)]]
dest_refs = [os.fsencode(dest)]
def update_refs(refs):
for ref, value in zip(dest_refs, values):
refs[ref] = value
return refs
try:
client, path = get_transport_and_path(url)
except Exception as exc:
raise SCMError("Could not get remote client") from exc
def progress(msg):
logger.trace("git send_pack: %s", msg)
client.send_pack(
path,
update_refs,
self.repo.object_store.generate_pack_data,
progress=progress,
)
return dest_refs, values
def fetch_refspecs(
self,
url: str,
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[bytes, bytes], bool]] = None,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
from dulwich.client import get_transport_and_path
from dulwich.objectspec import parse_reftuples
from dulwich.porcelain import DivergedBranches, check_diverged
from dulwich.porcelain import (
DivergedBranches,
check_diverged,
get_remote_repo,
)
fetch_refs = []
@@ -253,10 +319,12 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
]
try:
client, path = get_transport_and_path(url)
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
except Exception as exc:
raise SCMError("Could not get remote client") from exc
raise SCMError(
f"'{url}' is not a valid Git remote or URL"
) from exc
def progress(msg):
logger.trace("git fetch: %s", msg)
+12 -2
View File
@@ -362,6 +362,9 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
base = base[:-1]
yield "/".join([base, ref.name])
def iter_remote_refs(self, url: str, base: Optional[str] = None):
raise NotImplementedError
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
from git.exc import GitCommandError
@@ -379,7 +382,14 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
except GitCommandError:
pass
def push_refspec(self, url: str, src: Optional[str], dest: str):
def push_refspec(
self,
url: str,
src: Optional[str],
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
raise NotImplementedError
def fetch_refspecs(
@@ -387,7 +397,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
url: str,
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[bytes, bytes], bool]] = None,
on_diverged: Optional[Callable[[str, str], bool]] = None,
):
raise NotImplementedError
+33 -2
View File
@@ -5,14 +5,13 @@ import stat
import pytest
from funcy import first
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils.serialize import PythonFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
@pytest.mark.parametrize("name", [None, "foo"])
def test_new_simple(tmp_dir, scm, dvc, exp_stage, mocker, name):
from dvc.repo.experiments.utils import exp_refs_by_rev
baseline = scm.get_rev()
tmp_dir.gen("params.yaml", "foo: 2")
@@ -397,3 +396,35 @@ def test_packed_args_exists(tmp_dir, scm, dvc, exp_stage, caplog):
with caplog.at_level(logging.WARNING):
dvc.experiments.run(exp_stage.addressing)
assert "Temporary DVC file" in caplog.text
def test_list(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)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
tmp_dir.scm_gen("new", "new", commit="new")
baseline_c = scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=4"])
exp_c = first(results)
ref_info_c = first(exp_refs_by_rev(scm, exp_c))
assert dvc.experiments.ls() == {
baseline_c: [ref_info_c.name],
}
exp_list = dvc.experiments.ls(rev=ref_info_a.baseline_sha)
assert {key: set(val) for key, val in exp_list.items()} == {
baseline_a: {ref_info_a.name, ref_info_b.name}
}
exp_list = dvc.experiments.ls(all_=True)
assert {key: set(val) for key, val in exp_list.items()} == {
baseline_a: {ref_info_a.name, ref_info_b.name},
baseline_c: {ref_info_c.name},
}
+240
View File
@@ -0,0 +1,240 @@
import pytest
from funcy import first
from dvc.exceptions import DvcException
from dvc.repo.experiments.utils import exp_refs_by_rev
@pytest.fixture
def git_upstream(tmp_dir, erepo_dir):
url = "file://{}".format(erepo_dir.resolve().as_posix())
tmp_dir.scm.gitpython.repo.create_remote("upstream", url)
erepo_dir.remote = "upstream"
erepo_dir.url = url
return erepo_dir
@pytest.fixture
def git_downstream(tmp_dir, erepo_dir):
url = "file://{}".format(tmp_dir.resolve().as_posix())
erepo_dir.scm.gitpython.repo.create_remote("upstream", url)
erepo_dir.remote = "upstream"
erepo_dir.url = url
return erepo_dir
@pytest.mark.parametrize("use_url", [True, False])
def test_push(tmp_dir, scm, dvc, git_upstream, exp_stage, use_url):
from dvc.exceptions import InvalidArgumentError
remote = git_upstream.url if use_url else git_upstream.remote
with pytest.raises(InvalidArgumentError):
dvc.experiments.push(remote, "foo")
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
dvc.experiments.push(remote, ref_info.name)
assert git_upstream.scm.get_ref(str(ref_info)) == exp
git_upstream.scm.remove_ref(str(ref_info))
dvc.experiments.push(remote, str(ref_info))
assert git_upstream.scm.get_ref(str(ref_info)) == exp
def test_push_diverged(tmp_dir, scm, dvc, git_upstream, exp_stage):
git_upstream.scm_gen("foo", "foo", commit="init")
remote_rev = git_upstream.scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
git_upstream.scm.set_ref(str(ref_info), remote_rev)
with pytest.raises(DvcException):
dvc.experiments.push(git_upstream.remote, ref_info.name)
assert git_upstream.scm.get_ref(str(ref_info)) == remote_rev
dvc.experiments.push(git_upstream.remote, ref_info.name, force=True)
assert git_upstream.scm.get_ref(str(ref_info)) == exp
def test_push_checkpoint(tmp_dir, scm, dvc, git_upstream, checkpoint_stage):
results = dvc.experiments.run(
checkpoint_stage.addressing, params=["foo=2"]
)
exp_a = first(results)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
dvc.experiments.push(git_upstream.remote, ref_info_a.name, force=True)
assert git_upstream.scm.get_ref(str(ref_info_a)) == exp_a
results = dvc.experiments.run(
checkpoint_stage.addressing, checkpoint_resume=exp_a
)
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
tmp_dir.scm_gen("new", "new", commit="new")
dvc.experiments.push(git_upstream.remote, ref_info_b.name, force=True)
assert git_upstream.scm.get_ref(str(ref_info_b)) == exp_b
def test_push_ambiguous_name(tmp_dir, scm, dvc, git_upstream, exp_stage):
from dvc.exceptions import InvalidArgumentError
remote = git_upstream.remote
results = dvc.experiments.run(
exp_stage.addressing, params=["foo=2"], name="foo"
)
exp_a = first(results)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
tmp_dir.scm_gen("new", "new", commit="new")
results = dvc.experiments.run(
exp_stage.addressing, params=["foo=3"], name="foo"
)
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
dvc.experiments.push(remote, "foo")
assert git_upstream.scm.get_ref(str(ref_info_b)) == exp_b
tmp_dir.scm_gen("new", "new 2", commit="new 2")
with pytest.raises(InvalidArgumentError):
dvc.experiments.push(remote, "foo")
dvc.experiments.push(remote, str(ref_info_a))
assert git_upstream.scm.get_ref(str(ref_info_a)) == exp_a
@pytest.mark.parametrize("use_url", [True, False])
def test_list_remote(tmp_dir, scm, dvc, git_downstream, exp_stage, use_url):
baseline_a = scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp_a = first(results)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
tmp_dir.scm_gen("new", "new", commit="new")
baseline_c = scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=4"])
exp_c = first(results)
ref_info_c = first(exp_refs_by_rev(scm, exp_c))
remote = git_downstream.url if use_url else git_downstream.remote
assert git_downstream.scm.get_ref("HEAD") != scm.get_ref("HEAD")
downstream_exp = git_downstream.dvc.experiments
assert downstream_exp.ls(git_remote=remote) == {}
exp_list = downstream_exp.ls(rev=baseline_a, git_remote=remote)
assert {key: set(val) for key, val in exp_list.items()} == {
baseline_a: {ref_info_a.name, ref_info_b.name}
}
exp_list = downstream_exp.ls(all_=True, git_remote=remote)
assert {key: set(val) for key, val in exp_list.items()} == {
baseline_a: {ref_info_a.name, ref_info_b.name},
baseline_c: {ref_info_c.name},
}
@pytest.mark.parametrize("use_url", [True, False])
def test_pull(tmp_dir, scm, dvc, git_downstream, exp_stage, use_url):
from dvc.exceptions import InvalidArgumentError
remote = git_downstream.url if use_url else git_downstream.remote
downstream_exp = git_downstream.dvc.experiments
with pytest.raises(InvalidArgumentError):
downstream_exp.pull(remote, "foo")
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
downstream_exp.pull(remote, ref_info.name)
assert git_downstream.scm.get_ref(str(ref_info)) == exp
git_downstream.scm.remove_ref(str(ref_info))
downstream_exp.pull(remote, str(ref_info))
assert git_downstream.scm.get_ref(str(ref_info)) == exp
def test_pull_diverged(tmp_dir, scm, dvc, git_downstream, exp_stage):
git_downstream.scm_gen("foo", "foo", commit="init")
remote_rev = git_downstream.scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
git_downstream.scm.set_ref(str(ref_info), remote_rev)
downstream_exp = git_downstream.dvc.experiments
with pytest.raises(DvcException):
downstream_exp.pull(git_downstream.remote, ref_info.name)
assert git_downstream.scm.get_ref(str(ref_info)) == remote_rev
downstream_exp.pull(git_downstream.remote, ref_info.name, force=True)
assert git_downstream.scm.get_ref(str(ref_info)) == exp
def test_pull_checkpoint(tmp_dir, scm, dvc, git_downstream, checkpoint_stage):
results = dvc.experiments.run(
checkpoint_stage.addressing, params=["foo=2"]
)
exp_a = first(results)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
downstream_exp = git_downstream.dvc.experiments
downstream_exp.pull(git_downstream.remote, ref_info_a.name, force=True)
assert git_downstream.scm.get_ref(str(ref_info_a)) == exp_a
results = dvc.experiments.run(
checkpoint_stage.addressing, checkpoint_resume=exp_a
)
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
downstream_exp.pull(git_downstream.remote, ref_info_b.name, force=True)
assert git_downstream.scm.get_ref(str(ref_info_b)) == exp_b
def test_pull_ambiguous_name(tmp_dir, scm, dvc, git_downstream, exp_stage):
from dvc.exceptions import InvalidArgumentError
results = dvc.experiments.run(
exp_stage.addressing, params=["foo=2"], name="foo"
)
exp_a = first(results)
ref_info_a = first(exp_refs_by_rev(scm, exp_a))
tmp_dir.scm_gen("new", "new", commit="new")
results = dvc.experiments.run(
exp_stage.addressing, params=["foo=3"], name="foo"
)
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
remote = git_downstream.remote
downstream_exp = git_downstream.dvc.experiments
with pytest.raises(InvalidArgumentError):
downstream_exp.pull(remote, "foo")
downstream_exp.pull(remote, str(ref_info_b))
assert git_downstream.scm.get_ref(str(ref_info_b)) == exp_b
with git_downstream.scm.detach_head(ref_info_a.baseline_sha):
downstream_exp.pull(remote, "foo")
assert git_downstream.scm.get_ref(str(ref_info_a)) == exp_a
+47
View File
@@ -6,6 +6,9 @@ from dvc.command.experiments import (
CmdExperimentsBranch,
CmdExperimentsDiff,
CmdExperimentsGC,
CmdExperimentsList,
CmdExperimentsPull,
CmdExperimentsPush,
CmdExperimentsRun,
CmdExperimentsShow,
)
@@ -150,3 +153,47 @@ def test_experiments_branch(dvc, scm, mocker):
assert cmd.run() == 0
m.assert_called_once_with(cmd.repo, "expname", "branchname")
def test_experiments_list(dvc, scm, mocker):
cli_args = parse_args(
["experiments", "list", "origin", "--rev", "foo", "--all"]
)
assert cli_args.func == CmdExperimentsList
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.ls.ls", return_value={})
assert cmd.run() == 0
m.assert_called_once_with(
cmd.repo, git_remote="origin", rev="foo", all_=True
)
def test_experiments_push(dvc, scm, mocker):
cli_args = parse_args(
["experiments", "push", "origin", "experiment", "--force"]
)
assert cli_args.func == CmdExperimentsPush
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.push.push", return_value={})
assert cmd.run() == 0
m.assert_called_once_with(cmd.repo, "origin", "experiment", force=True)
def test_experiments_pull(dvc, scm, mocker):
cli_args = parse_args(
["experiments", "pull", "origin", "experiment", "--force"]
)
assert cli_args.func == CmdExperimentsPull
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.pull.pull", return_value={})
assert cmd.run() == 0
m.assert_called_once_with(cmd.repo, "origin", "experiment", force=True)
+10 -4
View File
@@ -211,7 +211,8 @@ def test_refs_containing(tmp_dir, scm):
assert expected == set(scm.get_refs_containing(init_rev))
def test_push_refspec(tmp_dir, scm, make_tmp_dir):
@pytest.mark.parametrize("use_url", [True, False])
def test_push_refspec(tmp_dir, scm, make_tmp_dir, use_url):
tmp_dir.scm_gen({"file": "0"}, commit="init")
init_rev = scm.get_rev()
tmp_dir.gen(
@@ -222,18 +223,23 @@ def test_push_refspec(tmp_dir, scm, make_tmp_dir):
)
remote_dir = make_tmp_dir("git-remote", scm=True)
url = "file://{}".format(remote_dir.resolve().as_posix())
scm.gitpython.repo.create_remote("origin", url)
scm.push_refspec(url, "refs/foo/bar", "refs/foo/bar")
with pytest.raises(SCMError):
scm.push_refspec("bad-remote", "refs/foo/bar", "refs/foo/bar")
remote = url if use_url else "origin"
scm.push_refspec(remote, "refs/foo/bar", "refs/foo/bar")
assert init_rev == remote_dir.scm.get_ref("refs/foo/bar")
remote_dir.scm.checkout("refs/foo/bar")
assert init_rev == remote_dir.scm.get_rev()
assert "0" == (remote_dir / "file").read_text()
scm.push_refspec(url, "refs/foo/", "refs/foo/")
scm.push_refspec(remote, "refs/foo/", "refs/foo/")
assert init_rev == remote_dir.scm.get_ref("refs/foo/baz")
scm.push_refspec(url, None, "refs/foo/baz")
scm.push_refspec(remote, None, "refs/foo/baz")
assert remote_dir.scm.get_ref("refs/foo/baz") is None