Compare commits

...
97 Commits
Author SHA1 Message Date
karajan1001 5413187c2d Give lock acquiring more time in concurrency situation.
fix: #8417

1. Give the executor initialization and result collection more time for
   acquiring locks.
2022-10-21 10:18:28 +08:00
karajan1001 d56376c70a exp show: Use batch call on scm.describe
fix: #8451

1. Change the call of scm.describe from individual revision to a
   collection of them. Accelerate the run speed of `exp show`
2. Bump scmrepo to 0.1.2
2022-10-20 17:22:25 +08:00
Saugat Pachhai (सौगात) 1d5d15644d debug: add --show-stack/--ss flag 2022-10-19 17:13:22 +03:00
Peter Rowlands a0e45cdd5d index: remove partial_imports in favor of repo.imports view 2022-10-19 17:12:16 +03:00
Peter Rowlands 7fe316b8e8 import-url: only unpartial imports in workspace 2022-10-19 17:12:16 +03:00
Peter Rowlands 3478a7e6a9 import-url: support cloud versioning 2022-10-19 17:12:16 +03:00
Peter Rowlands d1fa3d2c21 fetch: use data index save() for fetching imports 2022-10-19 17:12:16 +03:00
Peter Rowlands b594337a69 index/out: add out.get_entry() 2022-10-19 17:12:16 +03:00
Peter Rowlands 36f6f29ac8 index: set source fs/path for import data index entries 2022-10-19 17:12:16 +03:00
skshetry bbaf57c5b2 _debug: minor refactor on debugtools (#8454)
* _debug: minor refactor on debugtools

* delete test
2022-10-19 18:01:20 +05:45
David de la Iglesia Castro 61f8478885 tests: Remove test_ssh
Moved to dvc-ssh repo in https://github.com/iterative/dvc-ssh/pull/13
2022-10-19 12:35:11 +03:00
dependabot[bot] 4493da0f04 build(deps): Bump dvc-task from 0.1.3 to 0.1.4
Bumps [dvc-task](https://github.com/iterative/dvc-task) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/iterative/dvc-task/releases)
- [Commits](https://github.com/iterative/dvc-task/compare/0.1.3...0.1.4)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-19 15:40:56 +08:00
Peter Rowlands 7c225206ab fetch/push: use dvc-data TransferResult 2022-10-18 19:02:47 +09:00
Peter Rowlands be5a54bfb4 deps: bump dvc-data to 0.20.0 2022-10-18 19:02:47 +09:00
skshetry d9ce9b4fef testing.api_tests: remove comment from fixture (#8444) 2022-10-18 15:44:59 +05:45
dependabot[bot]andskshetry 689988592f build(deps-dev): Bump pylint from 2.15.2 to 2.15.4 (#8424)
* build(deps-dev): Bump pylint from 2.15.2 to 2.15.4

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.2 to 2.15.4.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.2...v2.15.4)

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

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

* Remove astroid

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2022-10-18 09:27:54 +05:45
dependabot[bot] 1ddfca856a build(deps): Bump dvc-data from 0.18.0 to 0.19.0 (#8442)
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.18.0...0.19.0)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 09:27:27 +05:45
dependabot[bot] e451ce7d0b build(deps-dev): Bump pytest-mock from 3.9.0 to 3.10.0 (#8402)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.9.0...v3.10.0)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 07:52:16 +05:45
pre-commit-ci[bot] de05659a93 [pre-commit.ci] pre-commit autoupdate (#8441)
updates:
- [github.com/codespell-project/codespell: v2.2.1 → v2.2.2](https://github.com/codespell-project/codespell/compare/v2.2.1...v2.2.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-18 07:47:20 +05:45
Daniele Trifirò e9244924d7 plots: allow definition of plots section as list 2022-10-17 12:06:31 +02:00
David de la Iglesia Castro 3bbf8fa7d2 config: ssh: Add passphrase and ask_passphrase options.
Bump `dvc-ssh`.

Fixes #8137
2022-10-14 12:11:31 +03:00
Peter Rowlands ed87c308ba deps: bump dvc-data to 0.18.0
- callbacks: update to dvc-data 0.18.0 behavior
2022-10-13 20:00:19 +09:00
Peter Rowlands add28f73f1 brancher: clear repo.index on each branch 2022-10-11 16:12:00 +03:00
Peter Rowlands 2bc693fb4c index: add IndexView and Index.targets_view() 2022-10-11 16:12:00 +03:00
Peter Rowlands ac57fa53fe index: cache target collection 2022-10-11 16:12:00 +03:00
Peter Rowlands 0ca0d94cb2 output: mypy fixes 2022-10-11 16:12:00 +03:00
David de la Iglesia Castro c12deaa62c exp show: Preserve full branch and tag names.
Closes #7933
2022-10-11 19:55:36 +09:00
pre-commit-ci[bot] 5606608867 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/ambv/black: 22.8.0 → 22.10.0](https://github.com/ambv/black/compare/22.8.0...22.10.0)
2022-10-11 14:43:51 +08:00
Ruslan Kuprieiev 6711b653a5 tests: add tests for dvcignore.walk 2022-10-10 21:43:44 +03:00
Ruslan Kuprieiev faef8eec2c tests: ignore: use dvcignore.find directly 2022-10-10 21:43:44 +03:00
Ruslan Kuprieiev 11766c8839 ignore: walk: support detail=True 2022-10-10 21:43:44 +03:00
Peter Rowlands 1180a2f051 deps: bump dvc-data to 0.17.1 2022-10-10 16:25:07 +09:00
Ronan Lamy f59a99d442 Skip some tests on FSs that don't support dirs (like http) 2022-10-07 19:03:57 +03:00
Ronan Lamy d5f5e14183 make get-url testable with the dvc plugins 2022-10-07 19:03:57 +03:00
Ronan Lamy 10f60bed58 make get-url consistent with ls-url
Also, move parse_external_url() to dvc.fs as it's now used both by
ls_url() and get_url().
2022-10-07 19:03:57 +03:00
Ronan Lamy 6a0d7f9c51 Extract Output.download() into a function
Also, move the method to Dependency, since it's only used there.
2022-10-07 19:03:57 +03:00
dependabot[bot] 8640beb998 build(deps): Bump dvc-render from 0.0.11 to 0.0.12
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.11 to 0.0.12.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.11...0.0.12)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

aiohttp now provides wheel for 3.11

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-22 19:31:24 +05:45
Daniele Trifirò 69c3c83833 import/import-url: ignore outs when using --no-download 2022-09-22 00:29:46 +03:00
David de la Iglesia Castro 122a68398e hydra: Fix sweeps on Defaults List.
Defaults List overrides (`defaults/foo=1,2`) were not working because of the inclusion of the `/` character.
Fixed it by updating how we construct the `merged_overrides` and added a regression test.
2022-09-21 15:48:19 +02:00
Martino Mensio 748c015638 fixed link to "get started: pipelines" docs 2022-09-21 13:45:39 +03:00
skshetry ba1ab86078 document dvcfs (#8332) 2022-09-21 11:30:15 +05:45
skshetry aaa467a96b fs.callback.RichCallback: add transient support (#8335) 2022-09-20 17:03:17 +05:45
Daniele Trifirò ed15e59115 deps: bump dvc-http to 2.27.2 2022-09-20 13:18:18 +03:00
skshetry e63cb31a66 dvcfs: remove get-file implementation, make dvcfs non-cachable (#8331)
dvcfs: remove get-file implementation
2022-09-20 15:24:44 +05:45
Peter Rowlands 4c1f172132 deps: bump dvc-data to 0.10.1 2022-09-20 18:30:35 +09:00
104 changed files with 2211 additions and 1644 deletions
-2
View File
@@ -66,8 +66,6 @@ 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: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
+3 -3
View File
@@ -6,7 +6,7 @@ repos:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 22.6.0
rev: 22.10.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
@@ -16,9 +16,9 @@ repos:
- id: codespell
args:
- --ignore-words-list
- ba,datas,fo,uptodate,cachable
- ba,datas,fo,uptodate,cachable,falsy
repo: https://github.com/codespell-project/codespell
rev: v2.1.0
rev: v2.2.2
- hooks:
- id: isort
language_version: python3
+1 -1
View File
@@ -82,7 +82,7 @@ To share and back up the *data cache*, DVC supports multiple remote storage plat
|Flowchart|
`DVC pipelines <https://dvc.org/doc/start/data-management/pipelines>`_ (computational graphs) connect code and data together.
`DVC pipelines <https://dvc.org/doc/start/data-management/data-pipelines>`_ (computational graphs) connect code and data together.
They specify all steps required to produce a model: input dependencies including code, data, commands to run; and output information to be saved.
Last but not least, `DVC Experiment Versioning <https://dvc.org/doc/start/experiments>`_ lets you prepare and run a large number of experiments.
+51 -17
View File
@@ -1,16 +1,14 @@
from contextlib import ExitStack, contextmanager
from datetime import datetime
from typing import TYPE_CHECKING, Callable, Union
from typing import TYPE_CHECKING, Callable, Optional, Union
if TYPE_CHECKING:
from argparse import Namespace
from types import FrameType
@contextmanager
def viztracer_profile(
path: Union[Callable[[], str], str],
max_stack_depth: int = -1,
):
def viztracer_profile(path: Union[Callable[[], str], str], depth: int = -1):
try:
import viztracer # pylint: disable=import-error
except ImportError:
@@ -18,7 +16,7 @@ def viztracer_profile(
yield
return
tracer = viztracer.VizTracer(max_stack_depth=max_stack_depth)
tracer = viztracer.VizTracer(max_stack_depth=depth)
tracer.start()
yield
@@ -109,6 +107,38 @@ def debug():
raise # prevent from jumping ahead
def _sigshow(_, frame: Optional["FrameType"]) -> None:
from shutil import get_terminal_size
from traceback import format_stack
from dvc.ui import ui
lines = "\u2015" * get_terminal_size().columns
ui.error_write(lines, "\n", *format_stack(frame), lines, sep="")
@contextmanager
def show_stack():
r"""Show stack trace on SIGQUIT (Ctrl-\) or SIGINFO (Ctrl-T on macOS)."""
import signal
signal.signal(signal.SIGQUIT, _sigshow)
try:
signal.signal(signal.SIGINFO, _sigshow) # only available on macOS
except AttributeError:
pass
yield
def _get_path_func(tool: str, ext: str):
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
def func(now: Optional["datetime"] = None) -> str:
return fmt.format(now=now or datetime.now())
return func
@contextmanager
def debugtools(args: "Namespace" = None, **kwargs):
kw = vars(args) if args else {}
@@ -121,19 +151,16 @@ def debugtools(args: "Namespace" = None, **kwargs):
stack.enter_context(profile(kw.get("cprofile_dump")))
if kw.get("instrument") or kw.get("instrument_open"):
stack.enter_context(instrument(kw.get("instrument_open", False)))
if kw.get("show_stack", False):
stack.enter_context(show_stack())
if kw.get("yappi"):
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
stack.enter_context(
yappi_profile(path=lambda: output.format(datetime.now()))
)
path_func = _get_path_func("callgrind", "out")
stack.enter_context(yappi_profile(path=path_func))
if kw.get("viztracer") or kw.get("viztracer_depth"):
output = "viztracer.dvc-{0:%Y%m%d}_{0:%H%M%S}.json"
stack.enter_context(
viztracer_profile(
path=lambda: output.format(datetime.now()),
max_stack_depth=kw.get("viztracer_depth") or -1,
)
)
path_func = _get_path_func("viztracer", "json")
depth = kw.get("viztracer_depth") or -1
prof = viztracer_profile(path=path_func, depth=depth)
stack.enter_context(prof)
yield
@@ -160,3 +187,10 @@ def add_debugging_flags(parser):
parser.add_argument(
"--instrument-open", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--show-stack",
"--ss",
action="store_true",
default=False,
help=SUPPRESS,
)
+2
View File
@@ -29,6 +29,7 @@ from dvc.commands import (
init,
install,
ls,
ls_url,
machine,
metrics,
move,
@@ -75,6 +76,7 @@ COMMANDS = [
install,
root,
ls,
ls_url,
freeze,
dag,
daemon,
+1 -1
View File
@@ -110,7 +110,7 @@ def add_parser(subparsers, parent_parser):
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
".dvc files and stage names."
),
).complete = completion.DVC_FILE
checkout_parser.set_defaults(func=CmdCheckout)
+4 -2
View File
@@ -70,7 +70,9 @@ def add_parser(subparsers, parent_parser):
commit_parser.add_argument(
"targets",
nargs="*",
help="stages or .dvc files to commit. Optional. "
"(Finds all DVC files in the workspace by default.)",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files and stage names."
),
).complete = completion.DVCFILES_AND_STAGE
commit_parser.set_defaults(func=CmdCommit)
+1 -1
View File
@@ -204,7 +204,7 @@ def add_parser(subparsers, parent_parser):
dag_parser.add_argument(
"target",
nargs="?",
help="Stage or output to show pipeline for (optional). "
help="Stage name or output to show pipeline for. "
"Finds all stages in the workspace by default.",
)
dag_parser.set_defaults(func=CmdDAG)
+2 -2
View File
@@ -120,7 +120,7 @@ def shared_parent_parser():
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
".dvc files and stage names."
),
).complete = completion.DVC_FILE
@@ -195,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
"--glob",
action="store_true",
default=False,
help="Pull cache for targets matching shell-style wildcards.",
help=argparse.SUPPRESS,
)
pull_parser.set_defaults(func=CmdDataPull)
+10 -6
View File
@@ -16,13 +16,17 @@ class CmdExperimentsList(CmdBase):
num=self.args.num,
git_remote=self.args.git_remote,
)
tags = self.repo.scm.describe(exps)
remained = {baseline for baseline, tag in tags.items() if tag is None}
base = "refs/heads/"
ref_heads = self.repo.scm.describe(remained, base=base)
for baseline in exps:
tag = self.repo.scm.describe(baseline)
if not tag:
branch = self.repo.scm.describe(baseline, base="refs/heads")
if branch:
tag = branch.split("/")[-1]
name = tag if tag else baseline[:7]
name = (
tags[baseline]
or ref_heads[baseline][len(base) :]
or baseline[:7]
)
if not name_only:
print(f"{name}:")
for exp_name in exps[baseline]:
+16 -5
View File
@@ -95,11 +95,8 @@ def _collect_rows(
exp = results.get("data", {})
if exp.get("running"):
state = "Running"
elif exp.get("queued"):
state = "Queued"
else:
state = exp.get("status")
if state == "Success":
state = fill_value
is_baseline = rev == "baseline"
@@ -476,6 +473,8 @@ class CmdExperimentsShow(CmdBase):
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
hide_queued=self.args.hide_queued,
hide_failed=self.args.hide_failed,
revs=self.args.rev,
num=self.args.num,
sha_only=self.args.sha,
@@ -594,6 +593,18 @@ def add_parser(experiments_subparsers, parent_parser):
default=False,
help="Always show git commit SHAs instead of branch/tag names.",
)
experiments_show_parser.add_argument(
"--hide-failed",
action="store_true",
default=False,
help="Hide failed experiments in the table.",
)
experiments_show_parser.add_argument(
"--hide-queued",
action="store_true",
default=False,
help="Hide queued experiments in the table.",
)
experiments_show_parser.add_argument(
"--json",
"--show-json",
+1 -1
View File
@@ -74,7 +74,7 @@ class CmdMergeDriver(CmdHookBase):
our = Dvcfile(dvc, self.args.our, verify=False)
their = Dvcfile(dvc, self.args.their, verify=False)
our.merge(ancestor, their)
our.merge(ancestor, their, allowed=["add", "remove", "change"])
return 0
finally:
+10
View File
@@ -25,6 +25,7 @@ class CmdImportUrl(CmdBase):
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
version_aware=self.args.version_aware,
)
except DvcException:
logger.exception(
@@ -108,5 +109,14 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
import_parser.add_argument(
"--version-aware",
action="store_true",
default=False,
help=(
"Import using cloud versioning. Implied if the URL contains a "
"version ID."
),
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImportUrl)
+43
View File
@@ -0,0 +1,43 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.ui import ui
from .ls import _prettify
logger = logging.getLogger(__name__)
class CmdListUrl(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
if entries:
entries = _prettify(entries, with_color=True)
ui.write("\n".join(entries))
return 0
def add_parser(subparsers, parent_parser):
LS_HELP = "List directory contents from URL."
lsurl_parser = subparsers.add_parser(
"list-url",
aliases=["ls-url"],
parents=[parent_parser],
description=append_doc_link(LS_HELP, "list-url"),
help=LS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lsurl_parser.add_argument(
"url", help="See `dvc import-url -h` for full list of supported URLs."
)
lsurl_parser.add_argument(
"-R",
"--recursive",
action="store_true",
help="Recursively list files.",
)
lsurl_parser.set_defaults(func=CmdListUrl)
+2 -15
View File
@@ -25,7 +25,7 @@ def _show_json(renderers, split=False):
def _adjust_vega_renderers(renderers):
from dvc.render import VERSION_FIELD
from dvc.render import REVISION_FIELD, VERSION_FIELD
from dvc_render import VegaRenderer
for r in renderers:
@@ -42,6 +42,7 @@ def _adjust_vega_renderers(renderers):
dp["rev"] = "::".join(vi.values())
else:
for dp in r.datapoints:
dp[REVISION_FIELD] = dp[VERSION_FIELD]["revision"]
dp.pop(VERSION_FIELD, {})
@@ -66,18 +67,6 @@ def _data_versions_count(renderer):
return len(set(x))
def _filter_unhandled_renderers(renderers):
# filtering out renderers currently unhandled by vscode extension
from dvc_render import VegaRenderer
def _is_json_viable(r):
return not (
isinstance(r, VegaRenderer) and _data_versions_count(r) > 1
)
return list(filter(_is_json_viable, renderers))
class CmdPlots(CmdBase):
def _func(self, *args, **kwargs):
raise NotImplementedError
@@ -156,12 +145,10 @@ class CmdPlots(CmdBase):
templates_dir=self.repo.plots.templates_dir,
)
if self.args.json:
renderers = _filter_unhandled_renderers(renderers)
_show_json(renderers, self.args.split)
return 0
_adjust_vega_renderers(renderers)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
+10 -1
View File
@@ -55,7 +55,16 @@ def add_arguments(repro_parser):
repro_parser.add_argument(
"targets",
nargs="*",
help="Stages to reproduce. 'dvc.yaml' by default.",
help=(
"""\
Stages to reproduce. 'dvc.yaml' by default.
The targets can be path to a dvc.yaml file or `.dvc` file,
or a stage name from dvc.yaml file from
current working directory. To run a stage from dvc.yaml
from other directories, the target must be a path followed by colon `:`
and then the stage name name.
"""
),
).complete = completion.DVCFILES_AND_STAGE
repro_parser.add_argument(
"-f",
+1 -1
View File
@@ -60,7 +60,7 @@ def add_parser(subparsers, parent_parser):
"--no-download",
action="store_true",
default=False,
help="Update .dvc file hash value(s)"
help="Update .dvc file git revision/hash value(s)"
" but do not download the file(s).",
)
update_parser.add_argument(
+4 -1
View File
@@ -176,7 +176,10 @@ class Config(dict):
if fs.exists(filename):
with fs.open(filename) as fobj:
conf_obj = ConfigObj(fobj)
try:
conf_obj = ConfigObj(fobj)
except UnicodeDecodeError as exc:
raise ConfigError(str(exc)) from exc
else:
conf_obj = ConfigObj()
return _parse_named(_lower_keys(conf_obj.dict()))
+2
View File
@@ -180,6 +180,8 @@ SCHEMA = {
"user": str,
"password": str,
"ask_password": Bool,
"passphrase": str,
"ask_passphrase": Bool,
"keyfile": str,
"timeout": Coerce(int),
"gss_auth": Bool,
+6 -9
View File
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.status import CompareStatusResult
from dvc_data.hashfile.transfer import TransferResult
logger = logging.getLogger(__name__)
@@ -106,14 +107,10 @@ class DataCloud:
dest_odb: "HashFileDB",
objs: Iterable["HashInfo"],
**kwargs,
):
from dvc.exceptions import FileTransferError
from dvc_data.hashfile.transfer import TransferError, transfer
) -> "TransferResult":
from dvc_data.hashfile.transfer import transfer
try:
return transfer(src_odb, dest_odb, objs, **kwargs)
except TransferError as exc:
raise FileTransferError(exc.fails) from exc
return transfer(src_odb, dest_odb, objs, **kwargs)
def push(
self,
@@ -121,7 +118,7 @@ class DataCloud:
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["HashFileDB"] = None,
):
) -> "TransferResult":
"""Push data items in a cloud-agnostic way.
Args:
@@ -148,7 +145,7 @@ class DataCloud:
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["HashFileDB"] = None,
):
) -> "TransferResult":
"""Pull data items in a cloud-agnostic way.
Args:
+32 -2
View File
@@ -1,6 +1,7 @@
from typing import Type
from typing import Dict, Type
from dvc.exceptions import DvcException
from dvc.fs import download as fs_download
from dvc.output import Output
@@ -30,5 +31,34 @@ class Dependency(Output):
) # type: Type[DvcException]
IsStageFileError = DependencyIsStageFileError # type: Type[DvcException]
def workspace_status(self) -> Dict[str, str]:
if self.fs.version_aware:
old_fs_path = self.fs_path
try:
self.fs_path = self.fs.path.version_path(self.fs_path, None)
if self.changed_meta():
return {str(self): "update available"}
finally:
self.fs_path = old_fs_path
return super().workspace_status()
def update(self, rev=None):
pass
if self.fs.version_aware:
self.fs_path = self.fs.path.version_path(self.fs_path, rev)
self.meta = self.get_meta()
self.def_path = self.fs.path.version_path(
self.def_path, self.meta.version_id
)
self.fs_path = self.fs.path.version_path(
self.fs_path, self.meta.version_id
)
def download(self, to, jobs=None):
fs_download(self.fs, self.fs_path, to, jobs=jobs)
def save(self):
super().save()
if self.fs.version_aware:
self.fs_path = self.fs.path.version_path(
self.fs_path, self.meta.version_id
)
+19 -5
View File
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from voluptuous import Required
from dvc.prompt import confirm
from dvc_data.hashfile.meta import Meta
from .base import Dependency
@@ -32,6 +33,7 @@ class RepoDependency(Dependency):
def __init__(self, def_repo, stage, *args, **kwargs):
self.def_repo = def_repo
self._objs: Dict[str, "HashFile"] = {}
self._meta: Dict[str, Meta] = {}
super().__init__(stage, *args, **kwargs)
def _parse_path(self, fs, fs_path):
@@ -94,12 +96,12 @@ class RepoDependency(Dependency):
def get_used_objs(
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _ = self._get_used_and_obj(**kwargs)
used, _, _ = self._get_used_and_obj(**kwargs)
return used
def _get_used_and_obj(
self, obj_only=False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], Meta, "HashFile"]:
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.utils import as_posix
@@ -131,7 +133,7 @@ class RepoDependency(Dependency):
pass
try:
object_store, _, obj = build(
object_store, meta, obj = build(
local_odb,
as_posix(self.def_path),
repo.dvcfs,
@@ -145,10 +147,12 @@ class RepoDependency(Dependency):
object_store.read_only = True
self._objs[rev] = obj
self._meta[rev] = meta
used_obj_ids[object_store].add(obj.hash_info)
if isinstance(obj, Tree):
used_obj_ids[object_store].update(oid for _, _, oid in obj)
return used_obj_ids, obj
return used_obj_ids, meta, obj
def _check_circular_import(self, odb, obj_ids):
from dvc.exceptions import CircularImportError
@@ -190,11 +194,21 @@ class RepoDependency(Dependency):
rev = self._get_rev(locked=locked)
if rev in self._objs:
return self._objs[rev]
_, obj = self._get_used_and_obj(
_, _, obj = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return obj
def get_meta(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True)
rev = self._get_rev(locked=locked)
if rev in self._meta:
return self._meta[rev]
_, meta, _ = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return meta
def _make_repo(self, locked=True, **kwargs):
from dvc.external_repo import external_repo
+9 -9
View File
@@ -172,7 +172,7 @@ class FileMixin:
def dump(self, stage, **kwargs):
raise NotImplementedError
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
@@ -204,12 +204,12 @@ class SingleStageFile(FileMixin):
def remove_stage(self, stage): # pylint: disable=unused-argument
self.remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
assert isinstance(ancestor, SingleStageFile)
assert isinstance(other, SingleStageFile)
stage = self.stage
stage.merge(ancestor.stage, other.stage)
stage.merge(ancestor.stage, other.stage, allowed=allowed)
self.dump(stage)
@@ -237,10 +237,10 @@ class PipelineFile(FileMixin):
self._dump_pipeline_file(stage)
if update_lock:
self._dump_lockfile(stage)
self._dump_lockfile(stage, **kwargs)
def _dump_lockfile(self, stage):
self._lockfile.dump(stage)
def _dump_lockfile(self, stage, **kwargs):
self._lockfile.dump(stage, **kwargs)
@staticmethod
def _check_if_parametrized(stage, action: str = "dump") -> None:
@@ -310,7 +310,7 @@ class PipelineFile(FileMixin):
else:
super().remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
@@ -366,7 +366,7 @@ class Lockfile(FileMixin):
return {SCHEMA_KWD: version}
def dump(self, stage, **kwargs):
stage_data = serialize.to_lockfile(stage)
stage_data = serialize.to_lockfile(stage, **kwargs)
with modify_yaml(self.path, fs=self.repo.fs) as data:
version = LOCKFILE_VERSION.from_dict(data)
@@ -411,7 +411,7 @@ class Lockfile(FileMixin):
else:
self.remove()
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
+5
View File
@@ -278,6 +278,11 @@ class PathMissingError(DvcException):
self.dvc_only = dvc_only
class URLMissingError(DvcException):
def __init__(self, url):
super().__init__(f"The path '{url}' does not exist")
class RemoteCacheRequiredError(DvcException):
def __init__(self, scheme, fs_path):
+30 -9
View File
@@ -2,6 +2,9 @@ from urllib.parse import urlparse
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
from dvc.config import ConfigError as RepoConfigError
from dvc.config_schema import SCHEMA, Invalid
# pylint: disable=unused-import
from dvc_objects.fs import utils # noqa: F401
from dvc_objects.fs import ( # noqa: F401
@@ -23,6 +26,7 @@ from dvc_objects.fs.errors import ( # noqa: F401
)
from dvc_objects.fs.path import Path # noqa: F401
from .callbacks import Callback
from .data import DataFileSystem # noqa: F401
from .dvc import DVCFileSystem # noqa: F401
from .git import GitFileSystem # noqa: F401
@@ -44,7 +48,23 @@ known_implementations.update(
# pylint: enable=unused-import
def get_fs_config(repo, config, **kwargs):
def download(fs, fs_path, to, jobs=None):
with Callback.as_tqdm_callback(
desc=f"Downloading {fs.path.name(fs_path)}",
unit="files",
) as cb:
fs.get(fs_path, to.fs_path, batch_size=jobs, callback=cb)
def parse_external_url(url, config=None):
remote_config = dict(config) if config else {}
remote_config["url"] = url
fs_cls, fs_config, fs_path = get_cloud_fs(None, **remote_config)
fs = fs_cls(**fs_config)
return fs, fs_path
def get_fs_config(config, **kwargs):
name = kwargs.get("name")
if name:
try:
@@ -55,10 +75,10 @@ def get_fs_config(repo, config, **kwargs):
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
else:
remote_conf = kwargs
return _resolve_remote_refs(repo, config, remote_conf)
return _resolve_remote_refs(config, remote_conf)
def _resolve_remote_refs(repo, config, remote_conf):
def _resolve_remote_refs(config, remote_conf):
# Support for cross referenced remotes.
# This will merge the settings, shadowing base ref with remote_conf.
# For example, having:
@@ -84,21 +104,22 @@ def _resolve_remote_refs(repo, config, remote_conf):
if parsed.scheme != "remote":
return remote_conf
base = get_fs_config(repo, config, name=parsed.netloc)
cls, _, _ = get_cloud_fs(repo, **base)
base = get_fs_config(config, name=parsed.netloc)
cls, _, _ = _get_cloud_fs(config, **base)
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
url = cls.sep.join((base["url"], relpath))
return {**base, **remote_conf, "url": url}
def get_cloud_fs(repo, **kwargs):
from dvc.config import ConfigError as RepoConfigError
from dvc.config_schema import SCHEMA, Invalid
repo_config = repo.config if repo else {}
return _get_cloud_fs(repo_config, **kwargs)
def _get_cloud_fs(repo_config, **kwargs):
core_config = repo_config.get("core", {})
remote_conf = get_fs_config(repo, repo_config, **kwargs)
remote_conf = get_fs_config(repo_config, **kwargs)
try:
remote_conf = SCHEMA["remote"][str](remote_conf)
except Invalid as exc:
+17 -5
View File
@@ -1,6 +1,6 @@
# pylint: disable=unused-import
from contextlib import ExitStack
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
from funcy import cached_property
@@ -24,6 +24,7 @@ class RichCallback(Callback):
bytes: bool = False, # pylint: disable=redefined-builtin
unit: str = None,
disable: bool = False,
transient: bool = True,
) -> None:
self._progress = progress
self.disable = disable
@@ -35,6 +36,7 @@ class RichCallback(Callback):
"visible": False,
"progress_type": None if bytes else "summary",
}
self._transient = transient
self._stack = ExitStack()
super().__init__(size=size, value=value)
@@ -47,7 +49,7 @@ class RichCallback(Callback):
return self._progress
progress = RichTransferProgress(
transient=True,
transient=self._transient,
disable=self.disable,
console=ui.error_console,
)
@@ -61,7 +63,8 @@ class RichCallback(Callback):
return self
def close(self):
self.progress.clear_task(self.task)
if self._transient:
self.progress.clear_task(self.task)
self._stack.close()
def call(self, hook_name=None, **kwargs):
@@ -72,8 +75,17 @@ class RichCallback(Callback):
visible=not self.disable,
)
def branch(self, path_1, path_2, kwargs, child: Optional[Callback] = None):
def branch(
self,
path_1: Union[str, BinaryIO],
path_2: str,
kwargs: Dict[str, Any],
child: "Callback" = None,
):
child = child or RichCallback(
progress=self.progress, desc=path_1, bytes=True
progress=self.progress,
desc=path_1 if isinstance(path_1, str) else path_2,
bytes=True,
transient=self._transient,
)
return super().branch(path_1, path_2, kwargs, child=child)
+38 -33
View File
@@ -10,9 +10,7 @@ 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
from .data import DataFileSystem
@@ -66,15 +64,7 @@ def _get_dvc_path(dvc_fs, subkey):
class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
"""DVC + git-tracked files fs.
Args:
repo: DVC or git repo.
subrepos: traverse to subrepos (by default, it ignores subrepos)
repo_factory: A function to initialize subrepo with, default is Repo.
kwargs: Additional keyword arguments passed to the `DataFileSystem()`.
"""
cachable = False
root_marker = "/"
def __init__(
@@ -86,6 +76,41 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
repo_factory: Optional[RepoFactory] = None,
**repo_kwargs: Any,
) -> None:
"""DVC + git-tracked files fs.
Args:
path (str, optional): URL or path to a DVC/Git repository.
Defaults to a DVC repository in the current working directory.
Both HTTP and SSH protocols are supported for remote Git repos
(e.g. [user@]server:project.git).
rev (str, optional): Any Git revision such as a branch or tag name,
a commit hash or a dvc experiment name.
Defaults to the default branch in case of remote repositories.
In case of a local repository, if rev is unspecified, it will
default to the working directory.
If the repo is not a Git repo, this option is ignored.
repo (:obj:`Repo`, optional): `Repo` instance.
subrepos (bool): traverse to subrepos.
By default, it ignores subrepos.
repo_factory (callable): A function to initialize subrepo with.
The default is `Repo`.
Examples:
- Opening a filesystem from repo in current working directory
>>> fs = DVCFileSystem()
- Opening a filesystem from local repository
>>> fs = DVCFileSystem("path/to/local/repository")
- Opening a remote repository
>>> fs = DVCFileSystem(
... "https://github.com/iterative/example-get-started",
... rev="main",
... )
"""
from pygtrie import Trie
super().__init__()
@@ -238,7 +263,8 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.open(dvc_path, mode=mode)
def isdvc(self, path, **kwargs):
def isdvc(self, path, **kwargs) -> bool:
"""Is this entry dvc-tracked?"""
key = self._get_key_from_relative(path)
_, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_path = _get_dvc_path(dvc_fs, subkey)
@@ -295,27 +321,6 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
return infos
def get_file( # pylint: disable=arguments-differ
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
):
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
fs = self.repo.fs
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):
key = self._get_key_from_relative(path)
ignore_subrepos = kwargs.get("ignore_subrepos", True)
+16 -3
View File
@@ -300,12 +300,25 @@ class DvcIgnoreFilter:
return [fs_dict[name] for name in chain(dirs, nondirs)]
def walk(self, fs: FileSystem, path: AnyFSPath, **kwargs):
detail = kwargs.get("detail", False)
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
if fs.protocol == Schemes.LOCAL:
for root, dirs, files in fs.walk(path, **kwargs):
dirs[:], files[:] = self(
root, dirs, files, ignore_subrepos=ignore_subrepos
)
if detail:
all_dnames = set(dirs.keys())
all_fnames = set(files.keys())
dnames, fnames = self(
root,
all_dnames,
all_fnames,
ignore_subrepos=ignore_subrepos,
)
list(map(dirs.pop, all_dnames - set(dnames)))
list(map(files.pop, all_fnames - set(fnames)))
else:
dirs[:], files[:] = self(
root, dirs, files, ignore_subrepos=ignore_subrepos
)
yield root, dirs, files
else:
yield from fs.walk(path, **kwargs)
+1 -1
View File
@@ -76,7 +76,7 @@ def _get_caches(cache):
def _get_remotes(config):
schemes = (
get_fs_cls(get_fs_config(None, config, name=remote)).protocol
get_fs_cls(get_fs_config(config, name=remote)).protocol
for remote in config["remote"]
)
+5 -2
View File
@@ -213,8 +213,6 @@ def set_loggers_level(level: int = logging.INFO) -> None:
def setup(level: int = logging.INFO) -> None:
colorama.init()
if level >= logging.DEBUG:
# Unclosed session errors for asyncio/aiohttp are only available
# on the tracing mode for extensive debug purposes. They are really
@@ -297,3 +295,8 @@ def setup(level: int = logging.INFO) -> None:
"disable_existing_loggers": False,
}
)
# NOTE: colorama init needs to go after logging config to avoid potential
# conflicts with libs that modify logging (like tensorflow/abseil)
# https://github.com/iterative/dvc/issues/8387
colorama.init()
+35 -21
View File
@@ -26,10 +26,12 @@ from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.istextfile import istextfile
from dvc_data.hashfile.meta import Meta
from dvc_data.hashfile.transfer import transfer as otransfer
from dvc_data.index import DataIndexEntry
from dvc_objects.errors import ObjectFormatError
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
from .fs.callbacks import DEFAULT_CALLBACK
from .utils import relpath
from .utils.fs import path_isin
@@ -38,6 +40,8 @@ if TYPE_CHECKING:
from dvc_data.index import DataIndexKey
from dvc_objects.db import ObjectDB
from .fs.callbacks import Callback
logger = logging.getLogger(__name__)
@@ -511,6 +515,23 @@ class Output:
key = self.fs.path.parts(no_drive)[1:]
return workspace, key
def get_entry(self) -> "DataIndexEntry":
from dvc.config import NoRemoteError
try:
remote = self.repo.cloud.get_remote_odb(self.remote)
except NoRemoteError:
remote = None
return DataIndexEntry(
meta=self.meta,
obj=self.obj,
hash_info=self.hash_info,
odb=self.odb,
cache=self.odb,
remote=remote,
)
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -530,7 +551,7 @@ class Output:
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 self.meta.version_id != self.get_meta().version_id
return False
def workspace_status(self):
@@ -765,11 +786,8 @@ 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.hash_info.isdir and (
kwargs.get("with_files") or self.files is not None
):
if self.obj:
obj = self.obj
@@ -800,15 +818,6 @@ class Output:
f"binary file '{self.fs_path}' cannot be used as metrics."
)
def download(self, to, jobs=None):
from dvc.fs.callbacks import Callback
with Callback.as_tqdm_callback(
desc=f"Downloading {self.fs.path.name(self.fs_path)}",
unit="files",
) as cb:
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
def get_obj(
self, filter_info: Optional[str] = None, **kwargs
) -> Optional["HashFile"]:
@@ -828,6 +837,7 @@ class Output:
else:
return None
assert obj
fs_path = self.fs.path
if filter_info and filter_info != self.fs_path:
prefix = fs_path.relparts(filter_info, self.fs_path)
@@ -838,7 +848,7 @@ class Output:
def checkout(
self,
force=False,
progress_callback=None,
progress_callback: "Callback" = DEFAULT_CALLBACK,
relink=False,
filter_info=None,
allow_missing=False,
@@ -846,9 +856,9 @@ class Output:
**kwargs,
):
if not self.use_cache:
if progress_callback:
progress_callback(
self.fs_path, self.get_files_number(filter_info)
if progress_callback != DEFAULT_CALLBACK:
progress_callback.relative_update(
self.get_files_number(filter_info)
)
return None
@@ -1127,7 +1137,7 @@ class Output:
"unable to auto-merge outputs that are not directories"
)
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
from dvc_data.hashfile.tree import MergeError as TreeMergeError
from dvc_data.hashfile.tree import du, merge
@@ -1144,7 +1154,11 @@ class Output:
try:
merged = merge(
self.odb, ancestor_info, self.hash_info, other.hash_info
self.odb,
ancestor_info,
self.hash_info,
other.hash_info,
allowed=allowed,
)
except TreeMergeError as exc:
raise MergeError(str(exc)) from exc
+8 -2
View File
@@ -2,7 +2,13 @@ import json
from collections import defaultdict
from typing import Dict, List, Union
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY
from dvc.render import (
REVISION_FIELD,
REVISIONS_KEY,
SRC_FIELD,
TYPE_KEY,
VERSION_FIELD,
)
from dvc.render.converter.image import ImageConverter
from dvc.render.converter.vega import VegaConverter
@@ -23,7 +29,7 @@ def _get_converter(
def _group_by_rev(datapoints):
grouped = defaultdict(list)
for datapoint in datapoints:
rev = datapoint.pop(REVISION_FIELD)
rev = datapoint.get(VERSION_FIELD, {}).get("revision")
grouped[rev].append(datapoint)
return dict(grouped)
+20 -14
View File
@@ -127,13 +127,20 @@ class VegaConverter(Converter):
),
)
)
self.steps.append(
(
"infer_y",
partial(
self._infer_y,
),
)
)
self.steps.append(
(
"generate_y",
partial(
self._generate_y_values,
y_values=self.plot_properties.get("y", None),
),
)
)
@@ -156,7 +163,7 @@ class VegaConverter(Converter):
} - {None}
self.inferred_properties["fields"] = fields
def _infer_y(self, datapoints: List[Dict]):
def _infer_y(self, datapoints: List[Dict], **kwargs):
if "y" not in self.plot_properties:
data_fields = list(first(datapoints))
skip = (
@@ -178,6 +185,7 @@ class VegaConverter(Converter):
)
else:
self.inferred_properties["y"] = inferred_y
return datapoints
def convert(
self,
@@ -197,7 +205,6 @@ class VegaConverter(Converter):
processed = deepcopy(data)
for step_name, step in self.steps:
if step_name not in skip:
processed = step( # type: ignore
processed,
@@ -205,25 +212,28 @@ class VegaConverter(Converter):
filename=filename,
)
self._infer_y(processed) # type: ignore
return processed, {**self.plot_properties, **self.inferred_properties}
def _generate_y_values( # noqa: C901
self,
datapoints: List[Dict],
y_values: Optional[Union[str, List, Dict]],
revision: str,
filename: str,
**kwargs,
) -> List[Dict]:
y_values = self.plot_properties.get(
"y", None
) or self.inferred_properties.get("y", None)
assert y_values is not None
result = []
properties_update = {}
def _add_version_info(datapoint, version_info):
tmp = datapoint.copy()
tmp[VERSION_FIELD] = version_info
tmp[REVISION_FIELD] = version_info["revision"]
return tmp
def _version_info(revision, filename=None, field=None):
@@ -250,12 +260,6 @@ class VegaConverter(Converter):
return tmp
if not y_values:
for dp in datapoints:
result.append(
_add_version_info(dp, _version_info(revision, filename))
)
if isinstance(y_values, str):
for datapoint in datapoints:
result.append(
@@ -290,13 +294,15 @@ class VegaConverter(Converter):
):
# if we use the same field from all files,
# we dont have to generate it
y_field = all_fields.pop()
for datapoint in datapoints:
result.append(
_add_version_info(
datapoint, _version_info(revision, filename)
datapoint,
_version_info(revision, filename, y_field),
)
)
properties_update.update({"y": all_fields.pop()})
properties_update.update({"y": y_field})
else:
for def_filename, val in y_values.items():
if isinstance(val, str):
+3 -40
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, List, Optional
from typing import TYPE_CHECKING, Callable, Optional
from funcy import cached_property
@@ -18,7 +18,6 @@ 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__)
@@ -68,6 +67,7 @@ class Repo:
from dvc.repo.imp_url import imp_url # type: ignore[misc]
from dvc.repo.install import install # type: ignore[misc]
from dvc.repo.ls import ls as _ls # type: ignore[misc]
from dvc.repo.ls_url import ls_url as _ls_url # type: ignore[misc]
from dvc.repo.move import move # type: ignore[misc]
from dvc.repo.pull import pull # type: ignore[misc]
from dvc.repo.push import push # type: ignore[misc]
@@ -80,6 +80,7 @@ class Repo:
from .data import status as data_status # type: ignore[misc]
ls = staticmethod(_ls)
ls_url = staticmethod(_ls_url)
get = staticmethod(_get)
get_url = staticmethod(_get_url)
@@ -445,44 +446,6 @@ 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
+1
View File
@@ -77,6 +77,7 @@ def brancher( # noqa: E302
from dvc.fs import GitFileSystem
for sha, names in found_revs.items():
self.__dict__.pop("index", None)
self.fs = GitFileSystem(scm=scm, rev=sha)
self.root_dir = self.fs.path.join("/", *repo_root_parts)
+8 -5
View File
@@ -7,7 +7,6 @@ from dvc.exceptions import (
CheckoutErrorSuggestGit,
NoOutputOrStageError,
)
from dvc.progress import Tqdm
from dvc.utils import relpath
from . import locked
@@ -80,6 +79,7 @@ def checkout(
allow_missing=False,
**kwargs,
):
from dvc.fs.callbacks import Callback
stats = {"added": [], "deleted": [], "modified": [], "failed": []}
if not targets:
@@ -91,13 +91,16 @@ def checkout(
pairs = _collect_pairs(self, targets, with_deps, recursive)
total = get_all_files_numbers(pairs)
with Tqdm(
total=total, unit="file", desc="Checkout", disable=total == 0
) as pbar:
with Callback.as_tqdm_callback(
unit="file",
desc="Checkout",
disable=total == 0,
) as cb:
cb.set_size(total)
for stage, filter_info in pairs:
result = stage.checkout(
force=force,
progress_callback=pbar.update_msg,
progress_callback=cb,
relink=relink,
filter_info=filter_info,
allow_missing=allow_missing,
+25 -14
View File
@@ -16,7 +16,7 @@ from .exceptions import (
InvalidExpRefError,
MultipleBranchError,
)
from .executor.base import BaseExecutor, ExecutorInfo
from .executor.base import BaseExecutor, ExecutorInfo, TaskStatus
from .queue.base import BaseStashQueue, QueueEntry
from .queue.celery import LocalCeleryQueue
from .queue.tempdir import TempDirQueue
@@ -211,8 +211,9 @@ class Experiments:
def _log_reproduced(self, revs: Iterable[str], tmp_dir: bool = False):
names = []
rev_names = self.get_exact_name(revs)
for rev in revs:
name = self.get_exact_name(rev)
name = rev_names[rev]
names.append(name if name else rev[:7])
ui.write("\nRan experiment(s): {}".format(", ".join(names)))
if tmp_dir:
@@ -411,23 +412,31 @@ class Experiments:
raise MultipleBranchError(rev, ref_infos)
return str(ref_infos[0])
def get_exact_name(self, rev: str):
def get_exact_name(self, revs: Iterable[str]) -> Dict[str, Optional[str]]:
"""Returns preferred name for the specified revision.
Prefers tags, branches (heads), experiments in that orer.
"""
result: Dict[str, Optional[str]] = {}
exclude = f"{EXEC_NAMESPACE}/*"
ref = self.scm.describe(rev, base=EXPS_NAMESPACE, exclude=exclude)
if ref:
try:
name = ExpRefInfo.from_ref(ref).name
if name:
return name
except InvalidExpRefError:
pass
if rev in self.stash_revs:
return self.stash_revs[rev].name
return None
ref_dict = self.scm.describe(
revs, base=EXPS_NAMESPACE, exclude=exclude
)
for rev in revs:
name: Optional[str] = None
ref = ref_dict[rev]
if ref:
try:
name = ExpRefInfo.from_ref(ref).name
except InvalidExpRefError:
pass
if not name:
if rev in self.stash_revs:
name = self.stash_revs[rev].name
elif rev in self.celery_queue.failed_stash.stash_revs:
name = self.celery_queue.failed_stash.stash_revs[rev].name
result[rev] = name
return result
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Any]:
"""Return info for running experiments."""
@@ -463,6 +472,8 @@ class Experiments:
if rev == "workspace":
# If we are appending to a checkpoint branch in a workspace
# run, show the latest checkpoint as running.
if info.status > TaskStatus.RUNNING:
return result
last_rev = self.scm.get_ref(EXEC_BRANCH)
if last_rev:
result[last_rev] = info.asdict()
+78 -18
View File
@@ -4,6 +4,7 @@ import pickle
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from enum import IntEnum
from functools import partial
from typing import (
TYPE_CHECKING,
@@ -35,6 +36,8 @@ from ..refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
EXPS_STASH,
@@ -47,10 +50,9 @@ if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
from dvc.repo.experiments.stash import ExpStashEntry
from dvc.stage import PipelineStage
from ..base import ExpStashEntry
logger = logging.getLogger(__name__)
@@ -64,6 +66,16 @@ class ExecutorResult(NamedTuple):
force: bool
class TaskStatus(IntEnum):
PENDING = 0
PREPARING = 1
RUNNING = 2
SUCCESS = 3
FAILED = 4
CANCELED = 5
FINISHED = 6
@dataclass
class ExecutorInfo:
git_url: str
@@ -73,13 +85,16 @@ class ExecutorInfo:
dvc_dir: str
name: Optional[str] = None
wdir: Optional[str] = None
collected: bool = False
result_hash: Optional[str] = None
result_ref: Optional[str] = None
result_force: bool = False
status: TaskStatus = TaskStatus.PENDING
@classmethod
def from_dict(cls, d):
if "collected" in d:
if d.pop("collected"):
d["status"] = TaskStatus.FINISHED
return cls(**d)
def asdict(self):
@@ -136,6 +151,7 @@ class BaseExecutor(ABC):
root_dir: str,
dvc_dir: str,
baseline_rev: str,
status: TaskStatus,
wdir: Optional[str] = None,
name: Optional[str] = None,
location: Optional[str] = None,
@@ -149,9 +165,17 @@ class BaseExecutor(ABC):
self.baseline_rev = baseline_rev
self.location: str = location or self.DEFAULT_LOCATION
self.result = result
self.status = status
@abstractmethod
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
"""Init git repo and populate it using exp refs from the specified
SCM instance.
"""
@@ -191,6 +215,7 @@ class BaseExecutor(ABC):
dvc_dir=self.dvc_dir,
name=self.name,
wdir=self.wdir,
status=self.status,
**result_dict,
)
@@ -212,6 +237,7 @@ class BaseExecutor(ABC):
root_dir=info.root_dir,
dvc_dir=info.dvc_dir,
baseline_rev=info.baseline_rev,
status=info.status,
name=info.name,
wdir=info.wdir,
result=result,
@@ -222,7 +248,6 @@ class BaseExecutor(ABC):
def from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
) -> _T:
@@ -232,7 +257,6 @@ class BaseExecutor(ABC):
def _from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
root_dir: str,
**kwargs,
@@ -241,12 +265,11 @@ class BaseExecutor(ABC):
root_dir=root_dir,
dvc_dir=relpath(repo.dvc_dir, repo.scm.root_dir),
baseline_rev=entry.baseline_rev,
status=TaskStatus.PREPARING,
name=entry.name,
wdir=relpath(os.getcwd(), repo.scm.root_dir),
**kwargs,
)
executor.init_git(repo.scm, branch=entry.branch)
executor.init_cache(repo, stash_rev)
return executor
@staticmethod
@@ -259,8 +282,12 @@ class BaseExecutor(ABC):
exp_data.update(to_lockfile(stage))
return dict_sha256(exp_data)
def cleanup(self):
pass
def cleanup(self, infofile: str):
if infofile is not None:
info = ExecutorInfo.load_json(infofile)
if info.status < TaskStatus.FAILED:
info.status = TaskStatus.FINISHED
info.dump_json(infofile)
# TODO: come up with better way to stash repro arguments
@staticmethod
@@ -324,11 +351,12 @@ class BaseExecutor(ABC):
logger.debug("Replacing existing experiment '%s'", orig_ref)
return True
if on_diverged:
return on_diverged(orig_ref, has_checkpoint)
self._raise_ref_conflict(
dest_scm, orig_ref, new_rev, has_checkpoint
)
if on_diverged:
on_diverged(orig_ref, has_checkpoint)
logger.debug("Reproduced existing experiment '%s'", orig_ref)
return False
@@ -411,11 +439,9 @@ class BaseExecutor(ABC):
exp_ref: Optional["ExpRefInfo"] = None
repro_force: bool = False
if infofile is not None:
info.dump_json(infofile)
with cls._repro_dvc(
info,
infofile,
log_errors=log_errors,
**kwargs,
) as dvc:
@@ -491,9 +517,6 @@ class BaseExecutor(ABC):
info.result_ref = ref
info.result_force = repro_force
if infofile is not None:
info.dump_json(infofile)
# ideally we would return stages here like a normal repro() call, but
# stages is not currently picklable and cannot be returned across
# multiprocessing calls
@@ -550,6 +573,7 @@ class BaseExecutor(ABC):
def _repro_dvc(
cls,
info: "ExecutorInfo",
infofile: str = None,
log_errors: bool = True,
**kwargs,
):
@@ -557,6 +581,9 @@ class BaseExecutor(ABC):
from dvc.stage.monitor import CheckpointKilledError
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
info.status = TaskStatus.RUNNING
if infofile is not None:
info.dump_json(infofile)
if cls.QUIET:
dvc.scm_context.quiet = cls.QUIET
old_cwd = os.getcwd()
@@ -568,15 +595,28 @@ class BaseExecutor(ABC):
try:
logger.debug("Running repro in '%s'", os.getcwd())
yield dvc
info.status = TaskStatus.SUCCESS
if infofile is not None:
info.dump_json(infofile)
except CheckpointKilledError:
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except DvcException:
if log_errors:
logger.exception("")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except Exception:
if log_errors:
logger.exception("unexpected error")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
finally:
dvc.close()
@@ -714,3 +754,23 @@ class BaseExecutor(ABC):
disable_other_loggers()
if level is not None:
dvc_logger.setLevel(level)
@contextmanager
def set_exec_refs(
self, scm: "Git", stash_rev: str, entry: "ExpStashEntry"
):
try:
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
yield
finally:
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
if scm.get_ref(ref):
scm.remove_ref(ref)
+42 -23
View File
@@ -19,7 +19,7 @@ from ..refs import (
EXEC_MERGE,
EXEC_NAMESPACE,
)
from .base import EXEC_TMP_DIR, BaseExecutor
from .base import EXEC_TMP_DIR, BaseExecutor, TaskStatus
if TYPE_CHECKING:
from scmrepo.git import Git
@@ -46,8 +46,8 @@ class BaseLocalExecutor(BaseExecutor):
def scm(self):
return SCM(self.root_dir)
def cleanup(self):
super().cleanup()
def cleanup(self, infofile: str):
super().cleanup(infofile)
self.scm.close()
del self.scm
@@ -67,15 +67,28 @@ class TempDirExecutor(BaseLocalExecutor):
QUIET = True
DEFAULT_LOCATION = "tempdir"
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
from dulwich.repo import Repo as DulwichRepo
from ..utils import push_refspec
DulwichRepo.init(os.fspath(self.root_dir))
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec)
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
with self.set_exec_refs(scm, stash_rev, entry):
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec)
if branch:
push_refspec(scm, self.git_url, branch, branch)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
@@ -84,17 +97,12 @@ class TempDirExecutor(BaseLocalExecutor):
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
head = EXEC_BRANCH if branch else EXEC_HEAD
self.scm.checkout(head, detach=True)
merge_rev = self.scm.get_ref(EXEC_MERGE)
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
if scm.get_ref(ref):
scm.remove_ref(ref)
try:
self.scm.merge(merge_rev, squash=True, commit=False)
except _SCMError as exc:
@@ -114,8 +122,8 @@ class TempDirExecutor(BaseLocalExecutor):
"""Initialize DVC cache."""
self._config(repo.odb.repo.path)
def cleanup(self):
super().cleanup()
def cleanup(self, infofile: str):
super().cleanup(infofile)
logger.debug("Removing tmpdir '%s'", self.root_dir)
remove(self.root_dir)
@@ -123,7 +131,6 @@ class TempDirExecutor(BaseLocalExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
wdir: Optional[str] = None,
**kwargs,
@@ -132,9 +139,7 @@ class TempDirExecutor(BaseLocalExecutor):
makedirs(parent_dir, exist_ok=True)
tmp_dir = mkdtemp(dir=parent_dir)
try:
executor = cls._from_stash_entry(
repo, stash_rev, entry, tmp_dir, **kwargs
)
executor = cls._from_stash_entry(repo, entry, tmp_dir, **kwargs)
logger.debug("Init temp dir executor in '%s'", tmp_dir)
return executor
except Exception:
@@ -152,18 +157,31 @@ class WorkspaceExecutor(BaseLocalExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
):
root_dir = repo.scm.root_dir
executor = cls._from_stash_entry(
repo, stash_rev, entry, root_dir, **kwargs
executor: "WorkspaceExecutor" = cls._from_stash_entry(
repo, entry, root_dir, **kwargs
)
logger.debug("Init workspace executor in '%s'", root_dir)
return executor
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
self._detach_stack.enter_context(
self.scm.detach_head(
self.scm.get_ref(EXEC_HEAD),
@@ -184,7 +202,9 @@ class WorkspaceExecutor(BaseLocalExecutor):
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
pass
def cleanup(self):
def cleanup(self, infofile: str):
super().cleanup(infofile)
remove(os.path.dirname(infofile))
with self._detach_stack:
self.scm.remove_ref(EXEC_BASELINE)
self.scm.remove_ref(EXEC_MERGE)
@@ -193,4 +213,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
if checkpoint and checkpoint != self._orig_checkpoint:
self.scm.set_ref(EXEC_APPLY, checkpoint)
super().cleanup()
@@ -1,269 +0,0 @@
import logging
import os
from abc import ABC
from collections import defaultdict, deque
from collections.abc import Mapping
from typing import TYPE_CHECKING, Deque, Dict, Generator, Optional, Tuple, Type
from dvc.proc.manager import ProcessManager
from ...exceptions import CheckpointExistsError, ExperimentExistsError
from ...refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
from ...stash import ExpStashEntry
from ..base import EXEC_PID_DIR, BaseExecutor
from ..local import TempDirExecutor, WorkspaceExecutor
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class BaseExecutorManager(ABC, Mapping): # noqa: B024
"""Manages executors for a collection of experiments to be run."""
EXECUTOR_CLS: Type = BaseExecutor
def __init__(
self,
scm: "Git",
wdir: str,
**kwargs,
):
from dvc.utils.fs import makedirs
self.scm = scm
makedirs(wdir, exist_ok=True)
self.wdir = wdir
self.proc = ProcessManager(self.pid_dir)
self._attached: Dict[str, "BaseExecutor"] = {}
self._detached: Dict[str, "BaseExecutor"] = dict(self._load_infos())
self._queue: Deque[Tuple[str, "BaseExecutor"]] = deque()
def __getitem__(self, key: str) -> "BaseExecutor":
try:
return self._attached[key]
except KeyError:
pass
return self._detached[key]
def __iter__(self):
yield from self._attached
yield from self._detached
def __len__(self):
return len(self._attached) + len(self._detached)
@property
def pid_dir(self) -> str:
return os.path.join(self.wdir, EXEC_PID_DIR)
def enqueue(self, rev: str, executor: "BaseExecutor"):
assert rev not in self
self._queue.append((rev, executor))
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
import json
from urllib.parse import urlparse
from ..base import ExecutorInfo
from ..ssh import SSHExecutor
def make_executor(info: "ExecutorInfo"):
if info.git_url:
scheme = urlparse(info.git_url).scheme
if scheme == "file":
cls: Type = TempDirExecutor
elif scheme == "ssh":
cls = SSHExecutor
else:
raise NotImplementedError
else:
cls = WorkspaceExecutor
return cls.from_info(info)
for name in self.proc:
infofile = self.get_infofile_path(name)
try:
with open(infofile, encoding="utf-8") as fobj:
info = ExecutorInfo.from_dict(json.load(fobj))
yield name, make_executor(info)
except OSError:
continue
def get_infofile_path(self, name: str) -> str:
return os.path.join(
self.pid_dir,
name,
f"{name}{BaseExecutor.INFOFILE_EXT}",
)
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
manager = cls(scm, wdir)
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
return manager
def _enqueue_stash_entries(
self,
scm: "Git",
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
try:
for stash_rev, entry in to_run.items():
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
executor = self.EXECUTOR_CLS.from_stash_entry(
repo,
stash_rev,
entry,
**kwargs,
)
self.enqueue(stash_rev, executor)
finally:
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
scm.remove_ref(ref)
def exec_queue(
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
):
"""Run dvc repro for queued executors in parallel."""
if detach:
raise NotImplementedError
# TODO use ProcessManager.spawn() to support detached runs
return self._exec_attached(repo, jobs=jobs)
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
import signal
from concurrent.futures import (
CancelledError,
ProcessPoolExecutor,
wait,
)
from multiprocessing import Manager
from dvc.stage.monitor import CheckpointKilledError
result: Dict[str, Dict[str, str]] = defaultdict(dict)
manager = Manager()
pid_q = manager.Queue()
with ProcessPoolExecutor(max_workers=jobs) as workers:
futures = {}
while self._queue:
rev, executor = self._queue.popleft()
infofile = self.get_infofile_path(rev)
future = workers.submit(
executor.reproduce,
info=executor.info,
rev=rev,
queue=pid_q,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
)
futures[future] = (rev, executor)
self._attached[rev] = executor
try:
wait(futures)
except KeyboardInterrupt:
# forward SIGINT to any running executor processes and
# cancel any remaining futures
workers.shutdown(wait=False)
pids = {}
for future, (rev, _) in futures.items():
if future.running():
# if future has already been started by the scheduler
# we still have to wait until it tells us its PID
while rev not in pids:
rev, pid = pid_q.get()
pids[rev] = pid
os.kill(pids[rev], signal.SIGINT)
elif not future.done():
future.cancel()
for future, (rev, executor) in futures.items():
rev, executor = futures[future]
try:
exc = future.exception()
if exc is None:
exec_result = future.result()
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
elif not isinstance(exc, CheckpointKilledError):
logger.error(
"Failed to reproduce experiment '%s'", rev[:7]
)
except CancelledError:
logger.error(
"Cancelled before attempting to reproduce experiment "
"'%s'",
rev[:7],
)
finally:
self.cleanup_executor(rev, executor)
return result
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
# NOTE: GitPython Repo instances cannot be re-used
# after process has received SIGINT or SIGTERM, so we
# need this hack to re-instantiate git instances after
# checkpoint runs. See:
# https://github.com/gitpython-developers/GitPython/issues/427
# del self.repo.scm
results = {}
def on_diverged(ref: str, checkpoint: bool):
ref_info = ExpRefInfo.from_ref(ref)
if checkpoint:
raise CheckpointExistsError(ref_info.name)
raise ExperimentExistsError(ref_info.name)
for ref in executor.fetch_exps(
self.scm,
force=exec_result.force,
on_diverged=on_diverged,
):
exp_rev = self.scm.get_ref(ref)
if exp_rev:
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
if exec_result.ref_info is not None:
executor.collect_cache(repo, exec_result.ref_info)
return results
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
from dvc.utils.fs import remove
executor.cleanup()
try:
self.proc.remove(rev)
except KeyError:
pass
remove(os.path.join(self.pid_dir, rev))
@@ -1,107 +0,0 @@
import logging
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional
from ...refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_HEAD, EXEC_MERGE
from ...stash import ExpStashEntry
from ..local import TempDirExecutor, WorkspaceExecutor
from .base import BaseExecutorManager
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class TempDirExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = TempDirExecutor
class WorkspaceExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = WorkspaceExecutor
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
manager = cls(scm, wdir)
try:
assert len(to_run) == 1
for stash_rev, entry in to_run.items():
scm.set_ref(EXEC_HEAD, entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
executor = cls.EXECUTOR_CLS.from_stash_entry(
repo,
stash_rev,
entry,
**kwargs,
)
manager.enqueue(stash_rev, executor)
finally:
for ref in (EXEC_MERGE,):
scm.remove_ref(ref)
return manager
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
results = {}
exp_rev = self.scm.get_ref(EXEC_BRANCH)
if exp_rev:
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
return results
def exec_queue(
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
):
"""Run a single WorkspaceExecutor.
Workspace execution is done within the main DVC process
(rather than in multiprocessing context)
"""
from dvc.exceptions import DvcException
from dvc.stage.monitor import CheckpointKilledError
assert len(self._queue) == 1
assert not detach
result: Dict[str, Dict[str, str]] = defaultdict(dict)
rev, executor = self._queue.popleft()
exec_name = "workspace"
infofile = self.get_infofile_path(exec_name)
try:
exec_result = executor.reproduce(
info=executor.info,
rev=rev,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
log_errors=not isinstance(executor, WorkspaceExecutor),
)
if not exec_result.exp_hash:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
)
if exec_result.ref_info:
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
except CheckpointKilledError:
# Checkpoint errors have already been logged
return {}
except DvcException:
raise
except Exception as exc:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
self.cleanup_executor(exec_name, executor)
return result
@@ -1,109 +0,0 @@
import logging
import posixpath
from collections import defaultdict
from typing import TYPE_CHECKING, Callable, Dict, Generator, Optional, Tuple
from ...stash import ExpStashEntry
from ..base import BaseExecutor
from ..ssh import SSHExecutor, _sshfs
from .base import BaseExecutorManager
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class SSHExecutorManager(BaseExecutorManager):
EXECUTOR_CLS = SSHExecutor
def __init__(
self,
scm: "Git",
wdir: str,
host: Optional[str] = None,
port: Optional[int] = None,
username: Optional[str] = None,
fs_factory: Optional[Callable] = None,
**kwargs,
):
assert host
super().__init__(scm, wdir, **kwargs)
self.host = host
self.port = port
self.username = username
self._fs_factory = fs_factory
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
# TODO: load existing infos using sshfs
yield from []
@classmethod
def from_stash_entries(
cls,
scm: "Git",
wdir: str,
repo: "Repo",
to_run: Dict[str, ExpStashEntry],
**kwargs,
):
machine_name: Optional[str] = kwargs.get("machine_name", None)
manager = cls(
scm, wdir, **repo.machine.get_executor_kwargs(machine_name)
)
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
return manager
def sshfs(self):
return _sshfs(self._fs_factory, host=self.host, port=self.port)
def get_infofile_path(self, name: str) -> str:
return f"{name}{BaseExecutor.INFOFILE_EXT}"
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
from dvc.exceptions import DvcException
from dvc.stage.monitor import CheckpointKilledError
assert len(self._queue) == 1
result: Dict[str, Dict[str, str]] = defaultdict(dict)
rev, executor = self._queue.popleft()
info = executor.info
infofile = posixpath.join(
info.root_dir,
info.dvc_dir,
"tmp",
self.get_infofile_path(rev),
)
try:
exec_result = executor.reproduce(
info=executor.info,
rev=rev,
infofile=infofile,
log_level=logger.getEffectiveLevel(),
fs_factory=self._fs_factory,
)
if not exec_result.exp_hash:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
)
if exec_result.ref_info:
result[rev].update(
self._collect_executor(repo, executor, exec_result)
)
except CheckpointKilledError:
# Checkpoint errors have already been logged
return {}
except DvcException:
raise
except Exception as exc:
raise DvcException(
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
self.cleanup_executor(rev, executor)
return result
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
executor.cleanup()
+20 -8
View File
@@ -15,7 +15,7 @@ from ..refs import (
EXEC_MERGE,
EXEC_NAMESPACE,
)
from .base import BaseExecutor, ExecutorInfo, ExecutorResult
from .base import BaseExecutor, ExecutorInfo, ExecutorResult, TaskStatus
if TYPE_CHECKING:
from queue import Queue
@@ -23,8 +23,8 @@ if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
from ..base import ExpRefInfo, ExpStashEntry
from dvc.repo.experiments.refs import ExpRefInfo
from dvc.repo.experiments.stash import ExpStashEntry
logger = logging.getLogger(__name__)
@@ -75,14 +75,12 @@ class SSHExecutor(BaseExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
):
machine_name: Optional[str] = kwargs.pop("machine_name", None)
executor = cls._from_stash_entry(
repo,
stash_rev,
entry,
cls.gen_dirname(entry.name),
location=machine_name,
@@ -119,9 +117,20 @@ class SSHExecutor(BaseExecutor):
}
return kwargs
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
scm: "Git",
stash_rev: str,
entry: "ExpStashEntry",
infofile: Optional[str],
branch: Optional[str] = None,
):
from ..utils import push_refspec
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
with self.sshfs() as fs:
fs.makedirs(self.root_dir)
self._ssh_cmd(fs, "git init .")
@@ -137,8 +146,11 @@ class SSHExecutor(BaseExecutor):
# TODO: support multiple client key retries in git backends
# (see https://github.com/iterative/dvc/issues/6508)
kwargs = self._git_client_args(fs)
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
with self.set_exec_refs(scm, stash_rev, entry):
refspec = f"{EXEC_NAMESPACE}/"
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
if branch:
push_refspec(scm, self.git_url, branch, branch, **kwargs)
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
+33 -17
View File
@@ -18,11 +18,12 @@ from typing import (
Union,
)
from funcy import cached_property
from funcy import cached_property, retry
from dvc.dependency import ParamsDependency
from dvc.env import DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.lock import LockError
from dvc.ui import ui
from ..exceptions import CheckpointExistsError, ExperimentExistsError
@@ -33,7 +34,7 @@ from ..executor.base import (
ExecutorResult,
)
from ..executor.local import WorkspaceExecutor
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
from ..refs import ExpRefInfo
from ..stash import ExpStash, ExpStashEntry
from ..utils import exp_refs_by_rev, scm_locked
@@ -560,8 +561,9 @@ class BaseStashQueue(ABC):
self.scm.add(list(params.keys()))
@staticmethod
@retry(180, errors=LockError, timeout=1)
@scm_locked
def setup_executor(
def init_executor(
exp: "Experiments",
queue_entry: QueueEntry,
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
@@ -576,21 +578,23 @@ class BaseStashQueue(ABC):
)
if stash_entry.stash_index is not None:
stash.drop(stash_entry.stash_index)
scm.set_ref(EXEC_HEAD, stash_entry.head_rev)
scm.set_ref(EXEC_MERGE, stash_rev)
scm.set_ref(EXEC_BASELINE, stash_entry.baseline_rev)
# Executor will be initialized with an empty git repo that
# we populate by pushing:
# EXEC_HEAD - the base commit for this experiment
# EXEC_MERGE - the unmerged changes (from our stash)
# to be reproduced
# EXEC_BASELINE - the baseline commit for this experiment
return executor_cls.from_stash_entry(
exp.repo, stash_rev, stash_entry, **kwargs
executor = executor_cls.from_stash_entry(
exp.repo, stash_entry, **kwargs
)
infofile = exp.celery_queue.get_infofile_path(stash_rev)
executor.init_git(
exp.repo.scm,
stash_rev,
stash_entry,
infofile,
branch=stash_entry.branch,
)
executor.init_cache(exp.repo, stash_rev)
return executor
def get_infofile_path(self, name: str) -> str:
return os.path.join(
self.pid_dir,
@@ -599,8 +603,9 @@ class BaseStashQueue(ABC):
)
@staticmethod
@retry(180, errors=LockError, timeout=1)
@scm_locked
def collect_executor(
def collect_git(
exp: "Experiments",
executor: BaseExecutor,
exec_result: ExecutorResult,
@@ -624,6 +629,17 @@ class BaseStashQueue(ABC):
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
return results
@classmethod
def collect_executor(
cls,
exp: "Experiments",
executor: BaseExecutor,
exec_result: ExecutorResult,
) -> Dict[str, str]:
results = cls.collect_git(exp, executor, exec_result)
if exec_result.ref_info is not None:
executor.collect_cache(exp.repo, exec_result.ref_info)
+7 -2
View File
@@ -23,7 +23,12 @@ from dvc.exceptions import DvcException
from dvc.ui import ui
from ..exceptions import UnresolvedQueueExpNamesError
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
from ..executor.base import (
EXEC_TMP_DIR,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
from .tasks import run_exp
@@ -248,7 +253,7 @@ class LocalCeleryQueue(BaseStashQueue):
def _load_collected(rev: str) -> Optional[ExecutorResult]:
executor_info = _load_info(rev)
if executor_info.collected:
if executor_info.status > TaskStatus.SUCCESS:
return executor_info.result
raise FileNotFoundError
+7 -21
View File
@@ -3,8 +3,6 @@ from typing import Any, Dict
from celery import shared_task
from celery.utils.log import get_task_logger
from dvc.utils.fs import remove
from ..executor.base import ExecutorInfo
from ..executor.local import TempDirExecutor
from .base import BaseStashQueue, QueueEntry
@@ -13,7 +11,7 @@ logger = get_task_logger(__name__)
@shared_task
def setup_exp(entry_dict: Dict[str, Any]) -> str:
def setup_exp(entry_dict: Dict[str, Any]) -> TempDirExecutor:
"""Setup an experiment.
Arguments:
@@ -28,7 +26,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
repo = Repo(entry.dvc_root)
# TODO: split executor.init_cache into separate subtask - we can release
# exp.scm_lock before DVC push
executor = BaseStashQueue.setup_executor(
executor = BaseStashQueue.init_executor(
repo.experiments,
entry,
TempDirExecutor,
@@ -36,7 +34,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
)
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
executor.info.dump_json(infofile)
return executor.root_dir
return executor
@shared_task
@@ -79,25 +77,14 @@ def collect_exp(
@shared_task
def cleanup_exp( # pylint: disable=unused-argument
tmp_dir: str, entry_dict: Dict[str, Any]
) -> None:
def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
"""Cleanup after an experiment.
Arguments:
tmp_dir: Temp directory to be removed.
entry_dict: Serialized QueueEntry for this experiment.
"""
remove(tmp_dir)
def _set_collected(infofile: str):
try:
executor_info = ExecutorInfo.load_json(infofile)
executor_info.collected = True
executor_info.dump_json(infofile)
except FileNotFoundError:
pass
executor.cleanup(infofile)
@shared_task
@@ -115,11 +102,10 @@ def run_exp(entry_dict: Dict[str, Any]) -> None:
repo = Repo(entry.dvc_root)
queue = repo.experiments.celery_queue
infofile = queue.get_infofile_path(entry.stash_rev)
root_dir = setup_exp.s(entry_dict)()
executor = setup_exp.s(entry_dict)()
try:
cmd = ["dvc", "exp", "exec-run", "--infofile", infofile]
proc_dict = queue.proc.run_signature(cmd, name=entry.stash_rev)()
collect_exp.s(proc_dict, entry_dict)()
finally:
cleanup_exp.s(root_dir, entry_dict)()
_set_collected(infofile)
cleanup_exp.s(executor, infofile)()
+5 -3
View File
@@ -11,6 +11,7 @@ from ..executor.base import (
BaseExecutor,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from ..executor.local import TempDirExecutor
from .base import BaseStashQueue, QueueEntry, QueueGetResult
@@ -60,7 +61,7 @@ class TempDirQueue(WorkspaceQueue):
stash_entry.name,
stash_entry.head_rev,
)
executor = self.setup_executor(
executor = self.init_executor(
self.repo.experiments,
entry,
TempDirExecutor,
@@ -75,8 +76,9 @@ class TempDirQueue(WorkspaceQueue):
for stash_rev in self.proc:
infofile = self.get_infofile_path(stash_rev)
executor_info = ExecutorInfo.load_json(infofile)
if not executor_info.collected and os.path.exists(
executor_info.root_dir
if (
not executor_info.status <= TaskStatus.SUCCESS
and os.path.exists(executor_info.root_dir)
):
yield QueueEntry(
self.repo.root_dir,
+3 -6
View File
@@ -1,12 +1,10 @@
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Collection, Dict, Generator, Optional
from funcy import first
from dvc.exceptions import DvcException
from dvc.utils.fs import remove
from ..exceptions import ExpQueueEmptyError
from ..executor.base import BaseExecutor, ExecutorResult
@@ -41,7 +39,7 @@ class WorkspaceQueue(BaseStashQueue):
stash_entry.name,
stash_entry.head_rev,
)
executor = self.setup_executor(self.repo.experiments, entry)
executor = self.init_executor(self.repo.experiments, entry)
return QueueGetResult(entry, executor)
def iter_queued(self) -> Generator[QueueEntry, None, None]:
@@ -118,9 +116,7 @@ class WorkspaceQueue(BaseStashQueue):
f"Failed to reproduce experiment '{rev[:7]}'"
) from exc
finally:
if self._EXEC_NAME == exec_name:
remove(os.path.join(self.pid_dir, exec_name))
executor.cleanup()
executor.cleanup(infofile)
return results
@staticmethod
@@ -135,6 +131,7 @@ class WorkspaceQueue(BaseStashQueue):
assert exec_result.exp_hash
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
return results
def get_result(self, entry: QueueEntry) -> Optional[ExecutorResult]:
+97 -30
View File
@@ -1,9 +1,11 @@
import logging
from collections import OrderedDict, defaultdict
from datetime import datetime
from enum import Enum
from itertools import chain
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
from dvc.repo.experiments.queue.base import QueueDoneResult
from dvc.repo.metrics.show import _gather_metrics
from dvc.repo.params.show import _gather_params
from dvc.scm import iter_revs
@@ -17,16 +19,21 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class ExpStatus(Enum):
Success = 0
Queued = 1
Running = 2
Failed = 3
@error_handler
def _collect_experiment_commit(
repo,
exp_rev,
stash=False,
sha_only=True,
repo: "Repo",
exp_rev: str,
status: ExpStatus = ExpStatus.Success,
param_deps=False,
running=None,
onerror: Optional[Callable] = None,
is_baseline: bool = False,
):
from dvc.dependency import ParamsDependency, RepoDependency
@@ -67,37 +74,35 @@ def _collect_experiment_commit(
if not (out.is_metric or out.is_plot)
}
res["queued"] = stash
if running is not None and exp_rev in running:
res["running"] = True
res["status"] = status.name
if status == ExpStatus.Running:
res["executor"] = running[exp_rev].get("location")
else:
res["running"] = False
res["executor"] = None
if not stash:
if status == ExpStatus.Failed:
res["error"] = {
"msg": "Experiment run failed.",
"type": "",
}
if status not in {ExpStatus.Queued, ExpStatus.Failed}:
vals = _gather_metrics(
repo, targets=None, rev=rev, recursive=False, onerror=onerror
)
res["metrics"] = vals
if not sha_only and rev != "workspace":
name: Optional[str] = None
if is_baseline:
for refspec in ["refs/tags", "refs/heads"]:
name = repo.scm.describe(rev, base=refspec)
if name:
break
if not name:
name = repo.experiments.get_exact_name(rev)
if name:
name = name.rsplit("/")[-1]
res["name"] = name
return res
def _collect_experiment_branch(
res, repo, branch, baseline, onerror: Optional[Callable] = None, **kwargs
res,
repo,
branch,
baseline,
onerror: Optional[Callable] = None,
running=None,
**kwargs,
):
from dvc.scm import resolve_rev
@@ -105,8 +110,14 @@ def _collect_experiment_branch(
prev = None
revs = list(repo.scm.branch_revs(exp_rev, baseline))
for rev in revs:
status = ExpStatus.Running if rev in running else ExpStatus.Success
collected_exp = _collect_experiment_commit(
repo, rev, onerror=onerror, **kwargs
repo,
rev,
onerror=onerror,
status=status,
running=running,
**kwargs,
)
if len(revs) > 1:
exp = {"checkpoint_tip": exp_rev}
@@ -129,12 +140,54 @@ def _collect_experiment_branch(
return res
def get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
rev_set = set()
baseline_set = set()
for baseline in result:
for rev in result[baseline]:
if rev == "baseline":
rev = baseline
baseline_set.add(rev)
if rev != "workspace":
rev_set.add(rev)
names: Dict[str, Optional[str]] = {}
for base in ("refs/tags/", "refs/heads/"):
if rev_set:
names.update(
(rev, ref[len(base) :])
for rev, ref in repo.scm.describe(
baseline_set, base=base
).items()
if ref is not None
)
rev_set.difference_update(names.keys())
exact_name = repo.experiments.get_exact_name(rev_set)
for baseline, baseline_results in result.items():
for rev, rev_result in baseline_results.items():
name: Optional[str] = None
if rev == "baseline":
rev = baseline
if rev == "workspace":
continue
name = names.get(rev, None)
name = name or exact_name[rev]
if name:
rev_result["data"]["name"] = name
# flake8: noqa: C901
def show(
repo: "Repo",
all_branches=False,
all_tags=False,
revs: Union[List[str], str, None] = None,
all_commits=False,
hide_queued=False,
hide_failed=False,
sha_only=False,
num=1,
param_deps=False,
@@ -163,14 +216,14 @@ def show(
running = repo.experiments.get_running_exps(fetch_refs=fetch_running)
for rev in found_revs:
status = ExpStatus.Running if rev in running else ExpStatus.Success
res[rev]["baseline"] = _collect_experiment_commit(
repo,
rev,
sha_only=sha_only,
status=status,
param_deps=param_deps,
running=running,
onerror=onerror,
is_baseline=True,
)
if rev == "workspace":
@@ -191,7 +244,6 @@ def show(
repo,
exp_ref,
rev,
sha_only=sha_only,
param_deps=param_deps,
running=running,
onerror=onerror,
@@ -202,7 +254,19 @@ def show(
repo.experiments.tempdir_queue.iter_active(),
repo.experiments.celery_queue.iter_active(),
repo.experiments.celery_queue.iter_queued(),
repo.experiments.celery_queue.iter_failed(),
):
if isinstance(entry, QueueDoneResult):
entry = entry.entry
if hide_failed:
continue
status = ExpStatus.Failed
elif entry.stash_rev in running:
status = ExpStatus.Running
else:
if hide_queued:
continue
status = ExpStatus.Queued
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs:
if stash_rev not in running or not running[stash_rev].get(
@@ -211,11 +275,14 @@ def show(
experiment = _collect_experiment_commit(
repo,
stash_rev,
sha_only=sha_only,
stash=stash_rev not in running,
status=status,
param_deps=param_deps,
running=running,
onerror=onerror,
)
res[entry.baseline_rev][stash_rev] = experiment
if not sha_only:
get_names(repo, res)
return res
+81 -99
View File
@@ -3,12 +3,15 @@ from contextlib import suppress
from typing import TYPE_CHECKING, Optional
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError, FileTransferError
from dvc.exceptions import DownloadError
from dvc.fs import Schemes
from . import locked
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import TargetType
from dvc_data.hashfile.transfer import TransferResult
from dvc_objects.db.base import ObjectDB
logger = logging.getLogger(__name__)
@@ -25,22 +28,6 @@ def _fetch_worktree(repo, remote):
*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)
@@ -58,7 +45,7 @@ def fetch(
run_cache=False,
revs=None,
odb: Optional["ObjectDB"] = None,
):
) -> int:
"""Download data items from a cloud and imported repositories
Returns:
@@ -71,6 +58,9 @@ def fetch(
config.NoRemoteError: thrown when downloading only local files and no
remote is configured
"""
from dvc.repo.imports import save_imports
from dvc_data.hashfile.transfer import TransferResult
if isinstance(targets, str):
targets = [targets]
@@ -79,110 +69,102 @@ def fetch(
if _remote.worktree:
return _fetch_worktree(self, _remote)
used = self.used_objs(
targets,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
revs=revs,
)
downloaded = 0
failed = 0
failed_count = 0
try:
if run_cache:
self.stage_cache.pull(remote)
except DownloadError as exc:
failed += exc.amount
failed_count += exc.amount
no_remote_msg: Optional[str] = None
result = TransferResult(set(), set())
try:
d, f = _fetch(
self,
targets,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
revs=revs,
odb=odb,
)
result.transferred.update(d)
result.failed.update(f)
except NoRemoteError as exc:
no_remote_msg = str(exc)
for rev in self.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
):
imported = save_imports(
self,
targets,
unpartial=not rev or rev == "workspace",
recursive=recursive,
)
result.transferred.update(imported)
result.failed.difference_update(imported)
failed_count += len(result.failed)
if failed_count:
if no_remote_msg:
logger.error(no_remote_msg)
raise DownloadError(failed_count)
return len(result.transferred)
def _fetch(
repo: "Repo",
targets: "TargetType",
remote: Optional[str] = None,
jobs: Optional[int] = None,
odb: Optional["ObjectDB"] = None,
**kwargs,
) -> "TransferResult":
from dvc_data.hashfile.transfer import TransferResult
result = TransferResult(set(), set())
used = repo.used_objs(
targets,
remote=remote,
jobs=jobs,
**kwargs,
)
if odb:
all_ids = set()
for _odb, obj_ids in used.items():
all_ids.update(obj_ids)
d, f = _fetch(
self,
d, f = repo.cloud.pull(
all_ids,
jobs=jobs,
remote=remote,
odb=odb,
)
downloaded += d
failed += f
result.transferred.update(d)
result.failed.update(f)
else:
for src_odb, obj_ids in sorted(
used.items(),
key=lambda item: item[0] is not None
and item[0].fs.protocol == Schemes.MEMORY,
):
d, f = _fetch(
self,
d, f = repo.cloud.pull(
obj_ids,
jobs=jobs,
remote=remote,
odb=src_odb,
)
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)
return downloaded
def _fetch(repo, obj_ids, **kwargs):
downloaded = 0
failed = 0
try:
downloaded += repo.cloud.pull(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
result.transferred.update(d)
result.failed.update(f)
return result
+10 -12
View File
@@ -1,19 +1,17 @@
import os
from dvc import output
from dvc.exceptions import URLMissingError
from dvc.fs import download, parse_external_url
from dvc.utils import resolve_output
def get_url(url, out=None, jobs=None):
from dvc import dependency, output
from dvc.utils import resolve_output
def get_url(url, out=None, *, config=None, jobs=None):
out = resolve_output(url, out)
if os.path.exists(url):
url = os.path.abspath(url)
out = os.path.abspath(out)
(dep,) = dependency.loads_from(None, [url])
(out,) = output.loads_from(None, [out], use_cache=False)
if not dep.exists:
raise dep.DoesNotExistError(dep)
dep.download(out, jobs=jobs)
src_fs, src_path = parse_external_url(url, config)
if not src_fs.exists(src_path):
raise URLMissingError(url)
download(src_fs, src_path, out, jobs=jobs)
+11 -1
View File
@@ -1,4 +1,5 @@
import os
from typing import TYPE_CHECKING
from dvc.repo.scm_context import scm_context
from dvc.utils import relpath, resolve_output, resolve_paths
@@ -7,6 +8,9 @@ from dvc.utils.fs import path_isin
from ..exceptions import InvalidArgumentError, OutputDuplicationError
from . import locked
if TYPE_CHECKING:
from dvc.dvcfile import DVCFile
@locked
@scm_context
@@ -27,6 +31,7 @@ def imp_url(
meta=None,
jobs=None,
fs_config=None,
version_aware: bool = False,
):
from dvc.dvcfile import Dvcfile
from dvc.stage import Stage, create_stage, restore_fields
@@ -54,6 +59,11 @@ def imp_url(
):
url = relpath(url, wdir)
if version_aware:
if fs_config is None:
fs_config = {}
fs_config["version_aware"] = True
stage = create_stage(
Stage,
self,
@@ -68,7 +78,7 @@ def imp_url(
out_obj = stage.outs[0]
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
dvcfile = Dvcfile(self, stage.path)
dvcfile: "DVCFile" = Dvcfile(self, stage.path) # type: ignore
dvcfile.remove()
try:
+132
View File
@@ -0,0 +1,132 @@
import logging
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, List, Set, Tuple, Union
if TYPE_CHECKING:
from dvc.dependency.base import Dependency
from dvc.repo import Repo
from dvc.repo.index import Index, IndexView
from dvc.stage import Stage
from dvc.types import TargetType
from dvc_data.hashfile import HashInfo
logger = logging.getLogger(__name__)
def unfetched_view(
index: "Index", targets: "TargetType", unpartial: bool = False, **kwargs
) -> Tuple["IndexView", List["Dependency"]]:
"""Return index view of imports which have not been fetched.
Returns:
Tuple in the form (view, changed_deps) where changed_imports is a list
of import dependencies that cannot be fetched due to changed data
source.
"""
changed_deps: List["Dependency"] = []
def need_fetch(stage: "Stage") -> bool:
if (
not stage.is_import
or stage.is_repo_import
or (stage.is_partial_import and not unpartial)
):
return False
out = stage.outs[0]
if not out.changed_cache():
return False
dep = stage.deps[0]
if dep.changed_checksum():
changed_deps.append(dep)
return False
return True
unfetched = index.targets_view(targets, filter_fn=need_fetch, **kwargs)
return unfetched, changed_deps
def partial_view(
index: "Index", targets: "TargetType", **kwargs
) -> "IndexView":
return index.targets_view(
targets,
filter_fn=lambda s: s.is_partial_import,
**kwargs,
)
def unpartial_imports(index: Union["Index", "IndexView"]) -> int:
"""Update any outs in the index which are no longer partial imports.
Returns:
Total number of files which were unpartialed.
"""
updated = 0
for out in index.outs:
# we need to use view[key] here and since the out fields have not been
# updated yet (out.get_entry() would return the partial-import state)
workspace, key = out.index_key
entry = index.data[workspace][key]
if out.stage.is_partial_import:
if out.stage.is_repo_import:
dep = out.stage.deps[0]
out.hash_info = dep.get_obj().hash_info
out.meta = dep.get_meta()
else:
out.hash_info = entry.hash_info
out.meta = entry.meta
out.stage.dump()
updated += out.meta.nfiles if out.meta.nfiles is not None else 1
return updated
def save_imports(
repo: "Repo", targets: "TargetType", unpartial: bool = False, **kwargs
) -> Set["HashInfo"]:
"""Save (download) imports from their original source location.
Imports which are already cached will not be downloaded.
Returns:
Objects which were downloaded from source location.
"""
from dvc.stage.exceptions import DataSourceChanged
from dvc.utils.fs import makedirs
from dvc_data.index import checkout, md5, save
from dvc_objects.fs.callbacks import Callback
downloaded: Set["HashInfo"] = set()
unfetched, changed = unfetched_view(
repo.index, targets, unpartial=unpartial, **kwargs
)
for dep in changed:
logger.warning(str(DataSourceChanged(f"{dep.stage} ({dep})")))
data_view = unfetched.data["repo"]
if len(data_view):
cache = repo.odb.local
if not cache.fs.exists(cache.path):
makedirs(cache.path)
with TemporaryDirectory(dir=cache.path) as tmpdir:
with Callback.as_tqdm_callback(
desc="Downloading imports from source",
unit="files",
) as cb:
checkout(data_view, tmpdir, cache.fs, callback=cb)
md5(data_view)
save(data_view, odb=cache, hardlink=True)
downloaded.update(
entry.hash_info
for _, entry in data_view.iteritems()
if not entry.meta.isdir and entry.hash_info is not None
)
if unpartial:
unpartial_imports(partial_view(repo.index, targets, **kwargs))
return downloaded
+141 -54
View File
@@ -1,13 +1,13 @@
from contextlib import suppress
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Sequence,
Set,
)
@@ -22,11 +22,11 @@ if TYPE_CHECKING:
from dvc.dependency import Dependency, ParamsDependency
from dvc.fs import FileSystem
from dvc.output import Output
from dvc.repo.stage import StageLoad
from dvc.repo.stage import StageInfo, StageLoad
from dvc.stage import Stage
from dvc.types import StrPath, TargetType
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.index import DataIndex
from dvc_data.index import DataIndex, DataIndexKey, DataIndexView
from dvc_objects.db import ObjectDB
@@ -64,6 +64,7 @@ class Index:
self.stage_collector: "StageLoad" = repo.stage
if stages is not None:
self.stages: List["Stage"] = stages
self._collected_targets: Dict[int, List["StageInfo"]] = {}
@cached_property
def stages(self) -> List["Stage"]: # pylint: disable=method-hidden
@@ -113,6 +114,62 @@ class Index:
return self.filter(is_stage_inside_path)
@staticmethod
def _hash_targets(
targets: Iterable[Optional[str]],
**kwargs: Any,
) -> int:
return hash(
(
frozenset(targets),
kwargs.get("with_deps", False),
kwargs.get("recursive", False),
)
)
def _collect_targets(
self, targets: Optional["TargetType"], **kwargs: Any
) -> List["StageInfo"]:
from itertools import chain
from dvc.repo.stage import StageInfo
from dvc.utils.collections import ensure_list
targets = ensure_list(targets)
if not targets:
return [StageInfo(stage) for stage in self.stages]
targets_hash = self._hash_targets(targets, **kwargs)
if targets_hash not in self._collected_targets:
self._collected_targets[targets_hash] = list(
chain.from_iterable(
self.stage_collector.collect_granular(target, **kwargs)
for target in targets
)
)
return self._collected_targets[targets_hash]
def targets_view(
self,
targets: "TargetType",
filter_fn: Optional[Callable[["Stage"], bool]] = None,
**kwargs: Any,
) -> "IndexView":
"""Return read-only view of index for the specified targets.
Args:
targets: Targets to collect
filter_fn: Optional stage filter to be applied after collecting
targets.
Additional kwargs will be passed into the stage collector.
"""
stage_infos = [
stage_info
for stage_info in self._collect_targets(targets, **kwargs)
if not filter_fn or filter_fn(stage_info.stage)
]
return IndexView(self, stage_infos)
@property
def outs(self) -> Iterator["Output"]:
for stage in self:
@@ -171,8 +228,7 @@ class Index:
def data(self) -> "Dict[str, DataIndex]":
from collections import defaultdict
from dvc.config import NoRemoteError
from dvc_data.index import DataIndex, DataIndexEntry
from dvc_data.index import DataIndex
by_workspace: dict = defaultdict(DataIndex)
@@ -184,22 +240,17 @@ class Index:
continue
workspace, key = out.index_key
try:
remote = self.repo.cloud.get_remote_odb(out.remote)
except NoRemoteError:
remote = None
data_index = by_workspace[workspace]
data_index[key] = DataIndexEntry(
meta=out.meta,
obj=out.get_obj() if out.files else out.obj,
hash_info=out.hash_info,
odb=out.odb,
cache=out.odb,
remote=remote,
)
if out.files:
out.obj = out.get_obj()
entry = out.get_entry()
if out.stage.is_import and not out.stage.is_repo_import:
entry.fs = out.stage.deps[0].fs
entry.path = out.stage.deps[0].fs_path
entry.meta = out.stage.deps[0].meta
data_index[key] = entry
return dict(by_workspace)
@@ -213,22 +264,11 @@ class Index:
jobs: int = None,
) -> "ObjectContainer":
from collections import defaultdict
from itertools import chain
from dvc.utils.collections import ensure_list
used: "ObjectContainer" = defaultdict(set)
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=with_deps
)
for target in collect_targets
pairs = self._collect_targets(
targets, recursive=recursive, with_deps=with_deps
)
for stage, filter_info in pairs:
for odb, objs in stage.get_used_objs(
remote=remote,
@@ -239,28 +279,6 @@ 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.
@@ -326,6 +344,75 @@ class Index:
return dict_md5(self.dumpd())
class IndexView:
"""Read-only view of Index.data using filtered stages."""
def __init__( # pylint: disable=redefined-outer-name
self, index: Index, stage_infos: Iterable["StageInfo"]
):
self._index = index
self._stages: Dict["Stage", Optional[str]] = dict(stage_infos)
def __len__(self) -> int:
return len(self._stages)
def __contains__(self, stage: "Stage") -> bool:
# as we are keeping stages inside a list, it might be slower.
return stage in self._stages
def __iter__(self) -> Iterator["Stage"]:
yield from self._stages
def __getitem__(self, item: str) -> "Stage":
"""Get a stage by its addressing attribute."""
for stage in self:
if stage.addressing == item:
return stage
raise KeyError(f"{item} - available stages are {self.stages}")
@property
def stages(self):
return list(self._stages)
@property
def deps(self) -> Iterator["Dependency"]:
for stage in self:
yield from stage.deps
@property
def outs(self) -> Iterator["Output"]:
for stage, filter_info in self._stages.items():
yield from stage.filter_outs(filter_info)
@cached_property
def _data_prefixes(self) -> Dict[str, Set["DataIndexKey"]]:
from collections import defaultdict
prefixes: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
for out in self.outs:
workspace, key = out.index_key
prefixes[workspace].add(key)
prefixes[workspace].update(key[:i] for i in range(len(key), 0, -1))
return prefixes
@cached_property
def data(self) -> "Dict[str, DataIndexView]":
from functools import partial
from dvc_data.index import view
def key_filter(workspace: str, key: "DataIndexKey"):
try:
return key in self._data_prefixes[workspace]
except KeyError:
return False
return {
workspace: view(index, partial(key_filter, workspace))
for workspace, index in self._index.data.items()
}
if __name__ == "__main__":
import logging
+29
View File
@@ -0,0 +1,29 @@
from dvc.exceptions import URLMissingError
from dvc.fs import parse_external_url
def ls_url(url, *, config=None, recursive=False):
fs, fs_path = parse_external_url(url, config=config)
try:
info = fs.info(fs_path)
except FileNotFoundError as exc:
raise URLMissingError(url) from exc
if info["type"] != "directory":
return [{"path": info["name"], "isdir": False}]
ret = []
for _, dirs, files in fs.walk(fs_path, detail=True):
if not recursive:
files.update(dirs)
for info in files.values():
ls_info = {
"path": fs.path.relpath(info["name"], fs_path),
"isdir": info["type"] == "directory",
}
ret.append(ls_info)
if not recursive:
break
return ret
+5 -1
View File
@@ -100,7 +100,7 @@ class Plots:
"filename":
{
"data_source": callable loading the data,
"props": propreties for the file if it is
"props": properties for the file if it is
plots type output
}
}
@@ -444,6 +444,10 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = dvcfile.load().get("plots", {})
if isinstance(dvcfile_defs, list):
dvcfile_defs = {
k: v for elem in dvcfile_defs for k, v in elem.items()
}
resolved = _resolve_definitions(
repo.dvcfs,
targets,
+13 -16
View File
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, Optional
from dvc.exceptions import FileTransferError, UploadError
from dvc.exceptions import UploadError
from ..utils import glob_targets
from . import locked
@@ -11,13 +11,10 @@ if TYPE_CHECKING:
def _push_worktree(repo, remote):
from dvc_data.hashfile.tree import tree_from_index
from dvc_data.index import build, checkout, collect
from dvc_data.index import checkout
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)
checkout(index, remote.path, remote.fs)
for stage in repo.index.stages:
for out in stage.outs:
@@ -106,18 +103,18 @@ def push(
if not include_imports and dest_odb and dest_odb.read_only:
continue
all_ids.update(obj_ids)
try:
pushed += self.cloud.push(all_ids, jobs, remote=remote, odb=odb)
except FileTransferError as exc:
raise UploadError(exc.amount)
result = self.cloud.push(all_ids, jobs, remote=remote, odb=odb)
if result.failed:
raise UploadError(len(result.failed))
pushed += len(result.transferred)
else:
for dest_odb, obj_ids in used.items():
if dest_odb and dest_odb.read_only:
continue
try:
pushed += self.cloud.push(
obj_ids, jobs, remote=remote, odb=odb or dest_odb
)
except FileTransferError as exc:
raise UploadError(exc.amount)
result = self.cloud.push(
obj_ids, jobs, remote=remote, odb=odb or dest_odb
)
if result.failed:
raise UploadError(len(result.failed))
pushed += len(result.transferred)
return pushed
+2 -3
View File
@@ -225,10 +225,9 @@ class StageLoad:
path, name = parse_target(target)
return self.load_one(path=path, name=name)
@staticmethod
def _get_filepath(path: str = None, name: str = None) -> str:
def _get_filepath(self, path: str = None, name: str = None) -> str:
if path:
return path
return self.repo.fs.path.realpath(path)
path = PIPELINE_FILE
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
+3 -1
View File
@@ -14,7 +14,9 @@ def _joint_status(pairs):
status_info = {}
for stage, filter_info in pairs:
if stage.frozen and not stage.is_repo_import:
if stage.frozen and not (
stage.is_repo_import or stage.is_versioned_import
):
logger.warning(
"%s is frozen. Its dependencies are"
" not going to be shown in the status output.",
+2 -2
View File
@@ -118,7 +118,7 @@ PLOT_DEFINITION = {
Output.PARAM_PLOT_TITLE: str,
Output.PARAM_PLOT_TEMPLATE: str,
}
PLOTS_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
SINGLE_PLOT_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
FOREACH_IN = {
Required(FOREACH_KWD): Any(dict, list, str),
Required(DO_KWD): STAGE_DEFINITION,
@@ -127,9 +127,9 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
}
MULTI_STAGE_SCHEMA = {
PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]),
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
VARS_KWD: VARS_SCHEMA,
PLOTS: PLOTS_SCHEMA,
}
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
+13 -5
View File
@@ -257,6 +257,10 @@ class Stage(params.StageParams):
return isinstance(self.deps[0], RepoDependency)
@property
def is_versioned_import(self):
return self.is_import and self.deps[0].fs.version_aware
@property
def is_checkpoint(self):
"""
@@ -568,6 +572,8 @@ class Stage(params.StageParams):
if kwargs.get("checkpoint_func", None) or no_download:
allow_missing = True
self.save(allow_missing=allow_missing)
if no_download:
self.ignore_outs()
if not no_commit:
self.commit(allow_missing=allow_missing)
@@ -618,7 +624,9 @@ class Stage(params.StageParams):
@rwlocked(read=["deps", "outs"])
def status(self, check_updates=False, filter_info=None):
ret = []
show_import = self.is_repo_import and check_updates
show_import = (
self.is_repo_import or self.is_versioned_import
) and check_updates
if not self.frozen or show_import:
self._status_deps(ret)
@@ -681,7 +689,7 @@ 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:
if self.is_partial_import and not self.is_repo_import:
return {}
used_objs = defaultdict(set)
@@ -706,7 +714,7 @@ class Stage(params.StageParams):
"unable to auto-merge DVC files with deleted outputs"
)
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
assert other
if not other.outs:
@@ -726,7 +734,7 @@ class Stage(params.StageParams):
self._check_can_merge(self, ancestor_out)
self._check_can_merge(other, ancestor_out)
self.outs[0].merge(ancestor_out, other.outs[0])
self.outs[0].merge(ancestor_out, other.outs[0], allowed=allowed)
def dump(self, **kwargs):
self.dvcfile.dump(self, **kwargs)
@@ -768,5 +776,5 @@ class PipelineStage(Stage):
def _changed_stage_entry(self):
return f"'cmd' of {self} has changed."
def merge(self, ancestor, other):
def merge(self, ancestor, other, allowed=None):
raise NotImplementedError
+1 -2
View File
@@ -9,7 +9,6 @@ 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.hashfile.transfer import _log_exceptions
if TYPE_CHECKING:
from dvc_objects.db import ObjectDB
@@ -222,7 +221,7 @@ class StageCache:
from_fs = from_odb.fs
to_fs = to_odb.fs
func = _log_exceptions(fs.generic.copy)
func = fs.generic.log_exceptions(fs.generic.copy)
runs = from_fs.path.join(from_odb.path, "runs")
http_odb = next(
+4 -1
View File
@@ -56,7 +56,10 @@ def sync_import(
stage.outs[0].checkout()
else:
stage.save_deps()
if not no_download:
if no_download:
if stage.is_repo_import:
stage.deps[0].update()
else:
stage.deps[0].download(
stage.outs[0],
jobs=jobs,
+1
View File
@@ -73,6 +73,7 @@ class StageLoader(Mapping):
item.hash_info = HashInfo(item.hash_name, hash_value)
if item.hash_info and item.hash_info.isdir:
item.meta.isdir = True
item.files = get_in(checksums, [key, path, item.PARAM_FILES])
@classmethod
def load_stage(cls, dvcfile, name, stage_data, lock_data=None):
+10 -3
View File
@@ -152,7 +152,7 @@ def to_pipeline_file(stage: "PipelineStage"):
}
def to_single_stage_lockfile(stage: "Stage") -> dict:
def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
assert stage.cmd
def _dumpd(item):
@@ -164,6 +164,13 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
*meta_d.items(),
]
if item.hash_info.isdir and kwargs.get("with_files"):
if item.obj:
obj = item.obj
else:
obj = item.get_obj()
ret.append((item.PARAM_FILES, obj.as_list(with_meta=True)))
return OrderedDict(ret)
res = OrderedDict([("cmd", stage.cmd)])
@@ -183,9 +190,9 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
return res
def to_lockfile(stage: "PipelineStage") -> dict:
def to_lockfile(stage: "PipelineStage", **kwargs) -> dict:
assert stage.name
return {stage.name: to_single_stage_lockfile(stage)}
return {stage.name: to_single_stage_lockfile(stage, **kwargs)}
def to_single_stage_file(stage: "Stage", **kwargs):
@@ -38,7 +38,7 @@ class TestAPI:
)
def test_filesystem(
self,
M, # from `pytest-test-utils`
M,
tmp_dir,
make_tmp_dir,
scm,
@@ -47,6 +47,8 @@ class TestAPI:
fs_kwargs,
clear_cache,
):
fs_kwargs = fs_kwargs.copy()
tmp_dir.scm_gen({"scripts": {"script1": "script1"}}, commit="scripts")
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}}, commit="data")
dvc.push()
@@ -2,6 +2,10 @@ import os
import pytest
from dvc.exceptions import URLMissingError
from dvc.repo import Repo
from dvc.repo.ls_url import ls_url, parse_external_url
class TestImport:
def test_import(self, tmp_dir, dvc, workspace):
@@ -134,3 +138,103 @@ class TestAdd:
assert (
workspace / "cache" / dir_hash_value[:2] / dir_hash_value[2:]
).is_file()
def match_files(fs, entries, expected):
entries_content = {
(fs.path.normpath(d["path"]), d["isdir"]) for d in entries
}
expected_content = {
(fs.path.normpath(d["path"]), d["isdir"]) for d in expected
}
assert entries_content == expected_content
class TestLsUrl:
@pytest.mark.parametrize("fname", ["foo", "foo.dvc", "dir/foo"])
def test_file(self, cloud, fname):
cloud.gen({fname: "foo contents"})
fs, fs_path = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / fname), config=cloud.config)
match_files(
fs,
result,
[{"path": fs.path.join(fs_path, fname), "isdir": False}],
)
def test_dir(self, cloud):
cloud.gen(
{"dir/foo": "foo contents", "dir/subdir/bar": "bar contents"}
)
if not (cloud / "dir").is_dir():
pytest.skip("Cannot create directories on this cloud")
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(str(cloud / "dir"), config=cloud.config)
match_files(
fs,
result,
[
{"path": "foo", "isdir": False},
{"path": "subdir", "isdir": True},
],
)
def test_recursive(self, cloud):
cloud.gen(
{"dir/foo": "foo contents", "dir/subdir/bar": "bar contents"}
)
if not (cloud / "dir").is_dir():
pytest.skip("Cannot create directories on this cloud")
fs, _ = parse_external_url(cloud.url, cloud.config)
result = ls_url(
str(cloud / "dir"), config=cloud.config, recursive=True
)
match_files(
fs,
result,
[
{"path": "foo", "isdir": False},
{"path": "subdir/bar", "isdir": False},
],
)
def test_nonexistent(self, cloud):
with pytest.raises(URLMissingError):
ls_url(str(cloud / "dir"), config=cloud.config)
class TestGetUrl:
def test_get_file(self, cloud, tmp_dir):
cloud.gen({"foo": "foo contents"})
Repo.get_url(str(cloud / "foo"), "foo_imported", config=cloud.config)
assert (tmp_dir / "foo_imported").is_file()
assert (tmp_dir / "foo_imported").read_text() == "foo contents"
def test_get_dir(self, cloud, tmp_dir):
cloud.gen({"foo": {"foo": "foo contents"}})
if not (cloud / "foo").is_dir():
pytest.skip("Cannot create directories on this cloud")
Repo.get_url(str(cloud / "foo"), "foo_imported", config=cloud.config)
assert (tmp_dir / "foo_imported").is_dir()
assert (tmp_dir / "foo_imported" / "foo").is_file()
assert (tmp_dir / "foo_imported" / "foo").read_text() == "foo contents"
@pytest.mark.parametrize("dname", [".", "dir", "dir/subdir"])
def test_get_url_to_dir(self, cloud, tmp_dir, dname):
cloud.gen({"src": {"foo": "foo contents"}})
if not (cloud / "src").is_dir():
pytest.skip("Cannot create directories on this cloud")
tmp_dir.gen({"dir": {"subdir": {}}})
Repo.get_url(str(cloud / "src" / "foo"), dname, config=cloud.config)
assert (tmp_dir / dname).is_dir()
assert (tmp_dir / dname / "foo").read_text() == "foo contents"
def test_get_url_nonexistent(self, cloud):
with pytest.raises(URLMissingError):
Repo.get_url(str(cloud / "nonexistent"), config=cloud.config)
+16 -23
View File
@@ -1,4 +1,3 @@
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, List
@@ -83,27 +82,21 @@ def to_hydra_overrides(path_overrides):
return parser.parse_overrides(overrides=path_overrides)
def dict_product(dicts):
import itertools
return [dict(zip(dicts, x)) for x in itertools.product(*dicts.values())]
def get_hydra_sweeps(path_overrides):
merged_overrides = []
path_sweeps = {}
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
overrides = to_hydra_overrides(overrides)
for override in overrides:
if override.value_type == ValueType.GLOB_CHOICE_SWEEP:
raise InvalidArgumentError(
f"Glob override '{override.input_line}' "
"is not supported."
)
path_sweeps[path] = BasicSweeper.split_arguments(overrides, None)[0]
return dict_product(path_sweeps)
+13 -14
View File
@@ -22,6 +22,7 @@ classifiers =
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options]
setup_requires =
@@ -62,12 +63,12 @@ install_requires =
rich>=10.13.0
pyparsing>=2.4.7
typing-extensions>=3.7.4
scmrepo==0.1.1
dvc-render==0.0.11
dvc-task==0.1.2
scmrepo==0.1.2
dvc-render==0.0.12
dvc-task==0.1.4
dvclive>=0.10.0
dvc-data==0.10.0
dvc-http==2.19.1
dvc-data==0.20.0
dvc-http==2.27.2
hydra-core>=1.1.0
iterative-telemetry==0.0.5
@@ -94,11 +95,11 @@ hdfs =
dvc-hdfs==2.19.0; python_version < '3.11'
oss = dvc-oss==2.19.0
s3 = dvc-s3==2.20.1
ssh = dvc-ssh==2.19.0
ssh = dvc-ssh==2.20.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 = dvc-ssh[gssapi]==2.19.0
ssh_gssapi = dvc-ssh[gssapi]==2.20.0
webdav = dvc-webdav==2.19.0
webhdfs = dvc-webhdfs==2.19.0
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
@@ -108,12 +109,12 @@ testing =
pytest-test-utils==0.0.8
tests =
%(terraform)s
dvc-ssh==2.19.0
dvc-ssh==2.20.0
# Test requirements
pytest==7.1.3
pytest-cov==3.0.0
pytest-cov==4.0.0
pytest-xdist==2.5.0
pytest-mock==3.8.2
pytest-mock==3.10.0
pytest-lazy-fixture==0.6.3
%(testing)s
# https://github.com/docker/docker-py/issues/2902
@@ -122,16 +123,14 @@ tests =
pytest-timeout==2.1.0
filelock==3.8.0
# pylint requirements
pylint==2.15.2
pylint==2.15.4
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.1.2
# see https://github.com/PyCQA/pylint/issues/7488
astroid==2.12.9
# we use this to suppress some messages in tests, eg: foo/bar naming,
# and, protected method calls in our tests
pylint-plugin-utils==0.7
# type-checking
mypy==0.971
mypy==0.982
types-requests>=2.27.15
types-tabulate>=0.8.6
types-toml>=0.10.4
+3 -1
View File
@@ -9,7 +9,9 @@ from dvc.exceptions import (
OutputNotFoundError,
PathMissingError,
)
from dvc.testing.test_api import TestAPI # noqa, pylint: disable=unused-import
from dvc.testing.api_tests import ( # noqa, pylint: disable=unused-import
TestAPI,
)
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils.fs import remove
+4 -114
View File
@@ -1,122 +1,12 @@
from textwrap import dedent
import pytest
from tests.func.test_repro_multistage import COPY_SCRIPT
from tests.unit.repo.experiments.conftest import ( # noqa pylint disable=unused-argument
from tests.unit.repo.experiments.conftest import ( # noqa, pylint disable=unused-argument
checkpoint_stage,
exp_stage,
failed_exp_stage,
test_queue,
)
DEFAULT_ITERATIONS = 2
CHECKPOINT_SCRIPT_FORMAT = dedent(
"""\
import os
import sys
import shutil
from dvc.api import make_checkpoint
checkpoint_file = {}
checkpoint_iterations = int({})
if os.path.exists(checkpoint_file):
with open(checkpoint_file) as fobj:
try:
value = int(fobj.read())
except ValueError:
value = 0
else:
with open(checkpoint_file, "w"):
pass
value = 0
shutil.copyfile({}, {})
if os.getenv("DVC_CHECKPOINT"):
for _ in range(checkpoint_iterations):
value += 1
with open(checkpoint_file, "w") as fobj:
fobj.write(str(value))
make_checkpoint()
"""
)
CHECKPOINT_SCRIPT = CHECKPOINT_SCRIPT_FORMAT.format(
"sys.argv[1]", "sys.argv[2]", "sys.argv[3]", "sys.argv[4]"
)
@pytest.fixture
def exp_stage(tmp_dir, scm, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
metrics_no_cache=["metrics.yaml"],
params=["foo"],
name="copy-file",
deps=["copy.py"],
)
scm.add(
[
"dvc.yaml",
"dvc.lock",
"copy.py",
"params.yaml",
"metrics.yaml",
".gitignore",
]
)
scm.commit("init")
return stage
@pytest.fixture
def checkpoint_stage(tmp_dir, scm, dvc, mocker):
mocker.patch("dvc.stage.run.Monitor.AWAIT", 0.01)
tmp_dir.gen("checkpoint.py", CHECKPOINT_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
cmd=(
f"python checkpoint.py foo {DEFAULT_ITERATIONS} "
"params.yaml metrics.yaml"
),
metrics_no_cache=["metrics.yaml"],
params=["foo"],
checkpoints=["foo"],
deps=["checkpoint.py"],
no_exec=True,
name="checkpoint-file",
)
scm.add(["dvc.yaml", "checkpoint.py", "params.yaml", ".gitignore"])
scm.commit("init")
stage.iterations = DEFAULT_ITERATIONS
return stage
@pytest.fixture
def failed_exp_stage(tmp_dir, scm, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.stage.add(
cmd="python -c 'import sys; sys.exit(1)'",
metrics_no_cache=["failed-metrics.yaml"],
params=["foo"],
name="failed-copy-file",
deps=["copy.py"],
)
scm.add(
[
"dvc.yaml",
"dvc.lock",
"copy.py",
"params.yaml",
"failed-metrics.yaml",
".gitignore",
]
)
scm.commit("init")
return stage
@pytest.fixture
def http_auth_patch(mocker):
+5 -6
View File
@@ -29,7 +29,6 @@ def test_init_from_stash(tmp_dir, scm, dvc, machine_instance, mocker):
mock_entry.name = ""
SSHExecutor.from_stash_entry(
dvc,
"abc123",
mock_entry,
machine_name="foo",
)
@@ -39,14 +38,14 @@ def test_init_from_stash(tmp_dir, scm, dvc, machine_instance, mocker):
@pytest.mark.needs_internet
@pytest.mark.parametrize("cloud", [pytest.lazy_fixture("git_ssh")])
def test_init_git(tmp_dir, scm, cloud):
def test_init_git(tmp_dir, scm, cloud, mocker):
tmp_dir.scm_gen({"foo": "foo", "dir": {"bar": "bar"}}, commit="init")
baseline_rev = scm.get_rev()
scm.set_ref(EXEC_HEAD, baseline_rev)
tmp_dir.gen("foo", "stashed")
scm.gitpython.git.stash()
rev = scm.resolve_rev("stash@{0}")
scm.set_ref(EXEC_MERGE, rev)
mock = mocker.Mock(baseline_rev=baseline_rev, head_rev=baseline_rev)
root_url = cloud / SSHExecutor.gen_dirname()
@@ -59,7 +58,8 @@ def test_init_git(tmp_dir, scm, cloud):
username=TEST_SSH_USER,
fs_factory=partial(_ssh_factory, cloud),
)
executor.init_git(scm)
infofile = str((root_url / "foo.run").path)
executor.init_git(scm, rev, mock, infofile=infofile)
assert root_url.path == executor._repo_abspath
fs = cloud._ssh
@@ -117,7 +117,6 @@ def test_reproduce(tmp_dir, scm, dvc, cloud, exp_stage, mocker):
SSHExecutor.reproduce(
info,
rev,
infofile=infofile,
fs_factory=partial(_ssh_factory, cloud),
)
assert mock_execute.called_once()
+3 -3
View File
@@ -38,7 +38,7 @@ def test_new_simple(tmp_dir, scm, dvc, exp_stage, mocker, name, workspace):
assert (tmp_dir / "metrics.yaml").read_text().strip() == "foo: 2"
exp_name = name if name else ref_info.name
assert dvc.experiments.get_exact_name(exp) == exp_name
assert dvc.experiments.get_exact_name([exp])[exp] == exp_name
assert resolve_rev(scm, exp_name) == exp
@@ -481,7 +481,7 @@ def test_subdir(tmp_dir, scm, dvc, workspace):
with fs.open("dir/metrics.yaml", mode="r", encoding="utf-8") as fobj:
assert fobj.read().strip() == "foo: 2"
assert dvc.experiments.get_exact_name(exp) == ref_info.name
assert dvc.experiments.get_exact_name([exp])[exp] == ref_info.name
assert resolve_rev(scm, ref_info.name) == exp
@@ -525,7 +525,7 @@ def test_subrepo(tmp_dir, scm, workspace):
with fs.open("dir/repo/metrics.yaml", mode="r", encoding="utf-8") as fobj:
assert fobj.read().strip() == "foo: 2"
assert subrepo.dvc.experiments.get_exact_name(exp) == ref_info.name
assert subrepo.dvc.experiments.get_exact_name([exp])[exp] == ref_info.name
assert resolve_rev(scm, ref_info.name) == exp
+106 -23
View File
@@ -12,6 +12,7 @@ from dvc.repo.experiments.executor.base import (
EXEC_TMP_DIR,
BaseExecutor,
ExecutorInfo,
TaskStatus,
)
from dvc.repo.experiments.queue.base import QueueEntry
from dvc.repo.experiments.refs import CELERY_STASH, ExpRefInfo
@@ -47,6 +48,21 @@ def test_show_no_commits(tmp_dir):
assert Repo.init().experiments.show() == {}
@pytest.mark.vscode
def test_show_branch_and_tag_name(tmp_dir, scm, dvc, exp_stage):
with tmp_dir.branch("new/branch", new=True):
tmp_dir.scm_gen("branch", "branch", "commit")
branch_rev = scm.get_rev()
result = dvc.experiments.show(all_branches=True)
assert result[branch_rev]["baseline"]["data"]["name"] == "new/branch"
scm.tag("new/tag")
tag_rev = scm.get_rev()
result = dvc.experiments.show(all_tags=True)
assert result[tag_rev]["baseline"]["data"]["name"] == "new/tag"
@pytest.mark.vscode
def test_show_simple(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.show()["workspace"] == {
@@ -62,8 +78,7 @@ def test_show_simple(tmp_dir, scm, dvc, exp_stage):
"metrics": {"metrics.yaml": {"data": {"foo": 1}}},
"outs": {},
"params": {"params.yaml": {"data": {"foo": 1}}},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"timestamp": None,
}
@@ -96,8 +111,7 @@ def test_show_experiment(tmp_dir, scm, dvc, exp_stage, workspace):
"metrics": {"metrics.yaml": {"data": {"foo": 1}}},
"outs": {},
"params": {"params.yaml": {"data": {"foo": 1}}},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"timestamp": timestamp,
"name": "master",
@@ -129,7 +143,7 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
assert len(results) == 2
exp = results[exp_rev]["data"]
assert exp["name"] == "test_name"
assert exp["queued"]
assert exp["status"] == "Queued"
assert exp["params"]["params.yaml"] == {"data": {"foo": 2}}
# test that only queued experiments for the current baseline are returned
@@ -144,10 +158,67 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
results = dvc.experiments.show()[new_rev]
assert len(results) == 2
exp = results[exp_rev]["data"]
assert exp["queued"]
assert exp["status"] == "Queued"
assert exp["params"]["params.yaml"] == {"data": {"foo": 3}}
@pytest.mark.vscode
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage):
baseline_rev = scm.get_rev()
timestamp = datetime.fromtimestamp(
scm.gitpython.repo.rev_parse(baseline_rev).committed_date
)
dvc.experiments.run(
failed_exp_stage.addressing, params=["foo=2"], queue=True
)
exp_rev = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
dvc.experiments.run(run_all=True)
experiments = dvc.experiments.show()[baseline_rev]
expected_baseline = {
"data": {
"deps": {
"copy.py": {
"hash": ANY,
"size": ANY,
"nfiles": None,
}
},
"metrics": {},
"outs": {},
"params": {"params.yaml": {"data": {"foo": 1}}},
"status": "Success",
"executor": None,
"timestamp": timestamp,
"name": "master",
}
}
expected_failed = {
"data": {
"timestamp": ANY,
"params": {"params.yaml": {"data": {"foo": 2}}},
"deps": {"copy.py": {"hash": None, "size": None, "nfiles": None}},
"outs": {},
"status": "Failed",
"executor": None,
"error": {
"msg": "Experiment run failed.",
"type": "",
},
}
}
assert len(experiments) == 2
for rev, exp in experiments.items():
if rev == "baseline":
assert exp == expected_baseline
else:
assert rev == exp_rev
assert exp == expected_failed
@pytest.mark.vscode
@pytest.mark.parametrize("workspace", [True, False])
def test_show_checkpoint(
@@ -174,7 +245,7 @@ def test_show_checkpoint(
for i, rev in enumerate(checkpoints):
if i == 0:
name = dvc.experiments.get_exact_name(rev)
name = dvc.experiments.get_exact_name([rev])[rev]
name = f"{rev[:7]} [{name}]"
fs = "╓"
elif i == len(checkpoints) - 1:
@@ -338,9 +409,12 @@ def test_show_sort(tmp_dir, scm, dvc, exp_stage, caplog):
@pytest.mark.vscode
def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
@pytest.mark.parametrize("status", [TaskStatus.RUNNING, TaskStatus.FAILED])
def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys, status):
pid_dir = os.path.join(dvc.tmp_dir, EXEC_TMP_DIR, EXEC_PID_DIR)
info = make_executor_info(location=BaseExecutor.DEFAULT_LOCATION)
info = make_executor_info(
location=BaseExecutor.DEFAULT_LOCATION, status=status
)
pidfile = os.path.join(
pid_dir,
"workspace",
@@ -349,7 +423,9 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
makedirs(os.path.dirname(pidfile), True)
(tmp_dir / pidfile).dump_json(info.asdict())
assert dvc.experiments.show()["workspace"] == {
print(dvc.experiments.show().get("workspace"))
assert dvc.experiments.show().get("workspace") == {
"baseline": {
"data": {
"deps": {
@@ -362,19 +438,22 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
"metrics": {"metrics.yaml": {"data": {"foo": 1}}},
"params": {"params.yaml": {"data": {"foo": 1}}},
"outs": {},
"queued": False,
"running": True,
"executor": info.location,
"status": "Running"
if status == TaskStatus.RUNNING
else "Success",
"executor": info.location
if status == TaskStatus.RUNNING
else None,
"timestamp": None,
}
}
}
capsys.readouterr()
assert main(["exp", "show", "--csv"]) == 0
cap = capsys.readouterr()
assert "Running" in cap.out
assert info.location in cap.out
if status == TaskStatus.RUNNING:
assert "Running" in cap.out
assert info.location in cap.out
def test_show_running_tempdir(tmp_dir, scm, dvc, exp_stage, mocker):
@@ -419,10 +498,10 @@ def test_show_running_tempdir(tmp_dir, scm, dvc, exp_stage, mocker):
[mocker.call(stash_rev, pidfile, True)],
)
exp_data = get_in(results, [baseline_rev, exp_rev, "data"])
assert exp_data["running"]
assert exp_data["status"] == "Running"
assert exp_data["executor"] == info.location
assert not results["workspace"]["baseline"]["data"]["running"]
assert results["workspace"]["baseline"]["data"]["status"] == "Success"
def test_show_running_celery(tmp_dir, scm, dvc, exp_stage, mocker):
@@ -444,11 +523,10 @@ def test_show_running_celery(tmp_dir, scm, dvc, exp_stage, mocker):
results = dvc.experiments.show()
exp_data = get_in(results, [baseline_rev, exp_rev, "data"])
assert not exp_data["queued"]
assert exp_data["running"]
assert exp_data["status"] == "Running"
assert exp_data["executor"] == info.location
assert not results["workspace"]["baseline"]["data"]["running"]
assert results["workspace"]["baseline"]["data"]["status"] == "Success"
def test_show_running_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, mocker):
@@ -470,6 +548,11 @@ def test_show_running_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, mocker):
"iter_active",
return_value=entries,
)
mocker.patch.object(
dvc.experiments.celery_queue,
"iter_failed",
return_value=[],
)
pidfile = queue.get_infofile_path(entries[0].stash_rev)
info = make_executor_info(
git_url="foo.git",
@@ -486,10 +569,10 @@ def test_show_running_checkpoint(tmp_dir, scm, dvc, checkpoint_stage, mocker):
results = dvc.experiments.show()
checkpoint_res = get_in(results, [baseline_rev, checkpoint_rev, "data"])
assert checkpoint_res["running"]
assert checkpoint_res["status"] == "Running"
assert checkpoint_res["executor"] == info.location
assert not results["workspace"]["baseline"]["data"]["running"]
assert results["workspace"]["baseline"]["data"]["status"] == "Success"
def test_show_with_broken_repo(tmp_dir, scm, dvc, exp_stage, caplog):
+1 -1
View File
@@ -34,7 +34,7 @@ from dvc.stage.exceptions import (
StageExternalOutputsError,
StagePathNotFoundError,
)
from dvc.testing.test_workspace import TestAdd
from dvc.testing.workspace_tests import TestAdd
from dvc.utils import LARGE_DIR_SIZE, relpath
from dvc.utils.fs import path_isin
from dvc.utils.serialize import YAMLFileCorruptedError, load_yaml
+1 -1
View File
@@ -9,7 +9,7 @@ import dvc_data
from dvc.cli import main
from dvc.external_repo import clean_repos
from dvc.stage.exceptions import StageNotFound
from dvc.testing.test_remote import ( # noqa, pylint: disable=unused-import
from dvc.testing.remote_tests import ( # noqa, pylint: disable=unused-import
TestRemote,
)
from dvc.utils.fs import remove
+42 -13
View File
@@ -15,6 +15,16 @@ from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.stage.loader import StageNotFound
from dvc.utils.strictyaml import YAMLValidationError
STAGE_EXAMPLE = {
"stage1": {
"cmd": "cp foo bar",
"desc": "stage desc",
"meta": {"key1": "value1", "key2": "value2"},
"deps": ["foo"],
"outs": [{"bar": {"desc": "bar desc", "meta": {"key": "value"}}}],
}
}
def test_run_load_one_for_multistage(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
@@ -386,19 +396,7 @@ def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, 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"}}}
],
}
}
}
data = {"stages": STAGE_EXAMPLE}
(tmp_dir / "dvc.yaml").dump(data)
stage = dvc.stage.load_one(name="stage1")
@@ -411,3 +409,34 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
# sanity check
stage.dump()
assert (tmp_dir / "dvc.yaml").parse() == data
@pytest.mark.parametrize(
"data",
(
{
"plots": {
"path/to/plot": {"x": "value", "y": "value"},
"path/to/another/plot": {"x": "value", "y": "value"},
},
"stages": STAGE_EXAMPLE,
},
{
"plots": [
{"path/to/plot": {"x": "value", "y": "value"}},
{"path/to/another/plot": {"x": "value", "y": "value"}},
],
"stages": STAGE_EXAMPLE,
},
),
)
def test_dvcfile_load_with_plots(tmp_dir, dvc, data):
(tmp_dir / "dvc.yaml").dump(data)
plots = list(dvc.plots.collect())
top_level_plots = plots[0]["workspace"]["definitions"]["data"]["dvc.yaml"][
"data"
]
assert all(
name in top_level_plots
for name in ("path/to/plot", "path/to/another/plot")
)
-55
View File
@@ -1,7 +1,6 @@
import os
from operator import itemgetter
from dvc.fs.callbacks import Callback
from dvc.repo import Repo
@@ -42,57 +41,3 @@ def test_walk_dont_ignore_subrepos(tmp_dir, scm, dvc):
assert set(get_dirs(next(dvc_fs.walk(path)))) == {".dvc", "subdir", ".git"}
assert set(get_dirs(next(scm_fs.walk("/")))) == {".dvc", "subdir"}
def test_callback_on_dvcfs(tmp_dir, dvc, scm, mocker):
tmp_dir.dvc_gen({"dir": {"bar": "bar"}}, commit="dvc")
tmp_dir.scm_gen({"dir": {"foo": "foo"}}, commit="git")
fs = dvc.dvcfs
callback = Callback()
fs.get(
"dir",
(tmp_dir / "dir2").fs_path,
callback=callback,
)
assert (tmp_dir / "dir2").read_text() == {"foo": "foo", "bar": "bar"}
assert callback.size == 2
assert callback.value == 2
callback = Callback()
branch = mocker.spy(callback, "branch")
fs.get(
os.path.join("dir", "foo"),
(tmp_dir / "foo").fs_path,
callback=callback,
)
size = os.path.getsize(tmp_dir / "dir" / "foo")
assert (tmp_dir / "foo").read_text() == "foo"
assert callback.size == 1
assert callback.value == 1
assert branch.call_count == 1
assert branch.spy_return.size == size
assert branch.spy_return.value == size
branch.reset_mock()
callback = Callback()
branch = mocker.spy(callback, "branch")
fs.get(
os.path.join("dir", "bar"),
(tmp_dir / "bar").fs_path,
callback=callback,
)
size = os.path.getsize(tmp_dir / "dir" / "bar")
assert (tmp_dir / "bar").read_text() == "bar"
assert callback.size == 1
assert callback.value == 1
assert branch.call_count == 1
assert branch.spy_return.size == size
assert branch.spy_return.value == size
+8 -3
View File
@@ -2,8 +2,9 @@ import os
import pytest
from dvc.dependency.base import DependencyDoesNotExistError
from dvc.exceptions import URLMissingError
from dvc.repo import Repo
from dvc.testing.workspace_tests import TestGetUrl as _TestGetUrl
def test_get_file(tmp_dir):
@@ -26,7 +27,7 @@ def test_get_dir(tmp_dir):
@pytest.mark.parametrize("dname", [".", "dir", "dir/subdir"])
def test_get_url_to_dir(tmp_dir, tmp_path_factory, dname):
def test_get_url_to_dir(tmp_dir, dname):
tmp_dir.gen({"src": {"foo": "foo contents"}, "dir": {"subdir": {}}})
Repo.get_url(os.path.join("src", "foo"), dname)
@@ -36,5 +37,9 @@ def test_get_url_to_dir(tmp_dir, tmp_path_factory, dname):
def test_get_url_nonexistent(tmp_dir):
with pytest.raises(DependencyDoesNotExistError):
with pytest.raises(URLMissingError):
Repo.get_url("nonexistent")
class TestGetUrl(_TestGetUrl):
pass
+65 -14
View File
@@ -18,10 +18,6 @@ def _to_pattern_info_list(str_list: List):
return [PatternInfo(a, "") for a in str_list]
def walk_files(dvc, *args):
yield from dvc.dvcignore.find(*args)
@pytest.mark.parametrize("filename", ["ignored", "тест"])
def test_ignore(tmp_dir, dvc, filename):
tmp_dir.gen({"dir": {filename: filename, "other": "text2"}})
@@ -29,13 +25,68 @@ def test_ignore(tmp_dir, dvc, filename):
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir)
result = dvc.dvcignore.find(dvc.fs, tmp_dir)
assert set(result) == {
(tmp_dir / DvcIgnore.DVCIGNORE_FILE).fs_path,
(tmp_dir / "dir" / "other").fs_path,
}
def test_walk(tmp_dir, dvc):
tmp_dir.gen(
{
"foo": "foo",
"bar": "bar",
"dir": {
"foo": "foo",
"bar": "bar",
"baz": "baz",
"subdir": {"foo": "foo", "qux": "qux"},
},
}
)
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/bar\nfoo")
dvc._reset()
result = list(dvc.dvcignore.walk(dvc.fs, tmp_dir))
assert result[0][0] == str(tmp_dir)
assert result[0][1] == ["dir"]
assert set(result[0][2]) == {"bar", ".dvcignore"}
assert result[1][0] == str(tmp_dir / "dir")
assert result[1][1] == ["subdir"]
assert result[1][2] == ["baz"]
assert result[2][0] == str(tmp_dir / "dir" / "subdir")
assert result[2][1] == []
assert result[2][2] == ["qux"]
result = list(dvc.dvcignore.walk(dvc.fs, tmp_dir, detail=True))
assert result == [
(
str(tmp_dir),
{"dir": dvc.fs.info(tmp_dir / "dir")},
{
"bar": dvc.fs.info(tmp_dir / "bar"),
".dvcignore": dvc.fs.info(tmp_dir / ".dvcignore"),
},
),
(
str(tmp_dir / "dir"),
{
"subdir": dvc.fs.info(tmp_dir / "dir" / "subdir"),
},
{
"baz": dvc.fs.info(tmp_dir / "dir" / "baz"),
},
),
(
str(tmp_dir / "dir" / "subdir"),
{},
{"qux": dvc.fs.info(tmp_dir / "dir" / "subdir" / "qux")},
),
]
def test_rename_ignored_file(tmp_dir, dvc):
tmp_dir.gen({"dir": {"ignored": "...", "other": "text"}})
@@ -133,7 +184,7 @@ def test_ignore_on_branch(tmp_dir, scm, dvc):
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir)
result = dvc.dvcignore.find(dvc.fs, tmp_dir)
assert set(result) == {
(tmp_dir / "foo").fs_path,
(tmp_dir / "bar").fs_path,
@@ -155,7 +206,7 @@ def test_match_nested(tmp_dir, dvc):
}
)
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir)
result = dvc.dvcignore.find(dvc.fs, tmp_dir)
assert set(result) == {
(tmp_dir / DvcIgnore.DVCIGNORE_FILE).fs_path,
(tmp_dir / "foo").fs_path,
@@ -167,7 +218,7 @@ def test_ignore_external(tmp_dir, scm, dvc, tmp_path_factory):
ext_dir = TmpDir(os.fspath(tmp_path_factory.mktemp("external_dir")))
ext_dir.gen({"y.backup": "y", "tmp": {"file": "ext tmp"}})
result = walk_files(dvc, dvc.fs, ext_dir)
result = dvc.dvcignore.find(dvc.fs, ext_dir)
assert set(result) == {
(ext_dir / "y.backup").fs_path,
(ext_dir / "tmp" / "file").fs_path,
@@ -207,7 +258,7 @@ def test_ignore_blank_line(tmp_dir, dvc):
tmp_dir.gen({"dir": {"ignored": "text", "other": "text2"}})
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "foo\n\ndir/ignored")
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir / "dir")
result = dvc.dvcignore.find(dvc.fs, tmp_dir / "dir")
assert set(result) == {(tmp_dir / "dir" / "other").fs_path}
@@ -242,7 +293,7 @@ def test_ignore_file_in_parent_path(
tmp_dir.gen(data_struct)
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "\n".join(pattern_list))
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir / "dir")
result = dvc.dvcignore.find(dvc.fs, tmp_dir / "dir")
assert set(result) == {
(tmp_dir / relpath).fs_path for relpath in result_set
}
@@ -265,7 +316,7 @@ def test_ignore_sub_directory(tmp_dir, dvc):
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "doc/fortz"}})
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir / "dir")
result = dvc.dvcignore.find(dvc.fs, tmp_dir / "dir")
assert set(result) == {
(tmp_dir / "dir" / "a" / "doc" / "fortz" / "a").fs_path,
(tmp_dir / "dir" / DvcIgnore.DVCIGNORE_FILE).fs_path,
@@ -277,7 +328,7 @@ def test_ignore_directory(tmp_dir, dvc):
tmp_dir.gen({"dir": {"fortz": {}, "a": {"fortz": {}}}})
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "fortz"}})
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir / "dir")
result = dvc.dvcignore.find(dvc.fs, tmp_dir / "dir")
assert set(result) == {
(tmp_dir / "dir" / DvcIgnore.DVCIGNORE_FILE).fs_path
}
@@ -288,7 +339,7 @@ def test_multi_ignore_file(tmp_dir, dvc, monkeypatch):
tmp_dir.gen(DvcIgnore.DVCIGNORE_FILE, "dir/subdir/*_ignore")
tmp_dir.gen({"dir": {DvcIgnore.DVCIGNORE_FILE: "!subdir/not_ignore"}})
dvc._reset()
result = walk_files(dvc, dvc.fs, tmp_dir / "dir")
result = dvc.dvcignore.find(dvc.fs, tmp_dir / "dir")
assert set(result) == {
(tmp_dir / "dir" / "subdir" / "not_ignore").fs_path,
(tmp_dir / "dir" / DvcIgnore.DVCIGNORE_FILE).fs_path,
@@ -386,7 +437,7 @@ def test_ignore_in_added_dir(tmp_dir, dvc):
dvc._reset()
ignored_path = tmp_dir / "dir" / "sub" / "ignored"
result = walk_files(dvc, dvc.fs, ignored_path)
result = dvc.dvcignore.find(dvc.fs, ignored_path)
assert set(result) == set()
assert ignored_path.exists()
+62
View File
@@ -217,6 +217,68 @@ def test_pull_imported_stage(tmp_dir, dvc, erepo_dir):
assert os.path.isfile(dst_cache)
def test_import_no_download(tmp_dir, scm, dvc, erepo_dir):
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
dvc.imp(os.fspath(erepo_dir), "foo", "foo_imported", no_download=True)
assert not os.path.exists("foo_imported")
dst_stage = Dvcfile(dvc, "foo_imported.dvc").stage
assert dst_stage.deps[0].def_repo == {
"url": os.fspath(erepo_dir),
"rev_lock": erepo_dir.scm.get_rev(),
}
assert scm.is_ignored("foo_imported")
def test_pull_import_no_download(tmp_dir, scm, dvc, erepo_dir):
with erepo_dir.chdir():
erepo_dir.scm_gen(os.path.join("foo", "bar"), b"bar", commit="add bar")
erepo_dir.dvc_gen(
os.path.join("foo", "baz"), b"baz contents", commit="add baz"
)
size = (
len(b"bar")
+ len(b"baz contents")
+ len((erepo_dir / "foo" / ".gitignore").read_bytes())
)
dvc.imp(os.fspath(erepo_dir), "foo", "foo_imported", no_download=True)
dvc.pull(["foo_imported.dvc"])
assert os.path.exists("foo_imported")
stage = Dvcfile(dvc, "foo_imported.dvc").stage
assert (
stage.outs[0].hash_info.value == "bdb8641831d8fcb03939637e09011c21.dir"
)
assert stage.outs[0].meta.size == size
assert stage.outs[0].meta.nfiles == 3
assert stage.outs[0].meta.isdir
def test_pull_import_no_download_rev_lock(
tmp_dir,
dvc,
erepo_dir,
):
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "foo content", commit="add")
dvc.imp(os.fspath(erepo_dir), "foo", "foo_imported", no_download=True)
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "modified foo content", commit="modify foo")
dvc.pull(["foo_imported.dvc"])
assert (tmp_dir / "foo_imported").read_text() == "foo content"
def test_cache_type_is_properly_overridden(tmp_dir, scm, dvc, erepo_dir):
with erepo_dir.chdir():
with erepo_dir.dvc.config.edit() as conf:
+19 -2
View File
@@ -8,9 +8,10 @@ import pytest
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dependency.base import Dependency, DependencyDoesNotExistError
from dvc.dvcfile import Dvcfile
from dvc.exceptions import InvalidArgumentError
from dvc.stage import Stage
from dvc.testing.test_workspace import TestImport as _TestImport
from dvc.testing.workspace_tests import TestImport as _TestImport
from dvc.utils.fs import makedirs
from tests.basic_env import TestDvc
@@ -282,7 +283,7 @@ def test_import_url_to_remote_status(tmp_dir, dvc, local_cloud, local_remote):
assert len(status) == 0
def test_import_url_no_download(tmp_dir, dvc, local_workspace):
def test_import_url_no_download(tmp_dir, scm, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
stage = dvc.imp_url(
@@ -292,6 +293,7 @@ def test_import_url_no_download(tmp_dir, dvc, local_workspace):
assert stage.deps[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
assert not dst.exists()
assert scm.is_ignored(dst)
out = stage.outs[0]
assert not out.hash_info
@@ -303,6 +305,21 @@ def test_import_url_no_download(tmp_dir, dvc, local_workspace):
]
def test_partial_import_pull(tmp_dir, scm, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
dvc.imp_url("remote://workspace/file", os.fspath(dst), no_download=True)
dvc.pull(["file.dvc"])
assert dst.exists()
stage = Dvcfile(dvc, "file.dvc").stage
assert stage.outs[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
assert stage.outs[0].meta.size == 12
def test_imp_url_with_annotations(M, tmp_dir, dvc, local_workspace):
local_workspace.gen("foo", "foo")
annot = {
+5
View File
@@ -0,0 +1,5 @@
from dvc.testing.workspace_tests import TestLsUrl as _TestLsUrl
class TestLsUrl(_TestLsUrl):
pass
+19 -6
View File
@@ -39,6 +39,22 @@ def _gen(tmp_dir, struct, name):
(None, {"foo": "foo"}, {"bar": "bar"}, {"foo": "foo", "bar": "bar"}),
(None, None, {"bar": "bar"}, {"bar": "bar"}),
(None, {"foo": "foo"}, None, {"foo": "foo"}),
(
{"foo": "foo"},
{"foo": "bar"},
{"foo": "foo", "baz": "baz"},
{"foo": "bar", "baz": "baz"},
),
({"foo": "foo"}, {}, {"foo": "foo", "bar": "bar"}, {"bar": "bar"}),
(
{"common": "common", "subdir": {"foo": "foo", "bar": "bar"}},
{"common": "common", "subdir": {"foo": "foo", "bar": "baz"}},
{"common": "common", "subdir": {"bar": "bar", "bizz": "bizz"}},
{
"common": "common",
"subdir": {"bar": "baz", "bizz": "bizz"},
},
),
],
)
def test_merge(tmp_dir, dvc, ancestor, our, their, merged):
@@ -74,18 +90,15 @@ def test_merge(tmp_dir, dvc, ancestor, our, their, merged):
{"foo": "foo"},
{"foo": "bar"},
{"foo": "baz"},
(
"unable to auto-merge directories with "
"diff that contains 'change'ed files"
),
("unable to auto-merge the following paths:\nfoo"),
),
(
{"common": "common", "foo": "foo"},
{"common": "common", "bar": "bar"},
{"baz": "baz"},
(
"unable to auto-merge directories with "
"diff that contains 'remove'ed files"
"unable to auto-merge the following paths:\n"
"both deleted: ('foo',)"
),
),
],
+17
View File
@@ -265,6 +265,23 @@ def test_collect_repo_ignored_dir_unignored_pattern(tmp_dir, dvc, scm):
assert dvc.stage.collect_repo() == [stage]
@pytest.mark.parametrize("with_deps", (False, True))
def test_collect_symlink(tmp_dir, dvc, with_deps):
tmp_dir.gen({"data": {"foo": "foo contents"}})
foo_path = os.path.join("data", "foo")
dvc.add(foo_path)
data_link = tmp_dir / "data_link"
data_link.symlink_to("data")
stage = list(
dvc.stage.collect(
target=str(data_link / "foo.dvc"), with_deps=with_deps
)
)[0]
assert stage.addressing == f"{foo_path}.dvc"
def test_stage_strings_representation(tmp_dir, dvc, run_copy):
tmp_dir.dvc_gen("foo", "foo")
stage1 = run_copy("foo", "bar", single_stage=True)
+16
View File
@@ -183,6 +183,22 @@ def test_compose_and_dump(tmp_dir, suffix, overrides, expected):
@pytest.mark.parametrize(
"overrides, expected",
[
(
{"params.yaml": ["defaults/foo=1,2"]},
[
{"params.yaml": ["defaults/foo=1"]},
{"params.yaml": ["defaults/foo=2"]},
],
),
(
{"params.yaml": ["+foo=1,2", "~bar", "++foobar=5,6"]},
[
{"params.yaml": ["+foo=1", "~bar=null", "++foobar=5"]},
{"params.yaml": ["+foo=1", "~bar=null", "++foobar=6"]},
{"params.yaml": ["+foo=2", "~bar=null", "++foobar=5"]},
{"params.yaml": ["+foo=2", "~bar=null", "++foobar=6"]},
],
),
(
{"params.yaml": ["foo=1,2", "bar=3,4"]},
[
+83 -23
View File
@@ -11,6 +11,7 @@ from bs4 import BeautifulSoup
from funcy import first
from dvc.cli import main
from dvc.render import REVISION_FIELD, VERSION_FIELD
JSON_OUT = "vis_data"
@@ -60,17 +61,6 @@ def extract_vega_specs(html_path, plots_ids):
return result
def filter_fields(datapoints: List[Dict], fields: List[str]):
tmp = deepcopy(datapoints)
for datapoint in tmp:
keys = set(datapoint.keys())
for key in keys:
if key not in fields:
datapoint.pop(key)
return tmp
def drop_fields(datapoints: List[Dict], fields: List[str]):
tmp = deepcopy(datapoints)
for datapoint in tmp:
@@ -219,10 +209,35 @@ def test_repo_with_plots(
html_result = extract_vega_specs(
html_path, ["linear.json", "confusion.json"]
)
assert json_result["linear.json"][0]["content"]["data"][
"values"
] == _update_datapoints(
linear_v1,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "linear.json",
"field": "y",
},
},
)
assert html_result["linear.json"]["data"]["values"] == _update_datapoints(
linear_v1,
{
"rev": "workspace",
REVISION_FIELD: "workspace",
},
)
assert json_result["confusion.json"][0]["content"]["data"][
"values"
] == _update_datapoints(
confusion_v1,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "confusion.json",
"field": "actual",
},
},
)
assert html_result["confusion.json"]["data"][
@@ -230,10 +245,9 @@ def test_repo_with_plots(
] == _update_datapoints(
confusion_v1,
{
"rev": "workspace",
REVISION_FIELD: "workspace",
},
)
verify_image(
tmp_dir, "workspace", "image.png", image_v1, html_path, json_result
)
@@ -280,17 +294,59 @@ def test_repo_with_plots(
html_path,
["../linear.json", "../confusion.json"],
)
assert json_result["../linear.json"][0]["content"]["data"][
"values"
] == _update_datapoints(
linear_v2,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "../linear.json",
"field": "y",
},
},
) + _update_datapoints(
linear_v1,
{
VERSION_FIELD: {
"revision": "HEAD",
"filename": "../linear.json",
"field": "y",
},
},
)
assert html_result["../linear.json"]["data"][
"values"
] == _update_datapoints(
linear_v2,
{
"rev": "workspace",
REVISION_FIELD: "workspace",
},
) + _update_datapoints(
linear_v1,
{
"rev": "HEAD",
REVISION_FIELD: "HEAD",
},
)
assert json_result["../confusion.json"][0]["content"]["data"][
"values"
] == _update_datapoints(
confusion_v2,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "../confusion.json",
"field": "actual",
},
},
) + _update_datapoints(
confusion_v1,
{
VERSION_FIELD: {
"revision": "HEAD",
"filename": "../confusion.json",
"field": "actual",
},
},
)
assert html_result["../confusion.json"]["data"][
@@ -298,12 +354,12 @@ def test_repo_with_plots(
] == _update_datapoints(
confusion_v2,
{
"rev": "workspace",
REVISION_FIELD: "workspace",
},
) + _update_datapoints(
confusion_v1,
{
"rev": "HEAD",
REVISION_FIELD: "HEAD",
},
)
@@ -390,11 +446,9 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
repo_state = repo_with_config_plots()
plots = next(repo_state)
html_path, json_result, split_json_result = call(capsys)
html_path, _, __ = call(capsys)
assert os.path.exists(html_path)
assert json_result == {}
assert split_json_result == {}
html_result = extract_vega_specs(
html_path,
[
@@ -405,9 +459,15 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
assert html_result["dvc.yaml::linear_train_vs_test"]["data"][
"values"
] == _update_datapoints(
plots["data"]["linear_train.json"], {"rev": "linear_train.json"}
plots["data"]["linear_train.json"],
{
REVISION_FIELD: "linear_train.json",
},
) + _update_datapoints(
plots["data"]["linear_test.json"], {"rev": "linear_test.json"}
plots["data"]["linear_test.json"],
{
REVISION_FIELD: "linear_test.json",
},
)
# TODO check json results once vscode is able to handle flexible plots
-19
View File
@@ -1,19 +0,0 @@
from dvc.cli import main
def test_viztracer(tmp_dir, dvc, mocker):
viztracer_profile = mocker.patch("dvc._debug.viztracer_profile")
assert main(["status", "--viztracer"]) == 0
args = viztracer_profile.call_args[1]
assert callable(args["path"])
assert args["max_stack_depth"] == -1
assert main(["status", "--viztracer", "--viztracer-depth", "5"]) == 0
args = viztracer_profile.call_args[1]
assert args["max_stack_depth"] == 5
assert main(["status", "--viztracer-depth", "2"]) == 0
args = viztracer_profile.call_args[1]
assert args["max_stack_depth"] == 2
+14 -20
View File
@@ -95,6 +95,8 @@ def test_experiments_show(dvc, scm, mocker):
"--all-tags",
"--all-branches",
"--all-commits",
"--hide-queued",
"--hide-failed",
"--sha",
"--param-deps",
"-n",
@@ -115,6 +117,8 @@ def test_experiments_show(dvc, scm, mocker):
all_tags=True,
all_branches=True,
all_commits=True,
hide_queued=True,
hide_failed=True,
num=1,
revs="foo",
sha_only=True,
@@ -433,8 +437,7 @@ def test_show_experiments_csv(capsys):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {
"scores.json": {
@@ -470,8 +473,7 @@ def test_show_experiments_csv(capsys):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {
"scores.json": {
@@ -506,8 +508,7 @@ def test_show_experiments_csv(capsys):
}
}
},
"queued": True,
"running": True,
"status": "Running",
"executor": None,
"metrics": {
"scores.json": {
@@ -561,8 +562,7 @@ def test_show_experiments_md(capsys):
"data": {
"timestamp": None,
"params": {"params.yaml": {"data": {"foo": 1}}},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {
"scores.json": {"data": {"bar": 0.9544670443829399}}
@@ -599,8 +599,7 @@ def test_show_experiments_sort_by(capsys, sort_order):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {},
}
@@ -617,8 +616,7 @@ def test_show_experiments_sort_by(capsys, sort_order):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {},
"name": "master",
@@ -634,8 +632,7 @@ def test_show_experiments_sort_by(capsys, sort_order):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {},
"name": "exp-89140",
@@ -651,8 +648,7 @@ def test_show_experiments_sort_by(capsys, sort_order):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {},
"name": "exp-43537",
@@ -668,8 +664,7 @@ def test_show_experiments_sort_by(capsys, sort_order):
}
}
},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {},
"name": "exp-4f89e",
@@ -915,8 +910,7 @@ def test_show_experiments_pcp(tmp_dir, mocker):
"data": {
"timestamp": None,
"params": {"params.yaml": {"data": {"foo": 1}}},
"queued": False,
"running": False,
"status": "Success",
"executor": None,
"metrics": {
"scores.json": {"data": {"bar": 0.9544670443829399}}
+3
View File
@@ -41,6 +41,7 @@ def test_import_url(mocker):
labels=None,
meta=None,
jobs=4,
version_aware=False,
)
@@ -98,6 +99,7 @@ def test_import_url_no_exec_download_flags(mocker, flag, expected):
type=None,
labels=None,
meta=None,
version_aware=False,
**expected
)
@@ -135,6 +137,7 @@ def test_import_url_to_remote(mocker):
labels=None,
meta=None,
jobs=None,
version_aware=False,
)
+24
View File
@@ -0,0 +1,24 @@
from dvc.cli import parse_args
from dvc.commands.ls_url import CmdListUrl
def test_ls_url(mocker):
cli_args = parse_args(["ls-url", "src"])
assert cli_args.func == CmdListUrl
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.Repo.ls_url", autospec=True)
assert cmd.run() == 0
m.assert_called_once_with("src", recursive=False)
def test_recursive(mocker):
cli_args = parse_args(["ls-url", "-R", "src"])
assert cli_args.func == CmdListUrl
cmd = cli_args.func(cli_args)
m = mocker.patch("dvc.repo.Repo.ls_url", autospec=True)
assert cmd.run() == 0
m.assert_called_once_with("src", recursive=True)
-4
View File
@@ -334,10 +334,6 @@ def test_plots_diff_json(dvc, mocker, capsys):
)
render_mock = mocker.patch("dvc_render.render_html")
show_json_mock = mocker.patch(
"dvc.commands.plots._filter_unhandled_renderers",
return_value=renderers,
)
show_json_mock = mocker.patch("dvc.commands.plots._show_json")
assert cmd.run() == 0
+2 -2
View File
@@ -8,7 +8,7 @@ def test_run(mocker, dvc):
[
"run",
"--name",
"nam",
"name",
"--deps",
"deps",
"--outs",
@@ -86,7 +86,7 @@ def test_run(mocker, dvc):
no_commit=True,
always_changed=True,
cmd="command",
name="nam",
name="name",
single_stage=False,
external=True,
desc="description",
+38 -2
View File
@@ -1,10 +1,12 @@
from dataclasses import dataclass
import pytest
from dvc_http import HTTPFileSystem, HTTPSFileSystem
from dvc_s3 import S3FileSystem
from dvc_ssh import SSHFileSystem
from dvc.config import RemoteNotFoundError
from dvc.fs import LocalFileSystem, get_fs_cls, get_fs_config
from dvc.fs import LocalFileSystem, get_cloud_fs, get_fs_cls, get_fs_config
url_cls_pairs = [
("s3://bucket/path", S3FileSystem),
@@ -36,5 +38,39 @@ def test_get_fs_cls(url, cls):
def test_get_fs_config():
result = get_fs_config({}, url="ssh://example.com:/dir/path")
assert result == {"url": "ssh://example.com:/dir/path"}
def test_get_fs_config_error():
with pytest.raises(RemoteNotFoundError):
get_fs_config(None, {"remote": {}}, name="myremote")
get_fs_config({"remote": {}}, name="myremote")
def test_remote_url():
config = {
"remote": {
"base": {"url": "http://example.com"},
"r1": {"url": "remote://base/r1", "user": "user"},
"r2": {"url": "remote://r1/r2", "password": "123"},
}
}
result = get_fs_config(config, url="remote://r2/foo")
assert result == {
"password": "123",
"user": "user",
"url": "http://example.com/r1/r2/foo",
}
@dataclass
class FakeRepo:
config: dict
def test_get_cloud_fs():
repo = FakeRepo({})
cls, config, path = get_cloud_fs(repo, url="ssh://example.com:/dir/path")
assert cls is SSHFileSystem
assert config == {"host": "example.com", "verify": False}
assert path == "/dir/path"
-184
View File
@@ -1,184 +0,0 @@
# pylint: disable=W0613
import getpass
import os
from unittest.mock import mock_open, patch
import pytest
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
def test_get_kwargs_from_urls():
user = "test"
host = "123.45.67.89"
port = 1234
path = "/path/to/dir"
# URL ssh://[user@]host.xz[:port]/path
url = f"ssh://{user}@{host}:{port}{path}"
assert SSHFileSystem._get_kwargs_from_urls(url) == {
"username": user,
"host": host,
"port": port,
}
# SCP-like URL ssh://[user@]host.xz:/absolute/path
url = f"ssh://{user}@{host}:{path}"
assert SSHFileSystem._get_kwargs_from_urls(url) == {
"username": user,
"host": host,
}
def test_init():
fs = SSHFileSystem(
user="test", host="12.34.56.78", port="1234", password="xxx"
)
assert fs.fs_args["username"] == "test"
assert fs.fs_args["host"] == "12.34.56.78"
assert fs.fs_args["port"] == "1234"
assert fs.fs_args["password"] == fs.fs_args["passphrase"] == "xxx"
def test_ssh_ask_password(mocker):
mocker.patch("dvc_ssh.ask_password", return_value="fish")
fs = SSHFileSystem(user="test", host="2.2.2.2", ask_password=True)
assert fs.fs_args["password"] == fs.fs_args["passphrase"] == "fish"
mock_ssh_config = """
Host example.com
User ubuntu
HostName 1.2.3.4
Port 1234
IdentityFile ~/.ssh/not_default.key
"""
@pytest.mark.parametrize(
"config,expected_host",
[
({"host": "example.com"}, "1.2.3.4"),
({"host": "not_in_ssh_config.com"}, "not_in_ssh_config.com"),
],
)
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_config,
)
def test_ssh_host_override_from_config(mock_file, config, expected_host):
fs = SSHFileSystem(**config)
assert fs.fs_args["host"] == expected_host
@pytest.mark.parametrize(
"config,expected_user",
[
({"host": "example.com", "user": "test1"}, "test1"),
({"host": "example.com"}, "ubuntu"),
({"host": "not_in_ssh_config.com", "user": "test1"}, "test1"),
({"host": "not_in_ssh_config.com"}, getpass.getuser()),
],
)
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_config,
)
def test_ssh_user(mock_file, config, expected_user):
fs = SSHFileSystem(**config)
assert fs.fs_args["username"] == expected_user
@pytest.mark.parametrize(
"config,expected_port",
[
({"host": "example.com"}, 1234),
({"host": "example.com", "port": 4321}, 4321),
({"host": "not_in_ssh_config.com"}, DEFAULT_PORT),
({"host": "not_in_ssh_config.com", "port": 2222}, 2222),
],
)
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_config,
)
def test_ssh_port(mock_file, config, expected_port):
fs = SSHFileSystem(**config)
assert fs.fs_args["port"] == expected_port
@pytest.mark.parametrize(
"config,expected_keyfile",
[
(
{"host": "example.com", "keyfile": "dvc_config.key"},
["dvc_config.key"],
),
(
{"host": "example.com"},
["~/.ssh/not_default.key"],
),
(
{
"host": "not_in_ssh_config.com",
"keyfile": "dvc_config.key",
},
["dvc_config.key"],
),
({"host": "not_in_ssh_config.com"}, None),
],
)
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_config,
)
def test_ssh_keyfile(mock_file, config, expected_keyfile):
fs = SSHFileSystem(**config)
expected_keyfiles = (
[os.path.expanduser(path) for path in expected_keyfile]
if expected_keyfile
else expected_keyfile
)
assert fs.fs_args.get("client_keys") == expected_keyfiles
mock_ssh_multi_key_config = """
IdentityFile file_1
Host example.com
IdentityFile file_2
"""
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_multi_key_config,
)
def test_ssh_multi_identity_files(mock_file):
fs = SSHFileSystem(host="example.com")
assert fs.fs_args.get("client_keys") == ["file_1", "file_2"]
@pytest.mark.parametrize(
"config,expected_gss_auth",
[
({"host": "example.com", "gss_auth": True}, True),
({"host": "example.com", "gss_auth": False}, False),
({"host": "not_in_ssh_config.com"}, False),
],
)
@patch(
"builtins.open",
new_callable=mock_open,
read_data=mock_ssh_config,
)
def test_ssh_gss_auth(mock_file, config, expected_gss_auth):
fs = SSHFileSystem(**config)
assert fs.fs_args["gss_auth"] == expected_gss_auth
+59 -9
View File
@@ -1,4 +1,10 @@
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY
from dvc.render import (
REVISION_FIELD,
REVISIONS_KEY,
SRC_FIELD,
TYPE_KEY,
VERSION_FIELD,
)
from dvc.render.convert import to_json
@@ -7,8 +13,18 @@ def test_to_json_vega(mocker):
vega_renderer.TYPE = "vega"
vega_renderer.get_filled_template.return_value = '{"this": "is vega"}'
vega_renderer.datapoints = [
{"x": 1, "y": 2, REVISION_FIELD: "foo", "filename": "foo.json"},
{"x": 2, "y": 1, REVISION_FIELD: "bar", "filename": "foo.json"},
{
"x": 1,
"y": 2,
VERSION_FIELD: {"revision": "foo"},
"filename": "foo.json",
},
{
"x": 2,
"y": 1,
VERSION_FIELD: {"revision": "bar"},
"filename": "foo.json",
},
]
result = to_json(vega_renderer)
assert result[0] == {
@@ -17,10 +33,20 @@ def test_to_json_vega(mocker):
"content": {"this": "is vega"},
"datapoints": {
"foo": [
{"x": 1, "y": 2, "filename": "foo.json"},
{
"x": 1,
"y": 2,
"filename": "foo.json",
VERSION_FIELD: {"revision": "foo"},
},
],
"bar": [
{"x": 2, "y": 1, "filename": "foo.json"},
{
"x": 2,
"y": 1,
"filename": "foo.json",
VERSION_FIELD: {"revision": "bar"},
},
],
},
}
@@ -34,8 +60,18 @@ def test_to_json_vega_split(mocker):
'{"this": "is split vega"}'
)
vega_renderer.datapoints = [
{"x": 1, "y": 2, REVISION_FIELD: "foo", "filename": "foo.json"},
{"x": 2, "y": 1, REVISION_FIELD: "bar", "filename": "foo.json"},
{
"x": 1,
"y": 2,
VERSION_FIELD: {"revision": "foo"},
"filename": "foo.json",
},
{
"x": 2,
"y": 1,
VERSION_FIELD: {"revision": "bar"},
"filename": "foo.json",
},
]
result = to_json(vega_renderer, split=True)
assert result[0] == {
@@ -43,8 +79,22 @@ def test_to_json_vega_split(mocker):
REVISIONS_KEY: ["bar", "foo"],
"content": {"this": "is split vega"},
"datapoints": {
"foo": [{"x": 1, "y": 2, "filename": "foo.json"}],
"bar": [{"x": 2, "y": 1, "filename": "foo.json"}],
"foo": [
{
"x": 1,
"y": 2,
"filename": "foo.json",
VERSION_FIELD: {"revision": "foo"},
}
],
"bar": [
{
"x": 2,
"y": 1,
"filename": "foo.json",
VERSION_FIELD: {"revision": "bar"},
}
],
},
}
vega_renderer.get_filled_template.assert_called_with(skip_anchors=["data"])
+10 -4
View File
@@ -89,14 +89,20 @@ def test_match_renderers(mocker):
assert len(renderers) == 1
assert renderers[0].datapoints == [
{
VERSION_FIELD: {"revision": "v1", "filename": "file.json"},
"rev": "v1",
VERSION_FIELD: {
"revision": "v1",
"filename": "file.json",
"field": "y",
},
"x": 1,
"y": 1,
},
{
VERSION_FIELD: {"revision": "v1", "filename": "file.json"},
"rev": "v1",
VERSION_FIELD: {
"revision": "v1",
"filename": "file.json",
"field": "y",
},
"x": 2,
"y": 2,
},
+33 -34
View File
@@ -57,42 +57,53 @@ def test_finding_lists(dictionary, expected_result):
@pytest.mark.parametrize(
"input_data,properties,expected_datapoints,expected_properties",
[
(
# default x and y
pytest.param(
{"metric": [{"v": 1}, {"v": 2}]},
{},
[
{
"v": 1,
"step": 0,
VERSION_FIELD: {"revision": "r", "filename": "f"},
"rev": "r",
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
},
{
"v": 2,
"step": 1,
VERSION_FIELD: {"revision": "r", "filename": "f"},
"rev": "r",
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
},
],
{"x": "step", "y": "v"},
id="default_x_y",
),
(
# filter fields
pytest.param(
{"metric": [{"v": 1, "v2": 0.1}, {"v": 2, "v2": 0.2}]},
{"fields": {"v"}},
[
{
"v": 1,
"step": 0,
VERSION_FIELD: {"revision": "r", "filename": "f"},
"rev": "r",
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
},
{
"v": 2,
"step": 1,
VERSION_FIELD: {"revision": "r", "filename": "f"},
"rev": "r",
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
},
],
{
@@ -100,9 +111,9 @@ def test_finding_lists(dictionary, expected_result):
"y": "v",
"fields": {"v", "step"},
},
id="filter_fields",
),
(
# choose x and y
pytest.param(
{"metric": [{"v": 1, "v2": 0.1}, {"v": 2, "v2": 0.2}]},
{"x": "v", "y": "v2"},
[
@@ -114,7 +125,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
},
{
"v": 2,
@@ -124,13 +134,12 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
},
],
{"x": "v", "y": "v2"},
id="choose_x_y",
),
(
# append x and y to filtered fields
pytest.param(
{
"metric": [
{"v": 1, "v2": 0.1, "v3": 0.01, "v4": 0.001},
@@ -148,7 +157,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v4",
},
"rev": "r",
},
{
"v": 2,
@@ -159,13 +167,12 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v4",
},
"rev": "r",
},
],
{"x": "v3", "y": "v4", "fields": {"v", "v3", "v4"}},
id="append_x_y_to_fields",
),
(
# find metric in nested structure
pytest.param(
{
"some": "noise",
"very": {
@@ -184,7 +191,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
},
{
"v": 2,
@@ -194,13 +200,12 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
},
],
{"x": "v", "y": "v2"},
id="find_in_nested_structure",
),
(
# provide list of fields in y def
pytest.param(
{"metric": [{"v": 1, "v2": 0.1}, {"v": 2, "v2": 0.2}]},
{"y": {"f": ["v", "v2"]}},
[
@@ -212,7 +217,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v",
},
"rev": "r",
"dvc_inferred_y_value": 1,
"step": 0,
},
@@ -224,7 +228,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
"dvc_inferred_y_value": 0.1,
"step": 0,
},
@@ -236,7 +239,6 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v",
},
"rev": "r",
"dvc_inferred_y_value": 2,
"step": 1,
},
@@ -248,12 +250,12 @@ def test_finding_lists(dictionary, expected_result):
"filename": "f",
"field": "v2",
},
"rev": "r",
"dvc_inferred_y_value": 0.2,
"step": 1,
},
],
{"x": "step", "y": "dvc_inferred_y_value", "y_label": "y"},
id="y_def_list",
),
],
)
@@ -275,6 +277,7 @@ def test_convert(
def test_convert_skip_step():
converter = VegaConverter()
converter.skip_step("append_index")
converter.skip_step("generate_y")
datapoints, resolved_properties = converter.convert(
data={"a": "b", "metric": [{"v": 1}, {"v": 2}]},
@@ -285,13 +288,9 @@ def test_convert_skip_step():
assert datapoints == [
{
"v": 1,
"rev": "r",
VERSION_FIELD: {"revision": "r", "filename": "f"},
},
{
"v": 2,
"rev": "r",
VERSION_FIELD: {"revision": "r", "filename": "f"},
},
]
assert resolved_properties == {"x": "step", "y": "v"}

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