Compare commits

..
111 Commits
Author SHA1 Message Date
Peter Rowlands 4ee4f137f2 exp pull: update for index fetch 2023-06-26 14:09:24 +09:00
Oded Messer a750a75016 Stable experiment range sorting (#9660)
* secondary sorting by rev alphanumerically. to introduce determinism

* add UT
2023-06-26 10:53:39 +09:00
Ruslan Kuprieiev cf976f17ae data: status: use ui.progress instead of ui.status (#9662)
With all fetch and checkout related things that we've done in the recent past,
we now have all the mechanisms necessary to provide actual progress (no total,
but still better than spinner) during `dvc data status` operations.
2023-06-26 00:10:44 +03:00
Ruslan Kuprieiev 7041fa5f42 checkout: use ui.progress (#9661)
* fs: callbacks: create our own TqdmCallback

We have a near identical implementation in dvc-objects, but this one is
meant to work with our Tqdm progress instance.

dvc-objects one is useful on other levels, but dvc should have itsown
that inherits from dvc-objects' Callback.

* progress: add as_callback

* checkout: use ui.progress
2023-06-25 23:14:49 +03:00
Ruslan Kuprieiev d898877175 checkout: use single tics instead of curly braces (#9659)
Typo during recent changes 🤦 

Related https://github.com/iterative/dvc/issues/9658
2023-06-24 00:06:05 +03:00
skshetry 1ca1d78b38 deps: bump gdrive to 2.20 (#9657) 2023-06-23 09:13:35 +05:45
David de la Iglesia Castro da6ebdbf8c Update schema.py 2023-06-23 00:10:11 +09:00
Peter Rowlands e995492095 output: check legacy hash name in output.add() 2023-06-22 22:21:34 +09:00
Peter Rowlands 276d499c9b deps: bump scmrepo minver 2023-06-22 22:04:16 +09:00
skshetry fd4696cc50 Revert "drop stage-level vars" (#9647)
Revert "drop stage-level vars (#9556)"

This reverts commit 358c5e0c39.
2023-06-22 18:44:51 +05:45
Peter Rowlands 90b9193250 output: preserve legacy hash_name on save() for unchanged data 2023-06-22 16:20:56 +09:00
Ruslan Kuprieiev 77e35145a9 checkout: delete output if we can't create it fully (#9642) 2023-06-22 02:54:53 +03:00
Dave Berenbaum 49c7c55ba3 exp: push/pull --rev help text shows default val (#9636) 2023-06-21 02:05:50 +03:00
Dave BerenbaumandRuslan Kuprieiev f28d5ee9e0 output: set remove to recursive=false (#9574)
* output: set remove to recursive=false

* try to make test pass on windows

* Update dvc/output.py

---------

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2023-06-21 01:33:04 +03:00
Ruslan Kuprieiev 621fe07a3b checkout: friendlier UI when tryint to remove uncached files
Related #9618
2023-06-20 21:09:00 +03:00
Ruslan Kuprieiev 8323923ce9 deps: bump dvc-data to >= 2.1.1 2023-06-20 18:29:52 +03:00
dberenbaum 04b50c5cd0 exp push/pull: default to head rev 2023-06-20 15:53:46 +09:00
dberenbaum b20a59d748 exp push/pull: default to all exps on head 2023-06-20 15:53:46 +09:00
dberenbaum bf2f08da09 exp list: fix ui for tags/branches 2023-06-20 15:52:18 +09:00
Ruslan Kuprieiev 1835c8c6e9 tests: unit: explicitly ask for dvc repo
Currently these rely on the root of the repo being in the dvc repo.
2023-06-19 03:44:10 +03:00
Ruslan Kuprieiev 7e1bb07f10 gha: don't build binary packages
The build process is much more stable these days and there is no reason
to keep building these packages here. Instead every package has itsown
building process (dvc-osxpkg, dvc-exe, dvc-s3-repo) and knows best how
to build and sign correctly.

pyinstaller hooks need to go into a `__pyinstaller` later on.
2023-06-19 03:04:04 +03:00
Ruslan Kuprieiev 92dc1f31f5 outputs: is_in_repo: don't try to realpath fs_path
Fixes #9621
2023-06-19 01:19:46 +03:00
Ruslan Kuprieiev e16c0c8b2f checkout/fetch: finer control over steps
See https://github.com/iterative/dvc-data/pull/390 and https://github.com/iterative/dvc-data/pull/391
2023-06-18 18:58:30 +03:00
Ruslan Kuprieiev 33e7645603 tests: odb: index: simplify test
For some unknown reason the mock in this test leaks into the outside tests.

I wasn't able to determine why in the reasonable amount of time, so instead just
simplifying it to not use mocker at all.

That whole file should really be migrated to dvc-data, but that needs some time.
2023-06-18 18:58:30 +03:00
Ruslan Kuprieiev c827654fe7 tests: use mocker instead of unittest.mock where possible
Had the "pleasure" of finding a mocker.patch leak into other tests and one
of the suspicions is that mixing unittest.mock and mocker might be somehow
related.

In any case, we should use mocker where possible anyway.
2023-06-18 17:10:45 +03:00
dberenbaum b8c666b1b7 exp show: quick fix for handling defaultdict in py<3.11.0 2023-06-16 22:54:40 +03:00
Ruslan Kuprieiev fd73b389c6 api: show/exp: add config option
Fixes #9613
2023-06-16 17:09:38 +03:00
Ruslan Kuprieiev 0433565f1f api: add config support for open/read
Fixes #9610
2023-06-15 18:30:14 +03:00
Peter Rowlands b5449503f0 benchmarks: update min dvc version for test_exp_show 2023-06-15 12:58:21 +09:00
Peter Rowlands fa14507bc8 benchmarks: use bench_dvc for commands that generate dvcfiles 2023-06-15 10:44:58 +09:00
Peter Rowlands 21458796ed benchmarks: update test_exp_show to use example-get-started 2023-06-15 10:44:58 +09:00
Ruslan Kuprieiev a6e49a72de diff: handle revisions pointing to the same sha
Fixes #9604
2023-06-14 18:48:18 +03:00
Ruslan Kuprieiev 97348cfba7 Revert "checkout/fetch: finer control over steps"
This reverts commit c5e3d42899.
2023-06-14 03:39:43 +03:00
Ruslan Kuprieiev c5e3d42899 checkout/fetch: finer control over steps
See https://github.com/iterative/dvc-data/pull/390 and https://github.com/iterative/dvc-data/pull/391
2023-06-13 23:11:14 +03:00
skshetry c6c2ce130a remove dvc._build (#9597) 2023-06-13 20:31:22 +05:45
skshetry be975ad9eb pyproject.toml: reformat project table (#9596) 2023-06-13 20:02:34 +05:45
daavoo d0aa1ce776 repro: Check for hash mismatch bewteen deleted dependencies and upstream outputs.
Only if `--allow-missing` is passed.

- Create `tests/func/repro` and extract `pull` and `allow_missing` to separate test files.

Closes #9530
2023-06-13 15:46:34 +03:00
Peter Rowlands 5ec4fa473c cache migrate: add utility for migrating local cache files to 3.x 2023-06-13 21:33:46 +09:00
Peter Rowlands 5fd54679c7 dvc-data: bump to 1.11.0 2023-06-13 21:33:46 +09:00
Ruslan Kuprieiev 5c1487e2ec deps: bump dvc-data to >=1.10.0 2023-06-13 14:45:36 +03:00
skshetry 30517f6676 deps: update dvc-task/dvc-s3/pytest-cov (#9592)
* remove deprecation warnings

* Update pyproject.toml
2023-06-13 15:39:34 +05:45
Ruslan Kuprieiev bbb77360e9 deps: bump dvc-data to >=1.9.0
Fixes #9541
2023-06-12 22:03:29 +03:00
Ruslan Kuprieiev 1af150a5e9 index: use out.hash_info for complete imports 2023-06-12 16:55:36 +03:00
Peter Rowlands cf59d801ce repo.index: use correct hash when rebuilding index entries 2023-06-12 16:22:25 +09:00
Peter Rowlands b733dea4c2 dvc-data: bump to 1.7.0 2023-06-12 16:22:25 +09:00
Ruslan Kuprieiev 2ed677057d fetch: cache collected
This avoids recollecting the cache files over and over again.

For example, `dvc fetch` noop in `dvc-bench` went from ~30sec down to ~10sec
for me.

Next step is to also cache remote index, so we don't have to waste those
~10sec checking files in the remote.

Related https://github.com/iterative/dvc-data/pull/376
2023-06-12 04:31:37 +03:00
Ruslan Kuprieiev 0597311439 deps: bump dvc-data to >=1.5.0 2023-06-12 01:04:24 +03:00
Ruslan Kuprieiev fc8ec6aa95 fetch: show progress when collecting 2023-06-10 22:48:16 +03:00
Ruslan Kuprieiev 3dca69b678 deps: bump dvc-data to >= 1.2.0
This brings https://github.com/iterative/dvc-data/pull/373 , which improves UI for `checkout` by not visually hanging during `diff` stage inside.
2023-06-10 21:33:25 +03:00
Ruslan Kuprieiev 4c0bb8d316 fetch: use index fetch 2023-06-10 21:18:57 +03:00
Ruslan Kuprieiev 1cb371d4d7 repo: commit: don't require target
Same as CLI.
2023-06-10 19:19:40 +03:00
Ruslan Kuprieiev c96795dcfb tests: checkout: fix test_checkout_relink_protected on windows 2023-06-09 22:36:33 +03:00
Ruslan Kuprieiev 8f768addf9 checkout: don't validate cache
Part of #9561
2023-06-09 18:41:19 +03:00
Ruslan Kuprieiev af1fb722c8 dvc: drop cached external outputs
Fixes #9531

Docs https://github.com/iterative/dvc.org/pull/4574 Kudos @dberenbaum
2023-06-09 18:34:36 +03:00
Ruslan Kuprieiev 8b1c1a31f3 deps: bump dvc-data to >=1.0.3 2023-06-09 18:16:51 +03:00
Ruslan Kuprieiev 262419ef35 deps: bump dvc-data to >=1.0.2
Related https://github.com/iterative/dvc/issues/9566
2023-06-09 16:26:31 +03:00
Peter Rowlands 265fc581c0 dvc-data: bump to 1.0.0 2023-06-09 15:49:44 +03:00
Peter Rowlands 80f414c19d remote: support legacy cache push/fetch 2023-06-09 15:49:44 +03:00
Peter Rowlands 2cd33e10ac output: serialize 3.x hash name in 'hash' field 2023-06-09 15:49:44 +03:00
Peter Rowlands 603f5fdba1 cache: move 3.x data objects into files/md5/ prefix 2023-06-09 15:49:44 +03:00
Dave Berenbaum 528eab0418 import: drop --file (#9562) 2023-06-09 10:03:32 +05:45
skshetry ed664143b4 dvc.build: move logic out of __init__ to dvc.build (#9563) 2023-06-08 08:19:59 +05:45
Saugat Pachhai (सौगात) 21a2504bcb dvc.utils.pkg: move PKG to top-level dvc
PKG has to be specified in dvc/_build.py now, and can be
imported using:

    from dvc import PKG

This is done to avoid importing from dvc/utils pkg which can
be expensive.
Also it's not an utility, so it is better that it remains at
the top-level namespace.
Note that we may have to fix the path for a few packages that we build,
like conda/snap/chocolatey.
2023-06-08 00:31:00 +03:00
Ruslan Kuprieiev d0291e8039 checkout: let index checkout error-out on missing hashes
This ensures we don't assume that everything needs to have hash_info to be
checked out (e.g. we can totally checkout directly from FileStorage).

Related https://github.com/iterative/dvc/pull/9424
2023-06-07 14:56:19 +03:00
skshetry 358c5e0c39 drop stage-level vars (#9556)
drop stage-level vars support
2023-06-07 11:10:08 +05:45
skshetry 9d7fba5c61 fix tests (#9555) 2023-06-07 10:28:54 +05:45
skshetry a1e339b4fa .dvc file: remove cmd support (#9554) 2023-06-07 09:30:13 +05:45
skshetry 6c58d438b3 tests: remove single_stage tests from repro/run tests (#9553) 2023-06-07 09:25:08 +05:45
skshetry 0c709b6ec1 .dvc file: remove metrics/plots support (#9552) 2023-06-06 20:42:09 +05:45
skshetry 0aeb7a8662 .dvc file: remove params support (#9551) 2023-06-06 20:05:36 +05:45
skshetry 46f3157f43 add: rename jobs to remote_jobs to mirror CLI; move validation to CLI (#9550)
- Follow up on https://github.com/iterative/dvc/pull/9522, renames jobs kwarg to remote_jobs
  to mirror CLI.
- Also moves validation logic to CLI.
2023-06-06 19:30:39 +05:45
skshetry 36fd69be21 add: remove file (#9548)
* add: remove `file`

Follow up to #9547.

* Update add.py

* fix tests
2023-06-06 18:44:19 +05:45
skshetry 3759535d43 tests: remove single_stage stage runs (#9549) 2023-06-06 18:38:07 +05:45
dberenbaum 2c4365513e add: deprecate --file 2023-06-06 14:57:07 +03:00
skshetry 26152292aa remove support for reading and generating Dvcfile (#9546) 2023-06-06 15:29:10 +05:45
skshetry 3e0f2bafbb add: small misc refactoring (#9540) 2023-06-05 18:41:17 +05:45
Ruslan Kuprieiev 78637713cf checkout: use index checkout
Needed for https://github.com/iterative/dvc/pull/9424
2023-06-04 23:27:01 +03:00
Ruslan Kuprieiev f9104275f3 deps: install tzdata for testing with celery (#9537)
Temporary fix for CI on windows with py >=3.9.
2023-06-04 22:18:23 +03:00
Ruslan Kuprieiev bce3b5d2a1 update: make sure we update hashes/checksums on --to-remote (#9536)
Discovered in #9444
2023-06-04 19:36:54 +03:00
Dave Berenbaum 6ace5ed380 add: tweak stage overlap error message (#9514)
* add: tweak stage overlap error message

* add: instructions for how to fix overlap error
2023-06-02 00:46:55 +03:00
Ruslan Kuprieiev 6543580100 dvcfs: don't use datafs isout/isdvc 2023-05-31 23:14:28 +03:00
David de la Iglesia Castro a1b891ce01 deps: bumps dvc-azure>=2.21.2
Closes #9506 via https://github.com/iterative/dvc-azure/pull/48
2023-05-31 14:22:27 +02:00
Dave Berenbaum 3a85326df3 add: rename jobs to remote-jobs (#9522) 2023-05-31 01:14:30 +03:00
skshetry 355ebc72eb add: remove --recursive, rename fname to file (#9513)
add: remove --recursive
2023-05-26 12:07:13 +05:45
daavoo 5a6f834088 repro: Skip pulling run cache if no-run-cache was passed. 2023-05-25 16:35:30 +02:00
daavoo e98bf38d4c repro: Continue on RunCacheNotSupported
Closes #9470
2023-05-25 16:35:30 +02:00
skshetry 192c9cc577 remove original PR from backport PR title (#9510) 2023-05-25 16:37:25 +05:45
skshetry 0e38525d8d cli: remove dvc run (#9508)
Note that `Repo.run()` API still exists, which will be removed in
successive PRs. A lot of tests depend on it that it'll take time
before removing it.
2023-05-25 16:27:14 +05:45
Dave Berenbaum 8acd943d52 exp list: show experiment shas (#9501)
* exp list: include sha; still needs remote support

* exp list: ignore rev for remotes

* exp list: update output format

* refactor
2023-05-25 15:05:12 +09:00
dependabot[bot] 79c88008f8 build(deps-dev): bump dvc-gs from 2.22.0 to 2.22.1
Bumps [dvc-gs](https://github.com/iterative/dvc-gs) from 2.22.0 to 2.22.1.
- [Release notes](https://github.com/iterative/dvc-gs/releases)
- [Commits](https://github.com/iterative/dvc-gs/compare/2.22.0...2.22.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
2023-05-24 13:53:00 +09:00
Peter Rowlands 9c9ca39fb4 exp: fix rebased imports 2023-05-23 15:50:58 +09:00
Peter Rowlands 89109fafcf exp: always log running experiment name at the start of a run 2023-05-23 15:43:50 +09:00
Peter Rowlands aa19e24670 exp: support switching to live workspace exp name 2023-05-23 15:43:50 +09:00
Peter Rowlands 629223cbc2 exp: drop deprecated get_running_exps()
- replaced with queue.collect_active()
2023-05-23 15:43:50 +09:00
Peter Rowlands 96449f157c plots diff: use exp switch_repo context instead of brancher 2023-05-23 15:43:50 +09:00
Peter Rowlands 8cb2646023 exp: add contextmanager for switching to active tempdir repo 2023-05-23 15:43:50 +09:00
David de la Iglesia Castro 25313f5d82 exp: Drop checkpoints (#9271)
exp: Drop `checkpoints`.

Closes #9221
2023-05-23 11:32:18 +05:45
David de la Iglesia Castro 182728d674 exp run / repro: Drop --metrics. (#9458)
* exp run / repro: Drop `--metrics`.

* exp run: Rename `-M` to `-m`.
2023-05-22 19:13:22 +05:45
David de la Iglesia Castro f245567613 exp: Drop gc. (#9273)
Closes #7991
2023-05-22 16:08:25 +05:45
skshetry 4163c8f57d set destination branch to PR title on backport (#9492) 2023-05-22 16:03:36 +05:45
skshetry 6532258e4a drop top-level plots definition as mapping support (#9486) 2023-05-22 15:08:22 +05:45
daavooandIvan Shcheklein eb45cda5d0 get_random_exp_name: Use custom random generator.
See https://github.com/iterative/dvclive/pull/576

Co-authored-by: Ivan Shcheklein <[email protected]>
2023-05-22 10:46:41 +02:00
skshetry abd4f4a2a8 drop Repo.stages (#9489)
Use repo.index.stages instead.
2023-05-22 12:51:40 +05:45
skshetry f41e535857 remove dvc.main.main alias (#9488)
You can import it using:
```python
from dvc.cli import main

main(...)
```
2023-05-22 12:46:46 +05:45
skshetry 1989c0fe98 stage add: introduce --run option (#9480)
* stage add: introduce --run option

Part of #5846.

* fix ui message
2023-05-22 12:27:20 +05:45
daavoo 3ddd4b87fc exp: Fix --rev args for ls pull and remove.
Closes #9471
2023-05-21 13:25:40 +02:00
skshetry e089ee0365 remove data ls and support for --meta/--desc/--label/--type cli flags (#9481) 2023-05-19 20:10:34 +05:45
David de la Iglesia Castro 02f7fcae73 exp show: Drop --pcp. (#9270)
Closes #9268
2023-05-19 15:36:33 +05:45
David de la Iglesia Castro cdba65f27e exp: Drop init. (#9269)
Drop `exp init`.

Closes #9267
2023-05-19 14:39:16 +05:45
skshetry 3959c51373 lockfile: drop 1.0 lockfile support (#9476) 2023-05-18 17:36:05 +05:45
skshetry 90aaf5a296 commands: remove --show-json/--show-md/--show-csv flags (#9475) 2023-05-18 16:36:14 +05:45
227 changed files with 4383 additions and 10224 deletions
-12
View File
@@ -1,12 +0,0 @@
/state
/lock
/config.local
/updater
/updater.lock
/state-journal
/state-wal
/cache
/pkg
/repos
/tmp
/experiments
-4
View File
@@ -1,4 +0,0 @@
['remote "dvc"']
url = https://remote.dvc.org/dvc
[core]
remote = dvc
+1
View File
@@ -7,3 +7,4 @@ pull_request_rules:
backport:
branches:
- "2.x"
title: ({{ destination_branch }}) {{ title }}
-60
View File
@@ -11,66 +11,6 @@ permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
binary:
permissions:
contents: write # for actions/upload-release-asset to upload release asset
strategy:
matrix:
include:
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
name: ${{ matrix.pkg }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: pip
cache-dependency-path: |
pyproject.toml
scripts/build-requirements.txt
- name: Set up Ruby 2.6
uses: ruby/setup-ruby@v1
if: matrix.pkg != 'exe'
with:
ruby-version: '2.6'
- name: Install fpm
if: matrix.pkg != 'exe'
run: gem install --no-document fpm
- name: Install deps
run: |
pip install --upgrade pip wheel setuptools
pip install .[all]
pip install -r scripts/build-requirements.txt
- name: Pull images
run: dvc pull
- name: Build ${{ matrix.pkg }}
run: python scripts/build.py ${{ matrix.pkg }}
- name: Publish ${{ matrix.pkg }}
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
asset_name: ${{ matrix.asset }}
asset_content_type: binary/octet-stream
pip:
runs-on: ubuntu-20.04
permissions:
+1
View File
@@ -4,6 +4,7 @@ DVC
Make your data science projects reproducible and shareable.
"""
import dvc.logger
from dvc.build import PKG # noqa: F401
from dvc.version import __version__, version_tuple # noqa: F401
dvc.logger.setup()
-6
View File
@@ -17,12 +17,6 @@ class Annotation:
labels: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
def update(self, **kwargs) -> "Annotation":
for attr, value in kwargs.items():
if value and hasattr(self, attr):
setattr(self, attr, value)
return self
def to_dict(self) -> Dict[str, str]:
return compact(asdict(self))
+1 -2
View File
@@ -2,7 +2,7 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import exp_save, exp_show, make_checkpoint
from .experiments import exp_save, exp_show
from .scm import all_branches, all_commits, all_tags
from .show import metrics_show, params_show
@@ -13,7 +13,6 @@ __all__ = [
"exp_save",
"exp_show",
"get_url",
"make_checkpoint",
"open",
"params_show",
"metrics_show",
+24 -5
View File
@@ -73,6 +73,7 @@ def open( # noqa, pylint: disable=redefined-builtin
remote: Optional[str] = None,
mode: str = "r",
encoding: Optional[str] = None,
config: Optional[Dict[str, Any]] = None,
):
"""
Opens a file tracked in a DVC project.
@@ -114,6 +115,8 @@ def open( # noqa, pylint: disable=redefined-builtin
Defaults to None.
This should only be used in text mode.
Mirrors the namesake parameter in builtin `open()`_.
config(dict, optional): config to be passed to the DVC repository.
Defaults to None.
Returns:
_OpenContextManager: A context manager that generatse a corresponding
@@ -209,14 +212,24 @@ def open( # noqa, pylint: disable=redefined-builtin
"rev": rev,
"mode": mode,
"encoding": encoding,
"config": config,
}
return _OpenContextManager(_open, args, kwargs)
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
repo_kwargs: Dict[str, Any] = {"subrepos": True, "uninitialized": True}
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None, config=None):
if remote:
repo_kwargs["config"] = {"core": {"remote": remote}}
if config is not None:
raise ValueError(
"can't specify both `remote` and `config` at the same time"
)
config = {"core": {"remote": remote}}
repo_kwargs: Dict[str, Any] = {
"subrepos": True,
"uninitialized": True,
"config": config,
}
with Repo.open(repo, rev=rev, **repo_kwargs) as _repo:
with _wrap_exceptions(_repo, path):
@@ -251,13 +264,19 @@ def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None, config=None):
"""
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
is used unless a rev argument is supplied. The default remote is tried
unless a remote argument is supplied.
"""
with open(
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
path,
repo=repo,
rev=rev,
remote=remote,
mode=mode,
encoding=encoding,
config=config,
) as fd:
return fd.read()
+4 -32
View File
@@ -1,40 +1,9 @@
import builtins
import os
from time import sleep
from typing import Dict, List, Optional, Union
from rich.text import Text
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
from dvc.repo import Repo
from dvc.repo.experiments.show import tabulate
from dvc.stage.monitor import CheckpointTask
def make_checkpoint():
"""
Signal DVC to create a checkpoint experiment.
If the current process is being run from DVC, this function will block
until DVC has finished creating the checkpoint. Otherwise, this function
will return immediately.
"""
if os.getenv(DVC_CHECKPOINT) is None:
return
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
signal_file = os.path.join(
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
)
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
# NOTE: force flushing/writing empty file to disk, otherwise when
# run in certain contexts (pytest) file may not actually be written
fobj.write("")
fobj.flush()
os.fsync(fobj.fileno())
while os.path.exists(signal_file):
sleep(0.1)
def exp_save(
@@ -96,6 +65,7 @@ def exp_show(
num: int = 1,
param_deps: bool = False,
force: bool = False,
config: Optional[Dict] = None,
) -> List[Dict]:
"""Get DVC experiments tracked in `repo`.
@@ -130,13 +100,15 @@ def exp_show(
When `force` is specified, DVC will reload all experiment data and
ignore any previously cached results.
Defaults to `False`.
config (dict, optional): config to be passed through to DVC project.
Defaults to `None`.
Returns:
List[Dict]: Each item in the list will contain a dictionary with
the info for an individual experiment.
See Examples below.
"""
with Repo.open(repo) as _repo:
with Repo.open(repo, config=config) as _repo:
experiments = _repo.experiments.show(
revs=revs,
num=num,
+8 -2
View File
@@ -41,6 +41,7 @@ def metrics_show(
*targets: str,
repo: Optional[str] = None,
rev: Optional[str] = None,
config: Optional[Dict] = None,
) -> Dict:
"""Get metrics tracked in `repo`.
@@ -69,6 +70,8 @@ def metrics_show(
hash or a dvc experiment name.
If `repo` is not a Git repo, this option is ignored.
If `None`, the current working tree will be used.
config (dict, optional): config to be passed through to DVC project.
Defaults to `None`.
Returns:
Dict: See Examples below.
@@ -136,7 +139,7 @@ def metrics_show(
https://git-scm.com/docs/revisions
"""
with Repo.open(repo) as _repo:
with Repo.open(repo, config=config) as _repo:
metrics = _repo.metrics.show(
targets=targets,
revs=rev if rev is None else [rev],
@@ -157,6 +160,7 @@ def params_show(
stages: Optional[Union[str, Iterable[str]]] = None,
rev: Optional[str] = None,
deps: bool = False,
config: Optional[Dict] = None,
) -> Dict:
"""Get parameters tracked in `repo`.
@@ -195,6 +199,8 @@ def params_show(
deps (bool, optional): Whether to retrieve only parameters that are
stage dependencies or not.
Defaults to `False`.
config (dict, optional): config to be passed through to DVC project.
Defaults to `None`.
Returns:
Dict: See Examples below.
@@ -379,7 +385,7 @@ def params_show(
if isinstance(stages, str):
stages = [stages]
with Repo.open(repo) as _repo:
with Repo.open(repo, config=config) as _repo:
params = _repo.params.show(
revs=rev if rev is None else [rev],
targets=targets,
+10
View File
@@ -0,0 +1,10 @@
from typing import Optional
try:
# file is created during dvc build
# pylint:disable=unused-import
from . import _build # type: ignore[attr-defined, import]
PKG: Optional[str] = _build.PKG # type: ignore[assignment]
except ImportError:
PKG = None # type: ignore[assignment]
+75 -14
View File
@@ -1,10 +1,24 @@
import os
from typing import TYPE_CHECKING, Optional, Tuple
from dvc.fs import GitFileSystem, Schemes
from dvc_data.hashfile.db import get_odb
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
if TYPE_CHECKING:
from dvc.repo import Repo
LEGACY_HASH_NAMES = {"md5-dos2unix", "params"}
def _get_odb(repo, settings, fs=None):
def _get_odb(
repo,
settings,
fs=None,
prefix: Optional[Tuple[str, ...]] = None,
hash_name: Optional[str] = None,
**kwargs,
):
from dvc.fs import get_cloud_fs
if not settings:
@@ -12,18 +26,16 @@ def _get_odb(repo, settings, fs=None):
cls, config, fs_path = get_cloud_fs(repo, **settings)
fs = fs or cls(**config)
if prefix:
fs_path = fs.path.join(fs_path, *prefix)
if hash_name:
config["hash_name"] = hash_name
return get_odb(fs, fs_path, state=repo.state, **config)
class CacheManager:
CACHE_DIR = "cache"
CLOUD_SCHEMES = [
Schemes.S3,
Schemes.GS,
Schemes.SSH,
Schemes.HDFS,
Schemes.WEBHDFS,
]
FILES_DIR = "files"
def __init__(self, repo):
self._repo = repo
@@ -53,25 +65,74 @@ class CacheManager:
if not isinstance(repo.fs, GitFileSystem):
kwargs["fs"] = repo.fs
odb = _get_odb(repo, settings, **kwargs)
odb = _get_odb(
repo,
settings,
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
**kwargs,
)
self._odb["repo"] = odb
self._odb[Schemes.LOCAL] = odb
legacy_odb = _get_odb(repo, settings, hash_name="md5-dos2unix", **kwargs)
self._odb["legacy"] = legacy_odb
def _init_odb(self, schemes):
for scheme in schemes:
remote = self.config.get(scheme)
settings = {"name": remote} if remote else None
self._odb[scheme] = _get_odb(self._repo, settings)
self._odb[scheme] = _get_odb(
self._repo,
settings,
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
)
def __getattr__(self, name):
if name not in self._odb and name in self.CLOUD_SCHEMES:
self._init_odb([name])
try:
return self._odb[name]
except KeyError as exc:
raise AttributeError from exc
def by_scheme(self):
self._init_odb(self.CLOUD_SCHEMES)
yield from self._odb.items()
@property
def local_cache_dir(self) -> str:
"""Return base local cache directory without any prefixes.
(i.e. `dvc cache dir`).
"""
return self.legacy.path
def migrate_2_to_3(repo: "Repo", dry: bool = False):
"""Migrate legacy 2.x objects to 3.x cache.
Legacy 'md5-dos2unix' objects will be re-hashed with 'md5', added to 3.x cache,
and then a link from the legacy 2.x location to the 3.x location will be created.
"""
from dvc.fs.callbacks import TqdmCallback
from dvc.ui import ui
from dvc_data.hashfile.db.migrate import migrate, prepare
src = repo.cache.legacy
dest = repo.cache.local
if dry:
oids = list(src._list_oids()) # pylint: disable=protected-access
ui.write(
f"{len(oids)} files will be re-hashed and migrated to the DVC 3.0 cache "
"location."
)
return
with TqdmCallback(
desc="Computing DVC 3.0 hashes",
unit="files",
) as cb:
migration = prepare(src, dest, callback=cb)
with TqdmCallback(
desc="Migrating to DVC 3.0 cache",
unit="files",
) as cb:
count = migrate(migration, callback=cb)
ui.write(f"Migrated {count} files to DVC 3.0 cache location.")
+4 -5
View File
@@ -75,7 +75,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
if isinstance(exc, RemoteMissingDepsError):
from dvc.utils.pkg import PKG
from dvc import PKG
proto = exc.protocol
by_pkg = {
@@ -83,9 +83,9 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
"conda": f"conda install -c conda-forge dvc-{proto}",
}
cmd = by_pkg.get(PKG)
if cmd:
if PKG in by_pkg:
link = format_link("https://dvc.org/doc/install")
cmd = by_pkg.get(PKG)
hint = (
"To install dvc with those dependencies, run:\n"
"\n"
@@ -190,8 +190,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
if level and level <= logging.DEBUG:
from platform import platform, python_implementation, python_version
from dvc import __version__
from dvc.utils.pkg import PKG
from dvc import PKG, __version__
pyv = " ".join([python_implementation(), python_version()])
pkg = f" ({PKG})" if PKG else ""
-20
View File
@@ -1,20 +0,0 @@
from argparse import _AppendAction
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
from funcy import ldistinct
items = getattr(namespace, self.dest) or []
items.extend(map(str.strip, values.split(",")))
setattr(namespace, self.dest, ldistinct(items))
class KeyValueArgs(_AppendAction):
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
items = getattr(namespace, self.dest) or {}
for value in filter(bool, values):
key, _, value = value.partition("=")
items[key.strip()] = value
setattr(namespace, self.dest, items)
-2
View File
@@ -41,7 +41,6 @@ from dvc.commands import (
remove,
repro,
root,
run,
stage,
unprotect,
update,
@@ -62,7 +61,6 @@ COMMANDS = [
remove,
move,
unprotect,
run,
repro,
data_sync,
gc,
+33 -64
View File
@@ -2,66 +2,56 @@ import argparse
import logging
from dvc.cli import completion
from dvc.cli.actions import KeyValueArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
logger = logging.getLogger(__name__)
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help="User description of the data.",
)
parser.add_argument(
"--meta",
metavar="key=value",
nargs=1,
action=KeyValueArgs,
help="Custom metadata to add to the data.",
)
parser.add_argument(
"--label",
dest="labels",
type=str,
action="append",
metavar="<str>",
help="Label for the data.",
)
parser.add_argument(
"--type",
type=str,
metavar="<str>",
help="Type of the data.",
)
class CmdAdd(CmdBase):
def validate_args(self) -> None:
from dvc.exceptions import InvalidArgumentError
args = self.args
invalid_opt = None
if args.to_remote or args.out:
message = "{option} can't be used with "
message += "--to-remote" if args.to_remote else "--out"
if len(args.targets) != 1:
invalid_opt = "multiple targets"
elif args.glob:
invalid_opt = "--glob option"
elif args.no_commit:
invalid_opt = "--no-commit option"
else:
message = "{option} can't be used without --to-remote"
if args.remote:
invalid_opt = "--remote"
elif args.remote_jobs:
invalid_opt = "--remote-jobs"
if invalid_opt is not None:
raise InvalidArgumentError(message.format(option=invalid_opt))
def run(self):
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
from dvc.exceptions import DvcException, InvalidArgumentError
try:
if len(self.args.targets) > 1 and self.args.file:
raise RecursiveAddingWhileUsingFilename()
self.validate_args()
except InvalidArgumentError:
logger.exception("")
return 1
try:
self.repo.add(
self.args.targets,
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
external=self.args.external,
glob=self.args.glob,
desc=self.args.desc,
out=self.args.out,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
remote=self.args.remote,
to_remote=self.args.to_remote,
jobs=self.args.jobs,
remote_jobs=self.args.remote_jobs,
force=self.args.force,
)
except FileNotFoundError:
@@ -83,36 +73,18 @@ def add_parser(subparsers, parent_parser):
help=ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively add files under directory targets.",
)
parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
parser.add_argument(
"--external",
action="store_true",
default=False,
help="Allow targets that are outside of the DVC repository.",
)
parser.add_argument(
"--glob",
action="store_true",
default=False,
help="Allows targets containing shell-style wildcards.",
)
parser.add_argument(
"--file",
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
)
parser.add_argument(
"-o",
"--out",
@@ -132,8 +104,7 @@ def add_parser(subparsers, parent_parser):
metavar="<name>",
)
parser.add_argument(
"-j",
"--jobs",
"--remote-jobs",
type=int,
help=(
"Only used along with '--to-remote'. "
@@ -150,8 +121,6 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Override local file or folder if exists.",
)
_add_annotating_args(parser)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
).complete = completion.FILE
+29 -2
View File
@@ -2,6 +2,7 @@ import argparse
import os
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.commands.config import CmdConfig
from dvc.ui import ui
@@ -36,6 +37,14 @@ class CmdCacheDir(CmdConfig):
return 0
class CmdCacheMigrate(CmdBase):
def run(self):
from dvc.cachemgr import migrate_2_to_3
migrate_2_to_3(self.repo, dry=self.args.dry)
return 0
def add_parser(subparsers, parent_parser):
from dvc.commands.config import parent_config_parser
@@ -54,8 +63,6 @@ def add_parser(subparsers, parent_parser):
help="Use `dvc cache CMD --help` for command-specific help.",
)
fix_subparsers(cache_subparsers)
parent_cache_config_parser = argparse.ArgumentParser(
add_help=False, parents=[parent_config_parser]
)
@@ -86,3 +93,23 @@ def add_parser(subparsers, parent_parser):
nargs="?",
).complete = completion.DIR
cache_dir_parser.set_defaults(func=CmdCacheDir)
CACHE_MIGRATE_HELP = "Migrate cached files to the DVC 3.0 cache location."
cache_migrate_parser = cache_subparsers.add_parser(
"migrate",
parents=[parent_parser],
description=append_doc_link(CACHE_HELP, "cache/migrate"),
help=CACHE_MIGRATE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_migrate_parser.add_argument(
"--dry",
help=(
"Only print actions which would be taken without actually migrating "
"any data."
),
action="store_true",
)
cache_migrate_parser.set_defaults(func=CmdCacheMigrate)
fix_subparsers(cache_subparsers)
-1
View File
@@ -176,7 +176,6 @@ def add_parser(subparsers, parent_parser):
)
dag_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
+2 -117
View File
@@ -1,14 +1,11 @@
import argparse
import logging
from operator import itemgetter
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
from typing import TYPE_CHECKING
from funcy import chunks, compact, log_durations
from dvc.cli import completion
from dvc.cli.actions import CommaSeparatedArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_help
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.ui import ui
from dvc.utils import colorize
@@ -132,63 +129,6 @@ class CmdDataStatus(CmdBase):
return self._show_status(status)
class CmdDataLs(CmdBase):
@staticmethod
def _show_table(
d: Iterable[Dict[str, Any]],
filter_types: Set[str],
filter_labels: Set[str],
markdown: bool = False,
) -> None:
from rich.style import Style
from dvc.compare import TabularData
td = TabularData(
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
)
for entry in sorted(d, key=itemgetter("path")):
typ = entry.get("type", "")
desc = entry.get("desc", "-")
labels = entry.get("labels", [])
if filter_types and typ not in filter_types:
continue
if filter_labels and filter_labels.isdisjoint(labels):
continue
rich_label = ui.rich_text()
for index, label in enumerate(labels):
if index:
rich_label.append(",")
style = Style(bold=label in filter_labels, color="green")
rich_label.append(label, style=style)
if markdown and desc:
desc = desc.partition("\n")[0]
path = ui.rich_text(entry["path"], style="cyan")
type_style = Style(bold=typ in filter_types, color="yellow")
typ = ui.rich_text(entry.get("type", ""), style=type_style)
td.append([path, typ or "-", rich_label or "-", desc])
td.render(markdown=markdown, rich_table=True)
def run(self):
from dvc.repo.data import ls
filter_labels = set(self.args.labels)
filter_types = set(self.args.type)
d = ls(self.repo, targets=self.args.targets, recursive=self.args.recursive)
self._show_table(
d,
filter_labels=filter_labels,
filter_types=filter_types,
markdown=self.args.markdown,
)
return 0
def add_parser(subparsers, parent_parser):
data_parser = subparsers.add_parser(
"data",
@@ -217,13 +157,6 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Show output in JSON format.",
)
data_status_parser.add_argument(
"--show-json",
action="store_true",
default=False,
dest="json",
help=argparse.SUPPRESS,
)
data_status_parser.add_argument(
"--granular",
action="store_true",
@@ -257,51 +190,3 @@ def add_parser(subparsers, parent_parser):
help="Use cached remote index (don't check remote).",
)
data_status_parser.set_defaults(func=CmdDataStatus)
DATA_LS_HELP = "List data tracked by DVC with its metadata."
data_ls_parser = data_subparsers.add_parser(
"ls",
aliases=["list"],
parents=[parent_parser],
description=append_doc_link(DATA_LS_HELP, "data/ls"),
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
data_ls_parser.add_argument(
"--md",
"--show-md",
dest="markdown",
action="store_true",
default=False,
help="Show tabulated output in the Markdown format (GFM).",
)
data_ls_parser.add_argument(
"--type",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of type to filter.",
)
data_ls_parser.add_argument(
"--labels",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of labels to filter.",
)
data_ls_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively list from the specified directories.",
)
data_ls_parser.add_argument(
"targets",
default=None,
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
),
).complete = completion.DVCFILES_AND_STAGE
data_ls_parser.set_defaults(func=CmdDataLs)
hide_subparsers_from_help(data_subparsers)
-1
View File
@@ -403,7 +403,6 @@ def add_parser(subparsers, _parent_parser):
)
status_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show status in JSON format.",
-2
View File
@@ -184,7 +184,6 @@ def add_parser(subparsers, parent_parser):
)
diff_parser.add_argument(
"--json",
"--show-json",
help="Format the output into a JSON",
action="store_true",
default=False,
@@ -197,7 +196,6 @@ def add_parser(subparsers, parent_parser):
)
diff_parser.add_argument(
"--md",
"--show-md",
help="Show tabulated output in the Markdown format (GFM).",
action="store_true",
dest="markdown",
-4
View File
@@ -7,8 +7,6 @@ from dvc.commands.experiments import (
clean,
diff,
exec_run,
gc,
init,
ls,
pull,
push,
@@ -25,8 +23,6 @@ SUB_COMMANDS = [
clean,
diff,
exec_run,
gc,
init,
ls,
pull,
push,
-2
View File
@@ -83,14 +83,12 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
experiments_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
-146
View File
@@ -1,146 +0,0 @@
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__)
def _raise_error_if_all_disabled(**kwargs):
if not any(kwargs.values()):
raise InvalidArgumentError(
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags`,"
" `--all-commits` or `--date` needs to be set."
)
class CmdExperimentsGC(CmdBase):
def run(self):
_raise_error_if_all_disabled(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
commit_date=self.args.commit_date,
workspace=self.args.workspace,
)
msg = "This will remove all experiments except those derived from "
msg += "the workspace"
if self.args.all_commits:
msg += " and all git commits"
else:
if self.args.all_branches and self.args.all_tags:
msg += " and all git branches and tags"
elif self.args.all_branches:
msg += " and all git branches"
elif self.args.all_tags:
msg += " and all git tags"
if self.args.commit_date:
msg += f" and all git commits before date {self.args.commit_date}"
msg += " of the current repo."
if self.args.queued:
msg += " Run queued experiments will be preserved."
else:
msg += " Run queued experiments will be removed."
logger.warning(msg)
msg = "Are you sure you want to proceed?"
if not self.args.force and not ui.confirm(msg):
return 1
removed = self.repo.experiments.gc(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
workspace=self.args.workspace,
commit_date=self.args.commit_date,
queued=self.args.queued,
)
if removed:
ui.write(
f"Removed {removed} experiments.",
"To remove unused cache files",
"use 'dvc gc'.",
)
else:
ui.write("No experiments to remove.")
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
EXPERIMENTS_GC_DESCRIPTION = (
"Removes all experiments which are not derived"
" from the specified Git revisions."
)
experiments_gc_parser = experiments_subparsers.add_parser(
"gc",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_GC_DESCRIPTION, "exp/gc"),
help=EXPERIMENTS_GC_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_gc_parser.add_argument(
"-w",
"--workspace",
action="store_true",
default=False,
help="Keep experiments derived from the current workspace.",
)
experiments_gc_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Keep experiments derived from the tips of all Git branches.",
)
experiments_gc_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Keep experiments derived from all Git tags.",
)
experiments_gc_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
help="Keep experiments derived from all Git commits.",
)
experiments_gc_parser.add_argument(
"--date",
type=str,
dest="commit_date",
metavar="<YYYY-MM-DD>",
default=None,
help=(
"Keep experiments from the commits after (inclusive) a certain "
"date. Date must match the extended ISO 8601 format (YYYY-MM-DD)."
),
)
experiments_gc_parser.add_argument(
"--queued",
action="store_true",
default=False,
help=(
"Keep queued experiments (experiments run queue will be cleared "
"by default)."
),
)
experiments_gc_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force garbage collection - automatically agree to all prompts.",
)
experiments_gc_parser.set_defaults(func=CmdExperimentsGC)
-219
View File
@@ -1,219 +0,0 @@
import argparse
import logging
from typing import TYPE_CHECKING, List
from funcy import compact
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.ui import ui
if TYPE_CHECKING:
from dvc.dependency import Dependency
from dvc.stage import PipelineStage
logger = logging.getLogger(__name__)
class CmdExperimentsInit(CmdBase):
DEFAULT_NAME = "train"
CODE = "src"
DATA = "data"
MODELS = "models"
DEFAULT_METRICS = "metrics.json"
DEFAULT_PARAMS = "params.yaml"
PLOTS = "plots"
DEFAULTS = {
"code": CODE,
"data": DATA,
"models": MODELS,
"metrics": DEFAULT_METRICS,
"params": DEFAULT_PARAMS,
"plots": PLOTS,
}
def run(self):
from dvc.commands.stage import parse_cmd
cmd = parse_cmd(self.args.command)
if not self.args.interactive and not cmd:
raise InvalidArgumentError("command is not specified")
from dvc.repo.experiments.init import init
defaults = {}
if not self.args.explicit:
config = self.repo.config["exp"]
defaults.update({**self.DEFAULTS, **config})
cli_args = compact(
{
"cmd": cmd,
"code": self.args.code,
"data": self.args.data,
"models": self.args.models,
"metrics": self.args.metrics,
"params": self.args.params,
"plots": self.args.plots,
"live": self.args.live,
}
)
initialized_stage, initialized_deps, initialized_out_dirs = init(
self.repo,
name=self.args.name,
type=self.args.type,
defaults=defaults,
overrides=cli_args,
interactive=self.args.interactive,
force=self.args.force,
)
self._post_init_display(
initialized_stage, initialized_deps, initialized_out_dirs
)
if self.args.run:
self.repo.experiments.run(targets=[initialized_stage.addressing])
return 0
def _post_init_display(
self,
stage: "PipelineStage",
new_deps: List["Dependency"],
new_out_dirs: List[str],
) -> None:
from dvc.utils import humanize
path_fmt = "[green]{}[/green]".format
if new_deps:
deps_paths = humanize.join(map(path_fmt, new_deps))
ui.write(f"Creating dependencies: {deps_paths}", styled=True)
if new_out_dirs:
out_dirs_paths = humanize.join(map(path_fmt, new_out_dirs))
ui.write(f"Creating output directories: {out_dirs_paths}", styled=True)
ui.write(
f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
styled=True,
)
if stage.outs or not self.args.run:
# separate the above status-like messages with help/tips section
ui.write(styled=True)
if stage.outs:
outs_paths = humanize.join(map(path_fmt, stage.outs))
tips = f"Ensure your experiment command creates {outs_paths}."
ui.write(tips, styled=True)
if not self.args.run:
ui.write(
'You can now run your experiment using [b]"dvc exp run"[/].',
styled=True,
)
else:
# separate between `exp.run` output and `dvc exp init` output
ui.write(styled=True)
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_INIT_HELP = "Quickly setup any project to use experiments."
experiments_init_parser = experiments_subparsers.add_parser(
"init",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
formatter_class=argparse.RawDescriptionHelpFormatter,
help=EXPERIMENTS_INIT_HELP,
)
experiments_init_parser.add_argument(
"command",
nargs=argparse.REMAINDER,
help="Command to execute.",
metavar="command",
)
experiments_init_parser.add_argument(
"--run",
action="store_true",
help="Run the experiment after initializing it",
)
experiments_init_parser.add_argument(
"--interactive",
"-i",
action="store_true",
help="Prompt for values that are not provided",
)
experiments_init_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Overwrite existing stage",
)
experiments_init_parser.add_argument(
"--explicit",
action="store_true",
default=False,
help="Only use the path values explicitly provided",
)
experiments_init_parser.add_argument(
"--name",
"-n",
help="Name of the stage to create (default: %(default)s)",
default=CmdExperimentsInit.DEFAULT_NAME,
)
experiments_init_parser.add_argument(
"--code",
help=(
"Path to the source file or directory "
"which your experiments depend"
f" (default: {CmdExperimentsInit.CODE})"
),
)
experiments_init_parser.add_argument(
"--data",
help=(
"Path to the data file or directory "
"which your experiments depend"
f" (default: {CmdExperimentsInit.DATA})"
),
)
experiments_init_parser.add_argument(
"--models",
help=(
"Path to the model file or directory for your experiments"
f" (default: {CmdExperimentsInit.MODELS})"
),
)
experiments_init_parser.add_argument(
"--params",
help=(
"Path to the parameters file for your experiments"
f" (default: {CmdExperimentsInit.DEFAULT_PARAMS})"
),
)
experiments_init_parser.add_argument(
"--metrics",
help=(
"Path to the metrics file for your experiments"
f" (default: {CmdExperimentsInit.DEFAULT_METRICS})"
),
)
experiments_init_parser.add_argument(
"--plots",
help=(
"Path to the plots file or directory for your experiments"
f" (default: {CmdExperimentsInit.PLOTS})"
),
)
experiments_init_parser.add_argument(
"--live",
help="Path to log dvclive outputs for your experiments",
)
experiments_init_parser.add_argument(
"--type",
choices=["default", "checkpoint"],
default="default",
help="Select type of stage to create (default: %(default)s)",
)
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
+33 -8
View File
@@ -3,6 +3,7 @@ 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__)
@@ -11,19 +12,36 @@ logger = logging.getLogger(__name__)
class CmdExperimentsList(CmdBase):
def run(self):
name_only = self.args.name_only
sha_only = self.args.sha_only
git_remote = self.args.git_remote
if sha_only and git_remote:
raise InvalidArgumentError("--sha-only not supported with git_remote.")
exps = self.repo.experiments.ls(
all_commits=self.args.all_commits,
rev=self.args.rev,
num=self.args.num,
git_remote=self.args.git_remote,
git_remote=git_remote,
)
for baseline in exps:
if not name_only:
ui.write(f"{baseline}:")
for exp_name in exps[baseline]:
indent = "" if name_only else "\t"
ui.write(f"{indent}{exp_name}")
if not (name_only or sha_only):
tag_base = "refs/tags/"
branch_base = "refs/heads/"
name = baseline[:7]
if baseline.startswith(tag_base):
name = baseline[len(tag_base) :]
elif baseline.startswith(branch_base):
name = baseline[len(branch_base) :]
ui.write(f"{name}:")
for exp_name, rev in exps[baseline]:
if name_only:
ui.write(exp_name)
elif sha_only:
ui.write(rev)
elif rev:
ui.write(f"\t{rev[:7]} [{exp_name}]")
else:
ui.write(f"\t{exp_name}")
return 0
@@ -40,11 +58,18 @@ def add_parser(experiments_subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_rev_selection_flags(experiments_list_parser, "List")
experiments_list_parser.add_argument(
display_group = experiments_list_parser.add_mutually_exclusive_group()
display_group.add_argument(
"--name-only",
"--names-only",
action="store_true",
help="Only output experiment names (without parent commits).",
help="Only output experiment names (without SHAs or parent commits).",
)
display_group.add_argument(
"--sha-only",
"--shas-only",
action="store_true",
help="Only output experiment commit SHAs (without names or parent commits).",
)
experiments_list_parser.add_argument(
"git_remote",
+1 -11
View File
@@ -3,23 +3,13 @@ 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 CmdExperimentsPull(CmdBase):
def raise_error_if_all_disabled(self):
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
raise InvalidArgumentError(
"Either provide an `experiment` argument, or use the "
"`--rev` or `--all-commits` flag."
)
def run(self):
self.raise_error_if_all_disabled()
pulled_exps = self.repo.experiments.pull(
self.args.git_remote,
self.args.experiment,
@@ -61,7 +51,7 @@ def add_parser(experiments_subparsers, parent_parser):
help=EXPERIMENTS_PULL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_rev_selection_flags(experiments_pull_parser, "Pull", False)
add_rev_selection_flags(experiments_pull_parser, "Pull", True)
experiments_pull_parser.add_argument(
"-f",
"--force",
+1 -11
View File
@@ -5,20 +5,12 @@ from typing import Any, Dict
from dvc.cli import completion
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 CmdExperimentsPush(CmdBase):
def raise_error_if_all_disabled(self):
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
raise InvalidArgumentError(
"Either provide an `experiment` argument, or use the "
"`--rev` or `--all-commits` flag."
)
@staticmethod
def log_result(result: Dict[str, Any], remote: str):
from dvc.utils import humanize
@@ -59,8 +51,6 @@ class CmdExperimentsPush(CmdBase):
def run(self):
from dvc.repo.experiments.push import UploadError
self.raise_error_if_all_disabled()
try:
result = self.repo.experiments.push(
self.args.git_remote,
@@ -102,7 +92,7 @@ def add_parser(experiments_subparsers, parent_parser):
help=EXPERIMENTS_PUSH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_rev_selection_flags(experiments_push_parser, "Push", False)
add_rev_selection_flags(experiments_push_parser, "Push", True)
experiments_push_parser.add_argument(
"-f",
"--force",
+2 -41
View File
@@ -1,39 +1,21 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.utils import append_doc_link
from dvc.commands.repro import CmdRepro
from dvc.commands.repro import add_arguments as add_repro_arguments
from dvc.exceptions import InvalidArgumentError
from dvc.ui import ui
logger = logging.getLogger(__name__)
class CmdExperimentsRun(CmdRepro):
def run(self):
from dvc.compare import show_metrics
if self.args.checkpoint_resume:
if self.args.reset:
raise InvalidArgumentError("--reset and --rev are mutually exclusive.")
if not (self.args.queue or self.args.tmp_dir):
raise InvalidArgumentError(
"--rev can only be used in conjunction with --queue or --temp."
)
if self.args.reset:
ui.write("Any existing checkpoints will be reset and re-run.")
results = self.repo.experiments.run(
self.repo.experiments.run(
name=self.args.name,
queue=self.args.queue,
run_all=self.args.run_all,
jobs=self.args.jobs,
params=self.args.set_param,
checkpoint_resume=self.args.checkpoint_resume,
reset=self.args.reset,
tmp_dir=self.args.tmp_dir,
machine=self.args.machine,
copy_paths=self.args.copy_paths,
@@ -41,11 +23,6 @@ class CmdExperimentsRun(CmdRepro):
**self._common_kwargs,
)
if self.args.metrics and results:
metrics = self.repo.metrics.show(revs=list(results))
metrics.pop("workspace", None)
show_metrics(metrics)
return 0
@@ -59,22 +36,6 @@ def add_parser(experiments_subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
_add_run_common(experiments_run_parser)
experiments_run_parser.add_argument(
"-r",
"--rev",
type=str,
dest="checkpoint_resume",
help=(
"Continue the specified checkpoint experiment. Can only be used "
"in conjunction with --queue or --temp."
),
metavar="<experiment_rev>",
).complete = completion.EXPERIMENT
experiments_run_parser.add_argument(
"--reset",
action="store_true",
help="Reset existing checkpoints and restart the experiment.",
)
experiments_run_parser.set_defaults(func=CmdExperimentsRun)
@@ -149,7 +110,7 @@ def _add_run_common(parser):
),
)
parser.add_argument(
"-M",
"-m",
"--message",
type=str,
default=None,
-1
View File
@@ -53,7 +53,6 @@ def add_parser(experiments_subparsers, parent_parser):
)
save_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
+11 -72
View File
@@ -1,13 +1,11 @@
import argparse
import logging
import os
import re
from datetime import date, datetime
from typing import TYPE_CHECKING, Dict, Iterable
from funcy import lmap
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.metrics import DEFAULT_PRECISION
@@ -27,9 +25,6 @@ logger = logging.getLogger(__name__)
experiment_types = {
"checkpoint_tip": "│ ╓",
"checkpoint_commit": "│ ╟",
"checkpoint_base": "├─╨",
"branch_commit": "├──",
"branch_base": "└──",
"baseline": "",
@@ -59,7 +54,7 @@ def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments( # noqa: C901, PLR0912
def show_experiments( # noqa: C901
td: "TabularData",
headers: Dict[str, Iterable[str]],
keep=None,
@@ -67,7 +62,6 @@ def show_experiments( # noqa: C901, PLR0912
pager=True,
csv=False,
markdown=False,
pcp=False,
**kwargs,
):
if keep:
@@ -110,51 +104,23 @@ def show_experiments( # noqa: C901, PLR0912
}
)
if kwargs.get("only_changed", False) or pcp:
if kwargs.get("only_changed", False):
td.drop_duplicates("cols", ignore_empty=False)
cols_to_drop = set()
if drop is not None:
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
if pcp:
cols_to_drop.add("Created")
td.drop(*cols_to_drop)
if pcp:
subset = {x for x in td.keys() if x != "Experiment"} # noqa: SIM118
td.dropna(
"rows",
how="all",
subset=subset,
)
td.drop_duplicates("rows", subset=subset)
if "Experiment" in td:
td.column("Experiment")[:] = [
# remove tree characters
str(x).encode("ascii", "ignore").strip().decode()
for x in td.column("Experiment")
]
out = kwargs.get("out") or "dvc_plots"
output_file = os.path.join(out, "index.html")
ui.write(
td.to_parallel_coordinates(
output_path=os.path.abspath(output_file),
color_by=kwargs.get("sort_by") or "Experiment",
)
)
if kwargs.get("open"):
return ui.open_browser(output_file)
else:
td.render(
pager=pager,
borders="horizontals",
rich_table=True,
header_styles=styles,
row_styles=row_styles,
csv=csv,
markdown=markdown,
)
td.render(
pager=pager,
borders="horizontals",
rich_table=True,
header_styles=styles,
row_styles=row_styles,
csv=csv,
markdown=markdown,
)
def _normalize_headers(names, count):
@@ -221,9 +187,6 @@ class CmdExperimentsShow(CmdBase):
csv=self.args.csv,
markdown=self.args.markdown,
only_changed=self.args.only_changed,
pcp=self.args.pcp,
out=self.args.out,
open=self.args.open,
)
return 0
@@ -316,21 +279,18 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_show_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Print output in JSON format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--csv",
"--show-csv",
action="store_true",
default=False,
help="Print output in csv format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
@@ -345,27 +305,6 @@ def add_parser(experiments_subparsers, parent_parser):
),
metavar="<n>",
)
experiments_show_parser.add_argument(
"--parallel-coordinates-plot",
"--pcp",
dest="pcp",
action="store_true",
default=False,
help="Generate a Parallel Coordinates Plot from the tabulated output.",
)
experiments_show_parser.add_argument(
"-o",
"--out",
default=None,
help="Destination folder to save the Parallel Coordinates Plot to",
metavar="<path>",
).complete = completion.DIR
experiments_show_parser.add_argument(
"--open",
action="store_true",
default=False,
help="Open the Parallel Coordinates Plot directly in the browser.",
)
experiments_show_parser.add_argument(
"--no-fetch",
dest="fetch_running",
-14
View File
@@ -18,14 +18,9 @@ class CmdImport(CmdBase):
self.args.url,
self.args.path,
out=self.args.out,
fname=self.args.file,
rev=self.args.rev,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
)
except CloneError:
@@ -42,8 +37,6 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = (
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
@@ -75,11 +68,6 @@ def add_parser(subparsers, parent_parser):
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
import_parser.add_argument(
"--file",
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
)
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
@@ -106,6 +94,4 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImport)
-13
View File
@@ -15,15 +15,10 @@ class CmdImportUrl(CmdBase):
self.repo.imp_url(
self.args.url,
out=self.args.out,
fname=self.args.file,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
remote=self.args.remote,
to_remote=self.args.to_remote,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
force=self.args.force,
version_aware=self.args.version_aware,
@@ -41,8 +36,6 @@ class CmdImportUrl(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = "Download or copy file from URL and take it under DVC control."
import_parser = subparsers.add_parser(
@@ -70,11 +63,6 @@ def add_parser(subparsers, parent_parser):
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to."
).complete = completion.DIR
import_parser.add_argument(
"--file",
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
).complete = completion.DIR
import_parser.add_argument(
"--to-remote",
action="store_true",
@@ -126,5 +114,4 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Import using cloud versioning. Implied if the URL contains a version ID.",
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImportUrl)
-1
View File
@@ -71,7 +71,6 @@ def add_parser(subparsers, parent_parser):
)
list_parser.add_argument(
"--json",
"--show-json",
action="store_true",
help="Show output in JSON format.",
)
-4
View File
@@ -140,14 +140,12 @@ def add_parser(subparsers, parent_parser):
)
metrics_show_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_show_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
@@ -223,14 +221,12 @@ def add_parser(subparsers, parent_parser):
)
metrics_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
-2
View File
@@ -105,14 +105,12 @@ def add_parser(subparsers, parent_parser):
)
params_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
params_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
-1
View File
@@ -430,7 +430,6 @@ def _add_ui_arguments(parser):
)
parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help=argparse.SUPPRESS,
-13
View File
@@ -16,12 +16,6 @@ class CmdRepro(CmdBase):
else:
ui.write("Use `dvc push` to send your updates to remote storage.")
if self.args.metrics:
from dvc.compare import show_metrics
metrics = self.repo.metrics.show()
show_metrics(metrics)
return 0
@property
@@ -105,13 +99,6 @@ and then the stage name name.
default=False,
help="Reproduce all stages in the specified directory.",
)
repro_parser.add_argument(
"-m",
"--metrics",
action="store_true",
default=False,
help="Show metrics after reproduction.",
)
repro_parser.add_argument(
"--downstream",
action="store_true",
-96
View File
@@ -1,96 +0,0 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.stage import parse_cmd
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRun(CmdBase):
def run(self):
if not any(
[
self.args.deps,
self.args.outs,
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.plots,
self.args.plots_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.checkpoints,
self.args.params,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
)
return 1
kwargs = vars(self.args)
kwargs.update(
{
"cmd": parse_cmd(self.args.command),
"fname": kwargs.pop("file"),
"no_exec": self.args.no_exec or bool(self.args.checkpoints),
"run_cache": not kwargs.pop("no_run_cache"),
}
)
try:
self.repo.run(**kwargs)
except DvcException:
logger.exception("")
return 1
return 0
def add_parser(subparsers, parent_parser):
from dvc.commands.stage import _add_common_args
RUN_HELP = "Generate a dvc.yaml file from a command and execute the command."
run_parser = subparsers.add_parser(
"run",
parents=[parent_parser],
description=append_doc_link(RUN_HELP, "run"),
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument("-n", "--name", help="Stage name.")
run_parser.add_argument("--file", metavar="<filename>", help=argparse.SUPPRESS)
run_parser.add_argument(
"--single-stage",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
_add_common_args(run_parser)
run_parser.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create dvc.yaml without actually running it.",
)
run_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
run_parser.add_argument(
"--no-run-cache",
action="store_true",
default=False,
help=(
"Execute the command even if this stage has already been run "
"with the same command/dependencies/outputs/etc before."
),
)
run_parser.set_defaults(func=CmdRun)
-11
View File
@@ -195,17 +195,6 @@ def _add_common_args(parser):
help="Declare output file or directory (do not put into DVC cache).",
metavar="<filename>",
).complete = completion.FILE
parser.add_argument(
"-c",
"--checkpoints",
action="append",
default=[],
help=(
"Declare checkpoint output file or directory for 'dvc exp run'. "
"Not compatible with 'dvc repro'."
),
metavar="<filename>",
).complete = completion.FILE
parser.add_argument(
"--external",
action="store_true",
-19
View File
@@ -22,7 +22,6 @@ from typing import (
from funcy import reraise
if TYPE_CHECKING:
from dvc.types import StrPath
from dvc.ui.table import CellT
@@ -161,24 +160,6 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
writer.writerow(row)
return buff.getvalue()
def to_parallel_coordinates(
self, output_path: "StrPath", color_by: Optional[str] = None
) -> "StrPath":
from dvc_render.html import render_html
from dvc_render.plotly import ParallelCoordinatesRenderer
render_html(
renderers=[
ParallelCoordinatesRenderer(
self.as_dict(),
color_by=color_by,
fill_value=self._fill_value,
)
],
output_file=output_path,
)
return output_path
def add_column(self, name: str) -> None:
self._columns[name] = Column([self._fill_value] * len(self))
self._keys.append(name)
+7 -7
View File
@@ -154,13 +154,13 @@ SCHEMA = {
"machine": Lower,
},
"cache": {
"local": str,
"s3": str,
"gs": str,
"hdfs": str,
"webhdfs": str,
"ssh": str,
"azure": str,
"local": str, # obsoleted
"s3": str, # obsoleted
"gs": str, # obsoleted
"hdfs": str, # obsoleted
"webhdfs": str, # obsoleted
"ssh": str, # obsoleted
"azure": str, # obsoleted
# This is for default local cache
"dir": str,
**LOCAL_COMMON,
+131 -14
View File
@@ -1,18 +1,18 @@
"""Manages dvc remotes that user can use with push/pull/status commands."""
import logging
from typing import TYPE_CHECKING, Iterable, Optional
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
from dvc.config import NoRemoteError, RemoteConfigError
from dvc.utils.objects import cached_property
from dvc_data.hashfile.db import get_index
from dvc_data.hashfile.transfer import TransferResult
if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.status import CompareStatusResult
from dvc_data.hashfile.transfer import TransferResult
logger = logging.getLogger(__name__)
@@ -29,13 +29,40 @@ class Remote:
@cached_property
def odb(self) -> "HashFileDB":
from dvc.cachemgr import CacheManager
from dvc_data.hashfile.db import get_odb
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
path = self.path
if self.worktree:
path = self.fs.path.join(path, ".dvc", "cache")
path = self.fs.path.join(
path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM
)
else:
path = self.fs.path.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
return get_odb(self.fs, path, hash_name=DEFAULT_ALGORITHM, **self.config)
return get_odb(self.fs, path, hash_name="md5", **self.config)
@cached_property
def legacy_odb(self) -> "HashFileDB":
from dvc_data.hashfile.db import get_odb
path = self.path
return get_odb(self.fs, path, hash_name="md5-dos2unix", **self.config)
def _split_legacy_hash_infos(
hash_infos: Iterable["HashInfo"],
) -> Tuple[Set["HashInfo"], Set["HashInfo"]]:
from dvc.cachemgr import LEGACY_HASH_NAMES
legacy = set()
default = set()
for hi in hash_infos:
if hi.name in LEGACY_HASH_NAMES:
legacy.add(hi)
else:
default.add(hi)
return legacy, default
class DataCloud:
@@ -101,12 +128,17 @@ class DataCloud:
self,
name: Optional[str] = None,
command: str = "<command>",
hash_name: str = "md5",
) -> "HashFileDB":
from dvc.cachemgr import LEGACY_HASH_NAMES
remote = self.get_remote(name=name, command=command)
if remote.fs.version_aware or remote.worktree:
raise NoRemoteError(
f"'{command}' is unsupported for cloud versioned remotes"
)
if hash_name in LEGACY_HASH_NAMES:
return remote.legacy_odb
return remote.odb
def _log_missing(self, status: "CompareStatusResult"):
@@ -150,14 +182,40 @@ class DataCloud:
By default remote from core.remote config option is used.
odb: optional ODB to push to. Overrides remote.
"""
odb = odb or self.get_remote_odb(remote, "push")
if odb is not None:
return self._push(objs, jobs=jobs, odb=odb)
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
result = TransferResult(set(), set())
if legacy_objs:
odb = self.get_remote_odb(remote, "push", hash_name="md5-dos2unix")
t, f = self._push(legacy_objs, jobs=jobs, odb=odb)
result.transferred.update(t)
result.failed.update(f)
if default_objs:
odb = self.get_remote_odb(remote, "push")
t, f = self._push(default_objs, jobs=jobs, odb=odb)
result.transferred.update(t)
result.failed.update(f)
return result
def _push(
self,
objs: Iterable["HashInfo"],
*,
jobs: Optional[int] = None,
odb: "HashFileDB",
) -> "TransferResult":
if odb.hash_name == "md5-dos2unix":
cache = self.repo.cache.legacy
else:
cache = self.repo.cache.local
return self.transfer(
self.repo.cache.local,
cache,
odb,
objs,
jobs=jobs,
dest_index=get_index(odb),
cache_odb=self.repo.cache.local,
cache_odb=cache,
validate_status=self._log_missing,
)
@@ -177,14 +235,41 @@ class DataCloud:
By default remote from core.remote config option is used.
odb: optional ODB to pull from. Overrides remote.
"""
odb = odb or self.get_remote_odb(remote, "pull")
if odb is not None:
return self._pull(objs, jobs=jobs, odb=odb)
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
result = TransferResult(set(), set())
if legacy_objs:
odb = self.get_remote_odb(remote, "pull", hash_name="md5-dos2unix")
assert odb.hash_name == "md5-dos2unix"
t, f = self._pull(legacy_objs, jobs=jobs, odb=odb)
result.transferred.update(t)
result.failed.update(f)
if default_objs:
odb = self.get_remote_odb(remote, "pull")
t, f = self._pull(default_objs, jobs=jobs, odb=odb)
result.transferred.update(t)
result.failed.update(f)
return result
def _pull(
self,
objs: Iterable["HashInfo"],
*,
jobs: Optional[int] = None,
odb: "HashFileDB",
) -> "TransferResult":
if odb.hash_name == "md5-dos2unix":
cache = self.repo.cache.legacy
else:
cache = self.repo.cache.local
return self.transfer(
odb,
self.repo.cache.local,
cache,
objs,
jobs=jobs,
src_index=get_index(odb),
cache_odb=self.repo.cache.local,
cache_odb=cache,
verify=odb.verify,
validate_status=self._log_missing,
)
@@ -206,17 +291,49 @@ class DataCloud:
is used.
odb: optional ODB to check status from. Overrides remote.
"""
from dvc_data.hashfile.status import CompareStatusResult
if odb is not None:
return self._status(objs, jobs=jobs, odb=odb)
result = CompareStatusResult(set(), set(), set(), set())
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
if legacy_objs:
odb = self.get_remote_odb(remote, "status", hash_name="md5-dos2unix")
assert odb.hash_name == "md5-dos2unix"
o, m, n, d = self._status(legacy_objs, jobs=jobs, odb=odb)
result.ok.update(o)
result.missing.update(m)
result.new.update(n)
result.deleted.update(d)
if default_objs:
odb = self.get_remote_odb(remote, "status")
o, m, n, d = self._status(default_objs, jobs=jobs, odb=odb)
result.ok.update(o)
result.missing.update(m)
result.new.update(n)
result.deleted.update(d)
return result
def _status(
self,
objs: Iterable["HashInfo"],
*,
jobs: Optional[int] = None,
odb: "HashFileDB",
):
from dvc_data.hashfile.status import compare_status
if not odb:
odb = self.get_remote_odb(remote, "status")
if odb.hash_name == "md5-dos2unix":
cache = self.repo.cache.legacy
else:
cache = self.repo.cache.local
return compare_status(
self.repo.cache.local,
cache,
odb,
objs,
jobs=jobs,
dest_index=get_index(odb),
cache_odb=self.repo.cache.local,
cache_odb=cache,
)
def get_url_for(self, remote, checksum):
+2 -2
View File
@@ -14,7 +14,6 @@ from .repo import RepoDependency
SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**ParamsDependency.PARAM_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
@@ -36,7 +35,8 @@ def loadd_from(stage, d_list):
for d in d_list:
p = d.pop(Output.PARAM_PATH, None)
files = d.pop(Output.PARAM_FILES, None)
ret.append(_get(stage, p, d, files=files))
hash_name = d.pop(Output.PARAM_HASH, None)
ret.append(_get(stage, p, d, files=files, hash_name=hash_name))
return ret
+2 -3
View File
@@ -2,10 +2,9 @@ import logging
import os
import typing
from collections import defaultdict
from typing import Dict
from typing import Any, Dict
import dpath
from voluptuous import Any
from dvc.exceptions import DvcException
from dvc.utils.serialize import ParseError, load_path
@@ -34,7 +33,6 @@ class BadParamFileError(DvcException):
class ParamsDependency(Dependency):
PARAM_PARAMS = "params"
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
DEFAULT_PARAMS_FILE = "params.yaml"
def __init__(self, stage, path, params=None, repo=None):
@@ -48,6 +46,7 @@ class ParamsDependency(Dependency):
repo = repo or stage.repo
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
super().__init__(stage, path, repo=repo)
self.hash_name = self.PARAM_PARAMS
self.hash_info = hash_info
def dumpd(self, **kwargs):
+1 -1
View File
@@ -80,7 +80,7 @@ class RepoDependency(Dependency):
from dvc.fs import DVCFileSystem
config = {"cache": self.repo.config["cache"]}
config["cache"]["dir"] = self.repo.cache.local.path
config["cache"]["dir"] = self.repo.cache.local_cache_dir
return DVCFileSystem(
url=self.def_repo[self.PARAM_URL],
+5 -48
View File
@@ -14,7 +14,6 @@ from typing import (
)
from dvc.exceptions import DvcException
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
from dvc.stage import serialize
from dvc.stage.exceptions import (
StageFileBadNameError,
@@ -36,7 +35,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
_T = TypeVar("_T")
DVC_FILE = "Dvcfile"
DVC_FILE_SUFFIX = ".dvc"
PROJECT_FILE = "dvc.yaml"
LOCK_FILE = "dvc.lock"
@@ -56,10 +54,7 @@ class ParametrizedDumpError(DvcException):
def is_valid_filename(path):
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
DVC_FILE,
PROJECT_FILE,
]
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) == PROJECT_FILE
def is_dvc_file(path):
@@ -358,36 +353,8 @@ class ProjectFile(FileMixin):
raise NotImplementedError
def get_lockfile_schema(d):
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
schema = {
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
LOCKFILE_VERSION.V2: COMPILED_LOCKFILE_V2_SCHEMA,
}
version = LOCKFILE_VERSION.from_dict(d)
return schema[version]
def migrate_lock_v1_to_v2(d, version_info):
stages = dict(d)
for key in stages:
d.pop(key)
# forcing order, meta should always be at the top
d.update(version_info)
d["stages"] = stages
def lockfile_schema(data: _T) -> _T:
schema = get_lockfile_schema(data)
return schema(data)
class Lockfile(FileMixin):
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
def _verify_filename(self):
pass # lockfile path is hardcoded, so no need to verify here
@@ -401,21 +368,12 @@ class Lockfile(FileMixin):
self._check_gitignored()
return {}, ""
@property
def latest_version_info(self):
version = LOCKFILE_VERSION.V2.value # pylint:disable=no-member
return {SCHEMA_KWD: version}
def dump(self, stage, **kwargs):
stage_data = serialize.to_lockfile(stage, **kwargs)
with modify_yaml(self.path, fs=self.repo.fs) as data:
version = LOCKFILE_VERSION.from_dict(data)
if version == LOCKFILE_VERSION.V1:
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
migrate_lock_v1_to_v2(data, self.latest_version_info)
elif not data:
data.update(self.latest_version_info)
if not data:
data.update({"schema": "2.0"})
# order is important, meta should always be at the top
logger.info("Generating lock file '%s'", self.relpath)
@@ -436,8 +394,7 @@ class Lockfile(FileMixin):
return
d, _ = self._load_yaml(round_trip=True)
version = LOCKFILE_VERSION.from_dict(d)
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
data = d.get("stages", {})
if stage.name not in data:
return
-2
View File
@@ -1,5 +1,3 @@
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_CHECKPOINT = "DVC_CHECKPOINT"
DVC_DAEMON = "DVC_DAEMON"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
-20
View File
@@ -182,11 +182,6 @@ class BadMetricError(DvcException):
)
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super().__init__("cannot use `fname` with multiple targets or `-R|--recursive`")
class OverlappingOutputPathsError(DvcException):
def __init__(self, parent, overlapping_out, message):
self.parent = parent
@@ -304,21 +299,6 @@ class URLMissingError(DvcException):
super().__init__(f"The path '{url}' does not exist")
class RemoteCacheRequiredError(DvcException):
def __init__(self, scheme, fs_path):
super().__init__(
(
"Current operation was unsuccessful because '{}' requires "
"existing cache on '{}' remote. See {} for information on how "
"to set up remote cache."
).format(
fs_path,
scheme,
format_link("https://man.dvc.org/config#cache"),
)
)
class IsADirectoryError(DvcException): # noqa,pylint:disable=redefined-builtin
"""Raised when a file operation is requested on a directory."""
+55 -5
View File
@@ -2,12 +2,9 @@
from contextlib import ExitStack
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
from dvc.progress import Tqdm
from dvc.utils.objects import cached_property
from dvc_objects.fs.callbacks import ( # noqa: F401
DEFAULT_CALLBACK,
Callback,
TqdmCallback,
)
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
if TYPE_CHECKING:
from rich.progress import TaskID
@@ -15,6 +12,59 @@ if TYPE_CHECKING:
from dvc.ui._rich_progress import RichTransferProgress
# NOTE: this is very similar to dvc-objects.fs.callbacks.TqdmCallback,
# but it works with our own Tqdm instance.
class TqdmCallback(Callback):
def __init__(
self,
size: Optional[int] = None,
value: int = 0,
progress_bar: Optional["Tqdm"] = None,
**tqdm_kwargs,
):
tqdm_kwargs["total"] = size or -1
self._tqdm_kwargs = tqdm_kwargs
self._progress_bar = progress_bar
self._stack = ExitStack()
super().__init__(size=size, value=value)
@cached_property
def progress_bar(self):
progress_bar = (
self._progress_bar
if self._progress_bar is not None
else Tqdm(**self._tqdm_kwargs)
)
return self._stack.enter_context(progress_bar)
def __enter__(self):
return self
def close(self):
self._stack.close()
def set_size(self, size):
# Tqdm tries to be smart when to refresh,
# so we try to force it to re-render.
super().set_size(size)
self.progress_bar.refresh()
def call(self, hook_name=None, **kwargs): # noqa: ARG002
self.progress_bar.update_to(self.value, total=self.size)
def branch(
self,
path_1: "Union[str, BinaryIO]",
path_2: str,
kwargs: Dict[str, Any],
child: Optional[Callback] = None,
):
child = child or TqdmCallback(
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
)
return super().branch(path_1, path_2, kwargs, child=child)
class RichCallback(Callback):
def __init__(
self,
+14 -7
View File
@@ -38,12 +38,17 @@ def _is_dvc_file(fname):
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
def _merge_info(repo, fs_info, dvc_info):
def _merge_info(repo, key, fs_info, dvc_info):
from . import utils
ret = {"repo": repo}
if dvc_info:
dvc_info["isout"] = any(
(len(out_key) <= len(key) and key[: len(out_key)] == out_key)
for out_key in repo.index.data_keys["repo"]
)
dvc_info["isdvc"] = dvc_info["isout"]
ret["dvc_info"] = dvc_info
ret["type"] = dvc_info["type"]
ret["size"] = dvc_info["size"]
@@ -263,10 +268,10 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
def isdvc(self, path, **kwargs) -> bool:
"""Is this entry dvc-tracked?"""
key = self._get_key_from_relative(path)
_, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
try:
return self.info(path).get("dvc_info", {}).get("isout", False)
except FileNotFoundError:
return False
def ls( # pylint: disable=arguments-differ # noqa: C901
self, path, detail=True, dvc_only=False, **kwargs
@@ -314,7 +319,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
continue
entry_path = self.path.join(path, name)
info = _merge_info(repo, fs_infos.get(name), dvc_infos.get(name))
info = _merge_info(
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
)
info["name"] = entry_path
infos.append(info)
paths.append(entry_path)
@@ -370,7 +377,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
if not dvc_info and not fs_info:
raise FileNotFoundError
info = _merge_info(repo, fs_info, dvc_info)
info = _merge_info(repo, subkey, fs_info, dvc_info)
info["name"] = path
return info
+2 -3
View File
@@ -6,13 +6,12 @@ import platform
import psutil
from dvc import __version__
from dvc import PKG, __version__
from dvc.exceptions import NotDvcRepoError
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
from dvc.repo import Repo
from dvc.scm import SCMError
from dvc.utils import error_link
from dvc.utils.pkg import PKG
SUBPROJECTS = (
"dvc_data",
@@ -68,7 +67,7 @@ def _get_caches(cache):
caches = (
cache_type
for cache_type, cache_instance in cache.by_scheme()
if cache_instance and cache_type != "repo"
if cache_instance and cache_type not in ("repo", "legacy")
)
# Caches will be always non-empty including the local cache
-2
View File
@@ -1,2 +0,0 @@
# TODO: remove in 3.0, kept for backward compatibility
from .cli import main # noqa: F401, pylint: disable=unused-import
+90 -59
View File
@@ -19,7 +19,6 @@ from dvc.exceptions import (
ConfirmRemoveError,
DvcException,
MergeError,
RemoteCacheRequiredError,
)
from dvc.utils.objects import cached_property
from dvc_data.hashfile import check as ocheck
@@ -27,6 +26,7 @@ from dvc_data.hashfile import load as oload
from dvc_data.hashfile.build import build
from dvc_data.hashfile.checkout import checkout
from dvc_data.hashfile.db import HashFileDB, add_update_tree
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.istextfile import istextfile
from dvc_data.hashfile.meta import Meta
@@ -64,7 +64,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH);
# 1) md5 (LOCAL, SSH) (actually DVC 2.x md5-dos2unix)
# 2) etag (S3, GS, OSS, AZURE, HTTP);
# 3) checksum (HDFS);
#
@@ -73,7 +73,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
HDFS_PARAM_CHECKSUM = "checksum"
S3_PARAM_CHECKSUM = "etag"
CHECKSUMS_SCHEMA = {
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
"md5": CHECKSUM_SCHEMA, # DVC 2.x md5-dos2unix
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
}
@@ -91,11 +91,11 @@ def loadd_from(stage, d_list):
metric = d.pop(Output.PARAM_METRIC, False)
plot = d.pop(Output.PARAM_PLOT, False)
persist = d.pop(Output.PARAM_PERSIST, False)
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
remote = d.pop(Output.PARAM_REMOTE, None)
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
files = d.pop(Output.PARAM_FILES, None)
push = d.pop(Output.PARAM_PUSH, True)
hash_name = d.pop(Output.PARAM_HASH, None)
ret.append(
_get(
stage,
@@ -105,11 +105,11 @@ def loadd_from(stage, d_list):
metric=metric,
plot=plot,
persist=persist,
checkpoint=checkpoint,
remote=remote,
**annot,
files=files,
push=push,
hash_name=hash_name,
)
)
return ret
@@ -122,7 +122,6 @@ def loads_from(
metric=False,
plot=False,
persist=False,
checkpoint=False,
remote=None,
push=True,
):
@@ -135,7 +134,6 @@ def loads_from(
metric=metric,
plot=plot,
persist=persist,
checkpoint=checkpoint,
remote=remote,
push=push,
)
@@ -191,7 +189,6 @@ def load_from_pipeline(stage, data, typ="outs"):
[
Output.PARAM_CACHE,
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
Output.PARAM_REMOTE,
Output.PARAM_PUSH,
*ANNOTATION_FIELDS,
@@ -232,17 +229,29 @@ def merge_file_meta_from_cloud(entry: Dict) -> Dict:
return entry
def _serialize_tree_obj_to_files(obj: "Tree") -> List[Dict[str, Any]]:
def _serialize_tree_obj_to_files(obj: Tree) -> List[Dict[str, Any]]:
key = obj.PARAM_RELPATH
return sorted(
(
{key: posixpath.sep.join(parts), **hi.to_dict(), **meta.to_dict()}
{
key: posixpath.sep.join(parts),
**_serialize_hi_to_dict(hi),
**meta.to_dict(),
}
for parts, meta, hi in obj
),
key=itemgetter(key),
)
def _serialize_hi_to_dict(hash_info: Optional[HashInfo]) -> Dict[str, Any]:
if hash_info:
if hash_info.name == "md5-dos2unix":
return {"md5": hash_info.value}
return hash_info.to_dict()
return {}
class OutputDoesNotExistError(DvcException):
def __init__(self, path):
msg = f"output '{path}' does not exist"
@@ -286,7 +295,6 @@ class Output:
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_CHECKPOINT = "checkpoint"
PARAM_FILES = "files"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
@@ -303,15 +311,7 @@ class Output:
PARAM_REMOTE = "remote"
PARAM_PUSH = "push"
PARAM_CLOUD = "cloud"
METRIC_SCHEMA = Any(
None,
bool,
{
PARAM_METRIC_TYPE: Any(str, None),
PARAM_METRIC_XPATH: Any(str, None),
},
)
PARAM_HASH = "hash"
DoesNotExistError: Type[DvcException] = OutputDoesNotExistError
IsNotFileOrDirError: Type[DvcException] = OutputIsNotFileOrDirError
@@ -327,7 +327,6 @@ class Output:
metric=False,
plot=False,
persist=False,
checkpoint=False,
desc=None,
type=None, # noqa: A002, pylint: disable=redefined-builtin
labels=None,
@@ -337,6 +336,7 @@ class Output:
fs_config=None,
files: Optional[List[Dict[str, Any]]] = None,
push: bool = True,
hash_name: Optional[str] = DEFAULT_ALGORITHM,
):
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
@@ -394,7 +394,6 @@ class Output:
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.checkpoint = checkpoint
self.can_push = push
self.fs_path = self._parse_path(self.fs, fs_path)
@@ -409,17 +408,30 @@ class Output:
)
self.meta.version_id = version_id
if self.is_in_repo:
self.hash_name = "md5"
else:
self.hash_name = self.fs.PARAM_CHECKSUM
self.hash_info = HashInfo(
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
self.hash_name, self.hash_info = self._compute_hash_info_from_meta(hash_name)
self._compute_meta_hash_info_from_files()
def _compute_hash_info_from_meta(
self, hash_name: Optional[str]
) -> Tuple[str, HashInfo]:
if self.is_in_repo:
if hash_name is None:
# Legacy 2.x output, use "md5-dos2unix" but read "md5" from
# file meta
hash_name = "md5-dos2unix"
meta_name = "md5"
else:
meta_name = hash_name
else:
hash_name = meta_name = self.fs.PARAM_CHECKSUM
assert hash_name
hash_info = HashInfo(
name=hash_name,
value=getattr(self.meta, meta_name, None),
)
return hash_name, hash_info
def _compute_meta_hash_info_from_files(self) -> None:
if self.files:
tree = Tree.from_list(self.files, hash_name=self.hash_name)
@@ -434,7 +446,7 @@ class Output:
self.meta.remote = first(f.get("remote") for f in self.files)
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
if not self.hash_info and self.hash_name != "md5":
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
md5 = getattr(self.meta, "md5", None)
if md5:
self.hash_info = HashInfo("md5", md5)
@@ -498,7 +510,7 @@ class Output:
return False
return self.repo and self.fs.path.isin(
self.fs.path.realpath(self.fs_path),
self.fs_path,
self.repo.root_dir,
)
@@ -511,11 +523,19 @@ class Output:
@property
def cache(self):
odb_name = "repo" if self.is_in_repo else self.protocol
odb = getattr(self.repo.cache, odb_name)
if self.use_cache and odb is None:
raise RemoteCacheRequiredError(self.fs.protocol, self.fs_path)
return odb
from dvc.cachemgr import LEGACY_HASH_NAMES
assert self.is_in_repo
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
return getattr(self.repo.cache, odb_name)
@property
def local_cache(self):
from dvc.cachemgr import LEGACY_HASH_NAMES
if self.hash_name in LEGACY_HASH_NAMES:
return self.repo.cache.legacy
return self.repo.cache.local
@property
def cache_path(self):
@@ -531,7 +551,7 @@ class Output:
if self.use_cache:
odb = self.cache
else:
odb = self.repo.cache.local
odb = self.local_cache
_, meta, obj = build(
odb,
self.fs_path,
@@ -672,6 +692,7 @@ class Output:
if self.metric:
self.verify_metric()
self._update_legacy_hash_name()
if self.use_cache:
_, self.meta, self.obj = build(
self.cache,
@@ -682,7 +703,7 @@ class Output:
)
else:
_, self.meta, self.obj = build(
self.repo.cache.local,
self.local_cache,
self.fs_path,
self.fs,
self.hash_name,
@@ -695,6 +716,10 @@ class Output:
self.hash_info = self.obj.hash_info
self.files = None
def _update_legacy_hash_name(self):
if self.hash_name == "md5-dos2unix" and self.changed_checksum():
self.hash_name = "md5"
def set_exec(self) -> None:
if self.isfile() and self.meta.isexec:
self.cache.set_exec(self.fs_path)
@@ -781,6 +806,8 @@ class Output:
return checkout_obj
def dumpd(self, **kwargs): # noqa: C901, PLR0912
from dvc.cachemgr import LEGACY_HASH_NAMES
ret: Dict[str, Any] = {}
with_files = (
(not self.IS_DEPENDENCY or self.stage.is_import)
@@ -791,7 +818,12 @@ class Output:
if not with_files:
meta_d = self.meta.to_dict()
meta_d.pop("isdir", None)
ret.update(self.hash_info.to_dict())
if self.hash_name in LEGACY_HASH_NAMES:
# 2.x checksums get serialized with file meta
name = "md5" if self.hash_name == "md5-dos2unix" else self.hash_name
ret.update({name: self.hash_info.value})
else:
ret.update(self.hash_info.to_dict())
ret.update(split_file_meta_from_cloud(meta_d))
if self.is_in_repo:
@@ -799,6 +831,9 @@ class Output:
else:
path = self.def_path
if self.hash_name not in LEGACY_HASH_NAMES:
ret[self.PARAM_HASH] = "md5"
ret[self.PARAM_PATH] = path
if not self.IS_DEPENDENCY:
@@ -822,9 +857,6 @@ class Output:
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
if self.checkpoint:
ret[self.PARAM_CHECKPOINT] = self.checkpoint
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
@@ -894,7 +926,6 @@ class Output:
relink: bool = False,
filter_info: Optional[str] = None,
allow_missing: bool = False,
checkpoint_reset: bool = False,
**kwargs,
) -> Optional[Tuple[bool, Optional[bool]]]:
# callback passed act as a aggregate callback.
@@ -914,11 +945,6 @@ class Output:
# backward compatibility
return None
if self.checkpoint and checkpoint_reset:
if self.exists:
self.remove()
return None
added = not self.exists
try:
@@ -935,14 +961,17 @@ class Output:
**kwargs,
)
except CheckoutError:
if allow_missing or self.checkpoint:
if allow_missing:
return None
raise
self.set_exec()
return added, False if added else modified
def remove(self, ignore_remove=False):
self.fs.remove(self.fs_path, recursive=True)
try:
self.fs.remove(self.fs_path, recursive=True)
except FileNotFoundError:
pass
if self.protocol != Schemes.LOCAL:
return
@@ -987,7 +1016,7 @@ class Output:
odb,
from_info,
from_fs,
"md5",
DEFAULT_ALGORITHM,
upload=upload,
no_progress_bar=no_progress_bar,
)
@@ -1128,7 +1157,9 @@ class Output:
return {}
if self.remote:
remote = self.repo.cloud.get_remote_odb(name=self.remote)
remote = self.repo.cloud.get_remote_odb(
name=self.remote, hash_name=self.hash_name
)
else:
remote = None
@@ -1167,13 +1198,14 @@ class Output:
self.hash_name,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
Output.PARAM_HASH,
]
for opt in ignored:
my.pop(opt, None)
other.pop(opt, None)
if my != other:
if my != other or self.hash_name != out.hash_name:
raise MergeError("unable to auto-merge outputs with different options")
if not out.is_dir_checksum:
@@ -1309,7 +1341,8 @@ class Output:
)
assert self.repo
cache = self.cache if self.use_cache else self.repo.cache.local
self._update_legacy_hash_name()
cache = self.cache if self.use_cache else self.local_cache
assert isinstance(cache, HashFileDB)
new: "HashFile"
@@ -1434,10 +1467,9 @@ ARTIFACT_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Output.PARAM_PATH): str,
Output.PARAM_PLOT: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Output.PARAM_CLOUD: CLOUD_SCHEMA,
Output.PARAM_HASH: str,
}
DIR_FILES_SCHEMA: Dict[str, Any] = {
@@ -1451,7 +1483,6 @@ SCHEMA = {
**ARTIFACT_SCHEMA,
**ANNOTATION_SCHEMA,
Output.PARAM_CACHE: bool,
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
Output.PARAM_REMOTE: str,
Output.PARAM_PUSH: bool,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
-39
View File
@@ -1,39 +0,0 @@
import enum
from collections.abc import Mapping
from voluptuous import validators
SCHEMA_KWD = "schema"
META_KWD = "meta"
def lockfile_version_schema(value):
expected = [LOCKFILE_VERSION.V2.value] # pylint: disable=no-member
msg = f"invalid schema version {value}, expected one of {expected}"
return validators.Any(*expected, msg=msg)(value)
class VersionEnum(str, enum.Enum):
@classmethod
def all_versions(cls):
return [v.value for v in cls]
class LOCKFILE_VERSION(VersionEnum): # noqa: N801
V1 = "1.0"
V2 = "2.0"
@classmethod
def from_dict(cls, data):
# 1) if it's empty or or is not a dict, use the latest one (V2).
# 2) use the `schema` identifier if it exists and is a supported
# version
# 3) if it's not in any of the supported version, use the latest one
# 4) if there's no identifier, it's a V1
if not data or not isinstance(data, Mapping):
return cls(cls.V2)
version = data.get(SCHEMA_KWD)
if version:
return cls(version if version in cls.all_versions() else cls.V2)
return cls(cls.V1)
+9
View File
@@ -2,12 +2,16 @@
import logging
import sys
from threading import RLock
from typing import TYPE_CHECKING
from tqdm import tqdm
from dvc.env import DVC_IGNORE_ISATTY
from dvc.utils import env2bool
if TYPE_CHECKING:
from dvc.fs.callbacks import TqdmCallback
logger = logging.getLogger(__name__)
tqdm.set_lock(RLock())
@@ -158,3 +162,8 @@ class Tqdm(tqdm):
d["ncols_desc"] = d["ncols_info"] = 1
d["prefix"] = ""
return d
def as_callback(self) -> "TqdmCallback":
from dvc.fs.callbacks import TqdmCallback
return TqdmCallback(progress_bar=self)
+10 -9
View File
@@ -8,7 +8,6 @@ from typing import (
Callable,
ContextManager,
Iterable,
List,
Optional,
Tuple,
Union,
@@ -368,6 +367,14 @@ class Repo:
return self._data_index
def drop_data_index(self) -> None:
try:
self.data_index.delete_node(("tree",))
except KeyError:
pass
self.data_index.commit()
self._reset()
def __repr__(self):
return f"{self.__class__.__name__}: '{self.root_dir}'"
@@ -421,8 +428,8 @@ class Repo:
flist = [self.config.files["local"]]
if tmp_dir := self.tmp_dir:
flist.append(tmp_dir)
if path_isin(self.cache.repo.path, self.root_dir):
flist.append(self.cache.repo.path)
if path_isin(self.cache.legacy.path, self.root_dir):
flist.append(self.cache.legacy.path)
for file in flist:
self.scm_context.ignore(file)
@@ -500,12 +507,6 @@ class Repo:
return used
@property
def stages(
self,
) -> List["Stage"]: # obsolete, only for backward-compatibility
return self.index.stages
def find_outs_by_path(self, path, outs=None, recursive=False, strict=True):
# using `outs_graph` to ensure graph checks are run
outs = outs or self.index.outs_graph
+128 -192
View File
@@ -1,34 +1,33 @@
import logging
import os
from contextlib import contextmanager
from itertools import tee
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Optional
import colorama
from typing import (
TYPE_CHECKING,
Dict,
Iterator,
List,
NamedTuple,
Optional,
Tuple,
Union,
)
from dvc.exceptions import (
CacheLinkError,
DvcException,
InvalidArgumentError,
OutputDuplicationError,
OutputNotFoundError,
OverlappingOutputPathsError,
RecursiveAddingWhileUsingFilename,
)
from dvc.repo.scm_context import scm_context
from dvc.ui import ui
from dvc.utils import LARGE_DIR_SIZE, glob_targets, resolve_output, resolve_paths
from dvc.utils.collections import ensure_list, validate
from dvc.utils import glob_targets, resolve_output, resolve_paths
from . import locked
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.stage import Stage
from dvc.types import TargetType
Stages = List["Stage"]
logger = logging.getLogger(__name__)
from dvc.types import StrOrBytesPath
class StageInfo(NamedTuple):
@@ -36,6 +35,54 @@ class StageInfo(NamedTuple):
output_exists: bool
def find_targets(
targets: Union["StrOrBytesPath", Iterator["StrOrBytesPath"]], glob: bool = False
) -> List[str]:
if isinstance(targets, (str, bytes, os.PathLike)):
targets_list = [os.fsdecode(targets)]
else:
targets_list = [os.fsdecode(target) for target in targets]
return glob_targets(targets_list, glob=glob)
PIPELINE_TRACKED_UPDATE_FMT = (
"cannot update {out!r}: overlaps with an output of {stage} in '{path}'.\n"
"Run the pipeline or use 'dvc commit' to force update it."
)
def get_or_create_stage(
repo: "Repo",
target: str,
out: Optional[str] = None,
to_remote: bool = False,
force: bool = False,
) -> StageInfo:
if out:
target = resolve_output(target, out, force=force)
path, wdir, out = resolve_paths(repo, target, always_local=to_remote and not out)
try:
(out_obj,) = repo.find_outs_by_path(target, strict=False)
stage = out_obj.stage
if not stage.is_data_source:
msg = PIPELINE_TRACKED_UPDATE_FMT.format(
out=out, stage=stage, path=stage.relpath
)
raise DvcException(msg)
return StageInfo(stage, output_exists=True)
except OutputNotFoundError:
stage = repo.stage.create(
single_stage=True,
validate=False,
fname=path,
wdir=wdir,
outs=[out],
force=force,
)
return StageInfo(stage, output_exists=False)
OVERLAPPING_CHILD_FMT = (
"Cannot add '{out}', because it is overlapping with other "
"DVC tracked output: '{parent}'.\n"
@@ -51,52 +98,8 @@ OVERLAPPING_PARENT_FMT = (
)
def check_recursive_and_fname(args):
if args.recursive and args.fname:
raise RecursiveAddingWhileUsingFilename()
def transform_targets(args):
from funcy import count_reps
counts = count_reps(ensure_list(args.targets))
dupes = [key for key, count in counts.items() if count > 1]
if dupes:
msg = ", ".join(f"[b]{key}[/]" for key in dupes)
ui.error_write(f"ignoring duplicated targets: {msg}", styled=True)
args.targets = list(counts)
def check_arg_combinations(args):
kwargs = args.kwargs
invalid_opt = None
to_remote = args.to_remote
to_cache = kwargs.get("out") and not to_remote
if to_remote or to_cache:
message = "{option} can't be used with "
message += "--to-remote" if to_remote else "-o"
if len(args.targets) != 1:
invalid_opt = "multiple targets"
elif args.no_commit:
invalid_opt = "--no-commit option"
elif args.recursive:
invalid_opt = "--recursive option"
elif kwargs.get("external"):
invalid_opt = "--external option"
else:
message = "{option} can't be used without --to-remote"
if kwargs.get("remote"):
invalid_opt = "--remote"
elif kwargs.get("jobs"):
invalid_opt = "--jobs"
if invalid_opt is not None:
raise InvalidArgumentError(message.format(option=invalid_opt))
@contextmanager
def translate_graph_error(stages: Stages) -> Iterator[None]:
def translate_graph_error(stages: List["Stage"]) -> Iterator[None]:
try:
yield
except OverlappingOutputPathsError as exc:
@@ -121,18 +124,21 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
)
def progress_iter(stages: List[StageInfo]) -> Iterator["StageInfo"]:
def progress_iter(stages: Dict[str, StageInfo]) -> Iterator[Tuple[str, StageInfo]]:
total = len(stages)
desc = "Adding..."
with ui.progress(stages, total=total, desc=desc, unit="file", leave=True) as pbar:
with ui.progress(
stages.items(), total=total, desc=desc, unit="file", leave=True
) as pbar:
if total == 1:
pbar.bar_format = desc
pbar.refresh()
for item in pbar:
for item, stage_info in pbar:
if total > 1:
pbar.set_msg(f"{item.stage.outs[0]}")
yield item
pbar.set_msg(str(stage_info.stage.outs[0]))
pbar.refresh()
yield item, stage_info
if total == 1: # restore bar format for stats
# pylint: disable=no-member
pbar.bar_format = pbar.BAR_FMT_DEFAULT
@@ -159,145 +165,75 @@ def warn_link_failures() -> Iterator[List[str]]:
ui.error_write(msg)
VALIDATORS = (
check_recursive_and_fname,
transform_targets,
check_arg_combinations,
)
def _add_transfer(
stage: "Stage",
source: str,
remote: Optional[str] = None,
to_remote: bool = False,
jobs: Optional[int] = None,
force: bool = False,
) -> None:
odb = None
if to_remote:
odb = stage.repo.cloud.get_remote_odb(remote, "add")
stage.transfer(source, odb=odb, to_remote=to_remote, jobs=jobs, force=force)
stage.dump()
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
out = stage.outs[0]
path = out.fs.path.abspath(source) if source else None
try:
stage.add_outs(path, no_commit=no_commit)
except CacheLinkError:
stage.dump()
raise
stage.dump()
@validate(*VALIDATORS)
@locked
@scm_context
def add(
repo: "Repo",
targets: "TargetType",
recursive: bool = False,
targets: Union["StrOrBytesPath", Iterator["StrOrBytesPath"]],
no_commit: bool = False,
fname: Optional[str] = None,
glob: bool = False,
out: Optional[str] = None,
remote: Optional[str] = None,
to_remote: bool = False,
**kwargs: Any,
):
to_cache = bool(kwargs.get("out")) and not to_remote
transfer = to_remote or to_cache
remote_jobs: Optional[int] = None,
force: bool = False,
) -> List["Stage"]:
add_targets = find_targets(targets, glob=glob)
if not add_targets:
return []
glob = kwargs.get("glob", False)
add_targets = collect_targets(repo, targets, recursive, glob)
# pass one for creating stages, other one is used for iterating here
add_targets, sources = tee(add_targets)
# collect targets and build stages as we go
desc = "Collecting targets"
stages_it = create_stages(repo, add_targets, fname, transfer, **kwargs)
stages = list(ui.progress(stages_it, desc=desc, unit="file"))
stages_list = [stage for stage, _ in stages]
msg = "Collecting stages from the workspace"
with translate_graph_error(stages_list), ui.status(msg) as status:
# remove existing stages that are to-be replaced with these
# new stages for the graph checks.
repo.check_graph(
stages=stages_list, callback=lambda: status.update("Checking graph")
stages_with_targets = {
target: get_or_create_stage(
repo,
target,
out=out,
to_remote=to_remote,
force=force,
)
for target in add_targets
}
odb = None
if to_remote:
odb = repo.cloud.get_remote_odb(kwargs.get("remote"), "add")
stages = [stage for stage, _ in stages_with_targets.values()]
msg = "Collecting stages from the workspace"
with translate_graph_error(stages), ui.status(msg) as st:
repo.check_graph(stages=stages, callback=lambda: st.update("Checking graph"))
if to_remote or out:
assert len(stages_with_targets) == 1, "multiple targets are unsupported"
(source, (stage, _)) = next(iter(stages_with_targets.items()))
_add_transfer(stage, source, remote, to_remote, jobs=remote_jobs, force=force)
return [stage]
with warn_link_failures() as link_failures:
for (stage, output_exists), source in zip(progress_iter(stages), sources):
out = stage.outs[0]
if to_remote or to_cache:
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
else:
try:
path = out.fs.path.abspath(source) if output_exists else None
stage.add_outs(path, no_commit=no_commit)
except CacheLinkError:
link_failures.append(str(stage.relpath))
stage.dump()
return stages_list
LARGE_DIR_RECURSIVE_ADD_WARNING = (
"You are adding a large directory '{target}' recursively.\n"
"Consider tracking it as a whole instead with "
"`{cyan}dvc add {target}{nc}`."
)
def collect_targets(
repo: "Repo",
targets: "TargetType",
recursive: bool = False,
glob: bool = False,
) -> Iterator[str]:
for target in glob_targets(ensure_list(targets), glob=glob):
expanded_targets = _find_all_targets(repo, target, recursive=recursive)
for index, path in enumerate(expanded_targets):
if index == LARGE_DIR_SIZE:
msg = LARGE_DIR_RECURSIVE_ADD_WARNING.format(
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
target=target,
)
ui.error_write(msg)
yield path
def _find_all_targets(
repo: "Repo", target: str, recursive: bool = False
) -> Iterator[str]:
from dvc.dvcfile import is_dvc_file
if os.path.isdir(target) and recursive:
files = repo.dvcignore.find(repo.fs, target)
yield from (
path
for path in files
if not repo.is_dvc_internal(path)
if not is_dvc_file(path)
if not repo.scm.belongs_to_scm(path)
if not repo.scm.is_tracked(path)
)
else:
yield target
def create_stages(
repo: "Repo",
targets: Iterator[str],
fname: Optional[str] = None,
transfer: bool = False,
external: bool = False,
force: bool = False,
**kwargs: Any,
) -> Iterator[StageInfo]:
for target in targets:
if kwargs.get("out"):
target = resolve_output(target, kwargs["out"], force=force)
path, wdir, out = resolve_paths(
repo, target, always_local=transfer and not kwargs.get("out")
)
try:
(out_obj,) = repo.find_outs_by_path(target, strict=False)
stage = out_obj.stage
if not stage.is_data_source:
raise DvcException(f"cannot update {out!r}: not a data source")
output_exists = True
except OutputNotFoundError:
stage = repo.stage.create(
single_stage=True,
validate=False,
fname=fname or path,
wdir=wdir,
outs=[out],
external=external,
force=force,
)
output_exists = False
out_obj = stage.outs[0]
out_obj.annot.update(**kwargs)
yield StageInfo(stage, output_exists)
for source, (stage, output_exists) in progress_iter(stages_with_targets):
try:
_add(stage, source if output_exists else None, no_commit=no_commit)
except CacheLinkError:
link_failures.append(stage.relpath)
return stages
+148 -50
View File
@@ -1,15 +1,21 @@
import logging
import os
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List
from dvc.exceptions import CheckoutError, CheckoutErrorSuggestGit, NoOutputOrStageError
from dvc.exceptions import (
CheckoutError,
CheckoutErrorSuggestGit,
DvcException,
NoOutputOrStageError,
)
from dvc.ui import ui
from dvc.utils import relpath
from . import locked
if TYPE_CHECKING:
from . import Repo
from .stage import StageInfo
from dvc_data.index import BaseDataIndex, DataIndexEntry
from dvc_objects.fs.base import FileSystem
logger = logging.getLogger(__name__)
@@ -30,37 +36,68 @@ def _remove_unused_links(repo):
return ret
def get_all_files_numbers(pairs):
return sum(stage.get_all_files_number(filter_info) for stage, filter_info in pairs)
def _build_out_changes(index, changes):
from dvc_data.index.checkout import MODIFY
out_keys = []
for out in index.outs:
if not out.use_cache:
continue
ws, key = out.index_key
if ws != "repo":
continue
out_keys.append(key)
out_changes = {}
for objects in changes.values():
for change in objects:
for out_key in out_keys:
if (
len(out_key) > len(change.key)
or change.key[: len(out_key)] != out_key
):
continue
if change.key == out_key:
out_changes[out_key] = change.typ
elif not out_changes.get(out_key):
out_changes[out_key] = MODIFY
break
return out_changes
def _collect_pairs(
self: "Repo", targets, with_deps: bool, recursive: bool
) -> Set["StageInfo"]:
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
pairs: Set["StageInfo"] = set()
for target in targets:
def _check_can_delete(
entries: List["DataIndexEntry"],
index: "BaseDataIndex",
path: str,
fs: "FileSystem",
):
entry_paths = []
for entry in entries:
try:
pairs.update(
self.stage.collect_granular(
target, with_deps=with_deps, recursive=recursive
)
)
except (
StageFileDoesNotExistError,
StageFileBadNameError,
NoOutputOrStageError,
) as exc:
if not target:
raise
raise CheckoutErrorSuggestGit(target) from exc
cache_fs, cache_path = index.storage_map.get_cache(entry)
except ValueError:
continue
return pairs
if cache_fs.exists(cache_path):
continue
entry_paths.append(fs.path.join(path, *entry.key))
if not entry_paths:
return
raise DvcException(
"Can't remove the following unsaved files without confirmation. "
"Use `--force` to force.\n" + "\n".join(entry_paths)
)
@locked
def checkout(
def checkout( # noqa: C901
self,
targets=None,
with_deps=False,
@@ -70,13 +107,14 @@ def checkout(
allow_missing=False,
**kwargs,
):
from dvc.fs.callbacks import Callback
from dvc.repo.index import build_data_index
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
from dvc_data.index.checkout import ADD, DELETE, MODIFY, apply, compare
stats: Dict[str, List[str]] = {
"added": [],
"deleted": [],
"modified": [],
"failed": [],
}
if not targets:
targets = [None]
@@ -85,28 +123,88 @@ def checkout(
if isinstance(targets, str):
targets = [targets]
pairs = _collect_pairs(self, targets, with_deps, recursive)
total = get_all_files_numbers(pairs)
with Callback.as_tqdm_callback(
def onerror(target, exc):
if target and isinstance(
exc,
(
StageFileDoesNotExistError,
StageFileBadNameError,
NoOutputOrStageError,
),
):
raise CheckoutErrorSuggestGit(target) from exc
raise # pylint: disable=misplaced-bare-raise
view = self.index.targets_view(
targets,
recursive=recursive,
with_deps=with_deps,
onerror=onerror,
)
with ui.progress(
unit="entry",
desc="Building data index",
) as pb:
old = build_data_index(
view,
self.root_dir,
self.fs,
compute_hash=True,
callback=pb.as_callback(),
)
new = view.data["repo"]
with ui.progress(
desc="Comparing indexes",
unit="entry",
) as pb:
diff = compare(old, new, relink=relink, delete=True, callback=pb.as_callback())
if not force:
_check_can_delete(diff.files_delete, new, self.root_dir, self.fs)
failed = set()
out_paths = [out.fs_path for out in view.outs if out.use_cache and out.is_in_repo]
def checkout_onerror(src_path, dest_path, _exc):
logger.debug(
"failed to create '%s' from '%s'", dest_path, src_path, exc_info=True
)
for out_path in out_paths:
if self.fs.path.isin_or_eq(dest_path, out_path):
failed.add(out_path)
with ui.progress(
unit="file",
desc="Checkout",
disable=total == 0,
) as cb:
cb.set_size(total)
for stage, filter_info in pairs:
result = stage.checkout(
force=force,
progress_callback=cb,
relink=relink,
filter_info=filter_info,
allow_missing=allow_missing,
**kwargs,
)
for key, items in result.items():
stats[key].extend(_fspath_dir(path) for path in items)
) as pb:
apply(
diff,
self.root_dir,
self.fs,
callback=pb.as_callback(),
update_meta=False,
onerror=checkout_onerror,
state=self.state,
**kwargs,
)
if stats.get("failed"):
raise CheckoutError(stats["failed"], stats)
out_changes = _build_out_changes(view, diff.changes)
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
for key, typ in out_changes.items():
out_path = self.fs.path.join(self.root_dir, *key)
if out_path in failed:
self.fs.remove(out_path, recursive=True)
else:
self.state.save_link(out_path, self.fs)
stats[typ_map[typ]].append(_fspath_dir(out_path))
if failed and not allow_missing:
raise CheckoutError([relpath(out_path) for out_path in failed], stats)
del stats["failed"]
return stats
+1 -1
View File
@@ -37,7 +37,7 @@ def prompt_to_commit(stage, changes, force=False):
@locked
def commit(
self,
target,
target=None,
with_deps=False,
recursive=False,
force=False,
+26 -35
View File
@@ -1,21 +1,12 @@
import os
import posixpath
from itertools import chain
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
TypedDict,
Union,
)
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, TypedDict, Union
from dvc.fs.callbacks import DEFAULT_CALLBACK
from dvc.ui import ui
if TYPE_CHECKING:
from dvc.fs.callbacks import Callback
from dvc.repo import Repo
from dvc.scm import Git, NoSCM
from dvc_data.index import DataIndex
@@ -61,6 +52,7 @@ def _diff(
not_in_cache: bool = False,
not_in_remote: bool = False,
remote_refresh: bool = False,
callback: "Callback" = DEFAULT_CALLBACK,
) -> Dict[str, List[str]]:
from dvc_data.index import StorageError
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
@@ -81,13 +73,12 @@ def _diff(
shallow=not granular,
hash_only=True,
with_unknown=True,
callback=callback,
):
if (
change.typ == UNCHANGED
and change.old
and change.new
and not change.old.hash_info
and not change.new.hash_info
and (not change.old or not change.old.hash_info)
and (not change.new or not change.new.hash_info)
):
# NOTE: emulating previous behaviour
continue
@@ -162,16 +153,27 @@ def _git_info(scm: Union["Git", "NoSCM"], untracked_files: str = "all") -> GitIn
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
from .index import build_data_index
with ui.status("Building workspace index"):
with ui.progress(
desc="Building workspace index",
unit="entry",
) as pb:
workspace = build_data_index(
repo.index, repo.root_dir, repo.fs, compute_hash=True
repo.index,
repo.root_dir,
repo.fs,
compute_hash=True,
callback=pb.as_callback(),
)
with ui.status("Calculating diff between index/workspace"):
with ui.progress(
desc="Calculating diff between index/workspace",
unit="entry",
) as pb:
return _diff(
repo.index.data["repo"],
workspace,
not_in_cache=True,
callback=pb.as_callback(),
**kwargs,
)
@@ -184,8 +186,11 @@ def _diff_head_to_index(
with repo.switch(head):
head_index = repo.index.data["repo"]
with ui.status("Calculating diff between head/index"):
return _diff(head_index, index, **kwargs)
with ui.progress(
desc="Calculating diff between head/index",
unit="entry",
) as pb:
return _diff(head_index, index, callback=pb.as_callback(), **kwargs)
class Status(TypedDict):
@@ -251,17 +256,3 @@ def status(
unchanged=list(unchanged),
git=git_info,
)
def ls(
repo: "Repo",
targets: Optional[List[Optional[str]]] = None,
recursive: bool = False,
) -> Iterator[Dict[str, Any]]:
targets = targets or [None]
pairs = chain.from_iterable(
repo.stage.collect_granular(target, recursive=recursive) for target in targets
)
for stage, filter_info in pairs:
for out in stage.filter_outs(filter_info):
yield {"path": str(out), **out.annot.to_dict()}
+7 -2
View File
@@ -156,8 +156,13 @@ def diff(
for target in old_missing & new_missing:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
old = indexes[a_rev]
new = indexes[b_rev]
if len(indexes.keys()) == 1:
# both a_rev and b_rev point to the same sha, nothing to compare
old = None
new = None
else:
old = indexes[a_rev]
new = indexes[b_rev]
with ui.status("Calculating diff"):
return _diff(old, new, with_missing=with_missing)
+1 -102
View File
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
from funcy import chain, first
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils import relpath
from dvc.utils.objects import cached_property
@@ -22,7 +21,6 @@ from .refs import (
CELERY_FAILED_STASH,
CELERY_STASH,
EXEC_APPLY,
EXEC_CHECKPOINT,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
WORKSPACE_STASH,
@@ -48,10 +46,7 @@ class Experiments:
repo (dvc.repo.Repo): repo instance that these experiments belong to.
"""
BRANCH_RE = re.compile(
r"^(?P<baseline_rev>[a-f0-9]{7})-(?P<exp_sha>[a-f0-9]+)"
r"(?P<checkpoint>-checkpoint)?$"
)
BRANCH_RE = re.compile(r"^(?P<baseline_rev>[a-f0-9]{7})-(?P<exp_sha>[a-f0-9]+)")
def __init__(self, repo):
from dvc.scm import NoSCMError
@@ -138,47 +133,18 @@ class Experiments:
def queue_one(
self,
queue: "BaseStashQueue",
checkpoint_resume: Optional[str] = None,
reset: bool = False,
**kwargs,
) -> "QueueEntry":
"""Queue a single experiment."""
if reset:
self.reset_checkpoints()
if kwargs.pop("machine", None) is not None:
# TODO: decide how to handle queued remote execution
raise NotImplementedError
if checkpoint_resume:
from dvc.scm import resolve_rev
resume_rev = resolve_rev(self.scm, checkpoint_resume)
try:
self.check_baseline(resume_rev)
checkpoint_resume = resume_rev
except BaselineMismatchError as exc:
raise DvcException(
f"Cannot resume from '{checkpoint_resume}' as it is not "
"derived from your current workspace."
) from exc
else:
checkpoint_resume = self._workspace_resume_rev()
return self.new(
queue,
checkpoint_resume=checkpoint_resume,
reset=reset,
**kwargs,
)
def _workspace_resume_rev(self) -> Optional[str]:
last_checkpoint = self._get_last_checkpoint()
last_applied = self._get_last_applied()
if last_checkpoint and last_applied:
return last_applied
return None
def reproduce_celery( # noqa: C901
self, entries: Optional[Iterable["QueueEntry"]] = None, **kwargs
) -> Dict[str, str]:
@@ -257,17 +223,12 @@ class Experiments:
self,
queue: "BaseStashQueue",
*args,
checkpoint_resume: Optional[str] = None,
**kwargs,
) -> "QueueEntry":
"""Create and enqueue a new experiment.
Experiment will be derived from the current workspace.
"""
if checkpoint_resume is not None:
return self._resume_checkpoint(
queue, *args, resume_rev=checkpoint_resume, **kwargs
)
name = kwargs.get("name", None)
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
@@ -281,59 +242,6 @@ class Experiments:
return queue.put(*args, **kwargs)
def _resume_checkpoint(
self,
queue: "BaseStashQueue",
*args,
resume_rev: Optional[str] = None,
**kwargs,
) -> "QueueEntry":
"""Create and queue a resumed checkpoint experiment."""
assert resume_rev
branch: Optional[str] = None
try:
allow_multiple = bool(kwargs.get("params", None))
branch = self.get_branch_by_rev(resume_rev, allow_multiple=allow_multiple)
if not branch:
raise DvcException(
f"Could not find checkpoint experiment '{resume_rev[:7]}'"
)
baseline_rev = self._get_baseline(branch)
except MultipleBranchError as exc:
baselines = {
info.baseline_sha for info in exc.ref_infos if info.baseline_sha
}
if len(baselines) == 1:
baseline_rev = baselines.pop()
else:
raise
logger.debug(
"Checkpoint run from '%s' with baseline '%s'",
resume_rev[:7],
baseline_rev,
)
return queue.put(
*args,
resume_rev=resume_rev,
baseline_rev=baseline_rev,
branch=branch,
**kwargs,
)
def _get_last_checkpoint(self) -> Optional[str]:
try:
last_checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
if last_checkpoint:
self.check_baseline(last_checkpoint)
return last_checkpoint
except BaselineMismatchError:
# If HEAD has moved since the the last checkpoint run,
# the specified checkpoint is no longer relevant
self.scm.remove_ref(EXEC_CHECKPOINT)
return None
def _get_last_applied(self) -> Optional[str]:
try:
last_applied = self.scm.get_ref(EXEC_APPLY)
@@ -346,10 +254,6 @@ class Experiments:
self.scm.remove_ref(EXEC_APPLY)
return None
def reset_checkpoints(self):
self.scm.remove_ref(EXEC_CHECKPOINT)
self.scm.remove_ref(EXEC_APPLY)
@unlocked_repo
def _reproduce_queue(
self,
@@ -479,11 +383,6 @@ class Experiments:
return save(self.repo, *args, **kwargs)
def gc(self, *args, **kwargs):
from dvc.repo.experiments.gc import gc
return gc(self.repo, *args, **kwargs)
def push(self, *args, **kwargs):
from dvc.repo.experiments.push import push
+6 -4
View File
@@ -10,6 +10,7 @@ from typing import (
Iterator,
List,
Optional,
Tuple,
Union,
)
@@ -369,12 +370,13 @@ def _describe(
def _sorted_ranges(exp_ranges: Iterable["ExpRange"]) -> List["ExpRange"]:
"""Return list of ExpRange sorted by timestamp."""
"""Return list of ExpRange sorted by (timestamp, rev)."""
def _head_timestamp(exp_range: "ExpRange") -> datetime:
def _head_timestamp(exp_range: "ExpRange") -> Tuple[datetime, str]:
head_exp = first(exp_range.revs)
if head_exp and head_exp.data and head_exp.data.timestamp:
return head_exp.data.timestamp
return datetime.fromtimestamp(0)
return head_exp.data.timestamp, head_exp.rev
return datetime.fromtimestamp(0), ""
return sorted(exp_ranges, key=_head_timestamp, reverse=True)
-15
View File
@@ -31,21 +31,6 @@ class ExperimentExistsError(DvcException):
self.name = name
class CheckpointExistsError(DvcException):
def __init__(self, name: str):
msg = (
"Reproduced checkpoint experiment conflicts with existing "
f"experiment '{name}'. To restart (and overwrite) the existing "
"experiment run:\n\n"
"\tdvc exp run -f ...\n\n"
"To resume the existing experiment, run:\n\n"
f"\tdvc exp apply {name}\n"
"\tdvc exp run\n"
)
super().__init__(msg)
self.name = name
class InvalidExpRefError(DvcException):
def __init__(self, ref):
super().__init__(f"'{ref}' is not a valid experiment refname.")
+11 -80
View File
@@ -6,7 +6,6 @@ from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from enum import IntEnum
from functools import partial
from itertools import chain
from typing import (
TYPE_CHECKING,
@@ -29,18 +28,12 @@ from scmrepo.exceptions import SCMError
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
from dvc.exceptions import DvcException
from dvc.repo.experiments.exceptions import CheckpointExistsError, ExperimentExistsError
from dvc.repo.experiments.refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
ExpRefInfo,
)
from dvc.repo.experiments.exceptions import ExperimentExistsError
from dvc.repo.experiments.refs import EXEC_BASELINE, EXEC_BRANCH, ExpRefInfo
from dvc.repo.experiments.utils import to_studio_params
from dvc.repo.metrics.show import _collect_top_level_metrics
from dvc.repo.params.show import _collect_top_level_params
from dvc.stage.serialize import to_lockfile
from dvc.ui import ui
from dvc.utils import dict_sha256, env2bool, relpath
from dvc.utils.fs import remove
from dvc.utils.studio import env_to_config
@@ -381,7 +374,7 @@ class BaseExecutor(ABC):
dest_scm: "Git",
refs: List[str],
force: bool = False,
on_diverged: Optional[Callable[[str, bool], None]] = None,
on_diverged: Optional[Callable[[str], None]] = None,
**kwargs,
) -> Iterable[str]:
"""Fetch reproduced experiment refs into the specified SCM.
@@ -390,41 +383,32 @@ class BaseExecutor(ABC):
dest_scm: Destination Git instance.
refs: reference names to be fetched from the remotes.
force: If True, diverged refs will be overwritten
on_diverged: Callback in the form on_diverged(ref, is_checkpoint)
on_diverged: Callback in the form on_diverged(ref)
to be called when an experiment ref has diverged.
Extra kwargs will be passed into the remote git client.
"""
if EXEC_CHECKPOINT in refs:
refs.remove(EXEC_CHECKPOINT)
has_checkpoint = True
else:
has_checkpoint = False
def on_diverged_ref(orig_ref: str, new_rev: str):
if force:
logger.debug("Replacing existing experiment '%s'", orig_ref)
return True
if on_diverged:
return on_diverged(orig_ref, has_checkpoint)
return on_diverged(orig_ref)
self._raise_ref_conflict(dest_scm, orig_ref, new_rev, has_checkpoint)
self._raise_ref_conflict(dest_scm, orig_ref, new_rev)
logger.debug("Reproduced existing experiment '%s'", orig_ref)
return False
# fetch experiments
try:
refspecs = [f"{ref}:{ref}" for ref in refs]
# update last run checkpoint (if it exists)
if has_checkpoint:
refspecs.append(f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}")
dest_scm.fetch_refspecs(
self.git_url,
refspecs,
on_diverged=on_diverged_ref,
force=force or has_checkpoint,
force=force,
**kwargs,
)
except SCMError:
@@ -477,6 +461,7 @@ class BaseExecutor(ABC):
from dvc.repo.checkout import checkout as dvc_checkout
from dvc.repo.reproduce import reproduce as dvc_reproduce
from dvc.stage import PipelineStage
from dvc.ui import ui
auto_push = env2bool(DVC_EXP_AUTO_PUSH)
git_remote = os.getenv(DVC_EXP_GIT_REMOTE, None)
@@ -524,44 +509,20 @@ class BaseExecutor(ABC):
repro_dry = kwargs.get("dry")
# NOTE: checkpoint outs are handled as a special type of persist
# out:
#
# - checkpoint out may not yet exist if this is the first time this
# experiment has been run, this is not an error condition for
# experiments
# - if experiment was run with --reset, the checkpoint out will be
# removed at the start of the experiment (regardless of any
# dvc.lock entry for the checkpoint out)
# - if run without --reset, the checkpoint out will be checked out
# using any hash present in dvc.lock (or removed if no entry
# exists in dvc.lock)
checkpoint_reset: bool = kwargs.pop("reset", False)
if not repro_dry:
dvc_checkout(
dvc,
targets=targets,
with_deps=targets is not None,
force=True,
quiet=True,
allow_missing=True,
checkpoint_reset=checkpoint_reset,
recursive=kwargs.get("recursive", False),
)
checkpoint_func = partial(
cls.checkpoint_callback,
dvc,
dvc.scm,
info.name,
repro_force or checkpoint_reset,
)
stages = dvc_reproduce(
dvc,
*args,
on_unchanged=filter_pipeline,
checkpoint_func=checkpoint_func,
**kwargs,
)
if paths := cls._get_top_level_paths(dvc):
@@ -573,7 +534,6 @@ class BaseExecutor(ABC):
ref, exp_ref, repro_force = cls._repro_commit(
dvc,
info,
stages,
exp_hash,
auto_push,
git_remote,
@@ -594,20 +554,17 @@ class BaseExecutor(ABC):
cls,
dvc,
info,
stages,
exp_hash,
auto_push,
git_remote,
repro_force,
message: Optional[str] = None,
) -> Tuple[Optional[str], Optional["ExpRefInfo"], bool]:
is_checkpoint = any(stage.is_checkpoint for stage in stages)
cls.commit(
dvc.scm,
exp_hash,
exp_name=info.name,
force=repro_force,
checkpoint=is_checkpoint,
message=message,
)
if auto_push:
@@ -642,7 +599,6 @@ class BaseExecutor(ABC):
from dvc_studio_client.post_live_metrics import post_live_metrics
from dvc.repo import Repo
from dvc.stage.monitor import CheckpointKilledError
with Repo(os.path.join(info.root_dir, info.dvc_dir)) as dvc:
info.status = TaskStatus.RUNNING
@@ -681,9 +637,6 @@ class BaseExecutor(ABC):
logger.debug("Running repro in '%s'", os.getcwd())
yield dvc
info.status = TaskStatus.SUCCESS
except CheckpointKilledError:
info.status = TaskStatus.FAILED
raise
except DvcException:
if log_errors:
logger.exception("")
@@ -748,24 +701,6 @@ class BaseExecutor(ABC):
exc,
)
@classmethod
def checkpoint_callback(
cls,
dvc: "Repo",
scm: "Git",
name: Optional[str],
force: bool,
unchanged: Iterable["PipelineStage"],
stages: Iterable["PipelineStage"],
):
exp_hash = cls.hash_exp(list(stages) + list(unchanged))
exp_rev = cls.commit(scm, exp_hash, exp_name=name, force=force, checkpoint=True)
if env2bool(DVC_EXP_AUTO_PUSH):
git_remote = os.getenv(DVC_EXP_GIT_REMOTE)
cls._auto_push(dvc, scm, git_remote)
ui.write(f"Checkpoint experiment iteration '{exp_rev[:7]}'.")
@classmethod
def commit(
cls,
@@ -773,7 +708,6 @@ class BaseExecutor(ABC):
exp_hash: str,
exp_name: Optional[str] = None,
force: bool = False,
checkpoint: bool = False,
message: Optional[str] = None,
):
"""Commit stages as an experiment and return the commit SHA."""
@@ -808,23 +742,20 @@ class BaseExecutor(ABC):
scm.commit(message, no_verify=True)
new_rev = scm.get_rev()
if check_conflict:
new_rev = cls._raise_ref_conflict(scm, branch, new_rev, checkpoint)
new_rev = cls._raise_ref_conflict(scm, branch, new_rev)
else:
scm.set_ref(branch, new_rev, old_ref=old_ref)
scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
if checkpoint:
scm.set_ref(EXEC_CHECKPOINT, new_rev)
return new_rev
@staticmethod
def _raise_ref_conflict(scm, ref, new_rev, checkpoint):
def _raise_ref_conflict(scm, ref, new_rev):
# If this commit is a duplicate of the existing commit at 'ref', return
# the existing commit. Otherwise, error out and require user to re-run
# with --force as needed
orig_rev = scm.get_ref(ref)
if scm.diff(orig_rev, new_rev):
if checkpoint:
raise CheckpointExistsError(ref)
raise ExperimentExistsError(ref)
return orig_rev
+1 -10
View File
@@ -10,10 +10,8 @@ from shortuuid import uuid
from dvc.lock import LockError
from dvc.repo.experiments.refs import (
EXEC_APPLY,
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
@@ -124,9 +122,6 @@ class TempDirExecutor(BaseLocalExecutor):
if self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
assert isinstance(self.scm, Git)
@@ -157,7 +152,7 @@ class TempDirExecutor(BaseLocalExecutor):
self, repo: "Repo", rev: str, run_cache: bool = True # noqa: ARG002
):
"""Initialize DVC cache."""
self._update_config({"cache": {"dir": repo.cache.repo.path}})
self._update_config({"cache": {"dir": repo.cache.local_cache_dir}})
def cleanup(self, infofile: Optional[str] = None):
super().cleanup(infofile)
@@ -189,7 +184,6 @@ class WorkspaceExecutor(BaseLocalExecutor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._detach_stack = ExitStack()
self._orig_checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
@classmethod
def from_stash_entry(
@@ -251,6 +245,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
self.scm.remove_ref(EXEC_MERGE)
if self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
if checkpoint and checkpoint != self._orig_checkpoint:
self.scm.set_ref(EXEC_APPLY, checkpoint)
+1 -4
View File
@@ -11,7 +11,6 @@ from funcy import first
from dvc.repo.experiments.refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
@@ -162,7 +161,6 @@ class SSHExecutor(BaseExecutor):
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
else:
self._ssh_cmd(fs, f"git symbolic-ref -d {EXEC_BRANCH}", check=False)
self._ssh_cmd(fs, f"git update-ref -d {EXEC_CHECKPOINT}", check=False)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
@@ -212,8 +210,7 @@ class SSHExecutor(BaseExecutor):
"""Collect DVC cache."""
from dvc.repo.experiments.pull import _pull_cache
with self.get_odb() as odb:
_pull_cache(repo, exp_ref, run_cache=run_cache, odb=odb)
_pull_cache(repo, exp_ref, run_cache=run_cache)
@contextmanager
def get_odb(self):
-53
View File
@@ -1,53 +0,0 @@
import logging
from typing import Optional
from dvc.repo import locked
from .utils import exp_refs, remove_exp_refs
logger = logging.getLogger(__name__)
@locked
def gc(
repo,
all_branches: Optional[bool] = False,
all_tags: Optional[bool] = False,
all_commits: Optional[bool] = False,
workspace: Optional[bool] = False,
commit_date: Optional[str] = None,
queued: Optional[bool] = False,
):
keep_revs = set(
repo.brancher(
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
commit_date=commit_date,
sha_only=True,
)
)
if workspace:
keep_revs.add(repo.scm.get_rev())
if not keep_revs:
return 0
to_remove = [
ref_info
for ref_info in exp_refs(repo.scm)
if ref_info.baseline_sha not in keep_revs
]
remove_exp_refs(repo.scm, to_remove)
removed = len(to_remove)
delete_stashes = []
stash = repo.experiments.celery_queue.stash
for stash_rev, entry in stash.stash_revs.items():
if not queued or entry.baseline_rev not in keep_revs:
delete_stashes.append(stash_rev)
if delete_stashes:
repo.experiments.celery_queue.remove(delete_stashes)
removed += len(delete_stashes)
return removed
-284
View File
@@ -1,284 +0,0 @@
import logging
import os
from contextlib import contextmanager
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
Optional,
TextIO,
Tuple,
Union,
)
from funcy import compact, lremove, lsplit
from dvc.exceptions import DvcException
from dvc.stage import PipelineStage
if TYPE_CHECKING:
from dvc.dependency import Dependency
from dvc.dvcfile import ProjectFile, SingleStageFile
from dvc.repo import Repo
from dvc.ui import ui
PROMPTS = {
"cmd": "[b]Command[/b] to execute",
"code": "Path to a [b]code[/b] file/directory",
"data": "Path to a [b]data[/b] file/directory",
"models": "Path to a [b]model[/b] file/directory",
"params": "Path to a [b]parameters[/b] file",
"metrics": "Path to a [b]metrics[/b] file",
"plots": "Path to a [b]plots[/b] file/directory",
}
def _prompts(
keys: Iterable[str],
defaults: Optional[Dict[str, str]] = None,
validator: Optional[Callable[[str, str], Union[str, Tuple[str, str]]]] = None,
allow_omission: bool = True,
stream: Optional[TextIO] = None,
) -> Dict[str, Optional[str]]:
from dvc.ui.prompt import Prompt
defaults = defaults or {}
return {
key: Prompt.prompt_(
PROMPTS[key],
console=ui.error_console,
default=defaults.get(key),
validator=partial(validator, key) if validator else None,
allow_omission=allow_omission,
stream=stream,
)
for key in keys
}
@contextmanager
def _disable_logging(highest_level=logging.CRITICAL):
previous_level = logging.root.manager.disable
logging.disable(highest_level)
try:
yield
finally:
logging.disable(previous_level)
def init_interactive(
defaults: Dict[str, str],
provided: Dict[str, str],
validator: Optional[Callable[[str, str], Union[str, Tuple[str, str]]]] = None,
stream: Optional[TextIO] = None,
) -> Dict[str, str]:
command_prompts = lremove(provided.keys(), ["cmd"])
dependencies_prompts = lremove(provided.keys(), ["code", "data", "params"])
output_keys = ["models"]
if "live" not in provided:
output_keys.extend(["metrics", "plots"])
outputs_prompts = lremove(provided.keys(), output_keys)
ret: Dict[str, str] = {}
if "cmd" in provided:
ret["cmd"] = provided["cmd"]
for heading, prompts, allow_omission in (
("", command_prompts, False),
("Enter experiment dependencies.", dependencies_prompts, True),
("Enter experiment outputs.", outputs_prompts, True),
):
if prompts and heading:
ui.error_write(heading, styled=True)
response = _prompts(
prompts,
defaults=defaults,
allow_omission=allow_omission,
validator=validator,
stream=stream,
)
ret.update(compact(response))
if prompts:
ui.error_write(styled=True)
return ret
def _check_stage_exists(
dvcfile: Union["ProjectFile", "SingleStageFile"],
name: str,
force: bool = False,
) -> None:
if not force and dvcfile.exists() and name in dvcfile.stages:
from dvc.stage.exceptions import DuplicateStageName
hint = "Use '--force' to overwrite."
raise DuplicateStageName(f"Stage '{name}' already exists in 'dvc.yaml'. {hint}")
def validate_prompts(repo: "Repo", key: str, value: str) -> Union[Any, Tuple[Any, str]]:
from dvc.ui.prompt import InvalidResponse
msg_format = "[yellow]'{0}' does not exist, the {1} will be created.[/]"
if key == "params":
from dvc.dependency.param import (
MissingParamsFile,
ParamsDependency,
ParamsIsADirectoryError,
)
assert isinstance(value, str)
try:
ParamsDependency(None, value, repo=repo).validate_filepath()
except MissingParamsFile:
return value, msg_format.format(value, "file")
except ParamsIsADirectoryError:
raise InvalidResponse( # noqa: B904
f"[prompt.invalid]'{value}' is a directory. "
"Please retry with an existing parameters file."
)
elif key in ("code", "data") and not os.path.exists(value):
typ = "file" if is_file(value) else "directory"
return value, msg_format.format(value, typ)
return value
def is_file(path: str) -> bool:
_, ext = os.path.splitext(path)
return bool(ext)
def init_deps(stage: PipelineStage) -> List["Dependency"]:
from funcy import rpartial
from dvc.dependency import ParamsDependency
from dvc.fs import localfs
new_deps = [dep for dep in stage.deps if not dep.exists]
params, deps = lsplit(rpartial(isinstance, ParamsDependency), new_deps)
# always create a file for params, detect file/folder based on extension
# for other dependencies
dirs = [dep.fs_path for dep in deps if not is_file(dep.fs_path)]
files = [dep.fs_path for dep in deps + params if is_file(dep.fs_path)]
for path in dirs:
localfs.makedirs(path)
for path in files:
localfs.makedirs(localfs.path.parent(path), exist_ok=True)
localfs.touch(path)
return new_deps
def init_out_dirs(stage: PipelineStage) -> List[str]:
from dvc.fs import localfs
new_dirs = []
# create dirs for outputs
for out in stage.outs:
path = out.def_path
if is_file(path):
path = localfs.path.parent(path)
if path and not localfs.exists(path):
localfs.makedirs(path)
new_dirs.append(path)
return new_dirs
def init(
repo: "Repo",
name: str = "train",
type: str = "default", # noqa: A002, pylint: disable=redefined-builtin
defaults: Optional[Dict[str, str]] = None,
overrides: Optional[Dict[str, str]] = None,
interactive: bool = False,
force: bool = False,
stream: Optional[TextIO] = None,
) -> Tuple[PipelineStage, List["Dependency"], List[str]]:
from dvc.dvcfile import PROJECT_FILE, load_file
dvcfile = load_file(repo, PROJECT_FILE)
_check_stage_exists(dvcfile, name, force=force)
defaults = defaults.copy() if defaults else {}
overrides = overrides.copy() if overrides else {}
if interactive:
defaults = init_interactive(
validator=partial(validate_prompts, repo),
defaults=defaults,
provided=overrides,
stream=stream,
)
elif "live" in overrides:
# suppress `metrics`/`plots` if live is selected.
defaults.pop("metrics", None)
defaults.pop("plots", None)
else:
defaults.pop("live", None) # suppress live otherwise
context: Dict[str, str] = {**defaults, **overrides}
assert "cmd" in context
params = context.get("params")
if params:
from dvc.dependency.param import (
MissingParamsFile,
ParamsDependency,
ParamsIsADirectoryError,
)
try:
ParamsDependency(None, params, repo=repo).validate_filepath()
except ParamsIsADirectoryError as exc:
raise DvcException(f"{exc}.") # noqa: B904 # swallow cause for display
except MissingParamsFile:
pass
if type == "checkpoint":
outs_key = "checkpoints"
metrics_key = "metrics_persist_no_cache"
plots_key = "plots_persist_no_cache"
else:
outs_key = "outs"
metrics_key = "metrics_no_cache"
plots_key = "plots_no_cache"
models = [context.get("models")]
metrics = [context.get("metrics")]
plots = [context.get("plots")]
if live_path := context.pop("live", None):
metrics.append(os.path.join(live_path, "metrics.json"))
plots.append(os.path.join(live_path, "plots"))
stage = repo.stage.create(
name=name,
cmd=context["cmd"],
deps=compact([context.get("code"), context.get("data")]),
params=[{params: None}] if params else None,
force=force,
**{
outs_key: compact(models),
metrics_key: compact(metrics),
plots_key: compact(plots),
},
)
assert isinstance(stage, PipelineStage)
with _disable_logging(), repo.scm_context(autostage=True, quiet=True):
stage.dump(update_lock=False)
initialized_out_dirs = init_out_dirs(stage)
stage.ignore_outs()
initialized_deps = init_deps(stage)
if params:
repo.scm_context.track_file(params)
return stage, initialized_deps, initialized_out_dirs
+14 -5
View File
@@ -1,6 +1,6 @@
import logging
from collections import defaultdict
from typing import List, Optional, Union
from typing import Dict, List, Optional, Tuple, Union
from dvc.repo import locked
from dvc.repo.scm_context import scm_context
@@ -19,7 +19,11 @@ def ls(
all_commits: bool = False,
num: int = 1,
git_remote: Optional[str] = None,
):
) -> Dict[str, List[Tuple[str, Optional[str]]]]:
"""List experiments.
Returns a dict mapping baseline revs to a list of (exp_name, exp_sha) tuples.
"""
rev_set = None
if not all_commits:
rev = rev or "HEAD"
@@ -36,9 +40,14 @@ def ls(
results = defaultdict(list)
for baseline in ref_info_dict:
name = baseline[:7]
name = baseline
if tags[baseline] or ref_heads[baseline]:
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
results[name] = [info.name for info in ref_info_dict[baseline]]
name = tags[baseline] or ref_heads[baseline]
for info in ref_info_dict[baseline]:
if git_remote:
exp_rev = None
else:
exp_rev = repo.scm.get_ref(str(info))
results[name].append((info.name, exp_rev))
return results
+25 -26
View File
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
def pull( # noqa: C901
repo,
git_remote: str,
exp_names: Union[Iterable[str], str],
exp_names: Optional[Union[Iterable[str], str]] = None,
all_commits=False,
rev: Optional[Union[List[str], str]] = None,
num=1,
@@ -32,30 +32,30 @@ def pull( # noqa: C901
exp_ref_set: Set["ExpRefInfo"] = set()
if all_commits:
exp_ref_set.update(exp_refs(repo.scm, git_remote))
elif exp_names:
if isinstance(exp_names, str):
exp_names = [exp_names]
exp_ref_dict = resolve_name(repo.scm, exp_names, git_remote)
unresolved_exp_names = []
for exp_name, exp_ref in exp_ref_dict.items():
if exp_ref is None:
unresolved_exp_names.append(exp_name)
else:
exp_ref_set.add(exp_ref)
if unresolved_exp_names:
raise UnresolvedExpNamesError(unresolved_exp_names)
else:
if exp_names:
if isinstance(exp_names, str):
exp_names = [exp_names]
exp_ref_dict = resolve_name(repo.scm, exp_names, git_remote)
unresolved_exp_names = []
for exp_name, exp_ref in exp_ref_dict.items():
if exp_ref is None:
unresolved_exp_names.append(exp_name)
else:
exp_ref_set.add(exp_ref)
if unresolved_exp_names:
raise UnresolvedExpNamesError(unresolved_exp_names)
if rev:
if isinstance(rev, str):
rev = [rev]
rev_dict = iter_revs(repo.scm, rev, num)
rev_set = set(rev_dict.keys())
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
for _, ref_info_list in ref_info_dict.items():
exp_ref_set.update(ref_info_list)
rev = rev or "HEAD"
if isinstance(rev, str):
rev = [rev]
rev_dict = iter_revs(repo.scm, rev, num)
rev_set = set(rev_dict.keys())
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
for _, ref_info_list in ref_info_dict.items():
exp_ref_set.update(ref_info_list)
pull_result = _pull(repo, git_remote, exp_ref_set, force)
@@ -107,10 +107,9 @@ def _pull_cache(
dvc_remote=None,
jobs=None,
run_cache=False,
odb=None,
):
if isinstance(refs, ExpRefInfo):
refs = [refs]
revs = list(exp_commits(repo.scm, refs))
logger.debug("dvc fetch experiment '%s'", refs)
repo.fetch(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs, odb=odb)
repo.fetch(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
+3 -2
View File
@@ -97,7 +97,7 @@ def exp_refs_from_rev(scm: "Git", rev: List[str], num: int = 1) -> Set["ExpRefIn
def push(
repo: "Repo",
git_remote: str,
exp_names: Union[List[str], str],
exp_names: Optional[Union[List[str], str]] = None,
all_commits: bool = False,
rev: Optional[Union[List[str], str]] = None,
num: int = 1,
@@ -111,7 +111,8 @@ def push(
exp_ref_set.update(exp_refs(repo.scm))
if exp_names:
exp_ref_set.update(exp_refs_from_names(repo.scm, ensure_list(exp_names)))
if rev:
else:
rev = rev or "HEAD"
if isinstance(rev, str):
rev = [rev]
exp_ref_set.update(exp_refs_from_rev(repo.scm, rev, num=num))
+5 -84
View File
@@ -21,10 +21,9 @@ from dvc_studio_client.post_live_metrics import get_studio_config
from funcy import retry
from dvc.dependency import ParamsDependency
from dvc.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME
from dvc.lock import LockError
from dvc.repo.experiments.exceptions import CheckpointExistsError, ExperimentExistsError
from dvc.repo.experiments.exceptions import ExperimentExistsError
from dvc.repo.experiments.executor.base import BaseExecutor
from dvc.repo.experiments.executor.local import WorkspaceExecutor
from dvc.repo.experiments.refs import ExpRefInfo
@@ -32,11 +31,9 @@ from dvc.repo.experiments.stash import ExpStash, ExpStashEntry
from dvc.repo.experiments.utils import (
EXEC_PID_DIR,
EXEC_TMP_DIR,
exp_refs_by_rev,
get_exp_rwlock,
get_random_exp_name,
)
from dvc.ui import ui
from dvc.utils.objects import cached_property
from dvc.utils.studio import config_to_env
@@ -300,11 +297,10 @@ class BaseStashQueue(ABC):
output.
"""
def _stash_exp( # noqa: PLR0915, C901
def _stash_exp( # noqa: C901
self,
*args,
params: Optional[Dict[str, List[str]]] = None,
resume_rev: Optional[str] = None,
baseline_rev: Optional[str] = None,
branch: Optional[str] = None,
name: Optional[str] = None,
@@ -315,7 +311,6 @@ class BaseStashQueue(ABC):
Args:
params: Dict mapping paths to `Hydra Override`_ patterns,
provided via `exp run --set-param`.
resume_rev: Optional checkpoint resume rev.
baseline_rev: Optional baseline rev for this experiment, defaults
to the current SCM rev.
branch: Optional experiment branch name. If specified, the
@@ -338,17 +333,6 @@ class BaseStashQueue(ABC):
if workspace:
self.stash.apply(workspace)
if resume_rev:
# move HEAD to the resume rev so that the stashed diff
# only contains changes relative to resume rev
stash_head = resume_rev
self.scm.set_ref(
"HEAD",
resume_rev,
message=f"dvc: resume from HEAD {resume_rev[:7]}",
)
self.scm.reset()
# update experiment params from command line
if params:
self._update_params(params)
@@ -357,60 +341,6 @@ class BaseStashQueue(ABC):
# & tempdir runs
self._stash_commit_deps(*args, **kwargs)
if resume_rev:
if branch:
branch_name = ExpRefInfo.from_ref(branch).name
else:
branch_name = f"{resume_rev[:7]}"
if self.scm.is_dirty(untracked_files=False):
ui.write(
(
"Modified checkpoint experiment based on "
f"'{branch_name}' will be created"
),
)
branch = None
elif not branch or self.scm.get_ref(branch) != resume_rev:
err_msg = [
"Nothing to do for unchanged checkpoint "
f"'{resume_rev[:7]}'. "
]
if branch:
err_msg.append(
"To resume from the head of this "
"experiment, use "
f"'dvc exp apply {branch_name}'."
)
else:
names = [
ref_info.name
for ref_info in exp_refs_by_rev(
self.scm, resume_rev
)
]
if len(names) > 3:
names[3:] = [f"... ({len(names) - 3} more)"]
err_msg.append(
"To resume an experiment containing this "
"checkpoint, apply one of these heads:\n"
"\t{}".format(", ".join(names))
)
raise DvcException("".join(err_msg))
else:
ui.write(
"Existing checkpoint experiment "
f"'{branch_name}' will be resumed"
)
if name:
logger.warning(
(
"Ignoring option '--name %s' for resumed "
"experiment. Existing experiment name will"
"be preserved instead."
),
name,
)
# save additional repro command line arguments
run_env = {
DVC_EXP_BASELINE_REV: baseline_rev,
@@ -418,8 +348,6 @@ class BaseStashQueue(ABC):
if not name:
name = get_random_exp_name(self.scm, baseline_rev)
run_env[DVC_EXP_NAME] = name
if resume_rev:
run_env[DVCLIVE_RESUME] = "1"
# save studio config to read later by dvc and dvclive
studio_config = get_studio_config(
@@ -445,12 +373,6 @@ class BaseStashQueue(ABC):
baseline_rev[:7],
)
finally:
if resume_rev:
# NOTE: this set_ref + reset() is equivalent to
# `git reset orig_head` (our SCM reset() only operates
# on HEAD rather than any arbitrary commit)
self.scm.set_ref("HEAD", orig_head, message="dvc: restore HEAD")
self.scm.reset()
# Revert any of our changes before prior unstashing
self.scm.reset(hard=True)
@@ -633,10 +555,8 @@ class BaseStashQueue(ABC):
) -> Dict[str, str]:
results = {}
def on_diverged(ref: str, checkpoint: bool):
def on_diverged(ref: str):
ref_info = ExpRefInfo.from_ref(ref)
if checkpoint:
raise CheckpointExistsError(ref_info.name)
raise ExperimentExistsError(ref_info.name)
refs = get_remote_executor_refs(exp.scm, executor.git_url)
@@ -785,6 +705,7 @@ class BaseStashQueue(ABC):
def active_repo(self, name: str) -> "Repo":
"""Return a Repo for the specified active experiment if it exists."""
from dvc.exceptions import DvcException
from dvc.repo import Repo
from dvc.repo.experiments.exceptions import (
ExpNotStartedError,
-8
View File
@@ -100,8 +100,6 @@ class TempDirQueue(WorkspaceQueue):
message: Optional[str] = None,
**kwargs,
) -> Dict[str, Dict[str, str]]:
from dvc.stage.monitor import CheckpointKilledError
results: Dict[str, Dict[str, str]] = defaultdict(dict)
exec_name = self._EXEC_NAME or entry.stash_rev
infofile = self.get_infofile_path(exec_name)
@@ -122,12 +120,6 @@ class TempDirQueue(WorkspaceQueue):
results[rev].update(
self.collect_executor(self.repo.experiments, executor, exec_result)
)
except CheckpointKilledError:
results[rev].update(
self.collect_executor(self.repo.experiments, executor, exec_result)
)
return results
except DvcException:
raise
except Exception as exc: # noqa: BLE001
+3 -10
View File
@@ -4,12 +4,7 @@ from typing import TYPE_CHECKING, Dict, List
from scmrepo.exceptions import SCMError
from dvc.repo.experiments.executor.base import ExecutorInfo, TaskStatus
from dvc.repo.experiments.refs import (
EXEC_CHECKPOINT,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
EXPS_STASH,
)
from dvc.repo.experiments.refs import EXEC_NAMESPACE, EXPS_NAMESPACE, EXPS_STASH
from dvc.repo.experiments.utils import get_exp_rwlock, iter_remote_refs
logger = logging.getLogger(__name__)
@@ -33,9 +28,7 @@ def get_remote_executor_refs(scm: "Git", remote_url: str) -> List[str]:
remote_url,
base=EXPS_NAMESPACE,
):
if ref == EXEC_CHECKPOINT or (
not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH
):
if not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
refs.append(ref)
return refs
@@ -67,7 +60,7 @@ def fetch_running_exp_from_temp_dir(
result[rev] = info.asdict()
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
def on_diverged(_ref: str, _checkpoint: bool):
def on_diverged(_ref: str):
return True
executor = TempDirExecutor.from_info(info)
-4
View File
@@ -101,7 +101,6 @@ class WorkspaceQueue(BaseStashQueue):
self, entry: QueueEntry, executor: "BaseExecutor", **kwargs
) -> Dict[str, Dict[str, str]]:
kwargs.pop("copy_paths", None)
from dvc.stage.monitor import CheckpointKilledError
from dvc_task.proc.process import ProcessInfo
results: Dict[str, Dict[str, str]] = defaultdict(dict)
@@ -127,9 +126,6 @@ class WorkspaceQueue(BaseStashQueue):
results[rev].update(
self.collect_executor(self.repo.experiments, executor, exec_result)
)
except CheckpointKilledError:
# Checkpoint errors have already been logged
return {}
except DvcException:
raise
except Exception as exc: # noqa: BLE001
-1
View File
@@ -15,7 +15,6 @@ CELERY_STASH = f"{CELERY_NAMESPACE}/stash"
CELERY_FAILED_STASH = f"{CELERY_NAMESPACE}/failed"
EXEC_NAMESPACE = f"{EXPS_NAMESPACE}/exec"
EXEC_APPLY = f"{EXEC_NAMESPACE}/EXEC_APPLY"
EXEC_CHECKPOINT = f"{EXEC_NAMESPACE}/EXEC_CHECKPOINT"
EXEC_BRANCH = f"{EXEC_NAMESPACE}/EXEC_BRANCH"
EXEC_BASELINE = f"{EXEC_NAMESPACE}/EXEC_BASELINE"
EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
-3
View File
@@ -87,9 +87,6 @@ def run( # noqa: C901, PLR0912
else:
sweeps = [path_overrides]
if not kwargs.get("checkpoint_resume", None):
kwargs["reset"] = True
for idx, sweep_overrides in enumerate(sweeps):
if hydra_sweep and name_prefix is not None:
kwargs["name"] = f"{name_prefix}-{idx+1}"
+1 -8
View File
@@ -60,6 +60,7 @@ class SerializableExp:
# 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,
@@ -69,7 +70,6 @@ class SerializableExp:
onerror=onerror_collect,
)
)
meta = cls._gather_meta(repo)
return cls(
rev=rev,
params=params,
@@ -94,16 +94,9 @@ class SerializableExp:
for out in repo.index.outs
if not (out.is_metric or out.is_plot)
},
meta=meta,
**kwargs,
)
@staticmethod
def _gather_meta(repo: "Repo") -> Dict[str, Any]:
return {
"has_checkpoints": any(stage.is_checkpoint for stage in repo.index.stages)
}
def dumpd(self) -> Dict[str, Any]:
return asdict(self)
+1 -1
View File
@@ -21,7 +21,7 @@ class ExpStashEntry(NamedTuple):
is not pushed onto the stash ref.
head_rev: HEAD Git commit to be checked out for this experiment.
baseline_rev: Experiment baseline commit.
branch: Optional exp (checkpoint) branch name for this experiment.
branch: Optional branch name for this experiment.
name: Optional exp name.
"""
-4
View File
@@ -26,7 +26,6 @@ from .refs import (
EXEC_APPLY,
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXPS_NAMESPACE,
ITER_SKIP_NAMESPACES,
STASHES,
@@ -221,7 +220,6 @@ def exp_commits(
def remove_exp_refs(scm: "Git", ref_infos: Iterable[ExpRefInfo]):
exec_branch = scm.get_ref(EXEC_BRANCH, follow=False)
exec_apply = scm.get_ref(EXEC_APPLY)
exec_checkpoint = scm.get_ref(EXEC_CHECKPOINT)
for ref_info in ref_infos:
ref = scm.get_ref(str(ref_info))
@@ -229,8 +227,6 @@ def remove_exp_refs(scm: "Git", ref_infos: Iterable[ExpRefInfo]):
scm.remove_ref(EXEC_BRANCH)
if exec_apply and exec_apply == ref:
scm.remove_ref(EXEC_APPLY)
if exec_checkpoint and exec_checkpoint == ref:
scm.remove_ref(EXEC_CHECKPOINT)
scm.remove_ref(str(ref_info))
+51 -138
View File
@@ -1,20 +1,9 @@
import logging
from contextlib import suppress
from typing import TYPE_CHECKING, Optional, Sequence
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError
from dvc.fs import Schemes
from . import locked
if TYPE_CHECKING:
from dvc.data_cloud import Remote
from dvc.repo import Repo
from dvc.types import TargetType
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.transfer import TransferResult
logger = logging.getLogger(__name__)
@@ -31,7 +20,6 @@ def fetch( # noqa: C901, PLR0913
all_commits=False,
run_cache=False,
revs=None,
odb: Optional["HashFileDB"] = None,
) -> int:
"""Download data items from a cloud and imported repositories
@@ -45,18 +33,15 @@ def fetch( # noqa: C901, PLR0913
config.NoRemoteError: thrown when downloading only local files and no
remote is configured
"""
from dvc.repo.imports import save_imports
from dvc_data.hashfile.transfer import TransferResult
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
if isinstance(targets, str):
targets = [targets]
worktree_remote: Optional["Remote"] = None
with suppress(NoRemoteError):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree or _remote.fs.version_aware:
worktree_remote = _remote
failed_count = 0
transferred_count = 0
@@ -66,133 +51,61 @@ def fetch( # noqa: C901, PLR0913
except DownloadError as exc:
failed_count += exc.amount
no_remote_msg: Optional[str] = None
result = TransferResult(set(), set())
try:
if worktree_remote is not None:
transferred_count += _fetch_worktree(
self,
worktree_remote,
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
targets=targets,
jobs=jobs,
with_deps=with_deps,
recursive=recursive,
)
else:
d, f = _fetch(
self,
targets,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
revs=revs,
odb=odb,
)
result.transferred.update(d)
result.failed.update(f)
except NoRemoteError as exc:
no_remote_msg = str(exc)
for rev in self.brancher(
indexes = []
index_keys = set()
for _ in self.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
):
imported = save_imports(
self,
targets,
unpartial=not rev or rev == "workspace",
recursive=recursive,
saved_remote = self.config["core"].get("remote")
try:
if remote:
self.config["core"]["remote"] = remote
idx = self.index.targets_view(
targets,
with_deps=with_deps,
recursive=recursive,
)
index_keys.add(idx.data_tree.hash_info.value)
indexes.append(idx.data["repo"])
finally:
if remote:
self.config["core"]["remote"] = saved_remote
cache_key = ("fetch", tokenize(sorted(index_keys)))
with Callback.as_tqdm_callback(
desc="Collecting",
unit="entry",
) as cb:
data = collect(
indexes, cache_index=self.data_index, cache_key=cache_key, callback=cb
)
result.transferred.update(imported)
result.failed.difference_update(imported)
failed_count += len(result.failed)
with Callback.as_tqdm_callback(
desc="Fetching",
unit="file",
) as cb:
try:
fetch_transferred, fetch_failed = ifetch(
data,
jobs=jobs,
callback=cb,
) # pylint: disable=assignment-from-no-return
finally:
for fs_index in data.values():
fs_index.close()
if fetch_transferred:
# NOTE: dropping cached index to force reloading from newly saved cache
self.drop_data_index()
transferred_count += fetch_transferred
failed_count += fetch_failed
if failed_count:
if no_remote_msg:
logger.error(no_remote_msg)
raise DownloadError(failed_count)
transferred_count += len(result.transferred)
return transferred_count
def _fetch(
repo: "Repo",
targets: "TargetType",
remote: Optional[str] = None,
jobs: Optional[int] = None,
odb: Optional["HashFileDB"] = None,
**kwargs,
) -> "TransferResult":
from dvc_data.hashfile.transfer import TransferResult
result = TransferResult(set(), set())
used = repo.used_objs(
targets,
remote=remote,
jobs=jobs,
**kwargs,
)
if odb:
all_ids = set()
for _odb, obj_ids in used.items():
all_ids.update(obj_ids)
d, f = repo.cloud.pull(
all_ids,
jobs=jobs,
remote=remote,
odb=odb,
)
result.transferred.update(d)
result.failed.update(f)
else:
for src_odb, obj_ids in sorted(
used.items(),
key=lambda item: item[0] is not None
and item[0].fs.protocol == Schemes.MEMORY,
):
d, f = repo.cloud.pull(
obj_ids,
jobs=jobs,
remote=remote,
odb=src_odb,
)
result.transferred.update(d)
result.failed.update(f)
return result
def _fetch_worktree(
repo: "Repo",
remote: "Remote",
revs: Optional[Sequence[str]] = None,
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
targets: Optional["TargetType"] = None,
jobs: Optional[int] = None,
**kwargs,
) -> int:
from dvc.repo.worktree import fetch_worktree
downloaded = 0
for _ in repo.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
):
downloaded += fetch_worktree(repo, remote, targets=targets, jobs=jobs, **kwargs)
return downloaded
+2 -2
View File
@@ -1,6 +1,6 @@
def imp(self, url, path, out=None, fname=None, rev=None, **kwargs):
def imp(self, url, path, out=None, rev=None, **kwargs):
erepo = {"url": url}
if rev is not None:
erepo["rev"] = rev
return self.imp_url(path, out=out, fname=fname, erepo=erepo, frozen=True, **kwargs)
return self.imp_url(path, out=out, erepo=erepo, frozen=True, **kwargs)
+1 -9
View File
@@ -18,17 +18,12 @@ def imp_url( # noqa: C901, PLR0913
self: "Repo",
url,
out=None,
fname=None,
erepo=None,
frozen=True,
no_download=False,
no_exec=False,
remote=None,
to_remote=False,
desc=None,
type=None, # noqa: A002, pylint: disable=redefined-builtin
labels=None,
meta=None,
jobs=None,
force=False,
fs_config=None,
@@ -62,7 +57,7 @@ def imp_url( # noqa: C901, PLR0913
stage = self.stage.create(
single_stage=True,
validate=False,
fname=fname or path,
fname=path,
wdir=wdir,
deps=[url],
outs=[out],
@@ -70,9 +65,6 @@ def imp_url( # noqa: C901, PLR0913
fs_config=fs_config,
)
out_obj = stage.outs[0]
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
try:
self.check_graph(stages={stage})
except OutputDuplicationError as exc:
+45 -29
View File
@@ -1,5 +1,6 @@
import logging
import os
from functools import partial
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, List, Set, Tuple, Union
@@ -17,17 +18,18 @@ logger = logging.getLogger(__name__)
def unfetched_view(
index: "Index", targets: "TargetType", unpartial: bool = False, **kwargs
) -> Tuple["IndexView", List["Dependency"]]:
) -> Tuple["IndexView", "IndexView", List["Dependency"]]:
"""Return index view of imports which have not been fetched.
Returns:
Tuple in the form (view, changed_deps) where changed_imports is a list
of import dependencies that cannot be fetched due to changed data
source.
Tuple in the form (legacy_view, view, changed_deps) where changed_imports is a
list of import dependencies that cannot be fetched due to changed data source.
"""
from dvc.cachemgr import LEGACY_HASH_NAMES
changed_deps: List["Dependency"] = []
def need_fetch(stage: "Stage") -> bool:
def need_fetch(stage: "Stage", legacy: bool = False) -> bool:
if not stage.is_import or (stage.is_partial_import and not unpartial):
return False
@@ -40,10 +42,19 @@ def unfetched_view(
changed_deps.append(dep)
return False
return True
if out.hash_name in LEGACY_HASH_NAMES and legacy:
return True
if out.hash_name not in LEGACY_HASH_NAMES and not legacy:
return True
return False
legacy_unfetched = index.targets_view(
targets,
stage_filter=partial(need_fetch, legacy=True),
**kwargs,
)
unfetched = index.targets_view(targets, stage_filter=need_fetch, **kwargs)
return unfetched, changed_deps
return legacy_unfetched, unfetched, changed_deps
def partial_view(index: "Index", targets: "TargetType", **kwargs) -> "IndexView":
@@ -89,38 +100,43 @@ def save_imports(
Objects which were downloaded from source location.
"""
from dvc.stage.exceptions import DataSourceChanged
from dvc_data.index import checkout, md5, save
from dvc_data.index import md5, save
from dvc_data.index.checkout import apply, compare
from dvc_objects.fs.callbacks import Callback
downloaded: Set["HashInfo"] = set()
unfetched, changed = unfetched_view(
legacy_unfetched, unfetched, changed = unfetched_view(
repo.index, targets, unpartial=unpartial, **kwargs
)
for dep in changed:
logger.warning(str(DataSourceChanged(f"{dep.stage} ({dep})")))
data_view = unfetched.data["repo"]
if len(data_view):
cache = repo.cache.local
if not cache.fs.exists(cache.path):
os.makedirs(cache.path)
with TemporaryDirectory(dir=cache.path) as tmpdir:
with Callback.as_tqdm_callback(
desc="Downloading imports from source",
unit="files",
) as cb:
checkout(data_view, tmpdir, cache.fs, callback=cb, storage="data")
md5(data_view)
save(data_view, odb=cache, hardlink=True)
for view, cache in [
(legacy_unfetched, repo.cache.legacy),
(unfetched, repo.cache.local),
]:
data_view = view.data["repo"]
if len(data_view):
if not cache.fs.exists(cache.path):
os.makedirs(cache.path)
with TemporaryDirectory(dir=cache.path) as tmpdir:
with Callback.as_tqdm_callback(
desc="Downloading imports from source",
unit="files",
) as cb:
diff = compare(None, data_view)
apply(diff, tmpdir, cache.fs, callback=cb, storage="remote")
md5(data_view, name=cache.hash_name)
save(data_view, odb=cache, hardlink=True)
downloaded.update(
entry.hash_info
for _, entry in data_view.iteritems()
if entry.meta is not None
and not entry.meta.isdir
and entry.hash_info is not None
)
downloaded.update(
entry.hash_info
for _, entry in data_view.iteritems()
if entry.meta is not None
and not entry.meta.isdir
and entry.hash_info is not None
)
if unpartial:
unpartial_imports(partial_view(repo.index, targets, **kwargs))
+98 -8
View File
@@ -19,6 +19,7 @@ from typing import (
from funcy.debug import format_time
from dvc.fs import LocalFileSystem
from dvc.fs.callbacks import DEFAULT_CALLBACK
from dvc.utils.objects import cached_property
if TYPE_CHECKING:
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
from pygtrie import Trie
from dvc.dependency import Dependency, ParamsDependency
from dvc.fs.callbacks import Callback
from dvc.output import Output
from dvc.repo import Repo
from dvc.repo.stage import StageInfo
@@ -113,14 +115,18 @@ def collect_files(
def _load_data_from_outs(index, prefix, outs):
from dvc_data.index import DataIndexEntry
from dvc_data.index import DataIndexEntry, Meta
parents = set()
for out in outs:
if not out.use_cache:
continue
ws, key = out.index_key
for key_len in range(1, len(key)):
parents.add((ws, key[:key_len]))
entry = DataIndexEntry(
key=key,
meta=out.meta,
@@ -130,15 +136,25 @@ def _load_data_from_outs(index, prefix, outs):
if out.stage.is_import and not out.stage.is_repo_import:
dep = out.stage.deps[0]
entry.meta = dep.meta
entry.hash_info = dep.hash_info
if out.hash_info:
entry.hash_info = out.hash_info
else:
# partial import
entry.hash_info = dep.hash_info
# FIXME PyGTrie-based DataIndex doesn't remove entry.key during
# index.add, so we have to set the entry manually here to make
# index.view() work correctly.
index[(*prefix, ws, *key)] = entry
for ws, key in parents:
index[(*prefix, ws, *key)] = DataIndexEntry(
key=key, meta=Meta(isdir=True), loaded=True
)
def _load_storage_from_out(storage_map, key, out):
from dvc.cachemgr import LEGACY_HASH_NAMES
from dvc.config import NoRemoteError
from dvc_data.index import FileStorage, ObjectStorage
@@ -157,13 +173,29 @@ def _load_storage_from_out(storage_map, key, out):
)
)
else:
storage_map.add_remote(ObjectStorage(key, remote.odb, index=remote.index))
odb = (
remote.legacy_odb if out.hash_name in LEGACY_HASH_NAMES else remote.odb
)
storage_map.add_remote(ObjectStorage(key, odb, index=remote.index))
except NoRemoteError:
pass
if out.stage.is_import:
dep = out.stage.deps[0]
storage_map.add_data(FileStorage(key, dep.fs, dep.fs_path))
if not out.hash_info:
from fsspec.utils import tokenize
# partial import
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)
),
)
)
storage_map.add_remote(FileStorage(key, dep.fs, dep.fs_path))
class Index:
@@ -534,10 +566,12 @@ class IndexView:
lambda: _DataPrefixes(set(), set())
)
for out, filter_info in self._filtered_outs:
if not out.use_cache:
continue
workspace, key = out.index_key
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
key = key + out.fs.path.relparts(filter_info, out.fs_path)
entry = self._index.data[workspace][key]
entry = self._index.data[workspace].get(key)
if entry and entry.meta and entry.meta.isdir:
prefixes[workspace].recursive.add(key)
prefixes[workspace].explicit.update(key[:i] for i in range(len(key), 0, -1))
@@ -550,6 +584,9 @@ class IndexView:
ret: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
for out, filter_info in self._filtered_outs:
if not out.use_cache:
continue
workspace, key = out.index_key
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
key = key + out.fs.path.relparts(filter_info, out.fs_path)
@@ -557,6 +594,23 @@ class IndexView:
return dict(ret)
@cached_property
def data_tree(self):
from dvc_data.hashfile.tree import Tree
tree = Tree()
for out in self.outs:
if not out.use_cache:
continue
ws, key = out.index_key
tree.add((ws, *key), out.meta, out.hash_info)
tree.digest()
return tree
@cached_property
def data(self) -> Dict[str, Union["DataIndex", "DataIndexView"]]:
from dvc_data.index import DataIndex, view
@@ -579,14 +633,15 @@ class IndexView:
return data
def build_data_index(
def build_data_index( # noqa: C901
index: Union["Index", "IndexView"],
path: str,
fs: "FileSystem",
workspace: str = "repo",
compute_hash: Optional[bool] = False,
callback: "Callback" = DEFAULT_CALLBACK,
) -> "DataIndex":
from dvc_data.index import DataIndex, DataIndexEntry
from dvc_data.index import DataIndex, DataIndexEntry, Meta
from dvc_data.index.build import build_entries, build_entry
from dvc_data.index.save import build_tree
@@ -595,21 +650,31 @@ def build_data_index(
ignore = index.repo.dvcignore
data = DataIndex()
parents = set()
for key in index.data_keys.get(workspace, set()):
out_path = fs.path.join(path, *key)
for key_len in range(1, len(key)):
parents.add(key[:key_len])
if not fs.exists(out_path):
continue
hash_name = _get_entry_hash_name(index, workspace, key)
try:
out_entry = build_entry(
out_path,
fs,
compute_hash=compute_hash,
state=index.repo.state,
hash_name=hash_name,
)
except FileNotFoundError:
out_entry = DataIndexEntry()
out_entry.key = key
data.add(out_entry)
callback.relative_update(1)
if not out_entry.meta or not out_entry.meta.isdir:
continue
@@ -620,6 +685,7 @@ def build_data_index(
compute_hash=compute_hash,
state=index.repo.state,
ignore=ignore,
hash_name=hash_name,
):
if not entry.key or entry.key == ("",):
# NOTE: whether the root will be returned by build_entries
@@ -628,12 +694,36 @@ def build_data_index(
entry.key = key + entry.key
data.add(entry)
callback.relative_update(1)
if compute_hash:
tree_meta, tree = build_tree(data, key)
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):
continue
direntry = DataIndexEntry(key=key, meta=Meta(isdir=True), loaded=True)
data.add(direntry)
callback.relative_update(1)
return data
def _get_entry_hash_name(
index: Union["Index", "IndexView"], workspace: str, key: "DataIndexKey"
) -> str:
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
try:
src_entry = index.data[workspace][key]
if src_entry.hash_info and src_entry.hash_info.name:
return src_entry.hash_info.name
except KeyError:
pass
return DEFAULT_ALGORITHM
+1 -1
View File
@@ -129,7 +129,7 @@ def _get_remote_config(url):
name = "auto-generated-upstream"
return {
"core": {"remote": name},
"remote": {name: {"url": repo.cache.local.path}},
"remote": {name: {"url": repo.cache.local_cache_dir}},
}
# Use original remote to make sure that we are using correct url,
+6 -9
View File
@@ -479,15 +479,12 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
for dvcfile, plots_def in top_plots.items():
dvcfile_path = _relpath(repo.dvcfs, dvcfile)
dvcfile_defs_dict: Dict[str, Union[Dict, None]] = {}
if isinstance(plots_def, list):
for elem in plots_def:
if isinstance(elem, str):
dvcfile_defs_dict[elem] = None
else:
k, v = list(elem.items())[0]
dvcfile_defs_dict[k] = v
else:
dvcfile_defs_dict = plots_def
for elem in plots_def:
if isinstance(elem, str):
dvcfile_defs_dict[elem] = None
else:
k, v = list(elem.items())[0]
dvcfile_defs_dict[k] = v
resolved = _resolve_definitions(
repo.dvcfs,
-6
View File
@@ -1,12 +1,8 @@
import logging
from typing import TYPE_CHECKING, Optional
from dvc.repo import locked
from dvc.utils import glob_targets
if TYPE_CHECKING:
from dvc_objects.db import ObjectDB
logger = logging.getLogger(__name__)
@@ -24,7 +20,6 @@ def pull( # noqa: PLR0913
all_commits=False,
run_cache=False,
glob=False,
odb: Optional["ObjectDB"] = None,
allow_missing=False,
):
if isinstance(targets, str):
@@ -42,7 +37,6 @@ def pull( # noqa: PLR0913
with_deps=with_deps,
recursive=recursive,
run_cache=run_cache,
odb=odb,
)
stats = self.checkout(
targets=expanded_targets,
+5 -44
View File
@@ -1,11 +1,9 @@
import logging
from functools import partial
from typing import TYPE_CHECKING, Iterator, List
from dvc.exceptions import DvcException, ReproductionError
from dvc.exceptions import ReproductionError
from dvc.repo.scm_context import scm_context
from dvc.stage.cache import RunCacheNotSupported
from dvc.stage.exceptions import CheckpointKilledError
from . import locked
@@ -18,21 +16,6 @@ logger = logging.getLogger(__name__)
def _reproduce_stage(stage: "Stage", **kwargs) -> List["Stage"]:
def _run_callback(repro_callback):
stage.dump(update_pipeline=False)
_track_stage(stage)
repro_callback([stage])
checkpoint_func = kwargs.pop("checkpoint_func", None)
if stage.is_checkpoint:
if checkpoint_func:
kwargs["checkpoint_func"] = partial(_run_callback, checkpoint_func)
else:
raise DvcException(
"Checkpoint stages are not supported in 'dvc repro'. "
"Checkpoint stages must be reproduced with 'dvc exp run'. "
)
if stage.frozen and not stage.is_import:
logger.warning(
"%s is frozen. Its dependencies are not going to be reproduced.",
@@ -44,10 +27,8 @@ def _reproduce_stage(stage: "Stage", **kwargs) -> List["Stage"]:
return []
if not kwargs.get("dry", False):
track = checkpoint_func is not None
stage.dump(update_pipeline=False)
if track:
_track_stage(stage)
_track_stage(stage)
return [stage]
@@ -182,22 +163,17 @@ def _reproduce_stages( # noqa: C901
steps = _get_steps(graph, stages, downstream, single_item)
force_downstream = kwargs.pop("force_downstream", False)
result = []
result: List["Stage"] = []
unchanged: List["Stage"] = []
# `ret` is used to add a cosmetic newline.
ret: List["Stage"] = []
checkpoint_func = kwargs.pop("checkpoint_func", None)
for i, stage in enumerate(steps):
for stage in steps:
if ret:
logger.info("")
if checkpoint_func:
kwargs["checkpoint_func"] = partial(
_repro_callback, checkpoint_func, unchanged
)
try:
kwargs["upstream"] = result + unchanged
ret = _reproduce_stage(stage, **kwargs)
if len(ret) == 0:
@@ -212,21 +188,6 @@ def _reproduce_stages( # noqa: C901
if ret:
result.extend(ret)
except CheckpointKilledError:
result.append(stage)
logger.warning(
(
"Checkpoint stage '%s' was interrupted remaining stages in"
" pipeline will not be reproduced."
),
stage.addressing,
)
logger.warning(
"skipped stages '%s'",
", ".join(s.addressing for s in steps[i + 1 :]),
)
break
except Exception as exc: # noqa: BLE001
raise ReproductionError(stage.addressing) from exc
+2
View File
@@ -21,6 +21,8 @@ def run(
force: bool = True,
**kwargs,
) -> Union["Stage", "PipelineStage"]:
assert not kwargs.get("single_stage")
assert not kwargs.get("fname")
kwargs.update({"force": force, "params": parse_params(kwargs.get("params", []))})
stage = self.stage.create(**kwargs)
+33 -41
View File
@@ -104,32 +104,6 @@ def _get_remote(
return repo.cloud.get_remote(name, command)
def fetch_worktree(
repo: "Repo",
remote: "Remote",
targets: Optional["TargetType"] = None,
jobs: Optional[int] = None,
**kwargs: Any,
) -> int:
from dvc_data.index import save
transferred = 0
for remote_name, view in worktree_view_by_remotes(
repo.index, push=True, targets=targets, **kwargs
):
remote_obj = _get_remote(repo, remote_name, remote, "fetch")
index = view.data["repo"]
total = len(index)
with Callback.as_tqdm_callback(
unit="file",
desc=f"Fetching from remote {remote_obj.name!r}",
disable=total == 0,
) as cb:
cb.set_size(total)
transferred += save(index, callback=cb, jobs=jobs, storage="remote")
return transferred
def push_worktree(
repo: "Repo",
remote: "Remote",
@@ -138,8 +112,7 @@ def push_worktree(
**kwargs: Any,
) -> int:
from dvc.repo.index import build_data_index
from dvc_data.index import checkout
from dvc_data.index.checkout import VersioningNotSupported
from dvc_data.index.checkout import VersioningNotSupported, apply, compare
pushed = 0
stages: Set["Stage"] = set()
@@ -165,6 +138,18 @@ def push_worktree(
else:
diff_kwargs = {}
with Callback.as_tqdm_callback(
unit="entry",
desc=f"Comparing indexes for remote {remote_obj.name!r}",
) as cb:
diff = compare(
old_index,
new_index,
callback=cb,
delete=remote_obj.worktree,
**diff_kwargs,
)
total = len(new_index)
with Callback.as_tqdm_callback(
unit="file",
@@ -173,18 +158,15 @@ def push_worktree(
) as cb:
cb.set_size(total)
try:
stats = checkout(
new_index,
apply(
diff,
remote_obj.path,
remote_obj.fs,
old=old_index,
delete=remote_obj.worktree,
callback=cb,
latest_only=remote_obj.worktree,
jobs=jobs,
**diff_kwargs,
)
pushed += sum(len(changes) for changes in stats.values())
pushed += sum(len(changes) for changes in diff.changes.values())
except VersioningNotSupported:
logger.exception("")
raise DvcException(
@@ -342,23 +324,33 @@ def _fetch_out_changes(
remote_index: Union["DataIndex", "DataIndexView"],
remote: "Remote",
):
from dvc_data.index import checkout
from dvc_data.index.checkout import apply, compare
old, new = _get_diff_indexes(out, local_index, remote_index)
with Callback.as_tqdm_callback(
unit="entry",
desc="Comparing indexes",
) as cb:
diff = compare(
old,
new,
delete=True,
meta_only=True,
meta_cmp_key=partial(_meta_checksum, remote.fs),
callback=cb,
)
total = len(new)
with Callback.as_tqdm_callback(
unit="file", desc=f"Updating '{out}'", disable=total == 0
) as cb:
cb.set_size(total)
checkout(
new,
apply(
diff,
out.repo.root_dir,
out.fs,
old=old,
delete=True,
update_meta=False,
meta_only=True,
meta_cmp_key=partial(_meta_checksum, remote.fs),
storage="data",
callback=cb,
)
+7 -11
View File
@@ -1,6 +1,6 @@
from collections.abc import Mapping
from voluptuous import Any, Optional, Required, Schema
from voluptuous import Any, Equal, Optional, Required, Schema
from dvc import dependency, output
from dvc.annotations import ANNOTATION_SCHEMA, ARTIFACT_SCHEMA
@@ -12,13 +12,11 @@ from dvc.output import (
Output,
)
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
from dvc.stage.params import StageParams
STAGES = "stages"
SINGLE_STAGE_SCHEMA = {
StageParams.PARAM_MD5: output.CHECKSUM_SCHEMA,
StageParams.PARAM_CMD: Any(str, list, None),
StageParams.PARAM_WDIR: Any(str, None),
StageParams.PARAM_DEPS: Any([dependency.SCHEMA], None),
StageParams.PARAM_OUTS: Any([output.SCHEMA], None),
@@ -35,6 +33,7 @@ DATA_SCHEMA = {
Required("path"): str,
Output.PARAM_CLOUD: CLOUD_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
Output.PARAM_HASH: str,
}
LOCK_FILE_STAGE_SCHEMA = {
Required(StageParams.PARAM_CMD): Any(str, list),
@@ -44,11 +43,9 @@ LOCK_FILE_STAGE_SCHEMA = {
}
LOCKFILE_STAGES_SCHEMA = {str: LOCK_FILE_STAGE_SCHEMA}
LOCKFILE_V1_SCHEMA = LOCKFILE_STAGES_SCHEMA
LOCKFILE_V2_SCHEMA = {
LOCKFILE_SCHEMA = {
Required("schema"): Equal("2.0", "invalid schema version"),
STAGES: LOCKFILE_STAGES_SCHEMA,
Required(SCHEMA_KWD): lockfile_version_schema,
}
OUT_PSTAGE_DETAILED_SCHEMA = {
@@ -56,7 +53,7 @@ OUT_PSTAGE_DETAILED_SCHEMA = {
**ANNOTATION_SCHEMA, # type: ignore[arg-type]
Output.PARAM_CACHE: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
"checkpoint": bool,
Output.PARAM_REMOTE: str,
Output.PARAM_PUSH: bool,
}
@@ -124,7 +121,7 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
}
MULTI_STAGE_SCHEMA = {
PLOTS: Any(SINGLE_PLOT_SCHEMA, [Any(str, SINGLE_PLOT_SCHEMA)]),
PLOTS: [Any(str, SINGLE_PLOT_SCHEMA)],
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
VARS_KWD: VARS_SCHEMA,
StageParams.PARAM_PARAMS: [str],
@@ -135,5 +132,4 @@ MULTI_STAGE_SCHEMA = {
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
COMPILED_MULTI_STAGE_SCHEMA = Schema(MULTI_STAGE_SCHEMA)
COMPILED_LOCK_FILE_STAGE_SCHEMA = Schema(LOCK_FILE_STAGE_SCHEMA)
COMPILED_LOCKFILE_V1_SCHEMA = Schema(LOCKFILE_V1_SCHEMA)
COMPILED_LOCKFILE_V2_SCHEMA = Schema(LOCKFILE_V2_SCHEMA)
COMPILED_LOCKFILE_SCHEMA = Schema(LOCKFILE_SCHEMA)
+37 -51
View File
@@ -89,7 +89,7 @@ class RawData:
generated_from: Optional[str] = None
def create_stage(cls: Type[_T], repo, path, external=False, **kwargs) -> _T:
def create_stage(cls: Type[_T], repo, path, **kwargs) -> _T:
from dvc.dvcfile import check_dvcfile_path
wdir = os.path.abspath(kwargs.get("wdir", None) or os.curdir)
@@ -101,8 +101,7 @@ def create_stage(cls: Type[_T], repo, path, external=False, **kwargs) -> _T:
stage = loads_from(cls, repo, path, wdir, kwargs)
fill_stage_outputs(stage, **kwargs)
if not external:
check_no_externals(stage)
check_no_externals(stage)
fill_stage_dependencies(
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config"])
)
@@ -277,14 +276,6 @@ class Stage(params.StageParams):
def is_versioned_import(self) -> bool:
return self.is_import and self.deps[0].fs.version_aware
@property
def is_checkpoint(self) -> bool:
"""
A stage containing checkpoint outs is always considered as changed
since the checkpoint out is a circular dependency.
"""
return any(out.checkpoint for out in self.outs)
def short_description(self) -> Optional["str"]:
desc: Optional["str"] = None
if self.desc:
@@ -294,47 +285,42 @@ class Stage(params.StageParams):
return line.strip()
return desc
def _read_env(self, out, checkpoint_func=None) -> Env:
env: Env = {}
if out.checkpoint and checkpoint_func:
from dvc.env import DVC_CHECKPOINT
env.update({DVC_CHECKPOINT: "1"})
return env
def env(self, checkpoint_func=None) -> Env:
def env(self) -> Env:
from dvc.env import DVC_ROOT
env: Env = {}
if self.repo:
env.update({DVC_ROOT: self.repo.root_dir})
for out in self.outs:
current = self._read_env(out, checkpoint_func=checkpoint_func)
if any(
env.get(key) != current.get(key)
for key in set(env.keys()).intersection(current.keys())
):
raise DvcException("Conflicting values for env variable")
env.update(current)
return env
def changed_deps(self, allow_missing: bool = False) -> bool:
def changed_deps(
self, allow_missing: bool = False, upstream: Optional[List] = None
) -> bool:
if self.frozen:
return False
if self.is_callback or self.always_changed or self.is_checkpoint:
if self.is_callback or self.always_changed:
return True
return self._changed_deps(allow_missing=allow_missing)
return self._changed_deps(allow_missing=allow_missing, upstream=upstream)
@rwlocked(read=["deps"])
def _changed_deps(self, allow_missing: bool = False) -> bool:
def _changed_deps(
self, allow_missing: bool = False, upstream: Optional[List] = None
) -> bool:
for dep in self.deps:
status = dep.status()
if status:
if allow_missing and status[str(dep)] == "deleted":
continue
if upstream and any(
dep.fs_path == out.fs_path and dep.hash_info != out.hash_info
for stage in upstream
for out in stage.outs
):
status[str(dep)] = "modified"
else:
continue
logger.debug(
"Dependency '%s' of %s changed because it is '%s'.",
dep,
@@ -368,12 +354,14 @@ class Stage(params.StageParams):
return changed
@rwlocked(read=["deps", "outs"])
def changed(self, allow_missing: bool = False) -> bool:
def changed(
self, allow_missing: bool = False, upstream: Optional[List] = None
) -> bool:
is_changed = (
# Short-circuit order: stage md5 is fast,
# deps are expected to change
self.changed_stage()
or self.changed_deps(allow_missing=allow_missing)
or self.changed_deps(allow_missing=allow_missing, upstream=upstream)
or self.changed_outs(allow_missing=allow_missing)
)
if is_changed:
@@ -384,7 +372,7 @@ class Stage(params.StageParams):
def remove_outs(self, ignore_remove=False, force=False) -> None:
"""Used mainly for `dvc remove --outs` and :func:`Stage.reproduce`."""
for out in self.outs:
if (out.persist or out.checkpoint) and not force:
if out.persist and not force:
out.unprotect()
continue
@@ -414,20 +402,23 @@ class Stage(params.StageParams):
source: str,
odb: Optional["ObjectDB"] = None,
to_remote: bool = False,
**kwargs: Any,
jobs: Optional[int] = None,
force: bool = False,
) -> None:
assert len(self.outs) == 1
(out,) = self.outs
out.transfer(source, odb=odb, jobs=kwargs.get("jobs"))
out.transfer(source, odb=odb, jobs=jobs)
if not to_remote:
out.checkout(force=kwargs.get("force"))
out.checkout(force=force)
out.ignore()
@rwlocked(read=["deps"], write=["outs"])
def reproduce(self, interactive=False, **kwargs) -> Optional["Stage"]:
if not (
kwargs.get("force", False)
or self.changed(kwargs.get("allow_missing", False))
or self.changed(
kwargs.get("allow_missing", False), kwargs.pop("upstream", None)
)
):
if not isinstance(self, PipelineStage) and self.is_data_source:
logger.info("'%s' didn't change, skipping", self.addressing)
@@ -525,7 +516,7 @@ class Stage(params.StageParams):
try:
out.save()
except OutputDoesNotExistError:
if not (allow_missing or out.checkpoint):
if not allow_missing:
raise
if old_out := old_versioned_outs.get(out.def_path):
@@ -560,7 +551,7 @@ class Stage(params.StageParams):
try:
out.commit(filter_info=filter_info, **kwargs)
except OutputDoesNotExistError:
if not (allow_missing or out.checkpoint):
if not allow_missing:
raise
except CacheLinkError:
link_failures.append(out.fs_path)
@@ -579,7 +570,7 @@ class Stage(params.StageParams):
try:
out.add(filter_info, **kwargs)
except (FileNotFoundError, OutputDoesNotExistError):
if not (allow_missing or out.checkpoint):
if not allow_missing:
raise
except CacheLinkError:
link_failures.append(filter_info or out.fs_path)
@@ -619,7 +610,7 @@ class Stage(params.StageParams):
self._check_missing_outputs()
if not dry:
if kwargs.get("checkpoint_func", None) or no_download:
if no_download:
allow_missing = True
no_cache_outs = any(
@@ -666,7 +657,7 @@ class Stage(params.StageParams):
for out in self.filter_outs(kwargs.get("filter_info")):
key, outs = self._checkout(
out,
allow_missing=allow_missing or self.is_checkpoint,
allow_missing=allow_missing,
**kwargs,
)
if key:
@@ -721,7 +712,7 @@ class Stage(params.StageParams):
ret.append({"changed outs": outs_status})
def _status_always_changed(self, ret) -> None:
if self.is_callback or self.always_changed or self.is_checkpoint:
if self.is_callback or self.always_changed:
ret.append("always changed")
def _status_stage(self, ret) -> None:
@@ -740,11 +731,6 @@ class Stage(params.StageParams):
for out in self.outs
)
def get_all_files_number(self, filter_info=None) -> int:
return sum(
out.get_files_number(filter_info) for out in self.filter_outs(filter_info)
)
def get_used_objs(
self, *args, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
+22 -14
View File
@@ -44,7 +44,12 @@ def _can_hash(stage):
return False
for out in stage.outs:
if out.protocol != "local" or not out.def_path or out.persist:
if (
out.protocol != "local"
or not out.def_path
or out.persist
or not out.is_in_repo
):
return False
return True
@@ -60,7 +65,7 @@ def _get_stage_hash(stage):
class StageCache:
def __init__(self, repo):
self.repo = repo
self.cache_dir = os.path.join(self.repo.cache.local.path, "runs")
self.cache_dir = os.path.join(self.repo.cache.legacy.path, "runs")
def _get_cache_dir(self, key):
return os.path.join(self.cache_dir, key[:2], key)
@@ -112,19 +117,18 @@ class StageCache:
cmd=cache["cmd"],
wdir=wdir,
outs=[out["path"] for out in cache["outs"]],
external=True,
)
StageLoader.fill_from_lock(stage, cache)
return stage
@contextmanager
def _cache_type_copy(self):
cache_types = self.repo.cache.local.cache_types
self.repo.cache.local.cache_types = ["copy"]
cache_types = self.repo.cache.legacy.cache_types
self.repo.cache.legacy.cache_types = ["copy"]
try:
yield
finally:
self.repo.cache.local.cache_types = cache_types
self.repo.cache.legacy.cache_types = cache_types
def _uncached_outs(self, stage, cache):
# NOTE: using temporary stage to avoid accidentally modifying original
@@ -137,7 +141,7 @@ class StageCache:
# NOTE: using copy link to make it look like a git-tracked file
with self._cache_type_copy():
for out in cached_stage.outs:
if out.def_path in outs_no_cache:
if out.def_path in outs_no_cache and out.is_in_repo:
yield out
def save(self, stage):
@@ -166,14 +170,14 @@ class StageCache:
COMPILED_LOCK_FILE_STAGE_SCHEMA(cache)
path = self._get_cache_path(cache_key, cache_value)
local_fs = self.repo.cache.local.fs
local_fs = self.repo.cache.legacy.fs
parent = local_fs.path.parent(path)
self.repo.cache.local.makedirs(parent)
self.repo.cache.legacy.makedirs(parent)
tmp = local_fs.path.join(parent, fs.utils.tmp_fname())
assert os.path.exists(parent)
assert os.path.isdir(parent)
dump_yaml(tmp, cache)
self.repo.cache.local.move(tmp, path)
self.repo.cache.legacy.move(tmp, path)
def restore(self, stage, run_cache=True, pull=False, dry=False):
from .serialize import to_single_stage_lockfile
@@ -258,12 +262,16 @@ 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")
return self.transfer(self.repo.cache.local, dest_odb)
dest_odb = odb or self.repo.cloud.get_remote_odb(
remote, "push --run-cache", hash_name="md5-dos2unix"
)
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")
return self.transfer(odb, self.repo.cache.local)
odb = odb or self.repo.cloud.get_remote_odb(
remote, "fetch --run-cache", hash_name="md5-dos2unix"
)
return self.transfer(odb, self.repo.cache.legacy)
def get_used_objs(self, used_run_cache, *args, **kwargs):
"""Return used cache for the specified run-cached stages."""
-8
View File
@@ -9,14 +9,6 @@ class StageCmdFailedError(DvcException):
super().__init__(msg)
class CheckpointKilledError(DvcException):
def __init__(self, cmd, status=None):
msg = f"failed to finish: {cmd}"
if status is not None:
msg += f", exited with {status}"
super().__init__(msg)
class StageFileDoesNotExistError(DvcException):
DVC_IGNORED = "is dvc-ignored"
DOES_NOT_EXIST = "does not exist"
+1
View File
@@ -12,6 +12,7 @@ def _update_import_on_remote(stage, remote, jobs):
"be updated with --to-remote"
)
stage.save_deps()
url = stage.deps[0].def_path
odb = stage.repo.cloud.get_remote_odb(remote, "update")
stage.outs[0].transfer(url, odb=odb, jobs=jobs, update=True)
+7 -11
View File
@@ -8,9 +8,7 @@ from funcy import get_in, lcat, once, project
from dvc import dependency, output
from dvc.parsing import FOREACH_KWD, JOIN, EntryNotFound
from dvc.parsing.versions import LOCKFILE_VERSION
from dvc.utils.objects import cached_property
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.meta import Meta
from . import PipelineStage, Stage, loads_from
@@ -20,7 +18,6 @@ from .utils import fill_stage_dependencies, resolve_paths
if TYPE_CHECKING:
from dvc.dvcfile import ProjectFile, SingleStageFile
from dvc.output import Output
logger = logging.getLogger(__name__)
@@ -39,11 +36,7 @@ class StageLoader(Mapping):
self.repo = self.dvcfile.repo
lockfile_data = lockfile_data or {}
version = LOCKFILE_VERSION.from_dict(lockfile_data)
if version == LOCKFILE_VERSION.V1:
self._lockfile_data = lockfile_data
else:
self._lockfile_data = lockfile_data.get("stages", {})
self._lockfile_data = lockfile_data.get("stages", {})
@cached_property
def lockfile_data(self) -> Dict[str, Any]:
@@ -57,7 +50,7 @@ class StageLoader(Mapping):
if not lock_data:
return
from dvc.output import merge_file_meta_from_cloud
from dvc.output import Output, merge_file_meta_from_cloud
assert isinstance(lock_data, dict)
items: Iterable[Tuple[str, "Output"]] = chain(
@@ -78,9 +71,12 @@ class StageLoader(Mapping):
info = info.copy()
info.pop("path", None)
hash_name = info.pop(Output.PARAM_HASH, None)
item.meta = Meta.from_dict(merge_file_meta_from_cloud(info))
hash_value = getattr(item.meta, item.hash_name, None)
item.hash_info = HashInfo(item.hash_name, hash_value)
# pylint: disable-next=protected-access
item.hash_name, item.hash_info = item._compute_hash_info_from_meta(
hash_name
)
files = get_in(checksums, [key, path, item.PARAM_FILES], None)
if files:
item.files = [merge_file_meta_from_cloud(f) for f in files]
-123
View File
@@ -1,123 +0,0 @@
import functools
import logging
import os
import subprocess # nosec B404
import threading
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, List
from dvc.stage.decorators import relock_repo
from dvc.stage.exceptions import CheckpointKilledError
if TYPE_CHECKING:
from dvc.stage import Stage
logger = logging.getLogger(__name__)
@dataclass
class MonitorTask:
stage: "Stage"
execute: Callable
proc: subprocess.Popen
done: threading.Event = threading.Event()
updated: threading.Event = threading.Event()
@property
def name(self) -> str:
raise NotImplementedError
@property
def SIGNAL_FILE(self) -> str: # noqa: N802
raise NotImplementedError
@property
def error_cls(self) -> type:
raise NotImplementedError
@property
def signal_path(self) -> str:
return os.path.join(self.stage.repo.tmp_dir, self.SIGNAL_FILE)
def after_run(self):
pass
class CheckpointTask(MonitorTask):
name = "checkpoint"
SIGNAL_FILE = "DVC_CHECKPOINT"
error_cls = CheckpointKilledError
def __init__(self, stage: "Stage", callback_func: Callable, proc: subprocess.Popen):
super().__init__(
stage,
functools.partial(CheckpointTask._run_callback, stage, callback_func),
proc,
)
@staticmethod
@relock_repo
def _run_callback(stage, callback_func):
stage.save(allow_missing=True)
stage.commit(allow_missing=True)
stage.unprotect_outs()
logger.debug("Running checkpoint callback for stage '%s'", stage)
callback_func()
class Monitor:
AWAIT: float = 1.0
def __init__(self, tasks: List[CheckpointTask]):
self.done = threading.Event()
self.tasks = tasks
self.monitor_thread = threading.Thread(
target=Monitor._loop, args=(self.tasks, self.done)
)
def __enter__(self):
self.monitor_thread.start()
def __exit__(self, exc_type, exc_val, exc_tb):
self.done.set()
self.monitor_thread.join()
for t in self.tasks:
t.after_run()
@staticmethod
def kill(proc):
if os.name == "nt":
return Monitor._kill_nt(proc)
proc.terminate()
proc.wait()
@staticmethod
def _kill_nt(proc):
# windows stages are spawned with shell=True, proc is the shell process
# and not the actual stage process - we have to kill the entire tree
subprocess.call( # nosec B607, B603
["taskkill", "/F", "/T", "/PID", str(proc.pid)]
)
@staticmethod
def _loop(tasks: List[MonitorTask], done: threading.Event):
while True:
for task in tasks:
if os.path.exists(task.signal_path):
try:
task.execute()
task.updated.set()
except Exception: # pylint: disable=broad-except
logger.exception(
"Error running '%s' task, '%s' will be aborted",
task.name,
task.stage,
)
Monitor.kill(task.proc)
finally:
logger.debug("Removing signal file for '%s' task", task.name)
os.remove(task.signal_path)
if done.wait(Monitor.AWAIT):
return

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