mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17f523bf86 | ||
|
|
a0bf2d997a | ||
|
|
8ed9b911db | ||
|
|
ef22b1d77e | ||
|
|
aaa825a9a8 | ||
|
|
2a40a55bb4 | ||
|
|
3aa9f51e36 | ||
|
|
1cfdc22549 | ||
|
|
2b889baa5e | ||
|
|
8589aa4001 | ||
|
|
8cbc4c43f1 | ||
|
|
417c327143 | ||
|
|
204eebaf15 | ||
|
|
3a6fd75d98 | ||
|
|
351b952d5f | ||
|
|
fe7db32311 | ||
|
|
0bd34c8977 | ||
|
|
48a63ca588 | ||
|
|
4459d6ac57 | ||
|
|
08a50ee36e | ||
|
|
f1ea64c4ef | ||
|
|
7ede45af2d | ||
|
|
51db1765ee | ||
|
|
9101fce761 | ||
|
|
3f842a205f | ||
|
|
77e900fed2 | ||
|
|
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
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
@@ -150,6 +151,7 @@ def get_parent_parser():
|
||||
return parent_parser
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_main_parser():
|
||||
parent_parser = get_parent_parser()
|
||||
|
||||
|
||||
+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 experiments temporary internal files."
|
||||
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)
|
||||
@@ -52,7 +52,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments in the DVC repository."
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments."
|
||||
|
||||
experiments_diff_parser = experiments_subparsers.add_parser(
|
||||
"diff",
|
||||
|
||||
@@ -78,7 +78,8 @@ class CmdExperimentsGC(CmdBase):
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||
EXPERIMENTS_GC_DESCRIPTION = (
|
||||
"Removes all experiments which are not derived from the specifiedGit revisions."
|
||||
"Removes all experiments which are not derived"
|
||||
" from the specified Git revisions."
|
||||
)
|
||||
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||
"gc",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -35,7 +35,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as an experiment."
|
||||
save_parser = experiments_subparsers.add_parser(
|
||||
"save",
|
||||
parents=[parent_parser],
|
||||
|
||||
@@ -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."
|
||||
|
||||
+4
-15
@@ -19,7 +19,7 @@ def _show_json(renderers, split=False):
|
||||
from dvc.render.convert import to_json
|
||||
|
||||
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
|
||||
ui.write_json(result)
|
||||
ui.write_json(result, highlight=False)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
@@ -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,10 +110,9 @@ class CmdPlots(CmdBase):
|
||||
plots_data = self._func(
|
||||
targets=self.args.targets,
|
||||
props=self._props(),
|
||||
config_files=self._config_files(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
if not plots_data and not self.args.json:
|
||||
ui.error_write(
|
||||
"No plots were loaded, visualization file will not be created."
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
+1
-5
@@ -117,7 +117,7 @@ class Config(dict):
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from appdirs import site_config_dir, user_config_dir
|
||||
from platformdirs import site_config_dir, user_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
@@ -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.
|
||||
|
||||
+46
-9
@@ -1,8 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import walk_values
|
||||
from voluptuous import All, Any, Coerce, Invalid, Lower, Optional, Range, Schema
|
||||
from funcy import once, walk_values
|
||||
from voluptuous import (
|
||||
REMOVE_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Bool = All(
|
||||
Lower,
|
||||
@@ -30,7 +43,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 +53,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 +64,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}://")
|
||||
|
||||
@@ -63,6 +78,27 @@ class RelPath(str):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureSchema(Schema):
|
||||
def __init__(self, schema, required=False):
|
||||
super().__init__(schema, required=required, extra=REMOVE_EXTRA)
|
||||
|
||||
@staticmethod
|
||||
@once
|
||||
def _log_deprecated(keys):
|
||||
# only run this once per session
|
||||
message = "%s config option%s unsupported"
|
||||
paths = ", ".join(f"'feature.{key}'" for key in keys)
|
||||
pluralize = " is" if len(keys) == 1 else "s are"
|
||||
logger.warning(message, paths, pluralize)
|
||||
|
||||
def __call__(self, data):
|
||||
ret = super().__call__(data)
|
||||
extra_keys = data.keys() - ret.keys()
|
||||
if extra_keys:
|
||||
self._log_deprecated(sorted(extra_keys))
|
||||
return ret
|
||||
|
||||
|
||||
REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
@@ -260,11 +296,12 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
# section for experimental features
|
||||
"feature": {
|
||||
Optional("machine", default=False): Bool,
|
||||
# enabled by default. It's of no use, kept for backward compatibility.
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
# only specified keys are validated, others get logged and then ignored/removed
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
"html_template": str,
|
||||
Optional("auto_open", default=False): Bool,
|
||||
|
||||
+12
-2
@@ -14,7 +14,14 @@ from dvc.utils import fix_env, is_binary
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
def _suppress_resource_warning(popen: Popen):
|
||||
"""Sets the returncode to avoid ResourceWarning when popen is garbage collected."""
|
||||
# only use for daemon processes.
|
||||
# See https://bugs.python.org/issue38890.
|
||||
popen.returncode = 0
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs) -> Popen:
|
||||
prefix = [sys.executable]
|
||||
if not is_binary():
|
||||
main_entrypoint = os.path.join(
|
||||
@@ -40,7 +47,10 @@ def _spawn_windows(cmd, env):
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
|
||||
popen = _popen(
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
|
||||
)
|
||||
_suppress_resource_warning(popen)
|
||||
|
||||
|
||||
def _spawn_posix(cmd, env):
|
||||
|
||||
+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.
|
||||
|
||||
+2
-2
@@ -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
|
||||
@@ -121,7 +121,7 @@ def _get_cloud_fs(repo_config, **kwargs):
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf) # type: ignore[index]
|
||||
except Invalid as exc:
|
||||
raise RepoConfigError(str(exc)) from None
|
||||
|
||||
|
||||
+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]
|
||||
|
||||
+35
-28
@@ -2,8 +2,9 @@ import logging
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, 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 +21,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 +29,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 +313,7 @@ class Output:
|
||||
IsStageFileError: Type[DvcException] = OutputIsStageFileError
|
||||
IsIgnoredError: Type[DvcException] = OutputIsIgnoredError
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
stage,
|
||||
path,
|
||||
@@ -709,7 +710,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 +720,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 +738,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 +828,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 +876,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
|
||||
@@ -1012,7 +1018,8 @@ class Output:
|
||||
except FileNotFoundError:
|
||||
if self.remote:
|
||||
kwargs["remote"] = self.remote
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
with suppress(Exception):
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
return self.obj
|
||||
@@ -1052,14 +1059,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,23 +0,0 @@
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class ProcessNotTerminatedError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' has not been terminated.")
|
||||
|
||||
|
||||
class ProcessNotFoundError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' does not exist.")
|
||||
|
||||
|
||||
class TimeoutExpired(DvcException):
|
||||
def __init__(self, cmd, timeout):
|
||||
super().__init__(f"'{cmd}' did not complete before timeout '{timeout}'")
|
||||
self.cmd = cmd
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class UnsupportedSignalError(DvcException):
|
||||
def __init__(self, sig):
|
||||
super().__init__(f"Unsupported signal: {sig}")
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Serverless process manager."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Generator, List, Optional, Tuple, Union
|
||||
|
||||
from funcy.flow import reraise
|
||||
from shortuuid import uuid
|
||||
|
||||
from .exceptions import ProcessNotTerminatedError, UnsupportedSignalError
|
||||
from .process import ManagedProcess, ProcessInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""Manager for controlling background ManagedProcess(es).
|
||||
|
||||
Spawned process entries are kept in the manager directory until they
|
||||
are explicitly removed (with remove() or cleanup()) so that return
|
||||
value and log information can be accessed after a process has completed.
|
||||
"""
|
||||
|
||||
def __init__(self, wdir: Optional[str] = None):
|
||||
self.wdir = wdir or "."
|
||||
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
if not os.path.exists(self.wdir):
|
||||
return
|
||||
yield from os.listdir(self.wdir)
|
||||
|
||||
def __getitem__(self, key: str) -> "ProcessInfo":
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
try:
|
||||
with open(info_path, encoding="utf-8") as fobj:
|
||||
return ProcessInfo.from_dict(json.load(fobj))
|
||||
except FileNotFoundError:
|
||||
raise KeyError # noqa: B904
|
||||
|
||||
@reraise(FileNotFoundError, KeyError)
|
||||
def __setitem__(self, key: str, value: "ProcessInfo"):
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
with open(info_path, "w", encoding="utf-8") as fobj:
|
||||
return json.dump(value.asdict(), fobj)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
path = os.path.join(self.wdir, key)
|
||||
if os.path.exists(path):
|
||||
remove(path)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def processes(self) -> Generator[Tuple[str, "ProcessInfo"], None, None]:
|
||||
for name in self:
|
||||
try:
|
||||
yield name, self[name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def spawn(self, args: Union[str, List[str]], name: Optional[str] = None):
|
||||
"""Run the given command in the background."""
|
||||
name = name or uuid()
|
||||
pid = ManagedProcess.spawn(
|
||||
args,
|
||||
wdir=os.path.join(self.wdir, name),
|
||||
name=name,
|
||||
)
|
||||
logger.debug(
|
||||
"Spawned managed process '%s' (PID: '%d')",
|
||||
name,
|
||||
pid,
|
||||
)
|
||||
|
||||
def send_signal(self, name: str, sig: int):
|
||||
"""Send `signal` to the specified named process."""
|
||||
process_info = self[name]
|
||||
if sys.platform == "win32" and sig not in (
|
||||
signal.SIGTERM,
|
||||
signal.CTRL_C_EVENT, # pylint: disable=no-member
|
||||
signal.CTRL_BREAK_EVENT, # pylint: disable=no-member
|
||||
):
|
||||
raise UnsupportedSignalError(sig)
|
||||
|
||||
def handle_closed_process():
|
||||
logging.warning("Process %s had already aborted unexpectedly.", name)
|
||||
process_info.returncode = -1
|
||||
self[name] = process_info
|
||||
|
||||
if process_info.returncode is None:
|
||||
try:
|
||||
os.kill(process_info.pid, sig)
|
||||
except ProcessLookupError:
|
||||
handle_closed_process()
|
||||
raise
|
||||
except OSError as exc:
|
||||
if getattr(exc, "winerror", None) == 87:
|
||||
handle_closed_process()
|
||||
raise ProcessLookupError from exc
|
||||
raise
|
||||
|
||||
def terminate(self, name: str):
|
||||
"""Terminate the specified named process."""
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
|
||||
def kill(self, name: str):
|
||||
"""Kill the specified named process."""
|
||||
if sys.platform == "win32":
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
else:
|
||||
self.send_signal(name, signal.SIGKILL) # pylint: disable=no-member
|
||||
|
||||
def remove(self, name: str, force: bool = False):
|
||||
"""Remove the specified named process from this manager.
|
||||
|
||||
If the specified process is still running, it will be forcefully killed
|
||||
if `force` is True`, otherwise an exception will be raised.
|
||||
|
||||
Raises:
|
||||
ProcessNotTerminatedError if the specified process is still
|
||||
running and was not forcefully killed.
|
||||
"""
|
||||
try:
|
||||
process_info = self[name]
|
||||
except KeyError:
|
||||
return
|
||||
if process_info.returncode is None and not force:
|
||||
raise ProcessNotTerminatedError(name)
|
||||
try:
|
||||
self.kill(name)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
del self[name]
|
||||
|
||||
def cleanup(self, force: bool = False):
|
||||
"""Remove stale (terminated) processes from this manager."""
|
||||
for name in self:
|
||||
try:
|
||||
self.remove(name, force)
|
||||
except ProcessNotTerminatedError:
|
||||
continue
|
||||
@@ -1,179 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import BinaryIO, List, Optional, Union
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
from .exceptions import TimeoutExpired
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: int
|
||||
stdin: Optional[str]
|
||||
stdout: Optional[str]
|
||||
stderr: Optional[str]
|
||||
returncode: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class ManagedProcess(AbstractContextManager):
|
||||
"""Run the specified command with redirected output.
|
||||
|
||||
stdout and stderr will both be redirected to <name>.out.
|
||||
Interactive processes (requiring stdin input) are currently unsupported.
|
||||
|
||||
Parameters:
|
||||
args: Command to be run.
|
||||
wdir: If specified, redirected output files will be placed in `wdir`.
|
||||
name: Name to use for this process, if not specified a UUID will be
|
||||
generated instead.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Union[str, List[str]],
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.args: List[str] = (
|
||||
shlex.split(args, posix=os.name == "posix")
|
||||
if isinstance(args, str)
|
||||
else list(args)
|
||||
)
|
||||
self.wdir = wdir
|
||||
self.name = name or uuid()
|
||||
self.returncode: Optional[int] = None
|
||||
self._stdout: Optional[BinaryIO] = None
|
||||
self._stderr: Optional[BinaryIO] = None
|
||||
|
||||
self._run()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.wait()
|
||||
|
||||
def _close_fds(self):
|
||||
if self._stdout:
|
||||
self._stdout.close()
|
||||
self._stdout = None
|
||||
if self._stderr:
|
||||
self._stderr.close()
|
||||
self._stderr = None
|
||||
|
||||
def _make_path(self, path: str) -> str:
|
||||
return os.path.join(self.wdir, path) if self.wdir else path
|
||||
|
||||
@cached_property
|
||||
def stdout_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.out")
|
||||
|
||||
@cached_property
|
||||
def info_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.json")
|
||||
|
||||
@cached_property
|
||||
def pidfile_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.pid")
|
||||
|
||||
@property
|
||||
def info(self) -> "ProcessInfo":
|
||||
return ProcessInfo(
|
||||
pid=self.pid,
|
||||
stdin=None,
|
||||
stdout=self.stdout_path,
|
||||
stderr=None,
|
||||
returncode=self.returncode,
|
||||
)
|
||||
|
||||
def _make_wdir(self):
|
||||
if self.wdir:
|
||||
os.makedirs(self.wdir, exist_ok=True)
|
||||
|
||||
def _dump(self):
|
||||
self._make_wdir()
|
||||
with open(self.info_path, "w", encoding="utf-8") as fobj:
|
||||
json.dump(self.info.asdict(), fobj)
|
||||
with open(self.pidfile_path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(str(self.pid))
|
||||
|
||||
def _run(self) -> None:
|
||||
self._make_wdir()
|
||||
logger.debug(
|
||||
"Appending output to '%s'",
|
||||
self.stdout_path,
|
||||
)
|
||||
self._stdout = open(self.stdout_path, "ab") # noqa: SIM115
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
self.args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=self._stdout,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
)
|
||||
self.pid: int = self._proc.pid
|
||||
self._dump()
|
||||
except Exception:
|
||||
if self._proc is not None:
|
||||
self._proc.kill()
|
||||
self._close_fds()
|
||||
raise
|
||||
|
||||
def wait(self, timeout: Optional[int] = None) -> Optional[int]:
|
||||
"""Block until a process started with `run` has completed.
|
||||
|
||||
Raises:
|
||||
TimeoutExpired if `timeout` was set and the process
|
||||
did not terminate after `timeout` seconds.
|
||||
"""
|
||||
if self.returncode is not None:
|
||||
return self.returncode
|
||||
try:
|
||||
self._proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise TimeoutExpired(exc.cmd, exc.timeout) from exc
|
||||
self.returncode = self._proc.returncode
|
||||
self._close_fds()
|
||||
self._dump()
|
||||
return self.returncode
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, *args, **kwargs) -> Optional[int]:
|
||||
"""Spawn a ManagedProcess command in the background.
|
||||
|
||||
Returns: The spawned process PID.
|
||||
"""
|
||||
import multiprocessing as mp
|
||||
|
||||
proc = mp.Process(
|
||||
target=cls._spawn,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
# Do not terminate the child daemon when the main process exits
|
||||
# pylint: disable=protected-access
|
||||
mp.process._children.discard(proc) # type: ignore[attr-defined]
|
||||
return proc.pid
|
||||
|
||||
@classmethod
|
||||
def _spawn(cls, *args, **kwargs):
|
||||
with cls(*args, **kwargs):
|
||||
pass
|
||||
+1
-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
|
||||
|
||||
|
||||
|
||||
+100
-27
@@ -3,7 +3,16 @@ 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,
|
||||
ContextManager,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.exceptions import FileMissingError
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
@@ -14,7 +23,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 +32,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 +109,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 +132,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 +149,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 +195,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 +204,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 +230,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 +265,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 +358,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 +366,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 +396,21 @@ class Repo:
|
||||
# fs.
|
||||
self._reset()
|
||||
|
||||
@property
|
||||
def data_index(self) -> "DataIndex":
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
if self._data_index is None:
|
||||
if self.index_db_dir:
|
||||
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"))
|
||||
else:
|
||||
self._data_index = DataIndex()
|
||||
|
||||
return self._data_index
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}: '{self.root_dir}'"
|
||||
|
||||
@@ -402,7 +472,12 @@ class Repo:
|
||||
|
||||
return brancher(self, *args, **kwargs)
|
||||
|
||||
def used_objs(
|
||||
def switch(self, rev: str) -> ContextManager[str]:
|
||||
from dvc.repo.brancher import switch
|
||||
|
||||
return switch(self, rev)
|
||||
|
||||
def used_objs( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
@@ -553,21 +628,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,
|
||||
|
||||
+72
-14
@@ -1,7 +1,17 @@
|
||||
from typing import Optional
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional, Tuple
|
||||
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def brancher( # noqa: E302
|
||||
self,
|
||||
@@ -60,6 +70,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"
|
||||
|
||||
@@ -79,21 +90,68 @@ def brancher( # noqa: E302
|
||||
)
|
||||
|
||||
try:
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self._reset_cached_indecies() # pylint: disable=W0212
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
if cwd_parts:
|
||||
cwd = self.fs.path.join("/", *cwd_parts) # type: ignore[unreachable]
|
||||
self.fs.path.chdir(cwd)
|
||||
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
if self.fs.exists(self.root_dir):
|
||||
try:
|
||||
_switch_fs(self, sha, repo_root_parts, cwd_parts)
|
||||
yield sha if sha_only else ",".join(names)
|
||||
except NotDvcRepoError:
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
pass
|
||||
finally:
|
||||
self.fs = saved_fs
|
||||
self.root_dir = saved_root
|
||||
|
||||
|
||||
def _switch_fs(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
repo_root_parts: Tuple[str, ...],
|
||||
cwd_parts: Tuple[str, ...],
|
||||
):
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if rev == "workspace":
|
||||
logger.trace("switching fs to workspace") # type: ignore[attr-defined]
|
||||
repo.fs = LocalFileSystem(url=repo.root_dir)
|
||||
return
|
||||
|
||||
logger.trace("switching fs to revision %s", rev[:7]) # type: ignore[attr-defined]
|
||||
assert isinstance(repo.scm, Git)
|
||||
fs = GitFileSystem(scm=repo.scm, rev=rev)
|
||||
root_dir = repo.fs.path.join("/", *repo_root_parts)
|
||||
if not fs.exists(root_dir):
|
||||
raise NotDvcRepoError(f"Commit '{rev[:7]}' does not contain a DVC repo")
|
||||
|
||||
repo.fs = fs
|
||||
repo.root_dir = root_dir
|
||||
|
||||
if cwd_parts:
|
||||
cwd = repo.fs.path.join("/", *cwd_parts)
|
||||
repo.fs.path.chdir(cwd)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def switch(repo: "Repo", rev: str) -> Iterator[str]:
|
||||
"""Switch to a specific revision."""
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if rev != "workspace":
|
||||
rev = resolve_rev(repo.scm, rev)
|
||||
|
||||
repo_root_parts = ()
|
||||
if repo.fs.path.isin(repo.root_dir, repo.scm.root_dir):
|
||||
repo_root_parts = repo.fs.path.relparts(repo.root_dir, repo.scm.root_dir)
|
||||
|
||||
cwd_parts = ()
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.scm.root_dir):
|
||||
cwd_parts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.scm.root_dir)
|
||||
|
||||
saved_fs = repo.fs
|
||||
saved_root = repo.root_dir
|
||||
try:
|
||||
_switch_fs(repo, rev, repo_root_parts, cwd_parts)
|
||||
yield rev
|
||||
finally:
|
||||
repo.fs = saved_fs
|
||||
repo.root_dir = saved_root
|
||||
|
||||
+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]
|
||||
|
||||
+52
-22
@@ -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(
|
||||
@@ -158,17 +182,16 @@ def _diff_head_to_index(
|
||||
) -> Dict[str, List[str]]:
|
||||
index = repo.index.data["repo"]
|
||||
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
|
||||
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 +218,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 +244,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,
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import get_in
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
@@ -547,55 +548,55 @@ 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),
|
||||
metrics=get_in(dvc.metrics.show(), ["", "data"]),
|
||||
)
|
||||
|
||||
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
|
||||
+113
-72
@@ -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.exceptions import DvcException
|
||||
from dvc.scm import Git, SCMError, iter_revs, resolve_rev
|
||||
from dvc.utils import error_handler, onerror_collect, relpath
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
from .serialize import SerializableError, SerializableExp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git.objects import GitCommit
|
||||
@@ -40,85 +39,116 @@ 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 or (
|
||||
isinstance(cached_exp, SerializableExp)
|
||||
and cached_exp.status == ExpStatus.Running.name
|
||||
):
|
||||
# expire cached queued exp entry once we start running it
|
||||
cache.delete(exp_rev)
|
||||
elif isinstance(cached_exp, SerializableError):
|
||||
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 not (
|
||||
exp_rev == "workspace"
|
||||
or status == ExpStatus.Running
|
||||
or param_deps
|
||||
or exp.contains_error
|
||||
):
|
||||
cache.put(exp, force=True)
|
||||
return _format_exp(exp)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
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]):
|
||||
with repo.switch(exp_rev) as 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,
|
||||
)
|
||||
|
||||
|
||||
def _collect_complete_experiment(
|
||||
@@ -170,6 +200,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 +219,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 +252,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 +415,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 +433,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 +451,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 +464,7 @@ def show(
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
if not hide_queued
|
||||
else {}
|
||||
@@ -439,6 +476,7 @@ def show(
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
|
||||
failed_experiments = (
|
||||
@@ -448,6 +486,7 @@ def show(
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
if not hide_failed
|
||||
else {}
|
||||
@@ -460,6 +499,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)
|
||||
|
||||
|
||||
+98
-34
@@ -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,
|
||||
@@ -281,51 +335,53 @@ class Index:
|
||||
|
||||
return dict(by_workspace)
|
||||
|
||||
@cached_property
|
||||
def data_tree(self):
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
tree = Tree()
|
||||
for out in self.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
ws, key = out.index_key
|
||||
|
||||
tree.add((ws, *key), out.meta, out.hash_info)
|
||||
|
||||
tree.digest()
|
||||
|
||||
return tree
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "Dict[str, DataIndex]":
|
||||
from collections import defaultdict
|
||||
prefix: "DataIndexKey"
|
||||
loaded = False
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import DataIndex, DataIndexEntry, Storage
|
||||
index = self.repo.data_index
|
||||
prefix = ("tree", self.data_tree.hash_info.value)
|
||||
if index.has_node(prefix):
|
||||
loaded = True
|
||||
|
||||
by_workspace: dict = defaultdict(DataIndex)
|
||||
if not loaded:
|
||||
_load_data_from_outs(index, prefix, self.outs)
|
||||
index.commit()
|
||||
|
||||
by_workspace["repo"] = DataIndex()
|
||||
by_workspace["local"] = DataIndex()
|
||||
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
|
||||
|
||||
workspace, key = out.index_key
|
||||
data_index = by_workspace[workspace]
|
||||
ws, key = out.index_key
|
||||
if ws not in by_workspace:
|
||||
by_workspace[ws] = index.view((*prefix, ws))
|
||||
|
||||
entry = DataIndexEntry(
|
||||
key=key,
|
||||
meta=out.meta,
|
||||
hash_info=out.hash_info,
|
||||
)
|
||||
data_index = by_workspace[ws]
|
||||
_load_storage_from_out(data_index.storage_map, key, out)
|
||||
|
||||
storage = Storage(odb=out.odb, cache=out.cache)
|
||||
try:
|
||||
storage.remote = self.repo.cloud.get_remote_odb(out.remote)
|
||||
except NoRemoteError:
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
storage.odb = dep.odb
|
||||
storage.fs = dep.fs
|
||||
storage.path = dep.fs_path
|
||||
|
||||
data_index.add(entry)
|
||||
data_index.storage_map[key] = storage
|
||||
|
||||
return dict(by_workspace)
|
||||
return by_workspace
|
||||
|
||||
@staticmethod
|
||||
def _hash_targets(
|
||||
@@ -526,6 +582,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 +607,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
|
||||
|
||||
+7
-4
@@ -1,5 +1,7 @@
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import InvalidArgumentError, UploadError
|
||||
from dvc.utils import glob_targets
|
||||
|
||||
@@ -13,7 +15,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@locked
|
||||
def push( # noqa: C901
|
||||
def push( # noqa: C901, PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
jobs=None,
|
||||
@@ -30,9 +32,10 @@ def push( # noqa: C901
|
||||
include_imports=False,
|
||||
):
|
||||
worktree_remote: Optional["Remote"] = None
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree or _remote.fs.version_aware:
|
||||
worktree_remote = _remote
|
||||
with suppress(NoRemoteError):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote and (_remote.worktree or _remote.fs.version_aware):
|
||||
worktree_remote = _remote
|
||||
|
||||
pushed = 0
|
||||
used_run_cache = self.stage_cache.push(remote, odb=odb) if run_cache else []
|
||||
|
||||
@@ -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()
|
||||
|
||||
+3
-22
@@ -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
|
||||
@@ -185,7 +165,7 @@ def clone(url: str, to_path: str, **kwargs):
|
||||
raise CloneError("SCM error") from exc
|
||||
|
||||
|
||||
def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
def resolve_rev(scm: Union["Git", "NoSCM"], rev: str) -> str:
|
||||
from scmrepo.exceptions import RevError as InternalRevError
|
||||
|
||||
from dvc.repo.experiments.utils import fix_exp_head
|
||||
@@ -193,6 +173,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
try:
|
||||
return scm.resolve_rev(fix_exp_head(scm, rev))
|
||||
except InternalRevError as exc:
|
||||
assert isinstance(scm, Git)
|
||||
# `scm` will only resolve git branch and tag names,
|
||||
# if rev is not a sha it may be an abbreviated experiment name
|
||||
if not (rev == "HEAD" or rev.startswith("refs/")):
|
||||
@@ -227,7 +208,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
|
||||
|
||||
+3
-3
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
@@ -167,9 +166,10 @@ class StageCache:
|
||||
COMPILED_LOCK_FILE_STAGE_SCHEMA(cache)
|
||||
|
||||
path = self._get_cache_path(cache_key, cache_value)
|
||||
parent = self.repo.cache.local.fs.path.parent(path)
|
||||
local_fs = self.repo.cache.local.fs
|
||||
parent = local_fs.path.parent(path)
|
||||
self.repo.cache.local.makedirs(parent)
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, dir=parent).name
|
||||
tmp = local_fs.path.join(parent, fs.utils.tmp_fname())
|
||||
assert os.path.exists(parent)
|
||||
assert os.path.isdir(parent)
|
||||
dump_yaml(tmp, cache)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+64
-6
@@ -17,7 +17,9 @@ from dvc.utils.objects import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.console import JustifyMethod, OverflowMethod
|
||||
from rich.status import Status
|
||||
from rich.style import Style
|
||||
from rich.text import Text as RichText
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
@@ -87,7 +89,7 @@ class Console:
|
||||
styled=styled,
|
||||
)
|
||||
|
||||
def write_json(
|
||||
def write_json( # noqa: PLR0913
|
||||
self,
|
||||
data: Any,
|
||||
indent: Optional[int] = None,
|
||||
@@ -105,8 +107,9 @@ class Console:
|
||||
if indent is None and self.isatty():
|
||||
indent = 2
|
||||
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
return console.print_json(
|
||||
from rich.json import JSON
|
||||
|
||||
json = JSON.from_data(
|
||||
data=data,
|
||||
indent=indent,
|
||||
highlight=bool(highlight),
|
||||
@@ -117,6 +120,50 @@ class Console:
|
||||
default=default,
|
||||
sort_keys=sort_keys,
|
||||
)
|
||||
if not highlight:
|
||||
return self.write(json.text, stderr=stderr)
|
||||
return self.rich_print(json, stderr=stderr, soft_wrap=True)
|
||||
|
||||
def rich_print(
|
||||
self,
|
||||
*objects: Any,
|
||||
sep: str = " ",
|
||||
end: str = "\n",
|
||||
stderr: bool = False,
|
||||
style: Optional[Union[str, "Style"]] = None,
|
||||
justify: Optional["JustifyMethod"] = None,
|
||||
overflow: Optional["OverflowMethod"] = None,
|
||||
no_wrap: Optional[bool] = None,
|
||||
emoji: Optional[bool] = None,
|
||||
markup: Optional[bool] = None,
|
||||
highlight: Optional[bool] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
crop: bool = True,
|
||||
soft_wrap: Optional[bool] = None,
|
||||
new_line_start: bool = False,
|
||||
) -> None:
|
||||
if stderr:
|
||||
console = self.error_console
|
||||
else:
|
||||
console = self.rich_console
|
||||
return console.print(
|
||||
*objects,
|
||||
sep=sep,
|
||||
end=end,
|
||||
style=style,
|
||||
justify=justify,
|
||||
overflow=overflow,
|
||||
no_wrap=no_wrap,
|
||||
emoji=emoji,
|
||||
markup=markup,
|
||||
highlight=highlight,
|
||||
width=width,
|
||||
height=height,
|
||||
crop=crop,
|
||||
soft_wrap=soft_wrap,
|
||||
new_line_start=new_line_start,
|
||||
)
|
||||
|
||||
def write(
|
||||
self,
|
||||
@@ -142,10 +189,21 @@ class Console:
|
||||
with Tqdm.external_write_mode(file=file):
|
||||
# if we are inside pager context, send the output to rich's buffer
|
||||
if styled or self._paginate.get():
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
if styled:
|
||||
return console.print(*objects, sep=sep, end=end)
|
||||
return console.out(*objects, sep=sep, end=end, highlight=False)
|
||||
return self.rich_print(*objects, sep=sep, end=end, stderr=stderr)
|
||||
return self.rich_print(
|
||||
sep.join(str(_object) for _object in objects),
|
||||
style=None,
|
||||
highlight=False,
|
||||
emoji=False,
|
||||
markup=False,
|
||||
no_wrap=True,
|
||||
overflow="ignore",
|
||||
crop=False,
|
||||
sep=sep,
|
||||
end=end,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
values = (self.formatter.format(obj, style) for obj in objects)
|
||||
return print(*values, sep=sep, end=end, file=file)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user