Compare commits

...
233 Commits
Author SHA1 Message Date
David de la Iglesia Castro dde456c923 plots: Fix match_defs_renderers when plots are defined in nt format.
`plots.show` returned `definitions` in native `nt` format but sources in `posixpath` format so the matching was failing.

Closes #8689

- Add `test_show_plots_defined_with_native_os_path`.
2023-03-06 19:16:29 +01:00
David de la Iglesia Castro cb9d3529af plots: Remove --from-config 2023-03-06 19:16:29 +01:00
David de la Iglesia Castro 876c10fcf5 plots: Remove unnecessary OrderedDict usage in load_sv. 2023-03-06 19:16:29 +01:00
skshetry 93eea0cd42 deps: bump scmrepo to 0.1.15; close subrepo.dvc on tests teardown (#9122) 2023-03-06 20:21:42 +05:45
skshetry 25fd274090 increase timeout for tests to 40 minutes 2023-03-06 20:04:26 +05:45
skshetry ddfa607437 tests: don't leak files/descriptors when tests finish (#9119)
close open files/descriptors when tests finish
2023-03-06 18:06:37 +05:45
skshetry c5a3566e49 deps (tests): require pytest-xdist>=3.2, use -n=logical workers and workstealing scheduler (#9116) 2023-03-06 15:54:45 +05:45
skshetry e7d4f5b24e tests: filter warnings (#9115) 2023-03-06 15:41:45 +05:45
Peter Rowlands 1d2892efac exp show: don't cache collection errors 2023-03-06 15:25:27 +09:00
dependabot[bot] 83835afa69 build(deps-dev): bump pylint from 2.16.2 to 2.16.3 (#9113)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.2 to 2.16.3.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.16.2...v2.16.3)

---
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-06 10:33:02 +05:45
skshetry 4c5d052d8c update ruff to v0.0.254 (#9110) 2023-03-04 17:26:59 +05:45
David de la Iglesia Castro e4d514c990 scm: Remove unused GitMergeError.
Not used after https://github.com/iterative/dvc/pull/9096
2023-03-03 18:09:57 +01:00
David de la Iglesia Castro baeecd1972 deps: Bump scmrepo>=0.1.14 2023-03-03 15:42:41 +01:00
David de la Iglesia Castro dbd098506c deps: Drop dvclive.
See https://github.com/iterative/dvclive/pull/481
2023-03-03 14:45:48 +01:00
Ruslan Kuprieiev 4779c248f8 deps: bump dvc-data to >=0.42.1 2023-03-02 17:53:07 +02:00
Peter Rowlands 2cf3a12e46 tests: unmark exp celery tests as xfail 2023-03-02 15:44:34 +09:00
Peter Rowlands 78fb5275e5 exp: add dvc exp clean command 2023-03-02 12:35:54 +09:00
Peter Rowlands ccbe2de89e exp: auto cleanup celery on queue worker exit 2023-03-02 12:35:54 +09:00
Peter Rowlands 54e39e4abd deps: bump dvc-task to >=0.2.0 2023-03-02 12:35:54 +09:00
Ruslan Kuprieiev 59a4c4f67c remote: set hash_name as md5
Fixes #7977
2023-03-01 13:26:24 +02:00
Peter Rowlands 1a7ae16f69 exp: use stash-apply instead of merge 2023-03-01 19:11:12 +09:00
Peter Rowlands 286f4fda3e repo: call Repo.close() on Repo.__exit__() 2023-03-01 17:49:27 +09:00
Peter Rowlands d2c63bffcb exp: ensure repo instance is closed after executor.reproduce 2023-03-01 17:49:27 +09:00
Peter Rowlands 2b41305c51 exp: fix queued run file descriptor leak 2023-03-01 17:49:27 +09:00
Ruslan Kuprieiev 546becddaf deps: bump dvc-gs to 2.22.0 2023-03-01 08:36:33 +02:00
David de la Iglesia Castro c6015a05a3 exp save: Don't relink during commit
Same change as in https://github.com/iterative/dvc/pull/9076 but for `exp save`
2023-02-28 22:29:57 +02:00
Ruslan Kuprieiev c0754ee5b3 deps: bump dvc-data to 0.42.0 2023-02-28 14:48:34 +02:00
skshetry 4db2e89938 build: use macos-11 to build package (#9091)
macos-10.15 image has been deprecated for a while, see https://github.blog/changelog/2022-07-20-github-actions-the-macos-10-15-actions-runner-image-is-being-deprecated-and-will-be-removed-by-8-30-22/ and https://github.com/actions/runner-images/issues/5583.

setup-ruby removed the support for these images yesterday: https://github.com/ruby/setup-ruby/pull/467.

which fails our package builds. Now using macos-11 to build packages.
2023-02-28 17:33:05 +05:45
skshetry 4a6f3ed64a dvc.testing: fix lockfile in docker_services fixture (#9088)
dvc.testing.docker_services: rollback to previous state

where we were not tearing down the containers
2023-02-28 16:54:33 +05:45
David de la Iglesia Castro 9f0c4350bd deps: Bump scmrepo>=0.1.13.
Closes #9080
2023-02-28 17:46:09 +09:00
Ruslan Kuprieiev a91528c3c9 Revert "testing: fixtures: fix docker regression"
This reverts commit b80d84e689.
2023-02-27 15:02:22 +02:00
Ruslan Kuprieiev b80d84e689 testing: fixtures: fix docker regression 2023-02-27 14:17:07 +02:00
skshetry 2a2c02c184 remove typing.cast (#9082) 2023-02-26 13:15:54 +05:45
Dave Berenbaum 35b648bbcf plots: shorten names in the legend (#8900)
* plots: shorten field names

* plots: handle missing field names

* plots: make compatible with py3.8

* plots: fix for missing data

* plots: add test for field shortening

* plots: strip separators regardless of os

* plots: don't shorten field name

* plots: drop test for shortening field name
2023-02-24 17:26:16 +01:00
Peter Rowlands 96ac321dd5 repo: use local root when determining state/index db dirname 2023-02-24 16:33:42 +09:00
Peter Rowlands 50fd8ca15d exp show: cache git refs per show() call 2023-02-24 16:33:26 +09:00
David de la Iglesia Castro 184cc35cf0 pull: Add allow_missing flag.
Allows `pull` to succeed even if some files or directories are missing.

Closes #4746
2023-02-24 09:11:50 +02:00
Ruslan Kuprieiev 81da91a973 exp: run: don't relink during commit
Part of #8809
2023-02-24 08:09:35 +02:00
Peter Rowlands ad5b91e51a exp show: cache collected git revs 2023-02-24 14:18:58 +09:00
Peter Rowlands 391cf80e7b exp: support caching collected exp data 2023-02-24 14:18:58 +09:00
Peter Rowlands 8d13486048 exp: add SerializableExp dataclasses 2023-02-24 14:18:58 +09:00
Ruslan Kuprieiev 37761a2627 data: status: get rid of --not-in-remote 2023-02-21 12:32:40 +02:00
Ruslan Kuprieiev 968a8fde99 data: status: introduce --remote-refresh 2023-02-21 12:32:40 +02:00
David de la Iglesia Castro 3043b6588f exp save: Use pathspec to handle include_untracked.
Closes #9061
2023-02-21 09:43:25 +01:00
47208533a2 Add metrics_show to dvc.api (#9042)
* refactor: add metrics_show, rename module to show

* tests: add functional tests for metrics_show

* api.show: return empty dict for no params/metrics

* test_show: Return relpaths in `metrics_repo`.

---------

Co-authored-by: codamuse <[email protected]>
Co-authored-by: David de la Iglesia Castro <[email protected]>
2023-02-20 21:53:38 +00:00
Ruslan Kuprieiev 09ccd9c7e4 repo: data_index: store in .dvc/tmp/index/data 2023-02-20 21:57:27 +02:00
Ruslan Kuprieiev cd9dd0a312 index: use has_node instead of ls
Current code doesn't work for prefixes longer than one, which makes index cache
effectively not work and it should've used has_node from the very start anyway.
2023-02-20 17:45:15 +02:00
Ruslan Kuprieiev 6fbbbeccc5 index: store data index under tree prefix
Tidying it up, so root of the index will look like this:

```
tree/
remote/
```

This hasn't been released yet, so we are not breaking anything.
2023-02-20 13:33:22 +02:00
Ruslan Kuprieiev c34cbf63af data: status: introduce --not-in-remote
Also supports cloud versioning.

Fixes #8761
2023-02-20 12:25:17 +02:00
Ruslan Kuprieiev b76aad8d56 data: status: rename with_missing to not_in_cache 2023-02-20 12:25:17 +02:00
Ruslan Kuprieiev 68bf095eae remote: rename index prefix from remotes to remote 2023-02-20 12:24:46 +02:00
Ruslan Kuprieiev ea6e01d0e0 dvcfs: implement get_file
Currently we use `open`, which is much slower. For example,

```
$ dvc get dvc-bench data/small/dataset
```

goes down from 110sec to 50sec.

Discovered as a part of https://github.com/iterative/dvc-bench/issues/419
2023-02-20 11:10:13 +02:00
dependabot[bot]andSaugat Pachhai 19038b32bd build(deps-dev): unpin pytest-docker
* fix docker fixtures and unpin pytest-docker
* git_server: mount volume to different /custom-cont-init.d

---------
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2023-02-20 12:57:31 +05:45
Peter Rowlands 648edd71eb exp: move apply tests into their own module 2023-02-20 14:36:38 +09:00
Peter Rowlands 0c95cde712 exp: remove unused ApplyConflictError 2023-02-20 14:36:38 +09:00
Peter Rowlands e218431f64 apply: add --no-force deprecation message 2023-02-20 14:36:38 +09:00
Peter Rowlands d3182e0dd9 exp apply: drop --no-force CLI option 2023-02-20 14:36:38 +09:00
Peter Rowlands e040e875dc exp apply: simplify apply behavior to use checkout instead of merge 2023-02-20 14:36:38 +09:00
Peter Rowlands 303da84b18 exp: add ApplyStash implementation 2023-02-20 14:36:38 +09:00
Peter Rowlands 03bfdf2663 deps: use ranges for dvc sub-packages 2023-02-20 14:35:08 +09:00
skshetry 8edaef0103 deps: drop importlib-resources and typing-extensions (#9055)
deps: remove importlib-resources and typing-extensions
2023-02-19 11:36:48 +05:45
skshetry fd8a8c2729 pyproject: format using pyproject-fmt (#9053)
I haven't added pyproject-fmt to pre-commit hook because of two reasons:
1. There is a bug that keeps adding empty lines after `[[tool.mypy.overrides]]`.
2. The list gets exploded to multiple lines. However, this is a minor issue.
2023-02-18 23:07:57 +05:45
dependabot[bot] 30337bf0ba build(deps): bump pyinstaller from 5.7.0 to 5.8.0 (#9013)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.7.0 to 5.8.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.7.0...v5.8.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-02-18 22:53:17 +05:45
skshetry 5426ca6f79 deps (tests): unpin test deps (#9052) 2023-02-18 22:41:50 +05:45
skshetry a89c25700f pyproject.toml: cleanup metadata (#9051) 2023-02-18 17:40:36 +05:45
dependabot[bot] 0e0bb8ad76 build(deps-dev): bump mypy from 0.991 to 1.0.1 (#9049)Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [mypy](https://github.com/python/mypy) from 0.991 to 1.0.1.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.991...v1.0.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-18 17:03:41 +05:45
skshetry 1de37556d5 bump ruff to v0.0.247 (#9050)
* bump ruff to v0.0.247

* ruff: enable pep8-naming check

rename some camelCased APIs, especially in logger
rename G variables to graph
ignore some variable namings and help messages in dvc.commands
2023-02-18 16:50:35 +05:45
Ruslan Kuprieiev 098685b8f3 deps: bump dvc-data to 0.40.4
Improves index view related performance (e.g. `dvc diff`).
2023-02-18 11:35:01 +02:00
Ruslan Kuprieiev 8f549c8a8d config: enable data index caching by default 2023-02-18 08:00:58 +02:00
Ruslan Kuprieiev e56d97bb28 gha: user ubuntu 20.04 for deb packages
18.04 will reach EOL in april and will be dropped by gha.

18.04 also comes with an old sqlite version which forces us to use obsolete
syntax.
2023-02-18 08:00:58 +02:00
Ruslan Kuprieiev 07b72c20e9 repo: move data_index to .dvc/tmp/index 2023-02-17 21:43:57 +02:00
Ruslan Kuprieiev 592f251481 repo: use tmp_dir if available 2023-02-17 21:43:57 +02:00
Ruslan Kuprieiev 890c0020ed Revert "repo: use tmp_dir if available"
This reverts commit 7ccf35b47e.
2023-02-17 21:25:46 +02:00
Ruslan Kuprieiev 7ce0b1cba2 Revert "repo: move data_index to .dvc/tmp/index"
This reverts commit d68599a5f7.
2023-02-17 21:25:46 +02:00
Ruslan Kuprieiev b8a9c37af5 Revert "config: enable data index caching by default"
This reverts commit 1af63acae5.
2023-02-17 21:25:46 +02:00
Ruslan Kuprieiev 1af63acae5 config: enable data index caching by default 2023-02-17 19:55:25 +02:00
Ruslan Kuprieiev d68599a5f7 repo: move data_index to .dvc/tmp/index 2023-02-17 19:55:25 +02:00
Ruslan Kuprieiev 7ccf35b47e repo: use tmp_dir if available 2023-02-17 19:55:25 +02:00
Ruslan Kuprieiev 4f49023a08 config: don't set default cache dir
Setting default values is very untypical for all other applications and the
info config has is not enough to make a decision on the default cache dir,
especially when dealing with gitfs.

Fixes #8705
2023-02-17 07:57:39 +02:00
skshetry d7ea7c358d Revert "scm: use dulwich backend when fetching exps during clone/pull" (#9041)
Revert "scm: use dulwich backend when fetching exps during clone/pull (#9023)"

This reverts commit ed1ab7fa75.
2023-02-16 18:07:45 +05:45
dependabot[bot] 3f0da8f6cc build(deps-dev): bump scmrepo from 0.1.10 to 0.1.11 (#9039)Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [scmrepo](https://github.com/iterative/scmrepo) from 0.1.10 to 0.1.11.
- [Release notes](https://github.com/iterative/scmrepo/releases)
- [Commits](https://github.com/iterative/scmrepo/compare/0.1.10...0.1.11)

---
updated-dependencies:
- dependency-name: scmrepo
  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-02-16 18:03:13 +05:45
David de la Iglesia Castro b7b7eff4a3 tests: Fix test_deleted.
Closes #6297
#9017 actually fixed this but the test was incorrect (something wrong with caplog).
2023-02-16 11:43:09 +01:00
skshetry ddeb32ed33 ui: log footer support message to standard error (#9034) 2023-02-16 11:31:31 +05:45
Ruslan Kuprieiev c7be921f44 cachemgr: don't use tmp_dir
We used to use it in gdrive before https://github.com/iterative/PyDrive2/pull/211
2023-02-16 06:43:20 +02:00
Ruslan Kuprieiev c95c3f0f7a deps: limit types-tqdm
See https://github.com/iterative/dvc/pull/9036
2023-02-16 06:43:20 +02:00
skshetry be824ede0d render: avoid deepcopying (#9033)
* render: avoid deepcopying

* remove unnecessary line
2023-02-15 16:25:45 +05:45
Ruslan Kuprieiev cbe46cccd8 data: status: fix typo
Leftover from #9031
2023-02-15 07:18:19 +02:00
Ruslan Kuprieiev 49b5157abe diff: use index 2023-02-15 06:40:02 +02:00
Ruslan Kuprieiev 6f87c14bf9 data: status: bring back ui.status 2023-02-15 05:57:50 +02:00
Ruslan Kuprieiev 0f80d16ba7 data: status: fix typing 2023-02-15 05:57:50 +02:00
Ruslan Kuprieiev 4b23b4f8ae repo: cache index for cloud versioning
This allows us to reuse cloud versioning index that we've built in other
operations like `data status`.

Note, this only works with `dvc config feature.data_index_cache true`
right now.

Also note that index is not populated for regular remotes, it will come
in a separate PR as there are some http-related issues to figure out.

Related #8962
2023-02-15 04:53:48 +02:00
Ruslan Kuprieiev 446871d84f index: build_data_index: use dvcignore
Fixes #9008
2023-02-14 21:33:43 +02:00
dependabot[bot] 99bbe15980 build(deps-dev): bump pylint from 2.16.1 to 2.16.2 (#9020)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.1 to 2.16.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.16.1...v2.16.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-02-14 22:35:59 +05:45
David de la Iglesia Castro 320771551b exp: Preserve staged changes after experiments.
- Detach head after stashing workspace.
- Use reinstate_index.
2023-02-14 16:59:10 +01:00
David de la Iglesia Castro 1676d1f344 Add test_stash_exp. 2023-02-14 16:59:10 +01:00
David de la Iglesia Castro c8ae2895f0 deps: bump scmrepo==0.1.10
https://github.com/iterative/scmrepo/releases/tag/0.1.10
2023-02-14 16:59:10 +01:00
skshetry ed1ab7fa75 scm: use dulwich backend when fetching exps during clone/pull (#9023)
refspecs uses pygit2 backend by default, which does not support git credential
helpers. This PR forces to use dulwich backend during clone/pull operations.
2023-02-14 19:18:40 +05:45
Karan Jakhar 73497dae9e Handle filenames which has ":" (#9000)
* Fixes #8773 issue

* added test for #8773 issue

* skip test for windows
2023-02-14 11:21:32 +00:00
skshetry eff775c760 brancher: add trace when switching filesystem (#9022)
brancher: add trace info when switching filesystem
2023-02-14 15:27:18 +05:45
Ruslan Kuprieiev e73d662686 repo: introduce data_index
Extracting from `index.data` as a "root" data index that contains indexes
for different revisions/workspaces.

This is a setup for putting cloud versioning remote indexes and also replacing
old state.
2023-02-13 06:31:44 +02:00
Ruslan Kuprieiev 86a6fb104a index: experimental persistent data index
Related https://github.com/iterative/dvc-data/issues/208
2023-02-13 02:57:30 +02:00
Ruslan Kuprieiev 3b097dcccd deps: bump dvc-data to 0.40.1 2023-02-12 09:12:08 +02:00
David de la Iglesia Castro e910dac0dc Update pyproject.toml 2023-02-10 10:59:32 +01:00
David de la Iglesia Castro 17a71d6011 plots: Rename template_path to html_template. 2023-02-10 10:59:32 +01:00
dependabot[bot] 5a6ccc8027 build(deps-dev): bump dvc-render from 0.1.1 to 0.1.2
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.1.1...0.1.2)

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

Signed-off-by: dependabot[bot] <[email protected]>
2023-02-10 10:59:32 +01:00
pre-commit-ci[bot] 6dccfcb3f9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-02-10 04:47:57 +02:00
karajan1001 2b0c365f35 Fix some mypy error. 2023-02-10 04:47:57 +02:00
dberenbaum 611e2f4311 update rich 2023-02-09 23:18:50 +02:00
dberenbaum 561a375b48 update: drop worktree from cli 2023-02-09 23:03:50 +02:00
skshetry e51345e276 get/import: lazy import dvc.scm; reduces dvc --help by ~20% (#8996) 2023-02-09 15:04:55 +05:45
skshetry 00e61e18ae logging: do not disable other loggers (#8995)
Closes https://github.com/iterative/dvc/issues/8631
2023-02-09 15:04:29 +05:45
skshetry 5490fc00d1 logger: check isatty stream to print colored logs or not instead of depending on colorama (#8993
logger: check isatty stream to print colored logs or not
2023-02-09 13:50:38 +05:45
skshetry f1043676da [WIP] logging: change from dictConfig to equivalent code for creating logge… (#8985
logging: change from dictConfig to equivalent code for creating loggers/handlers/formatters

dictConfig clears all handlers, and potentially disables existing loggers (we disabled this functionality
though). There's much more code than necessary in dictConfig than what we need.

I have tried to keep the setup equivalent to what we had in the config, so I have not tried to cleanup
the code.
2023-02-09 09:34:14 +05:45
Ruslan Kuprieiev f7ebcbb974 deps: bump dvc-data to 0.39.3 2023-02-09 05:44:50 +02:00
dependabot[bot] c637b963e0 build(deps-dev): bump dvc-azure from 2.21.0 to 2.21.1 (#8991)
Bumps [dvc-azure](https://github.com/iterative/dvc-azure) from 2.21.0 to 2.21.1.
- [Release notes](https://github.com/iterative/dvc-azure/releases)
- [Commits](https://github.com/iterative/dvc-azure/compare/2.21.0...2.21.1)

---
updated-dependencies:
- dependency-name: dvc-azure
  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-02-09 08:31:13 +05:45
Ruslan Kuprieiev 9156bb80db output: rename odb -> cache 2023-02-09 00:44:18 +02:00
Ruslan Kuprieiev 58295ca0ab repo: rename odb -> cache
We used to have it named `cache` before, but these days it is clear that
you can have three odbs: just odb (e.g. in-memory), cache (typically local)
or remote (typically cloud). Fixing naming here before proceeding with
introducing `out.odb` and `out.cache`.

In preparation for migration to https://github.com/iterative/dvc-data/pull/304
2023-02-08 23:05:45 +02:00
skshetry 4176007493 push: check if versioning is supported or not during checkout on cloud versioning (#8949)Fixes https://github.com/iterative/dvc/issues/8824
* push: check if versioning is supported or not during checkout on cloud versioning

* bump dvc-data to 0.38.4
2023-02-08 17:05:27 +05:45
dependabot[bot]andGao 9d7883f529 build(deps-dev): bump pytest-xdist from 3.1.0 to 3.2.0 (#8981)
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.1.0...v3.2.0)

---
updated-dependencies:
- dependency-name: pytest-xdist
  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>
Co-authored-by: Gao <[email protected]>
2023-02-08 07:31:21 +00:00
Peter Rowlands 93c556acdd index: fix index entries for partial import stages 2023-02-08 15:34:15 +09:00
Peter Rowlands 676b1e47e9 import-url: update stage md5 after unpartialing imports 2023-02-08 15:34:15 +09:00
Peter Rowlands 0b141ac08a testing: test cloud versioned import-url --no-download 2023-02-08 15:34:15 +09:00
dependabot[bot] b4db1e06a1 build(deps-dev): bump dvc-render from 0.1.0 to 0.1.1 (#8982)
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.1.0...0.1.1)

---
updated-dependencies:
- dependency-name: dvc-render
  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-02-08 09:11:42 +05:45
dependabot[bot] 0d0fff6ddc build(deps-dev): bump dvc-data from 0.38.1 to 0.38.3 (#8983)
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.38.1 to 0.38.3.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.38.1...0.38.3)

---
updated-dependencies:
- dependency-name: dvc-data
  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-02-08 09:11:28 +05:45
Hagai Kariti 6c8c26f741 Support setting a exp name prefix in hydra sweeps 2023-02-07 18:49:23 +01:00
Peter Rowlands f68a12558a testing: mark 00 prefix tests xfail on fs without find() 2023-02-07 15:17:29 +09:00
Ruslan Kuprieiev a8a5b29fb7 deps: bump dvc-data to 0.38.1 (#8976) 2023-02-07 06:58:15 +02:00
skshetry 037e2e54f0 serialization: reorder files section metadata (#8950
serialization: reorder files section
2023-02-07 10:05:36 +05:45
skshetry ae80889477 logging: defer calling colorama.init() in jupyter, otherwise initialize immediately (#8969)Closes https://github.com/iterative/dvc/issues/8957
logging: init colorama before logging if outside jupyter env

Otherwise, tty detection does not work as `colorama.init()` mutates
sys.stdout/sys.stderr, and after `dictConfig()`, the handlers will hold
on to the stream.
2023-02-07 10:05:05 +05:45
Gao e1acab5113 Use pygit2 backend for fetch_refspecs (#8907)
fix: #8762
1. partly solve #8787 and #8477
2. Improve the performance
3. Bump `scmrepo` into 0.1.9
2023-02-06 16:07:00 +02:00
d84196db62 studio: Move post_live_metrics from queue to executor (#8955)
* studio: Move `post_live_metrics` from `queue/workspace` to `executor/base`.

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

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

* Update dvc/repo/experiments/utils.py

Co-authored-by: skshetry <[email protected]>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2023-02-06 13:31:10 +00:00
Peter Rowlands 80c2815e67 deps: bump dvc-data to 0.37.8 2023-02-06 21:18:44 +09:00
Peter Rowlands e42a01a3dd testing: re-add 00 prefix remote traverse tests 2023-02-06 21:18:44 +09:00
Ruslan Kuprieiev 8e1b75feb5 index: build_data_index: handle empty dirs (#8959)
Fixes https://github.com/iterative/dvc/issues/8958
2023-02-06 01:44:15 +02:00
Saugat Pachhai (सौगात) 4ec6d64629 add .git-blame-ignore-revs 2023-02-04 10:02:31 +05:45
skshetry a86470ccbc update black and change line-length to 88 (#8953) 2023-02-04 05:28:07 +02:00
dependabot[bot] 5d2701341b build(deps-dev): bump pylint from 2.16.0 to 2.16.1 (#8944)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-03 14:06:36 +05:45
Peter Rowlands 97589a1358 testing: add tests for pull after import-url --version-aware 2023-02-03 15:50:57 +09:00
Peter Rowlands 9e72ce53cd deps: bump dvc-data to 0.37.5 2023-02-03 15:50:57 +09:00
Peter Rowlands e34a6d3b30 update: fix meta & version ID merge on worktree update 2023-02-03 13:23:04 +09:00
Peter Rowlands 25dcc33580 output: build from files in get_obj() regardless is hash_info set 2023-02-03 13:23:04 +09:00
frederik-sandfort 18fa1a0272 Custom header for dvc webdav (#8933)Fix https://github.com/iterative/dvc/issues/8931
* include custom auth header for webdav

* Revert "include custom auth header for webdav"

This reverts commit d2f2b2889b6bcf0429e4a28076f709a04642ae3a.

* include custom auth header for dvc webdav, ran unit tests

* update dvc-webdav version
2023-02-02 16:36:11 +05:45
dependabot[bot] 49a27f4140 build(deps-dev): bump dvc-render from 0.0.17 to 0.1.0 (#8935)
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.17 to 0.1.0.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.17...0.1.0)

---
updated-dependencies:
- dependency-name: dvc-render
  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-02-02 14:13:27 +05:45
dependabot[bot] 9b2aa79c18 build(deps-dev): bump pylint from 2.15.10 to 2.16.0 (#8936)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.10 to 2.16.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.10...v2.16.0)

---
updated-dependencies:
- dependency-name: pylint
  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-02-02 14:12:53 +05:45
Ruslan Kuprieiev 24023c6977 index: introduce data_keys (#8938)
`data_keys` are similar to `outs`, but they provide more granularity, so we
can use them in `build_data_index` to rebuild index views with filtered
outputs. E.g. when running `dvc diff dir/file`, workspace index should only
have `dir/file` in it.

This also could be used for standalone (non-output) data in the future.

Pre-requisite for #8930
2023-02-02 05:50:16 +02:00
Ruslan Kuprieiev 5310bcdcec deps: bump dvc-data to 0.37.3 (#8923) 2023-02-02 04:18:18 +02:00
dependabot[bot] ae92154cec build(deps-dev): bump beautifulsoup4 from 4.11.1 to 4.11.2 (#8928)
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.11.1 to 4.11.2.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  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-02-01 09:10:50 +05:45
Ruslan Kuprieiev f661cee8f6 stage: use repo.fs directly without copying (#8929)
Otherwise when using brancher we end up in a strange situation where even
though we are collecting targets from `gitfs`, we are still using `localfs`
as `self.fs`, which make operations like `self.fs.abspath` in
`collect_granular` return invalid paths.
2023-02-01 05:21:53 +02:00
Ruslan Kuprieiev e07c85a993 index: collect_targets: introduce onerror (#8925) 2023-02-01 03:49:27 +02:00
Ruslan Kuprieiev 58d9e379fe tests: gc: use timedelta again (#8926)
Leftover from #8912
2023-02-01 03:14:22 +02:00
skshetry 524162dc3f version/info: match dashes' length below DVC version (#8921) 2023-01-31 18:32:04 +05:45
skshetry aa15b21dce logging: log pkg version/type, platform info and cli arguments (#8918)
logging: log pkg version and type, platform info and cli arguments
2023-01-31 15:46:14 +05:45
Peter Rowlands 0404967591 deps: bump dvc-gs to 2.21.1 2023-01-31 17:26:44 +09:00
Peter Rowlands 465d886c29 testing: add tests for version-aware import-url 2023-01-31 15:55:44 +09:00
Peter Rowlands 459adfb40f import-url: disable push on version aware imports before saving 2023-01-31 15:55:44 +09:00
Peter Rowlands fe9f8654a3 testing: add test for worktree update 2023-01-31 15:55:44 +09:00
skshetry 8208846003 bump ruff to 0.0.238 (#8915) 2023-01-31 11:24:03 +05:45
Peter Rowlands 73aea2d972 ci: run dvc-s3 tests on DVC pull requests 2023-01-31 14:13:55 +09:00
Ruslan Kuprieiev 10184e9370 tests: gc: use timedelta (#8912)
Bug from https://github.com/iterative/dvc/pull/8880
2023-01-31 05:18:09 +02:00
Ruslan Kuprieiev 2690f05cee pre-commit: bump isort to 5.12.0
https://github.com/PyCQA/isort/issues/2077
2023-01-31 04:02:24 +02:00
Ruslan Kuprieiev 34c2c95115 index: expose rev property (#8909)
index: add rev property

This is very useful to identify indexes that are built from git revisions
and so are describing an immutable state and can be used for caching.

We were already using this for `__repr__`.
2023-01-30 20:18:54 +02:00
skshetry 5c6c1dc719 mypy: unignore module override (#8902) 2023-01-28 15:08:52 +05:45
98bcfad8e7 build(deps-dev): bump scmrepo from 0.1.6 to 0.1.7 (#8821)
* build(deps-dev): bump scmrepo from 0.1.6 to 0.1.7

Bumps [scmrepo](https://github.com/iterative/scmrepo) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/iterative/scmrepo/releases)
- [Commits](https://github.com/iterative/scmrepo/compare/0.1.6...0.1.7)

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

Signed-off-by: dependabot[bot] <[email protected]>

* mypy: remove scmrepo from ignored modules

* Fix mypy issues

---------

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]>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2023-01-28 14:26:00 +05:45
skshetry 7ab4c59d70 test: run pytest-style checks (#8901) 2023-01-28 14:10:53 +05:45
skshetry a137b2a6bc update ruff and enable flake8-logging-format and flake8-type-checking (#8894)
* update ruff and enable flake8-logging-format and flake8-type-checking check

* Update dvc/external_repo.py

* Update dvc/fs/dvc.py

* Update dvc/repo/experiments/__init__.py

* Update dvc/repo/experiments/queue/base.py

* Update pyproject.toml
2023-01-27 19:14:19 +05:45
Daniele Trifirò 6f0e8a8db5 tests: fix test_iter_revs mock 2023-01-27 12:52:46 +01:00
Daniele Trifirò d294945499 gc: fix usage with --date 2023-01-27 12:52:46 +01:00
Dave Berenbaum db70d1cc5c analytics: differentiate ci users (#8838) 2023-01-26 21:50:14 +05:45
Andrii Korzhuk ed6e11e856 Fixed typo in _format_field function 2023-01-26 13:50:40 +01:00
skshetry c1cd13c34f serialize: fix loading of cloud metadata from .dvc/dvc.lock files (#8889) 2023-01-26 16:18:03 +05:45
David de la Iglesia Castro 12dd746165 output: Pass version_aware=True to fs if files is present.
This will be ignored by filesystems that don't support `version_aware`.

Closes #8853
2023-01-26 10:48:59 +01:00
5165fb67a1 [pre-commit.ci] pre-commit autoupdate (#8869)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.223 → v0.0.230](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.223...v0.0.230)

* Update .pre-commit-config.yaml

* fix lint issues

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2023-01-26 14:24:09 +05:45
Peter Rowlands b2c1c4dccd data_cloud: move version_aware config check to get_remote 2023-01-26 17:21:47 +09:00
Peter Rowlands b060cadbf1 deps: bump dvc-data to 0.36.2 2023-01-26 17:21:47 +09:00
Peter Rowlands 7c55a9c7cd testing: add initial versioned remote tests 2023-01-26 17:03:50 +09:00
Peter Rowlands 037b5b2e0a testing: add cloud versioning remote fixtures 2023-01-26 17:03:50 +09:00
dependabot[bot] 99acb8e335 build(deps-dev): bump dvc-data from 0.36.0 to 0.36.1 (#8887)
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.36.0 to 0.36.1.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.36.0...0.36.1)

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

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-26 08:32:02 +05:45
Ruslan Kuprieiev b46cfbda5d deps: bump dvc-data to 0.36.0 (#8885) 2023-01-26 00:22:12 +02:00
Dave Berenbaum a8d193bf2a queue: update kill ui messages (#8798)
* queue: update kill ui messages

* queue kill: update ui, kill all valid tasks

* queue kill: update wording of ui
2023-01-25 20:08:27 +05:45
dependabot[bot] 0b1cf6c771 build(deps-dev): bump dvc-gs from 2.20.0 to 2.21.0 (#8841)
Bumps [dvc-gs](https://github.com/iterative/dvc-gs) from 2.20.0 to 2.21.0.
- [Release notes](https://github.com/iterative/dvc-gs/releases)
- [Commits](https://github.com/iterative/dvc-gs/compare/2.20.0...2.21.0)

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

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-25 19:29:21 +05:45
skshetry b3aa698cd4 dumpd: serialize files at the end in .dvc files (#8879) 2023-01-25 18:52:15 +05:45
skshetry bb9a3a0403 import/import-url: try not to delete dvcfile before import/import-url completes (#8876)
* import/import-url: try not to delete dvcfile before import/import-url completes

* use repo.stage.create instead of internal create_stage utility
2023-01-25 18:16:45 +05:45
skshetry 3427e115eb data status: check if change.old.hash_info is None or not (#8878)
data:status: check if change.old.hash_info is None or not
2023-01-25 14:24:38 +02:00
skshetry 3d5f4dc66c add support for output-specific remotes (#8862)
Also splits metadata in `files` to cloud vs local. But this happens
during dump/load, so actual behaviour is changed. During runtime,
the metadata is merged and loaded. During dump, the metadata
are split if there is a `remote` set.
2023-01-25 18:06:12 +05:45
David de la Iglesia Castro 80633f4fad stage.save: Remove merge_versioned option.
If we want `add`, `commit` and `move` to work for cloud versioning, option must be always True so it is the same as not having it.

Closes #8828
(We were not passing `merge_versioned=True` in `dvc commit`)
2023-01-25 10:35:37 +01:00
Ruslan Kuprieiev 8bda3650c5 deps: bump dvc-data to 0.35.3 2023-01-24 23:06:34 +02:00
David de la Iglesia Castro ea2e5dc3e5 output: Don't serialize hash_info or meta in cloud versioning.
Closes #8357
2023-01-24 18:40:39 +01:00
dependabot[bot] 61afdca3a6 build(deps-dev): bump dvc-data from 0.35.1 to 0.35.2 (#8871)
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.35.1 to 0.35.2.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.35.1...0.35.2)

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

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 20:47:16 +05:45
Ruslan Kuprieiev 6a6bc7035a data: status: use data index (#8846)
Pre-requisite for #8761
2023-01-23 21:29:32 +02:00
Ruslan Kuprieiev ccc97622fa deps: bump dvc-ssh to 2.21.0 (#8865)
* deps: bump dvc-ssh to 2.21.0

* Update pyproject.toml
2023-01-23 01:36:52 +02:00
Ruslan Kuprieiev 916b5d169f deps: bump dvc-azure to 2.21.0 2023-01-23 00:46:58 +02:00
Peter Rowlands 38e9b2ba95 deps: bump dvc-data to 0.35.1 2023-01-21 03:44:02 +09:00
Dave Berenbaumandskshetry 18f039d7c7 add viztracer async option (#8856)
* add viztracer async option

* Update dvc/_debug.py

Co-authored-by: skshetry <[email protected]>
2023-01-20 22:29:11 +05:45
skshetry f1c9e9365b remove setup.py file (#8851) 2023-01-20 20:26:13 +05:45
Ruslan Kuprieiev b50d1ee674 deps: bump dvc-data to 0.35.0 2023-01-20 04:16:12 +02:00
Peter Rowlands 1c33024b2c deps: bump dvc-data to 0.34.0 2023-01-19 19:09:45 +09:00
David de la Iglesia CastroandPeter Rowlands dd2d2dce19 param: Fix status check for Tuples.
Closes #8803

Update dvc/dependency/param.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2023-01-19 08:19:16 +01:00
David de la Iglesia Castro 5f558d003a deps: bump dvc-data to 0.33.0
- Update `test_gc`
2023-01-18 15:17:07 +02:00
David de la Iglesia Castro 3676f35b7c output.remove: Pass explicit recursive flag.
Per changes in https://github.com/iterative/dvc-objects/pull/176
2023-01-18 15:17:07 +02:00
David de la Iglesia Castro 9af6fcad4a exp run: Fix --dry option.
Closes #8831
2023-01-17 13:32:20 +01:00
skshetry 6d80b72434 lint: run lint checks on all platforms (#8832) 2023-01-17 10:51:57 +05:45
pre-commit-ci[bot] 46d9290fc2 [pre-commit.ci] pre-commit autoupdate (#8830)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.222 → v0.0.223](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.222...v0.0.223)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-17 08:55:33 +05:45
skshetry c47c2123fb cleanup linters: get rid of pylint-pytest, run pylint only on dvc/ directory, etc (#8822)
* cleanup linters: get rid of pylint-pytest, run pylint only on dvc/ directory, etc.

* remove pylint direcitves

* Apply suggestions from code review

* Update pyproject.toml
2023-01-17 08:35:19 +05:45
Dave Berenbaum d25d26ff74 exp: drop some words from exp names (#8811) 2023-01-16 17:11:04 +01:00
Peter Rowlands c2e4f51479 serialize: fix mypy error 2023-01-16 15:58:21 +09:00
Peter Rowlands 977a1f9f43 tests: update for dvc-objects batched transfer 2023-01-16 15:58:21 +09:00
Peter Rowlands 608269dc88 status: remove deprecated/unused dvc-data.hashfile.status flag 2023-01-16 15:58:21 +09:00
Peter Rowlands e48d0d1d9c tests: remove duplicated dvc-objects/dvc-data tests 2023-01-16 15:58:21 +09:00
Peter Rowlands bf08bd3f12 deps: bump dvc-data to 0.32.0 2023-01-16 15:58:21 +09:00
dependabot[bot] db5dfdd6bc build(deps-dev): bump pytest from 7.2.0 to 7.2.1 (#8819)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.0 to 7.2.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.0...7.2.1)

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

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 08:37:15 +05:45
dependabot[bot] 3b10f40ca3 build(deps-dev): bump dvc-task from 0.1.10 to 0.1.11 (#8820)
Bumps [dvc-task](https://github.com/iterative/dvc-task) from 0.1.10 to 0.1.11.
- [Release notes](https://github.com/iterative/dvc-task/releases)
- [Commits](https://github.com/iterative/dvc-task/compare/0.1.10...0.1.11)

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

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 08:36:39 +05:45
Ruslan Kuprieiev 4660443d0e deps: bump dvc-data to 0.31.4 2023-01-16 04:09:00 +02:00
Ruslan Kuprieiev a92ace0054 deps: bump dvc-data to 0.31.3 2023-01-16 01:16:35 +02:00
skshetry 309ae4d995 mypy: enable check-untyped-defs (#8815) 2023-01-15 21:40:40 +05:45
daniele e1254c9ece Add misc type hints (#8430) 2023-01-15 17:58:34 +05:45
skshetry 7a65ac06b2 mypy: don't ignore all imports, also disallow implicit optional (#8814)
* mypy: don't ignore all imports

* mypy: disallow implicit_optional
2023-01-15 16:25:03 +05:45
skshetry b0052aa666 pre-commit update: flake8/ruff/pyupgrade/black/isort (#8813) 2023-01-15 14:56:54 +05:45
skshetry da27174e02 dvcfile: rename PipelineFile to ProjectFile, get rid of Dvcfile class, cleanups, etc. (#8812)
dvcfile: rename PipelineFile to ProjectFile, get rid of Dvcfile class, cleanups etc
2023-01-15 12:45:10 +05:45
Ruslan Kuprieiev aabdddc2b7 index: introduce build_data_index
This is a generally useful function that builds a data index from repo index.

This is currently only used in cloud versioning, but it is going to be used in
`status`, `diff` and other data-centric operations
2023-01-15 01:45:09 +02:00
karajan1001 fc1a192c90 Bump pathspec to 0.10.3
fix: #8217
1. Replace group name to avoid conflict.
2. Disable failed tests.
3. Bump pathspec to 0.10.3
2023-01-13 22:47:36 +08:00
dependabot[bot]andSaugat Pachhai 178244f666 build(deps-dev): bump dvc-task from 0.1.9 to 0.1.10 (#8807)
* build(deps-dev): bump dvc-task from 0.1.9 to 0.1.10

Bumps [dvc-task](https://github.com/iterative/dvc-task) from 0.1.9 to 0.1.10.
- [Release notes](https://github.com/iterative/dvc-task/releases)
- [Commits](https://github.com/iterative/dvc-task/compare/0.1.9...0.1.10)

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

Signed-off-by: dependabot[bot] <[email protected]>

* remove xfail on json.JSONDecodeError/PermissionError

JSONDecodeError should be fixed by the new release. Next we need to investigate
PermissionError.

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2023-01-13 17:25:36 +05:45
skshetry dba7d3c320 mypy: use typesafe cached_property (#8799) 2023-01-13 09:08:59 +05:45
David de la Iglesia Castro 8b075eb754 stage: Skip run-cache if any out has cache: false.
Closes https://github.com/iterative/dvc/issues/4428
2023-01-12 19:10:01 +01:00
David de la Iglesia Castro f507e4d9c0 config: Reraise ConfigError from ConfigObjError.
Closes #8801
2023-01-12 18:11:54 +01:00
skshetry 5169205229 deps: bump minimum version req for grandalf to 0.7. 2023-01-12 18:38:45 +05:45
skshetry 3bcd683677 repo.check_graph: rename method (#8800) 2023-01-12 16:03:51 +05:45
Saugat Pachhai (सौगात) 30ec1cf5e9 index/stage: move repo collection logic to index 2023-01-11 19:38:43 +02:00
skshetry a814f04ee0 tests: xfail test_apply_failed on JSONDecodeError/PermissionError (#8796) 2023-01-11 18:04:23 +05:45
skshetry 90a27179eb ruff: ignore complex functions and remove dvc.utils.fs.makedirs (#8795)
We may want to prevent any new functions from getting complex.
2023-01-11 17:58:51 +05:45
skshetry 175c578579 ruff: ban relative parent/grandparent imports (#8794) 2023-01-11 16:48:46 +05:45
skshetry 5441964fdb add ruff (#8793)
* add ruff linter

* enable few more checks
2023-01-11 15:20:38 +05:45
Ivan Shcheklein d4bfe38a54 perf: remove fs exists check in plots, parallel data collect 2023-01-10 20:27:02 -08:00
dependabot[bot] a060049186 build(deps-dev): unpin grandalf
See https://github.com/bdcht/grandalf/issues/26#issuecomment-1378175273.
2023-01-11 08:44:49 +05:45
412 changed files with 8430 additions and 8426 deletions
+14 -7
View File
@@ -1,12 +1,19 @@
[flake8]
ignore=
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
P1, # unindexed parameters in the str.format, see:
# Whitespace before ':'
E203
# Too many leading '#' for block comment
E266
# Line break occurred before a binary operator
W503
# unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
max_line_length = 79
max-complexity = 15
select = B,C,E,F,W,T4,B902,T,P
P1
# use `!r` instead of surrounding by quotes
B028
max_line_length = 88
max-complexity = 10
select = B,C,E,F,W,T4,B902,T,P,TYP
show_source = true
count = true
min_python_version = 3.8.0
+10
View File
@@ -0,0 +1,10 @@
# The following commits will be ignored by the GitHub blame view.
# You can configure `git blame` to use this file as the default ignore file:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# or, you can also manually ignore commits from this file by doing:
# git blame --ignore-revs-file .git-blame-ignore-revs <filepath>
# see the `blame.markIgnoredLines` and `blame.markUnblamableLines` options as well.
# Refer to: https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
# update black to 2023 stable style and change line-length to 88
a86470ccbc693d6f0a4f8066cbd1357e5191c4ff
+3 -3
View File
@@ -17,9 +17,9 @@ jobs:
strategy:
matrix:
include:
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
name: ${{ matrix.pkg }}
+54
View File
@@ -0,0 +1,54 @@
name: Remote Plugin Tests
on:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
tests:
timeout-minutes: 45
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
pyv: ["3.10"]
plugin: ["dvc-s3"]
steps:
- uses: actions/checkout@v3
with:
path: dvc
- uses: actions/checkout@v3
with:
repository: iterative/${{ matrix.plugin }}
ref: main
path: ${{ matrix.plugin }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyv }}
cache: pip
cache-dependency-path: |
dvc/pyproject.toml
${{ matrix.plugin }}/setup.cfg
- name: Install plugin + DVC@PR
run: |
pip install --upgrade pip wheel
pip install "./dvc[testing]"
pip install -e "./${{ matrix.plugin }}[tests]"
- name: Run plugin tests
timeout-minutes: 15
working-directory: ${{ matrix.plugin }}
run: pytest -v -n=auto
+8 -34
View File
@@ -12,44 +12,18 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: # added using https://github.com/step-security/secure-workflows
permissions:
contents: read
jobs:
lint:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: pyproject.toml
- name: Install requirements
run: |
pip install --upgrade pip wheel
pip install ".[dev]"
- uses: pre-commit/[email protected]
tests:
timeout-minutes: 45
timeout-minutes: 50
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest]
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.8", "3.9", "3.10", "3.11"]
pytest-filter:
- "import or plot or live or experiment"
- "not (import or plot or live or experiment)"
include:
- {os: macos-latest, pyv: "3.8"}
- {os: macos-latest, pyv: "3.9"}
- {os: macos-latest, pyv: "3.10"}
- {os: macos-latest, pyv: "3.11"}
steps:
- uses: actions/checkout@v3
@@ -65,12 +39,12 @@ jobs:
run: |
pip install --upgrade pip wheel
pip install -e ".[dev]"
- uses: pre-commit/[email protected]
- name: run tests
timeout-minutes: 30
timeout-minutes: 40
run: >
pytest -nauto --timeout=300 --durations=100
pytest -n=logical --timeout=300 --durations=100
--cov --cov-report=xml --cov-report=term
-k "${{ matrix.pytest-filter }}"
- name: upload coverage report
uses: codecov/codecov-action@v3
with:
@@ -78,7 +52,7 @@ jobs:
fail_ci_if_error: false
check:
if: always()
needs: [lint, tests]
needs: [tests]
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
@@ -86,7 +60,7 @@ jobs:
jobs: ${{ toJSON(needs) }}
notify:
if: github.ref == 'refs/heads/main' && failure()
needs: [lint, tests]
needs: [tests]
runs-on: ubuntu-latest
steps:
- name: Slack Notification
+13 -6
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -29,34 +29,38 @@ repos:
- id: beautysh
args: ["-i", "2"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.0
rev: v3.3.1
hooks:
- id: pyupgrade
args:
- --py38-plus
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.254'
hooks:
- id: ruff
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
additional_dependencies: ["tomli"]
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-broken-line
- flake8-bugbear
- flake8-comprehensions
- flake8-debugger
- flake8-string-format
- flake8-typing-imports
- repo: local
hooks:
- id: mypy
@@ -65,11 +69,14 @@ repos:
files: ^dvc/
language: system
types: [python]
require_serial: true
- id: pylint
name: pylint
entry: pylint
files: ^dvc/
language: system
types: [python]
require_serial: true
- hooks:
- args:
- git-hook
+34 -25
View File
@@ -8,15 +8,19 @@ if TYPE_CHECKING:
@contextmanager
def viztracer_profile(path: Union[Callable[[], str], str], depth: int = -1):
def viztracer_profile(
path: Union[Callable[[], str], str],
depth: int = -1,
log_async: bool = False,
):
try:
import viztracer # pylint: disable=import-error
except ImportError:
print("Failed to run profiler, viztracer is not installed")
print("Failed to run profiler, viztracer is not installed") # noqa: T201
yield
return
tracer = viztracer.VizTracer(max_stack_depth=depth)
tracer = viztracer.VizTracer(max_stack_depth=depth, log_async=log_async)
tracer.start()
yield
@@ -27,14 +31,14 @@ def viztracer_profile(path: Union[Callable[[], str], str], depth: int = -1):
@contextmanager
def yappi_profile(
path: Union[Callable[[], str], str] = None,
path: Optional[Union[Callable[[], str], str]] = None,
wall_clock: Optional[bool] = True,
separate_threads: Optional[bool] = False,
):
try:
import yappi # pylint: disable=import-error
except ImportError:
print("Failed to run profiler, yappi is not installed")
print("Failed to run profiler, yappi is not installed") # noqa: T201
yield
return
@@ -60,7 +64,7 @@ def yappi_profile(
st.save(out, type="callgrind")
else:
if ctx_id is not None:
print(f"\nThread {ctx_id}")
print(f"\nThread {ctx_id}") # noqa: T201
st.print_all() # pylint:disable=no-member
if ctx_id is None:
threads.print_all() # pylint:disable=no-member
@@ -74,7 +78,7 @@ def instrument(html_output=False):
try:
from pyinstrument import Profiler # pylint: disable=import-error
except ImportError:
print("Failed to run profiler, pyinstrument is not installed")
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
yield
return
@@ -87,11 +91,11 @@ def instrument(html_output=False):
if html_output:
profiler.open_in_browser()
return
print(profiler.output_text(unicode=True, color=True))
print(profiler.output_text(unicode=True, color=True)) # noqa: T201
@contextmanager
def profile(dump_path: str = None):
def profile(dump_path: Optional[str] = None):
"""Run a cprofile"""
import cProfile
@@ -115,28 +119,31 @@ def debug():
try:
import ipdb as pdb # noqa: T100, pylint: disable=import-error
except ImportError:
import pdb # noqa: T100
import pdb # type: ignore[no-redef] # noqa: T100
pdb.post_mortem()
raise # prevent from jumping ahead
def _sigshow(_, frame: Optional["FrameType"]) -> None:
import sys
from shutil import get_terminal_size
from traceback import format_stack
from dvc.ui import ui
lines = "\u2015" * get_terminal_size().columns
ui.error_write(lines, "\n", *format_stack(frame), lines, sep="")
stack = format_stack(frame)
print(lines, "\n", *stack, lines, sep="", file=sys.stderr) # noqa: T201
@contextmanager
def show_stack():
r"""Show stack trace on SIGQUIT (Ctrl-\) or SIGINFO (Ctrl-T on macOS)."""
import signal
import sys
if sys.platform != "win32":
signal.signal(signal.SIGQUIT, _sigshow)
signal.signal(signal.SIGQUIT, _sigshow)
try:
signal.signal(signal.SIGINFO, _sigshow) # only available on macOS
except AttributeError:
@@ -154,7 +161,7 @@ def _get_path_func(tool: str, ext: str):
@contextmanager
def debugtools(args: "Namespace" = None, **kwargs):
def debugtools(args: Optional["Namespace"] = None, **kwargs):
kw = vars(args) if args else {}
kw.update(kwargs)
@@ -175,10 +182,15 @@ def debugtools(args: "Namespace" = None, **kwargs):
separate_threads=kw.get("yappi_separate_threads"),
)
)
if kw.get("viztracer") or kw.get("viztracer_depth"):
if (
kw.get("viztracer")
or kw.get("viztracer_depth")
or kw.get("viztracer_async")
):
path_func = _get_path_func("viztracer", "json")
depth = kw.get("viztracer_depth") or -1
prof = viztracer_profile(path=path_func, depth=depth)
log_async = kw.get("viztracer_async") or False
prof = viztracer_profile(path=path_func, depth=depth, log_async=log_async)
stack.enter_context(prof)
yield
@@ -186,12 +198,8 @@ def debugtools(args: "Namespace" = None, **kwargs):
def add_debugging_flags(parser):
from argparse import SUPPRESS
parser.add_argument(
"--cprofile", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--yappi", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument("--cprofile", action="store_true", default=False, help=SUPPRESS)
parser.add_argument("--yappi", action="store_true", default=False, help=SUPPRESS)
parser.add_argument(
"--yappi-separate-threads",
action="store_true",
@@ -202,10 +210,11 @@ def add_debugging_flags(parser):
"--viztracer", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument("--viztracer-depth", type=int, help=SUPPRESS)
parser.add_argument("--cprofile-dump", help=SUPPRESS)
parser.add_argument(
"--pdb", action="store_true", default=False, help=SUPPRESS
"--viztracer-async", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument("--cprofile-dump", help=SUPPRESS)
parser.add_argument("--pdb", action="store_true", default=False, help=SUPPRESS)
parser.add_argument(
"--instrument", action="store_true", default=False, help=SUPPRESS
)
+11 -5
View File
@@ -82,10 +82,9 @@ def send(path):
def _scm_in_use():
from scmrepo.noscm import NoSCM
from dvc.exceptions import NotDvcRepoError
from dvc.repo import Repo
from dvc.scm import NoSCM
from .scm import SCM, SCMError
@@ -102,17 +101,24 @@ def _runtime_info():
"""
Gather information from the environment where DVC runs to fill a report.
"""
from iterative_telemetry import find_or_create_user_id
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
from dvc import __version__
from dvc.utils import is_binary
ci_id = _generate_ci_id()
if ci_id:
group_id, user_id = ci_id
else:
group_id, user_id = None, find_or_create_user_id()
return {
"dvc_version": __version__,
"is_binary": is_binary(),
"scm_class": _scm_in_use(),
"system_info": _system_info(),
"user_id": find_or_create_user_id(),
"user_id": user_id,
"group_id": group_id,
}
@@ -125,7 +131,7 @@ def _system_info():
system = platform.system()
if system == "Windows":
version = sys.getwindowsversion()
version = sys.getwindowsversion() # type: ignore[attr-defined]
return {
"os": "windows",
+1 -1
View File
@@ -12,7 +12,7 @@ class Annotation:
PARAM_META: ClassVar[str] = "meta"
desc: Optional[str] = None
type: Optional[str] = None
type: Optional[str] = None # noqa: A003
labels: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
+2 -1
View File
@@ -3,13 +3,14 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import make_checkpoint
from .params import params_show
from .show import metrics_show, params_show
__all__ = [
"get_url",
"make_checkpoint",
"open",
"params_show",
"metrics_show",
"read",
"DVCFileSystem",
]
+8 -8
View File
@@ -27,23 +27,23 @@ def get_url(path, repo=None, rev=None, remote=None):
if not dvc_info:
raise OutputNotFoundError(path, repo)
dvc_repo = info["repo"]
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):
def __init__(
self, func, args, kwds
): # pylint: disable=super-init-not-called
def __init__(self, func, args, kwds): # pylint: disable=super-init-not-called
self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = func, args, kwds
self.func, self.args, self.kwds = ( # type: ignore[assignment]
func,
args,
kwds,
)
def __getattr__(self, name):
raise AttributeError(
"dvc.api.open() should be used in a with statement."
)
raise AttributeError("dvc.api.open() should be used in a with statement.")
def open( # noqa, pylint: disable=redefined-builtin
+151 -33
View File
@@ -1,12 +1,156 @@
import typing
from collections import Counter
from typing import Dict, Iterable, Optional, Union
from funcy import first
from dvc.exceptions import DvcException
from dvc.repo import Repo
def _onerror_raise(exception: Exception, *args, **kwargs):
raise exception
def _postprocess(results):
processed: Dict[str, Dict] = {}
for rev, rev_data in results.items():
if not rev_data:
continue
processed[rev] = {}
counts: typing.Counter[str] = Counter()
for file_data in rev_data["data"].values():
for k in file_data["data"]:
counts[k] += 1
for file_name, file_data in rev_data["data"].items():
to_merge = {
(k if counts[k] == 1 else f"{file_name}:{k}"): v
for k, v in file_data["data"].items()
}
processed[rev] = {**processed[rev], **to_merge}
if "workspace" in processed:
del processed["workspace"]
return processed
def metrics_show(
*targets: str,
repo: Optional[str] = None,
rev: Optional[str] = None,
) -> Dict:
"""Get metrics tracked in `repo`.
Without arguments, this function will retrieve all metrics from all tracked
metric files, for the current working tree.
See the options below to restrict the metrics retrieved.
Args:
*targets (str, optional): Names of the metric files to retrieve
metrics from. For example, "classifier_eval.json,
clustering_eval.json".
If no `targets` are provided, all metric files tracked in `dvc.yaml`
will be used.
Note that targets don't necessarily have to be defined in `dvc.yaml`.
repo (str, optional): Location of the DVC repository.
Defaults to the current project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
rev (str, optional): Name of the `Git revision`_ to retrieve metrics
from.
Defaults to `None`.
An example of git revision can be a branch or tag name, a commit
hash or a dvc experiment name.
If `repo` is not a Git repo, this option is ignored.
If `None`, the current working tree will be used.
Returns:
Dict: See Examples below.
Examples:
- No arguments.
Working on https://github.com/iterative/example-get-started
>>> import dvc.api
>>> metrics = dvc.api.metrics_show()
>>> print(json.dumps(metrics, indent=4))
{
"avg_prec": 0.9249974999612706,
"roc_auc": 0.9460213440787918
}
---
- Using `rev`.
Working on https://github.com/iterative/example-get-started
>>> import json
>>> import dvc.api
>>> metrics = dvc.api.metrics_show(rev="tune-hyperparams")
>>> print(json.dumps(metrics, indent=4))
{
"avg_prec": 0.9268792615819422,
"roc_auc": 0.945093365854111
}
---
- Using `targets`.
Working on https://github.com/iterative/example-get-started
>>> import json
>>> import dvc.api
>>> metrics = dvc.api.metrics_show("evaluation.json")
>>> print(json.dumps(metrics, indent=4))
{
"avg_prec": 0.9249974999612706,
"roc_auc": 0.9460213440787918
}
---
- Git URL as `repo`.
>>> import json
>>> import dvc.api
>>> metrics = dvc.api.metrics_show(
... repo="https://github.com/iterative/demo-fashion-mnist")
>>> print(json.dumps(metrics, indent=4))
{
"loss": 0.25284987688064575,
"accuracy": 0.9071000218391418
}
.. _Git revision:
https://git-scm.com/docs/revisions
"""
with Repo.open(repo) as _repo:
metrics = _repo.metrics.show(
targets=targets,
revs=rev if rev is None else [rev],
onerror=_onerror_raise,
)
metrics = _postprocess(metrics)
if not metrics:
return {}
return metrics[first(metrics)]
def params_show(
*targets: str,
repo: Optional[str] = None,
@@ -55,9 +199,6 @@ def params_show(
Returns:
Dict: See Examples below.
Raises:
DvcException: If no params are found in `repo`.
Examples:
- No arguments.
@@ -238,34 +379,6 @@ def params_show(
if isinstance(stages, str):
stages = [stages]
def _onerror_raise(result: Dict, exception: Exception, *args, **kwargs):
raise exception
def _postprocess(params):
processed = {}
for rev, rev_data in params.items():
processed[rev] = {}
counts = Counter()
for file_data in rev_data["data"].values():
for k in file_data["data"]:
counts[k] += 1
for file_name, file_data in rev_data["data"].items():
to_merge = {
(k if counts[k] == 1 else f"{file_name}:{k}"): v
for k, v in file_data["data"].items()
}
processed[rev] = {**processed[rev], **to_merge}
if "workspace" in processed:
del processed["workspace"]
if not processed:
raise DvcException("No params found")
return processed[first(processed)]
with Repo.open(repo) as _repo:
params = _repo.params.show(
revs=rev if rev is None else [rev],
@@ -275,4 +388,9 @@ def params_show(
stages=stages,
)
return _postprocess(params)
params = _postprocess(params)
if not params:
return {}
return params[first(params)]
+11 -5
View File
@@ -1,3 +1,5 @@
import os
from dvc.fs import GitFileSystem, Schemes
from dvc_data.hashfile.db import get_odb
@@ -9,12 +11,11 @@ def _get_odb(repo, settings, fs=None):
return None
cls, config, fs_path = get_cloud_fs(repo, **settings)
config["tmp_dir"] = repo.tmp_dir
fs = fs or cls(**config)
return get_odb(fs, fs_path, state=repo.state, **config)
class ODBManager:
class CacheManager:
CACHE_DIR = "cache"
CLOUD_SCHEMES = [
Schemes.S3,
@@ -29,17 +30,22 @@ class ODBManager:
self.config = config = repo.config["cache"]
self._odb = {}
default = None
if repo and repo.local_dvc_dir:
default = os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
local = config.get("local")
if local:
settings = {"name": local}
elif "dir" not in config:
elif "dir" not in config and not default:
settings = None
else:
from dvc.config_schema import LOCAL_COMMON
settings = {"url": config["dir"]}
for opt in LOCAL_COMMON.keys():
url = config.get("dir") or default
settings = {"url": url}
for opt in LOCAL_COMMON:
if opt in config:
settings[str(opt)] = config.get(opt)
+45 -20
View File
@@ -39,13 +39,17 @@ def parse_args(argv=None):
def _log_unknown_exceptions() -> None:
from dvc.info import get_dvc_info
from dvc.logger import FOOTER
from dvc.ui import ui
from dvc.utils import colorize
logger.exception("unexpected error")
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Version info for developers:\n%s", get_dvc_info())
logger.info(FOOTER)
return None
q = colorize("Having any troubles?", "yellow")
link = colorize("https://dvc.org/support", "blue")
footer = f"\n{q} Hit us up at {link}, we are always happy to help!"
ui.error_write(footer)
def _log_exceptions(exc: Exception) -> Optional[int]:
@@ -57,9 +61,11 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
if exc.errno == errno.EMFILE:
logger.exception(
"too many open files, please visit "
"{} to see how to handle this "
"problem".format(error_link("many-files")),
(
"too many open files, please visit "
"%s to see how to handle this problem"
),
error_link("many-files"),
extra={"tb_only": True},
)
else:
@@ -81,7 +87,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
if cmd:
link = format_link("https://dvc.org/doc/install")
hint = (
f"To install dvc with those dependencies, run:\n"
"To install dvc with those dependencies, run:\n"
"\n"
f"\t{cmd}\n"
"\n"
@@ -92,8 +98,10 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
hint = f"\nPlease report this bug to {link}. Thank you!"
logger.exception(
f"URL '{exc.url}' is supported but requires these missing "
f"dependencies: {exc.missing_deps}. {hint}",
"URL '%s' is supported but requires these missing dependencies: %s. %s",
exc.url,
exc.missing_deps,
hint,
extra={"tb_only": True},
)
return None
@@ -102,7 +110,9 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
link = format_link("https://man.dvc.org/remote/modify")
logger.exception("configuration error")
logger.exception(
f"{exc!s}\nLearn more about configuration settings at {link}.",
"%s\nLearn more about configuration settings at %s.",
exc,
link,
extra={"tb_only": True},
)
return 251
@@ -114,9 +124,14 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
directory = relpath(exc.directory)
logger.exception(
f"Could not open pickled '{exc.type}' cache.\n"
f"Remove the '{directory}' directory and then retry this command."
f"\nSee {error_link('pickle')} for more information.",
(
"Could not open pickled '%s' cache.\n"
"Remove the '%s' directory and then retry this command."
"\nSee %s for more information."
),
exc.type,
directory,
error_link("pickle"),
extra={"tb_only": True},
)
return None
@@ -131,7 +146,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
return None
def main(argv=None): # noqa: C901
def main(argv=None): # noqa: C901, PLR0912, PLR0915
"""Main entry point for dvc CLI.
Args:
@@ -143,7 +158,7 @@ def main(argv=None): # noqa: C901
from dvc._debug import debugtools
from dvc.config import ConfigError
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.logger import disable_other_loggers, set_loggers_level
from dvc.logger import set_loggers_level
# NOTE: stderr/stdout may be closed if we are running from dvc.daemon.
# On Linux we directly call cli.main after double forking and closing
@@ -156,9 +171,8 @@ def main(argv=None): # noqa: C901
logging.disable(logging.INFO)
args = None
disable_other_loggers()
outerLogLevel = logger.level
outer_log_level = logger.level
try:
args = parse_args(argv)
@@ -168,12 +182,23 @@ def main(argv=None): # noqa: C901
elif args.verbose == 1:
level = logging.DEBUG
elif args.verbose > 1:
level = logging.TRACE
level = logging.TRACE # type: ignore[attr-defined]
if level is not None:
set_loggers_level(level)
logger.trace(args)
if level and level <= logging.DEBUG:
from platform import platform, python_implementation, python_version
from dvc import __version__
from dvc.utils.pkg import PKG
pyv = " ".join([python_implementation(), python_version()])
pkg = f" ({PKG})" if PKG else ""
logger.debug("v%s%s, %s on %s", __version__, pkg, pyv, platform())
logger.debug("command: %s", " ".join(argv or sys.argv))
logger.trace(args) # type: ignore[attr-defined]
if not sys.stdout.closed and not args.quiet:
from dvc.ui import ui
@@ -218,7 +243,7 @@ def main(argv=None): # noqa: C901
return ret
finally:
logger.setLevel(outerLogLevel)
logger.setLevel(outer_log_level)
from dvc.external_repo import clean_repos
+2 -2
View File
@@ -2,7 +2,7 @@ from argparse import _AppendAction
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
def __call__(self, parser, namespace, values, option_string=None):
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
from funcy import ldistinct
items = getattr(namespace, self.dest) or []
@@ -11,7 +11,7 @@ class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
class KeyValueArgs(_AppendAction):
def __call__(self, parser, namespace, values, option_string=None):
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("=")
+6 -2
View File
@@ -1,6 +1,10 @@
import logging
import os
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from dvc.config import Config
logger = logging.getLogger(__name__)
@@ -8,13 +12,13 @@ logger = logging.getLogger(__name__)
class CmdBase(ABC):
UNINITIALIZED = False
def __init__(self, args):
def __init__(self, args: Any):
from dvc.repo import Repo
os.chdir(args.cd)
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
self.config = self.repo.config
self.config: "Config" = self.repo.config
self.args = args
def do_run(self):
+1 -3
View File
@@ -28,7 +28,5 @@ def hide_subparsers_from_help(subparsers):
# Need to set `add_help=False`, but avoid setting `help`
# (not even to `argparse.SUPPPRESS`).
# NOTE: The argument is the parent subparser, not the subcommand parser.
cmds = [
cmd for cmd, parser in subparsers.choices.items() if parser.add_help
]
cmds = [cmd for cmd, parser in subparsers.choices.items() if parser.add_help]
subparsers.metavar = "{{{}}}".format(",".join(cmds))
+1 -4
View File
@@ -41,10 +41,7 @@ def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
class CmdAdd(CmdBase):
def run(self):
from dvc.exceptions import (
DvcException,
RecursiveAddingWhileUsingFilename,
)
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
try:
if len(self.args.targets) > 1 and self.args.file:
+18 -7
View File
@@ -1,4 +1,5 @@
import argparse
import os
from dvc.cli import completion
from dvc.cli.utils import append_doc_link, fix_subparsers
@@ -9,13 +10,21 @@ from dvc.ui import ui
class CmdCacheDir(CmdConfig):
def run(self):
if self.args.value is None and not self.args.unset:
from dvc.config import ConfigError
if self.args.level:
conf = self.config.read(level=self.args.level)
else:
# Use merged config with default values
conf = self.config
self._check(conf, False, "cache", "dir")
ui.write(conf["cache"]["dir"])
try:
self._check(conf, False, "cache", "dir")
path = conf["cache"]["dir"]
except ConfigError:
if not self.config.dvc_dir or self.args.level:
raise
path = os.path.join(self.config.dvc_dir, "cache")
ui.write(path)
return 0
with self.config.edit(level=self.args.level) as conf:
if self.args.unset:
@@ -42,7 +51,7 @@ def add_parser(subparsers, parent_parser):
cache_subparsers = cache_parser.add_subparsers(
dest="cmd",
help="Use `dvc cache CMD --help` for command-specific " "help.",
help="Use `dvc cache CMD --help` for command-specific help.",
)
fix_subparsers(cache_subparsers)
@@ -68,10 +77,12 @@ def add_parser(subparsers, parent_parser):
)
cache_dir_parser.add_argument(
"value",
help="Path to cache directory. Relative paths are resolved relative "
"to the current directory and saved to config relative to the "
"config file location. If no path is provided, it returns the "
"current cache directory.",
help=(
"Path to cache directory. Relative paths are resolved relative "
"to the current directory and saved to config relative to the "
"config file location. If no path is provided, it returns the "
"current cache directory."
),
nargs="?",
).complete = completion.DIR
cache_dir_parser.set_defaults(func=CmdCacheDir)
+10 -10
View File
@@ -62,9 +62,7 @@ class CmdCheckIgnore(CmdBase):
raise DvcException("cannot have both `targets` and `--stdin`")
if self.args.non_matching and not self.args.details:
raise DvcException(
"`--non-matching` is only valid with `--details`"
)
raise DvcException("`--non-matching` is only valid with `--details`")
if self.args.all and not self.args.details:
raise DvcException("`--all` is only valid with `--details`")
@@ -80,9 +78,7 @@ class CmdCheckIgnore(CmdBase):
def add_parser(subparsers, parent_parser):
ADD_HELP = (
"Check whether files or directories are excluded due to `.dvcignore`."
)
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
parser = subparsers.add_parser(
"check-ignore",
@@ -103,16 +99,20 @@ def add_parser(subparsers, parent_parser):
"--all",
action="store_true",
default=False,
help="Include the target paths which don’t match any pattern "
"in the `--details` list.",
help=(
"Include the target paths which don't match any pattern "
"in the `--details` list."
),
)
parser.add_argument(
"-n",
"--non-matching",
action="store_true",
default=False,
help="Include the target paths which don’t match any pattern "
"in the `--details` list.",
help=(
"Include the target paths which don't match any pattern "
"in the `--details` list."
),
)
parser.add_argument(
"--stdin",
+2 -6
View File
@@ -22,9 +22,7 @@ def log_changes(stats):
continue
for entry in entries:
ui.write(
f"[{color}]{state[0].upper()}", entry, styled=True, sep="\t"
)
ui.write(f"[{color}]{state[0].upper()}", entry, styled=True, sep="\t")
class CmdCheckout(CmdBase):
@@ -46,9 +44,7 @@ class CmdCheckout(CmdBase):
if self.args.summary:
default_message = "No changes."
msg = get_summary(
sorted(stats.items(), key=operator.itemgetter(0))
)
msg = get_summary(sorted(stats.items(), key=operator.itemgetter(0)))
ui.write(msg or default_message)
else:
log_changes(stats)
+1 -5
View File
@@ -24,11 +24,7 @@ class CmdCommit(CmdBase):
force=self.args.force,
)
except DvcException:
logger.exception(
"failed to commit{}".format(
(" " + target) if target else ""
)
)
logger.exception("failed to commit%s", (" " + target) if target else "")
return 1
return 0
+9 -16
View File
@@ -17,9 +17,7 @@ def _name_type(value):
match = re.match(NAME_REGEX, value)
if not match:
raise argparse.ArgumentTypeError(
"name argument should look like "
"remote.name.option or "
"section.option"
"name argument should look like remote.name.option or section.option"
)
return (
bool(match.group("remote")),
@@ -37,13 +35,12 @@ class CmdConfig(CmdBaseNoRepo):
self.config = Config(validate=False)
def run(self):
if self.args.show_origin:
if any((self.args.value, self.args.unset)):
logger.error(
"--show-origin can't be used together with any of these "
"options: -u/--unset, value"
)
return 1
if self.args.show_origin and (self.args.value or self.args.unset):
logger.error(
"--show-origin can't be used together with any of these "
"options: -u/--unset, value"
)
return 1
if self.args.list:
return self._list()
@@ -71,9 +68,7 @@ class CmdConfig(CmdBaseNoRepo):
for level in levels:
conf = self.config.read(level)
prefix = self._config_file_prefix(
self.args.show_origin, self.config, level
)
prefix = self._config_file_prefix(self.args.show_origin, self.config, level)
configs = list(self._format_config(conf, prefix))
if configs:
ui.write("\n".join(configs))
@@ -132,9 +127,7 @@ class CmdConfig(CmdBaseNoRepo):
raise ConfigError(f"{name} '{section}' doesn't exist")
if opt and opt not in conf[section]:
raise ConfigError(
f"option '{opt}' doesn't exist in {name} '{section}'"
)
raise ConfigError(f"option '{opt}' doesn't exist in {name} '{section}'")
def _get_appropriate_levels(self, levels):
if levels:
+1 -1
View File
@@ -46,7 +46,7 @@ def add_parser(subparsers, parent_parser):
daemon_subparsers = daemon_parser.add_subparsers(
dest="cmd",
help="Use `dvc daemon CMD --help` for command-specific " "help.",
help="Use `dvc daemon CMD --help` for command-specific help.",
)
fix_subparsers(daemon_subparsers)
+28 -29
View File
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
from networkx import DiGraph
def _show_ascii(G: "DiGraph"):
def _show_ascii(graph: "DiGraph"):
from dvc.dagascii import draw
from dvc.repo.graph import get_pipelines
pipelines = get_pipelines(G)
pipelines = get_pipelines(graph)
ret = []
for pipeline in pipelines:
@@ -31,7 +31,7 @@ def _quote_label(node):
return label
def _show_dot(G: "DiGraph"):
def _show_dot(graph: "DiGraph"):
import io
import networkx as nx
@@ -39,15 +39,15 @@ def _show_dot(G: "DiGraph"):
dot_file = io.StringIO()
nx.relabel_nodes(G, _quote_label, copy=False)
write_dot(G.reverse(), dot_file)
nx.relabel_nodes(graph, _quote_label, copy=False)
write_dot(graph.reverse(), dot_file)
return dot_file.getvalue()
def _show_mermaid(G, markdown: bool = False):
def _show_mermaid(graph, markdown: bool = False):
from dvc.repo.graph import get_pipelines
pipelines = get_pipelines(G)
pipelines = get_pipelines(graph)
graph = "flowchart TD"
@@ -86,9 +86,7 @@ def _collect_targets(repo, target, outs):
targets.extend([str(out) for out in stage.outs])
continue
for out in outs_trie.itervalues( # noqa: B301
prefix=repo.fs.path.parts(path)
):
for out in outs_trie.itervalues(prefix=repo.fs.path.parts(path)): # noqa: B301
targets.extend(str(out))
return targets
@@ -102,43 +100,42 @@ def _transform(index, outs):
def _relabel(node) -> str:
return node.addressing if isinstance(node, Stage) else str(node)
G = index.outs_graph if outs else index.graph
return nx.relabel_nodes(G, _relabel, copy=True)
graph = index.outs_graph if outs else index.graph
return nx.relabel_nodes(graph, _relabel, copy=True)
def _filter(G, targets, full):
def _filter(graph, targets, full):
import networkx as nx
if not targets:
return G
return graph
H = G.copy()
new_graph = graph.copy()
if not full:
descendants = set()
for target in targets:
descendants.update(nx.descendants(G, target))
descendants.update(nx.descendants(graph, target))
descendants.add(target)
H.remove_nodes_from(set(G.nodes()) - descendants)
new_graph.remove_nodes_from(set(graph.nodes()) - descendants)
undirected = H.to_undirected()
undirected = new_graph.to_undirected()
connected = set()
for target in targets:
connected.update(nx.node_connected_component(undirected, target))
H.remove_nodes_from(set(H.nodes()) - connected)
return H
new_graph.remove_nodes_from(set(new_graph.nodes()) - connected)
return new_graph
def _build(repo, target=None, full=False, outs=False):
targets = _collect_targets(repo, target, outs)
G = _transform(repo.index, outs)
return _filter(G, targets, full)
graph = _transform(repo.index, outs)
return _filter(graph, targets, full)
class CmdDAG(CmdBase):
def run(self):
G = _build(
graph = _build(
self.repo,
target=self.args.target,
full=self.args.full,
@@ -146,12 +143,12 @@ class CmdDAG(CmdBase):
)
if self.args.dot:
ui.write(_show_dot(G))
ui.write(_show_dot(graph))
elif self.args.mermaid or self.args.markdown:
ui.write(_show_mermaid(G, self.args.markdown))
ui.write(_show_mermaid(graph, self.args.markdown))
else:
with ui.pager():
ui.write(_show_ascii(G))
ui.write(_show_ascii(graph))
return 0
@@ -204,7 +201,9 @@ def add_parser(subparsers, parent_parser):
dag_parser.add_argument(
"target",
nargs="?",
help="Stage name or output to show pipeline for. "
"Finds all stages in the workspace by default.",
help=(
"Stage name or output to show pipeline for. "
"Finds all stages in the workspace by default."
),
)
dag_parser.set_defaults(func=CmdDAG)
+24 -21
View File
@@ -8,11 +8,7 @@ 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, hide_subparsers_from_help
from dvc.ui import ui
from dvc.utils import colorize
@@ -25,12 +21,14 @@ logger = logging.getLogger(__name__)
class CmdDataStatus(CmdBase):
COLORS = {
"not_in_remote": "red",
"not_in_cache": "red",
"committed": "green",
"uncommitted": "yellow",
"untracked": "cyan",
}
LABELS = {
"not_in_remote": "Not in remote",
"not_in_cache": "Not in cache",
"committed": "DVC committed changes",
"uncommitted": "DVC uncommitted changes",
@@ -38,21 +36,21 @@ class CmdDataStatus(CmdBase):
"unchanged": "DVC unchanged files",
}
HINTS = {
"not_in_remote": ('use "dvc push <file>..." to upload files',),
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
"committed": (
"git commit the corresponding dvc files to update the repo",
),
"committed": ("git commit the corresponding dvc files to update the repo",),
"uncommitted": (
'use "dvc commit <file>..." to track changes',
'use "dvc checkout <file>..." to discard changes',
),
"untracked": (
'use "git add <file> ..." or '
'"dvc add <file>..." to commit to git or to dvc',
(
'use "git add <file> ..." or '
'"dvc add <file>..." to commit to git or to dvc'
),
),
"git_dirty": (
"there are {}changes not tracked by dvc, "
'use "git status" to see',
'there are {}changes not tracked by dvc, use "git status" to see',
),
}
@@ -72,7 +70,7 @@ class CmdDataStatus(CmdBase):
yield stage, items
@classmethod
def _show_status(cls, status: "DataStatus") -> int:
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
git_info = status.pop("git") # type: ignore[misc]
result = dict(cls._process_status(status))
if not result:
@@ -96,8 +94,7 @@ class CmdDataStatus(CmdBase):
if isinstance(stage_status, dict):
items = [
": ".join([state, file])
for file, state in stage_status.items()
": ".join([state, file]) for file, state in stage_status.items()
]
else:
items = stage_status
@@ -114,10 +111,13 @@ class CmdDataStatus(CmdBase):
return 0
def run(self) -> int:
with log_durations(logger.trace, "in data_status"): # type: ignore
with log_durations(
logger.trace, "in data_status" # type: ignore[attr-defined]
):
status = self.repo.data_status(
granular=self.args.granular,
untracked_files=self.args.untracked_files,
remote_refresh=self.args.remote_refresh,
)
if not self.args.unchanged:
@@ -178,9 +178,7 @@ class CmdDataLs(CmdBase):
filter_labels = set(self.args.labels)
filter_types = set(self.args.type)
d = ls(
self.repo, targets=self.args.targets, recursive=self.args.recursive
)
d = ls(self.repo, targets=self.args.targets, recursive=self.args.recursive)
self._show_table(
d,
filter_labels=filter_labels,
@@ -203,8 +201,7 @@ def add_parser(subparsers, parent_parser):
fix_subparsers(data_subparsers)
DATA_STATUS_HELP = (
"Show changes between the last git commit, "
"the dvcfiles and the workspace."
"Show changes between the last git commit, the dvcfiles and the workspace."
)
data_status_parser = data_subparsers.add_parser(
"status",
@@ -246,6 +243,12 @@ def add_parser(subparsers, parent_parser):
nargs="?",
help="Show untracked files.",
)
data_status_parser.add_argument(
"--remote-refresh",
action="store_true",
default=False,
help="Refresh remote index.",
)
data_status_parser.set_defaults(func=CmdDataStatus)
DATA_LS_HELP = "List data tracked by DVC with its metadata."
+22 -14
View File
@@ -40,6 +40,7 @@ class CmdDataPull(CmdDataBase):
recursive=self.args.recursive,
run_cache=self.args.run_cache,
glob=self.args.glob,
allow_missing=self.args.allow_missing,
)
self.log_summary(stats)
except (CheckoutError, DvcException) as exc:
@@ -122,7 +123,7 @@ def shared_parent_parser():
"Limit command scope to these tracked files/directories, "
".dvc files and stage names."
),
).complete = completion.DVC_FILE
).complete = completion.DVC_FILE # type: ignore[attr-defined]
return parent_parser
@@ -197,6 +198,12 @@ def add_parser(subparsers, _parent_parser):
default=False,
help=argparse.SUPPRESS,
)
pull_parser.add_argument(
"--allow-missing",
action="store_true",
default=False,
help="Ignore errors if some of the files or directories are missing.",
)
pull_parser.set_defaults(func=CmdDataPull)
# Push
@@ -262,9 +269,7 @@ def add_parser(subparsers, _parent_parser):
push_parser.set_defaults(func=CmdDataPush)
# Fetch
FETCH_HELP = (
"Download files or directories from remote storage to the cache."
)
FETCH_HELP = "Download files or directories from remote storage to the cache."
fetch_parser = subparsers.add_parser(
"fetch",
@@ -302,7 +307,7 @@ def add_parser(subparsers, _parent_parser):
"--with-deps",
action="store_true",
default=False,
help="Fetch cache for all dependencies of the " "specified target.",
help="Fetch cache for all dependencies of the specified target.",
)
fetch_parser.add_argument(
"-R",
@@ -320,9 +325,7 @@ def add_parser(subparsers, _parent_parser):
fetch_parser.set_defaults(func=CmdDataFetch)
# Status
STATUS_HELP = (
"Show changed stages, compare local cache and a remote storage."
)
STATUS_HELP = "Show changed stages, compare local cache and a remote storage."
status_parser = subparsers.add_parser(
"status",
@@ -360,24 +363,29 @@ def add_parser(subparsers, _parent_parser):
"--all-branches",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all branches.",
help=(
"Show status of a local cache compared to a remote repository "
"for all branches."
),
)
status_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all tags.",
help=(
"Show status of a local cache compared to a remote repository for all tags."
),
)
status_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all commits.",
help=(
"Show status of a local cache compared to a remote repository "
"for all commits."
),
)
status_parser.add_argument(
"-d",
+10 -15
View File
@@ -109,12 +109,10 @@ class CmdDiff(CmdBase):
ui.write()
if not sum(summary.values()):
return None
return
states_summary = ", ".join(
f"{summary[state]} {state}"
for state in states
if summary[state] > 0
f"{summary[state]} {state}" for state in states if summary[state] > 0
)
ui.write("files summary:", states_summary)
@@ -122,9 +120,7 @@ class CmdDiff(CmdBase):
from dvc.exceptions import DvcException
try:
diff = self.repo.diff(
self.args.a_rev, self.args.b_rev, self.args.targets
)
diff = self.repo.diff(self.args.a_rev, self.args.b_rev, self.args.targets)
show_hash = self.args.show_hash
hide_missing = self.args.b_rev or self.args.hide_missing
if hide_missing:
@@ -133,9 +129,11 @@ class CmdDiff(CmdBase):
for key, entries in diff.items():
entries = sorted(
entries,
key=lambda entry: entry["path"]["old"]
if isinstance(entry["path"], dict)
else entry["path"],
key=lambda entry: (
entry["path"]["old"]
if isinstance(entry["path"], dict)
else entry["path"]
),
)
if not show_hash:
for entry in entries:
@@ -170,10 +168,7 @@ def add_parser(subparsers, parent_parser):
diff_parser.add_argument(
"--targets",
nargs="*",
help=(
"Specific DVC-tracked files to compare. "
"Accepts one or more file paths."
),
help="Specific DVC-tracked files to compare. Accepts one or more file paths.",
metavar="<paths>",
).complete = completion.FILE
diff_parser.add_argument(
@@ -184,7 +179,7 @@ def add_parser(subparsers, parent_parser):
)
diff_parser.add_argument(
"b_rev",
help=("New Git commit to compare (defaults to the current workspace)"),
help="New Git commit to compare (defaults to the current workspace)",
nargs="?",
)
diff_parser.add_argument(
+4 -7
View File
@@ -1,13 +1,10 @@
import argparse
from dvc.cli.utils import (
append_doc_link,
fix_subparsers,
hide_subparsers_from_help,
)
from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_help
from dvc.commands.experiments import (
apply,
branch,
clean,
diff,
exec_run,
gc,
@@ -25,6 +22,7 @@ from dvc.commands.experiments import (
SUB_COMMANDS = [
apply,
branch,
clean,
diff,
exec_run,
gc,
@@ -54,8 +52,7 @@ def add_parser(subparsers, parent_parser):
experiments_subparsers = experiments_parser.add_subparsers(
dest="cmd",
help="Use `dvc experiments CMD --help` to display "
"command-specific help.",
help="Use `dvc experiments CMD --help` to display command-specific help.",
)
fix_subparsers(experiments_subparsers)
+10 -8
View File
@@ -4,25 +4,27 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
class CmdExperimentsApply(CmdBase):
def run(self):
self.repo.experiments.apply(
self.args.experiment, force=self.args.force
)
if not self.args.force:
ui.write(
"The --no-force option is deprecated and will be removed in a future"
" DVC release. To revert the result of 'exp apply', run:\n"
"\n\tgit reset --hard\n"
"\tgit stash apply refs/exps/apply/stash\n"
)
self.repo.experiments.apply(self.args.experiment)
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_APPLY_HELP = (
"Apply the changes from an experiment to your workspace."
)
EXPERIMENTS_APPLY_HELP = "Apply the changes from an experiment to your workspace."
experiments_apply_parser = experiments_subparsers.add_parser(
"apply",
parents=[parent_parser],
+1 -5
View File
@@ -9,14 +9,12 @@ logger = logging.getLogger(__name__)
class CmdExperimentsBranch(CmdBase):
def run(self):
self.repo.experiments.branch(self.args.experiment, self.args.branch)
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_BRANCH_HELP = "Promote an experiment to a Git branch."
experiments_branch_parser = experiments_subparsers.add_parser(
"branch",
@@ -28,7 +26,5 @@ 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", help="Name for the new Git branch")
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
+25
View File
@@ -0,0 +1,25 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
logger = logging.getLogger(__name__)
class CmdExperimentsClean(CmdBase):
def run(self):
self.repo.experiments.clean()
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_CLEAN_HELP = "Cleanup dvc exp internal tempfiles."
experiments_clean_parser = experiments_subparsers.add_parser(
"clean",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_CLEAN_HELP, "exp/clean"),
help=EXPERIMENTS_CLEAN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_clean_parser.set_defaults(func=CmdExperimentsClean)
+1 -5
View File
@@ -13,7 +13,6 @@ logger = logging.getLogger(__name__)
class CmdExperimentsDiff(CmdBase):
def run(self):
try:
diff = self.repo.experiments.diff(
a_rev=self.args.a_rev,
@@ -53,10 +52,7 @@ class CmdExperimentsDiff(CmdBase):
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_DIFF_HELP = (
"Show changes between experiments in the DVC repository."
)
EXPERIMENTS_DIFF_HELP = "Show changes between experiments in the DVC repository."
experiments_diff_parser = experiments_subparsers.add_parser(
"diff",
+1 -4
View File
@@ -9,10 +9,7 @@ class CmdExecutorRun(CmdBaseNoRepo):
"""Run an experiment executor."""
def run(self):
from dvc.repo.experiments.executor.base import (
BaseExecutor,
ExecutorInfo,
)
from dvc.repo.experiments.executor.base import BaseExecutor, ExecutorInfo
from dvc.utils.serialize import load_json
info = ExecutorInfo.from_dict(load_json(self.args.infofile))
+2 -7
View File
@@ -41,10 +41,7 @@ class CmdExperimentsGC(CmdBase):
msg += " and all git tags"
if self.args.commit_date:
msg += (
" and all git commits before date "
f"{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:
@@ -79,11 +76,9 @@ class CmdExperimentsGC(CmdBase):
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
EXPERIMENTS_GC_DESCRIPTION = (
"Removes all experiments which are not derived from the specified"
"Git revisions."
"Removes all experiments which are not derived from the specifiedGit revisions."
)
experiments_gc_parser = experiments_subparsers.add_parser(
"gc",
+27 -18
View File
@@ -92,9 +92,7 @@ class CmdExperimentsInit(CmdBase):
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 output directories: {out_dirs_paths}", styled=True)
ui.write(
f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
@@ -120,7 +118,6 @@ class CmdExperimentsInit(CmdBase):
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",
@@ -167,35 +164,47 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_init_parser.add_argument(
"--code",
help="Path to the source file or directory "
"which your experiments depend"
f" (default: {CmdExperimentsInit.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})",
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})",
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})",
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})",
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})",
help=(
"Path to the plots file or directory for your experiments"
f" (default: {CmdExperimentsInit.PLOTS})"
),
)
experiments_init_parser.add_argument(
"--live",
+3 -9
View File
@@ -11,9 +11,7 @@ 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]
):
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."
@@ -74,10 +72,7 @@ def add_parser(experiments_subparsers, parent_parser):
"--no-cache",
action="store_false",
dest="pull_cache",
help=(
"Do not pull cached outputs for this experiment from DVC remote "
"storage."
),
help="Do not pull cached outputs for this experiment from DVC remote storage.",
)
experiments_pull_parser.add_argument(
"-r",
@@ -92,8 +87,7 @@ def add_parser(experiments_subparsers, parent_parser):
type=int,
metavar="<number>",
help=(
"Number of jobs to run simultaneously when pulling from DVC "
"remote storage."
"Number of jobs to run simultaneously when pulling from DVC remote storage."
),
)
experiments_pull_parser.add_argument(
+7 -14
View File
@@ -12,16 +12,13 @@ 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]
):
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()
pushed_exps = self.repo.experiments.push(
@@ -47,8 +44,10 @@ class CmdExperimentsPush(CmdBase):
if not self.args.push_cache:
ui.write(
"To push cached outputs",
"for this experiment to DVC remote storage,"
"re-run this command without '--no-cache'.",
(
"for this experiment to DVC remote storage,"
"re-run this command without '--no-cache'."
),
)
return 0
@@ -76,10 +75,7 @@ def add_parser(experiments_subparsers, parent_parser):
"--no-cache",
action="store_false",
dest="push_cache",
help=(
"Do not push cached outputs for this experiment to DVC remote "
"storage."
),
help="Do not push cached outputs for this experiment to DVC remote storage.",
)
experiments_push_parser.add_argument(
"-r",
@@ -93,10 +89,7 @@ def add_parser(experiments_subparsers, parent_parser):
"--jobs",
type=int,
metavar="<number>",
help=(
"Number of jobs to run simultaneously when pushing to DVC remote "
"storage."
),
help="Number of jobs to run simultaneously when pushing to DVC remote storage.",
)
experiments_push_parser.add_argument(
"--run-cache",
+8 -1
View File
@@ -9,7 +9,9 @@ class CmdQueueWorker(CmdBase):
"""Run the exp queue worker."""
def run(self):
self.repo.experiments.celery_queue.worker.start(self.args.name)
self.repo.experiments.celery_queue.worker.start(
self.args.name, fsapp_clean=self.args.clean
)
return 0
@@ -22,4 +24,9 @@ def add_parser(experiments_subparsers, parent_parser):
add_help=False,
)
parser.add_argument("name", help="Celery worker name.")
parser.add_argument(
"--clean",
action="store_true",
help="Automatically cleanup celery broker on shutdown.",
)
parser.set_defaults(func=CmdQueueWorker)
-1
View File
@@ -24,7 +24,6 @@ class CmdExperimentsRemove(CmdBase):
)
def run(self):
self.check_arguments()
removed_list = self.repo.experiments.remove(
+2 -6
View File
@@ -17,13 +17,10 @@ class CmdExperimentsRun(CmdRepro):
if self.args.checkpoint_resume:
if self.args.reset:
raise InvalidArgumentError(
"--reset and --rev are mutually exclusive."
)
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."
"--rev can only be used in conjunction with --queue or --temp."
)
if self.args.reset:
@@ -51,7 +48,6 @@ class CmdExperimentsRun(CmdRepro):
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_RUN_HELP = "Run or resume an experiment."
experiments_run_parser = experiments_subparsers.add_parser(
"run",
-1
View File
@@ -11,7 +11,6 @@ logger = logging.getLogger(__name__)
class CmdExperimentsSave(CmdBase):
def run(self):
try:
ref = self.repo.experiments.save(
name=self.args.name,
+32 -34
View File
@@ -4,7 +4,7 @@ import os
import re
from collections import Counter, OrderedDict, defaultdict
from datetime import date, datetime
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Set
from funcy import lmap
@@ -37,9 +37,9 @@ def _update_names(names, items):
def _collect_names(all_experiments, **kwargs):
metric_names = defaultdict(dict)
param_names = defaultdict(dict)
deps_names = set()
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():
@@ -62,7 +62,7 @@ experiment_types = {
}
def _collect_rows(
def _collect_rows( # noqa: C901, PLR0912, PLR0913, PLR0915
base_rev,
experiments,
all_headers,
@@ -77,16 +77,14 @@ def _collect_rows(
fill_value=FILL_VALUE,
iso=False,
):
from scmrepo.git import Git
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
)
experiments = _sort_exp(experiments, sort_path, sort_name, sort_type, reverse)
new_checkpoint = True
for i, (rev, results) in enumerate(experiments.items()):
@@ -116,9 +114,7 @@ def _collect_rows(
typ = "baseline"
elif tip:
if tip == parent_tip:
typ = (
"checkpoint_tip" if new_checkpoint else "checkpoint_commit"
)
typ = "checkpoint_tip" if new_checkpoint else "checkpoint_commit"
elif parent_rev == base_rev:
typ = "checkpoint_base"
else:
@@ -135,9 +131,7 @@ def _collect_rows(
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["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)
@@ -250,7 +244,7 @@ def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
row[f"{fname}:{name}"] = value
def experiments_table(
def experiments_table( # noqa: PLR0913
all_experiments,
headers,
metric_headers,
@@ -314,7 +308,7 @@ def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments(
def show_experiments( # noqa: C901, PLR0912
all_experiments,
keep=None,
drop=None,
@@ -328,7 +322,7 @@ def show_experiments(
metric_names, param_names, deps_names = _collect_names(all_experiments)
headers = [
headers_list = [
"Experiment",
"rev",
"typ",
@@ -340,13 +334,13 @@ def show_experiments(
names = {**metric_names, **param_names}
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
counter.update(headers)
counter.update(headers_list)
metric_headers = _normalize_headers(metric_names, counter)
param_headers = _normalize_headers(param_names, counter)
td = experiments_table(
all_experiments,
headers,
headers_list,
metric_headers,
metric_names,
param_headers,
@@ -359,7 +353,7 @@ def show_experiments(
kwargs.get("iso"),
)
if keep:
for col in td.keys():
for col in td.keys(): # noqa: SIM118
if re.match(keep, col):
td.protect(col)
@@ -371,9 +365,7 @@ def show_experiments(
if not csv:
merge_headers = ["Experiment", "rev", "typ", "parent"]
td.column("Experiment")[:] = map(
prepare_exp_id, td.as_dict(merge_headers)
)
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
td.drop(*merge_headers[1:])
headers = {
@@ -410,13 +402,13 @@ def show_experiments(
cols_to_drop = set()
if drop is not None:
cols_to_drop = {col for col in td.keys() if re.match(drop, col)}
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"}
subset = {x for x in td.keys() if x != "Experiment"} # noqa: SIM118
td.dropna(
"rows",
how="all",
@@ -461,8 +453,12 @@ def _normalize_headers(names, count):
def _format_json(item):
from dvc.repo.experiments.show import _CachedError
if isinstance(item, (date, datetime)):
return item.isoformat()
if isinstance(item, _CachedError):
return {"type": getattr(item, "typ", "_CachedError"), "msg": str(item)}
return encode_exception(item)
@@ -480,6 +476,7 @@ class CmdExperimentsShow(CmdBase):
sha_only=self.args.sha,
param_deps=self.args.param_deps,
fetch_running=self.args.fetch_running,
force=self.args.force,
)
except DvcException:
logger.exception("failed to show experiments")
@@ -489,12 +486,10 @@ class CmdExperimentsShow(CmdBase):
ui.write_json(all_experiments, default=_format_json)
else:
precision = (
self.args.precision or None
if self.args.csv
else DEFAULT_PRECISION
self.args.precision or None if self.args.csv else DEFAULT_PRECISION
)
fill_value = "" if self.args.csv else FILL_VALUE
iso = True if self.args.csv else False
iso = self.args.csv
show_experiments(
all_experiments,
@@ -580,10 +575,7 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_show_parser.add_argument(
"--sort-order",
help=(
"Sort order to use with --sort-by."
" Defaults to ascending ('asc')."
),
help="Sort order to use with --sort-by. Defaults to ascending ('asc').",
choices=("asc", "desc"),
default="asc",
)
@@ -663,4 +655,10 @@ def add_parser(experiments_subparsers, parent_parser):
action="store_false",
help=argparse.SUPPRESS,
)
experiments_show_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force re-collection of experiments instead of loading from exp cache.",
)
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
+8 -11
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class CmdGC(CmdBase):
def run(self):
def run(self): # noqa: C901
from dvc.repo.gc import _validate_args
_validate_args(
@@ -40,14 +40,9 @@ class CmdGC(CmdBase):
elif self.args.all_tags:
msg += " and all git tags"
if self.args.commit_date:
msg += (
" and all git commits before date "
f"{self.args.commit_date}"
)
msg += f" and all git commits before date {self.args.commit_date}"
if self.args.rev:
msg += (
f" and last {self.args.num} commits from {self.args.rev}"
)
msg += f" and last {self.args.num} commits from {self.args.rev}"
if self.args.all_experiments:
msg += " and all experiments"
@@ -198,9 +193,11 @@ def add_parser(subparsers, parent_parser):
dest="repos",
type=str,
nargs="*",
help="Keep data files required by these projects "
"in addition to the current one. "
"Useful if you share a single cache across repos.",
help=(
"Keep data files required by these projects "
"in addition to the current one. "
"Useful if you share a single cache across repos."
),
metavar="<paths>",
)
gc_parser.set_defaults(func=CmdGC)
+9 -12
View File
@@ -3,10 +3,8 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.scm import CloneError
from ..cli.utils import append_doc_link
logger = logging.getLogger(__name__)
@@ -17,9 +15,7 @@ class CmdGet(CmdBaseNoRepo):
from dvc.ui import ui
try:
url = get_url(
self.args.path, repo=self.args.url, rev=self.args.rev
)
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")
@@ -35,6 +31,7 @@ class CmdGet(CmdBaseNoRepo):
def _get_file_from_repo(self):
from dvc.repo import Repo
from dvc.scm import CloneError
try:
Repo.get(
@@ -46,13 +43,11 @@ class CmdGet(CmdBaseNoRepo):
)
return 0
except CloneError:
logger.exception(f"failed to get '{self.args.path}'")
logger.exception("failed to get '%s'", self.args.path)
return 1
except DvcException:
logger.exception(
"failed to get '{}' from '{}'".format(
self.args.path, self.args.url
)
"failed to get '%s' from '%s'", self.args.path, self.args.url
)
return 1
@@ -88,8 +83,10 @@ def add_parser(subparsers, parent_parser):
get_parser.add_argument(
"--show-url",
action="store_true",
help="Print the storage location (URL) the target data would be "
"downloaded from, and exit.",
help=(
"Print the storage location (URL) the target data would be "
"downloaded from, and exit."
),
)
get_parser.add_argument(
"-j",
+1 -2
View File
@@ -3,10 +3,9 @@ import logging
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from ..cli.utils import append_doc_link
logger = logging.getLogger(__name__)
+4 -4
View File
@@ -64,15 +64,15 @@ class CmdPrePush(CmdHookBase):
class CmdMergeDriver(CmdHookBase):
def _run(self):
from dvc.dvcfile import Dvcfile
from dvc.dvcfile import load_file
from dvc.repo import Repo
dvc = Repo()
try:
ancestor = Dvcfile(dvc, self.args.ancestor, verify=False)
our = Dvcfile(dvc, self.args.our, verify=False)
their = Dvcfile(dvc, self.args.their, verify=False)
ancestor = load_file(dvc, self.args.ancestor, verify=False)
our = load_file(dvc, self.args.our, verify=False)
their = load_file(dvc, self.args.their, verify=False)
our.merge(ancestor, their, allowed=["add", "remove", "change"])
+10 -7
View File
@@ -5,13 +5,14 @@ from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.scm import CloneError
logger = logging.getLogger(__name__)
class CmdImport(CmdBase):
def run(self):
from dvc.scm import CloneError
try:
self.repo.imp(
self.args.url,
@@ -28,13 +29,13 @@ class CmdImport(CmdBase):
jobs=self.args.jobs,
)
except CloneError:
logger.exception(f"failed to import '{self.args.path}'")
logger.exception("failed to import '%s'", self.args.path)
return 1
except DvcException:
logger.exception(
"failed to import '{}' from '{}'.".format(
self.args.path, self.args.url
)
"failed to import '%s' from '%s'.",
self.args.path,
self.args.url,
)
return 1
return 0
@@ -90,8 +91,10 @@ def add_parser(subparsers, parent_parser):
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
help=(
"Create .dvc file including target data hash value(s)"
" but do not actually download the file(s)."
),
)
import_parser.add_argument(
"-j",
+23 -23
View File
@@ -29,10 +29,11 @@ class CmdImportUrl(CmdBase):
)
except DvcException:
logger.exception(
"failed to import {}. You could also try downloading "
"it manually, and adding it with `dvc add`.".format(
self.args.url
)
(
"failed to import %s. You could also try downloading "
"it manually, and adding it with `dvc add`."
),
self.args.url,
)
return 1
return 0
@@ -41,9 +42,7 @@ 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_HELP = "Download or copy file from URL and take it under DVC control."
import_parser = subparsers.add_parser(
"import-url",
@@ -54,16 +53,18 @@ def add_parser(subparsers, parent_parser):
)
import_parser.add_argument(
"url",
help="Location of the data to download. Supported URLs:\n"
"/absolute/path/to/file/or/dir\n"
"relative/path/to/file/or/dir\n"
"C:\\\\path\\to\\file\\or\\dir\n"
"https://example.com/path/to/file\n"
"s3://bucket/key/path\n"
"gs://bucket/path/to/file/or/dir\n"
"hdfs://example.com/path/to/file\n"
"ssh://example.com/absolute/path/to/file/or/dir\n"
"remote://remote_name/path/to/file/or/dir (see `dvc remote`)",
help=(
"Location of the data to download. Supported URLs:\n"
"/absolute/path/to/file/or/dir\n"
"relative/path/to/file/or/dir\n"
"C:\\\\path\\to\\file\\or\\dir\n"
"https://example.com/path/to/file\n"
"s3://bucket/key/path\n"
"gs://bucket/path/to/file/or/dir\n"
"hdfs://example.com/path/to/file\n"
"ssh://example.com/absolute/path/to/file/or/dir\n"
"remote://remote_name/path/to/file/or/dir (see `dvc remote`)"
),
)
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to."
@@ -96,8 +97,10 @@ def add_parser(subparsers, parent_parser):
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
help=(
"Create .dvc file including target data hash value(s)"
" but do not actually download the file(s)."
),
)
import_parser.add_argument(
"-j",
@@ -113,10 +116,7 @@ def add_parser(subparsers, parent_parser):
"--version-aware",
action="store_true",
default=False,
help=(
"Import using cloud versioning. Implied if the URL contains a "
"version ID."
),
help="Import using cloud versioning. Implied if the URL contains a version ID.",
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImportUrl)
+4 -4
View File
@@ -75,8 +75,9 @@ def add_parser(subparsers, parent_parser):
"--no-scm",
action="store_true",
default=False,
help="Initiate DVC in directory that is "
"not tracked by any SCM tool (e.g. Git).",
help=(
"Initiate DVC in directory that is not tracked by any SCM tool (e.g. Git)."
),
)
init_parser.add_argument(
"-f",
@@ -84,8 +85,7 @@ def add_parser(subparsers, parent_parser):
action="store_true",
default=False,
help=(
"Overwrite existing '.dvc/' directory. "
"This operation removes local cache."
"Overwrite existing '.dvc/' directory. This operation removes local cache."
),
)
init_parser.add_argument(
+4 -2
View File
@@ -31,7 +31,9 @@ def add_parser(subparsers, parent_parser):
"--use-pre-commit-tool",
action="store_true",
default=False,
help="Install DVC hooks using pre-commit "
"(https://pre-commit.com) if it is installed.",
help=(
"Install DVC hooks using pre-commit "
"(https://pre-commit.com) if it is installed."
),
)
install_parser.set_defaults(func=CmdInstall)
+1 -1
View File
@@ -20,7 +20,7 @@ class LsColors:
else:
self._codes[code] = color
def format(self, entry):
def format(self, entry): # noqa: A003
text = entry["path"]
if entry.get("isout", False) and "out" in self._codes:
+14 -32
View File
@@ -1,4 +1,5 @@
import argparse
from typing import Dict, List
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
@@ -6,7 +7,6 @@ from dvc.commands.config import CmdConfig
from dvc.compare import TabularData
from dvc.config import ConfigError
from dvc.exceptions import DvcException
from dvc.types import Dict, List
from dvc.ui import ui
from dvc.utils import format_link
@@ -18,7 +18,6 @@ class MachineDisabledError(ConfigError):
class CmdMachineConfig(CmdConfig):
def __init__(self, args):
super().__init__(args)
if not self.config["feature"].get("machine", False):
raise MachineDisabledError
@@ -163,20 +162,18 @@ class CmdMachineRename(CmdBase):
all_config = self.config.load_config_to_level(None)
if self.args.new in all_config.get("machine", {}):
raise ConfigError(
"Rename failed. Machine '{}' already exists.".format(
self.args.new
)
f"Rename failed. Machine '{self.args.new}' already exists."
)
ui.write(f"Rename machine '{self.args.name}' to '{self.args.new}'.")
def run(self):
self._check_before_rename()
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
conf["machine"][self.args.new] = conf["machine"][self.args.name]
try:
assert self.repo.machine
self.repo.machine.rename(self.args.name, self.args.new)
except DvcException as error:
del conf["machine"][self.args.new]
@@ -199,7 +196,7 @@ class CmdMachineDefault(CmdMachineConfig):
if self.args.name is None and not self.args.unset:
conf = self.config.read(self.args.level)
try:
print(conf["core"]["machine"])
ui.write(conf["core"]["machine"])
except KeyError:
ui.write("No default machine set")
return 1
@@ -208,9 +205,7 @@ class CmdMachineDefault(CmdMachineConfig):
if self.args.unset:
conf["core"].pop("machine", None)
else:
merged_conf = self.config.load_config_to_level(
self.args.level
)
merged_conf = self.config.load_config_to_level(self.args.level)
if (
self.args.name in conf["machine"]
or self.args.name in merged_conf["machine"]
@@ -218,8 +213,7 @@ class CmdMachineDefault(CmdMachineConfig):
conf["core"]["machine"] = self.args.name
else:
raise ConfigError(
"default machine must be present in machine "
"list."
"default machine must be present in machine list."
)
return 0
@@ -250,7 +244,6 @@ class CmdMachineStatus(CmdBase):
all_status: List[Dict],
td: TabularData,
):
if not all_status:
row = [
name,
@@ -309,7 +302,7 @@ class CmdMachineSsh(CmdBase):
return 0
def add_parser(subparsers, parent_parser):
def add_parser(subparsers, parent_parser): # noqa: PLR0915
from dvc.commands.config import parent_config_parser
machine_HELP = "Set up and manage cloud machines."
@@ -324,7 +317,7 @@ def add_parser(subparsers, parent_parser):
machine_subparsers = machine_parser.add_subparsers(
dest="cmd",
help="Use `dvc machine CMD --help` for " "command-specific help.",
help="Use `dvc machine CMD --help` for command-specific help.",
)
fix_subparsers(machine_subparsers)
@@ -342,8 +335,7 @@ def add_parser(subparsers, parent_parser):
"cloud",
help="Machine cloud. See full list of supported clouds at {}".format(
format_link(
"https://github.com/iterative/"
"terraform-provider-iterative#machine"
"https://github.com/iterative/terraform-provider-iterative#machine"
)
),
)
@@ -371,9 +363,7 @@ def add_parser(subparsers, parent_parser):
help=machine_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_default_parser.add_argument(
"name", nargs="?", help="Name of the machine"
)
machine_default_parser.add_argument("name", nargs="?", help="Name of the machine")
machine_default_parser.add_argument(
"-u",
"--unset",
@@ -413,9 +403,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_modify_parser.add_argument("name", help="Name of the machine")
machine_modify_parser.add_argument(
"option", help="Name of the option to modify."
)
machine_modify_parser.add_argument("option", help="Name of the option to modify.")
machine_modify_parser.add_argument(
"value", nargs="?", help="(optional) Value of the option."
)
@@ -448,9 +436,7 @@ def add_parser(subparsers, parent_parser):
help=machine_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_remove_parser.add_argument(
"name", help="Name of the machine to remove."
)
machine_remove_parser.add_argument("name", help="Name of the machine to remove.")
machine_remove_parser.set_defaults(func=CmdMachineRemove)
machine_CREATE_HELP = "Create and start a machine instance."
@@ -461,14 +447,10 @@ def add_parser(subparsers, parent_parser):
help=machine_CREATE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_create_parser.add_argument(
"name", help="Name of the machine to create."
)
machine_create_parser.add_argument("name", help="Name of the machine to create.")
machine_create_parser.set_defaults(func=CmdMachineCreate)
machine_STATUS_HELP = (
"List the status of running instances for one/all machines."
)
machine_STATUS_HELP = "List the status of running instances for one/all machines."
machine_status_parser = machine_subparsers.add_parser(
"status",
parents=[parent_parser],
+2 -6
View File
@@ -14,9 +14,7 @@ class CmdMove(CmdBase):
try:
self.repo.move(self.args.src, self.args.dst)
except DvcException:
msg = "failed to move '{}' -> '{}'".format(
self.args.src, self.args.dst
)
msg = f"failed to move '{self.args.src}' -> '{self.args.dst}'"
logger.exception(msg)
return 1
return 0
@@ -40,7 +38,5 @@ def add_parser(subparsers, parent_parser):
move_parser.add_argument(
"src", help="Source path to a data file or directory."
).complete = completion.FILE
move_parser.add_argument(
"dst", help="Destination path."
).complete = completion.FILE
move_parser.add_argument("dst", help="Destination path.").complete = completion.FILE
move_parser.set_defaults(func=CmdMove)
+1 -1
View File
@@ -79,7 +79,7 @@ def add_parser(subparsers, parent_parser):
params_diff_parser.add_argument(
"b_rev",
nargs="?",
help=("New Git commit to compare (defaults to the current workspace)"),
help="New Git commit to compare (defaults to the current workspace)",
)
params_diff_parser.add_argument(
"--targets",
+18 -50
View File
@@ -18,9 +18,7 @@ logger = logging.getLogger(__name__)
def _show_json(renderers, split=False):
from dvc.render.convert import to_json
result = {
renderer.name: to_json(renderer, split) for renderer in renderers
}
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
ui.write_json(result)
@@ -78,12 +76,6 @@ class CmdPlots(CmdBase):
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
return {k: v for k, v in props.items() if v is not None}
def _config_files(self):
config_files = None
if self.args.from_config:
config_files = {self.args.from_config}
return config_files
def _html_template_path(self):
html_template_path = self.args.html_template
if not html_template_path:
@@ -91,12 +83,11 @@ class CmdPlots(CmdBase):
"html_template", None
)
if html_template_path and not os.path.isabs(html_template_path):
html_template_path = os.path.join(
self.repo.dvc_dir, html_template_path
)
return 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
def run(self):
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
from pathlib import Path
from dvc.render.match import match_defs_renderers
@@ -107,14 +98,11 @@ class CmdPlots(CmdBase):
logger.error("please specify a target for `--show-vega`")
return 1
if len(self.args.targets) > 1:
logger.error(
"you can only specify one target for `--show-vega`"
)
logger.error("you can only specify one target for `--show-vega`")
return 1
if self.args.json:
logger.error(
"'--show-vega' and '--json' are mutually exclusive "
"options."
"'--show-vega' and '--json' are mutually exclusive options."
)
return 1
@@ -122,22 +110,18 @@ class CmdPlots(CmdBase):
plots_data = self._func(
targets=self.args.targets,
props=self._props(),
config_files=self._config_files(),
)
if not plots_data:
ui.error_write(
"No plots were loaded, "
"visualization file will not be created."
"No plots were loaded, visualization file will not be created."
)
out: str = self.args.out or self.repo.config.get("plots", {}).get(
"out_dir", "dvc_plots"
)
renderers_out = (
out if self.args.json else os.path.join(out, "static")
)
renderers_out = out if self.args.json else os.path.join(out, "static")
renderers = match_defs_renderers(
data=plots_data,
@@ -161,7 +145,7 @@ class CmdPlots(CmdBase):
render_html(
renderers=renderers,
output_file=output_file,
template_path=self._html_template_path(),
html_template=self._html_template_path(),
)
ui.write(output_file.as_uri())
@@ -223,9 +207,8 @@ class CmdPlotsTemplates(CmdBase):
return 0
raise InvalidArgumentError(f"Unexpected template: {target}.")
else:
for template in TEMPLATES:
ui.write(template.DEFAULT_NAME)
for template in TEMPLATES:
ui.write(template.DEFAULT_NAME)
return 0
except DvcException:
@@ -251,8 +234,7 @@ def add_parser(subparsers, parent_parser):
fix_subparsers(plots_subparsers)
SHOW_HELP = (
"Generate plots from target files or from `plots`"
" definitions in `dvc.yaml`."
"Generate plots from target files or from `plots` definitions in `dvc.yaml`."
)
plots_show_parser = plots_subparsers.add_parser(
"show",
@@ -275,8 +257,7 @@ def add_parser(subparsers, parent_parser):
plots_show_parser.set_defaults(func=CmdPlotsShow)
PLOTS_DIFF_HELP = (
"Show multiple versions of a plot by overlaying them "
"in a single image."
"Show multiple versions of a plot by overlaying them in a single image."
)
plots_diff_parser = plots_subparsers.add_parser(
"diff",
@@ -334,9 +315,7 @@ def add_parser(subparsers, parent_parser):
)
plots_modify_parser.set_defaults(func=CmdPlotsModify)
TEMPLATES_HELP = (
"List built-in plots templates or show JSON specification for one."
)
TEMPLATES_HELP = "List built-in plots templates or show JSON specification for one."
plots_templates_parser = plots_subparsers.add_parser(
"templates",
parents=[parent_parser],
@@ -362,11 +341,8 @@ def _add_props_arguments(parser):
"--template",
nargs="?",
default=None,
help=(
"Special JSON or HTML schema file to inject with the data. "
"See {}".format(
format_link("https://man.dvc.org/plots#plot-templates")
)
help="Special JSON or HTML schema file to inject with the data. See {}".format(
format_link("https://man.dvc.org/plots#plot-templates")
),
metavar="<path>",
).complete = completion.FILE
@@ -383,9 +359,7 @@ def _add_props_arguments(parser):
default=None, # Use default None to distinguish when it's not used
help="Provided CSV or TSV datafile does not have a header.",
)
parser.add_argument(
"--title", default=None, metavar="<text>", help="Plot title."
)
parser.add_argument("--title", default=None, metavar="<text>", help="Plot title.")
parser.add_argument(
"--x-label", default=None, help="X axis label", metavar="<text>"
)
@@ -433,9 +407,3 @@ def _add_ui_arguments(parser):
help="Custom HTML template for VEGA visualization.",
metavar="<path>",
)
parser.add_argument(
"--from-config",
default=None,
metavar="<path>",
help=argparse.SUPPRESS,
)
+1 -1
View File
@@ -26,7 +26,7 @@ def add_parser(subparsers, parent_parser):
queue_subparsers = queue_parser.add_subparsers(
dest="cmd",
help="Use `dvc queue CMD --help` to display " "command-specific help.",
help="Use `dvc queue CMD --help` to display command-specific help.",
)
fix_subparsers(queue_subparsers)
+1 -2
View File
@@ -20,8 +20,7 @@ class CmdQueueKill(CmdBase):
def add_parser(queue_subparsers, parent_parser):
QUEUE_KILL_HELP = (
"Gracefully interrupt running experiment queue tasks "
"(equivalent to Ctrl-C)"
"Gracefully interrupt running experiment queue tasks (equivalent to Ctrl-C)"
)
queue_kill_parser = queue_subparsers.add_parser(
"kill",
+1 -3
View File
@@ -34,9 +34,7 @@ def add_parser(queue_subparsers, parent_parser):
queue_logs_parser.add_argument(
"-e",
"--encoding",
help=(
"Text encoding for log output. Defaults to system locale encoding."
),
help="Text encoding for log output. Defaults to system locale encoding.",
metavar="<encoding>",
)
queue_logs_parser.add_argument(
-2
View File
@@ -28,7 +28,6 @@ class CmdQueueRemove(CmdBase):
)
def run(self):
self.check_arguments()
if self.args.all:
@@ -57,7 +56,6 @@ class CmdQueueRemove(CmdBase):
def add_parser(queue_subparsers, parent_parser):
QUEUE_REMOVE_HELP = "Remove queued and completed tasks from the queue."
queue_remove_parser = queue_subparsers.add_parser(
"remove",
+3 -11
View File
@@ -12,20 +12,15 @@ class CmdQueueStart(CmdBase):
"""Start exp queue workers."""
def run(self):
started = self.repo.experiments.celery_queue.start_workers(
self.args.jobs
)
started = self.repo.experiments.celery_queue.start_workers(self.args.jobs)
suffix = "s" if started > 1 else ""
ui.write(
f"Started '{started}' new experiments task queue worker{suffix}."
)
ui.write(f"Started '{started}' new experiments task queue worker{suffix}.")
return 0
def add_parser(queue_subparsers, parent_parser):
QUEUE_START_HELP = "Start the experiments task queue worker."
queue_start_parser = queue_subparsers.add_parser(
"start",
@@ -39,10 +34,7 @@ def add_parser(queue_subparsers, parent_parser):
"--jobs",
type=int,
default=1,
help=(
"Maximum number of concurrent queue workers to start. "
"Defaults to 1."
),
help="Maximum number of concurrent queue workers to start. Defaults to 1.",
metavar="<number>",
)
queue_start_parser.set_defaults(func=CmdQueueStart)
+9 -12
View File
@@ -1,33 +1,34 @@
import argparse
import logging
from typing import List, Mapping, Optional
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
from ..experiments.show import format_time
logger = logging.getLogger(__name__)
class CmdQueueStatus(CmdBase):
"""Show queue task and worker status."""
def run(self):
def run(self) -> int:
result: List[
Mapping[str, Optional[str]]
Dict[str, Optional[str]]
] = self.repo.experiments.celery_queue.status()
if result:
all_headers = ["Task", "Name", "Created", "Status"]
td = TabularData(all_headers)
for exp in result:
created = format_time(exp.get("timestamp"))
assert exp["rev"]
assert exp["status"]
td.append(
[
exp["rev"][:7],
exp.get("name", ""),
exp.get("name") or "",
created,
exp["status"],
]
@@ -38,9 +39,7 @@ class CmdQueueStatus(CmdBase):
ui.write()
worker_status = self.repo.experiments.celery_queue.worker_status()
active_count = len(
[name for name, task in worker_status.items() if task]
)
active_count = len([name for name, task in worker_status.items() if task])
idle_count = len(worker_status) - active_count
ui.write(f"Worker status: {active_count} active, {idle_count} idle")
@@ -49,9 +48,7 @@ class CmdQueueStatus(CmdBase):
def add_parser(queue_subparsers, parent_parser):
QUEUE_STATUS_HELP = (
"Show the status of experiments queue tasks and workers."
)
QUEUE_STATUS_HELP = "Show the status of experiments queue tasks and workers."
queue_status_parser = queue_subparsers.add_parser(
"status",
parents=[parent_parser],
-1
View File
@@ -26,7 +26,6 @@ class CmdQueueStop(CmdBase):
def add_parser(queue_subparsers, parent_parser):
QUEUE_STOP_HELP = "Stop all experiments task queue workers."
queue_stop_parser = queue_subparsers.add_parser(
"stop",
+5 -13
View File
@@ -95,9 +95,7 @@ class CmdRemoteDefault(CmdRemote):
if self.args.unset:
conf["core"].pop("remote", None)
else:
merged_conf = self.config.load_config_to_level(
self.args.level
)
merged_conf = self.config.load_config_to_level(self.args.level)
if (
self.args.name in conf["remote"]
or self.args.name in merged_conf["remote"]
@@ -164,7 +162,7 @@ def add_parser(subparsers, parent_parser):
remote_subparsers = remote_parser.add_subparsers(
dest="cmd",
help="Use `dvc remote CMD --help` for " "command-specific help.",
help="Use `dvc remote CMD --help` for command-specific help.",
)
fix_subparsers(remote_subparsers)
@@ -208,9 +206,7 @@ def add_parser(subparsers, parent_parser):
help=REMOTE_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_default_parser.add_argument(
"name", nargs="?", help="Name of the remote"
)
remote_default_parser.add_argument("name", nargs="?", help="Name of the remote")
remote_default_parser.add_argument(
"-u",
"--unset",
@@ -229,9 +225,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_modify_parser.add_argument("name", help="Name of the remote")
remote_modify_parser.add_argument(
"option", help="Name of the option to modify."
)
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
remote_modify_parser.add_argument(
"value", nargs="?", help="(optional) Value of the option."
)
@@ -262,9 +256,7 @@ def add_parser(subparsers, parent_parser):
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument(
"name", help="Name of the remote to remove."
)
remote_remove_parser.add_argument("name", help="Name of the remote to remove.")
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
REMOTE_RENAME_HELP = "Rename a DVC remote"
remote_rename_parser = remote_subparsers.add_parser(
+1 -2
View File
@@ -22,8 +22,7 @@ class CmdRemove(CmdBase):
def add_parser(subparsers, parent_parser):
REMOVE_HELP = (
"Remove stages from dvc.yaml and/or"
" stop tracking files or directories."
"Remove stages from dvc.yaml and/or stop tracking files or directories."
)
remove_parser = subparsers.add_parser(
"remove",
+13 -18
View File
@@ -10,9 +10,7 @@ class CmdRepro(CmdBase):
def run(self):
from dvc.ui import ui
stages = self.repo.reproduce(
**self._common_kwargs, **self._repro_kwargs
)
stages = self.repo.reproduce(**self._common_kwargs, **self._repro_kwargs)
if len(stages) == 0:
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
else:
@@ -55,16 +53,14 @@ def add_arguments(repro_parser):
repro_parser.add_argument(
"targets",
nargs="*",
help=(
"""\
help="""\
Stages to reproduce. 'dvc.yaml' by default.
The targets can be path to a dvc.yaml file or `.dvc` file,
or a stage name from dvc.yaml file from
current working directory. To run a stage from dvc.yaml
from other directories, the target must be a path followed by colon `:`
and then the stage name name.
"""
),
""",
).complete = completion.DVCFILES_AND_STAGE
repro_parser.add_argument(
"-f",
@@ -85,16 +81,14 @@ and then the stage name name.
"--single-item",
action="store_true",
default=False,
help="Reproduce only single data item without recursive dependencies "
"check.",
help="Reproduce only single data item without recursive dependencies check.",
)
repro_parser.add_argument(
"-p",
"--pipeline",
action="store_true",
default=False,
help="Reproduce the whole pipeline that the specified targets "
"belong to.",
help="Reproduce the whole pipeline that the specified targets belong to.",
)
repro_parser.add_argument(
"-P",
@@ -127,8 +121,10 @@ and then the stage name name.
"--force-downstream",
action="store_true",
default=False,
help="Reproduce all descendants of a changed stage even if their "
"direct dependencies didn't change.",
help=(
"Reproduce all descendants of a changed stage even if their "
"direct dependencies didn't change."
),
)
repro_parser.add_argument(
"--pull",
@@ -143,15 +139,14 @@ and then the stage name name.
"--dry",
action="store_true",
default=False,
help="Only print the commands that would be executed without "
"actually executing.",
help=(
"Only print the commands that would be executed without actually executing."
),
)
def add_parser(subparsers, parent_parser):
REPRO_HELP = (
"Reproduce complete or partial pipelines by executing their stages."
)
REPRO_HELP = "Reproduce complete or partial pipelines by executing their stages."
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
+3 -7
View File
@@ -39,7 +39,7 @@ class CmdRun(CmdBase):
{
"cmd": parse_cmd(self.args.command),
"fname": kwargs.pop("file"),
"no_exec": (self.args.no_exec or bool(self.args.checkpoints)),
"no_exec": self.args.no_exec or bool(self.args.checkpoints),
"run_cache": not kwargs.pop("no_run_cache"),
}
)
@@ -55,9 +55,7 @@ class CmdRun(CmdBase):
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_HELP = "Generate a dvc.yaml file from a command and execute the command."
run_parser = subparsers.add_parser(
"run",
parents=[parent_parser],
@@ -66,9 +64,7 @@ def add_parser(subparsers, parent_parser):
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("--file", metavar="<filename>", help=argparse.SUPPRESS)
run_parser.add_argument(
"--single-stage",
action="store_true",
+17 -19
View File
@@ -44,9 +44,7 @@ def generate_description(stage: "Stage") -> str:
return "; ".join(desc)
def prepare_description(
stage: "Stage", max_length: int = MAX_TEXT_LENGTH
) -> str:
def prepare_description(stage: "Stage", max_length: int = MAX_TEXT_LENGTH) -> str:
desc = stage.short_description() or generate_description(stage)
return truncate_text(desc, max_length)
@@ -57,9 +55,9 @@ def prepare_stages_data(
max_length: int = MAX_TEXT_LENGTH,
) -> Dict[str, str]:
return {
stage.addressing: prepare_description(stage, max_length=max_length)
if description
else ""
stage.addressing: (
prepare_description(stage, max_length=max_length) if description else ""
)
for stage in stages
}
@@ -67,7 +65,7 @@ def prepare_stages_data(
class CmdStageList(CmdBase):
def _get_stages(self) -> Iterable["Stage"]:
if self.args.all:
stages: List["Stage"] = self.repo.index.stages # type: ignore
stages: List["Stage"] = self.repo.index.stages
logger.trace( # type: ignore[attr-defined]
"%d no. of stages found", len(stages)
)
@@ -75,9 +73,7 @@ class CmdStageList(CmdBase):
# removing duplicates while maintaining order
collected = chain.from_iterable(
self.repo.stage.collect(
target=target, recursive=self.args.recursive
)
self.repo.stage.collect(target=target, recursive=self.args.recursive)
for target in self.args.targets
)
return dict.fromkeys(collected).keys()
@@ -95,7 +91,7 @@ class CmdStageList(CmdBase):
stages = self._get_stages()
data = prepare_stages_data(stages, description=not self.args.name_only)
ui.table(data.items())
ui.table(list(data.items()))
return 0
@@ -173,8 +169,7 @@ def _add_common_args(parser):
"--outs-no-cache",
action="append",
default=[],
help="Declare output file or directory "
"(do not put into DVC cache).",
help="Declare output file or directory (do not put into DVC cache).",
metavar="<filename>",
).complete = completion.FILE
parser.add_argument(
@@ -182,8 +177,10 @@ def _add_common_args(parser):
"--checkpoints",
action="append",
default=[],
help="Declare checkpoint output file or directory for 'dvc exp run'. "
"Not compatible with 'dvc repro'.",
help=(
"Declare checkpoint output file or directory for 'dvc exp run'. "
"Not compatible with 'dvc repro'."
),
metavar="<filename>",
).complete = completion.FILE
parser.add_argument(
@@ -196,16 +193,17 @@ def _add_common_args(parser):
"--outs-persist",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro.",
help="Declare output file or directory that will not be removed upon repro.",
metavar="<filename>",
)
parser.add_argument(
"--outs-persist-no-cache",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
help=(
"Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache)."
),
metavar="<filename>",
)
parser.add_argument(
+1 -3
View File
@@ -81,9 +81,7 @@ class CmdDataStatus(CmdDataBase):
if not self.repo.index.stages:
ui.write(self.EMPTY_PROJECT_MSG)
elif self.args.cloud or self.args.remote:
remote = self.args.remote or self.repo.config["core"].get(
"remote"
)
remote = self.args.remote or self.repo.config["core"].get("remote")
ui.write(self.IN_SYNC_MSG.format(remote=remote))
else:
ui.write(self.UP_TO_DATE_MSG)
+5 -4
View File
@@ -31,8 +31,7 @@ class CmdUpdate(CmdBase):
def add_parser(subparsers, parent_parser):
UPDATE_HELP = (
"Update data artifact imported (via dvc import or dvc import-url) "
"from an external DVC repository or URL, or update data from a "
"DVC worktree remote."
"from an external DVC repository or URL."
)
update_parser = subparsers.add_parser(
"update",
@@ -61,8 +60,10 @@ def add_parser(subparsers, parent_parser):
"--no-download",
action="store_true",
default=False,
help="Update .dvc file git revision/hash value(s)"
" but do not download the file(s).",
help=(
"Update .dvc file git revision/hash value(s)"
" but do not download the file(s)."
),
)
update_parser.add_argument(
"--to-remote",
+1 -3
View File
@@ -21,9 +21,7 @@ class CmdVersion(CmdBaseNoRepo):
def add_parser(subparsers, parent_parser):
VERSION_HELP = (
"Display the DVC version and system/environment information."
)
VERSION_HELP = "Display the DVC version and system/environment information."
version_parser = subparsers.add_parser(
"version",
parents=[parent_parser],
+34 -50
View File
@@ -58,15 +58,13 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
return self._columns[name]
def items(self) -> ItemsView[str, Column]:
projection = {k: self.column(k) for k in self.keys()}
projection = {k: self.column(k) for k in self.keys()} # noqa: SIM118
return projection.items()
def keys(self) -> List[str]:
return self._keys
def _iter_col_row(
self, row: Sequence["CellT"]
) -> Iterator[Tuple["CellT", Column]]:
def _iter_col_row(self, row: Sequence["CellT"]) -> Iterator[Tuple["CellT", Column]]:
for val, col in zip_longest(row, self.columns):
if col is None:
break
@@ -103,9 +101,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
...
@overload
def __setitem__(
self, item: slice, value: Iterable[Sequence["CellT"]]
) -> None:
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None:
...
def __setitem__(self, item, value) -> None:
@@ -113,8 +109,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
if isinstance(item, slice):
n = len(self.columns)
normalized_rows = (
chain(val, repeat(self._fill_value, n - len(val)))
for val in value
chain(val, repeat(self._fill_value, n - len(val))) for val in value
)
# we need to transpose those rows into columnar format
# as we work in terms of column-based arrays
@@ -167,7 +162,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
return buff.getvalue()
def to_parallel_coordinates(
self, output_path: "StrPath", color_by: str = None
self, output_path: "StrPath", color_by: Optional[str] = None
) -> "StrPath":
from dvc_render.html import render_html
from dvc_render.plotly import ParallelCoordinatesRenderer
@@ -195,7 +190,8 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
self.add_column(key)
row: List["CellT"] = [
with_value(d.get(key), self._fill_value) for key in self.keys()
with_value(d.get(key), self._fill_value)
for key in self.keys() # noqa: SIM118
]
self.append(row)
@@ -208,14 +204,12 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
ui.table(self, headers=self.keys(), **kwargs)
def as_dict(
self, cols: Iterable[str] = None
self, cols: Optional[Iterable[str]] = None
) -> Iterable[Dict[str, "CellT"]]:
keys = self.keys() if cols is None else set(cols)
return [
{k: self._columns[k][i] for k in keys} for i in range(len(self))
]
return [{k: self._columns[k][i] for k in keys} for i in range(len(self))]
def dropna(
def dropna( # noqa: C901, PLR0912
self,
axis: str = "rows",
how="any",
@@ -223,13 +217,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
):
if axis not in ["rows", "cols"]:
raise ValueError(
f"Invalid 'axis' value {axis}."
"Choose one of ['rows', 'cols']"
f"Invalid 'axis' value {axis}.Choose one of ['rows', 'cols']"
)
if how not in ["any", "all"]:
raise ValueError(
f"Invalid 'how' value {how}." "Choose one of ['any', 'all']"
)
raise ValueError(f"Invalid 'how' value {how}. Choose one of ['any', 'all']")
match_line: Set = set()
match_any = True
@@ -244,8 +235,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
if axis == "rows":
match_line.add(n_row)
break
else:
match_line.add(self.keys()[n_col])
match_line.add(self.keys()[n_col])
to_drop = match_line
if how == "all":
@@ -256,18 +246,14 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
to_drop -= match_line
if axis == "rows":
for name in self.keys():
for name in self.keys(): # noqa: SIM118
self._columns[name] = Column(
[
x
for n, x in enumerate(self._columns[name])
if n not in to_drop
]
[x for n, x in enumerate(self._columns[name]) if n not in to_drop]
)
else:
self.drop(*to_drop)
def drop_duplicates(
def drop_duplicates( # noqa: C901
self,
axis: str = "rows",
subset: Optional[Iterable[str]] = None,
@@ -275,8 +261,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
):
if axis not in ["rows", "cols"]:
raise ValueError(
f"Invalid 'axis' value {axis}."
"Choose one of ['rows', 'cols']"
f"Invalid 'axis' value {axis}.Choose one of ['rows', 'cols']"
)
if axis == "cols":
@@ -309,7 +294,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
else:
unique_rows.append(tuple_row)
for name in self.keys():
for name in self.keys(): # noqa: SIM118
self._columns[name] = Column(
[
x
@@ -324,12 +309,13 @@ def _normalize_float(val: float, precision: int):
def _format_field(
val: Any, precision: int = None, round_digits: bool = False
val: Any, precision: Optional[int] = None, round_digits: bool = False
) -> str:
def _format(_val):
if isinstance(_val, float) and precision:
func = round if round_digits else _normalize_float
return func(_val, precision)
if round_digits:
return round(_val, precision)
return _normalize_float(_val, precision)
if isinstance(_val, abc.Mapping):
return {k: _format(v) for k, v in _val.items()}
if isinstance(_val, list):
@@ -345,11 +331,11 @@ def diff_table(
old: bool = True,
no_path: bool = False,
show_changes: bool = True,
precision: int = None,
precision: Optional[int] = None,
round_digits: bool = False,
on_empty_diff: str = None,
a_rev: str = None,
b_rev: str = None,
on_empty_diff: Optional[str] = None,
a_rev: Optional[str] = None,
b_rev: Optional[str] = None,
) -> TabularData:
a_rev = a_rev or "HEAD"
b_rev = b_rev or "workspace"
@@ -361,9 +347,7 @@ def diff_table(
for item, change in sorted(diff_in_file.items()):
old_value = with_value(change.get("old"), fill_value)
new_value = with_value(change.get("new"), fill_value)
diff_value = with_value(
change.get("diff", on_empty_diff), fill_value
)
diff_value = with_value(change.get("diff", on_empty_diff), fill_value)
td.append(
[
fname,
@@ -387,18 +371,18 @@ def diff_table(
return td
def show_diff(
def show_diff( # noqa: PLR0913
diff,
title: str,
old: bool = True,
no_path: bool = False,
show_changes: bool = True,
precision: int = None,
precision: Optional[int] = None,
round_digits: bool = False,
on_empty_diff: str = None,
on_empty_diff: Optional[str] = None,
markdown: bool = False,
a_rev: str = None,
b_rev: str = None,
a_rev: Optional[str] = None,
b_rev: Optional[str] = None,
) -> None:
td = diff_table(
diff,
@@ -420,7 +404,7 @@ def metrics_table(
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
precision: int = None,
precision: Optional[int] = None,
round_digits: bool = False,
):
from dvc.utils.diff import format_dict
@@ -460,7 +444,7 @@ def show_metrics(
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
precision: int = None,
precision: Optional[int] = None,
round_digits: bool = False,
) -> None:
td = metrics_table(
+23 -15
View File
@@ -4,11 +4,18 @@ import os
import re
from contextlib import contextmanager
from functools import partial
from typing import TYPE_CHECKING, Dict, Optional
from funcy import cached_property, compact, memoize, re_find
from funcy import compact, memoize, re_find
from dvc.exceptions import DvcException, NotDvcRepoError
from .utils.objects import cached_property
if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc.types import DictStrAny, StrPath
logger = logging.getLogger(__name__)
@@ -84,7 +91,11 @@ class Config(dict):
CONFIG_LOCAL = "config.local"
def __init__(
self, dvc_dir=None, validate=True, fs=None, config=None
self,
dvc_dir: Optional["StrPath"] = None,
validate: bool = True,
fs: Optional["FileSystem"] = None,
config: Optional["DictStrAny"] = None,
): # pylint: disable=super-init-not-called
from dvc.fs import LocalFileSystem
@@ -116,7 +127,7 @@ class Config(dict):
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
@cached_property
def files(self):
def files(self) -> Dict[str, str]:
files = {
level: os.path.join(self.get_dir(level), self.CONFIG)
for level in ("system", "global")
@@ -139,10 +150,10 @@ class Config(dict):
dvc.config.Config: config object.
"""
config_file = os.path.join(dvc_dir, Config.CONFIG)
open(config_file, "w+", encoding="utf-8").close()
return Config(dvc_dir)
with open(config_file, "w+", encoding="utf-8"):
return Config(dvc_dir)
def load(self, validate=True, config=None):
def load(self, validate: bool = True, config: Optional["DictStrAny"] = None):
"""Loads config from all the config files.
Raises:
@@ -159,17 +170,13 @@ class Config(dict):
self.clear()
self.update(conf)
# Add resolved default cache.dir
if not self["cache"].get("dir") and self.dvc_dir:
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
def _get_fs(self, level):
# NOTE: this might be a Gitfs, which doesn't see things outside of
# the repo.
return self.fs if level == "repo" else self.wfs
def _load_config(self, level):
from configobj import ConfigObj
from configobj import ConfigObj, ConfigObjError
filename = self.files[level]
fs = self._get_fs(level)
@@ -180,6 +187,8 @@ class Config(dict):
conf_obj = ConfigObj(fobj)
except UnicodeDecodeError as exc:
raise ConfigError(str(exc)) from exc
except ConfigObjError as exc:
raise ConfigError(str(exc)) from exc
else:
conf_obj = ConfigObj()
return _parse_named(_lower_keys(conf_obj.dict()))
@@ -278,7 +287,7 @@ class Config(dict):
return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
def load_config_to_level(self, level=None):
merged_conf = {}
merged_conf: Dict = {}
for merge_level in self.LEVELS:
if merge_level == level:
break
@@ -324,7 +333,7 @@ class Config(dict):
def _parse_named(conf):
result = {"remote": {}, "machine": {}}
result: Dict[str, Dict] = {"remote": {}, "machine": {}}
for section, val in conf.items():
match = re_find(r'^\s*(remote|machine)\s*"(.*)"\s*$', section)
@@ -361,6 +370,5 @@ def merge(into, update):
def _lower_keys(data):
return {
k.lower(): _lower_keys(v) if isinstance(v, dict) else v
for k, v in data.items()
k.lower(): _lower_keys(v) if isinstance(v, dict) else v for k, v in data.items()
}
+9 -18
View File
@@ -2,16 +2,7 @@ import os
from urllib.parse import urlparse
from funcy import walk_values
from voluptuous import (
All,
Any,
Coerce,
Invalid,
Lower,
Optional,
Range,
Schema,
)
from voluptuous import All, Any, Coerce, Invalid, Lower, Optional, Range, Schema
Bool = All(
Lower,
@@ -34,14 +25,12 @@ def supported_cache_type(types):
unsupported = set(types) - {"reflink", "hardlink", "symlink", "copy"}
if unsupported:
raise Invalid(
"Unsupported cache type(s): {}".format(", ".join(unsupported))
)
raise Invalid("Unsupported cache type(s): {}".format(", ".join(unsupported)))
return types
def Choices(*choices):
def Choices(*choices): # noqa: N802
"""Checks that value belongs to the specified set of values
Args:
@@ -51,7 +40,7 @@ def Choices(*choices):
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
def ByUrl(mapping):
def ByUrl(mapping): # noqa: N802
schemas = walk_values(Schema, mapping)
def validate(data):
@@ -62,6 +51,8 @@ def ByUrl(mapping):
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
return schemas[""](data)
if parsed.netloc == "":
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
@@ -106,6 +97,7 @@ WEBDAV_COMMON = {
"password": str,
"ask_password": Bool,
"token": str,
"custom_auth_header": str,
"cert_path": str,
"key_path": str,
"timeout": Coerce(int),
@@ -257,9 +249,7 @@ SCHEMA = {
"machine": {
str: {
"cloud": All(Lower, Choices("aws", "azure")),
"region": All(
Lower, Choices("us-west", "us-east", "eu-west", "eu-north")
),
"region": All(Lower, Choices("us-west", "us-east", "eu-west", "eu-north")),
"image": str,
"spot": Bool,
"spot_price": Coerce(float),
@@ -275,6 +265,7 @@ SCHEMA = {
"feature": {
Optional("machine", default=False): Bool,
# enabled by default. It's of no use, kept for backward compatibility.
Optional("data_index_cache", default=False): Bool,
Optional("parametrization", default=True): Bool,
},
"plots": {
+19 -16
View File
@@ -25,21 +25,22 @@ def _popen(cmd, **kwargs):
def _spawn_windows(cmd, env):
from subprocess import (
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
)
if sys.platform == "win32":
from subprocess import (
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
)
# https://stackoverflow.com/a/7006424
# https://bugs.python.org/issue41619
creationflags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
# https://stackoverflow.com/a/7006424
# https://bugs.python.org/issue41619
creationflags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
def _spawn_posix(cmd, env):
@@ -49,17 +50,19 @@ def _spawn_posix(cmd, env):
# with PyInstaller has trouble with SystemExit exception and throws
# errors such as "[26338] Failed to execute script __main__"
try:
pid = os.fork() # pylint: disable=no-member
# pylint: disable-next=no-member
pid = os.fork() # type: ignore[attr-defined]
if pid > 0:
return
except OSError:
logger.exception("failed at first fork")
os._exit(1) # pylint: disable=protected-access
os.setsid() # pylint: disable=no-member
os.setsid() # type: ignore[attr-defined] # pylint: disable=no-member
try:
pid = os.fork() # pylint: disable=no-member
# pylint: disable-next=no-member
pid = os.fork() # type: ignore[attr-defined]
if pid > 0:
os._exit(0) # pylint: disable=protected-access
except OSError:
@@ -100,7 +103,7 @@ def daemon(args):
Args:
args (list): list of arguments to append to `dvc daemon` command.
"""
daemonize(["daemon", "-q"] + args)
daemonize(["daemon", "-q", *args])
def daemonize(cmd: List[str]):
+5 -8
View File
@@ -59,8 +59,7 @@ class AsciiCanvas:
def draw(self):
"""Draws ASCII canvas on the screen."""
lines = map("".join, self.canvas)
joined_lines = os.linesep.join(lines)
return joined_lines
return os.linesep.join(lines)
def point(self, x, y, char):
"""Create a point on ASCII canvas.
@@ -81,7 +80,7 @@ class AsciiCanvas:
self.canvas[y][x] = char
def line(self, x0, y0, x1, y1, char):
def line(self, x0, y0, x1, y1, char): # noqa: C901, PLR0912
"""Create a line on ASCII canvas.
Args:
@@ -214,8 +213,8 @@ def draw(vertices, edges):
# pylint: disable=too-many-locals
# NOTE: coordinates might me negative, so we need to shift
# everything to the positive plane before we actually draw it.
Xs = [] # pylint: disable=invalid-name
Ys = [] # pylint: disable=invalid-name
Xs = [] # noqa: N806, pylint: disable=invalid-name
Ys = [] # noqa: N806, pylint: disable=invalid-name
sug = _build_sugiyama_layout(vertices, edges)
@@ -273,8 +272,6 @@ def draw(vertices, edges):
vertex.view.h,
)
canvas.text(
int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data
)
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
return canvas.draw()
+34 -27
View File
@@ -3,12 +3,12 @@
import logging
from typing import TYPE_CHECKING, Iterable, Optional
from funcy import cached_property
from dvc.config import NoRemoteError, RemoteConfigError
from dvc.utils.objects import cached_property
from dvc_data.hashfile.db import get_index
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
@@ -18,30 +18,24 @@ logger = logging.getLogger(__name__)
class Remote:
def __init__(self, path, fs, **config):
def __init__(self, name: str, path: str, fs: "FileSystem", *, index=None, **config):
self.path = path
self.fs = fs
self.name = name
self.index = index
self.worktree = config.pop("worktree", False)
self.worktree: bool = config.pop("worktree", False)
self.config = config
if self.worktree:
version_aware = self.config.get("version_aware")
if version_aware is False:
raise RemoteConfigError(
"worktree remotes require version_aware cloud"
)
if version_aware is None:
self.fs.version_aware = True
@cached_property
def odb(self):
def odb(self) -> "HashFileDB":
from dvc_data.hashfile.db import get_odb
path = self.path
if self.worktree:
path = self.fs.path.join(path, ".dvc", "cache")
return get_odb(self.fs, path, **self.config)
return get_odb(self.fs, path, hash_name="md5", **self.config)
class DataCloud:
@@ -70,9 +64,23 @@ class DataCloud:
from dvc.fs import get_cloud_fs
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
if config.get("worktree"):
version_aware = config.get("version_aware")
if version_aware is False:
raise RemoteConfigError(
"worktree remotes require version_aware cloud"
)
if version_aware is None:
config["version_aware"] = True
fs = cls(**config)
config["tmp_dir"] = self.repo.index_db_dir
return Remote(fs_path, fs, **config)
if self.repo.data_index is not None:
index = self.repo.data_index.view(("remote", name))
else:
index = None
return Remote(name, fs_path, fs, index=index, **config)
if bool(self.repo.config["remote"]):
error_msg = (
@@ -108,8 +116,11 @@ class DataCloud:
for hash_info in status.missing
)
logger.warning(
"Some of the cache files do not exist neither locally "
f"nor on remote. Missing cache files:\n{missing_desc}"
(
"Some of the cache files do not exist neither locally "
"nor on remote. Missing cache files:\n%s"
),
missing_desc,
)
def transfer(
@@ -141,12 +152,12 @@ class DataCloud:
"""
odb = odb or self.get_remote_odb(remote, "push")
return self.transfer(
self.repo.odb.local,
self.repo.cache.local,
odb,
objs,
jobs=jobs,
dest_index=get_index(odb),
cache_odb=self.repo.odb.local,
cache_odb=self.repo.cache.local,
validate_status=self._log_missing,
)
@@ -169,11 +180,11 @@ class DataCloud:
odb = odb or self.get_remote_odb(remote, "pull")
return self.transfer(
odb,
self.repo.odb.local,
self.repo.cache.local,
objs,
jobs=jobs,
src_index=get_index(odb),
cache_odb=self.repo.odb.local,
cache_odb=self.repo.cache.local,
verify=odb.verify,
validate_status=self._log_missing,
)
@@ -184,7 +195,6 @@ class DataCloud:
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["HashFileDB"] = None,
log_missing: bool = True,
):
"""Check status of data items in a cloud-agnostic way.
@@ -195,21 +205,18 @@ class DataCloud:
cache to. By default remote from core.remote config option
is used.
odb: optional ODB to check status from. Overrides remote.
log_missing: log warning messages if file doesn't exist
neither in cache, neither in cloud.
"""
from dvc_data.hashfile.status import compare_status
if not odb:
odb = self.get_remote_odb(remote, "status")
return compare_status(
self.repo.odb.local,
self.repo.cache.local,
odb,
objs,
jobs=jobs,
log_missing=log_missing,
dest_index=get_index(odb),
cache_odb=self.repo.odb.local,
cache_odb=self.repo.cache.local,
)
def get_url_for(self, remote, checksum):
+5 -7
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Any, Mapping
from typing import Any, Mapping, Set
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
@@ -59,7 +59,7 @@ def _merge_params(s_list):
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
# figure out completely tracked params file, and ignore specific keys
wholly_tracked = set()
wholly_tracked: Set[str] = set()
for key in s_list:
if not isinstance(key, dict):
continue
@@ -74,7 +74,7 @@ def _merge_params(s_list):
if not isinstance(key, dict):
msg = "Only list of str/dict is supported. Got: "
msg += f"'{type(key).__name__}'."
raise ValueError(msg)
raise ValueError(msg) # noqa: TRY004
for k, params in key.items():
if k in wholly_tracked:
@@ -83,13 +83,11 @@ def _merge_params(s_list):
if not isinstance(params, list):
msg = "Expected list of params for custom params file "
msg += f"'{k}', got '{type(params).__name__}'."
raise ValueError(msg)
raise ValueError(msg) # noqa: TRY004
d[k].extend(params)
return d
def loads_params(stage, s_list):
d = _merge_params(s_list)
return [
ParamsDependency(stage, path, params) for path, params in d.items()
]
return [ParamsDependency(stage, path, params) for path, params in d.items()]
+5 -11
View File
@@ -25,11 +25,9 @@ class DependencyIsStageFileError(DvcException):
class Dependency(Output):
IS_DEPENDENCY = True
DoesNotExistError = DependencyDoesNotExistError # type: Type[DvcException]
IsNotFileOrDirError = (
DependencyIsNotFileOrDirError
) # type: Type[DvcException]
IsStageFileError = DependencyIsStageFileError # type: Type[DvcException]
DoesNotExistError: Type[DvcException] = DependencyDoesNotExistError
IsNotFileOrDirError: Type[DvcException] = DependencyIsNotFileOrDirError
IsStageFileError: Type[DvcException] = DependencyIsStageFileError
def workspace_status(self) -> Dict[str, str]:
if self.fs.version_aware:
@@ -49,9 +47,7 @@ class Dependency(Output):
self.def_path = self.fs.path.version_path(
self.def_path, self.meta.version_id
)
self.fs_path = self.fs.path.version_path(
self.fs_path, self.meta.version_id
)
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
def download(self, to, jobs=None):
fs_download(self.fs, self.fs_path, to, jobs=jobs)
@@ -59,9 +55,7 @@ class Dependency(Output):
def save(self):
super().save()
if self.fs.version_aware:
self.fs_path = self.fs.path.version_path(
self.fs_path, self.meta.version_id
)
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
def dumpd(self, **kwargs):
if self.fs.version_aware:
+22 -13
View File
@@ -4,7 +4,7 @@ import typing
from collections import defaultdict
from typing import Dict
import dpath.util
import dpath
from voluptuous import Any
from dvc.exceptions import DvcException
@@ -41,7 +41,10 @@ class ParamsDependency(Dependency):
self.params = list(params) if params else []
hash_info = HashInfo()
if isinstance(params, dict):
hash_info = HashInfo(self.PARAM_PARAMS, params)
hash_info = HashInfo(
self.PARAM_PARAMS,
params, # type: ignore[arg-type]
)
repo = repo or stage.repo
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
super().__init__(stage, path, repo=repo)
@@ -64,7 +67,10 @@ class ParamsDependency(Dependency):
for param in self.params:
if param in values:
info[param] = values[param]
self.hash_info = HashInfo(self.PARAM_PARAMS, info)
self.hash_info = HashInfo(
self.PARAM_PARAMS,
info, # type: ignore[arg-type]
)
def read_params(
self, flatten: bool = True, **kwargs: typing.Any
@@ -81,17 +87,17 @@ class ParamsDependency(Dependency):
if flatten:
for param in self.params:
try:
ret[param] = dpath.util.get(config, param, separator=".")
ret[param] = dpath.get(config, param, separator=".")
except KeyError:
continue
return ret
from dpath.util import merge
from dpath import merge
for param in self.params:
merge(
ret,
dpath.util.search(config, param, separator="."),
dpath.search(config, param, separator="."),
separator=".",
)
return ret
@@ -104,7 +110,8 @@ class ParamsDependency(Dependency):
from funcy import ldistinct
status = defaultdict(dict)
status: Dict[str, Any] = defaultdict(dict)
assert isinstance(self.hash_info.value, dict)
info = self.hash_info.value if self.hash_info else {}
actual = self.read_params()
@@ -119,9 +126,13 @@ class ParamsDependency(Dependency):
elif param not in info:
st = "new"
elif actual[param] != info[param]:
if (
isinstance(actual[param], tuple)
and list(actual[param]) == info[param]
):
continue
st = "modified"
else:
assert actual[param] == info[param]
continue
status[str(self)][param] = st
@@ -144,9 +155,7 @@ class ParamsDependency(Dependency):
try:
return load_path(self.fs_path, self.repo.fs)
except ParseError as exc:
raise BadParamFileError(
f"Unable to read parameters from '{self}'"
) from exc
raise BadParamFileError(f"Unable to read parameters from '{self}'") from exc
def get_hash(self):
info = self.read_params()
@@ -159,13 +168,13 @@ class ParamsDependency(Dependency):
)
)
return HashInfo(self.PARAM_PARAMS, info)
return HashInfo(self.PARAM_PARAMS, info) # type: ignore[arg-type]
def save(self):
if not self.exists:
raise self.DoesNotExistError(self)
if not self.isfile and not self.isdir:
if not self.isfile() and not self.isdir():
raise self.IsNotFileOrDirError(self)
self.ignore()
+26 -27
View File
@@ -1,18 +1,23 @@
import os
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, Union
from voluptuous import Required
from dvc.prompt import confirm
from dvc_data.hashfile.meta import Meta
from .base import Dependency
if TYPE_CHECKING:
from dvc_data.hashfile.file import HashFile
from typing import ContextManager
from dvc.output import Output
from dvc.repo import Repo
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
@@ -30,13 +35,13 @@ class RepoDependency(Dependency):
}
}
def __init__(self, def_repo, stage, *args, **kwargs):
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] = {}
self._meta: Dict[str, "Meta"] = {}
super().__init__(stage, *args, **kwargs)
def _parse_path(self, fs, fs_path):
def _parse_path(self, fs, fs_path): # noqa: ARG002
return None
@property
@@ -61,10 +66,10 @@ class RepoDependency(Dependency):
def save(self):
pass
def dumpd(self, **kwargs):
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, jobs=None):
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():
@@ -75,40 +80,38 @@ class RepoDependency(Dependency):
to.fs_path,
to.fs,
obj,
self.repo.odb.local,
self.repo.cache.local,
ignore=None,
state=self.repo.state,
prompt=confirm,
)
def update(self, rev=None):
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()
def changed_checksum(self):
def changed_checksum(self) -> bool:
# From current repo point of view what describes RepoDependency is its
# origin project url and rev_lock, and it makes RepoDependency
# immutable, hence its impossible for checksum to change.
return False
def get_used_objs(
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
def get_used_objs(self, **kwargs) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _, _ = self._get_used_and_obj(**kwargs)
return used
def _get_used_and_obj(
self, obj_only=False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], Meta, "HashFile"]:
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
local_odb = self.repo.odb.local
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)
@@ -154,7 +157,7 @@ class RepoDependency(Dependency):
used_obj_ids[object_store].update(oid for _, _, oid in obj)
return used_obj_ids, meta, obj
def _check_circular_import(self, odb, obj_ids):
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
@@ -169,16 +172,14 @@ class RepoDependency(Dependency):
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
):
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 (
@@ -209,15 +210,13 @@ class RepoDependency(Dependency):
)
return meta
def _make_repo(self, locked=True, **kwargs):
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=True):
def _get_rev(self, locked: bool = True):
d = self.def_repo
return (d.get(self.PARAM_REV_LOCK) if locked else None) or d.get(
self.PARAM_REV
)
return (d.get(self.PARAM_REV_LOCK) if locked else None) or d.get(self.PARAM_REV)
+50 -72
View File
@@ -7,13 +7,12 @@ from typing import (
Callable,
Dict,
List,
Optional,
Tuple,
TypeVar,
Union,
)
from funcy import cached_property
from dvc.exceptions import DvcException
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
from dvc.stage import serialize
@@ -22,21 +21,25 @@ from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageFileIsNotDvcFileError,
)
from dvc.types import AnyPath
from dvc.utils import relpath
from dvc.utils.collections import apply_diff
from dvc.utils.objects import cached_property
from dvc.utils.serialize import dump_yaml, modify_yaml
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import StrOrBytesPath
from .parsing import DataResolver
from .stage import Stage
logger = logging.getLogger(__name__)
_T = TypeVar("_T")
DVC_FILE = "Dvcfile"
DVC_FILE_SUFFIX = ".dvc"
PIPELINE_FILE = "dvc.yaml"
PIPELINE_LOCK = "dvc.lock"
PROJECT_FILE = "dvc.yaml"
LOCK_FILE = "dvc.lock"
class FileIsGitIgnored(DvcException):
@@ -55,18 +58,16 @@ class ParametrizedDumpError(DvcException):
def is_valid_filename(path):
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
DVC_FILE,
PIPELINE_FILE,
PROJECT_FILE,
]
def is_dvc_file(path):
return os.path.isfile(path) and (
is_valid_filename(path) or is_lock_file(path)
)
return os.path.isfile(path) and (is_valid_filename(path) or is_lock_file(path))
def is_lock_file(path):
return os.path.basename(path) == PIPELINE_LOCK
return os.path.basename(path) == LOCK_FILE
def is_git_ignored(repo, path):
@@ -74,9 +75,7 @@ def is_git_ignored(repo, path):
from dvc.scm import NoSCMError
try:
return isinstance(repo.fs, LocalFileSystem) and repo.scm.is_ignored(
path
)
return isinstance(repo.fs, LocalFileSystem) and repo.scm.is_ignored(path)
except NoSCMError:
return False
@@ -86,7 +85,7 @@ def check_dvcfile_path(repo, path):
raise StageFileBadNameError(
"bad DVC file name '{}'. DVC files should be named "
"'{}' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
relpath(path), PIPELINE_FILE, os.path.basename(path)
relpath(path), PROJECT_FILE, os.path.basename(path)
)
)
@@ -149,9 +148,7 @@ class FileMixin:
# 4. when the file is git ignored
if not self.exists():
dvc_ignored = self.repo.dvcignore.is_ignored_file(self.path)
raise StageFileDoesNotExistError(
self.path, dvc_ignored=dvc_ignored
)
raise StageFileDoesNotExistError(self.path, dvc_ignored=dvc_ignored)
self._verify_filename()
if not self.repo.fs.isfile(self.path):
@@ -161,7 +158,7 @@ class FileMixin:
return self._load_yaml(**kwargs)
@classmethod
def validate(cls, d: _T, fname: str = None) -> _T:
def validate(cls, d: _T, fname: Optional[str] = None) -> _T:
from dvc.utils.strictyaml import validate
return validate(d, cls.SCHEMA, path=fname) # type: ignore[arg-type]
@@ -176,7 +173,8 @@ class FileMixin:
**kwargs,
)
def remove(self, force=False): # pylint: disable=unused-argument
# pylint: disable-next=unused-argument
def remove(self, force=False): # noqa: ARG002
with contextlib.suppress(FileNotFoundError):
os.unlink(self.path)
@@ -189,23 +187,23 @@ class FileMixin:
class SingleStageFile(FileMixin):
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
from dvc.stage.loader import SingleStageLoader as LOADER
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
metrics: List[str] = []
plots: Dict[str, Any] = {}
plots: Any = {}
params: List[str] = []
@property
def stage(self):
def stage(self) -> "Stage":
data, raw = self._load()
return self.LOADER.load_stage(self, data, raw)
@property
def stages(self):
def stages(self) -> LOADER:
data, raw = self._load()
return self.LOADER(self, data, raw)
def dump(self, stage, **kwargs):
def dump(self, stage, **kwargs) -> None:
"""Dumps given stage appropriately in the dvcfile."""
from dvc.stage import PipelineStage
@@ -216,7 +214,8 @@ class SingleStageFile(FileMixin):
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
self.repo.scm_context.track_file(self.relpath)
def remove_stage(self, stage): # pylint: disable=unused-argument
# pylint: disable-next=unused-argument
def remove_stage(self, stage): # noqa: ARG002
self.remove()
def merge(self, ancestor, other, allowed=None):
@@ -228,11 +227,11 @@ class SingleStageFile(FileMixin):
self.dump(stage)
class PipelineFile(FileMixin):
class ProjectFile(FileMixin):
"""Abstraction for pipelines file, .yaml + .lock combined."""
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
from dvc.stage.loader import StageLoader as LOADER
from dvc.stage.loader import StageLoader as LOADER # noqa: N814
@property
def _lockfile(self):
@@ -242,6 +241,7 @@ class PipelineFile(FileMixin):
self.__dict__.pop("contents", None)
self.__dict__.pop("lockfile_contents", None)
self.__dict__.pop("resolver", None)
self.__dict__.pop("stages", None)
def dump(
self, stage, update_pipeline=True, update_lock=True, **kwargs
@@ -265,9 +265,7 @@ class PipelineFile(FileMixin):
@staticmethod
def _check_if_parametrized(stage, action: str = "dump") -> None:
if stage.raw_data.parametrized:
raise ParametrizedDumpError(
f"cannot {action} a parametrized {stage}"
)
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
def _dump_pipeline_file(self, stage):
self._check_if_parametrized(stage)
@@ -280,9 +278,7 @@ class PipelineFile(FileMixin):
data["stages"] = data.get("stages", {})
existing_entry = stage.name in data["stages"]
action = "Modifying" if existing_entry else "Adding"
logger.info(
"%s stage '%s' in '%s'", action, stage.name, self.relpath
)
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
if existing_entry:
orig_stage_data = data["stages"][stage.name]
@@ -294,39 +290,37 @@ class PipelineFile(FileMixin):
@property
def stage(self):
raise DvcException(
"PipelineFile has multiple stages. Please specify it's name."
)
raise DvcException("ProjectFile has multiple stages. Please specify it's name.")
@cached_property
def contents(self):
def contents(self) -> Dict[str, Any]:
return self._load()[0]
@cached_property
def lockfile_contents(self):
def lockfile_contents(self) -> Dict[str, Any]:
return self._lockfile.load()
@cached_property
def resolver(self):
def resolver(self) -> "DataResolver":
from .parsing import DataResolver
wdir = self.repo.fs.path.parent(self.path)
return DataResolver(self.repo, wdir, self.contents)
@property
def stages(self):
@cached_property
def stages(self) -> LOADER:
return self.LOADER(self, self.contents, self.lockfile_contents)
@property
def metrics(self):
def metrics(self) -> List[str]:
return self.contents.get("metrics", [])
@property
def plots(self):
def plots(self) -> Any:
return self.contents.get("plots", {})
@property
def params(self):
def params(self) -> List[str]:
return self.contents.get("params", [])
def remove(self, force=False):
@@ -360,10 +354,7 @@ class PipelineFile(FileMixin):
def get_lockfile_schema(d):
from dvc.schema import (
COMPILED_LOCKFILE_V1_SCHEMA,
COMPILED_LOCKFILE_V2_SCHEMA,
)
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
schema = {
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
@@ -416,15 +407,12 @@ class Lockfile(FileMixin):
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
)
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
migrate_lock_v1_to_v2(data, self.latest_version_info)
else:
if not data:
data.update(self.latest_version_info)
# order is important, meta should always be at the top
logger.info("Generating lock file '%s'", self.relpath)
elif not data:
data.update(self.latest_version_info)
# order is important, meta should always be at the top
logger.info("Generating lock file '%s'", self.relpath)
data["stages"] = data.get("stages", {})
modified = data["stages"].get(stage.name, {}) != stage_data.get(
@@ -460,20 +448,10 @@ class Lockfile(FileMixin):
raise NotImplementedError
class Dvcfile:
def __new__(cls, repo: "Repo", path: AnyPath, **kwargs: Any):
assert path
assert repo
return make_dvcfile(repo, path, **kwargs)
DVCFile = Union["PipelineFile", "SingleStageFile"]
def make_dvcfile(repo: "Repo", path: AnyPath, **kwargs: Any) -> DVCFile:
_, ext = os.path.splitext(str(path))
if ext in [".yaml", ".yml"]:
return PipelineFile(repo, path, **kwargs)
# fallback to single stage file for better error messages
def load_file(
repo: "Repo", path: "StrOrBytesPath", **kwargs: Any
) -> Union[ProjectFile, SingleStageFile]:
_, ext = os.path.splitext(path)
if ext in (".yaml", ".yml"):
return ProjectFile(repo, path, **kwargs)
return SingleStageFile(repo, path, **kwargs)
+12 -21
View File
@@ -1,4 +1,6 @@
"""Exceptions raised by the dvc."""
from typing import Dict, List
from dvc.utils import format_link
@@ -148,8 +150,9 @@ class CyclicGraphError(DvcException):
class ConfirmRemoveError(DvcException):
def __init__(self, path):
super().__init__(
"unable to remove '{}' without a confirmation. Use "
"`-f` to force.".format(path)
"unable to remove '{}' without a confirmation. Use `-f` to force.".format(
path
)
)
@@ -175,9 +178,7 @@ class BadMetricError(DvcException):
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super().__init__(
"cannot use `fname` with multiple targets or `-R|--recursive`"
)
super().__init__("cannot use `fname` with multiple targets or `-R|--recursive`")
class OverlappingOutputPathsError(DvcException):
@@ -204,9 +205,7 @@ class FileMissingError(DvcException):
def __init__(self, path, hint=None):
self.path = path
hint = "" if hint is None else f". {hint}"
super().__init__(
f"Can't find '{path}' neither locally nor on remote{hint}"
)
super().__init__(f"Can't find '{path}' neither locally nor on remote{hint}")
class FileTransferError(DvcException):
@@ -227,7 +226,7 @@ class UploadError(FileTransferError):
class CheckoutError(DvcException):
def __init__(self, target_infos, stats=None):
def __init__(self, target_infos: List[str], stats: Dict[str, List[str]]):
from dvc.utils import error_link
self.target_infos = target_infos
@@ -248,9 +247,7 @@ class CollectCacheError(DvcException):
class NoRemoteInExternalRepoError(DvcException):
def __init__(self, url):
super().__init__(
f"No DVC remote is specified in target repository '{url}'."
)
super().__init__(f"No DVC remote is specified in target repository '{url}'.")
class NoOutputInExternalRepoError(DvcException):
@@ -275,8 +272,7 @@ class PathMissingError(DvcException):
" neither as a DVC output nor as a Git-tracked file."
)
default_msg_dvc_only = (
"The path '{}' does not exist in the target repository '{}'"
" as an DVC output."
"The path '{}' does not exist in the target repository '{}' as an DVC output."
)
def __init__(self, path, repo, dvc_only=False):
@@ -292,7 +288,6 @@ class URLMissingError(DvcException):
class RemoteCacheRequiredError(DvcException):
def __init__(self, scheme, fs_path):
super().__init__(
(
"Current operation was unsuccessful because '{}' requires "
@@ -317,8 +312,7 @@ class NoOutputOrStageError(DvcException):
def __init__(self, target, file):
super().__init__(
f"'{target}' "
f"does not exist as an output or a stage name in '{file}'"
f"'{target}' does not exist as an output or a stage name in '{file}'"
)
@@ -327,11 +321,8 @@ class MergeError(DvcException):
class CacheLinkError(DvcException):
SUPPORT_LINK = "See {} for more information.".format(
format_link(
"https://dvc.org/doc/user-guide/troubleshooting#cache-types"
)
format_link("https://dvc.org/doc/user-guide/troubleshooting#cache-types")
)
def __init__(self, fs_paths):
+21 -21
View File
@@ -3,7 +3,7 @@ import os
import tempfile
import threading
from contextlib import contextmanager
from typing import TYPE_CHECKING, Dict
from typing import TYPE_CHECKING, Collection, Dict, Iterator, Optional, Tuple
from funcy import retry, wrap_with
@@ -20,7 +20,9 @@ from dvc.scm import CloneError, map_scm_exception
from dvc.utils import relpath
if TYPE_CHECKING:
from scmrepo import Git
from dvc.types import StrPath
from .scm import Git
logger = logging.getLogger(__name__)
@@ -28,12 +30,16 @@ logger = logging.getLogger(__name__)
@contextmanager
@map_scm_exception()
def external_repo(
url, rev=None, for_write=False, cache_dir=None, cache_types=None, **kwargs
):
from scmrepo.git import Git
url,
rev: Optional[str] = None,
for_write: bool = False,
cache_dir: Optional["StrPath"] = None,
cache_types: Optional[Collection[str]] = None,
**kwargs,
) -> Iterator["Repo"]:
from dvc.config import NoRemoteError
from dvc.fs import GitFileSystem
from dvc.scm import Git
logger.debug("Creating external repo %s@%s", url, rev)
path = _cached_clone(url, rev, for_write=for_write)
@@ -82,9 +88,7 @@ def external_repo(
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 NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
raise
except FileMissingError as exc:
raise PathMissingError(exc.path, url) from exc
@@ -108,7 +112,7 @@ def erepo_factory(url, root_dir, cache_config):
return make_repo
CLONES: Dict[str, str] = {}
CLONES: Dict[str, Tuple[str, bool]] = {}
CACHE_DIRS: Dict[str, str] = {}
@@ -145,7 +149,7 @@ def _get_remote_config(url):
name = "auto-generated-upstream"
return {
"core": {"remote": name},
"remote": {name: {"url": repo.odb.local.path}},
"remote": {name: {"url": repo.cache.local.path}},
}
# Use original remote to make sure that we are using correct url,
@@ -185,14 +189,14 @@ def _cached_clone(url, rev, for_write=False):
@wrap_with(threading.Lock())
def _clone_default_branch(url, rev, for_write=False):
def _clone_default_branch(url, rev, for_write=False): # noqa: C901, PLR0912
"""Get or create a clean clone of the url.
The cloned is reactualized with git pull unless rev is a known sha.
"""
from scmrepo.git import Git
from dvc.scm import Git
clone_path, shallow = CLONES.get(url, (None, False))
clone_path, shallow = CLONES.get(url) or (None, False)
git = None
try:
@@ -228,9 +232,7 @@ def _clone_default_branch(url, rev, for_write=False):
os.path.join(clone_path, Git.GIT_DIR, "shallow")
)
if shallow:
logger.debug(
"erepo: using shallow clone for branch '%s'", rev
)
logger.debug("erepo: using shallow clone for branch '%s'", rev)
except CloneError:
git_dir = os.path.join(clone_path, ".git")
if os.path.exists(git_dir):
@@ -267,7 +269,7 @@ def _merge_upstream(git: "Git"):
def _git_checkout(repo_path, rev):
from scmrepo.git import Git
from dvc.scm import Git
logger.debug("erepo: git checkout %s@%s", repo_path, rev)
git = Git(repo_path)
@@ -286,8 +288,6 @@ def _remove(path):
try:
os_retry(remove)(path)
except PermissionError:
logger.warning(
"Failed to remove '%s'", relpath(path), exc_info=True
)
logger.warning("Failed to remove '%s'", relpath(path), exc_info=True)
else:
remove(path)
+2 -2
View File
@@ -6,7 +6,6 @@ from dvc.config import ConfigError as RepoConfigError
from dvc.config_schema import SCHEMA, Invalid
# pylint: disable=unused-import
from dvc_objects.fs import utils # noqa: F401
from dvc_objects.fs import ( # noqa: F401
LocalFileSystem,
MemoryFileSystem,
@@ -17,6 +16,7 @@ from dvc_objects.fs import ( # noqa: F401
localfs,
registry,
system,
utils,
)
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
from dvc_objects.fs.errors import ( # noqa: F401
@@ -72,7 +72,7 @@ def get_fs_config(config, **kwargs):
except KeyError:
from dvc.config import RemoteNotFoundError
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
raise RemoteNotFoundError(f"remote '{name}' doesn't exist") # noqa: B904
else:
remote_conf = kwargs
return _resolve_remote_refs(config, remote_conf)
+14 -12
View File
@@ -2,8 +2,7 @@
from contextlib import ExitStack
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
from funcy import cached_property
from dvc.utils.objects import cached_property
from dvc_objects.fs.callbacks import ( # noqa: F401
DEFAULT_CALLBACK,
Callback,
@@ -11,6 +10,8 @@ from dvc_objects.fs.callbacks import ( # noqa: F401
)
if TYPE_CHECKING:
from rich.progress import TaskID
from dvc.ui._rich_progress import RichTransferProgress
@@ -19,10 +20,10 @@ class RichCallback(Callback):
self,
size: Optional[int] = None,
value: int = 0,
progress: "RichTransferProgress" = None,
desc: str = None,
bytes: bool = False, # pylint: disable=redefined-builtin
unit: str = None,
progress: Optional["RichTransferProgress"] = None,
desc: Optional[str] = None,
bytes: bool = False, # noqa: A002, pylint: disable=redefined-builtin
unit: Optional[str] = None,
disable: bool = False,
transient: bool = True,
) -> None:
@@ -41,7 +42,7 @@ class RichCallback(Callback):
super().__init__(size=size, value=value)
@cached_property
def progress(self):
def progress(self) -> "RichTransferProgress":
from dvc.ui import ui
from dvc.ui._rich_progress import RichTransferProgress
@@ -53,11 +54,12 @@ class RichCallback(Callback):
disable=self.disable,
console=ui.error_console,
)
return self._stack.enter_context(progress)
self._stack.enter_context(progress)
return progress
@cached_property
def task(self):
return self.progress.add_task(**self._task_kwargs)
def task(self) -> "TaskID":
return self.progress.add_task(**self._task_kwargs) # type: ignore[arg-type]
def __enter__(self):
return self
@@ -67,7 +69,7 @@ class RichCallback(Callback):
self.progress.clear_task(self.task)
self._stack.close()
def call(self, hook_name=None, **kwargs):
def call(self, hook_name=None, **kwargs): # noqa: ARG002
self.progress.update(
self.task,
completed=self.value,
@@ -80,7 +82,7 @@ class RichCallback(Callback):
path_1: Union[str, BinaryIO],
path_2: str,
kwargs: Dict[str, Any],
child: "Callback" = None,
child: Optional["Callback"] = None,
):
child = child or RichCallback(
progress=self.progress,
+12 -7
View File
@@ -1,13 +1,15 @@
import functools
import logging
import os
import threading
from funcy import cached_property, wrap_prop
from typing import TYPE_CHECKING
from dvc.utils import as_posix
from dvc_data.fs import DataFileSystem as _DataFileSystem
from dvc_objects.fs.base import FileSystem
if TYPE_CHECKING:
from dvc_data.fs import DataFileSystem as _DataFileSystem
logger = logging.getLogger(__name__)
@@ -19,9 +21,12 @@ class DataFileSystem(FileSystem):
def _prepare_credentials(self, **config):
return config
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
@functools.cached_property
def fs( # pylint: disable=invalid-overridden-method
self,
) -> "_DataFileSystem":
from dvc_data.fs import DataFileSystem as _DataFileSystem
return _DataFileSystem(**self.fs_args)
def isdvc(self, path, **kwargs):
+39 -29
View File
@@ -1,14 +1,15 @@
import errno
import functools
import logging
import ntpath
import os
import posixpath
import threading
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union
from fsspec.spec import AbstractFileSystem
from funcy import cached_property, wrap_prop, wrap_with
from funcy import wrap_with
from dvc_objects.fs.base import FileSystem
from dvc_objects.fs.path import Path
@@ -17,10 +18,11 @@ from .data import DataFileSystem
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import StrPath
logger = logging.getLogger(__name__)
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
RepoFactory = Union[Callable[..., "Repo"], Type["Repo"]]
Key = Tuple[str, ...]
@@ -115,10 +117,8 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
super().__init__()
if repo is None:
repo = self._make_repo(
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
)
assert repo
repo = self._make_repo(url=url, rev=rev, subrepos=subrepos, **repo_kwargs)
assert repo is not None
# pylint: disable=protected-access
repo_factory = repo._fs_conf["repo_factory"]
@@ -131,10 +131,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
def _getcwd():
relparts = ()
assert repo is not None
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.root_dir):
relparts = repo.fs.path.relparts(
repo.fs.path.getcwd(), repo.root_dir
)
relparts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.root_dir)
return self.root_marker + self.sep.join(relparts)
self.path = Path(self.sep, getcwd=_getcwd)
@@ -154,16 +153,16 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
if hasattr(repo, "dvc_dir"):
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
def _get_key(self, path) -> Key:
def _get_key(self, path: "StrPath") -> Key:
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
if parts == (".",):
parts = ()
if parts == (os.curdir,):
return ()
return parts
def _get_key_from_relative(self, path) -> Key:
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
parts = parts[1:]
return parts[1:]
return parts
def _from_key(self, parts: Key) -> str:
@@ -171,8 +170,6 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
@property
def repo_url(self):
if self.repo is None:
return None
return self.repo.url
@classmethod
@@ -212,9 +209,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
scm=self.repo.scm,
repo_factory=self.repo_factory,
)
self._datafss[key] = DataFileSystem(
index=repo.index.data["repo"]
)
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
self._subrepos_trie[key] = repo
def _is_dvc_repo(self, dir_path):
@@ -321,7 +316,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
ignore_subrepos = kwargs.get("ignore_subrepos", True)
return self._info(key, path, ignore_subrepos=ignore_subrepos)
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
def _info( # noqa: C901, PLR0912
self, key, path, ignore_subrepos=True, check_ignored=True
):
repo, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_info = None
@@ -364,35 +361,48 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
info["name"] = path
return info
def get_file(self, rpath, lpath, **kwargs): # pylint: disable=arguments-differ
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
try:
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
class DVCFileSystem(FileSystem):
protocol = "local"
PARAM_CHECKSUM = "md5"
def _prepare_credentials(self, **config):
def _prepare_credentials(self, **config) -> Dict[str, Any]:
return config
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
@functools.cached_property
# pylint: disable-next=invalid-overridden-method
def fs(self) -> "DVCFileSystem":
return _DVCFileSystem(**self.fs_args)
def isdvc(self, path, **kwargs):
def isdvc(self, path, **kwargs) -> bool:
return self.fs.isdvc(path, **kwargs)
@property
def path(self): # pylint: disable=invalid-overridden-method
def path(self) -> Path: # pylint: disable=invalid-overridden-method
return self.fs.path
@property
def repo(self):
def repo(self) -> "Repo":
return self.fs.repo
@property
def repo_url(self):
def repo_url(self) -> str:
return self.fs.repo_url
def from_os_path(self, path):
def from_os_path(self, path: str) -> str:
if os.path.isabs(path):
path = os.path.relpath(path, self.repo.root_dir)
+14 -14
View File
@@ -1,15 +1,14 @@
import threading
from typing import TYPE_CHECKING, Any
from funcy import cached_property, wrap_prop
import functools
from typing import TYPE_CHECKING, Any, Optional
from . import FileSystem
if TYPE_CHECKING:
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
from scmrepo.git import Git
from scmrepo.git.objects import GitTrie
from dvc.scm import Git
class GitFileSystem(FileSystem): # pylint:disable=abstract-method
"""Proxies the repo file access methods to Git objects"""
@@ -19,10 +18,10 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
def __init__(
self,
path: str = None,
rev: str = None,
scm: "Git" = None,
trie: "GitTrie" = None,
path: Optional[str] = None,
rev: Optional[str] = None,
scm: Optional["Git"] = None,
trie: Optional["GitTrie"] = None,
**kwargs: Any,
) -> None:
from dvc.scm import resolve_rev
@@ -39,15 +38,16 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
}
)
@wrap_prop(threading.Lock())
@cached_property
def fs(self) -> "FsspecGitFileSystem":
@functools.cached_property
def fs( # pylint: disable=invalid-overridden-method
self,
) -> "FsspecGitFileSystem":
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
return FsspecGitFileSystem(**self.fs_args)
@cached_property
def path(self):
@functools.cached_property
def path(self): # pylint: disable=invalid-overridden-method
return self.fs.path
@property
+30 -36
View File
@@ -3,14 +3,17 @@ import os
import re
from collections import namedtuple
from itertools import chain, groupby, takewhile
from typing import TYPE_CHECKING, List, Optional
from pathspec.patterns import GitWildMatchPattern
from pathspec.util import normalize_file
from pygtrie import Trie
from dvc.fs import AnyFSPath, FileSystem, Schemes, localfs
from dvc.fs import Schemes, localfs
from dvc.pathspec_math import PatternInfo, merge_patterns
from dvc.types import List, Optional
if TYPE_CHECKING:
from dvc.fs import AnyFSPath, FileSystem
logger = logging.getLogger(__name__)
@@ -24,26 +27,25 @@ class DvcIgnore:
class DvcIgnorePatterns(DvcIgnore):
def __init__(self, pattern_list, dirname, sep):
if pattern_list:
if isinstance(pattern_list[0], str):
pattern_list = [
PatternInfo(pattern, "") for pattern in pattern_list
]
from pathspec.patterns.gitwildmatch import _DIR_MARK
if pattern_list and isinstance(pattern_list[0], str):
pattern_list = [PatternInfo(pattern, "") for pattern in pattern_list]
self.sep = sep
self.pattern_list = pattern_list
self.dirname = dirname
self.regex_pattern_list = [
GitWildMatchPattern.pattern_to_regex(pattern_info.patterns)
for pattern_info in pattern_list
]
self.regex_pattern_list = []
for count, pattern in enumerate(pattern_list):
pattern, group = GitWildMatchPattern.pattern_to_regex(pattern.patterns)
if pattern:
pattern = pattern.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
self.regex_pattern_list.append((pattern, group))
self.ignore_spec = [
(ignore, re.compile("|".join(item[0] for item in group)))
for ignore, group in groupby(
self.regex_pattern_list, lambda x: x[1]
)
for ignore, group in groupby(self.regex_pattern_list, lambda x: x[1])
if ignore is not None
]
@@ -54,9 +56,7 @@ class DvcIgnorePatterns(DvcIgnore):
with fs.open(path, encoding="utf-8") as fobj:
path_spec_lines = [
PatternInfo(line, f"{name}:{line_no + 1}:{line}")
for line_no, line in enumerate(
map(str.strip, fobj.readlines())
)
for line_no, line in enumerate(map(str.strip, fobj.readlines()))
if line and not (line.strip().startswith("#"))
]
@@ -84,7 +84,7 @@ class DvcIgnorePatterns(DvcIgnore):
return False
if os.name == "nt":
path = normalize_file(path)
return normalize_file(path)
return path
def matches(self, dirname, basename, is_dir=False, details: bool = False):
@@ -148,9 +148,7 @@ class DvcIgnorePatterns(DvcIgnore):
return bool(self.pattern_list)
CheckIgnoreResult = namedtuple(
"CheckIgnoreResult", ["file", "match", "patterns"]
)
CheckIgnoreResult = namedtuple("CheckIgnoreResult", ["file", "match", "patterns"])
def _no_match(path):
@@ -195,8 +193,8 @@ class DvcIgnoreFilter:
def _get_key(self, path):
parts = self.fs.path.relparts(path, self.root_dir)
if parts == (".",):
parts = ()
if parts == (os.curdir,):
return ()
return parts
def _update_trie(self, dirname: str, trie: Trie) -> None:
@@ -239,9 +237,7 @@ class DvcIgnoreFilter:
dnames = []
for dname in dnames:
self._update_sub_repo(
self.fs.path.join(dirname, dname), ignore_trie
)
self._update_sub_repo(self.fs.path.join(dirname, dname), ignore_trie)
def _update_sub_repo(self, path, ignore_trie: Trie):
from dvc.repo import Repo
@@ -299,7 +295,7 @@ class DvcIgnoreFilter:
return [fs_dict[name] for name in chain(dirs, nondirs)]
def walk(self, fs: FileSystem, path: AnyFSPath, **kwargs):
def walk(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
detail = kwargs.get("detail", False)
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
if fs.protocol == Schemes.LOCAL:
@@ -323,7 +319,7 @@ class DvcIgnoreFilter:
else:
yield from fs.walk(path, **kwargs)
def find(self, fs: FileSystem, path: AnyFSPath, **kwargs):
def find(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
if fs.protocol == Schemes.LOCAL:
for root, _, files in self.walk(fs, path, **kwargs):
for file in files:
@@ -379,7 +375,7 @@ class DvcIgnoreFilter:
return False
def is_ignored_dir(self, path: str, ignore_subrepos: bool = True) -> bool:
"Only used in LocalFileSystem"
# only used in LocalFileSystem
path = self.fs.path.abspath(path)
if path == self.root_dir:
return False
@@ -387,7 +383,7 @@ class DvcIgnoreFilter:
return self._is_ignored(path, True, ignore_subrepos=ignore_subrepos)
def is_ignored_file(self, path: str, ignore_subrepos: bool = True) -> bool:
"Only used in LocalFileSystem"
# only used in LocalFileSystem
path = self.fs.path.abspath(path)
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
@@ -399,9 +395,7 @@ class DvcIgnoreFilter:
# https://github.com/iterative/dvc/issues/5046
full_target = self.fs.path.abspath(target)
if not self._outside_repo(full_target):
dirname, basename = self.fs.path.split(
self.fs.path.normpath(full_target)
)
dirname, basename = self.fs.path.split(self.fs.path.normpath(full_target))
pattern = self._get_trie_pattern(dirname)
if pattern:
matches = pattern.matches(
@@ -413,7 +407,7 @@ class DvcIgnoreFilter:
return _no_match(target)
def is_ignored(
self, fs: FileSystem, path: str, ignore_subrepos: bool = True
self, fs: "FileSystem", path: str, ignore_subrepos: bool = True
) -> bool:
# NOTE: can't use self.check_ignore(path).match for now, see
# https://github.com/iterative/dvc/issues/4555
@@ -423,9 +417,9 @@ class DvcIgnoreFilter:
return self.is_ignored_file(path, ignore_subrepos)
if fs.isdir(path):
return self.is_ignored_dir(path, ignore_subrepos)
return self.is_ignored_file(
return self.is_ignored_file(path, ignore_subrepos) or self.is_ignored_dir(
path, ignore_subrepos
) or self.is_ignored_dir(path, ignore_subrepos)
)
def init(path):
+10 -12
View File
@@ -19,18 +19,17 @@ SUBPROJECTS = (
"dvc_objects",
"dvc_render",
"dvc_task",
"dvclive",
"scmrepo",
)
package = "" if PKG is None else f"({PKG})"
package = "" if PKG is None else f" ({PKG})"
def get_dvc_info():
dvc_version = f"DVC version: {__version__}{package}"
info = [
f"DVC version: {__version__} {package}",
"---------------------------------",
f"Platform: Python {platform.python_version()} on "
f"{platform.platform()}",
dvc_version,
"-" * len(dvc_version),
f"Platform: Python {platform.python_version()} on {platform.platform()}",
f"Subprojects:{_get_subprojects()}",
f"Supports:{_get_supported_remotes()}",
]
@@ -41,14 +40,14 @@ def get_dvc_info():
# can't auto-create it, as it might cause issues if the user
# later decides to enable shared cache mode with
# `dvc config cache.shared group`.
if os.path.exists(repo.odb.local.path):
if os.path.exists(repo.cache.local.path):
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
fs_type = get_fs_type(repo.odb.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"))
info.append(f"Caches: {_get_caches(repo.odb)}")
info.append(f"Caches: {_get_caches(repo.cache)}")
info.append(f"Remotes: {_get_remotes(repo.config)}")
root_directory = repo.root_dir
@@ -84,7 +83,7 @@ def _get_remotes(config):
def _get_linktype_support_info(repo):
odb = repo.odb.local
odb = repo.cache.local
links = generic.test_links(
["reflink", "hardlink", "symlink"],
@@ -124,8 +123,7 @@ def _get_supported_remotes():
dependencies = []
for requirement in fs_cls.REQUIRES:
dependencies.append(
f"{requirement} = "
f"{importlib_metadata.version(requirement)}"
f"{requirement} = {importlib_metadata.version(requirement)}"
)
remote_info = scheme
+9 -13
View File
@@ -20,9 +20,7 @@ FAILED_TO_LOCK_MESSAGE = (
"Unable to acquire lock. Most likely another DVC process is running or "
"was terminated abruptly. Check the page {} for other possible reasons "
"and to learn how to resolve this."
).format(
format_link("https://dvc.org/doc/user-guide/troubleshooting#lock-issue")
)
).format(format_link("https://dvc.org/doc/user-guide/troubleshooting#lock-issue"))
class LockError(DvcException):
@@ -61,9 +59,7 @@ class LockBase(ABC):
class LockNoop(LockBase):
def __init__(
self, *args, **kwargs
): # pylint: disable=super-init-not-called
def __init__(self, *args, **kwargs): # pylint: disable=super-init-not-called
self._lock = False
def lock(self):
@@ -107,16 +103,14 @@ class Lock(LockBase):
with Tqdm(
bar_format="{desc}",
disable=not self._friendly,
desc=(
"If DVC froze, see `hardlink_lock` in {}".format(
format_link("https://man.dvc.org/config#core")
)
desc="If DVC froze, see `hardlink_lock` in {}".format(
format_link("https://man.dvc.org/config#core")
),
):
self._lock = zc.lockfile.LockFile(self._lockfile)
except zc.lockfile.LockError:
self._lock_failed = True
raise LockError(FAILED_TO_LOCK_MESSAGE)
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
def lock(self):
retries = 6
@@ -131,6 +125,7 @@ class Lock(LockBase):
if not self.is_locked:
raise DvcException("Unlock called on an unlocked lock")
assert self._lock
self._lock.close()
self._lock = None
@@ -185,14 +180,15 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
try:
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
def _set_claimfile(self):
super()._set_claimfile()
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
filename = hasher.hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
+98 -180
View File
@@ -1,63 +1,55 @@
"""Manages logging configuration for DVC repo."""
import logging
import logging.config
import logging.handlers
import sys
import colorama
from dvc.progress import Tqdm
FOOTER = (
"\n{yellow}Having any troubles?{nc}"
" Hit us up at {blue}https://dvc.org/support{nc},"
" we are always happy to help!"
).format(
blue=colorama.Fore.BLUE,
nc=colorama.Fore.RESET,
yellow=colorama.Fore.YELLOW,
)
def addLoggingLevel(levelName, levelNum, methodName=None):
def add_logging_level(level_name, level_num, method_name=None):
"""
Adds a new logging level to the `logging` module and the
currently configured logging class.
Uses the existing numeric levelNum if already defined.
Uses the existing numeric level_num if already defined.
Based on https://stackoverflow.com/questions/2183233
"""
if methodName is None:
methodName = levelName.lower()
if method_name is None:
method_name = level_name.lower()
# If the level name is already defined as a top-level `logging`
# constant, then adopt the existing numeric level.
if hasattr(logging, levelName):
existingLevelNum = getattr(logging, levelName)
assert isinstance(existingLevelNum, int)
levelNum = existingLevelNum
if hasattr(logging, level_name):
existing_level_num = getattr(logging, level_name)
assert isinstance(existing_level_num, int)
level_num = existing_level_num
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
def log_for_level(self, message, *args, **kwargs):
if self.isEnabledFor(level_num):
# pylint: disable=protected-access
self._log(levelNum, message, args, **kwargs)
self._log(level_num, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
def log_to_root(message, *args, **kwargs):
logging.log(level_num, message, *args, **kwargs)
# getLevelName resolves the numeric log level if already defined,
# otherwise returns a string
if not isinstance(logging.getLevelName(levelName), int):
logging.addLevelName(levelNum, levelName)
if not isinstance(logging.getLevelName(level_name), int):
logging.addLevelName(level_num, level_name)
if not hasattr(logging, levelName):
setattr(logging, levelName, levelNum)
if not hasattr(logging, level_name):
setattr(logging, level_name, level_num)
if not hasattr(logging.getLoggerClass(), methodName):
setattr(logging.getLoggerClass(), methodName, logForLevel)
if not hasattr(logging.getLoggerClass(), method_name):
setattr(logging.getLoggerClass(), method_name, log_for_level)
if not hasattr(logging, methodName):
setattr(logging, methodName, logToRoot)
if not hasattr(logging, method_name):
setattr(logging, method_name, log_to_root)
class LoggingException(Exception):
@@ -66,12 +58,11 @@ class LoggingException(Exception):
super().__init__(msg)
def excludeFilter(level):
class ExcludeLevelFilter(logging.Filter):
def filter(self, record):
return record.levelno < level
def exclude_filter(level: int):
def filter_fn(record: "logging.LogRecord") -> bool:
return record.levelno < level
return ExcludeLevelFilter
return filter_fn
class ColorFormatter(logging.Formatter):
@@ -86,7 +77,8 @@ class ColorFormatter(logging.Formatter):
exception cause, just the message and the traceback.
"""
color_code = {
reset = colorama.Fore.RESET
color_codes = {
"TRACE": colorama.Fore.GREEN,
"DEBUG": colorama.Fore.BLUE,
"WARNING": colorama.Fore.YELLOW,
@@ -94,51 +86,50 @@ class ColorFormatter(logging.Formatter):
"CRITICAL": colorama.Fore.RED,
}
def format(self, record):
def __init__(self, log_colors: bool = True) -> None:
super().__init__()
self.log_colors = log_colors
def format(self, record) -> str: # noqa: A003, C901
record.message = record.getMessage()
msg = self.formatMessage(record)
if record.levelname == "INFO":
if record.levelno == logging.INFO:
return msg
if record.exc_info:
if getattr(record, "tb_only", False):
cause = ""
else:
cause = ": ".join(_iter_causes(record.exc_info[1]))
ei = record.exc_info
if ei:
cause = ""
if not getattr(record, "tb_only", False):
cause = ": ".join(_iter_causes(ei[1]))
sep = " - " if msg and cause else ""
msg = msg + sep + cause
msg = "{message}{separator}{cause}".format(
message=msg or "",
separator=" - " if msg and cause else "",
cause=cause,
)
asctime = ""
if _is_verbose():
asctime = self.formatTime(record, self.datefmt)
if ei and not record.exc_text:
record.exc_text = self.formatException(ei)
if record.exc_text:
if msg[-1:] != "\n":
msg = msg + "\n"
msg = msg + record.exc_text + "\n"
if record.stack_info:
if msg[-1:] != "\n":
msg = msg + "\n"
msg = msg + self.formatStack(record.stack_info) + "\n"
if _is_verbose():
msg += _stack_trace(record.exc_info)
return "{asctime}{color}{levelname}{nc}: {msg}".format(
asctime=self.formatTime(record, self.datefmt),
color=self.color_code[record.levelname],
nc=colorama.Fore.RESET,
levelname=record.levelname,
msg=msg,
)
def formatTime(self, record, datefmt=None):
# only show if current level is set to DEBUG
# also, skip INFO as it is used for UI
if not _is_verbose() or record.levelno == logging.INFO:
return ""
return "{green}{date}{nc} ".format(
green=colorama.Fore.GREEN,
date=super().formatTime(record, datefmt),
nc=colorama.Fore.RESET,
)
level = record.levelname
if self.log_colors:
color = self.color_codes[level]
if asctime:
asctime = color + asctime + self.reset
level = color + level + self.reset
return asctime + (" " if asctime else "") + level + ": " + msg
class LoggerHandler(logging.StreamHandler):
def handleError(self, record):
def handleError(self, record): # noqa: N802
super().handleError(record)
raise LoggingException(record)
@@ -159,9 +150,7 @@ class LoggerHandler(logging.StreamHandler):
pass
msg = self.format(record)
Tqdm.write(
msg, file=self.stream, end=getattr(self, "terminator", "\n")
)
Tqdm.write(msg, file=self.stream, end=getattr(self, "terminator", "\n"))
self.flush()
except (BrokenPipeError, RecursionError):
raise
@@ -171,9 +160,7 @@ class LoggerHandler(logging.StreamHandler):
def _is_verbose():
return (
logging.NOTSET
< logging.getLogger("dvc").getEffectiveLevel()
<= logging.DEBUG
logging.NOTSET < logging.getLogger("dvc").getEffectiveLevel() <= logging.DEBUG
)
@@ -183,36 +170,44 @@ def _iter_causes(exc):
exc = exc.__cause__
def _stack_trace(exc_info):
import traceback
return (
"\n"
"{red}{line}{nc}\n"
"{trace}"
"{red}{line}{nc}".format(
red=colorama.Fore.RED,
line="-" * 60,
trace="".join(traceback.format_exception(*exc_info)),
nc=colorama.Fore.RESET,
)
)
def disable_other_loggers():
logging.captureWarnings(True)
loggerDict = logging.root.manager.loggerDict # pylint: disable=no-member
for logger_name, logger in loggerDict.items():
if logger_name != "dvc" and not logger_name.startswith("dvc."):
logger.disabled = True
def set_loggers_level(level: int = logging.INFO) -> None:
for name in ["dvc", "dvc_objects", "dvc_data"]:
logging.getLogger(name).setLevel(level)
def setup(level: int = logging.INFO) -> None:
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
colorama.init()
formatter = ColorFormatter(log_colors=log_colors and sys.stdout.isatty())
console_info = LoggerHandler(sys.stdout)
console_info.setLevel(logging.INFO)
console_info.setFormatter(formatter)
console_info.addFilter(exclude_filter(logging.WARNING))
console_debug = LoggerHandler(sys.stdout)
console_debug.setLevel(logging.DEBUG)
console_debug.setFormatter(formatter)
console_debug.addFilter(exclude_filter(logging.INFO))
add_logging_level("TRACE", logging.DEBUG - 5)
console_trace = LoggerHandler(sys.stdout)
console_trace.setLevel(logging.TRACE) # type: ignore[attr-defined]
console_trace.setFormatter(formatter)
console_trace.addFilter(exclude_filter(logging.DEBUG))
err_formatter = ColorFormatter(log_colors=log_colors and sys.stderr.isatty())
console_errors = LoggerHandler(sys.stderr)
console_errors.setLevel(logging.WARNING)
console_errors.setFormatter(err_formatter)
for name in ["dvc", "dvc_objects", "dvc_data"]:
logger = logging.getLogger(name)
logger.setLevel(level)
for handler in [console_info, console_debug, console_trace, console_errors]:
logger.addHandler(handler)
if level >= logging.DEBUG:
# Unclosed session errors for asyncio/aiohttp are only available
# on the tracing mode for extensive debug purposes. They are really
@@ -223,80 +218,3 @@ def setup(level: int = logging.INFO) -> None:
# action.
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
addLoggingLevel("TRACE", logging.DEBUG - 5)
logging.config.dictConfig(
{
"version": 1,
"filters": {
"exclude_errors": {"()": excludeFilter(logging.WARNING)},
"exclude_info": {"()": excludeFilter(logging.INFO)},
"exclude_debug": {"()": excludeFilter(logging.DEBUG)},
},
"formatters": {"color": {"()": ColorFormatter}},
"handlers": {
"console_info": {
"class": "dvc.logger.LoggerHandler",
"level": "INFO",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_errors"],
},
"console_debug": {
"class": "dvc.logger.LoggerHandler",
"level": "DEBUG",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_info"],
},
"console_trace": {
"class": "dvc.logger.LoggerHandler",
"level": "TRACE",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_debug"],
},
"console_errors": {
"class": "dvc.logger.LoggerHandler",
"level": "WARNING",
"formatter": "color",
"stream": "ext://sys.stderr",
},
},
"loggers": {
"dvc": {
"level": level,
"handlers": [
"console_info",
"console_debug",
"console_trace",
"console_errors",
],
},
"dvc_objects": {
"level": level,
"handlers": [
"console_info",
"console_debug",
"console_trace",
"console_errors",
],
},
"dvc_data": {
"level": level,
"handlers": [
"console_info",
"console_debug",
"console_trace",
"console_errors",
],
},
},
"disable_existing_loggers": False,
}
)
# NOTE: colorama init needs to go after logging config to avoid potential
# conflicts with libs that modify logging (like tensorflow/abseil)
# https://github.com/iterative/dvc/issues/8387
colorama.init()
+10 -7
View File
@@ -13,10 +13,10 @@ from typing import (
)
from dvc.exceptions import DvcException
from dvc.types import StrPath
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import StrPath
from .backend.base import BaseMachineBackend
@@ -67,16 +67,14 @@ class MachineBackends(Mapping):
initialized = self.initialized.get(key)
if not initialized:
backend = self.backends[key]
initialized = backend(
os.path.join(self.tmp_dir, key), **self.kwargs
)
initialized = backend(os.path.join(self.tmp_dir, key), **self.kwargs)
self.initialized[key] = initialized
return initialized
def __init__(
self,
selected: Optional[Iterable[str]],
tmp_dir: StrPath,
tmp_dir: "StrPath",
**kwargs,
) -> None:
selected = selected or list(self.DEFAULT)
@@ -127,6 +125,7 @@ class MachineManager:
self, repo: "Repo", backends: Optional[Iterable[str]] = None, **kwargs
):
self.repo = repo
assert self.repo.tmp_dir
tmp_dir = os.path.join(self.repo.tmp_dir, "machine")
self.backends = MachineBackends(backends, tmp_dir=tmp_dir, **kwargs)
@@ -167,7 +166,9 @@ class MachineManager:
except KeyError:
from dvc.config import MachineNotFoundError
raise MachineNotFoundError(f"machine '{name}' doesn't exist")
raise MachineNotFoundError( # noqa: B904
f"machine '{name}' doesn't exist"
)
else:
conf = kwargs
return conf
@@ -179,7 +180,9 @@ class MachineManager:
backend = self.CLOUD_BACKENDS[cloud]
return self.backends[backend]
except KeyError:
raise NoMachineError(f"Machine platform '{cloud}' unsupported")
raise NoMachineError( # noqa: B904
f"Machine platform '{cloud}' unsupported"
)
def create(self, name: Optional[str]):
"""Create and start the specified machine instance."""
+1 -3
View File
@@ -21,9 +21,7 @@ class BaseMachineBackend(ABC):
"""Stop and destroy all instances of the specified machine."""
@abstractmethod
def instances(
self, name: Optional[str] = None, **config
) -> Iterator[dict]:
def instances(self, name: Optional[str] = None, **config) -> Iterator[dict]:
"""Iterate over status of all instances of the specified machine."""
def close(self): # noqa: B027
+4 -9
View File
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Iterator, Optional
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
from dvc.exceptions import DvcException
from dvc.utils.fs import makedirs
from .base import BaseMachineBackend
@@ -30,7 +29,7 @@ def _sshfs(resource: dict):
class TerraformBackend(BaseMachineBackend):
def __init__(self, tmp_dir: "StrPath", **kwargs):
super().__init__(tmp_dir, **kwargs)
makedirs(tmp_dir, exist_ok=True)
os.makedirs(tmp_dir, exist_ok=True)
@contextmanager
def make_tpi(self, name: str):
@@ -39,7 +38,7 @@ class TerraformBackend(BaseMachineBackend):
try:
working_dir = os.path.join(self.tmp_dir, name)
makedirs(working_dir, exist_ok=True)
os.makedirs(working_dir, exist_ok=True)
yield TPIBackend(working_dir=working_dir)
except TPIError as exc:
raise DvcException("TPI operation failed") from exc
@@ -57,12 +56,8 @@ class TerraformBackend(BaseMachineBackend):
create = partialmethod(_tpi_func, "create") # type: ignore[assignment]
destroy = partialmethod(_tpi_func, "destroy") # type: ignore[assignment]
instances = partialmethod(
_tpi_func, "instances"
) # type: ignore[assignment]
run_shell = partialmethod(
_tpi_func, "run_shell"
) # type: ignore[assignment]
instances = partialmethod(_tpi_func, "instances") # type: ignore[assignment]
run_shell = partialmethod(_tpi_func, "run_shell") # type: ignore[assignment]
def get_executor_kwargs(self, name: str, **config) -> dict:
with self.make_tpi(name) as tpi:
+214 -183
View File
@@ -1,11 +1,13 @@
import logging
import os
import posixpath
from collections import defaultdict
from operator import itemgetter
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urlparse
from funcy import cached_property, collecting, project
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
from funcy import collecting, first, project
from voluptuous import All, And, Any, Coerce, Length, Lower, Required, SetTo
from dvc import prompt
from dvc.exceptions import (
@@ -17,7 +19,7 @@ from dvc.exceptions import (
MergeError,
RemoteCacheRequiredError,
)
from dvc_data.hashfile import Tree
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
@@ -26,7 +28,7 @@ from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.istextfile import istextfile
from dvc_data.hashfile.meta import Meta
from dvc_data.hashfile.transfer import transfer as otransfer
from dvc_data.index import DataIndexEntry
from dvc_data.hashfile.tree import Tree
from dvc_objects.errors import ObjectFormatError
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
@@ -36,11 +38,13 @@ 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__)
@@ -143,17 +147,17 @@ def _split_dict(d, keys):
def _merge_data(s_list):
d = defaultdict(dict)
d: Dict[str, Dict] = defaultdict(dict)
for key in s_list:
if isinstance(key, str):
d[key].update({})
continue
if not isinstance(key, dict):
raise ValueError(f"'{type(key).__name__}' not supported.")
raise ValueError(f"'{type(key).__name__}' not supported.") # noqa: TRY004
for k, flags in key.items():
if not isinstance(flags, dict):
raise ValueError(
raise ValueError( # noqa: TRY004
f"Expected dict for '{k}', got: '{type(flags).__name__}'"
)
d[k].update(flags)
@@ -203,6 +207,41 @@ def load_from_pipeline(stage, data, typ="outs"):
)
def split_file_meta_from_cloud(entry: Dict) -> Dict:
if remote_name := entry.pop(Meta.PARAM_REMOTE, None):
remote_meta = {}
for key in (
S3_PARAM_CHECKSUM,
HDFS_PARAM_CHECKSUM,
Meta.PARAM_VERSION_ID,
):
if value := entry.pop(key, None):
remote_meta[key] = value
if remote_meta:
entry[Output.PARAM_CLOUD] = {remote_name: remote_meta}
return entry
def merge_file_meta_from_cloud(entry: Dict) -> Dict:
cloud_meta = entry.pop(Output.PARAM_CLOUD, {})
if remote_name := first(cloud_meta):
entry.update(cloud_meta[remote_name])
entry[Meta.PARAM_REMOTE] = remote_name
return entry
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()}
for parts, meta, hi in obj
),
key=itemgetter(key),
)
class OutputDoesNotExistError(DvcException):
def __init__(self, path):
msg = f"output '{path}' does not exist"
@@ -257,6 +296,7 @@ class Output:
PARAM_PERSIST = "persist"
PARAM_REMOTE = "remote"
PARAM_PUSH = "push"
PARAM_CLOUD = "cloud"
METRIC_SCHEMA = Any(
None,
@@ -267,12 +307,12 @@ class Output:
},
)
DoesNotExistError = OutputDoesNotExistError # type: Type[DvcException]
IsNotFileOrDirError = OutputIsNotFileOrDirError # type: Type[DvcException]
IsStageFileError = OutputIsStageFileError # type: Type[DvcException]
IsIgnoredError = OutputIsIgnoredError # type: Type[DvcException]
DoesNotExistError: Type[DvcException] = OutputDoesNotExistError
IsNotFileOrDirError: Type[DvcException] = OutputIsNotFileOrDirError
IsStageFileError: Type[DvcException] = OutputIsStageFileError
IsIgnoredError: Type[DvcException] = OutputIsIgnoredError
def __init__(
def __init__( # noqa: PLR0913
self,
stage,
path,
@@ -283,30 +323,31 @@ class Output:
persist=False,
checkpoint=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
type=None, # noqa: A002, pylint: disable=redefined-builtin
labels=None,
meta=None,
remote=None,
repo=None,
fs_config=None,
files: List[Dict[str, Any]] = None,
files: Optional[List[Dict[str, Any]]] = None,
push: bool = True,
):
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
)
self.repo = stage.repo if not repo and stage else repo
meta = Meta.from_dict(info or {})
meta_d = merge_file_meta_from_cloud(info or {})
meta = Meta.from_dict(meta_d)
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
# set based on whether or not path is versioned
fs_kwargs = {"version_aware": True} if meta.version_id else {}
fs_kwargs = {}
if meta.version_id or files:
fs_kwargs["version_aware"] = True
if fs_config is not None:
fs_kwargs.update(**fs_config)
fs_cls, fs_config, fs_path = get_cloud_fs(
self.repo, url=path, **fs_kwargs
)
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path, **fs_kwargs)
self.fs = fs_cls(**fs_config)
if (
@@ -315,7 +356,7 @@ class Output:
and isinstance(stage.repo.fs, LocalFileSystem)
and path_isin(path, stage.repo.root_dir)
):
self.def_path = relpath(path, stage.wdir)
self.def_path: str = relpath(path, stage.wdir)
self.fs = stage.repo.fs
else:
self.def_path = path
@@ -339,6 +380,9 @@ class Output:
# should be absolute and don't contain remote:// refs.
self.stage = stage
self.meta = meta
if files is not None:
files = [merge_file_meta_from_cloud(f) for f in files]
self.files = files
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
@@ -350,6 +394,7 @@ class Output:
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj: Optional["HashFile"] = None
self.odb: Optional["HashFileDB"] = None
self.remote = remote
if self.fs.version_aware:
@@ -367,7 +412,23 @@ class Output:
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
if self.meta.nfiles or self.hash_info and self.hash_info.isdir:
self._compute_meta_hash_info_from_files()
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")
self.odb.add(tree.path, tree.fs, tree.hash_info.value)
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.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":
md5 = getattr(self.meta, "md5", None)
@@ -445,17 +506,17 @@ class Output:
return self.use_cache or self.stage.is_repo_import
@property
def odb(self):
def cache(self):
odb_name = "repo" if self.is_in_repo else self.protocol
odb = getattr(self.repo.odb, odb_name)
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
@property
def cache_path(self):
return self.odb.fs.unstrip_protocol(
self.odb.oid_to_path(self.hash_info.value)
return self.cache.fs.unstrip_protocol(
self.cache.oid_to_path(self.hash_info.value)
)
def get_hash(self):
@@ -464,9 +525,9 @@ class Output:
def _get_hash_meta(self):
if self.use_cache:
odb = self.odb
odb = self.cache
else:
odb = self.repo.odb.local
odb = self.repo.cache.local
_, meta, obj = build(
odb,
self.fs_path,
@@ -486,10 +547,9 @@ class Output:
return self.hash_info.isdir
def _is_path_dvcignore(self, path) -> bool:
if not self.IS_DEPENDENCY and self.dvcignore:
if self.dvcignore.is_ignored(self.fs, path, ignore_subrepos=False):
return True
return False
if self.IS_DEPENDENCY or not self.dvcignore:
return False
return self.dvcignore.is_ignored(self.fs, path, ignore_subrepos=False)
@property
def exists(self):
@@ -509,36 +569,6 @@ class Output:
key = self.fs.path.parts(no_drive)[1:]
return workspace, key
def get_entry(self) -> "DataIndexEntry":
from dvc.config import NoRemoteError
try:
remote = self.repo.cloud.get_remote_odb(self.remote)
except NoRemoteError:
remote = None
if self.files and not self.obj:
self.obj = self.get_obj()
entry = DataIndexEntry(
meta=self.meta,
obj=self.obj,
hash_info=self.hash_info,
odb=self.odb,
cache=self.odb,
remote=remote,
)
if self.stage.is_import and not self.stage.is_repo_import:
dep = self.stage.deps[0]
entry.fs = dep.fs
entry.path = dep.fs_path
entry.meta = dep.meta
if not entry.obj:
if not dep.obj and dep.files:
dep.obj = dep.get_obj()
entry.obj = dep.obj
return entry
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -551,7 +581,7 @@ class Output:
return True
try:
ocheck(self.odb, obj)
ocheck(self.cache, obj)
return False
except (FileNotFoundError, ObjectFormatError):
return True
@@ -561,7 +591,7 @@ class Output:
return self.meta.version_id != self.get_meta().version_id
return False
def workspace_status(self):
def workspace_status(self) -> Dict[str, str]:
if not self.exists:
return {str(self): "deleted"}
@@ -573,40 +603,40 @@ class Output:
return {}
def status(self):
def status(self) -> Dict[str, str]:
if self.hash_info and self.use_cache and self.changed_cache():
return {str(self): "not in cache"}
return self.workspace_status()
def changed(self):
def changed(self) -> bool:
status = self.status()
logger.debug(str(status))
return bool(status)
@property
def dvcignore(self):
def dvcignore(self) -> Optional["DvcIgnoreFilter"]:
if self.fs.protocol == "local":
return self.repo.dvcignore
return None
@property
def is_empty(self):
def is_empty(self) -> bool:
return self.fs.is_empty(self.fs_path)
def isdir(self):
def isdir(self) -> bool:
if self._is_path_dvcignore(self.fs_path):
return False
return self.fs.isdir(self.fs_path)
def isfile(self):
def isfile(self) -> bool:
if self._is_path_dvcignore(self.fs_path):
return False
return self.fs.isfile(self.fs_path)
# pylint: disable=no-member
def ignore(self):
def ignore(self) -> None:
if not self.use_scm_ignore:
return
@@ -615,7 +645,7 @@ class Output:
self.repo.scm_context.ignore(self.fspath)
def ignore_remove(self):
def ignore_remove(self) -> None:
if not self.use_scm_ignore:
return
@@ -623,11 +653,11 @@ class Output:
# pylint: enable=no-member
def save(self):
def save(self) -> None:
if not self.exists:
raise self.DoesNotExistError(self)
if not self.isfile and not self.isdir:
if not self.isfile() and not self.isdir():
raise self.IsNotFileOrDirError(self)
if self.is_empty:
@@ -640,7 +670,7 @@ class Output:
if self.use_cache:
_, self.meta, self.obj = build(
self.odb,
self.cache,
self.fs_path,
self.fs,
self.hash_name,
@@ -648,7 +678,7 @@ class Output:
)
else:
_, self.meta, self.obj = build(
self.repo.odb.local,
self.repo.cache.local,
self.fs_path,
self.fs,
self.hash_name,
@@ -656,18 +686,16 @@ class Output:
dry_run=True,
)
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
)
logger.debug("Output '%s' doesn't use cache. Skipping saving.", self)
self.hash_info = self.obj.hash_info
self.files = None
def set_exec(self):
def set_exec(self) -> None:
if self.isfile() and self.meta.isexec:
self.odb.set_exec(self.fs_path)
self.cache.set_exec(self.fs_path)
def _checkout(self, *args, **kwargs):
def _checkout(self, *args, **kwargs) -> Optional[bool]:
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
from dvc_data.hashfile.checkout import LinkError, PromptError
@@ -675,13 +703,13 @@ class Output:
try:
return checkout(*args, **kwargs)
except PromptError as exc:
raise ConfirmRemoveError(exc.path)
raise ConfirmRemoveError(exc.path) # noqa: B904
except LinkError as exc:
raise CacheLinkError([exc.path])
raise CacheLinkError([exc.path]) # noqa: B904
except _CheckoutError as exc:
raise CheckoutError(exc.paths)
raise CheckoutError(exc.paths, {}) # noqa: B904
def commit(self, filter_info=None):
def commit(self, filter_info=None, relink=True) -> None:
if not self.exists:
raise self.DoesNotExistError(self)
@@ -689,15 +717,16 @@ class Output:
if self.use_cache:
granular = (
self.is_dir_checksum
and filter_info
and filter_info != self.fs_path
self.is_dir_checksum and filter_info and filter_info != self.fs_path
)
# NOTE: trying to use hardlink during transfer only if we will be
# relinking later
hardlink = relink
if granular:
obj = self._commit_granular_dir(filter_info)
obj = self._commit_granular_dir(filter_info, hardlink)
else:
staging, _, obj = build(
self.odb,
self.cache,
filter_info or self.fs_path,
self.fs,
self.hash_name,
@@ -705,53 +734,61 @@ class Output:
)
otransfer(
staging,
self.odb,
self.cache,
{obj.hash_info},
shallow=False,
hardlink=True,
hardlink=hardlink,
)
self._checkout(
filter_info or self.fs_path,
self.fs,
obj,
self.odb,
relink=True,
state=self.repo.state,
prompt=prompt.confirm,
)
self.set_exec()
if relink:
self._checkout(
filter_info or self.fs_path,
self.fs,
obj,
self.cache,
relink=True,
state=self.repo.state,
prompt=prompt.confirm,
)
self.set_exec()
def _commit_granular_dir(self, filter_info):
prefix = self.fs.path.parts(
self.fs.path.relpath(filter_info, self.fs_path)
)
staging, _, save_obj = build(
self.odb,
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
staging, _, obj = build(
self.cache,
self.fs_path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
)
save_obj = save_obj.filter(prefix)
checkout_obj = save_obj.get_obj(self.odb, prefix)
assert isinstance(obj, Tree)
save_obj = obj.filter(prefix)
assert isinstance(save_obj, Tree)
checkout_obj = save_obj.get_obj(self.cache, prefix)
otransfer(
staging,
self.odb,
self.cache,
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
shallow=True,
hardlink=True,
hardlink=hardlink,
)
return checkout_obj
def dumpd(self, **kwargs):
meta = self.meta.to_dict()
meta.pop("isdir", None)
ret = {**self.hash_info.to_dict(), **meta}
def dumpd(self, **kwargs): # noqa: C901, PLR0912
ret: Dict[str, Any] = {}
with_files = (
(not self.IS_DEPENDENCY or self.stage.is_import)
and self.hash_info.isdir
and (kwargs.get("with_files") or self.files is not None)
)
if not with_files:
meta_d = self.meta.to_dict()
meta_d.pop("isdir", None)
ret.update(self.hash_info.to_dict())
ret.update(split_file_meta_from_cloud(meta_d))
if self.is_in_repo:
path = self.fs.path.as_posix(
relpath(self.fs_path, self.stage.wdir)
)
path = self.fs.path.as_posix(relpath(self.fs_path, self.stage.wdir))
else:
path = self.def_path
@@ -762,12 +799,12 @@ class Output:
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if (
isinstance(self.metric, dict)
and self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if self.metric:
ret[self.PARAM_METRIC] = self.metric
@@ -787,24 +824,19 @@ class Output:
if not self.can_push:
ret[self.PARAM_PUSH] = self.can_push
if (
(not self.IS_DEPENDENCY or self.stage.is_import)
and self.hash_info.isdir
and (kwargs.get("with_files") or self.files is not None)
):
if self.obj:
obj = self.obj
else:
obj = self.get_obj()
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
if with_files:
obj = self.obj or self.get_obj()
if obj:
assert isinstance(obj, Tree)
ret[self.PARAM_FILES] = [
split_file_meta_from_cloud(f)
for f in _serialize_tree_obj_to_files(obj)
]
return ret
def verify_metric(self):
if self.fs.protocol != "local":
raise DvcException(
f"verify metric is not supported for {self.protocol}"
)
raise DvcException(f"verify metric is not supported for {self.protocol}")
if not self.metric:
return
@@ -827,16 +859,15 @@ class Output:
obj: Optional["HashFile"] = None
if self.obj:
obj = self.obj
elif self.files:
tree = Tree.from_list(self.files, hash_name=self.hash_name)
tree.digest()
obj = tree
elif self.hash_info:
if self.files:
tree = Tree.from_list(self.files, hash_name=self.hash_name)
tree.digest()
obj = tree
else:
try:
obj = oload(self.odb, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
return None
try:
obj = oload(self.cache, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
return None
else:
return None
@@ -844,25 +875,24 @@ class Output:
fs_path = self.fs.path
if filter_info and filter_info != self.fs_path:
prefix = fs_path.relparts(filter_info, self.fs_path)
obj = obj.get_obj(self.odb, prefix)
assert isinstance(obj, Tree)
obj = obj.get_obj(self.cache, prefix)
return obj
def checkout(
self,
force=False,
force: bool = False,
progress_callback: "Callback" = DEFAULT_CALLBACK,
relink=False,
filter_info=None,
allow_missing=False,
checkpoint_reset=False,
relink: bool = False,
filter_info: Optional[str] = None,
allow_missing: bool = False,
checkpoint_reset: bool = False,
**kwargs,
):
) -> Optional[Tuple[bool, Optional[bool]]]:
if not self.use_cache:
if progress_callback != DEFAULT_CALLBACK:
progress_callback.relative_update(
self.get_files_number(filter_info)
)
progress_callback.relative_update(self.get_files_number(filter_info))
return None
obj = self.get_obj(filter_info=filter_info)
@@ -882,7 +912,7 @@ class Output:
filter_info or self.fs_path,
self.fs,
obj,
self.odb,
self.cache,
force=force,
progress_callback=progress_callback,
relink=relink,
@@ -898,7 +928,7 @@ class Output:
return added, False if added else modified
def remove(self, ignore_remove=False):
self.fs.remove(self.fs_path)
self.fs.remove(self.fs_path, recursive=True)
if self.protocol != Schemes.LOCAL:
return
@@ -923,7 +953,7 @@ class Output:
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
):
if odb is None:
odb = self.odb
odb = self.cache
cls, config, from_info = get_cloud_fs(self.repo, url=source)
from_fs = cls(**config)
@@ -975,15 +1005,15 @@ class Output:
def unprotect(self):
if self.exists:
self.odb.unprotect(self.fs_path)
self.cache.unprotect(self.fs_path)
def get_dir_cache(self, **kwargs):
if not self.is_dir_checksum:
raise DvcException("cannot get dir cache for file checksum")
obj = self.odb.get(self.hash_info.value)
obj = self.cache.get(self.hash_info.value)
try:
ocheck(self.odb, obj)
ocheck(self.cache, obj)
except FileNotFoundError:
if self.remote:
kwargs["remote"] = self.remote
@@ -993,7 +1023,7 @@ class Output:
return self.obj
try:
self.obj = oload(self.odb, self.hash_info)
self.obj = oload(self.cache, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
self.obj = None
@@ -1012,7 +1042,7 @@ class Output:
logger.debug("failed to pull cache for '%s'", self)
try:
ocheck(self.odb, self.odb.get(self.hash_info.value))
ocheck(self.cache, self.cache.get(self.hash_info.value))
except FileNotFoundError:
msg = (
"Missing cache for directory '{}'. "
@@ -1020,22 +1050,21 @@ class Output:
"Would you like to continue? Use '-f' to force."
)
if not force and not prompt.confirm(msg.format(self.fs_path)):
raise CollectCacheError(
raise CollectCacheError( # noqa: B904
"unable to fully collect used cache"
" without cache for directory '{}'".format(self)
)
return None
obj = self.get_obj()
assert obj is None or isinstance(obj, Tree)
if filter_info and filter_info != self.fs_path:
assert obj
prefix = self.fs.path.parts(
self.fs.path.relpath(filter_info, self.fs_path)
)
obj = obj.filter(prefix)
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
return obj.filter(prefix)
return obj
def get_used_objs(
def get_used_objs( # noqa: C901, PLR0911
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return filtered set of used object IDs for this out."""
@@ -1071,12 +1100,13 @@ class Output:
logger.warning(msg)
return {}
obj: Optional["HashFile"]
if self.is_dir_checksum:
obj = self._collect_used_dir_cache(**kwargs)
else:
obj = self.get_obj(filter_info=kwargs.get("filter_info"))
if not obj:
obj = self.odb.get(self.hash_info.value)
obj = self.cache.get(self.hash_info.value)
if not obj:
return {}
@@ -1137,14 +1167,10 @@ class Output:
other.pop(opt, None)
if my != other:
raise MergeError(
"unable to auto-merge outputs with different options"
)
raise MergeError("unable to auto-merge outputs with different options")
if not out.is_dir_checksum:
raise MergeError(
"unable to auto-merge outputs that are not directories"
)
raise MergeError("unable to auto-merge outputs that are not directories")
def merge(self, ancestor, other, allowed=None):
from dvc_data.hashfile.tree import MergeError as TreeMergeError
@@ -1163,7 +1189,7 @@ class Output:
try:
merged = merge(
self.odb,
self.cache,
ancestor_info,
self.hash_info,
other.hash_info,
@@ -1172,12 +1198,12 @@ class Output:
except TreeMergeError as exc:
raise MergeError(str(exc)) from exc
self.odb.add(merged.path, merged.fs, merged.oid)
self.cache.add(merged.path, merged.fs, merged.oid)
self.hash_info = merged.hash_info
self.files = None
self.meta = Meta(
size=du(self.odb, merged),
size=du(self.cache, merged),
nfiles=len(merged),
)
@@ -1219,7 +1245,8 @@ class Output:
if not self.obj or not other.hash_info.isdir:
return
other_obj = other.obj if other.obj is not None else other.get_obj()
assert isinstance(self.obj, Tree) and isinstance(other_obj, Tree)
assert isinstance(self.obj, Tree)
assert isinstance(other_obj, Tree)
updated = update_meta(self.obj, other_obj)
assert updated.hash_info == self.obj.hash_info
self.obj = updated
@@ -1233,6 +1260,8 @@ META_SCHEMA = {
Meta.PARAM_VERSION_ID: str,
}
CLOUD_SCHEMA = All({str: {**META_SCHEMA, **CHECKSUMS_SCHEMA}}, Length(max=1))
ARTIFACT_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
@@ -1240,12 +1269,14 @@ ARTIFACT_SCHEMA = {
Output.PARAM_PLOT: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Output.PARAM_CLOUD: CLOUD_SCHEMA,
}
DIR_FILES_SCHEMA: Dict[str, Any] = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Tree.PARAM_RELPATH): str,
Output.PARAM_CLOUD: CLOUD_SCHEMA,
}
SCHEMA = {
+33 -37
View File
@@ -14,10 +14,11 @@ from typing import (
Union,
)
from funcy import cached_property, collecting, first, isa, join, reraise
from funcy import collecting, first, isa, join, reraise
from dvc.exceptions import DvcException
from dvc.parsing.interpolate import ParseError
from dvc.utils.objects import cached_property
from .context import (
Context,
@@ -25,7 +26,6 @@ from .context import (
KeyNotInContext,
MergeError,
Node,
SeqOrMap,
VarsAlreadyLoaded,
)
from .interpolate import (
@@ -39,6 +39,10 @@ if TYPE_CHECKING:
from typing import NoReturn
from dvc.repo import Repo
from dvc.types import DictStrAny
from .context import SeqOrMap
logger = logging.getLogger(__name__)
@@ -52,7 +56,6 @@ DO_KWD = "do"
DEFAULT_PARAMS_FILE = "params.yaml"
JOIN = "@"
DictStr = Dict[str, Any]
class ResolveError(DvcException):
@@ -69,9 +72,7 @@ def _format_preamble(msg: str, path: str, spacing: str = " ") -> str:
def format_and_raise(exc: Exception, msg: str, path: str) -> "NoReturn":
spacing = (
"\n"
if isinstance(exc, (ParseError, MergeError, VarsAlreadyLoaded))
else " "
"\n" if isinstance(exc, (ParseError, MergeError, VarsAlreadyLoaded)) else " "
)
message = _format_preamble(msg, path, spacing) + str(exc)
@@ -81,7 +82,7 @@ def format_and_raise(exc: Exception, msg: str, path: str) -> "NoReturn":
def _reraise_err(
exc_cls: Type[Exception], *args, from_exc: Exception = None
exc_cls: Type[Exception], *args, from_exc: Optional[Exception] = None
) -> "NoReturn":
err = exc_cls(*args)
if from_exc and logger.isEnabledFor(logging.DEBUG):
@@ -90,7 +91,7 @@ def _reraise_err(
def check_syntax_errors(
definition: DictStr, name: str, path: str, where: str = "stages"
definition: "DictStrAny", name: str, path: str, where: str = "stages"
):
for key, d in definition.items():
try:
@@ -109,12 +110,11 @@ def split_foreach_name(name: str) -> Tuple[str, Optional[str]]:
return group, first(keys)
def check_interpolations(data: DictStr, where: str, path: str):
def func(s: DictStr) -> None:
def check_interpolations(data: "DictStrAny", where: str, path: str):
def func(s: "DictStrAny") -> None:
if is_interpolated_string(s):
raise ResolveError(
_format_preamble(f"'{where}'", path)
+ "interpolating is not allowed"
_format_preamble(f"'{where}'", path) + "interpolating is not allowed"
)
return recurse(func)(data)
@@ -124,7 +124,7 @@ Definition = Union["ForeachDefinition", "EntryDefinition"]
def make_definition(
resolver: "DataResolver", name: str, definition: DictStr, **kwargs
resolver: "DataResolver", name: str, definition: "DictStrAny", **kwargs
) -> Definition:
args = resolver, resolver.context, name, definition
if FOREACH_KWD in definition:
@@ -185,24 +185,22 @@ class DataResolver:
def resolve(self):
"""Used for testing purposes, otherwise use resolve_one()."""
data = join(map(self.resolve_one, self.get_keys()))
logger.trace( # type: ignore[attr-defined]
"Resolved dvc.yaml:\n%s", data
)
logger.trace("Resolved dvc.yaml:\n%s", data) # type: ignore[attr-defined]
return {STAGES_KWD: data}
def has_key(self, key: str):
return self._has_group_and_key(*split_foreach_name(key))
def _has_group_and_key(self, group: str, key: str = None):
def _has_group_and_key(self, group: str, key: Optional[str] = None):
try:
definition = self.definitions[group]
except KeyError:
return False
if key:
return isinstance(
definition, ForeachDefinition
) and definition.has_member(key)
return isinstance(definition, ForeachDefinition) and definition.has_member(
key
)
return not isinstance(definition, ForeachDefinition)
@collecting
@@ -223,7 +221,7 @@ class EntryDefinition:
resolver: DataResolver,
context: Context,
name: str,
definition: DictStr,
definition: "DictStrAny",
where: str = STAGES_KWD,
):
self.resolver = resolver
@@ -235,7 +233,7 @@ class EntryDefinition:
self.where = where
def _resolve_wdir(
self, context: Context, name: str, wdir: str = None
self, context: Context, name: str, wdir: Optional[str] = None
) -> str:
if not wdir:
return self.wdir
@@ -252,7 +250,7 @@ class EntryDefinition:
except ContextError as exc:
format_and_raise(exc, f"stage '{self.name}'", self.relpath)
def resolve_stage(self, skip_checks: bool = False) -> DictStr:
def resolve_stage(self, skip_checks: bool = False) -> "DictStrAny":
context = self.context
name = self.name
if not skip_checks:
@@ -300,15 +298,13 @@ class EntryDefinition:
def _resolve(
self, context: "Context", value: Any, key: str, skip_checks: bool
) -> DictStr:
) -> "DictStrAny":
try:
return context.resolve(
value, skip_interpolation_checks=skip_checks, key=key
)
except (ParseError, KeyNotInContext) as exc:
format_and_raise(
exc, f"'{self.where}.{self.name}.{key}'", self.relpath
)
format_and_raise(exc, f"'{self.where}.{self.name}.{key}'", self.relpath)
class IterationPair(NamedTuple):
@@ -322,7 +318,7 @@ class ForeachDefinition:
resolver: DataResolver,
context: Context,
name: str,
definition: DictStr,
definition: "DictStrAny",
where: str = STAGES_KWD,
):
self.resolver = resolver
@@ -347,13 +343,11 @@ class ForeachDefinition:
def resolved_iterable(self):
return self._resolve_foreach_data()
def _resolve_foreach_data(self) -> SeqOrMap:
def _resolve_foreach_data(self) -> "SeqOrMap":
try:
iterable = self.context.resolve(self.foreach_data, unwrap=False)
except (ContextError, ParseError) as exc:
format_and_raise(
exc, f"'{self.where}.{self.name}.foreach'", self.relpath
)
format_and_raise(exc, f"'{self.where}.{self.name}.foreach'", self.relpath)
# foreach data can be a resolved dictionary/list.
self._check_is_map_or_seq(iterable)
@@ -378,8 +372,10 @@ class ForeachDefinition:
if warn_for:
linking_verb = "is" if len(warn_for) == 1 else "are"
logger.warning(
"%s %s already specified, "
"will be overwritten for stages generated from '%s'",
(
"%s %s already specified, "
"will be overwritten for stages generated from '%s'"
),
" and ".join(warn_for),
linking_verb,
self.name,
@@ -415,13 +411,13 @@ class ForeachDefinition:
def _generate_name(self, key: str) -> str:
return f"{self.name}{JOIN}{key}"
def resolve_all(self) -> DictStr:
def resolve_all(self) -> "DictStrAny":
return join(map(self.resolve_one, self.normalized_iterable))
def resolve_one(self, key: str) -> DictStr:
def resolve_one(self, key: str) -> "DictStrAny":
return self._each_iter(key)
def _each_iter(self, key: str) -> DictStr:
def _each_iter(self, key: str) -> "DictStrAny":
err_message = f"Could not find '{key}' in foreach group '{self.name}'"
with reraise(KeyError, EntryNotFound(err_message)):
value = self.normalized_iterable[key]
+29 -39
View File
@@ -48,9 +48,7 @@ class MergeError(ContextError):
self.key = key
to_node = into[key]
if not isinstance(to_node, Node) or not isinstance(new, Node):
super().__init__(
f"cannot merge '{key}' as it already exists in {into}"
)
super().__init__(f"cannot merge '{key}' as it already exists in {into}")
return
assert isinstance(to_node, Node)
@@ -138,9 +136,7 @@ class Node:
@dataclass
class Value(Node):
_value: Any
meta: Meta = field(
compare=False, default_factory=_default_meta, repr=False
)
meta: Meta = field(compare=False, default_factory=_default_meta, repr=False)
def __repr__(self):
return repr(self._value)
@@ -159,7 +155,7 @@ class Value(Node):
PRIMITIVES = (int, float, str, bytes, bool)
class Container(Node, ABC): # noqa: B024
class Container(Node, ABC):
meta: Meta
data: Union[list, dict]
_key_transform = staticmethod(identity)
@@ -172,7 +168,7 @@ class Container(Node, ABC): # noqa: B024
return self._convert_with_meta(value, meta)
@staticmethod
def _convert_with_meta(value, meta: Meta = None):
def _convert_with_meta(value, meta: Optional[Meta] = None):
if value is None or isinstance(value, PRIMITIVES):
assert meta
return Value(value, meta=meta)
@@ -182,10 +178,7 @@ class Container(Node, ABC): # noqa: B024
assert meta
container = CtxDict if isinstance(value, dict) else CtxList
return container(value, meta=meta)
msg = (
"Unsupported value of type "
f"'{type(value).__name__}' in '{meta}'"
)
msg = f"Unsupported value of type '{type(value).__name__}' in '{meta}'"
raise TypeError(msg)
def __repr__(self):
@@ -219,16 +212,14 @@ class Container(Node, ABC): # noqa: B024
try:
d = self[index]
except LookupError as exc:
raise ValueError(
f"Could not find '{index}' in {self.data}"
) from exc
raise ValueError(f"Could not find '{index}' in {self.data}") from exc
if not rems:
return d
rem = rems[0]
if not isinstance(d, Container):
raise ValueError(
raise ValueError( # noqa: TRY004
f"{index} is a primitive value, cannot get '{rem}'"
)
return d.select(rem)
@@ -240,7 +231,7 @@ class Container(Node, ABC): # noqa: B024
class CtxList(Container, MutableSequence):
_key_transform = staticmethod(int)
def __init__(self, values: Sequence, meta: Meta = None):
def __init__(self, values: Sequence, meta: Optional[Meta] = None):
super().__init__(meta=meta)
self.data: list = []
self.extend(values)
@@ -263,7 +254,12 @@ class CtxList(Container, MutableSequence):
class CtxDict(Container, MutableMapping):
def __init__(self, mapping: Mapping = None, meta: Meta = None, **kwargs):
def __init__(
self,
mapping: Optional[Mapping] = None,
meta: Optional[Meta] = None,
**kwargs,
):
super().__init__(meta=meta)
self.data: dict = {}
@@ -301,7 +297,7 @@ class Context(CtxDict):
"""
super().__init__(*args, **kwargs)
self._track = False
self._tracked_data = defaultdict(dict)
self._tracked_data: Dict[str, Dict] = defaultdict(dict)
self.imports = {}
self._reserved_keys = {}
@@ -354,7 +350,7 @@ class Context(CtxDict):
@classmethod
def load_from(
cls, fs, path: str, select_keys: List[str] = None
cls, fs, path: str, select_keys: Optional[List[str]] = None
) -> "Context":
from dvc.utils.serialize import load_path
@@ -375,9 +371,7 @@ class Context(CtxDict):
data = {key: data[key] for key in select_keys}
except KeyError as exc:
key, *_ = exc.args
raise ParamsLoadError(
f"could not find '{key}' in '{path}'"
) from exc
raise ParamsLoadError(f"could not find '{key}' in '{path}'") from exc
meta = Meta(source=path, local=False)
ctx = cls(data, meta=meta)
@@ -414,27 +408,27 @@ class Context(CtxDict):
self.imports[path].extend(cp)
def check_loaded(self, path, item, keys):
if not keys and isinstance(self.imports[path], list):
imported = self.imports[path]
if not keys and isinstance(imported, list):
raise VarsAlreadyLoaded(
f"cannot load '{item}' as it's partially loaded already"
)
elif keys and self.imports[path] is None:
if keys and imported is None:
raise VarsAlreadyLoaded(
f"cannot partially load '{item}' as it's already loaded."
)
elif isinstance(self.imports[path], list):
if not set(keys).isdisjoint(set(self.imports[path])):
raise VarsAlreadyLoaded(
f"cannot load '{item}' as it's partially loaded already"
)
if isinstance(imported, list) and set(keys) & set(imported):
raise VarsAlreadyLoaded(
f"cannot load '{item}' as it's partially loaded already"
)
def load_from_vars(
self,
fs,
vars_: List,
wdir: str,
stage_name: str = None,
default: str = None,
stage_name: Optional[str] = None,
default: Optional[str] = None,
):
if default:
to_import = fs.path.join(wdir, default)
@@ -475,14 +469,12 @@ class Context(CtxDict):
But for now, just `item` and `key`, this should do.
"""
# using dict to make the error messages ordered
new = dict.fromkeys(
[key for key in keys if key not in self._reserved_keys]
)
new = dict.fromkeys([key for key in keys if key not in self._reserved_keys])
self._reserved_keys.update(new)
try:
yield
finally:
for key in new.keys():
for key in new:
self._reserved_keys.pop(key)
@contextmanager
@@ -537,9 +529,7 @@ class Context(CtxDict):
if is_exact_string(src, matches):
# replace "${enabled}", if `enabled` is a boolean, with it's actual
# value rather than it's string counterparts.
expr = get_expression(
matches[0], skip_checks=skip_interpolation_checks
)
expr = get_expression(matches[0], skip_checks=skip_interpolation_checks)
value = self.select(expr, unwrap=unwrap)
validate_value(value, key)
return value
+8 -15
View File
@@ -92,20 +92,18 @@ def _(obj: bool):
@to_str.register(dict)
def _(obj: dict):
def _(obj: dict): # noqa: C901
from dvc.config import Config
config = Config().get("parsing", {})
result = ""
for k, v in flatten(obj).items():
if isinstance(v, bool):
if v:
result += f"--{k} "
else:
if config.get("bool", "store_true") == "boolean_optional":
result += f"--no-{k} "
elif config.get("bool", "store_true") == "boolean_optional":
result += f"--no-{k} "
elif isinstance(v, str):
result += f"--{k} {escape_str(v)} "
@@ -115,9 +113,7 @@ def _(obj: dict):
if isinstance(i, str):
i = escape_str(i)
elif isinstance(i, Iterable):
raise ParseError(
f"Cannot interpolate nested iterable in '{k}'"
)
raise ParseError(f"Cannot interpolate nested iterable in '{k}'")
if config.get("list", "nargs") == "append":
result += f"--{k} {i} "
@@ -186,7 +182,7 @@ def parse_expr(s: str):
result = get_parser().parseString(s, parseAll=True)
except ParseException as exc:
format_and_raise_parse_error(exc)
raise AssertionError("unreachable")
raise AssertionError("unreachable") # noqa: B904
joined = result.asList()
assert len(joined) == 1
@@ -204,12 +200,9 @@ def validate_value(value, key):
not_primitive = value is not None and not isinstance(value, PRIMITIVES)
not_foreach = key is not None and "foreach" not in key
if not_primitive and not_foreach:
if isinstance(value, dict):
if key == "cmd":
return True
raise ParseError(
f"Cannot interpolate data of type '{type(value).__name__}'"
)
if isinstance(value, dict) and key == "cmd":
return True
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
def str_interpolate(

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