Compare commits

...
67 Commits
Author SHA1 Message Date
Matt Seddon 37a4a36dc7 exp save: use -m for --message short option 2023-10-09 09:03:44 -04:00
Dave Berenbaum 279f8cb68f repro --pull: don't fail for cloud-versioned remotes (#10001)
* repro --pull: don't fail for cloud-versioned remotes

* raise RunCacheNotSupported for cloud-versioned remotes
2023-10-06 18:11:30 +03:00
dberenbaum 8a70877748 fix artifact name test 2023-10-05 13:39:06 -04:00
dberenbaum f60091e64f bump gto 2023-10-05 13:39:06 -04:00
Matt Seddon 3661b8998b Enable forcing exp apply (unsaved files in the workspace) 2023-10-04 16:12:52 -04:00
Matt Seddon dcb678d606 exp rename: update help text 2023-10-04 15:14:49 -04:00
Ruslan Kuprieiev 6394a503be deps: bump dvc-data to >=2.18.1
To ensure people are getting a fix related to iterative/dvc#9826
2023-10-04 16:03:22 +03:00
Ruslan Kuprieiev 00c10eb6bd repo: bump site_cache_dir salt
Caused by a cache corruption fix in https://github.com/iterative/dvc-data/pull/447
2023-10-04 04:01:26 +03:00
Ruslan Kuprieiev b0a25f11b7 deps: bump dvc-data to >= 2.18.0 2023-10-04 04:01:26 +03:00
Dave Berenbaum bc1a700254 gc: add dry option and log num removed objs (#9952)
Closes #9330
2023-09-30 02:33:44 +00:00
Ruslan Kuprieiev 9f4b67cec4 site_cache_dir: use .dvc/tmp/btime and fallback to st_birthtime
This will ensure that we won't be using index cache if you delete your repo
and start/clone fresh.
2023-09-29 20:30:28 +03:00
Ruslan Kuprieiev 4d5a0163ea diff: handle granular targets 2023-09-28 21:54:06 +03:00
Ruslan Kuprieiev 0053b1ccd2 index: check parents when trying to detect hash_name 2023-09-28 21:54:06 +03:00
Ruslan Kuprieiev f981500862 cli: data_sync: show warning about missing default remote (#9979) 2023-09-26 23:29:33 +03:00
David de la Iglesia Castro a6e0d68cd2 run-cache: Load newest instead of first found in os.listdir. (#9908)
Closes https://github.com/iterative/dvc/issues/9420
2023-09-26 15:07:13 +03:00
David de la Iglesia Castro 2aa2bef9d6 artifacts: Bring back name_is_compatible. (#9972)
* artifacts: Bring back `name_is_compatible`.

Unfortunately, DVCLive was importing this function so I am bringing back the name for backwards compatibility.

* Update artifacts.py
2023-09-26 14:32:27 +03:00
skshetry 95c9790368 Revert "build: temporarily pin setuptools-scm" (#9978)
This reverts commit 7b40968741.
2023-09-26 13:47:31 +05:45
pre-commit-ci[bot] b8106e50fb [pre-commit.ci] pre-commit autoupdate (#9976)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.290 → v0.0.291](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.290...v0.0.291)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-26 08:55:57 +05:45
dependabot[bot] ccc84a0a43 build(deps-dev): bump pylint from 2.17.5 to 2.17.6 (#9977)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 2.17.5 to 2.17.6.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v2.17.5...v2.17.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-26 08:55:03 +05:45
Ruslan Kuprieiev 2e1166d520 fetch: don't clear pbars
There is more we can do during the `Fetching` stage, but it will
require additional tweaks, so just keeping it simple here.

Similar to https://github.com/iterative/dvc/pull/9974
2023-09-26 01:27:59 +03:00
Ruslan Kuprieiev 18b4d610d6 checkout: tweak messages and don't clear pbars 2023-09-25 23:17:26 +03:00
Ruslan Kuprieiev b77218aa5b fetch: use ui.progress instead of raw Callback 2023-09-25 22:21:42 +03:00
Peter Rowlands (변기호) 12b572596f repo: add API and CLI for reading artifacts (#9770)
* deps: add GTO as a dependency

* deps: bump dvc-studio-client to 0.13.0

* fs: return downloaded count in fs.download()

* utils: add type hints for resolve_output

* artifacts: add `dvc artifacts get`

* api: add `api.artifacts_show`

* artifacts get: support --remote, --remote-config

* gto: update to 1.3.0
2023-09-25 20:21:34 +05:45
dberenbaum e8362db9bc exp: force add include_untracked paths 2023-09-25 19:02:38 +09:00
skshetry 6502234022 ci: build: split publishing and building into separate jobs (#9970)
https://github.com/pypa/gh-action-pypi-publish suggests separating building from publishing
for security reasons.
2023-09-23 18:05:03 +05:45
Ruslan Kuprieiev 1fa9f1f6ea index: fix fs cache location
Existing logic might result in weird fs cache location like
`.dvc/cache/files/md5/fs`, while we really only want
`.dvc/cache/fs`, since it is not an odb cache.
2023-09-23 05:44:05 +03:00
Peter Rowlands 4645975ee3 gc -c: always gc both 2.x and 3.x odbs for remotes 2023-09-22 09:55:50 +09:00
Peter Rowlands 5139da5f8e output: return both 2.x and 3.x remote odbs in get_used_objects 2023-09-22 09:55:50 +09:00
Peter Rowlands f801eb4380 studio: remove redundant type cast 2023-09-21 15:20:27 +09:00
Peter Rowlands acb54a3462 deps: bump dvc-data to 2.16.3 2023-09-21 15:20:27 +09:00
Peter Rowlands 7b40968741 build: temporarily pin setuptools-scm 2023-09-21 15:08:28 +09:00
pre-commit-ci[bot] b85608121f [pre-commit.ci] pre-commit autoupdate (#9960)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.288 → v0.0.290](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.288...v0.0.290)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-19 09:29:23 +05:45
Ruslan Kuprieiev 4bf19debb4 index: don't load external outs 2023-09-19 01:21:04 +03:00
BradyJ27anddaavoo 51dc78a135 exp: add rename command (#9812)
* Add renaming for finished experiments

* Update arg parser

* Add rename by rev

* Add rename tests

* Change remove to rename; Refactor arguments error

* Remove --rev; Use resolve_name; Add test

* Update testing; Fix python3.10 rename failure

* Update rename test

* Update tests and error handling

* exp rename: Add `--force`.

Fail if experiment new_name exists but allow to overwrite it.

---------

Co-authored-by: daavoo <[email protected]>
2023-09-18 17:01:32 +00:00
Peter Rowlands 784a5b5199 api.DVCFileSystem: fix fsspec compliance 2023-09-16 00:05:31 +09:00
dberenbaum bf2af86397 drop irrelevant errors in tests/unit/render/test_match.py 2023-09-14 10:36:46 -04:00
dberenbaum 08d37912d6 add util function to group plot definitions 2023-09-14 10:36:46 -04:00
dberenbaum 0e66aa2b6a add integration test for dvclive 2.x->3.x plots 2023-09-14 10:36:46 -04:00
dberenbaum f9340ed7b9 add test cases for plots group_definitions 2023-09-14 10:36:46 -04:00
dberenbaum e6ddb31525 keep config_file in plot_id if multiple config_files 2023-09-14 10:36:46 -04:00
dberenbaum 7f2ddf943f dropped config_file from calls to _matches 2023-09-14 10:36:46 -04:00
dberenbaum 3d664f1dbf drop unused arg 2023-09-14 10:36:46 -04:00
Dave BerenbaumandDavid de la Iglesia Castro cdbed33386 Update dvc/render/match.py
Co-authored-by: David de la Iglesia Castro <[email protected]>
2023-09-14 10:36:46 -04:00
dberenbaum e1599ae280 add test for merging plot ids across config files 2023-09-14 10:36:46 -04:00
pre-commit-ci[bot] ee367dcb1b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-09-14 10:36:46 -04:00
dberenbaum 7e7d0e1dd8 merge plot ids across dvc.yaml files 2023-09-14 10:36:46 -04:00
dberenbaum d81a1bbf56 make exp run --dry imply --temp 2023-09-13 15:06:46 +09:00
Bilel Omrani ba0834caa0 Fix Repo instantiation error when using fsspec chaining API 2023-09-12 21:41:48 +03:00
pre-commit-ci[bot] 9a4578cc72 [pre-commit.ci] pre-commit autoupdate (#9937)
updates:
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)
- [github.com/astral-sh/ruff-pre-commit: v0.0.287 → v0.0.288](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.287...v0.0.288)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-12 20:44:17 +05:45
Helio Machadoandskshetry ccf461bc84 Migrate from PyPI tokens to Trusted Publishers (#9933)
* Migrate Python repositories to Trusted Publishers

* Update build.yaml

* Update .github/workflows/build.yaml

Co-authored-by: Helio Machado <[email protected]>

* Update build.yaml

---------

Co-authored-by: skshetry <[email protected]>
2023-09-12 20:27:37 +05:45
Ruslan Kuprieiev e9c7cd63a2 dvcfs: add tests for fsid 2023-09-11 21:20:12 +03:00
skshetry 33b6c1d876 params/metrics: cache remote files (#9932) 2023-09-11 19:13:47 +05:45
skshetry f2673acb53 metrics/params: support reading from cached files and directories, use repo-relative path consistently (#9909)
* metrics

* fix path handling

* fix dvc metrics diff

* params show/diff

* share common utils

* remove onerror_collect from exp_show

* extract read_params

* move around code

* fix path handling on Windows

* rename _show to _gather_metrics

* remove _collect_vars

* add tests

* Revert "remove _collect_vars"

This reverts commit 6d2cb500c32a882497406d66fb13e613790c3436.

* bring back _collect_vars

* fix tests

* fix paths
2023-09-11 17:41:37 +05:45
David de la Iglesia Castro 7e52c233ae import: Expose --force flag (#9927) 2023-09-08 18:43:13 +05:45
Peter Rowlands (변기호) 408d23f285 repo: use is_repo_import to check for imports in outs_graph (#9925)
repo: use is_repo_import to check for imports
2023-09-08 16:13:11 +05:45
David de la Iglesia Castro 7fb8a9a427 checkout: Expose --allow-missing arg. (#9919) 2023-09-07 09:55:50 +00:00
Ruslan Kuprieiev 784f5a5d63 deps: bump dvc-data to 2.16.0 2023-09-07 04:13:56 +03:00
Ruslan Kuprieiev 1710e379f5 dvcfs: add basic fsid
Implements fsspec's fsid https://github.com/fsspec/filesystem_spec/pull/1122

Required for #9904
2023-09-07 04:13:56 +03:00
Joe Scott aa9636e2ae Typo rwlock.py#107 Remove -> Removed
closes #9920
2023-09-06 09:21:29 -04:00
Peter Rowlands 65d412bfa3 stage.run: always set DVC_ROOT env var 2023-09-06 18:43:52 +09:00
Peter Rowlands 20606d480b exp show: drop internal legacy checkpoint support 2023-09-06 14:58:53 +09:00
dependabot[bot] e2aedb7e97 build(deps): bump actions/checkout from 3 to 4 (#9911)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 08:43:43 +05:45
pre-commit-ci[bot] bacae48f96 [pre-commit.ci] pre-commit autoupdate (#9910)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.286 → v0.0.287](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.286...v0.0.287)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-05 08:34:38 +05:45
Peter Rowlands bf119e70a9 exp run: track files for skipped/run-cached stages 2023-09-04 16:23:54 +09:00
BradyJ27 3528c37b66 move: Alias mv to move (#9905)
* Alias mv to move

* Add alias for mv

* Add aliases for rm, exp rm, exp ls
2023-09-03 22:11:41 +03:00
Ruslan Kuprieiev f47d21b0c2 [WIP] ls-url/get-url/import-url: introduce --fs-config (#9888)
ls-url/get-url/import-url: introduce --fs-config

Fixes #9886
2023-09-01 02:17:17 +03:00
Ruslan Kuprieiev 7a9da0f4a1 cli: utils: DictAction: limit split to 2 elements (#9887) 2023-08-30 03:15:21 +03:00
104 changed files with 2432 additions and 791 deletions
+59 -14
View File
@@ -1,21 +1,25 @@
name: Build package
name: Build and upload package
on:
push:
branches:
- main
release:
types: [released, prereleased]
types:
- published
workflow_dispatch:
env:
FORCE_COLOR: 1
permissions:
contents: read
id-token: write
jobs:
pip:
build:
name: Build & Verify package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -24,24 +28,65 @@ jobs:
python-version: 3.8
- run: python -m pip install --upgrade pip setuptools_scm build twine
- name: Force version for Test PyPI upload
if: ${{ !startsWith(github.ref, 'refs/tags') }}
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
- run: |
echo 'PKG = "pip"'>dvc/_build.py
python -m build
- run: echo 'PKG = "pip"'>dvc/_build.py
- run: python -m build
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
- run: twine check --strict dist/*
- name: Publish to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
- uses: actions/upload-artifact@v3
with:
name: Packages
path: dist/
- name: Publish to Test PyPI
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
test-pypi-publish:
name: Publish dev package to test.pypi.org
runs-on: ubuntu-latest
needs: build
if: ${{ github.event.action == 'published' || (github.repository == 'iterative/dvc' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
environment:
name: test-pypi
url: https://test.pypi.org/p/dvc
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Upload package to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
pypi-publish:
name: Publish released package to pypi.org
runs-on: ubuntu-latest
needs: build
if: github.event.action == 'published'
environment:
name: pypi
url: https://pypi.org/p/dvc
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
+2 -2
View File
@@ -23,11 +23,11 @@ jobs:
plugin: ["dvc-s3"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
path: dvc
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
repository: iterative/${{ matrix.plugin }}
ref: main
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
pyv: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
+2 -2
View File
@@ -24,7 +24,7 @@ repos:
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.7.0
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
@@ -32,7 +32,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.0.286'
rev: 'v0.0.291'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
+2
View File
@@ -1,5 +1,6 @@
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .artifacts import artifacts_show
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import exp_save, exp_show
@@ -10,6 +11,7 @@ __all__ = [
"all_branches",
"all_commits",
"all_tags",
"artifacts_show",
"exp_save",
"exp_show",
"get_url",
+47
View File
@@ -0,0 +1,47 @@
from typing import Any, Dict, Optional
from dvc.repo import Repo
def artifacts_show(
name: str,
version: Optional[str] = None,
stage: Optional[str] = None,
repo: Optional[str] = None,
) -> Dict[str, str]:
"""
Return path and Git revision for an artifact in a DVC project.
The resulting path and revision can be used in conjunction with other dvc.api
calls to open and read the artifact.
Args:
name (str): name of the artifact to open.
version (str, optional): version of the artifact to open. Defaults to
the latest version.
stage (str, optional): name of the model registry stage.
repo: (str, optional): path or URL for the DVC repo.
Returns:
Dictionary of the form:
{
"rev": ...,
"path": ...,
}
Raises:
dvc.exceptions.ArtifactNotFoundError: The specified artifact was not found in
the repo.
"""
if version and stage:
raise ValueError("Artifact version and stage are mutually exclusive.")
repo_kwargs: Dict[str, Any] = {
"subrepos": True,
"uninitialized": True,
}
with Repo.open(repo, **repo_kwargs) as _repo:
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
with _repo.switch(rev):
path = _repo.artifacts.get_path(name)
return {"rev": rev, "path": path}
+10 -8
View File
@@ -7,10 +7,6 @@ from funcy import first
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():
@@ -23,7 +19,6 @@ def _postprocess(results):
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
@@ -138,13 +133,17 @@ def metrics_show(
.. _Git revision:
https://git-scm.com/docs/revisions
"""
from dvc.repo.metrics.show import to_relpath
with Repo.open(repo, config=config) as _repo:
metrics = _repo.metrics.show(
targets=targets,
revs=rev if rev is None else [rev],
onerror=_onerror_raise,
on_error="raise",
)
metrics = {
k: to_relpath(_repo.fs, _repo.root_dir, v) for k, v in metrics.items()
}
metrics = _postprocess(metrics)
@@ -382,6 +381,8 @@ def params_show(
https://git-scm.com/docs/revisions
"""
from dvc.repo.metrics.show import to_relpath
if isinstance(stages, str):
stages = [stages]
@@ -389,10 +390,11 @@ def params_show(
params = _repo.params.show(
revs=rev if rev is None else [rev],
targets=targets,
deps=deps,
onerror=_onerror_raise,
deps_only=deps,
on_error="raise",
stages=stages,
)
params = {k: to_relpath(_repo.fs, _repo.root_dir, v) for k, v in params.items()}
params = _postprocess(params)
+16
View File
@@ -36,6 +36,7 @@ def _get_odb(
class CacheManager:
CACHE_DIR = "cache"
FILES_DIR = "files"
FS_DIR = "fs"
def __init__(self, repo):
self._repo = repo
@@ -73,6 +74,21 @@ class CacheManager:
legacy_odb = _get_odb(repo, settings, hash_name="md5-dos2unix", **kwargs)
self._odb["legacy"] = legacy_odb
@property
def fs_cache(self):
"""Filesystem-based cache.
Currently used as a temporary location to download files that we don't
yet have a regular oid (e.g. md5) for.
"""
from dvc_data.index import FileStorage
return FileStorage(
key=(),
fs=self.local.fs,
path=self.local.fs.path.join(self.default_local_cache_dir, self.FS_DIR),
)
def _init_odb(self, schemes):
for scheme in schemes:
remote = self.config.get(scheme)
+2
View File
@@ -7,6 +7,7 @@ from functools import lru_cache
from dvc import __version__
from dvc.commands import (
add,
artifacts,
cache,
check_ignore,
checkout,
@@ -89,6 +90,7 @@ COMMANDS = [
experiments,
check_ignore,
data,
artifacts,
]
+1 -1
View File
@@ -11,7 +11,7 @@ class DictAction(argparse.Action):
kvs = [values]
for kv in kvs:
key, value = kv.split("=")
key, value = kv.split("=", 1)
if not value:
raise argparse.ArgumentError(
self,
+133
View File
@@ -0,0 +1,133 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import DictAction, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdArtifactsGet(CmdBaseNoRepo):
def run(self):
from dvc.repo.artifacts import Artifacts
from dvc.scm import CloneError
from dvc.ui import ui
try:
count, out = Artifacts.get(
self.args.url,
name=self.args.name,
version=self.args.rev,
stage=self.args.stage,
force=self.args.force,
config=self.args.config,
remote=self.args.remote,
remote_config=self.args.remote_config,
out=self.args.out,
)
ui.write(f"Downloaded {count} file(s) to '{out}'")
return 0
except CloneError:
logger.exception("failed to get '%s'", self.args.name)
return 1
except DvcException:
logger.exception(
"failed to get '%s' from '%s'", self.args.name, self.args.url
)
return 1
def add_parser(subparsers, parent_parser):
ARTIFACTS_HELP = "DVC model registry artifact commands."
artifacts_parser = subparsers.add_parser(
"artifacts",
parents=[parent_parser],
description=append_doc_link(ARTIFACTS_HELP, "artifacts"),
help=ARTIFACTS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
artifacts_subparsers = artifacts_parser.add_subparsers(
dest="cmd",
help="Use `dvc artifacts CMD --help` to display command-specific help.",
)
fix_subparsers(artifacts_subparsers)
ARTIFACTS_GET_HELP = "Download an artifact from a DVC project."
get_parser = artifacts_subparsers.add_parser(
"get",
parents=[parent_parser],
description=append_doc_link(ARTIFACTS_GET_HELP, "artifacts/get"),
help=ARTIFACTS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
get_parser.add_argument("url", help="Location of DVC repository to download from")
get_parser.add_argument(
"name", help="Name of artifact in the repository"
).complete = completion.FILE
get_parser.add_argument(
"--rev",
nargs="?",
help="Artifact version",
metavar="<version>",
)
get_parser.add_argument(
"--stage",
nargs="?",
help="Artifact stage",
metavar="<stage>",
)
get_parser.add_argument(
"-o",
"--out",
nargs="?",
help="Destination path to download artifact to",
metavar="<path>",
).complete = completion.DIR
get_parser.add_argument(
"-j",
"--jobs",
type=int,
help=(
"Number of jobs to run simultaneously. "
"The default value is 4 * cpu_count(). "
),
metavar="<number>",
)
get_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override local file or folder if exists.",
)
get_parser.add_argument(
"--config",
type=str,
help=(
"Path to a config file that will be merged with the config "
"in the target repository."
),
)
get_parser.add_argument(
"--remote",
type=str,
help=(
"Remote name to set as a default in the target repository "
"(only applicable when downloading from DVC remote)."
),
)
get_parser.add_argument(
"--remote-config",
type=str,
nargs="*",
action=DictAction,
help=(
"Remote config options to merge with a remote's config (default or one "
"specified by '--remote') in the target repository (only applicable "
"when downloading from DVC remote)."
),
)
get_parser.set_defaults(func=CmdArtifactsGet)
+7
View File
@@ -37,6 +37,7 @@ class CmdCheckout(CmdBase):
force=self.args.force,
relink=self.args.relink,
recursive=self.args.recursive,
allow_missing=self.args.allow_missing,
)
except CheckoutError as _exc:
exc = _exc
@@ -101,6 +102,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Recreate links or copies from cache to workspace.",
)
checkout_parser.add_argument(
"--allow-missing",
action="store_true",
default=False,
help="Ignore errors if some of the files or directories are missing.",
)
checkout_parser.add_argument(
"targets",
nargs="*",
+4
View File
@@ -14,6 +14,10 @@ class CmdDataBase(CmdBase):
from dvc.utils.humanize import get_summary
default_msg = "Everything is up to date."
if not self.args.remote and not self.repo.config["core"].get("remote"):
ui.warn("No remote provided and no default remote set.")
ui.write(get_summary(stats.items()) or default_msg)
+2
View File
@@ -12,6 +12,7 @@ from dvc.commands.experiments import (
push,
queue_worker,
remove,
rename,
run,
save,
show,
@@ -28,6 +29,7 @@ SUB_COMMANDS = [
push,
queue_worker,
remove,
rename,
run,
save,
show,
+6 -13
View File
@@ -4,21 +4,13 @@ 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):
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)
self.repo.experiments.apply(self.args.experiment, force=self.args.force)
return 0
@@ -33,10 +25,11 @@ def add_parser(experiments_subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_apply_parser.add_argument(
"--no-force",
action="store_false",
dest="force",
help="Fail if this command would overwrite conflicting changes.",
"-f",
"--force",
action="store_true",
default=False,
help="Do not prompt when removing working directory files.",
)
experiments_apply_parser.add_argument(
"experiment", help="Experiment to be applied."
+1 -1
View File
@@ -49,7 +49,7 @@ def add_parser(experiments_subparsers, parent_parser):
),
)
exec_run_parser.add_argument(
"-M",
"-m",
"--message",
type=str,
default=None,
+1
View File
@@ -59,6 +59,7 @@ def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_LIST_HELP = "List local and remote experiments."
experiments_list_parser = experiments_subparsers.add_parser(
"list",
aliases=["ls"],
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_LIST_HELP, "exp/list"),
help=EXPERIMENTS_LIST_HELP,
+1
View File
@@ -50,6 +50,7 @@ def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
experiments_remove_parser = experiments_subparsers.add_parser(
"remove",
aliases=["rm"],
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_REMOVE_HELP, "exp/remove"),
help=EXPERIMENTS_REMOVE_HELP,
+68
View File
@@ -0,0 +1,68 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.ui import ui
logger = logging.getLogger(__name__)
class CmdExperimentsRename(CmdBase):
def run(self):
from dvc.utils import humanize
if not (self.args.experiment and self.args.name):
raise InvalidArgumentError(
"An experiment to rename and a new experiment name are required."
)
renamed = self.repo.experiments.rename(
exp_name=self.args.experiment,
new_name=self.args.name,
git_remote=self.args.git_remote,
force=self.args.force,
)
if renamed:
ui.write(f"Renamed experiments: {humanize.join(map(repr, renamed))}")
else:
ui.write("No experiments to rename.")
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_RENAME_HELP = "Rename experiments."
experiments_rename_parser = experiments_subparsers.add_parser(
"rename",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_RENAME_HELP, "exp/rename"),
help=EXPERIMENTS_RENAME_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
rename_group = experiments_rename_parser.add_mutually_exclusive_group()
rename_group.add_argument(
"-g",
"--git-remote",
metavar="<git_remote>",
help="Name or URL of the Git remote to rename the experiment from",
)
experiments_rename_parser.add_argument(
"experiment",
help="Experiment to rename.",
nargs="?",
metavar="<experiment>",
)
experiments_rename_parser.add_argument(
"name",
help="New name for the experiment.",
metavar="<name>",
)
experiments_rename_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Replace experiment if it already exists.",
)
experiments_rename_parser.set_defaults(func=CmdExperimentsRename)
+1 -1
View File
@@ -26,7 +26,7 @@ class CmdExperimentsRun(CmdRepro):
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_RUN_HELP = "Run or resume an experiment."
EXPERIMENTS_RUN_HELP = "Run an experiment."
experiments_run_parser = experiments_subparsers.add_parser(
"run",
parents=[parent_parser],
+1 -1
View File
@@ -72,7 +72,7 @@ def add_parser(experiments_subparsers, parent_parser):
metavar="<path>",
)
save_parser.add_argument(
"-M",
"-m",
"--message",
type=str,
default=None,
+7
View File
@@ -81,6 +81,7 @@ class CmdGC(CmdBase):
rev=self.args.rev,
num=self.args.num,
not_in_remote=self.args.not_in_remote,
dry=self.args.dry,
)
return 0
@@ -213,4 +214,10 @@ def add_parser(subparsers, parent_parser):
),
metavar="<paths>",
)
gc_parser.add_argument(
"--dry",
action="store_true",
default=False,
help=("Only print what would get removed without actually removing."),
)
gc_parser.set_defaults(func=CmdGC)
+9 -1
View File
@@ -3,7 +3,7 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.cli.utils import DictAction, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -19,6 +19,7 @@ class CmdGetUrl(CmdBaseNoRepo):
out=self.args.out,
jobs=self.args.jobs,
force=self.args.force,
fs_config=self.args.fs_config,
)
return 0
except DvcException:
@@ -58,4 +59,11 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Override local file or folder if exists.",
)
get_parser.add_argument(
"--fs-config",
type=str,
nargs="*",
action=DictAction,
help="Config options for the target url.",
)
get_parser.set_defaults(func=CmdGetUrl)
+8
View File
@@ -25,6 +25,7 @@ class CmdImport(CmdBase):
config=self.args.config,
remote=self.args.remote,
remote_config=self.args.remote_config,
force=self.args.force,
)
except CloneError:
logger.exception("failed to import '%s'", self.args.path)
@@ -65,6 +66,13 @@ def add_parser(subparsers, parent_parser):
help="Destination path to download files to",
metavar="<path>",
).complete = completion.DIR
import_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override destination file or folder if exists.",
)
import_parser.add_argument(
"--rev",
nargs="?",
+9 -1
View File
@@ -3,7 +3,7 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.cli.utils import DictAction, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -22,6 +22,7 @@ class CmdImportUrl(CmdBase):
jobs=self.args.jobs,
force=self.args.force,
version_aware=self.args.version_aware,
fs_config=self.args.fs_config,
)
except DvcException:
logger.exception(
@@ -114,4 +115,11 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Import using cloud versioning. Implied if the URL contains a version ID.",
)
import_parser.add_argument(
"--fs-config",
type=str,
nargs="*",
action=DictAction,
help="Config options for the target url.",
)
import_parser.set_defaults(func=CmdImportUrl)
+13 -2
View File
@@ -2,7 +2,7 @@ import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.cli.utils import DictAction, append_doc_link
from .ls import show_entries
@@ -13,7 +13,11 @@ class CmdListUrl(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
entries = Repo.ls_url(
self.args.url,
recursive=self.args.recursive,
fs_config=self.args.fs_config,
)
if entries:
show_entries(entries, with_color=True, with_size=self.args.size)
return 0
@@ -43,4 +47,11 @@ def add_parser(subparsers, parent_parser):
action="store_true",
help="Show sizes.",
)
lsurl_parser.add_argument(
"--fs-config",
type=str,
nargs="*",
action=DictAction,
help="Config options for the target url.",
)
lsurl_parser.set_defaults(func=CmdListUrl)
+42 -22
View File
@@ -4,7 +4,6 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils.serialize import encode_exception
@@ -20,17 +19,25 @@ class CmdMetricsBase(CmdBase):
class CmdMetricsShow(CmdMetricsBase):
def run(self):
try:
metrics = self.repo.metrics.show(
self.args.targets,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
recursive=self.args.recursive,
from dvc.repo.metrics.show import to_relpath
from dvc.utils import errored_revisions
metrics = self.repo.metrics.show(
self.args.targets,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
)
metrics = {
k: to_relpath(self.repo.fs, self.repo.root_dir, v)
for k, v in metrics.items()
}
if errored := errored_revisions(metrics):
ui.error_write(
"DVC failed to load some metrics for following revisions:"
f" '{', '.join(errored)}'."
)
except DvcException:
logger.exception("")
return 1
if self.args.json:
ui.write_json(metrics, default=encode_exception)
@@ -52,17 +59,26 @@ class CmdMetricsShow(CmdMetricsBase):
class CmdMetricsDiff(CmdMetricsBase):
def run(self):
try:
diff = self.repo.metrics.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
recursive=self.args.recursive,
all=self.args.all,
import os
from os.path import relpath
diff_result = self.repo.metrics.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
all=self.args.all,
)
errored = [rev for rev, err in diff_result.get("errors", {}).items() if err]
if errored:
ui.error_write(
"DVC failed to load some metrics for following revisions:"
f" '{', '.join(errored)}'."
)
except DvcException:
logger.exception("failed to show metrics diff")
return 1
start = relpath(os.getcwd(), self.repo.root_dir)
diff = diff_result.get("diff", {})
diff = {relpath(path, start): result for path, result in diff.items()}
if self.args.json:
ui.write_json(diff)
@@ -184,10 +200,14 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_diff_parser.add_argument(
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
"a_rev",
nargs="?",
help="Old Git commit to compare (defaults to HEAD)",
default="HEAD",
)
metrics_diff_parser.add_argument(
"b_rev",
default="workspace",
nargs="?",
help="New Git commit to compare (defaults to the current workspace)",
)
+1
View File
@@ -30,6 +30,7 @@ def add_parser(subparsers, parent_parser):
move_parser = subparsers.add_parser(
"move",
aliases=["mv"],
parents=[parent_parser],
description=append_doc_link(MOVE_DESCRIPTION, "move"),
help=MOVE_HELP,
+25 -12
View File
@@ -4,7 +4,6 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -14,17 +13,27 @@ class CmdParamsDiff(CmdBase):
UNINITIALIZED = True
def run(self):
try:
diff = self.repo.params.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
all=self.args.all,
deps=self.args.deps,
import os
from os.path import relpath
diff_result = self.repo.params.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
all=self.args.all,
deps_only=self.args.deps,
)
errored = [rev for rev, err in diff_result.get("errors", {}).items() if err]
if errored:
ui.error_write(
"DVC failed to load some metrics for following revisions:"
f" '{', '.join(errored)}'."
)
except DvcException:
logger.exception("failed to show params diff")
return 1
start = relpath(os.getcwd(), self.repo.root_dir)
diff = diff_result.get("diff", {})
diff = {relpath(path, start): result for path, result in diff.items()}
if self.args.json:
ui.write_json(diff)
@@ -74,10 +83,14 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
params_diff_parser.add_argument(
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
"a_rev",
nargs="?",
default="HEAD",
help="Old Git commit to compare (defaults to HEAD)",
)
params_diff_parser.add_argument(
"b_rev",
default="workspace",
nargs="?",
help="New Git commit to compare (defaults to the current workspace)",
)
+1
View File
@@ -26,6 +26,7 @@ def add_parser(subparsers, parent_parser):
)
remove_parser = subparsers.add_parser(
"remove",
aliases=["rm"],
parents=[parent_parser],
description=append_doc_link(REMOVE_HELP, "remove"),
help=REMOVE_HELP,
+1 -1
View File
@@ -135,7 +135,7 @@ class DataCloud:
remote = self.get_remote(name=name, command=command)
if remote.fs.version_aware or remote.worktree:
raise NoRemoteError(
raise RemoteConfigError(
f"'{command}' is unsupported for cloud versioned remotes"
)
if hash_name in LEGACY_HASH_NAMES:
+7 -3
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Any, Mapping, Set
from typing import Any, Dict, List, Mapping, Set
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
@@ -15,6 +15,7 @@ SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
Output.PARAM_FS_CONFIG: dict,
}
@@ -36,7 +37,10 @@ def loadd_from(stage, d_list):
p = d.pop(Output.PARAM_PATH, None)
files = d.pop(Output.PARAM_FILES, None)
hash_name = d.pop(Output.PARAM_HASH, None)
ret.append(_get(stage, p, d, files=files, hash_name=hash_name))
fs_config = d.pop(Output.PARAM_FS_CONFIG, None)
ret.append(
_get(stage, p, d, files=files, hash_name=hash_name, fs_config=fs_config)
)
return ret
@@ -54,7 +58,7 @@ def loads_from(stage, s_list, erepo=None, fs_config=None):
]
def _merge_params(s_list):
def _merge_params(s_list) -> Dict[str, List[str]]:
d = defaultdict(list)
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
+45 -30
View File
@@ -2,7 +2,7 @@ import logging
import os
import typing
from collections import defaultdict
from typing import Any, Dict
from typing import TYPE_CHECKING, Any, Dict, List, Optional
import dpath
@@ -12,6 +12,9 @@ from dvc_data.hashfile.hash_info import HashInfo
from .base import Dependency
if TYPE_CHECKING:
from dvc.fs import FileSystem
logger = logging.getLogger(__name__)
@@ -31,6 +34,37 @@ class BadParamFileError(DvcException):
pass
def read_param_file(
fs: "FileSystem",
path: str,
key_paths: Optional[List[str]] = None,
flatten: bool = False,
**load_kwargs,
) -> Any:
config = load_path(path, fs, **load_kwargs)
if not key_paths:
return config
ret = {}
if flatten:
for key_path in key_paths:
try:
ret[key_path] = dpath.get(config, key_path, separator=".")
except KeyError:
continue
return ret
from dpath import merge
for key_path in key_paths:
merge(
ret,
dpath.search(config, key_path, separator="."),
separator=".",
)
return ret
class ParamsDependency(Dependency):
PARAM_PARAMS = "params"
DEFAULT_PARAMS_FILE = "params.yaml"
@@ -75,31 +109,19 @@ class ParamsDependency(Dependency):
self, flatten: bool = True, **kwargs: typing.Any
) -> Dict[str, typing.Any]:
try:
config = self.read_file()
self.validate_filepath()
except MissingParamsFile:
config = {}
return {}
if not self.params:
return config
ret = {}
if flatten:
for param in self.params:
try:
ret[param] = dpath.get(config, param, separator=".")
except KeyError:
continue
return ret
from dpath import merge
for param in self.params:
merge(
ret,
dpath.search(config, param, separator="."),
separator=".",
try:
return read_param_file(
self.repo.fs,
self.fs_path,
list(self.params) if self.params else None,
flatten=flatten,
)
return ret
except ParseError as exc:
raise BadParamFileError(f"Unable to read parameters from '{self}'") from exc
def workspace_status(self):
if not self.exists:
@@ -149,13 +171,6 @@ class ParamsDependency(Dependency):
f"'{self}' is a directory, expected a parameters file"
)
def read_file(self):
self.validate_filepath()
try:
return load_path(self.fs_path, self.repo.fs)
except ParseError as exc:
raise BadParamFileError(f"Unable to read parameters from '{self}'") from exc
def get_hash(self):
info = self.read_params()
+22 -1
View File
@@ -1,6 +1,6 @@
"""Exceptions raised by the dvc."""
import errno
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List, Optional, Set
from dvc.utils import format_link
@@ -336,3 +336,24 @@ class CacheLinkError(DvcException):
class PrettyDvcException(DvcException):
def __pretty_exc__(self, **kwargs):
"""Print prettier exception message."""
class ArtifactNotFoundError(DvcException):
"""Thrown if an artifact is not found in the DVC repo.
Args:
name (str): artifact name.
"""
def __init__(
self,
name: str,
version: Optional[str] = None,
stage: Optional[str] = None,
):
self.name = name
self.version = version
self.stage = stage
desc = f" @ {stage or version}" if (stage or version) else ""
super().__init__(f"Unable to find artifact '{name}{desc}'")
+6 -2
View File
@@ -49,7 +49,9 @@ known_implementations.update(
# pylint: enable=unused-import
def download(fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None):
def download(
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
) -> int:
with Callback.as_tqdm_callback(
desc=f"Downloading {fs.path.name(fs_path)}",
unit="files",
@@ -63,7 +65,8 @@ def download(fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
if not path.endswith(fs.path.flavour.sep)
]
if not from_infos:
return localfs.makedirs(to, exist_ok=True)
localfs.makedirs(to, exist_ok=True)
return 0
to_infos = [
localfs.path.join(to, *fs.path.relparts(info, fs_path))
for info in from_infos
@@ -82,6 +85,7 @@ def download(fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
callback=cb,
batch_size=jobs,
)
return len(to_infos)
def parse_external_url(url, config=None):
+39 -4
View File
@@ -18,7 +18,7 @@ from .data import DataFileSystem
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import StrPath
from dvc.types import DictStrAny, StrPath
logger = logging.getLogger(__name__)
@@ -76,14 +76,22 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
cachable = False
root_marker = "/"
def __init__(
def __init__( # noqa: PLR0913
self,
url: Optional[str] = None,
rev: Optional[str] = None,
repo: Optional["Repo"] = None,
subrepos: bool = False,
repo_factory: Optional[RepoFactory] = None,
**repo_kwargs: Any,
fo: Optional[str] = None,
# pylint:disable-next=unused-argument
target_options: Optional[Dict[str, Any]] = None, # noqa: ARG002
# pylint:disable-next=unused-argument
target_protocol: Optional[str] = None, # noqa: ARG002
config: Optional["DictStrAny"] = None,
remote: Optional[str] = None,
remote_config: Optional["DictStrAny"] = None,
**kwargs,
) -> None:
"""DVC + git-tracked files fs.
@@ -103,6 +111,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
By default, it ignores subrepos.
repo_factory (callable): A function to initialize subrepo with.
The default is `Repo`.
config (dict): Repo config to be passed into `repo_factory`.
remote (str): Remote name to be passed into `repo_factory`.
remote_config(dict): Remote config to be passed into `repo_factory`.
Examples:
- Opening a filesystem from repo in current working directory
@@ -125,7 +136,15 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
super().__init__()
self._repo_stack = ExitStack()
if repo is None:
repo = self._make_repo(url=url, rev=rev, subrepos=subrepos, **repo_kwargs)
url = url if url is not None else fo
repo = self._make_repo(
url=url,
rev=rev,
subrepos=subrepos,
config=config,
remote=remote,
remote_config=remote_config,
)
assert repo is not None
# pylint: disable=protected-access
repo_factory = repo._fs_conf["repo_factory"]
@@ -162,6 +181,17 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
if hasattr(repo, "dvc_dir"):
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
@functools.cached_property
def fsid(self) -> str:
from fsspec.utils import tokenize
from dvc.scm import NoSCM
return "dvcfs_" + tokenize(
self.repo.url or self.repo.root_dir,
self.repo.get_rev() if not isinstance(self.repo.scm, NoSCM) else None,
)
def _get_key(self, path: "StrPath") -> Key:
path = os.fspath(path)
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
@@ -170,6 +200,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
return parts
def _get_key_from_relative(self, path) -> Key:
path = self._strip_protocol(path)
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
return parts[1:]
@@ -413,6 +444,10 @@ class DVCFileSystem(FileSystem):
def fs(self) -> "_DVCFileSystem":
return _DVCFileSystem(**self.fs_args)
@property
def fsid(self) -> str:
return self.fs.fsid
def isdvc(self, path, **kwargs) -> bool:
return self.fs.isdvc(path, **kwargs)
+20 -8
View File
@@ -44,7 +44,6 @@ from .utils.fs import path_isin
if TYPE_CHECKING:
from dvc_data.hashfile.obj import HashFile
from dvc_data.index import DataIndexKey
from dvc_objects.db import ObjectDB
from .ignore import DvcIgnoreFilter
@@ -97,6 +96,7 @@ def loadd_from(stage, d_list):
files = d.pop(Output.PARAM_FILES, None)
push = d.pop(Output.PARAM_PUSH, True)
hash_name = d.pop(Output.PARAM_HASH, None)
fs_config = d.pop(Output.PARAM_FS_CONFIG, None)
ret.append(
_get(
stage,
@@ -111,6 +111,7 @@ def loadd_from(stage, d_list):
files=files,
push=push,
hash_name=hash_name,
fs_config=fs_config,
)
)
return ret
@@ -313,6 +314,7 @@ class Output:
PARAM_PUSH = "push"
PARAM_CLOUD = "cloud"
PARAM_HASH = "hash"
PARAM_FS_CONFIG = "fs_config"
DoesNotExistError: Type[DvcException] = OutputDoesNotExistError
IsNotFileOrDirError: Type[DvcException] = OutputIsNotFileOrDirError
@@ -351,6 +353,7 @@ class Output:
if meta.version_id or files:
fs_kwargs["version_aware"] = True
self.def_fs_config = fs_config
if fs_config is not None:
fs_kwargs.update(**fs_config)
@@ -872,6 +875,9 @@ class Output:
ret[self.PARAM_PATH] = path
if self.def_fs_config:
ret[self.PARAM_FS_CONFIG] = self.def_fs_config
if not self.IS_DEPENDENCY:
ret.update(self.annot.to_dict())
if not self.use_cache:
@@ -1154,10 +1160,11 @@ class Output:
return obj.filter(prefix)
return obj
def get_used_objs( # noqa: C901
def get_used_objs( # noqa: C901, PLR0911
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
) -> Dict[Optional["HashFileDB"], Set["HashInfo"]]:
"""Return filtered set of used object IDs for this out."""
from dvc.cachemgr import LEGACY_HASH_NAMES
if not self.use_cache:
return {}
@@ -1196,13 +1203,17 @@ class Output:
return {}
if self.remote:
remote = self.repo.cloud.get_remote_odb(
remote_odb = self.repo.cloud.get_remote_odb(
name=self.remote, hash_name=self.hash_name
)
else:
remote = None
return {remote: self._named_obj_ids(obj)}
other_odb = self.repo.cloud.get_remote_odb(
name=self.remote,
hash_name=(
"md5" if self.hash_name in LEGACY_HASH_NAMES else "md5-dos2unix"
),
)
return {remote_odb: self._named_obj_ids(obj), other_odb: set()}
return {None: self._named_obj_ids(obj)}
def _named_obj_ids(self, obj):
name = str(self)
@@ -1537,4 +1548,5 @@ SCHEMA = {
Output.PARAM_REMOTE: str,
Output.PARAM_PUSH: bool,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
Output.PARAM_FS_CONFIG: dict,
}
+4 -9
View File
@@ -8,7 +8,7 @@ import dpath.options
from funcy import get_in, last
from dvc.repo.plots import _normpath, infer_data_sources
from dvc.utils.plots import get_plot_id
from dvc.utils.plots import group_definitions_by_id
from .convert import _get_converter
@@ -36,14 +36,9 @@ class PlotsData:
def group_definitions(self):
groups = defaultdict(list)
for rev, rev_content in self.data.items():
for config_file, config_file_content in (
rev_content.get("definitions", {}).get("data", {}).items()
):
for plot_id, plot_definition in config_file_content.get(
"data", {}
).items():
full_id = get_plot_id(plot_id, config_file)
groups[full_id].append((rev, plot_id, plot_definition))
definitions = rev_content.get("definitions", {}).get("data", {})
for plot_id, definition in group_definitions_by_id(definitions).items():
groups[plot_id].append((rev, *definition))
return dict(groups)
def get_definition_data(self, target_files, rev):
+26 -1
View File
@@ -577,6 +577,26 @@ class Repo:
return DVCFileSystem(repo=self, subrepos=self.subrepos, **self._fs_conf)
@cached_property
def _btime(self):
if not self.tmp_dir:
return None
# Not all python versions/filesystems/platforms provide creation
# time (st_birthtime, stx_btime, etc), so we use our own dummy
# file and its mtime instead.
path = os.path.join(self.tmp_dir, "btime")
try:
with open(path, "x"): # pylint: disable=unspecified-encoding
pass
except FileNotFoundError:
return None
except FileExistsError:
pass
return os.path.getmtime(path)
@cached_property
def site_cache_dir(self) -> str:
import getpass
@@ -609,12 +629,17 @@ class Repo:
# components were changed (useful to prevent newer dvc versions from
# using older broken cache). Please reset this back to 0 if other parts
# of the token components are changed.
salt = 0
salt = 1
# NOTE: This helps us avoid accidentally reusing cache for repositories
# that just happened to be at the same path as old deleted ones.
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
md5 = hashlib.md5( # noqa: S324 # nosec B324, B303
str(
(
root_dir,
btime,
getpass.getuser(),
version_tuple[0],
salt,
+283 -22
View File
@@ -1,42 +1,61 @@
import logging
import re
import os
import posixpath
from pathlib import Path
from typing import Dict, Optional
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
from dvc.annotations import Artifact
from dvc.dvcfile import PROJECT_FILE
from dvc.exceptions import InvalidArgumentError
from dvc.repo import Repo
from dvc.utils import relpath
from dvc.exceptions import (
ArtifactNotFoundError,
DvcException,
FileExistsLocallyError,
InvalidArgumentError,
)
from dvc.utils import relpath, resolve_output
from dvc.utils.objects import cached_property
from dvc.utils.serialize import modify_yaml
if TYPE_CHECKING:
from gto.tag import Tag as GTOTag
from scmrepo.git import GitTag
from dvc.repo import Repo
from dvc.scm import Git
logger = logging.getLogger(__name__)
# Constants are taken from GTO.
# When we make it a dependency, we can import them instead
SEPARATOR_IN_NAME = ":"
DIRNAME = r"[a-z0-9-_./]+"
NAME = r"[a-z0-9]([a-z0-9-/]*[a-z0-9])?"
NAME_RE = re.compile(f"^{NAME}$")
FULLNAME = f"((?P<dirname>{DIRNAME}){SEPARATOR_IN_NAME})?(?P<name>{NAME})"
FULLNAME_RE = re.compile(f"^{FULLNAME}$")
def check_name_format(name: str) -> None:
from gto.constants import assert_name_is_valid
from gto.exceptions import ValidationError
try:
assert_name_is_valid(name)
except ValidationError as exc:
raise InvalidArgumentError(
f"Can't use '{name}' as artifact name (ID)."
) from exc
def name_is_compatible(name: str) -> bool:
return bool(NAME_RE.search(name))
"""
Only needed by DVCLive per iterative/dvclive#715
Will be removed in future release.
"""
from gto.constants import assert_name_is_valid
from gto.exceptions import ValidationError
def check_name_format(name: str) -> None:
if not name_is_compatible(name):
raise InvalidArgumentError(
f"Can't use '{name}' as artifact name (ID)."
" You can use letters and numbers, and use '-' as separator"
" (but not at the start or end)."
)
try:
assert_name_is_valid(name)
return True
except ValidationError:
return False
def check_for_nested_dvc_repo(dvcfile: Path):
from dvc.repo import Repo
if dvcfile.is_absolute():
raise InvalidArgumentError("Use relative path to dvc.yaml.")
path = dvcfile.parent
@@ -48,11 +67,36 @@ def check_for_nested_dvc_repo(dvcfile: Path):
path = path.parent
def _reformat_name(name: str) -> str:
from gto.constants import SEPARATOR_IN_NAME, fullname_re
# NOTE: DVC accepts names like
# path/to/dvc.yaml:artifact_name
# but Studio/GTO tags are generated with
# path/to:artifact_name
m = fullname_re.match(name)
if m and m.group("dirname"):
group = m.group("dirname").rstrip(SEPARATOR_IN_NAME)
dirname, basename = posixpath.split(group)
if basename == PROJECT_FILE:
name = f"{dirname}{SEPARATOR_IN_NAME}{m.group('name')}"
return name
class Artifacts:
def __init__(self, repo: "Repo") -> None:
self.repo = repo
@cached_property
def scm(self) -> Optional["Git"]:
from dvc.scm import Git
if isinstance(self.repo.scm, Git):
return self.repo.scm
return None
def read(self) -> Dict[str, Dict[str, Artifact]]:
"""Read artifacts from dvc.yaml."""
artifacts: Dict[str, Dict[str, Artifact]] = {}
for (
dvcfile,
@@ -69,6 +113,7 @@ class Artifacts:
return artifacts
def add(self, name: str, artifact: Artifact, dvcfile: Optional[str] = None):
"""Add artifact to dvc.yaml."""
with self.repo.scm_context(quiet=True):
check_name_format(name)
dvcyaml = Path(dvcfile or PROJECT_FILE)
@@ -85,3 +130,219 @@ class Artifacts:
self.repo.scm_context.track_file(dvcfile)
return artifacts.get(name)
def get_rev(
self, name: str, version: Optional[str] = None, stage: Optional[str] = None
):
"""Return revision containing the given artifact."""
from gto.base import sort_versions
from gto.tag import find, parse_tag
assert not (version and stage)
name = _reformat_name(name)
tags: List["GitTag"] = find(
name=name, version=version, stage=stage, scm=self.scm
)
if not tags:
raise ArtifactNotFoundError(name, version=version, stage=stage)
if version or stage:
return tags[-1].target
gto_tags: List["GTOTag"] = sort_versions(parse_tag(tag) for tag in tags)
return gto_tags[0].tag.target
def get_path(self, name: str):
"""Return repo fspath for the given artifact."""
from gto.constants import SEPARATOR_IN_NAME, fullname_re
name = _reformat_name(name)
m = fullname_re.match(name)
if not m:
raise ArtifactNotFoundError(name)
dirname = m.group("dirname")
if dirname:
dirname = dirname.rstrip(SEPARATOR_IN_NAME)
dvcyaml = os.path.join(dirname, PROJECT_FILE) if dirname else PROJECT_FILE
artifact_name = m.group("name")
try:
artifact = self.read()[dvcyaml][artifact_name]
except KeyError as exc:
raise ArtifactNotFoundError(name) from exc
return os.path.join(dirname, artifact.path) if dirname else artifact.path
def download(
self,
name: str,
version: Optional[str] = None,
stage: Optional[str] = None,
out: Optional[str] = None,
force: bool = False,
jobs: Optional[int] = None,
) -> Tuple[int, str]:
"""Download the specified artifact."""
from dvc.fs import download as fs_download
logger.debug("Trying to download artifact '%s' via DVC", name)
rev = self.get_rev(name, version=version, stage=stage)
with self.repo.switch(rev):
path = self.get_path(name)
out = resolve_output(path, out, force=force)
fs = self.repo.dvcfs
fs_path = fs.from_os_path(path)
count = fs_download(
fs,
fs_path,
os.path.abspath(out),
jobs=jobs,
)
return count, out
@staticmethod
def _download_studio(
repo_url: str,
name: str,
version: Optional[str] = None,
stage: Optional[str] = None,
out: Optional[str] = None,
force: bool = False,
jobs: Optional[int] = None,
studio_config: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Tuple[int, str]:
from dvc_studio_client.model_registry import get_download_uris
from dvc.fs import Callback, HTTPFileSystem, generic, localfs
logger.debug("Trying to download artifact '%s' via studio", name)
out = out or os.getcwd()
to_infos: List[str] = []
from_infos: List[str] = []
if studio_config is None:
studio_config = {}
studio_config["repo_url"] = repo_url
try:
kwargs["dvc_studio_config"] = studio_config
for path, url in get_download_uris(
repo_url,
name,
version=version,
stage=stage,
**kwargs,
).items():
to_info = localfs.path.join(out, path)
if localfs.exists(to_info) and not force:
hint = "\nTo override it, re-run with '--force'."
raise FileExistsLocallyError( # noqa: TRY301
relpath(to_info), hint=hint
)
to_infos.append(to_info)
from_infos.append(url)
except DvcException:
raise
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
raise DvcException(
f"Failed to download artifact '{name}' via Studio"
) from exc
fs = HTTPFileSystem()
jobs = jobs or fs.jobs
with Callback.as_tqdm_callback(
desc=f"Downloading '{name}' from '{repo_url}'",
unit="files",
) as cb:
cb.set_size(len(from_infos))
generic.copy(
fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs
)
return len(to_infos), relpath(localfs.path.commonpath(to_infos))
@classmethod
def get( # noqa: C901, PLR0913
cls,
url: str,
name: str,
version: Optional[str] = None,
stage: Optional[str] = None,
config: Optional[Union[str, Dict[str, Any]]] = None,
remote: Optional[str] = None,
remote_config: Optional[Union[str, Dict[str, Any]]] = None,
out: Optional[str] = None,
force: bool = False,
jobs: Optional[int] = None,
):
from dvc.config import Config
from dvc.repo import Repo
if version and stage:
raise InvalidArgumentError(
"Artifact version and stage are mutually exclusive."
)
# NOTE: We try to download the artifact up to three times
# 1. via studio with studio config loaded from environment
# 2. via studio with studio config loaded from DVC repo 'studio'
# section + environment
# 3. via DVC remote
name = _reformat_name(name)
saved_exc: Optional[Exception] = None
try:
logger.trace("Trying studio-only config") # type: ignore[attr-defined]
return cls._download_studio(
url,
name,
version=version,
stage=stage,
out=out,
force=force,
jobs=jobs,
)
except FileExistsLocallyError:
raise
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
saved_exc = exc
if config and not isinstance(config, dict):
config = Config.load_file(config)
with Repo.open(
url=url,
subrepos=True,
uninitialized=True,
config=config,
remote=remote,
remote_config=remote_config,
) as repo:
logger.trace("Trying repo [studio] config") # type: ignore[attr-defined]
studio_config = dict(repo.config.get("studio"))
try:
return cls._download_studio(
url,
name,
version=version,
stage=stage,
out=out,
force=force,
jobs=jobs,
dvc_studio_config=studio_config,
)
except FileExistsLocallyError:
raise
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
saved_exc = exc
try:
return repo.artifacts.download(
name,
version=version,
stage=stage,
out=out,
force=force,
jobs=jobs,
)
except FileExistsLocallyError:
raise
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
if saved_exc:
logger.exception(str(saved_exc), exc_info=saved_exc.__cause__)
raise DvcException(
f"Failed to download artifact '{name}' via DVC remote"
) from exc
+5 -2
View File
@@ -140,7 +140,8 @@ def checkout( # noqa: C901
with ui.progress(
unit="entry",
desc="Building data index",
desc="Building workspace index",
leave=True,
) as pb:
old = build_data_index(
view,
@@ -155,6 +156,7 @@ def checkout( # noqa: C901
with ui.progress(
desc="Comparing indexes",
unit="entry",
leave=True,
) as pb:
diff = compare(old, new, relink=relink, delete=True, callback=pb.as_callback())
@@ -175,7 +177,8 @@ def checkout( # noqa: C901
with ui.progress(
unit="file",
desc="Checkout",
desc="Applying changes",
leave=True,
) as pb:
apply(
diff,
+17 -3
View File
@@ -22,7 +22,7 @@ def _hash(entry):
return None
def _diff(old, new, with_missing=False):
def _diff(old, new, data_keys, with_missing=False):
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
from dvc_data.index.diff import diff as idiff
@@ -34,12 +34,23 @@ def _diff(old, new, with_missing=False):
"not in cache": [],
}
def meta_cmp_key(meta):
if not meta:
return meta
return meta.isdir
for change in idiff(
old,
new,
with_renames=True,
hash_only=True,
meta_cmp_key=meta_cmp_key,
roots=data_keys,
):
if (change.old and change.old.isdir and not change.old.hash_info) or (
change.new and change.new.isdir and not change.new.hash_info
):
continue
if change.typ == ADD:
ret["added"].append(
{
@@ -116,6 +127,7 @@ def diff(
b_rev = "workspace"
with_missing = True
data_keys = set()
for rev in self.brancher(revs=[a_rev, b_rev]):
if rev == "workspace" and b_rev != "workspace":
# brancher always returns workspace, but we only need to compute
@@ -132,6 +144,8 @@ def diff(
recursive=recursive,
)
data_keys.update(view.data_keys.get("repo", set()))
if rev == "workspace":
from .index import build_data_index
@@ -165,4 +179,4 @@ def diff(
new = indexes[b_rev]
with ui.status("Calculating diff"):
return _diff(old, new, with_missing=with_missing)
return _diff(old, new, data_keys, with_missing=with_missing)
+5
View File
@@ -395,6 +395,11 @@ class Experiments:
return remove(self.repo, *args, **kwargs)
def rename(self, *args, **kwargs):
from dvc.repo.experiments.rename import rename
return rename(self.repo, *args, **kwargs)
def clean(self, *args, **kwargs):
from dvc.repo.experiments.clean import clean
-2
View File
@@ -24,8 +24,6 @@ class ExperimentExistsError(DvcException):
"Experiment conflicts with existing experiment "
f"'{name}'. To overwrite the existing experiment run:\n\n"
f"\tdvc exp {command} -f ...\n\n"
f"To {command} this experiment with a different name run:\n\n"
f"\tdvc exp {command} -n <new_name> ...\n"
)
super().__init__(msg)
self.name = name
+16 -15
View File
@@ -23,7 +23,6 @@ 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
@@ -254,15 +253,6 @@ class BaseExecutor(ABC):
**kwargs,
)
@classmethod
def _get_stage_files(cls, stages: List["Stage"]) -> List[str]:
from dvc.stage.utils import _get_stage_files
ret: List[str] = []
for stage in stages:
ret.extend(_get_stage_files(stage))
return ret
@classmethod
def _get_top_level_paths(cls, repo: "Repo") -> List["str"]:
return list(
@@ -305,7 +295,7 @@ class BaseExecutor(ABC):
stages = dvc.commit([], force=True, relink=False)
exp_hash = cls.hash_exp(stages)
if include_untracked:
dvc.scm.add(include_untracked)
dvc.scm.add(include_untracked, force=True) # type: ignore[call-arg]
cls.commit(
dvc.scm, # type: ignore[arg-type]
exp_hash,
@@ -518,10 +508,8 @@ class BaseExecutor(ABC):
recursive=kwargs.get("recursive", False),
)
kwargs["repro_fn"] = cls._repro_and_track
stages = dvc.reproduce(*args, **kwargs)
if paths := cls._get_stage_files(stages):
logger.debug("Staging stage-related files: %s", paths)
dvc.scm_context.add(paths)
if paths := cls._get_top_level_paths(dvc):
logger.debug("Staging top-level files: %s", paths)
dvc.scm_context.add(paths)
@@ -546,6 +534,17 @@ class BaseExecutor(ABC):
# multiprocessing calls
return ExecutorResult(exp_hash, exp_ref, repro_force)
@staticmethod
def _repro_and_track(stage: "Stage", **kwargs) -> Optional["Stage"]:
from dvc.repo.reproduce import _reproduce_stage
from dvc.stage.utils import _get_stage_files
ret = _reproduce_stage(stage, **kwargs)
if not kwargs.get("dry") and (paths := _get_stage_files(stage)):
logger.debug("Staging stage-related files: %s", paths)
stage.repo.scm_context.add(paths)
return ret
@classmethod
def _repro_commit(
cls,
@@ -644,13 +643,15 @@ class BaseExecutor(ABC):
info.status = TaskStatus.FAILED
raise
finally:
from dvc.repo.metrics.show import _gather_metrics
post_live_metrics(
"done",
info.baseline_rev,
info.name, # type: ignore[arg-type]
"dvc",
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
metrics=get_in(dvc.metrics.show(), ["", "data"]),
metrics=_gather_metrics(dvc, on_error="return"),
dvc_studio_config=dvc_studio_config,
)
+2
View File
@@ -348,6 +348,8 @@ class BaseStashQueue(ABC):
if not name:
name = get_random_exp_name(self.scm, baseline_rev)
run_env[DVC_EXP_NAME] = name
# Override DVC_ROOT env var to point to the parent DVC repo
# root (and not an executor tempdir root)
run_env[DVC_ROOT] = self.repo.root_dir
# save studio config to read later by dvc and dvclive
+60
View File
@@ -0,0 +1,60 @@
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from dvc.repo.experiments.exceptions import (
ExperimentExistsError,
UnresolvedExpNamesError,
)
from dvc.repo.experiments.utils import resolve_name
from dvc.scm import Git
from .refs import ExpRefInfo
if TYPE_CHECKING:
from dvc.repo import Repo
logger = logging.getLogger(__name__)
def rename(
repo: "Repo",
new_name: str,
exp_name: Union[str, None] = None,
git_remote: Optional[str] = None,
force: bool = False,
) -> Union[List[str], None]:
renamed: List[str] = []
remained: List[str] = []
assert isinstance(repo.scm, Git)
if exp_name == new_name:
return None
if exp_name:
results: Dict[str, Union[ExpRefInfo, None]] = resolve_name(
scm=repo.scm, exp_names=exp_name, git_remote=git_remote
)
for name, result in results.items():
if result is None:
remained.append(name)
continue
new_ref = ExpRefInfo(baseline_sha=result.baseline_sha, name=new_name)
if repo.scm.get_ref(str(new_ref)) and not force:
raise ExperimentExistsError(new_name)
_rename_exp(scm=repo.scm, ref_info=result, new_name=new_name)
renamed.append(name)
if remained:
raise UnresolvedExpNamesError(remained, git_remote=git_remote)
return renamed
def _rename_exp(scm: "Git", ref_info: "ExpRefInfo", new_name: str):
rev = scm.get_ref(str(ref_info))
scm.remove_ref(str(ref_info))
ref_info.name = new_name
scm.set_ref(str(ref_info), rev)
return new_name
+3
View File
@@ -30,6 +30,9 @@ def run( # noqa: C901, PLR0912
Returns a dict mapping new experiment SHAs to the results
of `repro` for that experiment.
"""
if kwargs.get("dry"):
tmp_dir = True
if run_all:
return repo.experiments.reproduce_celery(jobs=jobs)
+10 -27
View File
@@ -1,15 +1,16 @@
import json
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Literal, Optional
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Literal, 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
from dvc.utils import relpath
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.repo.metrics.show import FileResult
class DeserializeError(DvcException):
@@ -29,8 +30,8 @@ class SerializableExp:
rev: str
timestamp: Optional[datetime] = None
params: Dict[str, Any] = field(default_factory=dict)
metrics: Dict[str, Any] = field(default_factory=dict)
params: Dict[str, "FileResult"] = field(default_factory=dict)
metrics: Dict[str, "FileResult"] = field(default_factory=dict)
deps: Dict[str, "ExpDep"] = field(default_factory=dict)
outs: Dict[str, "ExpOut"] = field(default_factory=dict)
meta: Dict[str, Any] = field(default_factory=dict)
@@ -40,7 +41,6 @@ class SerializableExp:
cls,
repo: "Repo",
rev: Optional[str] = None,
onerror: Optional[Callable] = None,
param_deps: bool = False,
**kwargs,
) -> "SerializableExp":
@@ -51,25 +51,11 @@ class SerializableExp:
"""
from dvc.dependency import ParamsDependency, RepoDependency
if not onerror:
onerror = onerror_collect
rev = rev or repo.get_rev()
assert rev
# 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, deps=param_deps, onerror=onerror))
params = {k: dict(v) for k, v in params.items()}
metrics = dict(
_gather_metrics(
repo,
targets=None,
rev=rev[:7],
recursive=False,
onerror=onerror_collect,
)
)
params = _gather_params(repo, deps_only=param_deps, on_error="return")
metrics = _gather_metrics(repo, on_error="return")
return cls(
rev=rev,
params=params,
@@ -119,11 +105,8 @@ class SerializableExp:
@property
def contains_error(self) -> bool:
return (
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())
return any(value.get("error") for value in self.params.values()) or any(
value.get("error") for value in self.metrics.values()
)
+7 -10
View File
@@ -243,19 +243,16 @@ def _exp_range_rows(
is_base: bool = False,
**kwargs,
) -> Iterator[Tuple["CellT", ...]]:
for i, exp in enumerate(exp_range.revs):
from funcy import first
if len(exp_range.revs) > 1:
logger.debug("Returning tip commit for legacy checkpoint exp")
exp = first(exp_range.revs)
if exp:
row: Dict[str, "CellT"] = {k: fill_value for k in all_headers}
row["Experiment"] = exp.name or ""
row["rev"] = exp.rev[:7] if Git.is_sha(exp.rev) else exp.rev
if len(exp_range.revs) > 1:
if i == 0:
row["typ"] = "checkpoint_tip"
elif i == len(exp_range.revs) - 1:
row["typ"] = "checkpoint_base"
else:
row["typ"] = "checkpoint_commit"
else:
row["typ"] = "branch_base" if is_base else "branch_commit"
row["typ"] = "branch_base" if is_base else "branch_commit"
row["parent"] = ""
if exp_range.executor:
row["State"] = exp_range.executor.state.capitalize()
+10 -8
View File
@@ -2,6 +2,7 @@ import logging
from typing import List, Tuple
from dvc.exceptions import DownloadError
from dvc.ui import ui
from dvc_data.index import DataIndex, FileStorage
from . import locked
@@ -106,7 +107,6 @@ def fetch( # noqa: C901, PLR0913
"""
from fsspec.utils import tokenize
from dvc.fs.callbacks import Callback
from dvc_data.index.fetch import collect
from dvc_data.index.fetch import fetch as ifetch
@@ -140,29 +140,31 @@ def fetch( # noqa: C901, PLR0913
cache_key = ("fetch", tokenize(sorted(indexes.keys())))
with Callback.as_tqdm_callback(
with ui.progress(
desc="Collecting",
unit="entry",
) as cb:
leave=True,
) as pb:
data = collect(
indexes.values(),
"remote",
cache_index=self.data_index,
cache_key=cache_key,
callback=cb,
callback=pb.as_callback(),
)
data, unversioned_count = _log_unversioned(data)
failed_count += unversioned_count
with Callback.as_tqdm_callback(
with ui.progress(
desc="Fetching",
unit="file",
) as cb:
bar_format="{desc}",
leave=True,
) as pb:
try:
fetch_transferred, fetch_failed = ifetch(
data,
jobs=jobs,
callback=cb,
callback=pb.as_callback(),
) # pylint: disable=assignment-from-no-return
finally:
for fs_index in data:
+45 -26
View File
@@ -1,5 +1,5 @@
import logging
from typing import TYPE_CHECKING, List, Optional, Set, Tuple
from typing import TYPE_CHECKING, List, Optional
from dvc.exceptions import InvalidArgumentError
@@ -7,8 +7,7 @@ from . import locked
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc_data.hashfile.hash_info import HashInfo
from dvc_objects.db import ObjectDB
from dvc.repo.index import ObjectContainer
logger = logging.getLogger(__name__)
@@ -33,22 +32,25 @@ def _validate_args(**kwargs):
raise InvalidArgumentError("`--num` can only be used alongside `--rev`")
def _used_obj_ids_not_in_remote(repo, default_remote, jobs, remote_odb_to_obj_ids):
def _used_obj_ids_not_in_remote(
remote_odb_to_obj_ids: "ObjectContainer", jobs: Optional[int] = None
):
used_obj_ids = set()
remote_oids = set()
for remote_odb, obj_ids in remote_odb_to_obj_ids:
if remote_odb is None:
remote_odb = repo.cloud.get_remote_odb(default_remote, "gc --not-in-remote")
for remote_odb, obj_ids in remote_odb_to_obj_ids.items():
assert remote_odb
remote_oids.update(
remote_odb.list_oids_exists({x.value for x in obj_ids}, jobs=jobs)
remote_odb.list_oids_exists(
{x.value for x in obj_ids if x.value},
jobs=jobs,
)
)
used_obj_ids.update(obj_ids)
return {obj for obj in used_obj_ids if obj.value not in remote_oids}
@locked
def gc( # noqa: PLR0912, PLR0913, C901
def gc( # noqa: PLR0913, C901
self: "Repo",
all_branches: bool = False,
cloud: bool = False,
@@ -65,6 +67,7 @@ def gc( # noqa: PLR0912, PLR0913, C901
rev: Optional[str] = None,
num: Optional[int] = None,
not_in_remote: bool = False,
dry: bool = False,
):
# require `workspace` to be true to come into effect.
# assume `workspace` to be enabled if any of `all_tags`, `all_commits`,
@@ -92,8 +95,7 @@ def gc( # noqa: PLR0912, PLR0913, C901
repos = []
all_repos = [Repo(path) for path in repos]
odb_to_obj_ids: List[Tuple[Optional["ObjectDB"], Set["HashInfo"]]] = []
odb_to_obj_ids: "ObjectContainer" = {}
with ExitStack() as stack:
for repo in all_repos:
stack.enter_context(repo.lock)
@@ -112,33 +114,50 @@ def gc( # noqa: PLR0912, PLR0913, C901
revs=[rev] if rev else None,
num=num or 1,
).items():
odb_to_obj_ids.append((odb, obj_ids))
if not odb_to_obj_ids:
odb_to_obj_ids = [(None, set())]
if odb not in odb_to_obj_ids:
odb_to_obj_ids[odb] = set()
odb_to_obj_ids[odb].update(obj_ids)
if cloud or not_in_remote:
_merge_remote_obj_ids(self, remote, odb_to_obj_ids)
if not_in_remote:
used_obj_ids = _used_obj_ids_not_in_remote(self, remote, jobs, odb_to_obj_ids)
used_obj_ids = _used_obj_ids_not_in_remote(odb_to_obj_ids, jobs=jobs)
else:
used_obj_ids = set()
for _, obj_ids in odb_to_obj_ids:
used_obj_ids.update(obj_ids)
used_obj_ids.update(*odb_to_obj_ids.values())
for scheme, odb in self.cache.by_scheme():
if not odb:
continue
removed = ogc(odb, used_obj_ids, jobs=jobs)
if not removed:
num_removed = ogc(odb, used_obj_ids, jobs=jobs, dry=dry)
if num_removed:
logger.info("Removed %d objects from %s cache.", num_removed, scheme)
else:
logger.info("No unused '%s' cache to remove.", scheme)
if not cloud:
return
for remote_odb, obj_ids in odb_to_obj_ids:
if remote_odb is None:
remote_odb = repo.cloud.get_remote_odb(remote, "gc -c")
removed = ogc(remote_odb, obj_ids, jobs=jobs)
if removed:
for remote_odb, obj_ids in odb_to_obj_ids.items():
assert remote_odb is not None
num_removed = ogc(remote_odb, obj_ids, jobs=jobs, dry=dry)
if num_removed:
get_index(remote_odb).clear()
logger.info("Removed %d objects from remote.", num_removed)
else:
logger.info("No unused cache to remove from remote.")
def _merge_remote_obj_ids(
repo: "Repo", remote: Optional[str], used_objs: "ObjectContainer"
):
# Merge default remote used objects with remote-per-output used objects
default_obj_ids = used_objs.pop(None, set())
remote_odb = repo.cloud.get_remote_odb(remote, "gc -c", hash_name="md5")
if remote_odb not in used_objs:
used_objs[remote_odb] = set()
used_objs[remote_odb].update(default_obj_ids)
legacy_odb = repo.cloud.get_remote_odb(remote, "gc -c", hash_name="md5-dos2unix")
if legacy_odb not in used_objs:
used_objs[legacy_odb] = set()
used_objs[legacy_odb].update(default_obj_ids)
+2 -2
View File
@@ -6,12 +6,12 @@ from dvc.fs import download, parse_external_url
from dvc.utils import resolve_output
def get_url(url, out=None, *, config=None, jobs=None, force=False):
def get_url(url, out=None, *, fs_config=None, jobs=None, force=False):
out = resolve_output(url, out, force=force)
out = os.path.abspath(out)
(out,) = output.loads_from(None, [out], use_cache=False)
src_fs, src_path = parse_external_url(url, config)
src_fs, src_path = parse_external_url(url, fs_config)
if not src_fs.exists(src_path):
raise URLMissingError(url)
download(src_fs, src_path, out.fs_path, jobs=jobs)
+2 -3
View File
@@ -158,10 +158,9 @@ def build_outs_graph(graph, outs_trie):
outs_graph.add_nodes_from(outs_trie.values())
for stage in graph.nodes():
if stage.is_repo_import:
continue
for dep in stage.deps:
if dep.fs_path is None:
# RepoDependency don't have a path
continue
dep_key = dep.fs.path.parts(dep.fs_path)
overlapping = [n.value for n in outs_trie.prefixes(dep_key)]
if outs_trie.has_subtrie(dep_key):
+68 -30
View File
@@ -1,5 +1,6 @@
import logging
import time
from collections import defaultdict
from functools import partial
from typing import (
TYPE_CHECKING,
@@ -33,14 +34,14 @@ if TYPE_CHECKING:
from dvc.repo.stage import StageInfo
from dvc.stage import Stage
from dvc.types import TargetType
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.index import DataIndex, DataIndexKey, DataIndexView
from dvc_objects.db import ObjectDB
from dvc_objects.fs.base import FileSystem
logger = logging.getLogger(__name__)
ObjectContainer = Dict[Optional["ObjectDB"], Set["HashInfo"]]
ObjectContainer = Dict[Optional["HashFileDB"], Set["HashInfo"]]
def log_walk(seq):
@@ -186,12 +187,13 @@ def _load_storage_from_out(storage_map, key, out):
from fsspec.utils import tokenize
# partial import
fs_cache = out.repo.cache.fs_cache
storage_map.add_cache(
FileStorage(
key,
out.cache.fs,
out.cache.fs.path.join(
out.cache.path, "fs", dep.fs.protocol, tokenize(dep.fs_path)
fs_cache.fs,
fs_cache.fs.path.join(
fs_cache.path, dep.fs.protocol, tokenize(dep.fs_path)
),
)
)
@@ -318,6 +320,22 @@ class Index:
for stage in self.stages:
yield from stage.outs
@cached_property
def out_data_keys(self) -> Dict[str, Set["DataIndexKey"]]:
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
by_workspace["repo"] = set()
by_workspace["local"] = set()
for out in self.outs:
if not out.use_cache:
continue
ws, key = out.index_key
by_workspace[ws].add(key)
return dict(by_workspace)
@property
def decorated_outs(self) -> Iterator["Output"]:
for output in self.outs:
@@ -358,8 +376,6 @@ class Index:
@cached_property
def data_keys(self) -> Dict[str, Set["DataIndexKey"]]:
from collections import defaultdict
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
by_workspace["repo"] = set()
@@ -376,8 +392,6 @@ class Index:
@cached_property
def metric_keys(self) -> Dict[str, Set["DataIndexKey"]]:
from collections import defaultdict
from .metrics.show import _collect_top_level_metrics
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
@@ -399,8 +413,6 @@ class Index:
@cached_property
def plot_keys(self) -> Dict[str, Set["DataIndexKey"]]:
from collections import defaultdict
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
by_workspace["repo"] = set()
@@ -457,6 +469,9 @@ class Index:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
ws, key = out.index_key
if ws not in by_workspace:
by_workspace[ws] = index.view((*prefix, ws))
@@ -516,8 +531,6 @@ class Index:
jobs: Optional[int] = None,
push: bool = False,
) -> "ObjectContainer":
from collections import defaultdict
used: "ObjectContainer" = defaultdict(set)
pairs = self.collect_targets(targets, recursive=recursive, with_deps=with_deps)
for stage, filter_info in pairs:
@@ -636,9 +649,23 @@ class IndexView:
yield from {out for (out, _) in self._filtered_outs}
@cached_property
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
from collections import defaultdict
def out_data_keys(self) -> Dict[str, Set["DataIndexKey"]]:
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
by_workspace["repo"] = set()
by_workspace["local"] = set()
for out in self.outs:
if not out.use_cache:
continue
ws, key = out.index_key
by_workspace[ws].add(key)
return dict(by_workspace)
@cached_property
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
prefixes: Dict[str, "_DataPrefixes"] = defaultdict(
lambda: _DataPrefixes(set(), set())
)
@@ -656,8 +683,6 @@ class IndexView:
@cached_property
def data_keys(self) -> Dict[str, Set["DataIndexKey"]]:
from collections import defaultdict
ret: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
for out, filter_info in self._filtered_outs:
@@ -710,7 +735,7 @@ class IndexView:
return data
def build_data_index( # noqa: C901
def build_data_index( # noqa: C901, PLR0912
index: Union["Index", "IndexView"],
path: str,
fs: "FileSystem",
@@ -773,14 +798,6 @@ def build_data_index( # noqa: C901
data.add(entry)
callback.relative_update(1)
if compute_hash:
tree_meta, tree = build_tree(data, key, name=hash_name)
out_entry.meta = tree_meta
out_entry.hash_info = tree.hash_info
out_entry.loaded = True
data.add(out_entry)
callback.relative_update(1)
for key in parents:
parent_path = fs.path.join(path, *key)
if not fs.exists(parent_path):
@@ -789,6 +806,23 @@ def build_data_index( # noqa: C901
data.add(direntry)
callback.relative_update(1)
if compute_hash:
out_keys = index.out_data_keys.get(workspace, set())
data_keys = index.data_keys.get(workspace, set())
for key in data_keys.intersection(out_keys):
hash_name = _get_entry_hash_name(index, workspace, key)
out_entry = data.get(key)
if not out_entry or not out_entry.isdir:
continue
tree_meta, tree = build_tree(data, key, name=hash_name)
out_entry.meta = tree_meta
out_entry.hash_info = tree.hash_info
out_entry.loaded = True
data.add(out_entry)
callback.relative_update(1)
return data
@@ -797,10 +831,14 @@ def _get_entry_hash_name(
) -> str:
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
try:
src_entry = index.data[workspace][key]
for idx in reversed(range(len(key) + 1)):
prefix = key[:idx]
try:
src_entry = index.data[workspace][prefix]
except KeyError:
continue
if src_entry.hash_info and src_entry.hash_info.name:
return src_entry.hash_info.name
except KeyError:
pass
return DEFAULT_ALGORITHM
+2 -2
View File
@@ -2,8 +2,8 @@ from dvc.exceptions import URLMissingError
from dvc.fs import parse_external_url
def ls_url(url, *, config=None, recursive=False):
fs, fs_path = parse_external_url(url, config=config)
def ls_url(url, *, fs_config=None, recursive=False):
fs, fs_path = parse_external_url(url, config=fs_config)
try:
info = fs.info(fs_path)
except FileNotFoundError as exc:
+55 -15
View File
@@ -1,21 +1,61 @@
from dvc.utils.diff import diff as _diff
from typing import TYPE_CHECKING, Dict, TypedDict, Union
from funcy import compact
from dvc.utils.diff import diff as _diff_dict
from dvc.utils.diff import format_dict
if TYPE_CHECKING:
from dvc.repo import Repo
def diff(repo, *args, a_rev=None, b_rev=None, **kwargs):
from .show import Result
class DiffResult(TypedDict, total=False):
errors: Dict[str, Union[Exception, Dict[str, Exception]]]
diff: Dict[str, Dict[str, Dict]]
def _diff(
result: Dict[str, "Result"],
old_rev: str,
new_rev: str,
**kwargs,
) -> DiffResult:
old = result.get(old_rev, {})
new = result.get(new_rev, {})
old_data = old.get("data", {})
new_data = new.get("data", {})
res = DiffResult()
errors = res.setdefault("errors", {})
if old_error := old.get("error"):
errors[old_rev] = old_error
else:
errors[old_rev] = {f: d["error"] for f, d in old_data.items() if "error" in d}
if new_error := new.get("error"):
errors[new_rev] = new_error
else:
errors[new_rev] = {f: d["error"] for f, d in new_data.items() if "error" in d}
diff_data = _diff_dict(format_dict(old_data), format_dict(new_data), **kwargs)
res = DiffResult(errors=errors, diff=diff_data)
res["errors"] = compact(res.get("errors", {})) # type: ignore[assignment]
return compact(res) # type: ignore[no-any-return]
def diff(
repo: "Repo",
a_rev: str = "HEAD",
b_rev: str = "workspace",
all: bool = False, # noqa: A002 # pylint: disable=redefined-builtin
**kwargs,
) -> DiffResult:
if repo.scm.no_commits:
return {}
with_unchanged = kwargs.pop("all", False)
a_rev = a_rev or "HEAD"
b_rev = b_rev or "workspace"
metrics = repo.metrics.show(
*args, **kwargs, revs=[a_rev, b_rev], hide_workspace=False
)
old = metrics.get(a_rev, {}).get("data", {})
new = metrics.get(b_rev, {}).get("data", {})
return _diff(format_dict(old), format_dict(new), with_unchanged=with_unchanged)
metrics = repo.metrics.show(revs=[a_rev, b_rev], hide_workspace=False, **kwargs)
return _diff(metrics, a_rev, b_rev, with_unchanged=all)
+164 -92
View File
@@ -1,36 +1,38 @@
import logging
import os
from typing import TYPE_CHECKING, List
from itertools import chain
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
TypedDict,
Union,
)
from funcy import ldistinct
from scmrepo.exceptions import SCMError
from dvc.fs.dvc import DVCFileSystem
from dvc.repo import locked
from dvc.repo.collect import collect
from dvc.scm import NoSCMError
from dvc.utils import as_posix, error_handler, errored_revisions, onerror_collect
from dvc.utils import as_posix, expand_paths
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import load_path
if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc.output import Output
from dvc.repo import Repo
from dvc.scm import Git, NoSCM
logger = logging.getLogger(__name__)
def _is_metric(out: "Output") -> bool:
return bool(out.metric)
def _to_fs_paths(metrics: List["Output"]) -> List["str"]:
result = []
for out in metrics:
if out.metric:
result.append(out.repo.dvcfs.from_os_path(out.fs_path))
return result
def _collect_top_level_metrics(repo):
def _collect_top_level_metrics(repo: "Repo") -> Iterator[str]:
top_metrics = repo.index._metrics # pylint: disable=protected-access
for dvcfile, metrics in top_metrics.items():
wdir = repo.fs.path.relpath(repo.fs.path.parent(dvcfile), repo.root_dir)
@@ -39,14 +41,7 @@ def _collect_top_level_metrics(repo):
yield repo.fs.path.normpath(path)
def _collect_metrics(repo, targets, recursive):
metrics, fs_paths = collect(
repo, targets=targets, output_filter=_is_metric, recursive=recursive
)
return _to_fs_paths(metrics) + list(fs_paths)
def _extract_metrics(metrics, path, rev):
def _extract_metrics(metrics, path: str):
if isinstance(metrics, (int, float, str)):
return metrics
@@ -55,74 +50,157 @@ def _extract_metrics(metrics, path, rev):
ret = {}
for key, val in metrics.items():
m = _extract_metrics(val, path, rev)
m = _extract_metrics(val, path)
if m not in (None, {}):
ret[key] = m
else:
logger.debug(
(
"Could not parse '%s' metric from '%s' at '%s' "
"due to its unsupported type: '%s'"
),
"Could not parse %r metric from %r due to its unsupported type: %r",
key,
path,
rev,
type(val).__name__,
)
return ret
@error_handler
def _read_metric(path, fs, rev, **kwargs):
val = load_path(path, fs)
val = _extract_metrics(val, path, rev)
def _read_metric(fs: "FileSystem", path: str, **load_kwargs) -> Any:
val = load_path(path, fs, **load_kwargs)
val = _extract_metrics(val, path)
return val or {}
def _read_metrics(repo, metrics, rev, onerror=None):
fs = DVCFileSystem(repo=repo)
relpath = ""
if repo.root_dir != repo.fs.path.getcwd():
relpath = repo.fs.path.relpath(repo.root_dir, repo.fs.path.getcwd())
res = {}
def _read_metrics(
fs: "FileSystem", metrics: Iterable[str], **load_kwargs
) -> Iterator[Tuple[str, Union[Exception, Any]]]:
for metric in metrics:
rel_metric_path = os.path.join(relpath, *fs.path.parts(metric))
if not fs.isfile(metric):
if fs.isfile(rel_metric_path):
metric = rel_metric_path
else:
continue
try:
yield metric, _read_metric(fs, metric, **load_kwargs)
except Exception as exc: # noqa: BLE001 # pylint:disable=broad-exception-caught
logger.debug(exc)
yield metric, exc
res[rel_metric_path] = _read_metric(metric, fs, rev, onerror=onerror)
def metrics_from_target(repo: "Repo", targets: List[str]) -> Iterator["Output"]:
stages = chain.from_iterable(repo.stage.collect(target) for target in targets)
for stage in stages:
yield from stage.metrics
def _collect_metrics(
repo: "Repo",
targets: Optional[List[str]] = None,
stages: Optional[List[str]] = None,
outs_only: bool = False,
) -> List[str]:
metrics: List[str] = []
if targets:
# target is a repo-relative path
metrics.extend(targets)
if not targets or outs_only:
outs = metrics_from_target(repo, stages) if stages else repo.index.metrics
relpath = repo.fs.path.relpath
metrics.extend(relpath(out.fs_path, repo.root_dir) for out in outs)
if not targets and not outs_only and not stages:
# _collect_top_level_metrics returns repo-relative paths
metrics.extend(_collect_top_level_metrics(repo))
fs = repo.dvcfs
# convert to posixpath for DVCFileSystem
paths = (fs.from_os_path(metric) for metric in metrics)
# make paths absolute for DVCFileSystem
repo_paths = (f"{fs.root_marker}{path}" for path in paths)
return ldistinct(expand_paths(fs, repo_paths))
class FileResult(TypedDict, total=False):
data: Any
error: Exception
class Result(TypedDict, total=False):
data: Dict[str, FileResult]
error: Exception
def to_relpath(fs: "FileSystem", root_dir: str, d: Result) -> Result:
relpath = fs.path.relpath
cwd = fs.path.getcwd()
start = relpath(cwd, root_dir)
data = d.get("data")
if data is not None:
d["data"] = {relpath(path, start): result for path, result in data.items()}
return d
def _gather_metrics(
repo: "Repo",
targets: Optional[List[str]] = None,
outs_only: bool = False,
stages: Optional[List[str]] = None,
on_error: str = "return",
) -> Dict[str, FileResult]:
assert on_error in ("raise", "return", "ignore")
# `files` is a repo-relative posixpath that can be passed to DVCFileSystem
# It is absolute, i.e. has a root_marker `/` in front which we strip when returning
# the result and convert to appropriate repo-relative os.path.
files = _collect_metrics(repo, targets=targets, stages=stages, outs_only=outs_only)
data = {}
fs = repo.dvcfs
for fs_path, result in _read_metrics(fs, files, cache=True):
repo_path = fs_path.lstrip(fs.root_marker)
repo_os_path = os.sep.join(fs.path.parts(repo_path))
if not isinstance(result, Exception):
data.update({repo_os_path: FileResult(data=result)})
continue
if on_error == "raise":
raise result
if on_error == "return":
data.update({repo_os_path: FileResult(error=result)})
return data
def _hide_workspace(
scm: Union["Git", "NoSCM"], res: Dict[str, Result]
) -> Dict[str, Result]:
# Hide workspace params if they are the same as in the active branch
try:
active_branch = scm.active_branch()
except (SCMError, NoSCMError):
# SCMError - detached head
# NoSCMError - no repo case
pass
else:
if res.get("workspace") == res.get(active_branch):
res.pop("workspace", None)
return res
def _gather_metrics(repo, targets, rev, recursive, onerror=None):
metrics = _collect_metrics(repo, targets, recursive)
metrics.extend(_collect_top_level_metrics(repo))
return _read_metrics(repo, metrics, rev, onerror=onerror)
@locked
def show(
repo,
targets=None,
all_branches=False,
all_tags=False,
recursive=False,
revs=None,
all_commits=False,
onerror=None,
hide_workspace=True,
):
if onerror is None:
onerror = onerror_collect
repo: "Repo",
targets: Optional[List[str]] = None,
stages: Optional[List[str]] = None,
outs_only: bool = False,
all_branches: bool = False,
all_tags: bool = False,
revs: Optional[List[str]] = None,
all_commits: bool = False,
hide_workspace: bool = True,
on_error: str = "return",
) -> Dict[str, Result]:
assert on_error in ("raise", "return", "ignore")
targets = ensure_list(targets)
targets = [os.path.abspath(target) for target in ensure_list(targets)]
targets = [repo.dvcfs.from_os_path(target) for target in targets]
res = {}
@@ -132,29 +210,23 @@ def show(
all_tags=all_tags,
all_commits=all_commits,
):
res[rev] = error_handler(_gather_metrics)(
repo, targets, rev, recursive, onerror=onerror
)
try:
result = _gather_metrics(
repo,
targets=targets,
stages=stages,
outs_only=outs_only,
on_error=on_error,
)
res[rev] = Result(data=result)
except Exception as exc: # noqa: BLE001 # pylint:disable=broad-exception-caught
if on_error == "raise":
raise
logger.warning("failed to load metrics in revision %r, %s", rev, str(exc))
if on_error == "return":
res[rev] = Result(error=exc)
if hide_workspace:
# Hide workspace metrics if they are the same as in the active branch
try:
active_branch = repo.scm.active_branch()
except (SCMError, NoSCMError):
# SCMError - detached head
# NoSCMError - no repo case
pass
else:
if res.get("workspace") == res.get(active_branch):
res.pop("workspace", None)
errored = errored_revisions(res)
if errored:
from dvc.ui import ui
ui.error_write(
"DVC failed to load some metrics for following revisions:"
f" '{', '.join(errored)}'."
)
_hide_workspace(repo.scm, res)
return res
+15 -15
View File
@@ -1,21 +1,21 @@
from dvc.utils.diff import diff as _diff
from dvc.utils.diff import format_dict
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.repo.metrics.diff import DiffResult
def diff(repo, *args, a_rev=None, b_rev=None, **kwargs):
def diff(
repo: "Repo",
a_rev: str = "HEAD",
b_rev: str = "workspace",
all: bool = False, # noqa: A002 # pylint: disable=redefined-builtin
**kwargs,
) -> "DiffResult":
if repo.scm.no_commits:
return {}
with_unchanged = kwargs.pop("all", False)
from dvc.repo.metrics.diff import _diff
a_rev = a_rev or "HEAD"
b_rev = b_rev or "workspace"
params = repo.params.show(
*args, **kwargs, revs=[a_rev, b_rev], hide_workspace=False
)
old = params.get(a_rev, {}).get("data", {})
new = params.get(b_rev, {}).get("data", {})
return _diff(format_dict(old), format_dict(new), with_unchanged=with_unchanged)
params = repo.params.show(revs=[a_rev, b_rev], hide_workspace=False, **kwargs)
return _diff(params, a_rev, b_rev, with_unchanged=all)
+152 -136
View File
@@ -1,33 +1,24 @@
import logging
import os
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Tuple
from itertools import chain
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Union
from scmrepo.exceptions import SCMError
from dvc.dependency.param import ParamsDependency
from dvc.dependency.param import ParamsDependency, read_param_file
from dvc.repo import locked
from dvc.repo.collect import collect
from dvc.scm import NoSCMError
from dvc.repo.metrics.show import FileResult, Result
from dvc.stage import PipelineStage
from dvc.ui import ui
from dvc.utils import as_posix, error_handler, errored_revisions, onerror_collect
from dvc.utils import as_posix, expand_paths
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import load_path
if TYPE_CHECKING:
from dvc.output import Output
from dvc.fs import FileSystem
from dvc.repo import Repo
logger = logging.getLogger(__name__)
def _is_params(dep: "Output"):
return isinstance(dep, ParamsDependency)
def _collect_top_level_params(repo):
def _collect_top_level_params(repo: "Repo") -> Iterator[str]:
top_params = repo.index._params # pylint: disable=protected-access
for dvcfile, params in top_params.items():
wdir = repo.fs.path.relpath(repo.fs.path.parent(dvcfile), repo.root_dir)
@@ -36,67 +27,58 @@ def _collect_top_level_params(repo):
yield repo.fs.path.normpath(path)
def _collect_configs(
repo: "Repo", targets=None, deps=False, stages=None
) -> Tuple[List["Output"], List[str]]:
params, fs_paths = collect(
repo,
targets=targets or [],
deps=True,
output_filter=_is_params,
duplicates=deps or stages is not None,
)
all_fs_paths = fs_paths + [p.fs_path for p in params]
if not any([deps, targets, stages]):
default_params = repo.fs.path.join(
repo.root_dir, ParamsDependency.DEFAULT_PARAMS_FILE
def params_from_target(
repo: "Repo", targets: List[str]
) -> Iterator["ParamsDependency"]:
stages = chain.from_iterable(repo.stage.collect(target) for target in targets)
for stage in stages:
yield from stage.params
def _collect_params(
repo: "Repo",
targets: Union[List[str], Dict[str, List[str]], None] = None,
stages: Optional[List[str]] = None,
deps_only: bool = False,
default_file: Optional[str] = None,
) -> Dict[str, List[str]]:
from dvc.dependency import _merge_params
if isinstance(targets, list):
targets = {target: [] for target in targets}
params: List[Dict[str, List[str]]] = []
if targets:
# target is a repo-relative path
params.extend({file: params} for file, params in targets.items())
if not targets or stages:
deps = params_from_target(repo, stages) if stages else repo.index.params
relpath = repo.fs.path.relpath
params.extend(
{relpath(dep.fs_path, repo.root_dir): list(dep.params)} for dep in deps
)
if default_params not in all_fs_paths and repo.fs.exists(default_params):
fs_paths.append(default_params)
if targets and (deps or stages) and not params:
# A target has been provided but it is not used in the stages
fs_paths = []
return params, fs_paths
fs = repo.dvcfs
@error_handler
def _read_fs_path(fs, fs_path, **kwargs):
return load_path(fs_path, fs)
if not targets and not deps_only and not stages:
# _collect_top_level_params returns repo-relative paths
params.extend({param: []} for param in _collect_top_level_params(repo))
if default_file and fs.exists(f"{fs.root_marker}{default_file}"):
params.append({default_file: []})
# combine all the param files and the keypaths to track
all_params = _merge_params(params)
def _read_params(
repo,
params,
params_fs_paths,
deps=False,
onerror: Optional[Callable] = None,
stages: Optional[Iterable[str]] = None,
):
res: Dict[str, Dict] = defaultdict(lambda: defaultdict(dict))
fs_paths = copy(params_fs_paths)
if deps or stages:
for param in params:
if stages and param.stage.addressing not in stages:
continue
params_dict = error_handler(param.read_params)(
onerror=onerror, flatten=False
)
if params_dict:
name = os.sep.join(repo.fs.path.relparts(param.fs_path))
res[name]["data"].update(params_dict["data"])
if name in fs_paths:
fs_paths.remove(name)
else:
fs_paths += [param.fs_path for param in params]
for fs_path in fs_paths:
from_path = _read_fs_path(repo.fs, fs_path, onerror=onerror)
if from_path:
name = os.sep.join(repo.fs.path.relparts(fs_path))
res[name] = from_path
return res
ret = {}
for param, _params in all_params.items():
# convert to posixpath for DVCFileSystem
path = fs.from_os_path(param)
# make paths absolute for DVCFileSystem
repo_path = f"{fs.root_marker}{path}"
ret.update({file: _params for file in expand_paths(fs, [repo_path])})
return ret
def _collect_vars(repo, params, stages=None) -> Dict:
@@ -109,82 +91,116 @@ def _collect_vars(repo, params, stages=None) -> Dict:
for file, vars_ in stage.tracked_vars.items():
# `params` file are shown regardless of `tracked` or not
# to reduce noise and duplication, they are skipped
if file in params:
# `file` is relative
abspath = repo.fs.path.abspath(file)
repo_path = repo.dvcfs.from_os_path(abspath)
if repo_path in params:
continue
name = os.sep.join(repo.fs.path.parts(file))
vars_params[name].update(vars_)
return vars_params
vars_params[repo_path].update(vars_)
return dict(vars_params)
def _read_params(
fs: "FileSystem", params: Dict[str, List[str]], **load_kwargs
) -> Iterator[Tuple[str, Union[Exception, Any]]]:
for file_path, key_paths in params.items():
try:
yield file_path, read_param_file(fs, file_path, key_paths, **load_kwargs)
except Exception as exc: # noqa: BLE001 # pylint:disable=broad-exception-caught
logger.debug(exc)
yield file_path, exc
def _gather_params(
repo: "Repo",
targets: Union[List[str], Dict[str, List[str]], None] = None,
deps_only: bool = False,
stages: Optional[List[str]] = None,
on_error: str = "return",
):
assert on_error in ("raise", "return", "ignore")
# `files` is a repo-relative posixpath that can be passed to DVCFileSystem
# It is absolute, i.e. has a root_marker `/` in front which we strip when returning
# the result and convert to appropriate repo-relative os.path.
files_keypaths = _collect_params(
repo,
targets=targets,
stages=stages,
deps_only=deps_only,
default_file=ParamsDependency.DEFAULT_PARAMS_FILE,
)
data: Dict[str, FileResult] = {}
fs = repo.dvcfs
for fs_path, result in _read_params(fs, files_keypaths, cache=True):
repo_path = fs_path.lstrip(fs.root_marker)
repo_os_path = os.sep.join(fs.path.parts(repo_path))
if not isinstance(result, Exception):
data.update({repo_os_path: FileResult(data=result)})
continue
if on_error == "raise":
raise result
if on_error == "return":
data.update({repo_os_path: FileResult(error=result)})
if not (stages or targets):
data.update(
{
path: FileResult(data=result)
for path, result in _collect_vars(repo, data).items()
}
)
return data
@locked
def show(
repo,
revs=None,
targets=None,
deps=False,
onerror: Optional[Callable] = None,
stages=None,
hide_workspace=True,
):
if onerror is None:
onerror = onerror_collect
repo: "Repo",
targets: Optional[List[str]] = None,
stages: Optional[List[str]] = None,
deps_only: bool = False,
all_branches: bool = False,
all_tags: bool = False,
revs: Optional[List[str]] = None,
all_commits: bool = False,
hide_workspace: bool = True,
on_error: str = "return",
) -> Dict[str, Result]:
assert on_error in ("raise", "return", "ignore")
res = {}
targets = ensure_list(targets)
targets = [repo.dvcfs.from_os_path(target) for target in targets]
for branch in repo.brancher(revs=revs):
params = error_handler(_gather_params)(
repo=repo,
targets=targets,
deps=deps,
onerror=onerror,
stages=stages,
)
if params:
res[branch] = params
for rev in repo.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
):
try:
params = _gather_params(
repo=repo,
targets=targets,
stages=stages,
deps_only=deps_only,
on_error=on_error,
)
res[rev] = Result(data=params)
except Exception as exc: # noqa: BLE001 # pylint:disable=broad-exception-caught
if on_error == "raise":
raise
logger.warning("failed to load params in revision %r, %s", rev, str(exc))
if on_error == "return":
res[rev] = Result(error=exc)
if hide_workspace:
# Hide workspace params if they are the same as in the active branch
try:
active_branch = repo.scm.active_branch()
except (SCMError, NoSCMError):
# SCMError - detached head
# NoSCMError - no repo case
pass
else:
if res.get("workspace") == res.get(active_branch):
res.pop("workspace", None)
errored = errored_revisions(res)
if errored:
ui.error_write(
"DVC failed to load some parameters for following revisions:"
f" '{', '.join(errored)}'."
)
from dvc.repo.metrics.show import _hide_workspace
_hide_workspace(repo.scm, res)
return res
def _gather_params(repo, targets=None, deps=False, onerror=None, stages=None):
param_outs, params_fs_paths = _collect_configs(
repo, targets=targets, deps=deps, stages=stages
)
params_fs_paths.extend(_collect_top_level_params(repo=repo))
params = _read_params(
repo,
params=param_outs,
params_fs_paths=params_fs_paths,
deps=deps,
onerror=onerror,
stages=stages,
)
vars_params = _collect_vars(repo, params, stages=stages)
# NOTE: only those that are not added as a ParamDependency are
# included so we don't need to recursively merge them yet.
for key, vals in vars_params.items():
params[key]["data"] = vals
return params
+1 -1
View File
@@ -104,7 +104,7 @@ def _check_blockers(tmp_dir, lock, info, *, mode, waiters): # noqa: C901, PLR09
logger.warning(
(
"Process '%s' with (Pid %s), in RWLock-file '%s'"
" had been killed. Auto remove it from the lock file."
" had been killed. Auto removed it from the lock file."
),
cmd,
pid,
+13 -1
View File
@@ -43,10 +43,12 @@ from .utils import (
)
if TYPE_CHECKING:
from dvc.dependency import ParamsDependency
from dvc.dvcfile import ProjectFile, SingleStageFile
from dvc.output import Output
from dvc.repo import Repo
from dvc.types import StrPath
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_objects.db import ObjectDB
@@ -205,6 +207,16 @@ class Stage(params.StageParams):
def dvcfile(self, dvcfile: Union["ProjectFile", "SingleStageFile"]) -> None:
self._dvcfile = dvcfile
@property
def params(self) -> List["ParamsDependency"]:
from dvc.dependency import ParamsDependency
return [dep for dep in self.deps if isinstance(dep, ParamsDependency)]
@property
def metrics(self) -> List["Output"]:
return [out for out in self.outs if out.metric]
def __repr__(self):
return f"Stage: '{self.addressing}'"
@@ -721,7 +733,7 @@ class Stage(params.StageParams):
def get_used_objs(
self, *args, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
) -> Dict[Optional["HashFileDB"], Set["HashInfo"]]:
"""Return set of object IDs used by this stage."""
if self.is_partial_import and not self.is_repo_import:
return {}
+23 -10
View File
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple
from funcy import first
from dvc import fs
from dvc.config import RemoteConfigError
from dvc.exceptions import CollectCacheError, DvcException
from dvc.utils import dict_sha256, relpath
@@ -99,10 +100,16 @@ class StageCache:
if not os.path.exists(cache_dir):
return None
for value in os.listdir(cache_dir):
cache = self._load_cache(key, value)
if cache:
return cache
newest_entry = first(
sorted(
os.listdir(cache_dir),
key=lambda f: os.path.getmtime(os.path.join(cache_dir, f)),
reverse=True,
)
)
cache = self._load_cache(key, newest_entry)
if cache:
return cache
return None
@@ -268,15 +275,21 @@ class StageCache:
return ret
def push(self, remote: Optional[str], odb: Optional["ObjectDB"] = None):
dest_odb = odb or self.repo.cloud.get_remote_odb(
remote, "push --run-cache", hash_name="md5-dos2unix"
)
try:
dest_odb = odb or self.repo.cloud.get_remote_odb(
remote, "push --run-cache", hash_name="md5-dos2unix"
)
except RemoteConfigError as e:
raise RunCacheNotSupported(e) from e
return self.transfer(self.repo.cache.legacy, dest_odb)
def pull(self, remote: Optional[str], odb: Optional["ObjectDB"] = None):
odb = odb or self.repo.cloud.get_remote_odb(
remote, "fetch --run-cache", hash_name="md5-dos2unix"
)
try:
odb = odb or self.repo.cloud.get_remote_odb(
remote, "fetch --run-cache", hash_name="md5-dos2unix"
)
except RemoteConfigError as e:
raise RunCacheNotSupported(e) from e
return self.transfer(odb, self.repo.cache.legacy)
def get_used_objs(self, used_run_cache, *args, **kwargs):
+4
View File
@@ -39,10 +39,14 @@ def _enforce_cmd_list(cmd):
def prepare_kwargs(stage, run_env=None):
from dvc.env import DVC_ROOT
kwargs = {"cwd": stage.wdir, "env": fix_env(None), "close_fds": True}
if run_env:
kwargs["env"].update(run_env)
if DVC_ROOT not in kwargs["env"]:
kwargs["env"][DVC_ROOT] = stage.repo.root_dir
# NOTE: when you specify `shell=True`, `Popen` [1] will default to
# `/bin/sh` on *nix and will add ["/bin/sh", "-c"] to your command.
+8 -8
View File
@@ -173,7 +173,7 @@ class TestLsUrl:
def test_file(self, cloud, fname):
cloud.gen({fname: "foo contents"})
fs, fs_path = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / fname), config=cloud.config)
result = ls_url(str(cloud / fname), fs_config=cloud.config)
match_files(
fs,
result,
@@ -185,7 +185,7 @@ class TestLsUrl:
if not (cloud / "dir").is_dir():
pytest.skip("Cannot create directories on this cloud")
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / "dir"), config=cloud.config)
result = ls_url(str(cloud / "dir"), fs_config=cloud.config)
match_files(
fs,
result,
@@ -200,7 +200,7 @@ class TestLsUrl:
if not (cloud / "dir").is_dir():
pytest.skip("Cannot create directories on this cloud")
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / "dir"), config=cloud.config, recursive=True)
result = ls_url(str(cloud / "dir"), fs_config=cloud.config, recursive=True)
match_files(
fs,
result,
@@ -212,14 +212,14 @@ class TestLsUrl:
def test_nonexistent(self, cloud):
with pytest.raises(URLMissingError):
ls_url(str(cloud / "dir"), config=cloud.config)
ls_url(str(cloud / "dir"), fs_config=cloud.config)
class TestGetUrl:
def test_get_file(self, cloud, tmp_dir):
cloud.gen({"foo": "foo contents"})
Repo.get_url(str(cloud / "foo"), "foo_imported", config=cloud.config)
Repo.get_url(str(cloud / "foo"), "foo_imported", fs_config=cloud.config)
assert (tmp_dir / "foo_imported").is_file()
assert (tmp_dir / "foo_imported").read_text() == "foo contents"
@@ -229,7 +229,7 @@ class TestGetUrl:
if not (cloud / "foo").is_dir():
pytest.skip("Cannot create directories on this cloud")
Repo.get_url(str(cloud / "foo"), "foo_imported", config=cloud.config)
Repo.get_url(str(cloud / "foo"), "foo_imported", fs_config=cloud.config)
assert (tmp_dir / "foo_imported").is_dir()
assert (tmp_dir / "foo_imported" / "foo").is_file()
@@ -242,14 +242,14 @@ class TestGetUrl:
pytest.skip("Cannot create directories on this cloud")
tmp_dir.gen({"dir": {"subdir": {}}})
Repo.get_url(str(cloud / "src" / "foo"), dname, config=cloud.config)
Repo.get_url(str(cloud / "src" / "foo"), dname, fs_config=cloud.config)
assert (tmp_dir / dname).is_dir()
assert (tmp_dir / dname / "foo").read_text() == "foo contents"
def test_get_url_nonexistent(self, cloud):
with pytest.raises(URLMissingError):
Repo.get_url(str(cloud / "nonexistent"), config=cloud.config)
Repo.get_url(str(cloud / "nonexistent"), fs_config=cloud.config)
class TestToRemote:
+13 -2
View File
@@ -6,10 +6,13 @@ import logging
import os
import re
import sys
from typing import Dict, List, Optional, Tuple
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Optional, Tuple
import colorama
if TYPE_CHECKING:
from dvc.fs import FileSystem
logger = logging.getLogger(__name__)
LARGE_DIR_SIZE = 100
@@ -245,7 +248,7 @@ def env2bool(var, undefined=False):
return bool(re.search("1|y|yes|true", var, flags=re.I))
def resolve_output(inp, out, force=False):
def resolve_output(inp: str, out: Optional[str], force=False) -> str:
from urllib.parse import urlparse
from dvc.exceptions import FileExistsLocallyError
@@ -413,3 +416,11 @@ def errored_revisions(rev_data: Dict) -> List:
if nested_contains(data, "error"):
result.append(revision)
return result
def expand_paths(fs: "FileSystem", paths: Iterable[str]) -> Iterator[str]:
for path in paths:
if fs.isdir(path):
yield from fs.find(path)
else:
yield path
+32
View File
@@ -1,4 +1,36 @@
from collections import defaultdict
from typing import Dict, Tuple
def get_plot_id(config_plot_id: str, config_file_path: str = ""):
return (
f"{config_file_path}::{config_plot_id}" if config_file_path else config_plot_id
)
def group_definitions_by_id(
definitions: Dict[str, Dict]
) -> Dict[str, Tuple[str, Dict]]:
"""
Format ID and extracts plot_definition for each plot.
Arguments:
definitions: dict of {config_file: config_file_content}.
Returns:
Dict of {plot_id: (original_plot_id, plot_definition)}.
"""
groups_by_config: Dict = defaultdict(dict)
groups_by_id: Dict = {}
for config_file, config_file_content in definitions.items():
for plot_id, plot_definition in config_file_content.get("data", {}).items():
groups_by_config[plot_id][config_file] = (plot_id, plot_definition)
# only keep config_file if the same plot_id is in multiple config_files
for plot_id, configs in groups_by_config.items():
if len(configs) == 1:
groups_by_id[plot_id] = next(iter(configs.values()))
else:
for config_file, content in configs.items():
full_id = get_plot_id(plot_id, config_file)
groups_by_id[full_id] = content
return groups_by_id
+2 -2
View File
@@ -20,10 +20,10 @@ PARSERS.update(
)
def load_path(fs_path, fs):
def load_path(fs_path, fs, **kwargs):
suffix = fs.path.suffix(fs_path).lower()
loader = LOADERS[suffix]
return loader(fs_path, fs=fs)
return loader(fs_path, fs=fs, **kwargs)
DUMPERS: DefaultDict[str, DumperFn] = defaultdict( # noqa: F405
+4 -2
View File
@@ -69,10 +69,12 @@ class EncodingError(ParseError):
super().__init__(path, f"is not valid {encoding}")
def _load_data(path: "StrPath", parser: ParserFn, fs: Optional["FileSystem"] = None):
def _load_data(
path: "StrPath", parser: ParserFn, fs: Optional["FileSystem"] = None, **kwargs
):
open_fn = fs.open if fs else open
encoding = "utf-8"
with open_fn(path, encoding=encoding) as fd: # type: ignore[operator]
with open_fn(path, encoding=encoding, **kwargs) as fd: # type: ignore[operator]
with reraise(UnicodeDecodeError, EncodingError(path, encoding)):
return parser(fd.read(), path)
+2 -2
View File
@@ -10,8 +10,8 @@ class JSONFileCorruptedError(ParseError):
super().__init__(path, "JSON file structure is corrupted")
def load_json(path, fs=None):
return _load_data(path, parser=parse_json, fs=fs)
def load_json(path, fs=None, **kwargs):
return _load_data(path, parser=parse_json, fs=fs, **kwargs)
def parse_json(text, path, **kwargs):
+2 -2
View File
@@ -15,8 +15,8 @@ class PythonFileCorruptedError(ParseError):
super().__init__(path, message)
def load_py(path, fs=None):
return _load_data(path, parser=parse_py, fs=fs)
def load_py(path, fs=None, **kwargs):
return _load_data(path, parser=parse_py, fs=fs, **kwargs)
def parse_py(text, path):
+2 -2
View File
@@ -10,8 +10,8 @@ class TOMLFileCorruptedError(ParseError):
super().__init__(path, "TOML file structure is corrupted")
def load_toml(path, fs=None):
return _load_data(path, parser=parse_toml, fs=fs)
def load_toml(path, fs=None, **kwargs):
return _load_data(path, parser=parse_toml, fs=fs, **kwargs)
def _parse_toml(text, path):
+2 -2
View File
@@ -17,8 +17,8 @@ class YAMLFileCorruptedError(YAMLError):
super().__init__(path, "YAML file structure is corrupted")
def load_yaml(path, fs=None):
return _load_data(path, parser=parse_yaml, fs=fs)
def load_yaml(path, fs=None, **kwargs):
return _load_data(path, parser=parse_yaml, fs=fs, **kwargs)
def parse_yaml(text, path, typ="safe"):
+2 -2
View File
@@ -1,5 +1,5 @@
import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from urllib.parse import urljoin
import requests
@@ -73,7 +73,7 @@ def notify_refs(
logger.warning("failed to notify Studio: %s", msg.lower())
return {}
r = cast("Response", e.response)
r = e.response
d = ignore(Exception, default={})(r.json)()
status = r.status_code
if detail := d.get("detail"):
+4 -3
View File
@@ -34,16 +34,17 @@ dependencies = [
"configobj>=5.0.6",
"distro>=1.3",
"dpath<3,>=2.1.0",
"dvc-data>=2.15.0,<2.16.0",
"dvc-data>=2.18.1,<2.19.0",
"dvc-http>=2.29.0",
"dvc-render>=0.3.1,<1",
"dvc-studio-client>=0.9.2,<1",
"dvc-studio-client>=0.13.0,<1",
"dvc-task>=0.3.0,<1",
"flatten_dict<1,>=0.4.1",
# https://github.com/iterative/dvc/issues/9654
"flufl.lock>=5,<8",
"funcy>=1.14",
"grandalf<1,>=0.7",
"gto>=1.4.0,<2",
"hydra-core>=1.1",
"iterative-telemetry>=0.0.7",
"networkx>=2.5",
@@ -76,7 +77,7 @@ gs = ["dvc-gs==2.22.1"]
hdfs = ["dvc-hdfs==2.19"]
lint = [
"mypy==1.5.1",
"pylint==2.17.5",
"pylint==2.17.6",
"types-colorama",
"types-psutil",
"types-pyinstaller",
+3 -7
View File
@@ -159,9 +159,7 @@ def test_params_show_targets(params_repo):
"bar": 2,
"foobar": 3,
}
assert api.params_show("params.yaml", stages="stage-1") == {
"foo": 5,
}
assert api.params_show("params.yaml", stages="stage-1") == {"bar": 2, "foo": 5}
def test_params_show_deps(params_repo):
@@ -181,7 +179,7 @@ def test_params_show_stages(params_repo):
stages=["stage-1", "stage-2", "stage-3"]
)
assert api.params_show("params.json", stages="stage-3") == {"foobar": 3}
assert api.params_show("params.json", stages="stage-3") == {"bar": 2, "foobar": 3}
assert api.params_show(stages="stage-0") == {}
@@ -286,9 +284,7 @@ def test_params_show_untracked_target(params_repo, tmp_dir):
assert api.params_show("params_foo.yaml") == {"foo": 1}
assert api.params_show("params_foo.yaml", stages="stage-0") == {}
assert api.params_show("params_foo.yaml", deps=True) == {}
assert api.params_show("params_foo.yaml", stages="stage-0") == {"foo": 1}
def test_metrics_show_no_args(metrics_repo):
+36 -7
View File
@@ -5,8 +5,8 @@ from copy import deepcopy
import pytest
from dvc.annotations import Artifact
from dvc.exceptions import InvalidArgumentError
from dvc.repo.artifacts import name_is_compatible
from dvc.exceptions import ArtifactNotFoundError, InvalidArgumentError
from dvc.repo.artifacts import check_name_format
from dvc.utils.strictyaml import YAMLSyntaxError, YAMLValidationError
dvcyaml = {
@@ -43,7 +43,7 @@ def test_artifacts_read_subdir(tmp_dir, dvc):
def test_artifacts_read_bad_name(tmp_dir, dvc, caplog):
bad_name_dvcyaml = deepcopy(dvcyaml)
bad_name_dvcyaml["artifacts"]["bad_name"] = {"type": "model", "path": "bad.pkl"}
bad_name_dvcyaml["artifacts"]["_bad_name_"] = {"type": "model", "path": "bad.pkl"}
(tmp_dir / "dvc.yaml").dump(bad_name_dvcyaml)
@@ -54,7 +54,7 @@ def test_artifacts_read_bad_name(tmp_dir, dvc, caplog):
with caplog.at_level(logging.WARNING):
assert tmp_dir.dvc.artifacts.read() == {"dvc.yaml": artifacts}
assert "Can't use 'bad_name' as artifact name (ID)" in caplog.text
assert "Can't use '_bad_name_' as artifact name (ID)" in caplog.text
def test_artifacts_add_subdir(tmp_dir, dvc):
@@ -160,7 +160,7 @@ def test_artifacts_read_fails_on_id_duplication(tmp_dir, dvc):
],
)
def test_name_is_compatible(name):
assert name_is_compatible(name)
check_name_format(name)
@pytest.mark.parametrize(
@@ -172,7 +172,6 @@ def test_name_is_compatible(name):
"###",
"@@@",
"a model",
"a_model",
"-model",
"model-",
"model@1",
@@ -181,4 +180,34 @@ def test_name_is_compatible(name):
],
)
def test_name_is_compatible_fails(name):
assert not name_is_compatible(name)
with pytest.raises(InvalidArgumentError):
check_name_format(name)
def test_get_rev(tmp_dir, dvc, scm):
scm.tag("[email protected]#1", annotated=True, message="foo")
scm.tag("[email protected]#1", annotated=True, message="foo")
scm.tag("myart#dev#1", annotated=True, message="foo")
rev = scm.get_rev()
assert dvc.artifacts.get_rev("myart") == rev
assert dvc.artifacts.get_rev("myart", version="v1.0.0") == rev
assert dvc.artifacts.get_rev("subdir:myart", version="v2.0.0") == rev
assert dvc.artifacts.get_rev("subdir/dvc.yaml:myart", version="v2.0.0") == rev
with pytest.raises(ArtifactNotFoundError):
dvc.artifacts.get_rev("myart", version="v3.0.0")
with pytest.raises(ArtifactNotFoundError):
dvc.artifacts.get_rev("myart", stage="prod")
def test_get_path(tmp_dir, dvc):
(tmp_dir / "dvc.yaml").dump(dvcyaml)
subdir = tmp_dir / "subdir"
subdir.mkdir()
(subdir / "dvc.yaml").dump(dvcyaml)
assert dvc.artifacts.get_path("myart") == "myart.pkl"
assert dvc.artifacts.get_path("subdir:myart") == os.path.join("subdir", "myart.pkl")
assert dvc.artifacts.get_path("subdir/dvc.yaml:myart") == os.path.join(
"subdir", "myart.pkl"
)
+3 -1
View File
@@ -676,10 +676,12 @@ def test_experiment_unchanged(tmp_dir, scm, dvc, exp_stage):
assert len(dvc.experiments.ls()[scm.get_rev()]) == 2
def test_experiment_run_dry(tmp_dir, scm, dvc, exp_stage):
def test_experiment_run_dry(tmp_dir, scm, dvc, exp_stage, mocker):
repro = mocker.spy(dvc.experiments, "reproduce_one")
dvc.experiments.run(exp_stage.addressing, dry=True)
assert len(dvc.experiments.ls()["master"]) == 0
assert repro.call_args.kwargs["tmp_dir"] is True
def test_clean(tmp_dir, scm, dvc, mocker):
+33
View File
@@ -0,0 +1,33 @@
import pytest
from dvc.repo.experiments.exceptions import (
ExperimentExistsError,
UnresolvedExpNamesError,
)
from dvc.repo.experiments.utils import exp_refs_by_names
def test_rename_experiment_by_name(scm, dvc, exp_stage):
dvc.experiments.run(exp_stage.addressing, name="test-name", params=["foo=1"])
old_ref = exp_refs_by_names(scm, {"test-name"})
dvc.experiments.rename("new-name", "test-name")
new_ref = exp_refs_by_names(scm, {"new-name"})
assert scm.get_ref(str(old_ref["test-name"][0])) is None
assert scm.get_ref(str(new_ref["new-name"][0])) is not None
with pytest.raises(UnresolvedExpNamesError):
dvc.experiments.rename("new-name", "foo")
def test_same_name(dvc, exp_stage):
dvc.experiments.run(exp_stage.addressing, name="same-name", params=["foo=1"])
assert dvc.experiments.rename("same-name", "same-name") is None
def test_existing_name(dvc, exp_stage):
dvc.experiments.run(exp_stage.addressing, name="first-name", params=["foo=1"])
dvc.experiments.run(exp_stage.addressing, name="second-name", params=["foo=2"])
with pytest.raises(ExperimentExistsError):
dvc.experiments.rename("second-name", "first-name")
dvc.experiments.rename("second-name", "first-name", force=True)
+12
View File
@@ -156,6 +156,18 @@ def test_untracked_dvclock_is_included_in_exp(tmp_dir, scm, dvc):
assert fs.exists("dvc.lock")
def test_exp_save_include_untracked_force(tmp_dir, dvc, scm):
setup_stage(tmp_dir, dvc, scm)
new_file = tmp_dir / "new_file"
new_file.write_text("new_file")
dvc.scm.ignore(new_file)
exp = dvc.experiments.save(include_untracked=["new_file"])
fs = scm.get_fs(exp)
assert fs.exists("new_file")
def test_exp_save_custom_message(tmp_dir, dvc, scm):
setup_stage(tmp_dir, dvc, scm)
+40 -25
View File
@@ -5,6 +5,7 @@ import pytest
from dvc.cli import main
from dvc.utils import relpath
from dvc.utils.serialize import JSONFileCorruptedError
def test_metrics_diff_simple(tmp_dir, scm, dvc, run_copy_metrics):
@@ -21,7 +22,7 @@ def test_metrics_diff_simple(tmp_dir, scm, dvc, run_copy_metrics):
expected = {"m.yaml": {"": {"old": 1, "new": 3, "diff": 2}}}
assert dvc.metrics.diff(a_rev="HEAD~2") == expected
assert dvc.metrics.diff(a_rev="HEAD~2") == {"diff": expected}
def test_metrics_diff_yaml(tmp_dir, scm, dvc, run_copy_metrics):
@@ -47,7 +48,7 @@ def test_metrics_diff_yaml(tmp_dir, scm, dvc, run_copy_metrics):
}
}
assert dvc.metrics.diff(a_rev="HEAD~2") == expected
assert dvc.metrics.diff(a_rev="HEAD~2") == {"diff": expected}
def test_metrics_diff_json(tmp_dir, scm, dvc, run_copy_metrics):
@@ -72,7 +73,7 @@ def test_metrics_diff_json(tmp_dir, scm, dvc, run_copy_metrics):
"a.b.c": {"old": 1, "new": 3, "diff": 2},
}
}
assert dvc.metrics.diff(a_rev="HEAD~2") == expected
assert dvc.metrics.diff(a_rev="HEAD~2") == {"diff": expected}
def test_metrics_diff_json_unchanged(tmp_dir, scm, dvc, run_copy_metrics):
@@ -94,7 +95,7 @@ def test_metrics_diff_json_unchanged(tmp_dir, scm, dvc, run_copy_metrics):
assert dvc.metrics.diff(a_rev="HEAD~2") == {}
def test_metrics_diff_broken_json(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_broken_json(M, tmp_dir, scm, dvc, run_copy_metrics):
metrics = {"a": {"b": {"c": 1, "d": 1, "e": "3"}}}
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
@@ -108,11 +109,14 @@ def test_metrics_diff_broken_json(tmp_dir, scm, dvc, run_copy_metrics):
(tmp_dir / "m.json").write_text(json.dumps(metrics) + "ma\nlformed\n")
assert dvc.metrics.diff() == {
"m.json": {
"a.b.e": {"old": "3", "new": None},
"a.b.c": {"old": 1, "new": None},
"a.b.d": {"old": 1, "new": None},
}
"diff": {
"m.json": {
"a.b.e": {"old": "3", "new": None},
"a.b.c": {"old": 1, "new": None},
"a.b.d": {"old": 1, "new": None},
}
},
"errors": {"workspace": {"m.json": M.instance_of(JSONFileCorruptedError)}},
}
@@ -129,15 +133,17 @@ def test_metrics_diff_new_metric(tmp_dir, scm, dvc, run_copy_metrics):
)
assert dvc.metrics.diff() == {
"m.json": {
"a.b.e": {"old": None, "new": "3"},
"a.b.c": {"old": None, "new": 1},
"a.b.d": {"old": None, "new": 1},
"diff": {
"m.json": {
"a.b.e": {"old": None, "new": "3"},
"a.b.c": {"old": None, "new": 1},
"a.b.d": {"old": None, "new": 1},
}
}
}
def test_metrics_diff_deleted_metric(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_deleted_metric(M, tmp_dir, scm, dvc, run_copy_metrics):
metrics = {"a": {"b": {"c": 1, "d": 1, "e": "3"}}}
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
@@ -151,11 +157,14 @@ def test_metrics_diff_deleted_metric(tmp_dir, scm, dvc, run_copy_metrics):
(tmp_dir / "m.json").unlink()
assert dvc.metrics.diff() == {
"m.json": {
"a.b.e": {"old": "3", "new": None},
"a.b.c": {"old": 1, "new": None},
"a.b.d": {"old": 1, "new": None},
}
"diff": {
"m.json": {
"a.b.e": {"old": "3", "new": None},
"a.b.c": {"old": 1, "new": None},
"a.b.d": {"old": 1, "new": None},
}
},
"errors": {"workspace": {"m.json": M.instance_of(FileNotFoundError)}},
}
@@ -173,9 +182,11 @@ def test_metrics_diff_with_unchanged(tmp_dir, scm, dvc, run_copy_metrics):
tmp_dir.scm_gen("metrics.yaml", "foo: 3\nxyz: 10", commit="3")
assert dvc.metrics.diff(a_rev="HEAD~2", all=True) == {
"metrics.yaml": {
"foo": {"old": 1, "new": 3, "diff": 2},
"xyz": {"old": 10, "new": 10, "diff": 0},
"diff": {
"metrics.yaml": {
"foo": {"old": 1, "new": 3, "diff": 2},
"xyz": {"old": 10, "new": 10, "diff": 0},
}
}
}
@@ -206,7 +217,7 @@ def test_metrics_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
expected = {"m.yaml": {"": {"old": 3, "new": 4, "diff": 1}}}
assert dvc.metrics.diff() == expected
assert dvc.metrics.diff() == {"diff": expected}
def test_metrics_diff_cli(tmp_dir, scm, dvc, run_copy_metrics, caplog, capsys):
@@ -242,7 +253,9 @@ def test_metrics_diff_non_metrics(tmp_dir, scm, dvc):
_gen(3)
result = dvc.metrics.diff(targets=["some_file.yaml"], a_rev="HEAD~2")
assert result == {"some_file.yaml": {"foo": {"old": 1, "new": 3, "diff": 2}}}
assert result == {
"diff": {"some_file.yaml": {"foo": {"old": 1, "new": 3, "diff": 2}}}
}
@pytest.mark.parametrize(
@@ -264,7 +277,9 @@ def test_diff_top_level_metrics(tmp_dir, dvc, scm, dvcfile, metrics_file):
metrics_file.dump({"foo": 5})
assert dvc.metrics.diff() == {
relpath(directory / metrics_file): {"foo": {"diff": 2, "new": 5, "old": 3}}
"diff": {
relpath(directory / metrics_file): {"foo": {"diff": 2, "new": 5, "old": 3}}
}
}
+52 -15
View File
@@ -1,13 +1,16 @@
import json
import os
from os.path import join
import pytest
from funcy import get_in
from dvc.cli import main
from dvc.dvcfile import PROJECT_FILE
from dvc.exceptions import OverlappingOutputPathsError
from dvc.repo import Repo
from dvc.utils.fs import remove
from dvc.utils.serialize import JSONFileCorruptedError, YAMLFileCorruptedError
from dvc.utils.serialize import JSONFileCorruptedError
def test_show_simple(tmp_dir, dvc, run_copy_metrics):
@@ -32,11 +35,13 @@ def test_show_simple_from_subdir(tmp_dir, dvc, run_copy_metrics):
expected_path = os.path.join("subdir", "metrics.yaml")
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
expected_path = os.path.join("..", "subdir", "metrics.yaml")
expected_path = os.path.join("subdir", "metrics.yaml")
with subdir.chdir():
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
subdir2 = tmp_dir / "subdir2"
subdir2.mkdir()
expected_path = os.path.join("subdir", "metrics.yaml")
with subdir2.chdir():
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
@@ -129,7 +134,7 @@ def test_show_subrepo_with_preexisting_tags(tmp_dir, scm):
scm.commit("init metrics")
scm.tag("v1")
expected_path = os.path.join("subdir", "metrics.yaml")
expected_path = "metrics.yaml"
assert dvc.metrics.show(all_tags=True) == {
"workspace": {"data": {expected_path: {"data": {"foo": 1}}}},
"v1": {"data": {expected_path: {"data": {"foo": 1}}}},
@@ -201,13 +206,13 @@ def test_show_non_metric_branch(tmp_dir, scm, use_dvc):
assert not (tmp_dir / ".dvc").exists()
def test_non_metric_and_recursive_show(tmp_dir, dvc, run_copy_metrics):
def test_non_metric_and_dir_show(tmp_dir, dvc, run_copy_metrics):
tmp_dir.gen({"metrics_t.yaml": "foo: 1.1", "metrics": {"metric1.yaml": "bar: 1.2"}})
metric2 = os.fspath(tmp_dir / "metrics" / "metric2.yaml")
run_copy_metrics("metrics_t.yaml", metric2, name="copy-metric2", metrics=[metric2])
assert dvc.metrics.show(targets=["metrics_t.yaml", "metrics"], recursive=True) == {
assert dvc.metrics.show(targets=["metrics_t.yaml", "metrics"]) == {
"": {
"data": {
os.path.join("metrics", "metric1.yaml"): {"data": {"bar": 1.2}},
@@ -244,15 +249,18 @@ def test_show_malformed_metric(tmp_dir, scm, dvc, caplog):
)
def test_metrics_show_no_target(tmp_dir, dvc, capsys):
assert dvc.metrics.show(targets=["metrics.json"]) == {"": {}}
def test_metrics_show_no_target(M, tmp_dir, dvc, capsys):
assert dvc.metrics.show(targets=["metrics.json"]) == {
"": {"data": {"metrics.json": {"error": M.instance_of(FileNotFoundError)}}}
}
def test_show_no_metrics_files(tmp_dir, dvc, caplog):
assert dvc.metrics.show() == {"": {}}
assert dvc.metrics.show() == {"": {"data": {}}}
@pytest.mark.parametrize("clear_before_run", [True, False])
@pytest.mark.skip(reason="no longer raising graph errors")
def test_metrics_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
data_dir = tmp_dir / "data"
data_dir.mkdir()
@@ -286,13 +294,19 @@ def test_metrics_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
@pytest.mark.parametrize(
"file,error_path",
"file,error_path,err_type",
(
(PROJECT_FILE, ["workspace", "error"]),
("metrics.yaml", ["workspace", "data", "metrics.yaml", "error"]),
(PROJECT_FILE, ["workspace", "error", "type"], "YAMLSyntaxError"),
(
"metrics.yaml",
["workspace", "data", "metrics.yaml", "error", "type"],
"YAMLFileCorruptedError",
),
),
)
def test_log_errors(tmp_dir, scm, dvc, capsys, run_copy_metrics, file, error_path):
def test_log_errors(
tmp_dir, scm, dvc, capsys, run_copy_metrics, file, error_path, err_type
):
tmp_dir.gen("metrics_t.yaml", "m: 1.1")
run_copy_metrics(
"metrics_t.yaml",
@@ -306,11 +320,34 @@ def test_log_errors(tmp_dir, scm, dvc, capsys, run_copy_metrics, file, error_pat
with open(file, "a", encoding="utf-8") as fd:
fd.write("\nMALFORMED!")
result = dvc.metrics.show(revs=["v1"])
assert main(["metrics", "show", "--all-tags", "--json"]) == 0
_, error = capsys.readouterr()
out, error = capsys.readouterr()
result = json.loads(out)
assert isinstance(get_in(result, error_path), YAMLFileCorruptedError)
assert get_in(result, error_path) == err_type
assert (
"DVC failed to load some metrics for following revisions: 'workspace'." in error
)
def test_cached_metrics(tmp_dir, dvc, scm, remote):
tmp_dir.dvc_gen(
{
"dir": {"metrics.yaml": "foo: 3\nbar: 10"},
"dir2": {"metrics.yaml": "foo: 42\nbar: 4"},
}
)
dvc.push()
dvc.cache.local.clear()
(tmp_dir / "dvc.yaml").dump({"metrics": ["dir/metrics.yaml", "dir2"]})
assert dvc.metrics.show() == {
"": {
"data": {
join("dir", "metrics.yaml"): {"data": {"foo": 3, "bar": 10}},
join("dir2", "metrics.yaml"): {"data": {"foo": 42, "bar": 4}},
}
}
}
+50 -27
View File
@@ -27,7 +27,7 @@ def test_diff(tmp_dir, scm, dvc):
tmp_dir.scm_gen("params.yaml", "foo: qux", commit="qux")
assert dvc.params.diff(a_rev="HEAD~2") == {
"params.yaml": {"foo": {"old": "bar", "new": "qux"}}
"diff": {"params.yaml": {"foo": {"old": "bar", "new": "qux"}}}
}
@@ -40,17 +40,21 @@ def test_diff_dirty(tmp_dir, scm, dvc):
tmp_dir.scm_gen("params.yaml", "foo: baz", commit="baz")
tmp_dir.gen("params.yaml", "foo: qux")
assert dvc.params.diff() == {"params.yaml": {"foo": {"old": "baz", "new": "qux"}}}
assert dvc.params.diff() == {
"diff": {"params.yaml": {"foo": {"old": "baz", "new": "qux"}}}
}
def test_diff_new(tmp_dir, scm, dvc):
tmp_dir.gen("params.yaml", "foo: bar")
dvc.run(cmd="echo params.yaml", params=["foo"], name="echo-params")
assert dvc.params.diff() == {"params.yaml": {"foo": {"old": None, "new": "bar"}}}
assert dvc.params.diff() == {
"diff": {"params.yaml": {"foo": {"old": None, "new": "bar"}}}
}
def test_diff_deleted(tmp_dir, scm, dvc):
def test_diff_deleted(M, tmp_dir, scm, dvc):
tmp_dir.gen("params.yaml", "foo: bar")
dvc.run(cmd="echo params.yaml", params=["foo"], name="echo-params")
scm.add(["params.yaml", "Dvcfile"])
@@ -58,7 +62,10 @@ def test_diff_deleted(tmp_dir, scm, dvc):
(tmp_dir / "params.yaml").unlink()
assert dvc.params.diff() == {"params.yaml": {"foo": {"old": "bar", "new": None}}}
assert dvc.params.diff() == {
"diff": {"params.yaml": {"foo": {"old": "bar", "new": None}}},
"errors": {"workspace": {"params.yaml": M.instance_of(FileNotFoundError)}},
}
def test_diff_list(tmp_dir, scm, dvc):
@@ -70,8 +77,10 @@ def test_diff_list(tmp_dir, scm, dvc):
tmp_dir.gen("params.yaml", "foo:\n- bar\n- baz\n- qux")
assert dvc.params.diff() == {
"params.yaml": {
"foo": {"old": "['bar', 'baz']", "new": "['bar', 'baz', 'qux']"}
"diff": {
"params.yaml": {
"foo": {"old": "['bar', 'baz']", "new": "['bar', 'baz', 'qux']"}
}
}
}
@@ -85,7 +94,7 @@ def test_diff_dict(tmp_dir, scm, dvc):
tmp_dir.gen("params.yaml", "foo:\n bar: qux")
assert dvc.params.diff() == {
"params.yaml": {"foo.bar": {"old": "baz", "new": "qux"}}
"diff": {"params.yaml": {"foo.bar": {"old": "baz", "new": "qux"}}}
}
@@ -99,9 +108,11 @@ def test_diff_with_unchanged(tmp_dir, scm, dvc):
tmp_dir.scm_gen("params.yaml", "foo: qux\nxyz: val", commit="qux")
assert dvc.params.diff(a_rev="HEAD~2", all=True) == {
"params.yaml": {
"foo": {"old": "bar", "new": "qux"},
"xyz": {"old": "val", "new": "val"},
"diff": {
"params.yaml": {
"foo": {"old": "bar", "new": "qux"},
"xyz": {"old": "val", "new": "val"},
}
}
}
@@ -119,7 +130,7 @@ def test_pipeline_tracked_params(tmp_dir, scm, dvc, run_copy):
tmp_dir.scm_gen("params.yaml", "foo: qux\nxyz: val", commit="qux")
assert dvc.params.diff(a_rev="HEAD~2") == {
"params.yaml": {"foo": {"old": "bar", "new": "qux"}}
"diff": {"params.yaml": {"foo": {"old": "bar", "new": "qux"}}}
}
@@ -148,9 +159,11 @@ def test_vars_shows_on_params_diff(tmp_dir, scm, dvc):
}
(tmp_dir / "dvc.yaml").dump(d)
assert dvc.params.diff() == {
"test_params.yaml": {
"vars.model1.epoch": {"new": 15, "old": None},
"vars.model2.epoch": {"new": 35, "old": None},
"diff": {
"test_params.yaml": {
"vars.model1.epoch": {"new": 15, "old": None},
"vars.model2.epoch": {"new": 35, "old": None},
}
}
}
scm.add(["dvc.yaml", "test_params.yaml"])
@@ -159,15 +172,19 @@ def test_vars_shows_on_params_diff(tmp_dir, scm, dvc):
param_data["vars"]["model1"]["epoch"] = 20
(tmp_dir / params_file).dump(param_data)
assert dvc.params.diff() == {
"test_params.yaml": {"vars.model1.epoch": {"new": 20, "old": 15, "diff": 5}}
"diff": {
"test_params.yaml": {"vars.model1.epoch": {"new": 20, "old": 15, "diff": 5}}
}
}
data_dir = tmp_dir / "data"
data_dir.mkdir()
with data_dir.chdir():
assert dvc.params.diff() == {
relpath(params_file): {
"vars.model1.epoch": {"new": 20, "old": 15, "diff": 5}
"diff": {
str(params_file.relative_to(tmp_dir)): {
"vars.model1.epoch": {"new": 20, "old": 15, "diff": 5}
}
}
}
@@ -202,16 +219,18 @@ def test_diff_targeted(tmp_dir, scm, dvc, run_copy):
)
assert dvc.params.diff(a_rev="HEAD~2") == {
"params.yaml": {"foo": {"old": "bar", "new": "qux"}},
"other_params.yaml": {"xyz": {"old": "val", "new": "val3"}},
"diff": {
"params.yaml": {"foo": {"old": "bar", "new": "qux"}},
"other_params.yaml": {"xyz": {"old": "val", "new": "val3"}},
}
}
assert dvc.params.diff(a_rev="HEAD~2", targets=["params.yaml"]) == {
"params.yaml": {"foo": {"old": "bar", "new": "qux"}}
"diff": {"params.yaml": {"foo": {"old": "bar", "new": "qux"}}}
}
assert dvc.params.diff(a_rev="HEAD~2", targets=["other_params.yaml"]) == {
"other_params.yaml": {"xyz": {"old": "val", "new": "val3"}}
"diff": {"other_params.yaml": {"xyz": {"old": "val", "new": "val3"}}}
}
@@ -228,10 +247,12 @@ def test_diff_without_targets_specified(tmp_dir, dvc, scm, file):
params_file.dump({"foo": {"bar": "baz"}, "y": "100"})
assert dvc.params.diff() == {
file: {
"foo.bar": {"new": "baz", "old": "bar"},
"x": {"new": None, "old": "0"},
"y": {"new": "100", "old": None},
"diff": {
file: {
"foo.bar": {"new": "baz", "old": "bar"},
"x": {"new": None, "old": "0"},
"y": {"new": "100", "old": None},
}
}
}
@@ -255,7 +276,9 @@ def test_diff_top_level_params(tmp_dir, dvc, scm, dvcfile, params_file):
params_file.dump({"foo": 5})
assert dvc.params.diff() == {
relpath(directory / params_file): {"foo": {"diff": 2, "new": 5, "old": 3}}
"diff": {
relpath(directory / params_file): {"foo": {"diff": 2, "new": 5, "old": 3}}
}
}
+27 -53
View File
@@ -1,15 +1,13 @@
import operator
from functools import reduce
from os.path import join
import pytest
from dvc.repo import Repo
from dvc.repo.stage import PROJECT_FILE
from dvc.utils.serialize import YAMLFileCorruptedError
def test_show_empty(dvc):
assert dvc.params.show() == {}
assert dvc.params.show() == {"": {"data": {}}}
def test_show(tmp_dir, dvc):
@@ -98,7 +96,7 @@ def test_pipeline_params(tmp_dir, scm, dvc, run_copy):
tmp_dir.scm_gen("params.yaml", "foo: baz\nxyz: val\nabc: ignore", commit="baz")
tmp_dir.scm_gen("params.yaml", "foo: qux\nxyz: val\nabc: ignore", commit="qux")
assert dvc.params.show(revs=["master"], deps=True) == {
assert dvc.params.show(revs=["master"], deps_only=True) == {
"master": {"data": {"params.yaml": {"data": {"foo": "qux", "xyz": "val"}}}}
}
assert dvc.params.show(revs=["master"]) == {
@@ -120,41 +118,6 @@ def test_show_no_repo(tmp_dir):
}
@pytest.mark.parametrize(
"file,error_path",
(
(PROJECT_FILE, ["v1", "error"]),
("params_other.yaml", ["v1", "data", "params_other.yaml", "error"]),
),
)
def test_log_errors(tmp_dir, scm, dvc, capsys, file, error_path):
tmp_dir.gen("params_other.yaml", "foo: bar")
dvc.run(
cmd="echo params_other.yaml",
params=["params_other.yaml:foo"],
name="train",
)
rename = (tmp_dir / file).read_text()
with open(tmp_dir / file, "a", encoding="utf-8") as fd:
fd.write("\nmalformed!")
scm.add([PROJECT_FILE, "params_other.yaml"])
scm.commit("init")
scm.tag("v1")
(tmp_dir / file).write_text(rename)
result = dvc.params.show(revs=["v1"])
_, error = capsys.readouterr()
assert isinstance(
reduce(operator.getitem, error_path, result), YAMLFileCorruptedError
)
assert "DVC failed to load some parameters for following revisions: 'v1'." in error
@pytest.mark.parametrize("file", ["params.yaml", "other_params.yaml"])
def test_show_without_targets_specified(tmp_dir, dvc, scm, file):
params_file = tmp_dir / file
@@ -177,7 +140,7 @@ def test_deps_multi_stage(tmp_dir, scm, dvc, run_copy):
scm.add(["params.yaml", PROJECT_FILE])
scm.commit("add stage")
assert dvc.params.show(revs=["master"], deps=True) == {
assert dvc.params.show(revs=["master"], deps_only=True) == {
"master": {"data": {"params.yaml": {"data": {"foo": "bar", "xyz": "val"}}}}
}
@@ -190,21 +153,32 @@ def test_deps_with_targets(tmp_dir, scm, dvc, run_copy):
scm.add(["params.yaml", PROJECT_FILE])
scm.commit("add stage")
assert dvc.params.show(targets=["params.yaml"], deps=True) == {
"": {"data": {"params.yaml": {"data": {"foo": "bar", "xyz": "val"}}}}
assert dvc.params.show(targets=["params.yaml"], deps_only=True) == {
"": {
"data": {
"params.yaml": {"data": {"abc": "ignore", "foo": "bar", "xyz": "val"}}
}
}
}
def test_deps_with_bad_target(tmp_dir, scm, dvc, run_copy):
tmp_dir.gen(
def test_cached_params(tmp_dir, dvc, scm, remote):
tmp_dir.dvc_gen(
{
"foo": "foo",
"foobar": "",
"params.yaml": "foo: bar\nxyz: val\nabc: ignore",
"dir": {"params.yaml": "foo: 3\nbar: 10"},
"dir2": {"params.yaml": "foo: 42\nbar: 4"},
}
)
run_copy("foo", "bar", name="copy-foo-bar", params=["foo"])
run_copy("foo", "bar1", name="copy-foo-bar-1", params=["xyz"])
scm.add(["params.yaml", PROJECT_FILE])
scm.commit("add stage")
assert dvc.params.show(targets=["foobar"], deps=True) == {}
dvc.push()
dvc.cache.local.clear()
(tmp_dir / "dvc.yaml").dump({"params": ["dir/params.yaml", "dir2"]})
assert dvc.params.show() == {
"": {
"data": {
join("dir", "params.yaml"): {"data": {"foo": 3, "bar": 10}},
join("dir2", "params.yaml"): {"data": {"foo": 42, "bar": 4}},
}
}
}
+1 -1
View File
@@ -426,5 +426,5 @@ def test_show_plots_defined_with_native_os_path(tmp_dir, dvc, scm, capsys):
assert "errors" not in json_out
json_data = json_out["data"]
assert json_data[f"dvc.yaml::{top_level_plot}"]
assert json_data[f"{top_level_plot}"]
assert json_data[stage_plot]
+56
View File
@@ -668,3 +668,59 @@ def test_rename_multiple_files_same_hashes(tmp_dir, scm, dvc):
},
],
}
def test_diff_granular(tmp_dir, dvc, scm):
tmp_dir.gen(
{
"dir": {
"data": {
"subdir": {"subfoo": "subfoo", "subbar": "subbar"},
"foo": "foo",
"bar": "bar",
},
},
}
)
dvc.add(os.path.join("dir", "data"))
scm.add(os.path.join("dir", "data.dvc"))
scm.add(os.path.join("dir", ".gitignore"))
scm.commit("data")
assert dvc.diff() == {}
(tmp_dir / "dir" / "data" / "subdir" / "new").write_text("new")
assert dvc.diff() == {
"added": [
{
"hash": "22af645d1859cb5ca6da0c484f1f37ea",
"path": os.path.join("dir", "data", "subdir", "new"),
}
],
"deleted": [],
"modified": [
{
"hash": {
"new": "efa5b20d5f935dcc5555b26db6e19b76.dir",
"old": "1aca2c799df82929bbdd976557975546.dir",
},
"path": os.path.join("dir", "data", ""),
}
],
"not in cache": [],
"renamed": [],
}
assert dvc.diff(targets=[os.path.join("dir", "data", "subdir")]) == {
"added": [
{
"hash": "22af645d1859cb5ca6da0c484f1f37ea",
"path": os.path.join("dir", "data", "subdir", "new"),
}
],
"deleted": [],
"modified": [],
"not in cache": [],
"renamed": [],
}
+18 -2
View File
@@ -36,14 +36,14 @@ def test_gc_api(dvc, good_and_bad_cache):
dvc.gc(workspace=True)
odb = dvc.cache.local
good_cache, bad_cache = good_and_bad_cache
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
assert set(odb.all()) == good_cache
def test_gc_cli(dvc, good_and_bad_cache):
assert main(["gc", "-wf"]) == 0
odb = dvc.cache.local
good_cache, bad_cache = good_and_bad_cache
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
assert set(odb.all()) == good_cache
def test_gc_branches_tags(tmp_dir, dvc, scm):
@@ -423,3 +423,19 @@ def test_gc_cloud_remote_field(tmp_dir, scm, dvc, mocker, make_remote):
mocked_remove = mocker.spy(LocalFileSystem, "remove")
dvc.gc(workspace=True, cloud=True)
assert len(mocked_remove.mock_calls) == 2 # local and other_remote
def test_gc_dry(dvc, good_and_bad_cache):
dvc.gc(workspace=True, dry=True)
odb = dvc.cache.local
good_cache, bad_cache = good_and_bad_cache
assert set(odb.all()) != good_cache
def test_gc_logging(caplog, dvc, good_and_bad_cache):
with caplog.at_level(logging.INFO, logger="dvc"):
dvc.gc(workspace=True)
assert "Removed 3 objects from repo cache." in caplog.text
assert "No unused 'local' cache to remove." in caplog.text
assert "No unused 'legacy' cache to remove." in caplog.text
+10
View File
@@ -249,9 +249,19 @@ def test_import_url_fs_config(tmp_dir, dvc, workspace, mocker):
dep_init = mocker.spy(Dependency, "__init__")
dvc.imp_url(url, fs_config={"jobs": 42})
stage = load_file(dvc, "foo.dvc").stage
assert stage.deps[0].def_fs_config == {"jobs": 42}
dep_init_kwargs = dep_init.call_args[1]
assert dep_init_kwargs.get("fs_config") == {"jobs": 42}
assert get_fs_config.call_args_list[0][1] == {"url": "foo"}
assert get_fs_config.call_args_list[1][1] == {"url": url, "jobs": 42}
assert get_fs_config.call_args_list[2][1] == {"name": "workspace"}
dep_init.reset_mock()
dvc.pull("foo.dvc")
dep_init_kwargs = dep_init.call_args[1]
assert dep_init_kwargs.get("fs_config") == {"jobs": 42}
+55
View File
@@ -1,8 +1,10 @@
import os
import pytest
from funcy import first
from dvc.dvcfile import LOCK_FILE
from dvc.stage.cache import RunCacheNotSupported, _get_stage_hash
from dvc.utils.fs import remove
@@ -99,6 +101,7 @@ def test_memory_for_multiple_runs_of_same_stage(tmp_dir, dvc, run_copy, mocker):
from dvc.stage import run as _run
mock_restore = mocker.spy(dvc.stage_cache, "restore")
mocker.spy(dvc.stage_cache, "_load_cache")
mock_run = mocker.spy(_run, "cmd_run")
(tmp_dir / "bar").unlink()
@@ -123,6 +126,46 @@ def test_memory_for_multiple_runs_of_same_stage(tmp_dir, dvc, run_copy, mocker):
assert (tmp_dir / LOCK_FILE).exists()
def test_newest_entry_is_loaded_for_non_deterministic_stage(tmp_dir, dvc, mocker):
tmp_dir.gen("foo", "foo")
assert not os.path.exists(dvc.stage_cache.cache_dir)
dvc.stage.add(
name="non-deterministic",
cmd='python -c "from time import time; print(time())" > bar',
deps=["foo"],
outs=["bar"],
)
for i in range(4):
(stage,) = dvc.reproduce("non-deterministic", force=True)
assert _recurse_count_files(dvc.stage_cache.cache_dir) == i + 1
key = _get_stage_hash(stage)
cache_dir = dvc.stage_cache._get_cache_dir(key)
old_entries = os.listdir(cache_dir)
(stage,) = dvc.reproduce("non-deterministic", force=True)
newest_output = (tmp_dir / "bar").read_text()
newest_entry = first(e for e in os.listdir(cache_dir) if e not in old_entries)
from dvc.stage import run as _run
mock_restore = mocker.spy(dvc.stage_cache, "restore")
mock_load = mocker.spy(dvc.stage_cache, "_load_cache")
mock_run = mocker.spy(_run, "cmd_run")
(tmp_dir / "bar").unlink()
(tmp_dir / LOCK_FILE).unlink()
(stage,) = dvc.reproduce("non-deterministic")
assert (tmp_dir / LOCK_FILE).exists()
assert (tmp_dir / "bar").read_text() == newest_output
mock_run.assert_not_called()
mock_restore.assert_called_once_with(stage, dry=False)
mock_load.assert_called_with(key, newest_entry)
def test_memory_runs_of_multiple_stages(tmp_dir, dvc, run_copy, mocker):
tmp_dir.gen("foo", "foo")
assert not os.path.exists(dvc.stage_cache.cache_dir)
@@ -186,3 +229,15 @@ def test_restore_pull(tmp_dir, dvc, run_copy, mocker, local_remote):
assert (tmp_dir / "bar").exists()
assert not (tmp_dir / "foo").unlink()
assert (tmp_dir / LOCK_FILE).exists()
def test_push_pull_unsupported(tmp_dir, dvc, mocker, run_copy):
tmp_dir.gen("foo", "foo")
run_copy("foo", "bar", name="copy-foo-bar")
mocker.patch.object(
dvc.cloud, "get_remote_odb", side_effect=RunCacheNotSupported("foo")
)
with pytest.raises(RunCacheNotSupported):
dvc.push(run_cache=True)
with pytest.raises(RunCacheNotSupported):
dvc.pull(run_cache=True)
+55
View File
@@ -191,3 +191,58 @@ def repo_with_config_plots(tmp_dir, scm, dvc, run_copy_metrics):
}
return make
@pytest.fixture
def repo_with_dvclive_plots(tmp_dir, scm, dvc, run_copy_metrics):
def make():
metrics = [
{"step": 1, "metric": 0.1},
{"step": 2, "metric": 0.2},
{"step": 3, "metric": 0.3},
]
metrics_path = tmp_dir / "dvclive" / "plots" / "metrics" / "metric.tsv"
metrics_path.parent.mkdir(parents=True)
metrics_path.dump_json(metrics)
plots_config_v1 = [
{
"plots/metrics": {
"x": "step",
}
}
]
from dvc.utils.serialize import modify_yaml
dvcyaml_path_v1 = tmp_dir / "dvclive" / "dvc.yaml"
with modify_yaml(dvcyaml_path_v1) as dvcfile_content:
dvcfile_content["plots"] = plots_config_v1
scm.add([metrics_path, dvcyaml_path_v1])
scm.commit("add dvclive 2.x plots")
yield {
"data": {metrics_path: metrics},
"configs": {dvcyaml_path_v1: plots_config_v1},
}
plots_config_v2 = [
{
"dvclive/plots/metrics": {
"x": "step",
}
}
]
dvcyaml_path_v1.unlink()
dvcyaml_path_v2 = tmp_dir / "dvc.yaml"
with modify_yaml(dvcyaml_path_v2) as dvcfile_content:
dvcfile_content["plots"] = plots_config_v2
yield {
"data": {metrics_path: metrics},
"configs": {dvcyaml_path_v2: plots_config_v2},
}
return make
+18 -3
View File
@@ -440,8 +440,8 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
html_result = extract_vega_specs(
html_path,
[
"dvc.yaml::linear_train_vs_test",
"dvc.yaml::confusion_train_vs_test",
"linear_train_vs_test",
"confusion_train_vs_test",
],
)
ble = _update_datapoints(
@@ -456,5 +456,20 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
},
)
assert html_result["dvc.yaml::linear_train_vs_test"]["data"]["values"] == ble
assert html_result["linear_train_vs_test"]["data"]["values"] == ble
# TODO check json results once vscode is able to handle flexible plots
@pytest.mark.vscode
def test_repo_with_dvclive_plots(tmp_dir, capsys, repo_with_dvclive_plots):
next(repo_with_dvclive_plots())
for s in ("show", "diff"):
_, json_result, split_json_result = call(capsys, subcommand=s)
expected_result = {
"data": {
"dvclive/plots/metrics/metric.tsv": [],
},
}
assert json_result == expected_result
assert split_json_result == expected_result
+1
View File
@@ -16,6 +16,7 @@ def test_checkout(tmp_dir, dvc, mocker):
recursive=False,
relink=True,
with_deps=True,
allow_missing=False,
)
+66 -1
View File
@@ -9,6 +9,7 @@ from dvc.commands.experiments.ls import CmdExperimentsList
from dvc.commands.experiments.pull import CmdExperimentsPull
from dvc.commands.experiments.push import CmdExperimentsPush
from dvc.commands.experiments.remove import CmdExperimentsRemove
from dvc.commands.experiments.rename import CmdExperimentsRename
from dvc.commands.experiments.run import CmdExperimentsRun
from dvc.commands.experiments.save import CmdExperimentsSave
from dvc.commands.experiments.show import CmdExperimentsShow
@@ -26,7 +27,19 @@ def test_experiments_apply(dvc, scm, mocker):
assert cmd.run() == 0
m.assert_called_once_with(cmd.repo, "exp_rev")
m.assert_called_once_with(cmd.repo, "exp_rev", force=False)
def test_experiments_apply_force(dvc, scm, mocker):
cli_args = parse_args(["experiments", "apply", "exp_rev", "--force"])
assert cli_args.func == CmdExperimentsApply
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.apply.apply", return_value={})
assert cmd.run() == 0
m.assert_called_once_with(cmd.repo, "exp_rev", force=True)
def test_experiments_diff(dvc, scm, mocker):
@@ -418,6 +431,40 @@ def test_experiments_remove_invalid(dvc, scm, mocker, capsys, caplog):
)
def test_experiments_rename_flag(dvc, scm, mocker, capsys, caplog):
cli_args = parse_args(
[
"experiments",
"rename",
"--git-remote",
"myremote",
"exp-123",
"exp-234",
]
)
assert cli_args.func == CmdExperimentsRename
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.rename.rename", return_value={})
assert cmd.run() == 0
m.assert_called_once_with(
cmd.repo,
exp_name="exp-123",
new_name="exp-234",
git_remote="myremote",
force=False,
)
def test_experiments_rename_invalid(dvc, scm, mocker, capsys, caplog):
cmd = CmdExperimentsRename(parse_args(["exp", "rename", "exp-1"]))
with pytest.raises(InvalidArgumentError) as excinfo:
cmd.run()
assert (
str(excinfo.value)
== "An experiment to rename and a new experiment name are required."
)
def test_experiments_save(dvc, scm, mocker):
cli_args = parse_args(["exp", "save", "--name", "exp-name", "--force"])
assert cli_args.func == CmdExperimentsSave
@@ -432,6 +479,24 @@ def test_experiments_save(dvc, scm, mocker):
)
def test_experiments_save_message(dvc, scm, mocker):
cli_args = parse_args(["exp", "save", "-m", "custom commit message"])
assert cli_args.func == CmdExperimentsSave
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.experiments.save.save", return_value="acabb")
assert cmd.run() == 0
m.assert_called_once_with(
cmd.repo,
name=None,
force=False,
include_untracked=[],
message="custom commit message",
)
def test_experiments_clean(dvc, scm, mocker):
cli_args = parse_args(["experiments", "clean"])
assert cli_args.func == CmdExperimentsClean
+2
View File
@@ -22,6 +22,7 @@ def test_(dvc, scm, mocker):
"--force",
"--jobs",
"3",
"--dry",
"--projects",
"project1",
"project2",
@@ -49,6 +50,7 @@ def test_(dvc, scm, mocker):
rev=None,
num=None,
not_in_remote=False,
dry=True,
)
cli_args = parse_args(["gc"])
+1 -1
View File
@@ -11,4 +11,4 @@ def test_get_url(mocker):
assert cmd.run() == 0
m.assert_called_once_with("src", out="out", jobs=5, force=False)
m.assert_called_once_with("src", out="out", jobs=5, force=False, fs_config=None)
+3
View File
@@ -41,6 +41,7 @@ def test_import(mocker, dvc):
config="myconfig",
remote="myremote",
remote_config={"k1": "v1", "k2": "v2"},
force=False,
)
@@ -74,6 +75,7 @@ def test_import_no_exec(mocker, dvc):
config=None,
remote=None,
remote_config=None,
force=False,
)
@@ -107,4 +109,5 @@ def test_import_no_download(mocker, dvc):
config=None,
remote=None,
remote_config=None,
force=False,
)
+3
View File
@@ -34,6 +34,7 @@ def test_import_url(mocker, dvc):
jobs=4,
force=False,
version_aware=False,
fs_config=None,
)
@@ -83,6 +84,7 @@ def test_import_url_no_exec_download_flags(mocker, flag, expected, dvc):
jobs=None,
force=False,
version_aware=False,
fs_config=None,
**expected,
)
@@ -115,6 +117,7 @@ def test_import_url_to_remote(mocker, dvc):
jobs=None,
force=False,
version_aware=False,
fs_config=None,
)
+2 -2
View File
@@ -10,7 +10,7 @@ def test_ls_url(mocker):
assert cmd.run() == 0
m.assert_called_once_with("src", recursive=False)
m.assert_called_once_with("src", recursive=False, fs_config=None)
def test_recursive(mocker):
@@ -21,4 +21,4 @@ def test_recursive(mocker):
assert cmd.run() == 0
m.assert_called_once_with("src", recursive=True)
m.assert_called_once_with("src", recursive=True, fs_config=None)
+7 -12
View File
@@ -11,7 +11,6 @@ def test_metrics_diff(dvc, mocker, capsys):
"diff",
"HEAD~10",
"HEAD~1",
"-R",
"--all",
"--md",
"--targets",
@@ -24,7 +23,10 @@ def test_metrics_diff(dvc, mocker, capsys):
assert cli_args.func == CmdMetricsDiff
cmd = cli_args.func(cli_args)
diff = {"metrics.yaml": {"": {"old": 1, "new": 3}}}
diff = {
"diff": {"metrics.yaml": {"": {"old": 1, "new": 3}}},
"errors": {"workspace": Exception},
}
metrics_diff = mocker.patch("dvc.repo.metrics.diff.diff", return_value=diff)
show_diff_mock = mocker.patch("dvc.compare.show_diff")
@@ -35,11 +37,10 @@ def test_metrics_diff(dvc, mocker, capsys):
targets=["target1", "target2"],
a_rev="HEAD~10",
b_rev="HEAD~1",
recursive=True,
all=True,
)
show_diff_mock.assert_called_once_with(
diff,
diff["diff"],
title="Metric",
no_path=True,
precision=5,
@@ -57,7 +58,6 @@ def test_metrics_diff_json(dvc, mocker, capsys):
"diff",
"HEAD~10",
"HEAD~1",
"-R",
"--all",
"--json",
"--targets",
@@ -72,7 +72,7 @@ def test_metrics_diff_json(dvc, mocker, capsys):
assert cli_args.func == CmdMetricsDiff
cmd = cli_args.func(cli_args)
diff = {"metrics.yaml": {"": {"old": 1, "new": 3}}}
diff = {"diff": {"metrics.yaml": {"": {"old": 1, "new": 3}}}}
metrics_diff = mocker.patch("dvc.repo.metrics.diff.diff", return_value=diff)
show_diff_mock = mocker.patch("dvc.compare.show_diff")
@@ -83,11 +83,10 @@ def test_metrics_diff_json(dvc, mocker, capsys):
targets=["target1", "target2"],
a_rev="HEAD~10",
b_rev="HEAD~1",
recursive=True,
all=True,
)
show_diff_mock.assert_not_called()
assert json.dumps(diff) in out
assert json.dumps(diff["diff"]) in out
def test_metrics_show(dvc, mocker):
@@ -95,7 +94,6 @@ def test_metrics_show(dvc, mocker):
[
"metrics",
"show",
"-R",
"--all-tags",
"--all-branches",
"--all-commits",
@@ -116,7 +114,6 @@ def test_metrics_show(dvc, mocker):
m1.assert_called_once_with(
cmd.repo,
["target1", "target2"],
recursive=True,
all_tags=True,
all_branches=True,
all_commits=True,
@@ -138,7 +135,6 @@ def test_metrics_show_json(dvc, mocker, capsys):
"metrics",
"show",
"--json",
"-R",
"--all-tags",
"--all-branches",
"--all-commits",
@@ -163,7 +159,6 @@ def test_metrics_show_json(dvc, mocker, capsys):
metrics_show.assert_called_once_with(
cmd.repo,
["target1", "target2"],
recursive=True,
all_tags=True,
all_branches=True,
all_commits=True,

Some files were not shown because too many files have changed in this diff Show More