Push: accept multiple as input(#7196)

fix: #7196

New feature: now `dvc exp push` can accept a list of experiment names as input.

1. refactor of experiment push
2. add support to list input for the `exp push`
3. add function tests for the multi exp push
4. add unit test for the arg parse
This commit is contained in:
karajan1001
2022-01-18 17:31:55 +08:00
committed by Gao
parent 13238e9716
commit 9bb6494e55
8 changed files with 128 additions and 67 deletions
+4 -1
View File
@@ -88,6 +88,9 @@ def add_parser(experiments_subparsers, parent_parser):
metavar="<git_remote>",
)
experiments_push_parser.add_argument(
"experiment", help="Experiment to push.", metavar="<experiment>"
"experiment",
nargs="+",
help="Experiments to push.",
metavar="<experiment>",
).complete = completion.EXPERIMENT
experiments_push_parser.set_defaults(func=CmdExperimentsPush)
+41
View File
@@ -0,0 +1,41 @@
from dvc.exceptions import InvalidArgumentError
from dvc.types import Iterable, List, Optional
from .base import ExpRefInfo
class AmbiguousExpRefInfo(InvalidArgumentError):
def __init__(
self,
exp_name: str,
exp_ref_list: Iterable[ExpRefInfo],
):
msg = [
(
f"Ambiguous name '{exp_name}' refers to multiple experiments."
" Use one of the following full refnames instead:"
),
"",
]
msg.extend([f"\t{info}" for info in exp_ref_list])
super().__init__("\n".join(msg))
class UnresolvedExpNamesError(InvalidArgumentError):
def __init__(
self, unresolved_list: List[str], *args, git_remote: str = None
):
unresolved_names = ";".join(unresolved_list)
if not git_remote:
if len(unresolved_names) > 1:
super().__init__(
f"'{unresolved_names}' are not valid experiment names"
)
else:
super().__init__(
f"'{unresolved_names}' is not a valid experiment name"
)
else:
super().__init__(
f"Experiment '{unresolved_names}' does not exist "
f"in '{git_remote}'"
)
+48 -29
View File
@@ -1,9 +1,12 @@
import logging
from dvc.exceptions import DvcException, InvalidArgumentError
from dvc.exceptions import DvcException
from dvc.repo import locked
from dvc.repo.scm_context import scm_context
from dvc.scm import TqdmGit
from dvc.types import IterableStr, Union
from .exceptions import UnresolvedExpNamesError
from .utils import exp_commits, push_refspec, resolve_name
logger = logging.getLogger(__name__)
@@ -13,50 +16,66 @@ logger = logging.getLogger(__name__)
@scm_context
def push(
repo,
git_remote,
exp_name: str,
git_remote: str,
exp_names: Union[IterableStr, str],
*args,
force=False,
push_cache=False,
force: bool = False,
push_cache: bool = False,
**kwargs,
):
exp_ref_dict = resolve_name(repo.scm, exp_name)
exp_ref = exp_ref_dict[exp_name]
if not exp_ref:
raise InvalidArgumentError(
f"'{exp_name}' is not a valid experiment name"
)
if isinstance(exp_names, str):
exp_names = [exp_names]
exp_ref_dict = resolve_name(repo.scm, exp_names)
unresolved_exp_names = [
exp_name
for exp_name, exp_ref in exp_ref_dict.items()
if exp_ref is None
]
if unresolved_exp_names:
raise UnresolvedExpNamesError(unresolved_exp_names)
exp_ref_set = exp_ref_dict.values()
_push(repo, git_remote, exp_ref_set, force, push_cache, **kwargs)
def _push(
repo,
git_remote: str,
exp_refs,
force: bool,
push_cache: bool,
**kwargs,
):
def on_diverged(refname: str, rev: str) -> bool:
if repo.scm.get_ref(refname) == rev:
return True
exp_name = refname.split("/")[-1]
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("git push experiment '%s' -> '%s'", exp_ref, git_remote)
logger.debug(f"git push experiment '{exp_refs}' -> '{git_remote}'")
from dvc.scm import TqdmGit
with TqdmGit(desc="Pushing git refs") as pbar:
push_refspec(
repo.scm,
git_remote,
refname,
refname,
force=force,
on_diverged=on_diverged,
progress=pbar.update_git,
)
for exp_ref in exp_refs:
with TqdmGit(desc="Pushing git refs") as pbar:
push_refspec(
repo.scm,
git_remote,
str(exp_ref),
str(exp_ref),
force=force,
on_diverged=on_diverged,
progress=pbar.update_git,
)
if push_cache:
_push_cache(repo, exp_ref, **kwargs)
_push_cache(repo, exp_refs, **kwargs)
def _push_cache(repo, exp_ref, dvc_remote=None, jobs=None, run_cache=False):
revs = list(exp_commits(repo.scm, [exp_ref]))
logger.debug("dvc push experiment '%s'", exp_ref)
def _push_cache(repo, exp_refs, dvc_remote=None, jobs=None, run_cache=False):
revs = list(exp_commits(repo.scm, exp_refs))
logger.debug(f"dvc push experiment '{exp_refs}'")
repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
+1 -16
View File
@@ -21,24 +21,9 @@ from .base import (
EXPS_STASH,
ExpRefInfo,
)
from dvc.repo.experiments.exceptions import AmbiguousExpRefInfo
class AmbiguousExpRefInfo(InvalidArgumentError):
def __init__(
self,
exp_name: str,
exp_ref_list: Iterable[ExpRefInfo],
):
msg = [
(
f"Ambiguous name '{exp_name}' refers to multiple experiments."
" Use one of the following full refnames instead:"
),
"",
]
msg.extend([f"\t{info}" for info in exp_ref_list])
super().__init__("\n".join(msg))
def exp_refs(scm: "Git") -> Generator["ExpRefInfo", None, None]:
"""Iterate over all experiment refs."""
+2 -1
View File
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
if TYPE_CHECKING:
from os import PathLike
@@ -10,3 +10,4 @@ OptStr = Optional[str]
TargetType = Union[List[str], str]
DictStrAny = Dict[str, Any]
DictAny = Dict[Any, Any]
IterableStr = Iterable[str]
+28 -17
View File
@@ -13,19 +13,30 @@ def test_push(tmp_dir, scm, dvc, git_upstream, exp_stage, use_url):
remote = git_upstream.url if use_url else git_upstream.remote
with pytest.raises(InvalidArgumentError):
dvc.experiments.push(remote, "foo")
dvc.experiments.push(remote, ["foo"])
results = dvc.experiments.run(exp_stage.addressing, params=["foo=1"])
exp1 = first(results)
ref_info1 = first(exp_refs_by_rev(scm, exp1))
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
exp2 = first(results)
ref_info2 = first(exp_refs_by_rev(scm, exp2))
dvc.experiments.push(remote, ref_info.name)
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == exp
results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
exp3 = first(results)
ref_info3 = first(exp_refs_by_rev(scm, exp3))
git_upstream.tmp_dir.scm.remove_ref(str(ref_info))
dvc.experiments.push(remote, [ref_info1.name, ref_info2.name])
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info1)) == exp1
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info2)) == exp2
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info3)) is None
dvc.experiments.push(remote, str(ref_info))
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == exp
git_upstream.tmp_dir.scm.remove_ref(str(ref_info1))
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info1)) is None
dvc.experiments.push(remote, [ref_info1.name])
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info1)) == exp1
def test_push_diverged(tmp_dir, scm, dvc, git_upstream, exp_stage):
@@ -39,10 +50,10 @@ def test_push_diverged(tmp_dir, scm, dvc, git_upstream, exp_stage):
git_upstream.tmp_dir.scm.set_ref(str(ref_info), remote_rev)
with pytest.raises(DvcException):
dvc.experiments.push(git_upstream.remote, ref_info.name)
dvc.experiments.push(git_upstream.remote, [ref_info.name])
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == remote_rev
dvc.experiments.push(git_upstream.remote, ref_info.name, force=True)
dvc.experiments.push(git_upstream.remote, [ref_info.name], force=True)
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == exp
@@ -53,7 +64,7 @@ def test_push_checkpoint(tmp_dir, scm, dvc, git_upstream, checkpoint_stage):
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)
dvc.experiments.push(git_upstream.remote, [ref_info_a.name], force=True)
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info_a)) == exp_a
results = dvc.experiments.run(
@@ -64,7 +75,7 @@ def test_push_checkpoint(tmp_dir, scm, dvc, git_upstream, checkpoint_stage):
tmp_dir.scm_gen("new", "new", commit="new")
dvc.experiments.push(git_upstream.remote, ref_info_b.name, force=True)
dvc.experiments.push(git_upstream.remote, [ref_info_b.name], force=True)
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info_b)) == exp_b
@@ -86,15 +97,15 @@ def test_push_ambiguous_name(tmp_dir, scm, dvc, git_upstream, exp_stage):
exp_b = first(results)
ref_info_b = first(exp_refs_by_rev(scm, exp_b))
dvc.experiments.push(remote, "foo")
dvc.experiments.push(remote, ["foo"])
assert git_upstream.tmp_dir.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, ["foo"])
dvc.experiments.push(remote, str(ref_info_a))
dvc.experiments.push(remote, [str(ref_info_a)])
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info_a)) == exp_a
@@ -237,7 +248,7 @@ def test_push_pull_cache(
exp = first(results)
ref_info = first(exp_refs_by_rev(scm, exp))
dvc.experiments.push(remote, ref_info.name, push_cache=True)
dvc.experiments.push(remote, [ref_info.name], push_cache=True)
for x in range(2, checkpoint_stage.iterations + 1):
hash_ = digest(str(x))
path = os.path.join(local_remote.url, hash_[:2], hash_[2:])
@@ -285,4 +296,4 @@ def test_auth_error_push(tmp_dir, scm, dvc, exp_stage, http_auth_patch):
GitAuthError,
match=f"HTTP Git authentication is not supported: '{http_auth_patch}'",
):
dvc.experiments.push(http_auth_patch, ref_info.name)
dvc.experiments.push(http_auth_patch, [ref_info.name])
+1 -1
View File
@@ -106,7 +106,7 @@ def test_remove_remote(tmp_dir, scm, dvc, exp_stage, git_upstream, use_url):
exp_list.append(exp)
ref_info = first(exp_refs_by_rev(scm, exp))
ref_info_list.append(ref_info)
dvc.experiments.push(remote, ref_info.name)
dvc.experiments.push(remote, [ref_info.name])
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == exp
dvc.experiments.remove(
+3 -2
View File
@@ -221,7 +221,8 @@ def test_experiments_push(dvc, scm, mocker):
"experiments",
"push",
"origin",
"experiment",
"experiment1",
"experiment2",
"--force",
"--no-cache",
"--remote",
@@ -241,7 +242,7 @@ def test_experiments_push(dvc, scm, mocker):
m.assert_called_once_with(
cmd.repo,
"origin",
"experiment",
["experiment1", "experiment2"],
force=True,
push_cache=False,
dvc_remote="my-remote",