Compare commits

...
190 Commits
Author SHA1 Message Date
Peter Rowlands 7393ebdcb4 logger: init logging config before colorama 2022-10-04 21:27:24 +03:00
David de la Iglesia Castro cf651c52a1 hydra: Fix append and remove sweeps.
Closes #8376
2022-10-04 19:05:12 +02:00
Ronan Lamy a3f20eb6f4 ls-url: make config and recursive kwarg-only 2022-10-04 14:37:29 +03:00
Ronan Lamy d60edcd88e ls-url: fix doc URL 2022-10-04 14:37:29 +03:00
Ronan Lamy a6cdb9efd2 ls-url: remove redundant tests 2022-10-04 14:37:29 +03:00
Ronan Lamy 4a558edc59 ls-url: remove now unused _ls_url() 2022-10-04 14:37:29 +03:00
Ronan Lamy 8ff92cc7b0 ls-url: refactor tests so they work with all supported plugins.
Add `config` argument to ls_url() (only used in testing) to be able
to set up the FSs that do require it.
2022-10-04 14:37:29 +03:00
Ronan Lamy bd88000e2f ls-url: add ls-url tests for dvc plugins 2022-10-04 14:37:29 +03:00
Ronan Lamy 4da4c69985 ls-url: chain exceptions and simplify exc handling 2022-10-04 14:37:29 +03:00
Ronan Lamy 8a1c757497 fix test on Windows 2022-10-04 14:37:29 +03:00
Ronan Lamy 22655fa46d ls-url: update command test for --recursive 2022-10-04 14:37:29 +03:00
Ronan Lamy 103a0e2523 ls-url: simplify parse_external_url() 2022-10-04 14:37:29 +03:00
Ronan Lamy 0d025e5943 ls-url: add --recursive option 2022-10-04 14:37:29 +03:00
Ronan Lamy b01c4f8b05 ls-url: call get_cloud_fs() directly
Note: removes a spurious check on whether the target is a dvc file.
2022-10-04 14:37:29 +03:00
Ronan Lamy c27125d9c0 Replace Output with a local copy and remove all unused code 2022-10-04 14:37:29 +03:00
Ronan Lamy 71734a3d09 ls-url: fix test_file() on Windows and add another case 2022-10-04 14:37:29 +03:00
Ronan Lamy fa7619dfee ls-url: create basic version of dvc ls-url command
For now, this only takes a URL (or local path) and lists its contents
if the protocol supports it. The command fails if the target doesn't
exist and returns the (absolute) path to the target if it's not a
directory.
2022-10-04 14:37:29 +03:00
dependabot[bot] 67aaf6a192 build(deps-dev): Bump mypy from 0.981 to 0.982
Bumps [mypy](https://github.com/python/mypy) from 0.981 to 0.982.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.981...v0.982)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-04 12:02:42 +09:00
Daniele Trifirò 3adede8427 repo: use realpath to in _get_filepath when loading stages 2022-10-03 15:29:01 +02:00
Daniele Trifirò a686dded15 repo: fix crash while collecting stages with symlinks
closes #8345
2022-10-03 15:29:01 +02:00
Daniele Trifirò 8b1e61d3c5 update: update -help 2022-10-03 14:25:50 +02:00
Daniele Trifirò 4594087786 import: fix rev lock and pull with --no-download 2022-10-03 14:25:50 +02:00
Daniele Trifirò 5d69f57abf tests: add test_partial_import_pull 2022-10-03 14:25:50 +02:00
dependabot[bot] 53c886e407 build(deps-dev): Bump mypy from 0.971 to 0.981
Bumps [mypy](https://github.com/python/mypy) from 0.971 to 0.981.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.971...v0.981)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-02 12:44:42 +09:00
Ruslan Kuprieiev 890fbf3a6e deps: bump dvc-data to 0.14.0 2022-10-01 21:56:49 +03:00
skshetry 81abf2e412 dvcfs tests: copy pytest param instead of in-place update (#8388)
The param was being modified in-place, due to which successive runs would fail.
2022-10-01 14:07:38 +05:45
Ronan Lamy 3a9d0244b5 test: rename dvc.testing.test_*.py so pytest doesn't handle them as test files 2022-10-01 01:03:09 +03:00
dberenbaum 423166cb0f cli: remove foreach-group from help text 2022-09-30 19:54:03 +02:00
Peter Rowlands 4904286307 config: wrap UnicodeDecodeErrors on load 2022-09-30 11:45:43 +09:00
dependabot[bot] e556c632b3 build(deps-dev): Bump pytest-mock from 3.8.2 to 3.9.0
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.8.2 to 3.9.0.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.8.2...v3.9.0)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-29 14:37:06 +09:00
dependabot[bot] db0c6e9086 build(deps-dev): Bump pytest-cov from 3.0.0 to 4.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-29 13:48:47 +09:00
dependabot[bot] 4177a5dc9e build(deps): Bump dvc-task from 0.1.2 to 0.1.3
Bumps [dvc-task](https://github.com/iterative/dvc-task) from 0.1.2 to 0.1.3.
- [Release notes](https://github.com/iterative/dvc-task/releases)
- [Commits](https://github.com/iterative/dvc-task/compare/0.1.2...0.1.3)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-29 13:47:49 +09:00
Ronan Lamy 04af944e22 fs: test and slightly refactor get_fs_config() 2022-09-28 22:06:40 +03:00
pre-commit-ci[bot] 13b2ac3bd1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/ambv/black: 22.6.0 → 22.8.0](https://github.com/ambv/black/compare/22.6.0...22.8.0)
2022-09-28 17:07:09 +09:00
Paweł Redzyński 25ea5f35ec plots: pass all types of plots in --json option
Related: iterative/vscode-dvc#1757
2022-09-27 14:23:42 +02:00
karajan1001 6bd14b21ef Move infofile operation to executor and
Currently infofile dump was spread in both executor and queue classes.
put them all into executor to make them more managable. Move all ending
dump operations to the cleanup function, because the collect_result progress
might failed because of git operations, in which can guarantee them to
be run in a `finally` scope.
2022-09-26 16:25:47 +08:00
karajan1001 c2829f4ce8 Make executor info compatible with old versions. 2022-09-26 16:25:47 +08:00
karajan1001 d8a4ecd68f Remove the stale manager code 2022-09-26 16:25:47 +08:00
karajan1001 7fb9dcec12 Add tests for executor status
1. Move some basic test script from function tests to unit test.
2. Add success/failed tests for the status change of `tempdir`, `celery`, `workspace` running case.
2022-09-26 16:25:47 +08:00
karajan1001 f23bc71eea Use task status to replace collected.
fix: #8088

1. Use status to replace collected.
2. Make status show more accurate.

Fix lint
2022-09-26 16:25:47 +08:00
karajan1001 8b326137c6 Add a new attribute status to ExecutorInfo file
1. Add a new attribute status to ExecutorInfo file
2. Update running status to the executor infofile.
2022-09-26 16:25:47 +08:00
karajan1001 90109bccd7 Refactor seperate the initialization of executor and setup environment
Reason:
1. Need some more granular control over initialization progress of
   executor.

What done:
1. Seperate `init_git` and `init_cache` progress out from setup_executor
2. Move `set_ref` into `init_git`
2022-09-26 16:25:47 +08:00
Ruslan Kuprieiev 8c484645fa cloud-versioning: better handling for directories 2022-09-26 02:54:17 +03:00
dberenbaum c039e9a6e7 merge-driver: support removes and changes 2022-09-23 23:22:08 +03:00
Ruslan Kuprieiev 10cfa0d5d7 fetch: use md5() before save() 2022-09-23 15:12:19 +03:00
dependabot[bot] ecab4a22fe build(deps): Bump dvc-data from 0.10.1 to 0.12.0
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.10.1 to 0.12.0.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.10.1...0.12.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-23 15:12:19 +03:00
dberenbaum 4f354f5333 pull: hide glob option 2022-09-23 14:13:25 +03:00
skshetry f6b1f01a5c aiohttp: remove AIOHTTP_NO_EXTENSIONS envvar (#8353)
* aiohttp: remove AIOHTTP_NO_EXTENSIONS envvar

aiohttp now provides wheel for 3.11

* advertise 3.11 support
2022-09-23 16:23:37 +05:45
Saugat Pachhai (सौगात) 6d17bcce93 cli: add help for foreach 2022-09-23 13:36:34 +03:00
karajan1001 8da04c7c81 Solve the on_diverged function not executed error.
fix: #8348
2022-09-23 17:20:31 +08:00
Paweł Redzyńskiandpre-commit-ci[bot] c617769a1d deps: bump codespell (#8199)
[pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/codespell-project/codespell: v2.1.0 → v2.2.1](https://github.com/codespell-project/codespell/compare/v2.1.0...v2.2.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-22 19:31:24 +05:45
Daniele Trifirò 69c3c83833 import/import-url: ignore outs when using --no-download 2022-09-22 00:29:46 +03:00
David de la Iglesia Castro 122a68398e hydra: Fix sweeps on Defaults List.
Defaults List overrides (`defaults/foo=1,2`) were not working because of the inclusion of the `/` character.
Fixed it by updating how we construct the `merged_overrides` and added a regression test.
2022-09-21 15:48:19 +02:00
Martino Mensio 748c015638 fixed link to "get started: pipelines" docs 2022-09-21 13:45:39 +03:00
skshetry ba1ab86078 document dvcfs (#8332) 2022-09-21 11:30:15 +05:45
skshetry aaa467a96b fs.callback.RichCallback: add transient support (#8335) 2022-09-20 17:03:17 +05:45
Daniele Trifirò ed15e59115 deps: bump dvc-http to 2.27.2 2022-09-20 13:18:18 +03:00
skshetry e63cb31a66 dvcfs: remove get-file implementation, make dvcfs non-cachable (#8331)
dvcfs: remove get-file implementation
2022-09-20 15:24:44 +05:45
Peter Rowlands 4c1f172132 deps: bump dvc-data to 0.10.1 2022-09-20 18:30:35 +09:00
Ruslan Kuprieiev d2a31eb660 push: use update_pipeline=False for cloud versioning 2022-09-19 17:21:43 +03:00
Ruslan Kuprieiev 625b735502 deps: bump dvc-s3 2022-09-19 14:57:35 +03:00
skshetry 7e93fc8c4a lint: pin astroid version (#8323)
See https://github.com/PyCQA/pylint/issues/7488. Pylint is failing on our codebase.
2022-09-19 12:22:21 +05:45
skshetry 853a918d5c cli: rename cli util, hide data ls from the help (#8321) 2022-09-19 10:29:52 +05:45
skshetry b5f20f8467 use native concurrency (#8320) 2022-09-19 10:23:55 +05:45
skshetry 2f6bc70b45 remove mergify (#8319) 2022-09-19 09:50:56 +05:45
Daniele Trifirò 197f522175 deps: add testing group for dvc.testing requirements 2022-09-17 00:54:35 +03:00
Ruslan Kuprieiev 35e6f082a4 analytics: use iterative-telemetry for user_id lookup 2022-09-17 00:45:45 +03:00
Ruslan Kuprieiev 4531fec2b7 deps: bump dvc-data to 0.10.0 2022-09-16 22:56:56 +03:00
skshetry 391bb29be2 dvcfs: rename DvcFileSystem to DVCFileSystem (#8307) 2022-09-16 18:18:55 +05:45
Peter Rowlands 146bdbbe9b deps: bump dvc-azure to 2.20.4 2022-09-16 17:47:38 +09:00
Saugat Pachhai (सौगात) c1f659bdd7 dvcfs: prevent opening file object in write mode 2022-09-16 11:31:24 +03:00
dependabot[bot] 0fe8f4e368 build(deps): Bump dvc-render from 0.0.10 to 0.0.11
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.10 to 0.0.11.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.10...0.0.11)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-16 09:58:16 +02:00
Ruslan Kuprieiev 3cbf319e0d loader: don't forget to set meta.isdir
Leftover from #8273

Fixes #8300
2022-09-15 22:53:48 +03:00
Ruslan Kuprieiev 477a6e177f index: use tree obj if using inline files
Leftover from #8273
2022-09-15 21:25:28 +03:00
skshetry f854fc6365 try to split testsuites into two (#8297)
try to split tests
2022-09-15 22:11:02 +05:45
skshetry 1b90683e02 dvcfs: add fsspec-compliance tests (#8296)
dvcfs: add tests
2022-09-15 18:42:50 +05:45
skshetry d762372d55 schema: add strict schema validation for top-level plots (#8289)
schema: add strict schema validation for plots
2022-09-15 16:30:10 +05:45
Peter Rowlands f3d304580a schema: update lockfile schema for versioning and dir files 2022-09-15 13:13:31 +03:00
Peter Rowlands 0e0d46e232 output: lazy load tree from files list in get_obj 2022-09-15 13:13:31 +03:00
Peter Rowlands 95ae018ed8 push/fetch: support dirs in worktree remotes 2022-09-15 13:13:31 +03:00
Peter Rowlands c29e721823 output: support loading/dumping dir trees from metadata 2022-09-15 13:13:31 +03:00
Peter Rowlands a9c7032d50 dvc-data: bump to 0.8.0 2022-09-15 13:13:31 +03:00
Peter Rowlands ecb767d622 index: add workspace/key generation helper 2022-09-15 13:13:31 +03:00
Saugat Pachhai (सौगात) fa45a9bbba dvcfs: default open to binary mode 2022-09-15 13:11:52 +03:00
daniele f50ae589ba deps: remove unused mock dep (#8290) 2022-09-15 07:34:21 +05:45
Daniele Trifirò 611c114f01 import-url: pass fs_config down from imp_url to get_cloud_fs 2022-09-14 22:26:19 +03:00
skshetry fdc18d5483 expose dvcfs in dvc.api and add to fsspec's registry (#8287)
* expose dvcfs in dvc.api and add to fsspec's registry

* readd __all__ to avoid exposing modules
2022-09-14 20:02:56 +05:45
David de la Iglesia Castro 3b532b7d59 exp run: Support hydra basic sweeper.
Requires `--queue` to be used alongside `--set-param`.

Doesn't strictly require `hydra.config.enabled`.

Add `get_hydra_sweeps` to `dvc/utils/hydra.py`.  Uses hydra `BasicSweeper` to generate the different sweeps.

`glob` syntax for sweeps is not supported in this P.R.

Closes #8083

---

Using setup from https://hydra.cc/docs/tutorials/basic/your_first_app/composition/ :

```console
$ dvc exp run -S "db=mysql,postgresql" -S "schema=warehouse,school" --queue
Queueing with '{'params.yaml': ['db=mysql', 'schema=warehouse']}'.
Queued experiment '5ab98b8' for future execution.
Queueing with '{'params.yaml': ['db=mysql', 'schema=school']}'.
Queued experiment '57c2fb6' for future execution.
Queueing with '{'params.yaml': ['db=postgresql', 'schema=warehouse']}'.
Queued experiment 'b9d6391' for future execution.
Queueing with '{'params.yaml': ['db=postgresql', 'schema=school']}'.
Queued experiment '145cd55' for future execution.
```
2022-09-13 13:43:30 +02:00
skshetry e78c4455de dvcfs: remove unused config code for opening a repo, change signature (#8277)
dvcfs: remove unused code for opening a repo
2022-09-13 09:26:33 +05:45
skshetry 8e5f4a8f35 data ls: new command to show metadata with outputs (#8252)
data ls: add new command to show metadata for the outputs
2022-09-09 17:36:24 +05:45
Saugat Pachhai (सौगात) cb3159e586 dvcfs: remove config 2022-09-09 13:16:27 +03:00
skshetry 7b301b09a1 data status: fix type hints (#8275) 2022-09-09 13:38:41 +05:45
skshetry 55b16166c8 add/import/import-url: update help message for labels/type/desc (#8271) 2022-09-08 23:09:10 +05:45
Ronan Lamy c70d0bd14d fs: cleanup and rename _get_fs_pair_2() to _get_subrepo_info() 2022-09-08 17:39:32 +03:00
Ronan Lamy b99a0cd6ca fs: _get_dvc_path() cannot be called with dvc_fs=None 2022-09-08 17:39:32 +03:00
Ronan Lamy d4170b2ec0 fs: refactor _get_fs_pair_2() 2022-09-08 17:39:32 +03:00
Ronan Lamy a3181bb509 fs: remove _get_fs_pair() 2022-09-08 17:39:32 +03:00
Ronan Lamy 4d6b90502b fs: improve performance of _DvcFileSystem._update()
Avoid back and forth conversions between key tuples and path strings.
2022-09-08 17:39:32 +03:00
Ronan Lamy 947427f4d9 fs: remove dead code 2022-09-08 17:39:32 +03:00
Ronan Lamy 0482a1a88b fs: simplify data flow
This uses the fact that repo.fs is always self.repo.fs and that
self.repo.fs always exists.
2022-09-08 17:39:32 +03:00
Ronan Lamy b470459987 fs: simplify return values of _get_fs_pair_2() 2022-09-08 17:39:32 +03:00
Ronan Lamy 81a010674e fs: avoid costly redundant dvcignore check when calling ._info() from .ls() 2022-09-08 17:39:32 +03:00
Ronan Lamy d946a1328e fs: Extract ._info() from .info()
This avoids unnecessarily recomputing dvc_parts when calling .info()
from .ls()
2022-09-08 17:39:32 +03:00
Ronan Lamy ad7d39da20 fs: chop up ._get_fs_pair() 2022-09-08 17:39:32 +03:00
Saugat Pachhai (सौगात) 8432de5980 metadata: add support for dvc.yaml file 2022-09-08 15:10:33 +03:00
Saugat Pachhai (सौगात) c73764156e metadata: support custom metadata in .dvc files
This adds support for custom metadata in outputs under `meta` key.
Also adds `--meta` flag to `add`/`import`/`import-url` that supports
`key=value` values.
2022-09-08 14:10:35 +03:00
skshetry df60df5242 add metadata fields: label, type to data (#8232)
gto like metadata fields: labels and type
2022-09-08 16:33:47 +05:45
dependabot[bot] 504485ed6e build(deps-dev): Bump pylint from 2.15.0 to 2.15.2 (#8268)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.0 to 2.15.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.0...v2.15.2)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-08 10:57:12 +05:45
dependabot[bot] 1e989bde2c build(deps): Bump scmrepo from 0.1.0 to 0.1.1 (#8269)
Bumps [scmrepo](https://github.com/iterative/scmrepo) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/iterative/scmrepo/releases)
- [Commits](https://github.com/iterative/scmrepo/compare/0.1.0...0.1.1)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-08 10:56:53 +05:45
Ruslan Kuprieiev 7e2248bbe5 deps: bump dvc-data to 0.7.1 2022-09-07 23:07:35 +03:00
Daniele Trifirò a2d80b2403 silence flake8 errors (abstract class with no abstract method) 2022-09-07 21:51:33 +03:00
Daniele Trifirò 76e7c76ae5 data status: fix for scmrepo 0.1.0 2022-09-07 21:48:53 +03:00
Ruslan Kuprieiev 6cada6dc12 dvc: cloud versioning POC 2022-09-07 21:43:34 +03:00
Ruslan Kuprieiev 89d4461908 deps: temporarily limit dulwich to <0.20.46
Looks like the new release broke some data status logic.
2022-09-07 02:56:14 +03:00
Ruslan Kuprieiev a124d7e866 deps: bump dvc-data to 0.6.3 2022-09-07 02:23:29 +03:00
Ruslan Kuprieiev 628be2ae49 deps: bump dvc-data to 0.6.1 2022-09-06 20:30:20 +03:00
dependabot[bot] 7a71e06946 build(deps): Bump dvc-render from 0.0.9 to 0.0.10
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.9 to 0.0.10.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.9...0.0.10)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-06 17:12:36 +02:00
dependabot[bot] 91e54d8c27 build(deps): Bump dvc-data from 0.4.0 to 0.5.3 2022-09-06 17:17:21 +03:00
skshetry 071b2b8f81 add support for foreach target (#8210)
add general support for foreach target
2022-09-06 06:39:37 +05:45
skshetry 0b2b59a601 dvc.yaml: preserve outputs' desc on rewrites/updates to the stage (#8247)
preserve outputs' desc on rewrites/updates to the stage

We were not loading `desc` of the output, due to which we were not preserving the desc field over rewrites. Note that the rewrite only happens in select commands for `dvc.yaml`: `freeze`/`unfreeze`/`run`/`stage-add`, so we did not notice this before.
2022-09-05 18:49:39 +05:45
dependabot[bot] 65dc1cd08f build(deps-dev): Bump pytest from 7.1.2 to 7.1.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.2...7.1.3)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-03 02:04:01 +03:00
dependabot[bot] 1b103297b7 build(deps-dev): Bump dvc-azure from 2.20.0 to 2.20.2
Bumps [dvc-azure](https://github.com/iterative/dvc-azure) from 2.20.0 to 2.20.2.
- [Release notes](https://github.com/iterative/dvc-azure/releases)
- [Commits](https://github.com/iterative/dvc-azure/compare/2.20.0...2.20.2)

---
updated-dependencies:
- dependency-name: dvc-azure
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-03 02:03:46 +03:00
John Henning 887c73b3c6 adjusted formatting for dvc-hdfs 2022-09-02 23:04:59 +03:00
Peter Rowlands 660c17f654 output: save version_id meta 2022-09-01 19:36:53 +03:00
skshetry 49da2eef02 tests: set celery ping_task_timeout 3x the default (#8221)
tests: set celery ping_timeout 3x the default
2022-09-01 16:48:19 +05:45
Ruslan Kuprieiev aff65d42bb deps: bump dvc-azure and dvc-s3 to 2.20.0 2022-09-01 13:00:47 +03:00
skshetry 8968405770 data status: fix path for committed changes in Windows (#8220)
The path we get from GitFileSystem is posixpath, even on Windows. We need to
convert that posixpath to os.path here.
2022-09-01 10:02:08 +05:45
dependabot[bot] b6e8ba6758 build(deps-dev): Bump pylint from 2.14.5 to 2.15.0
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.14.5 to 2.15.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.14.5...v2.15.0)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...
2022-09-01 10:42:16 +08:00
Ruslan Kuprieiev e307a62dc0 deps: bump dvc-data to 0.4.0 2022-09-01 02:59:24 +03:00
Saugat Pachhai (सौगात) 639eeed43b skip tests that depend on hydra.compose in 3.11 2022-08-31 17:24:09 +02:00
David de la Iglesia Castro 91f12395f1 exp run: Support composing and dumping Hydra config.
The feature will be used depending on whether `config.hydra.enabled` is True or False.

Uses `hydra.initialize_config_dir` and `hydra.compose` (from https://hydra.cc/docs/advanced/compose_api/) to build the config and dump it to `params.yaml`. The content of the output file will be overwritten.

`config.hydra.config_dir` and `config.hydra.config_name` can be used to customize the values passed to `hydra.initialize_config_dir` and `hydra.compose`.

Can be combined with `--set-param` overrides.

Closes #8082
2022-08-31 17:24:09 +02:00
Ruslan Kuprieiev e7daeb1c3e deps: bump dvc-data to 0.3.0 2022-08-30 22:46:27 +03:00
Saugat Pachhai (सौगात) d3569035c8 output.get_obj: catch ObjectCorruptedError 2022-08-30 21:31:33 +03:00
David de la Iglesia Castro daee52e176 info: skip missing subprojects 2022-08-30 20:15:41 +03:00
skshetry a8415b01d0 data status: fix quoting on command hints for untracked files (#8211)
data status: fix quoting on command hints
2022-08-30 22:51:39 +05:45
Daniele Trifirò 77f56090ff do not checkout partials imports on fetch 2022-08-30 20:05:09 +03:00
Dave Berenbaum 1d3102f29b data status: update hints to include fetch and checkout (#8209) 2022-08-30 22:05:32 +05:45
skshetry a0cbcaa3c8 Delete CODEOWNERS 2022-08-30 13:12:57 +05:45
David de la Iglesia Castro ab780879d1 info: Include subprojects. 2022-08-30 09:22:39 +02:00
skshetry bfad82a2e3 data status: remove --withdirs, show unknowns in CLI (#8189)
* data status: remove --withdirs, show unknowns in CLI

* data status: only consider unknown from newer object during diff
2022-08-30 12:11:02 +05:45
skshetry e5eec59cb8 test on 3.11 (#8196)
We don't have aiohttp support yet, due to which we have to install
without cythonized extension.

Similarly, there has been some changes in how dataclasses disallows some
mutable arguments to be set as a class property, due to which
`hydra.conf` fails to import in 3.11.

See https://github.com/python/cpython/pull/29867. For now, I have
patched the module and avoid importing as a pytest-plugin.

dvc-hdfs/pyarrow is not there yet, we'll usually see a release 2-3
months after official reelase.

dvc-hdfs is skipped in 3.11 in extras_requires. `--set-params` will
return a nice error message if hydra cannot be imported in 3.11. I
decided to only patch that in tests.
2022-08-29 19:58:17 +05:45
Paweł Redzyński 2a4b83060e plots: support dirs in top level definitions
Fixes: #7914
2022-08-29 10:51:16 +02:00
David de la Iglesia Castro 9e2ebe9fcc resolve_rev: raise DvcException on empty repo. 2022-08-29 01:50:14 +03:00
David de la Iglesia Castro 2cef130191 repo: Handle no commits for exp show and plots diff.
Make it consistent with all the other commands that use `brancher` (i.e. `dvc diff`, `metrics diff`, `params diff`)

Closes #8163
2022-08-29 01:50:14 +03:00
David de la Iglesia Castro 17b9b1571e api: Add details forparams_show stages syntax.
Explain `stages` syntax for cases where the api is used from a directory different to the one the `dvc.yam` is located.
2022-08-29 01:49:38 +03:00
skshetry 37c8ef3406 Update dvc/commands/plots.py 2022-08-29 01:46:47 +03:00
b7aea414e6 plots templates: change ui to not dump to file
Update dvc/commands/plots.py

Co-authored-by: David de la Iglesia Castro <[email protected]>

Update tests/unit/command/test_plots.py

Co-authored-by: David de la Iglesia Castro <[email protected]>

Update dvc/commands/plots.py

Co-authored-by: Paweł Redzyński <[email protected]>

fixed test
2022-08-29 01:46:47 +03:00
pre-commit-ci[bot] 126a64618b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-08-29 01:43:43 +03:00
Alex Hall 8bdf9b1138 Better error message when using remove on file instead of dvc file 2022-08-29 01:43:43 +03:00
Ruslan Kuprieiev 9cb242c8a5 deps: bump dvc-http and dvc-gs 2022-08-28 23:13:18 +03:00
Ruslan Kuprieiev 11802426de pyinstaller: don't forget dvc_* plugins
Fixes #8194
2022-08-28 21:05:25 +03:00
dependabot[bot] b376044c45 build(deps): Bump dvc-data from 0.1.23 to 0.2.0
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.1.23 to 0.2.0.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.1.23...0.2.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-08-27 03:18:20 +03:00
David de la Iglesia Castro 7b3b4b373b api: Fix params_show for untracked targets.
If an untracked target is provided alongside `stages` or `deps`, don't load the parameters from the target and raise no params found instead.

Closes #8161
2022-08-26 12:00:45 +02:00
Peter Rowlands (변기호) 512bfc4305 tests: mark exp queue func tests xfail (#8186) 2022-08-25 21:04:05 +05:45
skshetry 54ee8c0d77 data status: make untracked files to be relative paths (#8184) 2022-08-25 19:29:43 +05:45
skshetry 063eb6904d data status: test cleanup and improvements (#8185) 2022-08-25 18:50:46 +05:45
Ruslan Kuprieiev 3e183d62ed deps: bump dvc-* plugins to 2.19.0
These are the same packages but with correct versions to sync up with existing packages that we generated in conda over the years.
2022-08-25 14:44:13 +03:00
David de la Iglesia Castro e815bbe211 metrics: show: Fix show from subdir.
This was a regression introduced in #7712.
Calling `repo.metrics.show` from a subdir stopped returning metrics because `fs.isfile(metric)` check returned false.
2022-08-25 13:02:22 +02:00
Saugat Pachhai (सौगात) 2374c72105 status: add unknown state 2022-08-25 13:23:18 +03:00
Saugat Pachhai (सौगात) 5cf1421c61 fix tests 2022-08-25 13:23:18 +03:00
skshetry a6a2feea62 Update setup.cfg 2022-08-25 13:23:18 +03:00
Saugat Pachhai (सौगात) 6c1d00c2d9 fix diff when no object exists by falling back to shallow_diff 2022-08-25 13:23:18 +03:00
Saugat Pachhai (सौगात) f72456f67c add shallow_diff 2022-08-25 13:23:18 +03:00
skshetry 2c556f58d3 data status: fix not-in-cache hint (#8181)
See https://github.com/iterative/dvc.org/pull/3812\#discussion_r936625617.
2022-08-25 15:39:59 +05:45
Ruslan Kuprieiev 460d08057c Create hook-fsspec.py 2022-08-24 23:39:46 +03:00
Ronan Lamy cc6deb24c8 fs: remove obsolete special cases 2022-08-23 23:20:42 +03:00
Ronan Lamy e681030c72 fs: simplify filtering out dvcfiles 2022-08-23 23:20:42 +03:00
Ronan Lamy 7a51548214 fs: Add fast path for the no subrepo case in _get_fs_pair() 2022-08-23 23:20:42 +03:00
Ronan Lamy 8e99fd0091 ls: Use fs.walk(..., detail=True) instead of separate call to fs.info() 2022-08-23 23:20:42 +03:00
Ronan Lamy 7e5a586f85 fs: Remove redundant .isdvc() check
fs.walk(..., dvc_only=True) already outputs dvc objects only, so
fs.isdvc(path, recursive=True) is always True.
2022-08-23 23:20:42 +03:00
Ronan Lamy c76a84b7b1 fs: remove one call to ._get_key() in _DvcFS._get_fs_pair() 2022-08-23 23:20:42 +03:00
Daniele Trifirò 26008f1559 import-url/update: add --no-download flag
add --no-download flag to dvc import-url/dvc update to only create/update .dvc
files without downloading the associated data.

Created .dvc files can be fetched using `dvc pull` or can be
updated using `dvc update --no-download`.
2022-08-23 23:07:01 +03:00
Daniele Trifirò 92d680ff94 tests: fix typo in test name 2022-08-23 23:07:01 +03:00
Daniele Trifirò c69c46d08c fix error message on fetch/pull/checkout failure 2022-08-23 23:07:01 +03:00
skshetry 6477ade557 build: skip install fpm and ruby on Windows (#8169) 2022-08-23 18:02:47 +05:45
skshetry 78dd045d29 ci: drop __token__, it's the default (#8157) 2022-08-22 08:40:31 +05:45
skshetry 2ddbbdf5a4 ci: refactor binary executable building workflow (#8156)
This now uses the matrix to build executables from same jobs definition.
2022-08-22 08:38:58 +05:45
skshetry 17a565023b build: bump ruby to 2.6 (#8154) 2022-08-20 23:32:23 +05:45
skshetry b5d5728bf8 deps: bump dvc-data to 0.1.22 and dvc-s3 to 0.0.2 (#8153)
* deps: bump dvc-data to 0.1.22

* deps: bump dvc-s3 to 0.0.2
2022-08-20 22:59:40 +05:45
skshetry 055641a51e deps: bump dvc-data to 0.1.21 (#8147) 2022-08-19 13:22:56 +05:45
Ruslan Kuprieiev fc509d4e4a Delete hook-googleapiclient.model.py
This is already provided by pyinstaller's standard hooks https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-googleapiclient.model.py
2022-08-18 21:07:57 +03:00
Ruslan Kuprieiev a67fea2235 doctor: ignore missing plugins 2022-08-18 21:02:41 +03:00
skshetry f062b52e05 Add python version label to the readme (#8146) 2022-08-18 18:34:58 +05:45
Paweł Redzyński 25166f3e02 plots: show-vega: adjust renderers before show vega
Fixes: iterative/cml#1112
2022-08-18 12:52:52 +02:00
Ruslan Kuprieiev a79ea15827 tests: webhdfs: fix linter 2022-08-18 12:43:12 +03:00
Ruslan Kuprieiev 8db9a2f70f deps: bump dvc-data to 0.1.19 2022-08-18 12:43:12 +03:00
David de la Iglesia Castro ff2f2e433f pyinstaller: Restrict to 5.0
5.2 didn't fix https://github.com/iterative/dvc/issues/7949
2022-08-18 12:42:11 +03:00
Ruslan Kuprieiev 3fc9f323a7 fs: don't import SSHFileSystem 2022-08-18 00:23:40 +03:00
Ruslan Kuprieiev bf0aa5e8aa repo: data: ignore pylint false-positives 2022-08-17 23:02:22 +03:00
Ruslan Kuprieiev 3b3866323e deps: bump dvc-data to 0.1.18 2022-08-17 23:02:22 +03:00
187 changed files with 4437 additions and 2289 deletions
-1
View File
@@ -1 +0,0 @@
* @iterative/dvc
-9
View File
@@ -1,9 +0,0 @@
pull_request_rules:
- name: backport patches to 1.11
conditions:
- base=main
- label=backport-1.11
actions:
backport:
branches:
- "1.11"
+6 -11
View File
@@ -1,23 +1,18 @@
name: benchmarks
on: [pull_request, workflow_dispatch]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
bench:
name: run benchmarks
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-18.04]
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: "3.10"
- uses: iterative/dvc-bench@main
with:
pytest_options: "-k 'test_init or test_help'"
+50 -110
View File
@@ -6,152 +6,92 @@ on:
release:
types: [released, prereleased]
workflow_dispatch:
jobs:
deb:
runs-on: ubuntu-18.04
binary:
strategy:
matrix:
include:
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
name: ${{ matrix.pkg }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
cache: pip
cache-dependency-path: |
setup.cfg
scripts/build-requirements.txt
- name: Set up Ruby 2.6
uses: ruby/setup-ruby@v1
if: matrix.pkg != 'exe'
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build deb
run: python scripts/build.py deb
- name: Publish deb
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_content_type: binary/octet-stream
rpm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build rpm
run: python scripts/build.py rpm
- name: Publish rpm
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_content_type: binary/octet-stream
osxpkg:
runs-on: macos-10.15
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
with:
ruby-version: 2.4
- name: Install deps
run: |
# available lxml (webdav dependency) wheels are built with an old
# SDK, which breaks osxpkg notarization. Building from scratch
# instead.
pip install .[all] --no-binary lxml
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build
run: python scripts/build.py osxpkg
- name: Publish
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
asset_content_type: binary/octet-stream
exe:
runs-on: windows-2019
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
ruby-version: '2.6'
- name: Install fpm
if: matrix.pkg != 'exe'
run: gem install --no-document fpm
- name: Install deps
run: |
pip install -U wheel pip setuptools
pip install .[all]
pip install -r scripts/build-requirements.txt
- name: Pull images
run: |
dvc pull
- name: Build
run: python scripts\build.py exe
shell: cmd
- name: Publish
run: dvc pull
- name: Build ${{ matrix.pkg }}
run: python scripts/build.py ${{ matrix.pkg }}
- name: Publish ${{ matrix.pkg }}
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/[email protected]
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
asset_name: dvc-${{ github.event.release.tag_name }}.exe
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
asset_name: ${{ matrix.asset }}
asset_content_type: binary/octet-stream
pip:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: setup.cfg
- name: Install
run: pip install ".[all,tests]"
run: |
pip install -U wheel pip build setuptools
pip install ".[all,tests]"
- name: Build packages
run: |
pip install -U pip
./scripts/build_package.sh
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
+18 -23
View File
@@ -1,25 +1,27 @@
name: Tests
on:
pull_request: {}
push: {}
push:
branches: [main]
pull_request:
schedule:
- cron: '5 1 * * *' # every day at 01:05
workflow_dispatch:
env:
DVC_TEST: "true"
HOMEBREW_NO_AUTO_UPDATE: 1
SHELL: /bin/bash
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
lint:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -31,7 +33,7 @@ jobs:
run: |
pip install --upgrade pip setuptools wheel
pip install collective.checkdocs==0.2
pip install ".[dev]" pre-commit
pip install ".[dev]"
- name: Check README
run: python setup.py checkdocs
- uses: pre-commit/[email protected]
@@ -43,6 +45,12 @@ jobs:
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.8", "3.9", "3.10"]
pytest-filter:
- "import or plot or live or experiment"
- "not (import or plot or live or experiment)"
include:
- {os: ubuntu-latest, pyv: "3.11-dev"}
steps:
- uses: actions/checkout@v3
with:
@@ -51,18 +59,8 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyv }}
- name: get pip cache dir
id: pip-cache-dir
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: set pip cache
id: pip-cache
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
restore-keys: |
${{ runner.os }}-pip-
cache: pip
cache-dependency-path: setup.cfg
- name: install
if: steps.cache.pip-cache-dir.cache-hit != 'true'
run: |
@@ -70,10 +68,7 @@ jobs:
pip install -e ".[dev]"
- name: run tests
timeout-minutes: 40
run: >-
python -m tests -n=auto
--cov-report=xml --cov-report=term
${{ env.extra_test_args }}
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
- name: upload coverage report
uses: codecov/codecov-action@v3
with:
+3 -3
View File
@@ -6,7 +6,7 @@ repos:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 22.6.0
rev: 22.8.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
@@ -16,9 +16,9 @@ repos:
- id: codespell
args:
- --ignore-words-list
- ba,datas,fo,uptodate,cachable
- ba,datas,fo,uptodate,cachable,falsy
repo: https://github.com/codespell-project/codespell
rev: v2.1.0
rev: v2.2.1
- hooks:
- id: isort
language_version: python3
+6 -2
View File
@@ -11,7 +11,7 @@
• `Contributing`_
• `Community and Support`_
|CI| |Maintainability| |Coverage| |VS Code| |DOI|
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
@@ -82,7 +82,7 @@ To share and back up the *data cache*, DVC supports multiple remote storage plat
|Flowchart|
`DVC pipelines <https://dvc.org/doc/start/data-management/pipelines>`_ (computational graphs) connect code and data together.
`DVC pipelines <https://dvc.org/doc/start/data-management/data-pipelines>`_ (computational graphs) connect code and data together.
They specify all steps required to produce a model: input dependencies including code, data, commands to run; and output information to be saved.
Last but not least, `DVC Experiment Versioning <https://dvc.org/doc/start/experiments>`_ lets you prepare and run a large number of experiments.
@@ -248,6 +248,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
:target: https://pypi.org/project/dvc
:alt: Python Version
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
:target: https://codecov.io/gh/iterative/dvc
:alt: Codecov
+3 -43
View File
@@ -102,6 +102,8 @@ def _runtime_info():
"""
Gather information from the environment where DVC runs to fill a report.
"""
from iterative_telemetry import find_or_create_user_id
from dvc import __version__
from dvc.utils import is_binary
@@ -110,7 +112,7 @@ def _runtime_info():
"is_binary": is_binary(),
"scm_class": _scm_in_use(),
"system_info": _system_info(),
"user_id": _find_or_create_user_id(),
"user_id": find_or_create_user_id(),
}
@@ -146,45 +148,3 @@ def _system_info():
# We don't collect data for any other system.
raise NotImplementedError
def _find_or_create_user_id():
"""
The user's ID is stored on a file under the global config directory.
The file should contain a JSON with a "user_id" key:
{"user_id": "16fd2706-8baf-433b-82eb-8c7fada847da"}
IDs are generated randomly with UUID.
"""
import uuid
from dvc.config import Config
from dvc.lock import Lock, LockError
from dvc.utils.fs import makedirs
config_dir = Config.get_dir("global")
fname = os.path.join(config_dir, "user_id")
lockfile = os.path.join(config_dir, "user_id.lock")
# Since the `fname` and `lockfile` are under the global config,
# we need to make sure such directory exist already.
makedirs(config_dir, exist_ok=True)
try:
with Lock(lockfile):
try:
with open(fname, encoding="utf-8") as fobj:
user_id = json.load(fobj)["user_id"]
except (FileNotFoundError, ValueError, KeyError):
user_id = str(uuid.uuid4())
with open(fname, "w", encoding="utf-8") as fobj:
json.dump({"user_id": user_id}, fobj)
return user_id
except LockError:
logger.debug("Failed to acquire '%s'", lockfile)
+35
View File
@@ -0,0 +1,35 @@
from dataclasses import asdict, dataclass, field, fields
from typing import Any, ClassVar, Dict, List, Optional
from funcy import compact
@dataclass
class Annotation:
PARAM_DESC: ClassVar[str] = "desc"
PARAM_TYPE: ClassVar[str] = "type"
PARAM_LABELS: ClassVar[str] = "labels"
PARAM_META: ClassVar[str] = "meta"
desc: Optional[str] = None
type: Optional[str] = None
labels: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
def update(self, **kwargs) -> "Annotation":
for attr, value in kwargs.items():
if value and hasattr(self, attr):
setattr(self, attr, value)
return self
def to_dict(self) -> Dict[str, str]:
return compact(asdict(self))
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
ANNOTATION_SCHEMA = {
Annotation.PARAM_DESC: str,
Annotation.PARAM_TYPE: str,
Annotation.PARAM_LABELS: [str],
Annotation.PARAM_META: object,
}
+12 -6
View File
@@ -1,9 +1,15 @@
from .data import ( # noqa, pylint: disable=redefined-builtin
get_url,
open,
read,
)
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import make_checkpoint
from .params import params_show
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
__all__ = [
"get_url",
"make_checkpoint",
"open",
"params_show",
"read",
"DVCFileSystem",
]
+4
View File
@@ -37,6 +37,10 @@ def params_show(
stages to retrieve parameters from.
Defaults to `None`.
If `None`, all parameters from all stages will be retrieved.
If this method is called from a different location to the one where
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
be provided as a prefix with the syntax `{relpath}:{stage}`.
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
from.
Defaults to `None`.
+1 -1
View File
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
)
return None
from dvc_data.build import IgnoreInCollectedDirError
from dvc_data.hashfile.build import IgnoreInCollectedDirError
if isinstance(exc, IgnoreInCollectedDirError):
logger.exception("")
+20
View File
@@ -0,0 +1,20 @@
from argparse import _AppendAction
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
def __call__(self, parser, namespace, values, option_string=None):
from funcy import ldistinct
items = getattr(namespace, self.dest) or []
items.extend(map(str.strip, values.split(",")))
setattr(namespace, self.dest, ldistinct(items))
class KeyValueArgs(_AppendAction):
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest) or {}
for value in filter(bool, values):
key, _, value = value.partition("=")
items[key.strip()] = value
setattr(namespace, self.dest, items)
+2
View File
@@ -29,6 +29,7 @@ from dvc.commands import (
init,
install,
ls,
ls_url,
machine,
metrics,
move,
@@ -75,6 +76,7 @@ COMMANDS = [
install,
root,
ls,
ls_url,
freeze,
dag,
daemon,
+5 -2
View File
@@ -21,10 +21,13 @@ def append_doc_link(help_message, path):
)
def fix_plumbing_subparsers(subparsers):
# metavar needs to be explicitly set in order to hide plumbing subcommands
def hide_subparsers_from_help(subparsers):
# metavar needs to be explicitly set in order to hide subcommands
# 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`).
# NOTE: The argument is the parent subparser, not the subcommand parser.
cmds = [
cmd for cmd, parser in subparsers.choices.items() if parser.add_help
]
+36 -9
View File
@@ -2,12 +2,43 @@ import argparse
import logging
from dvc.cli import completion
from dvc.cli.actions import KeyValueArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
logger = logging.getLogger(__name__)
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help="User description of the data.",
)
parser.add_argument(
"--meta",
metavar="key=value",
nargs=1,
action=KeyValueArgs,
help="Custom metadata to add to the data.",
)
parser.add_argument(
"--label",
dest="labels",
type=str,
action="append",
metavar="<str>",
help="Label for the data.",
)
parser.add_argument(
"--type",
type=str,
metavar="<str>",
help="Type of the data.",
)
class CmdAdd(CmdBase):
def run(self):
from dvc.exceptions import (
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
glob=self.args.glob,
desc=self.args.desc,
out=self.args.out,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
remote=self.args.remote,
to_remote=self.args.to_remote,
jobs=self.args.jobs,
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
)
_add_annotating_args(parser)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
).complete = completion.FILE
+1 -1
View File
@@ -110,7 +110,7 @@ def add_parser(subparsers, parent_parser):
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
".dvc files and stage names."
),
).complete = completion.DVC_FILE
checkout_parser.set_defaults(func=CmdCheckout)
+4 -2
View File
@@ -70,7 +70,9 @@ def add_parser(subparsers, parent_parser):
commit_parser.add_argument(
"targets",
nargs="*",
help="stages or .dvc files to commit. Optional. "
"(Finds all DVC files in the workspace by default.)",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files and stage names."
),
).complete = completion.DVCFILES_AND_STAGE
commit_parser.set_defaults(func=CmdCommit)
+1 -1
View File
@@ -204,7 +204,7 @@ def add_parser(subparsers, parent_parser):
dag_parser.add_argument(
"target",
nargs="?",
help="Stage or output to show pipeline for (optional). "
help="Stage name or output to show pipeline for. "
"Finds all stages in the workspace by default.",
)
dag_parser.set_defaults(func=CmdDAG)
+137 -21
View File
@@ -1,11 +1,18 @@
import argparse
import logging
from typing import TYPE_CHECKING
from operator import itemgetter
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
from funcy import chunks, compact, log_durations
from dvc.cli import completion
from dvc.cli.actions import CommaSeparatedArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.cli.utils import (
append_doc_link,
fix_subparsers,
hide_subparsers_from_help,
)
from dvc.ui import ui
from dvc.utils import colorize
@@ -31,15 +38,22 @@ class CmdDataStatus(CmdBase):
"unchanged": "DVC unchanged files",
}
HINTS = {
"not_in_cache": 'use "dvc pull <file>..." '
"to update your local storage",
"committed": "git commit the corresponding dvc files "
"to update the repo",
"uncommitted": 'use "dvc commit <file>..." to track changes',
"untracked": 'use "git add <file> ..." or '
'dvc add <file>..." to commit to git or to dvc',
"git_dirty": "there are {}changes not tracked by dvc, "
'use "git status" to see',
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
"committed": (
"git commit the corresponding dvc files to update the repo",
),
"uncommitted": (
'use "dvc commit <file>..." to track changes',
'use "dvc checkout <file>..." to discard changes',
),
"untracked": (
'use "git add <file> ..." or '
'"dvc add <file>..." to commit to git or to dvc',
),
"git_dirty": (
"there are {}changes not tracked by dvc, "
'use "git status" to see',
),
}
@staticmethod
@@ -76,8 +90,9 @@ class CmdDataStatus(CmdBase):
color = cls.COLORS.get(stage, None)
ui.write(header)
if hint := cls.HINTS.get(stage):
ui.write(f" ({hint})")
if hints := cls.HINTS.get(stage):
for hint in hints:
ui.write(f" ({hint})")
if isinstance(stage_status, dict):
items = [
@@ -92,9 +107,10 @@ class CmdDataStatus(CmdBase):
out = "\n".join(tabs + item for item in chunk)
ui.write(colorize(out, color))
if (hint := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
message = hint.format("other " if result else "")
ui.write(f"[blue]({message})[/]", styled=True)
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
for hint in hints:
message = hint.format("other " if result else "")
ui.write(f"[blue]({message})[/]", styled=True)
return 0
def run(self) -> int:
@@ -102,7 +118,6 @@ class CmdDataStatus(CmdBase):
status = self.repo.data_status(
granular=self.args.granular,
untracked_files=self.args.untracked_files,
with_dirs=self.args.with_dirs,
)
if not self.args.unchanged:
@@ -116,6 +131,65 @@ class CmdDataStatus(CmdBase):
return self._show_status(status)
class CmdDataLs(CmdBase):
@staticmethod
def _show_table(
d: Iterable[Dict[str, Any]],
filter_types: Set[str],
filter_labels: Set[str],
markdown: bool = False,
) -> None:
from rich.style import Style
from dvc.compare import TabularData
td = TabularData(
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
)
for entry in sorted(d, key=itemgetter("path")):
typ = entry.get("type", "")
desc = entry.get("desc", "-")
labels = entry.get("labels", [])
if filter_types and typ not in filter_types:
continue
if filter_labels and filter_labels.isdisjoint(labels):
continue
rich_label = ui.rich_text()
for index, label in enumerate(labels):
if index:
rich_label.append(",")
style = Style(bold=label in filter_labels, color="green")
rich_label.append(label, style=style)
if markdown and desc:
desc = desc.partition("\n")[0]
path = ui.rich_text(entry["path"], style="cyan")
type_style = Style(bold=typ in filter_types, color="yellow")
typ = ui.rich_text(entry.get("type", ""), style=type_style)
td.append([path, typ or "-", rich_label or "-", desc])
td.render(markdown=markdown, rich_table=True)
def run(self):
from dvc.repo.data import ls
filter_labels = set(self.args.labels)
filter_types = set(self.args.type)
d = ls(
self.repo, targets=self.args.targets, recursive=self.args.recursive
)
self._show_table(
d,
filter_labels=filter_labels,
filter_types=filter_types,
markdown=self.args.markdown,
)
return 0
def add_parser(subparsers, parent_parser):
data_parser = subparsers.add_parser(
"data",
@@ -172,10 +246,52 @@ def add_parser(subparsers, parent_parser):
nargs="?",
help="Show untracked files.",
)
data_status_parser.add_argument(
"--with-dirs",
data_status_parser.set_defaults(func=CmdDataStatus)
DATA_LS_HELP = "List data tracked by DVC with its metadata."
data_ls_parser = data_subparsers.add_parser(
"ls",
aliases=["list"],
parents=[parent_parser],
description=append_doc_link(DATA_LS_HELP, "data/ls"),
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
data_ls_parser.add_argument(
"--md",
"--show-md",
dest="markdown",
action="store_true",
default=False,
help=argparse.SUPPRESS,
help="Show tabulated output in the Markdown format (GFM).",
)
data_status_parser.set_defaults(func=CmdDataStatus)
data_ls_parser.add_argument(
"--type",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of type to filter.",
)
data_ls_parser.add_argument(
"--labels",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of labels to filter.",
)
data_ls_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively list from the specified directories.",
)
data_ls_parser.add_argument(
"targets",
default=None,
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
),
).complete = completion.DVCFILES_AND_STAGE
data_ls_parser.set_defaults(func=CmdDataLs)
hide_subparsers_from_help(data_subparsers)
+4 -3
View File
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
)
self.log_summary(stats)
except (CheckoutError, DvcException) as exc:
self.log_summary(getattr(exc, "stats", {}))
if stats := getattr(exc, "stats", {}):
self.log_summary(stats)
logger.exception("failed to pull data from the cloud")
return 1
@@ -119,7 +120,7 @@ def shared_parent_parser():
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
".dvc files and stage names."
),
).complete = completion.DVC_FILE
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
"--glob",
action="store_true",
default=False,
help="Pull cache for targets matching shell-style wildcards.",
help=argparse.SUPPRESS,
)
pull_parser.set_defaults(func=CmdDataPull)
+2 -2
View File
@@ -2,8 +2,8 @@ import argparse
from dvc.cli.utils import (
append_doc_link,
fix_plumbing_subparsers,
fix_subparsers,
hide_subparsers_from_help,
)
from dvc.commands.experiments import (
apply,
@@ -59,7 +59,7 @@ def add_parser(subparsers, parent_parser):
fix_subparsers(experiments_subparsers)
for cmd in SUB_COMMANDS:
cmd.add_parser(experiments_subparsers, parent_parser)
fix_plumbing_subparsers(experiments_subparsers)
hide_subparsers_from_help(experiments_subparsers)
def add_rev_selection_flags(
+1 -1
View File
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
if self.args.json:
ui.write_json(diff)
else:
elif diff:
from dvc.compare import show_diff
precision = self.args.precision or DEFAULT_PRECISION
+1 -1
View File
@@ -74,7 +74,7 @@ class CmdMergeDriver(CmdHookBase):
our = Dvcfile(dvc, self.args.our, verify=False)
their = Dvcfile(dvc, self.args.their, verify=False)
our.merge(ancestor, their)
our.merge(ancestor, their, allowed=["add", "remove", "change"])
return 0
finally:
+17 -10
View File
@@ -19,7 +19,11 @@ class CmdImport(CmdBase):
fname=self.args.file,
rev=self.args.rev,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
)
except DvcException:
@@ -33,6 +37,8 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = (
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
@@ -69,20 +75,19 @@ def add_parser(subparsers, parent_parser):
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
)
import_parser.add_argument(
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create .dvc file without actually downloading it.",
help="Only create .dvc file without actually importing target data.",
)
import_parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
no_download_exec_group.add_argument(
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
)
import_parser.add_argument(
"-j",
@@ -94,4 +99,6 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImport)
+17 -11
View File
@@ -17,9 +17,13 @@ class CmdImportUrl(CmdBase):
out=self.args.out,
fname=self.args.file,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
remote=self.args.remote,
to_remote=self.args.to_remote,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
)
except DvcException:
@@ -34,6 +38,8 @@ class CmdImportUrl(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = (
"Download or copy file from URL and take it under DVC control."
)
@@ -66,11 +72,19 @@ def add_parser(subparsers, parent_parser):
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
).complete = completion.DIR
import_parser.add_argument(
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create .dvc file without actually downloading it.",
help="Only create .dvc file without actually importing target data.",
)
no_download_exec_group.add_argument(
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
)
import_parser.add_argument(
"--to-remote",
@@ -94,13 +108,5 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
import_parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImportUrl)
+43
View File
@@ -0,0 +1,43 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.ui import ui
from .ls import _prettify
logger = logging.getLogger(__name__)
class CmdListUrl(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
if entries:
entries = _prettify(entries, with_color=True)
ui.write("\n".join(entries))
return 0
def add_parser(subparsers, parent_parser):
LS_HELP = "List directory contents from URL."
lsurl_parser = subparsers.add_parser(
"list-url",
aliases=["ls-url"],
parents=[parent_parser],
description=append_doc_link(LS_HELP, "list-url"),
help=LS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lsurl_parser.add_argument(
"url", help="See `dvc import-url -h` for full list of supported URLs."
)
lsurl_parser.add_argument(
"-R",
"--recursive",
action="store_true",
help="Recursively list files.",
)
lsurl_parser.set_defaults(func=CmdListUrl)
+25 -46
View File
@@ -25,7 +25,7 @@ def _show_json(renderers, split=False):
def _adjust_vega_renderers(renderers):
from dvc.render import VERSION_FIELD
from dvc.render import REVISION_FIELD, VERSION_FIELD
from dvc_render import VegaRenderer
for r in renderers:
@@ -42,6 +42,7 @@ def _adjust_vega_renderers(renderers):
dp["rev"] = "::".join(vi.values())
else:
for dp in r.datapoints:
dp[REVISION_FIELD] = dp[VERSION_FIELD]["revision"]
dp.pop(VERSION_FIELD, {})
@@ -66,18 +67,6 @@ def _data_versions_count(renderer):
return len(set(x))
def _filter_unhandled_renderers(renderers):
# filtering out renderers currently unhandled by vscode extension
from dvc_render import VegaRenderer
def _is_json_viable(r):
return not (
isinstance(r, VegaRenderer) and _data_versions_count(r) > 1
)
return list(filter(_is_json_viable, renderers))
class CmdPlots(CmdBase):
def _func(self, *args, **kwargs):
raise NotImplementedError
@@ -155,17 +144,16 @@ class CmdPlots(CmdBase):
out=renderers_out,
templates_dir=self.repo.plots.templates_dir,
)
if self.args.json:
_show_json(renderers, self.args.split)
return 0
_adjust_vega_renderers(renderers)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
ui.write_json(json.loads(renderer.get_filled_template()))
return 0
if self.args.json:
renderers = _filter_unhandled_renderers(renderers)
_show_json(renderers, self.args.split)
return 0
_adjust_vega_renderers(renderers)
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
@@ -222,31 +210,22 @@ class CmdPlotsModify(CmdPlots):
class CmdPlotsTemplates(CmdBase):
TEMPLATES_CHOICES = [
"simple",
"linear",
"confusion",
"confusion_normalized",
"scatter",
"smooth",
"bar_horizontal_sorted",
"bar_horizontal",
]
def run(self):
from dvc_render.vega_templates import dump_templates
from dvc.exceptions import InvalidArgumentError
from dvc_render.vega_templates import TEMPLATES
try:
out = (
os.path.join(os.getcwd(), self.args.out)
if self.args.out
else self.repo.plots.templates_dir
)
target = self.args.template
if target:
for template in TEMPLATES:
if target == template.DEFAULT_NAME:
ui.write_json(template.DEFAULT_CONTENT)
return 0
raise InvalidArgumentError(f"Unexpected template: {target}.")
targets = [self.args.target] if self.args.target else None
dump_templates(output=out, targets=targets)
templates_path = os.path.relpath(out, os.getcwd())
ui.write(f"Templates have been written into '{templates_path}'.")
else:
for template in TEMPLATES:
ui.write(template.DEFAULT_NAME)
return 0
except DvcException:
@@ -356,8 +335,7 @@ def add_parser(subparsers, parent_parser):
plots_modify_parser.set_defaults(func=CmdPlotsModify)
TEMPLATES_HELP = (
"Write built-in plots templates to a directory "
"(.dvc/plots by default)."
"List built-in plots templates or show JSON specification for one."
)
plots_templates_parser = plots_subparsers.add_parser(
"templates",
@@ -367,13 +345,14 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_templates_parser.add_argument(
"target",
"template",
default=None,
nargs="?",
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
help="Template to write. Writes all templates by default.",
help=(
"Template for which to show JSON specification. "
"List all template names by default."
),
)
_add_output_argument(plots_templates_parser, typ="templates")
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
+10 -1
View File
@@ -55,7 +55,16 @@ def add_arguments(repro_parser):
repro_parser.add_argument(
"targets",
nargs="*",
help="Stages to reproduce. 'dvc.yaml' by default.",
help=(
"""\
Stages to reproduce. 'dvc.yaml' by default.
The targets can be path to a dvc.yaml file or `.dvc` file,
or a stage name from dvc.yaml file from
current working directory. To run a stage from dvc.yaml
from other directories, the target must be a path followed by colon `:`
and then the stage name name.
"""
),
).complete = completion.DVCFILES_AND_STAGE
repro_parser.add_argument(
"-f",
+1 -1
View File
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
# removing duplicates while maintaining order
collected = chain.from_iterable(
self.repo.stage.collect(
target=target, recursive=self.args.recursive, accept_group=True
target=target, recursive=self.args.recursive
)
for target in self.args.targets
)
+8
View File
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
rev=self.args.rev,
recursive=self.args.recursive,
to_remote=self.args.to_remote,
no_download=self.args.no_download,
remote=self.args.remote,
jobs=self.args.jobs,
)
@@ -55,6 +56,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Update all stages in the specified directory.",
)
update_parser.add_argument(
"--no-download",
action="store_true",
default=False,
help="Update .dvc file git revision/hash value(s)"
" but do not download the file(s).",
)
update_parser.add_argument(
"--to-remote",
action="store_true",
+4 -1
View File
@@ -176,7 +176,10 @@ class Config(dict):
if fs.exists(filename):
with fs.open(filename) as fobj:
conf_obj = ConfigObj(fobj)
try:
conf_obj = ConfigObj(fobj)
except UnicodeDecodeError as exc:
raise ConfigError(str(exc)) from exc
else:
conf_obj = ConfigObj()
return _parse_named(_lower_keys(conf_obj.dict()))
+7
View File
@@ -78,7 +78,9 @@ 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("version_aware"): Bool,
}
LOCAL_COMMON = {
"type": supported_cache_type,
@@ -289,4 +291,9 @@ SCHEMA = {
"bool": All(Lower, Choices("store_true", "boolean_optional")),
"list": All(Lower, Choices("nargs", "append")),
},
"hydra": {
Optional("enabled", default=False): Bool,
"config_dir": str,
"config_name": str,
},
}
+40 -14
View File
@@ -3,16 +3,37 @@
import logging
from typing import TYPE_CHECKING, Iterable, Optional
from dvc_data.db import get_index
from funcy import cached_property
from dvc_data.hashfile.db import get_index
if TYPE_CHECKING:
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.status import CompareStatusResult
from dvc_data.hashfile.status import CompareStatusResult
logger = logging.getLogger(__name__)
class Remote:
def __init__(self, path, fs, **config):
self.path = path
self.fs = fs
self.worktree = config.pop("worktree", False)
self.config = config
@cached_property
def odb(self):
from dvc_data.hashfile.db import get_odb
path = self.path
if self.worktree:
path = self.fs.path.join(path, ".dvc", "cache")
return get_odb(self.fs, path, **self.config)
class DataCloud:
"""Class that manages dvc remotes.
@@ -27,18 +48,23 @@ class DataCloud:
def __init__(self, repo):
self.repo = repo
def get_remote_odb(
def get_remote(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "HashFileDB":
) -> "Remote":
from dvc.config import NoRemoteError
if not name:
name = self.repo.config["core"].get("remote")
if name:
return self._init_odb(name)
from dvc.fs import get_cloud_fs
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
fs = cls(**config)
config["tmp_dir"] = self.repo.index_db_dir
return Remote(fs_path, fs, **config)
if bool(self.repo.config["remote"]):
error_msg = (
@@ -55,13 +81,13 @@ class DataCloud:
raise NoRemoteError(error_msg)
def _init_odb(self, name):
from dvc.fs import get_cloud_fs
from dvc_data.db import get_odb
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
config["tmp_dir"] = self.repo.index_db_dir
return get_odb(cls(**config), fs_path, **config)
def get_remote_odb(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "HashFileDB":
remote = self.get_remote(name=name, command=command)
return remote.odb
def _log_missing(self, status: "CompareStatusResult"):
if status.missing:
@@ -82,7 +108,7 @@ class DataCloud:
**kwargs,
):
from dvc.exceptions import FileTransferError
from dvc_data.transfer import TransferError, transfer
from dvc_data.hashfile.transfer import TransferError, transfer
try:
return transfer(src_odb, dest_odb, objs, **kwargs)
@@ -164,7 +190,7 @@ class DataCloud:
log_missing: log warning messages if file doesn't exist
neither in cache, neither in cloud.
"""
from dvc_data.status import compare_status
from dvc_data.hashfile.status import compare_status
if not odb:
odb = self.get_remote_odb(remote, "status")
+12 -4
View File
@@ -18,7 +18,7 @@ SCHEMA: Mapping[str, Any] = {
}
def _get(stage, p, info):
def _get(stage, p, info, fs_config=None):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
@@ -27,7 +27,7 @@ def _get(stage, p, info):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
return Dependency(stage, p, info)
return Dependency(stage, p, info, fs_config=fs_config)
def loadd_from(stage, d_list):
@@ -38,10 +38,18 @@ def loadd_from(stage, d_list):
return ret
def loads_from(stage, s_list, erepo=None):
def loads_from(stage, s_list, erepo=None, fs_config=None):
assert isinstance(s_list, list)
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
return [_get(stage, s, info.copy()) for s in s_list]
return [
_get(
stage,
s,
info.copy(),
fs_config=fs_config,
)
for s in s_list
]
def _merge_params(s_list):
+6 -5
View File
@@ -39,14 +39,15 @@ class ParamsDependency(Dependency):
def __init__(self, stage, path, params=None, repo=None):
self.params = list(params) if params else []
info = (
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
)
hash_info = HashInfo()
if isinstance(params, dict):
hash_info = HashInfo(self.PARAM_PARAMS, params)
repo = repo or stage.repo
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
super().__init__(stage, path, info=info, repo=repo)
super().__init__(stage, path, repo=repo)
self.hash_info = hash_info
def dumpd(self):
def dumpd(self, **kwargs):
ret = super().dumpd()
if not self.hash_info:
ret[self.PARAM_PARAMS] = self.params or {}
+27 -13
View File
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from voluptuous import Required
from dvc.prompt import confirm
from dvc_data.hashfile.meta import Meta
from .base import Dependency
@@ -32,6 +33,7 @@ class RepoDependency(Dependency):
def __init__(self, def_repo, stage, *args, **kwargs):
self.def_repo = def_repo
self._objs: Dict[str, "HashFile"] = {}
self._meta: Dict[str, Meta] = {}
super().__init__(stage, *args, **kwargs)
def _parse_path(self, fs, fs_path):
@@ -59,11 +61,11 @@ class RepoDependency(Dependency):
def save(self):
pass
def dumpd(self):
def dumpd(self, **kwargs):
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def download(self, to, jobs=None):
from dvc_data.checkout import checkout
from dvc_data.hashfile.checkout import checkout
for odb, objs in self.get_used_objs().items():
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
@@ -94,17 +96,17 @@ class RepoDependency(Dependency):
def get_used_objs(
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _ = self._get_used_and_obj(**kwargs)
used, _, _ = self._get_used_and_obj(**kwargs)
return used
def _get_used_and_obj(
self, obj_only=False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], Meta, "HashFile"]:
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.utils import as_posix
from dvc_data.build import build
from dvc_data.objects.tree import Tree, TreeError
from dvc_data.hashfile.build import build
from dvc_data.hashfile.tree import Tree, TreeError
local_odb = self.repo.odb.local
locked = kwargs.pop("locked", True)
@@ -131,7 +133,7 @@ class RepoDependency(Dependency):
pass
try:
object_store, _, obj = build(
object_store, meta, obj = build(
local_odb,
as_posix(self.def_path),
repo.dvcfs,
@@ -145,16 +147,18 @@ class RepoDependency(Dependency):
object_store.read_only = True
self._objs[rev] = obj
self._meta[rev] = meta
used_obj_ids[object_store].add(obj.hash_info)
if isinstance(obj, Tree):
used_obj_ids[object_store].update(oid for _, _, oid in obj)
return used_obj_ids, obj
return used_obj_ids, meta, obj
def _check_circular_import(self, odb, obj_ids):
from dvc.exceptions import CircularImportError
from dvc.fs.dvc import DvcFileSystem
from dvc_data.db.reference import ReferenceHashFileDB
from dvc_data.objects.tree import Tree
from dvc.fs.dvc import DVCFileSystem
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
from dvc_data.hashfile.tree import Tree
if not isinstance(odb, ReferenceHashFileDB):
return
@@ -169,7 +173,7 @@ class RepoDependency(Dependency):
checked_urls = set()
for obj in iter_objs():
if not isinstance(obj.fs, DvcFileSystem):
if not isinstance(obj.fs, DVCFileSystem):
continue
if (
obj.fs.repo_url in checked_urls
@@ -190,11 +194,21 @@ class RepoDependency(Dependency):
rev = self._get_rev(locked=locked)
if rev in self._objs:
return self._objs[rev]
_, obj = self._get_used_and_obj(
_, _, obj = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return obj
def get_meta(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True)
rev = self._get_rev(locked=locked)
if rev in self._meta:
return self._meta[rev]
_, meta, _ = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return meta
def _make_repo(self, locked=True, **kwargs):
from dvc.external_repo import external_repo
+15 -12
View File
@@ -172,7 +172,7 @@ class FileMixin:
def dump(self, stage, **kwargs):
raise NotImplementedError
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
@@ -198,18 +198,18 @@ class SingleStageFile(FileMixin):
if self.verify:
check_dvcfile_path(self.repo, self.path)
logger.debug("Saving information to '%s'.", relpath(self.path))
dump_yaml(self.path, serialize.to_single_stage_file(stage))
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
self.repo.scm_context.track_file(self.relpath)
def remove_stage(self, stage): # pylint: disable=unused-argument
self.remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
assert isinstance(ancestor, SingleStageFile)
assert isinstance(other, SingleStageFile)
stage = self.stage
stage.merge(ancestor.stage, other.stage)
stage.merge(ancestor.stage, other.stage, allowed=allowed)
self.dump(stage)
@@ -237,15 +237,17 @@ class PipelineFile(FileMixin):
self._dump_pipeline_file(stage)
if update_lock:
self._dump_lockfile(stage)
self._dump_lockfile(stage, **kwargs)
def _dump_lockfile(self, stage):
self._lockfile.dump(stage)
def _dump_lockfile(self, stage, **kwargs):
self._lockfile.dump(stage, **kwargs)
@staticmethod
def _check_if_parametrized(stage):
def _check_if_parametrized(stage, action: str = "dump") -> None:
if stage.raw_data.parametrized:
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
raise ParametrizedDumpError(
f"cannot {action} a parametrized {stage}"
)
def _dump_pipeline_file(self, stage):
self._check_if_parametrized(stage)
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
self._lockfile.remove()
def remove_stage(self, stage):
self._check_if_parametrized(stage, "remove")
self._lockfile.remove_stage(stage)
if not self.exists():
return
@@ -307,7 +310,7 @@ class PipelineFile(FileMixin):
else:
super().remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
@@ -363,7 +366,7 @@ class Lockfile(FileMixin):
return {SCHEMA_KWD: version}
def dump(self, stage, **kwargs):
stage_data = serialize.to_lockfile(stage)
stage_data = serialize.to_lockfile(stage, **kwargs)
with modify_yaml(self.path, fs=self.repo.fs) as data:
version = LOCKFILE_VERSION.from_dict(data)
@@ -408,7 +411,7 @@ class Lockfile(FileMixin):
else:
self.remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
+5
View File
@@ -278,6 +278,11 @@ class PathMissingError(DvcException):
self.dvc_only = dvc_only
class URLMissingError(DvcException):
def __init__(self, url):
super().__init__(f"The path '{url}' does not exist")
class RemoteCacheRequiredError(DvcException):
def __init__(self, scheme, fs_path):
+31 -35
View File
@@ -2,6 +2,9 @@ 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
# pylint: disable=unused-import
from dvc_objects.fs import utils # noqa: F401
from dvc_objects.fs import ( # noqa: F401
@@ -10,6 +13,8 @@ from dvc_objects.fs import ( # noqa: F401
Schemes,
generic,
get_fs_cls,
known_implementations,
localfs,
registry,
system,
)
@@ -19,37 +24,30 @@ from dvc_objects.fs.errors import ( # noqa: F401
ConfigError,
RemoteMissingDepsError,
)
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
from dvc_objects.fs.implementations.azure import AzureFileSystem # noqa: F401
from dvc_objects.fs.implementations.gdrive import ( # noqa: F401
GDriveFileSystem,
)
from dvc_objects.fs.implementations.gs import GSFileSystem # noqa: F401
from dvc_objects.fs.implementations.hdfs import HDFSFileSystem # noqa: F401
from dvc_objects.fs.implementations.local import localfs # noqa: F401
from dvc_objects.fs.implementations.oss import OSSFileSystem # noqa: F401
from dvc_objects.fs.implementations.s3 import S3FileSystem # noqa: F401
from dvc_objects.fs.implementations.ssh import ( # noqa: F401
DEFAULT_PORT as DEFAULT_SSH_PORT,
)
from dvc_objects.fs.implementations.ssh import SSHFileSystem # noqa: F401
from dvc_objects.fs.implementations.webdav import ( # noqa: F401
WebDAVFileSystem,
WebDAVSFileSystem,
)
from dvc_objects.fs.implementations.webhdfs import ( # noqa: F401
WebHDFSFileSystem,
)
from dvc_objects.fs.path import Path # noqa: F401
from .data import DataFileSystem # noqa: F401
from .dvc import DvcFileSystem # noqa: F401
from .dvc import DVCFileSystem # noqa: F401
from .git import GitFileSystem # noqa: F401
known_implementations.update(
{
"dvc": {
"class": "dvc.fs.dvc.DVCFileSystem",
"err": "dvc is supported, but requires 'dvc' to be installed",
},
"git": {
"class": "dvc.fs.git.GitFileSystem",
"err": "git is supported, but requires 'dvc' to be installed",
},
}
)
# pylint: enable=unused-import
def get_fs_config(repo, config, **kwargs):
def get_fs_config(config, **kwargs):
name = kwargs.get("name")
if name:
try:
@@ -60,10 +58,10 @@ def get_fs_config(repo, config, **kwargs):
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
else:
remote_conf = kwargs
return _resolve_remote_refs(repo, config, remote_conf)
return _resolve_remote_refs(config, remote_conf)
def _resolve_remote_refs(repo, config, remote_conf):
def _resolve_remote_refs(config, remote_conf):
# Support for cross referenced remotes.
# This will merge the settings, shadowing base ref with remote_conf.
# For example, having:
@@ -89,21 +87,22 @@ def _resolve_remote_refs(repo, config, remote_conf):
if parsed.scheme != "remote":
return remote_conf
base = get_fs_config(repo, config, name=parsed.netloc)
cls, _, _ = get_cloud_fs(repo, **base)
base = get_fs_config(config, name=parsed.netloc)
cls, _, _ = _get_cloud_fs(config, **base)
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
url = cls.sep.join((base["url"], relpath))
return {**base, **remote_conf, "url": url}
def get_cloud_fs(repo, **kwargs):
from dvc.config import ConfigError as RepoConfigError
from dvc.config_schema import SCHEMA, Invalid
repo_config = repo.config if repo else {}
return _get_cloud_fs(repo_config, **kwargs)
def _get_cloud_fs(repo_config, **kwargs):
core_config = repo_config.get("core", {})
remote_conf = get_fs_config(repo, repo_config, **kwargs)
remote_conf = get_fs_config(repo_config, **kwargs)
try:
remote_conf = SCHEMA["remote"][str](remote_conf)
except Invalid as exc:
@@ -116,11 +115,8 @@ def get_cloud_fs(repo, **kwargs):
cls = get_fs_cls(remote_conf)
if cls == GDriveFileSystem and repo:
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
url = remote_conf.pop("url")
if issubclass(cls, WebDAVFileSystem):
if cls.protocol in ["webdav", "webdavs"]:
# For WebDAVFileSystem, provided url is the base path itself, so it
# should be treated as being a root path.
fs_path = cls.root_marker
+9 -3
View File
@@ -24,6 +24,7 @@ class RichCallback(Callback):
bytes: bool = False, # pylint: disable=redefined-builtin
unit: str = None,
disable: bool = False,
transient: bool = True,
) -> None:
self._progress = progress
self.disable = disable
@@ -35,6 +36,7 @@ class RichCallback(Callback):
"visible": False,
"progress_type": None if bytes else "summary",
}
self._transient = transient
self._stack = ExitStack()
super().__init__(size=size, value=value)
@@ -47,7 +49,7 @@ class RichCallback(Callback):
return self._progress
progress = RichTransferProgress(
transient=True,
transient=self._transient,
disable=self.disable,
console=ui.error_console,
)
@@ -61,7 +63,8 @@ class RichCallback(Callback):
return self
def close(self):
self.progress.clear_task(self.task)
if self._transient:
self.progress.clear_task(self.task)
self._stack.close()
def call(self, hook_name=None, **kwargs):
@@ -74,6 +77,9 @@ class RichCallback(Callback):
def branch(self, path_1, path_2, kwargs, child: Optional[Callback] = None):
child = child or RichCallback(
progress=self.progress, desc=path_1, bytes=True
progress=self.progress,
desc=path_1,
bytes=True,
transient=self._transient,
)
return super().branch(path_1, path_2, kwargs, child=child)
+144 -219
View File
@@ -1,17 +1,16 @@
import errno
import logging
import ntpath
import os
import posixpath
import threading
from contextlib import suppress
from itertools import takewhile
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
from fsspec.spec import AbstractFileSystem
from funcy import cached_property, wrap_prop, wrap_with
from dvc_objects.fs.base import FileSystem
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
from dvc_objects.fs.path import Path
from .data import DataFileSystem
@@ -22,29 +21,19 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
Key = Tuple[str, ...]
def as_posix(path: str) -> str:
return path.replace(ntpath.sep, posixpath.sep)
def _wrap_walk(dvc_fs, *args, **kwargs):
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
# NOT the same as dvc.dvcfile.is_dvc_file()!
def _is_dvc_file(fname):
from dvc.dvcfile import is_valid_filename
from dvc.ignore import DvcIgnore
def _ls(fs, path):
dnames = []
fnames = []
for entry in fs.ls(path, detail=True):
name = fs.path.name(entry["name"])
if entry["type"] == "directory":
dnames.append(name)
else:
fnames.append(name)
return dnames, fnames
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
def _merge_info(repo, fs_info, dvc_info):
@@ -70,40 +59,68 @@ def _merge_info(repo, fs_info, dvc_info):
return ret
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
"""DVC + git-tracked files fs.
def _get_dvc_path(dvc_fs, subkey):
return dvc_fs.path.join(*subkey) if subkey else ""
Args:
repo: DVC or git repo.
subrepos: traverse to subrepos (by default, it ignores subrepos)
repo_factory: A function to initialize subrepo with, default is Repo.
kwargs: Additional keyword arguments passed to the `DataFileSystem()`.
"""
class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
cachable = False
root_marker = "/"
PARAM_REPO_URL = "repo_url"
PARAM_REPO_ROOT = "repo_root"
PARAM_REV = "rev"
PARAM_CACHE_DIR = "cache_dir"
PARAM_CACHE_TYPES = "cache_types"
PARAM_SUBREPOS = "subrepos"
def __init__(
self,
url: Optional[str] = None,
rev: Optional[str] = None,
repo: Optional["Repo"] = None,
subrepos=False,
repo_factory: RepoFactory = None,
**kwargs,
):
super().__init__()
subrepos: bool = False,
repo_factory: Optional[RepoFactory] = None,
**repo_kwargs: Any,
) -> None:
"""DVC + git-tracked files fs.
Args:
path (str, optional): URL or path to a DVC/Git repository.
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).
rev (str, optional): Any Git revision such as a branch or tag name,
a commit hash or a dvc experiment name.
Defaults to the default branch in case of remote repositories.
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.
The default is `Repo`.
Examples:
- Opening a filesystem from repo in current working directory
>>> fs = DVCFileSystem()
- Opening a filesystem from local repository
>>> fs = DVCFileSystem("path/to/local/repository")
- Opening a remote repository
>>> fs = DVCFileSystem(
... "https://github.com/iterative/example-get-started",
... rev="main",
... )
"""
from pygtrie import Trie
super().__init__()
if repo is None:
repo, repo_factory = self._repo_from_fs_config(
subrepos=subrepos, **kwargs
repo = self._make_repo(
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
)
assert repo
# pylint: disable=protected-access
repo_factory = repo._fs_conf["repo_factory"]
if not repo_factory:
from dvc.repo import Repo
@@ -137,78 +154,35 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
if hasattr(repo, "dvc_dir"):
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
def _get_key(self, path):
def _get_key(self, path) -> Key:
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
if parts == (".",):
parts = ()
return parts
def _get_key_from_relative(self, path) -> Key:
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
parts = parts[1:]
return parts
def _from_key(self, parts: Key) -> str:
return self.repo.fs.path.join(self.repo.root_dir, *parts)
@property
def repo_url(self):
if self.repo is None:
return None
return self.repo.url
@property
def config(self):
return {
self.PARAM_REPO_URL: self.repo_url,
self.PARAM_REPO_ROOT: self.repo.root_dir,
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
self.PARAM_CACHE_DIR: os.path.abspath(self.repo.odb.local.path),
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
self.PARAM_SUBREPOS: self._traverse_subrepos,
}
@classmethod
def _repo_from_fs_config(
cls, **config
) -> Tuple["Repo", Optional["RepoFactory"]]:
from dvc.external_repo import erepo_factory, external_repo
def _make_repo(cls, **kwargs) -> "Repo":
from dvc.repo import Repo
url = config.get(cls.PARAM_REPO_URL)
root = config.get(cls.PARAM_REPO_ROOT)
assert url or root
with Repo.open(uninitialized=True, **kwargs) as repo:
return repo
def _open(*args, **kwargs):
# NOTE: if original repo was an erepo (and has a URL),
# we cannot use Repo.open() since it will skip erepo
# cache/remote setup for local URLs
if url is None:
return Repo.open(*args, **kwargs)
return external_repo(*args, **kwargs)
cache_dir = config.get(cls.PARAM_CACHE_DIR)
cache_config = (
{}
if not cache_dir
else {
"cache": {
"dir": cache_dir,
"type": config.get(cls.PARAM_CACHE_TYPES),
}
}
)
repo_kwargs: dict = {
"rev": config.get(cls.PARAM_REV),
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
"uninitialized": True,
}
factory: Optional["RepoFactory"] = None
if url is None:
repo_kwargs["config"] = cache_config
else:
repo_kwargs["cache_dir"] = cache_dir
factory = erepo_factory(url, root, cache_config)
with _open(
url if url else root,
**repo_kwargs,
) as repo:
return repo, factory
def _get_repo(self, path: str) -> "Repo":
def _get_repo(self, key: Key) -> "Repo":
"""Returns repo that the path falls in, using prefix.
If the path is already tracked/collected, it just returns the repo.
@@ -216,33 +190,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
Otherwise, it collects the repos that might be in the path's parents
and then returns the appropriate one.
"""
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
# outside of repo
return self.repo
key = self._get_key(path)
repo = self._subrepos_trie.get(key)
if repo:
return repo
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
prefix = self.repo.fs.path.join(
self.repo.root_dir,
*prefix_key, # pylint: disable=not-an-iterable
)
parents = (parent for parent in self.repo.fs.path.parents(path))
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
dirs.reverse()
self._update(dirs, starting_repo=repo)
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
self._update(dir_keys, starting_repo=repo)
return self._subrepos_trie.get(key) or self.repo
@wrap_with(threading.Lock())
def _update(self, dirs, starting_repo):
def _update(self, dir_keys, starting_repo):
"""Checks for subrepo in directories and updates them."""
repo = starting_repo
for d in dirs:
key = self._get_key(d)
for key in dir_keys:
d = self._from_key(key)
if self._is_dvc_repo(d):
repo = self.repo_factory(
d,
@@ -265,79 +227,68 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
return self.repo.fs.isdir(repo_path)
def _get_fs_pair(
self, path
) -> Tuple[
Optional["Repo"],
Optional[FileSystem],
Optional[str],
Optional[DataFileSystem],
Optional[str],
]:
def _get_subrepo_info(
self, key: Key
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
"""
Returns a pair of fss based on repo the path falls in, using prefix.
Returns information about the subrepo the key is part of.
"""
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
parts = parts[1:]
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
repo = self._get_repo(fs_path)
fs = repo.fs
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
if repo_parts[0] == os.curdir:
repo_parts = repo_parts[1:]
dvc_parts = parts[len(repo_parts) :]
if dvc_parts and dvc_parts[0] == os.curdir:
dvc_parts = dvc_parts[1:]
key = self._get_key(repo.root_dir)
dvc_fs = self._datafss.get(key)
if dvc_fs:
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
repo = self._get_repo(key)
repo_key: Key
if repo is self.repo:
repo_key = ()
subkey = key
else:
dvc_path = None
repo_key = self._get_key(repo.root_dir)
subkey = key[len(repo_key) :]
return repo, fs, fs_path, dvc_fs, dvc_path
dvc_fs = self._datafss.get(repo_key)
return repo, dvc_fs, subkey
def open(
self, path, mode="r", encoding="utf-8", **kwargs
def _open(
self, path, mode="rb", **kwargs
): # pylint: disable=arguments-renamed, arguments-differ
if "b" in mode:
encoding = None
if mode != "rb":
raise OSError(errno.EROFS, os.strerror(errno.EROFS))
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
key = self._get_key_from_relative(path)
fs_path = self._from_key(key)
try:
return fs.open(fs_path, mode=mode, encoding=encoding)
return self.repo.fs.open(fs_path, mode=mode)
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.open(dvc_path, mode=mode)
def isdvc(self, path, **kwargs):
_, _, _, dvc_fs, dvc_path = self._get_fs_pair(path)
def isdvc(self, path, **kwargs) -> bool:
"""Is this entry dvc-tracked?"""
key = self._get_key_from_relative(path)
_, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
def ls( # pylint: disable=arguments-differ
self, path, detail=True, dvc_only=False, **kwargs
):
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
dvcignore = repo.dvcignore
ignore_subrepos = kwargs.get("ignore_subrepos", True)
key = self._get_key_from_relative(path)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
names = set()
if dvc_fs:
with suppress(FileNotFoundError):
dvc_path = _get_dvc_path(dvc_fs, subkey)
for entry in dvc_fs.ls(dvc_path, detail=False):
names.add(dvc_fs.path.name(entry))
if not dvc_only and fs:
ignore_subrepos = kwargs.get("ignore_subrepos", True)
if not dvc_only:
fs = self.repo.fs
fs_path = self._from_key(key)
try:
for entry in dvcignore.ls(
for entry in repo.dvcignore.ls(
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
):
names.add(fs.path.name(entry))
@@ -345,26 +296,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
pass
dvcfiles = kwargs.get("dvcfiles", False)
def _func(fname):
from dvc.dvcfile import is_valid_filename
from dvc.ignore import DvcIgnore
if dvcfiles:
return True
return not (
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
)
names = filter(_func, names)
if not dvcfiles:
names = (name for name in names if not _is_dvc_file(name))
infos = []
paths = []
for name in names:
entry_path = self.path.join(path, name)
entry_key = key + (name,)
try:
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
info = self._info(
entry_key,
entry_path,
ignore_subrepos=ignore_subrepos,
check_ignored=False,
)
except FileNotFoundError:
continue
infos.append(info)
@@ -375,48 +321,39 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
return infos
def get_file( # pylint: disable=arguments-differ
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
):
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
if fs:
try:
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
return
except FileNotFoundError:
if not dvc_fs:
raise
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
def info(self, path, **kwargs):
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
dvcignore = repo.dvcignore
key = self._get_key_from_relative(path)
ignore_subrepos = kwargs.get("ignore_subrepos", True)
return self._info(key, path, ignore_subrepos=ignore_subrepos)
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
repo, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_info = None
if dvc_fs:
try:
dvc_info = dvc_fs.info(dvc_path)
dvc_info = dvc_fs.fs.index.info(subkey)
dvc_path = _get_dvc_path(dvc_fs, subkey)
dvc_info["name"] = dvc_path
except FileNotFoundError:
pass
fs_info = None
if fs:
try:
fs_info = fs.info(fs_path)
if dvcignore.is_ignored(
fs, fs_path, ignore_subrepos=ignore_subrepos
):
fs_info = None
except (FileNotFoundError, NotADirectoryError):
if not dvc_info:
raise
fs = self.repo.fs
fs_path = self._from_key(key)
try:
fs_info = fs.info(fs_path)
if check_ignored and repo.dvcignore.is_ignored(
fs, fs_path, ignore_subrepos=ignore_subrepos
):
fs_info = None
except (FileNotFoundError, NotADirectoryError):
if not dvc_info:
raise
# NOTE: if some parent in fs_path turns out to be a file, it means
# that the whole repofs branch doesn't exist.
if fs and not fs_info and dvc_info:
if dvc_info and not fs_info:
for parent in fs.path.parents(fs_path):
try:
if fs.info(parent)["type"] != "directory":
@@ -432,16 +369,8 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
info["name"] = path
return info
def checksum(self, path):
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
try:
return fs.checksum(fs_path)
except FileNotFoundError:
return dvc_fs.checksum(dvc_path)
class DvcFileSystem(FileSystem):
class DVCFileSystem(FileSystem):
protocol = "local"
PARAM_CHECKSUM = "md5"
@@ -451,7 +380,7 @@ class DvcFileSystem(FileSystem):
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
return _DvcFileSystem(**self.fs_args)
return _DVCFileSystem(**self.fs_args)
def isdvc(self, path, **kwargs):
return self.fs.isdvc(path, **kwargs)
@@ -468,10 +397,6 @@ class DvcFileSystem(FileSystem):
def repo_url(self):
return self.fs.repo_url
@property
def config(self):
return self.fs.config
def from_os_path(self, path):
if os.path.isabs(path):
path = os.path.relpath(path, self.repo.root_dir)
+1
View File
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
"""Proxies the repo file access methods to Git objects"""
protocol = "local"
PARAM_CHECKSUM = "md5"
def __init__(
self,
+28 -3
View File
@@ -14,6 +14,14 @@ from dvc.scm import SCMError
from dvc.utils import error_link
from dvc.utils.pkg import PKG
SUBPROJECTS = (
"dvc_data",
"dvc_objects",
"dvc_render",
"dvc_task",
"dvclive",
"scmrepo",
)
package = "" if PKG is None else f"({PKG})"
@@ -23,6 +31,7 @@ def get_dvc_info():
"---------------------------------",
f"Platform: Python {platform.python_version()} on "
f"{platform.platform()}",
f"Subprojects:{_get_subprojects()}",
f"Supports:{_get_supported_remotes()}",
]
@@ -67,7 +76,7 @@ def _get_caches(cache):
def _get_remotes(config):
schemes = (
get_fs_cls(get_fs_config(None, config, name=remote)).protocol
get_fs_cls(get_fs_config(config, name=remote)).protocol
for remote in config["remote"]
)
@@ -88,13 +97,29 @@ def _get_linktype_support_info(repo):
return ", ".join(links)
def _get_subprojects():
subprojects = []
for subproject in SUBPROJECTS:
try:
version = importlib_metadata.version(subproject)
subprojects.append(f"{subproject} = {version}")
except ImportError:
pass
return "\n\t" + "\n\t".join(subprojects)
def _get_supported_remotes():
supported_remotes = []
for scheme in registry:
if scheme in [Schemes.LOCAL, Schemes.MEMORY]:
if scheme in [Schemes.LOCAL, Schemes.MEMORY, "dvc", "git"]:
continue
try:
fs_cls = registry[scheme]
except ImportError:
continue
fs_cls = registry[scheme]
if not fs_cls.get_missing_deps():
dependencies = []
for requirement in fs_cls.REQUIRES:
+5 -2
View File
@@ -213,8 +213,6 @@ def set_loggers_level(level: int = logging.INFO) -> None:
def setup(level: int = logging.INFO) -> None:
colorama.init()
if level >= logging.DEBUG:
# Unclosed session errors for asyncio/aiohttp are only available
# on the tracing mode for extensive debug purposes. They are really
@@ -297,3 +295,8 @@ def setup(level: int = logging.INFO) -> None:
"disable_existing_loggers": False,
}
)
# NOTE: colorama init needs to go after logging config to avoid potential
# conflicts with libs that modify logging (like tensorflow/abseil)
# https://github.com/iterative/dvc/issues/8387
colorama.init()
+2 -1
View File
@@ -3,7 +3,8 @@ from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator, Optional
if TYPE_CHECKING:
from dvc.fs import SSHFileSystem
from dvc_ssh import SSHFileSystem
from dvc.types import StrPath
+3 -2
View File
@@ -3,8 +3,9 @@ from contextlib import contextmanager
from functools import partial, partialmethod
from typing import TYPE_CHECKING, Iterator, Optional
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
from dvc.exceptions import DvcException
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
from dvc.utils.fs import makedirs
from .base import BaseMachineBackend
@@ -68,7 +69,7 @@ class TerraformBackend(BaseMachineBackend):
resource = tpi.default_resource(name)
return {
"host": resource["instance_ip"],
"port": DEFAULT_SSH_PORT,
"port": DEFAULT_PORT,
"username": "ubuntu",
"fs_factory": partial(_sshfs, dict(resource)),
}
+1 -1
View File
@@ -1,5 +1,5 @@
from dvc.fs import GitFileSystem, Schemes
from dvc_data.db import get_odb
from dvc_data.hashfile.db import get_odb
def _get_odb(repo, settings, fs=None):
+158 -56
View File
@@ -1,10 +1,10 @@
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urlparse
from funcy import collecting, project
from funcy import cached_property, collecting, project
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
from dvc import prompt
@@ -17,29 +17,25 @@ from dvc.exceptions import (
MergeError,
RemoteCacheRequiredError,
)
from dvc_data import Tree
from dvc_data import check as ocheck
from dvc_data import load as oload
from dvc_data.build import build
from dvc_data.checkout import checkout
from dvc_data.hashfile import Tree
from dvc_data.hashfile import check as ocheck
from dvc_data.hashfile import load as oload
from dvc_data.hashfile.build import build
from dvc_data.hashfile.checkout import checkout
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.istextfile import istextfile
from dvc_data.hashfile.meta import Meta
from dvc_data.transfer import transfer as otransfer
from dvc_data.hashfile.transfer import transfer as otransfer
from dvc_objects.errors import ObjectFormatError
from .fs import (
HDFSFileSystem,
LocalFileSystem,
RemoteMissingDepsError,
S3FileSystem,
Schemes,
get_cloud_fs,
)
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
from .utils import relpath
from .utils.fs import path_isin
if TYPE_CHECKING:
from dvc_data.hashfile.obj import HashFile
from dvc_data.index import DataIndexKey
from dvc_objects.db import ObjectDB
logger = logging.getLogger(__name__)
@@ -65,10 +61,12 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
#
# so when a few types of outputs share the same name, we only need
# specify it once.
HDFS_PARAM_CHECKSUM = "checksum"
S3_PARAM_CHECKSUM = "etag"
CHECKSUMS_SCHEMA = {
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
}
@@ -85,9 +83,10 @@ def loadd_from(stage, d_list):
plot = d.pop(Output.PARAM_PLOT, False)
persist = d.pop(Output.PARAM_PERSIST, False)
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
desc = d.pop(Output.PARAM_DESC, False)
live = d.pop(Output.PARAM_LIVE, False)
remote = d.pop(Output.PARAM_REMOTE, None)
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
files = d.pop(Output.PARAM_FILES, None)
ret.append(
_get(
stage,
@@ -98,9 +97,10 @@ def loadd_from(stage, d_list):
plot=plot,
persist=persist,
checkpoint=checkpoint,
desc=desc,
live=live,
remote=remote,
**annot,
files=files,
)
)
return ret
@@ -192,6 +192,7 @@ def load_from_pipeline(stage, data, typ="outs"):
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
Output.PARAM_REMOTE,
*ANNOTATION_FIELDS,
],
)
@@ -245,6 +246,7 @@ class Output:
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_CHECKPOINT = "checkpoint"
PARAM_FILES = "files"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
@@ -257,7 +259,6 @@ class Output:
PARAM_PLOT_TITLE = "title"
PARAM_PLOT_HEADER = "header"
PARAM_PERSIST = "persist"
PARAM_DESC = "desc"
PARAM_LIVE = "live"
PARAM_LIVE_SUMMARY = "summary"
PARAM_LIVE_HTML = "html"
@@ -289,11 +290,29 @@ class Output:
checkpoint=False,
live=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
meta=None,
remote=None,
repo=None,
fs_config=None,
files: List[Dict[str, Any]] = None,
):
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
)
self.repo = stage.repo if not repo and stage else repo
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
meta = Meta.from_dict(info or {})
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
# set based on whether or not path is versioned
fs_kwargs = {"version_aware": True} if meta.version_id else {}
if fs_config is not None:
fs_kwargs.update(**fs_config)
fs_cls, fs_config, fs_path = get_cloud_fs(
self.repo, url=path, **fs_kwargs
)
self.fs = fs_cls(**fs_config)
if (
@@ -325,21 +344,38 @@ class Output:
# By resolved path, which contains actual location,
# should be absolute and don't contain remote:// refs.
self.stage = stage
self.meta = Meta.from_dict(info)
self.hash_info = HashInfo.from_dict(info)
self.meta = meta
self.files = files
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.checkpoint = checkpoint
self.live = live
self.desc = desc
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj = None
self.obj: Optional["HashFile"] = None
self.remote = remote
if self.fs.version_aware:
_, version_id = self.fs.path.coalesce_version(
self.def_path, self.meta.version_id
)
self.meta.version_id = version_id
if self.is_in_repo:
self.hash_name = "md5"
else:
self.hash_name = self.fs.PARAM_CHECKSUM
self.hash_info = HashInfo(
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
if self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
def _parse_path(self, fs, fs_path):
parsed = urlparse(self.def_path)
if (
@@ -380,6 +416,12 @@ class Output:
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
def clear(self):
self.hash_info = HashInfo.from_dict({})
self.meta = Meta.from_dict({})
self.obj = None
self.files = None
@property
def protocol(self):
return self.fs.protocol
@@ -419,21 +461,27 @@ class Output:
)
def get_hash(self):
_, hash_info = self._get_hash_meta()
return hash_info
def _get_hash_meta(self):
if self.use_cache:
odb = self.odb
name = self.odb.fs.PARAM_CHECKSUM
else:
odb = self.repo.odb.local
name = self.fs.PARAM_CHECKSUM
_, _, obj = build(
_, meta, obj = build(
odb,
self.fs_path,
self.fs,
name,
self.hash_name,
ignore=self.dvcignore,
dry_run=not self.use_cache,
)
return obj.hash_info
return meta, obj.hash_info
def get_meta(self) -> Meta:
meta, _ = self._get_hash_meta()
return meta
@property
def is_dir_checksum(self):
@@ -452,6 +500,17 @@ class Output:
return self.fs.exists(self.fs_path)
@cached_property
def index_key(self) -> Tuple[str, "DataIndexKey"]:
if self.is_in_repo:
workspace = "repo"
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
else:
workspace = self.fs.protocol
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
key = self.fs.path.parts(no_drive)[1:]
return workspace, key
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -469,6 +528,11 @@ class Output:
except (FileNotFoundError, ObjectFormatError):
return True
def changed_meta(self) -> bool:
if self.fs.version_aware and self.meta.version_id:
return self.meta.version_id == self.get_meta().version_id
return False
def workspace_status(self):
if not self.exists:
return {str(self): "deleted"}
@@ -551,11 +615,12 @@ class Output:
self.repo.odb.local,
self.fs_path,
self.fs,
self.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
dry_run=True,
)
self.hash_info = obj.hash_info
self.files = None
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
@@ -568,18 +633,19 @@ class Output:
self.odb,
self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
self.hash_info = self.obj.hash_info
self.files = None
def set_exec(self):
if self.isfile() and self.meta.isexec:
self.odb.set_exec(self.fs_path)
def _checkout(self, *args, **kwargs):
from dvc_data.checkout import CheckoutError as _CheckoutError
from dvc_data.checkout import LinkError, PromptError
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
from dvc_data.hashfile.checkout import LinkError, PromptError
kwargs.setdefault("ignore", self.dvcignore)
try:
@@ -610,7 +676,7 @@ class Output:
self.odb,
filter_info or self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
otransfer(
@@ -639,7 +705,7 @@ class Output:
self.odb,
self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
save_obj = save_obj.filter(prefix)
@@ -653,8 +719,10 @@ class Output:
)
return checkout_obj
def dumpd(self):
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
def dumpd(self, **kwargs):
meta = self.meta.to_dict()
meta.pop("isdir", None)
ret = {**self.hash_info.to_dict(), **meta}
if self.is_in_repo:
path = self.fs.path.as_posix(
@@ -668,9 +736,7 @@ class Output:
if self.IS_DEPENDENCY:
return ret
if self.desc:
ret[self.PARAM_DESC] = self.desc
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
@@ -699,6 +765,15 @@ class Output:
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if self.hash_info.isdir and (
kwargs.get("with_files") or self.files is not None
):
if self.obj:
obj = self.obj
else:
obj = self.get_obj()
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
return ret
def verify_metric(self):
@@ -731,14 +806,22 @@ class Output:
) as cb:
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
def get_obj(self, filter_info=None, **kwargs):
def get_obj(
self, filter_info: Optional[str] = None, **kwargs
) -> Optional["HashFile"]:
obj: Optional["HashFile"] = None
if self.obj:
obj = self.obj
elif self.hash_info:
try:
obj = oload(self.odb, self.hash_info)
except FileNotFoundError:
return None
if self.files:
tree = Tree.from_list(self.files, hash_name=self.hash_name)
tree.digest()
obj = tree
else:
try:
obj = oload(self.odb, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
return None
else:
return None
@@ -858,6 +941,7 @@ class Output:
)
self.hash_info = obj.hash_info
self.files = None
return obj
def get_files_number(self, filter_info=None):
@@ -928,6 +1012,7 @@ class Output:
obj = self.get_obj()
if filter_info and filter_info != self.fs_path:
assert obj
prefix = self.fs.path.parts(
self.fs.path.relpath(filter_info, self.fs_path)
)
@@ -1020,7 +1105,7 @@ class Output:
other = out.dumpd()
ignored = [
self.fs.PARAM_CHECKSUM,
self.hash_name,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
]
@@ -1039,9 +1124,9 @@ class Output:
"unable to auto-merge outputs that are not directories"
)
def merge(self, ancestor, other):
from dvc_data.objects.tree import MergeError as TreeMergeError
from dvc_data.objects.tree import du, merge
def merge(self, ancestor, other, allowed=None):
from dvc_data.hashfile.tree import MergeError as TreeMergeError
from dvc_data.hashfile.tree import du, merge
assert other
@@ -1056,7 +1141,11 @@ class Output:
try:
merged = merge(
self.odb, ancestor_info, self.hash_info, other.hash_info
self.odb,
ancestor_info,
self.hash_info,
other.hash_info,
allowed=allowed,
)
except TreeMergeError as exc:
raise MergeError(str(exc)) from exc
@@ -1064,6 +1153,7 @@ class Output:
self.odb.add(merged.path, merged.fs, merged.oid)
self.hash_info = merged.hash_info
self.files = None
self.meta = Meta(
size=du(self.odb, merged),
nfiles=len(merged),
@@ -1086,21 +1176,33 @@ class Output:
return bool(self.plot) or bool(self.live)
META_SCHEMA = {
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
Meta.PARAM_VERSION_ID: str,
}
ARTIFACT_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Output.PARAM_PATH): str,
Output.PARAM_PLOT: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
}
DIR_FILES_SCHEMA: Dict[str, Any] = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Tree.PARAM_RELPATH): str,
}
SCHEMA = {
**ARTIFACT_SCHEMA,
**ANNOTATION_SCHEMA,
Output.PARAM_CACHE: bool,
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
Output.PARAM_DESC: str,
Output.PARAM_REMOTE: str,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
+1 -1
View File
@@ -159,7 +159,7 @@ class Value(Node):
PRIMITIVES = (int, float, str, bytes, bool)
class Container(Node, ABC):
class Container(Node, ABC): # noqa: B024
meta: Meta
data: Union[list, dict]
_key_transform = staticmethod(identity)
+8 -2
View File
@@ -2,7 +2,13 @@ import json
from collections import defaultdict
from typing import Dict, List, Union
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY
from dvc.render import (
REVISION_FIELD,
REVISIONS_KEY,
SRC_FIELD,
TYPE_KEY,
VERSION_FIELD,
)
from dvc.render.converter.image import ImageConverter
from dvc.render.converter.vega import VegaConverter
@@ -23,7 +29,7 @@ def _get_converter(
def _group_by_rev(datapoints):
grouped = defaultdict(list)
for datapoint in datapoints:
rev = datapoint.pop(REVISION_FIELD)
rev = datapoint.get(VERSION_FIELD, {}).get("revision")
grouped[rev].append(datapoint)
return dict(grouped)
+20 -14
View File
@@ -127,13 +127,20 @@ class VegaConverter(Converter):
),
)
)
self.steps.append(
(
"infer_y",
partial(
self._infer_y,
),
)
)
self.steps.append(
(
"generate_y",
partial(
self._generate_y_values,
y_values=self.plot_properties.get("y", None),
),
)
)
@@ -156,7 +163,7 @@ class VegaConverter(Converter):
} - {None}
self.inferred_properties["fields"] = fields
def _infer_y(self, datapoints: List[Dict]):
def _infer_y(self, datapoints: List[Dict], **kwargs):
if "y" not in self.plot_properties:
data_fields = list(first(datapoints))
skip = (
@@ -178,6 +185,7 @@ class VegaConverter(Converter):
)
else:
self.inferred_properties["y"] = inferred_y
return datapoints
def convert(
self,
@@ -197,7 +205,6 @@ class VegaConverter(Converter):
processed = deepcopy(data)
for step_name, step in self.steps:
if step_name not in skip:
processed = step( # type: ignore
processed,
@@ -205,25 +212,28 @@ class VegaConverter(Converter):
filename=filename,
)
self._infer_y(processed) # type: ignore
return processed, {**self.plot_properties, **self.inferred_properties}
def _generate_y_values( # noqa: C901
self,
datapoints: List[Dict],
y_values: Optional[Union[str, List, Dict]],
revision: str,
filename: str,
**kwargs,
) -> List[Dict]:
y_values = self.plot_properties.get(
"y", None
) or self.inferred_properties.get("y", None)
assert y_values is not None
result = []
properties_update = {}
def _add_version_info(datapoint, version_info):
tmp = datapoint.copy()
tmp[VERSION_FIELD] = version_info
tmp[REVISION_FIELD] = version_info["revision"]
return tmp
def _version_info(revision, filename=None, field=None):
@@ -250,12 +260,6 @@ class VegaConverter(Converter):
return tmp
if not y_values:
for dp in datapoints:
result.append(
_add_version_info(dp, _version_info(revision, filename))
)
if isinstance(y_values, str):
for datapoint in datapoints:
result.append(
@@ -290,13 +294,15 @@ class VegaConverter(Converter):
):
# if we use the same field from all files,
# we dont have to generate it
y_field = all_fields.pop()
for datapoint in datapoints:
result.append(
_add_version_info(
datapoint, _version_info(revision, filename)
datapoint,
_version_info(revision, filename, y_field),
)
)
properties_update.update({"y": all_fields.pop()})
properties_update.update({"y": y_field})
else:
for def_filename, val in y_values.items():
if isinstance(val, str):
+47 -6
View File
@@ -3,7 +3,7 @@ import os
from collections import defaultdict
from contextlib import contextmanager
from functools import wraps
from typing import TYPE_CHECKING, Callable, Optional
from typing import TYPE_CHECKING, Callable, List, Optional
from funcy import cached_property
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc.repo.scm_context import SCMContext
from dvc.scm import Base
from dvc.stage import Stage
logger = logging.getLogger(__name__)
@@ -67,6 +68,7 @@ class Repo:
from dvc.repo.imp_url import imp_url # type: ignore[misc]
from dvc.repo.install import install # type: ignore[misc]
from dvc.repo.ls import ls as _ls # type: ignore[misc]
from dvc.repo.ls_url import ls_url as _ls_url # type: ignore[misc]
from dvc.repo.move import move # type: ignore[misc]
from dvc.repo.pull import pull # type: ignore[misc]
from dvc.repo.push import push # type: ignore[misc]
@@ -79,6 +81,7 @@ class Repo:
from .data import status as data_status # type: ignore[misc]
ls = staticmethod(_ls)
ls_url = staticmethod(_ls_url)
get = staticmethod(_get)
get_url = staticmethod(_get_url)
@@ -186,7 +189,7 @@ class Repo:
self.config = Config(self.dvc_dir, fs=self.fs, config=config)
self._uninitialized = uninitialized
# used by DvcFileSystem to determine if it should traverse subrepos
# used by DVCFileSystem to determine if it should traverse subrepos
self.subrepos = subrepos
self.cloud = DataCloud(self)
@@ -444,6 +447,44 @@ class Repo:
return used
def partial_imports(
self,
targets=None,
all_branches=False,
all_tags=False,
all_commits=False,
all_experiments=False,
commit_date: Optional[str] = None,
recursive=False,
revs=None,
num=1,
) -> List["Stage"]:
"""Get the stages related to the given target and collect dependencies
which are missing outputs.
This is useful to retrieve files which have been imported to the repo
using --no-download.
Returns:
A list of partially imported stages
"""
from itertools import chain
partial_imports = chain.from_iterable(
self.index.partial_imports(targets, recursive=recursive)
for _ in self.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
all_experiments=all_experiments,
commit_date=commit_date,
num=num,
)
)
return list(partial_imports)
@property
def stages(self): # obsolete, only for backward-compatibility
return self.index.stages
@@ -487,9 +528,9 @@ class Repo:
@cached_property
def dvcfs(self):
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
return DvcFileSystem(
return DVCFileSystem(
repo=self, subrepos=self.subrepos, **self._fs_conf
)
@@ -501,13 +542,13 @@ class Repo:
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
from dvc.fs.data import DataFileSystem
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
if os.path.isabs(path):
fs = DataFileSystem(index=self.index.data["local"])
fs_path = path
else:
fs = DvcFileSystem(repo=self, subrepos=True)
fs = DVCFileSystem(repo=self, subrepos=True)
fs_path = fs.from_os_path(path)
try:
+3 -2
View File
@@ -264,6 +264,7 @@ def create_stages(
outs=[out],
external=external,
)
if kwargs.get("desc"):
stage.outs[0].desc = kwargs["desc"]
out_obj = stage.outs[0]
out_obj.annot.update(**kwargs)
yield stage
+2 -2
View File
@@ -30,9 +30,9 @@ def _collect_paths(
recursive: bool = False,
rev: str = None,
) -> StrPaths:
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
fs = DvcFileSystem(repo=repo)
fs = DVCFileSystem(repo=repo)
fs_paths = [fs.from_os_path(target) for target in targets]
target_paths: StrPaths = []
+136 -79
View File
@@ -1,7 +1,21 @@
import os
import posixpath
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, cast
from itertools import chain
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
TypedDict,
cast,
)
from dvc.fs.git import GitFileSystem
from dvc.ui import ui
if TYPE_CHECKING:
@@ -10,19 +24,20 @@ if TYPE_CHECKING:
from dvc.output import Output
from dvc.repo import Repo
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.obj import HashFile
def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
def posixpath_to_os_path(path: str) -> str:
return path.replace(posixpath.sep, os.path.sep)
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
from dvc_objects.errors import ObjectFormatError
if not obj:
return False
if not obj.hash_info.value:
return False
assert obj.value
try:
cache.check(obj.hash_info.value)
cache.check(obj.value)
return True
except (FileNotFoundError, ObjectFormatError):
return False
@@ -30,31 +45,30 @@ def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
def _shallow_diff(
root: str,
old_obj: Optional["HashFile"],
new_obj: Optional["HashFile"],
old: Optional["HashInfo"],
new: Optional["HashInfo"],
cache: "HashFileDB",
) -> Dict[str, List[str]]:
# TODO: add support for shallow diff in dvc-data
# TODO: we may want to recursively do in_cache check
d = {}
from dvc_data.objects.tree import Tree
old_root = new_root = root
if old and old.isdir:
old_root = os.path.sep.join([root, ""])
if new and new.isdir:
new_root = os.path.sep.join([root, ""])
if isinstance(new_obj, Tree):
root = os.path.sep.join([root, ""])
if old and not _in_cache(old, cache):
d["not_in_cache"] = [old_root]
if not _in_cache(old_obj, cache):
d["not_in_cache"] = [root]
if old_obj is None and new_obj is None:
if not old and not new:
return d
if old_obj is None:
return {"added": [root], **d}
if new_obj is None:
return {"deleted": [root], **d}
if old_obj.hash_info != new_obj.hash_info:
return {"modified": [root], **d}
return {"unchanged": [root], **d}
if not new:
return {"deleted": [old_root], **d}
if not old:
return {"added": [new_root], **d}
if old != new:
return {"modified": [new_root], **d}
return {"unchanged": [new_root], **d}
def _granular_diff(
@@ -62,19 +76,12 @@ def _granular_diff(
old_obj: Optional["HashFile"],
new_obj: Optional["HashFile"],
cache: "HashFileDB",
with_dirs: bool = False,
) -> Dict[str, List[str]]:
from dvc_data.diff import ROOT
from dvc_data.diff import diff as odiff
from dvc_data.objects.tree import Tree
from dvc_data.hashfile.diff import ROOT
from dvc_data.hashfile.diff import diff as odiff
drop_root = False
trees = isinstance(old_obj, Tree) or isinstance(new_obj, Tree)
if trees:
drop_root = not with_dirs
def path_join(root: str, *paths: str) -> str:
if not trees and paths == ROOT:
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
if not isdir and paths == ROOT:
return root
return os.path.sep.join([root, *paths])
@@ -83,38 +90,46 @@ def _granular_diff(
output: Dict[str, List[str]] = defaultdict(list)
for state in ("added", "deleted", "modified", "unchanged"):
items = getattr(diff_data, state)
output[state].extend(
path_join(root, *item.new.key)
for item in items
if not (drop_root and item.new.key == ROOT)
)
# TODO: PERF: diff is checking not_in_cache for each even if we only
# need it for the index.
# BUG: not_in_cache file also shows up as modified in staged and
# unstaged. We currently don't know if it is really modified.
output["not_in_cache"].extend(
path_join(root, *item.new.key)
for item in items
if not item.old.in_cache
and not (drop_root and item.new.key == ROOT)
and state != "added"
)
return output
for item in items: # pylint: disable=not-an-iterable
entry = item.old if state == "deleted" else item.new
isdir = entry.oid.isdir if entry.oid else False
path = path_join(root, *entry.key, isdir=isdir)
output[state].append(path)
if not item.old.in_cache and state != "added":
output["not_in_cache"].append(path)
return dict(output)
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
if not obj:
return []
from dvc_data.hashfile.tree import Tree
sep = os.path.sep
if isinstance(obj, Tree):
return [sep.join([root, *key]) for key, _, _ in obj]
return [root]
def _diff(
root: str,
old_oid: Optional["HashInfo"],
old_obj: Optional["HashFile"],
new_oid: Optional["HashInfo"],
new_obj: Optional["HashFile"],
cache: "HashFileDB",
odb: "HashFileDB",
granular: bool = False,
with_dirs: bool = False,
) -> Dict[str, List[str]]:
if granular:
return _granular_diff(
root, old_obj, new_obj, cache, with_dirs=with_dirs
)
return _shallow_diff(root, old_obj, new_obj, cache)
if not granular:
return _shallow_diff(root, old_oid, new_oid, odb)
if (old_oid and not old_obj) or (new_oid and not new_obj):
# we don't have enough information to give full details
unknown = _get_obj_items(root, new_obj)
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
return {**shallow_diff, "unknown": unknown}
return _granular_diff(root, old_obj, new_obj, odb)
class GitInfo(TypedDict, total=False):
@@ -141,6 +156,8 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
empty_repo = False
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
if os.name == "nt":
untracked = [posixpath_to_os_path(path) for path in untracked]
# NOTE: order is important here.
return GitInfo(
staged=staged,
@@ -152,7 +169,7 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
from dvc_data.build import build
from dvc_data.hashfile.build import build
unstaged_diff = defaultdict(list)
for out in repo.index.outs:
@@ -175,8 +192,18 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
cache = repo.odb.repo
root = str(out)
old = out.get_obj()
with ui.status(f"Calculating diff for {root} between index/workspace"):
d = _diff(root, old, new, cache, **kwargs)
d = _diff(
root,
out.hash_info,
old,
new.hash_info if new else None,
new,
cache,
**kwargs,
)
for state, items in d.items():
if not items:
continue
@@ -188,7 +215,9 @@ def _diff_head_to_index(
repo: "Repo", head: str = "HEAD", **kwargs: Any
) -> Dict[str, List[str]]:
# we need to store objects from index and the HEAD to diff later
objs: Dict[str, Dict[str, "HashFile"]] = defaultdict(dict)
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
objs = defaultdict(dict)
staged_diff = defaultdict(list)
for rev in repo.brancher(revs=[head]):
for out in repo.index.outs:
@@ -197,15 +226,20 @@ def _diff_head_to_index(
continue
root = str(out)
if isinstance(out.fs, GitFileSystem):
root = posixpath_to_os_path(root)
typ = "index" if rev == "workspace" else head
objs[root][typ] = out.get_obj()
objs[root][typ] = (out.get_obj(), out.hash_info)
cache = repo.odb.repo
for root, obj_d in objs.items():
old = obj_d.get(head, None)
new = obj_d.get("index", None)
old_obj, old_oid = obj_d.get(head, (None, None))
new_obj, new_oid = obj_d.get("index", (None, None))
with ui.status(f"Calculating diff for {root} between head/index"):
d = _diff(root, old, new, cache, **kwargs)
d = _diff(
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
)
for state, items in d.items():
if not items:
continue
@@ -216,28 +250,36 @@ def _diff_head_to_index(
class Status(TypedDict):
not_in_cache: List[str]
committed: Dict[str, Any]
uncommitted: Dict[str, Any]
committed: Dict[str, List[str]]
uncommitted: Dict[str, List[str]]
untracked: List[str]
unchanged: List[str]
git: GitInfo
def _transform_git_paths_to_dvc(repo: "Repo", files: List[str]):
def _transform_git_paths_to_dvc(
repo: "Repo", files: Iterable[str]
) -> List[str]:
"""Transform files rel. to Git root to DVC root, and drop outside files."""
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
if rel in (os.curdir, ""):
return files
prefix = rel + os.sep
length = len(prefix)
return [file[length:] for file in files if file.startswith(prefix)]
# if we have repo root in a different location than scm's root,
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
# repo root and anything outside need to be filtered out.
if rel not in (os.curdir, ""):
prefix = rel + os.sep
length = len(prefix)
files = (file[length:] for file in files if file.startswith(prefix))
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
if start in (os.curdir, ""):
return list(files)
# we need to convert repo relative paths to curdir relative.
return [repo.fs.path.relpath(file, start) for file in files]
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
from scmrepo.exceptions import SCMError
from dvc.scm import NoSCMError
from dvc.scm import NoSCMError, SCMError
head = kwargs.pop("head", "HEAD")
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
@@ -265,3 +307,18 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
unchanged=list(unchanged),
git=git_info,
)
def ls(
repo: "Repo",
targets: List[Optional[str]] = None,
recursive: bool = False,
) -> Iterator[Dict[str, Any]]:
targets = targets or [None]
pairs = chain.from_iterable(
repo.stage.collect_granular(target, recursive=recursive)
for target in targets
)
for stage, filter_info in pairs:
for out in stage.filter_outs(filter_info):
yield {"path": str(out), **out.annot.to_dict()}
+3 -3
View File
@@ -23,9 +23,9 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
if self.scm.no_commits:
return {}
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
dvcfs = DvcFileSystem(repo=self)
dvcfs = DVCFileSystem(repo=self)
targets = ensure_list(targets)
targets = [dvcfs.from_os_path(target) for target in targets]
@@ -115,7 +115,7 @@ def _paths_checksums(repo, targets):
def _output_paths(repo, targets):
from dvc.fs import LocalFileSystem
from dvc_data.build import build
from dvc_data.hashfile.build import build
on_working_fs = isinstance(repo.fs, LocalFileSystem)
+3 -1
View File
@@ -16,7 +16,7 @@ from .exceptions import (
InvalidExpRefError,
MultipleBranchError,
)
from .executor.base import BaseExecutor, ExecutorInfo
from .executor.base import BaseExecutor, ExecutorInfo, TaskStatus
from .queue.base import BaseStashQueue, QueueEntry
from .queue.celery import LocalCeleryQueue
from .queue.tempdir import TempDirQueue
@@ -463,6 +463,8 @@ class Experiments:
if rev == "workspace":
# If we are appending to a checkpoint branch in a workspace
# run, show the latest checkpoint as running.
if info.status > TaskStatus.RUNNING:
return result
last_rev = self.scm.get_ref(EXEC_BRANCH)
if last_rev:
result[last_rev] = info.asdict()
+78 -18
View File
@@ -4,6 +4,7 @@ import pickle
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from enum import IntEnum
from functools import partial
from typing import (
TYPE_CHECKING,
@@ -35,6 +36,8 @@ from ..refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
EXPS_STASH,
@@ -47,10 +50,9 @@ if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
from dvc.repo.experiments.stash import ExpStashEntry
from dvc.stage import PipelineStage
from ..base import ExpStashEntry
logger = logging.getLogger(__name__)
@@ -64,6 +66,16 @@ class ExecutorResult(NamedTuple):
force: bool
class TaskStatus(IntEnum):
PENDING = 0
PREPARING = 1
RUNNING = 2
SUCCESS = 3
FAILED = 4
CANCELED = 5
FINISHED = 6
@dataclass
class ExecutorInfo:
git_url: str
@@ -73,13 +85,16 @@ class ExecutorInfo:
dvc_dir: str
name: Optional[str] = None
wdir: Optional[str] = None
collected: bool = False
result_hash: Optional[str] = None
result_ref: Optional[str] = None
result_force: bool = False
status: TaskStatus = TaskStatus.PENDING
@classmethod
def from_dict(cls, d):
if "collected" in d:
if d.pop("collected"):
d["status"] = TaskStatus.FINISHED
return cls(**d)
def asdict(self):
@@ -136,6 +151,7 @@ class BaseExecutor(ABC):
root_dir: str,
dvc_dir: str,
baseline_rev: str,
status: TaskStatus,
wdir: Optional[str] = None,
name: Optional[str] = None,
location: Optional[str] = None,
@@ -149,9 +165,17 @@ class BaseExecutor(ABC):
self.baseline_rev = baseline_rev
self.location: str = location or self.DEFAULT_LOCATION
self.result = result
self.status = status
@abstractmethod
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
"""Init git repo and populate it using exp refs from the specified
SCM instance.
"""
@@ -191,6 +215,7 @@ class BaseExecutor(ABC):
dvc_dir=self.dvc_dir,
name=self.name,
wdir=self.wdir,
status=self.status,
**result_dict,
)
@@ -212,6 +237,7 @@ class BaseExecutor(ABC):
root_dir=info.root_dir,
dvc_dir=info.dvc_dir,
baseline_rev=info.baseline_rev,
status=info.status,
name=info.name,
wdir=info.wdir,
result=result,
@@ -222,7 +248,6 @@ class BaseExecutor(ABC):
def from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
) -> _T:
@@ -232,7 +257,6 @@ class BaseExecutor(ABC):
def _from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
root_dir: str,
**kwargs,
@@ -241,12 +265,11 @@ class BaseExecutor(ABC):
root_dir=root_dir,
dvc_dir=relpath(repo.dvc_dir, repo.scm.root_dir),
baseline_rev=entry.baseline_rev,
status=TaskStatus.PREPARING,
name=entry.name,
wdir=relpath(os.getcwd(), repo.scm.root_dir),
**kwargs,
)
executor.init_git(repo.scm, branch=entry.branch)
executor.init_cache(repo, stash_rev)
return executor
@staticmethod
@@ -259,8 +282,12 @@ class BaseExecutor(ABC):
exp_data.update(to_lockfile(stage))
return dict_sha256(exp_data)
def cleanup(self):
pass
def cleanup(self, infofile: str):
if infofile is not None:
info = ExecutorInfo.load_json(infofile)
if info.status < TaskStatus.FAILED:
info.status = TaskStatus.FINISHED
info.dump_json(infofile)
# TODO: come up with better way to stash repro arguments
@staticmethod
@@ -324,11 +351,12 @@ class BaseExecutor(ABC):
logger.debug("Replacing existing experiment '%s'", orig_ref)
return True
if on_diverged:
return on_diverged(orig_ref, has_checkpoint)
self._raise_ref_conflict(
dest_scm, orig_ref, new_rev, has_checkpoint
)
if on_diverged:
on_diverged(orig_ref, has_checkpoint)
logger.debug("Reproduced existing experiment '%s'", orig_ref)
return False
@@ -411,11 +439,9 @@ class BaseExecutor(ABC):
exp_ref: Optional["ExpRefInfo"] = None
repro_force: bool = False
if infofile is not None:
info.dump_json(infofile)
with cls._repro_dvc(
info,
infofile,
log_errors=log_errors,
**kwargs,
) as dvc:
@@ -491,9 +517,6 @@ class BaseExecutor(ABC):
info.result_ref = ref
info.result_force = repro_force
if infofile is not None:
info.dump_json(infofile)
# ideally we would return stages here like a normal repro() call, but
# stages is not currently picklable and cannot be returned across
# multiprocessing calls
@@ -550,6 +573,7 @@ class BaseExecutor(ABC):
def _repro_dvc(
cls,
info: "ExecutorInfo",
infofile: str = None,
log_errors: bool = True,
**kwargs,
):
@@ -557,6 +581,9 @@ class BaseExecutor(ABC):
from dvc.stage.monitor import CheckpointKilledError
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
info.status = TaskStatus.RUNNING
if infofile is not None:
info.dump_json(infofile)
if cls.QUIET:
dvc.scm_context.quiet = cls.QUIET
old_cwd = os.getcwd()
@@ -568,15 +595,28 @@ class BaseExecutor(ABC):
try:
logger.debug("Running repro in '%s'", os.getcwd())
yield dvc
info.status = TaskStatus.SUCCESS
if infofile is not None:
info.dump_json(infofile)
except CheckpointKilledError:
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except DvcException:
if log_errors:
logger.exception("")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except Exception:
if log_errors:
logger.exception("unexpected error")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
finally:
dvc.close()
@@ -714,3 +754,23 @@ class BaseExecutor(ABC):
disable_other_loggers()
if level is not None:
dvc_logger.setLevel(level)
@contextmanager
def set_exec_refs(
self, scm: "Git", stash_rev: str, entry: "ExpStashEntry"
):
try:
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
yield
finally:
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
if scm.get_ref(ref):
scm.remove_ref(ref)
+42 -23
View File
@@ -19,7 +19,7 @@ from ..refs import (
EXEC_MERGE,
EXEC_NAMESPACE,
)
from .base import EXEC_TMP_DIR, BaseExecutor
from .base import EXEC_TMP_DIR, BaseExecutor, TaskStatus
if TYPE_CHECKING:
from scmrepo.git import Git
@@ -46,8 +46,8 @@ class BaseLocalExecutor(BaseExecutor):
def scm(self):
return SCM(self.root_dir)
def cleanup(self):
super().cleanup()
def cleanup(self, infofile: str):
super().cleanup(infofile)
self.scm.close()
del self.scm
@@ -67,15 +67,28 @@ class TempDirExecutor(BaseLocalExecutor):
QUIET = True
DEFAULT_LOCATION = "tempdir"
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
from dulwich.repo import Repo as DulwichRepo
from ..utils import push_refspec
DulwichRepo.init(os.fspath(self.root_dir))
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec)
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
with self.set_exec_refs(scm, stash_rev, entry):
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec)
if branch:
push_refspec(scm, self.git_url, branch, branch)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
@@ -84,17 +97,12 @@ class TempDirExecutor(BaseLocalExecutor):
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
head = EXEC_BRANCH if branch else EXEC_HEAD
self.scm.checkout(head, detach=True)
merge_rev = self.scm.get_ref(EXEC_MERGE)
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
if scm.get_ref(ref):
scm.remove_ref(ref)
try:
self.scm.merge(merge_rev, squash=True, commit=False)
except _SCMError as exc:
@@ -114,8 +122,8 @@ class TempDirExecutor(BaseLocalExecutor):
"""Initialize DVC cache."""
self._config(repo.odb.repo.path)
def cleanup(self):
super().cleanup()
def cleanup(self, infofile: str):
super().cleanup(infofile)
logger.debug("Removing tmpdir '%s'", self.root_dir)
remove(self.root_dir)
@@ -123,7 +131,6 @@ class TempDirExecutor(BaseLocalExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
wdir: Optional[str] = None,
**kwargs,
@@ -132,9 +139,7 @@ class TempDirExecutor(BaseLocalExecutor):
makedirs(parent_dir, exist_ok=True)
tmp_dir = mkdtemp(dir=parent_dir)
try:
executor = cls._from_stash_entry(
repo, stash_rev, entry, tmp_dir, **kwargs
)
executor = cls._from_stash_entry(repo, entry, tmp_dir, **kwargs)
logger.debug("Init temp dir executor in '%s'", tmp_dir)
return executor
except Exception:
@@ -152,18 +157,31 @@ class WorkspaceExecutor(BaseLocalExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
):
root_dir = repo.scm.root_dir
executor = cls._from_stash_entry(
repo, stash_rev, entry, root_dir, **kwargs
executor: "WorkspaceExecutor" = cls._from_stash_entry(
repo, entry, root_dir, **kwargs
)
logger.debug("Init workspace executor in '%s'", root_dir)
return executor
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
self._detach_stack.enter_context(
self.scm.detach_head(
self.scm.get_ref(EXEC_HEAD),
@@ -184,7 +202,9 @@ class WorkspaceExecutor(BaseLocalExecutor):
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
pass
def cleanup(self):
def cleanup(self, infofile: str):
super().cleanup(infofile)
remove(os.path.dirname(infofile))
with self._detach_stack:
self.scm.remove_ref(EXEC_BASELINE)
self.scm.remove_ref(EXEC_MERGE)
@@ -193,4 +213,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
if checkpoint and checkpoint != self._orig_checkpoint:
self.scm.set_ref(EXEC_APPLY, checkpoint)
super().cleanup()
@@ -1,269 +0,0 @@
import logging
import os
from abc import ABC
from collections import defaultdict, deque
from collections.abc import Mapping
from typing import TYPE_CHECKING, Deque, Dict, Generator, Optional, Tuple, Type
from dvc.proc.manager import ProcessManager
from ...exceptions import CheckpointExistsError, ExperimentExistsError
from ...refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
from ...stash import ExpStashEntry
from ..base import EXEC_PID_DIR, BaseExecutor
from ..local import TempDirExecutor, WorkspaceExecutor
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class BaseExecutorManager(ABC, Mapping):
"""Manages executors for a collection of experiments to be run."""
EXECUTOR_CLS: Type = BaseExecutor
def __init__(
self,
scm: "Git",
wdir: str,
**kwargs,
):
from dvc.utils.fs import makedirs
self.scm = scm
makedirs(wdir, exist_ok=True)
self.wdir = wdir
self.proc = ProcessManager(self.pid_dir)
self._attached: Dict[str, "BaseExecutor"] = {}
self._detached: Dict[str, "BaseExecutor"] = dict(self._load_infos())
self._queue: Deque[Tuple[str, "BaseExecutor"]] = deque()
def __getitem__(self, key: str) -> "BaseExecutor":
try:
return self._attached[key]
except KeyError:
pass
return self._detached[key]
def __iter__(self):
yield from self._attached
yield from self._detached
def __len__(self):
return len(self._attached) + len(self._detached)
@property
def pid_dir(self) -> str:
return os.path.join(self.wdir, EXEC_PID_DIR)
def enqueue(self, rev: str, executor: "BaseExecutor"):
assert rev not in self
self._queue.append((rev, executor))
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
import json
from urllib.parse import urlparse
from ..base import ExecutorInfo
from ..ssh import SSHExecutor
def make_executor(info: "ExecutorInfo"):
if info.git_url:
scheme = urlparse(info.git_url).scheme
if scheme == "file":
cls: Type = TempDirExecutor
elif scheme == "ssh":
cls = SSHExecutor
else:
raise NotImplementedError
else:
cls = WorkspaceExecutor
return cls.from_info(info)
for name in self.proc:
infofile = self.get_infofile_path(name)
try:
with open(infofile, encoding="utf-8") as fobj:
info = ExecutorInfo.from_dict(json.load(fobj))
yield name, make_executor(info)
except OSError:
continue
def get_infofile_path(self, name: str) -> str:
return os.path.join(
self.pid_dir,
name,
f"{name}{BaseExecutor.INFOFILE_EXT}",
)
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
manager = cls(scm, wdir)
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
return manager
def _enqueue_stash_entries(
self,
scm: "Git",
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
try:
for stash_rev, entry in to_run.items():
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
executor = self.EXECUTOR_CLS.from_stash_entry(
repo,
stash_rev,
entry,
**kwargs,
)
self.enqueue(stash_rev, executor)
finally:
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
scm.remove_ref(ref)
def exec_queue(
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
):
"""Run dvc repro for queued executors in parallel."""
if detach:
raise NotImplementedError
# TODO use ProcessManager.spawn() to support detached runs
return self._exec_attached(repo, jobs=jobs)
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
import signal
from concurrent.futures import (
CancelledError,
ProcessPoolExecutor,
wait,
)
from multiprocessing import Manager
from dvc.stage.monitor import CheckpointKilledError
result: Dict[str, Dict[str, str]] = defaultdict(dict)
manager = Manager()
pid_q = manager.Queue()
with ProcessPoolExecutor(max_workers=jobs) as workers:
futures = {}
while self._queue:
rev, executor = self._queue.popleft()
infofile = self.get_infofile_path(rev)
future = workers.submit(
executor.reproduce,
info=executor.info,
rev=rev,
queue=pid_q,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
)
futures[future] = (rev, executor)
self._attached[rev] = executor
try:
wait(futures)
except KeyboardInterrupt:
# forward SIGINT to any running executor processes and
# cancel any remaining futures
workers.shutdown(wait=False)
pids = {}
for future, (rev, _) in futures.items():
if future.running():
# if future has already been started by the scheduler
# we still have to wait until it tells us its PID
while rev not in pids:
rev, pid = pid_q.get()
pids[rev] = pid
os.kill(pids[rev], signal.SIGINT)
elif not future.done():
future.cancel()
for future, (rev, executor) in futures.items():
rev, executor = futures[future]
try:
exc = future.exception()
if exc is None:
exec_result = future.result()
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
elif not isinstance(exc, CheckpointKilledError):
logger.error(
"Failed to reproduce experiment '%s'", rev[:7]
)
except CancelledError:
logger.error(
"Cancelled before attempting to reproduce experiment "
"'%s'",
rev[:7],
)
finally:
self.cleanup_executor(rev, executor)
return result
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
# NOTE: GitPython Repo instances cannot be re-used
# after process has received SIGINT or SIGTERM, so we
# need this hack to re-instantiate git instances after
# checkpoint runs. See:
# https://github.com/gitpython-developers/GitPython/issues/427
# del self.repo.scm
results = {}
def on_diverged(ref: str, checkpoint: bool):
ref_info = ExpRefInfo.from_ref(ref)
if checkpoint:
raise CheckpointExistsError(ref_info.name)
raise ExperimentExistsError(ref_info.name)
for ref in executor.fetch_exps(
self.scm,
force=exec_result.force,
on_diverged=on_diverged,
):
exp_rev = self.scm.get_ref(ref)
if exp_rev:
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
if exec_result.ref_info is not None:
executor.collect_cache(repo, exec_result.ref_info)
return results
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
from dvc.utils.fs import remove
executor.cleanup()
try:
self.proc.remove(rev)
except KeyError:
pass
remove(os.path.join(self.pid_dir, rev))
@@ -1,107 +0,0 @@
import logging
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional
from ...refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_HEAD, EXEC_MERGE
from ...stash import ExpStashEntry
from ..local import TempDirExecutor, WorkspaceExecutor
from .base import BaseExecutorManager
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class TempDirExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = TempDirExecutor
class WorkspaceExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = WorkspaceExecutor
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
manager = cls(scm, wdir)
try:
assert len(to_run) == 1
for stash_rev, entry in to_run.items():
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
executor = cls.EXECUTOR_CLS.from_stash_entry(
repo,
stash_rev,
entry,
**kwargs,
)
manager.enqueue(stash_rev, executor)
finally:
for ref in (EXEC_MERGE,):
scm.remove_ref(ref)
return manager
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
results = {}
exp_rev = self.scm.get_ref(EXEC_BRANCH)
if exp_rev:
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
return results
def exec_queue(
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
):
"""Run a single WorkspaceExecutor.
Workspace execution is done within the main DVC process
(rather than in multiprocessing context)
"""
from dvc.exceptions import DvcException
from dvc.stage.monitor import CheckpointKilledError
assert len(self._queue) == 1
assert not detach
result: Dict[str, Dict[str, str]] = defaultdict(dict)
rev, executor = self._queue.popleft()
exec_name = "workspace"
infofile = self.get_infofile_path(exec_name)
try:
exec_result = executor.reproduce(
info=executor.info,
rev=rev,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
log_errors=not isinstance(executor, WorkspaceExecutor),
)
if not exec_result.exp_hash:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
)
if exec_result.ref_info:
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
except CheckpointKilledError:
# Checkpoint errors have already been logged
return {}
except DvcException:
raise
except Exception as exc:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
self.cleanup_executor(exec_name, executor)
return result
@@ -1,109 +0,0 @@
import logging
import posixpath
from collections import defaultdict
from typing import TYPE_CHECKING, Callable, Dict, Generator, Optional, Tuple
from ...stash import ExpStashEntry
from ..base import BaseExecutor
from ..ssh import SSHExecutor, _sshfs
from .base import BaseExecutorManager
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class SSHExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = SSHExecutor
def __init__(
self,
scm: "Git",
wdir: str,
host: Optional[str] = None,
port: Optional[int] = None,
username: Optional[str] = None,
fs_factory: Optional[Callable] = None,
**kwargs,
):
assert host
super().__init__(scm, wdir, **kwargs)
self.host = host
self.port = port
self.username = username
self._fs_factory = fs_factory
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
# TODO: load existing infos using sshfs
yield from []
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
machine_name: Optional[str] = kwargs.get("machine_name", None)
manager = cls(
scm, wdir, **repo.machine.get_executor_kwargs(machine_name)
)
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
return manager
def sshfs(self):
return _sshfs(self._fs_factory, host=self.host, port=self.port)
def get_infofile_path(self, name: str) -> str:
return f"{name}{BaseExecutor.INFOFILE_EXT}"
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
from dvc.exceptions import DvcException
from dvc.stage.monitor import CheckpointKilledError
assert len(self._queue) == 1
result: Dict[str, Dict[str, str]] = defaultdict(dict)
rev, executor = self._queue.popleft()
info = executor.info
infofile = posixpath.join(
info.root_dir,
info.dvc_dir,
"tmp",
self.get_infofile_path(rev),
)
try:
exec_result = executor.reproduce(
info=executor.info,
rev=rev,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
fs_factory=self._fs_factory,
)
if not exec_result.exp_hash:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
)
if exec_result.ref_info:
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
except CheckpointKilledError:
# Checkpoint errors have already been logged
return {}
except DvcException:
raise
except Exception as exc:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
self.cleanup_executor(rev, executor)
return result
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
executor.cleanup()
+21 -10
View File
@@ -5,10 +5,9 @@ import sys
from contextlib import contextmanager
from typing import TYPE_CHECKING, Callable, Iterable, Optional
from dvc_ssh import SSHFileSystem
from funcy import first
from dvc.fs import SSHFileSystem
from ..refs import (
EXEC_BRANCH,
EXEC_CHECKPOINT,
@@ -16,7 +15,7 @@ from ..refs import (
EXEC_MERGE,
EXEC_NAMESPACE,
)
from .base import BaseExecutor, ExecutorInfo, ExecutorResult
from .base import BaseExecutor, ExecutorInfo, ExecutorResult, TaskStatus
if TYPE_CHECKING:
from queue import Queue
@@ -24,8 +23,8 @@ if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
from ..base import ExpRefInfo, ExpStashEntry
from dvc.repo.experiments.refs import ExpRefInfo
from dvc.repo.experiments.stash import ExpStashEntry
logger = logging.getLogger(__name__)
@@ -76,14 +75,12 @@ class SSHExecutor(BaseExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
):
machine_name: Optional[str] = kwargs.pop("machine_name", None)
executor = cls._from_stash_entry(
repo,
stash_rev,
entry,
cls.gen_dirname(entry.name),
location=machine_name,
@@ -120,9 +117,20 @@ class SSHExecutor(BaseExecutor):
}
return kwargs
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
from ..utils import push_refspec
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
with self.sshfs() as fs:
fs.makedirs(self.root_dir)
self._ssh_cmd(fs, "git init .")
@@ -138,8 +146,11 @@ class SSHExecutor(BaseExecutor):
# TODO: support multiple client key retries in git backends
# (see https://github.com/iterative/dvc/issues/6508)
kwargs = self._git_client_args(fs)
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
with self.set_exec_refs(scm, stash_rev, entry):
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
if branch:
push_refspec(scm, self.git_url, branch, branch, **kwargs)
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
+44 -18
View File
@@ -1,5 +1,6 @@
import logging
import os
import sys
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import (
@@ -19,6 +20,7 @@ from typing import (
from funcy import cached_property
from dvc.dependency import ParamsDependency
from dvc.env import DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.ui import ui
@@ -31,7 +33,7 @@ from ..executor.base import (
ExecutorResult,
)
from ..executor.local import WorkspaceExecutor
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
from ..refs import ExpRefInfo
from ..stash import ExpStash, ExpStashEntry
from ..utils import exp_refs_by_rev, scm_locked
@@ -520,14 +522,36 @@ class BaseStashQueue(ABC):
provided via `exp run --set-param`.
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
logger.debug("Using experiment params '%s'", params)
from dvc.utils.hydra import apply_overrides
try:
from dvc.utils.hydra import apply_overrides, compose_and_dump
except ValueError:
if sys.version_info >= (3, 11):
raise DvcException(
"--set-param is not supported in Python >= 3.11"
)
raise
hydra_config = self.repo.config.get("hydra", {})
hydra_enabled = hydra_config.get("enabled", False)
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
for path, overrides in params.items():
apply_overrides(path, overrides)
if hydra_enabled and path == hydra_output_file:
config_dir = os.path.join(
self.repo.root_dir, hydra_config.get("config_dir", "conf")
)
config_name = hydra_config.get("config_name", "config")
compose_and_dump(
path,
config_dir,
config_name,
overrides,
)
else:
apply_overrides(path, overrides)
# Force params file changes to be staged in git
# Otherwise in certain situations the changes to params file may be
@@ -537,7 +561,7 @@ class BaseStashQueue(ABC):
@staticmethod
@scm_locked
def setup_executor(
def init_executor(
exp: "Experiments",
queue_entry: QueueEntry,
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
@@ -552,21 +576,23 @@ class BaseStashQueue(ABC):
)
if stash_entry.stash_index is not None:
stash.drop(stash_entry.stash_index)
scm.set_ref(EXEC_HEAD, stash_entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, stash_entry.baseline_rev)
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
return executor_cls.from_stash_entry(
exp.repo, stash_rev, stash_entry, **kwargs
executor = executor_cls.from_stash_entry(
exp.repo, stash_entry, **kwargs
)
infofile = exp.celery_queue.get_infofile_path(stash_rev)
executor.init_git(
exp.repo.scm,
stash_rev,
stash_entry,
infofile,
branch=stash_entry.branch,
)
executor.init_cache(exp.repo, stash_rev)
return executor
def get_infofile_path(self, name: str) -> str:
return os.path.join(
self.pid_dir,
+7 -2
View File
@@ -23,7 +23,12 @@ from dvc.exceptions import DvcException
from dvc.ui import ui
from ..exceptions import UnresolvedQueueExpNamesError
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
from ..executor.base import (
EXEC_TMP_DIR,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
from .tasks import run_exp
@@ -248,7 +253,7 @@ class LocalCeleryQueue(BaseStashQueue):
def _load_collected(rev: str) -> Optional[ExecutorResult]:
executor_info = _load_info(rev)
if executor_info.collected:
if executor_info.status > TaskStatus.SUCCESS:
return executor_info.result
raise FileNotFoundError
+7 -21
View File
@@ -3,8 +3,6 @@ from typing import Any, Dict
from celery import shared_task
from celery.utils.log import get_task_logger
from dvc.utils.fs import remove
from ..executor.base import ExecutorInfo
from ..executor.local import TempDirExecutor
from .base import BaseStashQueue, QueueEntry
@@ -13,7 +11,7 @@ logger = get_task_logger(__name__)
@shared_task
def setup_exp(entry_dict: Dict[str, Any]) -> str:
def setup_exp(entry_dict: Dict[str, Any]) -> TempDirExecutor:
"""Setup an experiment.
Arguments:
@@ -28,7 +26,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
repo = Repo(entry.dvc_root)
# TODO: split executor.init_cache into separate subtask - we can release
# exp.scm_lock before DVC push
executor = BaseStashQueue.setup_executor(
executor = BaseStashQueue.init_executor(
repo.experiments,
entry,
TempDirExecutor,
@@ -36,7 +34,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
)
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
executor.info.dump_json(infofile)
return executor.root_dir
return executor
@shared_task
@@ -79,25 +77,14 @@ def collect_exp(
@shared_task
def cleanup_exp( # pylint: disable=unused-argument
tmp_dir: str, entry_dict: Dict[str, Any]
) -> None:
def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
"""Cleanup after an experiment.
Arguments:
tmp_dir: Temp directory to be removed.
entry_dict: Serialized QueueEntry for this experiment.
"""
remove(tmp_dir)
def _set_collected(infofile: str):
try:
executor_info = ExecutorInfo.load_json(infofile)
executor_info.collected = True
executor_info.dump_json(infofile)
except FileNotFoundError:
pass
executor.cleanup(infofile)
@shared_task
@@ -115,11 +102,10 @@ def run_exp(entry_dict: Dict[str, Any]) -> None:
repo = Repo(entry.dvc_root)
queue = repo.experiments.celery_queue
infofile = queue.get_infofile_path(entry.stash_rev)
root_dir = setup_exp.s(entry_dict)()
executor = setup_exp.s(entry_dict)()
try:
cmd = ["dvc", "exp", "exec-run", "--infofile", infofile]
proc_dict = queue.proc.run_signature(cmd, name=entry.stash_rev)()
collect_exp.s(proc_dict, entry_dict)()
finally:
cleanup_exp.s(root_dir, entry_dict)()
_set_collected(infofile)
cleanup_exp.s(executor, infofile)()
+5 -3
View File
@@ -11,6 +11,7 @@ from ..executor.base import (
BaseExecutor,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from ..executor.local import TempDirExecutor
from .base import BaseStashQueue, QueueEntry, QueueGetResult
@@ -60,7 +61,7 @@ class TempDirQueue(WorkspaceQueue):
stash_entry.name,
stash_entry.head_rev,
)
executor = self.setup_executor(
executor = self.init_executor(
self.repo.experiments,
entry,
TempDirExecutor,
@@ -75,8 +76,9 @@ class TempDirQueue(WorkspaceQueue):
for stash_rev in self.proc:
infofile = self.get_infofile_path(stash_rev)
executor_info = ExecutorInfo.load_json(infofile)
if not executor_info.collected and os.path.exists(
executor_info.root_dir
if (
not executor_info.status <= TaskStatus.SUCCESS
and os.path.exists(executor_info.root_dir)
):
yield QueueEntry(
self.repo.root_dir,
+3 -6
View File
@@ -1,12 +1,10 @@
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Collection, Dict, Generator, Optional
from funcy import first
from dvc.exceptions import DvcException
from dvc.utils.fs import remove
from ..exceptions import ExpQueueEmptyError
from ..executor.base import BaseExecutor, ExecutorResult
@@ -41,7 +39,7 @@ class WorkspaceQueue(BaseStashQueue):
stash_entry.name,
stash_entry.head_rev,
)
executor = self.setup_executor(self.repo.experiments, entry)
executor = self.init_executor(self.repo.experiments, entry)
return QueueGetResult(entry, executor)
def iter_queued(self) -> Generator[QueueEntry, None, None]:
@@ -118,9 +116,7 @@ class WorkspaceQueue(BaseStashQueue):
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
if self._EXEC_NAME == exec_name:
remove(os.path.join(self.pid_dir, exec_name))
executor.cleanup()
executor.cleanup(infofile)
return results
@staticmethod
@@ -135,6 +131,7 @@ class WorkspaceQueue(BaseStashQueue):
assert exec_result.exp_hash
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
return results
def get_result(self, entry: QueueEntry) -> Optional[ExecutorResult]:
+44 -9
View File
@@ -1,6 +1,8 @@
import logging
from typing import Dict, Iterable, Optional
from dvc.dependency.param import ParamsDependency
from dvc.exceptions import InvalidArgumentError
from dvc.repo import locked
from dvc.ui import ui
from dvc.utils.cli_parse import to_path_overrides
@@ -26,26 +28,59 @@ def run(
Returns a dict mapping new experiment SHAs to the results
of `repro` for that experiment.
"""
from dvc.utils.hydra import to_hydra_overrides
if run_all:
entries = list(repo.experiments.celery_queue.iter_queued())
return repo.experiments.reproduce_celery(entries, jobs=jobs)
if params:
params = to_path_overrides(params)
path_overrides = to_path_overrides(params)
else:
path_overrides = {}
if queue:
if not kwargs.get("checkpoint_resume", None):
kwargs["reset"] = True
hydra_enabled = repo.config.get("hydra", {}).get("enabled", False)
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
if hydra_enabled and hydra_output_file not in path_overrides:
# Force `_update_params` even if `--set-param` was not used
path_overrides[hydra_output_file] = []
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
if not queue:
return repo.experiments.reproduce_one(
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
)
if hydra_sweep:
from dvc.utils.hydra import get_hydra_sweeps
sweeps = get_hydra_sweeps(path_overrides)
else:
sweeps = [path_overrides]
if not kwargs.get("checkpoint_resume", None):
kwargs["reset"] = True
for sweep_overrides in sweeps:
queue_entry = repo.experiments.queue_one(
repo.experiments.celery_queue,
targets=targets,
params=params,
params=sweep_overrides,
**kwargs,
)
if sweep_overrides:
ui.write(f"Queueing with overrides '{sweep_overrides}'.")
name = queue_entry.name or queue_entry.stash_rev[:7]
ui.write(f"Queued experiment '{name}' for future execution.")
return {}
return repo.experiments.reproduce_one(
targets=targets, params=params, tmp_dir=tmp_dir, **kwargs
)
return {}
+3
View File
@@ -142,6 +142,9 @@ def show(
fetch_running: bool = True,
):
if repo.scm.no_commits:
return {}
if onerror is None:
onerror = onerror_collect
+69
View File
@@ -1,6 +1,8 @@
import logging
from contextlib import suppress
from typing import TYPE_CHECKING, Optional
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError, FileTransferError
from dvc.fs import Schemes
@@ -12,6 +14,20 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _fetch_worktree(repo, remote):
from dvc_data.index import save
index = repo.index.data["repo"]
for key, entry in index.iteritems():
entry.fs = remote.fs
entry.path = remote.fs.path.join(
remote.path,
*key,
)
save(index)
return len(index)
@locked
def fetch(
self,
@@ -42,6 +58,11 @@ def fetch(
if isinstance(targets, str):
targets = [targets]
with suppress(NoRemoteError):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _fetch_worktree(self, _remote)
used = self.used_objs(
targets,
all_branches=all_branches,
@@ -93,6 +114,18 @@ def fetch(
downloaded += d
failed += f
d, f = _fetch_partial_imports(
self,
targets,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
recursive=recursive,
revs=revs,
)
downloaded += d
failed += f
if failed:
raise DownloadError(failed)
@@ -107,3 +140,39 @@ def _fetch(repo, obj_ids, **kwargs):
except FileTransferError as exc:
failed += exc.amount
return downloaded, failed
def _fetch_partial_imports(repo, targets, **kwargs):
from dvc.stage.exceptions import DataSourceChanged
downloaded = 0
failed = 0
for stage in repo.partial_imports(targets, **kwargs):
dep = stage.deps[0]
out = stage.outs[0]
try:
if dep.changed_checksum():
raise DataSourceChanged(f"{stage} ({dep})")
logger.info("Importing '%s' -> '%s'", dep, out)
if stage.is_repo_import:
obj = dep.get_obj()
meta = dep.get_meta()
out.hash_info = obj.hash_info
out.meta = meta
else:
out.transfer(
dep.def_path, jobs=kwargs.get("jobs"), odb=repo.odb.local
)
except DataSourceChanged as exc:
logger.warning(f"{exc}")
failed += 1
continue
if not any(
kwargs.get(kw, None)
for kw in ("all_branches", "all_tags", "all_commits", "revs")
):
stage.dump()
downloaded += 1
return downloaded, failed
+2 -2
View File
@@ -60,8 +60,8 @@ def gc(
from contextlib import ExitStack
from dvc.repo import Repo
from dvc_data.db import get_index
from dvc_data.gc import gc as ogc
from dvc_data.hashfile.db import get_index
from dvc_data.hashfile.gc import gc as ogc
if not repos:
repos = []
+11 -6
View File
@@ -17,11 +17,16 @@ def imp_url(
fname=None,
erepo=None,
frozen=True,
no_download=False,
no_exec=False,
remote=None,
to_remote=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
meta=None,
jobs=None,
fs_config=None,
):
from dvc.dvcfile import Dvcfile
from dvc.stage import Stage, create_stage, restore_fields
@@ -31,9 +36,9 @@ def imp_url(
self, out, always_local=to_remote and not out
)
if to_remote and no_exec:
if to_remote and (no_exec or no_download):
raise InvalidArgumentError(
"--no-exec can't be combined with --to-remote"
"--no-exec/--no-download cannot be combined with --to-remote"
)
if not to_remote and remote:
@@ -57,12 +62,12 @@ def imp_url(
deps=[url],
outs=[out],
erepo=erepo,
fs_config=fs_config,
)
restore_fields(stage)
if desc:
stage.outs[0].desc = desc
out_obj = stage.outs[0]
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
dvcfile = Dvcfile(self, stage.path)
dvcfile.remove()
@@ -80,7 +85,7 @@ def imp_url(
stage.save_deps()
stage.md5 = stage.compute_md5()
else:
stage.run(jobs=jobs)
stage.run(jobs=jobs, no_download=no_download)
stage.frozen = frozen
+25 -9
View File
@@ -183,15 +183,7 @@ class Index:
if not out.use_cache:
continue
if out.is_in_repo:
workspace = "repo"
key = self.repo.fs.path.relparts(
out.fs_path, self.repo.root_dir
)
else:
workspace = out.fs.protocol
no_drive = out.fs.path.flavour.splitdrive(out.fs_path)[1]
key = out.fs.path.parts(no_drive)[1:]
workspace, key = out.index_key
try:
remote = self.repo.cloud.get_remote_odb(out.remote)
@@ -200,11 +192,15 @@ class Index:
data_index = by_workspace[workspace]
if out.files:
out.obj = out.get_obj()
data_index[key] = DataIndexEntry(
meta=out.meta,
obj=out.obj,
hash_info=out.hash_info,
odb=out.odb,
cache=out.odb,
remote=remote,
)
@@ -246,6 +242,26 @@ class Index:
used[odb].update(objs)
return used
def partial_imports(
self,
targets: "TargetType" = None,
recursive: bool = False,
) -> List["Stage"]:
from itertools import chain
from dvc.utils.collections import ensure_list
collect_targets: Sequence[Optional[str]] = (None,)
if targets:
collect_targets = ensure_list(targets)
pairs = chain.from_iterable(
self.stage_collector.collect_granular(target, recursive=recursive)
for target in collect_targets
)
return [stage for stage, _ in pairs if stage.is_partial_import]
# Following methods help us treat the collection as a set-like structure
# and provides faux-immutability.
# These methods do not preserve stages order.
+13 -14
View File
@@ -1,11 +1,10 @@
import os
from itertools import chain
from typing import TYPE_CHECKING, Optional
from dvc.exceptions import PathMissingError
if TYPE_CHECKING:
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
from . import Repo
@@ -56,7 +55,7 @@ def ls(
def _ls(
repo: "Repo", path: str, recursive: bool = None, dvc_only: bool = False
):
fs: "DvcFileSystem" = repo.dvcfs
fs: "DVCFileSystem" = repo.dvcfs
fs_path = fs.from_os_path(path)
try:
@@ -66,15 +65,16 @@ def _ls(
infos = {}
for root, dirs, files in fs.walk(
fs_path, dvcfiles=True, dvc_only=dvc_only
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
):
entries = chain(files, dirs) if not recursive else files
if not recursive:
files.update(dirs)
for entry in entries:
entry_fs_path = fs.path.join(root, entry)
for name, entry in files.items():
entry_fs_path = fs.path.join(root, name)
relparts = fs.path.relparts(entry_fs_path, fs_path)
name = os.path.join(*relparts)
infos[name] = fs.info(entry_fs_path)
infos[name] = entry
if not recursive:
break
@@ -85,11 +85,10 @@ def _ls(
ret = {}
for name, info in infos.items():
dvc_info = info.get("dvc_info", {})
if fs.isdvc(info["name"], recursive=True) or not dvc_only:
ret[name] = {
"isout": dvc_info.get("isout", False),
"isdir": info["type"] == "directory",
"isexec": info.get("isexec", False),
}
ret[name] = {
"isout": dvc_info.get("isout", False),
"isdir": info["type"] == "directory",
"isexec": info.get("isexec", False),
}
return ret
+37
View File
@@ -0,0 +1,37 @@
from dvc.exceptions import URLMissingError
from dvc.fs import get_cloud_fs
def parse_external_url(url, config=None):
remote_config = dict(config) if config else {}
remote_config["url"] = url
fs_cls, fs_config, fs_path = get_cloud_fs(None, **remote_config)
fs = fs_cls(**fs_config)
return fs, fs_path
def ls_url(url, *, config=None, recursive=False):
fs, fs_path = parse_external_url(url, config=config)
try:
info = fs.info(fs_path)
except FileNotFoundError as exc:
raise URLMissingError(url) from exc
if info["type"] != "directory":
return [{"path": info["name"], "isdir": False}]
ret = []
for _, dirs, files in fs.walk(fs_path, detail=True):
if not recursive:
files.update(dirs)
for info in files.values():
ls_info = {
"path": fs.path.relpath(info["name"], fs_path),
"isdir": info["type"] == "directory",
}
ret.append(ls_info)
if not recursive:
break
return ret
+8 -6
View File
@@ -4,7 +4,7 @@ from typing import List
from scmrepo.exceptions import SCMError
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
from dvc.output import Output
from dvc.repo import locked
from dvc.repo.collect import StrPaths, collect
@@ -77,7 +77,7 @@ def _read_metric(path, fs, rev, **kwargs):
def _read_metrics(repo, metrics, rev, onerror=None):
fs = DvcFileSystem(repo=repo)
fs = DVCFileSystem(repo=repo)
relpath = ""
if repo.root_dir != repo.fs.path.getcwd():
@@ -85,12 +85,14 @@ def _read_metrics(repo, metrics, rev, onerror=None):
res = {}
for metric in metrics:
rel_metric_path = os.path.join(relpath, *fs.path.parts(metric))
if not fs.isfile(metric):
continue
if fs.isfile(rel_metric_path):
metric = rel_metric_path
else:
continue
res[os.path.join(relpath, *fs.path.parts(metric))] = _read_metric(
metric, fs, rev, onerror=onerror
)
res[rel_metric_path] = _read_metric(metric, fs, rev, onerror=onerror)
return res
+3 -1
View File
@@ -56,7 +56,9 @@ def _collect_configs(
default_params
):
fs_paths.append(default_params)
if targets and (deps or stages) and not params:
# A target has been provided but it is not used in the stages
fs_paths = []
return params, fs_paths
+78 -43
View File
@@ -100,7 +100,7 @@ class Plots:
"filename":
{
"data_source": callable loading the data,
"props": propreties for the file if it is
"props": properties for the file if it is
plots type output
}
}
@@ -152,9 +152,9 @@ class Plots:
props: Optional[Dict] = None,
onerror: Optional[Callable] = None,
):
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
fs = DvcFileSystem(repo=self.repo)
fs = DVCFileSystem(repo=self.repo)
props = props or {}
@@ -386,30 +386,56 @@ def _collect_output_plots(
return result
def _adjust_definitions_to_cwd(fs, config_relpath, plots_definitions):
result = defaultdict(dict)
def _id_is_path(plot_props=None):
if not plot_props:
return True
config_dirname = fs.path.dirname(config_relpath)
y_def = plot_props.get("y")
return not isinstance(y_def, dict)
for plot_id, plot_def in plots_definitions.items():
y_def = plot_def.get("y", None) if plot_def else None
if y_def is None or not isinstance(y_def, dict):
# plot_id is filename
new_plot_id = _normpath(fs.path.join(config_dirname, plot_id))
result[new_plot_id] = plot_def or {}
def _adjust_sources(fs, plot_props, config_dir):
new_plot_props = deepcopy(plot_props)
old_y = new_plot_props.pop("y", {})
new_y = {}
for filepath, val in old_y.items():
new_y[_normpath(fs.path.join(config_dir, filepath))] = val
new_plot_props["y"] = new_y
return new_plot_props
def _resolve_definitions(
fs, targets, props, config_path, definitions, onerror=None
):
config_dir = fs.path.dirname(config_path)
result: Dict[str, Dict] = {}
for plot_id, plot_props in definitions.items():
if plot_props is None:
plot_props = {}
if _id_is_path(plot_props):
data_path = _normpath(fs.path.join(config_dir, plot_id))
if _matches(targets, config_path, plot_id):
unpacked = unpack_if_dir(
fs,
data_path,
props={**plot_props, **props},
onerror=onerror,
)
dpath.util.merge(
result,
unpacked,
)
else:
new_plot_def = deepcopy(plot_def)
old_y = new_plot_def.pop("y")
new_y = {}
for filepath, val in old_y.items():
new_y[_normpath(fs.path.join(config_dirname, filepath))] = val
new_plot_def["y"] = new_y
result[plot_id] = new_plot_def
return dict(result)
if _matches(targets, config_path, plot_id):
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
dpath.util.merge(
result, {"data": {plot_id: {**adjusted_props, **props}}}
)
return result
def _collect_pipeline_files(repo, targets: List[str], props):
def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
from dvc.dvcfile import PipelineFile
result: Dict[str, Dict] = {}
@@ -417,21 +443,19 @@ def _collect_pipeline_files(repo, targets: List[str], props):
for dvcfile in dvcfiles:
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = _adjust_definitions_to_cwd(
repo.fs, dvcfile_path, dvcfile.load().get("plots", {})
dvcfile_defs = dvcfile.load().get("plots", {})
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs,
onerror=onerror,
)
dpath.util.merge(
result,
{dvcfile_path: resolved},
)
for plot_id, plot_props in dvcfile_defs.items():
if plot_props is None:
plot_props = {}
if _matches(targets, dvcfile_path, plot_id):
dpath.util.merge(
result,
{
dvcfile_path: {
"data": {plot_id: {**plot_props, **props}}
}
},
)
return result
@@ -448,12 +472,15 @@ def _collect_definitions(
result: Dict = defaultdict(dict)
props = props or {}
from dvc.fs.dvc import DvcFileSystem
from dvc.fs.dvc import DVCFileSystem
fs = DvcFileSystem(repo=repo)
fs = DVCFileSystem(repo=repo)
if not config_files:
dpath.util.merge(result, _collect_pipeline_files(repo, targets, props))
dpath.util.merge(
result,
_collect_pipeline_files(repo, targets, props, onerror=onerror),
)
if targets or (not targets and not config_files):
dpath.util.merge(
@@ -463,12 +490,20 @@ def _collect_definitions(
if config_files:
for path in config_files:
definitions = parse(fs, path)
definitions = _adjust_definitions_to_cwd(
repo.fs, path, definitions
)
definitions = parse(fs, path).get("data", {})
if definitions:
dpath.util.merge(result, {path: definitions})
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
path,
definitions,
onerror=onerror,
)
dpath.util.merge(
result,
{path: resolved},
)
for target in targets:
if not result or fs.exists(target):
+2
View File
@@ -12,6 +12,8 @@ def _revisions(repo, revs, experiment):
def diff(repo, *args, revs=None, experiment=False, **kwargs):
if repo.scm.no_commits:
return {}
return repo.plots.show(
*args, revs=_revisions(repo, revs, experiment), **kwargs
)
+47
View File
@@ -9,6 +9,49 @@ if TYPE_CHECKING:
from dvc_objects.db.base import ObjectDB
def _push_worktree(repo, remote):
from dvc_data.hashfile.tree import tree_from_index
from dvc_data.index import checkout
index = repo.index.data["repo"]
checkout(index, remote.path, remote.fs)
for stage in repo.index.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
index = repo.index.data[workspace]
entry = index[key]
if out.isdir():
old_tree = out.get_obj()
entry.hash_info = old_tree.hash_info
entry.meta = out.meta
for subkey, entry in index.iteritems(key):
if entry.meta.isdir:
continue
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
_, hash_info = old_tree.get(
repo.fs.path.relparts(fs_path, out.fs_path)
)
entry.hash_info = hash_info
tree_meta, new_tree = tree_from_index(index, key)
new_tree.digest()
out.obj = new_tree
out.hash_info = new_tree.hash_info
out.meta = tree_meta
else:
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage, with_files=True, update_pipeline=False)
return len(index)
@locked
def push(
self,
@@ -26,6 +69,10 @@ def push(
odb: Optional["ObjectDB"] = None,
include_imports=False,
):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _push_worktree(self, _remote)
used_run_cache = (
self.stage_cache.push(remote, odb=odb) if run_cache else []
)
+17 -1
View File
@@ -1,6 +1,13 @@
import logging
import typing
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageFileIsNotDvcFileError,
StageNotFound,
)
from . import locked
if typing.TYPE_CHECKING:
@@ -11,7 +18,16 @@ logger = logging.getLogger(__name__)
@locked
def remove(self: "Repo", target: str, outs: bool = False):
stages = self.stage.from_target(target)
try:
stages = self.stage.from_target(target, accept_group=False)
except (StageNotFound, StageFileDoesNotExistError) as e:
# If the user specified a tracked file as a target instead of a stage,
# e.g. `data.csv` instead of `data.csv.dvc`,
# give a more helpful error message.
if self.fs.exists(target + DVC_FILE_SUFFIX):
raise StageFileIsNotDvcFileError(target) from e
else:
raise
for stage in stages:
stage.remove(remove_outs=outs, force=outs)
-2
View File
@@ -102,7 +102,6 @@ def reproduce(
from .graph import get_pipeline, get_pipelines
glob = kwargs.pop("glob", False)
accept_group = not glob
if isinstance(targets, str):
targets = [targets]
@@ -137,7 +136,6 @@ def reproduce(
self.stage.collect(
target,
recursive=recursive,
accept_group=accept_group,
glob=glob,
)
)
+13 -25
View File
@@ -82,7 +82,6 @@ def _collect_specific_target(
target: str,
with_deps: bool,
recursive: bool,
accept_group: bool,
) -> Tuple[StageIter, "OptStr", "OptStr"]:
from dvc.dvcfile import is_valid_filename
@@ -98,13 +97,11 @@ def _collect_specific_target(
msg = "Checking if stage '%s' is in '%s'"
logger.debug(msg, target, PIPELINE_FILE)
if not (recursive and loader.fs.isdir(target)):
stages = _maybe_collect_from_dvc_yaml(
loader, target, with_deps, accept_group=accept_group
)
stages = _maybe_collect_from_dvc_yaml(loader, target, with_deps)
if stages:
return stages, file, name
elif not with_deps and is_valid_filename(file):
stages = loader.load_all(file, name, accept_group=accept_group)
stages = loader.load_all(file, name)
return stages, file, name
return [], file, name
@@ -209,7 +206,7 @@ class StageLoad:
return stage
def from_target(
self, target: str, accept_group: bool = False, glob: bool = False
self, target: str, accept_group: bool = True, glob: bool = False
) -> StageList:
"""
Returns a list of stage from the provided target.
@@ -228,10 +225,9 @@ class StageLoad:
path, name = parse_target(target)
return self.load_one(path=path, name=name)
@staticmethod
def _get_filepath(path: str = None, name: str = None) -> str:
def _get_filepath(self, path: str = None, name: str = None) -> str:
if path:
return path
return self.repo.fs.path.realpath(path)
path = PIPELINE_FILE
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
@@ -250,15 +246,12 @@ class StageLoad:
self,
stages: "StageLoader",
name: str = None,
accept_group: bool = False,
accept_group: bool = True,
glob: bool = False,
) -> Iterable[str]:
assert not (accept_group and glob)
if not name:
return stages.keys()
if accept_group and stages.is_foreach_generated(name):
return self._get_group_keys(stages, name)
if glob:
@@ -269,7 +262,7 @@ class StageLoad:
self,
path: str = None,
name: str = None,
accept_group: bool = False,
accept_group: bool = True,
glob: bool = False,
) -> StageList:
"""Load a list of stages from a file.
@@ -338,7 +331,6 @@ class StageLoad:
with_deps: bool = False,
recursive: bool = False,
graph: "DiGraph" = None,
accept_group: bool = False,
glob: bool = False,
) -> StageIter:
"""Collect list of stages from the provided target.
@@ -347,15 +339,14 @@ class StageLoad:
target: if not provided, all of the stages in the graph are
returned.
Target can be:
- a stage name in the `dvc.yaml` file.
- a foreach group name or a stage name in the `dvc.yaml` file.
- a generated stage name from a foreach group.
- a path to `dvc.yaml` or `.dvc` file.
- in case of a stage to a dvc.yaml file in a different
directory than current working directory, it can be a path
to dvc.yaml file, followed by a colon `:`, followed by stage
name (eg: `../dvc.yaml:build`).
- in case of `recursive`, it can be a path to a directory.
- in case of `accept_group`, it can be a group name of
`foreach` generated stage.
- in case of `glob`, it can be a wildcard pattern to match
stages. Example: `build*` for stages in `dvc.yaml` file, or
`../dvc.yaml:build*` for stages in dvc.yaml in a different
@@ -367,8 +358,6 @@ class StageLoad:
recursive: if true and if `target` is a directory, all of the
stages inside that directory is returned.
graph: graph to use. Defaults to `repo.graph`.
accept_group: if true, all of the `foreach` generated stages of
the specified target is returned.
glob: Use `target` as a pattern to match stages in a file.
"""
if not target:
@@ -380,7 +369,7 @@ class StageLoad:
path = self.fs.path.abspath(target)
return collect_inside_path(path, graph or self.graph)
stages = self.from_target(target, accept_group=accept_group, glob=glob)
stages = self.from_target(target, glob=glob)
if not with_deps:
return stages
@@ -392,14 +381,14 @@ class StageLoad:
with_deps: bool = False,
recursive: bool = False,
graph: "DiGraph" = None,
accept_group: bool = False,
) -> List[StageInfo]:
"""Collects a list of (stage, filter_info) from the given target.
Priority is in the order of following in case of ambiguity:
- .dvc file or .yaml file
- dir if recursive and directory exists
- stage_name
- foreach_group_name or stage_name
- generated stage name from a foreach group
- output file
Args:
@@ -418,7 +407,7 @@ class StageLoad:
target = as_posix(target)
stages, file, _ = _collect_specific_target(
self, target, with_deps, recursive, accept_group
self, target, with_deps, recursive
)
if not stages:
if not (recursive and self.fs.isdir(target)):
@@ -440,7 +429,6 @@ class StageLoad:
with_deps,
recursive,
graph,
accept_group=accept_group,
)
except StageFileDoesNotExistError as exc:
# collect() might try to use `target` as a stage name
+13 -1
View File
@@ -10,6 +10,7 @@ def update(
rev=None,
recursive=False,
to_remote=False,
no_download=False,
remote=None,
jobs=None,
):
@@ -21,6 +22,11 @@ def update(
if isinstance(targets, str):
targets = [targets]
if to_remote and no_download:
raise InvalidArgumentError(
"--to-remote can't be used with --no-download"
)
if not to_remote and remote:
raise InvalidArgumentError(
"--remote can't be used without --to-remote"
@@ -31,7 +37,13 @@ def update(
stages.update(self.stage.collect(target, recursive=recursive))
for stage in stages:
stage.update(rev, to_remote=to_remote, remote=remote, jobs=jobs)
stage.update(
rev,
to_remote=to_remote,
remote=remote,
no_download=no_download,
jobs=jobs,
)
dvcfile = Dvcfile(self, stage.path)
dvcfile.dump(stage)
+14 -7
View File
@@ -3,11 +3,11 @@ from collections.abc import Mapping
from voluptuous import Any, Optional, Required, Schema
from dvc import dependency, output
from dvc.output import CHECKSUMS_SCHEMA, Output
from dvc.annotations import ANNOTATION_SCHEMA
from dvc.output import CHECKSUMS_SCHEMA, DIR_FILES_SCHEMA, META_SCHEMA, Output
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
from dvc.stage.params import StageParams
from dvc_data.hashfile.meta import Meta
STAGES = "stages"
SINGLE_STAGE_SCHEMA = {
@@ -25,10 +25,9 @@ SINGLE_STAGE_SCHEMA = {
DATA_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required("path"): str,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
LOCK_FILE_STAGE_SCHEMA = {
Required(StageParams.PARAM_CMD): Any(str, list),
@@ -47,16 +46,15 @@ LOCKFILE_V2_SCHEMA = {
OUT_PSTAGE_DETAILED_SCHEMA = {
str: {
**ANNOTATION_SCHEMA, # type: ignore
Output.PARAM_CACHE: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Output.PARAM_DESC: str,
Output.PARAM_REMOTE: str,
}
}
PLOTS = "plots"
PLOTS_SCHEMA = dict
PLOT_PROPS = {
Output.PARAM_PLOT_TEMPLATE: str,
Output.PARAM_PLOT_X: str,
@@ -112,6 +110,15 @@ def either_or(primary, fallback, fallback_includes=None):
return validator
PLOT_DEFINITION = {
Output.PARAM_PLOT_X: str,
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
Output.PARAM_PLOT_X_LABEL: str,
Output.PARAM_PLOT_Y_LABEL: str,
Output.PARAM_PLOT_TITLE: str,
Output.PARAM_PLOT_TEMPLATE: str,
}
PLOTS_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
FOREACH_IN = {
Required(FOREACH_KWD): Any(dict, list, str),
Required(DO_KWD): STAGE_DEFINITION,
+1 -1
View File
@@ -160,7 +160,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
except InternalRevError as exc:
# `scm` will only resolve git branch and tag names,
# if rev is not a sha it may be an abbreviated experiment name
if not rev.startswith("refs/"):
if not (rev == "HEAD" or rev.startswith("refs/")):
from dvc.repo.experiments.utils import (
AmbiguousExpRefInfo,
resolve_name,
+48 -20
View File
@@ -78,6 +78,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
wdir = os.path.abspath(kwargs.get("wdir", None) or os.curdir)
path = os.path.abspath(path)
check_dvcfile_path(repo, path)
check_stage_path(repo, wdir, is_wdir=kwargs.get("wdir"))
check_stage_path(repo, os.path.dirname(path))
@@ -87,7 +88,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
if not external:
check_no_externals(stage)
fill_stage_dependencies(
stage, **project(kwargs, ["deps", "erepo", "params"])
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config"])
)
check_circular_dependency(stage)
check_duplicated_arguments(stage)
@@ -109,16 +110,13 @@ def restore_fields(stage):
# will be used to restore comments later
# noqa, pylint: disable=protected-access
stage._stage_text = old._stage_text
stage.meta = old.meta
stage.desc = old.desc
old_fields = {out.def_path: (out.desc, out.remote) for out in old.outs}
old_fields = {out.def_path: (out.annot, out.remote) for out in old.outs}
for out in stage.outs:
out_fields = old_fields.get(out.def_path, None)
if out_fields:
out.desc, out.remote = out_fields
if out_fields := old_fields.get(out.def_path, None):
out.annot, out.remote = out_fields
class Stage(params.StageParams):
@@ -242,6 +240,14 @@ class Stage(params.StageParams):
"""Whether the DVC file was created with `dvc import`."""
return not self.cmd and len(self.deps) == 1 and len(self.outs) == 1
@property
def is_partial_import(self) -> bool:
"""
Whether the DVC file was created using `dvc import --no-download`
or `dvc import-url --no-download`.
"""
return self.is_import and (not self.outs[0].hash_info)
@property
def is_repo_import(self):
if not self.is_import:
@@ -433,18 +439,30 @@ class Stage(params.StageParams):
return self
def update(self, rev=None, to_remote=False, remote=None, jobs=None):
def update(
self,
rev=None,
to_remote=False,
remote=None,
no_download=None,
jobs=None,
):
if not (self.is_repo_import or self.is_import):
raise StageUpdateError(self.relpath)
update_import(
self, rev=rev, to_remote=to_remote, remote=remote, jobs=jobs
self,
rev=rev,
to_remote=to_remote,
remote=remote,
no_download=no_download,
jobs=jobs,
)
def reload(self):
return self.dvcfile.stage
def dumpd(self):
return get_dump(self)
def dumpd(self, **kwargs):
return get_dump(self, **kwargs)
def compute_md5(self):
# `dvc add`ed files don't need stage md5
@@ -457,6 +475,7 @@ class Stage(params.StageParams):
def save(self, allow_missing=False):
self.save_deps(allow_missing=allow_missing)
self.save_outs(allow_missing=allow_missing)
self.md5 = self.compute_md5()
@@ -525,14 +544,16 @@ class Stage(params.StageParams):
no_commit=False,
force=False,
allow_missing=False,
no_download=False,
**kwargs,
):
if (self.cmd or self.is_import) and not self.frozen and not dry:
self.remove_outs(ignore_remove=False, force=False)
if not self.frozen and self.is_import:
jobs = kwargs.get("jobs", None)
self._sync_import(dry, force, jobs)
if (not self.frozen and self.is_import) or self.is_partial_import:
self._sync_import(
dry, force, kwargs.get("jobs", None), no_download
)
elif not self.frozen and self.cmd:
self._run_stage(dry, force, **kwargs)
else:
@@ -544,9 +565,11 @@ class Stage(params.StageParams):
self._check_missing_outputs()
if not dry:
if kwargs.get("checkpoint_func", None):
if kwargs.get("checkpoint_func", None) or no_download:
allow_missing = True
self.save(allow_missing=allow_missing)
if no_download:
self.ignore_outs()
if not no_commit:
self.commit(allow_missing=allow_missing)
@@ -555,8 +578,8 @@ class Stage(params.StageParams):
return run_stage(self, dry, force, **kwargs)
@rwlocked(read=["deps"], write=["outs"])
def _sync_import(self, dry, force, jobs):
sync_import(self, dry, force, jobs)
def _sync_import(self, dry, force, jobs, no_download):
sync_import(self, dry, force, jobs, no_download)
@rwlocked(read=["outs"])
def _check_missing_outputs(self):
@@ -571,6 +594,8 @@ class Stage(params.StageParams):
@rwlocked(write=["outs"])
def checkout(self, allow_missing=False, **kwargs):
stats = defaultdict(list)
if self.is_partial_import:
return {}
for out in self.filter_outs(kwargs.get("filter_info")):
key, outs = self._checkout(
out,
@@ -658,6 +683,9 @@ class Stage(params.StageParams):
self, *args, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return set of object IDs used by this stage."""
if self.is_partial_import and not self.is_repo_import:
return {}
used_objs = defaultdict(set)
for out in self.filter_outs(kwargs.get("filter_info")):
for odb, objs in out.get_used_objs(*args, **kwargs).items():
@@ -680,7 +708,7 @@ class Stage(params.StageParams):
"unable to auto-merge DVC files with deleted outputs"
)
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
assert other
if not other.outs:
@@ -700,7 +728,7 @@ class Stage(params.StageParams):
self._check_can_merge(self, ancestor_out)
self._check_can_merge(other, ancestor_out)
self.outs[0].merge(ancestor_out, other.outs[0])
self.outs[0].merge(ancestor_out, other.outs[0], allowed=allowed)
def dump(self, **kwargs):
self.dvcfile.dump(self, **kwargs)
@@ -742,5 +770,5 @@ class PipelineStage(Stage):
def _changed_stage_entry(self):
return f"'cmd' of {self} has changed."
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
+1 -1
View File
@@ -9,7 +9,7 @@ from funcy import cached_property, first
from dvc import fs
from dvc.exceptions import DvcException
from dvc.utils import dict_sha256, relpath
from dvc_data.transfer import _log_exceptions
from dvc_data.hashfile.transfer import _log_exceptions
if TYPE_CHECKING:
from dvc_objects.db import ObjectDB
+6 -1
View File
@@ -31,7 +31,7 @@ class StageFileIsNotDvcFileError(DvcException):
sname = fname + DVC_FILE_SUFFIX
if is_dvc_file(sname):
msg += f" Do you mean '{sname}'?"
msg += f". Do you mean '{sname}'?"
super().__init__(msg)
@@ -80,6 +80,11 @@ class MissingDataSource(DvcException):
super().__init__(msg)
class DataSourceChanged(DvcException):
def __init__(self, path: str):
super().__init__(f"data source changed: {path}")
class StageNotFound(DvcException, KeyError):
def __init__(self, file, name):
self.file = file.relpath
+27 -4
View File
@@ -17,20 +17,36 @@ def _update_import_on_remote(stage, remote, jobs):
stage.outs[0].transfer(url, odb=odb, jobs=jobs, update=True)
def update_import(stage, rev=None, to_remote=False, remote=None, jobs=None):
def update_import(
stage, rev=None, to_remote=False, remote=None, no_download=None, jobs=None
):
stage.deps[0].update(rev=rev)
outs = stage.outs
deps = stage.deps
frozen = stage.frozen
stage.frozen = False
if stage.outs:
stage.outs[0].clear()
try:
if to_remote:
_update_import_on_remote(stage, remote, jobs)
else:
stage.reproduce(jobs=jobs)
stage.reproduce(no_download=no_download, jobs=jobs)
finally:
if deps == stage.deps:
stage.outs = outs
stage.frozen = frozen
def sync_import(stage, dry=False, force=False, jobs=None):
def sync_import(
stage,
dry=False,
force=False,
jobs=None,
no_download=False,
):
"""Synchronize import's outs to the workspace."""
logger.info("Importing '%s' -> '%s'", stage.deps[0], stage.outs[0])
if dry:
@@ -40,4 +56,11 @@ def sync_import(stage, dry=False, force=False, jobs=None):
stage.outs[0].checkout()
else:
stage.save_deps()
stage.deps[0].download(stage.outs[0], jobs=jobs)
if no_download:
if stage.is_repo_import:
stage.deps[0].update()
else:
stage.deps[0].download(
stage.outs[0],
jobs=jobs,
)
+5 -1
View File
@@ -69,7 +69,11 @@ class StageLoader(Mapping):
info = info.copy()
info.pop("path", None)
item.meta = Meta.from_dict(info)
item.hash_info = HashInfo.from_dict(info)
hash_value = getattr(item.meta, item.hash_name, None)
item.hash_info = HashInfo(item.hash_name, hash_value)
if item.hash_info and item.hash_info.isdir:
item.meta.isdir = True
item.files = get_in(checksums, [key, path, item.PARAM_FILES])
@classmethod
def load_stage(cls, dvcfile, name, stage_data, lock_data=None):
+20 -10
View File
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, List, no_type_check
from funcy import post_processing
from dvc.dependency import ParamsDependency
from dvc.output import Output
from dvc.output import Annotation, Output
from dvc.utils.collections import apply_diff
from dvc.utils.serialize import parse_yaml_for_update
@@ -27,7 +27,7 @@ PARAM_METRIC = Output.PARAM_METRIC
PARAM_PLOT = Output.PARAM_PLOT
PARAM_PERSIST = Output.PARAM_PERSIST
PARAM_CHECKPOINT = Output.PARAM_CHECKPOINT
PARAM_DESC = Output.PARAM_DESC
PARAM_DESC = Annotation.PARAM_DESC
PARAM_REMOTE = Output.PARAM_REMOTE
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
@@ -38,8 +38,9 @@ sort_by_path = partial(sorted, key=attrgetter("def_path"))
@post_processing(OrderedDict)
def _get_flags(out):
if out.desc:
yield PARAM_DESC, out.desc
annot = out.annot.to_dict()
yield from annot.items()
if not out.use_cache:
yield PARAM_CACHE, False
if out.checkpoint:
@@ -151,16 +152,25 @@ def to_pipeline_file(stage: "PipelineStage"):
}
def to_single_stage_lockfile(stage: "Stage") -> dict:
def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
assert stage.cmd
def _dumpd(item):
meta_d = item.meta.to_dict()
meta_d.pop("isdir", None)
ret = [
(item.PARAM_PATH, item.def_path),
*item.hash_info.to_dict().items(),
*item.meta.to_dict().items(),
*meta_d.items(),
]
if item.hash_info.isdir and kwargs.get("with_files"):
if item.obj:
obj = item.obj
else:
obj = item.get_obj()
ret.append((item.PARAM_FILES, obj.as_list(with_meta=True)))
return OrderedDict(ret)
res = OrderedDict([("cmd", stage.cmd)])
@@ -180,13 +190,13 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
return res
def to_lockfile(stage: "PipelineStage") -> dict:
def to_lockfile(stage: "PipelineStage", **kwargs) -> dict:
assert stage.name
return {stage.name: to_single_stage_lockfile(stage)}
return {stage.name: to_single_stage_lockfile(stage, **kwargs)}
def to_single_stage_file(stage: "Stage"):
state = stage.dumpd()
def to_single_stage_file(stage: "Stage", **kwargs):
state = stage.dumpd(**kwargs)
# When we load a stage we parse yaml with a fast parser, which strips
# off all the comments and formatting. To retain those on update we do
+11 -6
View File
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Union
from funcy import concat, first, lsplit, rpartial, without
from dvc.annotations import ANNOTATION_FIELDS
from dvc.exceptions import InvalidArgumentError
from dvc_data.hashfile.meta import Meta
@@ -107,12 +108,16 @@ def _load_live_output(
return outs
def fill_stage_dependencies(stage, deps=None, erepo=None, params=None):
def fill_stage_dependencies(
stage, deps=None, erepo=None, params=None, fs_config=None
):
from dvc.dependency import loads_from, loads_params
assert not stage.deps
stage.deps = []
stage.deps += loads_from(stage, deps or [], erepo=erepo)
stage.deps += loads_from(
stage, deps or [], erepo=erepo, fs_config=fs_config
)
stage.deps += loads_params(stage, params or [])
@@ -194,9 +199,9 @@ def compute_md5(stage):
return dict_md5(
d,
exclude=[
*ANNOTATION_FIELDS,
stage.PARAM_LOCKED, # backward compatibility
stage.PARAM_FROZEN,
Output.PARAM_DESC,
Output.PARAM_METRIC,
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
@@ -221,7 +226,7 @@ def resolve_paths(fs, path, wdir=None):
return path, wdir
def get_dump(stage):
def get_dump(stage, **kwargs):
return {
key: value
for key, value in {
@@ -230,8 +235,8 @@ def get_dump(stage):
stage.PARAM_CMD: stage.cmd,
stage.PARAM_WDIR: resolve_wdir(stage.wdir, stage.path),
stage.PARAM_FROZEN: stage.frozen,
stage.PARAM_DEPS: [d.dumpd() for d in stage.deps],
stage.PARAM_OUTS: [o.dumpd() for o in stage.outs],
stage.PARAM_DEPS: [d.dumpd(**kwargs) for d in stage.deps],
stage.PARAM_OUTS: [o.dumpd(**kwargs) for o in stage.outs],
stage.PARAM_ALWAYS_CHANGED: stage.always_changed,
stage.PARAM_META: stage.meta,
}.items()
+125
View File
@@ -0,0 +1,125 @@
import pytest
from dvc import api
from dvc.api import DVCFileSystem
from dvc.utils.fs import remove
class TestAPI:
def test_get_url(self, tmp_dir, dvc, remote):
tmp_dir.dvc_gen("foo", "foo")
expected_url = (remote / "ac/bd18db4cc2f85cedef654fccc4a4d8").url
assert api.get_url("foo") == expected_url
def test_open(self, tmp_dir, dvc, remote):
tmp_dir.dvc_gen("foo", "foo-text")
dvc.push()
# Remove cache to force download
remove(dvc.odb.local.path)
with api.open("foo") as fd:
assert fd.read() == "foo-text"
@pytest.mark.parametrize(
"clear_cache", [True, False], ids=["cache", "no_cache"]
)
@pytest.mark.parametrize(
"fs_kwargs",
[
{},
{"url": "{path}"},
{"url": "{path}", "rev": "master"},
{"url": "file://{posixpath}"},
{"url": "file://{posixpath}", "rev": "master"},
],
ids=["current", "local", "local_rev", "git", "git_rev"],
)
def test_filesystem(
self,
M, # from `pytest-test-utils`
tmp_dir,
make_tmp_dir,
scm,
dvc,
remote,
fs_kwargs,
clear_cache,
):
fs_kwargs = fs_kwargs.copy()
tmp_dir.scm_gen({"scripts": {"script1": "script1"}}, commit="scripts")
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}}, commit="data")
dvc.push()
if clear_cache:
remove(dvc.odb.repo.path)
if url := fs_kwargs.get("url"):
fs_kwargs["url"] = url.format(
path=tmp_dir, posixpath=tmp_dir.as_posix()
)
fs = DVCFileSystem(**fs_kwargs)
assert fs.ls("/", detail=False) == M.unordered(
"/.gitignore", "/scripts", "/data"
)
assert fs.ls("scripts", detail=False) == ["scripts/script1"]
assert fs.ls("data", detail=False) == M.unordered(
"data/foo", "data/bar"
)
data_info = M.dict(
name="/data",
type="directory",
dvc_info=M.dict(isdvc=True, isout=True),
)
scripts_info = M.dict(name="/scripts", type="directory", isexec=False)
assert sorted(fs.ls("/"), key=lambda i: i["name"]) == [
M.dict(name="/.gitignore", type="file", isexec=False),
data_info,
scripts_info,
]
with pytest.raises(FileNotFoundError):
fs.info("/not-existing-path")
assert fs.info("/") == M.dict(name="/", isexec=False, type="directory")
assert fs.info("/data") == data_info
assert fs.info("/scripts") == scripts_info
assert fs.info("/data/foo") == M.dict(name="/data/foo", type="file")
assert fs.info("/scripts/script1") == M.dict(
name="/scripts/script1", type="file"
)
assert not fs.isdvc("/")
assert fs.isdvc("/data")
assert fs.isdvc("/data/foo")
assert not fs.isdvc("/scripts")
assert not fs.isdvc("/scripts/script1")
with pytest.raises((IsADirectoryError, PermissionError)):
fs.open("data")
with pytest.raises((IsADirectoryError, PermissionError)):
fs.open("scripts")
with fs.open("/data/foo") as fobj:
assert fobj.read() == b"foo"
with fs.open("/scripts/script1") as fobj:
assert fobj.read() == b"script1"
tmp = make_tmp_dir("temp-download")
fs.get_file("data/foo", tmp / "foo")
assert (tmp / "foo").read_text() == "foo"
fs.get_file("scripts/script1", tmp / "script1")
assert (tmp / "script1").read_text() == "script1"
fs.get("/", (tmp / "all").fs_path, recursive=True)
assert (tmp / "all").read_text() == {
".gitignore": "/data\n",
"data": {"bar": "bar", "foo": "foo"},
"scripts": {"script1": "script1"},
}
@@ -71,7 +71,7 @@ class TestRemote:
_check_status(status_dir, ok=dir_hashes)
# Remove and check status
remove(dvc.odb.local.path)
dvc.odb.local.clear()
status = dvc.cloud.status(foo_hashes)
_check_status(status, deleted={foo_hash})
-20
View File
@@ -1,20 +0,0 @@
from dvc import api
from dvc.utils.fs import remove
class TestAPI:
def test_get_url(self, tmp_dir, dvc, remote):
tmp_dir.dvc_gen("foo", "foo")
expected_url = (remote / "ac/bd18db4cc2f85cedef654fccc4a4d8").url
assert api.get_url("foo") == expected_url
def test_open(self, tmp_dir, dvc, remote):
tmp_dir.dvc_gen("foo", "foo-text")
dvc.push()
# Remove cache to force download
remove(dvc.odb.local.path)
with api.open("foo") as fd:
assert fd.read() == "foo-text"
@@ -2,6 +2,9 @@ import os
import pytest
from dvc.exceptions import URLMissingError
from dvc.repo.ls_url import ls_url, parse_external_url
class TestImport:
def test_import(self, tmp_dir, dvc, workspace):
@@ -134,3 +137,62 @@ class TestAdd:
assert (
workspace / "cache" / dir_hash_value[:2] / dir_hash_value[2:]
).is_file()
def match_files(fs, entries, expected):
entries_content = {
(fs.path.normpath(d["path"]), d["isdir"]) for d in entries
}
expected_content = {
(fs.path.normpath(d["path"]), d["isdir"]) for d in expected
}
assert entries_content == expected_content
class TestLsUrl:
@pytest.mark.parametrize("fname", ["foo", "foo.dvc", "dir/foo"])
def test_file(self, cloud, fname):
cloud.gen({fname: "foo contents"})
fs, fs_path = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / fname), config=cloud.config)
match_files(
fs,
result,
[{"path": fs.path.join(fs_path, fname), "isdir": False}],
)
def test_dir(self, cloud):
cloud.gen(
{"dir/foo": "foo contents", "dir/subdir/bar": "bar contents"}
)
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / "dir"), config=cloud.config)
match_files(
fs,
result,
[
{"path": "foo", "isdir": False},
{"path": "subdir", "isdir": True},
],
)
def test_recursive(self, cloud):
cloud.gen(
{"dir/foo": "foo contents", "dir/subdir/bar": "bar contents"}
)
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(
str(cloud / "dir"), config=cloud.config, recursive=True
)
match_files(
fs,
result,
[
{"path": "foo", "isdir": False},
{"path": "subdir/bar", "isdir": False},
],
)
def test_nonexistent(self, cloud):
with pytest.raises(URLMissingError):
ls_url(str(cloud / "dir"), config=cloud.config)

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