mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
exp: add rename command (#9812)
* Add renaming for finished experiments * Update arg parser * Add rename by rev * Add rename tests * Change remove to rename; Refactor arguments error * Remove --rev; Use resolve_name; Add test * Update testing; Fix python3.10 rename failure * Update rename test * Update tests and error handling * exp rename: Add `--force`. Fail if experiment new_name exists but allow to overwrite it. --------- Co-authored-by: daavoo <[email protected]>
This commit is contained in:
@@ -12,6 +12,7 @@ from dvc.commands.experiments import (
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
rename,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
@@ -28,6 +29,7 @@ SUB_COMMANDS = [
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
rename,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsRename(CmdBase):
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
if not (self.args.experiment and self.args.name):
|
||||
raise InvalidArgumentError(
|
||||
"An experiment to rename and a new experiment name are required."
|
||||
)
|
||||
renamed = self.repo.experiments.rename(
|
||||
exp_name=self.args.experiment,
|
||||
new_name=self.args.name,
|
||||
git_remote=self.args.git_remote,
|
||||
force=self.args.force,
|
||||
)
|
||||
if renamed:
|
||||
ui.write(f"Renamed experiments: {humanize.join(map(repr, renamed))}")
|
||||
else:
|
||||
ui.write("No experiments to rename.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_RENAME_HELP = "Rename experiments."
|
||||
experiments_rename_parser = experiments_subparsers.add_parser(
|
||||
"rename",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_RENAME_HELP, "exp/rename"),
|
||||
help=EXPERIMENTS_RENAME_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
rename_group = experiments_rename_parser.add_mutually_exclusive_group()
|
||||
rename_group.add_argument(
|
||||
"-g",
|
||||
"--git-remote",
|
||||
metavar="<git_remote>",
|
||||
help="Name or URL of the Git remote to rename the experiment from",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"experiment",
|
||||
help="Experiment to rename.",
|
||||
nargs="?",
|
||||
metavar="<experiment>",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"name",
|
||||
help="Name of new experiment.",
|
||||
metavar="<name>",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Replace experiment if it already exists.",
|
||||
)
|
||||
experiments_rename_parser.set_defaults(func=CmdExperimentsRename)
|
||||
@@ -395,6 +395,11 @@ class Experiments:
|
||||
|
||||
return remove(self.repo, *args, **kwargs)
|
||||
|
||||
def rename(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.rename import rename
|
||||
|
||||
return rename(self.repo, *args, **kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.clean import clean
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ class ExperimentExistsError(DvcException):
|
||||
"Experiment conflicts with existing experiment "
|
||||
f"'{name}'. To overwrite the existing experiment run:\n\n"
|
||||
f"\tdvc exp {command} -f ...\n\n"
|
||||
f"To {command} this experiment with a different name run:\n\n"
|
||||
f"\tdvc exp {command} -n <new_name> ...\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||
|
||||
from dvc.repo.experiments.exceptions import (
|
||||
ExperimentExistsError,
|
||||
UnresolvedExpNamesError,
|
||||
)
|
||||
from dvc.repo.experiments.utils import resolve_name
|
||||
from dvc.scm import Git
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def rename(
|
||||
repo: "Repo",
|
||||
new_name: str,
|
||||
exp_name: Union[str, None] = None,
|
||||
git_remote: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> Union[List[str], None]:
|
||||
renamed: List[str] = []
|
||||
remained: List[str] = []
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
if exp_name == new_name:
|
||||
return None
|
||||
|
||||
if exp_name:
|
||||
results: Dict[str, Union[ExpRefInfo, None]] = resolve_name(
|
||||
scm=repo.scm, exp_names=exp_name, git_remote=git_remote
|
||||
)
|
||||
for name, result in results.items():
|
||||
if result is None:
|
||||
remained.append(name)
|
||||
continue
|
||||
|
||||
new_ref = ExpRefInfo(baseline_sha=result.baseline_sha, name=new_name)
|
||||
if repo.scm.get_ref(str(new_ref)) and not force:
|
||||
raise ExperimentExistsError(new_name)
|
||||
|
||||
_rename_exp(scm=repo.scm, ref_info=result, new_name=new_name)
|
||||
renamed.append(name)
|
||||
|
||||
if remained:
|
||||
raise UnresolvedExpNamesError(remained, git_remote=git_remote)
|
||||
|
||||
return renamed
|
||||
|
||||
|
||||
def _rename_exp(scm: "Git", ref_info: "ExpRefInfo", new_name: str):
|
||||
rev = scm.get_ref(str(ref_info))
|
||||
scm.remove_ref(str(ref_info))
|
||||
ref_info.name = new_name
|
||||
scm.set_ref(str(ref_info), rev)
|
||||
return new_name
|
||||
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from dvc.repo.experiments.exceptions import (
|
||||
ExperimentExistsError,
|
||||
UnresolvedExpNamesError,
|
||||
)
|
||||
from dvc.repo.experiments.utils import exp_refs_by_names
|
||||
|
||||
|
||||
def test_rename_experiment_by_name(scm, dvc, exp_stage):
|
||||
dvc.experiments.run(exp_stage.addressing, name="test-name", params=["foo=1"])
|
||||
old_ref = exp_refs_by_names(scm, {"test-name"})
|
||||
dvc.experiments.rename("new-name", "test-name")
|
||||
new_ref = exp_refs_by_names(scm, {"new-name"})
|
||||
assert scm.get_ref(str(old_ref["test-name"][0])) is None
|
||||
assert scm.get_ref(str(new_ref["new-name"][0])) is not None
|
||||
with pytest.raises(UnresolvedExpNamesError):
|
||||
dvc.experiments.rename("new-name", "foo")
|
||||
|
||||
|
||||
def test_same_name(dvc, exp_stage):
|
||||
dvc.experiments.run(exp_stage.addressing, name="same-name", params=["foo=1"])
|
||||
assert dvc.experiments.rename("same-name", "same-name") is None
|
||||
|
||||
|
||||
def test_existing_name(dvc, exp_stage):
|
||||
dvc.experiments.run(exp_stage.addressing, name="first-name", params=["foo=1"])
|
||||
dvc.experiments.run(exp_stage.addressing, name="second-name", params=["foo=2"])
|
||||
|
||||
with pytest.raises(ExperimentExistsError):
|
||||
dvc.experiments.rename("second-name", "first-name")
|
||||
|
||||
dvc.experiments.rename("second-name", "first-name", force=True)
|
||||
@@ -9,6 +9,7 @@ from dvc.commands.experiments.ls import CmdExperimentsList
|
||||
from dvc.commands.experiments.pull import CmdExperimentsPull
|
||||
from dvc.commands.experiments.push import CmdExperimentsPush
|
||||
from dvc.commands.experiments.remove import CmdExperimentsRemove
|
||||
from dvc.commands.experiments.rename import CmdExperimentsRename
|
||||
from dvc.commands.experiments.run import CmdExperimentsRun
|
||||
from dvc.commands.experiments.save import CmdExperimentsSave
|
||||
from dvc.commands.experiments.show import CmdExperimentsShow
|
||||
@@ -418,6 +419,40 @@ def test_experiments_remove_invalid(dvc, scm, mocker, capsys, caplog):
|
||||
)
|
||||
|
||||
|
||||
def test_experiments_rename_flag(dvc, scm, mocker, capsys, caplog):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"experiments",
|
||||
"rename",
|
||||
"--git-remote",
|
||||
"myremote",
|
||||
"exp-123",
|
||||
"exp-234",
|
||||
]
|
||||
)
|
||||
assert cli_args.func == CmdExperimentsRename
|
||||
cmd = cli_args.func(cli_args)
|
||||
m = mocker.patch("dvc.repo.experiments.rename.rename", return_value={})
|
||||
assert cmd.run() == 0
|
||||
m.assert_called_once_with(
|
||||
cmd.repo,
|
||||
exp_name="exp-123",
|
||||
new_name="exp-234",
|
||||
git_remote="myremote",
|
||||
force=False,
|
||||
)
|
||||
|
||||
|
||||
def test_experiments_rename_invalid(dvc, scm, mocker, capsys, caplog):
|
||||
cmd = CmdExperimentsRename(parse_args(["exp", "rename", "exp-1"]))
|
||||
with pytest.raises(InvalidArgumentError) as excinfo:
|
||||
cmd.run()
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== "An experiment to rename and a new experiment name are required."
|
||||
)
|
||||
|
||||
|
||||
def test_experiments_save(dvc, scm, mocker):
|
||||
cli_args = parse_args(["exp", "save", "--name", "exp-name", "--force"])
|
||||
assert cli_args.func == CmdExperimentsSave
|
||||
|
||||
Reference in New Issue
Block a user