Skip to content

Commit c9049c5

Browse files
committed
studio: add subdir to live metrics post messages to support live experiments in monorepos
1 parent 5e2c441 commit c9049c5

File tree

6 files changed

+76
-63
lines changed

6 files changed

+76
-63
lines changed

dvc/repo/__init__.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
)
1414
from dvc.ignore import DvcIgnoreFilter
1515
from dvc.log import logger
16-
from dvc.utils import as_posix
1716
from dvc.utils.objects import cached_property
1817

1918
if TYPE_CHECKING:
@@ -351,16 +350,6 @@ def fs(self, fs: "FileSystem"):
351350
# fs.
352351
self._reset()
353352

354-
@property
355-
def subrepo_relpath(self) -> str:
356-
from dvc.fs import GitFileSystem
357-
358-
scm_root_dir = "/" if isinstance(self.fs, GitFileSystem) else self.scm.root_dir
359-
360-
relpath = as_posix(self.fs.relpath(self.root_dir, scm_root_dir))
361-
362-
return "" if relpath == "." else relpath
363-
364353
@property
365354
def data_index(self) -> "DataIndex":
366355
from dvc_data.index import DataIndex

dvc/repo/experiments/executor/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from dvc.stage.serialize import to_lockfile
2424
from dvc.utils import dict_sha256, env2bool, relpath
2525
from dvc.utils.fs import remove
26-
from dvc.utils.studio import env_to_config
26+
from dvc.utils.studio import (
27+
env_to_config,
28+
get_subrepo_relpath,
29+
)
2730

2831
if TYPE_CHECKING:
2932
from queue import Queue
@@ -624,6 +627,7 @@ def _repro_dvc(
624627
params=to_studio_params(dvc.params.show()),
625628
dvc_studio_config=dvc_studio_config,
626629
message=message,
630+
subdir=get_subrepo_relpath(dvc),
627631
)
628632
logger.debug("Running repro in '%s'", os.getcwd())
629633
yield dvc

dvc/utils/studio.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
DVC_STUDIO_URL,
1313
)
1414
from dvc.log import logger
15+
from dvc.utils import as_posix
1516

1617
if TYPE_CHECKING:
1718
from requests import Response
1819

20+
from dvc.repo import Repo
21+
22+
1923
logger = logger.getChild(__name__)
2024

2125
STUDIO_URL = "https://studio.iterative.ai"
@@ -111,3 +115,13 @@ def env_to_config(env: dict[str, Any]) -> dict[str, Any]:
111115
if DVC_STUDIO_URL in env:
112116
config["url"] = env[DVC_STUDIO_URL]
113117
return config
118+
119+
120+
def get_subrepo_relpath(repo: "Repo") -> str:
121+
from dvc.fs import GitFileSystem
122+
123+
scm_root_dir = "/" if isinstance(repo.fs, GitFileSystem) else repo.scm.root_dir
124+
125+
relpath = as_posix(repo.fs.relpath(repo.root_dir, scm_root_dir))
126+
127+
return "" if relpath == "." else relpath

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dependencies = [
3838
"dvc-data>=3.10,<3.12",
3939
"dvc-http>=2.29.0",
4040
"dvc-render>=1.0.1,<2",
41-
"dvc-studio-client>=0.19,<1",
41+
"dvc-studio-client>=0.20,<1",
4242
"dvc-task>=0.3.0,<1",
4343
"flatten_dict<1,>=0.4.1",
4444
# https://github.com/iterative/dvc/issues/9654

tests/integration/test_studio_live_experiments.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
DVC_STUDIO_TOKEN,
99
DVC_STUDIO_URL,
1010
)
11+
from dvc.repo import Repo
12+
from dvc.utils.studio import get_subrepo_relpath
1113

1214

15+
@pytest.mark.studio
1316
@pytest.mark.parametrize("tmp", [True, False])
1417
@pytest.mark.parametrize("offline", [True, False])
1518
def test_post_to_studio(
16-
tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp, offline
19+
M, tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp, offline
1720
):
1821
valid_response = mocker.MagicMock()
1922
valid_response.status_code = 200
@@ -66,9 +69,10 @@ def test_post_to_studio(
6669
}
6770

6871

72+
@pytest.mark.studio
6973
@pytest.mark.parametrize("tmp", [True, False])
7074
def test_post_to_studio_custom_message(
71-
tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp
75+
M, tmp_dir, dvc, scm, exp_stage, mocker, monkeypatch, tmp
7276
):
7377
valid_response = mocker.MagicMock()
7478
valid_response.status_code = 200
@@ -98,3 +102,53 @@ def test_post_to_studio_custom_message(
98102
"client": "dvc",
99103
"message": "foo",
100104
}
105+
106+
107+
@pytest.mark.studio
108+
def test_monorepo_relpath(tmp_dir, scm):
109+
from dvc.repo.destroy import destroy
110+
111+
tmp_dir.gen({"project_a": {}, "subdir/project_b": {}})
112+
113+
non_monorepo = Repo.init(tmp_dir)
114+
assert get_subrepo_relpath(non_monorepo) == ""
115+
116+
destroy(non_monorepo)
117+
118+
monorepo_project_a = Repo.init(tmp_dir / "project_a", subdir=True)
119+
120+
assert get_subrepo_relpath(monorepo_project_a) == "project_a"
121+
122+
monorepo_project_b = Repo.init(tmp_dir / "subdir" / "project_b", subdir=True)
123+
124+
assert get_subrepo_relpath(monorepo_project_b) == "subdir/project_b"
125+
126+
127+
@pytest.mark.studio
128+
def test_virtual_monorepo_relpath(tmp_dir, scm):
129+
from dvc.fs.git import GitFileSystem
130+
from dvc.repo.destroy import destroy
131+
132+
tmp_dir.gen({"project_a": {}, "subdir/project_b": {}})
133+
scm.commit("initial commit")
134+
gfs = GitFileSystem(scm=scm, rev="master")
135+
136+
non_monorepo = Repo.init(tmp_dir)
137+
non_monorepo.fs = gfs
138+
non_monorepo.root_dir = "/"
139+
140+
assert get_subrepo_relpath(non_monorepo) == ""
141+
142+
destroy(non_monorepo)
143+
144+
monorepo_project_a = Repo.init(tmp_dir / "project_a", subdir=True)
145+
monorepo_project_a.fs = gfs
146+
monorepo_project_a.root_dir = "/project_a"
147+
148+
assert get_subrepo_relpath(monorepo_project_a) == "project_a"
149+
150+
monorepo_project_b = Repo.init(tmp_dir / "subdir" / "project_b", subdir=True)
151+
monorepo_project_b.fs = gfs
152+
monorepo_project_b.root_dir = "/subdir/project_b"
153+
154+
assert get_subrepo_relpath(monorepo_project_b) == "subdir/project_b"

tests/unit/repo/test_repo.py

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -135,51 +135,3 @@ def test_dynamic_cache_initialization(tmp_dir, scm):
135135
dvc.close()
136136

137137
Repo(str(tmp_dir)).close()
138-
139-
140-
def test_monorepo_relpath(tmp_dir, scm):
141-
from dvc.repo.destroy import destroy
142-
143-
tmp_dir.gen({"project_a": {}, "subdir/project_b": {}})
144-
145-
non_monorepo = Repo.init(tmp_dir)
146-
assert non_monorepo.subrepo_relpath == ""
147-
148-
destroy(non_monorepo)
149-
150-
monorepo_project_a = Repo.init(tmp_dir / "project_a", subdir=True)
151-
152-
assert monorepo_project_a.subrepo_relpath == "project_a"
153-
154-
monorepo_project_b = Repo.init(tmp_dir / "subdir" / "project_b", subdir=True)
155-
156-
assert monorepo_project_b.subrepo_relpath == "subdir/project_b"
157-
158-
159-
def test_virtual_monorepo_relpath(tmp_dir, scm):
160-
from dvc.fs.git import GitFileSystem
161-
from dvc.repo.destroy import destroy
162-
163-
tmp_dir.gen({"project_a": {}, "subdir/project_b": {}})
164-
scm.commit("initial commit")
165-
gfs = GitFileSystem(scm=scm, rev="master")
166-
167-
non_monorepo = Repo.init(tmp_dir)
168-
non_monorepo.fs = gfs
169-
non_monorepo.root_dir = "/"
170-
171-
assert non_monorepo.subrepo_relpath == ""
172-
173-
destroy(non_monorepo)
174-
175-
monorepo_project_a = Repo.init(tmp_dir / "project_a", subdir=True)
176-
monorepo_project_a.fs = gfs
177-
monorepo_project_a.root_dir = "/project_a"
178-
179-
assert monorepo_project_a.subrepo_relpath == "project_a"
180-
181-
monorepo_project_b = Repo.init(tmp_dir / "subdir" / "project_b", subdir=True)
182-
monorepo_project_b.fs = gfs
183-
monorepo_project_b.root_dir = "/subdir/project_b"
184-
185-
assert monorepo_project_b.subrepo_relpath == "subdir/project_b"

0 commit comments

Comments
 (0)