mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-04 07:18:27 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086d51d9de | ||
|
|
1d8b2e5fa6 | ||
|
|
894f530d22 | ||
|
|
1b9d40960f | ||
|
|
696fabc745 | ||
|
|
780dc72301 | ||
|
|
1ca3ef14a3 | ||
|
|
a6c89cac8d | ||
|
|
593f1064cf | ||
|
|
35bb261b89 | ||
|
|
fe6d543c1f | ||
|
|
4351d32de4 | ||
|
|
efc2d7ad3d | ||
|
|
7c65739e7e | ||
|
|
cc4816f652 | ||
|
|
faa1bdfbce | ||
|
|
006b6f3513 | ||
|
|
1bc4c1dceb | ||
|
|
531b2110cf | ||
|
|
c1917ed751 | ||
|
|
43195a806d | ||
|
|
d41512c85c | ||
|
|
e9fb0b92aa | ||
|
|
5485275b92 | ||
|
|
1f80f2f2ed | ||
|
|
6b6dc01ac4 | ||
|
|
91d3ff0f69 | ||
|
|
701f48197e | ||
|
|
da3bbccf66 | ||
|
|
40484e8d57 | ||
|
|
48b56c83dc | ||
|
|
f8bbf20b77 | ||
|
|
cd2c4d0caf | ||
|
|
9b329d01bf | ||
|
|
5c442cb867 | ||
|
|
2d5c15bee1 | ||
|
|
7868c1301c | ||
|
|
dfe76c54d7 | ||
|
|
392c15e1d4 | ||
|
|
121d56b739 | ||
|
|
66406e6c99 | ||
|
|
8399bf612a | ||
|
|
6c1b786564 | ||
|
|
d1c5fcc1b8 | ||
|
|
4ee3271b0c | ||
|
|
dae8515933 | ||
|
|
f02dd83e53 | ||
|
|
a2066d75f9 | ||
|
|
283d8245e1 | ||
|
|
c52b12ac2a | ||
|
|
440cb9d2bc | ||
|
|
2d820cccad | ||
|
|
30c3e3e4d4 | ||
|
|
58029e9719 | ||
|
|
990e834f1d | ||
|
|
33a2449c91 | ||
|
|
e2ce95e17e | ||
|
|
53f9953cd1 | ||
|
|
e6419b939d | ||
|
|
37ccc5e8d8 | ||
|
|
8312eb9b42 | ||
|
|
c8745dd580 | ||
|
|
1cf8b5e582 | ||
|
|
12955da3c3 | ||
|
|
e73cc9b40a | ||
|
|
b56f6b69dd | ||
|
|
54a6709d99 | ||
|
|
e4a5a2aec5 | ||
|
|
16ecd53b71 | ||
|
|
612f3039e0 | ||
|
|
17f523bf86 | ||
|
|
a0bf2d997a | ||
|
|
8ed9b911db | ||
|
|
ef22b1d77e | ||
|
|
aaa825a9a8 | ||
|
|
2a40a55bb4 | ||
|
|
3aa9f51e36 | ||
|
|
1cfdc22549 | ||
|
|
2b889baa5e | ||
|
|
8589aa4001 | ||
|
|
8cbc4c43f1 | ||
|
|
417c327143 | ||
|
|
204eebaf15 | ||
|
|
3a6fd75d98 | ||
|
|
351b952d5f | ||
|
|
fe7db32311 | ||
|
|
0bd34c8977 | ||
|
|
48a63ca588 | ||
|
|
4459d6ac57 | ||
|
|
08a50ee36e | ||
|
|
f1ea64c4ef | ||
|
|
7ede45af2d | ||
|
|
51db1765ee | ||
|
|
9101fce761 | ||
|
|
3f842a205f | ||
|
|
77e900fed2 |
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Epic/Story Issue Template
|
||||
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
|
||||
title: 'Epic: New Feature'
|
||||
labels: epic
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
## Summary / Background
|
||||
What do you want to achieve, why? business context
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
What will be impacted and what won't be?
|
||||
What needs implementation and what is invariant?
|
||||
e.g.
|
||||
- user should be able to run workflow X from UI
|
||||
- enable workflow Y, Z from CLI
|
||||
|
||||
## Assumptions
|
||||
Product / UX assumptions as well as technical assumptions / limitations
|
||||
e.g.
|
||||
* Support only Python Runtime
|
||||
* Focus on DVC experiments only
|
||||
* Deployment environments don't change often and can be picked up from shared configuration
|
||||
|
||||
## Open Questions
|
||||
e.g.
|
||||
- How should access control work for shared artifacts (workflow X)
|
||||
- Python runtime assumption - is it really valid? in light of <...>
|
||||
|
||||
## Blockers / Dependencies
|
||||
List issues or other conditions / blockers
|
||||
|
||||
## General Approach
|
||||
Invocation example:
|
||||
```shell
|
||||
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Must have (p1)
|
||||
- [ ] subissue2
|
||||
- [ ] step 2
|
||||
- info
|
||||
- info
|
||||
|
||||
### Optional / followup (p2)
|
||||
- [ ] ⌛ step 3 wip
|
||||
- [ ] step 4
|
||||
|
||||
## Timelines
|
||||
|
||||
Put your estimations here. Update once certainty changes
|
||||
- end of week (Feb 3) for prototype with workflows X, Y
|
||||
- Feb 15 - MVP in production
|
||||
- Low priority followups can be done later
|
||||
@@ -43,11 +43,11 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.254'
|
||||
rev: 'v0.0.257'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
rev: v2.2.4
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ def is_enabled():
|
||||
enabled = not os.getenv(DVC_NO_ANALYTICS)
|
||||
if enabled:
|
||||
enabled = to_bool(
|
||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||
Config.from_cwd(validate=False).get("core", {}).get("analytics", "true")
|
||||
)
|
||||
|
||||
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .experiments import exp_save, make_checkpoint
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"exp_save",
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
|
||||
+66
-14
@@ -1,12 +1,30 @@
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional
|
||||
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.exceptions import FileMissingError, OutputNotFoundError, PathMissingError
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _wrap_exceptions(repo, url):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputInExternalRepoError, NoRemoteInExternalRepoError
|
||||
|
||||
try:
|
||||
yield
|
||||
except NoRemoteError as exc:
|
||||
raise NoRemoteInExternalRepoError(url) from exc
|
||||
except OutputNotFoundError as exc:
|
||||
if exc.repo is repo:
|
||||
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
|
||||
raise
|
||||
except FileMissingError as exc:
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
@@ -19,18 +37,20 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
with _wrap_exceptions(_repo, path):
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
|
||||
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
@@ -195,10 +215,42 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
with _repo.open_by_relpath(
|
||||
path, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
yield fd
|
||||
with _wrap_exceptions(_repo, path):
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
else:
|
||||
fs = DVCFileSystem(repo=_repo, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
if remote:
|
||||
remote_odb = _repo.cloud.get_remote_odb(name=remote)
|
||||
oid = fs.info(fs_path)["dvc_info"]["md5"]
|
||||
fs = remote_odb.fs
|
||||
fs_path = remote_odb.oid_to_path(oid)
|
||||
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
raise FileMissingError(path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
@@ -31,3 +32,40 @@ def make_checkpoint():
|
||||
os.fsync(fobj.fileno())
|
||||
while os.path.exists(signal_file):
|
||||
sleep(0.1)
|
||||
|
||||
|
||||
def exp_save(
|
||||
name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Create a new DVC experiment using `exp save`.
|
||||
|
||||
See https://dvc.org/doc/command-reference/exp/save.
|
||||
|
||||
Args:
|
||||
name (str, optional): specify a name for this experiment.
|
||||
If `None`, a default one will be generated, such as `urban-sign`.
|
||||
Defaults to `None`.
|
||||
force (bool): overwrite the experiment if an experiment with the same
|
||||
name already exists.
|
||||
Defaults to `False`.
|
||||
include_untracked (List[str], optional): specify untracked file(s) to
|
||||
be included in the saved experiment.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
str: The `Git revision`_ of the created experiment.
|
||||
|
||||
Raises:
|
||||
ExperimentExistsError: If an experiment with `name` already exists and
|
||||
`force=False`.
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
with Repo() as repo:
|
||||
return repo.experiments.save(
|
||||
name=name, force=force, include_untracked=include_untracked
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
@@ -150,6 +151,7 @@ def get_parent_parser():
|
||||
return parent_parser
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_main_parser():
|
||||
parent_parser = get_parent_parser()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class CmdCheckIgnore(CmdBase):
|
||||
target = input()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
if target == "":
|
||||
if not target:
|
||||
break
|
||||
if not self._check_one_file(target):
|
||||
ret = 0
|
||||
|
||||
@@ -32,7 +32,7 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
super().__init__(args)
|
||||
|
||||
self.config = Config(validate=False)
|
||||
self.config = Config.from_cwd(validate=False)
|
||||
|
||||
def run(self):
|
||||
if self.args.show_origin and (self.args.value or self.args.unset):
|
||||
|
||||
@@ -26,5 +26,13 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
experiments_branch_parser.add_argument(
|
||||
"experiment", help="Experiment to be promoted."
|
||||
)
|
||||
experiments_branch_parser.add_argument("branch", help="Name for the new Git branch")
|
||||
experiments_branch_parser.add_argument(
|
||||
"branch",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help=(
|
||||
"Optional name for the new Git branch. "
|
||||
"Defaults to '{experiment-name}-branch'."
|
||||
),
|
||||
)
|
||||
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||
|
||||
@@ -14,7 +14,7 @@ class CmdExperimentsClean(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_CLEAN_HELP = "Cleanup dvc exp internal tempfiles."
|
||||
EXPERIMENTS_CLEAN_HELP = "Cleanup experiments temporary internal files."
|
||||
experiments_clean_parser = experiments_subparsers.add_parser(
|
||||
"clean",
|
||||
parents=[parent_parser],
|
||||
|
||||
@@ -52,7 +52,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments in the DVC repository."
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments."
|
||||
|
||||
experiments_diff_parser = experiments_subparsers.add_parser(
|
||||
"diff",
|
||||
|
||||
@@ -78,7 +78,8 @@ class CmdExperimentsGC(CmdBase):
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||
EXPERIMENTS_GC_DESCRIPTION = (
|
||||
"Removes all experiments which are not derived from the specifiedGit revisions."
|
||||
"Removes all experiments which are not derived"
|
||||
" from the specified Git revisions."
|
||||
)
|
||||
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||
"gc",
|
||||
|
||||
@@ -19,9 +19,11 @@ class CmdExperimentsPush(CmdBase):
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
pushed_exps = self.repo.experiments.push(
|
||||
result = self.repo.experiments.push(
|
||||
self.args.git_remote,
|
||||
self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -33,14 +35,17 @@ class CmdExperimentsPush(CmdBase):
|
||||
jobs=self.args.jobs,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
|
||||
if pushed_exps:
|
||||
if pushed_exps := result.get("pushed"):
|
||||
ui.write(
|
||||
f"Pushed experiment '{pushed_exps}'"
|
||||
f"to Git remote '{self.args.git_remote}'."
|
||||
f"Pushed experiment {humanize.join(map(repr, pushed_exps))} "
|
||||
f"to Git remote {self.args.git_remote!r}."
|
||||
)
|
||||
else:
|
||||
ui.write("No experiments to push.")
|
||||
|
||||
if project_url := result.get("url"):
|
||||
ui.write("[yellow]View your experiments at", project_url, styled=True)
|
||||
|
||||
if not self.args.push_cache:
|
||||
ui.write(
|
||||
"To push cached outputs",
|
||||
|
||||
@@ -24,9 +24,11 @@ class CmdExperimentsRemove(CmdBase):
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
self.check_arguments()
|
||||
|
||||
removed_list = self.repo.experiments.remove(
|
||||
removed = self.repo.experiments.remove(
|
||||
exp_names=self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
@@ -34,8 +36,10 @@ class CmdExperimentsRemove(CmdBase):
|
||||
queue=self.args.queue,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
removed = ",".join(removed_list)
|
||||
ui.write(f"Removed experiments: {removed}")
|
||||
if removed:
|
||||
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
||||
else:
|
||||
ui.write("No experiments to remove.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as an experiment."
|
||||
save_parser = experiments_subparsers.add_parser(
|
||||
"save",
|
||||
parents=[parent_parser],
|
||||
|
||||
+2
-6
@@ -14,12 +14,8 @@ class CmdGet(CmdBaseNoRepo):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
try:
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
except DvcException:
|
||||
logger.exception("failed to show URL")
|
||||
return 1
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
+54
-11
@@ -1,9 +1,9 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
from funcy import first
|
||||
from funcy import compact, first, get_in
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
@@ -12,14 +12,51 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.render.match import RendererWithErrors
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_json(renderers, split=False):
|
||||
def _show_json(
|
||||
renderers_with_errors: List["RendererWithErrors"],
|
||||
split=False,
|
||||
errors: Optional[Dict[str, Exception]] = None,
|
||||
):
|
||||
from dvc.render.convert import to_json
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
|
||||
ui.write_json(result)
|
||||
all_errors: List[Dict] = []
|
||||
data = {}
|
||||
|
||||
for renderer, src_errors, def_errors in renderers_with_errors:
|
||||
name = renderer.name
|
||||
data[name] = to_json(renderer, split)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
"source": source,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, per_rev_src_errors in src_errors.items()
|
||||
for source, e in per_rev_src_errors.items()
|
||||
)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, e in def_errors.items()
|
||||
)
|
||||
|
||||
# these errors are not tied to any renderers
|
||||
errors = errors or {}
|
||||
all_errors.extend({"rev": rev, **encode_exception(e)} for rev, e in errors.items())
|
||||
|
||||
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
@@ -85,7 +122,7 @@ class CmdPlots(CmdBase):
|
||||
if html_template_path and not os.path.isabs(html_template_path):
|
||||
assert self.repo.dvc_dir
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path # noqa: RET504
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
from pathlib import Path
|
||||
@@ -112,7 +149,7 @@ class CmdPlots(CmdBase):
|
||||
props=self._props(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
if not plots_data and not self.args.json:
|
||||
ui.error_write(
|
||||
"No plots were loaded, visualization file will not be created."
|
||||
)
|
||||
@@ -122,21 +159,27 @@ class CmdPlots(CmdBase):
|
||||
)
|
||||
|
||||
renderers_out = out if self.args.json else os.path.join(out, "static")
|
||||
|
||||
renderers = match_defs_renderers(
|
||||
renderers_with_errors = match_defs_renderers(
|
||||
data=plots_data,
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
_show_json(renderers, self.args.split)
|
||||
errors = compact(
|
||||
{
|
||||
rev: get_in(data, ["definitions", "error"])
|
||||
for rev, data in plots_data.items()
|
||||
}
|
||||
)
|
||||
_show_json(renderers_with_errors, self.args.split, errors=errors)
|
||||
return 0
|
||||
|
||||
renderers = [r.renderer for r in renderers_with_errors]
|
||||
_adjust_vega_renderers(renderers)
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
ui.write_json(renderer.get_filled_template(as_string=False))
|
||||
return 0
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
|
||||
+13
-9
@@ -103,21 +103,25 @@ class Config(dict):
|
||||
self.wfs = LocalFileSystem()
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
if not dvc_dir:
|
||||
try:
|
||||
from dvc.repo import Repo
|
||||
|
||||
self.dvc_dir = Repo.find_dvc_dir()
|
||||
except NotDvcRepoError:
|
||||
self.dvc_dir = None
|
||||
else:
|
||||
if dvc_dir:
|
||||
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
|
||||
|
||||
self.load(validate=validate, config=config)
|
||||
|
||||
@classmethod
|
||||
def from_cwd(cls, fs: Optional["FileSystem"] = None, **kwargs):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
dvc_dir = Repo.find_dvc_dir(fs=fs)
|
||||
except NotDvcRepoError:
|
||||
dvc_dir = None
|
||||
|
||||
return cls(dvc_dir=dvc_dir, fs=fs, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from appdirs import site_config_dir, user_config_dir
|
||||
from platformdirs import site_config_dir, user_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
|
||||
+49
-12
@@ -1,8 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import walk_values
|
||||
from voluptuous import All, Any, Coerce, Invalid, Lower, Optional, Range, Schema
|
||||
from funcy import once, walk_values
|
||||
from voluptuous import (
|
||||
REMOVE_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Bool = All(
|
||||
Lower,
|
||||
@@ -49,9 +62,9 @@ def ByUrl(mapping): # noqa: N802
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if parsed.netloc == "":
|
||||
if not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
@@ -65,6 +78,27 @@ class RelPath(str):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureSchema(Schema):
|
||||
def __init__(self, schema, required=False):
|
||||
super().__init__(schema, required=required, extra=REMOVE_EXTRA)
|
||||
|
||||
@staticmethod
|
||||
@once
|
||||
def _log_deprecated(keys):
|
||||
# only run this once per session
|
||||
message = "%s config option%s unsupported"
|
||||
paths = ", ".join(f"'feature.{key}'" for key in keys)
|
||||
pluralize = " is" if len(keys) == 1 else "s are"
|
||||
logger.warning(message, paths, pluralize)
|
||||
|
||||
def __call__(self, data):
|
||||
ret = super().__call__(data)
|
||||
extra_keys = data.keys() - ret.keys()
|
||||
if extra_keys:
|
||||
self._log_deprecated(sorted(extra_keys))
|
||||
return ret
|
||||
|
||||
|
||||
REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
@@ -239,12 +273,12 @@ SCHEMA = {
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
@@ -262,12 +296,15 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
# section for experimental features
|
||||
"feature": {
|
||||
Optional("machine", default=False): Bool,
|
||||
# enabled by default. It's of no use, kept for backward compatibility.
|
||||
Optional("data_index_cache", default=False): Bool,
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
# only specified keys are validated, others get logged and then ignored/removed
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
Optional("push_exp_to_studio", default=False): Bool,
|
||||
"studio_token": str,
|
||||
"studio_url": str,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
"html_template": str,
|
||||
Optional("auto_open", default=False): Bool,
|
||||
|
||||
+20
-2
@@ -14,7 +14,14 @@ from dvc.utils import fix_env, is_binary
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
def _suppress_resource_warning(popen: Popen):
|
||||
"""Sets the returncode to avoid ResourceWarning when popen is garbage collected."""
|
||||
# only use for daemon processes.
|
||||
# See https://bugs.python.org/issue38890.
|
||||
popen.returncode = 0
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs) -> Popen:
|
||||
prefix = [sys.executable]
|
||||
if not is_binary():
|
||||
main_entrypoint = os.path.join(
|
||||
@@ -40,12 +47,22 @@ def _spawn_windows(cmd, env):
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
|
||||
popen = _popen(
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
|
||||
)
|
||||
_suppress_resource_warning(popen)
|
||||
|
||||
|
||||
def _spawn_posix(cmd, env):
|
||||
from dvc.cli import main
|
||||
|
||||
# `fork` will copy buffers, so we need to flush them before forking.
|
||||
# Otherwise, we will get duplicated outputs.
|
||||
if sys.stdout and not sys.stdout.closed:
|
||||
sys.stdout.flush()
|
||||
if sys.stderr and not sys.stderr.closed:
|
||||
sys.stderr.flush()
|
||||
|
||||
# NOTE: using os._exit instead of sys.exit, because dvc built
|
||||
# with PyInstaller has trouble with SystemExit exception and throws
|
||||
# errors such as "[26338] Failed to execute script __main__"
|
||||
@@ -72,6 +89,7 @@ def _spawn_posix(cmd, env):
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
sys.stderr.close()
|
||||
os.closerange(0, 3)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# workaround for MacOS bug
|
||||
|
||||
+4
-4
@@ -75,7 +75,7 @@ class DataCloud:
|
||||
config["version_aware"] = True
|
||||
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
config["tmp_dir"] = self.repo.site_cache_dir
|
||||
if self.repo.data_index is not None:
|
||||
index = self.repo.data_index.view(("remote", name))
|
||||
else:
|
||||
@@ -84,14 +84,14 @@ class DataCloud:
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
"no remote specified. Setup default remote with\n"
|
||||
f"no remote specified in {self.repo}. Setup default remote with\n"
|
||||
" dvc remote default <remote name>\n"
|
||||
"or use:\n"
|
||||
" dvc {} -r <remote name>".format(command)
|
||||
f" dvc {command} -r <remote name>"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"no remote specified. Create a default remote with\n"
|
||||
f"no remote specified in {self.repo}. Create a default remote with\n"
|
||||
" dvc remote add -d <remote name> <remote url>"
|
||||
)
|
||||
|
||||
|
||||
+21
-4
@@ -1,3 +1,4 @@
|
||||
import errno
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
@@ -36,11 +37,19 @@ class RepoDependency(Dependency):
|
||||
}
|
||||
|
||||
def __init__(self, def_repo: Dict[str, str], stage: "Stage", *args, **kwargs):
|
||||
from dvc.fs import DVCFileSystem
|
||||
|
||||
self.def_repo = def_repo
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
self._meta: Dict[str, "Meta"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
self.fs = DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
rev=self._get_rev(),
|
||||
)
|
||||
self.fs_path = self.def_path
|
||||
|
||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||
return None
|
||||
|
||||
@@ -106,7 +115,7 @@ class RepoDependency(Dependency):
|
||||
self, obj_only: bool = False, **kwargs
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "Meta", "HashFile"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.exceptions import NoOutputOrStageError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
@@ -143,8 +152,10 @@ class RepoDependency(Dependency):
|
||||
local_odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
except (FileNotFoundError, TreeError) as exc:
|
||||
raise PathMissingError(
|
||||
self.def_path, self.def_repo[self.PARAM_URL]
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT,
|
||||
os.strerror(errno.ENOENT) + f" in {self.def_repo[self.PARAM_URL]}",
|
||||
self.def_path,
|
||||
) from exc
|
||||
object_store = copy(object_store)
|
||||
object_store.read_only = True
|
||||
@@ -215,7 +226,13 @@ class RepoDependency(Dependency):
|
||||
|
||||
d = self.def_repo
|
||||
rev = self._get_rev(locked=locked)
|
||||
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
|
||||
return external_repo(
|
||||
d[self.PARAM_URL],
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _get_rev(self, locked: bool = True):
|
||||
d = self.def_repo
|
||||
|
||||
+1
-1
@@ -366,7 +366,7 @@ def get_lockfile_schema(d):
|
||||
|
||||
|
||||
def migrate_lock_v1_to_v2(d, version_info):
|
||||
stages = {k: v for k, v in d.items()}
|
||||
stages = dict(d)
|
||||
|
||||
for key in stages:
|
||||
d.pop(key)
|
||||
|
||||
+9
-4
@@ -31,14 +31,19 @@ class OutputDuplicationError(DvcException):
|
||||
|
||||
assert isinstance(output, str)
|
||||
assert all(hasattr(stage, "relpath") for stage in stages)
|
||||
msg = ""
|
||||
stage_names = [s.addressing for s in stages]
|
||||
stages_str = " ".join(stage_names)
|
||||
if len(stages) == 1:
|
||||
msg = "output '{}' is already specified in {}.".format(
|
||||
output, first(stages)
|
||||
)
|
||||
stage_name = first(stages)
|
||||
msg = f"output '{output}' is already specified in {stage_name}."
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages)
|
||||
output, "\n".join(f"\t- {s}" for s in stage_names)
|
||||
)
|
||||
msg += (
|
||||
f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
self.output = output
|
||||
|
||||
+8
-37
@@ -7,14 +7,7 @@ from typing import TYPE_CHECKING, Collection, Dict, Iterator, Optional, Tuple
|
||||
|
||||
from funcy import retry, wrap_with
|
||||
|
||||
from dvc.exceptions import (
|
||||
FileMissingError,
|
||||
NoOutputInExternalRepoError,
|
||||
NoRemoteInExternalRepoError,
|
||||
NotDvcRepoError,
|
||||
OutputNotFoundError,
|
||||
PathMissingError,
|
||||
)
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import CloneError, map_scm_exception
|
||||
from dvc.utils import relpath
|
||||
@@ -37,10 +30,6 @@ def external_repo(
|
||||
cache_types: Optional[Collection[str]] = None,
|
||||
**kwargs,
|
||||
) -> Iterator["Repo"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.fs import GitFileSystem
|
||||
from dvc.scm import Git
|
||||
|
||||
logger.debug("Creating external repo %s@%s", url, rev)
|
||||
path = _cached_clone(url, rev, for_write=for_write)
|
||||
# Local HEAD points to the tip of whatever branch we first cloned from
|
||||
@@ -55,43 +44,25 @@ def external_repo(
|
||||
config = _get_remote_config(url) if os.path.isdir(url) else {}
|
||||
config.update(cache_config)
|
||||
|
||||
main_root = "/"
|
||||
if for_write:
|
||||
scm = None
|
||||
root_dir = path
|
||||
fs = None
|
||||
else:
|
||||
scm = Git(path)
|
||||
fs = GitFileSystem(scm=scm, rev=rev)
|
||||
root_dir = "/"
|
||||
# we already checked out needed revision
|
||||
rev = None
|
||||
main_root = path
|
||||
|
||||
repo_kwargs = dict(
|
||||
root_dir=root_dir,
|
||||
root_dir=path,
|
||||
url=url,
|
||||
fs=fs,
|
||||
config=config,
|
||||
repo_factory=erepo_factory(url, root_dir, cache_config),
|
||||
scm=scm,
|
||||
repo_factory=erepo_factory(url, main_root, cache_config),
|
||||
rev=rev,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if "subrepos" not in repo_kwargs:
|
||||
repo_kwargs["subrepos"] = True
|
||||
|
||||
if "uninitialized" not in repo_kwargs:
|
||||
repo_kwargs["uninitialized"] = True
|
||||
|
||||
repo = Repo(**repo_kwargs)
|
||||
|
||||
try:
|
||||
yield repo
|
||||
except NoRemoteError as exc:
|
||||
raise NoRemoteInExternalRepoError(url) from exc
|
||||
except OutputNotFoundError as exc:
|
||||
if exc.repo is repo:
|
||||
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
|
||||
raise
|
||||
except FileMissingError as exc:
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
finally:
|
||||
repo.close()
|
||||
if for_write:
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def _get_cloud_fs(repo_config, **kwargs):
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf) # type: ignore[index]
|
||||
except Invalid as exc:
|
||||
raise RepoConfigError(str(exc)) from None
|
||||
|
||||
|
||||
+17
-4
@@ -251,12 +251,15 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
try:
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
kw = {}
|
||||
if kwargs.get("cache_remote_stream", False):
|
||||
kw["cache_odb"] = repo.cache.local
|
||||
return dvc_fs.open(dvc_path, mode=mode, **kw)
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
@@ -265,19 +268,22 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
def ls( # pylint: disable=arguments-differ # noqa: C901
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_exists = False
|
||||
dvc_infos = {}
|
||||
if dvc_fs:
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
|
||||
|
||||
fs_exists = False
|
||||
fs_infos = {}
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
@@ -291,11 +297,18 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
|
||||
fs_exists = bool(fs_infos) or fs.exists(fs_path)
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
names = set(dvc_infos.keys()) | set(fs_infos.keys())
|
||||
|
||||
if not names and (dvc_exists or fs_exists):
|
||||
# broken symlink or TreeError
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
for name in names:
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
|
||||
+2
-1
@@ -54,6 +54,7 @@ def get_dvc_info():
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
|
||||
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
except SCMError:
|
||||
@@ -138,7 +139,7 @@ def _get_supported_remotes():
|
||||
def get_fs_type(path):
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype != "":
|
||||
if part.fstype:
|
||||
try:
|
||||
mountpoint = pathlib.Path(part.mountpoint).resolve()
|
||||
partition[mountpoint] = part.fstype + " on " + part.device
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
@@ -426,7 +427,7 @@ class Output:
|
||||
self.hash_info = tree.hash_info
|
||||
self.meta.isdir = True
|
||||
self.meta.nfiles = len(self.files)
|
||||
self.meta.size = sum(f.get("size") for f in self.files)
|
||||
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
|
||||
self.meta.remote = first(f.get("remote") for f in self.files)
|
||||
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
@@ -1017,7 +1018,8 @@ class Output:
|
||||
except FileNotFoundError:
|
||||
if self.remote:
|
||||
kwargs["remote"] = self.remote
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
with suppress(Exception):
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
return self.obj
|
||||
|
||||
@@ -392,7 +392,7 @@ class ForeachDefinition:
|
||||
"""Convert sequence to Mapping with keys normalized."""
|
||||
iterable = self.resolved_iterable
|
||||
if isinstance(iterable, Mapping):
|
||||
return iterable
|
||||
return {to_str(k): v for k, v in iterable.items()}
|
||||
|
||||
assert isinstance(iterable, Sequence)
|
||||
if any(map(is_map_or_seq, iterable)):
|
||||
|
||||
@@ -95,7 +95,7 @@ def _(obj: bool):
|
||||
def _(obj: dict): # noqa: C901
|
||||
from dvc.config import Config
|
||||
|
||||
config = Config().get("parsing", {})
|
||||
config = Config.from_cwd().get("parsing", {})
|
||||
|
||||
result = ""
|
||||
for k, v in flatten(obj).items():
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class ProcessNotTerminatedError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' has not been terminated.")
|
||||
|
||||
|
||||
class ProcessNotFoundError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' does not exist.")
|
||||
|
||||
|
||||
class TimeoutExpired(DvcException):
|
||||
def __init__(self, cmd, timeout):
|
||||
super().__init__(f"'{cmd}' did not complete before timeout '{timeout}'")
|
||||
self.cmd = cmd
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class UnsupportedSignalError(DvcException):
|
||||
def __init__(self, sig):
|
||||
super().__init__(f"Unsupported signal: {sig}")
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Serverless process manager."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Generator, List, Optional, Tuple, Union
|
||||
|
||||
from funcy.flow import reraise
|
||||
from shortuuid import uuid
|
||||
|
||||
from .exceptions import ProcessNotTerminatedError, UnsupportedSignalError
|
||||
from .process import ManagedProcess, ProcessInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""Manager for controlling background ManagedProcess(es).
|
||||
|
||||
Spawned process entries are kept in the manager directory until they
|
||||
are explicitly removed (with remove() or cleanup()) so that return
|
||||
value and log information can be accessed after a process has completed.
|
||||
"""
|
||||
|
||||
def __init__(self, wdir: Optional[str] = None):
|
||||
self.wdir = wdir or "."
|
||||
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
if not os.path.exists(self.wdir):
|
||||
return
|
||||
yield from os.listdir(self.wdir)
|
||||
|
||||
def __getitem__(self, key: str) -> "ProcessInfo":
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
try:
|
||||
with open(info_path, encoding="utf-8") as fobj:
|
||||
return ProcessInfo.from_dict(json.load(fobj))
|
||||
except FileNotFoundError:
|
||||
raise KeyError # noqa: B904
|
||||
|
||||
@reraise(FileNotFoundError, KeyError)
|
||||
def __setitem__(self, key: str, value: "ProcessInfo"):
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
with open(info_path, "w", encoding="utf-8") as fobj:
|
||||
return json.dump(value.asdict(), fobj)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
path = os.path.join(self.wdir, key)
|
||||
if os.path.exists(path):
|
||||
remove(path)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def processes(self) -> Generator[Tuple[str, "ProcessInfo"], None, None]:
|
||||
for name in self:
|
||||
try:
|
||||
yield name, self[name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def spawn(self, args: Union[str, List[str]], name: Optional[str] = None):
|
||||
"""Run the given command in the background."""
|
||||
name = name or uuid()
|
||||
pid = ManagedProcess.spawn(
|
||||
args,
|
||||
wdir=os.path.join(self.wdir, name),
|
||||
name=name,
|
||||
)
|
||||
logger.debug(
|
||||
"Spawned managed process '%s' (PID: '%d')",
|
||||
name,
|
||||
pid,
|
||||
)
|
||||
|
||||
def send_signal(self, name: str, sig: int):
|
||||
"""Send `signal` to the specified named process."""
|
||||
process_info = self[name]
|
||||
if sys.platform == "win32" and sig not in (
|
||||
signal.SIGTERM,
|
||||
signal.CTRL_C_EVENT, # pylint: disable=no-member
|
||||
signal.CTRL_BREAK_EVENT, # pylint: disable=no-member
|
||||
):
|
||||
raise UnsupportedSignalError(sig)
|
||||
|
||||
def handle_closed_process():
|
||||
logging.warning("Process %s had already aborted unexpectedly.", name)
|
||||
process_info.returncode = -1
|
||||
self[name] = process_info
|
||||
|
||||
if process_info.returncode is None:
|
||||
try:
|
||||
os.kill(process_info.pid, sig)
|
||||
except ProcessLookupError:
|
||||
handle_closed_process()
|
||||
raise
|
||||
except OSError as exc:
|
||||
if getattr(exc, "winerror", None) == 87:
|
||||
handle_closed_process()
|
||||
raise ProcessLookupError from exc
|
||||
raise
|
||||
|
||||
def terminate(self, name: str):
|
||||
"""Terminate the specified named process."""
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
|
||||
def kill(self, name: str):
|
||||
"""Kill the specified named process."""
|
||||
if sys.platform == "win32":
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
else:
|
||||
self.send_signal(name, signal.SIGKILL) # pylint: disable=no-member
|
||||
|
||||
def remove(self, name: str, force: bool = False):
|
||||
"""Remove the specified named process from this manager.
|
||||
|
||||
If the specified process is still running, it will be forcefully killed
|
||||
if `force` is True`, otherwise an exception will be raised.
|
||||
|
||||
Raises:
|
||||
ProcessNotTerminatedError if the specified process is still
|
||||
running and was not forcefully killed.
|
||||
"""
|
||||
try:
|
||||
process_info = self[name]
|
||||
except KeyError:
|
||||
return
|
||||
if process_info.returncode is None and not force:
|
||||
raise ProcessNotTerminatedError(name)
|
||||
try:
|
||||
self.kill(name)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
del self[name]
|
||||
|
||||
def cleanup(self, force: bool = False):
|
||||
"""Remove stale (terminated) processes from this manager."""
|
||||
for name in self:
|
||||
try:
|
||||
self.remove(name, force)
|
||||
except ProcessNotTerminatedError:
|
||||
continue
|
||||
@@ -1,179 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import BinaryIO, List, Optional, Union
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
from .exceptions import TimeoutExpired
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: int
|
||||
stdin: Optional[str]
|
||||
stdout: Optional[str]
|
||||
stderr: Optional[str]
|
||||
returncode: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class ManagedProcess(AbstractContextManager):
|
||||
"""Run the specified command with redirected output.
|
||||
|
||||
stdout and stderr will both be redirected to <name>.out.
|
||||
Interactive processes (requiring stdin input) are currently unsupported.
|
||||
|
||||
Parameters:
|
||||
args: Command to be run.
|
||||
wdir: If specified, redirected output files will be placed in `wdir`.
|
||||
name: Name to use for this process, if not specified a UUID will be
|
||||
generated instead.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Union[str, List[str]],
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.args: List[str] = (
|
||||
shlex.split(args, posix=os.name == "posix")
|
||||
if isinstance(args, str)
|
||||
else list(args)
|
||||
)
|
||||
self.wdir = wdir
|
||||
self.name = name or uuid()
|
||||
self.returncode: Optional[int] = None
|
||||
self._stdout: Optional[BinaryIO] = None
|
||||
self._stderr: Optional[BinaryIO] = None
|
||||
|
||||
self._run()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.wait()
|
||||
|
||||
def _close_fds(self):
|
||||
if self._stdout:
|
||||
self._stdout.close()
|
||||
self._stdout = None
|
||||
if self._stderr:
|
||||
self._stderr.close()
|
||||
self._stderr = None
|
||||
|
||||
def _make_path(self, path: str) -> str:
|
||||
return os.path.join(self.wdir, path) if self.wdir else path
|
||||
|
||||
@cached_property
|
||||
def stdout_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.out")
|
||||
|
||||
@cached_property
|
||||
def info_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.json")
|
||||
|
||||
@cached_property
|
||||
def pidfile_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.pid")
|
||||
|
||||
@property
|
||||
def info(self) -> "ProcessInfo":
|
||||
return ProcessInfo(
|
||||
pid=self.pid,
|
||||
stdin=None,
|
||||
stdout=self.stdout_path,
|
||||
stderr=None,
|
||||
returncode=self.returncode,
|
||||
)
|
||||
|
||||
def _make_wdir(self):
|
||||
if self.wdir:
|
||||
os.makedirs(self.wdir, exist_ok=True)
|
||||
|
||||
def _dump(self):
|
||||
self._make_wdir()
|
||||
with open(self.info_path, "w", encoding="utf-8") as fobj:
|
||||
json.dump(self.info.asdict(), fobj)
|
||||
with open(self.pidfile_path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(str(self.pid))
|
||||
|
||||
def _run(self) -> None:
|
||||
self._make_wdir()
|
||||
logger.debug(
|
||||
"Appending output to '%s'",
|
||||
self.stdout_path,
|
||||
)
|
||||
self._stdout = open(self.stdout_path, "ab") # noqa: SIM115
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
self.args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=self._stdout,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
)
|
||||
self.pid: int = self._proc.pid
|
||||
self._dump()
|
||||
except Exception:
|
||||
if self._proc is not None:
|
||||
self._proc.kill()
|
||||
self._close_fds()
|
||||
raise
|
||||
|
||||
def wait(self, timeout: Optional[int] = None) -> Optional[int]:
|
||||
"""Block until a process started with `run` has completed.
|
||||
|
||||
Raises:
|
||||
TimeoutExpired if `timeout` was set and the process
|
||||
did not terminate after `timeout` seconds.
|
||||
"""
|
||||
if self.returncode is not None:
|
||||
return self.returncode
|
||||
try:
|
||||
self._proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise TimeoutExpired(exc.cmd, exc.timeout) from exc
|
||||
self.returncode = self._proc.returncode
|
||||
self._close_fds()
|
||||
self._dump()
|
||||
return self.returncode
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, *args, **kwargs) -> Optional[int]:
|
||||
"""Spawn a ManagedProcess command in the background.
|
||||
|
||||
Returns: The spawned process PID.
|
||||
"""
|
||||
import multiprocessing as mp
|
||||
|
||||
proc = mp.Process(
|
||||
target=cls._spawn,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
# Do not terminate the child daemon when the main process exits
|
||||
# pylint: disable=protected-access
|
||||
mp.process._children.discard(proc) # type: ignore[attr-defined]
|
||||
return proc.pid
|
||||
|
||||
@classmethod
|
||||
def _spawn(cls, *args, **kwargs):
|
||||
with cls(*args, **kwargs):
|
||||
pass
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Union
|
||||
|
||||
@@ -32,15 +31,17 @@ def to_json(renderer, split: bool = False) -> List[Dict]:
|
||||
if renderer.TYPE == "vega":
|
||||
grouped = _group_by_rev(renderer.datapoints)
|
||||
if split:
|
||||
content = renderer.get_filled_template(skip_anchors=["data"])
|
||||
content = renderer.get_filled_template(
|
||||
skip_anchors=["data"], as_string=False
|
||||
)
|
||||
else:
|
||||
content = renderer.get_filled_template()
|
||||
content = renderer.get_filled_template(as_string=False)
|
||||
if grouped:
|
||||
return [
|
||||
{
|
||||
TYPE_KEY: renderer.TYPE,
|
||||
REVISIONS_KEY: sorted(grouped.keys()),
|
||||
"content": json.loads(content),
|
||||
"content": content,
|
||||
"datapoints": grouped,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -56,7 +56,6 @@ def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
|
||||
datapoint = first(file2datapoints[filename])
|
||||
if field not in datapoint:
|
||||
raise FieldNotFoundError(field, datapoint.keys())
|
||||
return
|
||||
|
||||
|
||||
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
||||
@@ -245,7 +244,7 @@ class VegaConverter(Converter):
|
||||
for i, (y_file, y_field) in enumerate(ys):
|
||||
if num_xs > 1:
|
||||
x_file, x_field = xs[i]
|
||||
datapoints = [d.copy() for d in file2datapoints.get(y_file, [])]
|
||||
datapoints = [{**d} for d in file2datapoints.get(y_file, [])]
|
||||
|
||||
if props_update.get("y", None) == "dvc_inferred_y_value":
|
||||
_update_from_field(
|
||||
|
||||
+29
-6
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, DefaultDict, Dict, List, NamedTuple, Optional
|
||||
|
||||
import dpath
|
||||
import dpath.options
|
||||
from funcy import last
|
||||
from funcy import get_in, last
|
||||
|
||||
from dvc.repo.plots import _normpath, infer_data_sources
|
||||
from dvc.utils.plots import get_plot_id
|
||||
@@ -13,6 +13,8 @@ from .convert import _get_converter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
from dvc_render.base import Renderer
|
||||
|
||||
|
||||
dpath.options.ALLOW_EMPTY_STRING_KEYS = True
|
||||
|
||||
@@ -61,21 +63,31 @@ class PlotsData:
|
||||
return result
|
||||
|
||||
|
||||
def match_defs_renderers(
|
||||
class RendererWithErrors(NamedTuple):
|
||||
renderer: "Renderer"
|
||||
source_errors: Dict[str, Dict[str, Exception]]
|
||||
definition_errors: Dict[str, Exception]
|
||||
|
||||
|
||||
def match_defs_renderers( # noqa: C901, PLR0912
|
||||
data,
|
||||
out=None,
|
||||
templates_dir: Optional["StrPath"] = None,
|
||||
):
|
||||
) -> List[RendererWithErrors]:
|
||||
from dvc_render import ImageRenderer, VegaRenderer
|
||||
|
||||
plots_data = PlotsData(data)
|
||||
renderers = []
|
||||
renderer_cls = None
|
||||
|
||||
for plot_id, group in plots_data.group_definitions().items():
|
||||
plot_datapoints: List[Dict] = []
|
||||
props = _squash_plots_properties(group)
|
||||
final_props: Dict = {}
|
||||
|
||||
def_errors: Dict[str, Exception] = {}
|
||||
src_errors: DefaultDict[str, Dict[str, Exception]] = defaultdict(dict)
|
||||
|
||||
if out is not None:
|
||||
props["out"] = out
|
||||
if templates_dir is not None:
|
||||
@@ -94,13 +106,24 @@ def match_defs_renderers(
|
||||
|
||||
converter = _get_converter(renderer_cls, inner_id, props, definitions_data)
|
||||
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
for src in plot_sources:
|
||||
if error := get_in(data, [rev, "sources", "data", src, "error"]):
|
||||
src_errors[rev][src] = error
|
||||
|
||||
try:
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
except Exception as e: # noqa: BLE001, pylint: disable=broad-except
|
||||
def_errors[rev] = e
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
if renderer_cls is not None:
|
||||
renderers.append(renderer_cls(plot_datapoints, renderer_id, **final_props))
|
||||
renderer = renderer_cls(plot_datapoints, renderer_id, **final_props)
|
||||
renderers.append(RendererWithErrors(renderer, dict(src_errors), def_errors))
|
||||
return renderers
|
||||
|
||||
+50
-74
@@ -3,10 +3,17 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Tuple, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.exceptions import FileMissingError
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.utils import env2bool
|
||||
@@ -116,6 +123,8 @@ class Repo:
|
||||
if not scm:
|
||||
try:
|
||||
scm = SCM(root_dir or os.curdir)
|
||||
if scm.dulwich.repo.bare:
|
||||
raise NotDvcRepoError(f"{scm.root_dir} is a bare git repo")
|
||||
except SCMError:
|
||||
scm = SCM(os.curdir, no_scm=True)
|
||||
|
||||
@@ -125,38 +134,6 @@ class Repo:
|
||||
assert root_dir
|
||||
return root_dir, dvc_dir
|
||||
|
||||
def _get_database_dir(self, db_name: str) -> Optional[str]:
|
||||
from dvc.fs import localfs
|
||||
|
||||
# NOTE: by default, store SQLite-based remote indexes and state's
|
||||
# `links` and `md5s` caches in the repository itself to avoid any
|
||||
# possible state corruption in 'shared cache dir' scenario, but allow
|
||||
# user to override this through config when, say, the repository is
|
||||
# located on a mounted volume — see
|
||||
# https://github.com/iterative/dvc/issues/4420
|
||||
base_db_dir = self.config.get(db_name, {}).get("dir", None)
|
||||
if not base_db_dir:
|
||||
return self.tmp_dir
|
||||
|
||||
import hashlib
|
||||
|
||||
if self.local_dvc_dir:
|
||||
fs: "FileSystem" = localfs
|
||||
local_root = fs.path.parent(self.local_dvc_dir)
|
||||
else:
|
||||
fs = self.fs
|
||||
local_root = self.root_dir
|
||||
root_dir_hash = hashlib.sha224(local_root.encode("utf-8")).hexdigest()
|
||||
|
||||
db_dir = fs.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{fs.path.name(local_root)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
fs.makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__( # noqa: PLR0915
|
||||
self,
|
||||
root_dir: Optional[str] = None,
|
||||
@@ -232,8 +209,8 @@ class Repo:
|
||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||
friendly=True,
|
||||
)
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
os.makedirs(self.site_cache_dir, exist_ok=True)
|
||||
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
||||
else:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
@@ -388,16 +365,12 @@ class Repo:
|
||||
self._reset()
|
||||
|
||||
@property
|
||||
def data_index(self) -> Optional["DataIndex"]:
|
||||
def data_index(self) -> "DataIndex":
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
if not self.index_db_dir:
|
||||
return None
|
||||
|
||||
if self._data_index is None:
|
||||
index_dir = os.path.join(self.index_db_dir, "index", "data")
|
||||
index_dir = os.path.join(self.site_cache_dir, "index", "data")
|
||||
os.makedirs(index_dir, exist_ok=True)
|
||||
|
||||
self._data_index = DataIndex.open(os.path.join(index_dir, "db.db"))
|
||||
|
||||
return self._data_index
|
||||
@@ -435,9 +408,12 @@ class Repo:
|
||||
raise NotDvcRepoError(msg)
|
||||
|
||||
@classmethod
|
||||
def find_dvc_dir(cls, root=None) -> str:
|
||||
root_dir = cls.find_root(root)
|
||||
return os.path.join(root_dir, cls.DVC_DIR)
|
||||
def find_dvc_dir(cls, root=None, fs=None) -> str:
|
||||
from dvc.fs import localfs
|
||||
|
||||
fs = fs or localfs
|
||||
root_dir = cls.find_root(root, fs=fs)
|
||||
return fs.path.join(root_dir, cls.DVC_DIR)
|
||||
|
||||
@staticmethod
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False) -> "Repo":
|
||||
@@ -463,6 +439,11 @@ class Repo:
|
||||
|
||||
return brancher(self, *args, **kwargs)
|
||||
|
||||
def switch(self, rev: str) -> ContextManager[str]:
|
||||
from dvc.repo.brancher import switch
|
||||
|
||||
return switch(self, rev)
|
||||
|
||||
def used_objs( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
@@ -576,40 +557,35 @@ class Repo:
|
||||
return DVCFileSystem(repo=self, subrepos=self.subrepos, **self._fs_conf)
|
||||
|
||||
@cached_property
|
||||
def index_db_dir(self):
|
||||
return self._get_database_dir("index")
|
||||
def site_cache_dir(self) -> str:
|
||||
import hashlib
|
||||
|
||||
@contextmanager
|
||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
import platformdirs
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=self.index.data["local"])
|
||||
fs_path = path
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
cache_dir = platformdirs.site_cache_dir("dvc", "iterative", opinion=True)
|
||||
|
||||
if isinstance(self.fs, GitFileSystem):
|
||||
relparts = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
root_dir = os.path.join(self.scm.root_dir, *relparts)
|
||||
else:
|
||||
fs = DVCFileSystem(repo=self, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
root_dir = self.root_dir
|
||||
|
||||
repos_dir = os.path.join(cache_dir, "repo")
|
||||
|
||||
umask = os.umask(0)
|
||||
try:
|
||||
if remote:
|
||||
remote_odb = self.cloud.get_remote_odb(name=remote)
|
||||
oid = fs.info(fs_path)["dvc_info"]["md5"]
|
||||
fs = remote_odb.fs
|
||||
fs_path = remote_odb.oid_to_path(oid)
|
||||
os.makedirs(repos_dir, mode=0o777, exist_ok=True)
|
||||
finally:
|
||||
os.umask(umask)
|
||||
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
raise FileMissingError(path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
repo_token = hashlib.md5(os.fsencode(root_dir)).hexdigest() # noqa: S324
|
||||
|
||||
return os.path.join(repos_dir, repo_token)
|
||||
|
||||
def close(self):
|
||||
self.scm.close()
|
||||
|
||||
+68
-16
@@ -1,8 +1,15 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional, Tuple
|
||||
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -83,23 +90,68 @@ def brancher( # noqa: E302
|
||||
)
|
||||
|
||||
try:
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"switching fs to revision %s [%s]", sha[:7], ", ".join(names)
|
||||
)
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
if cwd_parts:
|
||||
cwd = self.fs.path.join("/", *cwd_parts) # type: ignore[unreachable]
|
||||
self.fs.path.chdir(cwd)
|
||||
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
if self.fs.exists(self.root_dir):
|
||||
try:
|
||||
_switch_fs(self, sha, repo_root_parts, cwd_parts)
|
||||
yield sha if sha_only else ",".join(names)
|
||||
except NotDvcRepoError:
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
pass
|
||||
finally:
|
||||
self.fs = saved_fs
|
||||
self.root_dir = saved_root
|
||||
|
||||
|
||||
def _switch_fs(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
repo_root_parts: Tuple[str, ...],
|
||||
cwd_parts: Tuple[str, ...],
|
||||
):
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if rev == "workspace":
|
||||
logger.trace("switching fs to workspace") # type: ignore[attr-defined]
|
||||
repo.fs = LocalFileSystem(url=repo.root_dir)
|
||||
return
|
||||
|
||||
logger.trace("switching fs to revision %s", rev[:7]) # type: ignore[attr-defined]
|
||||
assert isinstance(repo.scm, Git)
|
||||
fs = GitFileSystem(scm=repo.scm, rev=rev)
|
||||
root_dir = repo.fs.path.join("/", *repo_root_parts)
|
||||
if not fs.exists(root_dir):
|
||||
raise NotDvcRepoError(f"Commit '{rev[:7]}' does not contain a DVC repo")
|
||||
|
||||
repo.fs = fs
|
||||
repo.root_dir = root_dir
|
||||
|
||||
if cwd_parts:
|
||||
cwd = repo.fs.path.join("/", *cwd_parts)
|
||||
repo.fs.path.chdir(cwd)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def switch(repo: "Repo", rev: str) -> Iterator[str]:
|
||||
"""Switch to a specific revision."""
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if rev != "workspace":
|
||||
rev = resolve_rev(repo.scm, rev)
|
||||
|
||||
repo_root_parts = ()
|
||||
if repo.fs.path.isin(repo.root_dir, repo.scm.root_dir):
|
||||
repo_root_parts = repo.fs.path.relparts(repo.root_dir, repo.scm.root_dir)
|
||||
|
||||
cwd_parts = ()
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.scm.root_dir):
|
||||
cwd_parts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.scm.root_dir)
|
||||
|
||||
saved_fs = repo.fs
|
||||
saved_root = repo.root_dir
|
||||
try:
|
||||
_switch_fs(repo, rev, repo_root_parts, cwd_parts)
|
||||
yield rev
|
||||
finally:
|
||||
repo.fs = saved_fs
|
||||
repo.root_dir = saved_root
|
||||
|
||||
+1
-4
@@ -182,10 +182,7 @@ def _diff_head_to_index(
|
||||
) -> Dict[str, List[str]]:
|
||||
index = repo.index.data["repo"]
|
||||
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
|
||||
with ui.status("Calculating diff between head/index"):
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.repo import locked
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -154,7 +154,7 @@ def diff(
|
||||
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in old_missing & new_missing:
|
||||
raise PathMissingError(target, self)
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
|
||||
|
||||
old = indexes[a_rev]
|
||||
new = indexes[b_rev]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional
|
||||
|
||||
from funcy import chain, first
|
||||
@@ -18,10 +17,6 @@ from .exceptions import (
|
||||
InvalidExpRefError,
|
||||
MultipleBranchError,
|
||||
)
|
||||
from .executor.base import BaseExecutor
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .refs import (
|
||||
APPLY_STASH,
|
||||
CELERY_FAILED_STASH,
|
||||
@@ -38,6 +33,9 @@ from .utils import check_ref_format, exp_refs_by_rev, unlocked_repo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .queue.base import BaseStashQueue, QueueEntry
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,6 +63,11 @@ class Experiments:
|
||||
|
||||
@property
|
||||
def scm(self):
|
||||
from dvc.scm import SCMError
|
||||
|
||||
if self.repo.scm.no_commits:
|
||||
raise SCMError("Empty Git repo. Add a commit to use experiments.")
|
||||
|
||||
return self.repo.scm
|
||||
|
||||
@cached_property
|
||||
@@ -73,20 +76,28 @@ class Experiments:
|
||||
|
||||
@cached_property
|
||||
def args_file(self) -> str:
|
||||
from .executor.base import BaseExecutor
|
||||
|
||||
return os.path.join(self.repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
|
||||
@cached_property
|
||||
def workspace_queue(self) -> WorkspaceQueue:
|
||||
def workspace_queue(self) -> "WorkspaceQueue":
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
|
||||
return WorkspaceQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
@cached_property
|
||||
def tempdir_queue(self) -> TempDirQueue:
|
||||
def tempdir_queue(self) -> "TempDirQueue":
|
||||
from .queue.tempdir import TempDirQueue
|
||||
|
||||
# NOTE: tempdir and workspace stash is shared since both
|
||||
# implementations immediately push -> pop (queue length is only 0 or 1)
|
||||
return TempDirQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
@cached_property
|
||||
def celery_queue(self) -> LocalCeleryQueue:
|
||||
def celery_queue(self) -> "LocalCeleryQueue":
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
|
||||
return LocalCeleryQueue(self.repo, CELERY_STASH, CELERY_FAILED_STASH)
|
||||
|
||||
@cached_property
|
||||
@@ -193,8 +204,7 @@ class Experiments:
|
||||
)
|
||||
for entry in entries:
|
||||
# wait for task execution to start
|
||||
while not self.celery_queue.proc.get(entry.stash_rev):
|
||||
time.sleep(1)
|
||||
self.celery_queue.wait_for_start(entry, sleep_interval=1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
try:
|
||||
|
||||
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def branch(repo, exp_rev, branch_name, *args, **kwargs):
|
||||
def branch(repo, exp_rev, branch_name=None, **kwargs):
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
try:
|
||||
@@ -46,6 +46,8 @@ def branch(repo, exp_rev, branch_name, *args, **kwargs):
|
||||
if not ref_info:
|
||||
raise InvalidExpRevError(exp_rev)
|
||||
|
||||
branch_name = branch_name or f"{ref_info.name}-branch"
|
||||
|
||||
branch_ref = f"refs/heads/{branch_name}"
|
||||
if repo.scm.get_ref(branch_ref):
|
||||
raise InvalidArgumentError(f"Git branch '{branch_name}' already exists.")
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import get_in
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
@@ -255,6 +256,60 @@ class BaseExecutor(ABC):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def save(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
) -> ExecutorResult:
|
||||
from dvc.repo import Repo
|
||||
|
||||
exp_hash: Optional[str] = None
|
||||
exp_ref: Optional[ExpRefInfo] = None
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
old_cwd = os.getcwd()
|
||||
if info.wdir:
|
||||
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
|
||||
else:
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
try:
|
||||
stages = dvc.commit([], force=True, relink=False)
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if include_untracked:
|
||||
dvc.scm.add(include_untracked)
|
||||
cls.commit(
|
||||
dvc.scm, # type: ignore[arg-type]
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=force,
|
||||
)
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref = ExpRefInfo.from_ref(ref) if ref else None
|
||||
untracked = dvc.scm.untracked_files()
|
||||
if untracked:
|
||||
logger.warning(
|
||||
"The following untracked files were present in "
|
||||
"the workspace before saving but "
|
||||
"will not be included in the experiment commit:\n"
|
||||
"\t%s",
|
||||
", ".join(untracked),
|
||||
)
|
||||
info.result_hash = exp_hash
|
||||
info.result_ref = ref
|
||||
info.result_force = False
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except DvcException:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
return ExecutorResult(ref, exp_ref, info.result_force)
|
||||
|
||||
@staticmethod
|
||||
def hash_exp(stages: Iterable["PipelineStage"]) -> str:
|
||||
from dvc.stage import PipelineStage
|
||||
@@ -265,7 +320,7 @@ class BaseExecutor(ABC):
|
||||
exp_data.update(to_lockfile(stage))
|
||||
return dict_sha256(exp_data)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
if infofile is not None:
|
||||
info = ExecutorInfo.load_json(infofile)
|
||||
if info.status < TaskStatus.FAILED:
|
||||
@@ -590,6 +645,7 @@ class BaseExecutor(ABC):
|
||||
info.name,
|
||||
"dvc",
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
metrics=get_in(dvc.metrics.show(), ["", "data"]),
|
||||
)
|
||||
|
||||
if infofile is not None:
|
||||
|
||||
@@ -4,6 +4,7 @@ from contextlib import ExitStack
|
||||
from tempfile import mkdtemp
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from configobj import ConfigObj
|
||||
from funcy import retry
|
||||
from shortuuid import uuid
|
||||
|
||||
@@ -48,7 +49,7 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
def scm(self) -> Union["Git", "NoSCM"]:
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
super().cleanup(infofile)
|
||||
@@ -134,24 +135,31 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
self.scm.stash.apply(merge_rev)
|
||||
self._update_config(repo.config.read("local"))
|
||||
|
||||
def _config(self, cache_dir):
|
||||
def _update_config(self, update):
|
||||
local_config = os.path.join(
|
||||
self.root_dir,
|
||||
self.dvc_dir,
|
||||
"config.local",
|
||||
)
|
||||
logger.debug("Writing experiments local config '%s'", local_config)
|
||||
with open(local_config, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(f"[cache]\n dir = {cache_dir}")
|
||||
if os.path.exists(local_config):
|
||||
conf_obj = ConfigObj(local_config)
|
||||
conf_obj.merge(update)
|
||||
else:
|
||||
conf_obj = ConfigObj(update)
|
||||
if conf_obj:
|
||||
with open(local_config, "wb") as fobj:
|
||||
conf_obj.write(fobj)
|
||||
|
||||
def init_cache(
|
||||
self, repo: "Repo", rev: str, run_cache: bool = True # noqa: ARG002
|
||||
):
|
||||
"""Initialize DVC cache."""
|
||||
self._config(repo.cache.repo.path)
|
||||
self._update_config({"cache": {"dir": repo.cache.repo.path}})
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
super().cleanup(infofile)
|
||||
logger.debug("Removing tmpdir '%s'", self.root_dir)
|
||||
remove(self.root_dir)
|
||||
@@ -234,9 +242,10 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
|
||||
pass
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
super().cleanup(infofile)
|
||||
remove(os.path.dirname(infofile))
|
||||
if infofile:
|
||||
remove(os.path.dirname(infofile))
|
||||
with self._detach_stack:
|
||||
self.scm.remove_ref(EXEC_BASELINE)
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
|
||||
@@ -1,35 +1,82 @@
|
||||
import logging
|
||||
from typing import Iterable, List, Mapping, Optional, Set, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import group_by
|
||||
from funcy import compact, group_by
|
||||
from scmrepo.git.backend.base import SyncStatus
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import TqdmGit, iter_revs
|
||||
from dvc.scm import Git, TqdmGit, iter_revs
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import env2bool
|
||||
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import exp_commits, exp_refs, exp_refs_by_baseline, resolve_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def notify_refs_to_studio(
|
||||
repo: "Repo", git_remote: str, **refs: List[str]
|
||||
) -> Optional[str]:
|
||||
config = repo.config["feature"]
|
||||
refs = compact(refs)
|
||||
if not refs or env2bool("DVC_TEST"):
|
||||
return None
|
||||
|
||||
if not (config.get("studio_token") or config["push_exp_to_studio"]):
|
||||
logger.debug(
|
||||
"Either feature.studio_token or feature.push_exp_to_studio config "
|
||||
"needs to be set."
|
||||
)
|
||||
return None
|
||||
|
||||
import os
|
||||
|
||||
token = os.environ.get("STUDIO_TOKEN") or config.get("studio_token")
|
||||
if not token:
|
||||
logger.debug("Studio token not found.")
|
||||
return None
|
||||
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
from dvc.utils import studio
|
||||
|
||||
_, repo_url = get_remote_repo(repo.scm.dulwich.repo, git_remote)
|
||||
studio_url = config.get("studio_url")
|
||||
d = studio.notify_refs(repo_url, token, studio_url=studio_url, **refs)
|
||||
return d.get("url")
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def push( # noqa: C901
|
||||
repo,
|
||||
repo: "Repo",
|
||||
git_remote: str,
|
||||
exp_names: Union[Iterable[str], str],
|
||||
all_commits=False,
|
||||
all_commits: bool = False,
|
||||
rev: Optional[str] = None,
|
||||
num=1,
|
||||
num: int = 1,
|
||||
force: bool = False,
|
||||
push_cache: bool = False,
|
||||
**kwargs,
|
||||
) -> Iterable[str]:
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
exp_ref_set: Set["ExpRefInfo"] = set()
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
|
||||
@@ -69,11 +116,15 @@ def push( # noqa: C901
|
||||
push_result[SyncStatus.UP_TO_DATE] + push_result[SyncStatus.SUCCESS]
|
||||
)
|
||||
_push_cache(repo, push_cache_ref, **kwargs)
|
||||
return [ref.name for ref in push_result[SyncStatus.SUCCESS]]
|
||||
|
||||
refs = push_result[SyncStatus.SUCCESS]
|
||||
pushed_refs = [str(r) for r in refs]
|
||||
url = notify_refs_to_studio(repo, git_remote, pushed=pushed_refs)
|
||||
return {"pushed": [ref.name for ref in refs], "url": url}
|
||||
|
||||
|
||||
def _push(
|
||||
repo,
|
||||
repo: "Repo",
|
||||
git_remote: str,
|
||||
refs: Iterable["ExpRefInfo"],
|
||||
force: bool,
|
||||
@@ -83,7 +134,7 @@ def _push(
|
||||
from dvc.scm import GitAuthError
|
||||
|
||||
refspec_list = [f"{exp_ref}:{exp_ref}" for exp_ref in refs]
|
||||
logger.debug("git push experiment '%s' -> '%s'", refspec_list, git_remote)
|
||||
logger.debug("git push experiment %s -> '%s'", refspec_list, git_remote)
|
||||
|
||||
with TqdmGit(desc="Pushing git refs") as pbar:
|
||||
try:
|
||||
@@ -105,14 +156,15 @@ def _push(
|
||||
|
||||
|
||||
def _push_cache(
|
||||
repo,
|
||||
repo: "Repo",
|
||||
refs: Union[ExpRefInfo, Iterable["ExpRefInfo"]],
|
||||
dvc_remote=None,
|
||||
jobs=None,
|
||||
run_cache=False,
|
||||
dvc_remote: Optional[str] = None,
|
||||
jobs: Optional[int] = None,
|
||||
run_cache: bool = False,
|
||||
):
|
||||
if isinstance(refs, ExpRefInfo):
|
||||
refs = [refs]
|
||||
assert isinstance(repo.scm, Git)
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug("dvc push experiment '%s'", refs)
|
||||
repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
|
||||
@@ -17,6 +17,8 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc_studio_client.env import STUDIO_REPO_URL
|
||||
from dvc_studio_client.post_live_metrics import get_studio_repo_url
|
||||
from funcy import retry
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
@@ -415,6 +417,10 @@ class BaseStashQueue(ABC):
|
||||
run_env[DVC_EXP_NAME] = name
|
||||
if resume_rev:
|
||||
run_env[DVCLIVE_RESUME] = "1"
|
||||
studio_repo_url = get_studio_repo_url()
|
||||
if studio_repo_url is not None:
|
||||
run_env[STUDIO_REPO_URL] = studio_repo_url
|
||||
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save experiment as a stash commit
|
||||
|
||||
@@ -295,6 +295,28 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
# out of the active task list, and needs to be loaded here.
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
def wait(self, revs: Collection[str], **kwargs) -> None:
|
||||
"""Block until the specified tasks have completed."""
|
||||
revs = [revs] if isinstance(revs, str) else revs
|
||||
results = self.match_queue_entry_by_name(
|
||||
revs, self.iter_queued(), self.iter_done(), self.iter_failed()
|
||||
)
|
||||
for entry in results.values():
|
||||
if not entry:
|
||||
continue
|
||||
self.wait_for_start(entry, **kwargs)
|
||||
try:
|
||||
self.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def wait_for_start(self, entry: QueueEntry, sleep_interval: float = 0.001) -> None:
|
||||
"""Block until the specified task has been started."""
|
||||
import time
|
||||
|
||||
while not self.proc.get(entry.stash_rev):
|
||||
time.sleep(sleep_interval)
|
||||
|
||||
def _get_running_task_ids(self) -> Set[str]:
|
||||
running_task_ids: Set[str] = set()
|
||||
active_workers = self.worker_status()
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def remove( # noqa: C901
|
||||
def remove( # noqa: C901, PLR0912
|
||||
repo: "Repo",
|
||||
exp_names: Union[None, str, List[str]] = None,
|
||||
rev: Optional[str] = None,
|
||||
@@ -39,13 +39,6 @@ def remove( # noqa: C901
|
||||
removed.extend(celery_queue.clear(queued=True))
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
removed.extend(
|
||||
_remove_commited_exps(
|
||||
repo.scm, list(exp_refs(repo.scm, git_remote)), git_remote
|
||||
)
|
||||
)
|
||||
return removed
|
||||
|
||||
exp_ref_list: List["ExpRefInfo"] = []
|
||||
queue_entry_list: List["QueueEntry"] = []
|
||||
@@ -65,11 +58,14 @@ def remove( # noqa: C901
|
||||
queue_entry_list.append(result.queue_entry)
|
||||
|
||||
if remained:
|
||||
raise UnresolvedExpNamesError(remained)
|
||||
raise UnresolvedExpNamesError(remained, git_remote=git_remote)
|
||||
elif rev:
|
||||
exp_ref_dict = _resolve_exp_by_baseline(repo, rev, num, git_remote)
|
||||
removed.extend(exp_ref_dict.keys())
|
||||
exp_ref_list.extend(exp_ref_dict.values())
|
||||
elif all_commits:
|
||||
exp_ref_list.extend(exp_refs(repo.scm, git_remote))
|
||||
removed = [ref.name for ref in exp_ref_list]
|
||||
|
||||
if exp_ref_list:
|
||||
_remove_commited_exps(repo.scm, exp_ref_list, git_remote)
|
||||
@@ -79,15 +75,22 @@ def remove( # noqa: C901
|
||||
|
||||
remove_tasks(celery_queue, queue_entry_list)
|
||||
|
||||
if git_remote:
|
||||
from .push import notify_refs_to_studio
|
||||
|
||||
removed_refs = [str(r) for r in exp_ref_list]
|
||||
notify_refs_to_studio(repo, git_remote, removed=removed_refs)
|
||||
return removed
|
||||
|
||||
|
||||
def _resolve_exp_by_baseline(
|
||||
repo,
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
num: int,
|
||||
git_remote: Optional[str] = None,
|
||||
) -> Dict[str, "ExpRefInfo"]:
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
commit_ref_dict: Dict[str, "ExpRefInfo"] = {}
|
||||
rev_dict = iter_revs(repo.scm, [rev], num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
|
||||
@@ -2,13 +2,7 @@ import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from pathspec import PathSpec
|
||||
|
||||
from dvc.scm import Git
|
||||
|
||||
from .exceptions import ExperimentExistsError
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import check_ref_format, get_random_exp_name
|
||||
from funcy import first
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
@@ -17,34 +11,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _save_experiment(
|
||||
repo: "Repo",
|
||||
baseline_rev: str,
|
||||
force: bool,
|
||||
name: Optional[str],
|
||||
include_untracked: Optional[List[str]],
|
||||
) -> str:
|
||||
repo.commit([], force=True, relink=False)
|
||||
|
||||
name = name or get_random_exp_name(repo.scm, baseline_rev)
|
||||
ref_info = ExpRefInfo(baseline_rev, name)
|
||||
check_ref_format(repo.scm.dulwich, ref_info)
|
||||
ref = str(ref_info)
|
||||
if repo.scm.get_ref(ref) and not force:
|
||||
raise ExperimentExistsError(ref_info.name, command="save")
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
repo.scm.add([], update=True)
|
||||
if include_untracked:
|
||||
repo.scm.add(include_untracked)
|
||||
repo.scm.commit(f"dvc: commit experiment {name}", no_verify=True)
|
||||
exp_rev = repo.scm.get_rev()
|
||||
repo.scm.set_ref(ref, exp_rev, old_ref=None)
|
||||
|
||||
return exp_rev
|
||||
|
||||
|
||||
def save(
|
||||
repo: "Repo",
|
||||
name: Optional[str] = None,
|
||||
@@ -57,33 +23,16 @@ def save(
|
||||
"""
|
||||
logger.debug("Saving workspace in %s", os.getcwd())
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
queue = repo.experiments.workspace_queue
|
||||
entry = repo.experiments.new(queue=queue, name=name, force=force)
|
||||
executor = queue.init_executor(repo.experiments, entry)
|
||||
|
||||
_, _, untracked = repo.scm.status()
|
||||
if include_untracked:
|
||||
spec = PathSpec.from_lines("gitwildmatch", include_untracked)
|
||||
untracked = [file for file in untracked if not spec.match_file(file)]
|
||||
if untracked:
|
||||
logger.warning(
|
||||
(
|
||||
"The following untracked files were present in "
|
||||
"the workspace before saving but "
|
||||
"will not be included in the experiment commit:\n"
|
||||
"\t%s"
|
||||
),
|
||||
", ".join(untracked),
|
||||
try:
|
||||
save_result = executor.save(
|
||||
executor.info, force=force, include_untracked=include_untracked
|
||||
)
|
||||
result = queue.collect_executor(repo.experiments, executor, save_result)
|
||||
finally:
|
||||
executor.cleanup()
|
||||
|
||||
with repo.scm.detach_head(client="dvc") as orig_head:
|
||||
with repo.scm.stash_workspace() as workspace:
|
||||
try:
|
||||
if workspace is not None:
|
||||
repo.scm.stash.apply(workspace)
|
||||
|
||||
exp_rev = _save_experiment(
|
||||
repo, orig_head, force, name, include_untracked
|
||||
)
|
||||
finally:
|
||||
repo.scm.reset(hard=True)
|
||||
|
||||
return exp_rev
|
||||
return first(result)
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import (
|
||||
from scmrepo.exceptions import SCMError as InnerScmError
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import Git, RevError, SCMError, iter_revs, resolve_rev
|
||||
from dvc.scm import Git, SCMError, iter_revs, resolve_rev
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
from .serialize import SerializableError, SerializableExp
|
||||
@@ -81,7 +81,10 @@ def collect_experiment_commit(
|
||||
if exp_rev != "workspace" and not (force or param_deps):
|
||||
cached_exp = cache.get(exp_rev)
|
||||
if cached_exp:
|
||||
if status == ExpStatus.Running:
|
||||
if status == ExpStatus.Running or (
|
||||
isinstance(cached_exp, SerializableExp)
|
||||
and cached_exp.status == ExpStatus.Running.name
|
||||
):
|
||||
# expire cached queued exp entry once we start running it
|
||||
cache.delete(exp_rev)
|
||||
elif isinstance(cached_exp, SerializableError):
|
||||
@@ -97,7 +100,12 @@ def collect_experiment_commit(
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
if exp_rev != "workspace" and not param_deps and not exp.contains_error:
|
||||
if not (
|
||||
exp_rev == "workspace"
|
||||
or status == ExpStatus.Running
|
||||
or param_deps
|
||||
or exp.contains_error
|
||||
):
|
||||
cache.put(exp, force=True)
|
||||
return _format_exp(exp)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
@@ -114,10 +122,8 @@ def _collect_from_repo(
|
||||
**kwargs,
|
||||
) -> "SerializableExp":
|
||||
running = running or {}
|
||||
for rev in repo.brancher(revs=[exp_rev]):
|
||||
with repo.switch(exp_rev) as rev:
|
||||
if rev == "workspace":
|
||||
if exp_rev != "workspace":
|
||||
continue
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
@@ -143,7 +149,6 @@ def _collect_from_repo(
|
||||
executor=executor,
|
||||
error=error,
|
||||
)
|
||||
raise RevError(f"nonexistent exp rev: '{exp_rev}'")
|
||||
|
||||
|
||||
def _collect_complete_experiment(
|
||||
|
||||
@@ -35,6 +35,7 @@ from .refs import (
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
|
||||
EXEC_TMP_DIR = "exps"
|
||||
@@ -108,7 +109,9 @@ def exp_refs_by_rev(scm: "Git", rev: str) -> Generator[ExpRefInfo, None, None]:
|
||||
|
||||
|
||||
def exp_refs_by_baseline(
|
||||
scm: "Git", revs: Optional[Set[str]] = None, url: Optional[str] = None
|
||||
scm: "Git",
|
||||
revs: Optional[Set[str]] = None,
|
||||
url: Optional[str] = None,
|
||||
) -> Mapping[str, List[ExpRefInfo]]:
|
||||
"""Iterate over all experiment refs with the specified baseline."""
|
||||
all_exp_refs = exp_refs(scm, url)
|
||||
@@ -212,8 +215,6 @@ def exp_commits(
|
||||
refs = ref_infos if ref_infos else exp_refs(scm)
|
||||
for ref_info in refs:
|
||||
shas.update(scm.branch_revs(str(ref_info), ref_info.baseline_sha))
|
||||
if ref_info.baseline_sha:
|
||||
shas.add(ref_info.baseline_sha)
|
||||
yield from shas
|
||||
|
||||
|
||||
@@ -233,7 +234,7 @@ def remove_exp_refs(scm: "Git", ref_infos: Iterable[ExpRefInfo]):
|
||||
scm.remove_ref(str(ref_info))
|
||||
|
||||
|
||||
def fix_exp_head(scm: "Git", ref: Optional[str]) -> Optional[str]:
|
||||
def fix_exp_head(scm: Union["Git", "NoSCM"], ref: Optional[str]) -> Optional[str]:
|
||||
if ref:
|
||||
name, tail = Git.split_ref_pattern(ref)
|
||||
if name == "HEAD" and scm.get_ref(EXEC_BASELINE):
|
||||
@@ -285,7 +286,7 @@ def check_ref_format(scm: "Git", ref: ExpRefInfo):
|
||||
)
|
||||
|
||||
|
||||
def fetch_all_exps(scm: "Git", url: str, progress: Optional[Callable] = None):
|
||||
def fetch_all_exps(scm: "Git", url: str, progress: Optional[Callable] = None, **kwargs):
|
||||
refspecs = [
|
||||
f"{ref}:{ref}"
|
||||
for ref in iter_remote_refs(scm, url, base=EXPS_NAMESPACE)
|
||||
@@ -295,6 +296,7 @@ def fetch_all_exps(scm: "Git", url: str, progress: Optional[Callable] = None):
|
||||
url,
|
||||
refspecs,
|
||||
progress=progress,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -337,7 +339,7 @@ def to_studio_params(dvc_params):
|
||||
if not dvc_params:
|
||||
return result
|
||||
for rev_data in dvc_params.values():
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
for file_name, file_data in rev_data.get("data", {}).items():
|
||||
result[file_name] = file_data["data"]
|
||||
|
||||
return result
|
||||
|
||||
+6
-1
@@ -52,7 +52,12 @@ def get(url, path, out=None, rev=None, jobs=None):
|
||||
cache_types = ["reflink", "hardlink", "copy"]
|
||||
try:
|
||||
with external_repo(
|
||||
url=url, rev=rev, cache_dir=tmp_dir, cache_types=cache_types
|
||||
url=url,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
cache_dir=tmp_dir,
|
||||
cache_types=cache_types,
|
||||
) as repo:
|
||||
from dvc.fs.data import DataFileSystem
|
||||
|
||||
|
||||
+3
-3
@@ -110,10 +110,10 @@ def build_graph(stages, outs_trie=None):
|
||||
# Building graph
|
||||
graph.add_nodes_from(stages)
|
||||
for stage in stages:
|
||||
for dep in stage.deps:
|
||||
if dep.fs_path is None:
|
||||
continue
|
||||
if stage.is_repo_import:
|
||||
continue
|
||||
|
||||
for dep in stage.deps:
|
||||
dep_key = dep.fs.path.parts(dep.fs_path)
|
||||
overlapping = [n.value for n in outs_trie.prefixes(dep_key)]
|
||||
if outs_trie.has_subtrie(dep_key):
|
||||
|
||||
+16
-24
@@ -161,7 +161,7 @@ def _load_storage_from_out(storage_map, key, out):
|
||||
except NoRemoteError:
|
||||
pass
|
||||
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
if out.stage.is_import:
|
||||
dep = out.stage.deps[0]
|
||||
storage_map.add_data(FileStorage(key, dep.fs, dep.fs_path))
|
||||
|
||||
@@ -354,42 +354,34 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "Dict[str, DataIndex]":
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
prefix: "DataIndexKey"
|
||||
loaded = False
|
||||
|
||||
index = self.repo.data_index
|
||||
if index is None:
|
||||
index = DataIndex()
|
||||
|
||||
prefix = ("tree", self.data_tree.hash_info.value)
|
||||
if index.has_node(prefix):
|
||||
loaded = True
|
||||
|
||||
try:
|
||||
if not loaded:
|
||||
_load_data_from_outs(index, prefix, self.outs)
|
||||
index.commit()
|
||||
if not loaded:
|
||||
_load_data_from_outs(index, prefix, self.outs)
|
||||
index.commit()
|
||||
|
||||
by_workspace = {}
|
||||
by_workspace["repo"] = index.view((*prefix, "repo"))
|
||||
by_workspace["local"] = index.view((*prefix, "local"))
|
||||
by_workspace = {}
|
||||
by_workspace["repo"] = index.view((*prefix, "repo"))
|
||||
by_workspace["local"] = index.view((*prefix, "local"))
|
||||
|
||||
for out in self.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
for out in self.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
ws, key = out.index_key
|
||||
if ws not in by_workspace:
|
||||
by_workspace[ws] = index.view((*prefix, ws))
|
||||
ws, key = out.index_key
|
||||
if ws not in by_workspace:
|
||||
by_workspace[ws] = index.view((*prefix, ws))
|
||||
|
||||
data_index = by_workspace[ws]
|
||||
_load_storage_from_out(data_index.storage_map, key, out)
|
||||
data_index = by_workspace[ws]
|
||||
_load_storage_from_out(data_index.storage_map, key, out)
|
||||
|
||||
return by_workspace
|
||||
finally:
|
||||
index.close()
|
||||
return by_workspace
|
||||
|
||||
@staticmethod
|
||||
def _hash_targets(
|
||||
|
||||
+6
-1
@@ -12,7 +12,7 @@ from dvc.utils.fs import remove
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False):
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False): # noqa: C901
|
||||
"""
|
||||
Creates an empty repo on the given directory -- basically a
|
||||
`.dvc` directory with subdirectories for configuration and cache.
|
||||
@@ -75,6 +75,11 @@ def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False):
|
||||
|
||||
proj = Repo(root_dir)
|
||||
|
||||
if os.path.isdir(proj.site_cache_dir):
|
||||
proj.close()
|
||||
remove(proj.site_cache_dir)
|
||||
proj = Repo(root_dir)
|
||||
|
||||
with proj.scm_context(autostage=True) as context:
|
||||
files = [
|
||||
config.files["repo"],
|
||||
|
||||
@@ -20,16 +20,19 @@ def pre_commit_install(scm: "Git") -> None:
|
||||
"hooks": [
|
||||
{
|
||||
"id": "dvc-pre-commit",
|
||||
"additional_dependencies": [".[all]"],
|
||||
"language_version": "python3",
|
||||
"stages": ["commit"],
|
||||
},
|
||||
{
|
||||
"id": "dvc-pre-push",
|
||||
"additional_dependencies": [".[all]"],
|
||||
"language_version": "python3",
|
||||
"stages": ["push"],
|
||||
},
|
||||
{
|
||||
"id": "dvc-post-checkout",
|
||||
"additional_dependencies": [".[all]"],
|
||||
"language_version": "python3",
|
||||
"stages": ["post-checkout"],
|
||||
"always_run": True,
|
||||
|
||||
+1
-6
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
@@ -61,10 +59,7 @@ def _ls(
|
||||
fs: "DVCFileSystem" = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
fs_path = fs.info(fs_path)["name"]
|
||||
except FileNotFoundError:
|
||||
raise PathMissingError(path, repo, dvc_only=dvc_only) # noqa: B904
|
||||
fs_path = fs.info(fs_path)["name"]
|
||||
|
||||
infos = {}
|
||||
for root, dirs, files in fs.walk(
|
||||
|
||||
+54
-33
@@ -20,13 +20,14 @@ from typing import (
|
||||
|
||||
import dpath
|
||||
import dpath.options
|
||||
from funcy import distinct, first, project
|
||||
from funcy import first, ldistinct, project, reraise
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import error_handler, errored_revisions, onerror_collect
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.serialize import LOADERS
|
||||
from dvc.utils.serialize import PARSERS, EncodingError
|
||||
from dvc.utils.threadpool import ThreadPoolExecutor
|
||||
from dvc_render.image import ImageRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
@@ -39,6 +40,9 @@ dpath.options.ALLOW_EMPTY_STRING_KEYS = True
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SUPPORTED_IMAGE_EXTENSIONS = ImageRenderer.EXTENSIONS
|
||||
|
||||
|
||||
class PlotMetricTypeError(DvcException):
|
||||
def __init__(self, file):
|
||||
super().__init__(
|
||||
@@ -60,7 +64,11 @@ class PropsNotFoundError(DvcException):
|
||||
|
||||
@error_handler
|
||||
def _unpack_dir_files(fs, path, **kwargs):
|
||||
return list(fs.find(path))
|
||||
ret = list(fs.find(path))
|
||||
if not ret:
|
||||
# This will raise FileNotFoundError if it is a broken symlink or TreeError
|
||||
next(iter(fs.ls(path)), None)
|
||||
return ret
|
||||
|
||||
|
||||
class Plots:
|
||||
@@ -153,9 +161,7 @@ class Plots:
|
||||
props: Optional[Dict] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
):
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DVCFileSystem(repo=self.repo)
|
||||
fs = self.repo.dvcfs
|
||||
|
||||
props = props or {}
|
||||
|
||||
@@ -196,7 +202,10 @@ class Plots:
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
):
|
||||
_resolve_data_sources(data)
|
||||
short_rev = "workspace"
|
||||
if rev := getattr(self.repo.fs, "rev", None):
|
||||
short_rev = rev[:7]
|
||||
_resolve_data_sources(data, short_rev, cache_remote_stream=True)
|
||||
result.update(data)
|
||||
|
||||
errored = errored_revisions(result)
|
||||
@@ -265,7 +274,11 @@ def _is_plot(out: "Output") -> bool:
|
||||
return bool(out.plot)
|
||||
|
||||
|
||||
def _resolve_data_sources(plots_data: Dict):
|
||||
def _resolve_data_sources(
|
||||
plots_data: Dict, rev: str, cache_remote_stream: bool = False
|
||||
):
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
values = list(plots_data.values())
|
||||
to_resolve = []
|
||||
while values:
|
||||
@@ -278,16 +291,26 @@ def _resolve_data_sources(plots_data: Dict):
|
||||
def resolve(value):
|
||||
data_source = value.pop("data_source")
|
||||
assert callable(data_source)
|
||||
value.update(data_source())
|
||||
value.update(data_source(cache_remote_stream=cache_remote_stream))
|
||||
|
||||
if not to_resolve:
|
||||
return
|
||||
|
||||
executor = ThreadPoolExecutor(
|
||||
max_workers=4 * cpu_count(),
|
||||
max_workers=min(16, 4 * cpu_count()),
|
||||
thread_name_prefix="resolve_data",
|
||||
cancel_on_error=True,
|
||||
)
|
||||
with executor:
|
||||
# imap_unordered is lazy, wrapping to trigger it
|
||||
list(executor.imap_unordered(resolve, to_resolve))
|
||||
iterable = executor.imap_unordered(resolve, to_resolve)
|
||||
with Tqdm(
|
||||
iterable,
|
||||
total=len(to_resolve),
|
||||
desc=f"Reading plot's data from {rev}",
|
||||
unit="files",
|
||||
unit_scale=False,
|
||||
) as progress_iterable:
|
||||
list(progress_iterable)
|
||||
|
||||
|
||||
def _collect_plots(
|
||||
@@ -333,7 +356,7 @@ def infer_data_sources(plot_id, config=None):
|
||||
if isinstance(x, dict):
|
||||
sources.append(first(x.keys()))
|
||||
|
||||
return distinct(source for source in sources)
|
||||
return ldistinct(source for source in sources)
|
||||
|
||||
|
||||
def _matches(targets, config_file, plot_id):
|
||||
@@ -485,10 +508,7 @@ def _collect_definitions(
|
||||
result: Dict = defaultdict(dict)
|
||||
props = props or {}
|
||||
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
|
||||
fs = repo.dvcfs
|
||||
dpath.merge(
|
||||
result,
|
||||
_collect_pipeline_files(repo, targets, props, onerror=onerror),
|
||||
@@ -524,20 +544,25 @@ def unpack_if_dir(fs, path, props: Dict[str, str], onerror: Optional[Callable] =
|
||||
|
||||
|
||||
@error_handler
|
||||
def parse(fs, path, props=None, **kwargs):
|
||||
def parse(fs, path, props=None, **fs_kwargs):
|
||||
props = props or {}
|
||||
_, extension = os.path.splitext(path)
|
||||
if extension in (".tsv", ".csv"):
|
||||
header = props.get("header", True)
|
||||
if extension == ".csv":
|
||||
return _load_sv(path=path, fs=fs, delimiter=",", header=header)
|
||||
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", ".svg"):
|
||||
with fs.open(path, "rb") as fd:
|
||||
if extension in SUPPORTED_IMAGE_EXTENSIONS:
|
||||
with fs.open(path, mode="rb", **fs_kwargs) as fd:
|
||||
return fd.read()
|
||||
raise PlotMetricTypeError(path)
|
||||
|
||||
if extension not in PARSERS.keys() | {".yml", ".yaml", ".csv", ".tsv"}:
|
||||
raise PlotMetricTypeError(path)
|
||||
|
||||
with reraise(UnicodeDecodeError, EncodingError(path, "utf8")):
|
||||
with fs.open(path, mode="r", encoding="utf8", **fs_kwargs) as fd:
|
||||
contents = fd.read()
|
||||
|
||||
if extension in (".csv", ".tsv"):
|
||||
header = props.get("header", True)
|
||||
delim = "\t" if extension == ".tsv" else ","
|
||||
return _load_sv(contents, delimiter=delim, header=header)
|
||||
return PARSERS[extension](contents, path)
|
||||
|
||||
|
||||
def _plot_props(out: "Output") -> Dict:
|
||||
@@ -553,10 +578,7 @@ def _plot_props(out: "Output") -> Dict:
|
||||
return project(out.plot, PLOT_PROPS)
|
||||
|
||||
|
||||
def _load_sv(path, fs, delimiter=",", header=True):
|
||||
with fs.open(path, "r") as fd:
|
||||
content = fd.read()
|
||||
|
||||
def _load_sv(content, delimiter=",", header=True):
|
||||
if header:
|
||||
reader = csv.DictReader(io.StringIO(content), delimiter=delimiter)
|
||||
else:
|
||||
@@ -566,5 +588,4 @@ def _load_sv(path, fs, delimiter=",", header=True):
|
||||
delimiter=delimiter,
|
||||
fieldnames=[str(i) for i in range(len(first_row))],
|
||||
)
|
||||
|
||||
return list(reader)
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import InvalidArgumentError, UploadError
|
||||
from dvc.utils import glob_targets
|
||||
|
||||
@@ -30,9 +32,10 @@ def push( # noqa: C901, PLR0913
|
||||
include_imports=False,
|
||||
):
|
||||
worktree_remote: Optional["Remote"] = None
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree or _remote.fs.version_aware:
|
||||
worktree_remote = _remote
|
||||
with suppress(NoRemoteError):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote and (_remote.worktree or _remote.fs.version_aware):
|
||||
worktree_remote = _remote
|
||||
|
||||
pushed = 0
|
||||
used_run_cache = self.stage_cache.push(remote, odb=odb) if run_cache else []
|
||||
|
||||
+11
-2
@@ -5,7 +5,11 @@ from contextlib import suppress
|
||||
from functools import wraps
|
||||
from typing import Iterable, List, NamedTuple, Optional, Set, Tuple, Union
|
||||
|
||||
from dvc.exceptions import NoOutputOrStageError, OutputNotFoundError
|
||||
from dvc.exceptions import (
|
||||
NoOutputOrStageError,
|
||||
OutputDuplicationError,
|
||||
OutputNotFoundError,
|
||||
)
|
||||
from dvc.repo import lock_repo
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import as_posix, parse_target
|
||||
@@ -176,7 +180,12 @@ class StageLoad:
|
||||
|
||||
check_stage_exists(self.repo, stage, stage.path)
|
||||
|
||||
self.repo.check_graph(stages={stage})
|
||||
try:
|
||||
self.repo.check_graph(stages={stage})
|
||||
except OutputDuplicationError as exc:
|
||||
# Don't include the stage currently being added.
|
||||
exc.stages.remove(stage)
|
||||
raise OutputDuplicationError(exc.output, exc.stages) from None
|
||||
|
||||
restore_fields(stage)
|
||||
return stage
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
import logging
|
||||
from itertools import compress
|
||||
|
||||
from funcy.py3 import cat
|
||||
from itertools import chain, compress
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
@@ -29,7 +27,7 @@ def _joint_status(pairs):
|
||||
|
||||
def _local_status(self, targets=None, with_deps=False, recursive=False):
|
||||
targets = targets or [None]
|
||||
pairs = cat(
|
||||
pairs = chain.from_iterable(
|
||||
self.stage.collect_granular(t, with_deps=with_deps, recursive=recursive)
|
||||
for t in targets
|
||||
)
|
||||
|
||||
+2
-1
@@ -165,7 +165,7 @@ def clone(url: str, to_path: str, **kwargs):
|
||||
raise CloneError("SCM error") from exc
|
||||
|
||||
|
||||
def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
def resolve_rev(scm: Union["Git", "NoSCM"], rev: str) -> str:
|
||||
from scmrepo.exceptions import RevError as InternalRevError
|
||||
|
||||
from dvc.repo.experiments.utils import fix_exp_head
|
||||
@@ -173,6 +173,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
try:
|
||||
return scm.resolve_rev(fix_exp_head(scm, rev))
|
||||
except InternalRevError as exc:
|
||||
assert isinstance(scm, Git)
|
||||
# `scm` will only resolve git branch and tag names,
|
||||
# if rev is not a sha it may be an abbreviated experiment name
|
||||
if not (rev == "HEAD" or rev.startswith("refs/")):
|
||||
|
||||
+3
-3
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
@@ -167,9 +166,10 @@ class StageCache:
|
||||
COMPILED_LOCK_FILE_STAGE_SCHEMA(cache)
|
||||
|
||||
path = self._get_cache_path(cache_key, cache_value)
|
||||
parent = self.repo.cache.local.fs.path.parent(path)
|
||||
local_fs = self.repo.cache.local.fs
|
||||
parent = local_fs.path.parent(path)
|
||||
self.repo.cache.local.makedirs(parent)
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, dir=parent).name
|
||||
tmp = local_fs.path.join(parent, fs.utils.tmp_fname())
|
||||
assert os.path.exists(parent)
|
||||
assert os.path.isdir(parent)
|
||||
dump_yaml(tmp, cache)
|
||||
|
||||
+87
-6
@@ -1,4 +1,4 @@
|
||||
from contextlib import contextmanager
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -13,11 +13,15 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.console import JustifyMethod, OverflowMethod
|
||||
from rich.status import Status
|
||||
from rich.style import Style
|
||||
from rich.text import Text as RichText
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
@@ -25,6 +29,21 @@ if TYPE_CHECKING:
|
||||
from dvc.ui.table import Headers, Styles, TableData
|
||||
|
||||
|
||||
@contextmanager
|
||||
def disable_colorama():
|
||||
import sys
|
||||
|
||||
colorama.deinit()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if sys.stdout:
|
||||
sys.stdout.flush()
|
||||
if sys.stderr:
|
||||
sys.stderr.flush()
|
||||
colorama.reinit()
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(
|
||||
self, theme: Optional[Dict] = None, defaults: Optional[Dict] = None
|
||||
@@ -105,8 +124,9 @@ class Console:
|
||||
if indent is None and self.isatty():
|
||||
indent = 2
|
||||
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
return console.print_json(
|
||||
from rich.json import JSON
|
||||
|
||||
json = JSON.from_data(
|
||||
data=data,
|
||||
indent=indent,
|
||||
highlight=bool(highlight),
|
||||
@@ -117,6 +137,56 @@ class Console:
|
||||
default=default,
|
||||
sort_keys=sort_keys,
|
||||
)
|
||||
if not highlight:
|
||||
import os
|
||||
|
||||
# we don't need colorama to try to strip ansi codes
|
||||
# when highlighting is disabled
|
||||
ctx = nullcontext() if "DVC_TEST" in os.environ else disable_colorama()
|
||||
with ctx:
|
||||
return self.write(json.text, stderr=stderr)
|
||||
return self.rich_print(json, stderr=stderr, soft_wrap=True)
|
||||
|
||||
def rich_print(
|
||||
self,
|
||||
*objects: Any,
|
||||
sep: str = " ",
|
||||
end: str = "\n",
|
||||
stderr: bool = False,
|
||||
style: Optional[Union[str, "Style"]] = None,
|
||||
justify: Optional["JustifyMethod"] = None,
|
||||
overflow: Optional["OverflowMethod"] = None,
|
||||
no_wrap: Optional[bool] = None,
|
||||
emoji: Optional[bool] = None,
|
||||
markup: Optional[bool] = None,
|
||||
highlight: Optional[bool] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
crop: bool = True,
|
||||
soft_wrap: Optional[bool] = None,
|
||||
new_line_start: bool = False,
|
||||
) -> None:
|
||||
if stderr:
|
||||
console = self.error_console
|
||||
else:
|
||||
console = self.rich_console
|
||||
return console.print(
|
||||
*objects,
|
||||
sep=sep,
|
||||
end=end,
|
||||
style=style,
|
||||
justify=justify,
|
||||
overflow=overflow,
|
||||
no_wrap=no_wrap,
|
||||
emoji=emoji,
|
||||
markup=markup,
|
||||
highlight=highlight,
|
||||
width=width,
|
||||
height=height,
|
||||
crop=crop,
|
||||
soft_wrap=soft_wrap,
|
||||
new_line_start=new_line_start,
|
||||
)
|
||||
|
||||
def write(
|
||||
self,
|
||||
@@ -142,10 +212,21 @@ class Console:
|
||||
with Tqdm.external_write_mode(file=file):
|
||||
# if we are inside pager context, send the output to rich's buffer
|
||||
if styled or self._paginate.get():
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
if styled:
|
||||
return console.print(*objects, sep=sep, end=end)
|
||||
return console.out(*objects, sep=sep, end=end, highlight=False)
|
||||
return self.rich_print(*objects, sep=sep, end=end, stderr=stderr)
|
||||
return self.rich_print(
|
||||
sep.join(str(_object) for _object in objects),
|
||||
style=None,
|
||||
highlight=False,
|
||||
emoji=False,
|
||||
markup=False,
|
||||
no_wrap=True,
|
||||
overflow="ignore",
|
||||
crop=False,
|
||||
sep=sep,
|
||||
end=end,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
values = (self.formatter.format(obj, style) for obj in objects)
|
||||
return print(*values, sep=sep, end=end, file=file)
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ class Updater:
|
||||
from dvc.config import Config, to_bool
|
||||
|
||||
enabled = to_bool(
|
||||
Config(validate=False).get("core", {}).get("check_update", "true")
|
||||
Config.from_cwd(validate=False).get("core", {}).get("check_update", "true")
|
||||
)
|
||||
logger.debug("Check for update is %sabled.", "en" if enabled else "dis")
|
||||
return enabled
|
||||
|
||||
@@ -36,6 +36,8 @@ def compose_and_dump(
|
||||
with initialize_config_dir(config_dir, version_base=None):
|
||||
cfg = compose(config_name=config_name, overrides=overrides)
|
||||
|
||||
OmegaConf.resolve(cfg)
|
||||
|
||||
suffix = Path(output_file).suffix.lower()
|
||||
if suffix not in [".yml", ".yaml"]:
|
||||
dumper = DUMPERS[suffix]
|
||||
|
||||
@@ -12,6 +12,13 @@ LOADERS: DefaultDict[str, LoaderFn] = defaultdict( # noqa: F405
|
||||
)
|
||||
LOADERS.update({".toml": load_toml, ".json": load_json, ".py": load_py}) # noqa: F405
|
||||
|
||||
PARSERS: DefaultDict[str, ParserFn] = defaultdict( # noqa: F405
|
||||
lambda: parse_yaml # noqa: F405
|
||||
)
|
||||
PARSERS.update(
|
||||
{".toml": parse_toml, ".json": parse_json, ".py": parse_py} # noqa: F405
|
||||
)
|
||||
|
||||
|
||||
def load_path(fs_path, fs):
|
||||
suffix = fs.path.suffix(fs_path).lower()
|
||||
|
||||
@@ -177,8 +177,8 @@ def determine_linecol(
|
||||
value = get(data, paths, default=None)
|
||||
if value is not None:
|
||||
with suppress(AttributeError, TypeError):
|
||||
line = value.lc.line + 1
|
||||
col = value.lc.col + 1
|
||||
line = value.lc.line + 1 # type: ignore[attr-defined]
|
||||
col = value.lc.col + 1 # type: ignore[attr-defined]
|
||||
break
|
||||
step += 1
|
||||
*paths, _ = paths
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from funcy import compact, ignore
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from requests import Response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STUDIO_URL = "https://studio.iterative.ai"
|
||||
|
||||
|
||||
def post(
|
||||
endpoint: str,
|
||||
token: str,
|
||||
data: Dict[str, Any],
|
||||
url: Optional[str] = STUDIO_URL,
|
||||
max_retries: int = 3,
|
||||
timeout: int = 5,
|
||||
) -> "Response":
|
||||
endpoint = urljoin(url or STUDIO_URL, endpoint)
|
||||
session = requests.Session()
|
||||
session.mount(endpoint, HTTPAdapter(max_retries=max_retries))
|
||||
|
||||
logger.trace("Sending %s to %s", data, endpoint) # type: ignore[attr-defined]
|
||||
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
r = session.post(endpoint, json=data, headers=headers, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
|
||||
def notify_refs(
|
||||
repo_url: str,
|
||||
token: str,
|
||||
*,
|
||||
studio_url: Optional[str] = STUDIO_URL,
|
||||
**refs: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
extra_keys = refs.keys() - {"pushed", "removed"}
|
||||
assert not extra_keys, f"got extra args: {extra_keys}"
|
||||
|
||||
refs = compact(refs)
|
||||
if not refs:
|
||||
return {}
|
||||
|
||||
logger.debug(
|
||||
"notifying Studio%s about updated experiments",
|
||||
f" ({studio_url})" if studio_url else "",
|
||||
)
|
||||
data = {"repo_url": repo_url, "client": "dvc", "refs": refs}
|
||||
|
||||
try:
|
||||
r = post("/webhook/dvc", token, data, url=studio_url)
|
||||
except requests.RequestException as e:
|
||||
logger.trace("", exc_info=True) # type: ignore[attr-defined]
|
||||
|
||||
msg = str(e)
|
||||
if e.response is None:
|
||||
logger.warning("failed to notify Studio: %s", msg.lower())
|
||||
return {}
|
||||
|
||||
r = cast("Response", e.response)
|
||||
d = ignore(Exception, default={})(r.json)()
|
||||
status = r.status_code
|
||||
if detail := d.get("detail"):
|
||||
msg = f"{detail} ({status=})"
|
||||
logger.warning("failed to notify Studio: %s", msg.lower())
|
||||
else:
|
||||
d = r.json()
|
||||
|
||||
if d:
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"received response: %s (status=%r)", d, r.status_code
|
||||
)
|
||||
return d
|
||||
+13
-8
@@ -30,15 +30,14 @@ classifiers = [
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"appdirs>=1.4.3",
|
||||
"colorama>=0.3.9",
|
||||
"configobj>=5.0.6",
|
||||
"distro>=1.3",
|
||||
"dpath<3,>=2.1.0",
|
||||
"dvc-data>=0.42.1,<0.43",
|
||||
"dvc-data>=0.46.0,<0.47",
|
||||
"dvc-http",
|
||||
"dvc-render>=0.1.2",
|
||||
"dvc-studio-client>=0.1.1",
|
||||
"dvc-render>=0.3.1,<0.4.0",
|
||||
"dvc-studio-client>=0.6.1,<1",
|
||||
"dvc-task>=0.2.0,<1",
|
||||
"flatten_dict<1,>=0.4.1",
|
||||
"flufl.lock>=5",
|
||||
@@ -49,6 +48,7 @@ dependencies = [
|
||||
"networkx>=2.5",
|
||||
"packaging>=19",
|
||||
"pathspec>=0.10.3",
|
||||
"platformdirs<4,>=3.1.1",
|
||||
"psutil>=5.8",
|
||||
"pydot>=1.2.4",
|
||||
"pygtrie>=2.3.2",
|
||||
@@ -56,7 +56,7 @@ dependencies = [
|
||||
"requests>=2.22",
|
||||
"rich>=12",
|
||||
"ruamel.yaml>=0.17.11",
|
||||
"scmrepo>=0.1.15,<1",
|
||||
"scmrepo>=0.1.17,<1",
|
||||
"shortuuid>=0.5",
|
||||
"shtab<2,>=1.3.4",
|
||||
"tabulate>=0.8.7",
|
||||
@@ -73,9 +73,8 @@ gdrive = ["dvc-gdrive==2.19.1"]
|
||||
gs = ["dvc-gs==2.22.0"]
|
||||
hdfs = ["dvc-hdfs==2.19"]
|
||||
lint = [
|
||||
"mypy==1.0.1",
|
||||
"pylint==2.16.3",
|
||||
"types-appdirs",
|
||||
"mypy==1.1.1",
|
||||
"pylint==2.17.1",
|
||||
"types-colorama",
|
||||
"types-psutil",
|
||||
"types-requests",
|
||||
@@ -133,6 +132,8 @@ profile = "black"
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-ra --cov-config pyproject.toml --dist worksteal"
|
||||
filterwarnings = [
|
||||
"error::ResourceWarning",
|
||||
"error::pytest.PytestUnraisableExceptionWarning",
|
||||
"error::pytest_mock.PytestMockWarning",
|
||||
# remove when aiobotocore supports botocore>=1.29.13
|
||||
"ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
|
||||
@@ -204,6 +205,7 @@ module = [
|
||||
"celery.*",
|
||||
"configobj.*",
|
||||
"dpath.*",
|
||||
"distro",
|
||||
"dvc_http",
|
||||
"dvc_render.*",
|
||||
"dvc_ssh",
|
||||
@@ -218,9 +220,12 @@ module = [
|
||||
"networkx.*", # https://github.com/networkx/networkx/issues/3988
|
||||
"pygtrie.*",
|
||||
"pyinstrument",
|
||||
"pyparsing",
|
||||
"pytest_docker.plugin",
|
||||
"ruamel.*",
|
||||
"ruamel.yaml.*",
|
||||
"shortuuid",
|
||||
"shtab",
|
||||
"tpi.*",
|
||||
"viztracer",
|
||||
"voluptuous",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# NOTE: using strict version due to
|
||||
# https://github.com/iterative/dvc/issues/7949
|
||||
PyInstaller==5.8.0
|
||||
PyInstaller==5.9.0
|
||||
|
||||
+6
-23
@@ -1,4 +1,3 @@
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -174,22 +173,20 @@ def mocked_webbrowser_open(mocker):
|
||||
mocker.patch("webbrowser.open")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate(tmp_path_factory, monkeypatch) -> None:
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def isolate(tmp_path_factory):
|
||||
path = tmp_path_factory.mktemp("mock")
|
||||
home_dir = path / "home"
|
||||
home_dir.mkdir()
|
||||
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
if sys.platform == "win32":
|
||||
home_drive, home_path = os.path.splitdrive(home_dir)
|
||||
monkeypatch.setenv("USERPROFILE", str(home_dir))
|
||||
monkeypatch.setenv("HOMEDRIVE", home_drive)
|
||||
monkeypatch.setenv("HOMEPATH", home_path)
|
||||
|
||||
for env_var, sub_path in (
|
||||
("APPDATA", "Roaming"),
|
||||
("LOCALAPPDATA", "Local"),
|
||||
):
|
||||
for env_var, sub_path in (("APPDATA", "Roaming"), ("LOCALAPPDATA", "Local")):
|
||||
path = home_dir / "AppData" / sub_path
|
||||
path.mkdir(parents=True)
|
||||
monkeypatch.setenv(env_var, os.fspath(path))
|
||||
@@ -209,6 +206,8 @@ defaultBranch=master
|
||||
import pygit2
|
||||
|
||||
pygit2.settings.search_path[pygit2.GIT_CONFIG_LEVEL_GLOBAL] = str(home_dir)
|
||||
yield
|
||||
monkeypatch.undo()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -244,19 +243,3 @@ def run_copy_metrics(tmp_dir, copy_script):
|
||||
return stage
|
||||
|
||||
return run
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from funcy import first, get_in
|
||||
|
||||
from dvc import api
|
||||
from dvc.exceptions import FileMissingError, OutputNotFoundError, PathMissingError
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.testing.api_tests import TestAPI # noqa, pylint: disable=unused-import
|
||||
from dvc.testing.tmp_dir import make_subrepo
|
||||
from dvc.utils.fs import remove
|
||||
@@ -75,7 +75,7 @@ def test_missing(tmp_dir, dvc, remote):
|
||||
|
||||
remove("foo")
|
||||
|
||||
with pytest.raises(FileMissingError):
|
||||
with pytest.raises(PathMissingError):
|
||||
api.read("foo")
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ def test_open_not_cached(dvc):
|
||||
assert fd.read() == metric_content
|
||||
|
||||
os.remove(metric_file)
|
||||
with pytest.raises(FileMissingError):
|
||||
with pytest.raises(PathMissingError):
|
||||
api.read(metric_file)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
|
||||
from dvc import api
|
||||
from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
|
||||
|
||||
def test_exp_save(tmp_dir, dvc, scm, mocker):
|
||||
tmp_dir.scm_gen({"foo": "foo"}, commit="initial")
|
||||
|
||||
api.exp_save()
|
||||
|
||||
api.exp_save("foo")
|
||||
with pytest.raises(
|
||||
ExperimentExistsError,
|
||||
match="Experiment conflicts with existing experiment 'foo'.",
|
||||
):
|
||||
api.exp_save("foo")
|
||||
api.exp_save("foo", force=True)
|
||||
@@ -5,6 +5,9 @@ from tests.unit.repo.experiments.conftest import ( # noqa, pylint disable=unuse
|
||||
exp_stage,
|
||||
failed_checkpoint_stage,
|
||||
failed_exp_stage,
|
||||
session_app,
|
||||
session_queue,
|
||||
session_worker,
|
||||
test_queue,
|
||||
)
|
||||
|
||||
@@ -24,7 +27,7 @@ def http_auth_patch(mocker):
|
||||
|
||||
|
||||
@pytest.fixture(params=[True, False])
|
||||
def workspace(request, test_queue) -> bool: # noqa: F811
|
||||
def workspace(request, session_queue) -> bool: # noqa: F811
|
||||
return request.param
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from funcy import first
|
||||
|
||||
@@ -26,7 +28,12 @@ def test_apply(tmp_dir, scm, dvc, exp_stage):
|
||||
assert (tmp_dir / "metrics.yaml").read_text().strip() == "foo: 3"
|
||||
|
||||
|
||||
def test_apply_failed(tmp_dir, scm, dvc, failed_exp_stage, test_queue):
|
||||
@pytest.mark.xfail(
|
||||
os.name == "nt",
|
||||
strict=False,
|
||||
reason="https://github.com/iterative/dvc/issues/9143",
|
||||
)
|
||||
def test_apply_failed(tmp_dir, scm, dvc, failed_exp_stage, session_queue):
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
dvc.experiments.run(failed_exp_stage.addressing, params=["foo=2"], queue=True)
|
||||
@@ -35,7 +42,7 @@ def test_apply_failed(tmp_dir, scm, dvc, failed_exp_stage, test_queue):
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing, params=["foo=3"], queue=True, name="foo"
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
session_queue.wait(["foo"])
|
||||
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.apply("bar")
|
||||
|
||||
@@ -25,9 +25,7 @@ def test_new_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, mocker, workspace,
|
||||
exp = first(results)
|
||||
|
||||
new_mock.assert_called_once()
|
||||
for rev in dvc.brancher([exp]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
with dvc.switch(exp):
|
||||
fs = dvc.dvcfs
|
||||
with fs.open("foo") as fobj:
|
||||
assert fobj.read().strip() == str(checkpoint_stage.iterations)
|
||||
@@ -69,9 +67,7 @@ def test_resume_checkpoint(
|
||||
)
|
||||
exp = first(results)
|
||||
|
||||
for rev in dvc.brancher([exp]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
with dvc.switch(exp):
|
||||
fs = dvc.dvcfs
|
||||
with fs.open("foo") as fobj:
|
||||
assert fobj.read().strip() == str(2 * checkpoint_stage.iterations)
|
||||
@@ -94,9 +90,7 @@ def test_reset_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, caplog, workspace
|
||||
)
|
||||
exp = first(results)
|
||||
|
||||
for rev in dvc.brancher([exp]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
with dvc.switch(exp):
|
||||
fs = dvc.dvcfs
|
||||
with fs.open("foo") as fobj:
|
||||
assert fobj.read().strip() == str(checkpoint_stage.iterations)
|
||||
@@ -132,18 +126,14 @@ def test_resume_branch(tmp_dir, scm, dvc, checkpoint_stage, workspace):
|
||||
)
|
||||
checkpoint_b = first(results)
|
||||
|
||||
for rev in dvc.brancher([checkpoint_a]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
with dvc.switch(checkpoint_a):
|
||||
fs = dvc.dvcfs
|
||||
with fs.open("foo") as fobj:
|
||||
assert fobj.read().strip() == str(2 * checkpoint_stage.iterations)
|
||||
with fs.open("metrics.yaml") as fobj:
|
||||
assert fobj.read().strip() == "foo: 2"
|
||||
|
||||
for rev in dvc.brancher([checkpoint_b]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
with dvc.switch(checkpoint_b):
|
||||
fs = dvc.dvcfs
|
||||
with fs.open("foo") as fobj:
|
||||
assert fobj.read().strip() == str(2 * checkpoint_stage.iterations)
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
from funcy import first
|
||||
|
||||
|
||||
def test_diff_no_commits(tmp_dir):
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git
|
||||
|
||||
git = Git.init(tmp_dir.fs_path)
|
||||
assert git.no_commits
|
||||
|
||||
assert Repo.init().experiments.diff() == {}
|
||||
|
||||
|
||||
def test_diff_empty(tmp_dir, scm, dvc, exp_stage):
|
||||
assert dvc.experiments.diff() == {"params": {}, "metrics": {}}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import stat
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from configobj import ConfigObj
|
||||
from funcy import first
|
||||
|
||||
from dvc.dvcfile import PROJECT_FILE
|
||||
@@ -13,7 +14,7 @@ from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
from dvc.repo.experiments.queue.base import BaseStashQueue
|
||||
from dvc.repo.experiments.refs import CELERY_STASH
|
||||
from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.scm import resolve_rev
|
||||
from dvc.scm import SCMError, resolve_rev
|
||||
from dvc.stage.exceptions import StageFileDoesNotExistError
|
||||
from dvc.utils.serialize import PythonFileCorruptedError
|
||||
from tests.scripts import COPY_SCRIPT
|
||||
@@ -131,7 +132,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, copy_script):
|
||||
def test_update_py_params(tmp_dir, scm, dvc, session_queue, copy_script):
|
||||
tmp_dir.gen("params.py", "INT = 1\n")
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py params.py metrics.py",
|
||||
@@ -246,11 +247,12 @@ def test_branch(tmp_dir, scm, dvc, exp_stage):
|
||||
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.branch("foo", "branch-exists")
|
||||
dvc.experiments.branch("foo")
|
||||
dvc.experiments.branch("foo", "branch-name")
|
||||
dvc.experiments.branch(exp_a, "branch-rev")
|
||||
dvc.experiments.branch(ref_a, "branch-ref")
|
||||
|
||||
for name in ["branch-name", "branch-rev", "branch-ref"]:
|
||||
for name in ["foo-branch", "branch-name", "branch-rev", "branch-ref"]:
|
||||
assert name in scm.list_branches()
|
||||
assert scm.resolve_rev(name) == exp_a
|
||||
|
||||
@@ -597,17 +599,21 @@ def test_experiments_workspace_not_log_exception(caplog, dvc, scm):
|
||||
assert not caplog.text
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_run_env(tmp_dir, dvc, scm, mocker):
|
||||
dump_run_env = dedent(
|
||||
"""\
|
||||
import os
|
||||
from dvc_studio_client.env import STUDIO_REPO_URL
|
||||
from dvc.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME
|
||||
with open("DVC_EXP_BASELINE_REV", "w") as f:
|
||||
f.write(os.environ.get(DVC_EXP_BASELINE_REV, ""))
|
||||
with open("DVC_EXP_NAME", "w") as f:
|
||||
f.write(os.environ.get(DVC_EXP_NAME, ""))
|
||||
for v in (DVC_EXP_BASELINE_REV, DVC_EXP_NAME, STUDIO_REPO_URL):
|
||||
with open(v, "w") as f:
|
||||
f.write(os.environ.get(v, ""))
|
||||
"""
|
||||
)
|
||||
mocker.patch(
|
||||
"dvc.repo.experiments.queue.base.get_studio_repo_url", return_value="REPO_URL"
|
||||
)
|
||||
(tmp_dir / "dump_run_env.py").write_text(dump_run_env)
|
||||
baseline = scm.get_rev()
|
||||
dvc.stage.add(
|
||||
@@ -616,11 +622,13 @@ def test_run_env(tmp_dir, dvc, scm, mocker):
|
||||
)
|
||||
dvc.experiments.run()
|
||||
assert (tmp_dir / "DVC_EXP_BASELINE_REV").read_text().strip() == baseline
|
||||
assert (tmp_dir / "DVC_EXP_NAME").read_text().strip() != ""
|
||||
assert (tmp_dir / "DVC_EXP_NAME").read_text().strip()
|
||||
assert (tmp_dir / "STUDIO_REPO_URL").read_text().strip() == "REPO_URL"
|
||||
|
||||
dvc.experiments.run(name="foo")
|
||||
assert (tmp_dir / "DVC_EXP_BASELINE_REV").read_text().strip() == baseline
|
||||
assert (tmp_dir / "DVC_EXP_NAME").read_text().strip() == "foo"
|
||||
assert (tmp_dir / "STUDIO_REPO_URL").read_text().strip() == "REPO_URL"
|
||||
|
||||
|
||||
def test_experiment_unchanged(tmp_dir, scm, dvc, exp_stage):
|
||||
@@ -640,3 +648,38 @@ def test_clean(tmp_dir, scm, dvc, mocker):
|
||||
clean = mocker.spy(dvc.experiments.celery_queue.celery, "clean")
|
||||
dvc.experiments.clean()
|
||||
clean.assert_called_once_with()
|
||||
|
||||
|
||||
def test_experiment_no_commit(tmp_dir):
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
Git.init(tmp_dir.fs_path).close()
|
||||
|
||||
repo = Repo.init()
|
||||
assert repo.scm.no_commits
|
||||
|
||||
try:
|
||||
with pytest.raises(SCMError): # noqa: PT011
|
||||
repo.experiments.ls()
|
||||
finally:
|
||||
repo.close()
|
||||
|
||||
|
||||
def test_local_config_is_propagated_to_tmp(tmp_dir, scm, dvc):
|
||||
with dvc.config.edit("local") as conf:
|
||||
conf["cache"]["type"] = "hardlink"
|
||||
|
||||
stage = dvc.stage.add(
|
||||
cmd="cat .dvc/config.local > file", name="foo", outs_no_cache=["file"]
|
||||
)
|
||||
scm.add_commit(["dvc.yaml"], message="add dvc.yaml")
|
||||
|
||||
results = dvc.experiments.run(stage.addressing, tmp_dir=True)
|
||||
exp = first(results)
|
||||
fs = scm.get_fs(exp)
|
||||
|
||||
with fs.open("file") as fobj:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
assert conf_obj["cache"]["type"] == "hardlink"
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_when_params_is_omitted_in_interactive_mode(tmp_dir, scm, dvc):
|
||||
}
|
||||
}
|
||||
assert not (tmp_dir / "dvc.lock").exists()
|
||||
assert (tmp_dir / "script.py").read_text() == ""
|
||||
assert not (tmp_dir / "script.py").read_text()
|
||||
assert (tmp_dir / "data").is_dir()
|
||||
scm._reset()
|
||||
assert scm.is_tracked("dvc.yaml")
|
||||
@@ -194,7 +194,7 @@ def test_init_interactive_params_validation(tmp_dir, dvc, capsys):
|
||||
}
|
||||
}
|
||||
}
|
||||
assert (tmp_dir / "script.py").read_text() == ""
|
||||
assert not (tmp_dir / "script.py").read_text()
|
||||
assert (tmp_dir / "data").is_dir()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
@@ -226,7 +226,7 @@ def test_init_with_no_defaults_interactive(tmp_dir, dvc):
|
||||
}
|
||||
}
|
||||
}
|
||||
assert (tmp_dir / "script.py").read_text() == ""
|
||||
assert not (tmp_dir / "script.py").read_text()
|
||||
assert (tmp_dir / "data").is_dir()
|
||||
|
||||
|
||||
@@ -275,7 +275,7 @@ def test_init_default(tmp_dir, scm, dvc, interactive, overrides, inp, capsys):
|
||||
}
|
||||
}
|
||||
assert not (tmp_dir / "dvc.lock").exists()
|
||||
assert (tmp_dir / "script.py").read_text() == ""
|
||||
assert not (tmp_dir / "script.py").read_text()
|
||||
assert (tmp_dir / "data").is_dir()
|
||||
scm._reset()
|
||||
assert scm.is_tracked("dvc.yaml")
|
||||
@@ -345,7 +345,7 @@ def test_init_interactive_live(tmp_dir, scm, dvc, interactive, overrides, inp, c
|
||||
}
|
||||
}
|
||||
assert not (tmp_dir / "dvc.lock").exists()
|
||||
assert (tmp_dir / "script.py").read_text() == ""
|
||||
assert not (tmp_dir / "script.py").read_text()
|
||||
assert (tmp_dir / "data").is_dir()
|
||||
scm._reset()
|
||||
assert scm.is_tracked("dvc.yaml")
|
||||
|
||||
@@ -19,10 +19,12 @@ def test_celery_logs(
|
||||
failed_exp_stage,
|
||||
follow,
|
||||
capsys,
|
||||
test_queue,
|
||||
):
|
||||
celery_queue = dvc.experiments.celery_queue
|
||||
dvc.experiments.run(failed_exp_stage.addressing, queue=True)
|
||||
dvc.experiments.run(failed_exp_stage.addressing, queue=True, name="foo")
|
||||
dvc.experiments.run(run_all=True)
|
||||
test_queue.wait(["foo"])
|
||||
|
||||
done_result = first(celery_queue.iter_done())
|
||||
|
||||
@@ -33,10 +35,15 @@ def test_celery_logs(
|
||||
assert "failed to reproduce 'failed-copy-file'" in captured.out
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=False,
|
||||
reason="https://github.com/iterative/dvc/issues/9143",
|
||||
)
|
||||
def test_queue_remove_done(
|
||||
dvc,
|
||||
exp_stage,
|
||||
failed_exp_stage,
|
||||
test_queue,
|
||||
):
|
||||
queue_length = 3
|
||||
success_tasks = []
|
||||
@@ -57,6 +64,7 @@ def test_queue_remove_done(
|
||||
name=name_fail,
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
test_queue.wait(success_tasks + failed_tasks)
|
||||
assert len(celery_queue.failed_stash) == 3
|
||||
status = to_dict(celery_queue.status())
|
||||
assert len(status) == 6
|
||||
|
||||
@@ -76,7 +76,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)
|
||||
|
||||
assert dvc.experiments.push(git_upstream.remote, [ref_info.name]) == []
|
||||
assert dvc.experiments.push(git_upstream.remote, [ref_info.name]) == {
|
||||
"pushed": [],
|
||||
"url": None,
|
||||
}
|
||||
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info)) == remote_rev
|
||||
|
||||
dvc.experiments.push(git_upstream.remote, [ref_info.name], force=True)
|
||||
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo.experiments.exceptions import UnresolvedExpNamesError
|
||||
from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
|
||||
|
||||
@@ -121,6 +122,11 @@ def test_remove_remote(tmp_dir, scm, dvc, exp_stage, git_upstream, use_url):
|
||||
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info_list[1])) is None
|
||||
assert git_upstream.tmp_dir.scm.get_ref(str(ref_info_list[2])) == exp_list[2]
|
||||
|
||||
with pytest.raises(
|
||||
UnresolvedExpNamesError, match=f"Experiment 'foo' does not exist in '{remote}'"
|
||||
):
|
||||
dvc.experiments.remove(git_remote=remote, exp_names=["foo"])
|
||||
|
||||
|
||||
def test_remove_experiments_by_rev(tmp_dir, scm, dvc, exp_stage):
|
||||
baseline = scm.get_rev()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from funcy import first
|
||||
|
||||
@@ -8,18 +6,21 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def modified_exp_stage(exp_stage, tmp_dir):
|
||||
with open(tmp_dir / "copy.py", "a", encoding="utf-8") as fh:
|
||||
fh.write("\n# dummy change")
|
||||
def setup_stage(tmp_dir, dvc, scm):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
dvc.run(name="echo-foo", outs=["bar"], cmd="echo ${foo} > bar")
|
||||
scm.add(["dvc.yaml", "dvc.lock", ".gitignore", "params.yaml"])
|
||||
scm.commit("init")
|
||||
|
||||
|
||||
def test_exp_save_unchanged(tmp_dir, dvc, scm, exp_stage):
|
||||
def test_exp_save_unchanged(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
dvc.experiments.save()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", (None, "test"))
|
||||
def test_exp_save(tmp_dir, dvc, scm, exp_stage, name, modified_exp_stage):
|
||||
def test_exp_save(tmp_dir, dvc, scm, name):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
baseline = scm.get_rev()
|
||||
|
||||
exp = dvc.experiments.save(name=name)
|
||||
@@ -32,12 +33,12 @@ def test_exp_save(tmp_dir, dvc, scm, exp_stage, name, modified_exp_stage):
|
||||
assert resolve_rev(scm, exp_name) == exp
|
||||
|
||||
|
||||
def test_exp_save_overwrite_experiment(
|
||||
tmp_dir, dvc, scm, exp_stage, modified_exp_stage
|
||||
):
|
||||
def test_exp_save_overwrite_experiment(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
name = "dummy"
|
||||
dvc.experiments.save(name=name)
|
||||
|
||||
tmp_dir.gen("params.yaml", "foo: 2")
|
||||
with pytest.raises(ExperimentExistsError):
|
||||
dvc.experiments.save(name=name)
|
||||
|
||||
@@ -54,12 +55,14 @@ def test_exp_save_overwrite_experiment(
|
||||
"invalidname.",
|
||||
),
|
||||
)
|
||||
def test_exp_save_invalid_name(tmp_dir, dvc, scm, exp_stage, name):
|
||||
def test_exp_save_invalid_name(tmp_dir, dvc, scm, name):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.save(name=name, force=True)
|
||||
|
||||
|
||||
def test_exp_save_after_commit(tmp_dir, dvc, scm, exp_stage):
|
||||
def test_exp_save_after_commit(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
baseline = scm.get_rev()
|
||||
dvc.experiments.save(name="exp-1", force=True)
|
||||
|
||||
@@ -72,36 +75,44 @@ def test_exp_save_after_commit(tmp_dir, dvc, scm, exp_stage):
|
||||
|
||||
|
||||
def test_exp_save_with_staged_changes(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
tmp_dir.gen({"deleted": "deleted", "modified": "modified"})
|
||||
scm.add_commit(["deleted", "modified"], "init")
|
||||
|
||||
(tmp_dir / "deleted").unlink()
|
||||
tmp_dir.gen({"new_file": "new_file"})
|
||||
scm.add("new_file")
|
||||
(tmp_dir / "modified").write_text("foo")
|
||||
scm.add(["deleted", "new_file", "modified"])
|
||||
|
||||
dvc.experiments.save(name="exp")
|
||||
|
||||
_, _, unstaged = scm.status()
|
||||
assert "new_file" in unstaged
|
||||
exp_rev = dvc.experiments.save(name="exp")
|
||||
scm.checkout(exp_rev, force=True)
|
||||
assert not (tmp_dir / "deleted").exists()
|
||||
assert (tmp_dir / "new_file").exists()
|
||||
assert (tmp_dir / "modified").read_text() == "foo"
|
||||
|
||||
|
||||
def test_exp_save_include_untracked(tmp_dir, dvc, scm, exp_stage):
|
||||
def test_exp_save_include_untracked(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
|
||||
new_file = tmp_dir / "new_file"
|
||||
for i in range(2):
|
||||
new_file.write_text(f"exp-{i}")
|
||||
dvc.experiments.save(name=f"exp-{i}", include_untracked=["new_file"])
|
||||
new_file.write_text("new_file")
|
||||
dvc.experiments.save(name="exp", include_untracked=["new_file"])
|
||||
|
||||
_, _, unstaged = scm.status()
|
||||
assert "new_file" in unstaged
|
||||
assert new_file.read_text() == f"exp-{i}"
|
||||
|
||||
dvc.experiments.apply("exp-0")
|
||||
assert new_file.read_text() == "exp-0"
|
||||
assert new_file.read_text() == "new_file"
|
||||
|
||||
|
||||
def test_exp_save_include_untracked_warning(tmp_dir, dvc, scm, caplog, exp_stage):
|
||||
def test_exp_save_include_untracked_warning(tmp_dir, dvc, scm, mocker):
|
||||
"""Regression test for https://github.com/iterative/dvc/issues/9061"""
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
|
||||
new_dir = tmp_dir / "new_dir"
|
||||
new_dir.mkdir()
|
||||
(new_dir / "foo").write_text("foo")
|
||||
(new_dir / "bar").write_text("bar")
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="dvc.repo.experiments.save"):
|
||||
dvc.experiments.save(name="exp", include_untracked=["new_dir"])
|
||||
assert not caplog.records
|
||||
logger = mocker.patch("dvc.repo.experiments.executor.base.logger")
|
||||
|
||||
dvc.experiments.save(name="exp", include_untracked=["new_dir"])
|
||||
assert not logger.warning.called
|
||||
|
||||
@@ -41,17 +41,6 @@ def make_executor_info(**kwargs):
|
||||
return ExecutorInfo(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_no_commits(tmp_dir):
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git
|
||||
|
||||
git = Git.init(tmp_dir.fs_path)
|
||||
assert git.no_commits
|
||||
|
||||
assert Repo.init().experiments.show() == {}
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_branch_and_tag_name(tmp_dir, scm, dvc, exp_stage):
|
||||
with tmp_dir.branch("new/branch", new=True):
|
||||
@@ -167,15 +156,18 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage):
|
||||
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage, test_queue):
|
||||
baseline_rev = scm.get_rev()
|
||||
timestamp = datetime.fromtimestamp(
|
||||
scm.gitpython.repo.rev_parse(baseline_rev).committed_date
|
||||
)
|
||||
|
||||
dvc.experiments.run(failed_exp_stage.addressing, params=["foo=2"], queue=True)
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing, params=["foo=2"], queue=True, name="show-failed"
|
||||
)
|
||||
exp_rev = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
|
||||
dvc.experiments.run(run_all=True)
|
||||
test_queue.wait(["show-failed"])
|
||||
experiments = dvc.experiments.show()[baseline_rev]
|
||||
|
||||
expected_baseline = {
|
||||
@@ -553,15 +545,20 @@ def test_show_running_celery(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
assert results["workspace"]["baseline"]["data"]["status"] == "Success"
|
||||
|
||||
|
||||
def test_show_running_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, mocker):
|
||||
def test_show_running_checkpoint(
|
||||
tmp_dir, scm, dvc, checkpoint_stage, mocker, test_queue
|
||||
):
|
||||
from dvc.repo.experiments.executor.local import TempDirExecutor
|
||||
|
||||
baseline_rev = scm.get_rev()
|
||||
dvc.experiments.run(checkpoint_stage.addressing, params=["foo=2"], queue=True)
|
||||
dvc.experiments.run(
|
||||
checkpoint_stage.addressing, params=["foo=2"], queue=True, name="foo"
|
||||
)
|
||||
queue = dvc.experiments.celery_queue
|
||||
entries = list(queue.iter_queued())
|
||||
|
||||
run_results = dvc.experiments.run(run_all=True)
|
||||
test_queue.wait(["foo"])
|
||||
checkpoint_rev = first(run_results)
|
||||
exp_ref = first(exp_refs_by_rev(scm, checkpoint_rev))
|
||||
|
||||
|
||||
@@ -44,6 +44,23 @@ def test_with_dict_data(tmp_dir, dvc):
|
||||
assert not resolver.tracked_vars["build@model2"]
|
||||
|
||||
|
||||
def test_with_dict_with_non_str_keys(tmp_dir, dvc):
|
||||
resolver = DataResolver(dvc, tmp_dir.fs_path, {})
|
||||
context = Context()
|
||||
|
||||
foreach_data = {2021: {"thresh": "foo"}, 2022: {"thresh": "bar"}}
|
||||
data = {"foreach": foreach_data, "do": {"cmd": "echo ${key} ${item.thresh}"}}
|
||||
definition = ForeachDefinition(resolver, context, "build", data)
|
||||
|
||||
assert definition.resolve_one("2021") == {"build@2021": {"cmd": "echo 2021 foo"}}
|
||||
assert definition.resolve_one("2022") == {"build@2022": {"cmd": "echo 2022 bar"}}
|
||||
|
||||
# check that `foreach` item-key replacement didnot leave any leftovers.
|
||||
assert not context
|
||||
assert not resolver.tracked_vars["build@2021"]
|
||||
assert not resolver.tracked_vars["build@2022"]
|
||||
|
||||
|
||||
def test_with_composite_list(tmp_dir, dvc):
|
||||
resolver = DataResolver(dvc, tmp_dir.fs_path, {})
|
||||
|
||||
|
||||
@@ -418,5 +418,8 @@ def test_show_plots_defined_with_native_os_path(tmp_dir, dvc, scm, capsys):
|
||||
assert main(["plots", "show", "--json"]) == 0
|
||||
out, _ = capsys.readouterr()
|
||||
json_out = json.loads(out)
|
||||
assert json_out[f"dvc.yaml::{top_level_plot}"]
|
||||
assert json_out[stage_plot]
|
||||
assert "errors" not in json_out
|
||||
|
||||
json_data = json_out["data"]
|
||||
assert json_data[f"dvc.yaml::{top_level_plot}"]
|
||||
assert json_data[stage_plot]
|
||||
|
||||
@@ -245,7 +245,7 @@ def test_merging_two_levels(dvc):
|
||||
|
||||
def test_config_loads_without_error_for_non_dvc_repo(tmp_dir):
|
||||
# regression testing for https://github.com/iterative/dvc/issues/3328
|
||||
Config(validate=True)
|
||||
Config.from_cwd(validate=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -284,7 +284,7 @@ def test_config_gdrive_fields(tmp_dir, dvc):
|
||||
"profile": "myprofile",
|
||||
}
|
||||
|
||||
Config(validate=True)
|
||||
Config.from_cwd(validate=True)
|
||||
|
||||
|
||||
def test_config_remote(tmp_dir, dvc, capsys):
|
||||
|
||||
@@ -358,11 +358,9 @@ def setup_targets_test(tmp_dir):
|
||||
|
||||
|
||||
def test_targets_missing_path(tmp_dir, scm, dvc):
|
||||
from dvc.exceptions import PathMissingError
|
||||
|
||||
setup_targets_test(tmp_dir)
|
||||
|
||||
with pytest.raises(PathMissingError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
dvc.diff(targets=["missing"])
|
||||
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ def test_external_repo(erepo_dir, mocker):
|
||||
clone_spy = mocker.spy(Git, "clone")
|
||||
|
||||
with external_repo(url) as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "master"
|
||||
|
||||
with external_repo(url, rev="branch") as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "branch"
|
||||
|
||||
assert clone_spy.call_count == 1
|
||||
@@ -122,7 +122,7 @@ def test_relative_remote(erepo_dir, tmp_dir):
|
||||
with external_repo(url) as repo:
|
||||
assert os.path.isabs(repo.config["remote"]["upstream"]["url"])
|
||||
assert os.path.isdir(repo.config["remote"]["upstream"]["url"])
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "contents"
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ def test_shallow_clone_branch(erepo_dir, mocker):
|
||||
clone_spy = mocker.spy(Git, "clone")
|
||||
|
||||
with external_repo(url, rev="branch") as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "branch"
|
||||
|
||||
clone_spy.assert_called_with(url, ANY, shallow_branch="branch", progress=ANY)
|
||||
@@ -146,7 +146,7 @@ def test_shallow_clone_branch(erepo_dir, mocker):
|
||||
|
||||
mock_fetch = mocker.patch.object(Git, "fetch")
|
||||
with external_repo(url) as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "master"
|
||||
mock_fetch.assert_called_with(unshallow=True)
|
||||
|
||||
@@ -161,7 +161,7 @@ def test_shallow_clone_tag(erepo_dir, mocker):
|
||||
|
||||
clone_spy = mocker.spy(Git, "clone")
|
||||
with external_repo(url, rev="v1") as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "foo"
|
||||
|
||||
clone_spy.assert_called_with(url, ANY, shallow_branch="v1", progress=ANY)
|
||||
@@ -171,7 +171,7 @@ def test_shallow_clone_tag(erepo_dir, mocker):
|
||||
|
||||
mock_fetch = mocker.patch.object(Git, "fetch")
|
||||
with external_repo(url, rev="master") as repo:
|
||||
with repo.open_by_relpath("file") as fd:
|
||||
with repo.dvcfs.open("file") as fd:
|
||||
assert fd.read() == "bar"
|
||||
mock_fetch.assert_called_with(unshallow=True)
|
||||
|
||||
|
||||
@@ -276,8 +276,7 @@ def test_gc_all_experiments(tmp_dir, scm, dvc):
|
||||
(foo,) = tmp_dir.dvc_gen("foo", "foo", commit="foo")
|
||||
foo_hash = foo.outs[0].hash_info.value
|
||||
|
||||
(bar,) = tmp_dir.dvc_gen("foo", "bar", commit="bar")
|
||||
bar_hash = bar.outs[0].hash_info.value
|
||||
tmp_dir.dvc_gen("foo", "bar", commit="bar")
|
||||
baseline = scm.get_rev()
|
||||
|
||||
(baz,) = tmp_dir.dvc_gen("foo", "baz", commit="baz")
|
||||
@@ -288,12 +287,7 @@ def test_gc_all_experiments(tmp_dir, scm, dvc):
|
||||
|
||||
dvc.gc(all_experiments=True, force=True)
|
||||
|
||||
# all_experiments will include the experiment commit (baz) plus baseline
|
||||
# commit (bar)
|
||||
assert not (tmp_dir / ".dvc" / "cache" / foo_hash[:2] / foo_hash[2:]).exists()
|
||||
assert (
|
||||
tmp_dir / ".dvc" / "cache" / bar_hash[:2] / bar_hash[2:]
|
||||
).read_text() == "bar"
|
||||
assert (
|
||||
tmp_dir / ".dvc" / "cache" / baz_hash[:2] / baz_hash[2:]
|
||||
).read_text() == "baz"
|
||||
|
||||
@@ -217,7 +217,7 @@ def test_get_url_positive(tmp_dir, erepo_dir, caplog, local_cloud):
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.ERROR, logger="dvc"):
|
||||
assert main(["get", os.fspath(erepo_dir), "foo", "--show-url"]) == 0
|
||||
assert caplog.text == ""
|
||||
assert not caplog.text
|
||||
|
||||
|
||||
def test_get_url_not_existing(tmp_dir, erepo_dir, caplog):
|
||||
@@ -231,17 +231,15 @@ def test_get_url_not_existing(tmp_dir, erepo_dir, caplog):
|
||||
"--show-url",
|
||||
]
|
||||
)
|
||||
== 1
|
||||
!= 0
|
||||
)
|
||||
assert "failed to show URL" in caplog.text
|
||||
|
||||
|
||||
def test_get_url_git_only_repo(tmp_dir, scm, caplog):
|
||||
tmp_dir.scm_gen({"foo": "foo"}, commit="initial")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
assert main(["get", os.fspath(tmp_dir), "foo", "--show-url"]) == 1
|
||||
assert "failed to show URL" in caplog.text
|
||||
assert main(["get", os.fspath(tmp_dir), "foo", "--show-url"]) != 0
|
||||
|
||||
|
||||
def test_get_pipeline_tracked_outs(tmp_dir, dvc, scm, git_dir, run_copy, local_remote):
|
||||
|
||||
@@ -8,7 +8,7 @@ from dvc.annotations import Annotation
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.dvcfile import load_file
|
||||
from dvc.exceptions import DownloadError, PathMissingError
|
||||
from dvc.exceptions import DownloadError
|
||||
from dvc.fs import system
|
||||
from dvc.scm import Git
|
||||
from dvc.stage.exceptions import StagePathNotFoundError
|
||||
@@ -338,7 +338,7 @@ def test_push_wildcard_from_bare_git_repo(
|
||||
with dvc_repo.chdir():
|
||||
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dirextra")
|
||||
|
||||
with pytest.raises(PathMissingError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dir123")
|
||||
|
||||
|
||||
@@ -405,11 +405,11 @@ def test_pull_non_workspace(tmp_dir, scm, dvc, erepo_dir):
|
||||
|
||||
|
||||
def test_import_non_existing(erepo_dir, tmp_dir, dvc):
|
||||
with pytest.raises(PathMissingError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
tmp_dir.dvc.imp(os.fspath(erepo_dir), "invalid_output")
|
||||
|
||||
# https://github.com/iterative/dvc/pull/2837#discussion_r352123053
|
||||
with pytest.raises(PathMissingError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
tmp_dir.dvc.imp(os.fspath(erepo_dir), "/root/", "root")
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_init_quiet_should_not_display_welcome_screen(tmp_dir, scm, caplog):
|
||||
ret = main(["init", "--quiet"])
|
||||
|
||||
assert ret == 0
|
||||
assert caplog.text == ""
|
||||
assert not caplog.text
|
||||
|
||||
|
||||
def test_allow_init_dvc_subdir(tmp_dir, scm, monkeypatch):
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_unlock_lock_failed(tmp_dir, dvc, request, mocker):
|
||||
# repro of a stage) in with. We should see LockError exception here.
|
||||
with lock:
|
||||
lock.unlock()
|
||||
lock_ext.lock() # imitate an exernal process had time to lock it
|
||||
lock_ext.lock() # imitate an external process had time to lock it
|
||||
request.addfinalizer(lock_ext.unlock)
|
||||
with pytest.raises(LockError):
|
||||
lock.lock()
|
||||
|
||||
@@ -5,7 +5,6 @@ from operator import itemgetter
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import CloneError
|
||||
|
||||
@@ -151,7 +150,7 @@ def test_ls_repo_with_path_dir_dvc_only_empty(tmp_dir, dvc, scm):
|
||||
tmp_dir.scm_gen({"empty_scm_folder/": {}}, commit="add scm empty")
|
||||
tmp_dir.dvc_gen({"empty_dvc_folder": {}}, commit="empty dvc folder")
|
||||
|
||||
with pytest.raises(PathMissingError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
Repo.ls(os.fspath(tmp_dir), path="not_exist_folder")
|
||||
|
||||
assert Repo.ls(os.fspath(tmp_dir), path="empty_scm_folder") == []
|
||||
@@ -218,23 +217,21 @@ def test_ls_repo_with_missed_path(tmp_dir, dvc, scm):
|
||||
tmp_dir.scm_gen(FS_STRUCTURE, commit="init")
|
||||
tmp_dir.dvc_gen(DVC_STRUCTURE, commit="dvc")
|
||||
|
||||
with pytest.raises(PathMissingError) as exc_info:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
Repo.ls(os.fspath(tmp_dir), path="missed_path")
|
||||
assert not exc_info.value.dvc_only
|
||||
|
||||
|
||||
def test_ls_repo_with_missed_path_dvc_only(tmp_dir, dvc, scm):
|
||||
tmp_dir.scm_gen(FS_STRUCTURE, commit="init")
|
||||
tmp_dir.dvc_gen(DVC_STRUCTURE, commit="dvc")
|
||||
|
||||
with pytest.raises(PathMissingError) as exc_info:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
Repo.ls(
|
||||
os.fspath(tmp_dir),
|
||||
path="missed_path",
|
||||
recursive=True,
|
||||
dvc_only=True,
|
||||
)
|
||||
assert exc_info.value.dvc_only
|
||||
|
||||
|
||||
def test_ls_repo_with_removed_dvc_dir(tmp_dir, dvc, scm):
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
import os
|
||||
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc.dvcfile import LOCK_FILE, PROJECT_FILE
|
||||
from dvc.fs import system
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git
|
||||
|
||||
|
||||
def test_open_bare(tmp_dir, scm, dvc, tmp_path_factory):
|
||||
tmp_dir.dvc_gen(
|
||||
{
|
||||
"dir123": {"foo": "foo content"},
|
||||
"dirextra": {"extrafoo": "extra foo content"},
|
||||
},
|
||||
commit="initial",
|
||||
)
|
||||
|
||||
url = os.fspath(tmp_path_factory.mktemp("bare"))
|
||||
Git.init(url, bare=True).close()
|
||||
|
||||
scm.gitpython.repo.create_remote("origin", url)
|
||||
scm.gitpython.repo.remote("origin").push("master")
|
||||
|
||||
with Repo.open(url) as repo:
|
||||
assert repo.scm.root_dir != url
|
||||
|
||||
with Repo.open(url, uninitialized=True) as repo:
|
||||
assert repo.scm.root_dir != url
|
||||
|
||||
|
||||
def test_destroy(tmp_dir, dvc, run_copy):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user