mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde456c923 | ||
|
|
cb9d3529af | ||
|
|
876c10fcf5 | ||
|
|
93eea0cd42 | ||
|
|
25fd274090 | ||
|
|
ddfa607437 | ||
|
|
c5a3566e49 | ||
|
|
e7d4f5b24e | ||
|
|
1d2892efac | ||
|
|
83835afa69 | ||
|
|
4c5d052d8c | ||
|
|
e4d514c990 | ||
|
|
baeecd1972 | ||
|
|
dbd098506c | ||
|
|
4779c248f8 | ||
|
|
2cf3a12e46 | ||
|
|
78fb5275e5 | ||
|
|
ccbe2de89e | ||
|
|
54e39e4abd | ||
|
|
59a4c4f67c | ||
|
|
1a7ae16f69 | ||
|
|
286f4fda3e | ||
|
|
d2c63bffcb | ||
|
|
2b41305c51 | ||
|
|
546becddaf | ||
|
|
c6015a05a3 | ||
|
|
c0754ee5b3 | ||
|
|
4db2e89938 | ||
|
|
4a6f3ed64a | ||
|
|
9f0c4350bd | ||
|
|
a91528c3c9 | ||
|
|
b80d84e689 | ||
|
|
2a2c02c184 | ||
|
|
35b648bbcf | ||
|
|
96ac321dd5 | ||
|
|
50fd8ca15d | ||
|
|
184cc35cf0 | ||
|
|
81da91a973 | ||
|
|
ad5b91e51a | ||
|
|
391cf80e7b | ||
|
|
8d13486048 | ||
|
|
37761a2627 | ||
|
|
968a8fde99 | ||
|
|
3043b6588f | ||
|
|
47208533a2 | ||
|
|
09ccd9c7e4 | ||
|
|
cd9dd0a312 | ||
|
|
6fbbbeccc5 | ||
|
|
c34cbf63af | ||
|
|
b76aad8d56 | ||
|
|
68bf095eae | ||
|
|
ea6e01d0e0 | ||
|
|
19038b32bd | ||
|
|
648edd71eb | ||
|
|
0c95cde712 | ||
|
|
e218431f64 | ||
|
|
d3182e0dd9 | ||
|
|
e040e875dc | ||
|
|
303da84b18 | ||
|
|
03bfdf2663 | ||
|
|
8edaef0103 | ||
|
|
fd8a8c2729 | ||
|
|
30337bf0ba | ||
|
|
5426ca6f79 | ||
|
|
a89c25700f | ||
|
|
0e0bb8ad76 | ||
|
|
1de37556d5 | ||
|
|
098685b8f3 | ||
|
|
8f549c8a8d | ||
|
|
e56d97bb28 | ||
|
|
07b72c20e9 | ||
|
|
592f251481 | ||
|
|
890c0020ed | ||
|
|
7ce0b1cba2 | ||
|
|
b8a9c37af5 | ||
|
|
1af63acae5 | ||
|
|
d68599a5f7 | ||
|
|
7ccf35b47e | ||
|
|
4f49023a08 | ||
|
|
d7ea7c358d | ||
|
|
3f0da8f6cc | ||
|
|
b7b7eff4a3 | ||
|
|
ddeb32ed33 | ||
|
|
c7be921f44 | ||
|
|
c95c3f0f7a | ||
|
|
be824ede0d | ||
|
|
cbe46cccd8 | ||
|
|
49b5157abe | ||
|
|
6f87c14bf9 | ||
|
|
0f80d16ba7 | ||
|
|
4b23b4f8ae | ||
|
|
446871d84f | ||
|
|
99bbe15980 | ||
|
|
320771551b | ||
|
|
1676d1f344 | ||
|
|
c8ae2895f0 | ||
|
|
ed1ab7fa75 | ||
|
|
73497dae9e | ||
|
|
eff775c760 | ||
|
|
e73d662686 | ||
|
|
86a6fb104a | ||
|
|
3b097dcccd | ||
|
|
e910dac0dc | ||
|
|
17a71d6011 | ||
|
|
5a6ccc8027 | ||
|
|
6dccfcb3f9 | ||
|
|
2b0c365f35 | ||
|
|
611e2f4311 | ||
|
|
561a375b48 |
@@ -17,9 +17,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 50
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -41,9 +41,9 @@ jobs:
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
- name: run tests
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
run: >
|
||||
pytest -nauto --timeout=300 --durations=100
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
@@ -43,7 +43,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.238'
|
||||
rev: 'v0.0.254'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
|
||||
+2
-1
@@ -3,13 +3,14 @@ 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 .params import params_show
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"metrics_show",
|
||||
"read",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
@@ -4,10 +4,153 @@ from typing import Dict, Iterable, Optional, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def _onerror_raise(exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
|
||||
def _postprocess(results):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in results.items():
|
||||
if not rev_data:
|
||||
continue
|
||||
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = Counter()
|
||||
for file_data in rev_data["data"].values():
|
||||
for k in file_data["data"]:
|
||||
counts[k] += 1
|
||||
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
to_merge = {
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
return processed
|
||||
|
||||
|
||||
def metrics_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Get metrics tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all metrics from all tracked
|
||||
metric files, for the current working tree.
|
||||
|
||||
See the options below to restrict the metrics retrieved.
|
||||
|
||||
Args:
|
||||
*targets (str, optional): Names of the metric files to retrieve
|
||||
metrics from. For example, "classifier_eval.json,
|
||||
clustering_eval.json".
|
||||
If no `targets` are provided, all metric files tracked in `dvc.yaml`
|
||||
will be used.
|
||||
Note that targets don't necessarily have to be defined in `dvc.yaml`.
|
||||
repo (str, optional): Location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve metrics
|
||||
from.
|
||||
Defaults to `None`.
|
||||
An example of git revision can be a branch or tag name, a commit
|
||||
hash or a dvc experiment name.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
If `None`, the current working tree will be used.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show()
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `rev`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(rev="tune-hyperparams")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9268792615819422,
|
||||
"roc_auc": 0.945093365854111
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `targets`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show("evaluation.json")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Git URL as `repo`.
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(
|
||||
... repo="https://github.com/iterative/demo-fashion-mnist")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"loss": 0.25284987688064575,
|
||||
"accuracy": 0.9071000218391418
|
||||
}
|
||||
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
metrics = _repo.metrics.show(
|
||||
targets=targets,
|
||||
revs=rev if rev is None else [rev],
|
||||
onerror=_onerror_raise,
|
||||
)
|
||||
|
||||
metrics = _postprocess(metrics)
|
||||
|
||||
if not metrics:
|
||||
return {}
|
||||
|
||||
return metrics[first(metrics)]
|
||||
|
||||
|
||||
def params_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
@@ -56,9 +199,6 @@ def params_show(
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Raises:
|
||||
DvcException: If no params are found in `repo`.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
@@ -239,34 +379,6 @@ def params_show(
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
def _onerror_raise(_result: Dict, exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
def _postprocess(params):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in params.items():
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = Counter()
|
||||
for file_data in rev_data["data"].values():
|
||||
for k in file_data["data"]:
|
||||
counts[k] += 1
|
||||
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
to_merge = {
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
if not processed:
|
||||
raise DvcException("No params found")
|
||||
|
||||
return processed[first(processed)]
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
@@ -276,4 +388,9 @@ def params_show(
|
||||
stages=stages,
|
||||
)
|
||||
|
||||
return _postprocess(params)
|
||||
params = _postprocess(params)
|
||||
|
||||
if not params:
|
||||
return {}
|
||||
|
||||
return params[first(params)]
|
||||
+9
-3
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
@@ -9,7 +11,6 @@ def _get_odb(repo, settings, fs=None):
|
||||
return None
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
config["tmp_dir"] = repo.tmp_dir
|
||||
fs = fs or cls(**config)
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
@@ -29,16 +30,21 @@ class CacheManager:
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
default = None
|
||||
if repo and repo.local_dvc_dir:
|
||||
default = os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
|
||||
|
||||
local = config.get("local")
|
||||
|
||||
if local:
|
||||
settings = {"name": local}
|
||||
elif "dir" not in config:
|
||||
elif "dir" not in config and not default:
|
||||
settings = None
|
||||
else:
|
||||
from dvc.config_schema import LOCAL_COMMON
|
||||
|
||||
settings = {"url": config["dir"]}
|
||||
url = config.get("dir") or default
|
||||
settings = {"url": url}
|
||||
for opt in LOCAL_COMMON:
|
||||
if opt in config:
|
||||
settings[str(opt)] = config.get(opt)
|
||||
|
||||
+10
-5
@@ -39,12 +39,17 @@ def parse_args(argv=None):
|
||||
|
||||
def _log_unknown_exceptions() -> None:
|
||||
from dvc.info import get_dvc_info
|
||||
from dvc.logger import FOOTER
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
logger.exception("unexpected error")
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Version info for developers:\n%s", get_dvc_info())
|
||||
logger.info(FOOTER)
|
||||
|
||||
q = colorize("Having any troubles?", "yellow")
|
||||
link = colorize("https://dvc.org/support", "blue")
|
||||
footer = f"\n{q} Hit us up at {link}, we are always happy to help!"
|
||||
ui.error_write(footer)
|
||||
|
||||
|
||||
def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
@@ -141,7 +146,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def main(argv=None): # noqa: C901
|
||||
def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
"""Main entry point for dvc CLI.
|
||||
|
||||
Args:
|
||||
@@ -167,7 +172,7 @@ def main(argv=None): # noqa: C901
|
||||
|
||||
args = None
|
||||
|
||||
outerLogLevel = logger.level
|
||||
outer_log_level = logger.level
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
@@ -238,7 +243,7 @@ def main(argv=None): # noqa: C901
|
||||
|
||||
return ret
|
||||
finally:
|
||||
logger.setLevel(outerLogLevel)
|
||||
logger.setLevel(outer_log_level)
|
||||
|
||||
from dvc.external_repo import clean_repos
|
||||
|
||||
|
||||
+11
-2
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
@@ -9,13 +10,21 @@ from dvc.ui import ui
|
||||
class CmdCacheDir(CmdConfig):
|
||||
def run(self):
|
||||
if self.args.value is None and not self.args.unset:
|
||||
from dvc.config import ConfigError
|
||||
|
||||
if self.args.level:
|
||||
conf = self.config.read(level=self.args.level)
|
||||
else:
|
||||
# Use merged config with default values
|
||||
conf = self.config
|
||||
self._check(conf, False, "cache", "dir")
|
||||
ui.write(conf["cache"]["dir"])
|
||||
try:
|
||||
self._check(conf, False, "cache", "dir")
|
||||
path = conf["cache"]["dir"]
|
||||
except ConfigError:
|
||||
if not self.config.dvc_dir or self.args.level:
|
||||
raise
|
||||
path = os.path.join(self.config.dvc_dir, "cache")
|
||||
ui.write(path)
|
||||
return 0
|
||||
with self.config.edit(level=self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
|
||||
+23
-24
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
|
||||
from networkx import DiGraph
|
||||
|
||||
|
||||
def _show_ascii(G: "DiGraph"):
|
||||
def _show_ascii(graph: "DiGraph"):
|
||||
from dvc.dagascii import draw
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
@@ -31,7 +31,7 @@ def _quote_label(node):
|
||||
return label
|
||||
|
||||
|
||||
def _show_dot(G: "DiGraph"):
|
||||
def _show_dot(graph: "DiGraph"):
|
||||
import io
|
||||
|
||||
import networkx as nx
|
||||
@@ -39,15 +39,15 @@ def _show_dot(G: "DiGraph"):
|
||||
|
||||
dot_file = io.StringIO()
|
||||
|
||||
nx.relabel_nodes(G, _quote_label, copy=False)
|
||||
write_dot(G.reverse(), dot_file)
|
||||
nx.relabel_nodes(graph, _quote_label, copy=False)
|
||||
write_dot(graph.reverse(), dot_file)
|
||||
return dot_file.getvalue()
|
||||
|
||||
|
||||
def _show_mermaid(G, markdown: bool = False):
|
||||
def _show_mermaid(graph, markdown: bool = False):
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
graph = "flowchart TD"
|
||||
|
||||
@@ -100,43 +100,42 @@ def _transform(index, outs):
|
||||
def _relabel(node) -> str:
|
||||
return node.addressing if isinstance(node, Stage) else str(node)
|
||||
|
||||
G = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(G, _relabel, copy=True)
|
||||
graph = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(graph, _relabel, copy=True)
|
||||
|
||||
|
||||
def _filter(G, targets, full):
|
||||
def _filter(graph, targets, full):
|
||||
import networkx as nx
|
||||
|
||||
if not targets:
|
||||
return G
|
||||
return graph
|
||||
|
||||
H = G.copy()
|
||||
new_graph = graph.copy()
|
||||
if not full:
|
||||
descendants = set()
|
||||
for target in targets:
|
||||
descendants.update(nx.descendants(G, target))
|
||||
descendants.update(nx.descendants(graph, target))
|
||||
descendants.add(target)
|
||||
H.remove_nodes_from(set(G.nodes()) - descendants)
|
||||
new_graph.remove_nodes_from(set(graph.nodes()) - descendants)
|
||||
|
||||
undirected = H.to_undirected()
|
||||
undirected = new_graph.to_undirected()
|
||||
connected = set()
|
||||
for target in targets:
|
||||
connected.update(nx.node_connected_component(undirected, target))
|
||||
|
||||
H.remove_nodes_from(set(H.nodes()) - connected)
|
||||
|
||||
return H
|
||||
new_graph.remove_nodes_from(set(new_graph.nodes()) - connected)
|
||||
return new_graph
|
||||
|
||||
|
||||
def _build(repo, target=None, full=False, outs=False):
|
||||
targets = _collect_targets(repo, target, outs)
|
||||
G = _transform(repo.index, outs)
|
||||
return _filter(G, targets, full)
|
||||
graph = _transform(repo.index, outs)
|
||||
return _filter(graph, targets, full)
|
||||
|
||||
|
||||
class CmdDAG(CmdBase):
|
||||
def run(self):
|
||||
G = _build(
|
||||
graph = _build(
|
||||
self.repo,
|
||||
target=self.args.target,
|
||||
full=self.args.full,
|
||||
@@ -144,12 +143,12 @@ class CmdDAG(CmdBase):
|
||||
)
|
||||
|
||||
if self.args.dot:
|
||||
ui.write(_show_dot(G))
|
||||
ui.write(_show_dot(graph))
|
||||
elif self.args.mermaid or self.args.markdown:
|
||||
ui.write(_show_mermaid(G, self.args.markdown))
|
||||
ui.write(_show_mermaid(graph, self.args.markdown))
|
||||
else:
|
||||
with ui.pager():
|
||||
ui.write(_show_ascii(G))
|
||||
ui.write(_show_ascii(graph))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CmdDataStatus(CmdBase):
|
||||
COLORS = {
|
||||
"not_in_remote": "red",
|
||||
"not_in_cache": "red",
|
||||
"committed": "green",
|
||||
"uncommitted": "yellow",
|
||||
"untracked": "cyan",
|
||||
}
|
||||
LABELS = {
|
||||
"not_in_remote": "Not in remote",
|
||||
"not_in_cache": "Not in cache",
|
||||
"committed": "DVC committed changes",
|
||||
"uncommitted": "DVC uncommitted changes",
|
||||
@@ -34,6 +36,7 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_remote": ('use "dvc push <file>..." to upload files',),
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": ("git commit the corresponding dvc files to update the repo",),
|
||||
"uncommitted": (
|
||||
@@ -114,6 +117,7 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -239,6 +243,12 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--remote-refresh",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Refresh remote index.",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
|
||||
@@ -40,6 +40,7 @@ class CmdDataPull(CmdDataBase):
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
glob=self.args.glob,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
@@ -197,6 +198,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Ignore errors if some of the files or directories are missing.",
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
# Push
|
||||
|
||||
@@ -4,6 +4,7 @@ from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_
|
||||
from dvc.commands.experiments import (
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
@@ -21,6 +22,7 @@ from dvc.commands.experiments import (
|
||||
SUB_COMMANDS = [
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
|
||||
@@ -4,13 +4,21 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsApply(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.apply(self.args.experiment, force=self.args.force)
|
||||
if not self.args.force:
|
||||
ui.write(
|
||||
"The --no-force option is deprecated and will be removed in a future"
|
||||
" DVC release. To revert the result of 'exp apply', run:\n"
|
||||
"\n\tgit reset --hard\n"
|
||||
"\tgit stash apply refs/exps/apply/stash\n"
|
||||
)
|
||||
self.repo.experiments.apply(self.args.experiment)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsClean(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.clean()
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_CLEAN_HELP = "Cleanup dvc exp internal tempfiles."
|
||||
experiments_clean_parser = experiments_subparsers.add_parser(
|
||||
"clean",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_CLEAN_HELP, "exp/clean"),
|
||||
help=EXPERIMENTS_CLEAN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_clean_parser.set_defaults(func=CmdExperimentsClean)
|
||||
@@ -9,7 +9,9 @@ class CmdQueueWorker(CmdBase):
|
||||
"""Run the exp queue worker."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.worker.start(self.args.name)
|
||||
self.repo.experiments.celery_queue.worker.start(
|
||||
self.args.name, fsapp_clean=self.args.clean
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -22,4 +24,9 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
add_help=False,
|
||||
)
|
||||
parser.add_argument("name", help="Celery worker name.")
|
||||
parser.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
help="Automatically cleanup celery broker on shutdown.",
|
||||
)
|
||||
parser.set_defaults(func=CmdQueueWorker)
|
||||
|
||||
@@ -62,7 +62,7 @@ experiment_types = {
|
||||
}
|
||||
|
||||
|
||||
def _collect_rows( # noqa: C901
|
||||
def _collect_rows( # noqa: C901, PLR0912, PLR0913, PLR0915
|
||||
base_rev,
|
||||
experiments,
|
||||
all_headers,
|
||||
@@ -244,7 +244,7 @@ def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
|
||||
row[f"{fname}:{name}"] = value
|
||||
|
||||
|
||||
def experiments_table(
|
||||
def experiments_table( # noqa: PLR0913
|
||||
all_experiments,
|
||||
headers,
|
||||
metric_headers,
|
||||
@@ -308,7 +308,7 @@ def baseline_styler(typ):
|
||||
return {"style": "bold"} if typ == "baseline" else {}
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901
|
||||
def show_experiments( # noqa: C901, PLR0912
|
||||
all_experiments,
|
||||
keep=None,
|
||||
drop=None,
|
||||
@@ -453,8 +453,12 @@ def _normalize_headers(names, count):
|
||||
|
||||
|
||||
def _format_json(item):
|
||||
from dvc.repo.experiments.show import _CachedError
|
||||
|
||||
if isinstance(item, (date, datetime)):
|
||||
return item.isoformat()
|
||||
if isinstance(item, _CachedError):
|
||||
return {"type": getattr(item, "typ", "_CachedError"), "msg": str(item)}
|
||||
return encode_exception(item)
|
||||
|
||||
|
||||
@@ -472,6 +476,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
sha_only=self.args.sha,
|
||||
param_deps=self.args.param_deps,
|
||||
fetch_running=self.args.fetch_running,
|
||||
force=self.args.force,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show experiments")
|
||||
@@ -650,4 +655,10 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force re-collection of experiments instead of loading from exp cache.",
|
||||
)
|
||||
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
|
||||
|
||||
@@ -302,7 +302,7 @@ class CmdMachineSsh(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
def add_parser(subparsers, parent_parser): # noqa: PLR0915
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
machine_HELP = "Set up and manage cloud machines."
|
||||
|
||||
+2
-13
@@ -76,10 +76,6 @@ class CmdPlots(CmdBase):
|
||||
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
||||
return {k: v for k, v in props.items() if v is not None}
|
||||
|
||||
def _config_files(self):
|
||||
if self.args.from_config:
|
||||
return {self.args.from_config}
|
||||
|
||||
def _html_template_path(self):
|
||||
html_template_path = self.args.html_template
|
||||
if not html_template_path:
|
||||
@@ -91,7 +87,7 @@ class CmdPlots(CmdBase):
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path # noqa: RET504
|
||||
|
||||
def run(self) -> int: # noqa: C901
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
@@ -114,7 +110,6 @@ class CmdPlots(CmdBase):
|
||||
plots_data = self._func(
|
||||
targets=self.args.targets,
|
||||
props=self._props(),
|
||||
config_files=self._config_files(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
@@ -150,7 +145,7 @@ class CmdPlots(CmdBase):
|
||||
render_html(
|
||||
renderers=renderers,
|
||||
output_file=output_file,
|
||||
template_path=self._html_template_path(),
|
||||
html_template=self._html_template_path(),
|
||||
)
|
||||
|
||||
ui.write(output_file.as_uri())
|
||||
@@ -412,9 +407,3 @@ def _add_ui_arguments(parser):
|
||||
help="Custom HTML template for VEGA visualization.",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--from-config",
|
||||
default=None,
|
||||
metavar="<path>",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
@@ -31,8 +31,7 @@ class CmdUpdate(CmdBase):
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UPDATE_HELP = (
|
||||
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||
"from an external DVC repository or URL, or update data from a "
|
||||
"DVC worktree remote."
|
||||
"from an external DVC repository or URL."
|
||||
)
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
|
||||
+2
-2
@@ -209,7 +209,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
keys = self.keys() if cols is None else set(cols)
|
||||
return [{k: self._columns[k][i] for k in keys} for i in range(len(self))]
|
||||
|
||||
def dropna( # noqa: C901
|
||||
def dropna( # noqa: C901, PLR0912
|
||||
self,
|
||||
axis: str = "rows",
|
||||
how="any",
|
||||
@@ -371,7 +371,7 @@ def diff_table(
|
||||
return td
|
||||
|
||||
|
||||
def show_diff(
|
||||
def show_diff( # noqa: PLR0913
|
||||
diff,
|
||||
title: str,
|
||||
old: bool = True,
|
||||
|
||||
@@ -170,10 +170,6 @@ class Config(dict):
|
||||
self.clear()
|
||||
self.update(conf)
|
||||
|
||||
# Add resolved default cache.dir
|
||||
if not self["cache"].get("dir") and self.dvc_dir:
|
||||
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
|
||||
|
||||
def _get_fs(self, level):
|
||||
# NOTE: this might be a Gitfs, which doesn't see things outside of
|
||||
# the repo.
|
||||
|
||||
@@ -30,7 +30,7 @@ def supported_cache_type(types):
|
||||
return types
|
||||
|
||||
|
||||
def Choices(*choices):
|
||||
def Choices(*choices): # noqa: N802
|
||||
"""Checks that value belongs to the specified set of values
|
||||
|
||||
Args:
|
||||
@@ -40,7 +40,7 @@ def Choices(*choices):
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
def ByUrl(mapping):
|
||||
def ByUrl(mapping): # noqa: N802
|
||||
schemas = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
@@ -51,6 +51,8 @@ def ByUrl(mapping):
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
|
||||
return schemas[""](data)
|
||||
if parsed.netloc == "":
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
|
||||
@@ -263,6 +265,7 @@ SCHEMA = {
|
||||
"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,
|
||||
},
|
||||
"plots": {
|
||||
|
||||
+3
-3
@@ -80,7 +80,7 @@ class AsciiCanvas:
|
||||
|
||||
self.canvas[y][x] = char
|
||||
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901, PLR0912
|
||||
"""Create a line on ASCII canvas.
|
||||
|
||||
Args:
|
||||
@@ -213,8 +213,8 @@ def draw(vertices, edges):
|
||||
# pylint: disable=too-many-locals
|
||||
# NOTE: coordinates might me negative, so we need to shift
|
||||
# everything to the positive plane before we actually draw it.
|
||||
Xs = [] # pylint: disable=invalid-name
|
||||
Ys = [] # pylint: disable=invalid-name
|
||||
Xs = [] # noqa: N806, pylint: disable=invalid-name
|
||||
Ys = [] # noqa: N806, pylint: disable=invalid-name
|
||||
|
||||
sug = _build_sugiyama_layout(vertices, edges)
|
||||
|
||||
|
||||
+8
-3
@@ -18,10 +18,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", **config):
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", *, index=None, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
self.name = name
|
||||
self.index = index
|
||||
|
||||
self.worktree: bool = config.pop("worktree", False)
|
||||
self.config = config
|
||||
@@ -34,7 +35,7 @@ class Remote:
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
return get_odb(self.fs, path, hash_name="md5", **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
@@ -75,7 +76,11 @@ class DataCloud:
|
||||
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(name, fs_path, fs, **config)
|
||||
if self.repo.data_index is not None:
|
||||
index = self.repo.data_index.view(("remote", name))
|
||||
else:
|
||||
index = None
|
||||
return Remote(name, fs_path, fs, index=index, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
|
||||
@@ -2,9 +2,9 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Dict, cast
|
||||
from typing import Dict
|
||||
|
||||
import dpath.util
|
||||
import dpath
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -87,17 +87,17 @@ class ParamsDependency(Dependency):
|
||||
if flatten:
|
||||
for param in self.params:
|
||||
try:
|
||||
ret[param] = dpath.util.get(config, param, separator=".")
|
||||
ret[param] = dpath.get(config, param, separator=".")
|
||||
except KeyError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath.util import merge
|
||||
from dpath import merge
|
||||
|
||||
for param in self.params:
|
||||
merge(
|
||||
ret,
|
||||
dpath.util.search(config, param, separator="."),
|
||||
dpath.search(config, param, separator="."),
|
||||
separator=".",
|
||||
)
|
||||
return ret
|
||||
@@ -111,7 +111,8 @@ class ParamsDependency(Dependency):
|
||||
from funcy import ldistinct
|
||||
|
||||
status: Dict[str, Any] = defaultdict(dict)
|
||||
info = cast(dict, self.hash_info.value) if self.hash_info else {}
|
||||
assert isinstance(self.hash_info.value, dict)
|
||||
info = self.hash_info.value if self.hash_info else {}
|
||||
actual = self.read_params()
|
||||
|
||||
# NOTE: we want to preserve the order of params as specified in the
|
||||
|
||||
@@ -133,7 +133,7 @@ class RepoDependency(Dependency):
|
||||
self._check_circular_import(odb, obj_ids)
|
||||
used_obj_ids[odb].update(obj_ids)
|
||||
except (NoRemoteError, NoOutputOrStageError):
|
||||
pass # noqa: S110
|
||||
pass
|
||||
|
||||
try:
|
||||
object_store, meta, obj = build(
|
||||
|
||||
+6
-7
@@ -187,7 +187,7 @@ class FileMixin:
|
||||
|
||||
class SingleStageFile(FileMixin):
|
||||
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
||||
|
||||
metrics: List[str] = []
|
||||
plots: Any = {}
|
||||
@@ -231,7 +231,7 @@ class ProjectFile(FileMixin):
|
||||
"""Abstraction for pipelines file, .yaml + .lock combined."""
|
||||
|
||||
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import StageLoader as LOADER
|
||||
from dvc.stage.loader import StageLoader as LOADER # noqa: N814
|
||||
|
||||
@property
|
||||
def _lockfile(self):
|
||||
@@ -409,11 +409,10 @@ class Lockfile(FileMixin):
|
||||
if version == LOCKFILE_VERSION.V1:
|
||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
||||
else:
|
||||
if not data:
|
||||
data.update(self.latest_version_info)
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
elif not data:
|
||||
data.update(self.latest_version_info)
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
|
||||
@@ -189,7 +189,7 @@ def _cached_clone(url, rev, for_write=False):
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _clone_default_branch(url, rev, for_write=False): # noqa: C901
|
||||
def _clone_default_branch(url, rev, for_write=False): # noqa: C901, PLR0912
|
||||
"""Get or create a clean clone of the url.
|
||||
|
||||
The cloned is reactualized with git pull unless rev is a known sha.
|
||||
|
||||
+1
-1
@@ -6,7 +6,6 @@ from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import utils # noqa: F401
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
@@ -17,6 +16,7 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
utils,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
from dvc_objects.fs.errors import ( # noqa: F401
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# pylint: disable=unused-import
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||
@@ -54,7 +54,8 @@ class RichCallback(Callback):
|
||||
disable=self.disable,
|
||||
console=ui.error_console,
|
||||
)
|
||||
return cast(RichTransferProgress, self._stack.enter_context(progress))
|
||||
self._stack.enter_context(progress)
|
||||
return progress
|
||||
|
||||
@cached_property
|
||||
def task(self) -> "TaskID":
|
||||
|
||||
+17
-2
@@ -289,7 +289,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
):
|
||||
fs_infos[fs.path.name(info["name"])] = info
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass # noqa: S110
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
@@ -316,7 +316,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True): # noqa: C901
|
||||
def _info( # noqa: C901, PLR0912
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
@@ -359,6 +361,19 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def get_file(self, rpath, lpath, **kwargs): # pylint: disable=arguments-differ
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
|
||||
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
|
||||
@@ -19,7 +19,6 @@ SUBPROJECTS = (
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f" ({PKG})"
|
||||
|
||||
+24
-34
@@ -9,57 +9,47 @@ import colorama
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
FOOTER = (
|
||||
"\n{yellow}Having any troubles?{nc}"
|
||||
" Hit us up at {blue}https://dvc.org/support{nc},"
|
||||
" we are always happy to help!"
|
||||
).format(
|
||||
blue=colorama.Fore.BLUE,
|
||||
nc=colorama.Fore.RESET,
|
||||
yellow=colorama.Fore.YELLOW,
|
||||
)
|
||||
|
||||
|
||||
def addLoggingLevel(levelName, levelNum, methodName=None):
|
||||
def add_logging_level(level_name, level_num, method_name=None):
|
||||
"""
|
||||
Adds a new logging level to the `logging` module and the
|
||||
currently configured logging class.
|
||||
|
||||
Uses the existing numeric levelNum if already defined.
|
||||
Uses the existing numeric level_num if already defined.
|
||||
|
||||
Based on https://stackoverflow.com/questions/2183233
|
||||
"""
|
||||
if methodName is None:
|
||||
methodName = levelName.lower()
|
||||
if method_name is None:
|
||||
method_name = level_name.lower()
|
||||
|
||||
# If the level name is already defined as a top-level `logging`
|
||||
# constant, then adopt the existing numeric level.
|
||||
if hasattr(logging, levelName):
|
||||
existingLevelNum = getattr(logging, levelName)
|
||||
assert isinstance(existingLevelNum, int)
|
||||
levelNum = existingLevelNum
|
||||
if hasattr(logging, level_name):
|
||||
existing_level_num = getattr(logging, level_name)
|
||||
assert isinstance(existing_level_num, int)
|
||||
level_num = existing_level_num
|
||||
|
||||
def logForLevel(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(levelNum):
|
||||
def log_for_level(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(level_num):
|
||||
# pylint: disable=protected-access
|
||||
self._log(levelNum, message, args, **kwargs)
|
||||
self._log(level_num, message, args, **kwargs)
|
||||
|
||||
def logToRoot(message, *args, **kwargs):
|
||||
logging.log(levelNum, message, *args, **kwargs)
|
||||
def log_to_root(message, *args, **kwargs):
|
||||
logging.log(level_num, message, *args, **kwargs)
|
||||
|
||||
# getLevelName resolves the numeric log level if already defined,
|
||||
# otherwise returns a string
|
||||
if not isinstance(logging.getLevelName(levelName), int):
|
||||
logging.addLevelName(levelNum, levelName)
|
||||
if not isinstance(logging.getLevelName(level_name), int):
|
||||
logging.addLevelName(level_num, level_name)
|
||||
|
||||
if not hasattr(logging, levelName):
|
||||
setattr(logging, levelName, levelNum)
|
||||
if not hasattr(logging, level_name):
|
||||
setattr(logging, level_name, level_num)
|
||||
|
||||
if not hasattr(logging.getLoggerClass(), methodName):
|
||||
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
||||
if not hasattr(logging.getLoggerClass(), method_name):
|
||||
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
||||
|
||||
if not hasattr(logging, methodName):
|
||||
setattr(logging, methodName, logToRoot)
|
||||
if not hasattr(logging, method_name):
|
||||
setattr(logging, method_name, log_to_root)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
@@ -139,7 +129,7 @@ class ColorFormatter(logging.Formatter):
|
||||
|
||||
|
||||
class LoggerHandler(logging.StreamHandler):
|
||||
def handleError(self, record):
|
||||
def handleError(self, record): # noqa: N802
|
||||
super().handleError(record)
|
||||
raise LoggingException(record)
|
||||
|
||||
@@ -157,7 +147,7 @@ class LoggerHandler(logging.StreamHandler):
|
||||
if not _is_verbose():
|
||||
return
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
pass # noqa: S110
|
||||
pass
|
||||
|
||||
msg = self.format(record)
|
||||
Tqdm.write(msg, file=self.stream, end=getattr(self, "terminator", "\n"))
|
||||
@@ -200,7 +190,7 @@ def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||
console_debug.setFormatter(formatter)
|
||||
console_debug.addFilter(exclude_filter(logging.INFO))
|
||||
|
||||
addLoggingLevel("TRACE", logging.DEBUG - 5)
|
||||
add_logging_level("TRACE", logging.DEBUG - 5)
|
||||
|
||||
console_trace = LoggerHandler(sys.stdout)
|
||||
console_trace.setLevel(logging.TRACE) # type: ignore[attr-defined]
|
||||
|
||||
+32
-27
@@ -3,7 +3,7 @@ import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, cast
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, first, project
|
||||
@@ -20,7 +20,6 @@ from dvc.exceptions import (
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
@@ -29,6 +28,7 @@ from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
@@ -312,7 +312,7 @@ class Output:
|
||||
IsStageFileError: Type[DvcException] = OutputIsStageFileError
|
||||
IsIgnoredError: Type[DvcException] = OutputIsIgnoredError
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
stage,
|
||||
path,
|
||||
@@ -709,7 +709,7 @@ class Output:
|
||||
except _CheckoutError as exc:
|
||||
raise CheckoutError(exc.paths, {}) # noqa: B904
|
||||
|
||||
def commit(self, filter_info=None) -> None:
|
||||
def commit(self, filter_info=None, relink=True) -> None:
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self)
|
||||
|
||||
@@ -719,8 +719,11 @@ class Output:
|
||||
granular = (
|
||||
self.is_dir_checksum and filter_info and filter_info != self.fs_path
|
||||
)
|
||||
# NOTE: trying to use hardlink during transfer only if we will be
|
||||
# relinking later
|
||||
hardlink = relink
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info)
|
||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
||||
else:
|
||||
staging, _, obj = build(
|
||||
self.cache,
|
||||
@@ -734,41 +737,43 @@ class Output:
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
hardlink=True,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
self.set_exec()
|
||||
if relink:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info) -> Optional["HashFile"]:
|
||||
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
staging, _, save_obj = build(
|
||||
staging, _, obj = build(
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = cast(Tree, save_obj)
|
||||
save_obj = cast(Tree, save_obj.filter(prefix))
|
||||
assert isinstance(obj, Tree)
|
||||
save_obj = obj.filter(prefix)
|
||||
assert isinstance(save_obj, Tree)
|
||||
checkout_obj = save_obj.get_obj(self.cache, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
hardlink=True,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self, **kwargs): # noqa: C901
|
||||
def dumpd(self, **kwargs): # noqa: C901, PLR0912
|
||||
ret: Dict[str, Any] = {}
|
||||
with_files = (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
@@ -822,7 +827,7 @@ class Output:
|
||||
if with_files:
|
||||
obj = self.obj or self.get_obj()
|
||||
if obj:
|
||||
obj = cast("Tree", obj)
|
||||
assert isinstance(obj, Tree)
|
||||
ret[self.PARAM_FILES] = [
|
||||
split_file_meta_from_cloud(f)
|
||||
for f in _serialize_tree_obj_to_files(obj)
|
||||
@@ -870,7 +875,7 @@ class Output:
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
obj = cast(Tree, obj)
|
||||
assert isinstance(obj, Tree)
|
||||
obj = obj.get_obj(self.cache, prefix)
|
||||
|
||||
return obj
|
||||
@@ -1052,14 +1057,14 @@ class Output:
|
||||
return None
|
||||
|
||||
obj = self.get_obj()
|
||||
assert obj is None or isinstance(obj, Tree)
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
obj = cast(Tree, obj)
|
||||
return obj.filter(prefix)
|
||||
return cast(Tree, obj)
|
||||
return obj
|
||||
|
||||
def get_used_objs( # noqa: C901
|
||||
def get_used_objs( # noqa: C901, PLR0911
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
"""Return filtered set of used object IDs for this out."""
|
||||
|
||||
@@ -155,7 +155,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
class Container(Node, ABC):
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
|
||||
@@ -102,9 +102,8 @@ def _(obj: dict): # noqa: C901
|
||||
if isinstance(v, bool):
|
||||
if v:
|
||||
result += f"--{k} "
|
||||
else:
|
||||
if config.get("bool", "store_true") == "boolean_optional":
|
||||
result += f"--no-{k} "
|
||||
elif config.get("bool", "store_true") == "boolean_optional":
|
||||
result += f"--no-{k} "
|
||||
|
||||
elif isinstance(v, str):
|
||||
result += f"--{k} {escape_str(v)} "
|
||||
|
||||
@@ -19,7 +19,7 @@ class VersionEnum(str, enum.Enum):
|
||||
return [v.value for v in cls]
|
||||
|
||||
|
||||
class LOCKFILE_VERSION(VersionEnum):
|
||||
class LOCKFILE_VERSION(VersionEnum): # noqa: N801
|
||||
V1 = "1.0"
|
||||
V2 = "2.0"
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Tqdm(tqdm):
|
||||
"miniters": 1,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
iterable=None,
|
||||
disable=None,
|
||||
|
||||
@@ -29,10 +29,8 @@ def _group_by_rev(datapoints):
|
||||
|
||||
|
||||
def to_json(renderer, split: bool = False) -> List[Dict]:
|
||||
from copy import deepcopy
|
||||
|
||||
if renderer.TYPE == "vega":
|
||||
grouped = _group_by_rev(deepcopy(renderer.datapoints))
|
||||
grouped = _group_by_rev(renderer.datapoints)
|
||||
if split:
|
||||
content = renderer.get_filled_template(skip_anchors=["data"])
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from funcy import first, last
|
||||
@@ -194,7 +194,7 @@ class VegaConverter(Converter):
|
||||
return first(fields)
|
||||
return "x"
|
||||
|
||||
def flat_datapoints(self, revision): # noqa: C901
|
||||
def flat_datapoints(self, revision): # noqa: C901, PLR0912
|
||||
file2datapoints, properties = self.convert()
|
||||
|
||||
props_update = {}
|
||||
@@ -222,7 +222,13 @@ class VegaConverter(Converter):
|
||||
)
|
||||
|
||||
all_datapoints = []
|
||||
all_y_fields = {y_field for _, y_field in ys}
|
||||
if ys:
|
||||
all_y_files, all_y_fields = list(zip(*ys))
|
||||
all_y_fields = set(all_y_fields)
|
||||
all_y_files = set(all_y_files)
|
||||
else:
|
||||
all_y_files = set()
|
||||
all_y_fields = set()
|
||||
|
||||
# override to unified y field name if there are different y fields
|
||||
if len(all_y_fields) > 1:
|
||||
@@ -230,10 +236,16 @@ class VegaConverter(Converter):
|
||||
else:
|
||||
props_update["y"] = first(all_y_fields)
|
||||
|
||||
# get common prefix to drop from file names
|
||||
if len(all_y_files) > 1:
|
||||
common_prefix_len = len(os.path.commonpath(all_y_files))
|
||||
else:
|
||||
common_prefix_len = 0
|
||||
|
||||
for i, (y_file, y_field) in enumerate(ys):
|
||||
if num_xs > 1:
|
||||
x_file, x_field = xs[i]
|
||||
datapoints = deepcopy(file2datapoints.get(y_file, []))
|
||||
datapoints = [d.copy() for d in file2datapoints.get(y_file, [])]
|
||||
|
||||
if props_update.get("y", None) == "dvc_inferred_y_value":
|
||||
_update_from_field(
|
||||
@@ -259,12 +271,13 @@ class VegaConverter(Converter):
|
||||
"They have to have same length."
|
||||
)
|
||||
|
||||
y_file_short = y_file[common_prefix_len:].strip("/\\")
|
||||
_update_all(
|
||||
datapoints,
|
||||
update_dict={
|
||||
VERSION_FIELD: {
|
||||
"revision": revision,
|
||||
FILENAME_FIELD: y_file,
|
||||
FILENAME_FIELD: y_file_short,
|
||||
"field": y_field,
|
||||
}
|
||||
},
|
||||
|
||||
+10
-6
@@ -1,11 +1,12 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
import dpath
|
||||
import dpath.options
|
||||
import dpath.util
|
||||
from funcy import last
|
||||
|
||||
from dvc.repo.plots import infer_data_sources
|
||||
from dvc.repo.plots import _normpath, infer_data_sources
|
||||
from dvc.utils.plots import get_plot_id
|
||||
|
||||
from .convert import _get_converter
|
||||
@@ -43,17 +44,20 @@ class PlotsData:
|
||||
|
||||
def get_definition_data(self, target_files, rev):
|
||||
result = {}
|
||||
for file in target_files:
|
||||
for definition_file in target_files:
|
||||
if os.name == "nt":
|
||||
source_file = _normpath(definition_file).replace("\\", "/")
|
||||
else:
|
||||
source_file = definition_file
|
||||
file_content = (
|
||||
self.data.get(rev, {})
|
||||
.get("sources", {})
|
||||
.get("data", {})
|
||||
.get(file, {})
|
||||
.get(source_file, {})
|
||||
.get("data", {})
|
||||
)
|
||||
if file_content:
|
||||
result[file] = file_content
|
||||
|
||||
result[definition_file] = file_content
|
||||
return result
|
||||
|
||||
|
||||
|
||||
+86
-27
@@ -3,7 +3,7 @@ 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, cast
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from dvc.exceptions import FileMissingError
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
@@ -14,7 +14,7 @@ from dvc.utils.fs import path_isin
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem, GitFileSystem
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.lock import LockBase
|
||||
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import DictStrAny
|
||||
from dvc_data.hashfile.state import StateBase
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
from .experiments import Experiments
|
||||
from .index import Index
|
||||
@@ -99,17 +100,15 @@ class Repo:
|
||||
fs: Optional["FileSystem"] = None,
|
||||
uninitialized: bool = False,
|
||||
scm: Optional[Union["Git", "NoSCM"]] = None,
|
||||
) -> Tuple[str, Optional[str], Optional[str]]:
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
from dvc.fs import localfs
|
||||
from dvc.scm import SCM, SCMError
|
||||
|
||||
dvc_dir: Optional[str] = None
|
||||
tmp_dir: Optional[str] = None
|
||||
try:
|
||||
root_dir = self.find_root(root_dir, fs)
|
||||
fs = fs or localfs
|
||||
dvc_dir = fs.path.join(root_dir, self.DVC_DIR)
|
||||
tmp_dir = fs.path.join(dvc_dir, "tmp")
|
||||
except NotDvcRepoError:
|
||||
if not uninitialized:
|
||||
raise
|
||||
@@ -124,9 +123,11 @@ class Repo:
|
||||
root_dir = scm.root_dir
|
||||
|
||||
assert root_dir
|
||||
return root_dir, dvc_dir, tmp_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
|
||||
@@ -139,18 +140,24 @@ class Repo:
|
||||
|
||||
import hashlib
|
||||
|
||||
root_dir_hash = hashlib.sha224(self.root_dir.encode("utf-8")).hexdigest()
|
||||
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 = self.fs.path.join(
|
||||
db_dir = fs.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{self.fs.path.name(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
f"{fs.path.name(local_root)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
self.fs.makedirs(db_dir, exist_ok=True)
|
||||
fs.makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0915
|
||||
self,
|
||||
root_dir: Optional[str] = None,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
@@ -179,6 +186,7 @@ class Repo:
|
||||
self._fs_conf = {"repo_factory": repo_factory}
|
||||
self._fs = fs or localfs
|
||||
self._scm = scm
|
||||
self._data_index = None
|
||||
|
||||
if rev and not fs:
|
||||
self._scm = scm = SCM(root_dir or os.curdir)
|
||||
@@ -187,8 +195,10 @@ class Repo:
|
||||
|
||||
self.root_dir: str
|
||||
self.dvc_dir: Optional[str]
|
||||
self.tmp_dir: Optional[str]
|
||||
self.root_dir, self.dvc_dir, self.tmp_dir = self._get_repo_dirs(
|
||||
(
|
||||
self.root_dir,
|
||||
self.dvc_dir,
|
||||
) = self._get_repo_dirs(
|
||||
root_dir=root_dir,
|
||||
fs=self.fs,
|
||||
uninitialized=uninitialized,
|
||||
@@ -211,11 +221,11 @@ class Repo:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
self.cache = CacheManager(self)
|
||||
self.tmp_dir = None
|
||||
else:
|
||||
self.fs.makedirs(cast(str, self.tmp_dir), exist_ok=True)
|
||||
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
assert self.tmp_dir
|
||||
self.fs.makedirs(self.tmp_dir, exist_ok=True)
|
||||
|
||||
self.lock = make_lock(
|
||||
self.fs.path.join(self.tmp_dir, "lock"),
|
||||
tmp_dir=self.tmp_dir,
|
||||
@@ -246,6 +256,41 @@ class Repo:
|
||||
def __str__(self):
|
||||
return self.url or self.root_dir
|
||||
|
||||
@cached_property
|
||||
def local_dvc_dir(self):
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if not self.dvc_dir:
|
||||
return None
|
||||
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
return self.dvc_dir
|
||||
|
||||
if not isinstance(self.fs, GitFileSystem):
|
||||
return None
|
||||
|
||||
relparts = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
|
||||
dvc_dir = os.path.join(
|
||||
self.scm.root_dir,
|
||||
*relparts,
|
||||
self.DVC_DIR,
|
||||
)
|
||||
if os.path.exists(dvc_dir):
|
||||
return dvc_dir
|
||||
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def tmp_dir(self):
|
||||
if self.local_dvc_dir is None:
|
||||
return None
|
||||
|
||||
return os.path.join(self.local_dvc_dir, "tmp")
|
||||
|
||||
@cached_property
|
||||
def index(self) -> "Index":
|
||||
from dvc.repo.index import Index
|
||||
@@ -304,7 +349,7 @@ class Repo:
|
||||
return DvcIgnoreFilter(self.fs, self.root_dir)
|
||||
|
||||
def get_rev(self):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
assert self.scm
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
@@ -312,7 +357,8 @@ class Repo:
|
||||
|
||||
with map_scm_exception():
|
||||
return self.scm.get_rev()
|
||||
return cast("GitFileSystem", self.fs).rev
|
||||
assert isinstance(self.fs, GitFileSystem)
|
||||
return self.fs.rev
|
||||
|
||||
@cached_property
|
||||
def experiments(self) -> "Experiments":
|
||||
@@ -341,6 +387,21 @@ class Repo:
|
||||
# fs.
|
||||
self._reset()
|
||||
|
||||
@property
|
||||
def data_index(self) -> Optional["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")
|
||||
os.makedirs(index_dir, exist_ok=True)
|
||||
|
||||
self._data_index = DataIndex.open(os.path.join(index_dir, "db.db"))
|
||||
|
||||
return self._data_index
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}: '{self.root_dir}'"
|
||||
|
||||
@@ -402,7 +463,7 @@ class Repo:
|
||||
|
||||
return brancher(self, *args, **kwargs)
|
||||
|
||||
def used_objs(
|
||||
def used_objs( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
@@ -553,21 +614,19 @@ class Repo:
|
||||
def close(self):
|
||||
self.scm.close()
|
||||
self.state.close()
|
||||
if self._data_index is not None:
|
||||
self._data_index.close()
|
||||
|
||||
def _reset_cached_indecies(self):
|
||||
def _reset(self):
|
||||
self.scm._reset() # pylint: disable=protected-access
|
||||
self.state.close()
|
||||
self.__dict__.pop("index", None)
|
||||
self.__dict__.pop("dvcignore", None)
|
||||
self.__dict__.pop("dvcfs", None)
|
||||
self.__dict__.pop("datafs", None)
|
||||
|
||||
def _reset(self):
|
||||
self.state.close()
|
||||
self.scm._reset() # pylint: disable=protected-access
|
||||
self._reset_cached_indecies()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self._reset()
|
||||
self.scm.close()
|
||||
self.close()
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ VALIDATORS = (
|
||||
@validate(*VALIDATORS)
|
||||
@locked
|
||||
@scm_context
|
||||
def add( # noqa: C901
|
||||
def add(
|
||||
repo: "Repo",
|
||||
targets: "TargetType",
|
||||
recursive: bool = False,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def brancher( # noqa: E302
|
||||
self,
|
||||
@@ -60,6 +63,7 @@ def brancher( # noqa: E302
|
||||
|
||||
scm = self.scm
|
||||
|
||||
logger.trace("switching fs to workspace") # type: ignore[attr-defined]
|
||||
self.fs = LocalFileSystem(url=self.root_dir)
|
||||
yield "workspace"
|
||||
|
||||
@@ -82,7 +86,9 @@ def brancher( # noqa: E302
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self._reset_cached_indecies() # pylint: disable=W0212
|
||||
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)
|
||||
|
||||
|
||||
+6
-1
@@ -43,6 +43,7 @@ def commit(
|
||||
force=False,
|
||||
allow_missing=False,
|
||||
data_only=False,
|
||||
relink=True,
|
||||
):
|
||||
stages_info = [
|
||||
info
|
||||
@@ -60,6 +61,10 @@ def commit(
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(filter_info=stage_info.filter_info, allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info,
|
||||
allow_missing=allow_missing,
|
||||
relink=relink,
|
||||
)
|
||||
stage.dump(update_pipeline=False)
|
||||
return [s.stage for s in stages_info]
|
||||
|
||||
+51
-18
@@ -13,17 +13,20 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.index import DataIndex
|
||||
from dvc_data.index.diff import Change
|
||||
|
||||
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _adapt_typ(typ):
|
||||
def _adapt_typ(typ: str) -> str:
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY
|
||||
|
||||
if typ == MODIFY:
|
||||
@@ -38,7 +41,7 @@ def _adapt_typ(typ):
|
||||
return typ
|
||||
|
||||
|
||||
def _adapt_path(change):
|
||||
def _adapt_path(change: "Change") -> str:
|
||||
isdir = False
|
||||
if change.new and change.new.meta:
|
||||
isdir = change.new.meta.isdir
|
||||
@@ -54,9 +57,12 @@ def _diff(
|
||||
old: "DataIndex",
|
||||
new: "DataIndex",
|
||||
*,
|
||||
granular: Optional[bool] = False,
|
||||
with_missing: Optional[bool] = False,
|
||||
granular: bool = False,
|
||||
not_in_cache: bool = False,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.index import StorageError
|
||||
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
|
||||
|
||||
ret: Dict[str, List[str]] = {}
|
||||
@@ -91,14 +97,27 @@ def _diff(
|
||||
continue
|
||||
|
||||
if (
|
||||
with_missing
|
||||
not_in_cache
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map[change.key].cache.exists(change.old.hash_info.value)
|
||||
and not old.storage_map.cache_exists(change.old)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
_add_change("not_in_cache", change)
|
||||
|
||||
try:
|
||||
if (
|
||||
not_in_remote
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map.remote_exists(
|
||||
change.old, refresh=remote_refresh
|
||||
)
|
||||
):
|
||||
_add_change("not_in_remote", change)
|
||||
except StorageError:
|
||||
pass
|
||||
|
||||
_add_change(change.typ, change)
|
||||
|
||||
return ret
|
||||
@@ -143,14 +162,19 @@ def _git_info(scm: Union["Git", "NoSCM"], untracked_files: str = "all") -> GitIn
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from .index import build_data_index
|
||||
|
||||
workspace = build_data_index(repo.index, repo.root_dir, repo.fs, compute_hash=True)
|
||||
with ui.status("Building workspace index"):
|
||||
workspace = build_data_index(
|
||||
repo.index, repo.root_dir, repo.fs, compute_hash=True
|
||||
)
|
||||
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
with_missing=True,
|
||||
**kwargs,
|
||||
)
|
||||
with ui.status("Calculating diff between index/workspace"):
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
not_in_cache=True,
|
||||
not_in_remote=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _diff_head_to_index(
|
||||
@@ -164,11 +188,13 @@ def _diff_head_to_index(
|
||||
|
||||
head_index = repo.index.data["repo"]
|
||||
|
||||
return _diff(head_index, index, **kwargs)
|
||||
with ui.status("Calculating diff between head/index"):
|
||||
return _diff(head_index, index, **kwargs)
|
||||
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
not_in_remote: List[str]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
@@ -195,12 +221,18 @@ def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> List[str]
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
def status(
|
||||
repo: "Repo",
|
||||
untracked_files: str = "no",
|
||||
**kwargs: Any,
|
||||
) -> Status:
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
not_in_cache = uncommitted_diff.pop("not_in_cache", [])
|
||||
uncommitted_diff = _diff_index_to_wtree(
|
||||
repo,
|
||||
**kwargs,
|
||||
)
|
||||
unchanged = set(uncommitted_diff.pop("unchanged", []))
|
||||
|
||||
try:
|
||||
@@ -215,7 +247,8 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
untracked = _transform_git_paths_to_dvc(repo, untracked)
|
||||
# order matters here
|
||||
return Status(
|
||||
not_in_cache=not_in_cache,
|
||||
not_in_cache=uncommitted_diff.pop("not_in_cache", []),
|
||||
not_in_remote=uncommitted_diff.pop("not_in_remote", []),
|
||||
committed=committed_diff,
|
||||
uncommitted=uncommitted_diff,
|
||||
untracked=untracked,
|
||||
|
||||
+119
-228
@@ -1,39 +1,103 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.repo import locked
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import DVCFileSystem, FileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _path(entry):
|
||||
if entry and entry.meta and entry.meta.isdir:
|
||||
return os.path.join(*entry.key, "")
|
||||
return os.path.join(*entry.key)
|
||||
|
||||
|
||||
def _hash(entry):
|
||||
if entry and entry.hash_info:
|
||||
return entry.hash_info.value
|
||||
return None
|
||||
|
||||
|
||||
def _diff(old, new, with_missing=False):
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
|
||||
from dvc_data.index.diff import diff as idiff
|
||||
|
||||
ret: "Dict[str, List[Dict]]" = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
"renamed": [],
|
||||
"not in cache": [],
|
||||
}
|
||||
|
||||
for change in idiff(
|
||||
old,
|
||||
new,
|
||||
with_renames=True,
|
||||
hash_only=True,
|
||||
):
|
||||
if change.typ == ADD:
|
||||
ret["added"].append(
|
||||
{
|
||||
"path": _path(change.new),
|
||||
"hash": _hash(change.new),
|
||||
}
|
||||
)
|
||||
elif change.typ == DELETE:
|
||||
ret["deleted"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
elif change.typ == MODIFY:
|
||||
ret["modified"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": {
|
||||
"old": _hash(change.old),
|
||||
"new": _hash(change.new),
|
||||
},
|
||||
}
|
||||
)
|
||||
elif change.typ == RENAME:
|
||||
ret["renamed"].append(
|
||||
{
|
||||
"path": {
|
||||
"old": _path(change.old),
|
||||
"new": _path(change.new),
|
||||
},
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
with_missing
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map.cache_exists(change.old)
|
||||
):
|
||||
ret["not in cache"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
|
||||
return ret if any(ret.values()) else {}
|
||||
|
||||
|
||||
@locked
|
||||
def diff(
|
||||
self,
|
||||
a_rev: str = "HEAD",
|
||||
b_rev: Optional[str] = None,
|
||||
targets: Optional[List[str]] = None,
|
||||
recursive: bool = False,
|
||||
):
|
||||
"""
|
||||
By default, it compares the workspace with the last commit's fs.
|
||||
@@ -42,231 +106,58 @@ def diff(
|
||||
the concept of `index`, but it keeps the same interface, thus,
|
||||
`dvc diff` would be the same as `dvc diff HEAD`.
|
||||
"""
|
||||
|
||||
if self.scm.no_commits:
|
||||
return {}
|
||||
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
indexes = {}
|
||||
missing_targets = defaultdict(set)
|
||||
with_missing = False
|
||||
if not b_rev:
|
||||
b_rev = "workspace"
|
||||
with_missing = True
|
||||
|
||||
dvcfs = DVCFileSystem(repo=self)
|
||||
targets = [dvcfs.from_os_path(target) for target in ensure_list(targets)]
|
||||
|
||||
b_rev = b_rev if b_rev else "workspace"
|
||||
results = {}
|
||||
missing_targets = {}
|
||||
for rev in self.brancher(revs=[a_rev, b_rev]):
|
||||
if rev == "workspace" and rev != b_rev:
|
||||
if rev == "workspace" and b_rev != "workspace":
|
||||
# brancher always returns workspace, but we only need to compute
|
||||
# workspace paths/checksums if b_rev was None
|
||||
continue
|
||||
|
||||
targets_paths = None
|
||||
if targets:
|
||||
# convert targets to paths, and capture any missing targets
|
||||
targets_paths, missing_targets[rev] = _targets_to_paths(dvcfs, targets)
|
||||
def onerror(target, _exc):
|
||||
# pylint: disable-next=cell-var-from-loop
|
||||
missing_targets[rev].add(target) # noqa: B023
|
||||
|
||||
results[rev] = _paths_checksums(self, targets_paths)
|
||||
|
||||
if targets:
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in set(missing_targets[a_rev]) & set(missing_targets[b_rev]):
|
||||
raise PathMissingError(target, self)
|
||||
|
||||
old = results[a_rev]
|
||||
new = results[b_rev]
|
||||
|
||||
# Compare paths between the old and new fs.
|
||||
# set() efficiently converts dict keys to a set
|
||||
added = sorted(set(new) - set(old))
|
||||
deleted_or_missing = set(old) - set(new)
|
||||
if b_rev == "workspace":
|
||||
# missing status is only applicable when diffing local workspace
|
||||
# against a commit
|
||||
missing = sorted(_filter_missing(dvcfs, deleted_or_missing))
|
||||
else:
|
||||
missing = []
|
||||
deleted = sorted(deleted_or_missing - set(missing))
|
||||
modified = sorted(set(old) & set(new))
|
||||
|
||||
# Cases when file was changed and renamed are resulted
|
||||
# in having deleted and added record
|
||||
# To cover such cases we need to change hashing function
|
||||
# to produce rolling/chunking hash
|
||||
|
||||
renamed = _calculate_renamed(new, old, added, deleted)
|
||||
|
||||
for renamed_item in renamed:
|
||||
added.remove(renamed_item["path"]["new"])
|
||||
deleted.remove(renamed_item["path"]["old"])
|
||||
|
||||
ret = {
|
||||
"added": [{"path": path, "hash": new[path]} for path in added],
|
||||
"deleted": [{"path": path, "hash": old[path]} for path in deleted],
|
||||
"modified": [
|
||||
{"path": path, "hash": {"old": old[path], "new": new[path]}}
|
||||
for path in modified
|
||||
if old[path] != new[path]
|
||||
],
|
||||
"renamed": renamed,
|
||||
"not in cache": [{"path": path, "hash": old[path]} for path in missing],
|
||||
}
|
||||
|
||||
return ret if any(ret.values()) else {}
|
||||
|
||||
|
||||
def _paths_checksums(
|
||||
repo: "Repo", targets: Optional[List[str]]
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
A dictionary of checksums addressed by relpaths collected from
|
||||
the current fs outputs.
|
||||
|
||||
To help distinguish between a directory and a file output,
|
||||
the former one will come with a trailing slash in the path:
|
||||
|
||||
directory: "data/"
|
||||
file: "data"
|
||||
"""
|
||||
|
||||
return dict(_output_paths(repo, targets))
|
||||
|
||||
|
||||
def _output_paths( # noqa: C901
|
||||
repo: "Repo", targets: Optional[List[str]]
|
||||
) -> Iterable[Tuple[str, Optional[str]]]:
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
def _exists(output: "Output") -> bool:
|
||||
if on_working_fs:
|
||||
return output.exists
|
||||
return True
|
||||
|
||||
def _to_path(output: "Output") -> str:
|
||||
relparts = output.fs.path.relparts(output.fs_path)
|
||||
base = os.path.join(*relparts)
|
||||
if output.is_dir_checksum:
|
||||
return os.path.join(base, "")
|
||||
return base
|
||||
|
||||
for output in repo.index.outs:
|
||||
if not _exists(output):
|
||||
continue
|
||||
|
||||
yield_output = targets is None or any(
|
||||
output.fs.path.isin_or_eq(output.fs_path, target) for target in targets
|
||||
view = self.index.targets_view(
|
||||
targets,
|
||||
onerror=onerror,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
obj: Optional["HashFile"]
|
||||
if on_working_fs:
|
||||
_, _, obj = build(
|
||||
repo.cache.repo,
|
||||
output.fs_path,
|
||||
repo.cache.repo.fs,
|
||||
"md5",
|
||||
dry_run=True,
|
||||
ignore=output.dvcignore,
|
||||
)
|
||||
hash_info = obj.hash_info
|
||||
else:
|
||||
hash_info = output.hash_info
|
||||
obj = output.get_obj()
|
||||
if rev == "workspace":
|
||||
from .index import build_data_index
|
||||
|
||||
if yield_output:
|
||||
yield _to_path(output), hash_info.value
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
if output.is_dir_checksum and (
|
||||
yield_output
|
||||
or (
|
||||
isinstance(targets, list)
|
||||
and any(
|
||||
output.fs.path.isin(target, output.fs_path) for target in targets
|
||||
with ui.status("Building workspace index"):
|
||||
data = build_data_index(
|
||||
view,
|
||||
self.root_dir,
|
||||
self.fs,
|
||||
compute_hash=True,
|
||||
)
|
||||
)
|
||||
):
|
||||
obj = cast("Tree", obj)
|
||||
yield from _dir_output_paths(output.fs, output.fs_path, obj, targets)
|
||||
|
||||
|
||||
def _dir_output_paths(
|
||||
fs: "FileSystem",
|
||||
fs_path: str,
|
||||
obj: "Tree",
|
||||
targets: Optional[Iterable[str]] = None,
|
||||
) -> Iterator[Tuple[str, str]]:
|
||||
base = os.path.join(*fs.path.relparts(fs_path))
|
||||
for key, _, oid in obj:
|
||||
fname = fs.path.join(fs_path, *key)
|
||||
if targets is None or any(
|
||||
fs.path.isin_or_eq(fname, target) for target in targets
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
yield os.path.join(base, *key), oid.value
|
||||
|
||||
|
||||
def _filter_missing(dvcfs: "DVCFileSystem", paths: Set[str]) -> Iterator[str]:
|
||||
for path in paths:
|
||||
fs_path = dvcfs.from_os_path(path)
|
||||
try:
|
||||
info = dvcfs.info(fs_path)
|
||||
dvc_info = info.get("dvc_info", {})
|
||||
entry = dvc_info.get("entry")
|
||||
if (
|
||||
entry
|
||||
and info["type"] == "directory"
|
||||
and not dvcfs.repo.cache.local.exists(entry.hash_info.value)
|
||||
):
|
||||
yield path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def _targets_to_paths(
|
||||
dvcfs: "DVCFileSystem", targets: List[str]
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
paths: List[str] = []
|
||||
missing: List[str] = []
|
||||
|
||||
if not targets:
|
||||
return paths, missing
|
||||
|
||||
for target in targets:
|
||||
if dvcfs.exists(target):
|
||||
paths.append(dvcfs.repo.fs.path.abspath(target))
|
||||
else:
|
||||
missing.append(target)
|
||||
data = view.data["repo"]
|
||||
|
||||
return paths, missing
|
||||
assert rev not in indexes
|
||||
indexes[rev] = data
|
||||
|
||||
if targets:
|
||||
old_missing = missing_targets.get(a_rev, set())
|
||||
new_missing = missing_targets.get(b_rev, set())
|
||||
|
||||
def _calculate_renamed(
|
||||
new: Dict[str, Any],
|
||||
old: Dict[str, Any],
|
||||
added: Iterable[str],
|
||||
deleted: Iterable[str],
|
||||
) -> List[Dict[str, Dict[str, str]]]:
|
||||
old_inverted: Dict[str, List[str]] = defaultdict(list)
|
||||
# It is needed to be dict of lists to cover cases
|
||||
# when repo has paths with same hash
|
||||
for path, path_hash in old.items():
|
||||
old_inverted[path_hash].append(path)
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in old_missing & new_missing:
|
||||
raise PathMissingError(target, self)
|
||||
|
||||
renamed = []
|
||||
for path in added:
|
||||
path_hash = new[path]
|
||||
old_paths = old_inverted[path_hash]
|
||||
try:
|
||||
iterator = enumerate(old_paths)
|
||||
index = next(idx for idx, path in iterator if path in deleted)
|
||||
except StopIteration:
|
||||
continue
|
||||
old = indexes[a_rev]
|
||||
new = indexes[b_rev]
|
||||
|
||||
old_path = old_paths.pop(index)
|
||||
renamed.append({"path": {"old": old_path, "new": path}, "hash": path_hash})
|
||||
|
||||
return renamed
|
||||
with ui.status("Calculating diff"):
|
||||
return _diff(old, new, with_missing=with_missing)
|
||||
|
||||
@@ -11,6 +11,7 @@ from dvc.ui import ui
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
from .cache import ExpCache
|
||||
from .exceptions import (
|
||||
BaselineMismatchError,
|
||||
ExperimentExistsError,
|
||||
@@ -22,6 +23,7 @@ from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .refs import (
|
||||
APPLY_STASH,
|
||||
CELERY_FAILED_STASH,
|
||||
CELERY_STASH,
|
||||
EXEC_APPLY,
|
||||
@@ -31,6 +33,7 @@ from .refs import (
|
||||
WORKSPACE_STASH,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from .stash import ApplyStash
|
||||
from .utils import check_ref_format, exp_refs_by_rev, unlocked_repo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -86,6 +89,14 @@ class Experiments:
|
||||
def celery_queue(self) -> LocalCeleryQueue:
|
||||
return LocalCeleryQueue(self.repo, CELERY_STASH, CELERY_FAILED_STASH)
|
||||
|
||||
@cached_property
|
||||
def apply_stash(self) -> ApplyStash:
|
||||
return ApplyStash(self.scm, APPLY_STASH)
|
||||
|
||||
@cached_property
|
||||
def cache(self) -> ExpCache:
|
||||
return ExpCache(self.repo)
|
||||
|
||||
@property
|
||||
def stash_revs(self) -> Dict[str, "ExpStashEntry"]:
|
||||
revs = {}
|
||||
@@ -96,19 +107,9 @@ class Experiments:
|
||||
def reproduce_one(
|
||||
self,
|
||||
tmp_dir: bool = False,
|
||||
machine: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
if not (tmp_dir or machine):
|
||||
staged, _, _ = self.scm.status(untracked_files="no")
|
||||
if staged:
|
||||
logger.warning(
|
||||
"Your workspace contains staged Git changes which will be "
|
||||
"unstaged before running this experiment."
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
exp_queue: "BaseStashQueue" = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
@@ -417,7 +418,7 @@ class Experiments:
|
||||
try:
|
||||
name = ExpRefInfo.from_ref(ref).name
|
||||
except InvalidExpRefError:
|
||||
pass # noqa: S110
|
||||
pass
|
||||
if not name:
|
||||
if rev in self.stash_revs:
|
||||
name = self.stash_revs[rev].name
|
||||
@@ -493,3 +494,8 @@ class Experiments:
|
||||
from dvc.repo.experiments.remove import remove
|
||||
|
||||
return remove(self.repo, *args, **kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.clean import clean
|
||||
|
||||
return clean(self.repo, *args, **kwargs)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import Git
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from .exceptions import ApplyConflictError, BaselineMismatchError, InvalidExpRevError
|
||||
from .exceptions import BaselineMismatchError, InvalidExpRevError
|
||||
from .executor.base import BaseExecutor
|
||||
from .refs import EXEC_APPLY
|
||||
|
||||
@@ -21,11 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
def apply(repo: "Repo", rev: str, **kwargs): # noqa: C901
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import GitMergeError, RevError, resolve_rev
|
||||
from dvc.scm import RevError, resolve_rev
|
||||
|
||||
exps: "Experiments" = repo.experiments
|
||||
|
||||
@@ -34,7 +32,6 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
assert isinstance(repo.scm, Git)
|
||||
try:
|
||||
exp_rev = resolve_rev(repo.scm, rev)
|
||||
exps.check_baseline(exp_rev)
|
||||
except RevError as exc:
|
||||
(
|
||||
exp_ref_info,
|
||||
@@ -42,14 +39,9 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
) = exps.celery_queue.get_ref_and_entry_by_names(
|
||||
rev
|
||||
)[rev]
|
||||
baseline_sha = repo.scm.get_rev()
|
||||
if exp_ref_info:
|
||||
if baseline_sha != exp_ref_info.baseline_sha:
|
||||
raise InvalidExpRevError(rev) # noqa: B904
|
||||
exp_rev = repo.scm.get_ref(str(exp_ref_info))
|
||||
elif queue_entry:
|
||||
if queue_entry.baseline_rev != baseline_sha:
|
||||
raise InvalidExpRevError(rev)
|
||||
exp_rev = queue_entry.stash_rev
|
||||
is_stash = True
|
||||
else:
|
||||
@@ -57,68 +49,22 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
except BaselineMismatchError as exc:
|
||||
raise InvalidExpRevError(rev) from exc
|
||||
|
||||
# NOTE: we don't use scmrepo's stash_workspace() here since we need
|
||||
# finer control over the merge behavior when we unstash everything
|
||||
with _apply_workspace(repo, rev, force):
|
||||
try:
|
||||
repo.scm.merge(exp_rev, commit=False, squash=True)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=repo.scm) # noqa: B904
|
||||
|
||||
repo.scm.reset()
|
||||
|
||||
if is_stash:
|
||||
assert repo.tmp_dir is not None
|
||||
args_path = os.path.join(repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
remove(args_path)
|
||||
|
||||
_apply(repo, exp_rev, name=rev, is_stash=is_stash)
|
||||
dvc_checkout(repo, **kwargs)
|
||||
|
||||
repo.scm.set_ref(EXEC_APPLY, exp_rev)
|
||||
logger.info(
|
||||
"Changes for experiment '%s' have been applied to your current workspace.",
|
||||
rev,
|
||||
ui.write(
|
||||
f"Changes for experiment '{rev}' have been applied to your current workspace.",
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _apply_workspace(repo: "Repo", rev: str, force: bool):
|
||||
from scmrepo.exceptions import MergeConflictError
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
|
||||
exps: "Experiments" = repo.experiments
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
if repo.scm.is_dirty(untracked_files=True):
|
||||
logger.debug("Stashing workspace")
|
||||
stash_rev: Optional[str] = repo.scm.stash.push(include_untracked=True)
|
||||
else:
|
||||
stash_rev = None
|
||||
try:
|
||||
yield
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if stash_rev:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
if not stash_rev:
|
||||
return
|
||||
|
||||
try:
|
||||
repo.scm.reset()
|
||||
repo.scm.stash.apply(stash_rev, skip_conflicts=force)
|
||||
repo.scm.stash.drop()
|
||||
except (MergeConflictError, _SCMError) as exc:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise ApplyConflictError(rev) from exc
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
|
||||
|
||||
def _clean_and_pop(scm: "Git"):
|
||||
"""Revert any changes and pop the last stash entry."""
|
||||
scm.reset(hard=True)
|
||||
if scm.is_dirty(untracked_files=True):
|
||||
# drop any changes to untracked files before popping stash
|
||||
scm.stash.push(include_untracked=True)
|
||||
scm.stash.drop()
|
||||
scm.stash.pop()
|
||||
with exps.apply_stash.preserve_workspace(rev, name=name):
|
||||
with repo.scm.detach_head(rev, force=True):
|
||||
if is_stash:
|
||||
assert repo.tmp_dir is not None
|
||||
args_path = os.path.join(repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
remove(args_path)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from dvc.fs import localfs
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .serialize import DeserializeError, SerializableError, SerializableExp
|
||||
from .utils import EXEC_TMP_DIR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpCache:
|
||||
"""Serialized experiment state cache.
|
||||
|
||||
ODB with git SHAs as keys. Objects can be either SerializableExp or
|
||||
SerializableError.
|
||||
"""
|
||||
|
||||
CACHE_DIR = os.path.join(EXEC_TMP_DIR, "cache")
|
||||
|
||||
def __init__(self, repo: "Repo"):
|
||||
path = os.path.join(repo.tmp_dir, self.CACHE_DIR)
|
||||
self.odb = ObjectDB(localfs, path)
|
||||
|
||||
def delete(self, rev: str):
|
||||
self.odb.delete(rev)
|
||||
|
||||
def put(
|
||||
self,
|
||||
exp: Union[SerializableExp, SerializableError],
|
||||
rev: Optional[str] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
rev = rev or getattr(exp, "rev", None)
|
||||
assert rev
|
||||
assert rev != "workspace"
|
||||
if force or not self.odb.exists(rev):
|
||||
try:
|
||||
self.delete(rev)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.odb.add_bytes(rev, exp.as_bytes())
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache put '%s'", rev[:7]
|
||||
)
|
||||
|
||||
def get(self, rev: str) -> Optional[Union[SerializableExp, SerializableError]]:
|
||||
obj = self.odb.get(rev)
|
||||
try:
|
||||
with obj.fs.open(obj.path, "rb") as fobj:
|
||||
data = fobj.read()
|
||||
except FileNotFoundError:
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache miss '%s'", rev[:7]
|
||||
)
|
||||
return None
|
||||
for typ in (SerializableExp, SerializableError):
|
||||
try:
|
||||
exp = typ.from_bytes(data) # type: ignore[attr-defined]
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache load '%s'", rev[:7]
|
||||
)
|
||||
return exp
|
||||
except DeserializeError:
|
||||
continue
|
||||
logger.debug("ExpCache: unknown object type for '%s'", rev)
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def clean(repo: "Repo"):
|
||||
ui.write("Cleaning up dvc-task messages...")
|
||||
repo.experiments.celery_queue.celery.clean()
|
||||
ui.write("Done!")
|
||||
@@ -66,18 +66,6 @@ class MultipleBranchError(DvcException):
|
||||
self.ref_infos = ref_infos
|
||||
|
||||
|
||||
class ApplyConflictError(InvalidArgumentError):
|
||||
def __init__(self, name):
|
||||
super().__init__(
|
||||
f"Experiment '{name}' cannot be applied to because your current "
|
||||
"workspace contains changes which would be overwritten. Either "
|
||||
"'git stash' your current changes before applying this "
|
||||
"experiment, or re-run this command with '--force' to overwrite "
|
||||
"your current changes."
|
||||
)
|
||||
self.name = name
|
||||
|
||||
|
||||
class AmbiguousExpRefInfo(InvalidArgumentError):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -547,55 +547,54 @@ class BaseExecutor(ABC):
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
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:
|
||||
post_live_metrics(
|
||||
"start",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
"dvc",
|
||||
params=to_studio_params(dvc.params.show()),
|
||||
)
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
post_live_metrics(
|
||||
"done",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
"dvc",
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
)
|
||||
|
||||
with Repo(os.path.join(info.root_dir, info.dvc_dir)) as dvc:
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
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:
|
||||
post_live_metrics(
|
||||
"start",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
"dvc",
|
||||
params=to_studio_params(dvc.params.show()),
|
||||
)
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
post_live_metrics(
|
||||
"done",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
"dvc",
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
)
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
os.chdir(old_cwd)
|
||||
|
||||
@classmethod
|
||||
def _repro_args(cls, dvc):
|
||||
|
||||
@@ -5,7 +5,6 @@ from tempfile import mkdtemp
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from funcy import retry
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.lock import LockError
|
||||
@@ -20,7 +19,7 @@ from dvc.repo.experiments.refs import (
|
||||
EXPS_TEMP,
|
||||
)
|
||||
from dvc.repo.experiments.utils import EXEC_TMP_DIR, get_exp_rwlock
|
||||
from dvc.scm import SCM, Git, GitMergeError
|
||||
from dvc.scm import SCM, Git
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
@@ -134,10 +133,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
self.scm.checkout(head, detach=True)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm) # noqa: B904
|
||||
self.scm.stash.apply(merge_rev)
|
||||
|
||||
def _config(self, cache_dir):
|
||||
local_config = os.path.join(
|
||||
@@ -229,10 +225,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
)
|
||||
)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm) # noqa: B904
|
||||
self.scm.stash.apply(merge_rev)
|
||||
if branch:
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
elif scm.get_ref(EXEC_BRANCH):
|
||||
|
||||
@@ -169,7 +169,7 @@ class SSHExecutor(BaseExecutor):
|
||||
head = EXEC_BRANCH if branch else EXEC_HEAD
|
||||
self._ssh_cmd(fs, f"git checkout {head}")
|
||||
merge_rev = scm.get_ref(EXEC_MERGE)
|
||||
self._ssh_cmd(fs, f"git merge --squash --no-commit {merge_rev}")
|
||||
self._ssh_cmd(fs, f"git stash apply {merge_rev}")
|
||||
|
||||
if self._setup_script:
|
||||
self._init_setup_script(fs)
|
||||
|
||||
@@ -21,9 +21,9 @@ from dvc.exceptions import DvcException
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.dvcfile import ProjectFile, SingleStageFile
|
||||
from dvc.repo import Repo
|
||||
from dvc.dependency import Dependency
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -218,13 +218,12 @@ def init(
|
||||
provided=overrides,
|
||||
stream=stream,
|
||||
)
|
||||
elif "live" in overrides:
|
||||
# suppress `metrics`/`plots` if live is selected.
|
||||
defaults.pop("metrics", None)
|
||||
defaults.pop("plots", None)
|
||||
else:
|
||||
if "live" in overrides:
|
||||
# suppress `metrics`/`plots` if live is selected.
|
||||
defaults.pop("metrics", None)
|
||||
defaults.pop("plots", None)
|
||||
else:
|
||||
defaults.pop("live", None) # suppress live otherwise
|
||||
defaults.pop("live", None) # suppress live otherwise
|
||||
|
||||
context: Dict[str, str] = {**defaults, **overrides}
|
||||
assert "cmd" in context
|
||||
|
||||
@@ -295,7 +295,7 @@ class BaseStashQueue(ABC):
|
||||
output.
|
||||
"""
|
||||
|
||||
def _stash_exp( # noqa: C901
|
||||
def _stash_exp( # noqa: PLR0915, C901
|
||||
self,
|
||||
*args,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
@@ -323,12 +323,12 @@ class BaseStashQueue(ABC):
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
"""
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
with self.scm.stash_workspace(reinstate_index=True) as workspace:
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
|
||||
with self.scm.stash_workspace() as workspace:
|
||||
try:
|
||||
if workspace:
|
||||
self.stash.apply(workspace)
|
||||
@@ -472,6 +472,7 @@ class BaseStashQueue(ABC):
|
||||
force=True,
|
||||
allow_missing=True,
|
||||
data_only=True,
|
||||
relink=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -90,7 +90,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
"dvc_task.proc.tasks",
|
||||
],
|
||||
)
|
||||
app.conf.update({"task_acks_late": True})
|
||||
app.conf.update({"task_acks_late": True, "result_expires": None})
|
||||
return app
|
||||
|
||||
@cached_property
|
||||
@@ -138,6 +138,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
wdir_hash = hashlib.sha256(self.wdir.encode("utf-8")).hexdigest()[:6]
|
||||
node_name = f"dvc-exp-{wdir_hash}-{num}@localhost"
|
||||
cmd = ["exp", "queue-worker", node_name]
|
||||
if num == 1:
|
||||
# automatically run celery cleanup when primary worker shuts down
|
||||
cmd.append("--clean")
|
||||
name = f"dvc-exp-worker-{num}"
|
||||
|
||||
logger.debug("start a new worker: %s, node: %s", name, node_name)
|
||||
@@ -336,7 +339,8 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id,
|
||||
remained_entries[entry],
|
||||
)
|
||||
self.celery.backend.mark_as_failure(task_id, None)
|
||||
backend = self.celery.backend
|
||||
backend.mark_as_failure(task_id, None) # type: ignore[attr-defined]
|
||||
|
||||
if remained_revs:
|
||||
raise CannotKillTasksError(remained_revs)
|
||||
|
||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
|
||||
|
||||
def remove_tasks( # noqa: C901
|
||||
def remove_tasks( # noqa: C901, PLR0912
|
||||
celery_queue: "LocalCeleryQueue",
|
||||
queue_entries: Iterable["QueueEntry"],
|
||||
):
|
||||
@@ -40,7 +40,7 @@ def remove_tasks( # noqa: C901
|
||||
msg,
|
||||
queue_entry,
|
||||
) in celery_queue._iter_queued(): # pylint: disable=protected-access
|
||||
if queue_entry.stash_rev in stash_revs:
|
||||
if queue_entry.stash_rev in stash_revs and msg.delivery_tag:
|
||||
celery_queue.celery.reject(msg.delivery_tag)
|
||||
finally:
|
||||
celery_queue.stash.remove_revs(list(stash_revs.values()))
|
||||
@@ -56,7 +56,8 @@ def remove_tasks( # noqa: C901
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result is not None:
|
||||
result.forget()
|
||||
celery_queue.celery.purge(msg.delivery_tag)
|
||||
if msg.delivery_tag:
|
||||
celery_queue.celery.purge(msg.delivery_tag)
|
||||
finally:
|
||||
if celery_queue.failed_stash:
|
||||
celery_queue.failed_stash.remove_revs(failed_stash_revs)
|
||||
|
||||
@@ -28,17 +28,17 @@ def setup_exp(entry_dict: Dict[str, Any]) -> "BaseExecutor":
|
||||
from dvc.repo import Repo
|
||||
|
||||
entry = QueueEntry.from_dict(entry_dict)
|
||||
repo = Repo(entry.dvc_root)
|
||||
# TODO: split executor.init_cache into separate subtask - we can release
|
||||
# exp.scm_lock before DVC push
|
||||
executor = BaseStashQueue.init_executor(
|
||||
repo.experiments,
|
||||
entry,
|
||||
TempDirExecutor,
|
||||
location="dvc-task",
|
||||
)
|
||||
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor.info.dump_json(infofile)
|
||||
with Repo(entry.dvc_root) as repo:
|
||||
# TODO: split executor.init_cache into separate subtask - we can release
|
||||
# exp.scm_lock before DVC push
|
||||
executor = BaseStashQueue.init_executor(
|
||||
repo.experiments,
|
||||
entry,
|
||||
TempDirExecutor,
|
||||
location="dvc-task",
|
||||
)
|
||||
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor.info.dump_json(infofile)
|
||||
return executor
|
||||
|
||||
|
||||
@@ -59,23 +59,23 @@ def collect_exp(
|
||||
from dvc.repo import Repo
|
||||
|
||||
entry = QueueEntry.from_dict(entry_dict)
|
||||
repo = Repo(entry.dvc_root)
|
||||
celery_queue = repo.experiments.celery_queue
|
||||
infofile = celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
logger.debug("Collecting experiment info '%s'", str(executor_info))
|
||||
executor = TempDirExecutor.from_info(executor_info)
|
||||
exec_result = executor_info.result
|
||||
try:
|
||||
if exec_result is not None:
|
||||
BaseStashQueue.collect_executor(repo.experiments, executor, exec_result)
|
||||
else:
|
||||
logger.debug("Experiment failed (Exec result was None)")
|
||||
celery_queue.stash_failed(entry)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Log exceptions but do not re-raise so that task chain execution
|
||||
# continues
|
||||
logger.exception("Failed to collect experiment")
|
||||
with Repo(entry.dvc_root) as repo:
|
||||
celery_queue = repo.experiments.celery_queue
|
||||
infofile = celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
logger.debug("Collecting experiment info '%s'", str(executor_info))
|
||||
executor = TempDirExecutor.from_info(executor_info)
|
||||
exec_result = executor_info.result
|
||||
try:
|
||||
if exec_result is not None:
|
||||
BaseStashQueue.collect_executor(repo.experiments, executor, exec_result)
|
||||
else:
|
||||
logger.debug("Experiment failed (Exec result was None)")
|
||||
celery_queue.stash_failed(entry)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Log exceptions but do not re-raise so that task chain execution
|
||||
# continues
|
||||
logger.exception("Failed to collect experiment")
|
||||
return executor.root_dir
|
||||
|
||||
|
||||
@@ -102,9 +102,9 @@ def run_exp(entry_dict: Dict[str, Any]) -> None:
|
||||
from dvc.repo import Repo
|
||||
|
||||
entry = QueueEntry.from_dict(entry_dict)
|
||||
repo = Repo(entry.dvc_root)
|
||||
queue = repo.experiments.celery_queue
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
with Repo(entry.dvc_root) as repo:
|
||||
queue = repo.experiments.celery_queue
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
executor = setup_exp.s(entry_dict)()
|
||||
try:
|
||||
cmd = ["dvc", "exp", "exec-run", "--infofile", infofile]
|
||||
|
||||
@@ -7,6 +7,9 @@ from .exceptions import InvalidExpRefError
|
||||
EXPS_NAMESPACE = "refs/exps"
|
||||
EXPS_STASH = f"{EXPS_NAMESPACE}/stash"
|
||||
WORKSPACE_STASH = EXPS_STASH
|
||||
APPLY_NAMESPACE = f"{EXPS_NAMESPACE}/apply"
|
||||
APPLY_HEAD = f"{APPLY_NAMESPACE}/ORIG_HEAD"
|
||||
APPLY_STASH = f"{APPLY_NAMESPACE}/stash"
|
||||
CELERY_STASH = f"{EXPS_NAMESPACE}/celery/stash"
|
||||
CELERY_FAILED_STASH = f"{EXPS_NAMESPACE}/celery/failed"
|
||||
EXEC_NAMESPACE = f"{EXPS_NAMESPACE}/exec"
|
||||
@@ -18,6 +21,7 @@ EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
|
||||
EXEC_MERGE = f"{EXEC_NAMESPACE}/EXEC_MERGE"
|
||||
EXPS_TEMP = f"{EXPS_NAMESPACE}/temp"
|
||||
STASHES = {WORKSPACE_STASH, CELERY_STASH}
|
||||
ITER_SKIP_NAMESPACES = {APPLY_NAMESPACE, EXEC_NAMESPACE}
|
||||
|
||||
|
||||
class ExpRefInfo:
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from pathspec import PathSpec
|
||||
|
||||
from dvc.scm import Git
|
||||
|
||||
from .exceptions import ExperimentExistsError
|
||||
@@ -22,7 +24,7 @@ def _save_experiment(
|
||||
name: Optional[str],
|
||||
include_untracked: Optional[List[str]],
|
||||
) -> str:
|
||||
repo.commit([], force=True)
|
||||
repo.commit([], force=True, relink=False)
|
||||
|
||||
name = name or get_random_exp_name(repo.scm, baseline_rev)
|
||||
ref_info = ExpRefInfo(baseline_rev, name)
|
||||
@@ -59,7 +61,8 @@ def save(
|
||||
|
||||
_, _, untracked = repo.scm.status()
|
||||
if include_untracked:
|
||||
untracked = [file for file in untracked if file not in 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(
|
||||
(
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo.metrics.show import _gather_metrics
|
||||
from dvc.repo.params.show import _gather_params
|
||||
from dvc.utils import onerror_collect, relpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
class DeserializeError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class _ISOEncoder(json.JSONEncoder):
|
||||
def default(self, o: object) -> Any:
|
||||
if isinstance(o, datetime):
|
||||
return o.isoformat()
|
||||
return super().default(o)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SerializableExp:
|
||||
"""Serializable experiment state."""
|
||||
|
||||
rev: str
|
||||
timestamp: Optional[datetime] = None
|
||||
params: Dict[str, Any] = field(default_factory=dict)
|
||||
metrics: Dict[str, Any] = field(default_factory=dict)
|
||||
deps: Dict[str, "_ExpDep"] = field(default_factory=dict)
|
||||
outs: Dict[str, "_ExpOut"] = field(default_factory=dict)
|
||||
status: Optional[str] = None
|
||||
executor: Optional[str] = None
|
||||
error: Optional["SerializableError"] = None
|
||||
|
||||
@classmethod
|
||||
def from_repo(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
rev: Optional[str] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
) -> "SerializableExp":
|
||||
"""Returns a SerializableExp from the current repo state.
|
||||
|
||||
Params, metrics, deps, outs are filled via repo fs/index, all other fields
|
||||
should be passed via kwargs.
|
||||
"""
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
|
||||
if not onerror:
|
||||
onerror = onerror_collect
|
||||
|
||||
rev = rev or repo.get_rev()
|
||||
assert rev
|
||||
status: Optional[str] = kwargs.get("status")
|
||||
# NOTE: _gather_params/_gather_metrics return defaultdict which is not
|
||||
# supported in dataclasses.asdict() on all python releases
|
||||
# see https://bugs.python.org/issue35540
|
||||
params = dict(_gather_params(repo, onerror=onerror))
|
||||
if status and status.lower() in ("queued", "failed"):
|
||||
metrics: Dict[str, Any] = {}
|
||||
else:
|
||||
metrics = dict(
|
||||
_gather_metrics(
|
||||
repo,
|
||||
targets=None,
|
||||
rev=rev[:7],
|
||||
recursive=False,
|
||||
onerror=onerror_collect,
|
||||
)
|
||||
)
|
||||
return cls(
|
||||
rev=rev,
|
||||
params=params,
|
||||
metrics=metrics,
|
||||
deps={
|
||||
relpath(dep.fs_path, repo.root_dir): _ExpDep(
|
||||
hash=dep.hash_info.value if dep.hash_info else None,
|
||||
size=dep.meta.size if dep.meta else None,
|
||||
nfiles=dep.meta.nfiles if dep.meta else None,
|
||||
)
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
},
|
||||
outs={
|
||||
relpath(out.fs_path, repo.root_dir): _ExpOut(
|
||||
hash=out.hash_info.value if out.hash_info else None,
|
||||
size=out.meta.size if out.meta else None,
|
||||
nfiles=out.meta.nfiles if out.meta else None,
|
||||
use_cache=out.use_cache,
|
||||
is_data_source=out.stage.is_data_source,
|
||||
)
|
||||
for out in repo.index.outs
|
||||
if not (out.is_metric or out.is_plot)
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def dumpd(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
def as_bytes(self) -> bytes:
|
||||
return _ISOEncoder().encode(self.dumpd()).encode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
if "timestamp" in parsed:
|
||||
parsed["timestamp"] = datetime.fromisoformat(parsed["timestamp"])
|
||||
return cls(**parsed)
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise DeserializeError("failed to load SerializableExp") from exc
|
||||
|
||||
@property
|
||||
def contains_error(self) -> bool:
|
||||
return (
|
||||
self.error is not None
|
||||
or self.params.get("error")
|
||||
or any(value.get("error") for value in self.params.values())
|
||||
or self.metrics.get("error")
|
||||
or any(value.get("error") for value in self.metrics.values())
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ExpDep:
|
||||
hash: Optional[str] # noqa: A003
|
||||
size: Optional[int]
|
||||
nfiles: Optional[int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ExpOut:
|
||||
hash: Optional[str] # noqa: A003
|
||||
size: Optional[int]
|
||||
nfiles: Optional[int]
|
||||
use_cache: bool
|
||||
is_data_source: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SerializableError:
|
||||
msg: str
|
||||
type: str = "" # noqa: A003
|
||||
|
||||
def dumpd(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
def as_bytes(self) -> bytes:
|
||||
return json.dumps(self.dumpd()).encode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
return cls(**parsed)
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise DeserializeError("failed to load SerializableError") from exc
|
||||
+106
-70
@@ -18,12 +18,11 @@ from typing import (
|
||||
|
||||
from scmrepo.exceptions import SCMError as InnerScmError
|
||||
|
||||
from dvc.repo.metrics.show import _gather_metrics
|
||||
from dvc.repo.params.show import _gather_params
|
||||
from dvc.scm import Git, SCMError, iter_revs, resolve_rev
|
||||
from dvc.utils import error_handler, onerror_collect, relpath
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import Git, RevError, SCMError, iter_revs, resolve_rev
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
from .serialize import SerializableError, SerializableExp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git.objects import GitCommit
|
||||
@@ -40,85 +39,111 @@ class ExpStatus(Enum):
|
||||
Failed = 3
|
||||
|
||||
|
||||
class _CachedError(DvcException):
|
||||
def __init__(self, msg, typ, *args):
|
||||
super().__init__(msg, *args)
|
||||
self.typ = typ
|
||||
|
||||
|
||||
def _is_scm_error(collected_exp: Dict[str, Any]) -> bool:
|
||||
if "error" in collected_exp and (
|
||||
isinstance(collected_exp["error"], (SCMError, InnerScmError))
|
||||
isinstance(collected_exp["error"], (_CachedError, SCMError, InnerScmError))
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _show_onerror_collect(result: Dict, exception: Exception, *args, **kwargs):
|
||||
onerror_collect(result, exception, *args, **kwargs)
|
||||
result["data"] = {}
|
||||
def _format_exp(exp: SerializableExp) -> Dict[str, Any]:
|
||||
# SerializableExp always includes error but we need to strip it from show
|
||||
# output when it is false-y to maintain compatibility w/tools that consume
|
||||
# json output and assume that "error" key presence means there was an error
|
||||
exp_dict = exp.dumpd()
|
||||
if "error" in exp_dict and not exp_dict["error"]:
|
||||
del exp_dict["error"]
|
||||
return {"data": exp_dict}
|
||||
|
||||
|
||||
def _format_error(error: SerializableError):
|
||||
msg = error.msg or "None"
|
||||
return {"data": {}, "error": _CachedError(msg, error.type)}
|
||||
|
||||
|
||||
@error_handler
|
||||
def collect_experiment_commit(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
param_deps=False,
|
||||
running: Optional[Dict[str, Any]] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
cache = repo.experiments.cache
|
||||
# TODO: support filtering serialized exp when param_deps is set
|
||||
if exp_rev != "workspace" and not (force or param_deps):
|
||||
cached_exp = cache.get(exp_rev)
|
||||
if cached_exp:
|
||||
if status == ExpStatus.Running:
|
||||
# expire cached queued exp entry once we start running it
|
||||
cache.delete(exp_rev)
|
||||
elif isinstance(cached_exp, SerializableError):
|
||||
return _format_error(cached_exp)
|
||||
else:
|
||||
return _format_exp(cached_exp)
|
||||
try:
|
||||
exp = _collect_from_repo(
|
||||
repo,
|
||||
exp_rev,
|
||||
status=status,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
if exp_rev != "workspace" and not param_deps and not exp.contains_error:
|
||||
cache.put(exp, force=True)
|
||||
return _format_exp(exp)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
logger.debug("", exc_info=True)
|
||||
error = SerializableError(str(exc), type(exc).__name__)
|
||||
return _format_error(error)
|
||||
|
||||
result: Dict[str, Any] = defaultdict(dict)
|
||||
|
||||
def _collect_from_repo(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
running: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> "SerializableExp":
|
||||
running = running or {}
|
||||
for rev in repo.brancher(revs=[exp_rev]):
|
||||
if rev == "workspace":
|
||||
if exp_rev != "workspace":
|
||||
continue
|
||||
result["timestamp"] = None
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
result["timestamp"] = datetime.fromtimestamp(commit.commit_time)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
params = _gather_params(repo, targets=None, deps=param_deps, onerror=onerror)
|
||||
if params:
|
||||
result["params"] = params
|
||||
|
||||
result["deps"] = {
|
||||
relpath(dep.fs_path, repo.root_dir): {
|
||||
"hash": dep.hash_info.value,
|
||||
"size": dep.meta.size,
|
||||
"nfiles": dep.meta.nfiles,
|
||||
}
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
}
|
||||
result["outs"] = {
|
||||
relpath(out.fs_path, repo.root_dir): {
|
||||
"hash": out.hash_info.value,
|
||||
"size": out.meta.size,
|
||||
"nfiles": out.meta.nfiles,
|
||||
"use_cache": out.use_cache,
|
||||
"is_data_source": out.stage.is_data_source,
|
||||
}
|
||||
for out in repo.index.outs
|
||||
if not (out.is_metric or out.is_plot)
|
||||
}
|
||||
|
||||
result["status"] = status.name
|
||||
if status == ExpStatus.Running:
|
||||
result["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
executor: Optional[str] = running.get(exp_rev, {}).get("location", None)
|
||||
else:
|
||||
result["executor"] = None
|
||||
executor = None
|
||||
|
||||
if status == ExpStatus.Failed:
|
||||
result["error"] = {
|
||||
"msg": "Experiment run failed.",
|
||||
"type": "",
|
||||
}
|
||||
|
||||
if status not in {ExpStatus.Queued, ExpStatus.Failed}:
|
||||
vals = _gather_metrics(
|
||||
repo, targets=None, rev=rev, recursive=False, onerror=onerror
|
||||
error: Optional["SerializableError"] = SerializableError(
|
||||
"Experiment run failed."
|
||||
)
|
||||
result["metrics"] = vals
|
||||
else:
|
||||
error = None
|
||||
|
||||
return result
|
||||
return SerializableExp.from_repo(
|
||||
repo,
|
||||
rev=exp_rev,
|
||||
timestamp=timestamp,
|
||||
status=status.name,
|
||||
executor=executor,
|
||||
error=error,
|
||||
)
|
||||
raise RevError(f"nonexistent exp rev: '{exp_rev}'")
|
||||
|
||||
|
||||
def _collect_complete_experiment(
|
||||
@@ -170,6 +195,7 @@ def _collect_branch(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
running: Dict[str, Any],
|
||||
refs: Optional[Iterable[str]] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: Dict[str, Dict[str, Any]] = OrderedDict()
|
||||
@@ -188,7 +214,11 @@ def _collect_branch(
|
||||
commits: List[Tuple[str, "GitCommit", str, List[str]]] = []
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
for ref in repo.scm.iter_refs(base=str(ref_info)):
|
||||
if refs:
|
||||
ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
|
||||
else:
|
||||
ref_it = repo.scm.iter_refs(base=str(ref_info))
|
||||
for ref in ref_it:
|
||||
try:
|
||||
commit = repo.scm.resolve_commit(ref)
|
||||
exp_rev = resolve_rev(repo.scm, ref)
|
||||
@@ -217,19 +247,22 @@ def _collect_branch(
|
||||
|
||||
|
||||
def get_branch_names(
|
||||
scm: "Git", baseline_set: Iterable[str]
|
||||
scm: "Git", baseline_set: Iterable[str], refs: Optional[Iterable[str]] = None
|
||||
) -> Dict[str, Optional[str]]:
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for base in [
|
||||
bases = [
|
||||
f"refs/exps/{baseline[:2]}/{baseline[2:]}/" for baseline in baseline_set
|
||||
] + ["refs/heads/", "refs/tags/"]:
|
||||
for ref in scm.iter_refs(base=base):
|
||||
if ref:
|
||||
] + ["refs/heads/", "refs/tags/"]
|
||||
ref_it = iter(refs) if refs else scm.iter_refs()
|
||||
for ref in ref_it:
|
||||
for base in bases:
|
||||
if ref.startswith(base):
|
||||
try:
|
||||
rev = scm.get_ref(ref)
|
||||
names[rev] = ref[len(base) :]
|
||||
except KeyError:
|
||||
logger.debug("unreosolved ref %s", ref)
|
||||
logger.debug("unresolved ref %s", ref)
|
||||
break
|
||||
logger.debug("found refs for revs: %s", names)
|
||||
return names
|
||||
|
||||
@@ -377,13 +410,12 @@ def move_properties_to_head(result: Dict[str, Dict[str, Dict[str, Any]]]):
|
||||
if rev_result["status"] == ExpStatus.Running.name:
|
||||
head["status"] = ExpStatus.Running.name
|
||||
rev_result["status"] = ExpStatus.Success.name
|
||||
else:
|
||||
if rev_result["checkpoint_tip"] == rev:
|
||||
head = rev_result
|
||||
checkpoint = True
|
||||
elif rev_result["checkpoint_tip"] == rev:
|
||||
head = rev_result
|
||||
checkpoint = True
|
||||
|
||||
|
||||
def show(
|
||||
def show( # noqa: PLR0913
|
||||
repo: "Repo",
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
@@ -396,12 +428,11 @@ def show(
|
||||
param_deps=False,
|
||||
onerror: Optional[Callable] = None,
|
||||
fetch_running: bool = True,
|
||||
force: bool = False,
|
||||
):
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
|
||||
onerror = onerror or _show_onerror_collect
|
||||
|
||||
res: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
|
||||
if not any([revs, all_branches, all_tags, all_commits]):
|
||||
@@ -415,7 +446,8 @@ def show(
|
||||
found_revs.update(
|
||||
iter_revs(repo.scm, revs, num, all_branches, all_tags, all_commits)
|
||||
)
|
||||
branch_names = get_branch_names(repo.scm, found_revs)
|
||||
cached_refs = list(repo.scm.iter_refs())
|
||||
branch_names = get_branch_names(repo.scm, found_revs, refs=cached_refs)
|
||||
|
||||
running: Dict[str, Dict] = repo.experiments.get_running_exps(
|
||||
fetch_refs=fetch_running
|
||||
@@ -427,7 +459,7 @@ def show(
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
if not hide_queued
|
||||
else {}
|
||||
@@ -439,6 +471,7 @@ def show(
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
|
||||
failed_experiments = (
|
||||
@@ -448,6 +481,7 @@ def show(
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
if not hide_failed
|
||||
else {}
|
||||
@@ -460,6 +494,8 @@ def show(
|
||||
running=running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
refs=cached_refs,
|
||||
)
|
||||
|
||||
update_new(res, failed_experiments)
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Iterable, NamedTuple, Optional
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Iterable, Iterator, NamedTuple, Optional
|
||||
|
||||
from scmrepo.git import Stash
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc_objects.fs.local import localfs
|
||||
from dvc_objects.fs.utils import as_atomic
|
||||
|
||||
from .refs import APPLY_HEAD, APPLY_STASH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpStashEntry(NamedTuple):
|
||||
"""Experiment stash entry.
|
||||
@@ -72,3 +82,122 @@ class ExpStash(Stash):
|
||||
reverse=True,
|
||||
):
|
||||
self.drop(index)
|
||||
|
||||
|
||||
class ApplyStashEntry(NamedTuple):
|
||||
"""Apply stash entry.
|
||||
|
||||
stash_index: Stash index for this entry. Can be None if this commit
|
||||
is not pushed onto the stash ref.
|
||||
head_rev: HEAD Git commit prior to exp apply.
|
||||
rev: Applied experiment commit.
|
||||
name: Optional applied exp name.
|
||||
"""
|
||||
|
||||
stash_index: Optional[int]
|
||||
head_rev: str
|
||||
rev: str
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class ApplyStash(Stash):
|
||||
DEFAULT_STASH = APPLY_STASH
|
||||
MESSAGE_FORMAT = "dvc-exp-apply:{head_rev}:{rev}:{name}"
|
||||
MESSAGE_RE = re.compile(
|
||||
r"(?:commit: )"
|
||||
r"dvc-exp-apply:(?P<head_rev>[0-9a-f]+):(?P<rev>[0-9a-f]+)"
|
||||
r":(?P<name>[^~^:\\?\[\]*]*)"
|
||||
)
|
||||
|
||||
@property
|
||||
def stash_revs(self) -> Dict[str, ApplyStashEntry]:
|
||||
revs = {}
|
||||
for i, entry in enumerate(self):
|
||||
msg = entry.message.decode("utf-8").strip()
|
||||
m = self.MESSAGE_RE.match(msg)
|
||||
if m:
|
||||
revs[entry.new_sha.decode("utf-8")] = ApplyStashEntry(
|
||||
i,
|
||||
m.group("head_rev"),
|
||||
m.group("rev"),
|
||||
m.group("name"),
|
||||
)
|
||||
return revs
|
||||
|
||||
@classmethod
|
||||
def format_message(
|
||||
cls,
|
||||
head_rev: str,
|
||||
rev: str,
|
||||
name: Optional[str] = None,
|
||||
) -> str:
|
||||
return cls.MESSAGE_FORMAT.format(
|
||||
head_rev=head_rev, rev=rev, name=name if name else ""
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def preserve_workspace(
|
||||
self, rev: str, name: Optional[str] = None
|
||||
) -> Iterator[Optional[str]]:
|
||||
if len(self):
|
||||
logger.debug("Clearing existing exp-apply stash")
|
||||
self.clear()
|
||||
head = self.scm.get_rev()
|
||||
self.scm.set_ref(APPLY_HEAD, head)
|
||||
message = self.format_message(head, rev, name=name)
|
||||
stash_rev = self.push(message=message, include_untracked=True)
|
||||
try:
|
||||
yield stash_rev
|
||||
if stash_rev:
|
||||
self._apply_difference(stash_rev, rev)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.revert_workspace()
|
||||
raise
|
||||
|
||||
def _apply_difference(self, stash_rev: str, rev: str):
|
||||
"""Selectively apply changes from stash_rev.
|
||||
|
||||
Only changes to files from left which do not exist in right will be applied.
|
||||
"""
|
||||
self._copy_difference(stash_rev, rev)
|
||||
commit = self.scm.resolve_commit(stash_rev)
|
||||
for parent_rev in commit.parents:
|
||||
parent_commit = self.scm.resolve_commit(parent_rev)
|
||||
if parent_commit.message.startswith("untracked files on "):
|
||||
self._copy_difference(parent_rev, rev)
|
||||
|
||||
def _copy_difference(self, left_rev: str, right_rev: str):
|
||||
left_fs = self.scm.get_fs(left_rev)
|
||||
right_fs = self.scm.get_fs(right_rev)
|
||||
paths = [path for path in left_fs.find("/") if not right_fs.exists(path)]
|
||||
dest_paths = [
|
||||
localfs.path.join(self.scm.root_dir, left_fs.path.relpath(path, "/"))
|
||||
for path in paths
|
||||
]
|
||||
for src, dest in zip(paths, dest_paths):
|
||||
with as_atomic(localfs, dest, create_parents=True) as tmp_file:
|
||||
left_fs.get_file(src, tmp_file)
|
||||
|
||||
def revert_workspace(self):
|
||||
apply_head = self.scm.get_ref(self.ref)
|
||||
head = self.scm.get_rev()
|
||||
if apply_head != head:
|
||||
raise DvcException(
|
||||
f"Cannot revert workspace, current HEAD '{head[:7]}' does not match the"
|
||||
f" pre-apply HEAD '{apply_head[:7]}'"
|
||||
)
|
||||
self.scm.reset(hard=True)
|
||||
if len(self):
|
||||
# In the event that the apply-stash and current workspace contain
|
||||
# conflicting untracked files, we do:
|
||||
# 1. stash the current untracked files
|
||||
# 2. restore/pop the apply-stash (with untracked files)
|
||||
# 3. restore/pop the untracked files from (1) and ignore any conflicts
|
||||
# (forcefully reverting to the apply-stash version)
|
||||
workspace_rev = self.scm.stash.push(include_untracked=True)
|
||||
try:
|
||||
self.pop()
|
||||
finally:
|
||||
if workspace_rev:
|
||||
self.scm.stash.pop(skip_conflicts=True)
|
||||
self.scm.remove_ref(self.ref)
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@locked
|
||||
def fetch( # noqa: C901
|
||||
def fetch( # noqa: C901, PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
jobs=None,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ def _validate_args(**kwargs):
|
||||
|
||||
|
||||
@locked
|
||||
def gc(
|
||||
def gc( # noqa: PLR0913
|
||||
self: "Repo",
|
||||
all_branches: bool = False,
|
||||
cloud: bool = False,
|
||||
|
||||
+11
-11
@@ -33,10 +33,10 @@ def get_pipeline(pipelines, node):
|
||||
return found[0]
|
||||
|
||||
|
||||
def get_pipelines(G: "DiGraph"):
|
||||
def get_pipelines(graph: "DiGraph"):
|
||||
import networkx as nx
|
||||
|
||||
return [G.subgraph(c).copy() for c in nx.weakly_connected_components(G)]
|
||||
return [graph.subgraph(c).copy() for c in nx.weakly_connected_components(graph)]
|
||||
|
||||
|
||||
def collect_pipeline(stage: "Stage", graph: "DiGraph") -> Iterator["Stage"]:
|
||||
@@ -97,7 +97,7 @@ def build_graph(stages, outs_trie=None):
|
||||
|
||||
from .trie import build_outs_trie
|
||||
|
||||
G = nx.DiGraph()
|
||||
graph = nx.DiGraph()
|
||||
|
||||
# Use trie to efficiently find overlapping outs and deps
|
||||
outs_trie = outs_trie or build_outs_trie(stages)
|
||||
@@ -108,7 +108,7 @@ def build_graph(stages, outs_trie=None):
|
||||
raise StagePathAsOutputError(stage, str(out))
|
||||
|
||||
# Building graph
|
||||
G.add_nodes_from(stages)
|
||||
graph.add_nodes_from(stages)
|
||||
for stage in stages:
|
||||
for dep in stage.deps:
|
||||
if dep.fs_path is None:
|
||||
@@ -119,10 +119,10 @@ def build_graph(stages, outs_trie=None):
|
||||
if outs_trie.has_subtrie(dep_key):
|
||||
overlapping.extend(outs_trie.values(prefix=dep_key))
|
||||
|
||||
G.add_edges_from((stage, out.stage) for out in overlapping)
|
||||
check_acyclic(G)
|
||||
graph.add_edges_from((stage, out.stage) for out in overlapping)
|
||||
check_acyclic(graph)
|
||||
|
||||
return G
|
||||
return graph
|
||||
|
||||
|
||||
# NOTE: using stage graph instead of just list of stages to make sure that it
|
||||
@@ -131,9 +131,9 @@ def build_graph(stages, outs_trie=None):
|
||||
def build_outs_graph(graph, outs_trie):
|
||||
import networkx as nx
|
||||
|
||||
G = nx.DiGraph()
|
||||
outs_graph = nx.DiGraph()
|
||||
|
||||
G.add_nodes_from(outs_trie.values())
|
||||
outs_graph.add_nodes_from(outs_trie.values())
|
||||
for stage in graph.nodes():
|
||||
for dep in stage.deps:
|
||||
if dep.fs_path is None:
|
||||
@@ -145,5 +145,5 @@ def build_outs_graph(graph, outs_trie):
|
||||
overlapping.extend(outs_trie.values(prefix=dep_key))
|
||||
|
||||
for from_out in stage.outs:
|
||||
G.add_edges_from((from_out, out) for out in overlapping)
|
||||
return G
|
||||
outs_graph.add_edges_from((from_out, out) for out in overlapping)
|
||||
return outs_graph
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ from . import locked
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def imp_url( # noqa: C901
|
||||
def imp_url( # noqa: C901, PLR0913
|
||||
self: "Repo",
|
||||
url,
|
||||
out=None,
|
||||
|
||||
+8
-4
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, List, Set, Tuple, Union, cast
|
||||
from typing import TYPE_CHECKING, List, Set, Tuple, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency.base import Dependency
|
||||
@@ -9,7 +9,8 @@ if TYPE_CHECKING:
|
||||
from dvc.repo.index import Index, IndexView
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import TargetType
|
||||
from dvc_data.hashfile import HashInfo
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,6 +64,8 @@ def unpartial_imports(index: Union["Index", "IndexView"]) -> int:
|
||||
Returns:
|
||||
Total number of files which were unpartialed.
|
||||
"""
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
updated = 0
|
||||
for out in index.outs:
|
||||
# we need to use view[key] here and since the out fields have not been
|
||||
@@ -75,7 +78,8 @@ def unpartial_imports(index: Union["Index", "IndexView"]) -> int:
|
||||
out.hash_info = dep.get_obj().hash_info
|
||||
out.meta = dep.get_meta()
|
||||
else:
|
||||
out.hash_info = cast("HashInfo", entry.hash_info)
|
||||
assert isinstance(entry.hash_info, HashInfo)
|
||||
out.hash_info = entry.hash_info
|
||||
out.meta = entry.meta
|
||||
out.stage.md5 = out.stage.compute_md5()
|
||||
out.stage.dump()
|
||||
@@ -115,7 +119,7 @@ def save_imports(
|
||||
desc="Downloading imports from source",
|
||||
unit="files",
|
||||
) as cb:
|
||||
checkout(data_view, tmpdir, cache.fs, callback=cb)
|
||||
checkout(data_view, tmpdir, cache.fs, callback=cb, storage="data")
|
||||
md5(data_view)
|
||||
save(data_view, odb=cache, hardlink=True)
|
||||
|
||||
|
||||
+105
-33
@@ -112,6 +112,60 @@ def collect_files(
|
||||
dirs[:] = [d for d in dirs if not is_out_or_ignored(root, d)]
|
||||
|
||||
|
||||
def _load_data_from_outs(index, prefix, outs):
|
||||
from dvc_data.index import DataIndexEntry
|
||||
|
||||
for out in outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
ws, key = out.index_key
|
||||
|
||||
entry = DataIndexEntry(
|
||||
key=key,
|
||||
meta=out.meta,
|
||||
hash_info=out.hash_info,
|
||||
)
|
||||
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
dep = out.stage.deps[0]
|
||||
entry.meta = dep.meta
|
||||
entry.hash_info = dep.hash_info
|
||||
|
||||
# FIXME PyGTrie-based DataIndex doesn't remove entry.key during
|
||||
# index.add, so we have to set the entry manually here to make
|
||||
# index.view() work correctly.
|
||||
index[(*prefix, ws, *key)] = entry
|
||||
|
||||
|
||||
def _load_storage_from_out(storage_map, key, out):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import FileStorage, ObjectStorage
|
||||
|
||||
if out.odb:
|
||||
storage_map.add_data(ObjectStorage(key, out.odb))
|
||||
storage_map.add_cache(ObjectStorage(key, out.cache))
|
||||
try:
|
||||
remote = out.repo.cloud.get_remote(out.remote)
|
||||
if remote.fs.version_aware:
|
||||
storage_map.add_remote(
|
||||
FileStorage(
|
||||
key=key,
|
||||
fs=remote.fs,
|
||||
path=remote.fs.path.join(remote.path, *key),
|
||||
index=remote.index,
|
||||
)
|
||||
)
|
||||
else:
|
||||
storage_map.add_remote(ObjectStorage(key, remote.odb, index=remote.index))
|
||||
except NoRemoteError:
|
||||
pass
|
||||
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
dep = out.stage.deps[0]
|
||||
storage_map.add_data(FileStorage(key, dep.fs, dep.fs_path))
|
||||
|
||||
|
||||
class Index:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -282,50 +336,60 @@ class Index:
|
||||
return dict(by_workspace)
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "Dict[str, DataIndex]":
|
||||
from collections import defaultdict
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import DataIndex, DataIndexEntry, Storage
|
||||
|
||||
by_workspace: dict = defaultdict(DataIndex)
|
||||
|
||||
by_workspace["repo"] = DataIndex()
|
||||
by_workspace["local"] = DataIndex()
|
||||
def data_tree(self):
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
tree = Tree()
|
||||
for out in self.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
workspace, key = out.index_key
|
||||
data_index = by_workspace[workspace]
|
||||
ws, key = out.index_key
|
||||
|
||||
entry = DataIndexEntry(
|
||||
key=key,
|
||||
meta=out.meta,
|
||||
hash_info=out.hash_info,
|
||||
)
|
||||
tree.add((ws, *key), out.meta, out.hash_info)
|
||||
|
||||
storage = Storage(odb=out.odb, cache=out.cache)
|
||||
try:
|
||||
storage.remote = self.repo.cloud.get_remote_odb(out.remote)
|
||||
except NoRemoteError:
|
||||
pass
|
||||
tree.digest()
|
||||
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
dep = out.stage.deps[0]
|
||||
return tree
|
||||
|
||||
entry.meta = dep.meta
|
||||
entry.hash_info = dep.hash_info
|
||||
@cached_property
|
||||
def data(self) -> "Dict[str, DataIndex]":
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
storage.odb = dep.odb
|
||||
storage.fs = dep.fs
|
||||
storage.path = dep.fs_path
|
||||
prefix: "DataIndexKey"
|
||||
loaded = False
|
||||
|
||||
data_index.add(entry)
|
||||
data_index.storage_map[key] = storage
|
||||
index = self.repo.data_index
|
||||
if index is None:
|
||||
index = DataIndex()
|
||||
|
||||
return dict(by_workspace)
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
return by_workspace
|
||||
finally:
|
||||
index.close()
|
||||
|
||||
@staticmethod
|
||||
def _hash_targets(
|
||||
@@ -526,6 +590,10 @@ def build_data_index(
|
||||
from dvc_data.index.build import build_entries, build_entry
|
||||
from dvc_data.index.save import build_tree
|
||||
|
||||
ignore = None
|
||||
if workspace == "repo" and isinstance(fs, LocalFileSystem):
|
||||
ignore = index.repo.dvcignore
|
||||
|
||||
data = DataIndex()
|
||||
for key in index.data_keys.get(workspace, set()):
|
||||
out_path = fs.path.join(path, *key)
|
||||
@@ -547,7 +615,11 @@ def build_data_index(
|
||||
continue
|
||||
|
||||
for entry in build_entries(
|
||||
out_path, fs, compute_hash=compute_hash, state=index.repo.state
|
||||
out_path,
|
||||
fs,
|
||||
compute_hash=compute_hash,
|
||||
state=index.repo.state,
|
||||
ignore=ignore,
|
||||
):
|
||||
if not entry.key or entry.key == ("",):
|
||||
# NOTE: whether the root will be returned by build_entries
|
||||
|
||||
@@ -143,7 +143,7 @@ def show(
|
||||
except (SCMError, NoSCMError):
|
||||
# SCMError - detached head
|
||||
# NoSCMError - no repo case
|
||||
pass # noqa: S110
|
||||
pass
|
||||
else:
|
||||
if res.get("workspace") == res.get(active_branch):
|
||||
res.pop("workspace", None)
|
||||
|
||||
+3
-3
@@ -32,11 +32,11 @@ def move(self, from_path, to_path):
|
||||
It only works with outputs generated by `add` or `import`,
|
||||
also known as data sources.
|
||||
"""
|
||||
import dvc.output as Output
|
||||
from dvc import output
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.stage import Stage
|
||||
|
||||
from_out = Output.loads_from(Stage(self), [from_path])[0]
|
||||
from_out = output.loads_from(Stage(self), [from_path])[0]
|
||||
assert from_out.protocol == "local"
|
||||
|
||||
to_path = _expand_target_path(from_path, to_path)
|
||||
@@ -71,7 +71,7 @@ def move(self, from_path, to_path):
|
||||
else:
|
||||
to_path = os.path.relpath(to_path, stage.wdir)
|
||||
|
||||
to_out = Output.loads_from(stage, [to_path], out.use_cache, out.metric)[0]
|
||||
to_out = output.loads_from(stage, [to_path], out.use_cache, out.metric)[0]
|
||||
|
||||
out.move(to_out)
|
||||
stage.save()
|
||||
|
||||
@@ -153,7 +153,7 @@ def show(
|
||||
except (SCMError, NoSCMError):
|
||||
# SCMError - detached head
|
||||
# NoSCMError - no repo case
|
||||
pass # noqa: S110
|
||||
pass
|
||||
else:
|
||||
if res.get("workspace") == res.get(active_branch):
|
||||
res.pop("workspace", None)
|
||||
|
||||
+19
-53
@@ -2,7 +2,7 @@ import csv
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict, defaultdict
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from multiprocessing import cpu_count
|
||||
@@ -18,8 +18,8 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
import dpath
|
||||
import dpath.options
|
||||
import dpath.util
|
||||
from funcy import distinct, first, project
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -74,7 +74,6 @@ class Plots:
|
||||
recursive: bool = False,
|
||||
onerror: Optional[Callable] = None,
|
||||
props: Optional[Dict] = None,
|
||||
config_files: Optional[Set[str]] = None,
|
||||
) -> Iterator[Dict]:
|
||||
"""Collects plots definitions and data sources.
|
||||
|
||||
@@ -131,7 +130,6 @@ class Plots:
|
||||
targets=targets,
|
||||
revision=rev,
|
||||
onerror=onerror,
|
||||
config_files=config_files,
|
||||
props=props,
|
||||
)
|
||||
if definitions:
|
||||
@@ -186,7 +184,6 @@ class Plots:
|
||||
props=None,
|
||||
recursive=False,
|
||||
onerror=None,
|
||||
config_files: Optional[Set[str]] = None,
|
||||
):
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
@@ -198,7 +195,6 @@ class Plots:
|
||||
recursive,
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
config_files=config_files,
|
||||
):
|
||||
_resolve_data_sources(data)
|
||||
result.update(data)
|
||||
@@ -389,7 +385,7 @@ def _collect_output_plots(repo, targets, props, onerror: Optional[Callable] = No
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
dpath.util.merge(
|
||||
dpath.merge(
|
||||
result,
|
||||
{"": unpacked},
|
||||
)
|
||||
@@ -436,16 +432,13 @@ def _resolve_definitions(
|
||||
props={**plot_props, **props},
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
dpath.merge(
|
||||
result,
|
||||
unpacked,
|
||||
)
|
||||
else:
|
||||
if _matches(targets, config_path, plot_id):
|
||||
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
|
||||
dpath.util.merge(
|
||||
result, {"data": {plot_id: {**adjusted_props, **props}}}
|
||||
)
|
||||
elif _matches(targets, config_path, plot_id):
|
||||
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
|
||||
dpath.merge(result, {"data": {plot_id: {**adjusted_props, **props}}})
|
||||
|
||||
return result
|
||||
|
||||
@@ -474,7 +467,7 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
|
||||
dvcfile_defs_dict,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
dpath.merge(
|
||||
result,
|
||||
{dvcfile_path: resolved},
|
||||
)
|
||||
@@ -485,7 +478,6 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
|
||||
def _collect_definitions(
|
||||
repo: "Repo",
|
||||
targets=None,
|
||||
config_files: Optional[Set[str]] = None,
|
||||
props: Optional[Dict] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
@@ -497,39 +489,20 @@ def _collect_definitions(
|
||||
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
|
||||
if not config_files:
|
||||
dpath.util.merge(
|
||||
result,
|
||||
_collect_pipeline_files(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
dpath.merge(
|
||||
result,
|
||||
_collect_pipeline_files(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
|
||||
if targets or (not targets and not config_files):
|
||||
dpath.util.merge(
|
||||
result,
|
||||
_collect_output_plots(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
|
||||
if config_files:
|
||||
for path in config_files:
|
||||
definitions = parse(fs, path).get("data", {})
|
||||
if definitions:
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
path,
|
||||
definitions,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{path: resolved},
|
||||
)
|
||||
dpath.merge(
|
||||
result,
|
||||
_collect_output_plots(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
|
||||
for target in targets:
|
||||
if not result or fs.exists(target):
|
||||
unpacked = unpack_if_dir(fs, target, props=props, onerror=onerror)
|
||||
dpath.util.merge(result[""], unpacked)
|
||||
dpath.merge(result[""], unpacked)
|
||||
|
||||
return dict(result)
|
||||
|
||||
@@ -584,21 +557,14 @@ def _load_sv(path, fs, delimiter=",", header=True):
|
||||
with fs.open(path, "r") as fd:
|
||||
content = fd.read()
|
||||
|
||||
first_row = first(csv.reader(io.StringIO(content)))
|
||||
|
||||
if header:
|
||||
reader = csv.DictReader(io.StringIO(content), delimiter=delimiter)
|
||||
else:
|
||||
first_row = first(csv.reader(io.StringIO(content)))
|
||||
reader = csv.DictReader(
|
||||
io.StringIO(content),
|
||||
delimiter=delimiter,
|
||||
fieldnames=[str(i) for i in range(len(first_row))],
|
||||
)
|
||||
|
||||
fieldnames = reader.fieldnames or []
|
||||
data = list(reader)
|
||||
|
||||
return [
|
||||
OrderedDict([(field, data_point[field]) for field in fieldnames])
|
||||
for data_point in data
|
||||
]
|
||||
return list(reader)
|
||||
|
||||
+3
-1
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@locked
|
||||
def pull(
|
||||
def pull( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
jobs=None,
|
||||
@@ -25,6 +25,7 @@ def pull(
|
||||
run_cache=False,
|
||||
glob=False,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
allow_missing=False,
|
||||
):
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -48,6 +49,7 @@ def pull(
|
||||
with_deps=with_deps,
|
||||
force=force,
|
||||
recursive=recursive,
|
||||
allow_missing=allow_missing,
|
||||
)
|
||||
|
||||
stats["fetched"] = processed_files_count
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@locked
|
||||
def push( # noqa: C901
|
||||
def push( # noqa: C901, PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
jobs=None,
|
||||
|
||||
@@ -129,7 +129,7 @@ def reproduce( # noqa: C901
|
||||
|
||||
|
||||
def _reproduce_stages( # noqa: C901
|
||||
G, stages, downstream=False, single_item=False, on_unchanged=None, **kwargs
|
||||
graph, stages, downstream=False, single_item=False, on_unchanged=None, **kwargs
|
||||
):
|
||||
r"""Derive the evaluation of the given node for the given graph.
|
||||
|
||||
@@ -166,7 +166,7 @@ def _reproduce_stages( # noqa: C901
|
||||
|
||||
The derived evaluation of _downstream_ B would be: [B, D, E]
|
||||
"""
|
||||
steps = _get_steps(G, stages, downstream, single_item)
|
||||
steps = _get_steps(graph, stages, downstream, single_item)
|
||||
|
||||
force_downstream = kwargs.pop("force_downstream", False)
|
||||
result = []
|
||||
@@ -222,16 +222,16 @@ def _reproduce_stages( # noqa: C901
|
||||
return result
|
||||
|
||||
|
||||
def _get_steps(G, stages, downstream, single_item):
|
||||
def _get_steps(graph, stages, downstream, single_item):
|
||||
import networkx as nx
|
||||
|
||||
active = G.copy()
|
||||
active = graph.copy()
|
||||
if not single_item:
|
||||
# NOTE: frozen stages don't matter for single_item
|
||||
for stage in G:
|
||||
for stage in graph:
|
||||
if stage.frozen:
|
||||
# NOTE: disconnect frozen stage from its dependencies
|
||||
active.remove_edges_from(G.out_edges(stage))
|
||||
active.remove_edges_from(graph.out_edges(stage))
|
||||
|
||||
all_pipelines: List["Stage"] = []
|
||||
for stage in stages:
|
||||
|
||||
+16
-12
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Set, Tuple, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Set, Tuple, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
@@ -17,7 +17,6 @@ if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import TargetType
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_data.index import DataIndex, DataIndexView
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
|
||||
@@ -29,7 +28,8 @@ logger = logging.getLogger(__name__)
|
||||
def _meta_checksum(fs: "FileSystem", meta: "Meta") -> Any:
|
||||
if not meta or meta.isdir:
|
||||
return meta
|
||||
return getattr(meta, cast(str, fs.PARAM_CHECKSUM))
|
||||
assert fs.PARAM_CHECKSUM
|
||||
return getattr(meta, fs.PARAM_CHECKSUM)
|
||||
|
||||
|
||||
def worktree_view_by_remotes(
|
||||
@@ -119,12 +119,6 @@ def fetch_worktree(
|
||||
):
|
||||
remote_obj = _get_remote(repo, remote_name, remote, "fetch")
|
||||
index = view.data["repo"]
|
||||
for key in index.storage_map:
|
||||
storage = index.storage_map[key]
|
||||
storage.fs = remote_obj.fs
|
||||
storage.path = remote_obj.fs.path.join(remote_obj.path, *key)
|
||||
index.storage_map[key] = storage
|
||||
|
||||
total = len(index)
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file",
|
||||
@@ -132,7 +126,7 @@ def fetch_worktree(
|
||||
disable=total == 0,
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
transferred += save(index, callback=cb, jobs=jobs)
|
||||
transferred += save(index, callback=cb, jobs=jobs, storage="remote")
|
||||
return transferred
|
||||
|
||||
|
||||
@@ -196,6 +190,11 @@ def push_worktree(
|
||||
f"remote {remote_obj.name!r} does not support versioning"
|
||||
) from None
|
||||
|
||||
if remote_obj.index is not None:
|
||||
for key, entry in new_index.iteritems():
|
||||
remote_obj.index[key] = entry
|
||||
remote_obj.index.commit()
|
||||
|
||||
for out in view.outs:
|
||||
workspace, _key = out.index_key
|
||||
_merge_push_meta(out, repo.index.data[workspace], remote_obj.name)
|
||||
@@ -217,13 +216,15 @@ def _merge_push_meta(
|
||||
conflicts (i.e. the DVC output's version ID may not match the pushed/latest
|
||||
version ID as long when the file content of both versions is the same).
|
||||
"""
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_data.index.save import build_tree
|
||||
|
||||
_, key = out.index_key
|
||||
entry = index[key]
|
||||
repo = out.stage.repo
|
||||
if out.isdir():
|
||||
old_tree = cast("Tree", out.get_obj())
|
||||
old_tree = out.get_obj()
|
||||
assert isinstance(old_tree, Tree)
|
||||
entry.hash_info = old_tree.hash_info
|
||||
entry.meta = out.meta
|
||||
for subkey, entry in index.iteritems(key):
|
||||
@@ -357,6 +358,7 @@ def _fetch_out_changes(
|
||||
update_meta=False,
|
||||
meta_only=True,
|
||||
meta_cmp_key=partial(_meta_checksum, remote.fs),
|
||||
storage="data",
|
||||
callback=cb,
|
||||
)
|
||||
out.save()
|
||||
@@ -419,6 +421,7 @@ def _get_update_diff_index(
|
||||
remote_index: Union["DataIndex", "DataIndexView"],
|
||||
remote: "Remote",
|
||||
) -> "DataIndex":
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_data.index import DataIndex
|
||||
from dvc_data.index.diff import ADD, MODIFY, UNCHANGED, diff
|
||||
|
||||
@@ -438,7 +441,8 @@ def _get_update_diff_index(
|
||||
if out.isdir():
|
||||
if not entry.meta.isdir:
|
||||
fs_path = repo.fs.path.join(repo.root_dir, *entry.key)
|
||||
tree = cast("Tree", out.obj)
|
||||
tree = out.obj
|
||||
assert isinstance(tree, Tree)
|
||||
_, entry.hash_info = tree.get( # type: ignore[misc]
|
||||
repo.fs.path.relparts(fs_path, out.fs_path)
|
||||
)
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ def _infos_to_str(infos):
|
||||
)
|
||||
|
||||
|
||||
def _check_blockers(tmp_dir, lock, info, *, mode, waiters): # noqa: C901
|
||||
def _check_blockers(tmp_dir, lock, info, *, mode, waiters): # noqa: C901, PLR0912
|
||||
from .lock import LockError
|
||||
|
||||
non_existing_pid = set()
|
||||
|
||||
+1
-21
@@ -20,7 +20,6 @@ from scmrepo.noscm import NoSCM
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import format_link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.progress import GitProgressEvent
|
||||
@@ -58,25 +57,6 @@ class GitAuthError(SCMError):
|
||||
super().__init__(f"{reason}\n{doc}")
|
||||
|
||||
|
||||
class GitMergeError(SCMError):
|
||||
def __init__(self, msg: str, scm: Optional["Git"] = None) -> None:
|
||||
if scm and self._is_shallow(scm):
|
||||
url = format_link(
|
||||
"https://dvc.org/doc/user-guide/troubleshooting#git-shallow"
|
||||
)
|
||||
msg = (
|
||||
f"{msg}: `dvc exp` does not work in shallow Git repos. "
|
||||
f"See {url} for more information."
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
@staticmethod
|
||||
def _is_shallow(scm: "Git") -> bool:
|
||||
if os.path.exists(os.path.join(scm.root_dir, Git.GIT_DIR, "shallow")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def map_scm_exception(with_cause: bool = False) -> Iterator[None]:
|
||||
from scmrepo.exceptions import SCMError as InternalSCMError
|
||||
@@ -227,7 +207,7 @@ def _get_n_commits(scm: "Git", revs: List[str], num: int) -> List[str]:
|
||||
return results
|
||||
|
||||
|
||||
def iter_revs( # noqa: C901
|
||||
def iter_revs(
|
||||
scm: "Git",
|
||||
revs: Optional[List[str]] = None,
|
||||
num: int = 1,
|
||||
|
||||
@@ -140,7 +140,7 @@ class Stage(params.StageParams):
|
||||
# pylint:disable=no-value-for-parameter
|
||||
# rwlocked() confuses pylint
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
repo,
|
||||
path=None,
|
||||
@@ -545,13 +545,13 @@ class Stage(params.StageParams):
|
||||
)
|
||||
|
||||
@rwlocked(write=["outs"])
|
||||
def commit(self, allow_missing=False, filter_info=None) -> None:
|
||||
def commit(self, allow_missing=False, filter_info=None, **kwargs) -> None:
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
link_failures = []
|
||||
for out in self.filter_outs(filter_info):
|
||||
try:
|
||||
out.commit(filter_info=filter_info)
|
||||
out.commit(filter_info=filter_info, **kwargs)
|
||||
except OutputDoesNotExistError:
|
||||
if not (allow_missing or out.checkpoint):
|
||||
raise
|
||||
|
||||
@@ -29,7 +29,7 @@ class MonitorTask:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def SIGNAL_FILE(self) -> str:
|
||||
def SIGNAL_FILE(self) -> str: # noqa: N802
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
cast,
|
||||
no_type_check,
|
||||
)
|
||||
|
||||
@@ -24,7 +23,6 @@ from .utils import resolve_wdir, split_params_deps
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.stage import PipelineStage, Stage
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
PARAM_PARAMS = ParamsDependency.PARAM_PARAMS
|
||||
PARAM_PATH = ParamsDependency.PARAM_PATH
|
||||
@@ -155,6 +153,7 @@ def to_pipeline_file(stage: "PipelineStage"):
|
||||
|
||||
def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
|
||||
from dvc.output import _serialize_tree_obj_to_files, split_file_meta_from_cloud
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
assert stage.cmd
|
||||
|
||||
@@ -163,7 +162,7 @@ def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
|
||||
if item.hash_info.isdir and kwargs.get("with_files"):
|
||||
obj = item.obj or item.get_obj()
|
||||
if obj:
|
||||
obj = cast("Tree", obj)
|
||||
assert isinstance(obj, Tree)
|
||||
ret[item.PARAM_FILES] = [
|
||||
split_file_meta_from_cloud(f)
|
||||
for f in _serialize_tree_obj_to_files(obj)
|
||||
|
||||
+30
-33
@@ -26,8 +26,6 @@ __all__ = [
|
||||
"workspace",
|
||||
"make_workspace",
|
||||
"local_workspace",
|
||||
"docker",
|
||||
"docker_compose",
|
||||
"docker_compose_project_name",
|
||||
"docker_services",
|
||||
]
|
||||
@@ -215,48 +213,47 @@ def local_workspace(make_workspace):
|
||||
return make_workspace("workspace", typ="local")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def docker():
|
||||
# See https://travis-ci.community/t/docker-linux-containers-on-windows/301
|
||||
if os.environ.get("CI") and os.name == "nt":
|
||||
pytest.skip("disabled for Windows on Github Actions")
|
||||
|
||||
try:
|
||||
subprocess.check_output("docker ps", shell=True)
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
pytest.skip("no docker installed")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def docker_compose(docker): # noqa: ARG001
|
||||
try:
|
||||
subprocess.check_output("docker-compose version", shell=True)
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
pytest.skip("no docker-compose installed")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def docker_compose_project_name():
|
||||
return "pytest-dvc-test"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def docker_services(docker_compose_file, docker_compose_project_name, tmp_path_factory):
|
||||
# overriding `docker_services` fixture from `pytest_docker` plugin to
|
||||
# only launch docker images once.
|
||||
|
||||
def docker_services(
|
||||
tmp_path_factory,
|
||||
docker_compose_command,
|
||||
docker_compose_file,
|
||||
docker_compose_project_name,
|
||||
docker_setup,
|
||||
):
|
||||
from filelock import FileLock
|
||||
|
||||
# pylint: disable-next=import-error
|
||||
from pytest_docker.plugin import DockerComposeExecutor, Services
|
||||
|
||||
executor = DockerComposeExecutor(docker_compose_file, docker_compose_project_name)
|
||||
if os.environ.get("CI") and os.name == "nt":
|
||||
pytest.skip("disabled for Windows on CI")
|
||||
|
||||
try:
|
||||
subprocess.check_output("docker ps", stderr=subprocess.STDOUT, shell=True)
|
||||
except subprocess.CalledProcessError as err:
|
||||
out = (err.output or b"").decode("utf-8")
|
||||
pytest.skip(f"docker is not installed or the daemon is not running: {out}")
|
||||
|
||||
try:
|
||||
cmd = "docker-compose version"
|
||||
subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
|
||||
except subprocess.CalledProcessError as err:
|
||||
out = (err.output or b"").decode("utf-8")
|
||||
pytest.skip(f"docker-compose is not installed: {out}")
|
||||
|
||||
executor = DockerComposeExecutor(
|
||||
docker_compose_command, docker_compose_file, docker_compose_project_name
|
||||
)
|
||||
|
||||
# making sure we don't accidentally launch docker-compose in parallel,
|
||||
# as it might result in network conflicts. Inspired by:
|
||||
# https://github.com/pytest-dev/pytest-xdist#making-session-scoped-fixtures-execute-only-once
|
||||
lockfile = tmp_path_factory.getbasetemp().parent / "docker-compose.lock"
|
||||
with FileLock(str(lockfile)): # pylint:disable=abstract-class-instantiated
|
||||
executor.execute("up --build -d")
|
||||
|
||||
return Services(executor)
|
||||
with FileLock(os.fspath(lockfile)):
|
||||
executor.execute(docker_setup)
|
||||
# note: we are not tearing down the containers here
|
||||
return Services(executor)
|
||||
|
||||
@@ -282,3 +282,85 @@ class TestGetUrl:
|
||||
def test_get_url_nonexistent(self, cloud):
|
||||
with pytest.raises(URLMissingError):
|
||||
Repo.get_url(str(cloud / "nonexistent"), config=cloud.config)
|
||||
|
||||
|
||||
class TestToRemote:
|
||||
def test_add_to_remote(self, tmp_dir, dvc, remote, workspace):
|
||||
workspace.gen("foo", "foo")
|
||||
|
||||
url = "remote://workspace/foo"
|
||||
[stage] = dvc.add(url, to_remote=True)
|
||||
|
||||
assert not (tmp_dir / "foo").exists()
|
||||
assert (tmp_dir / "foo.dvc").exists()
|
||||
|
||||
assert len(stage.deps) == 0
|
||||
assert len(stage.outs) == 1
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
meta = stage.outs[0].meta
|
||||
assert hash_info.name == "md5"
|
||||
assert hash_info.value == "acbd18db4cc2f85cedef654fccc4a4d8"
|
||||
assert (remote / "ac" / "bd18db4cc2f85cedef654fccc4a4d8").read_text() == "foo"
|
||||
assert meta.size == len("foo")
|
||||
|
||||
def test_import_url_to_remote_file(self, tmp_dir, dvc, workspace, remote):
|
||||
workspace.gen("foo", "foo")
|
||||
|
||||
url = "remote://workspace/foo"
|
||||
stage = dvc.imp_url(url, to_remote=True)
|
||||
|
||||
assert stage.deps[0].hash_info.value is not None
|
||||
assert not (tmp_dir / "foo").exists()
|
||||
assert (tmp_dir / "foo.dvc").exists()
|
||||
|
||||
assert len(stage.deps) == 1
|
||||
assert stage.deps[0].def_path == url
|
||||
assert len(stage.outs) == 1
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
assert hash_info.name == "md5"
|
||||
assert hash_info.value == "acbd18db4cc2f85cedef654fccc4a4d8"
|
||||
assert (remote / "ac" / "bd18db4cc2f85cedef654fccc4a4d8").read_text() == "foo"
|
||||
assert stage.outs[0].meta.size == len("foo")
|
||||
|
||||
def test_import_url_to_remote_dir(self, tmp_dir, dvc, workspace, remote):
|
||||
import json
|
||||
|
||||
workspace.gen(
|
||||
{
|
||||
"data": {
|
||||
"foo": "foo",
|
||||
"bar": "bar",
|
||||
"sub_dir": {"baz": "sub_dir/baz"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
url = "remote://workspace/data"
|
||||
stage = dvc.imp_url(url, to_remote=True)
|
||||
|
||||
assert not (tmp_dir / "data").exists()
|
||||
assert (tmp_dir / "data.dvc").exists()
|
||||
|
||||
assert len(stage.deps) == 1
|
||||
assert stage.deps[0].def_path == url
|
||||
assert len(stage.outs) == 1
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
assert hash_info.name == "md5"
|
||||
assert hash_info.value == "55d05978954d1b2cd7b06aedda9b9e43.dir"
|
||||
file_parts = json.loads(
|
||||
(remote / "55" / "d05978954d1b2cd7b06aedda9b9e43.dir").read_text()
|
||||
)
|
||||
|
||||
assert len(file_parts) == 3
|
||||
assert {file_part["relpath"] for file_part in file_parts} == {
|
||||
"foo",
|
||||
"bar",
|
||||
"sub_dir/baz",
|
||||
}
|
||||
|
||||
for file_part in file_parts:
|
||||
md5 = file_part["md5"]
|
||||
assert (remote / md5[:2] / md5[2:]).read_text() == file_part["relpath"]
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ class Console:
|
||||
styled=styled,
|
||||
)
|
||||
|
||||
def write_json(
|
||||
def write_json( # noqa: PLR0913
|
||||
self,
|
||||
data: Any,
|
||||
indent: Optional[int] = None,
|
||||
|
||||
@@ -14,8 +14,8 @@ def apply_diff(src, dest): # noqa: C901
|
||||
|
||||
Used in Stage load/dump cycle to preserve comments and custom formatting.
|
||||
"""
|
||||
Seq = (list, tuple)
|
||||
Container = (Mapping, list, tuple)
|
||||
Seq = (list, tuple) # noqa: N806
|
||||
Container = (Mapping, list, tuple) # noqa: N806
|
||||
|
||||
def is_same_type(a, b):
|
||||
return any(
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import (
|
||||
Protocol,
|
||||
TextIO,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from funcy import reraise
|
||||
@@ -97,8 +96,7 @@ def _modify_data(
|
||||
dumper: DumpersFn,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
):
|
||||
exists_fn = cast(Callable[["StrPath"], bool], fs.exists if fs else os.path.exists)
|
||||
file_exists = exists_fn(cast(str, path))
|
||||
file_exists = fs.exists(os.fspath(path)) if fs else os.path.exists(path)
|
||||
data = _load_data(path, parser=parser, fs=fs) if file_exists else {}
|
||||
yield data
|
||||
_dump_data(path, data, dumper=dumper, fs=fs)
|
||||
|
||||
@@ -111,7 +111,7 @@ def _ast_tree_to_dict(tree, only_self_params=False, lineno=False):
|
||||
return result
|
||||
|
||||
|
||||
def _ast_assign_to_dict(assign, only_self_params=False, lineno=False):
|
||||
def _ast_assign_to_dict(assign, only_self_params=False, lineno=False): # noqa: PLR0912
|
||||
result = {}
|
||||
|
||||
if isinstance(assign, ast.AnnAssign):
|
||||
|
||||
@@ -169,7 +169,7 @@ def determine_linecol(
|
||||
number of steps upward to just 5. If it does not find any linecols, it'll
|
||||
abort.
|
||||
"""
|
||||
from dpath.util import get
|
||||
from dpath import get
|
||||
|
||||
step = 1
|
||||
line, col = None, None
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# pylint: disable=unused-import
|
||||
try:
|
||||
from ._dvc_version import version as __version__
|
||||
from ._dvc_version import version_tuple # noqa: F401
|
||||
from ._dvc_version import version_tuple
|
||||
except ImportError:
|
||||
__version__ = "UNKNOWN"
|
||||
version_tuple = (0, 0, __version__) # type: ignore[assignment]
|
||||
|
||||
+168
-163
@@ -1,65 +1,25 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61", "setuptools_scm[toml]>=7"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["setuptools>=61", "setuptools_scm[toml]>=7"]
|
||||
|
||||
[project]
|
||||
name = "dvc"
|
||||
description = "Git for data scientists - manage your code and data together"
|
||||
readme = "README.rst"
|
||||
keywords = [
|
||||
"ai",
|
||||
"collaboration",
|
||||
"data-science",
|
||||
"data-version-control",
|
||||
"machine-learning",
|
||||
"git",
|
||||
"developer-tools",
|
||||
"git",
|
||||
"machine-learning",
|
||||
"reproducibility",
|
||||
"collaboration",
|
||||
"ai",
|
||||
]
|
||||
license = { text = "Apache License 2.0" }
|
||||
authors = [{ name = "Dmitry Petrov", email = "[email protected]" }]
|
||||
maintainers = [{ name = "Iterative", email = "[email protected]" }]
|
||||
authors = [{ name = "Dmitry Petrov", email = "[email protected]" }]
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"colorama>=0.3.9",
|
||||
"configobj>=5.0.6",
|
||||
"voluptuous>=0.11.7",
|
||||
"requests>=2.22.0",
|
||||
"grandalf>=0.7,<1",
|
||||
"distro>=1.3.0",
|
||||
"appdirs>=1.4.3",
|
||||
"ruamel.yaml>=0.17.11",
|
||||
"tomlkit>=0.11.1",
|
||||
"funcy>=1.14",
|
||||
"pathspec>=0.10.3",
|
||||
"shortuuid>=0.5.0",
|
||||
"tqdm>=4.63.1,<5",
|
||||
"packaging>=19.0",
|
||||
"zc.lockfile>=1.2.1",
|
||||
"flufl.lock>=5",
|
||||
"networkx>=2.5",
|
||||
"psutil>=5.8.0",
|
||||
"pydot>=1.2.4",
|
||||
"importlib-resources>=5.2.2; python_version < '3.9'",
|
||||
"flatten_dict>=0.4.1,<1",
|
||||
"tabulate>=0.8.7",
|
||||
"pygtrie>=2.3.2",
|
||||
"dpath>=2.0.2,<3",
|
||||
"shtab>=1.3.4,<2",
|
||||
"rich>=10.13.0",
|
||||
"pyparsing>=2.4.7",
|
||||
"typing-extensions>=3.7.4",
|
||||
"scmrepo==0.1.9",
|
||||
"dvc-render==0.1.1",
|
||||
"dvc-task==0.1.11",
|
||||
"dvclive>=1.2.2",
|
||||
"dvc-data==0.39.3",
|
||||
"dvc-http",
|
||||
"hydra-core>=1.1.0",
|
||||
"iterative-telemetry==0.0.7",
|
||||
"dvc-studio-client>=0.1.1"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
@@ -68,134 +28,140 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
|
||||
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-http",
|
||||
"dvc-render>=0.1.2",
|
||||
"dvc-studio-client>=0.1.1",
|
||||
"dvc-task>=0.2.0,<1",
|
||||
"flatten_dict<1,>=0.4.1",
|
||||
"flufl.lock>=5",
|
||||
"funcy>=1.14",
|
||||
"grandalf<1,>=0.7",
|
||||
"hydra-core>=1.1",
|
||||
"iterative-telemetry>=0.0.7",
|
||||
"networkx>=2.5",
|
||||
"packaging>=19",
|
||||
"pathspec>=0.10.3",
|
||||
"psutil>=5.8",
|
||||
"pydot>=1.2.4",
|
||||
"pygtrie>=2.3.2",
|
||||
"pyparsing>=2.4.7",
|
||||
"requests>=2.22",
|
||||
"rich>=12",
|
||||
"ruamel.yaml>=0.17.11",
|
||||
"scmrepo>=0.1.15,<1",
|
||||
"shortuuid>=0.5",
|
||||
"shtab<2,>=1.3.4",
|
||||
"tabulate>=0.8.7",
|
||||
"tomlkit>=0.11.1",
|
||||
"tqdm<5,>=4.63.1",
|
||||
"voluptuous>=0.11.7",
|
||||
"zc.lockfile>=1.2.1",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
|
||||
azure = ["dvc-azure==2.21.1"]
|
||||
dev = ["dvc[azure,gdrive,gs,hdfs,lint,oss,s3,ssh,terraform,tests,webdav,webhdfs]"]
|
||||
gdrive = ["dvc-gdrive==2.19.1"]
|
||||
gs = ["dvc-gs==2.21.1"]
|
||||
hdfs = ["dvc-hdfs==2.19.0"]
|
||||
oss = ["dvc-oss==2.19.0"]
|
||||
s3 = ["dvc-s3==2.21.0"]
|
||||
ssh = ["dvc-ssh==2.21.0"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]==2.21.0"]
|
||||
webdav = ["dvc-webdav==2.19.1"]
|
||||
webhdfs = ["dvc-webhdfs==2.19.0"]
|
||||
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19.0"]
|
||||
terraform = ["tpi[ssh]>=2.1.0"]
|
||||
testing = ["pytest-test-utils==0.0.8"]
|
||||
tests = [
|
||||
"tpi[ssh]>=2.1.0",
|
||||
"dvc-ssh==2.21.0",
|
||||
"pytest-test-utils==0.0.8",
|
||||
# Test requirements
|
||||
"pytest==7.2.1",
|
||||
"pytest-cov==4.0.0",
|
||||
"pytest-xdist==3.2.0",
|
||||
"pytest-mock==3.10.0",
|
||||
"pytest-lazy-fixture==0.6.3",
|
||||
# https://github.com/docker/docker-py/issues/2902
|
||||
"pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'",
|
||||
"flaky==3.7.0",
|
||||
"pytest-timeout==2.1.0",
|
||||
"filelock==3.9.0",
|
||||
"beautifulsoup4==4.11.2",
|
||||
"pywin32>=225; sys_platform == 'win32'", # optional dependency
|
||||
"pylint==2.16.1",
|
||||
# type-checking
|
||||
"mypy==0.991",
|
||||
"types-requests",
|
||||
"types-tabulate",
|
||||
"types-toml",
|
||||
gs = ["dvc-gs==2.22.0"]
|
||||
hdfs = ["dvc-hdfs==2.19"]
|
||||
lint = [
|
||||
"mypy==1.0.1",
|
||||
"pylint==2.16.3",
|
||||
"types-appdirs",
|
||||
"types-colorama",
|
||||
"types-psutil",
|
||||
"types-requests",
|
||||
"types-tabulate",
|
||||
"types-toml",
|
||||
"types-tqdm",
|
||||
]
|
||||
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
|
||||
dev = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs,tests]"]
|
||||
|
||||
oss = ["dvc-oss==2.19"]
|
||||
s3 = ["dvc-s3==2.21"]
|
||||
ssh = ["dvc-ssh==2.21"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]==2.21"]
|
||||
terraform = ["tpi[ssh]>=2.1"]
|
||||
testing = ["pytest-test-utils"]
|
||||
tests = [
|
||||
"beautifulsoup4>=4.4",
|
||||
"dvc-ssh",
|
||||
"filelock",
|
||||
"flaky",
|
||||
"pytest<8,>=7",
|
||||
"pytest-cov",
|
||||
"pytest-docker>=1,<2",
|
||||
"pytest-lazy-fixture",
|
||||
"pytest-mock",
|
||||
"pytest-test-utils",
|
||||
"pytest-timeout>=2",
|
||||
"pytest-xdist>=3.2",
|
||||
'pywin32>=225; sys_platform == "win32"', # optional test dependency
|
||||
]
|
||||
webdav = ["dvc-webdav==2.19.1"]
|
||||
webhdfs = ["dvc-webhdfs==2.19"]
|
||||
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19"]
|
||||
[project.urls]
|
||||
Documentation = "https://dvc.org/doc"
|
||||
"Bug Tracker" = "https://github.com/iterative/dvc/issues"
|
||||
Issues = "https://github.com/iterative/dvc/issues"
|
||||
Source = "https://github.com/iterative/dvc"
|
||||
|
||||
[project.scripts]
|
||||
dvc = "dvc.cli:main"
|
||||
|
||||
[project.entry-points."fsspec.specs"]
|
||||
dvc = "dvc.api:DVCFileSystem"
|
||||
|
||||
[tool.setuptools]
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[tool.setuptools.packages]
|
||||
find = {namespaces = false, exclude=["tests", "tests.*"]}
|
||||
[tool.setuptools.packages.find]
|
||||
exclude = ["tests", "tests.*"]
|
||||
namespaces = false
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "dvc/_dvc_version.py"
|
||||
|
||||
[tool.black]
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
known_first_party = ["dvc", "dvc_data", "dvc_objects", "dvc_render", "dvc_task", "tests"]
|
||||
line_length = 88
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py38"
|
||||
show-source = true
|
||||
ignore = ["S101", "PT004", "PT007", "PT019", "SIM105", "SIM108", "SIM110", "SIM111", "SIM117", "TRY003", "TRY200", "TRY300", "TRY301", "PLR0913", "PLR2004", "RUF005"]
|
||||
select = ["F", "E", "W", "C90", "UP", "YTT", "S", "BLE", "B", "A", "C4", "T10", "ISC", "ICN", "G", "INP", "PIE", "T20", "PT", "Q", "RET501", "RET504", "SIM", "TID", "TCH", "ARG", "PGH", "PLC", "PLE", "PLR", "PLW", "TRY", "RUF"]
|
||||
# external flake8 codes that should be preserved
|
||||
external = ["B301", "B024", "C901", "E302", "F401", "W601"]
|
||||
|
||||
[tool.ruff.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
parametrize-names-type = "csv"
|
||||
raises-extend-require-match-for = ["dvc.exceptions.DvcException", "dvc.scm.SCMError", "scmrepo.exceptions.SCMError"]
|
||||
|
||||
[tool.ruff.flake8-tidy-imports]
|
||||
[tool.ruff.flake8-tidy-imports.banned-api]
|
||||
"funcy.cached_property".msg = "use `from dvc.utils.objects import cached_property` instead."
|
||||
|
||||
[tool.ruff.flake8-type-checking]
|
||||
exempt-modules = ["typing"]
|
||||
strict = true
|
||||
|
||||
[tool.ruff.flake8-unused-arguments]
|
||||
ignore-variadic-names = true
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"dvc/testing/*.py" = ["ARG002"]
|
||||
"scripts/**/*.py" = ["T201", "INP001", "TRY002"]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-ra --cov-config pyproject.toml --dist worksteal"
|
||||
filterwarnings = [
|
||||
"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",
|
||||
# also relates to botocore, but looks like it's not going to be fixed
|
||||
# https://github.com/boto/botocore/issues/2744
|
||||
"ignore:'urllib3.contrib.pyopenssl' module is deprecated:DeprecationWarning",
|
||||
# ruamel.yaml: https://sourceforge.net/p/ruamel-yaml/tickets/452/
|
||||
# google.cloud: https://github.com/googleapis/python-storage/issues/1000
|
||||
# google.logging: https://github.com/googleapis/python-logging/issues/730
|
||||
# Also happens with `zc.lockfile`.
|
||||
"ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning",
|
||||
# see https://github.com/celery/kombu/issues/1339
|
||||
# and https://github.com/celery/celery/issues/7528
|
||||
# drop when celery==5.3 && kombu==5.3 releases
|
||||
"ignore:SelectableGroups dict interface is deprecated:DeprecationWarning",
|
||||
# see https://github.com/networkx/networkx/issues/5723.
|
||||
"ignore:nx.nx_pydot.* depends on the pydot package, which has.*known issues and is not actively maintained:DeprecationWarning",
|
||||
# remove when new version of pytest-cov gets released
|
||||
# https://github.com/pytest-dev/pytest-cov/issues/557
|
||||
"ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning",
|
||||
]
|
||||
log_level = "debug"
|
||||
addopts = "-ra --cov-config pyproject.toml"
|
||||
markers = [
|
||||
"needs_internet: Might need network access for the tests",
|
||||
"vscode: Tests verifying contract between DVC and VSCode plugin",
|
||||
"studio: Tests verifying contract between DVC and Studio",
|
||||
"vscode: Tests verifying contract between DVC and VSCode plugin",
|
||||
]
|
||||
xfail_strict = true
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = ["error::pytest_mock.PytestMockWarning"]
|
||||
xfail_strict = true
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
@@ -205,46 +171,49 @@ source = ["dvc", "tests"]
|
||||
source = ["dvc"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if __name__ == .__main__.:",
|
||||
"if typing.TYPE_CHECKING:",
|
||||
"if TYPE_CHECKING:",
|
||||
"raise NotImplementedError",
|
||||
"raise AssertionError",
|
||||
"if typing.TYPE_CHECKING:",
|
||||
"@overload",
|
||||
"pragma: no cover",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
show_missing = true
|
||||
|
||||
[tool.mypy]
|
||||
# Error output
|
||||
check_untyped_defs = true
|
||||
files = ["dvc"]
|
||||
no_implicit_optional = true
|
||||
pretty = true
|
||||
show_column_numbers = true
|
||||
show_error_codes = true
|
||||
show_error_context = true
|
||||
show_traceback = true
|
||||
pretty = true
|
||||
no_implicit_optional = true
|
||||
check_untyped_defs = true
|
||||
strict_equality = true
|
||||
strict_concatenate = true
|
||||
# Warnings
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_configs = true
|
||||
warn_unreachable = true
|
||||
ignore_missing_imports = true
|
||||
files = ["dvc"]
|
||||
warn_unused_configs = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
ignore_missing_imports = true
|
||||
module = [
|
||||
"celery.*",
|
||||
"configobj.*",
|
||||
"dpath.*",
|
||||
"dvc_http",
|
||||
"dvc_render.*",
|
||||
"dvc_ssh",
|
||||
"dvc_studio_client.*",
|
||||
"flatten_dict",
|
||||
"fsspec.*",
|
||||
"funcy.*", # https://github.com/Suor/funcy/issues/106,
|
||||
"grandalf.*",
|
||||
"ipdb",
|
||||
"iterative_telemetry",
|
||||
"kombu.*",
|
||||
"networkx.*", # https://github.com/networkx/networkx/issues/3988
|
||||
"pygtrie.*",
|
||||
@@ -258,25 +227,61 @@ module = [
|
||||
"yappi",
|
||||
"zc.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
|
||||
|
||||
[tool.pylint.message_control]
|
||||
disable = [
|
||||
"format", "refactoring", "spelling", "design", "invalid-name", "fixme",
|
||||
"duplicate-code", "unused-wildcard-import", "cyclic-import", "wrong-import-order",
|
||||
"wrong-import-position", "ungrouped-imports", "multiple-imports",
|
||||
"missing-function-docstring", "missing-module-docstring",
|
||||
"missing-class-docstring", "raise-missing-from", "import-outside-toplevel",
|
||||
"cyclic-import", "design", "duplicate-code", "fixme", "format", "import-outside-toplevel", "invalid-name",
|
||||
"missing-class-docstring", "missing-function-docstring", "missing-module-docstring", "multiple-imports",
|
||||
"raise-missing-from", "refactoring", "spelling",
|
||||
"ungrouped-imports", "unused-wildcard-import", "wrong-import-order", "wrong-import-position",
|
||||
]
|
||||
enable = ["c-extension-no-member", "no-else-return"]
|
||||
|
||||
[tool.pylint.typecheck]
|
||||
generated-members = ["pytest.lazy_fixture", "logging.TRACE", "logger.trace", "sys.getwindowsversion", "argparse.Namespace"]
|
||||
generated-members = ["argparse.Namespace", "logger.trace", "logging.TRACE", "pytest.lazy_fixture", "sys.getwindowsversion"]
|
||||
signature-mutators = ["funcy.decorators.decorator"]
|
||||
|
||||
[tool.pylint.variables]
|
||||
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)"
|
||||
ignored-argument-names = "_.*|args|kwargs"
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
|
||||
[tool.ruff]
|
||||
# external flake8 codes that should be preserved
|
||||
external = ["B301", "C901", "E302", "W601"]
|
||||
ignore = ["N818", "S101", "PT004", "PT007", "PT019", "SIM105", "SIM108", "SIM110", "SIM117", "TRY003", "TRY200", "TRY300", "TRY301", "PLR2004", "PLW2901", "RUF005"]
|
||||
select = ["F", "E", "W", "C90", "N", "UP", "YTT", "S", "BLE", "B", "A", "C4", "T10", "ISC", "ICN", "G", "INP", "PIE", "T20", "PT", "Q", "RET501", "RET504", "SIM", "TID", "TCH", "ARG", "PGH", "PLC", "PLE", "PLR", "PLW", "TRY", "RUF"]
|
||||
show-source = true
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
parametrize-names-type = "csv"
|
||||
raises-extend-require-match-for = ["dvc.exceptions.DvcException", "dvc.scm.SCMError", "scmrepo.exceptions.SCMError"]
|
||||
|
||||
[tool.ruff.flake8-tidy-imports]
|
||||
[tool.ruff.flake8-tidy-imports.banned-api]
|
||||
"funcy.cached_property" = {msg = "use `from dvc.utils.objects import cached_property` instead."}
|
||||
|
||||
[tool.ruff.flake8-type-checking]
|
||||
strict = true
|
||||
|
||||
[tool.ruff.flake8-unused-arguments]
|
||||
ignore-variadic-names = true
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["dvc", "dvc_data", "dvc_objects", "dvc_render", "dvc_task", "tests"]
|
||||
|
||||
[tool.ruff.pep8-naming]
|
||||
ignore-names = ["M", "SCM"]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"dvc/commands/**" = ["N806"]
|
||||
"dvc/testing/**" = ["ARG002"]
|
||||
"scripts/**" = ["T201", "INP001", "TRY002"]
|
||||
|
||||
[tool.ruff.pylint]
|
||||
max-args = 10
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# NOTE: using strict version due to
|
||||
# https://github.com/iterative/dvc/issues/7949
|
||||
PyInstaller==5.7.0
|
||||
PyInstaller==5.8.0
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ if __name__ == "__main__":
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-n=auto", *sys.argv[1:]]))
|
||||
sys.exit(pytest.main(["-v", "-n=logical", *sys.argv[1:]]))
|
||||
|
||||
@@ -10,4 +10,4 @@ services:
|
||||
- 2222
|
||||
volumes:
|
||||
- ./remotes/user.key.pub:/tmp/key
|
||||
- ./remotes/git-init:/config/custom-cont-init.d
|
||||
- ./remotes/git-init:/custom-cont-init.d
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc import api
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def params_repo(tmp_dir, scm, dvc):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
tmp_dir.gen("params.json", '{"bar": 2, "foobar": 3}')
|
||||
tmp_dir.gen("other_params.json", '{"foo": {"bar": 4}}')
|
||||
|
||||
dvc.run(
|
||||
name="stage-0",
|
||||
cmd="echo stage-0",
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-1",
|
||||
cmd="echo stage-1",
|
||||
params=["foo", "params.json:bar"],
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-2",
|
||||
cmd="echo stage-2",
|
||||
params=["other_params.json:foo"],
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-3",
|
||||
cmd="echo stage-2",
|
||||
params=["params.json:foobar"],
|
||||
)
|
||||
|
||||
scm.add(
|
||||
[
|
||||
"params.yaml",
|
||||
"params.json",
|
||||
"other_params.json",
|
||||
"dvc.yaml",
|
||||
"dvc.lock",
|
||||
]
|
||||
)
|
||||
scm.commit("commit dvc files")
|
||||
|
||||
tmp_dir.gen("params.yaml", "foo: 5")
|
||||
scm.add(["params.yaml"])
|
||||
scm.commit("update params.yaml")
|
||||
|
||||
|
||||
def test_params_show_no_args(params_repo):
|
||||
assert api.params_show() == {
|
||||
"params.yaml:foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_targets(params_repo):
|
||||
assert api.params_show("params.yaml") == {"foo": 5}
|
||||
assert api.params_show("params.yaml", "params.json") == {
|
||||
"foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
}
|
||||
assert api.params_show("params.yaml", stages="stage-1") == {
|
||||
"foo": 5,
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_deps(params_repo):
|
||||
params = api.params_show(deps=True)
|
||||
assert params == {
|
||||
"params.yaml:foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_stages(params_repo):
|
||||
assert api.params_show(stages="stage-2") == {"foo": {"bar": 4}}
|
||||
|
||||
assert api.params_show() == api.params_show(
|
||||
stages=["stage-1", "stage-2", "stage-3"]
|
||||
)
|
||||
|
||||
assert api.params_show("params.json", stages="stage-3") == {"foobar": 3}
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show(stages="stage-0")
|
||||
|
||||
|
||||
def test_params_show_stage_addressing(tmp_dir, dvc):
|
||||
for subdir in {"subdir1", "subdir2"}:
|
||||
subdir = tmp_dir / subdir
|
||||
subdir.mkdir()
|
||||
with subdir.chdir():
|
||||
subdir.gen("params.yaml", "foo: 1")
|
||||
|
||||
dvc.run(name="stage-0", cmd="echo stage-0", params=["foo"])
|
||||
|
||||
for s in {"subdir1", "subdir2"}:
|
||||
dvcyaml = os.path.join(s, "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
with subdir.chdir():
|
||||
nested = subdir / "nested"
|
||||
nested.mkdir()
|
||||
with nested.chdir():
|
||||
dvcyaml = os.path.join("..", "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
|
||||
def test_params_show_revs(params_repo):
|
||||
assert api.params_show(rev="HEAD~1") == {
|
||||
"params.yaml:foo": 1,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_while_running_stage(tmp_dir, dvc):
|
||||
(tmp_dir / "params.yaml").dump({"foo": {"bar": 1}})
|
||||
(tmp_dir / "params.json").dump({"bar": 2})
|
||||
|
||||
tmp_dir.gen(
|
||||
"merge.py",
|
||||
dedent(
|
||||
"""
|
||||
import json
|
||||
from dvc import api
|
||||
with open("merged.json", "w") as f:
|
||||
json.dump(api.params_show(stages="merge"), f)
|
||||
"""
|
||||
),
|
||||
)
|
||||
dvc.stage.add(
|
||||
name="merge",
|
||||
cmd="python merge.py",
|
||||
params=["foo.bar", {"params.json": ["bar"]}],
|
||||
outs=["merged.json"],
|
||||
)
|
||||
|
||||
dvc.reproduce()
|
||||
|
||||
assert (tmp_dir / "merged.json").parse() == {"foo": {"bar": 1}, "bar": 2}
|
||||
|
||||
|
||||
def test_params_show_repo(tmp_dir, erepo_dir):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.scm_gen("params.yaml", "foo: 1", commit="Create params.yaml")
|
||||
erepo_dir.dvc.run(
|
||||
name="stage-1",
|
||||
cmd="echo stage-1",
|
||||
params=["foo"],
|
||||
)
|
||||
assert api.params_show(repo=erepo_dir) == {"foo": 1}
|
||||
|
||||
|
||||
def test_params_show_no_params_found(tmp_dir, dvc):
|
||||
# Empty repo
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show()
|
||||
|
||||
# params.yaml but no dvc.yaml
|
||||
(tmp_dir / "params.yaml").dump({"foo": 1})
|
||||
assert api.params_show() == {"foo": 1}
|
||||
|
||||
# dvc.yaml but no params.yaml
|
||||
(tmp_dir / "params.yaml").unlink()
|
||||
dvc.stage.add(name="echo", cmd="echo foo")
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show()
|
||||
|
||||
|
||||
def test_params_show_stage_without_params(tmp_dir, dvc):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
dvc.run(
|
||||
name="stage-0",
|
||||
cmd="echo stage-0",
|
||||
)
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show(stages="stage-0")
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show(deps=True)
|
||||
|
||||
|
||||
def test_params_show_untracked_target(params_repo, tmp_dir):
|
||||
tmp_dir.gen("params_foo.yaml", "foo: 1")
|
||||
|
||||
assert api.params_show("params_foo.yaml") == {"foo": 1}
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show("params_foo.yaml", stages="stage-0")
|
||||
|
||||
with pytest.raises(DvcException, match="No params found"):
|
||||
api.params_show("params_foo.yaml", deps=True)
|
||||
@@ -0,0 +1,363 @@
|
||||
import json
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc import api
|
||||
|
||||
TRAIN_METRICS: List[Dict[str, Dict[str, float]]] = [
|
||||
{
|
||||
"avg_prec": {"train": 0.85, "val": 0.75},
|
||||
"roc_auc": {"train": 0.80, "val": 0.70},
|
||||
},
|
||||
{
|
||||
"avg_prec": {"train": 0.97, "val": 0.92},
|
||||
"roc_auc": {"train": 0.98, "val": 0.94},
|
||||
},
|
||||
]
|
||||
TEST_METRICS: List[Dict[str, Dict[str, float]]] = [
|
||||
{"avg_prec": {"test": 0.72}, "roc_auc": {"test": 0.77}},
|
||||
{
|
||||
"avg_prec": {"test": 0.91},
|
||||
"roc_auc": {"test": 0.92},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def params_repo(tmp_dir, scm, dvc):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
tmp_dir.gen("params.json", '{"bar": 2, "foobar": 3}')
|
||||
tmp_dir.gen("other_params.json", '{"foo": {"bar": 4}}')
|
||||
|
||||
dvc.run(
|
||||
name="stage-0",
|
||||
cmd="echo stage-0",
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-1",
|
||||
cmd="echo stage-1",
|
||||
params=["foo", "params.json:bar"],
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-2",
|
||||
cmd="echo stage-2",
|
||||
params=["other_params.json:foo"],
|
||||
)
|
||||
|
||||
dvc.run(
|
||||
name="stage-3",
|
||||
cmd="echo stage-2",
|
||||
params=["params.json:foobar"],
|
||||
)
|
||||
|
||||
scm.add(
|
||||
[
|
||||
"params.yaml",
|
||||
"params.json",
|
||||
"other_params.json",
|
||||
"dvc.yaml",
|
||||
"dvc.lock",
|
||||
]
|
||||
)
|
||||
scm.commit("commit dvc files")
|
||||
|
||||
tmp_dir.gen("params.yaml", "foo: 5")
|
||||
scm.add(["params.yaml"])
|
||||
scm.commit("update params.yaml")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def metrics_repo(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
dvc.run(name="prepare", cmd="echo preparing data")
|
||||
scm.add(["dvc.yaml", "dvc.lock"])
|
||||
scm.commit("prepare data")
|
||||
sub_dir = tmp_dir / "eval"
|
||||
sub_dir.mkdir()
|
||||
tmp_dir.gen(
|
||||
"tmp_train_val_metrics.json",
|
||||
json.dumps(TRAIN_METRICS[0]),
|
||||
)
|
||||
train_metrics_file = os.path.join(sub_dir, "train_val_metrics.json")
|
||||
run_copy_metrics(
|
||||
"tmp_train_val_metrics.json",
|
||||
train_metrics_file,
|
||||
name="train",
|
||||
metrics_no_cache=[train_metrics_file],
|
||||
)
|
||||
(tmp_dir / "tmp_train_val_metrics.json").unlink()
|
||||
|
||||
scm.add(["dvc.yaml", "dvc.lock", train_metrics_file])
|
||||
scm.commit("train model")
|
||||
|
||||
test_metrics_file = os.path.join(sub_dir, "test_metrics.json")
|
||||
tmp_dir.gen("tmp_test_metrics.json", json.dumps(TEST_METRICS[0]))
|
||||
run_copy_metrics(
|
||||
"tmp_test_metrics.json",
|
||||
test_metrics_file,
|
||||
name="test",
|
||||
metrics_no_cache=[test_metrics_file],
|
||||
)
|
||||
(tmp_dir / "tmp_test_metrics.json").unlink()
|
||||
|
||||
scm.add(["dvc.yaml", "dvc.lock", test_metrics_file])
|
||||
scm.commit("test model")
|
||||
|
||||
with tmp_dir.branch("better-model", new=True):
|
||||
tmp_dir.gen(
|
||||
"tmp_train_val_metrics.json",
|
||||
json.dumps(TRAIN_METRICS[1]),
|
||||
)
|
||||
run_copy_metrics(
|
||||
"tmp_train_val_metrics.json",
|
||||
train_metrics_file,
|
||||
name="train",
|
||||
metrics_no_cache=[train_metrics_file],
|
||||
)
|
||||
(tmp_dir / "tmp_train_val_metrics.json").unlink()
|
||||
|
||||
scm.add(["dvc.yaml", "dvc.lock", train_metrics_file])
|
||||
scm.commit("train better model")
|
||||
|
||||
tmp_dir.gen("tmp_test_metrics.json", json.dumps(TEST_METRICS[1]))
|
||||
run_copy_metrics(
|
||||
"tmp_test_metrics.json",
|
||||
test_metrics_file,
|
||||
name="test",
|
||||
metrics_no_cache=[test_metrics_file],
|
||||
)
|
||||
(tmp_dir / "tmp_test_metrics.json").unlink()
|
||||
|
||||
scm.add(["dvc.yaml", "dvc.lock", test_metrics_file])
|
||||
scm.commit("test better model")
|
||||
|
||||
scm.checkout("master")
|
||||
|
||||
return (
|
||||
os.path.relpath(train_metrics_file, tmp_dir),
|
||||
os.path.relpath(test_metrics_file, tmp_dir),
|
||||
)
|
||||
|
||||
|
||||
def test_params_show_no_args(params_repo):
|
||||
assert api.params_show() == {
|
||||
"params.yaml:foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_targets(params_repo):
|
||||
assert api.params_show("params.yaml") == {"foo": 5}
|
||||
assert api.params_show("params.yaml", "params.json") == {
|
||||
"foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
}
|
||||
assert api.params_show("params.yaml", stages="stage-1") == {
|
||||
"foo": 5,
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_deps(params_repo):
|
||||
params = api.params_show(deps=True)
|
||||
assert params == {
|
||||
"params.yaml:foo": 5,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_stages(params_repo):
|
||||
assert api.params_show(stages="stage-2") == {"foo": {"bar": 4}}
|
||||
|
||||
assert api.params_show() == api.params_show(
|
||||
stages=["stage-1", "stage-2", "stage-3"]
|
||||
)
|
||||
|
||||
assert api.params_show("params.json", stages="stage-3") == {"foobar": 3}
|
||||
|
||||
assert api.params_show(stages="stage-0") == {}
|
||||
|
||||
|
||||
def test_params_show_stage_addressing(tmp_dir, dvc):
|
||||
for subdir in {"subdir1", "subdir2"}:
|
||||
subdir = tmp_dir / subdir
|
||||
subdir.mkdir()
|
||||
with subdir.chdir():
|
||||
subdir.gen("params.yaml", "foo: 1")
|
||||
|
||||
dvc.run(name="stage-0", cmd="echo stage-0", params=["foo"])
|
||||
|
||||
for s in {"subdir1", "subdir2"}:
|
||||
dvcyaml = os.path.join(s, "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
with subdir.chdir():
|
||||
nested = subdir / "nested"
|
||||
nested.mkdir()
|
||||
with nested.chdir():
|
||||
dvcyaml = os.path.join("..", "dvc.yaml")
|
||||
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
|
||||
|
||||
|
||||
def test_params_show_revs(params_repo):
|
||||
assert api.params_show(rev="HEAD~1") == {
|
||||
"params.yaml:foo": 1,
|
||||
"bar": 2,
|
||||
"foobar": 3,
|
||||
"other_params.json:foo": {"bar": 4},
|
||||
}
|
||||
|
||||
|
||||
def test_params_show_while_running_stage(tmp_dir, dvc):
|
||||
(tmp_dir / "params.yaml").dump({"foo": {"bar": 1}})
|
||||
(tmp_dir / "params.json").dump({"bar": 2})
|
||||
|
||||
tmp_dir.gen(
|
||||
"merge.py",
|
||||
dedent(
|
||||
"""
|
||||
import json
|
||||
from dvc import api
|
||||
with open("merged.json", "w") as f:
|
||||
json.dump(api.params_show(stages="merge"), f)
|
||||
"""
|
||||
),
|
||||
)
|
||||
dvc.stage.add(
|
||||
name="merge",
|
||||
cmd="python merge.py",
|
||||
params=["foo.bar", {"params.json": ["bar"]}],
|
||||
outs=["merged.json"],
|
||||
)
|
||||
|
||||
dvc.reproduce()
|
||||
|
||||
assert (tmp_dir / "merged.json").parse() == {"foo": {"bar": 1}, "bar": 2}
|
||||
|
||||
|
||||
def test_params_show_repo(tmp_dir, erepo_dir):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.scm_gen("params.yaml", "foo: 1", commit="Create params.yaml")
|
||||
erepo_dir.dvc.run(
|
||||
name="stage-1",
|
||||
cmd="echo stage-1",
|
||||
params=["foo"],
|
||||
)
|
||||
assert api.params_show(repo=erepo_dir) == {"foo": 1}
|
||||
|
||||
|
||||
def test_params_show_no_params_found(tmp_dir, dvc):
|
||||
# Empty repo
|
||||
assert api.params_show() == {}
|
||||
|
||||
# params.yaml but no dvc.yaml
|
||||
(tmp_dir / "params.yaml").dump({"foo": 1})
|
||||
assert api.params_show() == {"foo": 1}
|
||||
|
||||
# dvc.yaml but no params.yaml
|
||||
(tmp_dir / "params.yaml").unlink()
|
||||
dvc.stage.add(name="echo", cmd="echo foo")
|
||||
assert api.params_show() == {}
|
||||
|
||||
|
||||
def test_params_show_stage_without_params(tmp_dir, dvc):
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
|
||||
dvc.run(
|
||||
name="stage-0",
|
||||
cmd="echo stage-0",
|
||||
)
|
||||
|
||||
assert api.params_show(stages="stage-0") == {}
|
||||
|
||||
assert api.params_show(deps=True) == {}
|
||||
|
||||
|
||||
def test_params_show_untracked_target(params_repo, tmp_dir):
|
||||
tmp_dir.gen("params_foo.yaml", "foo: 1")
|
||||
|
||||
assert api.params_show("params_foo.yaml") == {"foo": 1}
|
||||
|
||||
assert api.params_show("params_foo.yaml", stages="stage-0") == {}
|
||||
|
||||
assert api.params_show("params_foo.yaml", deps=True) == {}
|
||||
|
||||
|
||||
def test_metrics_show_no_args(metrics_repo):
|
||||
train_metrics_file, test_metrics_file = metrics_repo
|
||||
assert api.metrics_show() == {
|
||||
f"{train_metrics_file}:avg_prec": TRAIN_METRICS[0]["avg_prec"],
|
||||
f"{train_metrics_file}:roc_auc": TRAIN_METRICS[0]["roc_auc"],
|
||||
f"{test_metrics_file}:avg_prec": TEST_METRICS[0]["avg_prec"],
|
||||
f"{test_metrics_file}:roc_auc": TEST_METRICS[0]["roc_auc"],
|
||||
}
|
||||
|
||||
|
||||
def test_metrics_show_targets(metrics_repo):
|
||||
train_metrics_file, test_metrics_file = metrics_repo
|
||||
assert api.metrics_show(train_metrics_file) == TRAIN_METRICS[0]
|
||||
assert api.metrics_show(test_metrics_file) == TEST_METRICS[0]
|
||||
assert api.metrics_show(train_metrics_file, test_metrics_file) == {
|
||||
f"{train_metrics_file}:avg_prec": TRAIN_METRICS[0]["avg_prec"],
|
||||
f"{train_metrics_file}:roc_auc": TRAIN_METRICS[0]["roc_auc"],
|
||||
f"{test_metrics_file}:avg_prec": TEST_METRICS[0]["avg_prec"],
|
||||
f"{test_metrics_file}:roc_auc": TEST_METRICS[0]["roc_auc"],
|
||||
}
|
||||
|
||||
|
||||
def test_metrics_show_no_metrics_found(tmp_dir, dvc):
|
||||
# Empty repo
|
||||
assert api.metrics_show() == {}
|
||||
|
||||
# dvc.yaml but no metrics
|
||||
dvc.stage.add(name="echo", cmd="echo foo")
|
||||
assert api.metrics_show() == {}
|
||||
|
||||
|
||||
def test_metrics_show_rev_without_metrics(metrics_repo):
|
||||
assert api.metrics_show(rev="HEAD~2") == {}
|
||||
|
||||
|
||||
def test_metrics_show_rev_with_metrics(metrics_repo):
|
||||
train_metrics_file, test_metrics_file = metrics_repo
|
||||
assert api.metrics_show(rev="HEAD~1") == TRAIN_METRICS[0]
|
||||
assert api.metrics_show(rev="HEAD") == {
|
||||
f"{train_metrics_file}:avg_prec": TRAIN_METRICS[0]["avg_prec"],
|
||||
f"{train_metrics_file}:roc_auc": TRAIN_METRICS[0]["roc_auc"],
|
||||
f"{test_metrics_file}:avg_prec": TEST_METRICS[0]["avg_prec"],
|
||||
f"{test_metrics_file}:roc_auc": TEST_METRICS[0]["roc_auc"],
|
||||
}
|
||||
assert api.metrics_show(rev="better-model~1") == {
|
||||
f"{train_metrics_file}:avg_prec": TRAIN_METRICS[1]["avg_prec"],
|
||||
f"{train_metrics_file}:roc_auc": TRAIN_METRICS[1]["roc_auc"],
|
||||
f"{test_metrics_file}:avg_prec": TEST_METRICS[0]["avg_prec"],
|
||||
f"{test_metrics_file}:roc_auc": TEST_METRICS[0]["roc_auc"],
|
||||
}
|
||||
assert api.metrics_show(rev="better-model") == {
|
||||
f"{train_metrics_file}:avg_prec": TRAIN_METRICS[1]["avg_prec"],
|
||||
f"{train_metrics_file}:roc_auc": TRAIN_METRICS[1]["roc_auc"],
|
||||
f"{test_metrics_file}:avg_prec": TEST_METRICS[1]["avg_prec"],
|
||||
f"{test_metrics_file}:roc_auc": TEST_METRICS[1]["roc_auc"],
|
||||
}
|
||||
|
||||
|
||||
def test_metrics_show_dirty_working_dir(metrics_repo, tmp_dir):
|
||||
train_metrics_file, test_metrics_file = metrics_repo
|
||||
new_metrics = {"acc": 1}
|
||||
(tmp_dir / train_metrics_file).unlink()
|
||||
(tmp_dir / train_metrics_file).dump(new_metrics)
|
||||
(tmp_dir / test_metrics_file).unlink()
|
||||
(tmp_dir / test_metrics_file).dump(new_metrics)
|
||||
|
||||
assert api.metrics_show() == {
|
||||
f"{train_metrics_file}:acc": new_metrics["acc"],
|
||||
f"{test_metrics_file}:acc": new_metrics["acc"],
|
||||
}
|
||||
@@ -100,7 +100,7 @@ def test_init_cache(tmp_dir, dvc, scm, cloud):
|
||||
@pytest.mark.needs_internet
|
||||
@pytest.mark.parametrize("cloud", [pytest.lazy_fixture("git_ssh")])
|
||||
def test_reproduce(tmp_dir, scm, dvc, cloud, exp_stage, mocker):
|
||||
from sshfs import SSHFileSystem as _sshfs
|
||||
from sshfs import SSHFileSystem as _sshfs # noqa: N813
|
||||
|
||||
rev = scm.get_rev()
|
||||
root_url = cloud / SSHExecutor.gen_dirname()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user