Compare commits

...
295 Commits
Author SHA1 Message Date
Saugat Pachhai (सौगात) a5ae8b19a4 fix db completions result 2025-10-07 21:06:58 +05:45
Saugat Pachhai (सौगात) f2b1c41cdf fix remote config completion 2025-10-07 14:34:39 +05:45
Saugat Pachhai (सौगात) ffd32dfe8a completion: support fish completion
Fixes #10879.
Upstream PR: https://github.com/iterative/shtab/pull/195.
2025-10-06 20:38:53 +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
skshetry 5fb8986dbd bench: check for dvc_bin.version instead of dvc_rev (#10764)
* bench: check for dvc_bin.version instead of dvc_rev

* Update dvc/testing/benchmarks/cli/stories/use_cases/test_sharing.py

* Update dvc/testing/benchmarks/cli/stories/use_cases/test_sharing.py
2025-06-05 11:41:04 +05:45
skshetry 18177a6579 tests(bench): also check if dvc_rev is set (#10763) 2025-06-05 09:26:27 +05:45
skshetry ea02a18b23 tests(bench): remove allow_anonymous_login config use from dvc<3.59.3 (#10762) 2025-06-05 09:21:44 +05:45
skshetry e04f1499a3 fix(dvcfs): rename argument url to repo (#10754)
See #10700 for the motivation.
2025-06-05 08:30:22 +05:45
skshetry 488c30d1ea tests(bench): adapt for pytest>=8.4 (#10761) 2025-06-04 10:13:28 +05:45
Clouds 88c3103cf3 fix: treated S3 configpath as a relpath (#10758)
Update config.py
2025-06-03 19:31:03 +05:45
pre-commit-ci[bot] c37bef7add [pre-commit.ci] pre-commit autoupdate (#10755)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.11 → v0.11.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.11...v0.11.12)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-03 11:59:26 +05:45
skshetry 7ea1daa2b8 deps: bump minver version for dvc-gs to 3.0.2 (#10757)
To support allow_anonymous_login that was added in dvc-gs 3.0.2.

Related:
- https://github.com/iterative/dvc-gs/pull/115
- https://github.com/iterative/dvc/pull/10756
- https://github.com/iterative/dvc/issues/5797
2025-06-03 10:55:35 +05:45
skshetry fd181c78d2 fs(gs): allow configuring anonymous login (#10756) 2025-06-03 10:41:08 +05:45
pre-commit-ci[bot] 1de7f89af8 [pre-commit.ci] pre-commit autoupdate (#10751)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.10 → v0.11.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.10...v0.11.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-27 07:10:08 +05:45
pre-commit-ci[bot]andskshetry b7e13dbd07 [pre-commit.ci] pre-commit autoupdate (#10748)
* [pre-commit.ci] pre-commit autoupdate

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

* Update .pre-commit-config.yaml

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2025-05-20 11:04:00 +05:45
pre-commit-ci[bot] dc513a98e4 [pre-commit.ci] pre-commit autoupdate (#10745)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-14 07:15:39 +05:45
skshetry 9908ca7f8e drop inputs passed to codecov-action (#10740)
It can already search and find these coverage files.
2025-05-07 14:34:27 +05:45
skshetry 0eebeb9900 tests: fix flaky test in test_get_used_objs (#10739)
Clear logs before assertions so that logs from other tests do not interfere with the test.
Some destructors may also run in between due to GC, so hopefully this will fix that issue too,
or reduce the chances of it happening.

See
https://github.com/iterative/dvc/actions/runs/14873217814/job/41775314725#step:6:97.

```console
>       assert first(caplog.messages) == expected_message
E       assert 'RC: discarding all clients' == "Output 'path... 'stage.dvc'."
E
E         + RC: discarding all clients
E         - Output 'path'(stage: 'stage.dvc') is missing version info. Cache for it will not be collected. Use `dvc repro` to get your pipeline up to date.
E         - You can also use `dvc commit stage.dvc` to associate existing 'path' with stage: 'stage.dvc'.
```
2025-05-07 12:40:51 +05:45
skshetry 415d4bc980 repo: try to support opening uninitialized/broken repositories (#10736)
This `_get_remote_config()` should use `uninitialized=True` so that it supports
more broader kinds of broken/partially-initialized repositories, including some that have
`.dvc` directory missing, or `.git` directory missing.

This partially reverts #10719. #10608 is also fixed, and no longer requires `core.no_scm` to be passed.
This was already supported by `dvc.api.get_url` as it uses `uninitialized=True`, but this was not respected
in `_get_remote_config()` where it would fail before.

That said, this whole `open_repo`/`_get_remote_config` is terribly broken. For one, it is opening
a local repository, and forcing it's remote config to a repository opened with `Repo(rev=...)`,
where the config may be different.
2025-05-06 08:39:23 +00:00
pre-commit-ci[bot] f3be7d761f [pre-commit.ci] pre-commit autoupdate (#10735)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-06 07:05:11 +05:45
Rodrigo Goyaandskshetry 4bde0e754e Forward args to _get_remote_config() and honour core/no_scm if present (#10719)
* Forward args to _get_remote_config and honour core/no_scm if present

* Pass full config, except cache, to Repo()

* Avoid modifying original kwargs contents

* Simplify if config

* Add test

* Relax error matching.. .. Avoid windows vs unix paths

* Update dvc/repo/open_repo.py

---------

Co-authored-by: skshetry <[email protected]>
2025-04-30 12:20:09 +05:45
skshetry 88f1ada4bf tests: func test for #4344 (#10732)
See https://github.com/iterative/dvc-data/pull/601 that fixed the issue.
The test is based on reproduction steps from:
https://github.com/iterative/dvc/issues/4344#issuecomment-2822000925
2025-04-29 13:25:28 +05:45
pre-commit-ci[bot] bd65c205df [pre-commit.ci] pre-commit autoupdate (#10731)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-29 04:52:42 +05:45
skshetry e6ce5f825d Revert "pre-commit: set autoupdate schedule to monthly" (#10730)
Revert "pre-commit: set autoupdate schedule to monthly (#10685)"

This reverts commit 25d2638a57.
2025-04-28 18:23:08 +05:45
dependabot[bot] d30d66c1d3 build(deps): bump astral-sh/setup-uv from 5 to 6 (#10729)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  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-04-28 14:55:14 +05:45
Fabian Zills 19f31d7cac Bring the banner back (#10728)
Update README.rst
2025-04-24 06:19:28 +05:45
Ivan Shcheklein 99d9ca714d remove banner in README (#10726) 2025-04-22 04:36:16 +00:00
dependabot[bot] a3b6722523 build(deps): bump rtCamp/action-slack-notify from 2.3.2 to 2.3.3 (#10718)
Bumps [rtCamp/action-slack-notify](https://github.com/rtcamp/action-slack-notify) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/rtcamp/action-slack-notify/releases)
- [Commits](https://github.com/rtcamp/action-slack-notify/compare/v2.3.2...v2.3.3)

---
updated-dependencies:
- dependency-name: rtCamp/action-slack-notify
  dependency-version: 2.3.3
  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-04-10 13:02:20 +05:45
skshetry b824bc4db7 ci(build): use envvar override to omit local version for Test PyPI upload (#10720)
Uses `SETUPTOOLS_SCM_OVERRIDES_FOR_DVC` to omit the local version
when uploading to Test PyPI. This is needed because (Test) PyPI
does not support local version.

This requires bumping minimum version of `setuptools_scm` to `8.0.0`.

The envvar is documented here: https://setuptools-scm.readthedocs.io/en/latest/overrides/.

Also see:
https://github.com/pypa/setuptools-scm/issues/455#issuecomment-2791301186.
2025-04-10 12:59:32 +05:45
pre-commit-ci[bot]andSaugat Pachhai 6cc349edb4 [pre-commit.ci] pre-commit autoupdate (#10717)
* [pre-commit.ci] pre-commit autoupdate

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

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

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

* 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-04-08 03:15:09 +00:00
Rory CreanandRMCrean e455ee69aa remote: show default when run "dvc remote list" (#10711)
* remote: show default when run "dvc remote list"

Fixes #10708

* Indent other remotes if there is a default remote

Now regardless of whether there is a default remote or not, all remotes are equally indented

* stricter test for listing remotes shows default

* Rewrite dvc remote list implemetation

- Default remote is indicated at end of the output and colored green
- Remotes no longer reordered to show default first
- Test updated to reflect changes and remove need for function escape_ansi.

---------

Co-authored-by: RMCrean <[email protected]>
2025-04-07 09:02:09 +05:45
skshetry 31270c14e3 pyproject: update license to PEP 639 format (#10709)
Requires bumping `setuptools` to 77.0.0 or later.

See:
- [PEP 639](https://peps.python.org/pep-0639)
- [Writing pyproject.toml - license](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license)
2025-03-25 09:48:50 +05:45
pre-commit-ci[bot] 28b7c8b723 [pre-commit.ci] pre-commit autoupdate (#10699)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.4 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-06 18:21:14 -08:00
skshetry 1d6ea68133 ls/ls-url: do not show entry with size<=0 for a directory (#10687)
ls/ls-url: do not show size=0 when the entry is a directory
2025-02-09 12:16:53 +05:45
skshetry 25d2638a57 pre-commit: set autoupdate schedule to monthly (#10685)
Update .pre-commit-config.yaml
2025-02-06 09:33:04 +05:45
dependabot[bot] 0b03714097 build(deps): bump mypy from 1.14.1 to 1.15.0 (#10684)
Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-06 09:30:37 +05:45
pre-commit-ci[bot] 9392bbdd4b [pre-commit.ci] pre-commit autoupdate (#10682)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-04 11:00:36 +05:45
pre-commit-ci[bot]andskshetry c4b18b710a [pre-commit.ci] pre-commit autoupdate (#10676)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2025-01-31 20:04:56 +05:45
da1e0b57b8 import: fix broken progressbar when importing folder (#10678)
Co-authored-by: Petr Chmelar <[email protected]>
Co-authored-by: skshetry <[email protected]>
2025-01-31 19:41:27 +05:45
pre-commit-ci[bot] b2a873d13b [pre-commit.ci] pre-commit autoupdate (#10674)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 15:34:13 +05:45
pre-commit-ci[bot] 89ef3463dc [pre-commit.ci] pre-commit autoupdate (#10671)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.0 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.0...v0.9.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-13 13:37:31 -08:00
skshetry 7d14acb5a7 3.13 support for Windows (#10667)
python/cpython#113829 changed `os.path.isabs` on Windows to not consider a path starting a single backslash to be an absolute path.

We do a lot of naive `posixpath` to `ntpath` conversions, and it broke DVC in multiple places. DVC is likely broken in many other places, and these bugs are difficult to identify.

In this commit, I have tried to fix in the places where the tests failed. And mostly by trying to imitate pre-3.13 behaviour.
Some of the changes may not be correct but keeps the old behaviour in place.

Also re-enabled the CI for all Python versions supported for Windows.
2025-01-10 18:39:38 +05:45
skshetry dd501554fe tests: try to make tests pass on Python 3.13 on unix platforms (#10665) 2025-01-09 22:36:35 +05:45
skshetry 54acfda687 fix implicit concatenated strings (#10666) 2025-01-09 22:05:33 +05:45
pre-commit-ci[bot] e70a1e9bfc [pre-commit.ci] pre-commit autoupdate (#10663)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-09 21:49:48 +05:45
dependabot[bot] aea0e6b6cc build(deps): bump mypy from 1.14.0 to 1.14.1 (#10662)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 21:14:40 +05:45
skshetry 2c6b917fc5 ls-url: add support for --tree/--level (#10664) 2025-01-09 21:12:34 +05:45
a43c1b31f7 chore(studio): Update Studio URL to new domain (#10538)
* chore(studio): Update Studio URL to new domain

The https://studio.dvc.ai is deprecated, and we switched to the new domain https://studio.datachain.ai.

The domain change is backwards compatible. The **older versions** of the `dvc` will still work. 🎉

Related PR: https://github.com/iterative/dvc-studio-client/pull/181

* Apply suggestions from code review

---------

Co-authored-by: Marcin Jasion <[email protected]>
Co-authored-by: Helio Machado <[email protected]>
2024-12-31 01:51:20 +00:00
pre-commit-ci[bot] 683a9f09bd [pre-commit.ci] pre-commit autoupdate (#10661)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-26 14:45:00 +05:45
dependabot[bot] 53a7230da0 build(deps): bump astral-sh/setup-uv from 4 to 5 (#10660)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 5.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v5)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  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>
2024-12-22 18:55:56 -08:00
dependabot[bot] f078a572dd build(deps): bump mypy from 1.13.0 to 1.14.0 (#10659)
Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-22 18:16:06 -08:00
pre-commit-ci[bot] 007041bfe9 [pre-commit.ci] pre-commit autoupdate (#10651)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-17 17:45:15 +05:45
pre-commit-ci[bot] 965dbea75e [pre-commit.ci] pre-commit autoupdate (#10646)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-10 13:53:15 +05:45
pre-commit-ci[bot] f5cbf97eea [pre-commit.ci] pre-commit autoupdate (#10642)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.0 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.0...v0.8.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-07 08:07:50 +05:45
Raphaël Michelandpre-commit-ci[bot] 198c8f2ffd Fix #10638 : makes remove return correct list when used with both --queue and -A (#10641)
* fixed a bug where using both `--queue` and `-A` would yield a "removed" list without the queued experiments

* added a test to validate the fix

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

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

* clearer name for test

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-01 08:21:46 -08:00
Raphaël Michelandpre-commit-ci[bot] 368c785410 Fix #10593 -- add --keep option for dvc experiments remove (#10633)
* Add keep_selected parameter, and corresponding code to keep only the selected exps (and remove all the other ones)

* test keep_selected_by_name

* test keep_selected_by_rev

* test keep_selected multiple, by name

* test keep all by name

* test keep by rev, with num=2

* added option to cli

* refactoring to meet pr needs

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

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

* fixed test_experiments to add keep_selected=False to remove tests

* rename parameter to match cli option

* follow the normal path, then invert the selection before removing

* fixed tests for list ordering + fixed test with non existent name, it didn't make sense to delete everything if an exp name did not exist

* changed cli option comment

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

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

* fixed typing issue

* updated parameter name

* removed handling queued experiments (since --queue would remove them all)

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

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

* code simplification, added __eq__ and __hash__ to be able to compare ExpRefs, updated and parametrized tests.

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

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

* fixed linting issues

* - --keep and --queue together raise an InvalidArgumentError
    - added a test to check if the error is raised
    - fixed CLI message

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

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

* re-run gh tests. Some tests which did not involve my changes started failing while they were passing fine before.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-30 10:43:13 -08:00
skshetry 64ccd9cfdb set DVC_NO_ANALYTICS on lint (#10640) 2024-11-29 18:48:01 +05:45
skshetry d38b2dbcb8 deps: bump configobj minver to 5.0.9 (#10636) 2024-11-26 10:59:28 +05:45
dependabot[bot] 8cb0f02bf9 build(deps): bump astral-sh/setup-uv from 3 to 4 (#10634)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 07:34:58 +05:45
pre-commit-ci[bot] 7e75b0616f [pre-commit.ci] pre-commit autoupdate (#10635)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-26 07:34:46 +05:45
pre-commit-ci[bot] fb24775bc0 [pre-commit.ci] pre-commit autoupdate (#10626)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.3 → v0.7.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.3...v0.7.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-19 11:36:20 -08:00
82e2446eb4 Update pre-commit to use pinned version of dvc and correct stage names (#10621)
Co-authored-by: skshetry <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-18 08:22:11 +05:45
dependabot[bot] 1e08cc5da3 build(deps): bump codecov/codecov-action from 3 to 5 (#10622)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 09:31:06 +05:45
pre-commit-ci[bot] e56a729e3b [pre-commit.ci] pre-commit autoupdate (#10619)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-12 09:52:05 +05:45
skshetry 9a050684f7 fix issue with pytest-benchmark==5 (#10618) 2024-11-11 17:54:08 +05:45
Ivan Shcheklein 2431ec6189 Rename dvcx to DataChain in DVC <> DataChain integration (#10611) 2024-11-05 12:27:45 +05:45
dependabot[bot] 70fc4e6cda build(deps): update platformdirs requirement from <4,>=3.1.1 to >=3.1.1,<5 (#10603)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 12:27:30 +05:45
pre-commit-ci[bot] bbc01b40d2 [pre-commit.ci] pre-commit autoupdate (#10610)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-05 10:45:10 +05:45
skshetry 6302e5f1d3 PEP 703 attestation is the default (#10607) 2024-10-31 07:00:59 +05:45
pre-commit-ci[bot] df5429f7d8 [pre-commit.ci] pre-commit autoupdate (#10606)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.0 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.0...v0.7.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-29 06:34:56 +05:45
dependabot[bot] f9583f0c3d build(deps): bump rtCamp/action-slack-notify from 2.3.1 to 2.3.2 (#10602)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 08:03:55 +05:45
dependabot[bot] 9e26a46323 build(deps): bump mypy from 1.12.1 to 1.13.0 (#10600)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 08:18:14 +05:45
skshetry 746f028f6c ls: implement --tree to show as a tree and --level to limit depth for recursion (#10598) 2024-10-23 07:56:24 +05:45
pre-commit-ci[bot] 3651939e11 [pre-commit.ci] pre-commit autoupdate (#10597)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-22 08:27:09 +05:45
Pete Bachant d236667048 Add option to show MD5 checksums in dvc ls (#10573) 2024-10-21 16:32:03 +05:45
dependabot[bot] d22a68f7a8 build(deps): bump mypy from 1.12.0 to 1.12.1 (#10595)
Bumps [mypy](https://github.com/python/mypy) from 1.12.0 to 1.12.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: mypy
  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>
2024-10-20 20:10:06 -07:00
dependabot[bot]andIvan Shcheklein 683ae7a951 build(deps): bump mypy from 1.11.2 to 1.12.0 (#10586)
* build(deps): bump mypy from 1.11.2 to 1.12.0

Bumps [mypy](https://github.com/python/mypy) from 1.11.2 to 1.12.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0)

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

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

* fix mypy complaintes

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ivan Shcheklein <[email protected]>
2024-10-20 18:42:48 -07:00
dependabot[bot] d5054ce861 build(deps): bump rtCamp/action-slack-notify from 2.3.0 to 2.3.1 (#10591)
Bumps [rtCamp/action-slack-notify](https://github.com/rtcamp/action-slack-notify) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/rtcamp/action-slack-notify/releases)
- [Commits](https://github.com/rtcamp/action-slack-notify/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: rtCamp/action-slack-notify
  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>
2024-10-19 18:10:03 -07:00
Saipavan Lingamallu e884f1b8f0 Update warning message gc.py (#10592)
Corrected warning message
2024-10-18 10:54:00 -07:00
pre-commit-ci[bot]andskshetry 9b5772fab8 [pre-commit.ci] pre-commit autoupdate (#10582)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2024-10-08 18:05:36 +05:45
Santiago SmithandSantiago Silva c1aaeffc2d fix: directory creation on pulling data (#10580)
Co-authored-by: Santiago Silva <[email protected]>
2024-10-08 17:55:24 +05:45
Ivan Shcheklein ad70dd322b bump scmrepo version (#10570)
Context https://github.com/iterative/dvc/issues/10563
2024-10-02 18:04:11 -07:00
pre-commit-ci[bot] 72b6971730 [pre-commit.ci] pre-commit autoupdate (#10574)
* [pre-commit.ci] pre-commit autoupdate

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

* [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>
2024-10-02 18:03:56 -07:00
skshetry ee1864af8e fix tests with the new pydot release (#10567) 2024-09-26 08:13:06 +05:45
skshetry 52877485d1 chore: ci: disable caching (#10565) 2024-09-25 08:41:30 +05:45
pre-commit-ci[bot] a923f110e3 [pre-commit.ci] pre-commit autoupdate (#10564)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 09:29:54 +05:45
skshetry b8266ed15f add/commit: use hardlink to transfer files to the cache when cache.type is hardlink and on relink mode (#10557) 2024-09-17 13:09:32 +05:45
dependabot[bot] 2c871ba432 build(deps): bump astral-sh/setup-uv from 2 to 3 (#10559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 08:13:24 +05:45
pre-commit-ci[bot] 32d11f1e05 [pre-commit.ci] pre-commit autoupdate (#10558)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-17 08:13:09 +05:45
pre-commit-ci[bot] f56343d6ae [pre-commit.ci] pre-commit autoupdate (#10551)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-10 09:07:55 +05:45
skshetry 7b8922b20f ci: use uv's official action, and uv build command (#10548) 2024-09-06 08:36:18 +05:45
pre-commit-ci[bot] 436a2ac793 [pre-commit.ci] pre-commit autoupdate (#10547)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-03 08:41:43 +05:45
skshetry eec3d48731 pyproject.toml: remove stale comment from dependency (#10546) 2024-08-30 18:51:56 +05:45
skshetry 5417ccd7b6 dvcfs.get: simplify and separate files into respective filesystems while walking (#10544)
dvcfs.get: simplify and separate files into respective filesystems while collecting/walking
2024-08-29 16:47:16 +00:00
skshetry f2b7b00af5 dvcfilesystem.get: prefer downloading from repo.fs before dvc.fs (#10543) 2024-08-29 13:06:21 +00:00
skshetry 4f3fb15f2e dvcfs: optimize get() by reducing index.info calls() (#10540) 2024-08-27 18:08:40 +05:45
pre-commit-ci[bot] 016f285fe3 [pre-commit.ci] pre-commit autoupdate (#10541)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-27 06:43:45 +05:45
dependabot[bot] f6dd641855 build(deps): bump mypy from 1.11.1 to 1.11.2 (#10539)
Bumps [mypy](https://github.com/python/mypy) from 1.11.1 to 1.11.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.1...v1.11.2)

---
updated-dependencies:
- dependency-name: mypy
  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>
2024-08-26 08:21:55 +05:45
Saugat Pachhai (सौगात) 3f2584d155 bench: add --benchmark-cprofile-dump 2024-08-22 14:35:23 +05:45
skshetry 705b7bbea6 simplify running benchmarks (#10536) 2024-08-21 21:09:59 +05:45
Saugat Pachhai (सौगात) 26ad01bec0 bench: drop add/checkout from test_sharing 2024-08-21 08:00:19 +05:45
Saugat Pachhai (सौगात) e76aaeac57 benchmarks: fix for local versions 2024-08-20 21:32:37 +05:45
skshetry 6f24d04b5e ci (bench): skip benchmark for test_import on versions >=3.53,<3.54.1 (#10535) 2024-08-20 21:22:08 +05:45
skshetry 2dcaea8119 dependency/repo: do not suppress OSError on info() (#10533) 2024-08-20 18:14:21 +05:45
Dave BerenbaumandSaugat Pachhai 81db9b4572 save import hash info to state (#10531)
* save import hash info to state

* clean up path handling

* avoid walking

* tests: do not use as_posix to check for output

---------

Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-08-20 13:41:37 +05:45
pre-commit-ci[bot] 98dde6cd27 [pre-commit.ci] pre-commit autoupdate (#10532)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.0 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.0...v0.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-20 04:19:17 +05:45
skshetry e42d094310 remove epic template (#10530) 2024-08-17 09:40:57 +05:45
skshetry 38ee0a01f7 update ruff to 0.6 (#10526) 2024-08-15 20:02:09 +05:45
dependabot[bot]andSaugat Pachhai d494126cc5 build(deps): bump mypy from 1.10.1 to 1.11.1 (#10495)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-08-15 19:59:05 +05:45
skshetry fd3b43df05 fix test_daemon.py::test_analytics flakiness (#10523)
Turns out `iterative-telemetry` spends a long time trying to
generate an id. Sometimes it gets frozen for a long time (>30s).
2024-08-14 16:55:18 +05:45
Saugat Pachhai (सौगात) b9bcb45aaa ci: add python version for cache suffix 2024-08-14 15:17:31 +05:45
skshetry 7bca71db2e ci: use pytest-split based on least_duration (#10522) 2024-08-14 14:58:55 +05:45
skshetry 52405a0e10 ci: use uv to install dependencies (#10507)
* ci: use uv to install dependencies

* split Windows tests into four for each python versions

* split tests and check into two jobs

* increase timeout
2024-08-14 13:24:56 +05:45
skshetry c514b0ea31 use uv for benchmark tests while installing/creating virtualenv (#10520) 2024-08-13 22:27:05 +05:45
skshetry 853116a0e3 bump dvc-data to >=3.16.2 (#10519) 2024-08-13 08:59:06 +05:45
skshetry 5e4daf6cfb deps: bump dvc-data to >=3.16.0 (#10513) 2024-08-13 07:15:05 +05:45
pre-commit-ci[bot] 991a816995 [pre-commit.ci] pre-commit autoupdate (#10518)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-13 06:40:30 +05:45
Jan Stratil 9735b5aaa9 dvc dependency repo: Catch FileNotFoundError (#10501)
- When checkouting non-existent file the _checkout_file recursively
  calls os.stat on a symlinked file which destination doesn't exist.
  This causes a FileNotFoundError but it is not catched and it causes
  an unexpected error instead.

Fixes #10500.
2024-08-12 20:49:26 +05:45
Ivan Shcheklein fa317d5b1d fix(readme): add style to datachain banner (#10517) 2024-08-12 13:36:48 +05:45
Ivan Shcheklein 1b0fb6487b add datachain link (#10514) 2024-08-08 10:57:31 -07:00
Saugat Pachhai (सौगात) 43e6229308 stabilize import db: remove experimental warning 2024-08-07 08:40:56 -04:00
skshetry 1bef0da4fb import_db: set dependency.is_in_repo to false (#10504) 2024-08-06 14:02:24 +05:45
pre-commit-ci[bot] 284d83f390 [pre-commit.ci] pre-commit autoupdate (#10493)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-06 06:51:31 +05:45
Dave Berenbaum 417e6d4b76 revert to --no-run-cache default for push/pull/fetch 2024-07-30 14:23:24 -04:00
dberenbaum 248ba3b6b6 limit reimport behavior to checkout 2024-07-24 09:37:32 -04:00
dberenbaum 8a91c2b303 add back code to avoid downloading imports 2024-07-24 09:37:32 -04:00
Dave Berenbaum 544dad75e9 try force overwriting run cache entries on push 2024-07-24 08:15:01 -04:00
pre-commit-ci[bot] 8f50f2509f [pre-commit.ci] pre-commit autoupdate (#10487)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-23 07:58:55 +05:45
pre-commit-ci[bot] cdc1857559 [pre-commit.ci] pre-commit autoupdate (#10484)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.1 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.1...v0.5.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-15 13:42:20 -07:00
Dave Berenbaum 358dd163da set lower bound on fsspec (#10479) 2024-07-11 17:53:37 +05:45
pre-commit-ci[bot] aae1a93cb5 [pre-commit.ci] pre-commit autoupdate (#10478)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-08 14:22:12 -07:00
pre-commit-ci[bot]andIvan Shcheklein c9b7218659 [pre-commit.ci] pre-commit autoupdate (#10473)
* [pre-commit.ci] pre-commit autoupdate

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

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

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

* lint daemon.py

* lint run.py

* lint fixtures.py

* lint checkout.py

* lint collect.py

* lint index.py

* lint index.py

* lint checkout.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ivan Shcheklein <[email protected]>
2024-07-06 17:05:42 +00:00
dependabot[bot] 88c4b9525d build(deps): update flufl-lock requirement from <8,>=5 to >=8.1.0,<9 (#10470)
---
updated-dependencies:
- dependency-name: flufl-lock
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:25:45 -07:00
pre-commit-ci[bot] 69d36eeb06 [pre-commit.ci] pre-commit autoupdate (#10466)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-27 20:09:40 +05:45
3201794553 chore(studio): Update Studio URL to new domain (#10468)
Co-authored-by: Marcin Jasion <[email protected]>
Co-authored-by: skshetry <[email protected]>
2024-06-27 16:43:17 +05:45
dependabot[bot] 4464f8b4b9 build(deps): bump mypy from 1.10.0 to 1.10.1 (#10467)
Bumps [mypy](https://github.com/python/mypy) from 1.10.0 to 1.10.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: mypy
  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>
2024-06-24 21:00:48 -07:00
pre-commit-ci[bot] efc7e05893 [pre-commit.ci] pre-commit autoupdate (#10463)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-18 08:21:54 +05:45
skshetry a555c3d29a skip stage cache push/pull test for http (#10462) 2024-06-15 21:51:20 +05:45
George Geddes 676645b1ca Read studio hostname from config when not present on cli or env (#10459) 2024-06-14 17:29:56 -07:00
pre-commit-ci[bot] 93c3100bad [pre-commit.ci] pre-commit autoupdate (#10455)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 06:41:09 +05:45
pre-commit-ci[bot] c74156288d [pre-commit.ci] pre-commit autoupdate (#10443)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-03 17:40:31 +05:45
George KussumotoandSaugat Pachhai 0e38ccc031 fix(pull): normalize dependency path (#10446)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-06-03 16:43:43 +05:45
Ivan Shcheklein b69b368ffe fix(benchmarks): force older pygit2 for older DVC versions (#10447) 2024-06-03 15:56:43 +05:45
6cc80fdeaa fix fish shell support (#10445)
Co-authored-by: Alexey Stern [email protected]
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-06-03 15:45:21 +05:45
Ivan Shcheklein 8f8d49d023 fix(push/pull): don't fail on unsupported run cache (#10441) 2024-05-27 14:37:44 +05:45
Dave Berenbaum 83ec952da1 improve repro --pull --run-cache test 2024-05-23 14:26:42 -04:00
Dave Berenbaum c0d7e2a8de test repro --pull failure 2024-05-23 14:26:42 -04:00
Dave Berenbaum 9aad09164c update repro --pull description 2024-05-23 14:26:42 -04:00
Dave Berenbaum 1f7733fec1 refactor 2024-05-23 14:26:42 -04:00
Dave Berenbaum 856c902b65 add tests 2024-05-23 14:26:42 -04:00
Dave Berenbaum ebb459f9e2 refactor dvc repro --pull 2024-05-23 14:26:42 -04:00
Dave Berenbaum 2e0b024efe Apply suggestions from code review 2024-05-23 13:45:48 -04:00
Dave Berenbaum 135fa26515 Update dvc/repo/artifacts.py 2024-05-23 13:45:48 -04:00
Dave Berenbaum ef507efa04 artifacts: get studio info from local config 2024-05-23 13:45:48 -04:00
Dave Berenbaum 05db79e7cf add dvc artifacts get --show-url 2024-05-23 12:46:21 -04:00
Dave Berenbaum 0a813bbcbc push/pull run cache by default 2024-05-22 08:21:55 -04:00
skshetry e82356ba92 deps: bump scmrepo to >=3.3.4 (#10432)
Closes https://github.com/iterative/dvc/issues/10431.

Upstream PR: https://github.com/iterative/scmrepo/pull/359
2024-05-20 18:13:25 +05:45
Sigurd Spieckermann 5bfacec974 import/get: escape non-pattern LFS path parts (#10416) 2024-05-13 10:24:06 -07:00
pre-commit-ci[bot] 1848074ac9 [pre-commit.ci] pre-commit autoupdate (#10409)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.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>
2024-05-10 10:52:02 +00:00
Sigurd Spieckermann a7c0f6ef39 import/get: optimize LFS prefetching (#10414) 2024-05-03 08:58:53 -07:00
Thomas Roehr 0a8a9deceb Update s3 dependency to dvc-s3>=3.2.0 2024-04-26 11:01:03 -04:00
Thomas Roehr c5d98681c0 s3: align config name for anonymous login 2024-04-26 11:01:03 -04:00
Thomas Roehr 15e05e4b3e dvc: s3: allow configuring anonymous access 2024-04-26 11:01:03 -04:00
pre-commit-ci[bot] 7df8b0c09b [pre-commit.ci] pre-commit autoupdate (#10397)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-26 15:25:52 +05:45
Amirmohammad Sadat Shokouhi 2e1faf2979 add replication factor argument for hdfs in config schema (#10396) 2024-04-25 12:03:35 -07:00
skshetry cbaeff2ea1 deps: bump scmrepo minver to >=3.3.2,<4 (#10406)
Update pyproject.toml
2024-04-25 14:41:58 +05:45
dependabot[bot] 132e625ae8 build(deps): bump mypy from 1.9.0 to 1.10.0 (#10405)
Bumps [mypy](https://github.com/python/mypy) from 1.9.0 to 1.10.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 09:54:50 +05:45
skshetry 8517cccff4 artifacts: add support for monorepo (#10386) 2024-04-16 21:13:02 +05:45
dberenbaum c52a25e011 fix tests 2024-04-16 11:15:52 -04:00
dberenbaum c4678736b0 add option to not relink 2024-04-16 11:15:52 -04:00
pre-commit-ci[bot] ec92cc14d1 [pre-commit.ci] pre-commit autoupdate (#10390)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-16 10:37:51 +05:45
Dave Berenbaum e6048287cb fix test for dvc-render updates to plots title anchor (#10387) 2024-04-11 19:23:03 +05:45
pre-commit-ci[bot] 72a353d8b9 [pre-commit.ci] pre-commit autoupdate (#10384)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-09 06:45:19 +05:45
pre-commit-ci[bot] dd2b515c97 [pre-commit.ci] pre-commit autoupdate (#10376)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-02 06:52:09 +05:45
skshetry c223cdaecd params/metrics: do not fail during dir expansion (#10375)
params/metrics: assume file when dir expansion raises error

Closes https://github.com/iterative/dvc/issues/10018.

We have to be careful when expanding paths. This PR fall backs to assume it
as a file when either of `isdir` and `find` raises error due to some mishaps.
The consequence of this assumption happens when we try to read the file.
In that case, if the directory exists, and we happen to try to open it
as a file, dvc will raise `IsADirectoryError`, but that is handled correctly
and won't fail catastrophically like this did.
2024-03-28 07:30:12 +05:45
skshetry 5379aa2348 add: raise DoesNotExistError only if the output path does not exist (#10373)
`dvc add` command incorrectly raises a `DoesNotExistError` when a
broken symlink exists in an output directory, and the target name
is same as the directory's name.

eg: If `data` is an output, and is the command is invoked as `dvc add data`
(i.e. no virtual directory operations to perform).

The expected behavior to raise a `FileNotFoundError`.
`DoesNotExistError` should only be raised if the output itself does not
exist.

Related: https://github.com/iterative/dvc/issues/3717
2024-03-26 11:16:35 +00:00
pre-commit-ci[bot]andSaugat Pachhai 3375a7c39f [pre-commit.ci] pre-commit autoupdate (#10372)
* [pre-commit.ci] pre-commit autoupdate

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

* fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-03-26 06:37:53 +00:00
dberenbaum 519c47f5fc sync: skip if remote doesn't match output remote 2024-03-25 15:12:33 -04:00
skshetry 6f6038888f sort imports for all libraries starting with dvc_ as a first party (#10371)
dvc_studio_client, dvc_ssh, etc. were not properly sorted.
2024-03-23 20:13:13 +05:45
skshetry e223f51e7e explicitly specify dependencies that are imported inside dvc (#10370)
Most of these are also transitive dependencies, but we should explicitly
specify them if they are imported within dvc.
2024-03-23 18:51:25 +05:45
skshetry fb628bf9ea add a regression test for #10309 (#10369)
https://github.com/iterative/dvc/issues/10309
2024-03-23 11:37:36 +05:45
dependabot[bot]andskshetry 4f4e977e53 build(deps-dev): update dvc-data requirement from <3.15,>=3.13 to >=3.15,<3.16 (#10368)
* build(deps-dev): update dvc-data requirement

Updates the requirements on [dvc-data](https://github.com/iterative/dvc-data) to permit the latest version.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/3.13.0...3.15.0)

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

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

* Update pyproject.toml

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2024-03-23 05:48:17 +00:00
skshetry 8b1f8847fe add CITATION file so that users can cite dvc (#10366) 2024-03-23 07:05:07 +05:45
skshetry c62a0380e9 templating: support on top-level params/metrics/plots/artifacts/datasets (#10359)
* templating: support on top-level params/metrics/plots/artifacts/datasets

* make resolving top-level ordered

* prevent updating parametrized dataset

* fix dataset tests

* add tests

* fix comment

* support parametrizing artifacts name
2024-03-22 12:22:16 +00:00
Rishabh SharmaandRishabh Sharma 8d8939fb67 DVC_STAGE: Added environment variable to track DVC stage name (#10357)
DVC_STAGE_NAME: Added environment variable to track DVC stage name

Fixes #10355

Co-authored-by: Rishabh Sharma <[email protected]>
2024-03-20 13:28:02 -07:00
dependabot[bot] f7a54bf2d0 build(deps): bump rtCamp/action-slack-notify from 2.2.1 to 2.3.0 (#10364)
Bumps [rtCamp/action-slack-notify](https://github.com/rtcamp/action-slack-notify) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/rtcamp/action-slack-notify/releases)
- [Commits](https://github.com/rtcamp/action-slack-notify/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: rtCamp/action-slack-notify
  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>
2024-03-20 08:15:27 +05:45
pre-commit-ci[bot]andSaugat Pachhai 1a9f5d3aa8 [pre-commit.ci] pre-commit autoupdate (#10362)
* [pre-commit.ci] pre-commit autoupdate

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

* fix lint issue

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-03-19 13:25:39 +05:45
Ivan Shcheklein 017a510b83 add attrs explicit dependency (#10354)
fixes AttrsInstance import failing, fixed by https://github.com/python-attrs/attrs/issues/987
2024-03-16 08:58:37 +05:45
skshetry a80d224c58 fix dvcx import (#10351) 2024-03-14 15:20:17 +05:45
pre-commit-ci[bot]andSaugat Pachhai 49709a4810 [pre-commit.ci] pre-commit autoupdate (#10350)
* [pre-commit.ci] pre-commit autoupdate

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

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

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

* 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]>
2024-03-13 16:43:10 +05:45
dberenbaum 7df58ce404 Revert "refactor notify_refs_to_studio"
This reverts commit c16d615b55f2bb359b731939ad22712a502d8a8d.
2024-03-11 10:47:01 -04:00
dberenbaum 827e58f305 Revert "fix mypy"
This reverts commit 029b68ef77b7f82d5e72c8bb48f3eb980fe8204e.
2024-03-11 10:47:01 -04:00
dberenbaum 8bc6ba7cd5 fix mypy 2024-03-11 10:47:01 -04:00
dberenbaum a96a446742 refactor notify_refs_to_studio 2024-03-11 10:47:01 -04:00
dberenbaum 837c8e8de4 exp: use exp git remote for studio repo url 2024-03-11 10:47:01 -04:00
dberenbaum 603da1ca1e api: update remote config tests 2024-03-11 09:47:05 -04:00
dberenbaum 377d0c9d58 get: update show-url to take remote args 2024-03-11 09:47:05 -04:00
dependabot[bot] 6bdf41a217 build(deps-dev): bump mypy from 1.8.0 to 1.9.0 (#10348)
Bumps [mypy](https://github.com/python/mypy) from 1.8.0 to 1.9.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.8.0...1.9.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 08:44:35 +05:45
dberenbaum 1aeb8750b6 drop debugging comment 2024-03-07 07:39:53 -05:00
dberenbaum 5aae934379 test auto push with tmp dir 2024-03-07 07:39:53 -05:00
dberenbaum de1338caaf write git config to exp tmp repos 2024-03-07 07:39:53 -05:00
Ivan Shchekleinandskshetry 9957410998 fix(plots): handle empty directories (#10344)
* fix(plots): handle empty directories

* Update tests/integration/plots/test_plots.py

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

* cleanup after review

---------

Co-authored-by: skshetry <[email protected]>
2024-03-07 12:50:49 +05:45
dberenbaum cf9855af45 handle auto push errors 2024-03-05 09:44:48 -05:00
skshetry 0001cc3a4b drop 2.x branch (#10339)
* drop 2.x branch

* Update tests.yaml
2024-03-05 14:39:51 +05:45
skshetry 440e1491f0 Delete .github/mergify.yml (#10338) 2024-03-05 14:37:44 +05:45
AlexandreKempfandSaugat Pachhai b0384f8f51 dvc studio login automatically push experiments by default (#10331)
* studio login set auto_push + add logs for the user

* set a separate global config dir for studio login tests

The `isolate` fixture sets a separate, _isolated_ global config directory.
But it is shared among other tests. So any mutations to it affects other
tests too.

We should force `studio login` tests to use a separate tmp_dir for global config.

* cleanup message

---------

Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-03-05 11:48:16 +05:45
cd1e228e55 experiments: auto push experiments (#10323)
* add auto_push for experiment on run and on save.
add config.exp.auto_push and config.exp.git_remote

* add func tests

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

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

* separate auto_push behaviour out of commit

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-03-04 14:20:56 +05:45
skshetry 3b2e031e60 replace flaky with pytest-rerunfailures (#10335)
* Revert "limit pytest version to <8.1 (#10334)"

This reverts commit bc95b650a4.

* replace flaky with pytest-rerunfailures

`flaky` project seems to be unmaintained (see box/flaky#192).
`pytest-rerunfailures` seems to be maintained under `pytest-dev`
and tests 5 minor releases of pytest.

Note that this plugin is not compatible with `flaky` plugin.
You will have to uninstall it if it exists.
2024-03-04 11:12:45 +05:45
skshetry e5ee82bc99 support newer pytest-docker (#10336) 2024-03-04 11:09:56 +05:45
skshetry bc95b650a4 limit pytest version to <8.1 (#10334)
Change in pytest==8.1.0 breaks `flaky` plugin.

See https://github.com/box/flaky/issues/198.
2024-03-04 10:31:58 +05:45
skshetry 257ea5f4cb tests: use default branch for rev (#10332)
`isolate` fixture don't run in plugin's tests.
So the tests will fail to run if you have `init.defaultBranch`
something other than `master`.
2024-03-01 11:16:59 +05:45
AlexandreKempf 5f7acd4cbe add subdir test to integration tests 2024-02-29 13:50:30 -05:00
AlexandreKempf 13325ea89b use explicit code to fix copy bug 2024-02-29 13:50:30 -05:00
AlexandreKempf fd71434df5 fix test 2024-02-29 13:50:30 -05:00
AlexandreKempf 2ff4ff2daa implement logic for closest parent detection 2024-02-29 13:50:30 -05:00
AlexandreKempf 0ee6d6eba8 add more extensive tests 2024-02-29 13:50:30 -05:00
pre-commit-ci[bot] 7c34370464 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-02-29 13:50:30 -05:00
AlexandreKempf b67b254287 add test 2024-02-29 13:50:30 -05:00
AlexandreKempf 212928a1fa fix plots nested configuration 2024-02-29 13:50:30 -05:00
dependabot[bot] 5be23cfd4e build(deps-dev): update dvc-data requirement from <3.14,>=3.13 to >=3.13,<3.15 (#10329)
build(deps-dev): update dvc-data requirement

Updates the requirements on [dvc-data](https://github.com/iterative/dvc-data) to permit the latest version.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/3.13.0...3.14.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-28 19:28:29 +05:45
skshetry 4d3a631e60 fix PathInfo instantiation in Python 3.12 (#10328) 2024-02-28 14:40:56 +05:45
281 changed files with 11584 additions and 2090 deletions
File diff suppressed because it is too large Load Diff
-60
View File
@@ -1,60 +0,0 @@
---
name: Epic/Story Issue Template
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
title: 'Epic: New Feature'
labels: epic
assignees:
---
## Summary / Background
What do you want to achieve, why? business context
...
## Scope
What will be impacted and what won't be?
What needs implementation and what is invariant?
e.g.
- user should be able to run workflow X from UI
- enable workflow Y, Z from CLI
## Assumptions
Product / UX assumptions as well as technical assumptions / limitations
e.g.
* Support only Python Runtime
* Focus on DVC experiments only
* Deployment environments don't change often and can be picked up from shared configuration
## Open Questions
e.g.
- How should access control work for shared artifacts (workflow X)
- Python runtime assumption - is it really valid? in light of <...>
## Blockers / Dependencies
List issues or other conditions / blockers
## General Approach
Invocation example:
```shell
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
```
## Steps
### Must have (p1)
- [ ] subissue2
- [ ] step 2
- info
- info
### Optional / followup (p2)
- [ ] ⌛ step 3 wip
- [ ] step 4
## Timelines
Put your estimations here. Update once certainty changes
- end of week (Feb 3) for prototype with workflows X, Y
- Feb 15 - MVP in production
- Low priority followups can be done later
-10
View File
@@ -1,10 +0,0 @@
pull_request_rules:
- name: backport patches to 2.x
conditions:
- base=main
- label=backport
actions:
backport:
branches:
- "2.x"
title: ({{ destination_branch }}) {{ title }}
+30 -4
View File
@@ -5,14 +5,40 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
FORCE_COLOR: "1"
PY_COLORS: "1"
jobs:
bench:
name: run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- uses: iterative/dvc-bench@main
python-version: "3.13"
- uses: actions/checkout@v5
with:
pytest_options: "-k 'test_init or test_help'"
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
- run: uv pip install '.[tests]' --system
- name: run benchmarks on base branch
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
- uses: actions/checkout@v5
with:
fetch-depth: 0
clean: false
- run: uv pip install '.[tests]' --system
- name: run benchmarks for PR
run: >
pytest --benchmark-compare --benchmark-compare-fail=min:5%
--benchmark-group-by name
dvc/testing/benchmarks/ -k 'test_init or test_help'
+19 -16
View File
@@ -19,30 +19,33 @@ jobs:
name: Build & Verify package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: 3.13
- run: python -m pip install --upgrade pip setuptools_scm build twine
- uses: astral-sh/setup-uv@v6
- name: Force version for Test PyPI upload
if: ${{ !startsWith(github.ref, 'refs/tags') }}
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
- name: Omit local version for Test PyPI upload
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: echo SETUPTOOLS_SCM_OVERRIDES_FOR_DVC='{local_scheme="no-local-version"}' >> $GITHUB_ENV
- run: echo 'PKG = "pip"'>dvc/_build.py
- run: python -m build
env:
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
- run: twine check --strict dist/*
- name: Build Python Package
run: |
echo 'PKG = "pip"'>dvc/_build.py
uv build
- name: Check dist
run: uv tool run twine check --strict dist/*
- uses: actions/upload-artifact@v4
with:
name: Packages
path: dist/
if-no-files-found: error
test-pypi-publish:
name: Publish dev package to test.pypi.org
@@ -52,13 +55,13 @@ jobs:
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@v5
with:
name: Packages
path: dist
@@ -77,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@v5
with:
name: Packages
path: dist
+2 -2
View File
@@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: ["main", "2.x"]
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
+13 -12
View File
@@ -18,35 +18,36 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
pyv: ["3.10"]
os: [ubuntu-latest]
pyv: ["3.13"]
plugin: ["dvc-s3"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
path: dvc
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: iterative/${{ 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 }}
cache: pip
cache-dependency-path: |
dvc/pyproject.toml
${{ matrix.plugin }}/setup.cfg
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
cache-suffix: ${{ matrix.pyv }}
- name: Install plugin + DVC@PR
run: |
pip install --upgrade pip wheel
pip install "./dvc[testing]"
pip install -e "./${{ matrix.plugin }}[tests]"
uv pip install "./dvc[testing]" --system
uv pip install -e "./${{ matrix.plugin }}[tests]" --system
- name: Run plugin tests
timeout-minutes: 15
+113 -21
View File
@@ -2,7 +2,7 @@ name: Tests
on:
push:
branches: [main, 2.x]
branches: [main]
pull_request:
schedule:
- cron: '5 1 * * *' # every day at 01:05
@@ -16,42 +16,134 @@ permissions:
contents: read
jobs:
lint:
timeout-minutes: 10
runs-on: ${{ matrix.os }}
env:
DVC_NO_ANALYTICS: true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
- run: uv pip install -e ".[dev]" --system
env:
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
- name: Cache mypy
uses: actions/cache@v4
with:
path: .mypy_cache
key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
- uses: pre-commit/[email protected]
tests:
timeout-minutes: 50
timeout-minutes: 20
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, macos-latest]
pyv: ["3.9", "3.10", "3.11", "3.12", "3.13"]
include:
- os: windows-latest
pyv: "3.9"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.9"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.9"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.9"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
- os: windows-latest
pyv: "3.10"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.10"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.10"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.10"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
- os: windows-latest
pyv: "3.11"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.11"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.11"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.11"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
- os: windows-latest
pyv: "3.12"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.12"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.12"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.12"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
- os: windows-latest
pyv: "3.13"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
- os: windows-latest
pyv: "3.13"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
- os: windows-latest
pyv: "3.13"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
- os: windows-latest
pyv: "3.13"
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.pyv }}
cache: pip
cache-dependency-path: pyproject.toml
- name: install
run: |
pip install --upgrade pip wheel
pip install -e ".[dev]"
- uses: pre-commit/[email protected]
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
cache-suffix: ${{ matrix.pyv }}
- run: uv pip install -e ".[dev]" pytest-split --system
env:
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
- name: run tests
timeout-minutes: 40
env:
COVERAGE_CORE: sysmon
PYTHONUTF8: 1
run: >
pytest -n=logical --timeout=300 --durations=100
--cov --cov-report=xml --cov-report=term
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
--cov --cov-report=xml --cov-report=term --durations-path=./.github/.test_durations
- name: upload coverage report
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: false
uses: codecov/codecov-action@v5
check:
if: always()
needs: [tests]
@@ -66,7 +158,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2.2.1
uses: rtCamp/action-slack-notify@v2.3.3
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_COLOR: ${{ job.status }}
+1 -1
View File
@@ -43,7 +43,7 @@ scripts/ci/gcp-creds.json
.gcp-creds.json
*~
/dvc/_dvc_version.py
/dvc/_version.py
.mypy_cache/
.pytest_cache/
+6 -6
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -22,13 +22,13 @@ repos:
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.2.2'
rev: 'v0.13.2'
hooks:
- id: ruff
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies: ["tomli"]
@@ -50,7 +50,7 @@ repos:
language: system
name: DVC pre-commit
stages:
- commit
- pre-commit
verbose: true
require_serial: true
- args:
@@ -61,7 +61,7 @@ repos:
language: system
name: DVC pre-push
stages:
- push
- pre-push
require_serial: true
- always_run: true
args:
+3 -3
View File
@@ -8,7 +8,7 @@
name: DVC pre-commit
require_serial: true
stages:
- commit
- pre-commit
verbose: true
- args:
- git-hook
@@ -20,7 +20,7 @@
name: DVC pre-push
require_serial: true
stages:
- push
- pre-push
- always_run: true
args:
- git-hook
@@ -29,7 +29,7 @@
id: dvc-post-checkout
language: python
language_version: python3
minimum_pre_commit_version: 2.2.0
minimum_pre_commit_version: 3.2.0
name: DVC post-checkout
require_serial: true
stages:
+25
View File
@@ -0,0 +1,25 @@
cff-version: 1.2.0
title: 'DVC: Data Version Control - Git for Data & Models"'
message: >-
If you use this software as part of a publication and wish
to cite it, please use the metadata from this file.
type: software
authors:
- name: The DVC team and contributors
website: 'https://github.com/iterative'
repository-code: 'https://github.com/iterative/dvc'
url: 'https://dvc.org'
abstract: >-
DVC is a tool for data science that takes advantage of existing software engineering toolset.
It helps machine learning teams manage large datasets, make projects reproducible, and collaborate better.
keywords:
- ai
- collaboration
- data-science
- data-version-control
- developer-tools
- git
- machine-learning
- python
- reproducibility
license: Apache-2.0
+5 -5
View File
@@ -213,7 +213,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
=========
@@ -240,11 +240,11 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/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/iterative/dvc/actions/workflows/tests.yaml/badge.svg
:target: https://github.com/iterative/dvc/actions/workflows/tests.yaml
:alt: GHA Tests
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
.. |Maintainability| image:: https://img.shields.io/codeclimate/maintainability/iterative/dvc
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
@@ -268,7 +268,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
+1
View File
@@ -3,6 +3,7 @@ DVC
----
Make your data science projects reproducible and shareable.
"""
import dvc.logger
from dvc.build import PKG # noqa: F401
from dvc.version import __version__, version_tuple # noqa: F401
+1
View File
@@ -1,4 +1,5 @@
"""Main entry point for DVC command line tool."""
import sys
from dvc.cli import main
+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()
@@ -162,7 +162,7 @@ def _get_path_func(tool: str, ext: str):
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
def func(now: Optional["datetime"] = None) -> str:
return fmt.format(now=now or datetime.now())
return fmt.format(now=now or datetime.now()) # noqa: DTZ005
return func
+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 -1
View File
@@ -1,7 +1,7 @@
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .artifacts import artifacts_show
from .data import get_url, open, read
from .data import get_url, open, read # noqa: A004
from .dataset import get as get_dataset
from .experiments import exp_save, exp_show
from .scm import all_branches, all_commits, all_tags
+13 -2
View File
@@ -1,3 +1,4 @@
import os
from typing import Any, Optional
from dvc.repo import Repo
@@ -36,12 +37,22 @@ def artifacts_show(
if version and stage:
raise ValueError("Artifact version and stage are mutually exclusive.")
from dvc.repo.artifacts import Artifacts
from dvc.utils import as_posix
repo_kwargs: dict[str, Any] = {
"subrepos": True,
"uninitialized": True,
}
dirname, _ = Artifacts.parse_path(name)
with Repo.open(repo, **repo_kwargs) as _repo:
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
with _repo.switch(rev):
path = _repo.artifacts.get_path(name)
return {"rev": rev, "path": path}
root = _repo.fs.root_marker
_dirname = _repo.fs.join(root, dirname) if dirname else root
with Repo(_dirname, fs=_repo.fs, scm=_repo.scm) as r:
path = r.artifacts.get_path(name)
path = _repo.fs.join(_repo.fs.root_marker, as_posix(path))
parts = _repo.fs.relparts(path, _repo.root_dir)
return {"rev": rev, "path": os.path.join(*parts)}
+42 -6
View File
@@ -25,7 +25,14 @@ def _wrap_exceptions(repo, url):
raise PathMissingError(exc.path, url) from exc
def get_url(path, repo=None, rev=None, remote=None):
def get_url(
path: str,
repo: Optional[str] = None,
rev: Optional[str] = None,
remote: Optional[str] = None,
config: Optional[dict[str, Any]] = None,
remote_config: Optional[dict[str, Any]] = None,
):
"""
Returns the URL to the storage location of a data file or directory tracked
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
@@ -35,15 +42,44 @@ def get_url(path, repo=None, rev=None, remote=None):
NOTE: This function does not check for the actual existence of the file or
directory in the remote storage.
Args:
path (str): location and file name of the target, relative to the root
of `repo`.
repo (str, optional): location of the DVC project or Git Repo.
Defaults to the current DVC project (found by walking up from the
current working directory tree).
It can be a URL or a file system path.
Both HTTP and SSH protocols are supported for online Git repos
(e.g. [user@]server:project.git).
rev (str, optional): Any `Git revision`_ such as a branch or tag name,
a commit hash or a dvc experiment name.
Defaults to HEAD.
If `repo` is not a Git repo, this option is ignored.
remote (str, optional): Name of the `DVC remote`_ used to form the
returned URL string.
Defaults to the `default remote`_ of `repo`.
For local projects, the cache is tried before the default remote.
config(dict, optional): config to be passed to the DVC repository.
Defaults to None.
remote_config(dict, optional): remote config to be passed to the DVC
repository.
Defaults to None.
Returns:
str: URL to the file or directory.
"""
from dvc.config import NoRemoteError
from dvc_data.index import StorageKeyError
repo_kwargs: dict[str, Any] = {}
if remote:
repo_kwargs["config"] = {"core": {"remote": remote}}
with Repo.open(
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
repo,
rev=rev,
subrepos=True,
uninitialized=True,
remote=remote,
config=config,
remote_config=remote_config,
) as _repo:
index, entry = _repo.get_data_index_entry(path)
with reraise(
@@ -249,7 +285,7 @@ def _open(
if TYPE_CHECKING:
from dvc.fs import FileSystem
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
fs: Union[FileSystem, DataFileSystem, DVCFileSystem]
if os.path.isabs(path):
fs = DataFileSystem(index=_repo.index.data["local"])
fs_path = path
+7 -6
View File
@@ -1,8 +1,8 @@
from typing import Literal, TypedDict, Union
class DVCXDataset(TypedDict):
type: Literal["dvcx"]
class DatachainDataset(TypedDict):
type: Literal["dc"]
name: str
version: int
@@ -20,7 +20,7 @@ class URLDataset(TypedDict):
path: str
def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
def get(name: str) -> Union[DatachainDataset, DVCDataset, URLDataset]:
from difflib import get_close_matches
from dvc.fs import get_cloud_fs
@@ -46,9 +46,9 @@ def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
path=dataset.lock.path,
sha=dataset.lock.rev_lock,
)
if dataset.type == "dvcx":
return DVCXDataset(
type="dvcx", name=dataset.name_version[0], version=dataset.lock.version
if dataset.type == "dc":
return DatachainDataset(
type="dc", name=dataset.name_version[0], version=dataset.lock.version
)
if dataset.type == "url":
fs_cls, _, path = get_cloud_fs(repo.config, url=dataset.lock.url)
@@ -64,3 +64,4 @@ def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
for file in dataset.lock.files
]
return URLDataset(type="url", files=files, path=versioned_path)
raise AssertionError("unreachable")
+1 -2
View File
@@ -27,8 +27,7 @@ def _postprocess(results):
}
processed[rev] = processed[rev] | to_merge
if "workspace" in processed:
del processed["workspace"]
processed.pop("workspace", None)
return processed
+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):
+6 -5
View File
@@ -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
+5 -2
View File
@@ -18,8 +18,11 @@ class CmdBase(ABC):
os.chdir(args.cd)
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
self.config: "Config" = self.repo.config
self.repo: Repo = Repo(
uninitialized=self.UNINITIALIZED,
_wait_for_lock=args.wait_for_lock,
)
self.config: Config = self.repo.config
self.args = args
def do_run(self):
+163 -3
View File
@@ -22,6 +22,36 @@ _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
}
_dvc_compgen_remote_config_vars() {
local cur prev words cword remote_name filtered
_init_completion || return
# last non-option word before cursor
local nonopts=()
for w in "${words[@]:1:$((cword-1))}"; do
[[ $w == -* ]] || nonopts+=("$w")
done
remote_name="${nonopts[-1]}"
if [[ -n $remote_name ]]; then
local _dvc_remote_config_vars=($(
dvc config --available-options -q 2> /dev/null \
| grep "^remote\\.${remote_name}\\." \
| sed "s/^remote\\.${remote_name}\\.//"
))
compgen -W "${_dvc_remote_config_vars[*]}" -- $1
fi
}
"""
ZSH_PREAMBLE = """
@@ -55,19 +85,149 @@ _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_config_config_vars_for_completion() {
dvc config --available-options -q 2> /dev/null
}
_dvc_compadd_config_vars() {
_describe 'config_vars' _dvc_config_vars
}
_dvc_compadd_remote_config_vars() {
local remote_name filtered
# last non-option word before cursor
remote_name=${${words[1,CURRENT-1]##-*}[-1]}
if [[ -n $remote_name ]]; then
filtered=$(_dvc_config_config_vars_for_completion \\
| grep "^remote.${remote_name}\\\\." \\
| sed "s/^remote\\\\.${remote_name}\\\\.//"
)
compadd -- $=filtered
fi
}
"""
FISH_PREAMBLE = """
function __fish_complete_dvc_files
__fish_complete_path | string match -re '\\*?.dvc|Dvcfile|dvc\\.yaml'
end
function __fish_complete_dvc_stages
for line in (dvc stage list -q)
set -l parts (string split -m1 ' ' -- $line)
set -l name $parts[1]
set -l desc (string trim $parts[2])
echo -e "$name $desc"
end
end
function __fish_complete_dvc_stages_and_files
__fish_complete_dvc_stages
__fish_complete_dvc_files
end
function __fish_complete_dvc_experiments
dvc exp list -q --all-commits --names-only
end
function __fish_complete_dvc_remotes
dvc remote list | cut -d' ' -f1
end
function __fish_complete_dvc_remote_config_vars
# last non-option word before cursor
set -l remote_name (commandline -opc | string match -rv '^-' | tail -n 1)
if test -n "$remote_name"
dvc config --available-options -q 2>/dev/null |
string match -re "^remote\\.$remote_name\\." |
string replace -r "^remote\\.$remote_name\\." ''
end
end
"""
PREAMBLE = {
"bash": BASH_PREAMBLE,
"zsh": ZSH_PREAMBLE,
"fish": FISH_PREAMBLE,
}
FILE = shtab.FILE
DIR = shtab.DIRECTORY
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
DVC_FILE = {
"bash": "_dvc_compgen_DVCFiles",
"zsh": "_dvc_compadd_DVCFiles",
"fish": '-f -a "(__fish_complete_dvc_files)"',
}
STAGE = {
"bash": "_dvc_compgen_stages",
"zsh": "_dvc_compadd_stages",
"fish": '-f -a "(__fish_complete_dvc_stages)"',
}
DVCFILES_AND_STAGE = {
"bash": "_dvc_compgen_stages_and_files",
"zsh": "_dvc_compadd_stages_and_files",
"fish": '-f -a "(__fish_complete_dvc_stages_and_files)"',
}
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
EXPERIMENT = {
"bash": "_dvc_compgen_exps",
"zsh": "_dvc_compadd_exps",
"fish": '-f -a "(__fish_complete_dvc_experiments)"',
}
REMOTE = {
"bash": "_dvc_compgen_remotes",
"zsh": "_dvc_compadd_remotes",
"fish": '-f -a "(__fish_complete_dvc_remotes)"',
}
CONFIG_VARS = {
"bash": "_dvc_compgen_config_vars",
"zsh": "_dvc_compadd_config_vars",
"fish": '-f -a "(__fish_complete_dvc_config_vars)"',
}
REMOTE_CONFIG_VARS = {
"bash": "_dvc_compgen_remote_config_vars",
"zsh": "_dvc_compadd_remote_config_vars",
"fish": '-f -a "(__fish_complete_dvc_remote_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)}
)
"""
indent = "\t\t".expandtabs(4) # 8 spaces
lines = (
"\n".join(
f"{indent}{c} \\"
for c in config_vars[:-1] # all but last
)
+ "\n"
+ f"{indent}{config_vars[-1]}"
) # last line without backslash
config_vars_arr_fish = f"""
function __fish_complete_dvc_config_vars
set -l _dvc_config_vars \\
{lines}
printf %s\\n $_dvc_config_vars
end
"""
for shell, preamble in PREAMBLE.items():
if shell != "fish":
ret[shell] = config_vars_arr + preamble
else:
ret[shell] = config_vars_arr_fish + preamble
return ret
+9
View File
@@ -1,4 +1,5 @@
"""Main parser for the dvc cli."""
import argparse
import os
from functools import lru_cache
@@ -199,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
@@ -40,7 +40,7 @@ def hide_subparsers_from_help(subparsers):
# from the 'positional arguments' choices list
# see: https://bugs.python.org/issue22848
# Need to set `add_help=False`, but avoid setting `help`
# (not even to `argparse.SUPPPRESS`).
# (not even to `argparse.SUPPRESS`).
# NOTE: The argument is the parent subparser, not the subcommand parser.
cmds = [cmd for cmd, parser in subparsers.choices.items() if parser.add_help]
subparsers.metavar = "{{{}}}".format(",".join(cmds))
+9 -1
View File
@@ -51,6 +51,7 @@ class CmdAdd(CmdBase):
to_remote=self.args.to_remote,
remote_jobs=self.args.remote_jobs,
force=self.args.force,
relink=self.args.relink,
)
except FileNotFoundError:
logger.exception("")
@@ -100,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,
@@ -119,6 +120,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Override local file or folder if exists.",
)
parser.add_argument(
"--no-relink",
dest="relink",
action="store_false",
help="Don't recreate links from cache to workspace.",
)
parser.set_defaults(relink=True)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
).complete = completion.FILE
+34 -1
View File
@@ -13,6 +13,9 @@ class CmdArtifactsGet(CmdBaseNoRepo):
from dvc.scm import CloneError
from dvc.ui import ui
if self.args.show_url:
return self._show_url()
try:
count, out = Artifacts.get(
self.args.url,
@@ -36,6 +39,28 @@ class CmdArtifactsGet(CmdBaseNoRepo):
)
return 1
def _show_url(self):
from dvc.api import artifacts_show, get_url
from dvc.ui import ui
artifact = artifacts_show(
self.args.name,
version=self.args.rev,
stage=self.args.stage,
repo=self.args.url,
)
url = get_url(
artifact["path"],
repo=self.args.url,
rev=artifact["rev"],
remote=self.args.remote,
remote_config=self.args.remote_config,
)
ui.write(url, force=True)
return 0
def add_parser(subparsers, parent_parser):
ARTIFACTS_HELP = "DVC model registry artifact commands."
@@ -84,6 +109,14 @@ def add_parser(subparsers, parent_parser):
help="Destination path to download artifact to",
metavar="<path>",
).complete = completion.DIR
get_parser.add_argument(
"--show-url",
action="store_true",
help=(
"Print the storage location (URL) the target data would be "
"downloaded from, and exit."
),
)
get_parser.add_argument(
"-j",
"--jobs",
@@ -116,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
+8
View File
@@ -20,6 +20,7 @@ class CmdCommit(CmdBase):
with_deps=self.args.with_deps,
recursive=self.args.recursive,
force=self.args.force,
relink=self.args.relink,
)
except DvcException:
logger.exception("failed to commit%s", (" " + target) if target else "")
@@ -64,6 +65,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Commit cache for subdirectories of the specified directory.",
)
commit_parser.add_argument(
"--no-relink",
dest="relink",
action="store_false",
help="Don't recreate links from cache to workspace.",
)
commit_parser.set_defaults(relink=True)
commit_parser.add_argument(
"targets",
nargs="*",
+3 -3
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
@@ -8,7 +8,7 @@ from dvc.ui import ui
logger = logger.getChild(__name__)
SUPPORTED_SHELLS = ["bash", "zsh"]
SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
class CmdCompletion(CmdBaseNoRepo):
@@ -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
+14 -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
@@ -40,6 +40,15 @@ class CmdConfig(CmdBaseNoRepo):
self.config = Config.from_cwd(validate=False)
def run(self):
if self.args.available_options:
from dvc.config_schema import contextual_config_vars_for_completion
ui.write(
"\n".join(list(contextual_config_vars_for_completion(self.config))),
force=True,
)
return 0
if self.args.show_origin and (self.args.value or self.args.unset):
logger.error(
"--show-origin can't be used together with any of these "
@@ -212,6 +221,9 @@ def add_parser(subparsers, parent_parser):
help=CONFIG_HELP,
formatter_class=formatter.RawDescriptionHelpFormatter,
)
config_parser.add_argument(
"--available-options", default=False, action="store_true"
)
config_parser.add_argument(
"-u",
"--unset",
@@ -224,7 +236,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",
+1 -1
View File
@@ -17,7 +17,7 @@ def _show_ascii(graph: "DiGraph"):
ret = []
for pipeline in pipelines:
ret.append(draw(pipeline.nodes, pipeline.edges))
ret.append(draw(pipeline.nodes, pipeline.edges)) # noqa: PERF401
return "\n".join(ret)
+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",
+14 -13
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",
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
)
pull_parser.add_argument(
"--run-cache",
action="store_true",
action=argparse.BooleanOptionalAction,
default=False,
help="Fetch run history for all stages.",
)
@@ -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",
@@ -262,7 +263,7 @@ def add_parser(subparsers, _parent_parser):
)
push_parser.add_argument(
"--run-cache",
action="store_true",
action=argparse.BooleanOptionalAction,
default=False,
help="Push run history for all stages.",
)
@@ -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",
@@ -324,7 +325,7 @@ def add_parser(subparsers, _parent_parser):
)
fetch_parser.add_argument(
"--run-cache",
action="store_true",
action=argparse.BooleanOptionalAction,
default=False,
help="Fetch run history for all stages.",
)
@@ -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",
+9 -8
View File
@@ -34,7 +34,7 @@ class CmdDatasetAdd(CmdBase):
url = dataset.spec.url
ver: str = ""
if dataset.type == "dvcx":
if dataset.type == "dc":
ver = f"v{dataset.lock.version}"
if dataset.type == "dvc":
if dataset.lock.path:
@@ -42,7 +42,7 @@ class CmdDatasetAdd(CmdBase):
if rev := dataset.lock.rev:
ver = rev
ver_part: Optional["Text"] = None
ver_part: Optional[Text] = None
if ver:
ver_part = ui.rich_text.assemble(" @ ", (ver, "repr.number"))
text = ui.rich_text.assemble("(", (url, "repr.url"), ver_part or "", ")")
@@ -55,7 +55,7 @@ class CmdDatasetAdd(CmdBase):
raise DvcException("--path can't be used without --dvc")
d = vars(self.args)
for key in ["dvc", "dvcx", "url"]:
for key in ["dvc", "dc", "url"]:
if url := d.pop(key, None):
d.update({"type": key, "url": url})
break
@@ -83,13 +83,13 @@ 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
v: Optional[tuple[str, str]] = None
if dataset.type == "dvcx":
assert new.type == "dvcx"
if dataset.type == "dc":
assert new.type == "dc"
if new.lock.version < dataset.lock.version:
action = "Downgrading"
@@ -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
@@ -168,7 +169,7 @@ def add_parser(subparsers, parent_parser):
url_exclusive_group = ds_add_parser.add_mutually_exclusive_group(required=True)
url_exclusive_group.add_argument(
"--dvcx", metavar="name", help="Name of the dvcx dataset to track"
"--dc", metavar="name", help="Name of the DataChain dataset to track"
)
url_exclusive_group.add_argument(
"--dvc",
@@ -219,7 +220,7 @@ remote://remote_name/path/to/file/or/dir (see `dvc remote`)
"--rev",
"--version",
nargs="?",
help="DVCX dataset version or Git revision (e.g. SHA, branch, tag)",
help="DataChain dataset version or Git revision (e.g. SHA, branch, tag)",
metavar="<version>",
)
ds_update_parser.set_defaults(func=CmdDatasetUpdate)
+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,
+9
View File
@@ -58,6 +58,15 @@ def add_parser(subparsers, parent_parser):
hide_subparsers_from_help(experiments_subparsers)
def add_keep_selection_flag(experiments_subcmd_parser):
experiments_subcmd_parser.add_argument(
"--keep",
action="store_true",
default=False,
help="Keep the selected experiments instead of removing them.",
)
def add_rev_selection_flags(
experiments_subcmd_parser, command: str, default: bool = True
):
+5 -3
View File
@@ -1,4 +1,6 @@
from dvc.cli import formatter
import argparse
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
@@ -69,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",
@@ -81,7 +83,7 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_pull_parser.add_argument(
"--run-cache",
action="store_true",
action=argparse.BooleanOptionalAction,
default=False,
help="Pull run history for all stages.",
)
+3 -2
View File
@@ -1,3 +1,4 @@
import argparse
from typing import Any
from dvc.cli import completion, formatter
@@ -110,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",
@@ -120,7 +121,7 @@ def add_parser(experiments_subparsers, parent_parser):
)
experiments_push_parser.add_argument(
"--run-cache",
action="store_true",
action=argparse.BooleanOptionalAction,
default=False,
help="Push run history for all stages.",
)
+3 -1
View File
@@ -34,6 +34,7 @@ class CmdExperimentsRemove(CmdBase):
num=self.args.num,
queue=self.args.queue,
git_remote=self.args.git_remote,
keep=self.args.keep,
)
if removed:
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
@@ -44,7 +45,7 @@ class CmdExperimentsRemove(CmdBase):
def add_parser(experiments_subparsers, parent_parser):
from . import add_rev_selection_flags
from . import add_keep_selection_flag, add_rev_selection_flags
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
experiments_remove_parser = experiments_subparsers.add_parser(
@@ -57,6 +58,7 @@ def add_parser(experiments_subparsers, parent_parser):
)
remove_group = experiments_remove_parser.add_mutually_exclusive_group()
add_rev_selection_flags(experiments_remove_parser, "Remove", False)
add_keep_selection_flag(experiments_remove_parser)
remove_group.add_argument(
"--queue", action="store_true", help="Remove all queued experiments."
)
+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",
+4 -4
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
@@ -54,13 +54,13 @@ class CmdGC(CmdBase):
msg += " and all experiments"
if self.args.not_in_remote:
msg += " that are present in the DVC remote"
msg += " that are not present in the DVC remote"
if self.args.repos:
msg += " of the current and the following repos:"
for repo_path in self.args.repos:
msg += "\n - %s" % os.path.abspath(repo_path)
msg += f"\n - {os.path.abspath(repo_path)}"
else:
msg += " of the current repo."
@@ -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",
+8 -2
View File
@@ -12,7 +12,13 @@ class CmdGet(CmdBaseNoRepo):
from dvc.api import get_url
from dvc.ui import ui
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
url = get_url(
self.args.path,
repo=self.args.url,
rev=self.args.rev,
remote=self.args.remote,
remote_config=self.args.remote_config,
)
ui.write(url, force=True)
return 0
@@ -115,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",
+13 -4
View File
@@ -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",
+178 -33
View File
@@ -1,3 +1,5 @@
from typing import Callable
from dvc.cli import completion, formatter
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import DictAction, append_doc_link
@@ -9,54 +11,178 @@ from dvc.ui import ui
logger = logger.getChild(__name__)
def _format_entry(entry, fmt):
from dvc.utils.humanize import naturalsize
def _get_formatter(with_color: bool = False) -> Callable[[dict], str]:
def fmt(entry: dict) -> str:
return entry["path"]
size = entry.get("size")
if size is None:
size = ""
else:
size = naturalsize(size)
return size, fmt(entry)
def show_entries(entries, with_color=False, with_size=False):
if with_color:
ls_colors = LsColors()
fmt = ls_colors.format
else:
return ls_colors.format
def fmt(entry):
return entry["path"]
return fmt
def _format_entry(entry, name, with_size=True, with_hash=False):
from dvc.utils.humanize import naturalsize
ret = []
if with_size:
ui.table([_format_entry(entry, fmt) for entry in entries])
size = entry.get("size")
if size is None or (size <= 0 and entry.get("isdir")):
size = ""
else:
size = naturalsize(size)
ret.append(size)
if with_hash:
md5 = entry.get("md5", "")
ret.append(md5)
ret.append(name)
return ret
def show_entries(entries, with_color=False, with_size=False, with_hash=False):
fmt = _get_formatter(with_color)
if with_size or with_hash:
colalign = ("right",) if with_size else None
ui.table(
[
_format_entry(
entry,
fmt(entry),
with_size=with_size,
with_hash=with_hash,
)
for entry in entries
],
colalign=colalign,
)
return
# NOTE: this is faster than ui.table for very large number of entries
ui.write("\n".join(fmt(entry) for entry in entries))
class TreePart:
Edge = "├── "
Line = "│ "
Corner = "└── "
Blank = " "
def _build_tree_structure(
entries, with_color=False, with_size=False, with_hash=False, _depth=0, _prefix=""
):
rows = []
fmt = _get_formatter(with_color)
num_entries = len(entries)
for i, (name, entry) in enumerate(entries.items()):
entry["path"] = name
is_last = i >= num_entries - 1
tree_part = ""
if _depth > 0:
tree_part = TreePart.Corner if is_last else TreePart.Edge
row = _format_entry(
entry,
_prefix + tree_part + fmt(entry),
with_size=with_size,
with_hash=with_hash,
)
rows.append(row)
if contents := entry.get("contents"):
new_prefix = _prefix
if _depth > 0:
new_prefix += TreePart.Blank if is_last else TreePart.Line
new_rows = _build_tree_structure(
contents,
with_color=with_color,
with_size=with_size,
with_hash=with_hash,
_depth=_depth + 1,
_prefix=new_prefix,
)
rows.extend(new_rows)
return rows
def show_tree(entries, with_color=False, with_size=False, with_hash=False):
import tabulate
rows = _build_tree_structure(
entries,
with_color=with_color,
with_size=with_size,
with_hash=with_hash,
)
colalign = ("right",) if with_size else None
_orig = tabulate.PRESERVE_WHITESPACE
tabulate.PRESERVE_WHITESPACE = True
try:
ui.table(rows, colalign=colalign)
finally:
tabulate.PRESERVE_WHITESPACE = _orig
class CmdList(CmdBaseNoRepo):
def run(self):
def _show_tree(self):
from dvc.repo.ls import ls_tree
entries = ls_tree(
self.args.url,
self.args.path,
rev=self.args.rev,
dvc_only=self.args.dvc_only,
config=self.args.config,
remote=self.args.remote,
remote_config=self.args.remote_config,
maxdepth=self.args.level,
)
show_tree(
entries,
with_color=True,
with_size=self.args.size,
with_hash=self.args.show_hash,
)
return 0
def _show_list(self):
from dvc.repo import Repo
try:
entries = Repo.ls(
self.args.url,
self.args.path,
rev=self.args.rev,
recursive=self.args.recursive,
dvc_only=self.args.dvc_only,
config=self.args.config,
remote=self.args.remote,
remote_config=self.args.remote_config,
entries = Repo.ls(
self.args.url,
self.args.path,
rev=self.args.rev,
recursive=self.args.recursive,
dvc_only=self.args.dvc_only,
config=self.args.config,
remote=self.args.remote,
remote_config=self.args.remote_config,
maxdepth=self.args.level,
)
if self.args.json:
ui.write_json(entries)
elif entries:
show_entries(
entries,
with_color=True,
with_size=self.args.size,
with_hash=self.args.show_hash,
)
if self.args.json:
ui.write_json(entries)
elif entries:
show_entries(entries, with_color=True, with_size=self.args.size)
return 0
return 0
def run(self):
if self.args.tree and self.args.json:
raise DvcException("Cannot use --tree and --json options together.")
try:
if self.args.tree:
return self._show_tree()
return self._show_list()
except FileNotFoundError:
logger.exception("")
return 1
@@ -85,6 +211,19 @@ def add_parser(subparsers, parent_parser):
action="store_true",
help="Recursively list files.",
)
list_parser.add_argument(
"-T",
"--tree",
action="store_true",
help="Recurse into directories as a tree.",
)
list_parser.add_argument(
"-L",
"--level",
metavar="depth",
type=int,
help="Limit the depth of recursion.",
)
list_parser.add_argument(
"--dvc-only", action="store_true", help="Show only DVC outputs."
)
@@ -111,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,
@@ -123,6 +262,12 @@ def add_parser(subparsers, parent_parser):
),
)
list_parser.add_argument("--size", action="store_true", help="Show sizes.")
list_parser.add_argument(
"--show-hash",
help="Display hash value for each item.",
action="store_true",
default=False,
)
list_parser.add_argument(
"path",
nargs="?",
+3 -1
View File
@@ -32,7 +32,9 @@ class LsColors:
if entry.get("isexec", False):
return self._format(text, code="ex")
_, ext = os.path.splitext(text)
stem, ext = os.path.splitext(text)
if not ext and stem.startswith("."):
ext = stem
return self._format(text, ext=ext)
def _format(self, text, code=None, ext=None):
+36 -4
View File
@@ -3,26 +3,45 @@ from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import DictAction, append_doc_link
from dvc.log import logger
from .ls import show_entries
from .ls import show_entries, show_tree
logger = logger.getChild(__name__)
class CmdListUrl(CmdBaseNoRepo):
def run(self):
from dvc.config import Config
def _show_tree(self, config):
from dvc.fs import parse_external_url
from dvc.repo.ls import _ls_tree
fs, fs_path = parse_external_url(
self.args.url, fs_config=self.args.fs_config, config=config
)
entries = _ls_tree(fs, fs_path, maxdepth=self.args.level)
show_tree(entries, with_color=True, with_size=self.args.size)
return 0
def _show_list(self, config):
from dvc.repo import Repo
entries = Repo.ls_url(
self.args.url,
recursive=self.args.recursive,
maxdepth=self.args.level,
fs_config=self.args.fs_config,
config=Config.from_cwd(),
config=config,
)
if entries:
show_entries(entries, with_color=True, with_size=self.args.size)
return 0
def run(self):
from dvc.config import Config
config = Config.from_cwd()
if self.args.tree:
return self._show_tree(config=config)
return self._show_list(config=config)
def add_parser(subparsers, parent_parser):
LS_HELP = "List directory contents from URL."
@@ -40,6 +59,19 @@ def add_parser(subparsers, parent_parser):
lsurl_parser.add_argument(
"-R", "--recursive", action="store_true", help="Recursively list files."
)
lsurl_parser.add_argument(
"-T",
"--tree",
action="store_true",
help="Recurse into directories as a tree.",
)
lsurl_parser.add_argument(
"-L",
"--level",
metavar="depth",
type=int,
help="Limit the depth of recursion.",
)
lsurl_parser.add_argument("--size", action="store_true", help="Show sizes.")
lsurl_parser.add_argument(
"--fs-config",
+1 -1
View File
@@ -72,7 +72,7 @@ class CmdPlots(CmdBase):
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
return html_template_path
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
def run(self) -> int: # noqa: C901, PLR0911
from pathlib import Path
from dvc.render.match import match_defs_renderers
+22 -6
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
@@ -110,8 +110,16 @@ class CmdRemoteDefault(CmdRemote):
class CmdRemoteList(CmdRemote):
def run(self):
conf = self.config.read(self.args.level)
default_remote = conf["core"].get("remote")
for name, remote_conf in conf["remote"].items():
ui.write(name, remote_conf["url"], sep="\t")
if name == default_remote:
text = f"{name}\t{remote_conf['url']}\t(default)"
color = "green"
else:
text = f"{name}\t{remote_conf['url']}"
color = ""
ui.write(ui.rich_text(text, style=color), styled=True)
return 0
@@ -202,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",
@@ -220,8 +230,12 @@ 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("option", help="Name of the option to modify.")
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."
).complete = completion.REMOTE_CONFIG_VARS
remote_modify_parser.add_argument(
"value", nargs="?", help="(optional) Value of the option."
)
@@ -252,7 +266,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 -4
View File
@@ -117,10 +117,7 @@ and then the stage name name.
"--pull",
action="store_true",
default=False,
help=(
"Try automatically pulling missing cache for outputs restored "
"from the run-cache."
),
help="Try automatically pulling missing data.",
)
repro_parser.add_argument(
"--allow-missing",
+1 -1
View File
@@ -68,7 +68,7 @@ def prepare_stages_data(
class CmdStageList(CmdBase):
def _get_stages(self) -> Iterable["Stage"]:
if self.args.all:
stages: list["Stage"] = self.repo.index.stages
stages: list[Stage] = self.repo.index.stages
logger.trace("%d no. of stages found", len(stages))
return stages
+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)
+21 -6
View File
@@ -13,17 +13,21 @@ logger = logger.getChild(__name__)
class CmdStudioLogin(CmdConfig):
def run(self):
from dvc_studio_client.auth import StudioAuthError, get_access_token
from dvc.env import DVC_STUDIO_URL
from dvc.ui import ui
from dvc.utils.studio import STUDIO_URL
name = self.args.name
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
scopes = self.args.scopes
from dvc_studio_client.auth import StudioAuthError, get_access_token
studio = self.config.get("studio", {})
name = self.args.name
hostname = (
self.args.hostname
or os.environ.get(DVC_STUDIO_URL)
or studio.get("url")
or STUDIO_URL
)
scopes = self.args.scopes
if studio.get("url", hostname) == hostname and "token" in studio:
raise DvcException(
"Token already exists. "
@@ -46,6 +50,15 @@ class CmdStudioLogin(CmdConfig):
self.save_config(hostname, access_token)
if not self.config["exp"].get("auto_push", True):
from dvc.ui import ui
ui.warn(
"exp.auto_push is disabled. \n"
"Enable with 'dvc config exp.auto_push true' "
"to automatically push experiments to Studio."
)
config_path = self.config.files["global"]
ui.write(f"Authentication complete. Saved token to {config_path}.")
return 0
@@ -54,6 +67,8 @@ class CmdStudioLogin(CmdConfig):
with self.config.edit("global") as conf:
conf["studio"]["token"] = token
conf["studio"]["url"] = hostname
if "auto_push" not in conf["exp"]:
conf["exp"]["auto_push"] = True
class CmdStudioLogout(CmdConfig):
+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",
+3 -5
View File
@@ -88,12 +88,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
return list(it)
@overload
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None:
...
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None: ...
@overload
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None:
...
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None: ...
def __setitem__(self, item, value) -> None:
it = value
@@ -162,7 +160,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
if key not in keys:
self.add_column(key)
row: list["CellT"] = [
row: list[CellT] = [
with_value(d.get(key), self._fill_value) for key in self.keys()
]
self.append(row)
+13
View File
@@ -1,5 +1,8 @@
"""DVC config objects."""
import ntpath
import os
import posixpath
import re
from contextlib import contextmanager
from functools import partial
@@ -136,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]:
@@ -267,12 +271,17 @@ class Config(dict):
if re.match(r"\w+://", path):
return path
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
return path
if os.path.isabs(path):
return path
# on windows convert slashes to backslashes
# to have path compatible with abs_conf_dir
if os.path.sep == "\\" and "/" in path:
if path.startswith("/"):
path = path.replace("/", "\\\\", 1)
path = path.replace("/", "\\")
expanded = os.path.expanduser(path)
@@ -304,6 +313,9 @@ class Config(dict):
if os.path.expanduser(path) != path:
return localfs.as_posix(path)
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
return path
if isinstance(path, RelPath) or not os.path.isabs(path):
path = relpath(path, conf_dir)
return localfs.as_posix(path)
@@ -329,6 +341,7 @@ class Config(dict):
"gdrive_user_credentials_file": func,
"gdrive_service_account_json_file_path": func,
"credentialpath": func,
"configpath": func,
"keyfile": func,
"cert_path": func,
"key_path": func,
+205 -145
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(
@@ -55,23 +60,27 @@ def Choices(*choices): # noqa: N802
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
def _get_schema_from_url(url: str) -> str:
parsed = urlparse(url)
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
return ""
if not parsed.netloc:
return ""
return parsed.scheme
def ByUrl(mapping): # noqa: N802
schemas = walk_values(Schema, mapping)
schemas: dict[str, Schema] = walk_values(Schema, mapping)
def validate(data):
if "url" not in data:
raise Invalid("expected 'url'")
parsed = urlparse(data["url"])
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
return schemas[""](data)
if not parsed.netloc:
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
return schemas[parsed.scheme](data)
scheme = _get_schema_from_url(data["url"])
if scheme not in schemas:
raise Invalid(f"Unsupported URL type {scheme}://")
return schemas[scheme](data)
return validate
@@ -112,17 +121,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 +163,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,145 +293,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,
"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,
**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, **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: {
@@ -329,13 +353,15 @@ 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,
},
"parsing": {
"bool": All(Lower, Choices("store_true", "boolean_optional")),
@@ -362,3 +388,37 @@ 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 = f"{path}.{k}" if path else k
if isinstance(v, dict):
yield from config_vars_for_completion(v, keypath)
else:
yield keypath
def contextual_config_vars_for_completion(config) -> "Iterator[str]":
yield from config_vars_for_completion()
for name, data in config["remote"].items():
if "url" not in data:
continue
scheme = _get_schema_from_url(data["url"])
if scheme not in REMOTE_SCHEMAS:
continue
schema = REMOTE_SCHEMAS[scheme]
if schema:
yield from config_vars_for_completion(schema, f"remote.{name}") # type: ignore[arg-type]
db_schema = SCHEMA["db"][str] # type: ignore[index]
for name in config["db"]:
yield from config_vars_for_completion(db_schema, f"db.{name}") # type: ignore[arg-type]
+8 -8
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
@@ -44,10 +44,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
popen = subprocess.Popen(
popen = subprocess.Popen( # noqa: S603
args,
close_fds=True,
shell=False, # noqa: S603
shell=False,
startupinfo=startupinfo,
creationflags=creationflags,
**kwargs,
@@ -149,7 +149,7 @@ def _spawn(
env: Optional[Mapping[str, str]] = None,
output_file: Optional[str] = None,
) -> int:
file: "AbstractContextManager[Any]" = nullcontext()
file: AbstractContextManager[Any] = nullcontext()
kwargs = {}
if output_file:
file = open(output_file, "ab") # noqa: SIM115
@@ -164,7 +164,7 @@ def _spawn(
return _detached_subprocess(executable + args, env=env, **kwargs)
def daemonize(args: list[str], executable: Union[None, str, list[str]] = None) -> None:
def daemonize(args: list[str], executable: Union[str, list[str], None] = None) -> None:
if os.name not in ("posix", "nt"):
return
+13 -13
View File
@@ -104,21 +104,21 @@ class AsciiCanvas:
if dx == 0:
y = y0
else:
y = y0 + int(round((x - x0) * dy / float(dx)))
y = y0 + round((x - x0) * dy / float(dx))
self.point(x, y, char)
elif y0 < y1:
for y in range(y0, y1 + 1):
if dy == 0:
x = x0
else:
x = x0 + int(round((y - y0) * dx / float(dy)))
x = x0 + round((y - y0) * dx / float(dy))
self.point(x, y, char)
else:
for y in range(y1, y0 + 1):
if dy == 0:
x = x0
else:
x = x1 + int(round((y - y1) * dx / float(dy)))
x = x1 + round((y - y1) * dx / float(dy))
self.point(x, y, char)
def text(self, x, y, text):
@@ -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()
@@ -258,8 +258,8 @@ def draw(vertices, edges):
maxx = max(Xs)
maxy = max(Ys)
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
canvas_lines = int(round(maxy - miny))
canvas_cols = math.ceil(math.ceil(maxx) - math.floor(minx)) + 1
canvas_lines = round(maxy - miny)
canvas = AsciiCanvas(canvas_cols, canvas_lines)
@@ -270,10 +270,10 @@ def draw(vertices, edges):
start = edge.view._pts[index - 1]
end = edge.view._pts[index]
start_x = int(round(start[0] - minx))
start_y = int(round(start[1] - miny))
end_x = int(round(end[0] - minx))
end_y = int(round(end[1] - miny))
start_x = round(start[0] - minx)
start_y = round(start[1] - miny)
end_x = round(end[0] - minx)
end_y = round(end[1] - miny)
assert start_x >= 0
assert start_y >= 0
@@ -288,12 +288,12 @@ def draw(vertices, edges):
y = vertex.view.xy[1]
canvas.box(
int(round(x - minx)),
int(round(y - miny)),
round(x - minx),
round(y - miny),
vertex.view.w,
vertex.view.h,
)
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
canvas.text(round(x - minx) + 1, round(y - miny) + 1, vertex.data)
return canvas.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
@@ -45,7 +45,7 @@ class Dependency(Output):
self.fs_path = self.fs.version_path(self.fs_path, self.meta.version_id)
def download(self, to, jobs=None):
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
return fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
def save(self):
super().save()
+13 -6
View File
@@ -22,9 +22,12 @@ def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
from dvc.ui import ui
from dvc.ui._rich_progress import DbDownloadProgress
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
console=ui.error_console,
) as progress:
with (
log_durations(logger.debug, f"Saving to {to}"),
DbDownloadProgress(
console=ui.error_console,
) as progress,
):
task = progress.add_task("Saving", total=None, output=to)
yield lambda n: progress.advance(task, advance=n)
progress.update(task, description="Saved", total=0)
@@ -34,13 +37,17 @@ class AbstractDependency(Dependency):
"""Dependency without workspace/fs/fs_path"""
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
self.repo: "Repo" = stage.repo
self.repo: Repo = stage.repo
self.stage = stage
self.fs = None
self.fs_path = None
self.def_path = None # type: ignore[assignment]
self.info = info or {}
@property
def is_in_repo(self):
return False
class DbDependency(AbstractDependency):
PARAM_CONNECTION = "connection"
@@ -67,8 +74,8 @@ class DbDependency(AbstractDependency):
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
def __repr__(self):
return "{}:{}".format(
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
return "{}: {}".format(
self.__class__.__name__, "".join(f"{k}={v}" for k, v in self.info.items())
)
def __str__(self):
+29 -2
View File
@@ -9,7 +9,9 @@ from .base import Dependency
if TYPE_CHECKING:
from dvc.fs import DVCFileSystem
from dvc.output import Output
from dvc.stage import Stage
from dvc_data.hashfile.hash_info import HashInfo
class RepoDependency(Dependency):
@@ -35,7 +37,7 @@ class RepoDependency(Dependency):
super().__init__(stage, *args, **kwargs)
self.fs = self._make_fs()
self.fs_path = as_posix(self.def_path)
self.fs_path = as_posix(self.fs.normpath(self.def_path))
def _parse_path(self, fs, fs_path): # noqa: ARG002
return None
@@ -91,6 +93,27 @@ class RepoDependency(Dependency):
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
}
def download(self, to: "Output", jobs: Optional[int] = None):
from dvc.fs import LocalFileSystem
files = super().download(to=to, jobs=jobs)
if not isinstance(to.fs, LocalFileSystem):
return
hashes: list[tuple[str, HashInfo, dict[str, Any]]] = []
for src_path, dest_path, maybe_info in files:
try:
info = maybe_info or self.fs.info(src_path)
hash_info = info["dvc_info"]["entry"].hash_info
dest_info = to.fs.info(dest_path)
except (KeyError, AttributeError):
# If no hash info found, just keep going and output will be hashed later
continue
if hash_info:
hashes.append((dest_path, hash_info, dest_info))
cache = to.cache if to.use_cache else to.local_cache
cache.state.save_many(hashes, to.fs)
def update(self, rev: Optional[str] = None):
if rev:
self.def_repo[self.PARAM_REV] = rev
@@ -123,11 +146,15 @@ class RepoDependency(Dependency):
else:
config = Config.load_file(conf)
# Setup config to the new DVCFileSystem to use the remote repo, but rely on the
# 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.
config["cache"] = self.repo.config["cache"]
config["cache"]["dir"] = self.repo.cache.local_cache_dir
return DVCFileSystem(
url=self.def_repo[self.PARAM_URL],
repo=self.def_repo[self.PARAM_URL],
rev=rev or self._get_rev(locked=locked),
subrepos=True,
config=config,
+22 -4
View File
@@ -1,4 +1,5 @@
import os
from typing import Optional
import platformdirs
@@ -20,7 +21,24 @@ def global_config_dir():
)
def site_cache_dir():
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
APPNAME, APPAUTHOR, opinion=True
)
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.
# as the following path is considered insecure.
# For details, see: https://github.com/tox-dev/platformdirs/pull/239
# FIXME: keeping the old behavior temporarily to avoid dependency conflict.
# In the future, consider migrating to a more secure directory.
return f"/var/tmp/{APPNAME}" # noqa: S108
return platformdirs.site_cache_dir(APPNAME, APPAUTHOR, opinion=True)
+87 -47
View File
@@ -68,10 +68,9 @@ def is_git_ignored(repo, path):
def check_dvcfile_path(repo, path):
if not is_valid_filename(path):
raise StageFileBadNameError(
"bad DVC file name '{}'. DVC files should be named "
"'{}' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
relpath(path), PROJECT_FILE, os.path.basename(path)
)
f"bad DVC file name '{relpath(path)}'. DVC files should be named "
f"'{PROJECT_FILE}' or have a '.dvc' suffix "
f"(e.g. '{os.path.basename(path)}.dvc')."
)
if is_git_ignored(repo, path):
@@ -163,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
@@ -199,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()
@@ -229,58 +238,79 @@ 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:
datasets: list[dict] = data.setdefault("datasets", [])
parsed = self.datasets if data else []
raw = data.setdefault("datasets", [])
loc = next(
(i for i, ds in enumerate(datasets) if ds["name"] == dataset["name"]),
(i for i, ds in enumerate(parsed) if ds["name"] == dataset["name"]),
None,
)
if loc is not None:
apply_diff(dataset, datasets[loc])
datasets[loc] = dataset
if raw[loc] != parsed[loc]:
raise ParametrizedDumpError(
"cannot update a parametrized dataset entry"
)
apply_diff(dataset, raw[loc])
raw[loc] = dataset
else:
datasets.append(dataset)
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)
@@ -308,29 +338,29 @@ class ProjectFile(FileMixin):
return self.LOADER(self, self.contents, self.lockfile_contents)
@property
def metrics(self) -> list[str]:
return self.contents.get("metrics", [])
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
return self.resolver.resolve_artifacts()
@property
def plots(self) -> Any:
return self.contents.get("plots", {})
def metrics(self) -> list[str]:
return self.resolver.resolve_metrics()
@property
def params(self) -> list[str]:
return self.contents.get("params", [])
return self.resolver.resolve_params()
@property
def plots(self) -> list[Any]:
return self.resolver.resolve_plots()
@property
def datasets(self) -> list[dict[str, Any]]:
return self.contents.get("datasets", [])
return self.resolver.resolve_datasets()
@property
def datasets_lock(self) -> list[dict[str, Any]]:
return self.lockfile_contents.get("datasets", [])
@property
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
return self.contents.get("artifacts", {})
def remove(self, force=False):
if not force:
logger.warning("Cannot remove pipeline file.")
@@ -394,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"})
@@ -404,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
+1
View File
@@ -19,3 +19,4 @@ DVC_STUDIO_URL = "DVC_STUDIO_URL"
DVC_SQLALCHEMY_ECHO = "DVC_SQLALCHEMY_ECHO"
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
DVC_STAGE = "DVC_STAGE"
+9 -9
View File
@@ -1,4 +1,5 @@
"""Exceptions raised by the dvc."""
import errno
from typing import TYPE_CHECKING, Optional
@@ -8,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):
@@ -132,11 +133,11 @@ class MoveNotDataSourceError(DvcException):
def __init__(self, path):
msg = (
"move is not permitted for stages that are not data sources. "
"You need to either move '{path}' to a new location and edit "
"it by hand, or remove '{path}' and create a new one at the "
f"You need to either move {path!r} to a new location and edit "
f"it by hand, or remove {path!r} and create a new one at the "
"desired location."
)
super().__init__(msg.format(path=path))
super().__init__(msg)
class NotDvcRepoError(DvcException):
@@ -241,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 "
@@ -270,9 +271,8 @@ class NoOutputInExternalRepoError(DvcException):
from dvc.utils import relpath
super().__init__(
"Output '{}' not found in target repository '{}'".format(
relpath(path, external_repo_path), external_repo_url
)
f"Output {relpath(path, external_repo_path)!r} "
f"not found in target repository '{external_repo_url}'"
)
+19 -8
View File
@@ -1,10 +1,11 @@
import glob
from itertools import repeat
from typing import Optional
from urllib.parse import urlparse
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
from dvc.config import ConfigError as RepoConfigError
from dvc.config_schema import SCHEMA, Invalid
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
# pylint: disable=unused-import
from dvc_objects.fs import ( # noqa: F401
@@ -19,7 +20,7 @@ from dvc_objects.fs import ( # noqa: F401
system,
utils,
)
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TCH001
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TC001
from dvc_objects.fs.errors import ( # noqa: F401
AuthError,
ConfigError,
@@ -47,12 +48,24 @@ known_implementations.update(
def download(
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
) -> int:
) -> list[tuple[str, str, Optional[dict]]]:
from dvc.scm import lfs_prefetch
from .callbacks import TqdmCallback
with TqdmCallback(desc=f"Downloading {fs.name(fs_path)}", unit="files") as cb:
if isinstance(fs, DVCFileSystem):
lfs_prefetch(
fs,
[
f"{fs.normpath(glob.escape(fs_path))}/**"
if fs.isdir(fs_path)
else glob.escape(fs_path)
],
)
if not glob.has_magic(fs_path):
return fs._get(fs_path, to, batch_size=jobs, callback=cb)
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
# download atomic and avoids fsspec glob/regex path expansion.
if fs.isdir(fs_path):
@@ -61,7 +74,7 @@ def download(
]
if not from_infos:
localfs.makedirs(to, exist_ok=True)
return 0
return []
to_infos = [
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
]
@@ -69,12 +82,10 @@ def download(
from_infos = [fs_path]
to_infos = [to]
if isinstance(fs, DVCFileSystem):
lfs_prefetch(fs, from_infos)
cb.set_size(len(from_infos))
jobs = jobs or fs.jobs
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
return len(to_infos)
return list(zip(from_infos, to_infos, repeat(None)))
def parse_external_url(url, fs_config=None, config=None):
+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,
+190 -16
View File
@@ -4,22 +4,28 @@ import ntpath
import os
import posixpath
import threading
from collections import deque
from contextlib import ExitStack, suppress
from collections import defaultdict, deque
from contextlib import ExitStack, nullcontext, suppress
from glob import has_magic
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
from fsspec.spec import AbstractFileSystem
from fsspec.spec import DEFAULT_CALLBACK, AbstractFileSystem
from funcy import wrap_with
from dvc.log import logger
from dvc_objects.fs.base import FileSystem
from dvc.utils.threadpool import ThreadPoolExecutor
from dvc_objects.fs.base import AnyFSPath, FileSystem
from .data import DataFileSystem
if TYPE_CHECKING:
from contextlib import AbstractContextManager
from dvc.repo import Repo
from dvc.types import DictStrAny, StrPath
from .callbacks import Callback
logger = logger.getChild(__name__)
RepoFactory = Union[Callable[..., "Repo"], type["Repo"]]
@@ -77,11 +83,10 @@ class _DVCFileSystem(AbstractFileSystem):
cachable = False
root_marker = "/"
def __init__( # noqa: PLR0913
def __init__(
self,
url: Optional[str] = None,
repo: Union["Repo", os.PathLike[str], str, None] = None,
rev: Optional[str] = None,
repo: Optional["Repo"] = None,
subrepos: bool = False,
repo_factory: Optional[RepoFactory] = None,
fo: Optional[str] = None,
@@ -95,7 +100,8 @@ class _DVCFileSystem(AbstractFileSystem):
"""DVC + git-tracked files fs.
Args:
path (str, optional): URL or path to a DVC/Git repository.
repo (str | os.PathLike[str] | Repo, optional): A url or a path to a DVC/Git
repository, or a `Repo` instance.
Defaults to a DVC repository in the current working directory.
Both HTTP and SSH protocols are supported for remote Git repos
(e.g. [user@]server:project.git).
@@ -105,7 +111,6 @@ class _DVCFileSystem(AbstractFileSystem):
In case of a local repository, if rev is unspecified, it will
default to the working directory.
If the repo is not a Git repo, this option is ignored.
repo (:obj:`Repo`, optional): `Repo` instance.
subrepos (bool): traverse to subrepos.
By default, it ignores subrepos.
repo_factory (callable): A function to initialize subrepo with.
@@ -130,13 +135,23 @@ class _DVCFileSystem(AbstractFileSystem):
... rev="main",
... )
"""
from dvc.repo import Repo
# kwargs.get("url") is for maintaining backward compatibility
repo = repo or fo or kwargs.get("url")
if isinstance(repo, Repo):
self._repo: Optional[Repo] = repo
url = None
else:
self._repo = None
url = os.fspath(repo) if repo else None
super().__init__()
self._repo = repo
self._repo_factory = repo_factory
self._traverse_subrepos = subrepos
self._repo_stack = ExitStack()
self._repo_kwargs = {
"url": url if url is not None else fo,
"url": url,
"rev": rev,
"subrepos": subrepos,
"config": config,
@@ -360,7 +375,7 @@ class _DVCFileSystem(AbstractFileSystem):
except FileNotFoundError:
return False
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901, PLR0912
key = self._get_key_from_relative(path)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
@@ -429,7 +444,7 @@ class _DVCFileSystem(AbstractFileSystem):
ignore_subrepos = kwargs.get("ignore_subrepos", True)
return self._info(key, path, ignore_subrepos=ignore_subrepos)
def _info( # noqa: C901, PLR0912
def _info( # noqa: C901
self, key, path, ignore_subrepos=True, check_ignored=True
):
repo, dvc_fs, subkey = self._get_subrepo_info(key)
@@ -474,9 +489,128 @@ class _DVCFileSystem(AbstractFileSystem):
info["name"] = path
return info
def get(
self,
rpath,
lpath,
recursive=False,
callback=DEFAULT_CALLBACK,
maxdepth=None,
batch_size=None,
**kwargs,
):
self._get(
rpath,
lpath,
recursive=recursive,
callback=callback,
maxdepth=maxdepth,
batch_size=batch_size,
**kwargs,
)
def _get( # noqa: C901, PLR0912, PLR0915
self,
rpath,
lpath,
recursive=False,
callback=DEFAULT_CALLBACK,
maxdepth=None,
batch_size=None,
**kwargs,
) -> list[tuple[str, str, Optional[dict]]]:
if (
isinstance(rpath, list)
or isinstance(lpath, list)
or has_magic(rpath)
or not self.exists(rpath)
or not recursive
):
super().get(
rpath,
lpath,
recursive=recursive,
callback=callback,
maxdepth=maxdepth,
**kwargs,
)
return []
if os.path.isdir(lpath) or lpath.endswith(os.path.sep):
lpath = self.join(lpath, os.path.basename(rpath))
if self.isfile(rpath):
with callback.branched(rpath, lpath) as child:
self.get_file(rpath, lpath, callback=child, **kwargs)
return [(rpath, lpath, None)]
result: list[tuple[str, str, Optional[dict]]] = []
_dirs: list[str] = []
_files: dict[FileSystem, list[tuple[str, str, Optional[dict]]]]
_files = defaultdict(list)
for root, dirs, files in self.walk(rpath, maxdepth=maxdepth, detail=True):
if files:
callback.set_size((callback.size or 0) + len(files))
parts = self.relparts(root, rpath)
if parts in ((os.curdir,), ("",)):
parts = ()
dest_root = os.path.join(lpath, *parts)
if not maxdepth or len(parts) < maxdepth - 1:
_dirs.extend(f"{dest_root}{os.path.sep}{d}" for d in dirs)
key = self._get_key_from_relative(root)
_, dvc_fs, _ = self._get_subrepo_info(key)
for name, info in files.items():
dvc_info = info.get("dvc_info")
fs_info = info.get("fs_info")
if dvc_fs and dvc_info and not fs_info:
fs = dvc_fs
fs_path = dvc_info["name"]
else:
fs = self.repo.fs
fs_path = fs_info["name"]
src_path = f"{root}{self.sep}{name}"
dest_path = f"{dest_root}{os.path.sep}{name}"
_files[fs].append((fs_path, dest_path, dvc_info))
result.append((src_path, dest_path, info))
os.makedirs(lpath, exist_ok=True)
for d in _dirs:
os.makedirs(d, exist_ok=True)
def get_file(arg: tuple[FileSystem, tuple[str, str, Optional[dict]]]):
fs, (src, dest, info) = arg
kw = kwargs
if isinstance(fs, DataFileSystem):
kw = kw | {"info": info}
with callback.branched(src, dest) as child:
fs.get_file(src, dest, callback=child, **kw)
if batch_size == 1:
ctx: AbstractContextManager = nullcontext()
map_fn: Callable = map
else:
ctx = ThreadPoolExecutor(max_workers=batch_size)
map_fn = ctx.imap_unordered
with ctx:
it = ((fs, f) for fs, files in _files.items() for f in files)
deque(callback.wrap(map_fn(get_file, it)), maxlen=0)
return result
def get_file(self, rpath, lpath, **kwargs):
dvc_info = kwargs.pop("info", {}).pop("dvc_info", None)
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
dirpath = os.path.dirname(lpath)
if dirpath:
# makedirs raises error if the string is empty
os.makedirs(dirpath, exist_ok=True)
try:
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
except FileNotFoundError:
@@ -485,7 +619,7 @@ class _DVCFileSystem(AbstractFileSystem):
raise
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
return dvc_fs.get_file(dvc_path, lpath, info=dvc_info, **kwargs)
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
if maxdepth is not None:
@@ -553,6 +687,45 @@ class DVCFileSystem(FileSystem):
def getcwd(self):
return self.fs.getcwd()
def _get(
self,
from_info: Union[AnyFSPath, list[AnyFSPath]],
to_info: Union[AnyFSPath, list[AnyFSPath]],
callback: "Callback" = DEFAULT_CALLBACK,
recursive: bool = False,
batch_size: Optional[int] = None,
**kwargs,
) -> list[tuple[str, str, Optional[dict]]]:
# FileSystem.get is non-recursive by default if arguments are lists
# otherwise, it's recursive.
recursive = not (isinstance(from_info, list) and isinstance(to_info, list))
return self.fs._get(
from_info,
to_info,
callback=callback,
recursive=recursive,
batch_size=batch_size,
**kwargs,
)
def get(
self,
from_info: Union[AnyFSPath, list[AnyFSPath]],
to_info: Union[AnyFSPath, list[AnyFSPath]],
callback: "Callback" = DEFAULT_CALLBACK,
recursive: bool = False,
batch_size: Optional[int] = None,
**kwargs,
) -> None:
self._get(
from_info,
to_info,
callback=callback,
batch_size=batch_size,
recursive=recursive,
**kwargs,
)
@property
def fsid(self) -> str:
return self.fs.fsid
@@ -569,9 +742,10 @@ class DVCFileSystem(FileSystem):
return self.fs.repo_url
def from_os_path(self, path: str) -> str:
if os.path.isabs(path):
if os.path.isabs(path) or (
os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path
):
path = os.path.relpath(path, self.repo.root_dir)
return as_posix(path)
def close(self):
+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):
+213 -100
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,24 +503,23 @@ 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
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
@@ -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):
@@ -123,7 +124,7 @@ def _get_supported_remotes():
if not fs_cls.get_missing_deps():
dependencies = []
for requirement in fs_cls.REQUIRES:
dependencies.append(
dependencies.append( # noqa: PERF401
f"{requirement} = {importlib_metadata.version(requirement)}"
)
+47 -20
View File
@@ -2,12 +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
@@ -87,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
@@ -100,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:
@@ -149,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
@@ -172,11 +189,21 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
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):
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
try:
super().lock(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
@@ -185,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)
+16 -4
View File
@@ -5,6 +5,8 @@ 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
@@ -36,7 +38,7 @@ def add_logging_level(level_name, level_num, method_name=None):
self._log(level_num, message, args, **kwargs)
def log_to_root(message, *args, **kwargs):
logging.log(level_num, message, *args, **kwargs)
logging.log(level_num, message, *args, **kwargs) # noqa: LOG015
# getLevelName resolves the numeric log level if already defined,
# otherwise returns a string
@@ -53,7 +55,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 +176,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:
+54 -29
View File
@@ -45,7 +45,7 @@ if TYPE_CHECKING:
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__)
@@ -267,9 +267,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):
@@ -390,7 +390,7 @@ class Output:
self.can_push = push
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj: Optional["HashFile"] = None
self.obj: Optional[HashFile] = None
self.remote = remote
@@ -431,7 +431,7 @@ class Output:
self.meta.nfiles = len(self.files)
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
self.meta.remote = first(f.get("remote") for f in self.files)
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
elif self.meta.nfiles or (self.hash_info and self.hash_info.isdir):
self.meta.isdir = True
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
md5 = getattr(self.meta, "md5", None)
@@ -542,7 +542,9 @@ class Output:
desc=f"Collecting files and computing hashes in {self}",
disable=no_progress_bar,
) as pb:
return build(*args, callback=pb.as_callback(), **kwargs)
kwargs["callback"] = pb.as_callback()
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
return build(*args, **kwargs)
def _get_hash_meta(self):
if self.use_cache:
@@ -730,6 +732,7 @@ class Output:
from dvc_data.hashfile.checkout import LinkError, PromptError
kwargs.setdefault("ignore", self.dvcignore)
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
try:
return checkout(*args, **kwargs)
except PromptError as exc:
@@ -749,11 +752,9 @@ class Output:
granular = (
self.is_dir_checksum and filter_info and filter_info != self.fs_path
)
# NOTE: trying to use hardlink during transfer only if we will be
# relinking later
hardlink = relink
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
if granular:
obj = self._commit_granular_dir(filter_info, hardlink)
obj = self._commit_granular_dir(filter_info, hardlink=hardlink)
else:
staging, _, obj = self._build(
self.cache,
@@ -786,6 +787,7 @@ class Output:
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=cb,
old=obj,
)
self.set_exec()
@@ -902,7 +904,7 @@ class Output:
def get_obj(
self, filter_info: Optional[str] = None, **kwargs
) -> Optional["HashFile"]:
obj: Optional["HashFile"] = None
obj: Optional[HashFile] = None
if self.obj:
obj = self.obj
elif self.files:
@@ -1158,7 +1160,7 @@ class Output:
logger.warning(msg)
return {}
obj: Optional["HashFile"]
obj: Optional[HashFile]
if self.is_dir_checksum:
obj = self._collect_used_dir_cache(**kwargs)
else:
@@ -1201,8 +1203,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:
@@ -1353,7 +1356,7 @@ class Output:
cache = self.cache if self.use_cache else self.local_cache
assert isinstance(cache, HashFileDB)
new: "HashFile"
new: HashFile
try:
assert self.hash_name
staging, meta, obj = self._build(
@@ -1365,7 +1368,7 @@ class Output:
dry_run=not self.use_cache,
)
except FileNotFoundError as exc:
if self.fs_path == path:
if not self.exists:
raise self.DoesNotExistError(self) from exc
if not self.is_dir_checksum:
raise
@@ -1398,12 +1401,14 @@ class Output:
assert staging
assert obj.hash_info
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
with TqdmCallback(desc=f"Adding {self} to cache", unit="file") as cb:
otransfer(
staging,
self.cache,
{obj.hash_info},
hardlink=relink,
hardlink=hardlink,
shallow=False,
callback=cb,
)
@@ -1421,6 +1426,7 @@ class Output:
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=callback,
old=obj,
)
self.set_exec()
return obj
@@ -1447,25 +1453,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)
+92 -3
View File
@@ -21,6 +21,7 @@ from .context import (
VarsAlreadyLoaded,
)
from .interpolate import (
check_expression,
check_recursive_parse_errors,
is_interpolated_string,
recurse,
@@ -38,10 +39,16 @@ if TYPE_CHECKING:
logger = logger.getChild(__name__)
STAGES_KWD = "stages"
VARS_KWD = "vars"
WDIR_KWD = "wdir"
ARTIFACTS_KWD = "artifacts"
DATASETS_KWD = "datasets"
METRICS_KWD = "metrics"
PARAMS_KWD = "params"
PLOTS_KWD = "plots"
STAGES_KWD = "stages"
FOREACH_KWD = "foreach"
MATRIX_KWD = "matrix"
DO_KWD = "do"
@@ -163,6 +170,27 @@ class DataResolver:
for name, definition in stages_data.items()
}
self.artifacts = [
ArtifactDefinition(self, self.context, name, definition, ARTIFACTS_KWD)
for name, definition in d.get(ARTIFACTS_KWD, {}).items()
]
self.datasets = [
TopDefinition(self, self.context, str(i), definition, DATASETS_KWD)
for i, definition in enumerate(d.get(DATASETS_KWD, []))
]
self.metrics = [
TopDefinition(self, self.context, str(i), definition, METRICS_KWD)
for i, definition in enumerate(d.get(METRICS_KWD, []))
]
self.params = [
TopDefinition(self, self.context, str(i), definition, PARAMS_KWD)
for i, definition in enumerate(d.get(PARAMS_KWD, []))
]
self.plots = [
TopDefinition(self, self.context, str(i), definition, PLOTS_KWD)
for i, definition in enumerate(d.get(PLOTS_KWD, []))
]
def resolve_one(self, name: str):
group, key = split_group_name(name)
@@ -186,6 +214,27 @@ class DataResolver:
logger.trace("Resolved dvc.yaml:\n%s", data)
return {STAGES_KWD: data}
# Top-level sections are eagerly evaluated, whereas stages are lazily evaluated,
# one-by-one.
def resolve_artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
d: dict[str, Optional[dict[str, Any]]] = {}
for item in self.artifacts:
d.update(item.resolve())
return d
def resolve_datasets(self) -> list[dict[str, Any]]:
return [item.resolve() for item in self.datasets]
def resolve_metrics(self) -> list[str]:
return [item.resolve() for item in self.metrics]
def resolve_params(self) -> list[str]:
return [item.resolve() for item in self.params]
def resolve_plots(self) -> list[Any]:
return [item.resolve() for item in self.plots]
def has_key(self, key: str):
return self._has_group_and_key(*split_group_name(key))
@@ -514,10 +563,10 @@ class MatrixDefinition:
iterable = self.resolved_iterable
assert isinstance(iterable, Mapping)
ret: dict[str, "DictStrAny"] = {}
ret: dict[str, DictStrAny] = {}
matrix = {key: enumerate(v) for key, v in iterable.items()}
for combination in product(*matrix.values()):
d: "DictStrAny" = {}
d: DictStrAny = {}
fragments: list[str] = []
for k, (i, v) in zip(matrix.keys(), combination):
d[k] = v
@@ -565,3 +614,43 @@ class MatrixDefinition:
return entry.resolve_stage(skip_checks=True)
except ContextError as exc:
format_and_raise(exc, f"stage '{generated}'", self.relpath)
class TopDefinition:
def __init__(
self,
resolver: DataResolver,
context: Context,
name: str,
definition: "Any",
where: str,
):
self.resolver = resolver
self.context = context
self.name = name
self.definition = definition
self.where = where
self.relpath = self.resolver.relpath
def resolve(self):
try:
check_recursive_parse_errors(self.definition)
return self.context.resolve(self.definition)
except (ParseError, ContextError) as exc:
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
class ArtifactDefinition(TopDefinition):
def resolve(self) -> dict[str, Optional[dict[str, Any]]]:
try:
check_expression(self.name)
name = self.context.resolve(self.name)
if not isinstance(name, str):
typ = type(name).__name__
raise ResolveError(
f"failed to resolve '{self.where}.{self.name}'"
f" in '{self.relpath}': expected str, got " + typ
)
except (ParseError, ContextError) as exc:
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
return {name: super().resolve()}
+8 -7
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
@@ -105,7 +105,7 @@ class Meta:
@staticmethod
def update_path(meta: "Meta", path: Union[str, int]):
dpaths = meta.dpaths[:] + [str(path)]
dpaths = [*meta.dpaths, str(path)]
return replace(meta, dpaths=dpaths)
def __str__(self):
@@ -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)
@@ -176,8 +176,9 @@ class Container(Node, ABC):
return value
if isinstance(value, (list, dict)):
assert meta
container = CtxDict if isinstance(value, dict) else CtxList
return container(value, meta=meta)
if isinstance(value, dict):
return CtxDict(value, meta=meta)
return CtxList(value, meta=meta)
msg = f"Unsupported value of type '{type(value).__name__}' in '{meta}'"
raise TypeError(msg)
@@ -271,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(
+10 -6
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):
@@ -27,11 +33,9 @@ def _remove_slash(rule):
def _match_all_level(rule):
if rule[:-1].find("/") >= 0 and not rule.startswith("**/"):
if rule.startswith("/"):
rule = rule[1:]
rule = rule.removeprefix("/")
return False, rule
if rule.startswith("**/"):
rule = rule[3:]
rule = rule.removeprefix("**/")
return True, rule
+2 -1
View File
@@ -1,4 +1,5 @@
"""Manages progress bars for DVC repo."""
import logging
import sys
from threading import RLock
@@ -40,7 +41,7 @@ class Tqdm(tqdm):
"miniters": 1,
}
def __init__( # noqa: PLR0913
def __init__(
self,
iterable=None,
disable=None,
+2 -2
View File
@@ -23,7 +23,7 @@ def _lists(blob: Union[dict, list]) -> Iterable[list]:
if isinstance(blob, list):
yield blob
else:
for _, value in blob.items():
for value in blob.values():
if isinstance(value, dict):
yield from _lists(value)
elif isinstance(value, list):
@@ -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:
+20 -15
View File
@@ -144,10 +144,11 @@ 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
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
from dvc.fs import GitFileSystem, LocalFileSystem
from dvc.lock import LockNoop, make_lock
from dvc.repo.artifacts import Artifacts
from dvc.repo.datasets import Datasets
@@ -161,12 +162,13 @@ class Repo:
self.url = url
self._fs_conf = {"repo_factory": repo_factory}
self._fs = fs or localfs
self._fs = fs or LocalFileSystem()
self._scm = scm
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)
@@ -184,12 +186,12 @@ class Repo:
# used by DVCFileSystem to determine if it should traverse subrepos
self.subrepos = subrepos
self.cloud: "DataCloud" = DataCloud(self)
self.stage: "StageLoad" = StageLoad(self)
self.cloud: DataCloud = DataCloud(self)
self.stage: StageLoad = StageLoad(self)
self.lock: "LockBase"
self.lock: LockBase
self.cache: CacheManager
self.state: "StateBase"
self.state: StateBase
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
self.lock = LockNoop()
self.state = StateNoop()
@@ -204,8 +206,14 @@ 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 (
checksum_jobs := self.config["core"].get("checksum_jobs")
):
self.fs.hash_jobs = checksum_jobs
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
else:
self.lock = LockNoop()
@@ -551,11 +559,7 @@ class Repo:
if out.protocol == "local" and match(fs_path, out.fs_path):
return True
if recursive and out.fs.isin(out.fs_path, fs_path):
return True
return False
return recursive and out.fs.isin(out.fs_path, fs_path)
matched = list(filter(func, outs))
if not matched:
@@ -608,7 +612,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):
@@ -637,10 +641,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)
+14 -3
View File
@@ -171,11 +171,16 @@ def _add_transfer(
stage.dump()
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
def _add(
stage: "Stage",
source: Optional[str] = None,
no_commit: bool = False,
relink: bool = True,
) -> None:
out = stage.outs[0]
path = out.fs.abspath(source) if source else None
try:
stage.add_outs(path, no_commit=no_commit)
stage.add_outs(path, no_commit=no_commit, relink=relink)
except CacheLinkError:
stage.dump()
raise
@@ -194,6 +199,7 @@ def add(
to_remote: bool = False,
remote_jobs: Optional[int] = None,
force: bool = False,
relink: bool = True,
) -> list["Stage"]:
add_targets = find_targets(targets, glob=glob)
if not add_targets:
@@ -224,7 +230,12 @@ def add(
with warn_link_failures() as link_failures:
for source, (stage, output_exists) in progress_iter(stages_with_targets):
try:
_add(stage, source if output_exists else None, no_commit=no_commit)
_add(
stage,
source if output_exists else None,
no_commit=no_commit,
relink=relink,
)
except CacheLinkError:
link_failures.append(stage.relpath)
return stages
+66 -24
View File
@@ -12,7 +12,7 @@ from dvc.exceptions import (
InvalidArgumentError,
)
from dvc.log import logger
from dvc.utils import relpath, resolve_output
from dvc.utils import as_posix, relpath, resolve_output
from dvc.utils.objects import cached_property
from dvc.utils.serialize import modify_yaml
@@ -99,7 +99,7 @@ class Artifacts:
"""Read artifacts from dvc.yaml."""
artifacts: dict[str, dict[str, Artifact]] = {}
for dvcfile, dvcfile_artifacts in self.repo.index._artifacts.items():
dvcyaml = relpath(dvcfile, self.repo.root_dir)
dvcyaml = self.repo.fs.relpath(dvcfile, self.repo.root_dir)
artifacts[dvcyaml] = {}
for name, value in dvcfile_artifacts.items():
try:
@@ -137,18 +137,16 @@ class Artifacts:
assert not (version and stage)
name = _reformat_name(name)
tags: list["GitTag"] = find(
name=name, version=version, stage=stage, scm=self.scm
)
tags: list[GitTag] = find(name=name, version=version, stage=stage, scm=self.scm)
if not tags:
raise ArtifactNotFoundError(name, version=version, stage=stage)
if version or stage:
return tags[-1].target
gto_tags: list["GTOTag"] = sort_versions(parse_tag(tag) for tag in tags)
gto_tags: list[GTOTag] = sort_versions(parse_tag(tag) for tag in tags)
return gto_tags[0].tag.target
def get_path(self, name: str):
"""Return repo fspath for the given artifact."""
@classmethod
def parse_path(cls, name: str) -> tuple[Optional[str], str]:
from gto.constants import SEPARATOR_IN_NAME, fullname_re
name = _reformat_name(name)
@@ -158,13 +156,37 @@ class Artifacts:
dirname = m.group("dirname")
if dirname:
dirname = dirname.rstrip(SEPARATOR_IN_NAME)
dvcyaml = os.path.join(dirname, PROJECT_FILE) if dirname else PROJECT_FILE
artifact_name = m.group("name")
return dirname, m.group("name")
def get_path(self, name: str):
"""Return fspath for the given artifact relative to the git root."""
from dvc.fs import GitFileSystem
dirname, artifact_name = self.parse_path(name)
# `name`/`dirname` are expected to be a git root relative.
# We convert it to dvc-root relative path so that we can read artifacts
# from dvc.yaml file.
# But we return dirname intact, as we want to return a git-root relative path.
# This is useful when reading from `dvcfs` from remote.
fs = self.repo.fs
assert self.scm
if isinstance(fs, GitFileSystem):
scm_root = fs.root_marker
else:
scm_root = self.scm.root_dir
dirparts = posixpath.normpath(dirname).split(posixpath.sep) if dirname else ()
abspath = fs.join(scm_root, *dirparts, PROJECT_FILE)
rela = fs.relpath(abspath, self.repo.root_dir)
try:
artifact = self.read()[dvcyaml][artifact_name]
artifact = self.read()[rela][artifact_name]
except KeyError as exc:
raise ArtifactNotFoundError(name) from exc
return os.path.join(dirname, artifact.path) if dirname else artifact.path
path = posixpath.join(dirname or "", artifact.path)
parts = posixpath.normpath(path).split(posixpath.sep)
return os.path.join(*parts)
def download(
self,
@@ -177,15 +199,28 @@ class Artifacts:
) -> tuple[int, str]:
"""Download the specified artifact."""
from dvc.fs import download as fs_download
from dvc.repo import Repo
logger.debug("Trying to download artifact '%s' via DVC", name)
rev = self.get_rev(name, version=version, stage=stage)
dirname, _ = self.parse_path(name)
with self.repo.switch(rev):
path = self.get_path(name)
root = self.repo.fs.root_marker
_dirname = self.repo.fs.join(root, dirname) if dirname else root
with Repo(_dirname, fs=self.repo.fs, scm=self.repo.scm) as r:
path = r.artifacts.get_path(name)
path = self.repo.fs.join(root, as_posix(path))
path = self.repo.fs.relpath(path, self.repo.root_dir)
# when the `repo` is a subrepo, the path `/subrepo/myart.pkl` for dvcfs
# should be translated as `/myart.pkl`,
# i.e. relative to the root of the subrepo
path = self.repo.fs.join(root, path)
path = self.repo.fs.normpath(path)
out = resolve_output(path, out, force=force)
fs = self.repo.dvcfs
fs_path = fs.from_os_path(path)
count = fs_download(fs, fs_path, os.path.abspath(out), jobs=jobs)
count = len(fs_download(fs, path, os.path.abspath(out), jobs=jobs))
return count, out
@staticmethod
@@ -200,10 +235,9 @@ class Artifacts:
dvc_studio_config: Optional[dict[str, Any]] = None,
**kwargs,
) -> tuple[int, str]:
from dvc_studio_client.model_registry import get_download_uris
from dvc.fs import HTTPFileSystem, generic, localfs
from dvc.fs.callbacks import TqdmCallback
from dvc_studio_client.model_registry import get_download_uris
logger.debug("Trying to download artifact '%s' via studio", name)
out = out or os.getcwd()
@@ -231,7 +265,7 @@ class Artifacts:
from_infos.append(url)
except DvcException:
raise
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise DvcException(
f"Failed to download artifact '{name}' via Studio"
) from exc
@@ -249,7 +283,7 @@ class Artifacts:
return len(to_infos), relpath(localfs.commonpath(to_infos))
@classmethod
def get( # noqa: PLR0913
def get(
cls,
url: str,
name: str,
@@ -278,6 +312,13 @@ class Artifacts:
name = _reformat_name(name)
saved_exc: Optional[Exception] = None
local_dvc_studio_config = Config().get("studio", {})
args_dvc_studio_config = {}
if config and not isinstance(config, dict):
config = Config.load_file(config)
args_dvc_studio_config = config.get("studio", {})
try:
logger.trace("Trying studio-only config")
return cls._download_studio(
@@ -288,14 +329,13 @@ class Artifacts:
out=out,
force=force,
jobs=jobs,
dvc_studio_config=local_dvc_studio_config | args_dvc_studio_config,
)
except FileExistsLocallyError:
raise
except Exception as exc: # noqa: BLE001
saved_exc = exc
if config and not isinstance(config, dict):
config = Config.load_file(config)
with Repo.open(
url=url,
subrepos=True,
@@ -305,7 +345,7 @@ class Artifacts:
remote_config=remote_config,
) as repo:
logger.trace("Trying repo [studio] config")
dvc_studio_config = dict(repo.config.get("studio"))
repo_dvc_studio_config = repo.config.get("studio", {})
try:
return cls._download_studio(
url,
@@ -315,7 +355,9 @@ class Artifacts:
out=out,
force=force,
jobs=jobs,
dvc_studio_config=dvc_studio_config,
dvc_studio_config=local_dvc_studio_config
| repo_dvc_studio_config
| args_dvc_studio_config,
)
except FileExistsLocallyError:
raise
@@ -333,7 +375,7 @@ class Artifacts:
)
except FileExistsLocallyError:
raise
except Exception as exc: # noqa: BLE001
except Exception as exc:
if saved_exc:
logger.exception(str(saved_exc), exc_info=saved_exc.__cause__)
raise DvcException(
+53 -25
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]
@@ -125,10 +138,12 @@ def checkout( # noqa: C901
(StageFileDoesNotExistError, StageFileBadNameError, NoOutputOrStageError),
):
raise CheckoutErrorSuggestGit(target) from exc
raise
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
+377 -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
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,158 @@ 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 _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 view.iteritems(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,
+25 -23
View File
@@ -16,8 +16,7 @@ from dvc.types import StrPath
from dvc_data.hashfile.meta import Meta
if TYPE_CHECKING:
from dql.dataset import DatasetRecord # type: ignore[import]
from dvcx.dataset import DatasetVersion # type: ignore[import]
from datachain.dataset import DatasetRecord, DatasetVersion # type: ignore[import]
from typing_extensions import Self
from dvc.repo import Repo
@@ -30,10 +29,10 @@ def _get_dataset_record(name: str) -> "DatasetRecord":
from dvc.exceptions import DvcException
try:
from dvcx.catalog import get_catalog # type: ignore[import]
from datachain.catalog import get_catalog # type: ignore[import]
except ImportError as exc:
raise DvcException("dvcx is not installed") from exc
raise DvcException("datachain is not installed") from exc
catalog = get_catalog()
return catalog.get_remote_dataset(name)
@@ -77,7 +76,7 @@ class SerDe:
return asdict(self, filter=filter_defaults, value_serializer=value_serializer)
@classmethod
def from_dict(cls: type["Self"], d: dict[str, Any]) -> "Self":
def from_dict(cls, d: dict[str, Any]) -> "Self":
_fields = fields(cast("type[AttrsInstance]", cls))
kwargs = {f.name: d[f.name] for f in _fields if f.name in d}
return cls(**kwargs)
@@ -87,7 +86,7 @@ class SerDe:
class DatasetSpec(SerDe):
name: str
url: str
type: Literal["dvc", "dvcx", "url"]
type: Literal["dvc", "dc", "url"]
@frozen(kw_only=True)
@@ -109,7 +108,7 @@ class DVCDatasetLock(DVCDatasetSpec):
@frozen(kw_only=True)
class DVCXDatasetLock(DatasetSpec):
class DatachainDatasetLock(DatasetSpec):
version: int
created_at: datetime = field(converter=to_datetime)
@@ -161,13 +160,13 @@ class DVCDataset:
@frozen(kw_only=True)
class DVCXDataset:
class DatachainDataset:
manifest_path: str
spec: "DatasetSpec"
lock: "Optional[DVCXDatasetLock]" = field(default=None)
lock: "Optional[DatachainDatasetLock]" = field(default=None)
_invalidated: bool = field(default=False, eq=False, repr=False)
type: ClassVar[Literal["dvcx"]] = "dvcx"
type: ClassVar[Literal["dc"]] = "dc"
@property
def pinned(self) -> bool:
@@ -194,7 +193,7 @@ class DVCXDataset:
name, _version = self.name_version
version = version if version is not None else _version
version_info = _get_dataset_info(name, record=record, version=version)
lock = DVCXDatasetLock(
lock = DatachainDatasetLock(
**self.spec.to_dict(),
version=version_info.version,
created_at=version_info.created_at,
@@ -227,9 +226,9 @@ class URLDataset:
return evolve(self, lock=lock)
Lock = Union[DVCDatasetLock, DVCXDatasetLock, URLDatasetLock]
Lock = Union[DVCDatasetLock, DatachainDatasetLock, URLDatasetLock]
Spec = Union[DatasetSpec, DVCDatasetSpec]
Dataset = Union[DVCDataset, DVCXDataset, URLDataset]
Dataset = Union[DVCDataset, DatachainDataset, URLDataset]
class DatasetNotFoundError(DvcException, KeyError):
@@ -243,7 +242,7 @@ class DatasetNotFoundError(DvcException, KeyError):
class Datasets(Mapping[str, Dataset]):
def __init__(self, repo: "Repo") -> None:
self.repo: "Repo" = repo
self.repo: Repo = repo
def __repr__(self):
return repr(dict(self))
@@ -308,13 +307,13 @@ class Datasets(Mapping[str, Dataset]):
raise ValueError("type should be present in spec")
if typ == "dvc":
return DVCDatasetSpec.from_dict(spec)
if typ in {"dvcx", "url"}:
if typ in {"dc", "url"}:
return DatasetSpec.from_dict(spec)
raise ValueError(f"unknown dataset type: {spec.get('type', '')}")
@staticmethod
def _lock_from_info(lock: Optional[dict[str, Any]]) -> Optional[Lock]:
kl = {"dvc": DVCDatasetLock, "dvcx": DVCXDatasetLock, "url": URLDatasetLock}
kl = {"dvc": DVCDatasetLock, "dc": DatachainDatasetLock, "url": URLDatasetLock}
if lock and (cls := kl.get(lock.get("type", ""))): # type: ignore[assignment]
return cls.from_dict(lock) # type: ignore[attr-defined]
return None
@@ -357,9 +356,9 @@ class Datasets(Mapping[str, Dataset]):
lock=lock,
invalidated=_invalidated,
)
if spec.type == "dvcx":
assert lock is None or isinstance(lock, DVCXDatasetLock)
return DVCXDataset(
if spec.type == "dc":
assert lock is None or isinstance(lock, DatachainDatasetLock)
return DatachainDataset(
manifest_path=manifest_path,
spec=spec,
lock=lock,
@@ -375,7 +374,7 @@ class Datasets(Mapping[str, Dataset]):
manifest_path: StrPath = "dvc.yaml",
**kwargs: Any,
) -> Dataset:
assert type in {"dvc", "dvcx", "url"}
assert type in {"dvc", "dc", "url"}
kwargs.update({"name": name, "url": url, "type": type})
dataset = self._build_dataset(os.path.abspath(manifest_path), kwargs)
dataset = dataset.update(self.repo)
@@ -390,13 +389,16 @@ class Datasets(Mapping[str, Dataset]):
if dataset.type == "url" and (version or kwargs.get("rev")):
raise ValueError("cannot update version/revision for a url")
if dataset.type == "dvcx" and version is not None:
if dataset.type == "dc" and version is not None:
if not isinstance(version, int):
raise TypeError(
f"dvcx version has to be an integer, got {type(version).__name__!r}"
"DataChain dataset version has to be an integer, "
f"got {type(version).__name__!r}"
)
if version < 1:
raise ValueError(f"dvcx version should be >=1, got {version}")
raise ValueError(
f"DataChain dataset version should be >=1, got {version}"
)
new = dataset.update(self.repo, **kwargs)
+7 -1
View File
@@ -26,7 +26,7 @@ def _diff(old, new, data_keys, with_missing=False):
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
from dvc_data.index.diff import diff as idiff
ret: "dict[str, list[dict]]" = {
ret: dict[str, list[dict]] = {
"added": [],
"deleted": [],
"modified": [],
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@ def du(
path: Optional[str] = None,
rev: Optional[str] = None,
summarize: bool = False,
config: Union[None, dict[str, Any], str] = None,
config: Union[dict[str, Any], str, None] = None,
remote: Optional[str] = None,
remote_config: Optional[dict] = None,
):
+3 -3
View File
@@ -119,7 +119,7 @@ class Experiments:
**kwargs,
):
"""Reproduce and checkout a single (standalone) experiment."""
exp_queue: "BaseStashQueue" = (
exp_queue: BaseStashQueue = (
self.tempdir_queue if tmp_dir else self.workspace_queue
)
self.queue_one(exp_queue, **kwargs)
@@ -205,7 +205,7 @@ class Experiments:
Experiment will be derived from the current workspace.
"""
name = kwargs.get("name", None)
name = kwargs.get("name")
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
if name:
@@ -249,7 +249,7 @@ class Experiments:
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
results: dict[str, str] = {}
for _, exp_result in exec_results.items():
for exp_result in exec_results.values():
results.update(exp_result)
return results
+2 -2
View File
@@ -25,7 +25,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
from dvc.repo.checkout import checkout as dvc_checkout
from dvc.scm import RevError, resolve_rev
exps: "Experiments" = repo.experiments
exps: Experiments = repo.experiments
is_stash: bool = False
@@ -57,7 +57,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
exps: "Experiments" = repo.experiments
exps: Experiments = repo.experiments
with exps.apply_stash.preserve_workspace(rev, name=name):
with repo.scm.detach_head(rev, force=True):

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