Compare commits

...
282 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
Dave Berenbaum 45c5c83699 show hydra dumped results in debug mode (#9460)
* show hydra dumped results in debug mode

* update hydra composition trace msg
2023-05-18 13:47:58 +05:45
daavoo ecb71c8263 exp push: Handle rev arg as list. 2023-05-18 09:54:10 +02:00
David de la Iglesia Castro e6e3912da0 testing: Lazy import pytest_virtualenv (#9469) 2023-05-17 16:27:57 +03:00
Dave Berenbaum 7f6890ff25 Live metrics config (#9433)
* exp: live metrics config token and offline

* refactor live metrics config

* use get_studio_config

* revert to using env vars for studio config

* update dvc-studio-client to support live metrics config changes

* upgrade dvc-studio-client

* don't change environ when posting live metrics events
2023-05-17 09:21:01 +00:00
skshetry 99daf5d113 add: fail loudly if there's no dir cache (#9466) 2023-05-17 12:03:06 +05:45
dependabot[bot]andskshetry 83ffb8803e build(deps-dev): bump pylint from 2.17.2 to 2.17.4 (#9418)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.2 to 2.17.4.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.17.2...v2.17.4)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2023-05-17 09:22:30 +05:45
dependabot[bot] ace8464acb build(deps-dev): bump mypy from 1.2.0 to 1.3.0 (#9439)
Bumps [mypy](https://github.com/python/mypy) from 1.2.0 to 1.3.0.
- [Commits](https://github.com/python/mypy/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 09:21:45 +05:45
skshetry 0dbdfd2b9b run tests and codeql on 2.x branch (#9465) 2023-05-17 09:17:36 +05:45
skshetryandpre-commit-ci[bot] 6d4251798a ci(Mergify): configuration update (#9463)
* ci(Mergify): configuration update

Signed-off-by: null <null>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: null <null>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-17 09:09:33 +05:45
skshetry 11d031935d packaging: use PyPI trusted publishers (#9459)
https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers
2023-05-17 06:26:19 +05:45
daavoo 64beb1578a repro: Add --allow-missing.
Point 2 in https://github.com/iterative/dvc/pull/9375#issuecomment-1531683194
2023-05-16 22:43:24 +02:00
dberenbaum f888544554 gc: disallow --remote without other flags 2023-05-16 17:18:57 +02:00
daavoo 1e538ed87b repro: Move pull logic to inside Stage. 2023-05-16 16:11:43 +02:00
Ruslan Kuprieiev e69deaa132 import: don't forget to set other cache config options (#9423) 2023-05-16 15:53:41 +03:00
Peter Rowlands be0a1a4a12 exp: fix temporary ref namespacing 2023-05-16 15:59:48 +09:00
skshetry 004f49744e tests: isolate dvc config and directories (#9450)
* tests: isolate dvc config and directories

by introducing envvars to change default values. This lets pytest
handle the deletion of tmpdirs, instead of filling /var/tmp with
hundreds of thousands of directories. Also isolates test run from
faulty global/system config files.

* move to dvc/env and sort
2023-05-15 18:16:02 +05:45
Alexander GuschinandOded Messer 45181504ea Add API to add artifacts (#9345)
* api call to add/write artifact

* return dvcyaml path on add

* create dir for dvc.yaml if needed

* create dir for dvc.yaml if needed - fix

* allow nested artifact names

* make dvc get download artifacts

* Apply suggestions from code review

Co-authored-by: Oded Messer <[email protected]>

* use feedback

* remove 'dvc get' from this PR, keep only adding artifacts

* allow first char of artifact name to be a number

* add note about constants

* fix parsing artifact name

* remove 'parse_name' and write tests for 'add'

* Apply suggestions from code review

Co-authored-by: Oded Messer <[email protected]>

* updates from comments

* fix for studio

* fix again

* making check_name_format to raise an exception

* Update dvc/repo/artifacts.py

---------

Co-authored-by: Oded Messer <[email protected]>
2023-05-15 07:55:43 +00:00
skshetry 915dd5062e disable nested progress bar during checkout (#9446) 2023-05-12 18:43:33 +05:45
skshetry d08c85f7d1 fix checkout callback during add/commit (#9445)
* fix checkout callback

Fix https://github.com/iterative/dvc/issues/6226.

* bump minversion of dvc-data to 0.49.1
2023-05-12 18:14:13 +05:45
skshetry 2342099bd8 add: support virtual operation (#9389) 2023-05-11 08:31:25 +05:45
dberenbaum 06d8e3c639 add random seed to benchmark stories 2023-05-10 11:08:57 +02:00
David de la Iglesia Castro 195e67c0c1 repro: pull all missing files (#9395)
* reproduce: Update `pull` to also pull `run cache`.

* reproduce: Update `pull` to also pull changed stages.
2023-05-10 00:29:44 +03:00
Peter Rowlands c75a5583b3 testing: sanitize venv names containing slashes 2023-05-09 18:52:42 +09:00
David de la Iglesia Castro 981baa8c30 studio: Send message on start event.
- Bump `dvc-studio-client>=0.9.0,<1`

Part of #8870
2023-05-08 20:08:47 +02:00
David de la Iglesia Castro aa517bc90e exp save: Support --message.
Custom commit message to use when committing the experiment.

Part of #8870
2023-05-08 20:08:47 +02:00
David de la Iglesia Castro c74dc20b97 exp run: Support --message.
Custom commit message to use when committing the experiment.

Part of #8870
2023-05-08 20:08:47 +02:00
David de la Iglesia Castro 1ba3944944 api: exp_show: Postprocess empty strings as None.
Makes it easier to manipulate when converting to pandas DataFrame.
2023-05-08 17:10:27 +02:00
David de la Iglesia Castro 8620b6e5af exp: Update --rev to support append action.
Closes #9390
2023-05-08 14:15:07 +02:00
Ruslan Kuprieiev 6b6084a848 tests: import: check that dvcfs is using main repo's cache (#9417)
Followup for https://github.com/iterative/dvc/pull/9415
2023-05-08 00:34:00 +03:00
Ruslan Kuprieiev 967dd5ea2b import: don't forget to set cache.dir (#9415)
* import: don't forget to set cache.dir

* repo: open: try setting remote for local repos
2023-05-07 05:28:58 +03:00
Ruslan Kuprieiev ad39bf14ed repo: external_repo -> _external_repo 2023-05-07 04:14:25 +03:00
Ruslan Kuprieiev 01416df5a4 get: use Repo.open instead of external_repo
This should use `Repo.open` just like `dvc import`, to benefit from
handling local repos without cloning and redundant downloaads.
2023-05-07 04:14:25 +03:00
Ruslan Kuprieiev eb592e7e33 repo: consolidate Repo.open logic 2023-05-07 04:14:25 +03:00
dberenbaum 2db07b047e data status: exclude remote status by default 2023-05-06 18:21:11 +03:00
pre-commit-ci[bot] 8ecc66ae9a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-05-05 22:25:38 +02:00
paulourbano dfa5f4d9a0 utils: removing diff key from result dictionary 2023-05-05 22:25:38 +02:00
paulourbano 75952c4e36 tests: changing tests to reverted diff functionality 2023-05-05 22:25:38 +02:00
paulourbano 31b0b8de73 tests: adapting tests to new diff output
Issue #7960
2023-05-05 22:25:38 +02:00
paulourbano fae47e6122 metrics: adds support for strings in metrics
Fixes #7960
2023-05-05 22:25:38 +02:00
Ruslan Kuprieiev ac3eef439f gha: try setting PYTHONUTF8
Trying to see if that will help with encoding error on windows with python 3.11
2023-05-05 21:55:31 +03:00
Ruslan Kuprieiev b044bcce5e tests: lockfile: use different way of changing a cmd 2023-05-05 04:16:58 +03:00
Ruslan Kuprieiev a251cffdb5 deps: unpin ruamel.yaml 2023-05-05 04:16:58 +03:00
Peter Rowlands 37858be02f testing: separate session and function level scope behavior for dvc_bin 2023-05-04 17:05:43 +09:00
Ruslan Kuprieiev 9b7240e9e0 deps: temporarily pin ruamel.yaml
Temporary workaround for https://github.com/iterative/dvc/issues/9397
2023-05-04 03:22:48 +03:00
Peter Rowlands e362438f03 testing.benchmarks: support installing remote deps in benchmark venv 2023-05-03 18:19:31 +09:00
Peter Rowlands 6c79f6bbfc testing: register pytest plugin 2023-05-03 14:58:54 +09:00
Peter Rowlands 58cd1cbc09 testing.benchmarks: separate fixtures and config 2023-05-03 14:58:54 +09:00
Peter Rowlands 96e3b48afd testing: import dvc-bench benchmarks 2023-05-03 14:58:54 +09:00
dberenbaum 1f03c40dbe add: msg for overlapping parent 2023-05-02 19:03:19 +03:00
skshetry e93e8f05a3 exp push: notify studio even if cache push fails (#9380)
* exp push: notify Studio even when cache push fails

* exp push: refactor and extract exp_ref resolving logic as the function is getting complex
2023-05-02 08:46:10 +05:45
David de la Iglesia Castro 0f1326e547 api: exp_show: Cleanup flags.
Per https://github.com/iterative/dvc/pull/9356#discussion_r1180782320
2023-05-01 19:29:47 +02:00
David de la Iglesia Castro 2a04263f02 api: Add scm module with all_{X} methods for listing revs. 2023-04-28 12:33:03 +02:00
David de la Iglesia Castro 348446050e api: Add exp_show.
Closes #8775
Closes #7167
2023-04-28 12:33:03 +02:00
Ruslan Kuprieiev 7e1036b30d doctor: add system/global config location 2023-04-28 01:23:44 +03:00
Ruslan Kuprieiev a15dbc8e03 info: get_fs_type -> _get_fs_type
This is not used anywhere outside so should be private like the rest. Likely a leftover from the past.
2023-04-28 00:53:03 +03:00
Ruslan Kuprieiev 4e02197245 info: replace self -> repo
Minor cleanup of an old leftover.
2023-04-28 00:40:11 +03:00
Peter Rowlands 4ab7ef06e5 exp: pass -v loglevel into dvc-task workers 2023-04-27 20:39:26 +09:00
Peter Rowlands dae2faf36d dvc-task: bump to 0.2.1 2023-04-27 20:39:26 +09:00
David de la Iglesia Castro 58f10a4661 gc: Add ui hint for not_in_remote. 2023-04-27 10:30:39 +02:00
David de la Iglesia Castro 64e787fa02 gc: Handle remote field in --cloud and --not-in-remote. 2023-04-27 10:30:39 +02:00
David de la Iglesia Castro c7e2ec9707 gc: Add --not-in-remote arg.
Closes #2036
2023-04-27 10:30:39 +02:00
skshetry a4cc198ac7 exp push: soft wrap link so that the URL does not break (#9368)
soft wrap link so that URL does not break
2023-04-27 06:50:33 +05:45
Oded Messer d203bf9024 Fix: Prettifying unexpected error on FileExists for get/get-url/imp-url/add and introducing --force (#9365) 2023-04-27 00:41:09 +00:00
David de la Iglesia Castro baeec8ec2d Update pyproject.toml 2023-04-26 10:38:08 +02:00
dependabot[bot] 9c689a3b51 build(deps-dev): update dvc-render requirement
Updates the requirements on [dvc-render](https://github.com/iterative/dvc-render) to permit the latest version.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.3.1...0.4.0)

---
updated-dependencies:
- dependency-name: dvc-render
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-04-26 10:38:08 +02:00
skshetry e1bf418e72 tests: lazy tpi imports, ignore deprecation warning from setuptools (#9362) 2023-04-24 21:34:07 +05:45
David de la Iglesia Castro b6d8c07d4d studio: Ensure studio_repo_url works with exp run --queue.
Closes #9349
2023-04-21 21:04:48 +02:00
Peter Rowlands b814531c84 exp: use mocked queue in test_apply_failed 2023-04-21 16:17:01 +09:00
Peter Rowlands 3b3cd0a0b2 exp: drop test_queue_remove_done func test in favor of mocked unit test 2023-04-21 16:17:01 +09:00
skshetry b2101a85ad reproduce: remove callback (#9354)
Callback was added for working around scm_context, since we don't use it, we don't need callback now.
2023-04-21 13:01:00 +05:45
dberenbaum ba760c56e5 exp: show active experiments first 2023-04-21 12:51:34 +09:00
dependabot[bot] 787c72ea18 build(deps-dev): bump dvc-gdrive from 2.19.1 to 2.19.2
Bumps [dvc-gdrive](https://github.com/iterative/dvc-gdrive) from 2.19.1 to 2.19.2.
- [Release notes](https://github.com/iterative/dvc-gdrive/releases)
- [Commits](https://github.com/iterative/dvc-gdrive/compare/2.19.1...2.19.2)

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

Signed-off-by: dependabot[bot] <[email protected]>
2023-04-20 17:01:04 +09:00
dependabot[bot] a2b7a59322 build(deps-dev): bump dvc-s3 from 2.21 to 2.22.0
Bumps [dvc-s3](https://github.com/iterative/dvc-s3) from 2.21 to 2.22.0.
- [Release notes](https://github.com/iterative/dvc-s3/releases)
- [Commits](https://github.com/iterative/dvc-s3/compare/2.21.0...2.22.0)

---
updated-dependencies:
- dependency-name: dvc-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-04-20 17:00:21 +09:00
Peter Rowlands ec090c5680 exp: use new collection in show/cli commands
- Updates 'dvc exp show --json' to use new data format
2023-04-20 16:58:32 +09:00
Peter Rowlands 58f74ae543 exp: standardize exp data collection 2023-04-20 16:58:32 +09:00
Peter Rowlands 2e8bcbf719 exp: add structured datatypes for collection 2023-04-20 16:58:32 +09:00
skshetry 10f523430b exp run: do not autostage on dvc repro (#9344) 2023-04-19 16:11:13 +05:45
skshetry 5563a595d5 utils.studio: allow any base url to be set, not just the hostname (#9341)
This keeps it in symmetry with
https://github.com/iterative/dvc-studio-client/pull/28, and allows
setting the base url with path, not just limited to netloc.

This was always intended to work, urljoin with the added slash was
causing this bug.
2023-04-18 22:22:26 +05:45
skshetry dd3deb6cbd add DVC_STUDIO_URL envvar support (#9340)
add DVC_STUDIO_URL envvar
2023-04-18 09:56:17 +05:45
skshetry 7841020b2b [DO NOT MERGE] release: remove feature flags for experiment sharing (#9257)
release: remove feature flags for experiment sharing
2023-04-17 12:53:27 +05:45
dberenbaum 1c394986db fix tests 2023-04-15 23:46:47 +03:00
dberenbaum 870935ebdc import-url: make version-aware and to-remote exclusive 2023-04-15 23:46:47 +03:00
Saugat Pachhai (सौगात) 1be82fa56d tests: remove unnecessary variable interpolation
dvc.run does not expand variables. It is only done when it's actually read from dvc.yaml file.
So during dvc run, the shell will try to expand the variable, which does not exist.

I don't think that the intention was to use shell variables. But I don't think we should ever
use them, as they will fail on some shells (eg: fish) and keep the scripts as simple as possible.
2023-04-14 13:01:53 +02:00
Ruslan Kuprieiev ce0f6c187e deps: put lower bound on dvc-http
People are still running into `ImportError: cannot import name 'fsspec_loop' from 'fsspec.asyn' ` because of it.
2023-04-12 22:26:31 +03:00
pre-commit-ci[bot] 559ec392f6 [pre-commit.ci] pre-commit autoupdate (#9319)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.260 → v0.0.261](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.260...v0.0.261)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-11 14:58:43 +03:00
Peter Rowlands c71a4082e6 testing: expose force=... in scm_add 2023-04-10 13:17:47 +03:00
Peter Rowlands ce269ffa58 exp: use explicit scm.add(force=True) when necessary
- add test for dir dep containing gitignored files
2023-04-10 13:17:47 +03:00
Peter Rowlands 61d0893d7c deps: bump scmrepo to 1.0.0 2023-04-10 13:17:47 +03:00
dependabot[bot] eef5fb9b37 build(deps-dev): bump mypy from 1.1.1 to 1.2.0 (#9313)
Bumps [mypy](https://github.com/python/mypy) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v1.1.1...v1.2.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-07 10:39:06 +05:45
Peter Rowlands f03d555747 deps: bump dvc-ssh to 2.22.1 2023-04-06 19:28:30 +09:00
skshetry a91993616f logger: add DVC_SHOW_TRACEBACK envvar to show traceback on exception (#9310)
This will come useful in debugging, where you don't have to add --verbose just to
see where it failed or go through verbose logs (which might be useful when working
with UI issues as you want to see what the user sees).

Ideally this should be handled in main when we exit with error instead of using
logger.{warning,error,exception} to log exception as the intention is to only log
last exception that makes it exit. But since we don't have that capability and
we don't abuse logging.{warning,error,exception} to log and suppress exceptions, this is a good
enough solution for now without complicating `dvc.cli.main()`.
2023-04-06 09:56:19 +05:45
skshetry 3c570a2177 exp push: improve ui (#9308)
* exp push: improve ui

* fix misc suggestions
- show url even when the refs are up-to-date.
- show url at the end
- add Git remote information in up-to-date UI message.

* fix diverged exps message
2023-04-06 09:54:39 +05:45
David de la Iglesia Castro 3312a90949 exp run: Add --copy-paths arg.
List of paths to copy inside the temp directory. Only used if `--temp` or `--queue` is specified.

Closes #5800
2023-04-05 23:17:07 +02:00
Alexander Guschin 32bebc0246 Add top-level artifacts: section (#9220)
* add artifacts section

* adding and removing artifacts

* delete code for add/remove; add tests

* fix pre-commit

* remove extra piece of writing to dvc.yaml

* remove extra piece

* simplify kwargs

* allow same names for artifacts in different dvc.yaml files

* set ID as path; don't return dvc.yaml(s) with no artifacts

* fix PR feedback

* issue warning at incorrect artifact name

* issue warning at incorrect artifact name

* reference right schema

* add r to regexp

* add tests for regexp

* set artifacts for SingleStageFile

* fix windows test

* make path required; simplify regexp and remove slash from it

* allow to use gto's artifacts.yaml

* remove extra piece of code for migration from gto
2023-04-05 13:31:35 +00:00
David de la Iglesia Castro 35648134f7 exp run: git add param files that will be modified.
Only if they are currently untracked and `--temp` or `--queue` is passed.

Closes #7842
2023-04-05 10:28:18 +02:00
Peter Rowlands df09e522ec deps: bump scmrepo minver to 0.2.1 2023-04-05 16:50:03 +09:00
Ruslan Kuprieiev 40f7f34889 api: data: set default remote instead of using it directly
Fixes #9139
Fixes #7341 (see https://github.com/iterative/dvc/pull/7363#issuecomment-1032959843)
2023-04-05 02:47:35 +03:00
skshetry cf52efb8c3 notify_refs; rename studio_url to base_url (#9303) 2023-04-04 17:43:05 +05:45
skshetry 8be71be540 utils.studio: disallow redirects, misc API refactoring (#9301) 2023-04-04 13:15:08 +05:45
pre-commit-ci[bot]andskshetry 071a5b7ba4 [pre-commit.ci] pre-commit autoupdate (#9251)
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.257 → v0.0.260](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.257...v0.0.260)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2023-04-04 10:12:52 +05:45
dependabot[bot] ed6cf093a8 build(deps-dev): bump pylint from 2.17.1 to 2.17.2 (#9299)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.1 to 2.17.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.17.1...v2.17.2)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-04 10:11:47 +05:45
skshetry 7c3c47b4a2 bump ruff to 0.0.260 and enable bandit (#9300)
* bump ruff to 0.0.260 and enable bandit

* install toml extra for bandit

* suppress insecure use of hash md5
2023-04-04 10:01:43 +05:45
David de la Iglesia Castro 3a2259821b exp save: Include LOCK_FILE 2023-04-03 10:33:52 +02:00
Ruslan Kuprieiev 649ee67e4b config: introduce core.site_cache_dir 2023-04-03 00:29:44 +03:00
Ruslan Kuprieiev 7cdb3b0ae2 repo: site_cache_dir: isolate by user
Related #9249
2023-04-03 00:29:44 +03:00
Ruslan Kuprieiev ecdc44e010 dep: repo: use dvcfs
Makes dvc get/import/etc use dvcfs, which already supports cloud versioning,
circular imports and other stuff.

Also makes dvc import behave more like dvc import-url, so that we can use
the same existing logic for fetching those using index instead of objects.

Fixes #8789
Related iterative/studio#4782
2023-04-02 03:55:02 +03:00
David de la Iglesia Castro 24e10e0278 exp save: Track top level files 2023-03-31 19:23:40 +02:00
David de la Iglesia Castro 7c8e6e3156 exp run: Track top level files. 2023-03-31 19:23:40 +02:00
David de la Iglesia Castro d2812fa48f studio: Don't fail on empty params. 2023-03-31 19:23:40 +02:00
skshetry 086d51d9de plots json: return definition errors unrelated to renderers (#9293) 2023-03-31 16:34:26 +05:45
David de la Iglesia Castro 1d8b2e5fa6 exp run: Propagate .dvc/config.local in TempDirExecutor.
Closes #7254
2023-03-31 09:02:22 +02:00
David de la Iglesia Castro 894f530d22 deps: bump dvc-studio-client>=0.6.1,<1 2023-03-30 11:23:07 +02:00
skshetry 1b9d40960f utils.studio: reduce verbosity on exception logging (#9284) 2023-03-30 12:55:19 +05:45
skshetry 696fabc745 exp-push: show url to the Studio project (#9283) 2023-03-30 12:52:09 +05:45
skshetry 780dc72301 Revert "exp-push: show url to the Studio project" (#9282)
Revert "exp-push: show url to the Studio project (#9278)"

This reverts commit 1ca3ef14a3.
2023-03-30 08:28:44 +05:45
skshetry 1ca3ef14a3 exp-push: show url to the Studio project (#9278) 2023-03-30 08:27:47 +05:45
skshetry a6c89cac8d replace funcy.cat with itertools.chain.from_iterable (#9279)
It's just an alias to the function from itertools.
2023-03-29 20:56:26 +05:45
skshetry 593f1064cf utils.studio: refactor to return json response (#9277) 2023-03-29 20:02:40 +05:45
Ruslan Kuprieiev 35bb261b89 config: don't look for .dvc dir implicitly
Currently if dvc_dir doesn't exists (e.g. we are creating an uninitialized
repo), config will try looking for `.dvc` dir in the current directory,
which might be inside of another dvc repository resulting in mixed configs.

Use explicit `from_cwd` method to make `Config` try to find repo configs
if available to avoid the usage mixup.
2023-03-29 16:27:54 +03:00
David de la Iglesia Castro fe6d543c1f tests: Add UnresolvedExpNamesError check in test_remove_remote 2023-03-29 12:56:59 +02:00
dberenbaum 4351d32de4 exp remove: fix ui for removing from git remotes 2023-03-29 12:56:59 +02:00
skshetry efc2d7ad3d remove unused type-ignore comment (#9258) 2023-03-28 14:30:33 +05:45
skshetry 7c65739e7e refactor notify_refs_to_studio to take in repo instance as dependency (#9256) 2023-03-28 13:53:31 +05:45
skshetry cc4816f652 utils.studio: require token (#9255)
This is a simple refactoring which makes the caller to choose appropriate token instead of
the studio.notify_refs making a decision for them.
2023-03-28 13:32:05 +05:45
skshetry faa1bdfbce use git remote to find out remote repo url when pinging Studio (#9254) 2023-03-28 12:17:48 +05:45
skshetry 006b6f3513 exp push/remove: show message on failure when notifying to Studio (#9253) 2023-03-28 09:40:41 +05:45
skshetry 1bc4c1dceb exp share refactor - ping on exp:remove, do not fail on error (#9248)
studio experiments share refactor

1) Pings Studio on exp remove
2) Does not fail on any error during request.post
3) UI improvement on exp remove
2023-03-27 22:51:50 +05:45
skshetry 531b2110cf Revert "exp push: add trailing slash to the endpoint url" (#9243)
Revert "exp push: add trailing slash to the endpoint url (#9240)"

This reverts commit 5485275b92.
2023-03-27 14:19:12 +05:45
skshetry c1917ed751 output: ignore missing size from files when calculating total size (#9242)
output: ignore missing size from files when calculating hash info
2023-03-27 12:56:43 +05:45
skshetry 43195a806d plots: return errors in json format (#9146)
* plots: return errors in json format

* plots --json: move plots data into data key

* rename  revision key to rev

* move imports to top-level

* add tests
2023-03-27 12:53:43 +05:45
Ruslan Kuprieiev d41512c85c deps: bump dvc-data to 0.46.0 (#9241)
* utils: determine_linecol: ignore mypy attr-defined error

* deps: bump dvc-data to 0.46.0
2023-03-26 22:06:23 +03:00
David de la Iglesia CastroandDave Berenbaum e9fb0b92aa exp branch: Make branch arg optional.
Default to `f"{ref_info.name}-branch"` where ref_info is inferred from the `exp_rev` argument.

Co-authored-by: Dave Berenbaum <[email protected]>
2023-03-24 19:11:06 +01:00
skshetry 5485275b92 exp push: add trailing slash to the endpoint url (#9240) 2023-03-24 22:35:20 +05:45
Peter Rowlands 1f80f2f2ed deps: bump scmrepo to 0.1.17 2023-03-24 19:31:27 +09:00
David de la Iglesia Castro 6b6dc01ac4 exp save: Reintroduce implementation based on executor/queue.
Closes #9058 .

Ensures behavior of `_stash_exp` in `exp run` is matched in `exp save` without duplicating logic.

The simpler implementation ended up being worse for maintenance when wanting to make `exp run` and `exp save` behavior in sync.
2023-03-24 10:43:24 +01:00
Peter Rowlands 91d3ff0f69 exp: mark q remove test as flaky 2023-03-24 17:53:40 +09:00
dependabot[bot] 701f48197e build(deps): bump pyinstaller from 5.8.0 to 5.9.0 (#9169)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.8.0 to 5.9.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.8.0...v5.9.0)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-23 16:45:11 +00:00
Dave Berenbaum da3bbccf66 Update duplicate output error msg (#9138)
* update dup output error msg

* update dup output error msg

* stage add: add test for duplicated output error msg

* cleanup OutputDuplicationError code
2023-03-23 15:54:44 +00:00
Ruslan Kuprieiev 40484e8d57 dvcfs: handle broken symlink/TreeError (#9231)
* plots: handle broken symlink/TreeError

* dvcfs: ls: raise FileNotFoundError on broken symlink/TreeError

* deps: bump dvc-data to 0.45.1
2023-03-23 15:38:21 +02:00
pre-commit-ci[bot]andDavid de la Iglesia Castro 48b56c83dc [pre-commit.ci] pre-commit autoupdate (#9168)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.254 → v0.0.257](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.254...v0.0.257)
- [github.com/codespell-project/codespell: v2.2.2 → v2.2.4](https://github.com/codespell-project/codespell/compare/v2.2.2...v2.2.4)

* pre-commit: fix ruff errors

* pre-commit: fix flake8

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: David de la Iglesia Castro <[email protected]>
2023-03-23 13:05:31 +00:00
Peter Rowlands f8bbf20b77 exp: use individual q for timeout dependent tests 2023-03-23 19:18:52 +09:00
Peter Rowlands cd2c4d0caf tests: remark flaky queue tests as xfail on windows 2023-03-23 19:18:52 +09:00
David de la Iglesia Castro 9b329d01bf api: Add exp_save. 2023-03-23 10:33:55 +01:00
dependabot[bot] 5c442cb867 build(deps-dev): bump pylint from 2.17.0 to 2.17.1 (#9229)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.0 to 2.17.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.17.0...v2.17.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-23 09:24:52 +01:00
Ruslan Kuprieiev 2d5c15bee1 erepo: move legacy exceptions to api (#9228)
* erepo: get rid of NoRemoteInExternalRepoError

This exception hasn't been making any sense at all since we've introduced
get and import, as they can be chained in repos.

* erepo: get rid of NoOutputInExternalRepoError

This is dead code these days.

* erepo: get rid of FileMissingError and PathMissingError

* api: preserve legacy exc wrappers
2023-03-23 01:40:42 +02:00
Peter Rowlands 7868c1301c exp: enforce waiting for task completion in CI 2023-03-22 18:58:42 +09:00
Ruslan Kuprieiev dfe76c54d7 erepo: set subrepos/uninitialized explicitly 2023-03-22 02:26:44 +02:00
Ruslan Kuprieiev 392c15e1d4 erepo: pass rev instead of fs/scm
Towards unifying `Repo.open` and `external_repo` behaviour to get rid
of the latter.
2023-03-22 00:21:25 +02:00
David de la Iglesia Castro 121d56b739 hydra: resolve OmegaConf interpolations. (#9212)
Closes #9196
2023-03-20 21:38:22 +05:45
skshetry 66406e6c99 scm: fetch_all_exps pass kwargs to scm.fetch_refspecs (#9214) 2023-03-20 21:26:24 +05:45
skshetry 8399bf612a sconfig: add support for studio token (#9213)
config: add support for studio token
2023-03-20 20:59:52 +05:45
daniele 6c1b786564 parsing: fix foreach resolution with int keys (#8904) 2023-03-20 18:40:30 +05:45
Oded Messerandpre-commit-ci[bot] d1c5fcc1b8 Add template for epic/story issues (#9208)
* Add template

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-20 09:56:55 +01:00
skshetry 4ee3271b0c dvc.repo.exp: import some modules lazily (#9207) 2023-03-18 17:24:01 +05:45
skshetry dae8515933 plots: small refactorings (#9206) 2023-03-17 19:42:27 +05:45
Saugat Pachhai (सौगात) f02dd83e53 close all streams in child 2023-03-17 14:43:44 +02:00
skshetry a2066d75f9 add a note why we are flushing (#9205) 2023-03-17 17:52:04 +05:45
skshetry 283d8245e1 flush streams before fork (#9203) 2023-03-17 17:14:34 +05:45
skshetry c52b12ac2a dvcfs: support caching remote streams; use that in plots (#9183)
* dvcfs: support caching remote streams; use that in plots

* plots: show progress bar, cap max_workers to 16

* bump dvc-data to 0.44.1
2023-03-17 16:04:16 +05:45
Peter Rowlands 440cb9d2bc ui: flush stdout/stderr before colorama re-init 2023-03-17 14:52:30 +09:00
David de la Iglesia Castro 2d820cccad deps: bump dvc-render>=0.3.1<0.4.0.
For https://github.com/iterative/dvc-render/pull/124
2023-03-16 21:36:02 +01:00
skshetry 30c3e3e4d4 exp push: set default endpoint (#9198) 2023-03-16 17:50:38 +05:45
skshetry 58029e9719 exp push: push experiments revs to studio (#9160)
* exp push: push experiments revs to studio

* log json data that is sent
2023-03-16 11:24:50 +05:45
Ruslan Kuprieiev 990e834f1d repo: open: handle bare git repos
If we are asked to open a potentially unitialized repo, we need to make sure
it is not a bare git repo, so that we don't accidentally start putting dvc
files inside git control dir.

Discovered while working on #8789
2023-03-16 02:03:31 +02:00
Ruslan Kuprieiev 33a2449c91 deps: repo: use _get_rev when creating dvcfs 2023-03-16 01:05:04 +02:00
Ruslan Kuprieiev e2ce95e17e deps: repo: pass url explicitly to DVCFileSystem
Otherwise our wrapper ignores it.
2023-03-16 00:35:15 +02:00
Ruslan Kuprieiev 53f9953cd1 index: data: support imports
Stepping stone to simplifying `dvc fetch/pull` by using index.

Fetch handles regular imports through index already, but not repo
imports because their processing is much more involved (e.g. chained
imports) in the current arch.

With `FileStorage` support introduced into `DataIndex` and `datafs`
supporting imports overall, `dvcfs` can now handle repo imports (even
chained ones). This will soon allow us to handle repo imports the same
way we handle regular ones, improve performance and get rid of a lot of
messy code (e.g. DependencyRepo).

Related https://github.com/iterative/scmrepo/issues/207
Related https://github.com/iterative/dvc-data/pull/315
Related https://github.com/iterative/studio/issues/5261
2023-03-15 20:32:13 +02:00
skshetry e6419b939d do not disable colorama in tests (#9187)
skip disabling colorama in tests

It might affect pytest as it wraps sys.stderr/sys.stdout for capsys fixture.
This is a temporary hack, as we might get rid of colorama soon.
2023-03-15 18:23:26 +05:45
Ruslan Kuprieiev 37ccc5e8d8 repo: create site_cache_dir with proper perms
Fixes #9184
2023-03-15 14:26:17 +02:00
skshetry 8312eb9b42 ui: disable colorama on write_json when highlighting is disabled (#9186)
On a large json output (say ~300MB), this saves about ~6 seconds of time.
2023-03-15 17:42:45 +05:45
Ruslan Kuprieiev c8745dd580 deps: bump dvc-data to 0.44.0 2023-03-15 03:41:19 +02:00
346 changed files with 10309 additions and 13154 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
+60
View File
@@ -0,0 +1,60 @@
---
name: Epic/Story Issue Template
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
title: 'Epic: New Feature'
labels: epic
assignees:
---
## Summary / Background
What do you want to achieve, why? business context
...
## Scope
What will be impacted and what won't be?
What needs implementation and what is invariant?
e.g.
- user should be able to run workflow X from UI
- enable workflow Y, Z from CLI
## Assumptions
Product / UX assumptions as well as technical assumptions / limitations
e.g.
* Support only Python Runtime
* Focus on DVC experiments only
* Deployment environments don't change often and can be picked up from shared configuration
## Open Questions
e.g.
- How should access control work for shared artifacts (workflow X)
- Python runtime assumption - is it really valid? in light of <...>
## Blockers / Dependencies
List issues or other conditions / blockers
## General Approach
Invocation example:
```shell
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
```
## Steps
### Must have (p1)
- [ ] subissue2
- [ ] step 2
- info
- info
### Optional / followup (p2)
- [ ] ⌛ step 3 wip
- [ ] step 4
## Timelines
Put your estimations here. Update once certainty changes
- end of week (Feb 3) for prototype with workflows X, Y
- Feb 15 - MVP in production
- Low priority followups can be done later
+10
View File
@@ -0,0 +1,10 @@
pull_request_rules:
- name: backport patches to 2.x
conditions:
- base=main
- label=backport
actions:
backport:
branches:
- "2.x"
title: ({{ destination_branch }}) {{ title }}
+1 -1
View File
@@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: ["main"]
branches: ["main", "2.x"]
pull_request:
branches: ["main"]
schedule:
+2 -63
View File
@@ -11,68 +11,10 @@ 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:
id-token: write
steps:
- uses: actions/checkout@v3
with:
@@ -100,13 +42,10 @@ jobs:
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
- name: Publish to Test PyPI
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
+3 -1
View File
@@ -2,7 +2,7 @@ name: Tests
on:
push:
branches: [main]
branches: [main, 2.x]
pull_request:
schedule:
- cron: '5 1 * * *' # every day at 01:05
@@ -42,6 +42,8 @@ jobs:
- uses: pre-commit/[email protected]
- name: run tests
timeout-minutes: 40
env:
PYTHONUTF8: 1
run: >
pytest -n=logical --timeout=300 --durations=100
--cov --cov-report=xml --cov-report=term
+10 -3
View File
@@ -35,7 +35,7 @@ repos:
args:
- --py38-plus
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
@@ -43,11 +43,11 @@ repos:
hooks:
- id: isort
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.254'
rev: 'v0.0.261'
hooks:
- id: ruff
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
rev: v2.2.4
hooks:
- id: codespell
additional_dependencies: ["tomli"]
@@ -61,6 +61,13 @@ repos:
- flake8-debugger
- flake8-string-format
- flake8-typing-imports
- repo: https://github.com/PyCQA/bandit
rev: '1.7.5'
hooks:
- id: bandit
args: ["-c", "pyproject.toml", "--quiet", "--format=custom"]
files: ^dvc/
additional_dependencies: [".[toml]"]
- repo: local
hooks:
- id: mypy
+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()
+1 -1
View File
@@ -47,7 +47,7 @@ def is_enabled():
enabled = not os.getenv(DVC_NO_ANALYTICS)
if enabled:
enabled = to_bool(
Config(validate=False).get("core", {}).get("analytics", "true")
Config.from_cwd(validate=False).get("core", {}).get("analytics", "true")
)
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
+22 -5
View File
@@ -2,6 +2,7 @@ from dataclasses import asdict, dataclass, field, fields
from typing import Any, ClassVar, Dict, List, Optional
from funcy import compact
from voluptuous import Required
@dataclass
@@ -16,11 +17,23 @@ 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))
@dataclass
class Artifact:
PARAM_PATH: ClassVar[str] = "path"
PARAM_DESC: ClassVar[str] = "desc"
PARAM_TYPE: ClassVar[str] = "type"
PARAM_LABELS: ClassVar[str] = "labels"
PARAM_META: ClassVar[str] = "meta"
path: str
desc: Optional[str] = None
type: Optional[str] = None # noqa: A003
labels: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, str]:
return compact(asdict(self))
@@ -33,3 +46,7 @@ ANNOTATION_SCHEMA = {
Annotation.PARAM_LABELS: [str],
Annotation.PARAM_META: object,
}
ARTIFACT_SCHEMA = {
Required(Artifact.PARAM_PATH): str,
**ANNOTATION_SCHEMA, # type: ignore[arg-type]
}
+7 -2
View File
@@ -2,12 +2,17 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import make_checkpoint
from .experiments import exp_save, exp_show
from .scm import all_branches, all_commits, all_tags
from .show import metrics_show, params_show
__all__ = [
"all_branches",
"all_commits",
"all_tags",
"exp_save",
"exp_show",
"get_url",
"make_checkpoint",
"open",
"params_show",
"metrics_show",
+88 -19
View File
@@ -1,12 +1,30 @@
from contextlib import _GeneratorContextManager as GCM
from typing import Optional
from contextlib import contextmanager
from typing import Any, Dict, Optional
from funcy import reraise
from dvc.exceptions import OutputNotFoundError, PathMissingError
from dvc.exceptions import FileMissingError, OutputNotFoundError, PathMissingError
from dvc.repo import Repo
@contextmanager
def _wrap_exceptions(repo, url):
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputInExternalRepoError, NoRemoteInExternalRepoError
try:
yield
except NoRemoteError as exc:
raise NoRemoteInExternalRepoError(url) from exc
except OutputNotFoundError as exc:
if exc.repo is repo:
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
raise
except FileMissingError as exc:
raise PathMissingError(exc.path, url) from exc
def get_url(path, repo=None, rev=None, remote=None):
"""
Returns the URL to the storage location of a data file or directory tracked
@@ -19,18 +37,20 @@ def get_url(path, repo=None, rev=None, remote=None):
directory in the remote storage.
"""
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
fs_path = _repo.dvcfs.from_os_path(path)
with reraise(FileNotFoundError, PathMissingError(path, repo)):
info = _repo.dvcfs.info(fs_path)
with _wrap_exceptions(_repo, path):
fs_path = _repo.dvcfs.from_os_path(path)
dvc_info = info.get("dvc_info")
if not dvc_info:
raise OutputNotFoundError(path, repo)
with reraise(FileNotFoundError, PathMissingError(path, repo)):
info = _repo.dvcfs.info(fs_path)
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
md5 = dvc_info["md5"]
dvc_info = info.get("dvc_info")
if not dvc_info:
raise OutputNotFoundError(path, repo)
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
md5 = dvc_info["md5"]
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
class _OpenContextManager(GCM):
@@ -53,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.
@@ -94,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
@@ -189,25 +212,71 @@ 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):
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
with _repo.open_by_relpath(
path, remote=remote, mode=mode, encoding=encoding
) as fd:
yield fd
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None, config=None):
if 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):
import os
from typing import TYPE_CHECKING, Union
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
from dvc.fs.data import DataFileSystem
from dvc.fs.dvc import DVCFileSystem
if TYPE_CHECKING:
from dvc.fs import FileSystem
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
if os.path.isabs(path):
fs = DataFileSystem(index=_repo.index.data["local"])
fs_path = path
else:
fs = DVCFileSystem(repo=_repo, subrepos=True)
fs_path = fs.from_os_path(path)
try:
with fs.open(
fs_path,
mode=mode,
encoding=encoding,
) as fobj:
yield fobj
except FileNotFoundError as exc:
raise FileMissingError(path) from exc
except IsADirectoryError as exc:
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
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()
+111 -24
View File
@@ -1,33 +1,120 @@
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.stage.monitor import CheckpointTask
from dvc.repo.experiments.show import tabulate
def make_checkpoint():
def exp_save(
name: Optional[str] = None,
force: bool = False,
include_untracked: Optional[List[str]] = None,
):
"""
Signal DVC to create a checkpoint experiment.
Create a new DVC experiment using `exp save`.
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.
See https://dvc.org/doc/command-reference/exp/save.
Args:
name (str, optional): specify a name for this experiment.
If `None`, a default one will be generated, such as `urban-sign`.
Defaults to `None`.
force (bool): overwrite the experiment if an experiment with the same
name already exists.
Defaults to `False`.
include_untracked (List[str], optional): specify untracked file(s) to
be included in the saved experiment.
Defaults to `None`.
Returns:
str: The `Git revision`_ of the created experiment.
Raises:
ExperimentExistsError: If an experiment with `name` already exists and
`force=False`.
.. _Git revision:
https://git-scm.com/docs/revisions
"""
if os.getenv(DVC_CHECKPOINT) is None:
return
with Repo() as repo:
return repo.experiments.save(
name=name, force=force, include_untracked=include_untracked
)
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 _postprocess(exp_rows):
for exp_row in exp_rows:
for k, v in exp_row.items():
if isinstance(v, Text):
v_str = str(v)
try:
exp_row[k] = float(v_str)
except ValueError:
exp_row[k] = v_str
if not exp_row[k]:
exp_row[k] = None
return exp_rows
def exp_show(
repo: Optional[str] = None,
revs: Optional[Union[str, List[str]]] = None,
num: int = 1,
param_deps: bool = False,
force: bool = False,
config: Optional[Dict] = None,
) -> List[Dict]:
"""Get DVC experiments tracked in `repo`.
Without arguments, this function will retrieve all experiments derived from
the Git `HEAD`.
See the options below to customize the experiments retrieved.
Args:
repo (str, optional): location of the DVC repository.
Defaults to the current project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
revs (Union[str, List[str]], optional): Git revision(s) (e.g. branch,
tag, SHA commit) to use as a reference point to start listing
experiments.
Defaults to `None`, which will use `HEAD` as starting point.
num (int, optional): show experiments from the last `num` commits
(first parents) starting from the `revs` baseline.
Give a negative value to include all first-parent commits (similar
to `git log -n`).
Defaults to 1.
param_deps (bool, optional): include only parameters that are stage
dependencies.
Defaults to `False`.
force (bool, optional): force re-collection of experiments instead of
loading from internal experiments cache.
DVC caches `exp_show` data for completed experiments to improve
performance of subsequent calls.
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, config=config) as _repo:
experiments = _repo.experiments.show(
revs=revs,
num=num,
param_deps=param_deps,
force=force,
)
td, _ = tabulate(experiments, fill_value=None)
return _postprocess(td.as_dict())
+54
View File
@@ -0,0 +1,54 @@
from typing import List, Optional
from dvc.repo import Repo
def all_branches(repo: Optional[str] = None) -> List[str]:
"""Get all Git branches in a DVC repository.
Args:
repo (str, optional): location of the DVC repository.
Defaults to the current project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
Returns:
List[str]: Names of the Git branches.
"""
with Repo.open(repo) as _repo:
return _repo.scm.list_branches()
def all_commits(repo: Optional[str] = None) -> List[str]:
"""Get all Git commits in a DVC repository.
Args:
repo (str, optional): location of the DVC repository.
Defaults to the current project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
Returns:
List[str]: SHAs of the Git commits.
"""
with Repo.open(repo) as _repo:
return _repo.scm.list_all_commits()
def all_tags(repo: Optional[str] = None) -> List[str]:
"""Get all Git tags in a DVC repository.
Args:
repo (str, optional): location of the DVC repository.
Defaults to the current project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
Returns:
List[str]: Names of the Git tags.
"""
with Repo.open(repo) as _repo:
return _repo.scm.list_tags()
+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.")
+5 -6
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 ""
@@ -245,7 +244,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
finally:
logger.setLevel(outer_log_level)
from dvc.external_repo import clean_repos
from dvc.repo.open_repo import clean_repos
# Remove cached repos in the end of the call, these are anonymous
# so won't be reused by any other subsequent run anyway.
-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,
+44 -65
View File
@@ -2,68 +2,61 @@ 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:
logger.exception("")
return 1
except DvcException:
logger.exception("")
return 1
@@ -80,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",
@@ -129,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'. "
@@ -140,8 +114,13 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
_add_annotating_args(parser)
parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override local file or folder if exists.",
)
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 -1
View File
@@ -39,7 +39,7 @@ class CmdCheckIgnore(CmdBase):
target = input()
except (KeyboardInterrupt, EOFError):
break
if target == "":
if not target:
break
if not self._check_one_file(target):
ret = 0
+1 -1
View File
@@ -15,7 +15,7 @@ class CmdCompletion(CmdBaseNoRepo):
shell = self.args.shell
parser = self.args.parser
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE) # nosec B604
ui.write(script, force=True)
return 0
+1 -1
View File
@@ -32,7 +32,7 @@ class CmdConfig(CmdBaseNoRepo):
super().__init__(args)
self.config = Config(validate=False)
self.config = Config.from_cwd(validate=False)
def run(self):
if self.args.show_origin and (self.args.value or self.args.unset):
-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",
+11 -119
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
@@ -117,6 +114,7 @@ class CmdDataStatus(CmdBase):
status = self.repo.data_status(
granular=self.args.granular,
untracked_files=self.args.untracked_files,
not_in_remote=self.args.not_in_remote,
remote_refresh=self.args.remote_refresh,
)
@@ -131,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",
@@ -216,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",
@@ -244,57 +178,15 @@ def add_parser(subparsers, parent_parser):
help="Show untracked files.",
)
data_status_parser.add_argument(
"--remote-refresh",
"--not-in-remote",
action="store_true",
default=False,
help="Refresh remote index.",
help="Show files not in remote.",
)
data_status_parser.add_argument(
"--no-remote-refresh",
dest="remote_refresh",
action="store_false",
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",
+1 -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,
@@ -82,6 +78,7 @@ def add_rev_selection_flags(
experiments_subcmd_parser.add_argument(
"--rev",
type=str,
action="append",
default=None,
help=msg,
metavar="<commit>",
+9 -1
View File
@@ -26,5 +26,13 @@ def add_parser(experiments_subparsers, parent_parser):
experiments_branch_parser.add_argument(
"experiment", help="Experiment to be promoted."
)
experiments_branch_parser.add_argument("branch", help="Name for the new Git branch")
experiments_branch_parser.add_argument(
"branch",
nargs="?",
default=None,
help=(
"Optional name for the new Git branch. "
"Defaults to '{experiment-name}-branch'."
),
)
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
-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",
+19
View File
@@ -19,6 +19,8 @@ class CmdExecutorRun(CmdBaseNoRepo):
queue=None,
log_level=logger.getEffectiveLevel(),
infofile=self.args.infofile,
copy_paths=self.args.copy_paths,
message=self.args.message,
)
return 0
@@ -36,4 +38,21 @@ def add_parser(experiments_subparsers, parent_parser):
help="Path to executor info file",
default=None,
)
exec_run_parser.add_argument(
"-C",
"--copy-paths",
action="append",
default=[],
help=(
"List of ignored or untracked paths to copy into the temp directory."
" Only used if `--temp` or `--queue` is specified."
),
)
exec_run_parser.add_argument(
"-M",
"--message",
type=str,
default=None,
help="Custom commit message to use when committing the experiment.",
)
exec_run_parser.set_defaults(func=CmdExecutorRun)
-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",
+55 -27
View File
@@ -1,46 +1,74 @@
import argparse
import logging
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
def join_exps(exps):
return humanize.join([f"[bold]{e}[/]" for e in exps])
if diverged_exps := result.get("diverged"):
exps = join_exps(diverged_exps)
ui.error_write(
f"[yellow]Local experiment {exps} has diverged "
"from remote experiment with the same name.\n"
"To override the remote experiment re-run with '--force'.",
styled=True,
)
if uptodate_exps := result.get("up_to_date"):
exps = join_exps(uptodate_exps)
verb = "are" if len(uptodate_exps) > 1 else "is"
ui.write(
f"Experiment {exps} {verb} up to date on Git remote {remote!r}.",
styled=True,
)
if pushed_exps := result.get("success"):
exps = join_exps(pushed_exps)
ui.write(f"Pushed experiment {exps} to Git remote {remote!r}.", styled=True)
if not uptodate_exps and not pushed_exps:
ui.write("No experiments to push.")
if uploaded := result.get("uploaded"):
stats = {"uploaded": uploaded}
ui.write(humanize.get_summary(stats.items()))
if project_url := result.get("url"):
ui.rich_print(
"View your experiments at", project_url, style="yellow", soft_wrap=True
)
def run(self):
self.raise_error_if_all_disabled()
from dvc.repo.experiments.push import UploadError
pushed_exps = self.repo.experiments.push(
self.args.git_remote,
self.args.experiment,
all_commits=self.args.all_commits,
rev=self.args.rev,
num=self.args.num,
force=self.args.force,
push_cache=self.args.push_cache,
dvc_remote=self.args.dvc_remote,
jobs=self.args.jobs,
run_cache=self.args.run_cache,
)
if pushed_exps:
ui.write(
f"Pushed experiment '{pushed_exps}'"
f"to Git remote '{self.args.git_remote}'."
try:
result = self.repo.experiments.push(
self.args.git_remote,
self.args.experiment,
all_commits=self.args.all_commits,
rev=self.args.rev,
num=self.args.num,
force=self.args.force,
push_cache=self.args.push_cache,
dvc_remote=self.args.dvc_remote,
jobs=self.args.jobs,
run_cache=self.args.run_cache,
)
else:
ui.write("No experiments to push.")
except UploadError as e:
self.log_result(e.result, self.args.git_remote)
raise
self.log_result(result, self.args.git_remote)
if not self.args.push_cache:
ui.write(
"To push cached outputs",
@@ -64,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",
+7 -3
View File
@@ -24,9 +24,11 @@ class CmdExperimentsRemove(CmdBase):
)
def run(self):
from dvc.utils import humanize
self.check_arguments()
removed_list = self.repo.experiments.remove(
removed = self.repo.experiments.remove(
exp_names=self.args.experiment,
all_commits=self.args.all_commits,
rev=self.args.rev,
@@ -34,8 +36,10 @@ class CmdExperimentsRemove(CmdBase):
queue=self.args.queue,
git_remote=self.args.git_remote,
)
removed = ",".join(removed_list)
ui.write(f"Removed experiments: {removed}")
if removed:
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
else:
ui.write("No experiments to remove.")
return 0
+20 -40
View File
@@ -1,49 +1,28 @@
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,
message=self.args.message,
**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
@@ -57,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)
@@ -136,3 +99,20 @@ def _add_run_common(parser):
# )
# metavar="<name>",
)
parser.add_argument(
"-C",
"--copy-paths",
action="append",
default=[],
help=(
"List of ignored or untracked paths to copy into the temp directory."
" Only used if `--temp` or `--queue` is specified."
),
)
parser.add_argument(
"-m",
"--message",
type=str,
default=None,
help="Custom commit message to use when committing the experiment.",
)
+8 -1
View File
@@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase):
name=self.args.name,
force=self.args.force,
include_untracked=self.args.include_untracked,
message=self.args.message,
)
except DvcException:
logger.exception("failed to save experiment")
@@ -52,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.",
@@ -75,4 +75,11 @@ def add_parser(experiments_subparsers, parent_parser):
help="List of untracked paths to include in the experiment.",
metavar="<path>",
)
save_parser.add_argument(
"-M",
"--message",
type=str,
default=None,
help="Custom commit message to use when committing the experiment.",
)
save_parser.set_defaults(func=CmdExperimentsSave)
+29 -373
View File
@@ -1,20 +1,16 @@
import argparse
import logging
import os
import re
from collections import Counter, OrderedDict, defaultdict
from datetime import date, datetime
from typing import TYPE_CHECKING, Any, Dict, Set
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
from dvc.exceptions import DvcException, InvalidArgumentError
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils.flatten import flatten
from dvc.utils.serialize import encode_exception
if TYPE_CHECKING:
@@ -28,263 +24,13 @@ FILL_VALUE_ERRORED = "!"
logger = logging.getLogger(__name__)
def _update_names(names, items):
for name, item in items:
item = item.get("data", {})
if isinstance(item, dict):
item = flatten(item)
names[name].update({key: None for key in item})
def _collect_names(all_experiments, **kwargs):
metric_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
param_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
deps_names: Set[str] = set()
for _, experiments in all_experiments.items():
for exp_data in experiments.values():
exp = exp_data.get("data", {})
_update_names(metric_names, exp.get("metrics", {}).items())
_update_names(param_names, exp.get("params", {}).items())
for dep_name in exp.get("deps", {}):
deps_names.add(dep_name)
return metric_names, param_names, sorted(deps_names)
experiment_types = {
"checkpoint_tip": "│ ╓",
"checkpoint_commit": "│ ╟",
"checkpoint_base": "├─╨",
"branch_commit": "├──",
"branch_base": "└──",
"baseline": "",
}
def _collect_rows( # noqa: C901, PLR0912, PLR0913, PLR0915
base_rev,
experiments,
all_headers,
metric_headers,
param_headers,
metric_names,
param_names,
deps_names,
precision=DEFAULT_PRECISION,
sort_by=None,
sort_order=None,
fill_value=FILL_VALUE,
iso=False,
):
from dvc.scm import Git
if sort_by:
sort_path, sort_name, sort_type = _sort_column(
sort_by, metric_names, param_names
)
reverse = sort_order == "desc"
experiments = _sort_exp(experiments, sort_path, sort_name, sort_type, reverse)
new_checkpoint = True
for i, (rev, results) in enumerate(experiments.items()):
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
row_dict = {k: fill_value for k in all_headers}
exp = results.get("data", {})
state = exp.get("status")
if state == "Success":
state = fill_value
is_baseline = rev == "baseline"
if is_baseline:
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
else:
name_rev = rev[:7]
tip = exp.get("checkpoint_tip")
parent_rev = exp.get("checkpoint_parent", "")
parent_exp = experiments.get(parent_rev, {}).get("data", {})
parent_tip = parent_exp.get("checkpoint_tip")
parent = ""
if is_baseline:
typ = "baseline"
elif tip:
if tip == parent_tip:
typ = "checkpoint_tip" if new_checkpoint else "checkpoint_commit"
elif parent_rev == base_rev:
typ = "checkpoint_base"
else:
typ = "checkpoint_commit"
parent = parent_rev[:7]
elif i < len(experiments) - 1:
typ = "branch_commit"
else:
typ = "branch_base"
if not is_baseline:
new_checkpoint = not (tip and tip == parent_tip)
row_dict["Experiment"] = exp.get("name", "")
row_dict["rev"] = name_rev
row_dict["typ"] = typ
row_dict["Created"] = format_time(exp.get("timestamp"), fill_value, iso)
row_dict["parent"] = parent
row_dict["State"] = state
row_dict["Executor"] = exp.get("executor", fill_value)
_extend_row(
row_dict,
metric_names,
metric_headers,
exp.get("metrics", {}).items(),
precision,
fill_value=fill_value,
)
_extend_row(
row_dict,
param_names,
param_headers,
exp.get("params", {}).items(),
precision,
fill_value=fill_value,
)
for dep in deps_names:
hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
if hash_info is not None:
hash_info = hash_info[:7]
row_dict[dep] = hash_info
yield list(row_dict.values())
def _sort_column(sort_by, metric_names, param_names):
path, _, sort_name = sort_by.rpartition(":")
matches = set()
if path:
if path in metric_names and sort_name in metric_names[path]:
matches.add((path, sort_name, "metrics"))
if path in param_names and sort_name in param_names[path]:
matches.add((path, sort_name, "params"))
else:
for path in metric_names:
if sort_name in metric_names[path]:
matches.add((path, sort_name, "metrics"))
for path in param_names:
if sort_name in param_names[path]:
matches.add((path, sort_name, "params"))
if len(matches) == 1:
return matches.pop()
if len(matches) > 1:
raise InvalidArgumentError(
"Ambiguous sort column '{}' matched '{}'".format(
sort_by,
", ".join([f"{path}:{name}" for path, name, _ in matches]),
)
)
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
def _sort(item):
rev, exp = item
exp_data = exp.get("data", {})
tip = exp_data.get("checkpoint_tip")
if tip and tip != rev:
# Sort checkpoint experiments by tip commit
return _sort((tip, experiments[tip]))
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
val = flatten(data).get(sort_name)
return val is None, val
ret = OrderedDict()
if "baseline" in experiments:
ret["baseline"] = experiments.pop("baseline")
ret.update(sorted(experiments.items(), key=_sort, reverse=reverse))
return ret
def format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
if datetime_obj is None:
return fill_value
if iso:
return datetime_obj.isoformat()
if datetime_obj.date() == date.today():
fmt = "%I:%M %p"
else:
fmt = "%b %d, %Y"
return datetime_obj.strftime(fmt)
def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
from dvc.compare import _format_field, with_value
for fname, data in items:
item = data.get("data", {})
item = flatten(item) if isinstance(item, dict) else {fname: item}
for name in names[fname]:
value = with_value(
item.get(name),
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
)
# wrap field data in ui.rich_text, otherwise rich may
# interpret unescaped braces from list/dict types as rich
# markup tags
value = ui.rich_text(str(_format_field(value, precision)))
if name in headers:
row[name] = value
else:
row[f"{fname}:{name}"] = value
def experiments_table( # noqa: PLR0913
all_experiments,
headers,
metric_headers,
metric_names,
param_headers,
param_names,
deps_names,
sort_by=None,
sort_order=None,
precision=DEFAULT_PRECISION,
fill_value=FILL_VALUE,
iso=False,
) -> "TabularData":
from funcy import lconcat
from dvc.compare import TabularData
all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
td = TabularData(all_headers, fill_value=fill_value)
for base_rev, experiments in all_experiments.items():
rows = _collect_rows(
base_rev,
experiments,
all_headers,
metric_headers,
param_headers,
metric_names,
param_names,
deps_names,
sort_by=sort_by,
sort_order=sort_order,
precision=precision,
fill_value=fill_value,
iso=iso,
)
td.extend(rows)
return td
def prepare_exp_id(kwargs) -> "RichText":
exp_name = kwargs["Experiment"]
rev = kwargs["rev"]
@@ -308,50 +54,16 @@ def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments( # noqa: C901, PLR0912
all_experiments,
def show_experiments( # noqa: C901
td: "TabularData",
headers: Dict[str, Iterable[str]],
keep=None,
drop=None,
pager=True,
csv=False,
markdown=False,
pcp=False,
**kwargs,
):
from funcy.seqs import flatten as flatten_list
metric_names, param_names, deps_names = _collect_names(all_experiments)
headers_list = [
"Experiment",
"rev",
"typ",
"Created",
"parent",
"State",
"Executor",
]
names = {**metric_names, **param_names}
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
counter.update(headers_list)
metric_headers = _normalize_headers(metric_names, counter)
param_headers = _normalize_headers(param_names, counter)
td = experiments_table(
all_experiments,
headers_list,
metric_headers,
metric_names,
param_headers,
param_names,
deps_names,
kwargs.get("sort_by"),
kwargs.get("sort_order"),
kwargs.get("precision"),
kwargs.get("fill_value"),
kwargs.get("iso"),
)
if keep:
for col in td.keys(): # noqa: SIM118
if re.match(keep, col):
@@ -368,11 +80,6 @@ def show_experiments( # noqa: C901, PLR0912
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
td.drop(*merge_headers[1:])
headers = {
"metrics": metric_headers,
"params": param_headers,
"deps": deps_names,
}
styles = {
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
"Created": {"header_style": "black on grey93"},
@@ -397,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):
@@ -453,19 +132,17 @@ def _normalize_headers(names, count):
def _format_json(item):
from dvc.repo.experiments.show import _CachedError
if isinstance(item, (date, datetime)):
return item.isoformat()
if isinstance(item, _CachedError):
return {"type": getattr(item, "typ", "_CachedError"), "msg": str(item)}
return encode_exception(item)
class CmdExperimentsShow(CmdBase):
def run(self):
from dvc.repo.experiments.show import tabulate
try:
all_experiments = self.repo.experiments.show(
exps = self.repo.experiments.show(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
@@ -483,30 +160,33 @@ class CmdExperimentsShow(CmdBase):
return 1
if self.args.json:
ui.write_json(all_experiments, default=_format_json)
ui.write_json([exp.dumpd() for exp in exps], default=_format_json)
else:
precision = (
self.args.precision or None if self.args.csv else DEFAULT_PRECISION
)
fill_value = "" if self.args.csv else FILL_VALUE
iso = self.args.csv
td, headers = tabulate(
exps,
precision=precision,
fill_value=fill_value,
iso=iso,
sort_by=self.args.sort_by,
sort_order=self.args.sort_order,
)
show_experiments(
all_experiments,
td,
headers,
keep=self.args.keep,
drop=self.args.drop,
sort_by=self.args.sort_by,
sort_order=self.args.sort_order,
precision=precision,
fill_value=fill_value,
iso=iso,
pager=not self.args.no_pager,
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
@@ -599,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",
@@ -628,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",
+15 -2
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class CmdGC(CmdBase):
def run(self): # noqa: C901
def run(self): # noqa: C901, PLR0912
from dvc.repo.gc import _validate_args
_validate_args(
@@ -22,6 +22,8 @@ class CmdGC(CmdBase):
workspace=self.args.workspace,
rev=self.args.rev,
num=self.args.num,
cloud=self.args.cloud,
not_in_remote=self.args.not_in_remote,
)
if self.args.rev:
@@ -46,6 +48,10 @@ class CmdGC(CmdBase):
if self.args.all_experiments:
msg += " and all experiments"
if self.args.not_in_remote:
msg += " that are present in the DVC remote"
if self.args.repos:
msg += " of the current and the following repos:"
@@ -74,6 +80,7 @@ class CmdGC(CmdBase):
workspace=self.args.workspace,
rev=self.args.rev,
num=self.args.num,
not_in_remote=self.args.not_in_remote,
)
return 0
@@ -157,12 +164,18 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Keep data files for all experiments.",
)
gc_parser.add_argument(
"--not-in-remote",
action="store_true",
default=False,
help="Keep data files that are not present in the remote.",
)
gc_parser.add_argument(
"-c",
"--cloud",
action="store_true",
default=False,
help="Collect garbage in remote repository.",
help="Collect garbage in remote storage in addition to local cache.",
)
gc_parser.add_argument(
"-r",
+10 -6
View File
@@ -14,12 +14,8 @@ class CmdGet(CmdBaseNoRepo):
from dvc.api import get_url
from dvc.ui import ui
try:
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
ui.write(url, force=True)
except DvcException:
logger.exception("failed to show URL")
return 1
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
ui.write(url, force=True)
return 0
@@ -40,6 +36,7 @@ class CmdGet(CmdBaseNoRepo):
out=self.args.out,
rev=self.args.rev,
jobs=self.args.jobs,
force=self.args.force,
)
return 0
except CloneError:
@@ -98,4 +95,11 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
get_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override local file or folder if exists.",
)
get_parser.set_defaults(func=CmdGet)
+13 -1
View File
@@ -14,7 +14,12 @@ class CmdGetUrl(CmdBaseNoRepo):
from dvc.repo import Repo
try:
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
Repo.get_url(
self.args.url,
out=self.args.out,
jobs=self.args.jobs,
force=self.args.force,
)
return 0
except DvcException:
logger.exception("failed to get '%s'", self.args.url)
@@ -46,4 +51,11 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
get_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override local file or folder if exists.",
)
get_parser.set_defaults(func=CmdGetUrl)
-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)
+8 -13
View File
@@ -15,16 +15,12 @@ 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,
)
except DvcException:
@@ -40,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(
@@ -69,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",
@@ -112,11 +101,17 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
import_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override local file or folder if exists.",
)
import_parser.add_argument(
"--version-aware",
action="store_true",
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",
+53 -11
View File
@@ -1,9 +1,9 @@
import argparse
import json
import logging
import os
from typing import TYPE_CHECKING, Dict, List, Optional
from funcy import first
from funcy import compact, first, get_in
from dvc.cli import completion
from dvc.cli.command import CmdBase
@@ -12,14 +12,51 @@ from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils import format_link
if TYPE_CHECKING:
from dvc.render.match import RendererWithErrors
logger = logging.getLogger(__name__)
def _show_json(renderers, split=False):
def _show_json(
renderers_with_errors: List["RendererWithErrors"],
split=False,
errors: Optional[Dict[str, Exception]] = None,
):
from dvc.render.convert import to_json
from dvc.utils.serialize import encode_exception
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
ui.write_json(result, highlight=False)
all_errors: List[Dict] = []
data = {}
for renderer, src_errors, def_errors in renderers_with_errors:
name = renderer.name
data[name] = to_json(renderer, split)
all_errors.extend(
{
"name": name,
"rev": rev,
"source": source,
**encode_exception(e),
}
for rev, per_rev_src_errors in src_errors.items()
for source, e in per_rev_src_errors.items()
)
all_errors.extend(
{
"name": name,
"rev": rev,
**encode_exception(e),
}
for rev, e in def_errors.items()
)
# these errors are not tied to any renderers
errors = errors or {}
all_errors.extend({"rev": rev, **encode_exception(e)} for rev, e in errors.items())
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
def _adjust_vega_renderers(renderers):
@@ -85,7 +122,7 @@ class CmdPlots(CmdBase):
if html_template_path and not os.path.isabs(html_template_path):
assert self.repo.dvc_dir
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
return html_template_path # noqa: RET504
return html_template_path
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
from pathlib import Path
@@ -122,21 +159,27 @@ class CmdPlots(CmdBase):
)
renderers_out = out if self.args.json else os.path.join(out, "static")
renderers = match_defs_renderers(
renderers_with_errors = match_defs_renderers(
data=plots_data,
out=renderers_out,
templates_dir=self.repo.plots.templates_dir,
)
if self.args.json:
_show_json(renderers, self.args.split)
errors = compact(
{
rev: get_in(data, ["definitions", "error"])
for rev, data in plots_data.items()
}
)
_show_json(renderers_with_errors, self.args.split, errors=errors)
return 0
renderers = [r.renderer for r in renderers_with_errors]
_adjust_vega_renderers(renderers)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
ui.write_json(json.loads(renderer.get_filled_template()))
ui.write_json(renderer.get_filled_template(as_string=False))
return 0
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
@@ -387,7 +430,6 @@ def _add_ui_arguments(parser):
)
parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help=argparse.SUPPRESS,
+3 -5
View File
@@ -1,10 +1,8 @@
import argparse
import logging
from typing import Dict, List, Optional
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.experiments.show import format_time
from dvc.compare import TabularData
from dvc.ui import ui
@@ -15,9 +13,9 @@ class CmdQueueStatus(CmdBase):
"""Show queue task and worker status."""
def run(self) -> int:
result: List[
Dict[str, Optional[str]]
] = self.repo.experiments.celery_queue.status()
from dvc.repo.experiments.show import format_time
result = self.repo.experiments.celery_queue.status()
if result:
all_headers = ["Task", "Name", "Created", "Status"]
td = TabularData(all_headers)
+7 -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
@@ -38,6 +32,7 @@ class CmdRepro(CmdBase):
"recursive": self.args.recursive,
"force_downstream": self.args.force_downstream,
"pull": self.args.pull,
"allow_missing": self.args.allow_missing,
}
@property
@@ -104,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",
@@ -135,6 +123,12 @@ and then the stage name name.
"from the run-cache."
),
)
repro_parser.add_argument(
"--allow-missing",
action="store_true",
default=False,
help=("Skip stages with missing data but no other changes."),
)
repro_parser.add_argument(
"--dry",
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)
+30 -12
View File
@@ -1,5 +1,6 @@
import argparse
import logging
from contextlib import contextmanager
from itertools import chain, filterfalse
from typing import TYPE_CHECKING, Dict, Iterable, List
@@ -119,8 +120,22 @@ def parse_cmd(commands: List[str]) -> str:
return " ".join(map(quote_argument, commands))
@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)
class CmdStageAdd(CmdBase):
def run(self):
from dvc.repo import lock_repo
kwargs = vars(self.args)
kwargs.update(
{
@@ -128,7 +143,15 @@ class CmdStageAdd(CmdBase):
"params": parse_params(self.args.params),
}
)
self.repo.stage.add(**kwargs)
with self.repo.scm_context, lock_repo(self.repo):
with _disable_logging(logging.INFO):
stage = self.repo.stage.add(**kwargs)
logger.info("Added stage %r in %r", stage.addressing, stage.relpath)
if self.args.run:
stage.run()
stage.dump(update_pipeline=False)
return 0
@@ -172,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",
@@ -257,6 +269,12 @@ def _add_common_args(parser):
"This doesn't affect any DVC operations."
),
)
parser.add_argument(
"--run",
action="store_true",
default=False,
help="Execute the stage after generating it.",
)
parser.add_argument(
"command",
nargs=argparse.REMAINDER,
+1 -20
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
@@ -35,7 +34,7 @@ def with_value(value, default):
class TabularData(MutableSequence[Sequence["CellT"]]):
def __init__(self, columns: Sequence[str], fill_value: str = ""):
def __init__(self, columns: Sequence[str], fill_value: Optional[str] = ""):
self._columns: Dict[str, Column] = {name: Column() for name in columns}
self._keys: List[str] = list(columns)
self._fill_value = fill_value
@@ -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)
+15 -14
View File
@@ -79,9 +79,6 @@ class Config(dict):
ConfigError: thrown if config has an invalid format.
"""
APPNAME = "dvc"
APPAUTHOR = "iterative"
SYSTEM_LEVELS = ("system", "global")
REPO_LEVELS = ("repo", "local")
# In the order they shadow each other
@@ -103,28 +100,32 @@ class Config(dict):
self.wfs = LocalFileSystem()
self.fs = fs or self.wfs
if not dvc_dir:
try:
from dvc.repo import Repo
self.dvc_dir = Repo.find_dvc_dir()
except NotDvcRepoError:
self.dvc_dir = None
else:
if dvc_dir:
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
self.load(validate=validate, config=config)
@classmethod
def from_cwd(cls, fs: Optional["FileSystem"] = None, **kwargs):
from dvc.repo import Repo
try:
dvc_dir = Repo.find_dvc_dir(fs=fs)
except NotDvcRepoError:
dvc_dir = None
return cls(dvc_dir=dvc_dir, fs=fs, **kwargs)
@classmethod
def get_dir(cls, level):
from platformdirs import site_config_dir, user_config_dir
from dvc.dirs import global_config_dir, system_config_dir
assert level in ("global", "system")
if level == "global":
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
return global_config_dir()
if level == "system":
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
return system_config_dir()
@cached_property
def files(self) -> Dict[str, str]:
+17 -9
View File
@@ -62,9 +62,9 @@ def ByUrl(mapping): # noqa: N802
parsed = urlparse(data["url"])
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
return schemas[""](data)
if parsed.netloc == "":
if not parsed.netloc:
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
@@ -150,16 +150,17 @@ SCHEMA = {
Optional("autostage", default=False): Bool,
Optional("experiments"): Bool, # obsoleted
Optional("check_update", default=True): Bool,
"site_cache_dir": str,
"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,
@@ -214,6 +215,7 @@ SCHEMA = {
"timeout": Coerce(int),
"gss_auth": Bool,
"allow_agent": Bool,
"max_sessions": Coerce(int),
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
@@ -325,4 +327,10 @@ SCHEMA = {
"config_dir": str,
"config_name": str,
},
"studio": {
"token": str,
"url": str,
"repo_url": str,
Optional("offline", default=False): Bool,
},
}
+11 -3
View File
@@ -5,7 +5,7 @@ import logging
import os
import platform
import sys
from subprocess import Popen
from subprocess import Popen # nosec B404
from typing import List
from dvc.env import DVC_DAEMON
@@ -28,12 +28,12 @@ def _popen(cmd, **kwargs) -> Popen:
os.path.abspath(os.path.dirname(__file__)), "__main__.py"
)
prefix += [main_entrypoint]
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs)
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs) # nosec B603
def _spawn_windows(cmd, env):
if sys.platform == "win32":
from subprocess import (
from subprocess import ( # nosec B404
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
@@ -56,6 +56,13 @@ def _spawn_windows(cmd, env):
def _spawn_posix(cmd, env):
from dvc.cli import main
# `fork` will copy buffers, so we need to flush them before forking.
# Otherwise, we will get duplicated outputs.
if sys.stdout and not sys.stdout.closed:
sys.stdout.flush()
if sys.stderr and not sys.stderr.closed:
sys.stderr.flush()
# NOTE: using os._exit instead of sys.exit, because dvc built
# with PyInstaller has trouble with SystemExit exception and throws
# errors such as "[26338] Failed to execute script __main__"
@@ -82,6 +89,7 @@ def _spawn_posix(cmd, env):
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
os.closerange(0, 3)
if platform.system() == "Darwin":
# workaround for MacOS bug
+134 -17
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:
@@ -84,14 +111,14 @@ class DataCloud:
if bool(self.repo.config["remote"]):
error_msg = (
"no remote specified. Setup default remote with\n"
f"no remote specified in {self.repo}. Setup default remote with\n"
" dvc remote default <remote name>\n"
"or use:\n"
" dvc {} -r <remote name>".format(command)
f" dvc {command} -r <remote name>"
)
else:
error_msg = (
"no remote specified. Create a default remote with\n"
f"no remote specified in {self.repo}. Create a default remote with\n"
" dvc remote add -d <remote name> <remote url>"
)
@@ -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):
+24 -152
View File
@@ -1,24 +1,14 @@
import os
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Dict, Optional, Union
from voluptuous import Required
from dvc.prompt import confirm
from dvc.utils import as_posix
from .base import Dependency
if TYPE_CHECKING:
from typing import ContextManager
from dvc.output import Output
from dvc.repo import Repo
from dvc.fs import DVCFileSystem
from dvc.stage import Stage
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.meta import Meta
from dvc_data.hashfile.obj import HashFile
from dvc_objects.db import ObjectDB
class RepoDependency(Dependency):
@@ -37,10 +27,11 @@ class RepoDependency(Dependency):
def __init__(self, def_repo: Dict[str, str], stage: "Stage", *args, **kwargs):
self.def_repo = def_repo
self._objs: Dict[str, "HashFile"] = {}
self._meta: Dict[str, "Meta"] = {}
super().__init__(stage, *args, **kwargs)
self.fs = self._make_fs()
self.fs_path = as_posix(self.def_path)
def _parse_path(self, fs, fs_path): # noqa: ARG002
return None
@@ -52,8 +43,8 @@ class RepoDependency(Dependency):
return f"{self.def_path} ({self.def_repo[self.PARAM_URL]})"
def workspace_status(self):
current = self.get_obj(locked=True).hash_info
updated = self.get_obj(locked=False).hash_info
current = self._make_fs(locked=True).repo.get_rev()
updated = self._make_fs(locked=False).repo.get_rev()
if current != updated:
return {str(self): "update available"}
@@ -64,33 +55,18 @@ class RepoDependency(Dependency):
return self.workspace_status()
def save(self):
pass
rev = self.fs.repo.get_rev()
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def download(self, to: "Output", jobs: Optional[int] = None):
from dvc_data.hashfile.checkout import checkout
for odb, objs in self.get_used_objs().items():
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
obj = self.get_obj()
checkout(
to.fs_path,
to.fs,
obj,
self.repo.cache.local,
ignore=None,
state=self.repo.state,
prompt=confirm,
)
def update(self, rev: Optional[str] = None):
if rev:
self.def_repo[self.PARAM_REV] = rev
with self._make_repo(locked=False) as repo:
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
self.fs = self._make_fs(rev=rev, locked=False)
self.def_repo[self.PARAM_REV_LOCK] = self.fs.repo.get_rev()
def changed_checksum(self) -> bool:
# From current repo point of view what describes RepoDependency is its
@@ -98,124 +74,20 @@ class RepoDependency(Dependency):
# immutable, hence its impossible for checksum to change.
return False
def get_used_objs(self, **kwargs) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _, _ = self._get_used_and_obj(**kwargs)
return used
def _make_fs(
self, rev: Optional[str] = None, locked: bool = True
) -> "DVCFileSystem":
from dvc.fs import DVCFileSystem
def _get_used_and_obj(
self, obj_only: bool = False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "Meta", "HashFile"]:
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.utils import as_posix
from dvc_data.hashfile.build import build
from dvc_data.hashfile.tree import Tree, TreeError
config = {"cache": self.repo.config["cache"]}
config["cache"]["dir"] = self.repo.cache.local_cache_dir
local_odb = self.repo.cache.local
locked = kwargs.pop("locked", True)
with self._make_repo(locked=locked, cache_dir=local_odb.path) as repo:
used_obj_ids = defaultdict(set)
rev = repo.get_rev()
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev
if not obj_only:
try:
for odb, obj_ids in repo.used_objs(
[os.path.join(repo.root_dir, self.def_path)],
force=True,
jobs=kwargs.get("jobs"),
recursive=True,
).items():
if odb is None:
odb = repo.cloud.get_remote_odb()
odb.read_only = True
self._check_circular_import(odb, obj_ids)
used_obj_ids[odb].update(obj_ids)
except (NoRemoteError, NoOutputOrStageError):
pass
try:
object_store, meta, obj = build(
local_odb,
as_posix(self.def_path),
repo.dvcfs,
local_odb.fs.PARAM_CHECKSUM,
)
except (FileNotFoundError, TreeError) as exc:
raise PathMissingError(
self.def_path, self.def_repo[self.PARAM_URL]
) from exc
object_store = copy(object_store)
object_store.read_only = True
self._objs[rev] = obj
self._meta[rev] = meta
used_obj_ids[object_store].add(obj.hash_info)
if isinstance(obj, Tree):
used_obj_ids[object_store].update(oid for _, _, oid in obj)
return used_obj_ids, meta, obj
def _check_circular_import(self, odb: "ObjectDB", obj_ids: Set["HashInfo"]) -> None:
from dvc.exceptions import CircularImportError
from dvc.fs.dvc import DVCFileSystem
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
from dvc_data.hashfile.tree import Tree
if not isinstance(odb, ReferenceHashFileDB):
return
def iter_objs():
for hash_info in obj_ids:
if hash_info.isdir:
tree = Tree.load(odb, hash_info)
yield from (odb.get(hi.value) for _, _, hi in tree)
else:
assert hash_info.value
yield odb.get(hash_info.value)
checked_urls = set()
for obj in iter_objs():
if not isinstance(obj.fs, DVCFileSystem):
continue
if obj.fs.repo_url in checked_urls or obj.fs.repo.root_dir in checked_urls:
continue
self_url = self.repo.url or self.repo.root_dir
if (
obj.fs.repo_url is not None
and obj.fs.repo_url == self_url
or obj.fs.repo.root_dir == self.repo.root_dir
):
raise CircularImportError(self, obj.fs.repo_url, self_url)
checked_urls.update([obj.fs.repo_url, obj.fs.repo.root_dir])
def get_obj(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True)
rev = self._get_rev(locked=locked)
if rev in self._objs:
return self._objs[rev]
_, _, obj = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
return DVCFileSystem(
url=self.def_repo[self.PARAM_URL],
rev=rev or self._get_rev(locked=locked),
subrepos=True,
config=config,
)
return obj
def get_meta(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True)
rev = self._get_rev(locked=locked)
if rev in self._meta:
return self._meta[rev]
_, meta, _ = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return meta
def _make_repo(self, locked: bool = True, **kwargs) -> "ContextManager[Repo]":
from dvc.external_repo import external_repo
d = self.def_repo
rev = self._get_rev(locked=locked)
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
def _get_rev(self, locked: bool = True):
d = self.def_repo
+26
View File
@@ -0,0 +1,26 @@
import os
import platformdirs
from . import env
APPNAME = "dvc"
APPAUTHOR = "iterative"
def system_config_dir():
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.site_config_dir(
APPNAME, APPAUTHOR
)
def global_config_dir():
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
APPNAME, APPAUTHOR
)
def site_cache_dir():
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
APPNAME, APPAUTHOR, opinion=True
)
+10 -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):
@@ -192,6 +187,7 @@ class SingleStageFile(FileMixin):
metrics: List[str] = []
plots: Any = {}
params: List[str] = []
artifacts: Dict[str, Optional[Dict[str, Any]]] = {}
@property
def stage(self) -> "Stage":
@@ -323,6 +319,10 @@ class ProjectFile(FileMixin):
def params(self) -> List[str]:
return self.contents.get("params", [])
@property
def artifacts(self) -> Dict[str, Optional[Dict[str, Any]]]:
return self.contents.get("artifacts", {})
def remove(self, force=False):
if not force:
logger.warning("Cannot remove pipeline file.")
@@ -353,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 = {k: v for k, v in d.items()}
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
@@ -396,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)
@@ -431,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
+14 -8
View File
@@ -1,11 +1,17 @@
DVC_CHECKPOINT = "DVC_CHECKPOINT"
DVC_DAEMON = "DVC_DAEMON"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
DVC_EXP_NAME = "DVC_EXP_NAME"
DVC_GLOBAL_CONFIG_DIR = "DVC_GLOBAL_CONFIG_DIR"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
DVC_PAGER = "DVC_PAGER"
DVC_ROOT = "DVC_ROOT"
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
DVC_EXP_NAME = "DVC_EXP_NAME"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
DVC_STUDIO_URL = "DVC_STUDIO_URL"
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
+22 -32
View File
@@ -1,4 +1,5 @@
"""Exceptions raised by the dvc."""
import errno
from typing import Dict, List
from dvc.utils import format_link
@@ -31,14 +32,19 @@ class OutputDuplicationError(DvcException):
assert isinstance(output, str)
assert all(hasattr(stage, "relpath") for stage in stages)
msg = ""
stage_names = [s.addressing for s in stages]
stages_str = " ".join(stage_names)
if len(stages) == 1:
msg = "output '{}' is already specified in {}.".format(
output, first(stages)
)
stage_name = first(stages)
msg = f"output '{output}' is already specified in {stage_name}."
else:
msg = "output '{}' is already specified in stages:\n{}".format(
output, "\n".join(f"\t- {s.addressing}" for s in stages)
output, "\n".join(f"\t- {s}" for s in stage_names)
)
msg += (
f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
)
super().__init__(msg)
self.stages = stages
self.output = output
@@ -176,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
@@ -201,6 +202,18 @@ class ETagMismatchError(DvcException):
)
class FileExistsLocallyError(FileExistsError, DvcException):
def __init__(self, path, hint=None):
import os.path
self.path = path
hint = "" if hint is None else f". {hint}"
path_typ = "directory" if os.path.isdir(path) else "file"
msg = f"The {path_typ} '{path}' already exists locally{hint}"
super().__init__(msg)
self.errno = errno.EEXIST
class FileMissingError(DvcException):
def __init__(self, path, hint=None):
self.path = path
@@ -286,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."""
@@ -333,14 +331,6 @@ class CacheLinkError(DvcException):
self.fs_paths = fs_paths
class CircularImportError(DvcException):
def __init__(self, dep, a, b):
super().__init__(
f"'{dep}' contains invalid circular import. "
f"DVC repo '{a}' already imports from '{b}'."
)
class PrettyDvcException(DvcException):
def __pretty_exc__(self, **kwargs):
"""Print prettier exception message."""
+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,
+31 -11
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"]
@@ -251,33 +256,39 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
try:
return self.repo.fs.open(fs_path, mode=mode)
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.open(dvc_path, mode=mode)
kw = {}
if kwargs.get("cache_remote_stream", False):
kw["cache_odb"] = repo.cache.local
return dvc_fs.open(dvc_path, mode=mode, **kw)
def isdvc(self, path, **kwargs) -> bool:
"""Is this entry dvc-tracked?"""
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
def ls( # pylint: disable=arguments-differ # noqa: C901
self, path, detail=True, dvc_only=False, **kwargs
):
key = self._get_key_from_relative(path)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_exists = False
dvc_infos = {}
if dvc_fs:
dvc_path = _get_dvc_path(dvc_fs, subkey)
with suppress(FileNotFoundError):
dvc_path = _get_dvc_path(dvc_fs, subkey)
for info in dvc_fs.ls(dvc_path, detail=True):
dvc_infos[dvc_fs.path.name(info["name"])] = info
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
fs_exists = False
fs_infos = {}
ignore_subrepos = kwargs.get("ignore_subrepos", True)
if not dvc_only:
@@ -291,17 +302,26 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
except (FileNotFoundError, NotADirectoryError):
pass
fs_exists = bool(fs_infos) or fs.exists(fs_path)
dvcfiles = kwargs.get("dvcfiles", False)
infos = []
paths = []
names = set(dvc_infos.keys()) | set(fs_infos.keys())
if not names and (dvc_exists or fs_exists):
# broken symlink or TreeError
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
for name in names:
if not dvcfiles and _is_dvc_file(name):
continue
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)
@@ -357,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
+21 -10
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",
@@ -32,6 +31,7 @@ def get_dvc_info():
f"Platform: Python {platform.python_version()} on {platform.platform()}",
f"Subprojects:{_get_subprojects()}",
f"Supports:{_get_supported_remotes()}",
f"Config:{_get_config_dirs()}",
]
try:
@@ -42,7 +42,7 @@ def get_dvc_info():
# `dvc config cache.shared group`.
if os.path.exists(repo.cache.local.path):
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
fs_type = get_fs_type(repo.cache.local.path)
fs_type = _get_fs_type(repo.cache.local.path)
info.append(f"Cache directory: {fs_type}")
else:
info.append("Cache types: " + error_link("no-dvc-cache"))
@@ -51,7 +51,7 @@ def get_dvc_info():
info.append(f"Remotes: {_get_remotes(repo.config)}")
root_directory = repo.root_dir
fs_root = get_fs_type(os.path.abspath(root_directory))
fs_root = _get_fs_type(os.path.abspath(root_directory))
info.append(f"Workspace directory: {fs_root}")
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
@@ -67,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
@@ -136,10 +136,21 @@ def _get_supported_remotes():
return "\n\t" + ",\n\t".join(supported_remotes)
def get_fs_type(path):
def _get_config_dirs():
from dvc.config import Config
dirs = [
f"Global: {Config.get_dir('global')}",
f"System: {Config.get_dir('system')}",
]
return "\n\t" + "\n\t".join(dirs)
def _get_fs_type(path):
partition = {}
for part in psutil.disk_partitions(all=True):
if part.fstype != "":
if part.fstype:
try:
mountpoint = pathlib.Path(part.mountpoint).resolve()
partition[mountpoint] = part.fstype + " on " + part.device
@@ -155,11 +166,11 @@ def get_fs_type(path):
return ("unknown", "none")
def _get_dvc_repo_info(self):
if self.config.get("core", {}).get("no_scm", False):
def _get_dvc_repo_info(repo):
if repo.config.get("core", {}).get("no_scm", False):
return "dvc (no_scm)"
if self.root_dir != self.scm.root_dir:
if repo.root_dir != repo.scm.root_dir:
return "dvc (subdir), git"
return "dvc, git"
+3 -1
View File
@@ -187,7 +187,9 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
hasher = hashlib.md5( # nosec B324, B303 # noqa: S324
self._claimfile.encode()
)
filename = hasher.hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
+13 -4
View File
@@ -3,10 +3,12 @@
import logging
import logging.config
import logging.handlers
import os
import sys
import colorama
from dvc.env import DVC_SHOW_TRACEBACK
from dvc.progress import Tqdm
@@ -86,9 +88,10 @@ class ColorFormatter(logging.Formatter):
"CRITICAL": colorama.Fore.RED,
}
def __init__(self, log_colors: bool = True) -> None:
def __init__(self, log_colors: bool = True, show_traceback: bool = False) -> None:
super().__init__()
self.log_colors = log_colors
self.show_traceback = show_traceback
def format(self, record) -> str: # noqa: A003, C901
record.message = record.getMessage()
@@ -106,8 +109,10 @@ class ColorFormatter(logging.Formatter):
msg = msg + sep + cause
asctime = ""
if _is_verbose():
verbose = _is_verbose()
if verbose:
asctime = self.formatTime(record, self.datefmt)
if verbose or self.show_traceback:
if ei and not record.exc_text:
record.exc_text = self.formatException(ei)
if record.exc_text:
@@ -146,7 +151,8 @@ class LoggerHandler(logging.StreamHandler):
self.emit_pretty_exception(exc, verbose=_is_verbose())
if not _is_verbose():
return
except Exception: # noqa, pylint: disable=broad-except
# pylint: disable-next=broad-except
except Exception: # noqa: BLE001, S110 # nosec B110
pass
msg = self.format(record)
@@ -197,7 +203,10 @@ def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
console_trace.setFormatter(formatter)
console_trace.addFilter(exclude_filter(logging.DEBUG))
err_formatter = ColorFormatter(log_colors=log_colors and sys.stderr.isatty())
show_traceback = bool(os.environ.get(DVC_SHOW_TRACEBACK))
err_formatter = ColorFormatter(
log_colors=log_colors and sys.stderr.isatty(), show_traceback=show_traceback
)
console_errors = LoggerHandler(sys.stderr)
console_errors.setLevel(logging.WARNING)
console_errors.setFormatter(err_formatter)
-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
+293 -96
View File
@@ -1,10 +1,11 @@
import errno
import logging
import os
import posixpath
from collections import defaultdict
from contextlib import suppress
from operator import itemgetter
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union
from urllib.parse import urlparse
from funcy import collecting, first, project
@@ -18,33 +19,32 @@ from dvc.exceptions import (
ConfirmRemoveError,
DvcException,
MergeError,
RemoteCacheRequiredError,
)
from dvc.utils.objects import cached_property
from dvc_data.hashfile import check as ocheck
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
from dvc_data.hashfile.transfer import transfer as otransfer
from dvc_data.hashfile.tree import Tree
from dvc_data.hashfile.tree import Tree, du
from dvc_objects.errors import ObjectFormatError
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
from .fs.callbacks import DEFAULT_CALLBACK
from .fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
from .utils import relpath
from .utils.fs import path_isin
if TYPE_CHECKING:
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.obj import HashFile
from dvc_data.index import DataIndexKey
from dvc_objects.db import ObjectDB
from .fs.callbacks import Callback
from .ignore import DvcIgnoreFilter
logger = logging.getLogger(__name__)
@@ -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"
@@ -276,12 +285,16 @@ class OutputIsIgnoredError(DvcException):
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
class CheckoutCallback(TqdmCallback):
# disable branching for checkouts
branch = Callback.branch # type: ignore[assignment]
class Output:
IS_DEPENDENCY = False
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_CHECKPOINT = "checkpoint"
PARAM_FILES = "files"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
@@ -298,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
@@ -322,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,
@@ -332,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 {}
@@ -389,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)
@@ -404,21 +408,32 @@ 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:
from dvc_data.hashfile.db import HashFileDB
tree = Tree.from_list(self.files, hash_name=self.hash_name)
tree.digest(with_meta=True)
self.odb = HashFileDB(tree.fs, tree.path + ".odb")
@@ -427,11 +442,11 @@ class Output:
self.hash_info = tree.hash_info
self.meta.isdir = True
self.meta.nfiles = len(self.files)
self.meta.size = sum(f.get("size") for f in self.files)
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
self.meta.remote = first(f.get("remote") for f in self.files)
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
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)
@@ -495,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,
)
@@ -508,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):
@@ -528,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,
@@ -669,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,
@@ -679,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,
@@ -692,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)
@@ -741,15 +769,18 @@ class Output:
hardlink=hardlink,
)
if relink:
self._checkout(
filter_info or self.fs_path,
self.fs,
obj,
self.cache,
relink=True,
state=self.repo.state,
prompt=prompt.confirm,
)
rel = self.fs.path.relpath(filter_info or self.fs_path)
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
self._checkout(
filter_info or self.fs_path,
self.fs,
obj,
self.cache,
relink=True,
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=cb,
)
self.set_exec()
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
@@ -775,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)
@@ -785,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:
@@ -793,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:
@@ -816,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
@@ -888,12 +926,18 @@ 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.
# do not let checkout to call set_size and change progressbar.
class CallbackProxy(Callback):
def relative_update(self, inc: int = 1) -> None:
progress_callback.relative_update(inc)
return super().relative_update(inc)
callback = CallbackProxy()
if not self.use_cache:
if progress_callback != DEFAULT_CALLBACK:
progress_callback.relative_update(self.get_files_number(filter_info))
callback.relative_update(self.get_files_number(filter_info))
return None
obj = self.get_obj(filter_info=filter_info)
@@ -901,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:
@@ -915,21 +954,24 @@ class Output:
obj,
self.cache,
force=force,
progress_callback=progress_callback,
progress_callback=callback,
relink=relink,
state=self.repo.state,
prompt=prompt.confirm,
**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
@@ -974,7 +1016,7 @@ class Output:
odb,
from_info,
from_fs,
"md5",
DEFAULT_ALGORITHM,
upload=upload,
no_progress_bar=no_progress_bar,
)
@@ -1008,7 +1050,7 @@ class Output:
if self.exists:
self.cache.unprotect(self.fs_path)
def get_dir_cache(self, **kwargs):
def get_dir_cache(self, **kwargs) -> Optional["Tree"]:
if not self.is_dir_checksum:
raise DvcException("cannot get dir cache for file checksum")
@@ -1022,14 +1064,17 @@ class Output:
self.repo.cloud.pull([obj.hash_info], **kwargs)
if self.obj:
assert isinstance(self.obj, Tree)
return self.obj
try:
self.obj = oload(self.cache, self.hash_info)
obj = oload(self.cache, self.hash_info)
assert isinstance(obj, Tree)
except (FileNotFoundError, ObjectFormatError):
self.obj = None
obj = None
return self.obj
self.obj = obj
return obj
def _collect_used_dir_cache(
self, remote=None, force=False, jobs=None, filter_info=None
@@ -1066,7 +1111,7 @@ class Output:
return obj.filter(prefix)
return obj
def get_used_objs( # noqa: C901, PLR0911
def get_used_objs( # noqa: C901
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return filtered set of used object IDs for this out."""
@@ -1076,9 +1121,7 @@ class Output:
push: bool = kwargs.pop("push", False)
if self.stage.is_repo_import:
if push:
return {}
return self.get_used_external(**kwargs)
return {}
if push and not self.can_push:
return {}
@@ -1114,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
@@ -1130,15 +1175,6 @@ class Output:
oids.add(oid)
return oids
def get_used_external(
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
if not self.use_cache or not self.stage.is_repo_import:
return {}
(dep,) = self.stage.deps
return dep.get_used_objs(**kwargs)
def _validate_output_path(self, path, stage=None):
from dvc.dvcfile import is_valid_filename
@@ -1162,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:
@@ -1176,7 +1213,7 @@ class Output:
def merge(self, ancestor, other, allowed=None):
from dvc_data.hashfile.tree import MergeError as TreeMergeError
from dvc_data.hashfile.tree import du, merge
from dvc_data.hashfile.tree import merge
assert other
@@ -1209,6 +1246,168 @@ class Output:
nfiles=len(merged),
)
def unstage(self, path: str) -> Tuple["Meta", "Tree"]:
from pygtrie import Trie
from dvc_objects.fs.path import Path
assert isinstance(self.fs.path, Path)
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
if self.hash_info:
tree = self.get_dir_cache()
if tree is None:
raise DvcException(f"could not read {self.hash_info.value!r}")
else:
tree = Tree()
trie = tree.as_trie()
assert isinstance(trie, Trie)
try:
del trie[rel_key:] # type: ignore[misc]
except KeyError:
raise FileNotFoundError( # noqa: B904
errno.ENOENT,
os.strerror(errno.ENOENT),
self.fs.path.relpath(path),
)
new = tree.from_trie(trie)
new.digest()
return Meta(nfiles=len(new), isdir=True), new
def apply(
self,
path: str,
obj: Union["Tree", "HashFile"],
meta: "Meta",
) -> Tuple["Meta", "Tree"]:
from pygtrie import Trie
from dvc_objects.fs.path import Path
assert isinstance(self.fs.path, Path)
append_only = True
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
if self.hash_info:
tree = self.get_dir_cache()
if tree is None:
raise DvcException(f"could not read {self.hash_info.value!r}")
else:
tree = Tree()
trie = tree.as_trie()
assert isinstance(trie, Trie)
try:
del trie[rel_key:] # type: ignore[misc]
except KeyError:
pass
else:
append_only = False
items = {}
if isinstance(obj, Tree):
items = {(*rel_key, *key): (m, o) for key, m, o in obj}
else:
items = {rel_key: (meta, obj.hash_info)}
trie.update(items)
new = Tree.from_trie(trie)
new.digest()
size = self.meta.size if self.meta and self.meta.size else None
if append_only and size and meta.size is not None:
# if files were only appended, we can sum to the existing size
size += meta.size
elif self.hash_info and self.hash_info == new.hash_info:
# if hashes are same, sizes must have been the same
size = self.meta.size
else:
size = None
meta = Meta(nfiles=len(new), size=size, isdir=True)
return meta, new
def add( # noqa: C901
self, path: Optional[str] = None, no_commit: bool = False, relink: bool = True
) -> Optional["HashFile"]:
path = path or self.fs_path
if self.hash_info and not self.is_dir_checksum and self.fs_path != path:
raise DvcException(
f"Cannot modify '{self}' which is being tracked as a file"
)
assert self.repo
self._update_legacy_hash_name()
cache = self.cache if self.use_cache else self.local_cache
assert isinstance(cache, HashFileDB)
new: "HashFile"
try:
assert self.hash_name
staging, meta, obj = build(
cache,
path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
dry_run=not self.use_cache,
)
except FileNotFoundError as exc:
if self.fs_path == path:
raise self.DoesNotExistError(self) from exc
if not self.is_dir_checksum:
raise
meta, new = self.unstage(path)
staging, obj = None, None
else:
assert obj
assert staging
if self.fs_path != path:
meta, new = self.apply(path, obj, meta)
add_update_tree(staging, new)
else:
new = obj
self.obj = new
self.hash_info = self.obj.hash_info
self.meta = meta
self.files = None
self.ignore()
if no_commit or not self.use_cache:
return obj
if isinstance(new, Tree):
add_update_tree(cache, new)
if not obj:
return obj
assert staging
assert obj.hash_info
otransfer(staging, self.cache, {obj.hash_info}, hardlink=relink, shallow=False)
if relink:
rel = self.fs.path.relpath(path)
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as callback:
self._checkout(
path,
self.fs,
obj,
self.cache,
relink=True,
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=callback,
)
self.set_exec()
return obj
@property
def fspath(self):
return self.fs_path
@@ -1268,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] = {
@@ -1285,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],
+1 -1
View File
@@ -392,7 +392,7 @@ class ForeachDefinition:
"""Convert sequence to Mapping with keys normalized."""
iterable = self.resolved_iterable
if isinstance(iterable, Mapping):
return iterable
return {to_str(k): v for k, v in iterable.items()}
assert isinstance(iterable, Sequence)
if any(map(is_map_or_seq, iterable)):
+2 -2
View File
@@ -73,7 +73,7 @@ def embrace(s: str):
def escape_str(value):
if os.name == "nt":
from subprocess import list2cmdline
from subprocess import list2cmdline # nosec B404
return list2cmdline([value])
from shlex import quote
@@ -95,7 +95,7 @@ def _(obj: bool):
def _(obj: dict): # noqa: C901
from dvc.config import Config
config = Config().get("parsing", {})
config = Config.from_cwd().get("parsing", {})
result = ""
for k, v in flatten(obj).items():
-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)
+5 -4
View File
@@ -1,4 +1,3 @@
import json
from collections import defaultdict
from typing import Dict, List, Union
@@ -32,15 +31,17 @@ def to_json(renderer, split: bool = False) -> List[Dict]:
if renderer.TYPE == "vega":
grouped = _group_by_rev(renderer.datapoints)
if split:
content = renderer.get_filled_template(skip_anchors=["data"])
content = renderer.get_filled_template(
skip_anchors=["data"], as_string=False
)
else:
content = renderer.get_filled_template()
content = renderer.get_filled_template(as_string=False)
if grouped:
return [
{
TYPE_KEY: renderer.TYPE,
REVISIONS_KEY: sorted(grouped.keys()),
"content": json.loads(content),
"content": content,
"datapoints": grouped,
}
]
+1 -2
View File
@@ -56,7 +56,6 @@ def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
datapoint = first(file2datapoints[filename])
if field not in datapoint:
raise FieldNotFoundError(field, datapoint.keys())
return
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
@@ -245,7 +244,7 @@ class VegaConverter(Converter):
for i, (y_file, y_field) in enumerate(ys):
if num_xs > 1:
x_file, x_field = xs[i]
datapoints = [d.copy() for d in file2datapoints.get(y_file, [])]
datapoints = [{**d} for d in file2datapoints.get(y_file, [])]
if props_update.get("y", None) == "dvc_inferred_y_value":
_update_from_field(
+29 -6
View File
@@ -1,10 +1,10 @@
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Optional
from typing import TYPE_CHECKING, DefaultDict, Dict, List, NamedTuple, Optional
import dpath
import dpath.options
from funcy import last
from funcy import get_in, last
from dvc.repo.plots import _normpath, infer_data_sources
from dvc.utils.plots import get_plot_id
@@ -13,6 +13,8 @@ from .convert import _get_converter
if TYPE_CHECKING:
from dvc.types import StrPath
from dvc_render.base import Renderer
dpath.options.ALLOW_EMPTY_STRING_KEYS = True
@@ -61,21 +63,31 @@ class PlotsData:
return result
def match_defs_renderers(
class RendererWithErrors(NamedTuple):
renderer: "Renderer"
source_errors: Dict[str, Dict[str, Exception]]
definition_errors: Dict[str, Exception]
def match_defs_renderers( # noqa: C901, PLR0912
data,
out=None,
templates_dir: Optional["StrPath"] = None,
):
) -> List[RendererWithErrors]:
from dvc_render import ImageRenderer, VegaRenderer
plots_data = PlotsData(data)
renderers = []
renderer_cls = None
for plot_id, group in plots_data.group_definitions().items():
plot_datapoints: List[Dict] = []
props = _squash_plots_properties(group)
final_props: Dict = {}
def_errors: Dict[str, Exception] = {}
src_errors: DefaultDict[str, Dict[str, Exception]] = defaultdict(dict)
if out is not None:
props["out"] = out
if templates_dir is not None:
@@ -94,13 +106,24 @@ def match_defs_renderers(
converter = _get_converter(renderer_cls, inner_id, props, definitions_data)
dps, rev_props = converter.flat_datapoints(rev)
for src in plot_sources:
if error := get_in(data, [rev, "sources", "data", src, "error"]):
src_errors[rev][src] = error
try:
dps, rev_props = converter.flat_datapoints(rev)
except Exception as e: # noqa: BLE001, pylint: disable=broad-except
def_errors[rev] = e
continue
if not final_props and rev_props:
final_props = rev_props
plot_datapoints.extend(dps)
if "title" not in final_props:
final_props["title"] = renderer_id
if renderer_cls is not None:
renderers.append(renderer_cls(plot_datapoints, renderer_id, **final_props))
renderer = renderer_cls(plot_datapoints, renderer_id, **final_props)
renderers.append(RendererWithErrors(renderer, dict(src_errors), def_errors))
return renderers
+35 -60
View File
@@ -8,14 +8,11 @@ from typing import (
Callable,
ContextManager,
Iterable,
List,
Optional,
Tuple,
Union,
)
from dvc.exceptions import FileMissingError
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError
from dvc.ignore import DvcIgnoreFilter
from dvc.utils import env2bool
@@ -125,6 +122,8 @@ class Repo:
if not scm:
try:
scm = SCM(root_dir or os.curdir)
if scm.dulwich.repo.bare:
raise NotDvcRepoError(f"{scm.root_dir} is a bare git repo")
except SCMError:
scm = SCM(os.curdir, no_scm=True)
@@ -151,6 +150,7 @@ class Repo:
from dvc.data_cloud import DataCloud
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
from dvc.lock import LockNoop, make_lock
from dvc.repo.artifacts import Artifacts
from dvc.repo.metrics import Metrics
from dvc.repo.params import Params
from dvc.repo.plots import Plots
@@ -224,6 +224,7 @@ class Repo:
self.metrics: Metrics = Metrics(self)
self.plots: Plots = Plots(self)
self.params: Params = Params(self)
self.artifacts: Artifacts = Artifacts(self)
self.stage_collection_error_handler: Optional[
Callable[[str, Exception], None]
@@ -285,18 +286,9 @@ class Repo:
@staticmethod
def open(url, *args, **kwargs): # noqa: A003
if url is None:
url = os.getcwd()
from .open_repo import open_repo
if os.path.exists(url):
try:
return Repo(url, *args, **kwargs)
except NotDvcRepoError:
pass # fallthrough to external_repo
from dvc.external_repo import external_repo
return external_repo(url, *args, **kwargs)
return open_repo(url, *args, **kwargs)
@cached_property
def scm(self) -> Union["Git", "NoSCM"]:
@@ -375,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}'"
@@ -408,9 +408,12 @@ class Repo:
raise NotDvcRepoError(msg)
@classmethod
def find_dvc_dir(cls, root=None) -> str:
root_dir = cls.find_root(root)
return os.path.join(root_dir, cls.DVC_DIR)
def find_dvc_dir(cls, root=None, fs=None) -> str:
from dvc.fs import localfs
fs = fs or localfs
root_dir = cls.find_root(root, fs=fs)
return fs.path.join(root_dir, cls.DVC_DIR)
@staticmethod
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False) -> "Repo":
@@ -425,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)
@@ -504,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
@@ -555,13 +552,13 @@ class Repo:
@cached_property
def site_cache_dir(self) -> str:
import getpass
import hashlib
import platformdirs
from dvc.dirs import site_cache_dir
from dvc.fs import GitFileSystem
cache_dir = platformdirs.site_cache_dir("dvc", "iterative", opinion=True)
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
if isinstance(self.fs, GitFileSystem):
relparts = ()
@@ -572,41 +569,19 @@ class Repo:
else:
root_dir = self.root_dir
repo_token = hashlib.md5(os.fsencode(root_dir)).hexdigest() # noqa: S324
return os.path.join(cache_dir, "repo", repo_token)
@contextmanager
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
from dvc.fs.data import DataFileSystem
from dvc.fs.dvc import DVCFileSystem
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
if os.path.isabs(path):
fs = DataFileSystem(index=self.index.data["local"])
fs_path = path
else:
fs = DVCFileSystem(repo=self, subrepos=True)
fs_path = fs.from_os_path(path)
repos_dir = os.path.join(cache_dir, "repo")
umask = os.umask(0)
try:
if remote:
remote_odb = self.cloud.get_remote_odb(name=remote)
oid = fs.info(fs_path)["dvc_info"]["md5"]
fs = remote_odb.fs
fs_path = remote_odb.oid_to_path(oid)
os.makedirs(repos_dir, mode=0o777, exist_ok=True)
finally:
os.umask(umask)
with fs.open(
fs_path,
mode=mode,
encoding=encoding,
) as fobj:
yield fobj
except FileNotFoundError as exc:
raise FileMissingError(path) from exc
except IsADirectoryError as exc:
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
md5 = hashlib.md5( # noqa: S324 # nosec B324, B303
str((root_dir, getpass.getuser())).encode()
)
repo_token = md5.hexdigest()
return os.path.join(repos_dir, repo_token)
def close(self):
self.scm.close()
+153 -184
View File
@@ -1,96 +1,120 @@
import logging
import os
from contextlib import contextmanager
from itertools import tee
from typing import TYPE_CHECKING, Any, Iterator, List, Optional
import colorama
from typing import (
TYPE_CHECKING,
Dict,
Iterator,
List,
NamedTuple,
Optional,
Tuple,
Union,
)
from dvc.exceptions import (
CacheLinkError,
InvalidArgumentError,
DvcException,
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
OVERLAPPING_OUTPUT_FMT = (
class StageInfo(NamedTuple):
stage: "Stage"
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"
"To include '{out}' in '{parent}', run "
"'dvc commit {parent_stage}'"
)
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))
OVERLAPPING_PARENT_FMT = (
"Cannot add '{parent}', because it is overlapping with other "
"DVC tracked output: '{out}'.\n"
"To include '{out}' in '{parent}', run "
"'dvc remove {out_stage}' and then 'dvc add {parent}'"
)
@contextmanager
def translate_graph_error(stages: Stages) -> Iterator[None]:
def translate_graph_error(stages: List["Stage"]) -> Iterator[None]:
try:
yield
except OverlappingOutputPathsError as exc:
msg = OVERLAPPING_OUTPUT_FMT.format(
out=exc.overlapping_out.fs_path,
parent=exc.parent.fs_path,
parent_stage=exc.parent.stage.addressing,
)
if exc.parent in [o for s in stages for o in s.outs]:
msg = OVERLAPPING_PARENT_FMT.format(
out=exc.overlapping_out,
parent=exc.parent,
out_stage=exc.overlapping_out.stage.addressing,
)
else:
msg = OVERLAPPING_CHILD_FMT.format(
out=exc.overlapping_out,
parent=exc.parent,
parent_stage=exc.parent.stage.addressing,
)
raise OverlappingOutputPathsError( # noqa: B904
exc.parent, exc.overlapping_out, msg
)
@@ -100,18 +124,21 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
)
def progress_iter(stages: Stages) -> Iterator["Stage"]:
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 stage in pbar:
for item, stage_info in pbar:
if total > 1:
pbar.set_msg(f"{stage.outs[0]}")
yield stage
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
@@ -138,133 +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"))
msg = "Collecting stages from the workspace"
with translate_graph_error(stages), 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, 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, source in zip(progress_iter(stages), sources):
if to_remote or to_cache:
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
else:
try:
stage.save()
if not no_commit:
stage.commit()
except CacheLinkError:
link_failures.append(str(stage.relpath))
stage.dump()
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
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,
**kwargs: Any,
) -> Iterator["Stage"]:
for target in targets:
if kwargs.get("out"):
target = resolve_output(target, kwargs["out"])
path, wdir, out = resolve_paths(
repo, target, always_local=transfer and not kwargs.get("out")
)
stage = repo.stage.create(
single_stage=True,
validate=False,
fname=fname or path,
wdir=wdir,
outs=[out],
external=external,
)
out_obj = stage.outs[0]
out_obj.annot.update(**kwargs)
yield stage
+87
View File
@@ -0,0 +1,87 @@
import logging
import re
from pathlib import Path
from typing import Dict, Optional
from dvc.annotations import Artifact
from dvc.dvcfile import PROJECT_FILE
from dvc.exceptions import InvalidArgumentError
from dvc.repo import Repo
from dvc.utils import relpath
from dvc.utils.serialize import modify_yaml
logger = logging.getLogger(__name__)
# Constants are taken from GTO.
# When we make it a dependency, we can import them instead
SEPARATOR_IN_NAME = ":"
DIRNAME = r"[a-z0-9-_./]+"
NAME = r"[a-z0-9]([a-z0-9-/]*[a-z0-9])?"
NAME_RE = re.compile(f"^{NAME}$")
FULLNAME = f"((?P<dirname>{DIRNAME}){SEPARATOR_IN_NAME})?(?P<name>{NAME})"
FULLNAME_RE = re.compile(f"^{FULLNAME}$")
def name_is_compatible(name: str) -> bool:
return bool(NAME_RE.search(name))
def check_name_format(name: str) -> None:
if not name_is_compatible(name):
raise InvalidArgumentError(
f"Can't use '{name}' as artifact name (ID)."
" You can use letters and numbers, and use '-' as separator"
" (but not at the start or end)."
)
def check_for_nested_dvc_repo(dvcfile: Path):
if dvcfile.is_absolute():
raise InvalidArgumentError("Use relative path to dvc.yaml.")
path = dvcfile.parent
while path.name:
if (path / Repo.DVC_DIR).is_dir():
raise InvalidArgumentError(
f"Nested DVC repos like {path} are not supported."
)
path = path.parent
class Artifacts:
def __init__(self, repo: "Repo") -> None:
self.repo = repo
def read(self) -> Dict[str, Dict[str, Artifact]]:
artifacts: Dict[str, Dict[str, Artifact]] = {}
for (
dvcfile,
dvcfile_artifacts,
) in self.repo.index._artifacts.items(): # pylint: disable=protected-access
dvcyaml = relpath(dvcfile, self.repo.root_dir)
artifacts[dvcyaml] = {}
for name, value in dvcfile_artifacts.items():
try:
check_name_format(name)
except InvalidArgumentError as e:
logger.warning(e.msg)
artifacts[dvcyaml][name] = Artifact(**value)
return artifacts
def add(self, name: str, artifact: Artifact, dvcfile: Optional[str] = None):
with self.repo.scm_context(quiet=True):
check_name_format(name)
dvcyaml = Path(dvcfile or PROJECT_FILE)
check_for_nested_dvc_repo(
dvcyaml.relative_to(self.repo.root_dir)
if dvcyaml.is_absolute()
else dvcyaml
)
with modify_yaml(dvcyaml) as data:
artifacts = data.setdefault("artifacts", {})
artifacts.update({name: artifact.to_dict()})
self.repo.scm_context.track_file(dvcfile)
return artifacts.get(name)
+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 -36
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,17 +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,
not_in_remote=True,
callback=pb.as_callback(),
**kwargs,
)
@@ -185,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):
@@ -252,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()}
+9 -4
View File
@@ -1,9 +1,9 @@
import errno
import logging
import os
from collections import defaultdict
from typing import Dict, List, Optional
from dvc.exceptions import PathMissingError
from dvc.repo import locked
from dvc.ui import ui
@@ -154,10 +154,15 @@ def diff(
# check for overlapping missing targets between a_rev and b_rev
for target in old_missing & new_missing:
raise PathMissingError(target, self)
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
old = indexes[a_rev]
new = indexes[b_rev]
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)
+30 -127
View File
@@ -1,12 +1,10 @@
import logging
import os
import re
import time
from typing import TYPE_CHECKING, Dict, Iterable, Optional
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
@@ -18,16 +16,11 @@ from .exceptions import (
InvalidExpRefError,
MultipleBranchError,
)
from .executor.base import BaseExecutor
from .queue.celery import LocalCeleryQueue
from .queue.tempdir import TempDirQueue
from .queue.workspace import WorkspaceQueue
from .refs import (
APPLY_STASH,
CELERY_FAILED_STASH,
CELERY_STASH,
EXEC_APPLY,
EXEC_CHECKPOINT,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
WORKSPACE_STASH,
@@ -38,6 +31,9 @@ from .utils import check_ref_format, exp_refs_by_rev, unlocked_repo
if TYPE_CHECKING:
from .queue.base import BaseStashQueue, QueueEntry
from .queue.celery import LocalCeleryQueue
from .queue.tempdir import TempDirQueue
from .queue.workspace import WorkspaceQueue
from .stash import ExpStashEntry
logger = logging.getLogger(__name__)
@@ -50,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
@@ -78,20 +71,28 @@ class Experiments:
@cached_property
def args_file(self) -> str:
from .executor.base import BaseExecutor
return os.path.join(self.repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
@cached_property
def workspace_queue(self) -> WorkspaceQueue:
def workspace_queue(self) -> "WorkspaceQueue":
from .queue.workspace import WorkspaceQueue
return WorkspaceQueue(self.repo, WORKSPACE_STASH)
@cached_property
def tempdir_queue(self) -> TempDirQueue:
def tempdir_queue(self) -> "TempDirQueue":
from .queue.tempdir import TempDirQueue
# NOTE: tempdir and workspace stash is shared since both
# implementations immediately push -> pop (queue length is only 0 or 1)
return TempDirQueue(self.repo, WORKSPACE_STASH)
@cached_property
def celery_queue(self) -> LocalCeleryQueue:
def celery_queue(self) -> "LocalCeleryQueue":
from .queue.celery import LocalCeleryQueue
return LocalCeleryQueue(self.repo, CELERY_STASH, CELERY_FAILED_STASH)
@cached_property
@@ -112,6 +113,8 @@ class Experiments:
def reproduce_one(
self,
tmp_dir: bool = False,
copy_paths: Optional[List[str]] = None,
message: Optional[str] = None,
**kwargs,
):
"""Reproduce and checkout a single (standalone) experiment."""
@@ -119,7 +122,9 @@ class Experiments:
self.tempdir_queue if tmp_dir else self.workspace_queue
)
self.queue_one(exp_queue, **kwargs)
results = self._reproduce_queue(exp_queue)
results = self._reproduce_queue(
exp_queue, copy_paths=copy_paths, message=message
)
exp_rev = first(results)
if exp_rev is not None:
self._log_reproduced(results, tmp_dir=tmp_dir)
@@ -128,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]:
@@ -198,8 +174,7 @@ class Experiments:
)
for entry in entries:
# wait for task execution to start
while not self.celery_queue.proc.get(entry.stash_rev):
time.sleep(1)
self.celery_queue.wait_for_start(entry, sleep_interval=1)
self.celery_queue.follow(entry)
# wait for task collection to complete
try:
@@ -248,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()
@@ -272,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)
@@ -337,12 +254,14 @@ 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, queue: "BaseStashQueue", **kwargs) -> Dict[str, str]:
def _reproduce_queue(
self,
queue: "BaseStashQueue",
copy_paths: Optional[List[str]] = None,
message: Optional[str] = None,
**kwargs,
) -> Dict[str, str]:
"""Reproduce queued experiments.
Arguments:
@@ -352,7 +271,7 @@ class Experiments:
dict mapping successfully reproduced experiment revs to their
results.
"""
exec_results = queue.reproduce()
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
results: Dict[str, str] = {}
for _, exp_result in exec_results.items():
@@ -434,17 +353,6 @@ class Experiments:
result[rev] = name
return result
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
"""Return info for running experiments."""
result = {}
for queue in (
self.workspace_queue,
self.tempdir_queue,
self.celery_queue,
):
result.update(queue.get_running_exps(fetch_refs))
return result
def apply(self, *args, **kwargs):
from dvc.repo.experiments.apply import apply
@@ -475,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
+3 -1
View File
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
@locked
@scm_context
def branch(repo, exp_rev, branch_name, *args, **kwargs):
def branch(repo, exp_rev, branch_name=None, **kwargs):
from dvc.scm import resolve_rev
try:
@@ -46,6 +46,8 @@ def branch(repo, exp_rev, branch_name, *args, **kwargs):
if not ref_info:
raise InvalidExpRevError(exp_rev)
branch_name = branch_name or f"{ref_info.name}-branch"
branch_ref = f"refs/heads/{branch_name}"
if repo.scm.get_ref(branch_ref):
raise InvalidArgumentError(f"Git branch '{branch_name}' already exists.")
+47
View File
@@ -0,0 +1,47 @@
from contextlib import ExitStack, contextmanager
from typing import TYPE_CHECKING, Iterator, Tuple
from dvc.repo.experiments.exceptions import InvalidExpRevError
from dvc.scm import RevError
if TYPE_CHECKING:
from dvc.repo import Repo
@contextmanager
def switch_repo(
repo: "Repo",
rev: str,
) -> Iterator[Tuple["Repo", str]]:
"""Return a repo instance (brancher) switched to rev.
If rev is the name of a running experiment, the returned instance will be
the live repo wherever the experiment is running.
NOTE: This will not resolve git SHA's that only exist in queued exp workspaces
(it will only match queued exp names).
"""
try:
with repo.switch(rev):
yield repo, rev
return
except RevError as exc:
orig_exc = exc
exps = repo.experiments
if rev == exps.workspace_queue.get_running_exp():
yield repo, "workspace"
return
for queue in (exps.tempdir_queue, exps.celery_queue):
try:
active_repo = queue.active_repo(rev)
except InvalidExpRevError:
continue
stack = ExitStack()
stack.enter_context(active_repo)
stack.enter_context(active_repo.switch("workspace"))
with stack:
yield active_repo, rev
return
raise orig_exc
+382
View File
@@ -0,0 +1,382 @@
import itertools
import logging
import os
from datetime import datetime
from typing import (
TYPE_CHECKING,
Collection,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
Union,
)
from funcy import first
from scmrepo.exceptions import SCMError as InnerSCMError
from dvc.scm import Git, SCMError, iter_revs
from .exceptions import InvalidExpRefError
from .refs import EXEC_BRANCH, ExpRefInfo
from .serialize import ExpRange, ExpState, SerializableError, SerializableExp
if TYPE_CHECKING:
from dvc.repo import Repo
from .cache import ExpCache
logger = logging.getLogger(__name__)
def collect_rev(
repo: "Repo",
rev: str,
param_deps: bool = False,
force: bool = False,
cache: Optional["ExpCache"] = None,
**kwargs,
) -> ExpState:
"""Collect experiment state for the given revision.
Exp will be loaded from cache when available unless rev is 'workspace' or
force is set.
"""
from dvc.fs import LocalFileSystem
cache = cache or repo.experiments.cache
assert cache
# TODO: support filtering serialized exp when param_deps is set
if rev != "workspace" and not (force or param_deps):
cached_exp = cache.get(rev)
if cached_exp:
if isinstance(cached_exp, SerializableError):
return ExpState(rev=rev, error=cached_exp)
return ExpState(rev=rev, data=cached_exp)
if rev == "workspace" and isinstance(repo.fs, LocalFileSystem):
orig_cwd: Optional[str] = os.getcwd()
os.chdir(repo.root_dir)
else:
orig_cwd = None
try:
data = _collect_rev(
repo,
rev,
param_deps=param_deps,
force=force,
**kwargs,
)
if not (rev == "workspace" or param_deps or data.contains_error):
cache.put(data, force=True)
return ExpState(rev=rev, data=data)
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
logger.debug("", exc_info=True)
error = SerializableError(str(exc), type(exc).__name__)
return ExpState(rev=rev, error=error)
finally:
if orig_cwd:
os.chdir(orig_cwd)
def _collect_rev(
repo: "Repo",
rev: str,
param_deps: bool = False,
**kwargs,
) -> SerializableExp:
with repo.switch(rev) as rev:
if rev == "workspace":
timestamp: Optional[datetime] = None
else:
commit = repo.scm.resolve_commit(rev)
timestamp = datetime.fromtimestamp(commit.commit_time)
return SerializableExp.from_repo(
repo,
rev=rev,
param_deps=param_deps,
timestamp=timestamp,
)
def collect_branch(
repo: "Repo",
rev: str,
end_rev: Optional[str] = None,
**kwargs,
) -> Iterator["ExpState"]:
"""Iterate over exp states in a Git branch.
Git branch will be traversed in reverse, starting from rev.
Args:
rev: Branch tip (head).
end_rev: If specified, traversal will stop when end_rev is reached
(exclusive, end_rev will not be collected).
"""
try:
for branch_rev in repo.scm.branch_revs(rev, end_rev):
yield collect_rev(repo, branch_rev, **kwargs)
except (SCMError, InnerSCMError):
pass
def collect_exec_branch(
repo: "Repo",
baseline_rev: str,
**kwargs,
) -> Iterator["ExpState"]:
"""Iterate over active experiment branch for the current executor."""
last_rev = repo.scm.get_ref(EXEC_BRANCH) or repo.scm.get_rev()
last_rev = repo.scm.get_rev()
yield collect_rev(repo, "workspace", **kwargs)
if last_rev != baseline_rev:
yield from collect_branch(repo, last_rev, baseline_rev, **kwargs)
def collect_queued(
repo: "Repo",
baseline_revs: Collection[str],
**kwargs,
) -> Dict[str, List["ExpRange"]]:
"""Collect queued experiments derived from the specified revisions.
Args:
repo: Repo.
baseline_revs: Resolved baseline Git SHAs.
Returns:
Dict mapping baseline revision to list of queued experiments.
"""
if not baseline_revs:
return {}
return repo.experiments.celery_queue.collect_queued_data(baseline_revs, **kwargs)
def collect_active(
repo: "Repo",
baseline_revs: Collection[str],
**kwargs,
) -> Dict[str, List["ExpRange"]]:
"""Collect active (running) experiments derived from the specified revisions.
Args:
repo: Repo.
baseline_revs: Resolved baseline Git SHAs.
Returns:
Dict mapping baseline revision to list of active experiments.
"""
if not baseline_revs:
return {}
result: Dict[str, List["ExpRange"]] = {}
exps = repo.experiments
for queue in (exps.workspace_queue, exps.tempdir_queue, exps.celery_queue):
for baseline, active_exps in queue.collect_active_data(
baseline_revs, **kwargs
).items():
if baseline in result:
result[baseline].extend(active_exps)
else:
result[baseline] = list(active_exps)
return result
def collect_failed(
repo: "Repo",
baseline_revs: Collection[str],
**kwargs,
) -> Dict[str, List["ExpRange"]]:
"""Collect failed experiments derived from the specified revisions.
Args:
repo: Repo.
baseline_revs: Resolved baseline Git SHAs.
Returns:
Dict mapping baseline revision to list of active experiments.
"""
if not baseline_revs:
return {}
return repo.experiments.celery_queue.collect_failed_data(baseline_revs, **kwargs)
def collect_successful(
repo: "Repo",
baseline_revs: Collection[str],
**kwargs,
) -> Dict[str, List["ExpRange"]]:
"""Collect successful experiments derived from the specified revisions.
Args:
repo: Repo.
baseline_revs: Resolved baseline Git SHAs.
Returns:
Dict mapping baseline revision to successful experiments.
"""
result: Dict[str, List["ExpRange"]] = {}
for baseline_rev in baseline_revs:
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
return result
def _collect_baseline(
repo: "Repo",
baseline_rev: str,
**kwargs,
) -> Iterator["ExpRange"]:
"""Iterate over experiments derived from a baseline revision.
Args:
repo: Repo.
baseline_revs: Resolved baseline Git SHAs.
Yields:
Tuple of (timestamp, exp_range).
"""
ref_info = ExpRefInfo(baseline_sha=baseline_rev)
refs: Optional[Iterable[str]] = kwargs.get("refs")
if refs:
ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
else:
ref_it = repo.scm.iter_refs(base=str(ref_info))
for ref in ref_it:
try:
ref_info = ExpRefInfo.from_ref(ref)
exp_rev = repo.scm.get_ref(ref)
if not exp_rev:
continue
except (InvalidExpRefError, SCMError, InnerSCMError):
continue
exps = list(collect_branch(repo, exp_rev, baseline_rev, **kwargs))
if exps:
exps[0].name = ref_info.name
yield ExpRange(exps, name=ref_info.name)
def collect(
repo: "Repo",
revs: Union[List[str], str, None] = None,
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
num: int = 1,
hide_queued: bool = False,
hide_failed: bool = False,
sha_only: bool = False,
**kwargs,
) -> List["ExpState"]:
"""Collect baseline revisions and derived experiments."""
assert isinstance(repo.scm, Git)
if repo.scm.no_commits:
return []
if not any([revs, all_branches, all_tags, all_commits]):
revs = ["HEAD"]
if isinstance(revs, str):
revs = [revs]
cached_refs = list(repo.scm.iter_refs())
baseline_revs = list(
iter_revs(
repo.scm,
revs=revs,
num=num,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
)
)
if sha_only:
baseline_names: Dict[str, Optional[str]] = {}
else:
baseline_names = _describe(repo.scm, baseline_revs, refs=cached_refs)
workspace_data = collect_rev(repo, "workspace", **kwargs)
result: List["ExpState"] = [workspace_data]
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
active = collect_active(repo, baseline_revs, **kwargs)
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
successful = collect_successful(repo, baseline_revs, **kwargs)
for baseline_rev in baseline_revs:
baseline_data = collect_rev(repo, baseline_rev)
experiments = list(
itertools.chain.from_iterable(
_sorted_ranges(collected.get(baseline_rev, []))
for collected in (active, successful, queued, failed)
)
)
result.append(
ExpState(
rev=baseline_rev,
name=baseline_names.get(baseline_rev),
data=baseline_data.data,
error=baseline_data.error,
experiments=experiments if experiments else None,
)
)
return result
def _describe(
scm: "Git",
revs: Iterable[str],
refs: Optional[Iterable[str]] = None,
) -> Dict[str, Optional[str]]:
"""Describe revisions using a tag, branch.
The first matching name will be returned for each rev. Names are preferred in this
order:
- current branch (if rev matches HEAD and HEAD is a branch)
- tags
- branches
Returns:
Dict mapping revisions from revs to a name.
"""
head_rev = scm.get_rev()
head_ref = scm.get_ref("HEAD", follow=False)
if head_ref and head_ref.startswith("refs/heads/"):
head_branch = head_ref[len("refs/heads/") :]
else:
head_branch = None
tags = {}
branches = {}
ref_it = iter(refs) if refs else scm.iter_refs()
for ref in ref_it:
is_tag = ref.startswith("refs/tags/")
is_branch = ref.startswith("refs/heads/")
if not (is_tag or is_branch):
continue
rev = scm.get_ref(ref)
if not rev:
logger.debug("unresolved ref %s", ref)
continue
if is_tag and rev not in tags:
tags[rev] = ref[len("refs/tags/") :]
if is_branch and rev not in branches:
branches[rev] = ref[len("refs/heads/") :]
names: Dict[str, Optional[str]] = {}
for rev in revs:
if rev == head_rev and head_branch:
names[rev] = head_branch
else:
names[rev] = tags.get(rev) or branches.get(rev)
return names
def _sorted_ranges(exp_ranges: Iterable["ExpRange"]) -> List["ExpRange"]:
"""Return list of ExpRange sorted by (timestamp, rev)."""
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, head_exp.rev
return datetime.fromtimestamp(0), ""
return sorted(exp_ranges, key=_head_timestamp, reverse=True)
+7 -8
View File
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
from dvc.repo.experiments.show import collect_experiment_commit
from dvc.repo.experiments.collect import collect_rev
from dvc.scm import resolve_rev
if repo.scm.no_commits:
@@ -15,22 +15,21 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
if a_rev:
rev = resolve_rev(repo.scm, a_rev)
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
else:
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
rev = resolve_rev(repo.scm, "HEAD")
old = collect_rev(repo, rev, param_deps=param_deps)
if b_rev:
rev = resolve_rev(repo.scm, b_rev)
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
else:
new = collect_experiment_commit(repo, "workspace", param_deps=param_deps)
rev = "workspace"
new = collect_rev(repo, rev, param_deps=param_deps)
with_unchanged = kwargs.pop("all", False)
return {
key: _diff(
format_dict(old.get("data", {}).get(key, {})),
format_dict(new.get("data", {}).get(key, {})),
format_dict(getattr(old.data, key, {})),
format_dict(getattr(new.data, key, {})),
with_unchanged=with_unchanged,
)
for key in ["metrics", "params"]
+7 -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.")
@@ -114,3 +99,10 @@ class UnresolvedRunningExpNamesError(UnresolvedExpNamesError):
class ExpQueueEmptyError(DvcException):
pass
class ExpNotStartedError(DvcException):
def __init__(self, name: str):
super().__init__(
f"Queued experiment '{name}' exists but has not started running yet"
)
+145 -87
View File
@@ -1,17 +1,19 @@
import logging
import os
import pickle
import pickle # nosec B403
import shutil
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,
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Optional,
@@ -26,18 +28,15 @@ 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
if TYPE_CHECKING:
from queue import Queue
@@ -256,6 +255,80 @@ class BaseExecutor(ABC):
**kwargs,
)
@classmethod
def _get_top_level_paths(cls, repo: "Repo") -> List["str"]:
return list(
chain(
_collect_top_level_metrics(repo),
_collect_top_level_params(repo),
repo.index._plot_sources, # pylint: disable=protected-access
)
)
@classmethod
def save(
cls,
info: "ExecutorInfo",
force: bool = False,
include_untracked: Optional[List[str]] = None,
message: Optional[str] = None,
) -> ExecutorResult:
from dvc.dvcfile import LOCK_FILE
from dvc.repo import Repo
exp_hash: Optional[str] = None
exp_ref: Optional[ExpRefInfo] = None
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
old_cwd = os.getcwd()
if info.wdir:
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
else:
os.chdir(dvc.root_dir)
include_untracked = include_untracked or []
include_untracked.extend(cls._get_top_level_paths(dvc))
# dvc repro automatically stages dvc.lock. Running redundant `git add`
# on it causes an error when exiting the detached head context.
if LOCK_FILE in dvc.scm.untracked_files():
include_untracked.append(LOCK_FILE)
try:
stages = dvc.commit([], force=True, relink=False)
exp_hash = cls.hash_exp(stages)
if include_untracked:
dvc.scm.add(include_untracked)
cls.commit(
dvc.scm, # type: ignore[arg-type]
exp_hash,
exp_name=info.name,
force=force,
message=message,
)
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
exp_ref = ExpRefInfo.from_ref(ref) if ref else None
untracked = dvc.scm.untracked_files()
if untracked:
logger.warning(
"The following untracked files were present in "
"the workspace before saving but "
"will not be included in the experiment commit:\n"
"\t%s",
", ".join(untracked),
)
info.result_hash = exp_hash
info.result_ref = ref
info.result_force = False
info.status = TaskStatus.SUCCESS
except DvcException:
info.status = TaskStatus.FAILED
raise
finally:
dvc.close()
os.chdir(old_cwd)
return ExecutorResult(ref, exp_ref, info.result_force)
@staticmethod
def hash_exp(stages: Iterable["PipelineStage"]) -> str:
from dvc.stage import PipelineStage
@@ -266,7 +339,7 @@ class BaseExecutor(ABC):
exp_data.update(to_lockfile(stage))
return dict_sha256(exp_data)
def cleanup(self, infofile: str):
def cleanup(self, infofile: Optional[str] = None):
if infofile is not None:
info = ExecutorInfo.load_json(infofile)
if info.status < TaskStatus.FAILED:
@@ -293,7 +366,7 @@ class BaseExecutor(ABC):
@staticmethod
def unpack_repro_args(path):
with open(path, "rb") as fobj:
data = pickle.load(fobj)
data = pickle.load(fobj) # noqa: S301 # nosec B301
return data["args"], data["kwargs"]
def fetch_exps(
@@ -301,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.
@@ -310,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:
@@ -383,6 +447,8 @@ class BaseExecutor(ABC):
infofile: Optional[str] = None,
log_errors: bool = True,
log_level: Optional[int] = None,
copy_paths: Optional[List[str]] = None,
message: Optional[str] = None,
**kwargs,
) -> "ExecutorResult":
"""Run dvc repro and return the result.
@@ -395,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)
@@ -415,10 +482,15 @@ class BaseExecutor(ABC):
exp_ref: Optional["ExpRefInfo"] = None
repro_force: bool = False
if info.name:
ui.write(f"Reproducing experiment '{info.name}'")
with cls._repro_dvc(
info,
infofile,
log_errors=log_errors,
copy_paths=copy_paths,
message=message,
**kwargs,
) as dvc:
if auto_push:
@@ -437,56 +509,36 @@ 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):
logger.debug("Staging top-level files: %s", paths)
dvc.scm_context.add(paths)
exp_hash = cls.hash_exp(stages)
if not repro_dry:
ref, exp_ref, repro_force = cls._repro_commit(
dvc,
info,
stages,
exp_hash,
auto_push,
git_remote,
repro_force,
message=message,
)
info.result_hash = exp_hash
info.result_ref = ref
@@ -502,19 +554,18 @@ 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:
cls._auto_push(dvc, dvc.scm, git_remote)
@@ -541,12 +592,13 @@ class BaseExecutor(ABC):
info: "ExecutorInfo",
infofile: Optional[str] = None,
log_errors: bool = True,
copy_paths: Optional[List[str]] = None,
message: Optional[str] = None,
**kwargs,
):
) -> Iterator["Repo"]:
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
@@ -555,25 +607,36 @@ class BaseExecutor(ABC):
if cls.QUIET:
dvc.scm_context.quiet = cls.QUIET
old_cwd = os.getcwd()
for path in copy_paths or []:
cls._copy_path(os.path.realpath(path), os.path.join(dvc.root_dir, path))
if info.wdir:
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
else:
os.chdir(dvc.root_dir)
args_path = os.path.join(dvc.tmp_dir, cls.PACKED_ARGS_FILE)
if os.path.exists(args_path):
_, kwargs = cls.unpack_repro_args(args_path)
dvc_studio_config = dvc.config.get("studio")
# set missing config options using saved config
# inferring repo url will fail if not set here
run_env_config = env_to_config(kwargs.get("run_env", {}))
dvc_studio_config = {**run_env_config, **dvc_studio_config}
try:
post_live_metrics(
"start",
info.baseline_rev,
info.name,
info.name, # type: ignore[arg-type]
"dvc",
params=to_studio_params(dvc.params.show()),
dvc_studio_config=dvc_studio_config,
message=message,
)
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("")
@@ -588,10 +651,11 @@ class BaseExecutor(ABC):
post_live_metrics(
"done",
info.baseline_rev,
info.name,
info.name, # type: ignore[arg-type]
"dvc",
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
metrics=get_in(dvc.metrics.show(), ["", "data"]),
dvc_studio_config=dvc_studio_config,
)
if infofile is not None:
@@ -605,7 +669,7 @@ class BaseExecutor(ABC):
args, kwargs = cls.unpack_repro_args(args_path)
remove(args_path)
# explicitly git rm/unstage the args file
dvc.scm.add([args_path])
dvc.scm.add([args_path], force=True)
else:
args = []
kwargs = {}
@@ -637,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,
@@ -662,7 +708,7 @@ 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."""
rev = scm.get_rev()
@@ -692,26 +738,24 @@ class BaseExecutor(ABC):
logger.debug("Commit to new experiment branch '%s'", branch)
scm.add([], update=True)
scm.commit(f"dvc: commit experiment {exp_hash}", no_verify=True)
message = message or f"dvc: commit experiment {exp_hash}"
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
@@ -724,6 +768,20 @@ class BaseExecutor(ABC):
if level is not None:
dvc_logger.setLevel(level)
@staticmethod
def _copy_path(src, dst):
try:
if os.path.isfile(src):
shutil.copy(src, dst)
elif os.path.isdir(src):
shutil.copytree(src, dst)
else:
raise DvcException(
f"Unable to copy '{src}'. It is not a file or directory."
)
except OSError as exc:
raise DvcException(f"Unable to copy '{src}' to '{dst}'.") from exc
@contextmanager
def set_temp_refs(self, scm: "Git", temp_dict: Dict[str, str]):
try:
+21 -21
View File
@@ -4,19 +4,18 @@ from contextlib import ExitStack
from tempfile import mkdtemp
from typing import TYPE_CHECKING, Optional, Union
from configobj import ConfigObj
from funcy import retry
from shortuuid import uuid
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,
EXPS_TEMP,
TEMP_NAMESPACE,
)
from dvc.repo.experiments.utils import EXEC_TMP_DIR, get_exp_rwlock
from dvc.scm import SCM, Git
@@ -48,7 +47,7 @@ class BaseLocalExecutor(BaseExecutor):
def scm(self) -> Union["Git", "NoSCM"]:
return SCM(self.root_dir)
def cleanup(self, infofile: str):
def cleanup(self, infofile: Optional[str] = None):
self.scm.close()
del self.scm
super().cleanup(infofile)
@@ -89,9 +88,9 @@ class TempDirExecutor(BaseLocalExecutor):
if infofile:
self.info.dump_json(infofile)
temp_head = f"{EXPS_TEMP}/head-{uuid()}"
temp_merge = f"{EXPS_TEMP}/merge-{uuid()}"
temp_baseline = f"{EXPS_TEMP}/baseline-{uuid()}"
temp_head = f"{TEMP_NAMESPACE}/head-{uuid()}"
temp_merge = f"{TEMP_NAMESPACE}/merge-{uuid()}"
temp_baseline = f"{TEMP_NAMESPACE}/baseline-{uuid()}"
temp_ref_dict = {
temp_head: entry.head_rev,
@@ -123,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)
@@ -134,24 +130,31 @@ class TempDirExecutor(BaseLocalExecutor):
merge_rev = self.scm.get_ref(EXEC_MERGE)
self.scm.stash.apply(merge_rev)
self._update_config(repo.config.read("local"))
def _config(self, cache_dir):
def _update_config(self, update):
local_config = os.path.join(
self.root_dir,
self.dvc_dir,
"config.local",
)
logger.debug("Writing experiments local config '%s'", local_config)
with open(local_config, "w", encoding="utf-8") as fobj:
fobj.write(f"[cache]\n dir = {cache_dir}")
if os.path.exists(local_config):
conf_obj = ConfigObj(local_config)
conf_obj.merge(update)
else:
conf_obj = ConfigObj(update)
if conf_obj:
with open(local_config, "wb") as fobj:
conf_obj.write(fobj)
def init_cache(
self, repo: "Repo", rev: str, run_cache: bool = True # noqa: ARG002
):
"""Initialize DVC cache."""
self._config(repo.cache.repo.path)
self._update_config({"cache": {"dir": repo.cache.local_cache_dir}})
def cleanup(self, infofile: str):
def cleanup(self, infofile: Optional[str] = None):
super().cleanup(infofile)
logger.debug("Removing tmpdir '%s'", self.root_dir)
remove(self.root_dir)
@@ -181,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(
@@ -234,14 +236,12 @@ class WorkspaceExecutor(BaseLocalExecutor):
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
pass
def cleanup(self, infofile: str):
def cleanup(self, infofile: Optional[str] = None):
super().cleanup(infofile)
remove(os.path.dirname(infofile))
if infofile:
remove(os.path.dirname(infofile))
with self._detach_stack:
self.scm.remove_ref(EXEC_BASELINE)
self.scm.remove_ref(EXEC_MERGE)
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)
+4 -5
View File
@@ -3,7 +3,7 @@ import os
import posixpath
import sys
from contextlib import contextmanager
from typing import TYPE_CHECKING, Callable, Iterable, Optional
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional
from dvc_ssh import SSHFileSystem
from funcy import first
@@ -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):
@@ -242,6 +239,8 @@ class SSHExecutor(BaseExecutor):
infofile: Optional[str] = None,
log_errors: bool = True,
log_level: Optional[int] = None,
copy_paths: Optional[List[str]] = None, # noqa: ARG003
message: Optional[str] = None, # noqa: ARG003
**kwargs,
) -> "ExecutorResult":
"""Reproduce an experiment on a remote machine over SSH.
-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

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