Compare commits

...
100 Commits
Author SHA1 Message Date
Staszek Markowski d086bbbd3a exp: add support for --no-hydra flag (#10866) 2025-12-02 16:05:13 +05:45
Saugat Pachhai (सौगात) 046d2fd72e update copyright 2025-12-01 16:54:40 +05:45
skshetry cb96cf1487 fix name (#10920) 2025-12-01 15:52:37 +05:45
dependabot[bot] cf5168af74 build(deps-dev): bump mypy from 1.18.2 to 1.19.0 (#10919)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 12:02:39 +05:45
Saugat Pachhai (सौगात) dc123102cc fix tests 2025-11-27 09:00:19 +05:45
Saugat Pachhai (सौगात) 438259cc70 fix repository owner name to treeverse 2025-11-27 08:54:23 +05:45
pre-commit-ci[bot] 7b978fec5b [pre-commit.ci] pre-commit autoupdate (#10918)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-26 23:24:34 +05:45
dependabot[bot] 9f901e57a2 build(deps): bump actions/checkout from 5 to 6 (#10916)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-21 09:03:04 +05:45
pre-commit-ci[bot] ab5ac17925 [pre-commit.ci] pre-commit autoupdate (#10914)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-18 18:23:58 +05:45
pre-commit-ci[bot] 72abd013cf [pre-commit.ci] pre-commit autoupdate (#10908)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-11 10:21:29 +05:45
pre-commit-ci[bot] fa5c33ed24 [pre-commit.ci] pre-commit autoupdate (#10906)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-04 13:56:29 +05:45
pre-commit-ci[bot] 9dda30b7a0 [pre-commit.ci] pre-commit autoupdate (#10901)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-28 09:18:51 +05:45
skshetry 19c146e392 fix(data-status): iterate/expand index upto tracked directory in shallow mode (#10900) 2025-10-27 17:06:07 +00:00
dependabot[bot] 749c22bbcb build(deps): bump actions/upload-artifact from 4 to 5 (#10897)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 09:18:20 +05:45
dependabot[bot] 19a0bf48f1 build(deps): bump actions/download-artifact from 5 to 6 (#10898)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 09:18:08 +05:45
skshetry bda614baa8 support Python 3.14 (#10896) 2025-10-24 19:26:50 +05:45
pre-commit-ci[bot] d6fa8b67ff [pre-commit.ci] pre-commit autoupdate (#10894)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-21 09:30:04 +05:45
skshetry 498a4d5d3a move: fix error message when dst stage file exists (#10893) 2025-10-17 19:07:51 +05:45
pre-commit-ci[bot] b74711d5fe [pre-commit.ci] pre-commit autoupdate (#10892)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-14 13:05:03 +05:45
skshetry 61d5de54e5 support moving missing file keeping the metadata intact (#10890) 2025-10-13 05:34:14 +00:00
skshetry ca7454980d move: support moving output from import/import-url stages (#10889) 2025-10-12 17:28:41 +00:00
skshetry 8912ca7d2b exp: add a retry when removing temporary refs to avoid FileLocked errors (#10840) 2025-10-12 10:05:21 +05:45
dependabot[bot] f13ae75a45 build(deps): bump astral-sh/setup-uv from 6 to 7 (#10883)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 12:51:45 +05:45
dependabot[bot] aa090ecf8d build(deps): bump github/codeql-action from 3 to 4 (#10884)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 12:40:20 +05:45
pre-commit-ci[bot] 0c4a5cbbb7 [pre-commit.ci] pre-commit autoupdate (#10882)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.13.2 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.2...v0.13.3)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-07 09:05:01 +05:45
skshetry 2b19c724c5 exp run: add test for running inside a linked worktree (#10834) 2025-10-04 06:20:43 +00:00
pre-commit-ci[bot] 28b92c8b03 [pre-commit.ci] pre-commit autoupdate (#10877)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.13.1 → v0.13.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.1...v0.13.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-30 10:42:39 +05:45
pre-commit-ci[bot] cd740fed42 [pre-commit.ci] pre-commit autoupdate (#10872)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-23 08:41:34 +05:45
dependabot[bot] 5f31ed2b08 build(deps): update pytest-rerunfailures requirement from <16.0 to <17.0 (#10859)
Updates the requirements on [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/1.0.0...16.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-version: '16.0'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-20 23:20:43 +05:45
pre-commit-ci[bot]andSaugat Pachhai 6e896d396c [pre-commit.ci] pre-commit autoupdate (#10869)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.12 → v0.13.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.12...v0.13.0)

* fix ruff errors

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2025-09-20 10:22:37 +05:45
dependabot[bot] 336c8695da build(deps): bump mypy from 1.18.1 to 1.18.2 (#10871)
Bumps [mypy](https://github.com/python/mypy) from 1.18.1 to 1.18.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.18.2
  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>
2025-09-20 08:57:28 +05:45
dependabot[bot] 3915fa26aa build(deps): bump mypy from 1.17.1 to 1.18.1 (#10868)
Bumps [mypy](https://github.com/python/mypy) from 1.17.1 to 1.18.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.17.1...v1.18.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.18.1
  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>
2025-09-15 12:43:46 +05:45
pre-commit-ci[bot] 114fd8075c [pre-commit.ci] pre-commit autoupdate (#10867)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.12.12)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-11 09:43:27 +05:45
dependabot[bot] b39a888948 build(deps): bump actions/setup-python from 5 to 6 (#10865)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  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>
2025-09-05 15:05:32 +05:45
skshetry c1af66c0a6 completion: support completing config variables (#10864) 2025-09-04 12:00:10 +05:45
skshetry 75c5d17d1e fix(data status): handle missing DVC repo at Git HEAD (#10862)
Avoid raising errors in `data_status()` when Git HEAD does not contain a DVC
repository. Instead, fall back to an empty index.

Tests now cover both unborn and committed Git repos, with and without subdirs.
2025-09-02 12:15:20 +05:45
pre-commit-ci[bot] ea9dd5fc3e [pre-commit.ci] pre-commit autoupdate (#10860)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.10 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.10...v0.12.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-02 11:12:59 +05:45
skshetry 763cbf6bd1 feat(cli): support initializing DVC repository in an arbitrary directory (#10861)
Previously, it could be only initialized in the current working directory.
2025-09-02 10:52:27 +05:45
skshetry 6f4b8c98fc completion: complete remote in zsh/bash (#10858) 2025-08-31 11:25:31 +05:45
skshetry a8bda305cd feat(cli): support multiple --keep and --drop arguments in exp show (#10857)
* feat(cli/exp-show): support multiple --keep and --drop arguments

Closes #10851.

* set upper bound for pytest-rerunfailures to <16.0

v16.0 started breaking in CI.
See https://github.com/pytest-dev/pytest-rerunfailures/issues/303.
2025-08-29 21:13:15 +05:45
skshetry 4f4c5b1d01 ignore: simplify and reuse same implementation across check-ignore and is_ignored API (#10856) 2025-08-28 17:09:33 +05:45
skshetry 76cfe92960 minor refactor on check-ignore (#10854) 2025-08-27 19:14:29 +05:45
pre-commit-ci[bot] 79454557bb [pre-commit.ci] pre-commit autoupdate (#10853)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-26 12:29:43 +05:45
skshetry 85046843ef fix(status): treat target as a dataset path for cloud instead of search path for dvcfiles (#10849) 2025-08-19 12:04:18 +05:45
pre-commit-ci[bot] e10b27acbd [pre-commit.ci] pre-commit autoupdate (#10848)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-19 10:17:08 +05:45
skshetry 5a86586f3a feat(data status): detect file moves/renames by default (#10846) 2025-08-18 21:23:01 +05:45
skshetry d142d0d6af fix dvc.yaml filepath in error message when reading from git revisions (#10847) 2025-08-16 13:44:48 +00:00
skshetry 9b5ee43ce1 fix(diff): do not show missing cache entries on diff result (#10845) 2025-08-16 05:11:54 +00:00
skshetry 6e223633e7 fix(diff): show in "Not in cache" if dir entry is missing (#10844) 2025-08-15 17:17:57 +00:00
skshetry 29386a0dbb data status: add --remote option to check files against a different remote (#10843) 2025-08-15 21:23:46 +05:45
skshetry e027bc6a87 checkout/pull: report granular summary of changes (#10842) 2025-08-15 20:54:13 +05:45
skshetry 82efd3e780 fix some, suppress some issues from ty check (#10841) 2025-08-14 21:05:49 +05:45
skshetry a68dda4164 commit: batch dump stages, do not re-read on save from the workspace (#10839) 2025-08-14 16:45:42 +05:45
skshetry 4b1adfd646 fix(perf): only load a stage/.dvc file if the target is an address to that stage (#10838) 2025-08-13 15:54:04 +05:45
dependabot[bot] 748c45655e build(deps): bump actions/checkout from 4 to 5 (#10837)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 16:47:06 +05:45
pre-commit-ci[bot] 225bfa6a19 [pre-commit.ci] pre-commit autoupdate (#10836)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-12 09:10:42 +05:45
KoyoMatsuda-hacarus 09079d50ff fix: remove apostrophe and hyphen words from NOUNS and ADJECTIVES. (#10833) 2025-08-07 20:41:40 +05:45
skshetry 5485e5662e exp run: fix failure when dependencies are from inside submodules (#10832) 2025-08-06 21:32:00 +05:45
skshetry e619b6dccd exp run: fix failure when dependencies are from inside submodules (#10831) 2025-08-06 21:07:07 +05:45
dependabot[bot] 396e71f30c build(deps): bump actions/download-artifact from 4 to 5 (#10830)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 09:10:09 +05:45
pre-commit-ci[bot] c6a393e3fe [pre-commit.ci] pre-commit autoupdate (#10828)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-05 08:59:19 +05:45
B. John Braddock a72402c843 hashing: Add usedforsecurity=False on hashlib calls (#10826) 2025-08-03 11:19:44 +05:45
dependabot[bot] 08f3197cdd build(deps): bump mypy from 1.17.0 to 1.17.1 (#10824)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-02 08:39:23 +05:45
Samandar MirjalilovandSaugat Pachhai 1f325bc21e exp: add --hide-workspace flag to exp show (#10798)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2025-07-31 14:23:00 +05:45
skshetry ca73078b80 fix(data status): handle missing .dir file from the cache on not_in_remote checks (#10822) 2025-07-31 07:57:15 +00:00
skshetry fade6c5991 cli: status: return integer instead of bool on --quiet (#10820) 2025-07-29 09:07:24 +05:45
pre-commit-ci[bot] 446a59c806 [pre-commit.ci] pre-commit autoupdate (#10819)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-29 08:16:34 +05:45
skshetry a370f80f90 try to fix test with new rich (#10815) 2025-07-28 15:49:58 +05:45
skshetry dc13186759 analytics: send remote types and git-remote's path hashes (#10816) 2025-07-28 15:07:41 +05:45
skshetry ba1281a41c queue: show a hint to check celery logs if logs are missing and is a failed experiment (#10814) 2025-07-28 11:20:28 +05:45
pre-commit-ci[bot] abb5977a38 [pre-commit.ci] pre-commit autoupdate (#10811)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-22 00:12:09 +05:45
Saugat Pachhai (सौगात) 50487e931f Set COVERAGE_CORE during pytest run 2025-07-21 14:23:19 +05:45
skshetry 397f4c6f28 skip install related tests if git is missing (#10809) 2025-07-21 14:11:15 +05:45
skshetry 1f5bace250 improve performance of tests in Python>=3.12 when used with coverage (#10810) 2025-07-21 14:09:31 +05:45
skshetry 5a68e821f2 stop using gitpython in tests (#10808) 2025-07-21 11:59:27 +05:45
skshetry b4d8690799 data status: fix remote check progress bar (#10804) 2025-07-16 18:13:43 +05:45
skshetry 7f0c835eed Update badges 2025-07-15 21:19:37 +05:45
skshetry c2506abf74 Update README.rst 2025-07-15 17:01:56 +05:45
dependabot[bot] d938a1c700 build(deps): bump mypy from 1.16.1 to 1.17.0 (#10802)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 11:06:26 +05:45
pre-commit-ci[bot] a0b6b12a4f [pre-commit.ci] pre-commit autoupdate (#10801)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-15 09:23:00 +05:45
skshetry 1b9b1f870a try to reset log-level after setting it (#10800) 2025-07-11 20:17:29 +05:45
pre-commit-ci[bot] be1b529bdf [pre-commit.ci] pre-commit autoupdate (#10799)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-08 16:36:47 +05:45
pre-commit-ci[bot] da55c0da1f [pre-commit.ci] pre-commit autoupdate (#10797)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-01 11:07:42 +05:45
skshetry a23810f0b5 partially rollback batch remote checks (#10796) 2025-06-30 17:19:26 +05:45
Italos Estilon, PhDandSaugat Pachhai 0adf6679d9 cache: use configured cache directory for fs cache path (#10795)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2025-06-30 12:29:40 +05:45
skshetry 75083cc472 bench: git-ignore dataset fixture used for data status (#10793) 2025-06-27 21:27:40 +05:45
skshetry 493279c7e8 perf(data status): batch remote checks (#10792) 2025-06-27 13:42:20 +00:00
dependabot[bot] 95c5ea81e0 build(deps): bump mypy from 1.15.0 to 1.16.1 (#10781)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-26 19:28:24 +05:45
skshetry 3aca0edfce test_lock: increase timeout (#10791) 2025-06-26 13:24:55 +00:00
pre-commit-ci[bot] 5bec7d1bb9 [pre-commit.ci] pre-commit autoupdate (#10786)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-26 18:49:25 +05:45
f3836ab29a [cli,lock] Allow wait_for_lock (#10784)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2025-06-26 18:34:57 +05:45
skshetryandpre-commit-ci[bot] e278c9cdf4 fix(data status): show committed changes in a new Git repository (#10790)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-26 11:11:04 +00:00
skshetry aac32e1b64 data status: support targets to limit scope of command (#10789) 2025-06-26 16:21:17 +05:45
Thorvald Molthe BallestadandSaugat Pachhai 45fd1025e2 feat: ignore files not in remote when push is false (#10749)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2025-06-26 15:02:20 +05:45
Rodrigo Goya c7c7ba69fb (fix) DVC_SITE_CACHE_DIR environment variable should override core.site_cache_dir (#10778)
* Prioritize DVC_SITE_CACHE_DIR over core.site_cache_dir

* Add tests for proper prioritization of env var and config

* Tests require python-3.9 compatible typing, use Optional

* Remove comment

* Fixup
2025-06-12 14:19:30 +05:45
Rodrigo Goya 346b1c064e (fix) site_cache_dir() must return DVC_SITE_CACHE_DIR if it exists (#10776)
* DVC_SITE_CACHE_DIR must be sent first, deal with platformdirs afterwards

* Test should check default behaviour when no env var is set

* Add test: site_cache_dir() must return the env var if set
2025-06-12 07:12:04 +00:00
pre-commit-ci[bot] b89cb893a1 [pre-commit.ci] pre-commit autoupdate (#10773)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.12 → v0.11.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.12...v0.11.13)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-10 07:25:40 +05:45
skshetry 549821e6be deps: bump dvc-s3 to >=3.2.1 (#10771)
Fixes https://github.com/iterative/dvc/issues/10767.
Related:
- https://github.com/iterative/dvc-s3/pull/129
- https://github.com/iterative/dvc/discussions/10768
2025-06-09 08:00:44 +05:45
skshetry bf26126e37 release(test-pypi): set dynamic url (#10766) 2025-06-06 22:51:08 +05:45
skshetry 8f1c4424d3 release(env): set dynamic url (#10765) 2025-06-06 22:48:20 +05:45
183 changed files with 4147 additions and 1228 deletions
+4 -4
View File
@@ -737,8 +737,8 @@
"tests/func/test_config.py::test_config_gdrive_fields": 0.12703910000016094,
"tests/func/test_config.py::test_config_get[args0-0-False]": 0.20948869999995168,
"tests/func/test_config.py::test_config_get[args1-0-myremote]": 0.1829517000001033,
"tests/func/test_config.py::test_config_get[args2-0-iterative]": 0.19686669999998685,
"tests/func/test_config.py::test_config_get[args3-0-iterative]": 0.1675059000000374,
"tests/func/test_config.py::test_config_get[args2-0-treeverse]": 0.19686669999998685,
"tests/func/test_config.py::test_config_get[args3-0-treeverse]": 0.1675059000000374,
"tests/func/test_config.py::test_config_get[args4-251-option 'profile' doesn't exist]": 0.18096200000002227,
"tests/func/test_config.py::test_config_get[args5-0-gs://bucket/path]": 0.12368970000022728,
"tests/func/test_config.py::test_config_get[args6-0-gs://bucket/path]": 0.13007389999995667,
@@ -2642,7 +2642,7 @@
"tests/unit/test_updater.py::test_is_enabled[config0-True]": 0.13130560000013247,
"tests/unit/test_updater.py::test_is_enabled[config1-True]": 0.14342600000009043,
"tests/unit/test_updater.py::test_is_enabled[config2-False]": 0.13593219999984285,
"tests/unit/test_updater.py::test_notify_message[None-Find the latest release at https://github.com/iterative/dvc/releases/latest.]": 0.014024399999925663,
"tests/unit/test_updater.py::test_notify_message[None-Find the latest release at https://github.com/treeverse/dvc/releases/latest.]": 0.014024399999925663,
"tests/unit/test_updater.py::test_notify_message[binary-To upgrade, uninstall dvc and reinstall from https://dvc.org.]": 0.013217200000326557,
"tests/unit/test_updater.py::test_notify_message[brew-To upgrade, run 'brew upgrade dvc'.]": 0.013467300000229443,
"tests/unit/test_updater.py::test_notify_message[choco-To upgrade, run 'choco upgrade dvc'.]": 0.014645900000004985,
@@ -2652,7 +2652,7 @@
"tests/unit/test_updater.py::test_notify_message[osxpkg-To upgrade, uninstall dvc and reinstall from https://dvc.org.]": 0.013980499999888707,
"tests/unit/test_updater.py::test_notify_message[pip-To upgrade, run 'pip install --upgrade dvc'.]": 0.015155299999833005,
"tests/unit/test_updater.py::test_notify_message[rpm-To upgrade, run 'yum update dvc'.]": 0.014036500000202068,
"tests/unit/test_updater.py::test_notify_message[unknown-Find the latest release at https://github.com/iterative/dvc/releases/latest.]": 0.01609110000003966,
"tests/unit/test_updater.py::test_notify_message[unknown-Find the latest release at https://github.com/treeverse/dvc/releases/latest.]": 0.01609110000003966,
"tests/unit/ui/test_console.py::test_capsys_works": 0.003642499999841675,
"tests/unit/ui/test_console.py::test_write": 0.004320600000255581,
"tests/unit/ui/test_console.py::test_write_json[False-{\"hello\": \"world\", \"date\": \"1970-01-01 00:00:00\"}\\n]": 0.004433300000073359,
+1 -1
View File
@@ -57,7 +57,7 @@ $ dvc doctor
**Additional Information (if any):**
<!--
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.
Please check https://github.com/treeverse/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
+1 -1
View File
@@ -1,5 +1,5 @@
* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here.
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/treeverse/dvc.org) and linked it here.
Thank you for the contribution - we'll try to review it as soon as possible. 🙏
+5 -5
View File
@@ -14,16 +14,16 @@ jobs:
name: run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
@@ -32,7 +32,7 @@ jobs:
- name: run benchmarks on base branch
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
clean: false
+10 -10
View File
@@ -19,15 +19,15 @@ jobs:
name: Build & Verify package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: "3.14"
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v7
- name: Omit local version for Test PyPI upload
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
@@ -41,7 +41,7 @@ jobs:
- name: Check dist
run: uv tool run twine check --strict dist/*
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: Packages
path: dist/
@@ -51,17 +51,17 @@ jobs:
name: Publish dev package to test.pypi.org
runs-on: ubuntu-latest
needs: build
if: ${{ github.event.action == 'published' || (github.repository == 'iterative/dvc' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
if: ${{ github.event.action == 'published' || (github.repository == 'treeverse/dvc' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
environment:
name: test-pypi
url: https://test.pypi.org/p/dvc
url: https://test.pypi.org/p/dvc/${{ github.event.release.tag_name }}
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
name: Packages
path: dist
@@ -80,13 +80,13 @@ jobs:
environment:
name: pypi
url: https://pypi.org/p/dvc
url: https://pypi.org/p/dvc/${{ github.event.release.tag_name }}
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
name: Packages
path: dist
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-extended
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+6 -6
View File
@@ -19,26 +19,26 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
pyv: ["3.13"]
pyv: ["3.14"]
plugin: ["dvc-s3"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
path: dvc
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
repository: iterative/${{ matrix.plugin }}
repository: treeverse/${{ matrix.plugin }}
ref: main
path: ${{ matrix.plugin }}
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.pyv }}
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
+21 -8
View File
@@ -26,14 +26,14 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@v6
python-version: '3.14'
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
@@ -55,7 +55,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
pyv: ["3.9", "3.10", "3.11", "3.12", "3.13"]
pyv: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
include:
- os: windows-latest
pyv: "3.9"
@@ -117,15 +117,27 @@ jobs:
- os: windows-latest
pyv: "3.13"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
- os: windows-latest
pyv: "3.14"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.14"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.14"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.14"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.pyv }}
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
@@ -137,6 +149,7 @@ jobs:
- name: run tests
timeout-minutes: 40
env:
COVERAGE_CORE: sysmon
PYTHONUTF8: 1
run: >
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
+2 -2
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -22,7 +22,7 @@ repos:
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.12'
rev: 'v0.14.6'
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
+1 -1
View File
@@ -4,5 +4,5 @@
"data-science", "data-version-control", "machine-learning", "git",
"developer-tools", "reproducibility", "collaboration", "ai", "python"],
"contributors": [
{"name": "DVC team", "type": "Other", "affiliation": "Iterative"}]
{"name": "DVC team", "type": "Other", "affiliation": "Treeverse"}]
}
+2 -2
View File
@@ -6,8 +6,8 @@ message: >-
type: software
authors:
- name: The DVC team and contributors
website: 'https://github.com/iterative'
repository-code: 'https://github.com/iterative/dvc'
website: 'https://github.com/treeverse'
repository-code: 'https://github.com/treeverse/dvc'
url: 'https://dvc.org'
abstract: >-
DVC is a tool for data science that takes advantage of existing software engineering toolset.
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017-2021 Iterative, Inc.
Copyright 2025 Treeverse.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+11 -17
View File
@@ -167,7 +167,7 @@ To install the development version, run:
.. code-block:: bash
pip install git+git://github.com/iterative/dvc
pip install "dvc @ git+https://github.com/treeverse/dvc"
Package (Platform-specific)
---------------------------
@@ -175,7 +175,7 @@ Package (Platform-specific)
|Packages|
Self-contained packages for Linux, Windows, and Mac are available.
The latest version of the packages can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
The latest version of the packages can be found on the GitHub `releases page <https://github.com/treeverse/dvc/releases>`_.
Ubuntu / Debian (deb)
^^^^^^^^^^^^^^^^^^^^^
@@ -198,8 +198,6 @@ Fedora / CentOS (rpm)
Contributing
============
|Maintainability|
Contributions are welcome!
Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more details.
Thanks to all our contributors!
@@ -213,7 +211,7 @@ Community and Support
* `Forum <https://discuss.dvc.org/>`_
* `Discord Chat <https://dvc.org/chat>`_
* `Email <mailto:[email protected]>`_
* `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
* `Mailing List <https://dvc.org/community#subscribe>`_
Copyright
=========
@@ -237,23 +235,19 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://dvc.org
:alt: DVC logo
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/treeverse/vscode-dvc/main/extension/docs/overview.gif
:alt: DVC Extension for VS Code
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
:target: https://github.com/iterative/dvc/actions
.. |CI| image:: https://github.com/treeverse/dvc/actions/workflows/tests.yaml/badge.svg
:target: https://github.com/treeverse/dvc/actions/workflows/tests.yaml
:alt: GHA Tests
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
:target: https://pypi.org/project/dvc
:alt: Python Version
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
:target: https://codecov.io/gh/iterative/dvc
.. |Coverage| image:: https://codecov.io/gh/treeverse/dvc/branch/main/graph/badge.svg
:target: https://codecov.io/gh/treeverse/dvc
:alt: Codecov
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
@@ -268,7 +262,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://formulae.brew.sh/formula/dvc
:alt: Homebrew
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
.. |Conda| image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
:target: https://anaconda.org/conda-forge/dvc
:alt: Conda-forge
@@ -292,8 +286,8 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://dvc.org/img/flow.gif
:alt: how_dvc_works
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
:target: https://github.com/iterative/dvc/graphs/contributors
.. |Contribs| image:: https://contrib.rocks/image?repo=treeverse/dvc
:target: https://github.com/treeverse/dvc/graphs/contributors
:alt: Contributors
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
+5 -5
View File
@@ -14,7 +14,7 @@ def viztracer_profile(
log_async: bool = False,
):
try:
import viztracer
import viztracer # ty: ignore[unresolved-import]
except ImportError:
print("Failed to run profiler, viztracer is not installed") # noqa: T201
yield
@@ -37,7 +37,7 @@ def yappi_profile(
separate_threads: Optional[bool] = False,
):
try:
import yappi
import yappi # ty: ignore[unresolved-import]
except ImportError:
print("Failed to run profiler, yappi is not installed") # noqa: T201
yield
@@ -79,7 +79,7 @@ def yappi_profile(
def instrument(html_output=False):
"""Run a statistical profiler"""
try:
from pyinstrument import Profiler
from pyinstrument import Profiler # ty: ignore[unresolved-import]
except ImportError:
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
yield
@@ -123,7 +123,7 @@ def debug():
yield
except Exception:
try:
import ipdb as pdb # noqa: T100
import ipdb as pdb # noqa: T100 # ty: ignore[unresolved-import]
except ImportError:
import pdb # type: ignore[no-redef] # noqa: T100
pdb.post_mortem()
@@ -206,7 +206,7 @@ def add_debugging_flags(parser):
from argparse import SUPPRESS
# For detailed info see:
# https://github.com/iterative/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
# https://github.com/treeverse/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
args, _ = parser.parse_known_args()
verbose = args.verbose
+73 -13
View File
@@ -1,10 +1,14 @@
import json
import os
from typing import TYPE_CHECKING, Optional
from dvc.log import logger
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
if TYPE_CHECKING:
from dvc.scm import Base
logger = logger.getChild(__name__)
@@ -67,6 +71,7 @@ def send(path):
thus, the need of removing such file afterwards.
"""
import requests
from requests.exceptions import RequestException
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
headers = {"content-type": "application/json"}
@@ -81,7 +86,7 @@ def send(path):
try:
requests.post(url, json=report, headers=headers, timeout=5)
except requests.exceptions.RequestException as e:
except RequestException as e:
logger.trace("", exc_info=True)
logger.debug("failed to send analytics report %s", str(e))
@@ -89,20 +94,62 @@ def send(path):
os.remove(path)
def _scm_in_use():
from dvc.exceptions import NotDvcRepoError
from dvc.repo import Repo
from dvc.scm import NoSCM
def _git_remote_url(scm: Optional["Base"]) -> Optional[str]:
from dvc.scm import Git
from .scm import SCM, SCMError
if not isinstance(scm, Git):
return None
from dulwich.porcelain import get_remote_repo
dulwich_repo = scm.dulwich.repo
try:
_remote, url = get_remote_repo(dulwich_repo)
except IndexError:
# IndexError happens when the head is detached
_remote, url = get_remote_repo(dulwich_repo, b"origin")
# Dulwich returns (None, "origin") if no remote set
if (_remote, url) == (None, "origin"):
return None
return url
def _scm_in_use(scm: Optional["Base"]) -> Optional[str]:
return type(scm).__name__ if scm else None
def _parse_git_remote_path(remote_url: str) -> str:
from urllib.parse import urlparse
from scmrepo.urls import is_scp_style_url
parsed = urlparse(remote_url)
# Windows Path also gets parsed with a drive letter as scheme
# https://github.com/python/cpython/issues/86381
if parsed.scheme and parsed.scheme in ("http", "https", "git", "ssh"):
return parsed.path.strip("/")
if is_scp_style_url(remote_url):
# handle scp-style URL
parts = remote_url.split(":", 1)
if len(parts) == 2:
_, path = parts
return path.rstrip("/")
return remote_url
def _git_remote_path_hash(scm: Optional["Base"]) -> Optional[str]:
"""Return a hash of the git remote path."""
import hashlib
try:
scm = SCM(root_dir=Repo.find_root())
return type(scm).__name__
except SCMError:
return NoSCM.__name__
except NotDvcRepoError:
pass
if remote_url := _git_remote_url(scm):
path = _parse_git_remote_path(remote_url)
h = hashlib.md5(path.encode("utf-8"), usedforsecurity=False) # for FIPS
return h.hexdigest()
except Exception: # noqa: BLE001
logger.debug("Failed to get git remote path", exc_info=True)
return None
def _runtime_info():
@@ -112,6 +159,8 @@ def _runtime_info():
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
from dvc import __version__
from dvc.info import _get_remotes
from dvc.repo import Repo
from dvc.utils import is_binary
ci_id = _generate_ci_id()
@@ -120,13 +169,24 @@ def _runtime_info():
else:
group_id, user_id = None, find_or_create_user_id()
scm = None
remotes = None
try:
repo = Repo()
scm = repo.scm
remotes = _get_remotes(repo.config)
except Exception as exc: # noqa: BLE001
logger.debug("failed to open repo: %s", exc)
return {
"dvc_version": __version__,
"is_binary": is_binary(),
"scm_class": _scm_in_use(),
"scm_class": _scm_in_use(scm),
"system_info": _system_info(),
"user_id": user_id,
"group_id": group_id,
"remotes": remotes,
"git_remote_hash": _git_remote_path_hash(scm),
}
+1
View File
@@ -64,3 +64,4 @@ def get(name: str) -> Union[DatachainDataset, DVCDataset, URLDataset]:
for file in dataset.lock.files
]
return URLDataset(type="url", files=files, path=versioned_path)
raise AssertionError("unreachable")
+1 -1
View File
@@ -86,7 +86,7 @@ class CacheManager:
return FileStorage(
key=(),
fs=self.local.fs,
path=self.local.fs.join(self.default_local_cache_dir, self.FS_DIR),
path=self.local.fs.join(self.local_cache_dir, self.FS_DIR),
)
def _init_odb(self, schemes):
+7 -6
View File
@@ -96,7 +96,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
f"See {link} for more info."
)
else:
link = format_link("https://github.com/iterative/dvc/issues")
link = format_link("https://github.com/treeverse/dvc/issues")
hint = f"\nPlease report this bug to {link}. Thank you!"
logger.exception(
@@ -157,6 +157,8 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
Returns:
int: command's return code.
"""
from contextlib import ExitStack
from dvc._debug import debugtools
from dvc.config import ConfigError
from dvc.exceptions import DvcException, NotDvcRepoError
@@ -173,12 +175,11 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
logging.disable(logging.INFO)
args = None
outer_log_level = logger.level
level = None
stack = ExitStack()
try:
args = parse_args(argv)
level = None
if args.quiet:
level = logging.CRITICAL
elif args.verbose == 1:
@@ -187,7 +188,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
level = logging.TRACE # type: ignore[attr-defined]
if level is not None:
set_loggers_level(level)
stack.enter_context(set_loggers_level(level))
if level and level <= logging.DEBUG:
from platform import platform, python_implementation, python_version
@@ -247,7 +248,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
return ret
finally:
logger.setLevel(outer_log_level)
stack.close()
from dvc.repo.open_repo import clean_repos
+4 -1
View File
@@ -18,7 +18,10 @@ class CmdBase(ABC):
os.chdir(args.cd)
self.repo: Repo = Repo(uninitialized=self.UNINITIALIZED)
self.repo: Repo = Repo(
uninitialized=self.UNINITIALIZED,
_wait_for_lock=args.wait_for_lock,
)
self.config: Config = self.repo.config
self.args = args
+36
View File
@@ -22,6 +22,15 @@ _dvc_compgen_exps() {
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
compgen -W "${_dvc_exps[*]}" -- $1
}
_dvc_compgen_remotes() {
local _dvc_remotes=($(dvc remote list | cut -d' ' -f1))
compgen -W "${_dvc_remotes[*]}" -- $1
}
_dvc_compgen_config_vars() {
compgen -W "${_dvc_config_vars[*]}" -- $1
}
"""
ZSH_PREAMBLE = """
@@ -55,6 +64,14 @@ _dvc_compadd_stages_and_files() {
_dvc_compadd_exps() {
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
}
_dvc_compadd_remotes() {
_describe 'remotes' "($(dvc remote list | cut -d' ' -f1))"
}
_dvc_compadd_config_vars() {
_describe 'config_vars' _dvc_config_vars
}
"""
PREAMBLE = {
@@ -71,3 +88,22 @@ DVCFILES_AND_STAGE = {
"zsh": "_dvc_compadd_stages_and_files",
}
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
REMOTE = {"bash": "_dvc_compgen_remotes", "zsh": "_dvc_compadd_remotes"}
CONFIG_VARS = {"bash": "_dvc_compgen_config_vars", "zsh": "_dvc_compadd_config_vars"}
def get_preamble() -> dict[str, str]:
from dvc.config_schema import config_vars_for_completion
ret: dict[str, str] = {}
config_vars = list(config_vars_for_completion())
nl = "\n\t".expandtabs(4)
config_vars_arr = f"""
_dvc_config_vars=(
{nl.join(config_vars)}
)
"""
for shell, preamble in PREAMBLE.items():
ret[shell] = config_vars_arr + preamble
return ret
+8
View File
@@ -200,6 +200,14 @@ def get_main_parser():
type=str,
)
parser.add_argument(
"--wait-for-lock",
action="store_true",
default=False,
help="Wait for the lock if it is already held by another process, instead of"
" failing immediately.",
)
# Sub commands
subparsers = parser.add_subparsers(
title="Available Commands",
+1 -1
View File
@@ -101,7 +101,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
help="Remote storage to download to",
metavar="<name>",
)
).complete = completion.REMOTE
parser.add_argument(
"--remote-jobs",
type=int,
+1 -1
View File
@@ -149,7 +149,7 @@ def add_parser(subparsers, parent_parser):
"Remote name to set as a default in the target repository "
"(only applicable when downloading from DVC remote)."
),
)
).complete = completion.REMOTE
get_parser.add_argument(
"--remote-config",
type=str,
+19 -18
View File
@@ -1,20 +1,28 @@
from typing import TYPE_CHECKING
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.ui import ui
if TYPE_CHECKING:
from dvc.ignore import CheckIgnoreResult
class CmdCheckIgnore(CmdBase):
def __init__(self, args):
super().__init__(args)
self.ignore_filter = self.repo.dvcignore
def _show_results(self, result):
if not result.match and not self.args.non_matching:
return
def _show_results(self, result: "CheckIgnoreResult"):
if not result.match and not self.args.details:
return None
if self.args.details:
patterns = result.patterns
pattern_infos = result.pattern_infos
patterns = [str(pi) for pi in pattern_infos]
if not patterns and self.args.non_matching:
patterns = ["::"]
if not self.args.all:
patterns = patterns[-1:]
@@ -22,13 +30,11 @@ class CmdCheckIgnore(CmdBase):
ui.write(pattern, result.file, sep="\t")
else:
ui.write(result.file)
return bool(result.pattern_infos)
def _check_one_file(self, target):
result = self.ignore_filter.check_ignore(target)
self._show_results(result)
if result.match:
return 0
return 1
return bool(self._show_results(result))
def _interactive_mode(self):
ret = 1
@@ -39,14 +45,14 @@ class CmdCheckIgnore(CmdBase):
break
if not target:
break
if not self._check_one_file(target):
if self._check_one_file(target):
ret = 0
return ret
def _normal_mode(self):
ret = 1
for target in self.args.targets:
if not self._check_one_file(target):
if self._check_one_file(target):
ret = 0
return ret
@@ -76,6 +82,8 @@ class CmdCheckIgnore(CmdBase):
def add_parser(subparsers, parent_parser):
import argparse
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
parser = subparsers.add_parser(
@@ -93,14 +101,7 @@ def add_parser(subparsers, parent_parser):
help="Show the exclude patterns along with each target path.",
)
parser.add_argument(
"-a",
"--all",
action="store_true",
default=False,
help=(
"Include the target paths which don't match any pattern "
"in the `--details` list."
),
"-a", "--all", action="store_true", default=False, help=argparse.SUPPRESS
)
parser.add_argument(
"-n",
+7 -6
View File
@@ -1,5 +1,3 @@
import operator
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
@@ -30,7 +28,7 @@ class CmdCheckout(CmdBase):
stats, exc = None, None
try:
stats = self.repo.checkout(
result = self.repo.checkout(
targets=self.args.targets,
with_deps=self.args.with_deps,
force=self.args.force,
@@ -40,14 +38,17 @@ class CmdCheckout(CmdBase):
)
except CheckoutError as _exc:
exc = _exc
stats = exc.stats
result = exc.result
if self.args.summary:
default_message = "No changes."
msg = get_summary(sorted(stats.items(), key=operator.itemgetter(0)))
stats = result["stats"]
assert isinstance(stats, dict)
msg = get_summary(stats.items())
ui.write(msg or default_message)
else:
log_changes(stats)
result.pop("stats", {})
log_changes(result)
if exc:
raise exc
+2 -2
View File
@@ -1,6 +1,6 @@
from dvc.cli import formatter
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.completion import PREAMBLE
from dvc.cli.completion import get_preamble
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.ui import ui
@@ -17,7 +17,7 @@ class CmdCompletion(CmdBaseNoRepo):
shell = self.args.shell
parser = self.args.parser
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
script = shtab.complete(parser, shell=shell, preamble=get_preamble())
ui.write(script, force=True)
return 0
+2 -2
View File
@@ -3,7 +3,7 @@ import os
from funcy import set_in
from dvc.cli import formatter
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.log import logger
@@ -224,7 +224,7 @@ def add_parser(subparsers, parent_parser):
nargs="?",
type=_name_type,
help="Option name (section.option or remote.name.option).",
)
).complete = completion.CONFIG_VARS
config_parser.add_argument("value", nargs="?", help="Option value.")
config_parser.add_argument(
"-l",
+36 -12
View File
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, ClassVar
from funcy import chunks, compact, log_durations
from dvc.cli import formatter
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
@@ -10,6 +10,7 @@ from dvc.ui import ui
from dvc.utils import colorize
if TYPE_CHECKING:
from dvc.repo.data import GitInfo
from dvc.repo.data import Status as DataStatus
@@ -55,20 +56,15 @@ class CmdDataStatus(CmdBase):
def _process_status(status: "DataStatus"):
"""Flatten stage status, and filter empty stage status contents."""
for stage, stage_status in status.items():
items = stage_status
if isinstance(stage_status, dict):
items = {
file: state
for state, files in stage_status.items()
for file in files
}
if not items:
if not stage_status or (
isinstance(stage_status, dict) and not any(stage_status.values())
):
continue
yield stage, items
yield stage, stage_status
@classmethod
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
git_info = status.pop("git") # type: ignore[misc]
git_info: GitInfo = status.pop("git") # type: ignore[misc]
result = dict(cls._process_status(status))
if not result:
no_changes = "No changes"
@@ -90,7 +86,16 @@ class CmdDataStatus(CmdBase):
ui.write(f" ({hint})")
if isinstance(stage_status, dict):
items = [f"{state}: {file}" for file, state in stage_status.items()]
items = [
f"{state}: "
+ (
" -> ".join(change.values())
if isinstance(change, dict)
else change
)
for state, changes in stage_status.items()
for change in changes
]
else:
items = stage_status
@@ -108,8 +113,10 @@ class CmdDataStatus(CmdBase):
def run(self) -> int:
with log_durations(logger.trace, "in data_status"):
status = self.repo.data_status(
targets=self.args.targets,
granular=self.args.granular,
untracked_files=self.args.untracked_files,
remote=self.args.remote,
not_in_remote=self.args.not_in_remote,
remote_refresh=self.args.remote_refresh,
)
@@ -126,9 +133,12 @@ class CmdDataStatus(CmdBase):
def add_parser(subparsers, parent_parser):
data_help = "Commands related to data management."
data_parser = subparsers.add_parser(
"data",
parents=[parent_parser],
description=append_doc_link(data_help, "data/status"),
help=data_help,
formatter_class=formatter.RawDescriptionHelpFormatter,
)
data_subparsers = data_parser.add_subparsers(
@@ -147,6 +157,14 @@ def add_parser(subparsers, parent_parser):
formatter_class=formatter.RawDescriptionHelpFormatter,
help=DATA_STATUS_HELP,
)
data_status_parser.add_argument(
"targets",
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files and stage names."
),
).complete = completion.FILE # type: ignore[attr-defined]
data_status_parser.add_argument(
"--json",
action="store_true",
@@ -173,6 +191,12 @@ def add_parser(subparsers, parent_parser):
nargs="?",
help="Show untracked files.",
)
data_status_parser.add_argument(
"-r",
"--remote",
help="Remote storage to check (only applicable with --not-in-remote).",
metavar="<name>",
).complete = completion.REMOTE
data_status_parser.add_argument(
"--not-in-remote",
action="store_true",
+11 -10
View File
@@ -22,17 +22,18 @@ class CmdDataBase(CmdBase):
class CmdDataPull(CmdDataBase):
def log_summary(self, stats):
def log_summary(self, result):
from dvc.commands.checkout import log_changes
log_changes(stats)
stats = result.pop("stats", {})
log_changes(result)
super().log_summary(stats)
def run(self):
from dvc.exceptions import CheckoutError, DvcException
try:
stats = self.repo.pull(
result = self.repo.pull(
targets=self.args.targets,
jobs=self.args.jobs,
remote=self.args.remote,
@@ -46,10 +47,10 @@ class CmdDataPull(CmdDataBase):
glob=self.args.glob,
allow_missing=self.args.allow_missing,
)
self.log_summary(stats)
self.log_summary(result)
except (CheckoutError, DvcException) as exc:
if stats := getattr(exc, "stats", {}):
self.log_summary(stats)
if result := getattr(exc, "result", {}):
self.log_summary(result)
logger.exception("failed to pull data from the cloud")
return 1
@@ -149,7 +150,7 @@ def add_parser(subparsers, _parent_parser):
)
pull_parser.add_argument(
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
)
).complete = completion.REMOTE
pull_parser.add_argument(
"-a",
"--all-branches",
@@ -224,7 +225,7 @@ def add_parser(subparsers, _parent_parser):
)
push_parser.add_argument(
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
)
).complete = completion.REMOTE
push_parser.add_argument(
"-a",
"--all-branches",
@@ -286,7 +287,7 @@ def add_parser(subparsers, _parent_parser):
)
fetch_parser.add_argument(
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
)
).complete = completion.REMOTE
fetch_parser.add_argument(
"-a",
"--all-branches",
@@ -379,7 +380,7 @@ def add_parser(subparsers, _parent_parser):
"--remote",
help="Remote storage to compare local cache to",
metavar="<name>",
)
).complete = completion.REMOTE
status_parser.add_argument(
"-a",
"--all-branches",
+2 -1
View File
@@ -83,7 +83,7 @@ class CmdDatasetUpdate(CmdBase):
return CmdDatasetAdd.display(name, new, action)
if dataset == new:
ui.write("[yellow]Nothing to update[/]", styled=True)
return
return None
assert new.lock
@@ -112,6 +112,7 @@ class CmdDatasetUpdate(CmdBase):
assert new.type == "url"
stats = diff_files(dataset.lock.files, new.lock.files)
log_changes(stats)
return None
def run(self):
from difflib import get_close_matches
+1 -1
View File
@@ -60,7 +60,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
type=str,
help="Remote name to set as a default in the target repository.",
)
).complete = completion.REMOTE
du_parser.add_argument(
"--remote-config",
type=str,
+2 -2
View File
@@ -1,6 +1,6 @@
import argparse
from dvc.cli import formatter
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
@@ -71,7 +71,7 @@ def add_parser(experiments_subparsers, parent_parser):
dest="dvc_remote",
metavar="<name>",
help="Name of the DVC remote to use when pulling cached outputs.",
)
).complete = completion.REMOTE
experiments_pull_parser.add_argument(
"-j",
"--jobs",
+1 -1
View File
@@ -111,7 +111,7 @@ def add_parser(experiments_subparsers, parent_parser):
dest="dvc_remote",
metavar="<name>",
help="Name of the DVC remote to use when pushing cached outputs.",
)
).complete = completion.REMOTE
experiments_push_parser.add_argument(
"-j",
"--jobs",
+11
View File
@@ -20,6 +20,7 @@ class CmdExperimentsRun(CmdRepro):
tmp_dir=self.args.tmp_dir,
copy_paths=self.args.copy_paths,
message=self.args.message,
no_hydra=self.args.no_hydra,
**self._common_kwargs,
)
@@ -107,4 +108,14 @@ def _add_run_common(parser):
default=None,
help="Custom commit message to use when committing the experiment.",
)
parser.add_argument(
"--no-hydra",
action="store_true",
default=False,
help=(
"Disables automatically updating `params.yaml` with Hydra configuration. "
" You can still use `--set-param` to update individual params if needed."
" Default is False."
),
)
parser.add_argument("-M", dest="message", help=argparse.SUPPRESS) # obsolete
+19 -8
View File
@@ -1,6 +1,6 @@
import argparse
import re
from collections.abc import Iterable
from collections.abc import Collection, Iterable
from datetime import date, datetime
from typing import TYPE_CHECKING
@@ -59,17 +59,16 @@ def baseline_styler(typ):
def show_experiments(
td: "TabularData",
headers: dict[str, Iterable[str]],
keep=None,
drop=None,
keep: Collection[str] = (),
drop: Collection[str] = (),
pager=True,
csv=False,
markdown=False,
**kwargs,
):
if keep:
for col in td.keys(): # noqa: SIM118
if re.match(keep, col):
td.protect(col)
keep_re = re.compile("|".join(keep))
td.protect(*(col for col in td.keys() if keep_re.match(col))) # noqa: SIM118
for col in ("State", "Executor"):
if td.is_empty(col):
@@ -110,8 +109,9 @@ def show_experiments(
td.drop_duplicates("cols", ignore_empty=False)
cols_to_drop = set()
if drop is not None:
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
if drop:
drop_re = re.compile("|".join(drop))
cols_to_drop = {col for col in td.keys() if drop_re.match(col)} # noqa: SIM118
td.drop(*cols_to_drop)
td.render(
@@ -150,6 +150,7 @@ class CmdExperimentsShow(CmdBase):
all_commits=self.args.all_commits,
hide_queued=self.args.hide_queued,
hide_failed=self.args.hide_failed,
hide_workspace=self.args.hide_workspace,
revs=self.args.rev,
num=self.args.num,
sha_only=self.args.sha,
@@ -236,11 +237,15 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_show_parser.add_argument(
"--drop",
action="append",
default=[],
help="Remove the columns matching the specified regex pattern.",
metavar="<regex_pattern>",
)
experiments_show_parser.add_argument(
"--keep",
action="append",
default=[],
help="Preserve the columns matching the specified regex pattern.",
metavar="<regex_pattern>",
)
@@ -279,6 +284,12 @@ def add_parser(experiments_subparsers, parent_parser):
default=False,
help="Hide queued experiments in the table.",
)
experiments_show_parser.add_argument(
"--hide-workspace",
action="store_true",
default=False,
help="Hide workspace row in the table.",
)
experiments_show_parser.add_argument(
"--json",
action="store_true",
+2 -2
View File
@@ -1,6 +1,6 @@
import os
from dvc.cli import formatter
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
@@ -188,7 +188,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
help="Remote storage to collect garbage in",
metavar="<name>",
)
).complete = completion.REMOTE
gc_parser.add_argument(
"--skip-failed",
action="store_true",
+1 -1
View File
@@ -121,7 +121,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
type=str,
help="Remote name to set as a default in the target repository.",
)
).complete = completion.REMOTE
get_parser.add_argument(
"--remote-config",
type=str,
+1 -1
View File
@@ -115,7 +115,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
type=str,
help="Remote name to set as a default in the target repository.",
)
).complete = completion.REMOTE
import_parser.add_argument(
"--remote-config",
type=str,
+1 -1
View File
@@ -73,7 +73,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
help="Remote storage to download to",
metavar="<name>",
)
).complete = completion.REMOTE
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
+14 -5
View File
@@ -29,7 +29,7 @@ def _welcome_message():
"{yellow}------------{nc}\n"
f"- Check out the documentation: {fmt_link('https://dvc.org/doc')}\n"
f"- Get help and share ideas: {fmt_link('https://dvc.org/chat')}\n"
f"- Star us on GitHub: {fmt_link('https://github.com/iterative/dvc')}"
f"- Star us on GitHub: {fmt_link('https://github.com/treeverse/dvc')}"
).format(yellow=colorama.Fore.YELLOW, nc=colorama.Fore.RESET)
ui.write(msg)
@@ -42,7 +42,7 @@ class CmdInit(CmdBaseNoRepo):
try:
with Repo.init(
".",
self.args.directory,
no_scm=self.args.no_scm,
force=self.args.force,
subdir=self.args.subdir,
@@ -57,10 +57,11 @@ class CmdInit(CmdBaseNoRepo):
def add_parser(subparsers, parent_parser):
"""Setup parser for `dvc init`."""
INIT_HELP = "Initialize DVC in the current directory."
INIT_HELP = "Initialize DVC repository."
INIT_DESCRIPTION = (
"Initialize DVC in the current directory. Expects directory\n"
"to be a Git repository unless --no-scm option is specified."
"Initialize DVC repository in the given directory (defaults to the current "
"working directory).\n"
"Expects directory to be a Git repository unless --no-scm option is specified."
)
init_parser = subparsers.add_parser(
@@ -70,6 +71,14 @@ def add_parser(subparsers, parent_parser):
help=INIT_HELP,
formatter_class=formatter.RawDescriptionHelpFormatter,
)
init_parser.add_argument(
"directory",
nargs="?",
default=".",
help=(
"Directory to initialize DVC in. Defaults to the current working directory."
),
)
init_parser.add_argument(
"--no-scm",
action="store_true",
+1 -1
View File
@@ -250,7 +250,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
type=str,
help="Remote name to set as a default in the target repository.",
)
).complete = completion.REMOTE
list_parser.add_argument(
"--remote-config",
type=str,
+10 -4
View File
@@ -1,4 +1,4 @@
from dvc.cli import formatter
from dvc.cli import completion, formatter
from dvc.cli.utils import append_doc_link
from dvc.commands.config import CmdConfig
from dvc.ui import ui
@@ -210,7 +210,9 @@ def add_parser(subparsers, parent_parser):
help=REMOTE_DEFAULT_HELP,
formatter_class=formatter.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"
).complete = completion.REMOTE
remote_default_parser.add_argument(
"-u",
"--unset",
@@ -228,7 +230,9 @@ def add_parser(subparsers, parent_parser):
help=REMOTE_MODIFY_HELP,
formatter_class=formatter.RawDescriptionHelpFormatter,
)
remote_modify_parser.add_argument("name", help="Name of the remote")
remote_modify_parser.add_argument(
"name", help="Name of the remote"
).complete = completion.REMOTE
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."
@@ -260,7 +264,9 @@ def add_parser(subparsers, parent_parser):
help=REMOTE_REMOVE_HELP,
formatter_class=formatter.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."
).complete = completion.REMOTE
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
REMOTE_RENAME_HELP = "Rename a DVC remote"
remote_rename_parser = remote_subparsers.add_parser(
+1 -1
View File
@@ -71,7 +71,7 @@ class CmdDataStatus(CmdDataBase):
return 0
if self.args.quiet:
return bool(st)
return int(bool(st))
if st:
self._show(st, indent)
+1 -1
View File
@@ -74,7 +74,7 @@ def add_parser(subparsers, parent_parser):
"--remote",
help="Remote storage to perform updates to",
metavar="<name>",
)
).complete = completion.REMOTE
update_parser.add_argument(
"-j",
"--jobs",
+1
View File
@@ -139,6 +139,7 @@ class Config(dict):
return global_config_dir()
if level == "system":
return system_config_dir()
return None
@cached_property
def files(self) -> dict[str, str]:
+172 -141
View File
@@ -1,4 +1,5 @@
import os
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from funcy import once, walk_values
@@ -10,6 +11,7 @@ from voluptuous import (
Exclusive,
Invalid,
Lower,
Marker,
Optional,
Range,
Schema,
@@ -17,6 +19,9 @@ from voluptuous import (
from dvc.log import logger
if TYPE_CHECKING:
from collections.abc import Iterator
logger = logger.getChild(__name__)
Bool = All(
@@ -112,17 +117,19 @@ class FeatureSchema(Schema):
return ret
DEPRECATED = "==DEPRECATED=="
REMOTE_COMMON = {
"url": str,
"checksum_jobs": All(Coerce(int), Range(1)),
"jobs": All(Coerce(int), Range(1)),
Optional("worktree"): Bool,
Optional("no_traverse"): Bool, # obsoleted
Optional("no_traverse", description=DEPRECATED): Bool, # obsoleted
Optional("version_aware"): Bool,
}
LOCAL_COMMON = {
"type": supported_cache_type,
Optional("protected", default=False): Bool, # obsoleted
Optional("protected", default=False, description=DEPRECATED): Bool, # obsoleted
"shared": All(Lower, Choices("group")),
Optional("slow_link_warning", default=True): Bool,
Optional("verify", default=False): Bool,
@@ -152,6 +159,127 @@ WEBDAV_COMMON = {
Optional("verify", default=False): Bool,
}
REMOTE_SCHEMAS = {
"": LOCAL_COMMON | REMOTE_COMMON,
"s3": {
"region": str,
"profile": str,
"credentialpath": str,
"configpath": str,
"endpointurl": str,
"access_key_id": str,
"secret_access_key": str,
"session_token": str,
Optional(
"listobjects", default=False, description=DEPRECATED
): Bool, # obsoleted
Optional("use_ssl", default=True): Bool,
Optional("allow_anonymous_login", default=False): Bool,
"ssl_verify": Any(Bool, str),
"sse": str,
"sse_kms_key_id": str,
"sse_customer_algorithm": str,
"sse_customer_key": str,
"acl": str,
"grant_read": str,
"grant_read_acp": str,
"grant_write_acp": str,
"grant_full_control": str,
"cache_regions": bool,
"read_timeout": Coerce(int),
"connect_timeout": Coerce(int),
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"gs": {
"projectname": str,
"credentialpath": str,
"endpointurl": str,
Optional("verify", default=False): Bool,
Optional("allow_anonymous_login", default=False): Bool,
**REMOTE_COMMON,
},
"ssh": {
"type": supported_cache_type,
"port": Coerce(int),
"user": str,
"password": str,
"ask_password": Bool,
"passphrase": str,
"ask_passphrase": Bool,
"keyfile": str,
"timeout": Coerce(int),
"gss_auth": Bool,
"allow_agent": Bool,
"max_sessions": Coerce(int),
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"hdfs": {
"user": str,
"kerb_ticket": str,
"replication": int,
**REMOTE_COMMON,
},
"webhdfs": {
"kerberos": Bool,
"kerberos_principal": str,
"proxy_to": str,
"ssl_verify": Any(Bool, str),
"token": str,
"use_https": Bool,
"user": str,
"password": str,
"data_proxy_target": str,
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"azure": {
"connection_string": str,
"sas_token": str,
"account_name": str,
"account_key": str,
"tenant_id": str,
"client_id": str,
"client_secret": str,
"allow_anonymous_login": Bool,
"exclude_environment_credential": Bool,
"exclude_visual_studio_code_credential": Bool,
"exclude_shared_token_cache_credential": Bool,
"exclude_managed_identity_credential": Bool,
Optional("verify", default=False): Bool,
"timeout": Coerce(int),
"read_timeout": Coerce(int),
"connection_timeout": Coerce(int),
**REMOTE_COMMON,
},
"oss": {
"oss_key_id": str,
"oss_key_secret": str,
"oss_endpoint": str,
Optional("verify", default=True): Bool,
**REMOTE_COMMON,
},
"gdrive": {
"profile": str,
"gdrive_use_service_account": Bool,
"gdrive_client_id": str,
"gdrive_client_secret": str,
"gdrive_user_credentials_file": str,
"gdrive_service_account_user_email": str,
"gdrive_service_account_json_file_path": str,
Optional("gdrive_trash_only", default=False): Bool,
Optional("gdrive_acknowledge_abuse", default=False): Bool,
Optional("verify", default=True): Bool,
**REMOTE_COMMON,
},
"http": HTTP_COMMON | REMOTE_COMMON,
"https": HTTP_COMMON | REMOTE_COMMON,
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
"remote": {str: object}, # Any of the above options are valid
}
SCHEMA = {
"core": {
"remote": Lower,
@@ -161,152 +289,37 @@ SCHEMA = {
Optional("hardlink_lock", default=False): Bool,
Optional("no_scm", default=False): Bool,
Optional("autostage", default=False): Bool,
Optional("experiments"): Bool, # obsoleted
Optional("experiments", description=DEPRECATED): Bool, # obsoleted
Optional("check_update", default=True): Bool,
"site_cache_dir": str,
"machine": Lower,
},
"cache": {
"local": str, # obsoleted
"s3": str, # obsoleted
"gs": str, # obsoleted
"hdfs": str, # obsoleted
"webhdfs": str, # obsoleted
"ssh": str, # obsoleted
"azure": str, # obsoleted
Marker("local", description=DEPRECATED): str, # obsoleted
Marker("s3", description=DEPRECATED): str, # obsoleted
Marker("gs", description=DEPRECATED): str, # obsoleted
Marker("hdfs", description=DEPRECATED): str, # obsoleted
Marker("webhdfs", description=DEPRECATED): str, # obsoleted
Marker("ssh", description=DEPRECATED): str, # obsoleted
Marker("azure", description=DEPRECATED): str, # obsoleted
# This is for default local cache
"dir": str,
**LOCAL_COMMON,
},
"remote": {
str: ByUrl(
{
"": LOCAL_COMMON | REMOTE_COMMON,
"s3": {
"region": str,
"profile": str,
"credentialpath": str,
"configpath": str,
"endpointurl": str,
"access_key_id": str,
"secret_access_key": str,
"session_token": str,
Optional("listobjects", default=False): Bool, # obsoleted
Optional("use_ssl", default=True): Bool,
Optional("allow_anonymous_login", default=False): Bool,
"ssl_verify": Any(Bool, str),
"sse": str,
"sse_kms_key_id": str,
"sse_customer_algorithm": str,
"sse_customer_key": str,
"acl": str,
"grant_read": str,
"grant_read_acp": str,
"grant_write_acp": str,
"grant_full_control": str,
"cache_regions": bool,
"read_timeout": Coerce(int),
"connect_timeout": Coerce(int),
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"gs": {
"projectname": str,
"credentialpath": str,
"endpointurl": str,
Optional("verify", default=False): Bool,
Optional("allow_anonymous_login", default=False): Bool,
**REMOTE_COMMON,
},
"ssh": {
"type": supported_cache_type,
"port": Coerce(int),
"user": str,
"password": str,
"ask_password": Bool,
"passphrase": str,
"ask_passphrase": Bool,
"keyfile": str,
"timeout": Coerce(int),
"gss_auth": Bool,
"allow_agent": Bool,
"max_sessions": Coerce(int),
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"hdfs": {
"user": str,
"kerb_ticket": str,
"replication": int,
**REMOTE_COMMON,
},
"webhdfs": {
"kerberos": Bool,
"kerberos_principal": str,
"proxy_to": str,
"ssl_verify": Any(Bool, str),
"token": str,
"use_https": Bool,
"user": str,
"password": str,
"data_proxy_target": str,
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"azure": {
"connection_string": str,
"sas_token": str,
"account_name": str,
"account_key": str,
"tenant_id": str,
"client_id": str,
"client_secret": str,
"allow_anonymous_login": Bool,
"exclude_environment_credential": Bool,
"exclude_visual_studio_code_credential": Bool,
"exclude_shared_token_cache_credential": Bool,
"exclude_managed_identity_credential": Bool,
Optional("verify", default=False): Bool,
"timeout": Coerce(int),
"read_timeout": Coerce(int),
"connection_timeout": Coerce(int),
**REMOTE_COMMON,
},
"oss": {
"oss_key_id": str,
"oss_key_secret": str,
"oss_endpoint": str,
Optional("verify", default=True): Bool,
**REMOTE_COMMON,
},
"gdrive": {
"profile": str,
"gdrive_use_service_account": Bool,
"gdrive_client_id": str,
"gdrive_client_secret": str,
"gdrive_user_credentials_file": str,
"gdrive_service_account_user_email": str,
"gdrive_service_account_json_file_path": str,
Optional("gdrive_trash_only", default=False): Bool,
Optional("gdrive_acknowledge_abuse", default=False): Bool,
Optional("verify", default=True): Bool,
**REMOTE_COMMON,
},
"http": HTTP_COMMON | REMOTE_COMMON,
"https": HTTP_COMMON | REMOTE_COMMON,
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
"remote": {str: object}, # Any of the above options are valid
}
)
str: ByUrl(REMOTE_SCHEMAS),
},
"state": {
"dir": str, # obsoleted
"row_limit": All(Coerce(int), Range(1)), # obsoleted
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
Marker("dir", description=DEPRECATED): str, # obsoleted
Marker("row_limit", description=DEPRECATED): All(
Coerce(int), Range(1)
), # obsoleted
Marker("row_cleanup_quota", description=DEPRECATED): All(
Coerce(int), Range(0, 100)
), # obsoleted
},
"index": {
"dir": str, # obsoleted
Marker("dir", description=DEPRECATED): str, # obsoleted
},
"machine": {
str: {
@@ -336,13 +349,13 @@ SCHEMA = {
"out_dir": str,
},
"exp": {
"code": str,
"data": str,
"models": str,
"metrics": str,
"params": str,
"plots": str,
"live": str,
Marker("code", description=DEPRECATED): str,
Marker("data", description=DEPRECATED): str,
Marker("models", description=DEPRECATED): str,
Marker("metrics", description=DEPRECATED): str,
Marker("params", description=DEPRECATED): str,
Marker("plots", description=DEPRECATED): str,
Marker("live", description=DEPRECATED): str,
"auto_push": Bool,
"git_remote": str,
},
@@ -371,3 +384,21 @@ SCHEMA = {
},
},
}
def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[str]":
for k, v in d.items():
if k in ("machine", "feature"):
continue
if isinstance(k, Marker):
if k.description == DEPRECATED:
continue
k = k.schema
if not isinstance(k, str):
continue
keypath = path + k
if isinstance(v, dict):
yield from config_vars_for_completion(v, keypath + ".")
else:
yield keypath
+4 -4
View File
@@ -32,10 +32,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
assert os.name == "nt"
from subprocess import ( # type: ignore[attr-defined]
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
CREATE_NEW_PROCESS_GROUP, # ty: ignore[unresolved-import]
CREATE_NO_WINDOW, # ty: ignore[unresolved-import]
STARTF_USESHOWWINDOW, # ty: ignore[unresolved-import]
STARTUPINFO, # ty: ignore[unresolved-import]
)
# https://stackoverflow.com/a/7006424
+1 -1
View File
@@ -195,7 +195,7 @@ def _build_sugiyama_layout(vertices, edges):
sug.yspace = VertexViewer.HEIGHT
sug.xspace = minw
sug.route_edge = route_with_lines
sug.route_edge = route_with_lines # ty: ignore[unresolved-attribute]
sug.draw()
+1 -1
View File
@@ -84,7 +84,7 @@ class Client:
except Exception as exc:
if callable(onerror):
onerror()
logger.exception(
logger.exception( # noqa: LOG007
"Could not connect to the database. "
"Check your database credentials and try again.",
exc_info=False,
+1 -1
View File
@@ -41,7 +41,7 @@ def _get(stage, p, info, **kwargs):
return Dependency(stage, p, info, **kwargs)
def loadd_from(stage, d_list):
def loadd_from(stage, d_list) -> list[Dependency]:
ret = []
for d in d_list:
p = d.pop(Output.PARAM_PATH, None)
+1
View File
@@ -110,6 +110,7 @@ class ParamsDependency(Dependency):
except MissingParamsFile:
return {}
assert self.repo
try:
return read_param_file(
self.repo.fs,
+1
View File
@@ -150,6 +150,7 @@ class RepoDependency(Dependency):
# local cache instead of the remote's cache. This avoids re-streaming of data,
# but messes up the call to `_get_remote_config()` downstream, which will need
# to ignore cache parameters.
assert self.repo
config["cache"] = self.repo.config["cache"]
config["cache"]["dir"] = self.repo.cache.local_cache_dir
+9 -4
View File
@@ -1,4 +1,5 @@
import os
from typing import Optional
import platformdirs
@@ -20,10 +21,16 @@ def global_config_dir():
)
def site_cache_dir():
def site_cache_dir(config_site_cache_dir: Optional[str] = None):
from platformdirs import PlatformDirs
from platformdirs.unix import Unix
if dvc_site_cache_dir := os.getenv(env.DVC_SITE_CACHE_DIR):
return dvc_site_cache_dir
if config_site_cache_dir is not None:
return config_site_cache_dir
if issubclass(Unix, PlatformDirs):
# Return the cache directory shared by users, e.g. `/var/tmp/$appname`
# NOTE: platformdirs>=5 changed `site_cache_dir` to return /var/cache/$appname.
@@ -34,6 +41,4 @@ def site_cache_dir():
# In the future, consider migrating to a more secure directory.
return f"/var/tmp/{APPNAME}" # noqa: S108
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
APPNAME, APPAUTHOR, opinion=True
)
return platformdirs.site_cache_dir(APPNAME, APPAUTHOR, opinion=True)
+63 -28
View File
@@ -162,6 +162,9 @@ class FileMixin:
def dump(self, stage, **kwargs):
raise NotImplementedError
def dump_stages(self, stages, **kwargs):
raise NotImplementedError
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
@@ -198,6 +201,13 @@ class SingleStageFile(FileMixin):
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
self.repo.scm_context.track_file(self.relpath)
def dump_stages(self, stages, **kwargs) -> None:
if not stages:
return None
assert len(stages) == 1, "SingleStageFile can only dump one stage."
return self.dump(stages[0], **kwargs)
def remove_stage(self, stage): # noqa: ARG002
self.remove()
@@ -228,17 +238,27 @@ class ProjectFile(FileMixin):
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
"""Dumps given stage appropriately in the dvcfile."""
return self.dump_stages(
[stage], update_pipeline=update_pipeline, update_lock=update_lock, **kwargs
)
def dump_stages(self, stages, update_pipeline=True, update_lock=True, **kwargs):
from dvc.stage import PipelineStage
assert isinstance(stage, PipelineStage)
if not stages:
return
for stage in stages:
assert isinstance(stage, PipelineStage)
if self.verify:
check_dvcfile_path(self.repo, self.path)
if update_pipeline and not stage.is_data_source:
self._dump_pipeline_file(stage)
if update_pipeline:
self._dump_pipeline_file(stages)
if update_lock:
self._dump_lockfile(stage, **kwargs)
self._dump_lockfile(stages, **kwargs)
def dump_dataset(self, dataset):
with modify_yaml(self.path, fs=self.repo.fs) as data:
@@ -260,32 +280,37 @@ class ProjectFile(FileMixin):
raw.append(dataset)
self.repo.scm_context.track_file(self.relpath)
def _dump_lockfile(self, stage, **kwargs):
self._lockfile.dump(stage, **kwargs)
def _dump_lockfile(self, stages, **kwargs):
self._lockfile.dump_stages(stages, **kwargs)
@staticmethod
def _check_if_parametrized(stage, action: str = "dump") -> None:
if stage.raw_data.parametrized:
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
def _dump_pipeline_file(self, stage):
self._check_if_parametrized(stage)
stage_data = serialize.to_pipeline_file(stage)
def _dump_pipeline_file(self, stages):
stages = stages if isinstance(stages, list) else [stages]
if not stages:
return
for stage in stages:
self._check_if_parametrized(stage)
with modify_yaml(self.path, fs=self.repo.fs) as data:
if not data:
logger.info("Creating '%s'", self.relpath)
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)
if existing_entry:
orig_stage_data = data["stages"][stage.name]
apply_diff(stage_data[stage.name], orig_stage_data)
else:
data["stages"].update(stage_data)
for stage in stages:
stage_data = serialize.to_pipeline_file(stage)
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)
if existing_entry:
orig_stage_data = data["stages"][stage.name]
apply_diff(stage_data[stage.name], orig_stage_data)
else:
data["stages"].update(stage_data)
self.repo.scm_context.track_file(self.relpath)
@@ -399,9 +424,12 @@ class Lockfile(FileMixin):
data.setdefault("stages", {})
self.repo.scm_context.track_file(self.relpath)
def dump(self, stage, **kwargs):
stage_data = serialize.to_lockfile(stage, **kwargs)
def dump_stages(self, stages, **kwargs):
if not stages:
return
is_modified = False
log_updated = False
with modify_yaml(self.path, fs=self.repo.fs) as data:
if not data:
data.update({"schema": "2.0"})
@@ -409,17 +437,24 @@ class Lockfile(FileMixin):
logger.info("Generating lock file '%s'", self.relpath)
data["stages"] = data.get("stages", {})
modified = data["stages"].get(stage.name, {}) != stage_data.get(
stage.name, {}
)
if modified:
logger.info("Updating lock file '%s'", self.relpath)
for stage in stages:
stage_data = serialize.to_lockfile(stage, **kwargs)
modified = data["stages"].get(stage.name, {}) != stage_data.get(
stage.name, {}
)
if modified:
is_modified = True
if not log_updated:
logger.info("Updating lock file '%s'", self.relpath)
log_updated = True
data["stages"].update(stage_data)
data["stages"].update(stage_data)
if modified:
if is_modified:
self.repo.scm_context.track_file(self.relpath)
def dump(self, stage, **kwargs):
self.dump_stages([stage], **kwargs)
def remove_stage(self, stage):
if not self.exists():
return
+3 -3
View File
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
from dvc.stage import Stage
class DvcException(Exception):
class DvcException(Exception): # noqa: N818
"""Base class for all dvc exceptions."""
def __init__(self, msg, *args):
@@ -242,11 +242,11 @@ class UploadError(FileTransferError):
class CheckoutError(DvcException):
def __init__(self, target_infos: list[str], stats: dict[str, list[str]]):
def __init__(self, target_infos: list[str], result: dict):
from dvc.utils import error_link
self.target_infos = target_infos
self.stats = stats
self.result = result
targets = [str(t) for t in target_infos]
m = (
"Checkout failed for following targets:\n{}\nIs your "
+2 -2
View File
@@ -1,8 +1,8 @@
from contextlib import ExitStack
from typing import TYPE_CHECKING, BinaryIO, Optional, Union
import fsspec
from fsspec.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
from fsspec.callbacks import TqdmCallback as _TqdmCallback
from dvc.progress import Tqdm
from dvc.utils.objects import cached_property
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
from dvc.ui._rich_progress import RichTransferProgress
class TqdmCallback(fsspec.callbacks.TqdmCallback):
class TqdmCallback(_TqdmCallback):
def __init__(
self,
size: Optional[int] = None,
+1 -1
View File
@@ -15,7 +15,7 @@ Examples
from urllib.parse import urlsplit
from upath import UPath
from upath import UPath # ty: ignore[unresolved-import]
class DVCPath(UPath):
+215 -102
View File
@@ -1,8 +1,9 @@
import functools
import os
import re
from collections import namedtuple
from collections.abc import Iterable, Iterator
from itertools import chain, groupby, takewhile
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Union, overload
from pathspec.patterns import GitWildMatchPattern
from pathspec.util import normalize_file
@@ -13,7 +14,9 @@ from dvc.log import logger
from dvc.pathspec_math import PatternInfo, merge_patterns
if TYPE_CHECKING:
from dvc.fs import AnyFSPath, FileSystem
from typing_extensions import Self
from dvc.fs import FileSystem
logger = logger.getChild(__name__)
@@ -21,36 +24,79 @@ logger = logger.getChild(__name__)
class DvcIgnore:
DVCIGNORE_FILE = ".dvcignore"
def __call__(self, root, dirs, files):
def __call__(
self, root: str, dirs: list[str], files: list[str]
) -> tuple[list[str], list[str]]:
raise NotImplementedError
class DvcIgnorePatterns(DvcIgnore):
def __init__(self, pattern_list, dirname, sep):
def __init__(
self, pattern_list: Iterable[Union[PatternInfo, str]], dirname: str, sep: str
) -> None:
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 = []
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])
if ignore is not None
pattern_infos = [
pattern if isinstance(pattern, PatternInfo) else PatternInfo(pattern, "")
for pattern in pattern_list
]
self.sep = sep
self.pattern_list: list[PatternInfo] = []
self.dirname = dirname
self.find_matching_pattern = functools.cache(self._find_matching_pattern)
regex_pattern_list: list[tuple[str, bool, bool, PatternInfo]] = []
for count, pattern_info in enumerate(pattern_infos):
regex, ignore = GitWildMatchPattern.pattern_to_regex(pattern_info.patterns)
if regex is not None and ignore is not None:
self.pattern_list.append(pattern_info)
regex = regex.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
regex_pattern_list.append(
(regex, ignore, pattern_info.patterns.endswith("/"), pattern_info)
)
def keyfunc(item: tuple[str, bool, bool, PatternInfo]) -> tuple[bool, bool]:
_, ignore, dir_only_pattern, _ = item
return ignore, dir_only_pattern
self.ignore_spec: list[
tuple[
re.Pattern[str],
bool,
bool,
dict[Optional[str], tuple[str, PatternInfo]],
]
]
self.ignore_spec = []
for (ignore, dir_only_pattern), group in groupby(
regex_pattern_list, key=keyfunc
):
if ignore:
# For performance, we combine all exclude patterns.
# But we still need to figure out which pattern matched which rule,
# (eg: to show in `dvc check-ignore`).
# So, we use named groups and keep a map of group name to pattern.
pattern_map: dict[Optional[str], tuple[str, PatternInfo]] = {
f"rule_{i}": (regex, pi)
for i, (regex, _, _, pi) in enumerate(group)
}
combined_regex = "|".join(
f"(?P<{name}>{regex})" for name, (regex, _) in pattern_map.items()
)
self.ignore_spec.append(
(re.compile(combined_regex), ignore, dir_only_pattern, pattern_map)
)
else:
# unignored patterns are not combined with `|`.
for regex, _, _, pi in group:
pattern_map = {None: (regex, pi)}
self.ignore_spec.append(
(re.compile(regex), ignore, dir_only_pattern, pattern_map)
)
@classmethod
def from_file(cls, path, fs, name):
def from_file(cls, path: str, fs: "FileSystem", name: str) -> "Self":
assert fs.isabs(path)
dirname = fs.normpath(fs.dirname(path))
with fs.open(path, encoding="utf-8") as fobj:
@@ -62,13 +108,15 @@ class DvcIgnorePatterns(DvcIgnore):
return cls(path_spec_lines, dirname, fs.sep)
def __call__(self, root: list[str], dirs: list[str], files: list[str]):
def __call__(
self, root: str, dirs: list[str], files: list[str]
) -> tuple[list[str], list[str]]:
files = [f for f in files if not self.matches(root, f)]
dirs = [d for d in dirs if not self.matches(root, d, True)]
return dirs, files
def _get_normalize_path(self, dirname, basename):
def _get_normalize_path(self, dirname: str, basename: str) -> Optional[str]:
# NOTE: `relpath` is too slow, so we have to assume that both
# `dirname` and `self.dirname` are relative or absolute together.
@@ -81,62 +129,105 @@ class DvcIgnorePatterns(DvcIgnore):
# NOTE: `os.path.join` is ~x5.5 slower
path = f"{rel}{self.sep}{basename}"
else:
return False
return None
if os.name == "nt":
return normalize_file(path)
return path
def matches(self, dirname, basename, is_dir=False, details: bool = False):
@overload
def matches(
self,
dirname: str,
basename: str,
is_dir: bool = False,
details: Literal[False] = ...,
) -> bool: ...
@overload
def matches(
self,
dirname: str,
basename: str,
is_dir: bool = False,
details: Literal[True] = ...,
) -> tuple[bool, list[PatternInfo]]: ...
@overload
def matches(
self,
dirname: str,
basename: str,
is_dir: bool = False,
details: bool = False,
) -> Union[bool, tuple[bool, list[PatternInfo]]]: ...
def matches(
self,
dirname: str,
basename: str,
is_dir: bool = False,
details: bool = False,
) -> Union[bool, tuple[bool, list[PatternInfo]]]:
path = self._get_normalize_path(dirname, basename)
if not path:
return False
if details:
return self._ignore_details(path, is_dir)
return self.ignore(path, is_dir)
def ignore(self, path, is_dir):
def matches(pattern, path, is_dir) -> bool:
matches_ = bool(pattern.match(path))
if is_dir:
matches_ |= bool(pattern.match(f"{path}/"))
return matches_
result = False
_match: list[PatternInfo] = []
if path:
result, _match = self._ignore(path, is_dir)
return (result, _match) if details else result
for ignore, pattern in self.ignore_spec[::-1]:
if matches(pattern, path, is_dir):
result = ignore
break
return result
def _find_matching_pattern(
self, path: str, is_dir: bool
) -> tuple[bool, list[PatternInfo]]:
paths = [path]
if is_dir and not path.endswith("/"):
paths.append(f"{path}/")
def _ignore_details(self, path, is_dir: bool):
result = []
for (regex, _), pattern_info in list(
zip(self.regex_pattern_list, self.pattern_list)
for pattern, ignore, dir_only_pattern, pattern_map in reversed(
self.ignore_spec
):
# skip system pattern
if not pattern_info.file_info:
if dir_only_pattern and not is_dir:
continue
for p in paths:
match = pattern.match(p)
if not match:
continue
if ignore:
group_name, _match = next(
(
(name, _match)
for name, _match in match.groupdict().items()
if name.startswith("rule_") and _match is not None
)
)
else:
# unignored patterns are not combined with `|`,
# so there are no groups.
group_name = None
_regex, pattern_info = pattern_map[group_name]
return ignore, [pattern_info]
return False, []
regex = re.compile(regex)
def _ignore(self, path: str, is_dir: bool) -> tuple[bool, list[PatternInfo]]:
parts = path.split("/")
result = False
matches: list[PatternInfo] = []
for i in range(1, len(parts) + 1):
rel_path = "/".join(parts[:i])
result, _matches = self.find_matching_pattern(
rel_path, is_dir or i < len(parts)
)
if i < len(parts) and not result:
continue
matches.extend(_matches)
if result:
break
return result, matches
matches = bool(regex.match(path))
if is_dir:
matches |= bool(regex.match(f"{path}/"))
if matches:
result.append(pattern_info.file_info)
return result
def __hash__(self):
def __hash__(self) -> int:
return hash(self.dirname + ":" + str(self.pattern_list))
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, DvcIgnorePatterns):
return NotImplemented
return (self.dirname == other.dirname) & (
@@ -144,21 +235,18 @@ class DvcIgnorePatterns(DvcIgnore):
== [pattern.patterns for pattern in other.pattern_list]
)
def __bool__(self):
def __bool__(self) -> bool:
return bool(self.pattern_list)
CheckIgnoreResult = namedtuple( # noqa: PYI024
"CheckIgnoreResult", ["file", "match", "patterns"]
)
def _no_match(path):
return CheckIgnoreResult(path, False, ["::"])
class CheckIgnoreResult(NamedTuple):
file: str
match: bool
pattern_infos: list[PatternInfo]
class DvcIgnoreFilter:
def __init__(self, fs, root_dir):
def __init__(self, fs: "FileSystem", root_dir: str) -> None:
from dvc.repo import Repo
default_ignore_patterns = [
@@ -193,7 +281,7 @@ class DvcIgnoreFilter:
ignore_subrepos=True,
)
def _get_key(self, path):
def _get_key(self, path: str) -> tuple[str, ...]:
parts = self.fs.relparts(path, self.root_dir)
if parts == (os.curdir,):
return ()
@@ -241,7 +329,7 @@ class DvcIgnoreFilter:
for dname in dnames:
self._update_sub_repo(self.fs.join(dirname, dname), ignore_trie)
def _update_sub_repo(self, path, ignore_trie: Trie):
def _update_sub_repo(self, path: str, ignore_trie: Trie) -> None:
from dvc.repo import Repo
if path == self.root_dir:
@@ -268,7 +356,9 @@ class DvcIgnoreFilter:
else:
ignore_trie[key] = new_pattern
def __call__(self, root, dirs, files, ignore_subrepos=True):
def __call__(
self, root: str, dirs: list[str], files: list[str], ignore_subrepos: bool = True
) -> tuple[list[str], list[str]]:
abs_root = self.fs.abspath(root)
ignore_pattern = self._get_trie_pattern(
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
@@ -277,7 +367,24 @@ class DvcIgnoreFilter:
dirs, files = ignore_pattern(abs_root, dirs, files)
return dirs, files
def ls(self, fs, path, detail=True, **kwargs):
@overload
def ls(
self, fs: "FileSystem", path: str, detail: Literal[True], **kwargs: Any
) -> list[dict[str, Any]]: ...
@overload
def ls(
self, fs: "FileSystem", path: str, detail: Literal[False], **kwargs
) -> list[str]: ...
@overload
def ls(
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs
) -> Union[list[str], list[dict[str, Any]]]: ...
def ls(
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs: Any
) -> Union[list[str], list[dict[str, Any]]]:
fs_dict = {}
dirs = []
nondirs = []
@@ -297,22 +404,29 @@ 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: str, **kwargs: Any
) -> Iterator[
Union[
tuple[str, list[str], list[str]],
tuple[str, dict[str, dict], dict[str, dict]],
]
]:
detail = kwargs.get("detail", False)
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
if fs.protocol == Schemes.LOCAL:
for root, dirs, files in fs.walk(path, **kwargs):
if detail:
all_dnames = set(dirs.keys())
all_fnames = set(files.keys())
assert isinstance(dirs, dict)
assert isinstance(files, dict)
dnames, fnames = self(
root,
all_dnames,
all_fnames,
list(dirs),
list(files),
ignore_subrepos=ignore_subrepos,
)
list(map(dirs.pop, all_dnames - set(dnames)))
list(map(files.pop, all_fnames - set(fnames)))
list(map(dirs.pop, dirs.keys() - set(dnames)))
list(map(files.pop, files.keys() - set(fnames)))
else:
dirs[:], files[:] = self(
root, dirs, files, ignore_subrepos=ignore_subrepos
@@ -321,7 +435,7 @@ class DvcIgnoreFilter:
else:
yield from fs.walk(path, **kwargs)
def find(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
def find(self, fs: "FileSystem", path: str, **kwargs: Any) -> Iterator[str]:
if fs.protocol == Schemes.LOCAL:
for root, _, files in self.walk(fs, path, **kwargs):
for file in files:
@@ -331,7 +445,7 @@ class DvcIgnoreFilter:
yield from fs.find(path)
def _get_trie_pattern(
self, dirname, dnames: Optional["list"] = None, ignore_subrepos=True
self, dirname: str, dnames: Optional[list[str]] = None, ignore_subrepos=True
) -> Optional["DvcIgnorePatterns"]:
if ignore_subrepos:
ignores_trie = self.ignores_trie_fs
@@ -367,7 +481,7 @@ class DvcIgnoreFilter:
def _is_ignored(
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
):
) -> bool:
if self._outside_repo(path):
return False
dirname, basename = self.fs.split(self.fs.normpath(path))
@@ -389,30 +503,29 @@ class DvcIgnoreFilter:
path = self.fs.abspath(path)
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
def _outside_repo(self, path):
def _outside_repo(self, path: str) -> bool:
return not self.fs.isin_or_eq(path, self.root_dir)
def check_ignore(self, target):
def check_ignore(self, target: str) -> CheckIgnoreResult:
# NOTE: can only be used in `dvc check-ignore`, see
# https://github.com/iterative/dvc/issues/5046
# https://github.com/treeverse/dvc/issues/5046
full_target = self.fs.abspath(target)
matched_patterns: list[PatternInfo] = []
ignore = False
if not self._outside_repo(full_target):
dirname, basename = self.fs.split(self.fs.normpath(full_target))
pattern = self._get_trie_pattern(dirname)
if pattern:
matches = pattern.matches(
dirname, basename, self.fs.isdir(full_target), True
ignore, matched_patterns = pattern.matches(
dirname, basename, self.fs.isdir(full_target), details=True
)
if matches:
return CheckIgnoreResult(target, True, matches)
return _no_match(target)
return CheckIgnoreResult(target, ignore, matched_patterns)
def is_ignored(
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
# https://github.com/treeverse/dvc/issues/4555
if fs.protocol != Schemes.LOCAL:
return False
if fs.isfile(path):
@@ -424,7 +537,7 @@ class DvcIgnoreFilter:
)
def init(path):
def init(path: Union[str, os.PathLike[str]]) -> str:
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
if os.path.exists(dvcignore):
return dvcignore
@@ -439,7 +552,7 @@ def init(path):
return dvcignore
def destroy(path):
def destroy(path: Union[str, os.PathLike[str]]) -> None:
from dvc.utils.fs import remove
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
+7 -6
View File
@@ -48,7 +48,10 @@ def get_dvc_info():
info.append("Cache types: " + error_link("no-dvc-cache"))
info.append(f"Caches: {_get_caches(repo.cache)}")
info.append(f"Remotes: {_get_remotes(repo.config)}")
configured_remotes = _get_remotes(repo.config)
remotes = ", ".join(configured_remotes) if configured_remotes else None
info.append(f"Remotes: {remotes}")
root_directory = repo.root_dir
fs_root = _get_fs_type(os.path.abspath(root_directory))
@@ -75,12 +78,10 @@ def _get_caches(cache):
def _get_remotes(config):
schemes = (
return [
get_fs_cls(get_fs_config(config, name=remote)).protocol
for remote in config["remote"]
)
return ", ".join(schemes) or "None"
]
def _get_linktype_support_info(repo):
@@ -157,7 +158,7 @@ def _get_fs_type(path):
except PermissionError:
pass
# need to follow the symlink: https://github.com/iterative/dvc/issues/5065
# need to follow the symlink: https://github.com/treeverse/dvc/issues/5065
path = pathlib.Path(path).resolve()
for parent in itertools.chain([path], path.parents):
+46 -20
View File
@@ -2,13 +2,13 @@
import hashlib
import os
import time
from abc import ABC, abstractmethod
from datetime import timedelta
from typing import Optional, Union
import flufl.lock
import zc.lockfile
from funcy import retry
from dvc.exceptions import DvcException
from dvc.progress import Tqdm
@@ -88,9 +88,10 @@ class Lock(LockBase):
Uses zc.lockfile as backend.
"""
def __init__(self, lockfile, friendly=False, **kwargs):
def __init__(self, lockfile, friendly=False, wait=False, **kwargs):
super().__init__(lockfile)
self._friendly = friendly
self._wait = wait
self._lock = None
self._lock_failed = False
@@ -101,23 +102,38 @@ class Lock(LockBase):
def _do_lock(self):
try:
self._lock_failed = False
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")
),
):
self._lock = zc.lockfile.LockFile(self._lockfile)
self._lock = zc.lockfile.LockFile(self._lockfile)
except zc.lockfile.LockError:
self._lock_failed = True
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
def lock(self):
retries = 6
delay = DEFAULT_TIMEOUT / retries
lock_retry = retry(retries, LockError, timeout=delay)(self._do_lock)
lock_retry()
"""Acquire the lock, either waiting forever, or after default_retries."""
default_retries = 6
delay = DEFAULT_TIMEOUT / default_retries
attempts = 0
max_retries = float("inf") if self._wait else default_retries
with Tqdm(
bar_format="{desc}",
disable=not self._friendly,
desc="Waiting to acquire lock. "
"If DVC froze, see `hardlink_lock` in {}.".format(
format_link("https://man.dvc.org/config#core")
),
) as pbar:
while True:
try:
self._do_lock()
return
except LockError:
attempts += 1
if attempts > max_retries:
raise
time.sleep(delay)
finally:
pbar.update()
def unlock(self):
if self._lock_failed:
@@ -150,7 +166,7 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
tmp_dir (str): a directory to store claim files.
"""
def __init__(self, lockfile, tmp_dir=None, **kwargs):
def __init__(self, lockfile, tmp_dir=None, wait=False, **kwargs):
import socket
self._tmp_dir = tmp_dir
@@ -166,18 +182,28 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
# number to generate the resulting lock file name, which is unique
# enough for our application.
#
# [1] https://github.com/iterative/dvc/issues/2582
# [1] https://github.com/treeverse/dvc/issues/2582
self._hostname = socket.gethostname()
self._lifetime = timedelta(days=365) # Lock for good by default
self._separator = flufl.lock.SEP
self._set_claimfile()
self._owned = True
self._wait = wait
self._retry_errnos = []
self._friendly = kwargs.get("friendly", False)
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
try:
super().lock(timeout or timedelta(seconds=DEFAULT_TIMEOUT))
if not self._wait:
timeout = timeout or timedelta(seconds=DEFAULT_TIMEOUT)
with Tqdm(
bar_format="{desc}",
disable=not (self._wait and self._friendly),
desc="Waiting to acquire lock",
):
super().lock(timeout)
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
@@ -186,11 +212,11 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
hasher = hashlib.md5(self._claimfile.encode(), usedforsecurity=False)
filename = hasher.hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False, wait=False):
cls = HardlinkLock if hardlink_lock else Lock
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly)
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly, wait=wait)
+15 -5
View File
@@ -1,10 +1,10 @@
"""Manages logging configuration for DVC repo."""
import logging
import logging.config
import logging.handlers
import os
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from typing import ClassVar
import colorama
@@ -53,7 +53,7 @@ def add_logging_level(level_name, level_num, method_name=None):
setattr(logging, method_name, log_to_root)
class LoggingException(Exception):
class LoggingException(Exception): # noqa: N818
def __init__(self, record):
msg = f"failed to log {record!s}"
super().__init__(msg)
@@ -174,9 +174,19 @@ def _iter_causes(exc):
exc = exc.__cause__
def set_loggers_level(level: int = logging.INFO) -> None:
@contextmanager
def set_loggers_level(level: int = logging.INFO) -> Iterator[None]:
ret: dict[logging.Logger, int] = {}
for name in ["dvc", "dvc_objects", "dvc_data"]:
logging.getLogger(name).setLevel(level)
_logger = logging.getLogger(name)
ret[_logger] = _logger.getEffectiveLevel()
_logger.setLevel(level)
try:
yield
finally:
for _logger, old_lvl in ret.items():
_logger.setLevel(old_lvl)
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
+63 -26
View File
@@ -42,10 +42,11 @@ from .utils import relpath
from .utils.fs import path_isin
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc_data.hashfile.obj import HashFile
from dvc_data.index import DataIndexKey
from .ignore import DvcIgnoreFilter
from .ignore import CheckIgnoreResult, DvcIgnoreFilter
logger = logger.getChild(__name__)
@@ -83,7 +84,7 @@ def _get(stage, path, **kwargs):
return Output(stage, path, **kwargs)
def loadd_from(stage, d_list):
def loadd_from(stage, d_list) -> list["Output"]:
ret = []
for d in d_list:
p = d.pop(Output.PARAM_PATH)
@@ -267,9 +268,9 @@ class OutputIsStageFileError(DvcException):
class OutputIsIgnoredError(DvcException):
def __init__(self, match):
lines = "\n".join(match.patterns)
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
def __init__(self, result: "CheckIgnoreResult"):
pi = result.pattern_infos[-1]
super().__init__(f"Path {result.file!r} is ignored by {pi!s}")
class CheckoutCallback(TqdmCallback):
@@ -329,7 +330,7 @@ class Output:
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
)
self.repo = stage.repo if not repo and stage else repo
self.repo: Optional[Repo] = stage.repo if not repo and stage else repo
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
@@ -511,6 +512,7 @@ class Output:
from dvc.cachemgr import LEGACY_HASH_NAMES
assert self.is_in_repo
assert self.repo
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
return getattr(self.repo.cache, odb_name)
@@ -518,6 +520,7 @@ class Output:
def local_cache(self):
from dvc.cachemgr import LEGACY_HASH_NAMES
assert self.repo
if self.hash_name in LEGACY_HASH_NAMES:
return self.repo.cache.legacy
return self.repo.cache.local
@@ -585,6 +588,7 @@ class Output:
def index_key(self) -> tuple[str, "DataIndexKey"]:
if self.is_in_repo:
workspace = "repo"
assert self.repo
key = self.repo.fs.relparts(self.fs_path, self.repo.root_dir)
else:
workspace = self.fs.protocol
@@ -640,6 +644,7 @@ class Output:
@property
def dvcignore(self) -> Optional["DvcIgnoreFilter"]:
if self.fs.protocol == "local":
assert self.repo
return self.repo.dvcignore
return None
@@ -661,6 +666,7 @@ class Output:
if not self.use_scm_ignore:
return
assert self.repo
if self.repo.scm.is_tracked(self.fspath):
raise OutputAlreadyTrackedError(self)
@@ -670,6 +676,7 @@ class Output:
if not self.use_scm_ignore:
return
assert self.repo
self.repo.scm_context.ignore_remove(self.fspath)
def save(self) -> None:
@@ -776,6 +783,7 @@ class Output:
callback=cb,
)
if relink:
assert self.repo
rel = self.fs.relpath(filter_info or self.fs_path)
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
self._checkout(
@@ -957,6 +965,7 @@ class Output:
added = not self.exists
try:
assert self.repo
modified = self._checkout(
filter_info or self.fs_path,
self.fs,
@@ -987,18 +996,24 @@ class Output:
if ignore_remove:
self.ignore_remove()
def move(self, out):
def move(self, out: "Output") -> None:
src_exists = self.exists
if src_exists:
self.fs.move(self.fs_path, out.fs_path)
else:
logger.warning("%r missing", self.fspath)
if self.protocol == "local" and self.use_scm_ignore:
assert self.repo
self.repo.scm_context.ignore_remove(self.fspath)
self.fs.move(self.fs_path, out.fs_path)
self.def_path = out.def_path
self.fs_path = out.fs_path
self.save()
self.commit()
if self.protocol == "local" and self.use_scm_ignore:
self.repo.scm_context.ignore(self.fspath)
try:
self.save()
self.commit()
except self.DoesNotExistError:
self.ignore()
def transfer(
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
@@ -1079,6 +1094,7 @@ class Output:
if self.remote:
kwargs["remote"] = self.remote
with suppress(Exception):
assert self.repo
self.repo.cloud.pull([obj.hash_info], **kwargs)
if self.obj:
@@ -1172,6 +1188,7 @@ class Output:
return {}
if self.remote:
assert self.repo
remote_odb = self.repo.cloud.get_remote_odb(
name=self.remote, hash_name=self.hash_name
)
@@ -1203,8 +1220,9 @@ class Output:
if stage:
abs_path = os.path.join(stage.wdir, path)
if self._is_path_dvcignore(abs_path):
check = stage.repo.dvcignore.check_ignore(abs_path)
raise self.IsIgnoredError(check)
check: CheckIgnoreResult = stage.repo.dvcignore.check_ignore(abs_path)
if check.match:
raise self.IsIgnoredError(check)
def _check_can_merge(self, out):
if self.protocol != out.protocol:
@@ -1452,25 +1470,44 @@ class Output:
self.remote = other.remote
self.can_push = other.can_push
def merge_version_meta(self, other: "Output"):
def _get_versioned_meta(
self,
) -> Optional[
tuple["HashInfo", Optional["Meta"], Optional[Union["HashFile", "Tree"]]]
]:
if self.files is not None or (
self.meta is not None and self.meta.version_id is not None
):
old_obj = self.obj if self.obj is not None else self.get_obj()
return self.hash_info, self.meta, old_obj
return None
def merge_version_meta(
self,
old_hi: "HashInfo",
old_meta: Optional["Meta"],
old_obj: Optional[Union["HashFile", "Tree"]],
):
"""Merge version meta for files which are unchanged from other."""
if not self.hash_info:
return
return None
if self.hash_info.isdir:
return self._merge_dir_version_meta(other)
if self.hash_info != other.hash_info:
return
self.meta = other.meta
return self._merge_dir_version_meta(old_hi, old_obj)
if self.hash_info != old_hi:
return None
self.meta = old_meta
return None
def _merge_dir_version_meta(self, other: "Output"):
def _merge_dir_version_meta(
self, old_hi: "HashInfo", old_obj: Optional[Union["HashFile", "Tree"]]
):
from dvc_data.hashfile.tree import update_meta
if not self.obj or not other.hash_info.isdir:
if not self.obj or not old_hi.isdir:
return
other_obj = other.obj if other.obj is not None else other.get_obj()
assert isinstance(self.obj, Tree)
assert isinstance(other_obj, Tree)
updated = update_meta(self.obj, other_obj)
assert isinstance(old_obj, Tree)
updated = update_meta(self.obj, old_obj)
assert updated.hash_info == self.obj.hash_info
self.obj = updated
self.files = updated.as_list(with_meta=True)
+4 -4
View File
@@ -66,7 +66,7 @@ class ParamsLoadError(ContextError):
pass
class KeyNotInContext(ContextError, KeyError):
class KeyNotInContext(ContextError, KeyError): # noqa: N818
def __init__(self, key: str) -> None:
self.key: str = key
super().__init__(f"Could not find '{key}'")
@@ -75,7 +75,7 @@ class KeyNotInContext(ContextError, KeyError):
return self.msg
class VarsAlreadyLoaded(ContextError):
class VarsAlreadyLoaded(ContextError): # noqa: N818
pass
@@ -155,7 +155,7 @@ class Value(Node):
PRIMITIVES = (int, float, str, bytes, bool)
class Container(Node, ABC):
class Container(Node, ABC): # noqa: PLW1641
meta: Meta
data: Union[list, dict]
_key_transform = staticmethod(identity)
@@ -272,7 +272,7 @@ class CtxDict(Container, MutableMapping):
if not isinstance(key, str):
# limitation for the interpolation
# ignore other kinds of keys
return
return None
return super().__setitem__(key, value)
def merge_update(self, other, overwrite=False):
+1
View File
@@ -202,6 +202,7 @@ def validate_value(value, key):
if isinstance(value, dict) and key == "cmd":
return True
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
return None
def str_interpolate(
+8 -2
View File
@@ -2,13 +2,19 @@
# Including changing base dir of path specification patterns and merging
# of two path specification patterns with different base
# All the operations follow the documents of `gitignore`
from collections import namedtuple
from typing import NamedTuple
from pathspec.util import normalize_file
from dvc.utils import relpath
PatternInfo = namedtuple("PatternInfo", ["patterns", "file_info"]) # noqa: PYI024
class PatternInfo(NamedTuple):
patterns: str
file_info: str
def __str__(self) -> str:
return self.file_info or f":{self.patterns}"
def _not_ignore(rule):
+1 -1
View File
@@ -167,7 +167,7 @@ class VegaConverter(Converter):
if isinstance(y, list):
return "y"
if not isinstance(y, dict):
return
return None
fields = {field for _, field in _file_field(y)}
if len(fields) == 1:
-1
View File
@@ -2,7 +2,6 @@ import os
from collections import defaultdict
from typing import TYPE_CHECKING, NamedTuple, Optional
import dpath
import dpath.options
from funcy import get_in, last
+12 -5
View File
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
from dvc.fs.data import DataFileSystem
from dvc.fs.dvc import DVCFileSystem
from dvc.lock import LockBase
from dvc.output import Output
from dvc.scm import Git, NoSCM
from dvc.stage import Stage
from dvc.types import DictStrAny
@@ -144,6 +145,7 @@ class Repo:
scm: Optional[Union["Git", "NoSCM"]] = None,
remote: Optional[str] = None,
remote_config: Optional["DictStrAny"] = None,
_wait_for_lock: bool = False,
):
from dvc.cachemgr import CacheManager
from dvc.data_cloud import DataCloud
@@ -166,7 +168,8 @@ class Repo:
self._config = config
self._remote = remote
self._remote_config = remote_config
self._data_index = None
self._data_index: Optional[DataIndex] = None
self._wait_for_lock = _wait_for_lock
if rev and not fs:
self._scm = scm = SCM(root_dir or os.curdir)
@@ -204,6 +207,7 @@ class Repo:
tmp_dir=self.tmp_dir,
hardlink_lock=self.config["core"].get("hardlink_lock", False),
friendly=True,
wait=self._wait_for_lock,
)
os.makedirs(self.site_cache_dir, exist_ok=True)
if not fs and (
@@ -541,7 +545,9 @@ class Repo:
return used
def find_outs_by_path(self, path, outs=None, recursive=False, strict=True):
def find_outs_by_path(
self, path, outs=None, recursive=False, strict=True
) -> list["Output"]:
# using `outs_graph` to ensure graph checks are run
outs = outs or self.index.outs_graph
@@ -609,7 +615,7 @@ class Repo:
from dvc.fs import GitFileSystem
from dvc.version import version_tuple
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
cache_dir = site_cache_dir(self.config["core"].get("site_cache_dir"))
subdir = None
if isinstance(self.fs, GitFileSystem):
@@ -638,10 +644,11 @@ class Repo:
# that just happened to be at the same path as old deleted ones.
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
md5 = hashlib.md5( # noqa: S324
md5 = hashlib.md5(
str(
(root_dir, subdir, btime, getpass.getuser(), version_tuple[0], salt)
).encode()
).encode(),
usedforsecurity=False,
)
repo_token = md5.hexdigest()
return os.path.join(repos_dir, repo_token)
+1 -1
View File
@@ -40,7 +40,7 @@ def check_name_format(name: str) -> None:
def name_is_compatible(name: str) -> bool:
"""
Only needed by DVCLive per iterative/dvclive#715
Only needed by DVCLive per treeverse/dvclive#715
Will be removed in future release.
"""
from gto.constants import assert_name_is_valid
+52 -24
View File
@@ -1,4 +1,5 @@
import os
from collections import defaultdict
from typing import TYPE_CHECKING
from dvc.exceptions import (
@@ -14,7 +15,9 @@ from dvc.utils import relpath
from . import locked
if TYPE_CHECKING:
from dvc_data.index import BaseDataIndex, DataIndexEntry
from dvc.repo.index import IndexView
from dvc_data.index import BaseDataIndex, DataIndexEntry, DataIndexKey
from dvc_data.index.diff import Change
from dvc_objects.fs.base import FileSystem
logger = logger.getChild(__name__)
@@ -36,10 +39,12 @@ def _remove_unused_links(repo):
return ret
def _build_out_changes(index, changes):
def _build_out_changes(
index: "IndexView", changes: dict["DataIndexKey", "Change"]
) -> dict["DataIndexKey", tuple[str, dict[str, int]]]:
from dvc_data.index.checkout import MODIFY
out_keys = []
out_keys: list[DataIndexKey] = []
for out in index.outs:
if not out.use_cache:
continue
@@ -47,19 +52,28 @@ def _build_out_changes(index, changes):
ws, key = out.index_key
if ws != "repo":
continue
out_keys.append(key)
out_changes = {}
out_stats: dict[DataIndexKey, dict[str, int]]
out_stats = defaultdict(lambda: defaultdict(int))
out_changes: dict[DataIndexKey, tuple[str, dict[str, int]]] = {}
for key, change in changes.items():
typ = change.typ
isdir = change.new and change.new.isdir
for out_key in out_keys:
if len(out_key) > len(key) or key[: len(out_key)] != out_key:
continue
stats = out_stats[out_key]
if not isdir:
stats[typ] += 1
if key == out_key:
out_changes[out_key] = change.typ
elif not out_changes.get(out_key):
out_changes[out_key] = MODIFY
out_changes[out_key] = typ, stats
elif out_key not in out_changes:
typ = MODIFY
out_changes[out_key] = typ, stats
break
return out_changes
@@ -107,14 +121,13 @@ def checkout( # noqa: C901
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
from dvc_data.index.checkout import ADD, DELETE, MODIFY, apply, compare
stats: dict[str, list[str]] = {
"added": [],
"deleted": [],
"modified": [],
}
stats = {"modified": 0, "added": 0, "deleted": 0}
changes: dict[str, list[str]] = {"modified": [], "added": [], "deleted": []}
if not targets:
targets = [None]
stats["deleted"] = _remove_unused_links(self)
changes["deleted"] = _remove_unused_links(self)
stats["deleted"] = len(changes["deleted"])
if isinstance(targets, str):
targets = [targets]
@@ -127,8 +140,10 @@ def checkout( # noqa: C901
raise CheckoutErrorSuggestGit(target) from exc
raise # noqa: PLE0704
view = self.index.targets_view(
targets, recursive=recursive, with_deps=with_deps, onerror=onerror
from .index import index_from_targets
view = index_from_targets(
self, targets=targets, recursive=recursive, with_deps=with_deps, onerror=onerror
)
with ui.progress(unit="entry", desc="Building workspace index", leave=True) as pb:
@@ -149,7 +164,10 @@ def checkout( # noqa: C901
def checkout_onerror(src_path, dest_path, _exc):
logger.debug(
"failed to create '%s' from '%s'", dest_path, src_path, exc_info=True
"failed to create '%s' from '%s'",
dest_path,
src_path,
exc_info=True, # noqa: LOG014
)
for out_path in out_paths:
@@ -171,16 +189,26 @@ def checkout( # noqa: C901
out_changes = _build_out_changes(view, diff.changes)
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
for key, typ in out_changes.items():
for key, (typ, _stats) in out_changes.items():
out_path = self.fs.join(self.root_dir, *key)
if out_path in failed:
self.fs.remove(out_path, recursive=True)
else:
self.state.save_link(out_path, self.fs)
stats[typ_map[typ]].append(_fspath_dir(out_path))
continue
self.state.save_link(out_path, self.fs)
for t, count in _stats.items():
stats_typ = typ_map[t]
stats[stats_typ] += count
changes[typ_map[typ]].append(_fspath_dir(out_path))
for changelist in changes.values():
# group directories first, then files. But keep them alphabetically sorted
changelist.sort(key=lambda p: (not p.endswith(os.sep), p))
result = changes | {"stats": stats}
if failed and not allow_missing:
raise CheckoutError([relpath(out_path) for out_path in failed], stats)
return stats
result["failed"] = [relpath(out_path) for out_path in failed]
raise CheckoutError([relpath(out_path) for out_path in failed], result)
return result
+3 -3
View File
@@ -43,13 +43,13 @@ def _collect_paths(
def _filter_outs(
outs: Outputs, fs_paths: StrPaths, duplicates=False
repo: "Repo", outs: Outputs, fs_paths: StrPaths, duplicates=False
) -> tuple[Outputs, StrPaths]:
res_outs: Outputs = []
fs_res_paths = fs_paths
for out in outs:
fs_path = out.repo.dvcfs.from_os_path(out.fs_path)
fs_path = repo.dvcfs.from_os_path(out.fs_path)
if fs_path in fs_paths:
res_outs.append(out)
if not duplicates:
@@ -77,4 +77,4 @@ def collect(
target_paths = _collect_paths(repo, targets, recursive=recursive)
return _filter_outs(outs, target_paths, duplicates=duplicates)
return _filter_outs(repo, outs, target_paths, duplicates=duplicates)
+32 -22
View File
@@ -1,3 +1,4 @@
from itertools import groupby
from typing import TYPE_CHECKING
from dvc import prompt
@@ -52,29 +53,38 @@ def commit(
data_only=False,
relink=True,
):
stages_info = [
info
for info in self.stage.collect_granular(
target, with_deps=with_deps, recursive=recursive
)
if not data_only or info.stage.is_data_source
]
for stage_info in stages_info:
stage = stage_info.stage
if force:
stage.save(allow_missing=allow_missing)
else:
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
committed_stages = []
groups = groupby(
[
info
for info in self.stage.collect_granular(
target, with_deps=with_deps, recursive=recursive
)
if not data_only or info.stage.is_data_source
],
key=lambda info: info.stage.dvcfile,
)
for dvcfile, stages_info_group in groups:
to_dump = []
for stage_info in stages_info_group:
stage = stage_info.stage
if force:
stage.save(allow_missing=allow_missing)
stage.commit(
filter_info=stage_info.filter_info,
allow_missing=allow_missing,
relink=relink,
)
stage.dump(update_pipeline=False)
return [s.stage for s in stages_info]
else:
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
stage.save(allow_missing=allow_missing)
stage.commit(
filter_info=stage_info.filter_info,
allow_missing=allow_missing,
relink=relink,
)
to_dump.append(stage)
dvcfile.dump_stages(to_dump, update_pipeline=False)
committed_stages.extend(to_dump)
return committed_stages
@locked
+397 -78
View File
@@ -1,38 +1,34 @@
import os
import posixpath
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, TypedDict, Union
from collections import defaultdict, deque
from collections.abc import Iterable, Iterator, Mapping
from typing import TYPE_CHECKING, Optional, TypedDict, Union
from dvc.fs.callbacks import DEFAULT_CALLBACK
from dvc.fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
from dvc.log import logger
from dvc.ui import ui
from dvc_data.index import DataIndexDirError
if TYPE_CHECKING:
from dvc.fs.callbacks import Callback
from dvc.repo import Repo
from dvc.scm import Git, NoSCM
from dvc_data.index import DataIndex
from dvc_data.index import (
BaseDataIndex,
DataIndex,
DataIndexEntry,
DataIndexKey,
DataIndexView,
)
from dvc_data.index.diff import Change
from dvc_objects.fs.base import FileSystem
logger = logger.getChild(__name__)
def posixpath_to_os_path(path: str) -> str:
return path.replace(posixpath.sep, os.path.sep)
def _adapt_typ(typ: str) -> str:
from dvc_data.index.diff import ADD, DELETE, MODIFY
if typ == MODIFY:
return "modified"
if typ == ADD:
return "added"
if typ == DELETE:
return "deleted"
return typ
def _adapt_path(change: "Change") -> str:
isdir = False
if change.new and change.new.meta:
@@ -45,27 +41,98 @@ def _adapt_path(change: "Change") -> str:
return os.path.sep.join(key)
def _adapt_path_from_entry(entry: "DataIndexEntry") -> str:
key = entry.key
assert key
if entry.meta and entry.meta.isdir:
key = (*key, "")
return os.sep.join(key)
def _get_missing_paths(
to_check: Mapping["FileSystem", Mapping[str, Iterable["DataIndexEntry"]]],
batch_size: Optional[int] = None,
callback: "Callback" = DEFAULT_CALLBACK,
) -> Iterator[str]:
for fs, paths_map in to_check.items():
if batch_size == 1 or (batch_size is None and fs.protocol == "local"):
results = list(callback.wrap(map(fs.exists, paths_map)))
else:
results = fs.exists(
list(paths_map), batch_size=batch_size, callback=callback
)
for cache_path, exists in zip(paths_map, results):
if exists:
continue
for entry in paths_map[cache_path]:
yield _adapt_path_from_entry(entry)
class StorageCallback(Callback):
def __init__(self, parent_cb: Callback) -> None:
super().__init__(size=0, value=0)
self.parent_cb = parent_cb
def set_size(self, size: int) -> None:
# This is a no-op to prevent `fs.exists` from trying to set the size
pass
def relative_update(self, value: int = 1) -> None:
self.parent_cb.relative_update(value)
def absolute_update(self, value: int) -> None:
self.parent_cb.relative_update(value - self.value)
class Rename(TypedDict):
old: str
new: str
class DiffResult(TypedDict, total=False):
modified: list[str]
added: list[str]
deleted: list[str]
renamed: list[Rename]
unchanged: list[str]
unknown: list[str]
not_in_cache: list[str]
def _diff(
old: "DataIndex",
new: "DataIndex",
old: "BaseDataIndex",
new: "BaseDataIndex",
*,
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
granular: bool = False,
not_in_cache: bool = False,
not_in_remote: bool = False,
remote_refresh: bool = False,
batch_size: Optional[int] = None,
callback: "Callback" = DEFAULT_CALLBACK,
) -> dict[str, list[str]]:
from dvc_data.index import StorageError
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
with_renames: bool = False,
) -> DiffResult:
from dvc_data.index.diff import (
ADD,
DELETE,
MODIFY,
RENAME,
UNCHANGED,
UNKNOWN,
diff,
)
ret: dict[str, list[str]] = {}
ret: DiffResult = defaultdict(list) # type: ignore[assignment]
change_types = {
MODIFY: "modified",
ADD: "added",
DELETE: "deleted",
RENAME: "renamed",
}
def _add_change(typ, change):
typ = _adapt_typ(typ)
if typ not in ret:
ret[typ] = []
ret[typ].append(_adapt_path(change))
to_check: dict[FileSystem, dict[str, list[DataIndexEntry]]] = defaultdict(
lambda: defaultdict(list)
)
for change in diff(
old,
@@ -74,45 +141,72 @@ def _diff(
shallow=not granular,
hash_only=True,
with_unknown=True,
with_renames=with_renames,
callback=callback,
):
typ = change.typ
# The index is a trie, so even when we filter by a specific path
# like `dir/file`, all parent nodes leading to that path (e.g., `dir/`)
# still appear in the view. As a result, keys like `dir/` will be present
# even if only `dir/file` matches the filter.
# We need to skip such entries to avoid showing root of tracked directories.
if filter_keys:
# RENAME does not have a `change.key`
if typ == RENAME:
assert change.new
key = change.new.key
# match with "new" key only
assert key
else:
key = change.key
if not any(key[: len(fk)] == fk for fk in filter_keys):
continue
if (
change.typ == UNCHANGED
typ == UNCHANGED
and (not change.old or not change.old.hash_info)
and (not change.new or not change.new.hash_info)
):
# NOTE: emulating previous behaviour
continue
if change.typ == UNKNOWN and not change.new:
if typ == UNKNOWN and not change.new:
# NOTE: emulating previous behaviour
continue
if (
not_in_cache
and change.old
and change.old.hash_info
and not old.storage_map.cache_exists(change.old)
):
# NOTE: emulating previous behaviour
_add_change("not_in_cache", change)
if not_in_cache and change.old and change.old.hash_info:
old_entry = change.old
cache_fs, cache_path = old.storage_map.get_cache(old_entry)
# check later in batches
to_check[cache_fs][cache_path].append(old_entry)
try:
if (
not_in_remote
and change.old
and change.old.hash_info
and not old.storage_map.remote_exists(
change.old, refresh=remote_refresh
)
):
_add_change("not_in_remote", change)
except StorageError:
pass
change_typ = change_types.get(typ, typ)
if typ == RENAME:
assert change.old is not None
assert change.new is not None
ret["renamed"].append(
{
"old": _adapt_path_from_entry(change.old),
"new": _adapt_path_from_entry(change.new),
}
)
else:
ret[change_typ].append(_adapt_path(change)) # type: ignore[literal-required]
_add_change(change.typ, change)
return ret
total_items = sum(
len(entries) for paths in to_check.values() for entries in paths.values()
)
with TqdmCallback(size=total_items, desc="Checking cache", unit="entry") as cb:
missing_items = list(
_get_missing_paths(
to_check, batch_size=batch_size, callback=StorageCallback(cb)
),
)
if missing_items:
ret["not_in_cache"] = missing_items
return dict(ret) # type: ignore[return-value]
class GitInfo(TypedDict, total=False):
@@ -151,7 +245,49 @@ def _git_info(scm: Union["Git", "NoSCM"], untracked_files: str = "all") -> GitIn
)
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> dict[str, list[str]]:
def filter_index(
index: Union["DataIndex", "DataIndexView"],
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
) -> "BaseDataIndex":
from dvc_data.index.view import DataIndexView
if not filter_keys:
return index
if isinstance(index, DataIndexView):
orig_index = index._index
parent_filter_fn = index.filter_fn
else:
orig_index = index
parent_filter_fn = None
def filter_fn(key: "DataIndexKey") -> bool:
if parent_filter_fn is not None and not parent_filter_fn(key):
return False
for filter_key in filter_keys:
# eg: if key is "dir/file" and filter_key is "dir/", return True
if key[: len(filter_key)] == filter_key:
return True
# eg: if key is `dir/` and filter_key is `dir/file`, also return True.
# This ensures we include parent prefixes needed to reach matching leaves.
# Intermediate prefixes must be retained to access nested keys.
if filter_key[: len(key)] == key:
return True
return False
from dvc_data.index import view
return view(orig_index, filter_fn=filter_fn)
def _diff_index_to_wtree(
repo: "Repo",
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
granular: bool = False,
batch_size: Optional[int] = None,
with_renames: bool = False,
) -> DiffResult:
from .index import build_data_index
with ui.progress(desc="Building workspace index", unit="entry") as pb:
@@ -162,37 +298,69 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> dict[str, list[str]]:
compute_hash=True,
callback=pb.as_callback(),
)
workspace_view = filter_index(workspace, filter_keys=filter_keys)
with ui.progress(
desc="Calculating diff between index/workspace",
unit="entry",
) as pb:
index = repo.index.data["repo"]
view = filter_index(index, filter_keys=filter_keys)
return _diff(
repo.index.data["repo"],
workspace,
view,
workspace_view,
filter_keys=filter_keys,
granular=granular,
not_in_cache=True,
with_renames=with_renames,
batch_size=batch_size,
callback=pb.as_callback(),
**kwargs,
)
def _diff_head_to_index(
repo: "Repo", head: str = "HEAD", **kwargs: Any
) -> dict[str, list[str]]:
index = repo.index.data["repo"]
repo: "Repo",
head: str = "HEAD",
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
granular: bool = False,
with_renames: bool = False,
) -> DiffResult:
from dvc.exceptions import NotDvcRepoError
from dvc.scm import RevError
from dvc_data.index import DataIndex
with repo.switch(head):
head_index = repo.index.data["repo"]
index = repo.index.data["repo"]
index_view = filter_index(index, filter_keys=filter_keys)
try:
with repo.switch(head):
head_index = repo.index.data["repo"]
head_view = filter_index(head_index, filter_keys=filter_keys)
except RevError:
logger.debug("failed to switch to '%s'", head)
head_view = DataIndex()
except NotDvcRepoError as exc:
# NOTE: this only gets raised on subdir repos at the moment,
# which looks like a bug in `repo.switch`.
logger.warning(exc)
head_view = DataIndex()
with ui.progress(desc="Calculating diff between head/index", unit="entry") as pb:
return _diff(head_index, index, callback=pb.as_callback(), **kwargs)
return _diff(
head_view,
index_view,
filter_keys=filter_keys,
granular=granular,
with_renames=with_renames,
callback=pb.as_callback(),
)
class Status(TypedDict):
not_in_cache: list[str]
not_in_remote: list[str]
committed: dict[str, list[str]]
uncommitted: dict[str, list[str]]
committed: DiffResult
uncommitted: DiffResult
untracked: list[str]
unchanged: list[str]
git: GitInfo
@@ -217,27 +385,178 @@ def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> list[str]
return [repo.fs.relpath(file, start) for file in files]
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
def iter_index(
index: Union["BaseDataIndex", "DataIndexView"], shallow: bool = False
) -> Iterator[tuple["DataIndexKey", Optional["DataIndexEntry"]]]:
if not shallow:
yield from index.iteritems(shallow=shallow)
return
# only iterate until we find entries with hash_info in shallow mode
todo: deque[tuple[DataIndexKey, dict]] = deque([((), index.info(()))])
while todo:
key, info = todo.popleft()
entry = info.get("entry")
if info.get("type") == "directory" and not (entry and entry.hash_info):
try:
todo.extend(index.ls(key, detail=True))
except (KeyError, DataIndexDirError):
pass
yield key, entry
def _get_entries_not_in_remote(
repo: "Repo",
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
granular: bool = False,
remote_refresh: bool = False,
) -> list[str]:
"""Get entries that are not in remote storage."""
from dvc.repo.worktree import worktree_view
from dvc_data.index import StorageKeyError
entries: dict[DataIndexKey, DataIndexEntry] = {}
def _onerror(entry, exc):
if not isinstance(exc, DataIndexDirError):
raise exc
# We don't have the contents of this dir file, so we will only check this key.
entries[entry.key] = entry
# View into the index, with only pushable entries
index = worktree_view(repo.index, push=True)
data_index = index.data["repo"]
orig_data_index_onerror = data_index.onerror
data_index.onerror = _onerror
view = filter_index(data_index, filter_keys=filter_keys) # type: ignore[arg-type]
missing_entries = []
storage_map = view.storage_map
n = 0
with TqdmCallback(size=n, desc="Checking remote", unit="entry") as cb:
for key, entry in iter_index(view, shallow=not granular):
if not (entry and entry.hash_info):
continue
# The index is a trie, so even when we filter by a specific path
# like `dir/file`, all parent nodes leading to that path (e.g., `dir/`)
# still appear in the view. As a result, keys like `dir/` will be present
# even if only `dir/file` matches the filter.
# We need to skip such entries to avoid showing root of tracked directories.
if filter_keys and not any(
key[: len(filter_key)] == filter_key for filter_key in filter_keys
):
continue
entries[key] = entry
n += 1
cb.set_size(n)
for key, entry in entries.items():
k = (*key, "") if entry.meta and entry.meta.isdir else key
try:
if not storage_map.remote_exists(entry, refresh=remote_refresh):
missing_entries.append(os.path.sep.join(k))
except StorageKeyError:
pass
finally:
cb.relative_update()
data_index.onerror = orig_data_index_onerror
return missing_entries
def _matches_target(p: str, targets: Iterable[str]) -> bool:
sep = os.sep
return any(p == t or p.startswith(t + sep) for t in targets)
def _prune_keys(filter_keys: Iterable["DataIndexKey"]) -> list["DataIndexKey"]:
sorted_keys = sorted(set(filter_keys), key=len)
result: list[DataIndexKey] = []
for key in sorted_keys:
if not any(key[: len(prefix)] == prefix for prefix in result):
result.append(key)
return result
def status( # noqa: PLR0913
repo: "Repo",
targets: Optional[Iterable[Union[os.PathLike[str], str]]] = None,
*,
granular: bool = False,
untracked_files: str = "no",
remote: Optional[str] = None,
not_in_remote: bool = False,
remote_refresh: bool = False,
config: Optional[dict] = None,
batch_size: Optional[int] = None,
head: str = "HEAD",
with_renames: bool = True,
) -> Status:
from dvc.scm import NoSCMError, SCMError
head = kwargs.pop("head", "HEAD")
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
config = config or {}
if remote and not_in_remote:
logger.debug("Using remote %r", remote)
core = config.setdefault("core", {})
core["remote"] = remote
repo.config.merge(config)
targets = targets or []
filter_keys: list[DataIndexKey] = [repo.fs.relparts(os.fspath(t)) for t in targets]
# try to remove duplicate and overlapping keys
filter_keys = _prune_keys(filter_keys)
uncommitted_diff = _diff_index_to_wtree(
repo,
filter_keys=filter_keys,
granular=granular,
batch_size=batch_size,
with_renames=with_renames,
)
unchanged = set(uncommitted_diff.pop("unchanged", []))
entries_not_in_remote: list[str] = []
if not_in_remote:
entries_not_in_remote = _get_entries_not_in_remote(
repo,
filter_keys=filter_keys,
granular=granular,
remote_refresh=remote_refresh,
)
try:
committed_diff = _diff_head_to_index(repo, head=head, **kwargs)
committed_diff = _diff_head_to_index(
repo,
filter_keys=filter_keys,
head=head,
granular=granular,
with_renames=with_renames,
)
except (SCMError, NoSCMError):
committed_diff = {}
else:
unchanged &= set(committed_diff.pop("unchanged", []))
git_info = _git_info(repo.scm, untracked_files=untracked_files)
untracked = git_info.get("untracked", [])
untracked = _transform_git_paths_to_dvc(repo, untracked)
scm_filter_targets = {
os.path.relpath(os.path.abspath(t), repo.scm.root_dir) for t in targets
}
untracked_it: Iterable[str] = git_info.get("untracked", [])
if scm_filter_targets:
untracked_it = (
f for f in untracked_it if _matches_target(f, scm_filter_targets)
)
untracked = _transform_git_paths_to_dvc(repo, untracked_it)
# order matters here
return Status(
not_in_cache=uncommitted_diff.pop("not_in_cache", []),
not_in_remote=uncommitted_diff.pop("not_in_remote", []),
not_in_remote=entries_not_in_remote,
committed=committed_diff,
uncommitted=uncommitted_diff,
untracked=untracked,
+6
View File
@@ -45,6 +45,12 @@ def _diff(old, new, data_keys, with_missing=False):
with_renames=True,
meta_cmp_key=meta_cmp_key,
roots=data_keys,
# Include unknown entries from missing dir entry, so that they don't
# get reported as added/modified/deleted.
# Also return unchanged entries so that we can check if they are missing
# from cache.
with_unknown=True,
with_unchanged=with_missing,
):
if (change.old and change.old.isdir and not change.old.hash_info) or (
change.new and change.new.isdir and not change.new.hash_info
+6 -2
View File
@@ -274,6 +274,7 @@ def collect(
num: int = 1,
hide_queued: bool = False,
hide_failed: bool = False,
hide_workspace: bool = False,
sha_only: bool = False,
**kwargs,
) -> list["ExpState"]:
@@ -303,8 +304,11 @@ def collect(
repo.scm, baseline_revs, refs=cached_refs, logger=logger
)
workspace_data = collect_rev(repo, "workspace", **kwargs)
result: list[ExpState] = [workspace_data]
result: list[ExpState] = []
if not hide_workspace:
workspace_data = collect_rev(repo, "workspace", **kwargs)
result.append(workspace_data)
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
active = collect_active(repo, baseline_revs, **kwargs)
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
+1 -1
View File
@@ -50,7 +50,7 @@ class MultipleBranchError(DvcException):
self.ref_infos = ref_infos
class AmbiguousExpRefInfo(InvalidArgumentError):
class AmbiguousExpRefInfo(InvalidArgumentError): # noqa: N818
def __init__(self, exp_name: str, exp_ref_list: Iterable["ExpRefInfo"]):
msg = [
(
+40 -23
View File
@@ -1,20 +1,22 @@
import logging
import os
import pickle
import shutil
from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator
from contextlib import contextmanager
from contextlib import AbstractContextManager, contextmanager
from dataclasses import asdict, dataclass
from enum import IntEnum
from itertools import chain
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, Union
import funcy
from funcy import nullcontext
from scmrepo.exceptions import SCMError
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.logger import set_loggers_level
from dvc.repo.experiments.exceptions import ExperimentExistsError
from dvc.repo.experiments.refs import EXEC_BASELINE, EXEC_BRANCH, ExpRefInfo
from dvc.repo.experiments.utils import to_studio_params
@@ -297,7 +299,9 @@ class BaseExecutor(ABC):
stages = dvc.commit([], recursive=recursive, force=True, relink=False)
exp_hash = cls.hash_exp(stages)
if include_untracked:
dvc.scm.add(include_untracked, force=True) # type: ignore[call-arg]
from dvc.scm import add_no_submodules
add_no_submodules(dvc.scm, include_untracked, force=True) # type: ignore[call-arg]
with cls.auto_push(dvc):
cls.commit(
@@ -466,8 +470,8 @@ class BaseExecutor(ABC):
if queue is not None:
queue.put((rev, os.getpid()))
if log_errors and log_level is not None:
cls._set_log_level(log_level)
log_ctx = cls._set_log_level(log_level) if log_errors else nullcontext()
exp_hash: Optional[str] = None
exp_ref: Optional[ExpRefInfo] = None
@@ -476,14 +480,17 @@ class BaseExecutor(ABC):
if info.name:
ui.write(f"Reproducing experiment '{info.name}'")
with cls._repro_dvc(
info,
infofile,
log_errors=log_errors,
copy_paths=copy_paths,
message=message,
**kwargs,
) as dvc:
with (
log_ctx,
cls._repro_dvc(
info,
infofile,
log_errors=log_errors,
copy_paths=copy_paths,
message=message,
**kwargs,
) as dvc,
):
args, kwargs = cls._repro_args(dvc)
if args:
targets: Optional[Union[list, str]] = args[0]
@@ -509,7 +516,9 @@ class BaseExecutor(ABC):
stages = dvc.reproduce(*args, **kwargs)
if paths := cls._get_top_level_paths(dvc):
logger.debug("Staging top-level files: %s", paths)
dvc.scm_context.add(paths)
from dvc.scm import add_no_submodules
add_no_submodules(dvc.scm, paths)
exp_hash = cls.hash_exp(stages)
if not repro_dry:
@@ -786,13 +795,10 @@ class BaseExecutor(ABC):
return orig_rev
@staticmethod
def _set_log_level(level):
# When executor.reproduce is run in a multiprocessing child process,
# dvc.cli.main will not be called for that child process so we need to
# setup logging ourselves
dvc_logger = logging.getLogger("dvc")
def _set_log_level(level: Optional[int]) -> AbstractContextManager[None]:
if level is not None:
dvc_logger.setLevel(level)
return set_loggers_level(level)
return nullcontext()
@staticmethod
def _copy_path(src, dst):
@@ -810,11 +816,22 @@ class BaseExecutor(ABC):
@contextmanager
def set_temp_refs(self, scm: "Git", temp_dict: dict[str, str]):
# Retry ref set, get, and remove operations to handle transient issues during
# concurrent Git access.
# Dulwich deletes parent directories of refs if they happen to be empty after
# removing a ref, which can interfere with `set_ref` in other processes.
# `remove_ref` may also fail with a `FileLocked` error when refs are packed,
# since multiple processes might attempt to write to the same file.
retry = funcy.retry(10, errors=Exception, timeout=0.1)
set_ref = retry(scm.set_ref)
get_ref = retry(scm.get_ref)
remove_ref = retry(scm.remove_ref)
try:
for ref, rev in temp_dict.items():
scm.set_ref(ref, rev)
set_ref(ref, rev)
yield
finally:
for ref in temp_dict:
if scm.get_ref(ref):
scm.remove_ref(ref)
if get_ref(ref):
remove_ref(ref)
+7 -4
View File
@@ -38,7 +38,7 @@ logger = logger.getChild(__name__)
@dataclass(frozen=True)
class QueueEntry:
class QueueEntry: # noqa: PLW1641
dvc_root: str
scm_root: str
stash_ref: str
@@ -287,6 +287,7 @@ class BaseStashQueue(ABC):
baseline_rev: Optional[str] = None,
branch: Optional[str] = None,
name: Optional[str] = None,
no_hydra: bool = False,
**kwargs,
) -> QueueEntry:
"""Stash changes from the workspace as an experiment.
@@ -302,6 +303,7 @@ class BaseStashQueue(ABC):
name: Optional experiment name. If specified this will be used as
the human-readable name in the experiment branch ref. Has no
effect of branch is specified.
no_hydra: Disable Hydra from automatically overwriting all params.
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
@@ -318,7 +320,7 @@ class BaseStashQueue(ABC):
# update experiment params from command line
if params:
self._update_params(params)
self._update_params(params, no_hydra=no_hydra)
# DVC commit data deps to preserve state across workspace
# & tempdir runs
@@ -441,12 +443,13 @@ class BaseStashQueue(ABC):
f"from '{config_path}': {param_list}"
)
def _update_params(self, params: dict[str, list[str]]):
def _update_params(self, params: dict[str, list[str]], no_hydra: bool = False):
"""Update param files with the provided `Hydra Override`_ patterns.
Args:
params: Dict mapping paths to `Hydra Override`_ patterns,
provided via `exp run --set-param`.
no_hydra: Disable Hydra from automatically overwriting all params.
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
@@ -456,7 +459,7 @@ class BaseStashQueue(ABC):
logger.debug("Using experiment params '%s'", params)
hydra_config = self.repo.config.get("hydra", {})
hydra_enabled = hydra_config.get("enabled", False)
hydra_enabled = hydra_config.get("enabled", False) and not no_hydra
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
for path, overrides in params.items():
if hydra_enabled and path == hydra_output_file:
+9 -4
View File
@@ -1,3 +1,4 @@
import glob
import hashlib
import locale
import logging
@@ -69,7 +70,7 @@ class LocalCeleryQueue(BaseStashQueue):
def celery(self) -> "FSApp":
from kombu.transport.filesystem import Channel
# related to https://github.com/iterative/dvc-task/issues/61
# related to https://github.com/treeverse/dvc-task/issues/61
Channel.QoS.restore_at_shutdown = False
from dvc_task.app import FSApp
@@ -434,9 +435,13 @@ class LocalCeleryQueue(BaseStashQueue):
try:
proc_info = self.proc[queue_entry.stash_rev]
except KeyError:
raise DvcException( # noqa: B904
f"No output logs found for experiment '{rev}'"
)
message = f"No output logs found for experiment '{rev}'"
if self.match_queue_entry_by_name({rev}, self.iter_failed()).get(rev):
message += "\nExperiment likely failed during setup."
if celery_logs := glob.glob(os.path.join(self.wdir, "*.out")):
message += " Check the celery logs for more details:"
message += f"\n\tcat {' '.join(celery_logs)}".expandtabs(4)
raise DvcException(message) # noqa: B904
with open(
proc_info.stdout, encoding=encoding or locale.getpreferredencoding()
) as fobj:
+4 -1
View File
@@ -22,6 +22,7 @@ def run( # noqa: C901, PLR0912
queue: bool = False,
copy_paths: Optional[Iterable[str]] = None,
message: Optional[str] = None,
no_hydra: bool = False,
**kwargs,
) -> dict[str, str]:
"""Reproduce the specified targets as an experiment.
@@ -67,7 +68,7 @@ def run( # noqa: C901, PLR0912
else:
path_overrides = {}
hydra_enabled = repo.config.get("hydra", {}).get("enabled", False)
hydra_enabled = repo.config.get("hydra", {}).get("enabled", False) and not no_hydra
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
if hydra_enabled and hydra_output_file not in path_overrides:
# Force `_update_params` even if `--set-param` was not used
@@ -80,6 +81,7 @@ def run( # noqa: C901, PLR0912
tmp_dir=tmp_dir,
copy_paths=copy_paths,
message=message,
no_hydra=no_hydra,
**kwargs,
)
@@ -100,6 +102,7 @@ def run( # noqa: C901, PLR0912
params=sweep_overrides,
copy_paths=copy_paths,
message=message,
no_hydra=no_hydra,
**kwargs,
)
if sweep_overrides:
+2
View File
@@ -30,6 +30,7 @@ def show(
num: int = 1,
hide_queued: bool = False,
hide_failed: bool = False,
hide_workspace: bool = False,
sha_only: bool = False,
**kwargs,
) -> list["ExpState"]:
@@ -42,6 +43,7 @@ def show(
num=num,
hide_queued=hide_queued,
hide_failed=hide_failed,
hide_workspace=hide_workspace,
sha_only=sha_only,
**kwargs,
)
File diff suppressed because one or more lines are too long
+8 -3
View File
@@ -10,6 +10,7 @@ from . import locked
if TYPE_CHECKING:
from dvc.output import Output
from dvc.repo import Repo
from dvc.stage import Stage
logger = logger.getChild(__name__)
@@ -19,12 +20,13 @@ def _make_index_onerror(onerror, rev):
def _onerror(entry, exc):
if onerror:
return onerror(rev, entry, exc)
return None
return _onerror
def _collect_indexes( # noqa: PLR0913
repo,
repo: "Repo",
targets=None,
remote=None,
all_branches=False,
@@ -40,6 +42,8 @@ def _collect_indexes( # noqa: PLR0913
onerror=None,
push=False,
):
from .index import index_from_targets
indexes = {}
collection_exc = None
@@ -67,7 +71,8 @@ def _collect_indexes( # noqa: PLR0913
try:
repo.config.merge(config)
idx = repo.index.targets_view(
idx = index_from_targets(
repo,
targets,
with_deps=with_deps,
recursive=recursive,
@@ -94,7 +99,7 @@ def _collect_indexes( # noqa: PLR0913
@locked
def fetch( # noqa: PLR0913
self,
self: "Repo",
targets=None,
jobs=None,
remote=None,
+91 -29
View File
@@ -17,6 +17,7 @@ from dvc.utils.objects import cached_property
if TYPE_CHECKING:
from networkx import DiGraph
from pygtrie import Trie
from typing_extensions import Self
from dvc.dependency import Dependency
from dvc.fs.callbacks import Callback
@@ -66,14 +67,14 @@ def collect_files(
is_local_fs = isinstance(fs, LocalFileSystem)
def is_ignored(path):
def is_ignored(path: str) -> bool:
# apply only for the local fs
return is_local_fs and scm.is_ignored(path)
def is_dvcfile_and_not_ignored(root, file):
def is_dvcfile_and_not_ignored(root: str, file: str) -> bool:
return is_valid_filename(file) and not is_ignored(f"{root}{sep}{file}")
def is_out_or_ignored(root, directory):
def is_out_or_ignored(root: str, directory: str) -> bool:
dir_path = f"{root}{sep}{directory}"
# trailing slash needed to check if a directory is gitignored
return dir_path in outs or is_ignored(f"{dir_path}{sep}")
@@ -83,6 +84,8 @@ def collect_files(
walk_iter = log_walk(walk_iter)
for root, dirs, files in walk_iter:
assert isinstance(dirs, list)
assert isinstance(files, list)
dvcfile_filter = partial(is_dvcfile_and_not_ignored, root)
for file in filter(dvcfile_filter, files):
file_path = fs.join(root, file)
@@ -318,32 +321,9 @@ class Index:
repo: "Repo",
onerror: Optional[Callable[[str, Exception], None]] = None,
) -> "Index":
stages = []
metrics = {}
plots = {}
params = {}
artifacts = {}
datasets = {}
datasets_lock = {}
onerror = onerror or repo.stage_collection_error_handler
for _, idx in collect_files(repo, onerror=onerror):
stages.extend(idx.stages)
metrics.update(idx._metrics)
plots.update(idx._plots)
params.update(idx._params)
artifacts.update(idx._artifacts)
datasets.update(idx._datasets)
datasets_lock.update(idx._datasets_lock)
return cls(
repo,
stages=stages,
metrics=metrics,
plots=plots,
params=params,
artifacts=artifacts,
datasets=datasets,
datasets_lock=datasets_lock,
return cls.from_indexes(
repo, (idx for _, idx in collect_files(repo, onerror=onerror))
)
@classmethod
@@ -364,7 +344,7 @@ class Index:
else {},
)
def update(self, stages: Iterable["Stage"]) -> "Index":
def update(self, stages: Iterable["Stage"]) -> "Self":
stages = set(stages)
# we remove existing stages with same hashes at first
# and then re-add the new ones later.
@@ -379,6 +359,36 @@ class Index:
datasets=self._datasets,
)
@classmethod
def from_indexes(cls, repo, idxs: Iterable["Self"]) -> "Self":
stages = []
metrics = {}
plots = {}
params = {}
artifacts = {}
datasets = {}
datasets_lock = {}
for idx in idxs:
stages.extend(idx.stages)
metrics.update(idx._metrics)
plots.update(idx._plots)
params.update(idx._params)
artifacts.update(idx._artifacts)
datasets.update(idx._datasets)
datasets_lock.update(idx._datasets_lock)
return cls(
repo,
stages=stages,
metrics=metrics,
plots=plots,
params=params,
artifacts=artifacts,
datasets=datasets,
datasets_lock=datasets_lock,
)
@cached_property
def outs_trie(self) -> "Trie":
from dvc.repo.trie import build_outs_trie
@@ -735,6 +745,10 @@ class IndexView:
for stage in self.stages:
yield from stage.deps
@property
def index(self) -> "Index":
return self._index
@property
def _filtered_outs(self) -> Iterator[tuple["Output", Optional[str]]]:
for stage, filter_info in self._stage_infos:
@@ -927,3 +941,51 @@ def _get_entry_hash_name(
return src_entry.hash_info.name
return DEFAULT_ALGORITHM
def index_from_targets(
repo: "Repo",
targets: Optional["TargetType"] = None,
stage_filter: Optional[Callable[["Stage"], bool]] = None,
outs_filter: Optional[Callable[["Output"], bool]] = None,
max_size: Optional[int] = None,
types: Optional[list[str]] = None,
with_deps: bool = False,
recursive: bool = False,
**kwargs: Any,
) -> "IndexView":
from dvc.stage.exceptions import StageFileDoesNotExistError, StageNotFound
from dvc.utils import parse_target
index: Optional[Index] = None
if targets and all(targets) and not with_deps and not recursive:
indexes: list[Index] = []
try:
for target in targets:
if not target:
continue
file, name = parse_target(target)
if file and not name:
index = Index.from_file(repo, file)
else:
stages = repo.stage.collect(target)
index = Index(repo, stages=list(stages))
indexes.append(index)
except (StageFileDoesNotExistError, StageNotFound):
pass
else:
index = Index.from_indexes(repo, indexes)
targets = None
if index is None:
index = repo.index
return index.targets_view(
targets,
stage_filter=stage_filter,
outs_filter=outs_filter,
max_size=max_size,
types=types,
recursive=recursive,
with_deps=with_deps,
**kwargs,
)
+1 -1
View File
@@ -63,7 +63,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False): # noqa:
remove(dvc_dir)
os.mkdir(dvc_dir)
os.makedirs(dvc_dir, exist_ok=True)
config = Config.init(dvc_dir)
+2 -2
View File
@@ -16,7 +16,7 @@ def pre_commit_install(scm: "Git") -> None:
config_path = os.path.join(scm.root_dir, ".pre-commit-config.yaml")
with modify_yaml(config_path) as config:
entry = {
"repo": "https://github.com/iterative/dvc",
"repo": "https://github.com/treeverse/dvc",
"rev": ".".join(map(str, version_tuple[:3])),
"hooks": [
{
@@ -75,7 +75,7 @@ def install(self: "Repo", use_pre_commit_tool: bool = False) -> None:
scm = self.scm
if not isinstance(scm, Git):
return
return None
driver = "dvc git-hook merge-driver --ancestor %O --our %A --their %B "
scm.install_merge_driver("dvc", "DVC merge driver", driver)
+41 -17
View File
@@ -1,10 +1,13 @@
import os
from typing import TYPE_CHECKING
from dvc.exceptions import MoveNotDataSourceError
from dvc.repo.scm_context import scm_context
from . import locked
if TYPE_CHECKING:
from . import Repo
def _expand_target_path(from_path, to_path):
if os.path.isdir(to_path):
@@ -14,7 +17,7 @@ def _expand_target_path(from_path, to_path):
@locked
@scm_context
def move(self, from_path, to_path):
def move(self: "Repo", from_path, to_path):
"""
Renames an output file and modifies the stage associated
to reflect the change on the pipeline.
@@ -32,9 +35,12 @@ def move(self, from_path, to_path):
It only works with outputs generated by `add` or `import`,
also known as data sources.
"""
from dvc import output
from dvc import dependency, output
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.exceptions import MoveNotDataSourceError
from dvc.stage import Stage
from dvc.stage.exceptions import StageFileAlreadyExistsError
from dvc_objects.fs.local import LocalFileSystem
from_out = output.loads_from(Stage(self), [from_path])[0]
assert from_out.protocol == "local"
@@ -45,6 +51,7 @@ def move(self, from_path, to_path):
assert len(outs) == 1
out = outs[0]
stage = out.stage
deps = stage.deps
if not stage.is_data_source:
raise MoveNotDataSourceError(stage.addressing)
@@ -58,21 +65,38 @@ def move(self, from_path, to_path):
)
new_wdir = os.path.abspath(os.path.join(os.curdir, os.path.dirname(to_path)))
to_path = os.path.relpath(to_path, new_wdir)
new_stage = self.stage.create(
single_stage=True,
fname=new_fname,
wdir=new_wdir,
outs=[to_path],
meta=stage.meta,
)
os.unlink(stage.path)
stage = new_stage
try:
new_stage = self.stage.create(
single_stage=True,
fname=new_fname,
wdir=new_wdir,
outs=[to_path],
meta=stage.meta,
frozen=stage.frozen,
always_changed=stage.always_changed,
desc=stage.desc,
)
except StageFileAlreadyExistsError:
# reraise to remove `--force` hint
raise StageFileAlreadyExistsError(f"{new_fname!r} already exists") from None
else:
new_stage = stage
to_path = os.path.relpath(to_path, stage.wdir)
to_out = output.loads_from(stage, [to_path], out.use_cache, out.metric)[0]
def with_dep_path_adjusted(dep: dependency.Dependency):
d = dep.dumpd()
if isinstance(dep.fs, LocalFileSystem) and not os.path.isabs(dep.def_path):
return d | {"path": os.path.relpath(dep.fspath, new_stage.wdir)}
return d
out.move(to_out)
stage.save()
stage.dump()
new_stage.outs = output.loadd_from(new_stage, [out.dumpd() | {"path": to_path}])
new_stage.deps = dependency.loadd_from(
new_stage, [with_dep_path_adjusted(dep) for dep in deps]
)
out.move(new_stage.outs[0])
new_stage.md5 = new_stage.compute_md5()
new_stage.dump()
if stage != new_stage:
stage.dvcfile.remove()
self.scm_context.track_file(stage.dvcfile.relpath)
return stage, new_stage
+4 -4
View File
@@ -32,7 +32,7 @@ logger = logger.getChild(__name__)
def onerror_collect(result: dict, exception: Exception, *args, **kwargs):
logger.debug("", exc_info=True)
logger.debug("", exc_info=True) # noqa: LOG014
result["error"] = exception
@@ -346,14 +346,14 @@ def _get_data_targets(definitions: dict):
def infer_data_sources(plot_id, config=None):
y = config.get("y", None)
y = config.get("y", None) if config else None
if isinstance(y, dict):
sources = list(y.keys())
else:
sources = [plot_id]
x = config.get("x", None)
x = config.get("x", None) if config else None
if isinstance(x, dict):
sources.append(first(x.keys()))
@@ -507,7 +507,7 @@ def _collect_pipeline_files(repo, targets: list[str], props, onerror=None):
@error_handler
def _collect_definitions(
repo: "Repo",
targets=None,
targets: list[str],
props: Optional[dict] = None,
onerror: Optional[Callable] = None,
**kwargs,
+7 -5
View File
@@ -39,7 +39,7 @@ def pull( # noqa: PLR0913
run_cache=run_cache,
)
try:
stats = self.checkout(
result = self.checkout(
targets=expanded_targets,
with_deps=with_deps,
force=force,
@@ -47,8 +47,10 @@ def pull( # noqa: PLR0913
allow_missing=allow_missing,
)
except CheckoutError as exc:
exc.stats["fetched"] = processed_files_count
# put fetched counts first
exc.result["stats"] = {"fetched": processed_files_count} | exc.result["stats"]
raise
stats["fetched"] = processed_files_count
return stats
else:
# put fetched counts first
result["stats"] = {"fetched": processed_files_count} | result["stats"]
return result
+4 -2
View File
@@ -152,10 +152,12 @@ def push( # noqa: PLR0913
finally:
ws_idx = indexes.get("workspace")
if ws_idx is not None:
from dvc.repo.index import IndexView
_index = ws_idx.index if isinstance(ws_idx, IndexView) else ws_idx
_update_meta(
self.index,
_index,
targets=glob_targets(targets, glob=glob),
remote=remote,
with_deps=with_deps,
recursive=recursive,
)
+7 -3
View File
@@ -41,10 +41,12 @@ class SCMContext:
def add(self, paths: Union[str, Iterable[str]]) -> None:
from scmrepo.exceptions import UnsupportedIndexFormat
from dvc.scm import add_no_submodules
try:
return self.scm.add(paths)
add_no_submodules(self.scm, paths)
except UnsupportedIndexFormat:
link = "https://github.com/iterative/dvc/issues/610"
link = "https://github.com/treeverse/dvc/issues/610"
add_cmd = self._make_git_add_cmd([relpath(path) for path in paths])
logger.info("")
msg = (
@@ -56,7 +58,7 @@ class SCMContext:
def track_changed_files(self) -> None:
"""Stage files that have changed."""
if not self.files_to_track:
return
return None
logger.debug("Staging files: %s", self.files_to_track)
return self.add(self.files_to_track)
@@ -74,6 +76,7 @@ class SCMContext:
logger.debug("Added '%s' to gitignore file.", path)
self.track_file(relpath(gitignore_file))
return self.ignored_paths.append(path)
return None
def ignore_remove(self, path: str) -> None:
from scmrepo.exceptions import FileNotInRepoError
@@ -88,6 +91,7 @@ class SCMContext:
if gitignore_file:
return self.track_file(relpath(gitignore_file))
return None
@contextmanager
def __call__(
+1 -1
View File
@@ -130,7 +130,7 @@ def status( # noqa: PLR0913
remote=remote,
all_tags=all_tags,
all_commits=all_commits,
recursive=True,
recursive=recursive,
)
ignored = list(
+5 -4
View File
@@ -152,7 +152,7 @@ def _merge_push_meta( # noqa: C901
else:
if entry.hash_info:
out.hash_info = entry.hash_info
if out.meta.version_id is None:
if out.meta is None or out.meta.version_id is None:
out.meta = entry.meta
if out.meta:
out.meta.remote = remote
@@ -181,7 +181,7 @@ def _update_worktree_out(
):
from dvc_data.index import build
remote_name = out.remote or out.meta.remote
remote_name = out.remote or (out.meta.remote if out.meta is not None else None)
if not remote_name:
logger.warning("Could not update '%s', it was never pushed to a remote", out)
return
@@ -212,11 +212,12 @@ def _update_worktree_out(
logger.warning("Could not update '%s', directory is empty in the remote", out)
return
_fetch_out_changes(out, local_index, remote_index, remote)
_fetch_out_changes(repo, out, local_index, remote_index, remote)
_update_out_meta(repo, out, local_index, remote_index, remote)
def _fetch_out_changes(
repo: "Repo",
out: "Output",
local_index: Union["DataIndex", "DataIndexView"],
remote_index: Union["DataIndex", "DataIndexView"],
@@ -242,7 +243,7 @@ def _fetch_out_changes(
cb.set_size(total)
apply(
diff,
out.repo.root_dir,
repo.root_dir,
out.fs,
update_meta=False,
storage="data",
+38 -3
View File
@@ -1,17 +1,18 @@
"""Manages source control systems (e.g. Git)."""
import os
from collections.abc import Iterator, Mapping
from collections.abc import Iterable, Iterator, Mapping
from contextlib import contextmanager
from functools import partial
from typing import TYPE_CHECKING, Literal, Optional, Union, overload
from funcy import group_by
from scmrepo.base import Base # noqa: F401
from scmrepo.base import Base # noqa: TC002
from scmrepo.git import Git
from scmrepo.noscm import NoSCM
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.progress import Tqdm
if TYPE_CHECKING:
@@ -19,6 +20,8 @@ if TYPE_CHECKING:
from dvc.fs import FileSystem
logger = logger.getChild(__name__)
class SCMError(DvcException):
"""Base class for source control management errors."""
@@ -42,7 +45,7 @@ class NoSCMError(SCMError):
super().__init__(msg)
class InvalidRemoteSCMRepo(SCMError):
class InvalidRemoteSCMRepo(SCMError): # noqa: N818
pass
@@ -283,3 +286,35 @@ def lfs_prefetch(fs: "FileSystem", paths: list[str]):
include=[(path if path.startswith("/") else f"/{path}") for path in paths],
progress=pbar.update_git,
)
def add_no_submodules(
scm: "Base",
paths: Union[str, Iterable[str]],
**kwargs,
) -> None:
"""Stage paths to Git, excluding those inside submodules."""
if isinstance(paths, str):
paths = [paths]
submodule_roots = {os.path.join(scm.root_dir, sub) for sub in scm.list_submodules()}
repo_paths: list[str] = []
skipped_paths: list[str] = []
for p in paths:
abs_path = os.path.abspath(p)
if any(
abs_path == root or abs_path.startswith(root + os.sep)
for root in submodule_roots
):
skipped_paths.append(p)
else:
repo_paths.append(p)
if skipped_paths:
msg = "Skipping staging for path(s) inside submodules: %s"
logger.debug(msg, ", ".join(skipped_paths))
scm.add(repo_paths, **kwargs)
+12 -25
View File
@@ -32,7 +32,7 @@ from .utils import (
)
if TYPE_CHECKING:
from dvc.dependency import ParamsDependency
from dvc.dependency import Dependency, ParamsDependency
from dvc.dvcfile import ProjectFile, SingleStageFile
from dvc.output import Output
from dvc.repo import Repo
@@ -121,7 +121,7 @@ def restore_fields(stage: "Stage") -> None:
old_outs = {out.def_path: out for out in old.outs}
for out in stage.outs:
old_out = old_outs.get(out.def_path, None)
old_out = old_outs.get(out.def_path)
if old_out is not None:
out.restore_fields(old_out)
@@ -153,8 +153,8 @@ class Stage(params.StageParams):
self._path = path
self.cmd = cmd
self.wdir = wdir
self.outs = outs
self.deps = deps
self.outs: list[Output] = outs
self.deps: list[Dependency] = deps
self.md5 = md5
self.frozen = locked or frozen
self.always_changed = always_changed
@@ -509,34 +509,20 @@ class Stage(params.StageParams):
if not allow_missing:
raise
def get_versioned_outs(self) -> dict[str, "Output"]:
from .exceptions import StageFileDoesNotExistError, StageNotFound
try:
old = self.reload()
except (StageFileDoesNotExistError, StageNotFound):
return {}
return {
out.def_path: out
for out in old.outs
if out.files is not None
or (out.meta is not None and out.meta.version_id is not None)
}
def save_outs(self, allow_missing: bool = False):
from dvc.output import OutputDoesNotExistError
old_versioned_outs = self.get_versioned_outs()
for out in self.outs:
# old state just before saving so to merge them later
old_state = out._get_versioned_meta()
try:
out.save()
except OutputDoesNotExistError:
if not allow_missing:
raise
if old_out := old_versioned_outs.get(out.def_path):
out.merge_version_meta(old_out)
if old_state:
out.merge_version_meta(*old_state)
def ignore_outs(self) -> None:
for out in self.outs:
@@ -579,8 +565,9 @@ class Stage(params.StageParams):
from dvc.output import OutputDoesNotExistError
link_failures = []
old_versioned_outs = self.get_versioned_outs()
for out in self.filter_outs(filter_info):
# old state just before saving so to merge them later
old_state = out._get_versioned_meta()
try:
out.add(filter_info, **kwargs)
except (FileNotFoundError, OutputDoesNotExistError):
@@ -589,8 +576,8 @@ class Stage(params.StageParams):
except CacheLinkError:
link_failures.append(filter_info or out.fs_path)
if old_out := old_versioned_outs.get(out.def_path):
out.merge_version_meta(old_out)
if old_state:
out.merge_version_meta(*old_state)
if link_failures:
raise CacheLinkError(link_failures)
+1 -1
View File
@@ -82,7 +82,7 @@ class DataSourceChanged(DvcException):
super().__init__(f"data source changed: {path}")
class StageNotFound(DvcException, KeyError):
class StageNotFound(DvcException, KeyError): # noqa: N818
def __init__(self, file, name):
self.file = file.relpath
self.name = name
+2 -2
View File
@@ -51,7 +51,7 @@ def _warn_if_fish(executable):
"Be aware that it might cause problems by overwriting "
"your current environment variables with values defined "
"in 'config.fish', which might affect your command. See "
"https://github.com/iterative/dvc/issues/1307. "
"https://github.com/treeverse/dvc/issues/1307. "
)
@@ -99,7 +99,7 @@ def prepare_kwargs(stage, run_env=None):
#
# [1] https://github.com/python/cpython/blob/3.7/Lib/subprocess.py
# #L1426
# [2] https://github.com/iterative/dvc/issues/2506
# [2] https://github.com/treeverse/dvc/issues/2506
# #issuecomment-535396799
kwargs["shell"] = os.name == "nt"
return kwargs
+2 -1
View File
@@ -168,6 +168,7 @@ def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
for f in _serialize_tree_obj_to_files(obj)
]
else:
assert item.meta is not None
meta_d = item.meta.to_dict()
meta_d.pop("isdir", None)
ret.update(_serialize_hi_to_dict(item.hash_info))
@@ -177,7 +178,7 @@ def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
res = OrderedDict([("cmd", stage.cmd)])
params, deps = split_params_deps(stage)
deps, outs = (
[_dumpd(item) for item in sorted(items, key=attrgetter("def_path"))]
[_dumpd(item) for item in sorted(items, key=attrgetter("def_path"))] # type: ignore[call-overload]
for items in [deps, stage.outs]
)
params = _serialize_params_values(params)
+1 -1
View File
@@ -4,7 +4,7 @@ dvc.testing.benchmarks
======================
Benchmark test definitions are now part of ``dvc.testing``.
For CLI usage and `bench.dvc.org <https://bench.dvc.org>`_ details see `dvc-bench <https://github.com/iterative/dvc-bench>`_.
For CLI usage and `bench.dvc.org <https://bench.dvc.org>`_ details see `dvc-bench <https://github.com/treeverse/dvc-bench>`_.
``dvc.testing.benchmarks`` structure:
@@ -4,12 +4,15 @@ from shutil import rmtree
def test_data_status(bench_dvc, tmp_dir, scm, dvc, make_dataset):
args = ("data", "status")
dataset = make_dataset(cache=True, files=True, dvcfile=True, commit=False)
scm.ignore(dataset)
rmtree(dvc.tmp_dir)
bench_dvc(*args, name="new")
bench_dvc(*args, name="noop")
tmp_dir.scm_add(dataset.with_suffix(".dvc").name, commit="add dataset")
tmp_dir.scm_add(
[dataset.with_suffix(".dvc").name, ".gitignore"], commit="add dataset"
)
(dataset / "new").write_text("new")
bench_dvc(*args, name="changed")
@@ -26,12 +29,15 @@ def test_data_status_all_flags(bench_dvc, tmp_dir, scm, dvc, make_dataset):
"--json",
)
dataset = make_dataset(cache=True, files=True, dvcfile=True, commit=False)
scm.ignore(dataset)
rmtree(dvc.tmp_dir)
bench_dvc(*args, name="new")
bench_dvc(*args, name="noop")
tmp_dir.scm_add(dataset.with_suffix(".dvc").name, commit="add dataset")
tmp_dir.scm_add(
[dataset.with_suffix(".dvc").name, ".gitignore"], commit="add dataset"
)
(dataset / "new").write_text("new")
bench_dvc(*args, name="changed")

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