Compare commits

...
118 Commits
Author SHA1 Message Date
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
138 changed files with 3145 additions and 1091 deletions
-1
View File
@@ -1 +0,0 @@
* @iterative/dvc
+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 }}
+9 -4
View File
@@ -43,6 +43,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:
@@ -68,12 +74,11 @@ jobs:
run: |
pip install --upgrade pip setuptools wheel
pip install -e ".[dev]"
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.pyv == '3.11-dev' && '1' }}
- 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:
+5 -1
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|
@@ -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
+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)
+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
+132 -20
View File
@@ -1,9 +1,12 @@
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.ui import ui
@@ -31,15 +34,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 +86,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 +103,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 +114,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 +127,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 +242,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,
help=DATA_LS_HELP,
)
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)
+2 -1
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
+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
+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)
+24 -32
View File
@@ -155,11 +155,6 @@ class CmdPlots(CmdBase):
out=renderers_out,
templates_dir=self.repo.plots.templates_dir,
)
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)
@@ -167,6 +162,12 @@ class CmdPlots(CmdBase):
_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
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
if renderers:
@@ -222,31 +223,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 +348,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 +358,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)
+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 hash value(s)"
" but do not download the file(s).",
)
update_parser.add_argument(
"--to-remote",
action="store_true",
+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 {}
+6 -6
View File
@@ -59,11 +59,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)
@@ -103,8 +103,8 @@ class RepoDependency(Dependency):
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)
@@ -153,8 +153,8 @@ class RepoDependency(Dependency):
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_data.hashfile.db.reference import ReferenceHashFileDB
from dvc_data.hashfile.tree import Tree
if not isinstance(odb, ReferenceHashFileDB):
return
+6 -3
View File
@@ -198,7 +198,7 @@ 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
@@ -243,9 +243,11 @@ class PipelineFile(FileMixin):
self._lockfile.dump(stage)
@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
+17 -25
View File
@@ -10,6 +10,8 @@ from dvc_objects.fs import ( # noqa: F401
Schemes,
generic,
get_fs_cls,
known_implementations,
localfs,
registry,
system,
)
@@ -19,33 +21,26 @@ 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 .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
@@ -116,11 +111,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
+119 -203
View File
@@ -4,12 +4,12 @@ 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.utils.fs import makedirs
from dvc_objects.fs.base import FileSystem
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
from dvc_objects.fs.path import Path
@@ -22,29 +22,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,6 +60,10 @@ def _merge_info(repo, fs_info, dvc_info):
return ret
def _get_dvc_path(dvc_fs, subkey):
return dvc_fs.path.join(*subkey) if subkey else ""
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
"""DVC + git-tracked files fs.
@@ -82,28 +76,25 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
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:
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 +128,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 +164,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 +201,64 @@ 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
_, 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)
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 +266,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)
@@ -378,45 +294,57 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
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)
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
fs = self.repo.fs
if fs:
try:
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
return
except FileNotFoundError:
if not dvc_fs:
raise
if self.isdir(rpath):
makedirs(lpath, exist_ok=True)
return None
try:
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
return
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
dvc_path = _get_dvc_path(dvc_fs, subkey)
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,14 +360,6 @@ 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):
protocol = "local"
@@ -468,10 +388,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,
+27 -2
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()}",
]
@@ -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:
+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):
+155 -54
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,18 @@ class Output:
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if (
self.use_cache
and self.is_in_repo
and self.hash_info.isdir
and (kwargs.get("with_files") or self.files is not None)
):
if self.obj:
obj = self.obj
else:
obj = self.get_obj()
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
return ret
def verify_metric(self):
@@ -731,14 +809,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 +944,7 @@ class Output:
)
self.hash_info = obj.hash_info
self.files = None
return obj
def get_files_number(self, filter_info=None):
@@ -928,6 +1015,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 +1108,7 @@ class Output:
other = out.dumpd()
ignored = [
self.fs.PARAM_CHECKSUM,
self.hash_name,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
]
@@ -1040,8 +1128,8 @@ class Output:
)
def merge(self, ancestor, other):
from dvc_data.objects.tree import MergeError as TreeMergeError
from dvc_data.objects.tree import du, merge
from dvc_data.hashfile.tree import MergeError as TreeMergeError
from dvc_data.hashfile.tree import du, merge
assert other
@@ -1064,6 +1152,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 +1175,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)
+40 -1
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__)
@@ -444,6 +445,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
+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
+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()}
+1 -1
View File
@@ -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)
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class BaseExecutorManager(ABC, Mapping):
class BaseExecutorManager(ABC, Mapping): # noqa: B024
"""Manages executors for a collection of experiments to be run."""
EXECUTOR_CLS: Type = BaseExecutor
+1 -2
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,
+27 -3
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
@@ -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
+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
+79
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,36 @@ 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)
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
entry = repo.index.data[workspace][key]
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage)
return len(index)
@locked
def fetch(
self,
@@ -42,6 +74,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 +130,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 +156,33 @@ 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)
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 -10
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)
@@ -202,9 +194,10 @@ class Index:
data_index[key] = DataIndexEntry(
meta=out.meta,
obj=out.obj,
obj=out.get_obj() if out.files else out.obj,
hash_info=out.hash_info,
odb=out.odb,
cache=out.odb,
remote=remote,
)
@@ -246,6 +239,28 @@ 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, with_deps=True
)
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.
+11 -12
View File
@@ -1,5 +1,4 @@
import os
from itertools import chain
from typing import TYPE_CHECKING, Optional
from dvc.exceptions import PathMissingError
@@ -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
+6 -4
View File
@@ -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
+73 -38
View File
@@ -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
@@ -453,7 +477,10 @@ def _collect_definitions(
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
)
+50
View File
@@ -9,6 +9,52 @@ 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 build, checkout, collect
index = repo.index.data["repo"]
checkout(index, remote.path, remote.fs, force=True)
build(index, remote.path, remote.fs)
if any(out.isdir() for out in repo.index.outs):
collect(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)
return len(index)
@locked
def push(
self,
@@ -26,6 +72,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,
)
)
+11 -22
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.
@@ -250,15 +247,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 +263,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 +332,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 +340,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 +359,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 +370,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 +382,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 +408,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 +430,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,
+43 -17
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,7 +565,7 @@ 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 not no_commit:
@@ -555,8 +576,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 +592,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 +681,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:
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():
+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
+24 -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,8 @@ 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 not no_download:
stage.deps[0].download(
stage.outs[0],
jobs=jobs,
)
+4 -1
View File
@@ -69,7 +69,10 @@ 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
@classmethod
def load_stage(cls, dvcfile, name, stage_data, lock_data=None):
+10 -7
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:
@@ -155,10 +156,12 @@ def to_single_stage_lockfile(stage: "Stage") -> 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(),
]
return OrderedDict(ret)
@@ -185,8 +188,8 @@ def to_lockfile(stage: "PipelineStage") -> dict:
return {stage.name: to_single_stage_lockfile(stage)}
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()
+103
View File
@@ -1,4 +1,7 @@
import pytest
from dvc import api
from dvc.api import DvcFileSystem
from dvc.utils.fs import remove
@@ -18,3 +21,103 @@ class TestAPI:
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,
tmp_dir,
make_tmp_dir,
scm,
dvc,
remote,
fs_kwargs,
clear_cache,
):
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"},
}
+63 -7
View File
@@ -1,21 +1,50 @@
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, List
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from hydra.types import RunMode
from omegaconf import OmegaConf
from dvc.exceptions import InvalidArgumentError
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
from .serialize import MODIFIERS
from .serialize import DUMPERS, MODIFIERS
if TYPE_CHECKING:
from dvc.types import StrPath
def compose_and_dump(
output_file: "StrPath",
config_dir: str,
config_name: str,
overrides: List[str],
) -> None:
"""Compose Hydra config and dumpt it to `output_file`.
Args:
output_file: File where the composed config will be dumped.
config_dir: Folder containing the Hydra config files.
Must be absolute file system path.
config_name: Name of the config file containing defaults,
without the .yaml extension.
overrides: List of `Hydra Override`_ patterns.
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
with initialize_config_dir(config_dir, version_base=None):
cfg = compose(config_name=config_name, overrides=overrides)
dumper = DUMPERS[Path(output_file).suffix.lower()]
dumper(output_file, OmegaConf.to_object(cfg))
def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
"""Update `path` params with the provided `Hydra Override`_ patterns.
@@ -32,11 +61,7 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
modify_data = MODIFIERS[suffix]
with modify_data(path) as original_data:
try:
parser = OverridesParser.create()
parsed = parser.parse_overrides(overrides=overrides)
ConfigLoaderImpl.validate_sweep_overrides_legal(
parsed, run_mode=RunMode.RUN, from_shell=True
)
parsed = to_hydra_overrides(overrides)
new_data = OmegaConf.create(
to_omegaconf(original_data),
@@ -51,3 +76,34 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
merge_dicts(original_data, new_data)
remove_missing_keys(original_data, new_data)
def to_hydra_overrides(path_overrides):
parser = OverridesParser.create()
return parser.parse_overrides(overrides=path_overrides)
def get_hydra_sweeps(path_overrides):
merged_overrides = []
for path, overrides in path_overrides.items():
merged_overrides.extend(
[f"{path}___{override}" for override in overrides]
)
hydra_overrides = to_hydra_overrides(merged_overrides)
for hydra_override in hydra_overrides:
if hydra_override.value_type == ValueType.GLOB_CHOICE_SWEEP:
raise InvalidArgumentError(
f"Glob override '{hydra_override.input_line}' "
"is not supported."
)
splits = BasicSweeper.split_arguments(hydra_overrides, None)[0]
sweeps = []
for split in splits:
sweep_overrides = defaultdict(list)
for merged_override in split:
path, override = merged_override.split("___")
sweep_overrides[path].append(override)
sweeps.append(dict(sweep_overrides))
return sweeps
+3 -3
View File
@@ -30,11 +30,11 @@ line_length = 79
[tool.pytest.ini_options]
log_level = "debug"
addopts = "-ra"
addopts = "-ra -p no:hydra_pytest"
markers = [
"needs_internet: Might need network access for the tests",
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio"
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio",
]
xfail_strict = true
+1 -1
View File
@@ -1,3 +1,3 @@
# NOTE: using strict version due to
# https://github.com/iterative/dvc/issues/7949
PyInstaller==5.2
PyInstaller==5.0
+1 -1
View File
@@ -1,5 +1,5 @@
import argparse
import importlib
import importlib.util
import pathlib
import textwrap
from subprocess import STDOUT, check_call
+20 -3
View File
@@ -1,6 +1,6 @@
import os
import pathlib
from subprocess import STDOUT, check_call
from subprocess import STDOUT, check_call, check_output
path = pathlib.Path(__file__).parent.absolute()
hooks = path / "hooks"
@@ -21,10 +21,27 @@ check_call(
stderr=STDOUT,
)
check_call(
out = check_output(
[
path / "dist" / "dvc" / "dvc",
"doctor",
],
stderr=STDOUT,
)
).decode()
remotes = [
"s3",
"oss",
"gdrive",
"gs",
"hdfs",
"http",
"webhdfs",
"azure",
"ssh",
"webdav",
]
print(out)
for remote in remotes:
assert f"\t{remote}" in out, f"Missing support for {remote}"
+12 -2
View File
@@ -16,5 +16,15 @@ datas += copy_metadata("webdav4")
datas += copy_metadata("aiohttp")
datas += copy_metadata("aiohttp_retry")
# https://github.com/pypa/setuptools/issues/1963
hiddenimports = ["pkg_resources.py2_warn"]
hiddenimports = [
"dvc_azure",
"dvc_gdrive",
"dvc_gs",
"dvc_hdfs",
"dvc_oss",
"dvc_s3",
"dvc_webdav",
"dvc_webhdfs",
# https://github.com/pypa/setuptools/issues/1963
"pkg_resources.py2_warn",
]
+1
View File
@@ -0,0 +1 @@
hiddenimports = ["fsspec.implementations.memory"]
@@ -1,8 +0,0 @@
"""Temporary fix for https://github.com/iterative/dvc/issues/5618."""
from PyInstaller.utils.hooks import ( # pylint:disable=import-error
collect_data_files,
copy_metadata,
)
datas = collect_data_files("googleapiclient.discovery")
datas += copy_metadata("google_api_python_client")
+21 -34
View File
@@ -62,14 +62,12 @@ install_requires =
rich>=10.13.0
pyparsing>=2.4.7
typing-extensions>=3.7.4
fsspec[http]>=2021.10.1
aiohttp-retry>=2.4.5
scmrepo==0.0.25
dvc-render==0.0.9
scmrepo==0.1.1
dvc-render==0.0.10
dvc-task==0.1.2
dvclive>=0.10.0
dvc-data==0.1.17
dvc-http==0.0.2
dvc-data==0.8.0
dvc-http==2.19.1
hydra-core>=1.1.0
[options.extras_require]
@@ -82,46 +80,34 @@ all =
%(s3)s
%(ssh)s
%(webdav)s
%(webhdfs)s
dev =
%(all)s
%(tests)s
dvc-data[cli]
# Extra dependencies for remote integrations
azure =
adlfs>=2021.10.0
azure-identity>=1.4.0
knack
gdrive = pydrive2[fsspec]>=1.10.1
gs = gcsfs>=2021.11.1
azure = dvc-azure==2.20.2
gdrive = dvc-gdrive==2.19.0
gs = dvc-gs==2.19.1
hdfs =
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
# explicitly specify pyarrow here
pyarrow>=1
fsspec[arrow]
oss = ossfs>=2021.8.0
s3 =
s3fs[boto3]>=2022.02.0
aiobotocore[boto3]>2
ssh =
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
# explicitly specify bcrypt here
bcrypt
sshfs[bcrypt]>=2022.6.0
dvc-hdfs==2.19.0; python_version < '3.11'
oss = dvc-oss==2.19.0
s3 = dvc-s3==2.20.0
ssh = dvc-ssh==2.19.0
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for Linux, so it will fail to compile if user doesn't have all the
# requirements, including kerberos itself.
ssh_gssapi = sshfs[gssapi]>=2022.6.0
webdav = webdav4>=0.9.3
# not to break `dvc[webhdfs]`
webhdfs =
ssh_gssapi = dvc-ssh[gssapi]==2.19.0
webdav = dvc-webdav==2.19.0
webhdfs = dvc-webhdfs==2.19.0
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
webhdfs_kerberos = requests-kerberos==0.14.0
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
terraform = tpi[ssh]>=2.1.0
tests =
%(terraform)s
dvc_ssh==0.0.1a0
dvc-ssh==2.19.0
# Test requirements
pytest==7.1.2
pytest==7.1.3
pytest-cov==3.0.0
pytest-xdist==2.5.0
pytest-mock==3.8.2
@@ -130,11 +116,10 @@ tests =
# https://github.com/docker/docker-py/issues/2902
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
flaky==3.7.0
mock==4.0.3
pytest-timeout==2.1.0
filelock==3.8.0
# pylint requirements
pylint==2.14.5
pylint==2.15.2
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.1.2
# we use this to suppress some messages in tests, eg: foo/bar naming,
@@ -160,6 +145,8 @@ exclude =
[options.entry_points]
console_scripts =
dvc = dvc.cli:main
fsspec.specs =
dvc = dvc.api:DvcFileSystem
[flake8]
ignore=
+12
View File
@@ -250,3 +250,15 @@ def run_copy_metrics(tmp_dir, run_copy):
return stage
return run
@pytest.fixture(autouse=True)
def mock_hydra_conf(mocker):
if sys.version_info < (3, 11):
return
# `hydra.conf` fails to import in 3.11, it raises ValueError due to changes
# in dataclasses. See https://github.com/python/cpython/pull/29867.
# NOTE: using sentinel here so that any imports from `hydra.conf`
# return a mock.
sys.modules["hydra.conf"] = mocker.sentinel
+37
View File
@@ -1,3 +1,4 @@
import os
from textwrap import dedent
import pytest
@@ -67,6 +68,9 @@ def test_params_show_targets(params_repo):
"bar": 2,
"foobar": 3,
}
assert api.params_show("params.yaml", stages="stage-1") == {
"foo": 5,
}
def test_params_show_deps(params_repo):
@@ -92,6 +96,27 @@ def test_params_show_stages(params_repo):
api.params_show(stages="stage-0")
def test_params_show_stage_addressing(tmp_dir, dvc):
for subdir in {"subdir1", "subdir2"}:
subdir = tmp_dir / subdir
subdir.mkdir()
with subdir.chdir():
subdir.gen("params.yaml", "foo: 1")
dvc.run(name="stage-0", cmd="echo stage-0", params=["foo"])
for s in {"subdir1", "subdir2"}:
dvcyaml = os.path.join(s, "dvc.yaml")
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
with subdir.chdir():
nested = subdir / "nested"
nested.mkdir()
with nested.chdir():
dvcyaml = os.path.join("..", "dvc.yaml")
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
def test_params_show_revs(params_repo):
assert api.params_show(rev="HEAD~1") == {
"params.yaml:foo": 1,
@@ -168,3 +193,15 @@ def test_params_show_stage_without_params(tmp_dir, dvc):
with pytest.raises(DvcException, match="No params found"):
api.params_show(deps=True)
def test_params_show_untracked_target(params_repo, tmp_dir):
tmp_dir.gen("params_foo.yaml", "foo: 1")
assert api.params_show("params_foo.yaml") == {"foo": 1}
with pytest.raises(DvcException, match="No params found"):
api.params_show("params_foo.yaml", stages="stage-0")
with pytest.raises(DvcException, match="No params found"):
api.params_show("params_foo.yaml", deps=True)
+1 -1
View File
@@ -3,7 +3,7 @@ import pytest
from dvc.exceptions import DownloadError, UploadError
from dvc.fs import LocalFileSystem
from dvc.utils.fs import remove
from dvc_data.db import get_index
from dvc_data.hashfile.db import get_index
@pytest.fixture
+3 -1
View File
@@ -3,7 +3,9 @@ from textwrap import dedent
import pytest
from tests.func.test_repro_multistage import COPY_SCRIPT
from tests.unit.repo.experiments.conftest import test_queue # noqa
from tests.unit.repo.experiments.conftest import ( # noqa pylint disable=unused-argument
test_queue,
)
DEFAULT_ITERATIONS = 2
CHECKPOINT_SCRIPT_FORMAT = dedent(
+1 -1
View File
@@ -4,9 +4,9 @@ from functools import partial
from urllib.parse import urlparse
import pytest
from dvc_ssh import SSHFileSystem
from dvc_ssh.tests.cloud import TEST_SSH_KEY_PATH, TEST_SSH_USER
from dvc.fs import SSHFileSystem
from dvc.repo.experiments.executor.base import ExecutorInfo, ExecutorResult
from dvc.repo.experiments.executor.ssh import SSHExecutor
from dvc.repo.experiments.refs import EXEC_HEAD, EXEC_MERGE
+11
View File
@@ -1,6 +1,17 @@
from funcy import first
def test_diff_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().experiments.diff() == {}
def test_diff_empty(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.diff() == {"params": {}, "metrics": {}}
@@ -104,20 +104,6 @@ def test_failed_exp_workspace(
)
@pytest.mark.parametrize(
"changes, expected",
[
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
],
)
def test_modify_params(params_repo, dvc, changes, expected):
dvc.experiments.run(params=changes)
# pylint: disable=unspecified-encoding
with open("params.yaml", mode="r") as fobj:
assert fobj.read().strip() == expected
def test_apply(tmp_dir, scm, dvc, exp_stage):
from dvc.exceptions import InvalidArgumentError
from dvc.repo.experiments.exceptions import ApplyConflictError
+2 -1
View File
@@ -42,6 +42,7 @@ def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
return name_list
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
@pytest.mark.parametrize("follow", [True, False])
def test_celery_logs(
tmp_dir,
@@ -64,7 +65,7 @@ def test_celery_logs(
assert "failed to reproduce 'failed-copy-file'" in captured.out
@pytest.mark.flaky(max_runs=3, min_passes=1)
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
def test_queue_remove_done(dvc, failed_tasks, success_tasks):
assert len(dvc.experiments.celery_queue.failed_stash) == 3
status = to_dict(dvc.experiments.celery_queue.status())
+143
View File
@@ -0,0 +1,143 @@
import sys
import pytest
from dvc.exceptions import InvalidArgumentError
from ..utils.test_hydra import hydra_setup
@pytest.mark.parametrize(
"changes, expected",
[
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
],
)
def test_modify_params(params_repo, dvc, changes, expected):
dvc.experiments.run(params=changes)
# pylint: disable=unspecified-encoding
with open("params.yaml", mode="r") as fobj:
assert fobj.read().strip() == expected
@pytest.mark.parametrize(
"hydra_enabled",
[
pytest.param(
True,
marks=pytest.mark.skipif(
sys.version_info >= (3, 11), reason="unsupported on 3.11"
),
),
False,
],
)
@pytest.mark.parametrize(
"config_dir,config_name",
[
(None, None),
(None, "bar"),
("conf", "bar"),
],
)
def test_hydra_compose_and_dump(
tmp_dir, params_repo, dvc, hydra_enabled, config_dir, config_name
):
hydra_setup(
tmp_dir,
config_dir=config_dir or "conf",
config_name=config_name or "config",
)
dvc.experiments.run()
assert (tmp_dir / "params.yaml").parse() == {
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": False,
}
with dvc.config.edit() as conf:
if hydra_enabled:
conf["hydra"]["enabled"] = True
if config_dir is not None:
conf["hydra"]["config_dir"] = config_dir
if config_name is not None:
conf["hydra"]["config_name"] = config_name
dvc.experiments.run()
if hydra_enabled:
assert (tmp_dir / "params.yaml").parse() == {
"db": {"driver": "mysql", "user": "omry", "pass": "secret"},
}
dvc.experiments.run(params=["db=postgresql"])
assert (tmp_dir / "params.yaml").parse() == {
"db": {
"driver": "postgresql",
"user": "foo",
"pass": "bar",
"timeout": 10,
}
}
else:
assert (tmp_dir / "params.yaml").parse() == {
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": False,
}
@pytest.mark.parametrize(
"hydra_enabled,overrides,expected",
[
(
pytest.param(
True,
marks=pytest.mark.xfail(
sys.version_info >= (3, 11), reason="unsupported on 3.11"
),
),
["db=mysql,postgresql"],
[
{"params.yaml": ["db=mysql"]},
{"params.yaml": ["db=postgresql"]},
],
),
(
False,
["foo=bar,baz"],
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
),
],
)
def test_hydra_sweep(
tmp_dir, params_repo, dvc, mocker, hydra_enabled, overrides, expected
):
patched = mocker.patch.object(dvc.experiments, "queue_one")
if hydra_enabled:
hydra_setup(
tmp_dir,
config_dir="conf",
config_name="config",
)
with dvc.config.edit() as conf:
conf["hydra"]["enabled"] = True
dvc.experiments.run(params=overrides, queue=True)
assert patched.call_count == len(expected)
for e in expected:
patched.assert_any_call(
mocker.ANY,
params=e,
reset=True,
targets=None,
)
def test_hydra_sweep_requires_queue(params_repo, dvc):
with pytest.raises(InvalidArgumentError):
dvc.experiments.run(params=["db=mysql,postgresql"])
+12
View File
@@ -35,6 +35,18 @@ def make_executor_info(**kwargs):
return ExecutorInfo(**kwargs)
@pytest.mark.vscode
def test_show_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().experiments.show() == {}
@pytest.mark.vscode
def test_show_simple(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.show()["workspace"] == {
+26
View File
@@ -21,6 +21,32 @@ def test_show_simple(tmp_dir, dvc, run_copy_metrics):
}
def test_show_simple_from_subdir(tmp_dir, dvc, run_copy_metrics):
subdir = tmp_dir / "subdir"
subdir.mkdir()
tmp_dir.gen("metrics_t.yaml", "1.1")
run_copy_metrics(
"metrics_t.yaml",
"subdir/metrics.yaml",
metrics=["subdir/metrics.yaml"],
)
expected_path = os.path.join("subdir", "metrics.yaml")
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
expected_path = os.path.join("..", "subdir", "metrics.yaml")
with subdir.chdir():
assert dvc.metrics.show() == {
"": {"data": {expected_path: {"data": 1.1}}}
}
subdir2 = tmp_dir / "subdir2"
subdir2.mkdir()
with subdir2.chdir():
assert dvc.metrics.show() == {
"": {"data": {expected_path: {"data": 1.1}}}
}
def test_show(tmp_dir, dvc, run_copy_metrics):
tmp_dir.gen("metrics_t.yaml", "foo: 1.1")
run_copy_metrics(
+14
View File
@@ -1,3 +1,5 @@
import pytest
from tests.utils.plots import get_plot
@@ -46,3 +48,15 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
assert get_plot(
diff_result, "workspace", "definitions", file="", endkey="data"
) == {"metric.json": props}
@pytest.mark.vscode
def test_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().plots.diff() == {}
+32 -3
View File
@@ -12,6 +12,7 @@ import pytest
import dvc as dvc_module
import dvc_data
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.exceptions import (
@@ -103,7 +104,7 @@ def test_add_unsupported_file(dvc):
def test_add_directory(tmp_dir, dvc):
from dvc_data import load
from dvc_data.hashfile import load
(stage,) = tmp_dir.dvc_gen({"dir": {"file": "file"}})
@@ -457,7 +458,7 @@ class TestAddCommit(TestDvc):
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
tmp_dir.gen({"data/data": "foo"})
counter = mocker.spy(dvc_data.build, "_build_tree")
counter = mocker.spy(dvc_data.hashfile.build, "_build_tree")
ret = main(["add", "data"])
assert ret == 0
assert counter.mock.call_count == 3
@@ -881,7 +882,7 @@ def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys):
tmp_dir.gen("foo", "foo")
mocker.patch(
"dvc_data.checkout.test_links",
"dvc_data.hashfile.checkout.test_links",
return_value=[],
)
dvc.add("foo")
@@ -903,6 +904,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
remote: testremote
meta: some metadata
"""
@@ -916,6 +921,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
remote: testremote
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
@@ -1161,3 +1170,23 @@ def test_add_ignore_duplicated_targets(tmp_dir, dvc, capsys):
_, err = capsys.readouterr()
assert len(stages) == 3
assert "ignoring duplicated targets: foo, bar" in err
def test_add_with_annotations(M, tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
(stage,) = dvc.add("foo", **annot)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
(stage,) = dvc.add("foo", type="t2")
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
+1 -1
View File
@@ -90,7 +90,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
class TestCheckoutCorruptedCacheDir(TestDvc):
def test(self):
from dvc_data import load
from dvc_data.hashfile import load
# NOTE: using 'copy' so that cache and link don't have same inode
ret = main(["config", "cache.type", "copy"])
+14
View File
@@ -47,6 +47,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key1: value1
key2: value2
remote: testremote
meta: some metadata
"""
@@ -61,6 +68,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key1: value1
key2: value2
remote: testremote
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
+15 -1
View File
@@ -13,8 +13,8 @@ from dvc.testing.test_remote import ( # noqa, pylint: disable=unused-import
TestRemote,
)
from dvc.utils.fs import remove
from dvc_data.db.local import LocalHashFileDB
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
def test_cloud_cli(tmp_dir, dvc, remote, mocker):
@@ -273,6 +273,20 @@ def test_pull_external_dvc_imports(tmp_dir, dvc, scm, erepo_dir):
assert (tmp_dir / "new_dir" / "bar").read_text() == "bar"
def test_pull_partial_import(tmp_dir, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
stage = dvc.imp_url(
"remote://workspace/file", os.fspath(dst), no_download=True
)
result = dvc.pull("file")
assert result["fetched"] == 1
assert dst.exists()
assert stage.outs[0].get_hash().value == "d10b4c3ff123b26dc068d43a8bef2d23"
def test_pull_external_dvc_imports_mixed(
tmp_dir, dvc, scm, erepo_dir, local_remote
):
+31
View File
@@ -0,0 +1,31 @@
from operator import itemgetter
from dvc.repo.data import ls
def test_data_ls(tmp_dir, dvc):
assert not list(ls(dvc))
tmp_dir.dvc_gen("bar", "bar")
tmp_dir.gen("foo", "foo")
dvc.add(
"foo",
meta={"key": "value"},
labels=["l1", "l2"],
type="t1",
desc="foo",
)
foo_entry = {
"path": "foo",
"desc": "foo",
"type": "t1",
"labels": ["l1", "l2"],
"meta": {"key": "value"},
}
assert sorted(ls(dvc), key=itemgetter("path")) == [
{"path": "bar"},
foo_entry,
]
assert list(ls(dvc, targets=["foo"])) == [foo_entry]
assert list(ls(dvc, targets=["foo"], recursive=True)) == [foo_entry]
+322 -53
View File
@@ -1,6 +1,10 @@
from os import fspath
from os.path import join
import pytest
from dvc.repo import Repo
from dvc.repo.data import _transform_git_paths_to_dvc, posixpath_to_os_path
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils.fs import remove
@@ -14,24 +18,55 @@ EMPTY_STATUS = {
}
def test_file(M, tmp_dir, scm, dvc):
@pytest.mark.parametrize("path", [None, ("sub", "repo")])
def test_git_to_dvc_path_wdir_transformation(tmp_dir, scm, path):
struct = {"dir": {"foo": "foo", "bar": "bar"}, "file": "file", "dir2": {}}
tmp_dir.gen(struct)
subdir = tmp_dir.joinpath(*path) if path else tmp_dir
make_subrepo(subdir, scm)
dvc = subdir.dvc
with subdir.chdir():
subdir.gen(struct)
_, _, untracked = scm.status(untracked_files="all")
# make order independent of the platforms for easier test assertions
untracked = sorted(map(posixpath_to_os_path, untracked), reverse=True)
assert _transform_git_paths_to_dvc(dvc, untracked) == [
"file",
join("dir", "foo"),
join("dir", "bar"),
]
with (subdir / "dir").chdir():
assert _transform_git_paths_to_dvc(dvc, untracked) == [
join("..", "file"),
"foo",
"bar",
]
with (subdir / "dir2").chdir():
assert _transform_git_paths_to_dvc(dvc, untracked) == [
join("..", "file"),
join("..", "dir", "foo"),
join("..", "dir", "bar"),
]
def test_file(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen("foo", "foo", commit="add foo")
tmp_dir.dvc_gen("foo", "foobar")
remove(tmp_dir / "foo")
expected = {
**EMPTY_STATUS,
"committed": {"modified": ["foo"]},
"uncommitted": {"deleted": ["foo"]},
"git": M.instance_of(dict),
"not_in_cache": [],
"unchanged": [],
"untracked": [],
"git": M.dict(),
}
assert dvc.data_status() == expected
assert dvc.data_status(granular=True) == expected
def test_directory(M, tmp_dir, scm, dvc):
def test_directory(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar", "foobar": "foobar"}})
remove(tmp_dir / "dir")
@@ -39,12 +74,10 @@ def test_directory(M, tmp_dir, scm, dvc):
tmp_dir.gen("untracked", "untracked")
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"uncommitted": {"modified": [join("dir", "")]},
"git": M.instance_of(dict),
"not_in_cache": [],
"unchanged": [],
"untracked": [],
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
@@ -52,20 +85,52 @@ def test_directory(M, tmp_dir, scm, dvc):
"added": M.unordered(
join("dir", "bar"),
join("dir", "foobar"),
)
),
"modified": [join("dir", "")],
},
"uncommitted": {
"added": [join("dir", "baz")],
"modified": [join("dir", "bar")],
"modified": M.unordered(join("dir", ""), join("dir", "bar")),
"deleted": [join("dir", "foobar")],
},
"git": M.instance_of(dict),
"git": M.dict(),
"not_in_cache": [],
"unchanged": [join("dir", "foo")],
"untracked": ["untracked"],
}
def test_tracked_directory_deep(M, tmp_dir, dvc, scm):
"""Test for a directory not in cwd, but nested inside other directories."""
(tmp_dir / "sub").gen({"dir": {"foo": "foo"}})
dvc.add(fspath(tmp_dir / "sub" / "dir"))
scm.add_commit(["sub/dir.dvc", "sub/.gitignore"], message="add sub/dir")
(tmp_dir / "sub" / "dir").gen("bar", "bar")
dvc.commit(None, force=True)
(tmp_dir / "sub" / "dir").gen("foobar", "foobar")
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("sub", "dir", "")]},
"uncommitted": {"modified": [join("sub", "dir", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"committed": {
"added": [join("sub", "dir", "bar")],
"modified": [join("sub", "dir", "")],
},
"uncommitted": {
"added": [join("sub", "dir", "foobar")],
"modified": [join("sub", "dir", "")],
},
"git": M.dict(),
"unchanged": [join("sub", "dir", "foo")],
}
def test_new_empty_git_repo(M, tmp_dir, scm):
dvc = Repo.init()
assert dvc.data_status() == {
@@ -81,52 +146,19 @@ def test_noscm_repo(dvc):
assert dvc.data_status() == EMPTY_STATUS
def test_unchanged(M, tmp_dir, scm, dvc):
def test_unchanged(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
assert dvc.data_status() == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"git": M.dict(),
"unchanged": M.unordered("bar", join("dir", "")),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo")),
}
def test_not_in_cache(M, tmp_dir, scm, dvc):
# TODO: investigation required, might return wrong results
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
remove(dvc.odb.local.path)
assert dvc.data_status() == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("bar", join("dir", "")),
"git": M.instance_of(dict),
"unchanged": ["bar"],
"uncommitted": {"added": [join("dir", "")]},
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("bar"),
"git": M.instance_of(dict),
"unchanged": [],
"committed": {"modified": ["bar"]},
"uncommitted": {"modified": ["bar"], "added": [join("dir", "foo")]},
}
def test_withdirs(M, tmp_dir, scm, dvc):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
assert dvc.data_status(granular=True, with_dirs=True) == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo"), join("dir", "")),
"git": M.dict(),
"unchanged": M.unordered("bar", join("dir", ""), join("dir", "foo")),
}
@@ -146,7 +178,8 @@ def test_skip_uncached_pipeline_outputs(tmp_dir, dvc, run_copy_metrics):
def test_output_with_newly_added_stage(tmp_dir, dvc):
dvc.stage.add(deps=["bar"], outs=["foo"], name="copy", cmd="cp foo bar")
assert dvc.data_status() == {**EMPTY_STATUS, "not_in_cache": ["foo"]}
assert dvc.data_status() == EMPTY_STATUS
assert dvc.data_status(granular=True) == EMPTY_STATUS
def test_subdir(M, tmp_dir, scm):
@@ -161,7 +194,243 @@ def test_subdir(M, tmp_dir, scm):
dvc = subrepo.dvc
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo")),
"git": M.dict(),
"unchanged": M.unordered(
"bar", join("dir", ""), join("dir", "foo")
),
"untracked": ["untracked"],
}
def test_untracked_newly_added_files(M, tmp_dir, dvc, scm):
tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.gen("foobar", "foobar")
expected = {
**EMPTY_STATUS,
"untracked": M.unordered(
join("dir", "foo"), join("dir", "bar"), "foobar"
),
"git": M.dict(),
}
assert dvc.data_status(untracked_files="all") == expected
assert dvc.data_status(granular=True, untracked_files="all") == expected
def test_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.dvc_gen("foobar", "foobar")
remove(dvc.odb.repo.path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"uncommitted": {
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
},
"not_in_cache": M.unordered(
"foobar",
join("dir", ""),
),
"git": M.dict(),
}
def test_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.dvc_gen("foobar", "foobar")
for path in [dvc.odb.repo.path, "dir", "foobar"]:
remove(path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
def test_git_committed_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
remove(dvc.odb.local.path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
"unchanged": M.unordered("foobar", join("dir", "")),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"not_in_cache": M.unordered(
"foobar",
join("dir", ""),
),
"uncommitted": {
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
},
"git": M.dict(),
"unchanged": M.unordered("foobar", join("dir", "")),
}
def test_git_committed_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
for path in [dvc.odb.repo.path, "dir", "foobar"]:
remove(path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"committed": {},
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
"git": M.dict(),
}
def test_partial_missing_cache(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
# remove "foo" from cache
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path("acbd18db4cc2f85cedef654fccc4a4d8"))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": [join("dir", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"added": M.unordered(
join("dir", ""), join("dir", "foo"), join("dir", "bar")
)
},
"not_in_cache": [join("dir", "foo")],
"git": M.dict(),
}
def test_missing_dir_object_from_head(M, tmp_dir, dvc, scm):
(stage,) = tmp_dir.dvc_gen(
{"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir"
)
remove("dir")
tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"modified": [join("dir", "")],
"unknown": [join("dir", "foobar")],
},
"git": M.dict(),
}
def test_missing_dir_object_from_index(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
remove("dir")
(stage,) = tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"not_in_cache": [join("dir", "")],
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"modified": [join("dir", "")],
},
"uncommitted": {"unknown": [join("dir", "foobar")]},
"not_in_cache": [join("dir", "")],
"git": M.dict(),
}
def test_root_from_dir_to_file(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
remove("data")
tmp_dir.gen("data", "file")
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": [join("data", "")]},
"uncommitted": {"modified": ["data"]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"added": M.unordered(
join("data", ""), join("data", "foo"), join("data", "bar")
)
},
"uncommitted": {
"deleted": M.unordered(join("data", "foo"), join("data", "bar")),
"modified": ["data"],
},
"git": M.dict(),
}
def test_root_from_file_to_dir(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen("data", "file")
remove("data")
tmp_dir.gen({"data": {"foo": "foo", "bar": "bar"}})
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": ["data"]},
"uncommitted": {"modified": [join("data", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {"added": ["data"]},
"uncommitted": {
"modified": [join("data", "")],
"added": M.unordered(join("data", "foo"), join("data", "bar")),
},
"git": M.dict(),
}
+30 -1
View File
@@ -3,6 +3,7 @@ import textwrap
import pytest
from dvc.annotations import Annotation
from dvc.dvcfile import (
PIPELINE_FILE,
PIPELINE_LOCK,
@@ -335,7 +336,7 @@ def test_dvcfile_dump_preserves_desc(tmp_dir, dvc, run_copy):
(tmp_dir / dvcfile.path).dump(data)
assert stage.desc == stage_desc
stage.outs[0].desc = out_desc
stage.outs[0].annot.desc = out_desc
dvcfile.dump(stage)
loaded = dvcfile._load()[0]
assert loaded == data
@@ -382,3 +383,31 @@ def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, name):
dvcfile.dump(stage)
assert str(exc.value) == f"cannot dump a parametrized stage: '{name}'"
def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
data = {
"stages": {
"stage1": {
"cmd": "cp foo bar",
"desc": "stage desc",
"meta": {"key1": "value1", "key2": "value2"},
"deps": ["foo"],
"outs": [
{"bar": {"desc": "bar desc", "meta": {"key": "value"}}}
],
}
}
}
(tmp_dir / "dvc.yaml").dump(data)
stage = dvc.stage.load_one(name="stage1")
assert stage.meta == {"key1": "value1", "key2": "value2"}
assert stage.desc == "stage desc"
assert stage.outs[0].annot == Annotation(
desc="bar desc", meta={"key": "value"}
)
# sanity check
stage.dump()
assert (tmp_dir / "dvc.yaml").parse() == data
+2 -2
View File
@@ -7,8 +7,8 @@ from dvc.external_repo import CLONES, external_repo
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils import relpath
from dvc.utils.fs import makedirs, remove
from dvc_data.build import build
from dvc_data.transfer import transfer
from dvc_data.hashfile.build import build
from dvc_data.hashfile.transfer import transfer
def test_external_repo(erepo_dir, mocker):
+1 -1
View File
@@ -11,7 +11,7 @@ from dvc.exceptions import CollectCacheError
from dvc.fs import LocalFileSystem
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
from dvc_data.db.local import LocalHashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
from tests.basic_env import TestDir, TestDvcGit
+1 -1
View File
@@ -10,7 +10,7 @@ from dvc.pathspec_math import PatternInfo, merge_patterns
from dvc.repo import Repo
from dvc.testing.tmp_dir import TmpDir
from dvc.types import List
from dvc_data.build import IgnoreInCollectedDirError
from dvc_data.hashfile.build import IgnoreInCollectedDirError
from dvc_data.hashfile.utils import get_mtime_and_size
+25 -1
View File
@@ -6,6 +6,7 @@ import pytest
from funcy import first
from scmrepo.git import Git
from dvc.annotations import Annotation
from dvc.config import NoRemoteError
from dvc.dvcfile import Dvcfile
from dvc.exceptions import DownloadError, PathMissingError
@@ -527,7 +528,7 @@ def test_import_with_no_exec(tmp_dir, dvc, erepo_dir):
def test_import_with_jobs(mocker, dvc, erepo_dir):
import dvc_data.transfer as otransfer
import dvc_data.hashfile.transfer as otransfer
with erepo_dir.chdir():
erepo_dir.dvc_gen(
@@ -627,3 +628,26 @@ def test_parameterized_repo(tmp_dir, dvc, scm, erepo_dir, paths):
"url": os.fspath(erepo_dir),
"rev_lock": erepo_dir.scm.get_rev(),
}
def test_import_with_annotations(M, tmp_dir, scm, dvc, erepo_dir):
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
stage = dvc.imp(os.fspath(erepo_dir), "foo", "foo", no_exec=True, **annot)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
stage = dvc.imp(
os.fspath(erepo_dir), "foo", "foo", no_exec=True, type="t2"
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
+83 -2
View File
@@ -5,8 +5,9 @@ from uuid import uuid4
import pytest
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dependency.base import DependencyDoesNotExistError
from dvc.dependency.base import Dependency, DependencyDoesNotExistError
from dvc.exceptions import InvalidArgumentError
from dvc.stage import Stage
from dvc.testing.test_workspace import TestImport as _TestImport
@@ -132,7 +133,7 @@ class TestImport(_TestImport):
return False
def test_import_url_preserve_meta(tmp_dir, dvc):
def test_import_url_preserve_fields(tmp_dir, dvc):
text = textwrap.dedent(
"""\
# top comment
@@ -142,6 +143,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
outs:
- path: bar # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
meta: some metadata
"""
)
@@ -160,6 +167,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
outs:
- path: bar # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
meta: some metadata
@@ -267,3 +280,71 @@ def test_import_url_to_remote_status(tmp_dir, dvc, local_cloud, local_remote):
status = dvc.status()
assert len(status) == 0
def test_import_url_no_download(tmp_dir, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
stage = dvc.imp_url(
"remote://workspace/file", os.fspath(dst), no_download=True
)
assert stage.deps[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
assert not dst.exists()
out = stage.outs[0]
assert not out.hash_info
assert out.meta.size is None
status = dvc.status()
assert status["file.dvc"] == [
{"changed outs": {"file": "deleted"}},
]
def test_imp_url_with_annotations(M, tmp_dir, dvc, local_workspace):
local_workspace.gen("foo", "foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
stage = dvc.imp_url(
"remote://workspace/foo",
os.fspath(tmp_dir / "foo"),
no_exec=True,
**annot,
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
stage = dvc.imp_url(
"remote://workspace/foo",
os.fspath(tmp_dir / "foo"),
no_exec=True,
type="t2",
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
def test_import_url_fs_config(tmp_dir, dvc, workspace, mocker):
import dvc.fs as dvc_fs
workspace.gen("foo", "foo")
url = "remote://workspace/foo"
get_fs_config = mocker.spy(dvc_fs, "get_fs_config")
dep_init = mocker.spy(Dependency, "__init__")
dvc.imp_url(url, fs_config={"jobs": 42})
dep_init_kwargs = dep_init.call_args[1]
assert dep_init_kwargs.get("fs_config") == {"jobs": 42}
assert get_fs_config.call_args_list[0][1] == {"url": "foo"}
assert get_fs_config.call_args_list[1][1] == {"url": url, "jobs": 42}
assert get_fs_config.call_args_list[2][1] == {"name": "workspace"}
+1 -1
View File
@@ -43,7 +43,7 @@ def test_cache(tmp_dir, dvc):
def test_cache_load_bad_dir_cache(tmp_dir, dvc):
from dvc_data import load
from dvc_data.hashfile import load
dir_hash = "123.dir"
fname = os.fspath(dvc.odb.local.oid_to_path(dir_hash))

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