Compare commits

...
265 Commits
Author SHA1 Message Date
Saugat Pachhai (सौगात) 334f297ad3 deps: remove 3.11 checks for hydra; has 3.11 support now 2022-12-09 10:32:12 +05:45
Peter Rowlands ace3ba0ef9 worktree push: disable rev flags 2022-12-09 09:21:19 +09:00
Peter Rowlands 959d149381 worktree fetch: support revs flags 2022-12-09 09:21:19 +09:00
Peter Rowlands 7a65d67e7f worktree: support recursive/with_deps targets 2022-12-09 09:21:19 +09:00
Peter Rowlands e9be6779ea worktree: disable run-cache push/fetch 2022-12-09 09:21:19 +09:00
Peter Rowlands 876d123f04 index: use lazy view loading 2022-12-08 20:56:05 +09:00
Peter Rowlands 2bfe403a28 deps: bump dvc-data to 0.28.4 2022-12-08 20:56:05 +09:00
Peter Rowlands 9bef123182 worktree: fix default worktree remote/odb exception 2022-12-08 11:01:42 +09:00
Peter Rowlands e98ca36f39 worktree push: only index known (tracked) files/dirs in the remote 2022-12-07 18:48:34 +09:00
Peter Rowlands b9bf73f043 push/fetch: support targets in cloud version push/fetch 2022-12-07 18:48:34 +09:00
dependabot[bot] 7628fe9d44 build(deps-dev): Bump pytest-xdist from 3.0.2 to 3.1.0
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.0.2 to 3.1.0.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.0.2...v3.1.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-12-06 20:16:21 +05:45
Peter Rowlands d415a05460 remote: disable gc and status -c for versioned remotes 2022-12-06 19:33:52 +09:00
Peter Rowlands f5699ce16b update: disable rev/to-remote/no-download for worktrees 2022-12-06 15:14:17 +09:00
Peter Rowlands baeedead3a update: add support for worktree update 2022-12-06 15:14:17 +09:00
Peter Rowlands 5630db55ad worktree: add support for updating outs 2022-12-06 15:14:17 +09:00
Peter Rowlands f1bd47ce0e index: make collect_targets public 2022-12-06 15:14:17 +09:00
dependabot[bot] 74a13a4d9a build(deps-dev): Bump dvc-render from 0.0.14 to 0.0.15
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.14 to 0.0.15.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.14...0.0.15)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-12-02 08:30:37 +01:00
dberenbaum b7732a591d stage add: don't fail if unable to create .gitignore 2022-12-01 16:43:31 +01:00
Saugat Pachhai (सौगात) d57bb23c93 fix windows path handling 2022-12-01 15:56:26 +01:00
Saugat Pachhai (सौगात) 28402dedf1 add tests 2022-12-01 15:56:26 +01:00
Saugat Pachhai (सौगात) df60f5d9bf collect top-level params/plots/metrics together with stages 2022-12-01 15:56:26 +01:00
Saugat Pachhai (सौगात) 7ea73b2596 wip: top level params and metrics 2022-12-01 15:56:26 +01:00
Peter Rowlands aa2e830511 deps: bump dvc-data to 0.28.3 2022-12-01 16:02:59 +09:00
dependabot[bot] 6fdd6673f3 build(deps-dev): Bump pylint from 2.15.5 to 2.15.7
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.5 to 2.15.7.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.5...v2.15.7)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-30 16:33:01 +01:00
dberenbaum fac5f18dea plots: add tests for errors in vega converter 2022-11-29 16:08:29 +01:00
dberenbaum d8068430e7 plots: fix multi-file plots 2022-11-29 16:08:29 +01:00
Peter Rowlands 25b1ef9971 worktree push: separate worktree from version_aware 2022-11-29 22:45:16 +09:00
Peter Rowlands df563e43c1 remote: make worktree imply version_aware 2022-11-29 22:45:16 +09:00
karajan1001 3ba6b1f0bf Solve the unexpected error at the end of the queued tasks running
fix: #8615
1. This is because, at the end of the tasks,
the temp running directly will be deleted

1. Only fetch result during running (not during the result collection)
2. Bump into scmrepo 0.1.4 in which we wrapped `KeyError` into
   `SCMError`.
2022-11-29 15:24:19 +08:00
Peter Rowlands 6a9fbaf3d2 deps: bump dvc-data to 0.28.2 2022-11-29 14:37:11 +09:00
David de la Iglesia Castro a036eaf817 exp: save: Add include_untracked.
Allow to include a list of potentially untracked files.
Covers the DVCLive use case where in the first run the generated files won't be tracked in Git yet.
2022-11-28 15:49:34 +01:00
David de la Iglesia Castro 665185b9e7 exp: save: Handle staged changes.
Staged changes were causing a merge error.
Warn and unstage instead of erroring, to match exp run behavior.
2022-11-28 15:49:34 +01:00
David de la Iglesia Castro 43e281521e exp save: cleanup.
Remove metrics.

Move tests to separate file.

Fix `experiments.get_exact_name` usage.

Remove unused checkpoint logic.
2022-11-28 15:49:34 +01:00
Daniele Trifirò 73b46a5789 exp save: initial implementation 2022-11-28 15:49:34 +01:00
David de la Iglesia Castro 8124396218 exp: Expose baseline and name via run_env.
Required by DVCLive for the experiments onboarding and live metrics features.

Related (not sure if closes): https://github.com/iterative/dvc/issues/6668
2022-11-28 13:13:04 +01:00
Peter Rowlands e95ae5613d index: skip data index load on empty view 2022-11-28 18:49:32 +09:00
karajan1001 ca4dd08e37 Use temp branch name to avoid locking the exec branches in temp dir executor
fix: #8448
In our current temp dir executor, we use will first set EXEC_* branches
to revisions retrieved from stash entry. And push them to remote, but
because the push operation is very slow, our locking operation will make
the whole repository unusable.

1. Now we set revisions from stash entry to some temp branches and then
   push them to remote.
2022-11-28 17:04:16 +08:00
karajan1001 0ebc253a43 rwlock auto solve corrupted lockfile 2022-11-28 17:04:16 +08:00
karajan1001 6bfce5a5db Use a more granular lock on exp operations.
For now in `experiment.new`, `init_executor` and `collect_exectuor` we
lock the whole scm repo. We can make a more granular lock in these
operations.
2022-11-28 17:04:16 +08:00
karajan1001 5fa33a06bc Move EXEC_TMP_DIR and EXEC_PID_DIR to experiment.utils 2022-11-28 17:04:16 +08:00
Peter Rowlands 6a75f3c327 deps: bump dvc-data to 0.28.1 2022-11-28 16:10:31 +09:00
Ivan Shcheklein d7587556c9 exp list: cleanup and move logic inside repo api 2022-11-25 16:15:17 -08:00
skshetry bb80eab77a deps: add support for hdfs in Python 3.11 2022-11-25 11:16:42 +05:45
Ruslan Kuprieiev ab3f8fb1a4 Create hook-asyncssh.py
Fixes #7577
2022-11-24 23:25:00 +02:00
Yury 6edca79b79 repro dry: show information if the stage is cached
Fixes: #8342
2022-11-24 21:49:52 +02:00
Ruslan Kuprieiev 2a888970e8 build: fpm: don't double quote cli option
Leftover from #8611
2022-11-24 01:34:18 +02:00
Ruslan Kuprieiev 73fdfc8059 build: fpm: don't create .build-id/* files
These are leaking into our rpm packages.

Related https://github.com/iterative/dvc-s3-repo/issues/86
2022-11-24 01:02:44 +02:00
Ruslan Kuprieiev 8dc11417d2 testing: api: test opening a file in subdir
Related to #8420
2022-11-23 17:47:13 +02:00
b02855fb93 ui: Fix WSL check in open_browser (#8604)
* ui: Fix WSL check in `open_browser`

* Update dvc/ui/__init__.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

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

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

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-23 09:57:57 +00:00
Peter Rowlands af255a46b4 worktree: use progress bars in push/fetch 2022-11-23 13:05:51 +09:00
Peter Rowlands 2748492f6b worktree push: don't push existing versions 2022-11-23 13:05:51 +09:00
Peter Rowlands c69d3188a6 deps: bump dvc-data to 0.28.0 2022-11-22 19:04:18 +09:00
karajan1001 9b9c04e633 Solve the problem during executor running
fix #7693

The problem was when we gathered experiments, and at the same time,
the executor are creating new commits. It will make the checkpoint
top/head return by `get_running_exps`/`get_ref`/`branch_revs` not the
same one. And cause the problem described in #7693

To solve this we did
1. move the `get_rev` to the beginning of `exp show` to make sure the
   return results in `branch_revs`
2. Add a new function to move all of the properties that only belong to
   `head` of the checkpoint from the wrong place to the top.
3. Add a new unit test for it.
4. Force fetch_running_exp to return data.
5. fix a bug in tmp_dir_executor.
2022-11-22 17:44:55 +08:00
karajan1001 350e0fb1f7 queue kill: we can manually mark problematic tasks as failure
fix: #8461
fix: #8473
For now, for those inactive but uncompleted tasks, they still shown as
Running in `exp show` and `task queue`, we cannot remove them because
they are regarded as running, we cann't kill them because their are no
real process behind them.

1. Now we can examine these false running tasks and mark them as failure
   in `queue kill` command. #8461
2. Better msg handle for initliaizing tasks in `queue kill`#8473
3. Add new unit tests for both cases.
4. bump into new dvc-task 0.1.6
2022-11-22 13:33:23 +08:00
dberenbaum f14358bc72 plots: add test for default x label 2022-11-21 21:05:12 +01:00
David de la Iglesia Castro 705bc6ec82 Added test_infer_x_label 2022-11-21 21:05:12 +01:00
dberenbaum 61a02e6f56 plots: set default x label 2022-11-21 21:05:12 +01:00
Saugat Pachhai (सौगात) 8e5226ece0 cli: remove version action 2022-11-21 20:37:05 +05:45
Peter Rowlands 2a1af0b880 add: merge existing file meta for versioned dirs 2022-11-21 19:18:41 +09:00
Peter Rowlands 014d149512 out: move restore_fields logic into Output 2022-11-21 19:18:41 +09:00
Peter Rowlands 77ae4aa92e deps: bump dvc-azure to 2.20.5 2022-11-21 18:21:02 +09:00
Peter Rowlands 588471a811 deps: bump dvc-data to 0.27.0 2022-11-21 18:21:02 +09:00
dependabot[bot] e59c19e752 build(deps-dev): Bump dvc-render from 0.0.13 to 0.0.14
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.13 to 0.0.14.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.13...0.0.14)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-21 10:08:09 +01:00
David de la Iglesia CastroandDave Berenbaum b0a92414fa hydra: Use OmegaConf.to_yaml for dumping .yaml output. (#8587)
* hydra: Use `OmegaConf.to_yaml` for dumping `.yaml` output.

Combining `OmegaConf.to_object` with our yaml DUMPER (ruaml.yaml) was not correctly handling strings like 'no'.

`OmegaConf.to_yaml` uses a custom string representer that correctly handles those cases.

Fixes #8583

* Update dvc/utils/hydra.py

Co-authored-by: Dave Berenbaum <[email protected]>

Co-authored-by: Dave Berenbaum <[email protected]>
2022-11-19 12:37:18 +00:00
Peter Rowlands dae500d7c7 worktree: use view instead of full index in push/fetch 2022-11-19 09:55:58 +09:00
Peter Rowlands a01bc2918e push/fetch: move worktree logic into repo.worktree 2022-11-19 09:55:58 +09:00
David de la Iglesia Castro a33bbc97d5 parsing: Escape str interpolation in dict unpacking.
Use subprocess.list2cmdline (windows) / shlex.quote

Closes #8203
Closes #8370
2022-11-18 20:00:16 +01:00
Paweł Redzyński ba2e8bdac7 plots: data conversion: adjust for viewer backend (#8421)
* plots: vega: converter: support multi series data split and props inference

Fixes: #7754, #8399
Related: iterative/studio#4369

* plots: converter: make converter infer x and y labels if not provided

* plots: vega: converter: roll back to datapoints format

* plots: vega: converter: refactor

* plots: infer labels on

* plots: get_plot_id: fix naming

* plots: vega: converter: notes regarding converting
2022-11-18 12:36:00 +00:00
Peter Rowlands 4d6e045989 status -c: exclude push: false outs 2022-11-18 16:32:29 +09:00
Peter Rowlands 00eea4560e import-url: default to push: false for cloud-versioned imports 2022-11-18 16:32:29 +09:00
Peter Rowlands 6e02dddabd output: check push in get_used_objs 2022-11-18 16:32:29 +09:00
Peter Rowlands b550c5e625 output: add PARAM_PUSH 2022-11-18 16:32:29 +09:00
Ruslan KuprieievandPeter Rowlands b36861ada9 tests: import: use odb.clear()
Co-authored-by: Peter Rowlands <[email protected]>
2022-11-17 10:56:01 +02:00
Ruslan Kuprieiev d482e26c99 deps: bump dvc-data to 0.26.0 2022-11-17 10:56:01 +02:00
dependabot[bot] a8ad59ae4e build(deps-dev): Bump dvc-render from 0.0.12 to 0.0.13
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.12 to 0.0.13.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.12...0.0.13)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-16 11:13:05 +01:00
Ivan Shcheklein 76cc649f5e fix/8550+: cover with test 2022-11-16 15:16:37 +05:45
Ivan Shcheklein d52e1aa17b fix: exp list ref heads handling 2022-11-16 15:16:37 +05:45
Saugat Pachhai (सौगात) 65f61acffa tests: garbage collect sqlite connections on Windows + Python 3.11
There seems to be a regression in Python 3.11, where the sqlite
connections are not deallocated, due to some internal changes in
Python 3.11, where they are now using LRU cache. They are not deallocated
until `gc.collect()` is not called.

See https://github.com/python/cpython/issues/97641.
This affects only Windows, because when we try to remove the tempdir for
the exp run, the sqlite connection is open which prevents us from
deleting that folder.

Although this may happen in real scenario in `exp run`, I am only fixing
the tests by mocking `dvc.close()` and extending it by calling
`gc.collect()` after it. We could also mock `State.close()` but didnot
want to mock something that is not in dvc itself.

The `diskcache` uses threadlocal for connections, so they are expected
to be garbage collected, and therefore does not provide a good way to
close the connections. The only API it offers is `self.close()` and that
only closes main thread's connection. If we had access to connection,
an easier way would have been to explicitly call `conn.close()`.
But we don''t have such option at the moment.

Related: https://github.com/iterative/dvc/pull/8404#issuecomment-1310024661
GHA Failure: https://github.com/iterative/dvc/actions/runs/3437324559/jobs/5731929385#step:5:57
2022-11-16 08:02:43 +05:45
Peter Rowlands fdc689954f pyinstaller: drop google-api-client hooks 2022-11-15 22:07:05 +09:00
Peter Rowlands 076c48532d deps: bump dvc-gdrive to 2.19.1 2022-11-15 22:07:05 +09:00
David de la Iglesia Castro e4ea463c73 hydra: Raise error when name and sweeps. 2022-11-14 21:34:55 +01:00
Saugat Pachhai (सौगात) 87d8269365 typecheck: fix type error in _git_info 2022-11-14 10:21:31 +05:45
Peter Rowlands a753ee215f output: fix get_entry dep object generation for imports 2022-11-13 14:41:21 +02:00
dependabot[bot] 014a38fae2 build(deps-dev): Bump iterative-telemetry from 0.0.5 to 0.0.6
Bumps [iterative-telemetry](https://github.com/iterative/telemetry-python) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/iterative/telemetry-python/releases)
- [Commits](https://github.com/iterative/telemetry-python/compare/0.0.5...0.0.6)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-11 14:54:07 +09:00
Peter Rowlands 8913f5a6fe mypy: allow implicit-optional 2022-11-11 14:17:17 +09:00
dependabot[bot] 4ce9825c2b build(deps-dev): Bump mypy from 0.982 to 0.990
Bumps [mypy](https://github.com/python/mypy) from 0.982 to 0.990.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.982...v0.990)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-11 14:17:17 +09:00
dependabot[bot] e9c5b5fa84 build(deps-dev): Bump dvc-gs from 2.19.1 to 2.20.0
Bumps [dvc-gs](https://github.com/iterative/dvc-gs) from 2.19.1 to 2.20.0.
- [Release notes](https://github.com/iterative/dvc-gs/releases)
- [Commits](https://github.com/iterative/dvc-gs/compare/2.19.1...2.20.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-11 13:33:10 +09:00
karajan1001 fa54c1af7c Solve run_all flaky tests
1. Modify run all to include currently running exps.
2. bump dvc-task to 0.1.5
2022-11-11 07:04:43 +05:45
karajan1001 9fbbfd07a6 Fix some celery queue realted ci failure
fix: #8403
1. remove some of the flaky mark
2. In `get_result` make sure the celery task is completed.
2022-11-11 07:04:43 +05:45
karajan1001 929de7c651 Solve the crash on getting name of applied experiment branch
fix: #8526
1. The current `get_names` will crash if an experiment branch had
   already been given a new branch name to it.

1. Solve the crash when we getting the name of a newly fixed experiment
   branch.
2. Add a new unit test to it.
2022-11-10 13:18:45 +08:00
blakeNaccarato 49af6e1435 plots: support svg
Add SVG to parser and parametrize an existing functionality test on SVG as well as JPG.

Fixes #6714.
2022-11-09 19:23:49 +02:00
skshetry a6bfc7db4e ci: test with 3.11 on all OSes (#8543)
* ci: test with 3.11 on all OSes

* Update tests.yaml

* Update tests.yaml
2022-11-09 12:05:27 +00:00
Yury 872a0d663e dvc exceptions CyclicGraphError: add more clear message for the exception
Fixes: #7915
2022-11-09 14:33:08 +05:45
dependabot[bot] c01583f3dd build(deps-dev): Bump dvc-task from 0.1.4 to 0.1.5
Bumps [dvc-task](https://github.com/iterative/dvc-task) from 0.1.4 to 0.1.5.
- [Release notes](https://github.com/iterative/dvc-task/releases)
- [Commits](https://github.com/iterative/dvc-task/compare/0.1.4...0.1.5)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-09 16:37:14 +09:00
karajan1001 e0d41c7012 Make exp show handle errors better
fix: #8350

We shouldn't add locks to exp show to prevent race conditions
as this command is calling continuously by the extension.
So we make a better way to handle all kinds of reference failures.

1. Drop all based experiments if got a SCMError in baseline.
2. Drop all checkpoint experiments if any commit got a SCMError in
   one of checkpoint commit.
3. Drop the entire experiments if got a SCMError.
4. Add tests for each of the conditions above.
2022-11-08 20:12:22 +08:00
Peter Rowlands 628b992442 index: fix granular dir output view filtering 2022-11-08 19:28:10 +09:00
Peter Rowlands 99e7f52bbd index: support filtering by output in views 2022-11-08 19:28:10 +09:00
skshetry b2a275ee9e Create codeql.yml 2022-11-08 13:21:20 +05:45
skshetry 7e423b668f pkging: also post released packages to test.pypi.org 2022-11-07 18:57:53 +05:45
Saugat Pachhai (सौगात) 2c3956bdb0 exp init: fix live metrics path 2022-11-07 12:45:32 +01:00
Saugat Pachhai (सौगात) a48a19ce31 tests: ignore versions for subprojects 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro 1de983f0de Remove live section support. 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro eb20a8e049 deps: Bump dvclive to >= 1.0 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro 78e25747a4 exp init: Update DVCLive paths.
Closes #8531
2022-11-07 12:45:32 +01:00
dberenbaum 240f0ebdc3 fix hydra_sweeps referenced before assignment 2022-11-04 15:59:26 +01:00
skshetry 69f78d3bb6 ci: bench: use 3.11 in benchmarks 2022-11-04 18:43:02 +05:45
skshetry 740fe504c9 Update README.rst 2022-11-04 18:27:50 +05:45
skshetry 7bb84286f5 readme: show downloads badge 2022-11-04 18:27:50 +05:45
Peter Rowlands 3891e81bdd index: load dir entries before generating target views 2022-11-04 19:09:48 +09:00
Peter Rowlands bb62833897 index: move import specific entry building out of index.data() property 2022-11-04 19:09:48 +09:00
Peter Rowlands fb703150ce output: load precomputed md5 from PARAM_FILES for non-local fs 2022-11-04 19:09:48 +09:00
Peter Rowlands 33d4ead8b6 import-url: save files for version-aware dir 2022-11-04 19:09:48 +09:00
Peter Rowlands 76b1f8c907 deps: bump dvc-data to 0.25.3 2022-11-04 19:09:48 +09:00
Paweł Redzyński d7152e360e Update dvc/repo/plots/__init__.py 2022-11-04 09:54:45 +05:45
dberenbaum ab69b88a21 plots: allow top-level strings 2022-11-04 09:54:45 +05:45
dependabot[bot] 43a8eab2e0 build(deps-dev): Bump dvc-s3 from 2.20.1 to 2.21.0
Bumps [dvc-s3](https://github.com/iterative/dvc-s3) from 2.20.1 to 2.21.0.
- [Release notes](https://github.com/iterative/dvc-s3/releases)
- [Commits](https://github.com/iterative/dvc-s3/compare/2.20.1...2.21.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2022-11-04 08:09:29 +05:45
Saugat Pachhai (सौगात) 15f35e827e fix windows 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) 4d17c171f1 tests: use mocker 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) c27005a8b7 migrate test_{repro,run}*.py to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) ecd371f8ae migrate test_move.py to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) bda1388bff migrate test_gc.py to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) c02bd7f318 migrate some files to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) fe1d805c80 migrate test_cli.py to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) f9b0de883f migrate test_checkout.py to pytest 2022-11-03 23:07:28 +05:45
Saugat Pachhai (सौगात) 1cad8ea7fd migrate test_{add,analytics,init,unprotect,local,process}.py to pytest 2022-11-03 23:07:28 +05:45
David de la Iglesia Castro 08a643e005 hydra: Raise lazy DvcException for Python >= 3.11 2022-11-03 12:14:18 +01:00
Peter Rowlands 97a8b6978c deps: bump scmrepo to 0.1.3 2022-11-03 15:56:50 +09:00
Peter Rowlands b44099367e exp run: catch hydra import in 3.11 2022-11-03 15:38:53 +09:00
skshetry 29b3dc1f9e fix 3.10 in setup-python 2022-11-01 16:35:24 +05:45
Saugat Pachhai (सौगात) 6e73bd0b5b use setuptools_scm provided version 2022-11-01 12:44:31 +02:00
skshetry fa5a9abdeb build: bump pyinstaller packages python version to 3.10 (#8511)
bump pyinstaller packages python version to 3.10

As 3.11 is now released, let's bump pyinstaller packages to 3.10 (we always keep them 1 version behind).
2022-11-01 16:24:00 +05:45
dependabot[bot] 30775f7b31 build(deps): Bump pyinstaller from 5.6.1 to 5.6.2 (#8499)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.6.1 to 5.6.2.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.6.1...v5.6.2)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-01 15:19:02 +05:45
Peter Rowlands e07aca9fd3 commit: skip changed_entries check on force commit 2022-11-01 18:05:41 +09:00
skshetry d2265f36ba ci: run only single macOS job (#8509)
* ci: run only single macOS job

* ci: use alls-green@release gate check
2022-11-01 14:30:46 +05:45
Saugat Pachhai (सौगात) d548618291 codecov: disable patch status 2022-11-01 14:02:59 +05:45
skshetry 48ff332f4a tests: remove old cleanup handler for Windows subprocess (#8508) 2022-11-01 13:57:50 +05:45
Saugat Pachhai (सौगात) 97f5ffc3c6 codecov: re-add codecov.yml 2022-11-01 13:56:54 +05:45
skshetry f6849d2f06 chore: remove bin dir and simplify tests.__main__ (#8507) 2022-11-01 13:05:53 +05:45
skshetry a172240536 Update release.yml 2022-11-01 11:43:25 +05:45
skshetry 129a4fbb92 fix release note config, it has to be inside changelog key (#8504) 2022-11-01 11:42:05 +05:45
skshetry c101514677 ci: add release note config (#8503) 2022-11-01 11:37:11 +05:45
skshetry 5e883bc00c tests: remove TestDir (#8502)
remove TestDir
2022-11-01 11:28:09 +05:45
skshetry 4b720e5820 remove .codecov.yml and MANIFEST.in, move .coveragerc to pyproject.toml (#8500)
* remove .codecov.yml and MANIFEST.in, move .coveragerc to pyproject.toml

* remove scripts/pypi_upload.sh
2022-11-01 10:11:55 +05:45
skshetry c0054fa57e remove zip-safe (#8498)
See https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html. `zip_safe` is not required these days.
2022-11-01 07:50:18 +05:45
skshetry 6049494cef set name so that dependency graph works (#8497)
See https://github.com/orgs/community/discussions/6456
2022-11-01 07:09:21 +05:45
skshetry 120a5879bb ci: push package to test.pypi.org (#8495)
* ci: push package to test.pypi.org

* include dvc.testing and license file
2022-10-31 23:34:00 +05:45
Step Security Bot c8e39523c3 [StepSecurity] ci: Harden GitHub Actions (#8496)
Signed-off-by: StepSecurity Bot <[email protected]>
2022-10-31 23:27:18 +05:45
skshetry 7648e715f8 move project config to pyproject.toml (#8493)
* move config to pyproject.toml

* ci: use pyproject.toml for cache key

* pyproject.toml: use recursive dependencies for dev and all

* Update pyproject.toml
2022-10-31 22:43:48 +05:45
skshetry ca5ad6457f ci: cache lint deps, decrease test timeout (#8494) 2022-10-31 22:15:54 +05:45
dependabot[bot] de848d9598 build(deps-dev): Bump pytest-xdist from 2.5.0 to 3.0.2 (#8474)
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 2.5.0 to 3.0.2.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v2.5.0...v3.0.2)

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

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-31 21:23:57 +05:45
dependabot[bot] 4330f1a70f build(deps): Bump pyinstaller from 5.0 to 5.6.1 (#8475)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.0 to 5.6.1.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.0...v5.6.1)

---
updated-dependencies:
- dependency-name: pyinstaller
  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-31 21:23:43 +05:45
dependabot[bot] 144e21b552 build(deps-dev): Bump pytest from 7.1.3 to 7.2.0 (#8479)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.3...7.2.0)

---
updated-dependencies:
- dependency-name: pytest
  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-31 21:20:21 +05:45
dependabot[bot] 7945428e5e build(deps-dev): Bump pylint from 2.15.4 to 2.15.5 (#8463)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.4 to 2.15.5.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.4...v2.15.5)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-31 21:19:58 +05:45
skshetry 6df70d388e update pre-commit; add some more hooks (#8492)
* update pre-commit; add some more hooks

* Apply suggestions from code review
2022-10-31 21:04:11 +05:45
skshetry 4ca2158b9d tests: retry celery queue on timeout (#8488)
retry celery queue on timeout
2022-10-31 19:38:05 +05:45
Ruslan Kuprieiev f87e78d754 deps: bump dvc-data to 0.25.2 2022-10-31 14:41:23 +02:00
Ruslan Kuprieiev 5899440b46 deps: bump dvc-data to 0.25.1 2022-10-30 00:59:39 +03:00
Ruslan Kuprieiev 61ce52f259 yappi: add --yappi-separate-threads flag 2022-10-29 21:23:37 +03:00
Ruslan Kuprieiev 475ef3ce65 Delete release-drafter.yml 2022-10-29 19:39:55 +03:00
karajan1001 22eb9051f6 Move the function out of rwlock 2022-10-29 08:53:02 +08:00
karajan1001 9ef1d1d6e6 Move away tempdir related change from basequeue. 2022-10-29 08:53:02 +08:00
karajan1001 d5cc54ad35 dvc exp show auto solve corrupted rwlock info
fix: #8450

1. Add a new function check_rwlock to check and autocorrect rwlock info.
2. Add related tests for it.
2022-10-29 08:53:02 +08:00
karajan1001 47ece317a5 Refactor: move get_running_exps to queue
Move logic of `get_running_exps` from `repo.experiments` to each queue.
2022-10-29 08:53:02 +08:00
Ruslan Kuprieiev 8f82a421a0 index: support multiple targets within output in IndexView
stage_infos might contain the same stage multiple times, but with different
filter_info, so we shouldn't convert it to dict, as that makes us lose all
but one filter_info's.
2022-10-25 13:37:44 +03:00
karajan1001 ed2ccaf54c Use celery status as the exp show status
fix: #8349
`tasks status turned to queued for 1 second before turned into success.`
is because in previous the status is using the output of
`get_running_exps` in `repo.expereiments` and when entering the
collect result progress, the `get_running_exps` will mark the tasks to
be not running because the `info.result` is not None. Here we use the
output of the celery queue as the universal standard for the tasks
status.

1. Use celery status as the exp show status
2022-10-24 14:41:27 +08:00
Ruslan Kuprieiev 23534063ca push: use build_tree instead of tree_from_index 2022-10-21 17:02:33 +03:00
Ruslan Kuprieiev d56957416e index: data: don't forget to set entry.key 2022-10-21 17:02:33 +03:00
dependabot[bot] 269e12287c build(deps): Bump dvc-data from 0.20.0 to 0.22.0
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.20.0 to 0.22.0.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.20.0...0.22.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-10-21 17:02:33 +03:00
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
224 changed files with 8470 additions and 6868 deletions
-10
View File
@@ -1,10 +0,0 @@
comment: false
coverage:
status:
project:
default:
target: 90
threshold: 10%
patch:
default:
threshold: 25%
-16
View File
@@ -1,16 +0,0 @@
[run]
branch = True
source = dvc
omit =
*__main__.py
[report]
exclude_lines =
if __name__ == .__main__.:
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
pass
raise NotImplementedError
# pragma: no cover
# pragma: no cover
\.\.\.$
-1
View File
@@ -1,4 +1,3 @@
/tests
/dvc
/.github
/bin
+12
View File
@@ -0,0 +1,12 @@
[flake8]
ignore=
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
P1, # unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
max_line_length = 79
max-complexity = 15
select = B,C,E,F,W,T4,B902,T,P
show_source = true
count = true
+1 -1
View File
@@ -8,7 +8,7 @@ about: Create a bug report to help us improve DVC
<!--
## Issue name
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
Example: `repro: doesn't detect input changes`
-->
@@ -2,4 +2,3 @@
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project
---
+12
View File
@@ -0,0 +1,12 @@
codecov:
notify:
wait_for_ci: true
coverage:
status:
project:
default:
target: auto
threshold: 2%
patch: off
github_checks:
annotations: false
-49
View File
@@ -1,49 +0,0 @@
# Config for https://github.com/apps/release-drafter
name-template: "$RESOLVED_VERSION 🦉 "
tag-template: "$RESOLVED_VERSION"
categories:
- title: "🚀 New Features and Enhancements "
labels:
- "feature"
- "enhancement"
- "ui"
- title: "💣 Breaking Changes "
labels:
- "breaking-change"
- title: "🏇 Optimizations "
labels:
- "optimize"
- "performance"
- title: "🐛 Bug Fixes "
labels:
- "bugfix"
- "bug"
- title: "🔨 Maintenance "
labels:
- "maintenance"
- "refactoring"
- "chore"
- "build"
- "ci"
change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
version-resolver:
major:
labels:
- "major"
minor:
labels:
- "minor"
patch:
labels:
- "patch"
default: patch
exclude-labels:
- "skip-changelog"
template: |
> Refer to https://dvc.org/doc/install for installation instructions.
## Changes
$CHANGES
Thanks again to $CONTRIBUTORS for the contributions! 🎉
+16
View File
@@ -0,0 +1,16 @@
changelog:
exclude:
labels: ["skip-changelog"]
categories:
- title: 🛠 Breaking Changes
labels: ["breaking-change"]
- title: 🚀 New Features and Enhancements
labels: ["feature", "enhancement", "ui"]
- title: ⚡ Optimizations
labels: ["optimize", "performance"]
- title: 🐛 Bug Fixes
labels: ["bugfix", "bug"]
- title: 🔨 Maintenance
labels: ["maintenance", "refactoring", "chore", "build", "ci"]
- title: Other Changes
labels: ["*"]
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
- uses: iterative/dvc-bench@main
with:
pytest_options: "-k 'test_init or test_help'"
+39
View File
@@ -0,0 +1,39 @@
name: "CodeQL"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: '32 19 * * 2'
workflow_dispatch:
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['python']
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+26 -11
View File
@@ -7,8 +7,13 @@ on:
types: [released, prereleased]
workflow_dispatch:
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
binary:
permissions:
contents: write # for actions/upload-release-asset to upload release asset
strategy:
matrix:
include:
@@ -24,13 +29,13 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: "3.10"
cache: pip
cache-dependency-path: |
setup.cfg
pyproject.toml
scripts/build-requirements.txt
- name: Set up Ruby 2.6
@@ -45,7 +50,7 @@ jobs:
- name: Install deps
run: |
pip install -U wheel pip setuptools
pip install --upgrade pip wheel setuptools
pip install .[all]
pip install -r scripts/build-requirements.txt
@@ -77,21 +82,31 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: setup.cfg
- name: Install
run: pip install --upgrade pip wheel
- name: Force version for Test PyPI uploads
if: ${{ !startsWith(github.ref, 'refs/tags') }}
run: |
pip install -U wheel pip build setuptools
pip install ".[all,tests]"
pip install setuptools_scm
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
- name: Build packages
run: |
pip install -U pip
./scripts/build_package.sh
run: ./scripts/build_package.sh
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
- name: Publish to Test PyPI
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
+26 -18
View File
@@ -17,6 +17,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
lint:
timeout-minutes: 10
@@ -29,13 +32,12 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: pyproject.toml
- name: Install requirements
run: |
pip install --upgrade pip setuptools wheel
pip install collective.checkdocs==0.2
pip install --upgrade pip wheel
pip install ".[dev]"
- name: Check README
run: python setup.py checkdocs
- uses: pre-commit/[email protected]
tests:
timeout-minutes: 45
@@ -43,13 +45,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.8", "3.9", "3.10"]
pytest-filter:
os: [ubuntu-20.04, windows-latest]
pyv: ["3.8", "3.9", "3.10", "3.11"]
pytest-filter:
- "import or plot or live or experiment"
- "not (import or plot or live or experiment)"
include:
- {os: ubuntu-latest, pyv: "3.11-dev"}
- {os: macos-latest, pyv: "3.8"}
- {os: macos-latest, pyv: "3.9"}
- {os: macos-latest, pyv: "3.10"}
- {os: macos-latest, pyv: "3.11"}
steps:
- uses: actions/checkout@v3
@@ -60,27 +65,30 @@ jobs:
with:
python-version: ${{ matrix.pyv }}
cache: pip
cache-dependency-path: setup.cfg
cache-dependency-path: pyproject.toml
- name: install
if: steps.cache.pip-cache-dir.cache-hit != 'true'
run: |
pip install --upgrade pip setuptools wheel
pip install --upgrade pip 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 }}"
timeout-minutes: 20
run: pytest -nauto --durations 100 --cov --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
- name: upload coverage report
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: false
check:
if: always()
needs: [lint, tests]
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
notify:
if: github.ref == 'refs/heads/main' && failure()
needs:
- lint
- tests
needs: [lint, tests]
runs-on: ubuntu-latest
steps:
- name: Slack Notification
+43 -28
View File
@@ -1,39 +1,62 @@
default_language_version:
python: python3
ci:
skip: [mypy, pylint, dvc-pre-commit]
repos:
- hooks:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 22.6.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
exclude: "tests/func/utils/test_strict_yaml.py"
args: ['--assume-in-merge']
- id: check-toml
- id: check-yaml
- hooks:
- id: codespell
args:
- --ignore-words-list
- ba,datas,fo,uptodate,cachable
repo: https://github.com/codespell-project/codespell
rev: v2.1.0
- hooks:
- id: isort
language_version: python3
repo: https://github.com/timothycrosley/isort
- id: debug-statements
exclude: "dvc/_debug.py"
- id: end-of-file-fixer
- id: mixed-line-ending
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/lovesegfault/beautysh
rev: v6.2.1
hooks:
- id: beautysh
args: ["-i", "2"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.0
hooks:
- id: pyupgrade
args:
- --py38-plus
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
- hooks:
hooks:
- id: isort
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
additional_dependencies: ["tomli"]
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-broken-line
- flake8-bugbear
- flake8-comprehensions
- flake8-debugger
- flake8-string-format
repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: local
hooks:
- id: mypy
@@ -47,14 +70,6 @@ repos:
entry: pylint
language: system
types: [python]
- hooks:
- args:
- -i
- "2"
id: beautysh
language_version: python3
repo: https://github.com/lovesegfault/beautysh
rev: v6.2.1
- hooks:
- args:
- git-hook
-1
View File
@@ -74,4 +74,3 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
-3
View File
@@ -1,3 +0,0 @@
include LICENSE
recursive-include requirements *.txt
recursive-include dvc/repo/plots/templates *.json
+7 -3
View File
@@ -13,7 +13,7 @@
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|PyPI| |PyPI Downloads| |Packages| |Brew| |Conda| |Choco| |Snap|
|
@@ -82,7 +82,7 @@ To share and back up the *data cache*, DVC supports multiple remote storage plat
|Flowchart|
`DVC pipelines <https://dvc.org/doc/start/data-management/pipelines>`_ (computational graphs) connect code and data together.
`DVC pipelines <https://dvc.org/doc/start/data-management/data-pipelines>`_ (computational graphs) connect code and data together.
They specify all steps required to produce a model: input dependencies including code, data, commands to run; and output information to be saved.
Last but not least, `DVC Experiment Versioning <https://dvc.org/doc/start/experiments>`_ lets you prepare and run a large number of experiments.
@@ -276,6 +276,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://pypi.org/project/dvc
:alt: PyPI
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/dvc.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold
:target: https://pypi.org/project/dvc
:alt: PyPI Downloads
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
:target: https://github.com/iterative/dvc/releases/latest
:alt: deb|pkg|rpm|exe
@@ -292,6 +296,6 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://github.com/iterative/dvc/graphs/contributors
:alt: Contributors
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
:alt: VS Code Extension
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
PYTHONPATH=$DVC_HOME python -m dvc $@
-3
View File
@@ -1,3 +0,0 @@
@echo OFF
set PYTHONPATH=%DVC_HOME%
python -m dvc %*
+1 -1
View File
@@ -4,6 +4,6 @@ DVC
Make your data science projects reproducible and shareable.
"""
import dvc.logger
from dvc.version import __version__ # noqa: F401
from dvc.version import __version__, version_tuple # noqa: F401
dvc.logger.setup()
+82 -23
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
@@ -29,7 +27,9 @@ def viztracer_profile(
@contextmanager
def yappi_profile(
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
path: Union[Callable[[], str], str] = None,
wall_clock: Optional[bool] = True,
separate_threads: Optional[bool] = False,
):
try:
import yappi # pylint: disable=import-error
@@ -44,14 +44,26 @@ def yappi_profile(
yield
yappi.stop()
# pylint:disable=no-member
if path:
stats = yappi.get_func_stats()
fpath = path() if callable(path) else path
stats.save(fpath, "callgrind")
threads = yappi.get_thread_stats()
stats = {}
if separate_threads:
for thread in threads:
ctx_id = thread.id
stats[ctx_id] = yappi.get_func_stats(ctx_id=ctx_id)
else:
yappi.get_func_stats().print_all()
yappi.get_thread_stats().print_all()
stats[None] = yappi.get_func_stats()
fpath = path() if callable(path) else path
for ctx_id, st in stats.items():
if fpath:
out = f"{fpath}-{ctx_id}" if ctx_id is not None else fpath
st.save(out, type="callgrind")
else:
if ctx_id is not None:
print(f"\nThread {ctx_id}")
st.print_all() # pylint:disable=no-member
if ctx_id is None:
threads.print_all() # pylint:disable=no-member
yappi.clear_stats()
@@ -109,6 +121,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 +165,21 @@ 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"
path_func = _get_path_func("callgrind", "out")
stack.enter_context(
yappi_profile(path=lambda: output.format(datetime.now()))
)
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,
yappi_profile(
path=path_func,
separate_threads=kw.get("yappi_separate_threads"),
)
)
if kw.get("viztracer") or kw.get("viztracer_depth"):
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
@@ -146,6 +192,12 @@ def add_debugging_flags(parser):
parser.add_argument(
"--yappi", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--yappi-separate-threads",
action="store_true",
default=False,
help=SUPPRESS,
)
parser.add_argument(
"--viztracer", action="store_true", default=False, help=SUPPRESS
)
@@ -160,3 +212,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,
)
+5 -16
View File
@@ -2,8 +2,8 @@
import argparse
import logging
import os
import sys
from dvc import __version__
from dvc.commands import (
add,
cache,
@@ -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,
@@ -126,17 +128,6 @@ class DvcParser(argparse.ArgumentParser):
return args
class VersionAction(argparse.Action): # pragma: no cover
# pylint: disable=too-few-public-methods
"""Shows DVC version and exits."""
def __call__(self, parser, namespace, values, option_string=None):
from dvc import __version__
print(__version__)
sys.exit(0)
def get_parent_parser():
"""Create instances of a parser containing common arguments shared among
all the commands.
@@ -184,13 +175,11 @@ def get_main_parser():
help="Show this help message and exit.",
)
# NOTE: On some python versions action='version' prints to stderr
# instead of stdout https://bugs.python.org/issue18920
parser.add_argument(
"-V",
"--version",
action=VersionAction,
nargs=0,
action="version",
version=__version__,
help="Show program's version.",
)
+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)
+2
View File
@@ -18,6 +18,7 @@ from dvc.commands.experiments import (
queue_worker,
remove,
run,
save,
show,
)
@@ -34,6 +35,7 @@ SUB_COMMANDS = [
queue_worker,
remove,
run,
save,
show,
]
+4 -8
View File
@@ -3,6 +3,7 @@ import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -16,18 +17,13 @@ class CmdExperimentsList(CmdBase):
num=self.args.num,
git_remote=self.args.git_remote,
)
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]
if not name_only:
print(f"{name}:")
ui.write(f"{baseline}:")
for exp_name in exps[baseline]:
indent = "" if name_only else "\t"
print(f"{indent}{exp_name}")
ui.write(f"{indent}{exp_name}")
return 0
+79
View File
@@ -0,0 +1,79 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.ui import ui
logger = logging.getLogger(__name__)
class CmdExperimentsSave(CmdBase):
def run(self):
try:
ref = self.repo.experiments.save(
name=self.args.name,
force=self.args.force,
include_untracked=self.args.include_untracked,
)
except DvcException:
logger.exception("failed to save experiment")
return 1
if self.args.json:
ui.write_json({"ref": ref})
else:
name = self.repo.experiments.get_exact_name([ref])[ref]
ui.write(f"Experiment has been saved as: {name}")
ui.write(
"\nTo promote an experiment to a Git branch run:\n\n"
"\tdvc exp branch <exp> <branch>\n"
)
return 0
def add_parser(experiments_subparsers, parent_parser):
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
save_parser = experiments_subparsers.add_parser(
"save",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_SAVE_HELP, "exp/save"),
help=EXPERIMENTS_SAVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
save_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Save even if hash value for dependencies/outputs changed.",
)
save_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
save_parser.add_argument(
"-n",
"--name",
default=None,
help=(
"Human-readable experiment name. If not specified, a name will "
"be auto-generated."
),
metavar="<name>",
)
save_parser.add_argument(
"-I",
"--include-untracked",
action="append",
default=[],
help="List of untracked paths to include in the experiment.",
metavar="<path>",
)
save_parser.set_defaults(func=CmdExperimentsSave)
+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",
-20
View File
@@ -238,26 +238,6 @@ def _add_common_args(parser):
help="Declare output plot file (do not put into DVC cache).",
metavar="<path>",
)
parser.add_argument(
"--live", help="Declare output as dvclive.", metavar="<path>"
)
parser.add_argument(
"--live-no-cache",
help="Declare output as dvclive (do not put into DVC cache).",
metavar="<path>",
)
parser.add_argument(
"--live-no-summary",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--live-no-html",
action="store_true",
default=False,
help="Signal dvclive logger to not produce training report.",
)
parser.add_argument(
"-w",
"--wdir",
+3 -2
View File
@@ -31,7 +31,8 @@ class CmdUpdate(CmdBase):
def add_parser(subparsers, parent_parser):
UPDATE_HELP = (
"Update data artifact imported (via dvc import or dvc import-url) "
"from an external DVC repository or URL."
"from an external DVC repository or URL, or update data from a "
"DVC worktree remote."
)
update_parser = subparsers.add_parser(
"update",
@@ -60,7 +61,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,
+19 -11
View File
@@ -5,12 +5,14 @@ from typing import TYPE_CHECKING, Iterable, Optional
from funcy import cached_property
from dvc.config import NoRemoteError, RemoteConfigError
from dvc_data.hashfile.db import get_index
if TYPE_CHECKING:
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.status import CompareStatusResult
from dvc_data.hashfile.transfer import TransferResult
logger = logging.getLogger(__name__)
@@ -22,6 +24,14 @@ class Remote:
self.worktree = config.pop("worktree", False)
self.config = config
if self.worktree:
version_aware = self.config.get("version_aware")
if version_aware is False:
raise RemoteConfigError(
"worktree remotes require version_aware cloud"
)
if version_aware is None:
self.fs.version_aware = True
@cached_property
def odb(self):
@@ -53,8 +63,6 @@ class DataCloud:
name: Optional[str] = None,
command: str = "<command>",
) -> "Remote":
from dvc.config import NoRemoteError
if not name:
name = self.repo.config["core"].get("remote")
@@ -87,6 +95,10 @@ class DataCloud:
command: str = "<command>",
) -> "HashFileDB":
remote = self.get_remote(name=name, command=command)
if remote.fs.version_aware or remote.worktree:
raise NoRemoteError(
f"'{command}' is unsupported for cloud versioned remotes"
)
return remote.odb
def _log_missing(self, status: "CompareStatusResult"):
@@ -106,14 +118,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 +129,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 +156,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:
+6 -4
View File
@@ -1,7 +1,7 @@
from collections import defaultdict
from typing import Any, Mapping
from dvc.output import ARTIFACT_SCHEMA, Output
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
from .base import Dependency
from .param import ParamsDependency
@@ -15,10 +15,11 @@ SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**ParamsDependency.PARAM_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
def _get(stage, p, info, fs_config=None):
def _get(stage, p, info, **kwargs):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
@@ -27,14 +28,15 @@ def _get(stage, p, info, fs_config=None):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
return Dependency(stage, p, info, fs_config=fs_config)
return Dependency(stage, p, info, **kwargs)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(Output.PARAM_PATH, None)
ret.append(_get(stage, p, d))
files = d.pop(Output.PARAM_FILES, None)
ret.append(_get(stage, p, d, files=files))
return ret
+37 -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,39 @@ 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
)
def dumpd(self, **kwargs):
if self.fs.version_aware:
kwargs["with_files"] = True
return super().dumpd(**kwargs)
+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
+2 -3
View File
@@ -2,11 +2,10 @@ DVC_CHECKPOINT = "DVC_CHECKPOINT"
DVC_DAEMON = "DVC_DAEMON"
DVC_PAGER = "DVC_PAGER"
DVC_ROOT = "DVC_ROOT"
DVCLIVE_PATH = "DVCLIVE_PATH"
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
DVCLIVE_HTML = "DVCLIVE_HTML"
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
DVC_EXP_NAME = "DVC_EXP_NAME"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
+15 -3
View File
@@ -132,10 +132,17 @@ class NotDvcRepoError(DvcException):
class CyclicGraphError(DvcException):
def __init__(self, stages):
assert isinstance(stages, list)
msg = "Pipeline has a cycle involving: {}.".format(
", ".join(s.addressing for s in stages)
stage_part = "stage" if len(stages) == 1 else "stages"
msg = (
"Same item(s) are defined as both a dependency and an output "
"in {stage_part}: {stage}."
)
super().__init__(
msg.format(
stage_part=stage_part,
stage=", ".join(s.addressing for s in stages),
)
)
super().__init__(msg)
class ConfirmRemoveError(DvcException):
@@ -278,6 +285,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()
+1 -1
View File
@@ -26,7 +26,7 @@ class BaseMachineBackend(ABC):
) -> Iterator[dict]:
"""Iterate over status of all instances of the specified machine."""
def close(self):
def close(self): # noqa: B027
pass
@abstractmethod
+130 -78
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__)
@@ -83,10 +87,10 @@ def loadd_from(stage, d_list):
plot = d.pop(Output.PARAM_PLOT, False)
persist = d.pop(Output.PARAM_PERSIST, False)
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
live = d.pop(Output.PARAM_LIVE, False)
remote = d.pop(Output.PARAM_REMOTE, None)
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
files = d.pop(Output.PARAM_FILES, None)
push = d.pop(Output.PARAM_PUSH, True)
ret.append(
_get(
stage,
@@ -97,10 +101,10 @@ def loadd_from(stage, d_list):
plot=plot,
persist=persist,
checkpoint=checkpoint,
live=live,
remote=remote,
**annot,
files=files,
push=push,
)
)
return ret
@@ -114,8 +118,8 @@ def loads_from(
plot=False,
persist=False,
checkpoint=False,
live=False,
remote=None,
push=True,
):
return [
_get(
@@ -127,8 +131,8 @@ def loads_from(
plot=plot,
persist=persist,
checkpoint=checkpoint,
live=live,
remote=remote,
push=push,
)
for s in s_list
]
@@ -162,29 +166,21 @@ def load_from_pipeline(stage, data, typ="outs"):
stage.PARAM_OUTS,
stage.PARAM_METRICS,
stage.PARAM_PLOTS,
stage.PARAM_LIVE,
):
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
metric = typ == stage.PARAM_METRICS
plot = typ == stage.PARAM_PLOTS
live = typ == stage.PARAM_LIVE
if live:
# `live` is single object
data = [data]
d = _merge_data(data)
for path, flags in d.items():
plt_d, live_d = {}, {}
plt_d = {}
if plot:
from dvc.schema import PLOT_PROPS
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
if live:
from dvc.schema import LIVE_PROPS
live_d, flags = _split_dict(flags, keys=LIVE_PROPS.keys())
extra = project(
flags,
[
@@ -192,6 +188,7 @@ def load_from_pipeline(stage, data, typ="outs"):
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
Output.PARAM_REMOTE,
Output.PARAM_PUSH,
*ANNOTATION_FIELDS,
],
)
@@ -202,7 +199,6 @@ def load_from_pipeline(stage, data, typ="outs"):
info={},
plot=plt_d or plot,
metric=metric,
live=live_d or live,
**extra,
)
@@ -259,10 +255,8 @@ class Output:
PARAM_PLOT_TITLE = "title"
PARAM_PLOT_HEADER = "header"
PARAM_PERSIST = "persist"
PARAM_LIVE = "live"
PARAM_LIVE_SUMMARY = "summary"
PARAM_LIVE_HTML = "html"
PARAM_REMOTE = "remote"
PARAM_PUSH = "push"
METRIC_SCHEMA = Any(
None,
@@ -288,7 +282,6 @@ class Output:
plot=False,
persist=False,
checkpoint=False,
live=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
@@ -297,6 +290,7 @@ class Output:
repo=None,
fs_config=None,
files: List[Dict[str, Any]] = None,
push: bool = True,
):
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
@@ -351,7 +345,7 @@ class Output:
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.checkpoint = checkpoint
self.live = live
self.can_push = push
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj: Optional["HashFile"] = None
@@ -373,8 +367,12 @@ class Output:
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
if self.hash_info and self.hash_info.isdir:
if self.meta.nfiles or self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
if not self.hash_info and self.hash_name != "md5":
md5 = getattr(self.meta, "md5", None)
if md5:
self.hash_info = HashInfo("md5", md5)
def _parse_path(self, fs, fs_path):
parsed = urlparse(self.def_path)
@@ -511,6 +509,36 @@ 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
if self.files and not self.obj:
self.obj = self.get_obj()
entry = DataIndexEntry(
meta=self.meta,
obj=self.obj,
hash_info=self.hash_info,
odb=self.odb,
cache=self.odb,
remote=remote,
)
if self.stage.is_import and not self.stage.is_repo_import:
dep = self.stage.deps[0]
entry.fs = dep.fs
entry.path = dep.fs_path
entry.meta = dep.meta
if not entry.obj:
if not dep.obj and dep.files:
dep.obj = dep.get_obj()
entry.obj = dep.obj
return entry
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -530,7 +558,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):
@@ -610,8 +638,16 @@ class Output:
if self.metric:
self.verify_metric()
if not self.use_cache:
_, self.meta, obj = build(
if self.use_cache:
_, self.meta, self.obj = build(
self.odb,
self.fs_path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
)
else:
_, self.meta, self.obj = build(
self.repo.odb.local,
self.fs_path,
self.fs,
@@ -619,23 +655,11 @@ class Output:
ignore=self.dvcignore,
dry_run=True,
)
self.hash_info = obj.hash_info
self.files = None
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
)
return
assert not self.IS_DEPENDENCY
_, self.meta, self.obj = build(
self.odb,
self.fs_path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
)
self.hash_info = self.obj.hash_info
self.files = None
@@ -733,41 +757,38 @@ class Output:
ret[self.PARAM_PATH] = path
if self.IS_DEPENDENCY:
return ret
if not self.IS_DEPENDENCY:
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if self.metric:
ret[self.PARAM_METRIC] = self.metric
if self.metric:
ret[self.PARAM_METRIC] = self.metric
if self.plot:
ret[self.PARAM_PLOT] = self.plot
if self.plot:
ret[self.PARAM_PLOT] = self.plot
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
if self.checkpoint:
ret[self.PARAM_CHECKPOINT] = self.checkpoint
if self.checkpoint:
ret[self.PARAM_CHECKPOINT] = self.checkpoint
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if self.live:
ret[self.PARAM_LIVE] = self.live
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if not self.can_push:
ret[self.PARAM_PUSH] = self.can_push
if (
self.use_cache
and self.is_in_repo
(not self.IS_DEPENDENCY or self.stage.is_import)
and self.hash_info.isdir
and (kwargs.get("with_files") or self.files is not None)
):
@@ -800,15 +821,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 +840,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 +851,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 +859,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
@@ -1030,9 +1043,15 @@ class Output:
if not self.use_cache:
return {}
push: bool = kwargs.pop("push", False)
if self.stage.is_repo_import:
if push:
return {}
return self.get_used_external(**kwargs)
if push and not self.can_push:
return {}
if not self.hash_info:
msg = (
"Output '{}'({}) is missing version info. "
@@ -1127,7 +1146,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 +1163,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
@@ -1168,11 +1191,39 @@ class Output:
@property
def is_metric(self) -> bool:
return bool(self.metric) or bool(self.live)
return bool(self.metric)
@property
def is_plot(self) -> bool:
return bool(self.plot) or bool(self.live)
return bool(self.plot)
def restore_fields(self, other: "Output"):
"""Restore attributes that need to be preserved when serialized."""
self.annot = other.annot
self.remote = other.remote
self.can_push = other.can_push
def merge_version_meta(self, other: "Output"):
"""Merge version meta for files which are unchanged from other."""
if not self.hash_info:
return
if self.hash_info.isdir:
return self._merge_dir_version_meta(other)
if self.hash_info != other.hash_info:
return
self.meta = other.meta
def _merge_dir_version_meta(self, other: "Output"):
from dvc_data.hashfile.tree import update_meta
if not self.obj or not other.hash_info.isdir:
return
other_obj = other.obj if other.obj is not None else other.get_obj()
assert isinstance(self.obj, Tree) and isinstance(other_obj, Tree)
updated = update_meta(self.obj, other_obj)
assert updated.hash_info == self.obj.hash_info
self.obj = updated
self.files = updated.as_list(with_meta=True)
META_SCHEMA = {
@@ -1203,5 +1254,6 @@ SCHEMA = {
Output.PARAM_CACHE: bool,
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
Output.PARAM_REMOTE: str,
Output.PARAM_PUSH: bool,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
+1 -1
View File
@@ -36,7 +36,7 @@ from .interpolate import (
)
if TYPE_CHECKING:
from typing_extensions import NoReturn
from typing import NoReturn
from dvc.repo import Repo
+14 -4
View File
@@ -1,3 +1,4 @@
import os
import re
import typing
from collections.abc import Iterable, Mapping
@@ -9,10 +10,9 @@ from dvc.exceptions import DvcException
from dvc.utils.flatten import flatten
if typing.TYPE_CHECKING:
from typing import List, Match
from typing import List, Match, NoReturn
from pyparsing import ParseException
from typing_extensions import NoReturn
from .context import Context
@@ -71,6 +71,16 @@ def embrace(s: str):
return BRACE_OPEN + s + BRACE_CLOSE
def escape_str(value):
if os.name == "nt":
from subprocess import list2cmdline
return list2cmdline([value])
from shlex import quote
return quote(value)
@singledispatch
def to_str(obj) -> str:
return str(obj)
@@ -98,12 +108,12 @@ def _(obj: dict):
result += f"--no-{k} "
elif isinstance(v, str):
result += f"--{k} '{v}' "
result += f"--{k} {escape_str(v)} "
elif isinstance(v, Iterable):
for n, i in enumerate(v):
if isinstance(i, str):
i = f"'{i}'"
i = escape_str(i)
elif isinstance(i, Iterable):
raise ParseError(
f"Cannot interpolate nested iterable in '{k}'"
+11 -5
View File
@@ -2,20 +2,26 @@ 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
def _get_converter(
renderer_class, props
renderer_class, renderer_id, props, data
) -> Union[VegaConverter, ImageConverter]:
from dvc_render import ImageRenderer, VegaRenderer
if renderer_class.TYPE == VegaRenderer.TYPE:
return VegaConverter(props)
return VegaConverter(renderer_id, data, props)
if renderer_class.TYPE == ImageRenderer.TYPE:
return ImageConverter(props)
return ImageConverter(renderer_id, data, props)
raise ValueError(f"Invalid renderer class {renderer_class}")
@@ -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)
+14 -4
View File
@@ -1,9 +1,19 @@
from typing import Dict, Optional
from typing import Any, Dict, List, Optional, Tuple
class Converter:
def __init__(self, plot_properties: Optional[Dict] = None):
self.plot_properties = plot_properties or {}
def __init__(
self,
plot_id: str,
data: Dict[str, Any] = None,
properties: Optional[Dict] = None,
):
self.plot_id = plot_id
self.properties = properties or {}
self.data = data or {}
def convert(self, data, revision: str, filename: str, **kwargs):
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
raise NotImplementedError
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
raise NotImplementedError
+27 -19
View File
@@ -1,6 +1,6 @@
import base64
import os
from typing import TYPE_CHECKING, Dict, List, Tuple
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
@@ -34,25 +34,33 @@ class ImageConverter(Converter):
base64_str = base64.b64encode(image_data).decode()
return f"data:image;base64,{base64_str}"
def convert(
self, data, revision: str, filename: str, **kwargs
) -> Tuple[List[Dict], Dict]:
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
datas = []
for filename, image_data in self.data.items():
datas.append((filename, "", image_data))
return datas, self.properties
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
"""
Convert the DVC Plots content to DVC Render datapoints.
Return both generated datapoints and updated properties.
"""
path = self.plot_properties.get("out")
if path:
if not os.path.isdir(path):
os.makedirs(path, exist_ok=True)
src = self._write_image(
os.path.abspath(path), revision, filename, data
)
else:
src = self._encode_image(data)
datapoint = {
REVISION_FIELD: revision,
FILENAME_FIELD: filename,
SRC_FIELD: src,
}
return [datapoint], self.plot_properties
path = self.properties.get("out")
datapoints = []
datas, properties = self.convert()
for filename, _, image_data in datas:
if path:
if not os.path.isdir(path):
os.makedirs(path, exist_ok=True)
src = self._write_image(
os.path.abspath(path), revision, filename, image_data
)
else:
src = self._encode_image(image_data)
datapoint = {
REVISION_FIELD: revision,
FILENAME_FIELD: filename,
SRC_FIELD: src,
}
datapoints.append(datapoint)
return datapoints, properties
+272 -259
View File
@@ -1,90 +1,100 @@
from copy import deepcopy
from functools import partial
from typing import Dict, Iterable, List, Optional, Set, Union
from typing import Any, Dict, Iterable, List, Tuple, Union
from funcy import first, project
from funcy import first, last
from dvc.exceptions import DvcException
from dvc.render import (
FILENAME_FIELD,
INDEX_FIELD,
REVISION_FIELD,
VERSION_FIELD,
)
from dvc.render import FILENAME_FIELD, INDEX_FIELD, VERSION_FIELD
from . import Converter
class FieldsNotFoundError(DvcException):
def __init__(self, expected_fields, found_fields):
expected_str = ", ".join(expected_fields)
class FieldNotFoundError(DvcException):
def __init__(self, expected_field, found_fields):
found_str = ", ".join(found_fields)
super().__init__(
f"Could not find all provided fields ('{expected_str}') "
f"Could not find provided field ('{expected_field}') "
f"in data fields ('{found_str}')."
)
class PlotDataStructureError(DvcException):
def __init__(self):
super().__init__(
"Plot data extraction failed. Please see "
"https://man.dvc.org/plots for supported data formats."
)
def _lists(blob: Union[Dict, List]) -> Iterable[List]:
if isinstance(blob, list):
yield blob
else:
for _, value in blob.items():
if isinstance(value, dict):
yield from _lists(value)
elif isinstance(value, list):
yield value
def _filter_fields(
datapoints: List[Dict], fields: Set, **kwargs
) -> List[Dict]:
if not fields:
return datapoints
assert isinstance(fields, set)
new_data = []
for data_point in datapoints:
keys = set(data_point.keys())
if not fields <= keys:
raise FieldsNotFoundError(fields, keys)
new_data.append(project(data_point, fields))
return new_data
def _file_field(*args):
for axis_def in args:
if axis_def is not None:
for file, val in axis_def.items():
if isinstance(val, str):
yield file, val
elif isinstance(val, list):
for field in val:
yield file, field
def _lists(dictionary: Dict):
for _, value in dictionary.items():
if isinstance(value, dict):
yield from _lists(value)
elif isinstance(value, list):
yield value
def _find(
filename: str,
field: str,
data_series: List[Tuple[str, str, Any]],
):
for data_file, data_field, data in data_series:
if data_file == filename and data_field == field:
return data_file, data_field, data
return None
def _find_first_list(
data: Union[Dict, List], fields: Set, **kwargs
) -> List[Dict]:
fields = fields or set()
if not isinstance(data, dict):
return data
for lst in _lists(data):
if (
all(isinstance(dp, dict) for dp in lst)
# if fields is empty, it will match any set
and set(first(lst).keys()) & fields == fields
):
return lst
raise PlotDataStructureError()
def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
if filename in file2datapoints:
datapoint = first(file2datapoints[filename])
if field not in datapoint:
raise FieldNotFoundError(field, datapoint.keys())
return
def _append_index(datapoints: List[Dict], **kwargs) -> List[Dict]:
if INDEX_FIELD in first(datapoints).keys():
return datapoints
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
x = properties.get("x", None)
if x is not None and isinstance(x, dict):
for filename, field in _file_field(x):
_verify_field(file2datapoints, filename, field)
yield filename, field
for index, data_point in enumerate(datapoints):
data_point[INDEX_FIELD] = index
return datapoints
def _get_ys(properties, file2datapoints: Dict[str, List[Dict]]):
y = properties.get("y", None)
if y is not None:
for filename, field in _file_field(y):
_verify_field(file2datapoints, filename, field)
yield filename, field
def _is_datapoints(lst: List[Dict]):
"""
check if dict keys match, datapoints with different keys mgiht lead
to unexpected behavior
"""
return all(isinstance(item, dict) for item in lst) and set(
first(lst).keys()
) == {key for keys in lst for key in keys}
def get_datapoints(file_content: Dict):
result: List[Dict[str, Any]] = []
for lst in _lists(file_content):
if _is_datapoints(lst):
for index, datapoint in enumerate(lst):
if len(result) <= index:
result.append({})
result[index].update(datapoint)
return result
class VegaConverter(Converter):
@@ -95,222 +105,225 @@ class VegaConverter(Converter):
('x', 'y') it will attempt to fill in the blanks.
"""
def __init__(self, plot_properties: Optional[Dict] = None):
super().__init__(plot_properties)
def __init__(
self, plot_id: str, data: Dict = None, properties: Dict = None
):
super().__init__(plot_id, data, properties)
self.plot_id = plot_id
self.inferred_properties: Dict = {}
self.steps = []
# TODO we should be handling that in `convert`,
# to avoid stateful `self.inferred_properties`
self._infer_x_y()
self._infer_x()
self._infer_fields()
def _infer_y_from_data(self):
if self.plot_id in self.data:
for lst in _lists(self.data[self.plot_id]):
if all(isinstance(item, dict) for item in lst):
datapoint = first(lst)
field = last(datapoint.keys())
self.inferred_properties["y"] = {self.plot_id: field}
break
self.steps.append(
(
"find_data",
partial(
_find_first_list,
fields=self.inferred_properties.get("fields", set())
- {INDEX_FIELD},
),
)
)
def _infer_x_y(self):
def _infer_files(from_name, to_name):
from_value = self.properties.get(from_name, None)
to_value = self.properties.get(to_name, None)
if not self.plot_properties.get("x", None):
self.steps.append(("append_index", partial(_append_index)))
self.steps.append(
(
"filter_fields",
partial(
_filter_fields,
fields=self.inferred_properties.get("fields", set()),
),
)
)
self.steps.append(
(
"generate_y",
partial(
self._generate_y_values,
y_values=self.plot_properties.get("y", None),
),
)
)
def _infer_x(self):
if not self.plot_properties.get("x", None):
self.inferred_properties["x"] = INDEX_FIELD
def skip_step(self, name: str):
self.steps = [(_name, fn) for _name, fn in self.steps if _name != name]
def _infer_fields(self):
fields = self.plot_properties.get("fields", set())
if fields:
fields = {
*fields,
self.plot_properties.get("x", None),
self.plot_properties.get("y", None),
self.inferred_properties.get("x", None),
} - {None}
self.inferred_properties["fields"] = fields
def _infer_y(self, datapoints: List[Dict]):
if "y" not in self.plot_properties:
data_fields = list(first(datapoints))
skip = (
REVISION_FIELD,
self.plot_properties.get("x", None)
or self.inferred_properties.get("x"),
FILENAME_FIELD,
VERSION_FIELD,
)
inferred_y = first(
f for f in reversed(data_fields) if f not in skip
)
if "y" in self.inferred_properties:
previous_y = self.inferred_properties["y"]
if previous_y != inferred_y:
raise DvcException(
f"Inferred y ('{inferred_y}' value does not match"
f"previously matched one ('f{previous_y}')."
)
if isinstance(to_value, str):
self.inferred_properties[to_name] = {}
if isinstance(from_value, dict):
for file in from_value.keys():
self.inferred_properties[to_name][file] = to_value
else:
self.inferred_properties[to_name][self.plot_id] = to_value
else:
self.inferred_properties["y"] = inferred_y
self.inferred_properties[to_name] = to_value
_infer_files("y", "x")
_infer_files("x", "y")
if self.inferred_properties.get("y", None) is None:
self._infer_y_from_data()
def _find_datapoints(self):
result = {}
for file, content in self.data.items():
result[file] = get_datapoints(content)
return result
@staticmethod
def infer_y_label(properties):
y_label = properties.get("y_label", None)
if y_label is None:
y = properties.get("y", None)
if isinstance(y, dict):
fields = {field for _, field in _file_field(y)}
if len(fields) == 1:
y_label = first(fields)
else:
y_label = "y"
elif isinstance(y, list):
y_label = "y"
elif isinstance(y, str):
y_label = y
return y_label
@staticmethod
def infer_x_label(properties):
x_label = properties.get("x_label", None)
if x_label is None:
x = properties.get("x", None)
if isinstance(x, dict):
fields = {field for _, field in _file_field(x)}
if len(fields) == 1:
x_label = first(fields)
else:
x_label = "x"
else:
x_label = INDEX_FIELD
return x_label
def flat_datapoints(self, revision):
file2datapoints, properties = self.convert()
props_update = {}
xs = list(_get_xs(properties, file2datapoints))
# assign "step" if no x provided
if not xs:
x_file, x_field = (
None,
INDEX_FIELD,
)
else:
x_file, x_field = xs[0]
props_update["x"] = x_field
ys = list(_get_ys(properties, file2datapoints))
num_xs = len(xs)
num_ys = len(ys)
if num_xs > 1 and num_xs != num_ys:
raise DvcException(
f"Cannot have different number of x and y data sources. Found "
f"{num_xs} x and {num_ys} y data sources."
)
all_datapoints = []
all_y_fields = {y_field for _, y_field in ys}
# override to unified y field name if there are different y fields
if len(all_y_fields) > 1:
props_update["y"] = "dvc_inferred_y_value"
else:
props_update["y"] = first(all_y_fields)
for i, (y_file, y_field) in enumerate(ys):
if num_xs > 1:
x_file, x_field = xs[i]
datapoints = deepcopy(file2datapoints.get(y_file, []))
if props_update.get("y", None) == "dvc_inferred_y_value":
_update_from_field(
datapoints,
field="dvc_inferred_y_value",
source_field=y_field,
)
if x_field == INDEX_FIELD and x_file is None:
_update_from_index(datapoints, INDEX_FIELD)
else:
x_datapoints = file2datapoints.get(x_file, [])
try:
_update_from_field(
datapoints,
field=x_field,
source_datapoints=x_datapoints,
)
except IndexError:
raise DvcException(
f"Cannot join '{x_field}' from '{x_file}' and "
f"'{y_field}' from '{y_file}'. "
"They have to have same length."
)
_update_all(
datapoints,
update_dict={
VERSION_FIELD: {
"revision": revision,
FILENAME_FIELD: y_file,
"field": y_field,
}
},
)
all_datapoints.extend(datapoints)
if not all_datapoints:
return [], {}
properties = {**properties, **props_update}
return all_datapoints, properties
def convert(
self,
data,
revision: str,
filename: str,
skip: List = None,
**kwargs,
):
"""
Convert the data. Fill necessary fields ('x', 'y') and return both
generated datapoints and updated properties.
generated datapoints and updated properties. If `x` is not provided,
leave it as None, fronteds should handle it.
NOTE: Studio uses this method.
The only thing studio FE handles is filling `x` and `y`.
`x/y_label` should be filled here.
Datapoints are not stripped according to config, because users
might be utilizing other fields in their custom plots.
"""
if not skip:
skip = []
datapoints = self._find_datapoints()
properties = {**self.properties, **self.inferred_properties}
processed = deepcopy(data)
properties["y_label"] = self.infer_y_label(properties)
properties["x_label"] = self.infer_x_label(properties)
for step_name, step in self.steps:
return datapoints, properties
if step_name not in skip:
processed = step( # type: ignore
processed,
revision=revision,
filename=filename,
)
self._infer_y(processed) # type: ignore
return processed, {**self.plot_properties, **self.inferred_properties}
def _update_from_field(
target_datapoints: List[Dict],
field: str,
source_datapoints: List[Dict] = None,
source_field: str = None,
):
if source_datapoints is None:
source_datapoints = target_datapoints
if source_field is None:
source_field = field
def _generate_y_values( # noqa: C901
self,
datapoints: List[Dict],
y_values: Optional[Union[str, List, Dict]],
revision: str,
filename: str,
**kwargs,
) -> List[Dict]:
if len(source_datapoints) != len(target_datapoints):
raise IndexError(
"Source and target datapoints must have the same length"
)
result = []
properties_update = {}
for index, datapoint in enumerate(target_datapoints):
source_datapoint = source_datapoints[index]
if source_field in source_datapoint:
datapoint[field] = source_datapoint[source_field]
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):
res = {"revision": revision}
if filename is not None:
res["filename"] = filename
if field is not None:
res["field"] = field
return res
def _update_from_index(datapoints: List[Dict], new_field: str):
for index, datapoint in enumerate(datapoints):
datapoint[new_field] = index
def _generate_y(datapoint, field):
tmp = datapoint.copy()
tmp["dvc_inferred_y_value"] = datapoint[field]
tmp = _add_version_info(
tmp, _version_info(revision, filename, field)
)
if (
"y_label" not in properties_update
and "y_label" not in self.plot_properties
):
properties_update["y_label"] = "y"
properties_update["y"] = "dvc_inferred_y_value"
def _update_all(datapoints: List[Dict], update_dict: Dict):
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(
_add_version_info(
datapoint, _version_info(revision, filename, y_values)
)
)
if isinstance(y_values, list):
for datapoint in datapoints:
for y_val in y_values:
if y_val in datapoint:
result.append(_generate_y(datapoint, y_val))
if isinstance(y_values, dict):
def _to_set(values: Iterable):
result = set()
for val in values:
if isinstance(val, list):
for elem in val:
result.add(elem)
else:
result.add(val)
return result
all_fields = _to_set(y_values.values())
if (
all([isinstance(field, str) for field in all_fields])
and len(all_fields) == 1
):
# if we use the same field from all files,
# we dont have to generate it
for datapoint in datapoints:
result.append(
_add_version_info(
datapoint, _version_info(revision, filename)
)
)
properties_update.update({"y": all_fields.pop()})
else:
for def_filename, val in y_values.items():
if isinstance(val, str):
fields = [val]
if isinstance(val, list):
fields = val
for datapoint in datapoints:
for field in fields:
if field in datapoint and def_filename in filename:
result.append(_generate_y(datapoint, field))
self.inferred_properties = {
**self.inferred_properties,
**properties_update,
}
return result
for datapoint in datapoints:
datapoint.update(update_dict)
+9 -10
View File
@@ -42,7 +42,7 @@ class PlotsData:
return dict(groups)
def get_definition_data(self, target_files, rev):
result = []
result = {}
for file in target_files:
file_content = (
self.data.get(rev, {})
@@ -52,7 +52,7 @@ class PlotsData:
.get("data", {})
)
if file_content:
result.append((file, file_content))
result[file] = file_content
return result
@@ -91,15 +91,14 @@ def match_defs_renderers(
renderer_cls = VegaRenderer
renderer_id = plot_id
converter = _get_converter(renderer_cls, props)
converter = _get_converter(
renderer_cls, inner_id, props, definitions_data
)
for filename, plot_data in definitions_data:
dps, final_props = converter.convert(
revision=rev,
filename=filename,
data=plot_data,
)
plot_datapoints.extend(dps)
dps, rev_props = converter.flat_datapoints(rev)
if not final_props and rev_props:
final_props = rev_props
plot_datapoints.extend(dps)
if "title" not in final_props:
final_props["title"] = renderer_id
+6 -41
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)
@@ -268,7 +269,7 @@ class Repo:
except SCMError:
if self._uninitialized:
# might not be a git/dvc repo at all
# used in `params/metrics/plots/live` targets
# used in `params/metrics/plots` targets
return SCM(self.root_dir, no_scm=True)
raise
@@ -400,6 +401,7 @@ class Repo:
used_run_cache=None,
revs=None,
num=1,
push: bool = False,
):
"""Get the stages related to the given target and collect
the `info` of its outputs.
@@ -434,6 +436,7 @@ class Repo:
jobs=jobs,
recursive=recursive,
with_deps=with_deps,
push=push,
).items():
used[odb].update(objs)
@@ -445,44 +448,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 -1
View File
@@ -187,7 +187,7 @@ def add( # noqa: C901
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
else:
try:
stage.save()
stage.save(merge_versioned=True)
if not no_commit:
stage.commit()
except CacheLinkError:
+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,
+6 -3
View File
@@ -56,10 +56,13 @@ def commit(
]
for stage_info in stages_info:
stage = stage_info.stage
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
if force:
stage.save(allow_missing=allow_missing)
else:
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
stage.save(allow_missing=allow_missing)
stage.commit(
filter_info=stage_info.filter_info, allow_missing=allow_missing
)
+5 -3
View File
@@ -12,6 +12,7 @@ from typing import (
Optional,
Tuple,
TypedDict,
Union,
cast,
)
@@ -19,10 +20,9 @@ from dvc.fs.git import GitFileSystem
from dvc.ui import ui
if TYPE_CHECKING:
from scmrepo.base import Base
from dvc.output import Output
from dvc.repo import Repo
from dvc.scm import Git, NoSCM
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.obj import HashFile
@@ -140,7 +140,9 @@ class GitInfo(TypedDict, total=False):
is_dirty: bool
def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
def _git_info(
scm: Union["Git", "NoSCM"], untracked_files: str = "all"
) -> GitInfo:
from scmrepo.exceptions import SCMError
from dvc.scm import NoSCM
+54 -88
View File
@@ -2,9 +2,9 @@ import logging
import os
import re
import time
from typing import Any, Dict, Iterable, Optional
from typing import Dict, Iterable, Optional
from funcy import cached_property, first
from funcy import cached_property, chain, first
from dvc.exceptions import DvcException
from dvc.ui import ui
@@ -16,7 +16,7 @@ from .exceptions import (
InvalidExpRefError,
MultipleBranchError,
)
from .executor.base import BaseExecutor, ExecutorInfo
from .executor.base import BaseExecutor
from .queue.base import BaseStashQueue, QueueEntry
from .queue.celery import LocalCeleryQueue
from .queue.tempdir import TempDirQueue
@@ -25,7 +25,6 @@ from .refs import (
CELERY_FAILED_STASH,
CELERY_STASH,
EXEC_APPLY,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
@@ -33,7 +32,7 @@ from .refs import (
ExpRefInfo,
)
from .stash import ExpStashEntry
from .utils import exp_refs_by_rev, scm_locked, unlocked_repo
from .utils import exp_refs_by_rev, unlocked_repo
logger = logging.getLogger(__name__)
@@ -51,18 +50,12 @@ class Experiments:
)
def __init__(self, repo):
from dvc.lock import make_lock
from dvc.scm import NoSCMError
if repo.config["core"].get("no_scm", False):
raise NoSCMError
self.repo = repo
self.scm_lock = make_lock(
os.path.join(self.repo.tmp_dir, "exp_scm_lock"),
tmp_dir=self.repo.tmp_dir,
hardlink_lock=repo.config["core"].get("hardlink_lock", False),
)
@property
def scm(self):
@@ -172,7 +165,18 @@ class Experiments:
) -> Dict[str, str]:
results: Dict[str, str] = {}
if entries is None:
entries = list(self.celery_queue.iter_queued())
entries = list(
chain(
self.celery_queue.iter_active(),
self.celery_queue.iter_queued(),
)
)
logger.debug(
"reproduce all these entries '%s'",
entries,
)
if not entries:
return results
@@ -190,7 +194,10 @@ class Experiments:
time.sleep(1)
self.celery_queue.follow(entry)
# wait for task collection to complete
result = self.celery_queue.get_result(entry)
try:
result = self.celery_queue.get_result(entry)
except FileNotFoundError:
result = None
if result is None or result.exp_hash is None:
name = entry.name or entry.stash_rev[:7]
failed.append(name)
@@ -211,8 +218,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:
@@ -239,7 +247,6 @@ class Experiments:
if self.scm.get_ref(str(exp_ref)):
raise ExperimentExistsError(exp_ref.name)
@scm_locked
def new(
self,
queue: BaseStashQueue,
@@ -265,7 +272,6 @@ class Experiments:
except ExperimentExistsError as err:
if not (kwargs.get("force", False) or kwargs.get("reset", False)):
raise err
return queue.put(*args, **kwargs)
def _resume_checkpoint(
@@ -411,86 +417,41 @@ 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
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Any]:
"""Return info for running experiments."""
result = {}
infofile = self.workspace_queue.get_infofile_path("workspace")
result.update(
self._fetch_running_exp("workspace", infofile, fetch_refs)
ref_dict = self.scm.describe(
revs, base=EXPS_NAMESPACE, exclude=exclude
)
for queue in (self.tempdir_queue, self.celery_queue):
for entry in queue.iter_active():
infofile = queue.get_infofile_path(entry.stash_rev)
result.update(
self._fetch_running_exp(
entry.stash_rev, infofile, fetch_refs
)
)
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 _fetch_running_exp(
self, rev: str, infofile: str, fetch_refs: bool
) -> Dict[str, Any]:
from dvc.scm import InvalidRemoteSCMRepo
from dvc.utils.serialize import load_json
from .executor.local import TempDirExecutor
result: Dict[str, Any] = {}
try:
info = ExecutorInfo.from_dict(load_json(infofile))
except OSError:
return result
if info.result is None:
if rev == "workspace":
# If we are appending to a checkpoint branch in a workspace
# run, show the latest checkpoint as running.
last_rev = self.scm.get_ref(EXEC_BRANCH)
if last_rev:
result[last_rev] = info.asdict()
else:
result[rev] = info.asdict()
else:
result[rev] = info.asdict()
if info.git_url and fetch_refs:
def on_diverged(_ref: str, _checkpoint: bool):
return False
executor = TempDirExecutor.from_info(info)
try:
for ref in executor.fetch_exps(
self.scm,
on_diverged=on_diverged,
):
logger.debug(
"Updated running experiment '%s'.", ref
)
last_rev = self.scm.get_ref(ref)
result[rev]["last"] = last_rev
if last_rev:
result[last_rev] = info.asdict()
except InvalidRemoteSCMRepo:
# ignore stale info files
del result[rev]
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
"""Return info for running experiments."""
result = {}
for queue in (
self.workspace_queue,
self.tempdir_queue,
self.celery_queue,
):
result.update(queue.get_running_exps(fetch_refs))
return result
def apply(self, *args, **kwargs):
@@ -518,6 +479,11 @@ class Experiments:
return run(self.repo, *args, **kwargs)
def save(self, *args, **kwargs):
from dvc.repo.experiments.save import save
return save(self.repo, *args, **kwargs)
def gc(self, *args, **kwargs):
from dvc.repo.experiments.gc import gc
+5 -5
View File
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
from dvc.repo.experiments.show import _collect_experiment_commit
from dvc.repo.experiments.show import collect_experiment_commit
from dvc.scm import resolve_rev
if repo.scm.no_commits:
@@ -15,15 +15,15 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
if a_rev:
rev = resolve_rev(repo.scm, a_rev)
old = _collect_experiment_commit(repo, rev, param_deps=param_deps)
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
else:
old = _collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
if b_rev:
rev = resolve_rev(repo.scm, b_rev)
new = _collect_experiment_commit(repo, rev, param_deps=param_deps)
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
else:
new = _collect_experiment_commit(
new = collect_experiment_commit(
repo, "workspace", param_deps=param_deps
)
+87 -55
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,
@@ -11,6 +12,7 @@ from typing import (
Callable,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Tuple,
@@ -19,6 +21,8 @@ from typing import (
Union,
)
from scmrepo.exceptions import SCMError
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
from dvc.exceptions import DvcException
from dvc.stage.serialize import to_lockfile
@@ -31,15 +35,7 @@ from ..exceptions import (
ExperimentExistsError,
UnchangedExperimentError,
)
from ..refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
EXPS_STASH,
ExpRefInfo,
)
from ..refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_CHECKPOINT, ExpRefInfo
if TYPE_CHECKING:
from queue import Queue
@@ -47,23 +43,28 @@ 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__)
EXEC_TMP_DIR = "exps"
EXEC_PID_DIR = "run"
class ExecutorResult(NamedTuple):
exp_hash: Optional[str]
ref_info: Optional["ExpRefInfo"]
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 +74,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 +140,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 +154,18 @@ 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,
repo: "Repo",
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 +205,7 @@ class BaseExecutor(ABC):
dvc_dir=self.dvc_dir,
name=self.name,
wdir=self.wdir,
status=self.status,
**result_dict,
)
@@ -212,6 +227,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 +238,6 @@ class BaseExecutor(ABC):
def from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
**kwargs,
) -> _T:
@@ -232,7 +247,6 @@ class BaseExecutor(ABC):
def _from_stash_entry(
cls: Type[_T],
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
root_dir: str,
**kwargs,
@@ -241,12 +255,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 +272,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
@@ -290,6 +307,7 @@ class BaseExecutor(ABC):
def fetch_exps(
self,
dest_scm: "Git",
refs: List[str],
force: bool = False,
on_diverged: Callable[[str, bool], None] = None,
**kwargs,
@@ -298,56 +316,54 @@ class BaseExecutor(ABC):
Args:
dest_scm: Destination Git instance.
refs: reference names to be fetched from the remotes.
force: If True, diverged refs will be overwritten
on_diverged: Callback in the form on_diverged(ref, is_checkpoint)
to be called when an experiment ref has diverged.
Extra kwargs will be passed into the remote git client.
"""
from ..utils import iter_remote_refs
refs = []
has_checkpoint = False
for ref in iter_remote_refs(
dest_scm,
self.git_url,
base=EXPS_NAMESPACE,
**kwargs,
):
if ref == EXEC_CHECKPOINT:
has_checkpoint = True
elif not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
refs.append(ref)
if EXEC_CHECKPOINT in refs:
refs.remove(EXEC_CHECKPOINT)
has_checkpoint = True
else:
has_checkpoint = False
def on_diverged_ref(orig_ref: str, new_rev: str):
if force:
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
# fetch experiments
dest_scm.fetch_refspecs(
self.git_url,
[f"{ref}:{ref}" for ref in refs],
on_diverged=on_diverged_ref,
force=force,
**kwargs,
)
# update last run checkpoint (if it exists)
if has_checkpoint:
try:
dest_scm.fetch_refspecs(
self.git_url,
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
force=True,
[f"{ref}:{ref}" for ref in refs],
on_diverged=on_diverged_ref,
force=force,
**kwargs,
)
# update last run checkpoint (if it exists)
if has_checkpoint:
dest_scm.fetch_refspecs(
self.git_url,
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
force=True,
**kwargs,
)
except SCMError:
pass
return refs
@classmethod
@@ -411,11 +427,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 +505,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 +561,7 @@ class BaseExecutor(ABC):
def _repro_dvc(
cls,
info: "ExecutorInfo",
infofile: str = None,
log_errors: bool = True,
**kwargs,
):
@@ -557,6 +569,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,17 +583,23 @@ class BaseExecutor(ABC):
try:
logger.debug("Running repro in '%s'", os.getcwd())
yield dvc
info.status = TaskStatus.SUCCESS
except CheckpointKilledError:
info.status = TaskStatus.FAILED
raise
except DvcException:
if log_errors:
logger.exception("")
info.status = TaskStatus.FAILED
raise
except Exception:
if log_errors:
logger.exception("unexpected error")
info.status = TaskStatus.FAILED
raise
finally:
if infofile is not None:
info.dump_json(infofile)
dvc.close()
os.chdir(old_cwd)
@@ -714,3 +735,14 @@ class BaseExecutor(ABC):
disable_other_loggers()
if level is not None:
dvc_logger.setLevel(level)
@contextmanager
def set_temp_refs(self, scm: "Git", temp_dict: Dict[str, str]):
try:
for ref, rev in temp_dict.items():
scm.set_ref(ref, rev)
yield
finally:
for ref in temp_dict:
if scm.get_ref(ref):
scm.remove_ref(ref)
+156 -47
View File
@@ -2,11 +2,14 @@ import logging
import os
from contextlib import ExitStack
from tempfile import mkdtemp
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, List, Optional
from funcy import cached_property
from funcy import cached_property, retry
from scmrepo.exceptions import SCMError as _SCMError
from shortuuid import uuid
from dvc.exceptions import DvcException
from dvc.lock import LockError
from dvc.scm import SCM, GitMergeError
from dvc.utils.fs import makedirs, remove
@@ -18,16 +21,19 @@ from ..refs import (
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
EXPS_TEMP,
ExpRefInfo,
)
from .base import EXEC_TMP_DIR, BaseExecutor
from ..utils import EXEC_TMP_DIR, get_exp_rwlock
from .base import BaseExecutor, ExecutorResult, TaskStatus
if TYPE_CHECKING:
from scmrepo.git import Git
from dvc.repo import Repo
from ..refs import ExpRefInfo
from ..stash import ExpStashEntry
from .base import ExecutorInfo
logger = logging.getLogger(__name__)
@@ -46,10 +52,10 @@ class BaseLocalExecutor(BaseExecutor):
def scm(self):
return SCM(self.root_dir)
def cleanup(self):
super().cleanup()
def cleanup(self, infofile: str):
self.scm.close()
del self.scm
super().cleanup(infofile)
def collect_cache(
self, repo: "Repo", exp_ref: "ExpRefInfo", run_cache: bool = True
@@ -67,23 +73,62 @@ class TempDirExecutor(BaseLocalExecutor):
QUIET = True
DEFAULT_LOCATION = "tempdir"
def init_git(self, scm: "Git", branch: Optional[str] = None):
@retry(180, errors=LockError, timeout=1)
def init_git(
self,
repo: "Repo",
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)
if branch:
push_refspec(scm, self.git_url, branch, branch)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
elif self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
self.status = TaskStatus.PREPARING
if infofile:
self.info.dump_json(infofile)
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
temp_head = f"{EXPS_TEMP}/head-{uuid()}"
temp_merge = f"{EXPS_TEMP}/merge-{uuid()}"
temp_baseline = f"{EXPS_TEMP}/baseline-{uuid()}"
temp_ref_dict = {
temp_head: entry.head_rev,
temp_merge: stash_rev,
temp_baseline: entry.baseline_rev,
}
with get_exp_rwlock(
repo, writes=[temp_head, temp_merge, temp_baseline]
), self.set_temp_refs(scm, temp_ref_dict):
# 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
refspec = [
(temp_head, EXEC_HEAD),
(temp_merge, EXEC_MERGE),
(temp_baseline, EXEC_BASELINE),
]
if branch:
refspec.append((branch, branch))
with get_exp_rwlock(repo, reads=[branch]):
push_refspec(scm, self.git_url, refspec)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
else:
push_refspec(scm, self.git_url, refspec)
if self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
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
@@ -91,10 +136,6 @@ class TempDirExecutor(BaseLocalExecutor):
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 +155,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 +164,6 @@ class TempDirExecutor(BaseLocalExecutor):
def from_stash_entry(
cls,
repo: "Repo",
stash_rev: str,
entry: "ExpStashEntry",
wdir: Optional[str] = None,
**kwargs,
@@ -132,9 +172,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,39 +190,57 @@ 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):
self._detach_stack.enter_context(
self.scm.detach_head(
self.scm.get_ref(EXEC_HEAD),
force=True,
client="dvc",
@retry(180, errors=LockError, timeout=1)
def init_git(
self,
repo: "Repo",
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)
with get_exp_rwlock(repo, writes=[EXEC_NAMESPACE]):
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),
force=True,
client="dvc",
)
)
)
merge_rev = self.scm.get_ref(EXEC_MERGE)
try:
self.scm.merge(merge_rev, squash=True, commit=False)
except _SCMError as exc:
raise GitMergeError(str(exc), scm=self.scm)
if branch:
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
elif scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
merge_rev = self.scm.get_ref(EXEC_MERGE)
try:
self.scm.merge(merge_rev, squash=True, commit=False)
except _SCMError as exc:
raise GitMergeError(str(exc), scm=self.scm)
if branch:
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
elif scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
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 +249,57 @@ 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()
@classmethod
def save(
cls,
info: "ExecutorInfo",
force: bool = False,
include_untracked: Optional[List[str]] = None,
) -> ExecutorResult:
from dvc.repo import Repo
exp_hash: Optional[str] = None
exp_ref: Optional[ExpRefInfo] = None
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
old_cwd = os.getcwd()
if info.wdir:
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
else:
os.chdir(dvc.root_dir)
try:
stages = dvc.commit([], force=force)
exp_hash = cls.hash_exp(stages)
if include_untracked:
dvc.scm.add(include_untracked)
cls.commit(
dvc.scm,
exp_hash,
exp_name=info.name,
force=force,
)
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
exp_ref = ExpRefInfo.from_ref(ref) if ref else None
untracked = dvc.scm.untracked_files()
if untracked:
logger.warning(
"The following untracked files were present in "
"the workspace before saving but "
"will not be included in the experiment commit:\n"
"\t%s",
", ".join(untracked),
)
info.result_hash = exp_hash
info.result_ref = ref
info.result_force = False
info.status = TaskStatus.SUCCESS
except DvcException:
info.status = TaskStatus.FAILED
raise
finally:
dvc.close()
os.chdir(old_cwd)
return ExecutorResult(ref, exp_ref, info.result_force)
@@ -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()
+29 -9
View File
@@ -9,13 +9,14 @@ from dvc_ssh import SSHFileSystem
from funcy import first
from ..refs import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
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 +24,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 +76,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 +118,21 @@ class SSHExecutor(BaseExecutor):
}
return kwargs
def init_git(self, scm: "Git", branch: Optional[str] = None):
def init_git(
self,
repo: "Repo",
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,10 +148,19 @@ 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)
ref_dict = {
EXEC_HEAD: entry.head_rev,
EXEC_MERGE: stash_rev,
EXEC_BASELINE: entry.baseline_rev,
}
with self.set_temp_refs(scm, ref_dict):
exec_namespace = f"{EXEC_NAMESPACE}/"
refspec = [(exec_namespace, exec_namespace)]
push_refspec(scm, self.git_url, refspec, **kwargs)
if branch:
push_refspec(scm, self.git_url, branch, branch, **kwargs)
push_refspec(scm, self.git_url, [(branch, branch)], **kwargs)
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
else:
self._ssh_cmd(
+10 -8
View File
@@ -248,11 +248,6 @@ def init(
except MissingParamsFile:
pass
models = context.get("models")
live_path = context.pop("live", None)
live_metrics = f"{live_path}.json" if live_path else None
live_plots = os.path.join(live_path, "scalars") if live_path else None
if type == "checkpoint":
outs_key = "checkpoints"
metrics_key = "metrics_persist_no_cache"
@@ -262,6 +257,13 @@ def init(
metrics_key = "metrics_no_cache"
plots_key = "plots_no_cache"
models = [context.get("models")]
metrics = [context.get("metrics")]
plots = [context.get("plots")]
if live_path := context.pop("live", None):
metrics.append(os.path.join(live_path, "metrics.json"))
plots.append(os.path.join(live_path, "plots"))
stage = repo.stage.create(
name=name,
cmd=context["cmd"],
@@ -269,9 +271,9 @@ def init(
params=[{params: None}] if params else None,
force=force,
**{
outs_key: compact([models]),
metrics_key: compact([context.get("metrics"), live_metrics]),
plots_key: compact([context.get("plots"), live_plots]),
outs_key: compact(models),
metrics_key: compact(metrics),
plots_key: compact(plots),
},
)
+17 -14
View File
@@ -6,7 +6,7 @@ from dvc.repo.scm_context import scm_context
from dvc.scm import iter_revs
from dvc.types import Optional
from .utils import exp_refs, exp_refs_by_baseline
from .utils import exp_refs_by_baseline
logger = logging.getLogger(__name__)
@@ -20,19 +20,22 @@ def ls(
num: int = 1,
git_remote: Optional[str] = None,
):
results = defaultdict(list)
if all_commits:
gen = exp_refs(repo.scm, git_remote)
for info in gen:
results[info.baseline_sha].append(info.name)
return results
rev = rev or "HEAD"
revs = iter_revs(repo.scm, [rev], num)
rev_set = set(revs.keys())
rev_set = None
if not all_commits:
revs = iter_revs(repo.scm, [rev or "HEAD"], num)
rev_set = set(revs.keys())
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
for rev, ref_info_list in ref_info_dict.items():
results[rev] = [ref_info.name for ref_info in ref_info_list]
tags = repo.scm.describe(ref_info_dict.keys())
remained = {baseline for baseline, tag in tags.items() if tag is None}
base = "refs/heads"
ref_heads = repo.scm.describe(remained, base=base)
results = defaultdict(list)
for baseline in ref_info_dict:
name = baseline[:7]
if tags[baseline] or ref_heads[baseline]:
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
results[name] = [info.name for info in ref_info_dict[baseline]]
return results
+86 -53
View File
@@ -1,6 +1,5 @@
import logging
import os
import sys
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import (
@@ -18,24 +17,21 @@ 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.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.lock import LockError
from dvc.ui import ui
from ..exceptions import CheckpointExistsError, ExperimentExistsError
from ..executor.base import (
EXEC_PID_DIR,
EXEC_TMP_DIR,
BaseExecutor,
ExecutorResult,
)
from ..executor.base import BaseExecutor, 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
from ..utils import EXEC_PID_DIR, EXEC_TMP_DIR, exp_refs_by_rev, get_exp_rwlock
from .utils import get_remote_executor_refs
if TYPE_CHECKING:
from scmrepo.git import Git
@@ -268,6 +264,7 @@ class BaseStashQueue(ABC):
finish any active experiments before shutting down.
"""
@abstractmethod
def logs(
self,
rev: str,
@@ -399,7 +396,13 @@ class BaseStashQueue(ABC):
)
# save additional repro command line arguments
run_env = {DVCLIVE_RESUME: "1"} if resume_rev else {}
run_env = {
DVC_EXP_BASELINE_REV: baseline_rev,
}
if name:
run_env[DVC_EXP_NAME] = name
if resume_rev:
run_env[DVCLIVE_RESUME] = "1"
self._pack_args(*args, run_env=run_env, **kwargs)
# save experiment as a stash commit
@@ -524,16 +527,9 @@ class BaseStashQueue(ABC):
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
logger.debug("Using experiment params '%s'", params)
from dvc.utils.hydra import apply_overrides, compose_and_dump
try:
from dvc.utils.hydra import apply_overrides, compose_and_dump
except ValueError:
if sys.version_info >= (3, 11):
raise DvcException(
"--set-param is not supported in Python >= 3.11"
)
raise
logger.debug("Using experiment params '%s'", params)
hydra_config = self.repo.config.get("hydra", {})
hydra_enabled = hydra_config.get("enabled", False)
@@ -560,36 +556,50 @@ class BaseStashQueue(ABC):
self.scm.add(list(params.keys()))
@staticmethod
@scm_locked
def setup_executor(
@retry(180, errors=LockError, timeout=1)
def get_stash_entry(
exp: "Experiments",
queue_entry: QueueEntry,
) -> "ExpStashEntry":
stash = ExpStash(exp.scm, queue_entry.stash_ref)
stash_rev = queue_entry.stash_rev
with get_exp_rwlock(exp.repo, writes=[queue_entry.stash_ref]):
stash_entry = stash.stash_revs.get(
stash_rev,
ExpStashEntry(None, stash_rev, stash_rev, None, None),
)
if stash_entry.stash_index is not None:
stash.drop(stash_entry.stash_index)
return stash_entry
@classmethod
def init_executor(
cls,
exp: "Experiments",
queue_entry: QueueEntry,
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
**kwargs,
) -> BaseExecutor:
scm = exp.scm
stash = ExpStash(scm, queue_entry.stash_ref)
stash_entry = cls.get_stash_entry(exp, queue_entry)
executor = executor_cls.from_stash_entry(
exp.repo, stash_entry, **kwargs
)
stash_rev = queue_entry.stash_rev
stash_entry = stash.stash_revs.get(
infofile = exp.celery_queue.get_infofile_path(stash_rev)
executor.init_git(
exp.repo,
exp.repo.scm,
stash_rev,
ExpStashEntry(None, stash_rev, stash_rev, None, None),
stash_entry,
infofile,
branch=stash_entry.branch,
)
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.init_cache(exp.repo, stash_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
)
return executor
def get_infofile_path(self, name: str) -> str:
return os.path.join(
@@ -599,8 +609,8 @@ class BaseStashQueue(ABC):
)
@staticmethod
@scm_locked
def collect_executor(
@retry(180, errors=LockError, timeout=1)
def collect_git(
exp: "Experiments",
executor: BaseExecutor,
exec_result: ExecutorResult,
@@ -613,16 +623,31 @@ class BaseStashQueue(ABC):
raise CheckpointExistsError(ref_info.name)
raise ExperimentExistsError(ref_info.name)
for ref in executor.fetch_exps(
exp.scm,
force=exec_result.force,
on_diverged=on_diverged,
):
exp_rev = exp.scm.get_ref(ref)
if exp_rev:
assert exec_result.exp_hash
logger.debug("Collected experiment '%s'.", exp_rev[:7])
results[exp_rev] = exec_result.exp_hash
refs = get_remote_executor_refs(exp.scm, executor.git_url)
with get_exp_rwlock(exp.repo, writes=refs):
for ref in executor.fetch_exps(
exp.scm,
refs,
force=exec_result.force,
on_diverged=on_diverged,
):
exp_rev = exp.scm.get_ref(ref)
if exp_rev:
assert exec_result.exp_hash
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)
@@ -690,3 +715,11 @@ class BaseStashQueue(ABC):
entry.stash_rev,
message=f"commit: {msg}",
)
@abstractmethod
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
"""Get the execution info of the currently running experiments
Args:
fetch_ref (bool): fetch completed checkpoints or not.
"""
+110 -38
View File
@@ -15,7 +15,7 @@ from typing import (
)
from celery.result import AsyncResult
from funcy import cached_property
from funcy import cached_property, first
from kombu.message import Message
from dvc.daemon import daemonize
@@ -23,9 +23,13 @@ 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 ExecutorInfo, ExecutorResult
from ..refs import CELERY_STASH
from ..utils import EXEC_TMP_DIR, get_exp_rwlock
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
from .exceptions import CannotKillTasksError
from .tasks import run_exp
from .utils import fetch_running_exp_from_temp_dir
if TYPE_CHECKING:
from dvc_task.app import FSApp
@@ -162,7 +166,8 @@ class LocalCeleryQueue(BaseStashQueue):
def put(self, *args, **kwargs) -> QueueEntry:
"""Stash an experiment and add it to the queue."""
entry = self._stash_exp(*args, **kwargs)
with get_exp_rwlock(self.repo, writes=["workspace", CELERY_STASH]):
entry = self._stash_exp(*args, **kwargs)
self.celery.signature(run_exp.s(entry.asdict())).delay()
return entry
@@ -181,6 +186,7 @@ class LocalCeleryQueue(BaseStashQueue):
continue
args, kwargs, _embed = msg.decode()
entry_dict = kwargs.get("entry_dict", args[0])
logger.debug("Found queued task %s", entry_dict["stash_rev"])
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
@@ -197,6 +203,7 @@ class LocalCeleryQueue(BaseStashQueue):
task_id = msg.headers["id"]
result: AsyncResult = AsyncResult(task_id)
if not result.ready():
logger.debug("Found active task %s", entry.stash_rev)
yield _TaskEntry(result, entry)
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
@@ -205,6 +212,7 @@ class LocalCeleryQueue(BaseStashQueue):
task_id = msg.headers["id"]
result: AsyncResult = AsyncResult(task_id)
if result.ready():
logger.debug("Found done task %s", entry.stash_rev)
yield _TaskEntry(result, entry)
def iter_active(self) -> Generator[QueueEntry, None, None]:
@@ -237,67 +245,115 @@ class LocalCeleryQueue(BaseStashQueue):
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
raise NotImplementedError
def get_result(
def _load_info(self, rev: str) -> ExecutorInfo:
infofile = self.get_infofile_path(rev)
return ExecutorInfo.load_json(infofile)
def _get_done_result(
self, entry: QueueEntry, timeout: Optional[float] = None
) -> Optional[ExecutorResult]:
) -> Optional["ExecutorResult"]:
from celery.exceptions import TimeoutError as _CeleryTimeout
def _load_info(rev: str) -> ExecutorInfo:
infofile = self.get_infofile_path(rev)
return ExecutorInfo.load_json(infofile)
def _load_collected(rev: str) -> Optional[ExecutorResult]:
executor_info = _load_info(rev)
if executor_info.collected:
for msg, processed_entry in self._iter_processed():
if entry.stash_rev == processed_entry.stash_rev:
task_id = msg.headers["id"]
result: AsyncResult = AsyncResult(task_id)
if not result.ready():
logger.debug(
"Waiting for exp task '%s' to complete", result.id
)
try:
result.get(timeout=timeout)
except _CeleryTimeout as exc:
raise DvcException(
"Timed out waiting for exp to finish."
) from exc
executor_info = self._load_info(entry.stash_rev)
return executor_info.result
raise FileNotFoundError
raise FileNotFoundError
def get_result(
self, entry: QueueEntry, timeout: Optional[float] = None
) -> Optional["ExecutorResult"]:
try:
return _load_collected(entry.stash_rev)
return self._get_done_result(entry, timeout)
except FileNotFoundError:
# Infofile will not be created until execution begins
pass
for queue_entry in self.iter_queued():
if entry.stash_rev == queue_entry.stash_rev:
raise DvcException("Experiment has not been started.")
for result, active_entry in self._iter_active_tasks():
if entry.stash_rev == active_entry.stash_rev:
logger.debug(
"Waiting for exp task '%s' to complete", result.id
)
try:
result.get(timeout=timeout)
except _CeleryTimeout as exc:
raise DvcException(
"Timed out waiting for exp to finish."
) from exc
executor_info = _load_info(entry.stash_rev)
return executor_info.result
# NOTE: It's possible for an exp to complete while iterating through
# other queued and active tasks, in which case the exp will get moved
# out of the active task list, and needs to be loaded here.
return _load_collected(entry.stash_rev)
return self._get_done_result(entry, timeout)
def _get_running_task_ids(self) -> Set[str]:
running_task_ids: Set[str] = set()
active_workers = self.worker_status()
for _, tasks in active_workers.items():
task = first(tasks)
if task:
running_task_ids.add(task["id"])
return running_task_ids
def _try_to_kill_tasks(
self, to_kill: Dict[QueueEntry, str]
) -> Dict[QueueEntry, str]:
fail_to_kill_entries: Dict[QueueEntry, str] = {}
for queue_entry, rev in to_kill.items():
try:
self.proc.kill(queue_entry.stash_rev)
logger.debug(f"Task {rev} had been killed.")
except ProcessLookupError:
fail_to_kill_entries[queue_entry] = rev
return fail_to_kill_entries
def _mark_inactive_tasks_failure(self, remained_entries):
remained_revs: List[str] = []
running_ids = self._get_running_task_ids()
logger.debug(f"Current running tasks ids: {running_ids}.")
for msg, entry in self._iter_processed():
if entry not in remained_entries:
continue
task_id = msg.headers["id"]
if task_id in running_ids:
remained_revs.append(remained_entries[entry])
else:
result: AsyncResult = AsyncResult(task_id)
if not result.ready():
logger.debug(
f"Task id {task_id} rev {remained_entries[entry]} "
"marked as failure."
)
self.celery.backend.mark_as_failure(task_id, None)
if remained_revs:
raise CannotKillTasksError(remained_revs)
def kill(self, revs: Collection[str]) -> None:
to_kill: Set[QueueEntry] = set()
name_dict: Dict[
str, Optional[QueueEntry]
] = self.match_queue_entry_by_name(set(revs), self.iter_active())
missing_rev: List[str] = []
to_kill: Dict[QueueEntry, str] = {}
missing_revs: List[str] = []
for rev, queue_entry in name_dict.items():
if queue_entry is None:
missing_rev.append(rev)
missing_revs.append(rev)
else:
to_kill.add(queue_entry)
to_kill[queue_entry] = rev
if missing_revs:
raise UnresolvedQueueExpNamesError(missing_revs)
if missing_rev:
raise UnresolvedQueueExpNamesError(missing_rev)
fail_to_kill_entries: Dict[QueueEntry, str] = self._try_to_kill_tasks(
to_kill
)
for queue_entry in to_kill:
self.proc.kill(queue_entry.stash_rev)
if fail_to_kill_entries:
self._mark_inactive_tasks_failure(fail_to_kill_entries)
def shutdown(self, kill: bool = False):
self.celery.control.shutdown()
@@ -353,10 +409,11 @@ class LocalCeleryQueue(BaseStashQueue):
) as fobj:
ui.write(fobj.read())
def worker_status(self) -> Dict:
def worker_status(self) -> Dict[str, List[Dict]]:
"""Return the current active celery worker"""
status = self.celery.control.inspect().active() or {}
logger.debug(f"Worker status: {status}")
return status
def clear(self, *args, **kwargs):
@@ -368,3 +425,18 @@ class LocalCeleryQueue(BaseStashQueue):
from .remove import celery_remove
return celery_remove(self, *args, **kwargs)
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
"""Get the execution info of the currently running experiments
Args:
fetch_ref (bool): fetch completed checkpoints or not.
"""
result: Dict[str, Dict] = {}
for entry in self.iter_active():
result.update(
fetch_running_exp_from_temp_dir(
self, entry.stash_rev, fetch_refs
)
)
return result
+12
View File
@@ -0,0 +1,12 @@
from typing import Collection
from dvc.exceptions import DvcException
class CannotKillTasksError(DvcException):
def __init__(self, revs: Collection[str]):
rev_str = ",".join(revs)
super().__init__(
f"Task '{rev_str}' is initializing, please wait a few seconds "
"until the experiments start running to retry the kill operation."
)
+12 -22
View File
@@ -1,19 +1,21 @@
from typing import Any, Dict
from typing import TYPE_CHECKING, 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
if TYPE_CHECKING:
from ..executor.base import BaseExecutor
logger = get_task_logger(__name__)
@shared_task
def setup_exp(entry_dict: Dict[str, Any]) -> str:
def setup_exp(entry_dict: Dict[str, Any]) -> "BaseExecutor":
"""Setup an experiment.
Arguments:
@@ -28,7 +30,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 +38,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 +81,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 +106,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)()
+15 -4
View File
@@ -6,14 +6,15 @@ from funcy import cached_property, first
from ..exceptions import ExpQueueEmptyError
from ..executor.base import (
EXEC_PID_DIR,
EXEC_TMP_DIR,
BaseExecutor,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from ..executor.local import TempDirExecutor
from ..utils import EXEC_PID_DIR, EXEC_TMP_DIR
from .base import BaseStashQueue, QueueEntry, QueueGetResult
from .utils import fetch_running_exp_from_temp_dir
from .workspace import WorkspaceQueue
if TYPE_CHECKING:
@@ -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,7 +76,7 @@ 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(
if executor_info.status <= TaskStatus.SUCCESS and os.path.exists(
executor_info.root_dir
):
yield QueueEntry(
@@ -96,3 +97,13 @@ class TempDirQueue(WorkspaceQueue):
exec_result: ExecutorResult,
) -> Dict[str, str]:
return BaseStashQueue.collect_executor(exp, executor, exec_result)
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
result: Dict[str, Dict] = {}
for entry in self.iter_active():
result.update(
fetch_running_exp_from_temp_dir(
self, entry.stash_rev, fetch_refs
)
)
return result
+86
View File
@@ -0,0 +1,86 @@
import logging
from typing import TYPE_CHECKING, Dict, List
from scmrepo.exceptions import SCMError
from ..executor.base import ExecutorInfo, TaskStatus
from ..refs import EXEC_CHECKPOINT, EXEC_NAMESPACE, EXPS_NAMESPACE, EXPS_STASH
from ..utils import get_exp_rwlock, iter_remote_refs
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from scmrepo.git import Git
from .base import BaseStashQueue
def get_remote_executor_refs(scm: "Git", remote_url: str) -> List[str]:
"""Get result list refs from a remote repository
Args:
remote_url : remote executor's url
"""
refs = []
for ref in iter_remote_refs(
scm,
remote_url,
base=EXPS_NAMESPACE,
):
if ref == EXEC_CHECKPOINT or (
not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH
):
refs.append(ref)
return refs
def fetch_running_exp_from_temp_dir(
queue: "BaseStashQueue", rev: str, fetch_refs: bool
) -> Dict[str, Dict]:
"""Fetch status of running exps out of current working directory
Args:
queue (BaseStashQueue):
rev (str): stash revision of the experiment
fetch_refs (bool): fetch running checkpoint results to local or not.
Returns:
Dict[str, Dict]: _description_
"""
from dvc.scm import InvalidRemoteSCMRepo
from dvc.utils.serialize import load_json
from ..executor.local import TempDirExecutor
result: Dict[str, Dict] = {}
infofile = queue.get_infofile_path(rev)
try:
info = ExecutorInfo.from_dict(load_json(infofile))
except OSError:
return result
if info.status <= TaskStatus.RUNNING:
result[rev] = info.asdict()
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
def on_diverged(_ref: str, _checkpoint: bool):
return True
executor = TempDirExecutor.from_info(info)
try:
refs = get_remote_executor_refs(queue.scm, executor.git_url)
with get_exp_rwlock(queue.repo, writes=refs):
for ref in executor.fetch_exps(
queue.scm,
refs,
on_diverged=on_diverged,
):
logger.debug("Updated running experiment '%s'.", ref)
last_rev = queue.scm.get_ref(ref)
result[rev]["last"] = last_rev
if last_rev:
result[last_rev] = info.asdict()
except (InvalidRemoteSCMRepo, SCMError):
# ignore stale info files
del result[rev]
return result
+106 -9
View File
@@ -1,17 +1,28 @@
import json
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Collection, Dict, Generator, Optional
from typing import TYPE_CHECKING, Collection, Dict, Generator, List, Optional
import psutil
from funcy import first
from voluptuous import Invalid
from dvc.exceptions import DvcException
from dvc.lock import make_lock
from dvc.rwlock import RWLOCK_FILE, RWLOCK_LOCK, SCHEMA
from dvc.utils import relpath
from dvc.utils.fs import remove
from ..exceptions import ExpQueueEmptyError
from ..executor.base import BaseExecutor, ExecutorResult
from ..executor.base import (
BaseExecutor,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from ..executor.local import WorkspaceExecutor
from ..refs import EXEC_BRANCH
from ..refs import EXEC_BRANCH, WORKSPACE_STASH
from ..utils import get_exp_rwlock
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
if TYPE_CHECKING:
@@ -24,7 +35,8 @@ class WorkspaceQueue(BaseStashQueue):
_EXEC_NAME: Optional[str] = "workspace"
def put(self, *args, **kwargs) -> QueueEntry:
return self._stash_exp(*args, **kwargs)
with get_exp_rwlock(self.repo, writes=["workspace", WORKSPACE_STASH]):
return self._stash_exp(*args, **kwargs)
def get(self) -> QueueGetResult:
revs = self.stash.stash_revs
@@ -41,7 +53,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 +130,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 +145,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]:
@@ -153,3 +164,89 @@ class WorkspaceQueue(BaseStashQueue):
follow: bool = False,
):
raise NotImplementedError
def check_rwlock(
self,
hardlink: bool = False,
autocorrect: bool = False,
) -> bool:
"""Check and autocorrect the RWLock status for file paths.
Args:
hardlink (bool): use hardlink lock to guard rwlock file when on
edit.
autocorrect (bool): autocorrect corrupted rwlock file.
Return:
(bool): if the pid alive.
"""
path = self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_FILE)
rwlock_guard = make_lock(
self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_LOCK),
tmp_dir=self.repo.tmp_dir,
hardlink_lock=hardlink,
)
with rwlock_guard:
try:
with self.repo.fs.open(path, encoding="utf-8") as fobj:
lock: Dict[str, List[Dict]] = SCHEMA(json.load(fobj))
file_path = first(lock["read"])
if not file_path:
return False
lock_info = first(lock["read"][file_path])
pid = int(lock_info["pid"])
if psutil.pid_exists(pid):
return True
cmd = lock_info["cmd"]
logger.warning(
"Process '%s' with (Pid %s), in RWLock-file '%s'"
" had been killed.",
cmd,
pid,
relpath(path),
)
except FileNotFoundError:
return False
except json.JSONDecodeError:
logger.warning(
"Unable to read RWLock-file '%s'. JSON structure is"
" corrupted",
relpath(path),
)
except Invalid:
logger.warning("RWLock-file '%s' format error.", relpath(path))
if autocorrect:
logger.warning(
"Delete corrupted RWLock-file '%s'", relpath(path)
)
remove(path)
return False
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
from dvc.utils.serialize import load_json
assert self._EXEC_NAME
result: Dict[str, Dict] = {}
if not self.check_rwlock(autocorrect=True):
return result
infofile = self.get_infofile_path(self._EXEC_NAME)
try:
info = ExecutorInfo.from_dict(load_json(infofile))
except OSError:
return result
if info.status < TaskStatus.FAILED:
# If we are appending to a checkpoint branch in a workspace
# run, show the latest checkpoint as running.
if info.status == TaskStatus.SUCCESS:
return result
last_rev = self.scm.get_ref(EXEC_BRANCH)
if last_rev:
result[last_rev] = info.asdict()
else:
result[self._EXEC_NAME] = info.asdict()
return result
+6 -12
View File
@@ -16,6 +16,7 @@ EXEC_BRANCH = f"{EXEC_NAMESPACE}/EXEC_BRANCH"
EXEC_BASELINE = f"{EXEC_NAMESPACE}/EXEC_BASELINE"
EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
EXEC_MERGE = f"{EXEC_NAMESPACE}/EXEC_MERGE"
EXPS_TEMP = f"{EXPS_NAMESPACE}/temp"
STASHES = {WORKSPACE_STASH, CELERY_STASH}
@@ -23,9 +24,7 @@ class ExpRefInfo:
namespace = EXPS_NAMESPACE
def __init__(
self, baseline_sha: Optional[str] = None, name: Optional[str] = None
):
def __init__(self, baseline_sha: str, name: Optional[str] = None):
self.baseline_sha = baseline_sha
self.name: str = name if name else ""
@@ -33,7 +32,7 @@ class ExpRefInfo:
return "/".join(self.parts)
def __repr__(self):
baseline = f"'{self.baseline_sha}'" if self.baseline_sha else "None"
baseline = f"'{self.baseline_sha}'"
name = f"'{self.name}'" if self.name else "None"
return f"ExpRefInfo(baseline_sha={baseline}, name={name})"
@@ -41,11 +40,7 @@ class ExpRefInfo:
def parts(self):
return (
(self.namespace,)
+ (
(self.baseline_sha[:2], self.baseline_sha[2:])
if self.baseline_sha
else ()
)
+ ((self.baseline_sha[:2], self.baseline_sha[2:]))
+ ((self.name,) if self.name else ())
)
@@ -54,14 +49,13 @@ class ExpRefInfo:
try:
parts = ref.split("/")
if (
len(parts) < 2
or len(parts) == 3
len(parts) < 4
or len(parts) > 5
or "/".join(parts[:2]) != EXPS_NAMESPACE
):
raise InvalidExpRefError(ref)
except ValueError:
raise InvalidExpRefError(ref)
baseline_sha = parts[2] + parts[3] if len(parts) >= 4 else None
baseline_sha = parts[2] + parts[3]
name = parts[4] if len(parts) == 5 else None
return cls(baseline_sha, name)
+1 -2
View File
@@ -152,8 +152,7 @@ def _remove_commited_exps(
push_refspec(
scm,
remote,
None,
str(ref_info),
[(None, str(ref_info))],
progress=pbar.update_git,
)
else:
+18 -15
View File
@@ -28,14 +28,24 @@ def run(
Returns a dict mapping new experiment SHAs to the results
of `repro` for that experiment.
"""
from dvc.utils.hydra import to_hydra_overrides
if run_all:
entries = list(repo.experiments.celery_queue.iter_queued())
return repo.experiments.reproduce_celery(entries, jobs=jobs)
return repo.experiments.reproduce_celery(jobs=jobs)
hydra_sweep = None
if params:
from dvc.utils.hydra import to_hydra_overrides
path_overrides = to_path_overrides(params)
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
else:
path_overrides = {}
@@ -45,23 +55,16 @@ def run(
# Force `_update_params` even if `--set-param` was not used
path_overrides[hydra_output_file] = []
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
if not queue:
return repo.experiments.reproduce_one(
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
)
if hydra_sweep:
if kwargs.get("name"):
raise InvalidArgumentError(
"Sweep overrides can't be used alongside `--name`"
)
from dvc.utils.hydra import get_hydra_sweeps
sweeps = get_hydra_sweeps(path_overrides)
+44
View File
@@ -0,0 +1,44 @@
import logging
import os
from typing import TYPE_CHECKING, List, Optional
from funcy import first
if TYPE_CHECKING:
from dvc.repo import Repo
logger = logging.getLogger(__name__)
def save(
repo: "Repo",
name: Optional[str] = None,
force: bool = False,
include_untracked: Optional[List[str]] = None,
) -> Optional[str]:
"""Save the current workspace status as an experiment.
Returns the saved experiment's SHAs.
"""
queue = repo.experiments.workspace_queue
logger.debug("Saving workspace in %s", os.getcwd())
staged, _, _ = repo.scm.status(untracked_files="no")
if staged:
logger.warning(
"Your workspace contains staged Git changes which will be "
"unstaged before saving this experiment."
)
repo.scm.reset()
entry = repo.experiments.new(queue=queue, name=name, force=force)
executor = queue.init_executor(repo.experiments, entry)
save_result = executor.save(
executor.info, force=force, include_untracked=include_untracked
)
result = queue.collect_executor(repo.experiments, executor, save_result)
exp_rev = first(result)
return exp_rev
+368 -109
View File
@@ -1,52 +1,89 @@
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 typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Set,
Tuple,
Union,
)
from scmrepo.exceptions import SCMError as InnerScmError
from dvc.repo.metrics.show import _gather_metrics
from dvc.repo.params.show import _gather_params
from dvc.scm import iter_revs
from dvc.scm import SCMError, iter_revs, resolve_rev
from dvc.utils import error_handler, onerror_collect, relpath
from .refs import ExpRefInfo
if TYPE_CHECKING:
from scmrepo.git import Git
from scmrepo.git.objects import GitCommit
from dvc.repo import Repo
logger = logging.getLogger(__name__)
class ExpStatus(Enum):
Success = 0
Queued = 1
Running = 2
Failed = 3
def _is_scm_error(collected_exp: Dict[str, Any]) -> bool:
if "error" in collected_exp and (
isinstance(collected_exp["error"], SCMError)
or isinstance(collected_exp["error"], InnerScmError)
):
return True
return False
def _show_onerror_collect(result: Dict, exception: Exception, *args, **kwargs):
onerror_collect(result, exception, *args, **kwargs)
result["data"] = {}
@error_handler
def _collect_experiment_commit(
repo,
exp_rev,
stash=False,
sha_only=True,
def collect_experiment_commit(
repo: "Repo",
exp_rev: str,
status: ExpStatus = ExpStatus.Success,
param_deps=False,
running=None,
running: Optional[Dict[str, Any]] = None,
onerror: Optional[Callable] = None,
is_baseline: bool = False,
):
) -> Dict[str, Any]:
from dvc.dependency import ParamsDependency, RepoDependency
res: Dict[str, Optional[Any]] = defaultdict(dict)
result: Dict[str, Any] = defaultdict(dict)
running = running or {}
for rev in repo.brancher(revs=[exp_rev]):
if rev == "workspace":
if exp_rev != "workspace":
continue
res["timestamp"] = None
result["timestamp"] = None
else:
commit = repo.scm.resolve_commit(rev)
res["timestamp"] = datetime.fromtimestamp(commit.commit_time)
result["timestamp"] = datetime.fromtimestamp(commit.commit_time)
params = _gather_params(
repo, rev=rev, targets=None, deps=param_deps, onerror=onerror
)
if params:
res["params"] = params
result["params"] = params
res["deps"] = {
result["deps"] = {
relpath(dep.fs_path, repo.root_dir): {
"hash": dep.hash_info.value,
"size": dep.meta.size,
@@ -55,7 +92,7 @@ def _collect_experiment_commit(
for dep in repo.index.deps
if not isinstance(dep, (ParamsDependency, RepoDependency))
}
res["outs"] = {
result["outs"] = {
relpath(out.fs_path, repo.root_dir): {
"hash": out.hash_info.value,
"size": out.meta.size,
@@ -67,66 +104,288 @@ 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["executor"] = running[exp_rev].get("location")
result["status"] = status.name
if status == ExpStatus.Running:
result["executor"] = running.get(exp_rev, {}).get("location", None)
else:
res["running"] = False
res["executor"] = None
if not stash:
result["executor"] = None
if status == ExpStatus.Failed:
result["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
result["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
return result
def _collect_experiment_branch(
res, repo, branch, baseline, onerror: Optional[Callable] = None, **kwargs
):
from dvc.scm import resolve_rev
def _collect_complete_experiment(
repo: "Repo",
baseline: str,
exp_rev: str,
running: Dict[str, Any],
revs: List[str],
**kwargs,
) -> Dict[str, Dict[str, Any]]:
results: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
checkpoint: bool = True if len(revs) > 1 else False
prev = ""
exp_rev = resolve_rev(repo.scm, branch)
prev = None
revs = list(repo.scm.branch_revs(exp_rev, baseline))
for rev in revs:
collected_exp = _collect_experiment_commit(
repo, rev, onerror=onerror, **kwargs
status = ExpStatus.Running if rev in running else ExpStatus.Success
collected_exp = collect_experiment_commit(
repo,
rev,
status=status,
running=running,
**kwargs,
)
if len(revs) > 1:
if _is_scm_error(collected_exp):
return {}
if checkpoint:
exp = {"checkpoint_tip": exp_rev}
if prev:
res[prev]["data"][ # type: ignore[unreachable]
results[prev]["data"][ # type: ignore[unreachable]
"checkpoint_parent"
] = rev
if rev in res:
res[rev]["data"].update(exp)
res.move_to_end(rev)
if rev in results:
results[rev]["data"].update(exp)
results.move_to_end(rev)
else:
exp.update(collected_exp["data"])
else:
exp = collected_exp["data"]
if rev not in res:
res[rev] = {"data": exp}
if rev not in results:
results[rev] = {"data": exp}
prev = rev
if len(revs) > 1:
res[prev]["data"]["checkpoint_parent"] = baseline
return res
if checkpoint and prev:
results[prev]["data"]["checkpoint_parent"] = baseline
return results
def _collect_branch(
repo: "Repo",
baseline: str,
running: Dict[str, Any],
**kwargs,
) -> Dict[str, Dict[str, Any]]:
results: Dict[str, Dict[str, Any]] = OrderedDict()
status = ExpStatus.Running if baseline in running else ExpStatus.Success
results["baseline"] = collect_experiment_commit(
repo,
baseline,
status=status,
running=running,
**kwargs,
)
if baseline == "workspace" or _is_scm_error(results["baseline"]):
return results
ref_info = ExpRefInfo(baseline_sha=baseline)
commits: List[Tuple[str, "GitCommit", str, List[str]]] = []
for ref in repo.scm.iter_refs(base=str(ref_info)):
try:
commit = repo.scm.resolve_commit(ref)
exp_rev = resolve_rev(repo.scm, ref)
revs = list(repo.scm.branch_revs(exp_rev, baseline))
except (SCMError, InnerScmError):
continue
commits.append((ref, commit, exp_rev, revs))
for exp_ref, _, exp_rev, revs in sorted(
commits, key=lambda x: x[1].commit_time, reverse=True
):
ref_info = ExpRefInfo.from_ref(exp_ref)
assert ref_info.baseline_sha == baseline
collected_exp = _collect_complete_experiment(
repo,
baseline=baseline,
exp_rev=exp_rev,
running=running,
revs=revs,
**kwargs,
)
if _is_scm_error(collected_exp):
continue
results.update(collected_exp)
return results
def get_branch_names(
scm: "Git", baseline_set: Iterable[str]
) -> Dict[str, Optional[str]]:
names: Dict[str, Optional[str]] = {}
for base in [
f"refs/exps/{baseline[:2]}/{baseline[2:]}/"
for baseline in baseline_set
] + ["refs/heads/", "refs/tags/"]:
for ref in scm.iter_refs(base=base):
if ref:
try:
rev = scm.get_ref(ref)
names[rev] = ref[len(base) :]
except KeyError:
logger.debug("unreosolved ref %s", ref)
logger.debug("found refs for revs: %s", names)
return names
def update_names(
repo: "Repo",
branch_names: Dict[str, Optional[str]],
result: Dict[str, Dict[str, Any]],
):
rev_set = set()
for baseline in result:
for rev in result[baseline]:
if rev == "baseline":
rev = baseline
if rev != "workspace":
rev_set.add(rev)
if rev_set:
rev_set.difference_update(branch_names.keys())
exact_name = repo.experiments.get_exact_name(rev_set)
for baseline, baseline_results in result.items():
name_set: Set[str] = set()
for rev, rev_result in baseline_results.items():
name: Optional[str] = None
if rev == "baseline":
rev = baseline
if rev == "workspace":
continue
name = branch_names.get(rev, None) or exact_name[rev]
if name and name not in name_set:
name_set.add(name)
rev_result["data"]["name"] = name
def _collect_active_experiment(
repo: "Repo",
found_revs: Dict[str, List[str]],
running: Dict[str, Any],
**kwargs,
) -> Dict[str, Dict[str, Any]]:
result: Dict[str, Dict] = defaultdict(OrderedDict)
for entry in chain(
repo.experiments.tempdir_queue.iter_active(),
repo.experiments.celery_queue.iter_active(),
):
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs and (
stash_rev not in running or not running[stash_rev].get("last")
):
collected_exp = collect_experiment_commit(
repo,
stash_rev,
status=ExpStatus.Running,
running=running,
**kwargs,
)
if _is_scm_error(collected_exp):
continue
result[entry.baseline_rev][stash_rev] = collected_exp
return result
def _collect_queued_experiment(
repo: "Repo",
found_revs: Dict[str, List[str]],
running: Dict[str, Any],
**kwargs,
) -> Dict[str, Dict[str, Any]]:
result: Dict[str, Dict] = defaultdict(OrderedDict)
for entry in repo.experiments.celery_queue.iter_queued():
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs:
collected_exp = collect_experiment_commit(
repo,
stash_rev,
status=ExpStatus.Queued,
running=running,
**kwargs,
)
if _is_scm_error(collected_exp):
continue
result[entry.baseline_rev][stash_rev] = collected_exp
return result
def _collect_failed_experiment(
repo: "Repo",
found_revs: Dict[str, List[str]],
running: Dict[str, Any],
**kwargs,
) -> Dict[str, Dict[str, Any]]:
result: Dict[str, Dict] = defaultdict(OrderedDict)
for queue_done_result in repo.experiments.celery_queue.iter_failed():
entry = queue_done_result.entry
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs:
collected_exp = collect_experiment_commit(
repo,
stash_rev,
status=ExpStatus.Failed,
running=running,
**kwargs,
)
if _is_scm_error(collected_exp):
continue
result[entry.baseline_rev][stash_rev] = collected_exp
return result
def update_new(
to_dict: Dict[str, Dict[str, Any]], from_dict: Dict[str, Dict[str, Any]]
):
for baseline, experiments in from_dict.items():
for rev, experiment in experiments.items():
to_dict[baseline][rev] = to_dict[baseline].get(rev, experiment)
def move_properties_to_head(result: Dict[str, Dict[str, Dict[str, Any]]]):
for _, baseline_results in result.items():
checkpoint: bool = False
head: Dict[str, Any] = {}
for rev, rev_data in baseline_results.items():
if (
"data" not in rev_data
or rev_data["data"].get("checkpoint_tip", None) is None
):
checkpoint = False
head = {}
continue
rev_result: Dict[str, Any] = rev_data["data"]
if (
checkpoint is True
and rev_result["checkpoint_tip"] == head["checkpoint_tip"]
):
if "name" in rev_result and "name" not in head:
head["name"] = rev_result["name"]
del rev_result["name"]
if rev_result["executor"]:
if not head["executor"]:
head["executor"] = rev_result["executor"]
rev_result["executor"] = None
if rev_result["status"] == ExpStatus.Running.name:
head["status"] = ExpStatus.Running.name
rev_result["status"] = ExpStatus.Success.name
else:
if rev_result["checkpoint_tip"] == rev:
head = rev_result
checkpoint = True
def show(
@@ -135,6 +394,8 @@ def show(
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,
@@ -145,8 +406,7 @@ def show(
if repo.scm.no_commits:
return {}
if onerror is None:
onerror = onerror_collect
onerror = onerror or _show_onerror_collect
res: Dict[str, Dict] = defaultdict(OrderedDict)
@@ -159,63 +419,62 @@ def show(
found_revs.update(
iter_revs(repo.scm, revs, num, all_branches, all_tags, all_commits)
)
branch_names = get_branch_names(repo.scm, found_revs)
running = repo.experiments.get_running_exps(fetch_refs=fetch_running)
running: Dict[str, Dict] = repo.experiments.get_running_exps(
fetch_refs=fetch_running
)
for rev in found_revs:
res[rev]["baseline"] = _collect_experiment_commit(
queued_experiment = (
_collect_queued_experiment(
repo,
rev,
sha_only=sha_only,
found_revs,
running,
param_deps=param_deps,
onerror=onerror,
)
if not hide_queued
else {}
)
active_experiment = _collect_active_experiment(
repo,
found_revs,
running,
param_deps=param_deps,
onerror=onerror,
)
failed_experiments = (
_collect_failed_experiment(
repo,
found_revs,
running,
param_deps=param_deps,
onerror=onerror,
)
if not hide_failed
else {}
)
for baseline in found_revs:
res[baseline] = _collect_branch(
repo,
baseline,
running=running,
param_deps=param_deps,
running=running,
onerror=onerror,
is_baseline=True,
)
if rev == "workspace":
continue
update_new(res, failed_experiments)
ref_info = ExpRefInfo(baseline_sha=rev)
commits = [
(ref, repo.scm.resolve_commit(ref))
for ref in repo.scm.iter_refs(base=str(ref_info))
]
for exp_ref, _ in sorted(
commits, key=lambda x: x[1].commit_time, reverse=True
):
ref_info = ExpRefInfo.from_ref(exp_ref)
assert ref_info.baseline_sha == rev
_collect_experiment_branch(
res[rev],
repo,
exp_ref,
rev,
sha_only=sha_only,
param_deps=param_deps,
running=running,
onerror=onerror,
)
update_new(res, active_experiment)
update_new(res, queued_experiment)
if not sha_only:
update_names(repo, branch_names, res)
move_properties_to_head(res)
# collect standalone & celery experiments
for entry in chain(
repo.experiments.tempdir_queue.iter_active(),
repo.experiments.celery_queue.iter_active(),
repo.experiments.celery_queue.iter_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(
"last"
):
experiment = _collect_experiment_commit(
repo,
stash_rev,
sha_only=sha_only,
stash=stash_rev not in running,
param_deps=param_deps,
running=running,
onerror=onerror,
)
res[entry.baseline_rev][stash_rev] = experiment
return res
+47 -26
View File
@@ -1,6 +1,9 @@
import os
import sys
from collections import defaultdict
from functools import wraps
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Generator,
@@ -9,6 +12,7 @@ from typing import (
Mapping,
Optional,
Set,
Tuple,
Union,
)
@@ -16,6 +20,7 @@ from scmrepo.git import Git
from dvc.exceptions import InvalidArgumentError
from dvc.repo.experiments.exceptions import AmbiguousExpRefInfo
from dvc.rwlock import rwlock
from .refs import (
EXEC_APPLY,
@@ -28,18 +33,34 @@ from .refs import (
ExpRefInfo,
)
if TYPE_CHECKING:
from dvc.repo import Repo
def scm_locked(f):
# Lock the experiments workspace so that we don't try to perform two
# different sequences of git operations at once
@wraps(f)
def wrapper(exp, *args, **kwargs):
from dvc.scm import map_scm_exception
with map_scm_exception(), exp.scm_lock:
return f(exp, *args, **kwargs)
EXEC_TMP_DIR = "exps"
EXEC_PID_DIR = "run"
return wrapper
def get_exp_rwlock(
repo: "Repo",
reads: Optional[List[str]] = None,
writes: Optional[List[str]] = None,
):
reads = reads or []
writes = writes or []
cmd = " ".join(sys.argv)
path = os.path.join(repo.tmp_dir, EXEC_TMP_DIR)
repo.fs.makedirs(path, exist_ok=True)
return rwlock(
path,
repo.fs,
cmd,
reads,
writes,
repo.config["core"].get("hardlink_lock", False),
)
def unlocked_repo(f):
@@ -79,13 +100,13 @@ def exp_refs_by_rev(scm: "Git", rev: str) -> Generator[ExpRefInfo, None, None]:
def exp_refs_by_baseline(
scm: "Git", revs: Set[str], url: Optional[str] = None
scm: "Git", revs: Optional[Set[str]] = None, url: Optional[str] = None
) -> Mapping[str, List[ExpRefInfo]]:
"""Iterate over all experiment refs with the specified baseline."""
all_exp_refs = exp_refs(scm, url)
result = defaultdict(list)
for ref in all_exp_refs:
if ref.baseline_sha in revs:
if revs is None or ref.baseline_sha in revs:
result[ref.baseline_sha].append(ref)
return result
@@ -108,8 +129,7 @@ def iter_remote_refs(
def push_refspec(
scm: "Git",
url: str,
src: Optional[str],
dest: str,
push_list=List[Tuple[Optional[str], str]],
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
@@ -119,20 +139,21 @@ def push_refspec(
from ...scm import GitAuthError, SCMError
if not src:
refspecs = [f":{dest}"]
elif src.endswith("/"):
refspecs = []
dest = dest.rstrip("/") + "/"
for ref in scm.iter_refs(base=src):
refname = ref.split("/")[-1]
refspecs.append(f"{ref}:{dest}{refname}")
else:
if dest.endswith("/"):
refname = src.split("/")[-1]
refspecs = [f"{src}:{dest}/{refname}"]
refspecs = []
for src, dest in push_list:
if not src:
refspecs.append(f":{dest}")
elif src.endswith("/"):
dest = dest.rstrip("/") + "/"
for ref in scm.iter_refs(base=src):
refname = ref.split("/")[-1]
refspecs.append(f"{ref}:{dest}{refname}")
else:
refspecs = [f"{src}:{dest}"]
if dest.endswith("/"):
refname = src.split("/")[-1]
refspecs.append(f"{src}:{dest}/{refname}")
else:
refspecs.append(f"{src}:{dest}")
try:
results = scm.push_refspecs(
+119 -111
View File
@@ -1,49 +1,23 @@
import logging
from contextlib import suppress
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Sequence
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.cloud import Remote
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__)
def _fetch_worktree(repo, remote):
from dvc_data.index import save
index = repo.index.data["repo"]
for key, entry in index.iteritems():
entry.fs = remote.fs
entry.path = remote.fs.path.join(
remote.path,
*key,
)
save(index)
for stage in repo.index.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
entry = repo.index.data[workspace][key]
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage)
return len(index)
@locked
def fetch(
self,
@@ -58,7 +32,7 @@ def fetch(
run_cache=False,
revs=None,
odb: Optional["ObjectDB"] = None,
):
) -> int:
"""Download data items from a cloud and imported repositories
Returns:
@@ -71,118 +45,152 @@ 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]
worktree_remote: Optional["Remote"] = None
with suppress(NoRemoteError):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _fetch_worktree(self, _remote)
if _remote.worktree or _remote.fs.version_aware:
worktree_remote = _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
transferred_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:
if worktree_remote is not None:
transferred_count += _fetch_worktree(
self,
worktree_remote,
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
targets=targets,
with_deps=with_deps,
recursive=recursive,
)
else:
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)
transferred_count += len(result.transferred)
return transferred_count
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
result.transferred.update(d)
result.failed.update(f)
return result
d, f = _fetch_partial_imports(
self,
targets,
def _fetch_worktree(
repo: "Repo",
remote: "Remote",
revs: Optional[Sequence[str]] = None,
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
targets: Optional["TargetType"] = None,
**kwargs,
) -> int:
from dvc.repo.worktree import fetch_worktree
downloaded = 0
for _ in repo.brancher(
revs=revs,
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)
):
downloaded += fetch_worktree(repo, remote, targets=targets, **kwargs)
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
+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)
+14 -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:
@@ -87,6 +97,9 @@ def imp_url(
else:
stage.run(jobs=jobs, no_download=no_download)
if not no_exec and stage.deps[0].fs.version_aware:
stage.outs[0].can_push = False
stage.frozen = frozen
dvcfile.dump(stage)
+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, stage_filter=need_fetch, **kwargs)
return unfetched, changed_deps
def partial_view(
index: "Index", targets: "TargetType", **kwargs
) -> "IndexView":
return index.targets_view(
targets,
stage_filter=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
+221 -55
View File
@@ -1,14 +1,17 @@
from contextlib import suppress
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from funcy import cached_property, nullcontext
@@ -22,11 +25,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,14 +67,51 @@ 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"]] = {}
self._metrics: Dict[str, List[str]] = {}
self._plots: Dict[str, Any] = {}
self._params: Dict[str, List[str]] = {}
@cached_property
def stages(self) -> List["Stage"]: # pylint: disable=method-hidden
# note that ideally we should be keeping this in a set as it is unique,
# hashable and has no concept of orderliness on its own. But we depend
# on this to be somewhat ordered for status/metrics/plots, etc.
return self._collect()
@cached_property
def _top_metrics(self):
self._collect()
return self._metrics
@cached_property
def _top_plots(self):
self._collect()
return self._plots
@cached_property
def _top_params(self):
self._collect()
return self._params
def _collect(self):
if "stages" in self.__dict__:
return self.stages
onerror = self.repo.stage_collection_error_handler
return self.stage_collector.collect_repo(onerror=onerror)
# pylint: disable=protected-access
(
stages,
metrics,
plots,
params,
) = self.stage_collector._collect_all_from_repo(onerror=onerror)
self.stages = stages
self._metrics = metrics
self._plots = plots
self._params = params
return stages
def __repr__(self) -> str:
from dvc.fs import LocalFileSystem
@@ -113,6 +153,72 @@ 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: Optional["TargetType"],
stage_filter: Optional[Callable[["Stage"], bool]] = None,
outs_filter: Optional[Callable[["Output"], bool]] = None,
**kwargs: Any,
) -> "IndexView":
"""Return read-only view of index for the specified targets.
Args:
targets: Targets to collect
stage_filter: Optional stage filter to be applied after collecting
targets.
outs_filter: Optional output filter to be applied after collecting
targets.
Additional kwargs will be passed into the stage collector.
Note:
If both stage_filter and outs_filter are provided, stage_filter
will be applied first, and the resulting view will only contain
outputs from stages that matched stage_filter. Outputs from stages
that did not match will be excluded from the view (whether or not
the output would have matched outs_filter).
"""
stage_infos = [
stage_info
for stage_info in self.collect_targets(targets, **kwargs)
if not stage_filter or stage_filter(stage_info.stage)
]
return IndexView(self, stage_infos, outs_filter=outs_filter)
@property
def outs(self) -> Iterator["Output"]:
for stage in self:
@@ -171,8 +277,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 +289,11 @@ 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,
)
entry = out.get_entry()
entry.key = key
data_index.add(entry)
return dict(by_workspace)
@@ -211,56 +305,25 @@ class Index:
force: bool = False,
recursive: bool = False,
jobs: int = None,
push: bool = False,
) -> "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,
force=force,
jobs=jobs,
filter_info=filter_info,
push=push,
).items():
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 +389,109 @@ class Index:
return dict_md5(self.dumpd())
class _DataPrefixes(NamedTuple):
explicit: Set["DataIndexKey"]
recursive: Set["DataIndexKey"]
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"],
outs_filter: Optional[Callable[["Output"], bool]],
):
self._index = index
self._stage_infos = stage_infos
# NOTE: stage_infos might have the same stage multiple times but with
# different filter_info
self._stages = list({stage for stage, _ in stage_infos})
self._outs_filter = outs_filter
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 self._stages
@property
def deps(self) -> Iterator["Dependency"]:
for stage in self:
yield from stage.deps
@property
def _filtered_outs(self) -> Iterator[Tuple["Output", Optional[str]]]:
for stage, filter_info in self._stage_infos:
for out in stage.filter_outs(filter_info):
if not self._outs_filter or self._outs_filter(out):
yield out, filter_info
@property
def outs(self) -> Iterator["Output"]:
yield from {out for (out, _) in self._filtered_outs}
@cached_property
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
from collections import defaultdict
prefixes: Dict[str, "_DataPrefixes"] = defaultdict(
lambda: _DataPrefixes(set(), set())
)
for out, filter_info in self._filtered_outs:
workspace, key = out.index_key
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
key = key + out.fs.path.relparts(filter_info, out.fs_path)
if out.meta.isdir:
prefixes[workspace].recursive.add(key)
prefixes[workspace].explicit.update(
key[:i] for i in range(len(key), 0, -1)
)
return prefixes
@cached_property
def data(self) -> Dict[str, Union["DataIndex", "DataIndexView"]]:
from functools import partial
from dvc_data.index import DataIndex, view
def key_filter(workspace: str, key: "DataIndexKey"):
try:
prefixes = self._data_prefixes[workspace]
return key in prefixes.explicit or any(
key[: len(prefix)] == prefix
for prefix in prefixes.recursive
)
except KeyError:
return False
data: Dict[str, Union["DataIndex", "DataIndexView"]] = {}
for workspace, data_index in self._index.data.items():
if self._stages:
data[workspace] = view(
data_index, partial(key_filter, workspace)
)
else:
data[workspace] = DataIndex()
return data
if __name__ == "__main__":
import logging
-19
View File
@@ -1,19 +0,0 @@
import logging
from typing import TYPE_CHECKING, Optional
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from dvc.output import Output
def summary_fs_path(out: "Output") -> Optional[str]:
from dvc.output import Output
assert out.live
has_summary = True
if isinstance(out.live, dict):
has_summary = out.live.get(Output.PARAM_LIVE_SUMMARY, True)
if has_summary:
return out.fs.path.with_suffix(out.fs_path, ".json")
return None
+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
+19 -7
View File
@@ -8,9 +8,13 @@ from dvc.fs.dvc import DVCFileSystem
from dvc.output import Output
from dvc.repo import locked
from dvc.repo.collect import StrPaths, collect
from dvc.repo.live import summary_fs_path
from dvc.scm import NoSCMError
from dvc.utils import error_handler, errored_revisions, onerror_collect
from dvc.utils import (
as_posix,
error_handler,
errored_revisions,
onerror_collect,
)
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import load_path
@@ -18,7 +22,7 @@ logger = logging.getLogger(__name__)
def _is_metric(out: Output) -> bool:
return bool(out.metric) or bool(out.live)
return bool(out.metric)
def _to_fs_paths(metrics: List[Output]) -> StrPaths:
@@ -26,13 +30,20 @@ def _to_fs_paths(metrics: List[Output]) -> StrPaths:
for out in metrics:
if out.metric:
result.append(out.repo.dvcfs.from_os_path(out.fs_path))
elif out.live:
fs_path = summary_fs_path(out)
if fs_path:
result.append(out.repo.dvcfs.from_os_path(fs_path))
return result
def _collect_top_level_metrics(repo):
top_metrics = repo.index._top_metrics # pylint: disable=protected-access
for dvcfile, metrics in top_metrics.items():
wdir = repo.fs.path.relpath(
repo.fs.path.parent(dvcfile), repo.root_dir
)
for file in metrics:
path = repo.fs.path.join(wdir, as_posix(file))
yield repo.fs.path.normpath(path)
def _collect_metrics(repo, targets, revision, recursive):
metrics, fs_paths = collect(
repo,
@@ -99,6 +110,7 @@ def _read_metrics(repo, metrics, rev, onerror=None):
def _gather_metrics(repo, targets, rev, recursive, onerror=None):
metrics = _collect_metrics(repo, targets, rev, recursive)
metrics.extend(_collect_top_level_metrics(repo))
return _read_metrics(repo, metrics, rev, onerror=onerror)
+18 -1
View File
@@ -20,7 +20,12 @@ from dvc.repo.collect import collect
from dvc.scm import NoSCMError
from dvc.stage import PipelineStage
from dvc.ui import ui
from dvc.utils import error_handler, errored_revisions, onerror_collect
from dvc.utils import (
as_posix,
error_handler,
errored_revisions,
onerror_collect,
)
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import load_path
@@ -35,6 +40,17 @@ def _is_params(dep: "Output"):
return isinstance(dep, ParamsDependency)
def _collect_top_level_params(repo):
top_params = repo.index._top_params # pylint: disable=protected-access
for dvcfile, params in top_params.items():
wdir = repo.fs.path.relpath(
repo.fs.path.parent(dvcfile), repo.root_dir
)
for file in params:
path = repo.fs.path.join(wdir, as_posix(file))
yield repo.fs.path.normpath(path)
def _collect_configs(
repo: "Repo", rev, targets=None, deps=False, stages=None
) -> Tuple[List["Output"], List[str]]:
@@ -176,6 +192,7 @@ def _gather_params(
param_outs, params_fs_paths = _collect_configs(
repo, rev, targets=targets, deps=deps, stages=stages
)
params_fs_paths.extend(_collect_top_level_params(repo=repo))
params = _read_params(
repo,
params=param_outs,
+38 -28
View File
@@ -14,11 +14,12 @@ from typing import (
List,
Optional,
Set,
Union,
)
import dpath.options
import dpath.util
from funcy import cached_property, first, project
from funcy import cached_property, distinct, first, project
from dvc.exceptions import DvcException
from dvc.utils import error_handler, errored_revisions, onerror_collect
@@ -100,7 +101,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
}
}
@@ -265,7 +266,7 @@ class Plots:
def _is_plot(out: "Output") -> bool:
return bool(out.plot) or bool(out.live)
return bool(out.plot)
def _resolve_data_sources(plots_data: Dict):
@@ -313,16 +314,18 @@ def _get_data_targets(definitions: Dict):
def infer_data_sources(plot_id, config=None):
def _deduplicate(lst: List):
return list({elem: None for elem in lst}.keys())
y = config.get("y", None)
if isinstance(y, dict):
sources = list(y.keys())
else:
sources = [plot_id]
return _deduplicate(source for source in sources)
x = config.get("x", None)
if isinstance(x, dict):
sources.append(first(x.keys()))
return distinct(source for source in sources)
def _matches(targets, config_file, plot_id):
@@ -436,26 +439,33 @@ def _resolve_definitions(
def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
from dvc.dvcfile import PipelineFile
result: Dict[str, Dict] = {}
dvcfiles = {stage.dvcfile for stage in repo.index.stages}
for dvcfile in dvcfiles:
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = dvcfile.load().get("plots", {})
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs,
onerror=onerror,
)
dpath.util.merge(
result,
{dvcfile_path: resolved},
)
top_plots = repo.index._top_plots # pylint: disable=protected-access
for dvcfile, plots_def in top_plots.items():
dvcfile_path = _relpath(repo.dvcfs, dvcfile)
dvcfile_defs_dict: Dict[str, Union[Dict, None]] = {}
if isinstance(plots_def, list):
for elem in plots_def:
if isinstance(elem, str):
dvcfile_defs_dict[elem] = None
else:
k, v = list(elem.items())[0]
dvcfile_defs_dict[k] = v
else:
dvcfile_defs_dict = plots_def
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs_dict,
onerror=onerror,
)
dpath.util.merge(
result,
{dvcfile_path: resolved},
)
return result
@@ -542,7 +552,7 @@ def parse(fs, path, props=None, **kwargs):
return _load_sv(path=path, fs=fs, delimiter="\t", header=header)
if extension in LOADERS or extension in (".yml", ".yaml"):
return LOADERS[extension](path=path, fs=fs)
if extension in (".jpeg", ".jpg", ".gif", ".png"):
if extension in (".jpeg", ".jpg", ".gif", ".png", ".svg"):
with fs.open(path, "rb") as fd:
return fd.read()
raise PlotMetricTypeError(path)
@@ -551,7 +561,7 @@ def parse(fs, path, props=None, **kwargs):
def _plot_props(out: "Output") -> Dict:
from dvc.schema import PLOT_PROPS
if not (out.plot or out.live):
if not (out.plot):
raise NotAPlotError(out)
if isinstance(out.plot, list):
raise DvcException("Multiple plots per data file not supported.")

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