Compare commits

...
112 Commits
Author SHA1 Message Date
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
148 changed files with 4728 additions and 4960 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
+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 -16
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,25 +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]"
- 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.10.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,falsy
repo: https://github.com/codespell-project/codespell
rev: v2.2.2
- 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
+6 -2
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|
|
@@ -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()
+3 -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,
@@ -128,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.
@@ -186,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.",
)
+4 -6
View File
@@ -18,15 +18,13 @@ class CmdExperimentsList(CmdBase):
)
tags = self.repo.scm.describe(exps)
remained = {baseline for baseline, tag in tags.items() if tag is None}
base = "refs/heads/"
base = "refs/heads"
ref_heads = self.repo.scm.describe(remained, base=base)
for baseline in exps:
name = (
tags[baseline]
or ref_heads[baseline][len(base) :]
or baseline[:7]
)
name = baseline[:7]
if tags[baseline] or ref_heads[baseline]:
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
if not name_only:
print(f"{name}:")
for exp_name in exps[baseline]:
-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",
+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
+5
View File
@@ -62,3 +62,8 @@ class Dependency(Output):
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)
-3
View File
@@ -2,9 +2,6 @@ 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_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
+10 -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):
+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
+101 -63
View File
@@ -87,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,
@@ -101,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
@@ -118,8 +118,8 @@ def loads_from(
plot=False,
persist=False,
checkpoint=False,
live=False,
remote=None,
push=True,
):
return [
_get(
@@ -131,8 +131,8 @@ def loads_from(
plot=plot,
persist=persist,
checkpoint=checkpoint,
live=live,
remote=remote,
push=push,
)
for s in s_list
]
@@ -166,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,
[
@@ -196,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,
],
)
@@ -206,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,
)
@@ -263,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,
@@ -292,7 +282,6 @@ class Output:
plot=False,
persist=False,
checkpoint=False,
live=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
@@ -301,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 {}
@@ -355,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
@@ -377,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)
@@ -523,7 +517,10 @@ class Output:
except NoRemoteError:
remote = None
return DataIndexEntry(
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,
@@ -531,6 +528,16 @@ class Output:
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()
@@ -631,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,
@@ -640,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
@@ -754,40 +757,40 @@ 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 not self.can_push:
ret[self.PARAM_PUSH] = self.can_push
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if self.hash_info.isdir and (
kwargs.get("with_files") or self.files is not None
if (
(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)
):
if self.obj:
obj = self.obj
@@ -1040,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. "
@@ -1182,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 = {
@@ -1217,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}'"
+3 -3
View File
@@ -14,14 +14,14 @@ 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}")
+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
+263 -265
View File
@@ -1,90 +1,101 @@
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_x(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
x = properties.get("x", None)
if x is not None and isinstance(x, dict):
filename, field = first(_file_field(x))
_verify_field(file2datapoints, filename, field)
return filename, field
return None
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,228 +106,215 @@ 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(
(
"infer_y",
partial(
self._infer_y,
),
)
)
self.steps.append(
(
"generate_y",
partial(
self._generate_y_values,
),
)
)
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], **kwargs):
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
return datapoints
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 = {}
x = _get_x(properties, file2datapoints)
# assign "step" if no x provided
if not x:
x_file, x_field = (
None,
INDEX_FIELD,
)
else:
x_file, x_field = x
props_update["x"] = x_field
ys = list(_get_ys(properties, file2datapoints))
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 y_file, y_field in ys:
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 "
"'{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:
if step_name not in skip:
processed = step( # type: ignore
processed,
revision=revision,
filename=filename,
)
return datapoints, properties
return processed, {**self.plot_properties, **self.inferred_properties}
def _generate_y_values( # noqa: C901
self,
datapoints: List[Dict],
revision: str,
filename: str,
**kwargs,
) -> List[Dict]:
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
y_values = self.plot_properties.get(
"y", None
) or self.inferred_properties.get("y", None)
if len(source_datapoints) != len(target_datapoints):
raise IndexError(
"Source and target datapoints must have the same length"
)
assert y_values is not None
for index, datapoint in enumerate(target_datapoints):
source_datapoint = source_datapoints[index]
if source_field in source_datapoint:
datapoint[field] = source_datapoint[source_field]
result = []
properties_update = {}
def _add_version_info(datapoint, version_info):
tmp = datapoint.copy()
tmp[VERSION_FIELD] = version_info
return tmp
def _update_from_index(datapoints: List[Dict], new_field: str):
for index, datapoint in enumerate(datapoints):
datapoint[new_field] = index
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 _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"
def _update_all(datapoints: List[Dict], update_dict: Dict):
properties_update["y"] = "dvc_inferred_y_value"
return tmp
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
y_field = all_fields.pop()
for datapoint in datapoints:
result.append(
_add_version_info(
datapoint,
_version_info(revision, filename, y_field),
)
)
properties_update.update({"y": y_field})
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
+3 -1
View File
@@ -269,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
@@ -401,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.
@@ -435,6 +436,7 @@ class Repo:
jobs=jobs,
recursive=recursive,
with_deps=with_deps,
push=push,
).items():
used[odb].update(objs)
+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:
+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
+17 -3
View File
@@ -4,7 +4,7 @@ import re
import time
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
@@ -171,7 +171,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
@@ -189,7 +200,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)
+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
)
+2 -9
View File
@@ -602,29 +602,22 @@ class BaseExecutor(ABC):
logger.debug("Running repro in '%s'", os.getcwd())
yield dvc
info.status = TaskStatus.SUCCESS
if infofile is not None:
info.dump_json(infofile)
except CheckpointKilledError:
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except DvcException:
if log_errors:
logger.exception("")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
except Exception:
if log_errors:
logger.exception("unexpected error")
info.status = TaskStatus.FAILED
if infofile is not None:
info.dump_json(infofile)
raise
finally:
if infofile is not None:
info.dump_json(infofile)
dvc.close()
os.chdir(old_cwd)
+1 -1
View File
@@ -47,9 +47,9 @@ class BaseLocalExecutor(BaseExecutor):
return SCM(self.root_dir)
def cleanup(self, infofile: str):
super().cleanup(infofile)
self.scm.close()
del self.scm
super().cleanup(infofile)
def collect_cache(
self, repo: "Repo", exp_ref: "ExpRefInfo", run_cache: bool = True
+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),
},
)
+3 -10
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 (
@@ -269,6 +268,7 @@ class BaseStashQueue(ABC):
finish any active experiments before shutting down.
"""
@abstractmethod
def logs(
self,
rev: str,
@@ -525,16 +525,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)
+90 -42
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,13 +23,9 @@ from dvc.exceptions import DvcException
from dvc.ui import ui
from ..exceptions import UnresolvedQueueExpNamesError
from ..executor.base import (
EXEC_TMP_DIR,
ExecutorInfo,
ExecutorResult,
TaskStatus,
)
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
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
@@ -187,6 +183,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]:
@@ -203,6 +200,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]:
@@ -211,6 +209,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]:
@@ -243,67 +242,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.status > TaskStatus.SUCCESS:
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()
@@ -359,10 +406,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):
+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."
)
+2 -3
View File
@@ -77,9 +77,8 @@ 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.status <= TaskStatus.SUCCESS
and os.path.exists(executor_info.root_dir)
if executor_info.status <= TaskStatus.SUCCESS and os.path.exists(
executor_info.root_dir
):
yield QueueEntry(
self.repo.root_dir,
+4 -2
View File
@@ -1,6 +1,8 @@
import logging
from typing import TYPE_CHECKING, Dict
from scmrepo.exceptions import SCMError
from ..executor.base import ExecutorInfo, TaskStatus
logger = logging.getLogger(__name__)
@@ -39,7 +41,7 @@ def fetch_running_exp_from_temp_dir(
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
def on_diverged(_ref: str, _checkpoint: bool):
return False
return True
executor = TempDirExecutor.from_info(info)
try:
@@ -52,7 +54,7 @@ def fetch_running_exp_from_temp_dir(
result[rev]["last"] = last_rev
if last_rev:
result[last_rev] = info.asdict()
except InvalidRemoteSCMRepo:
except (InvalidRemoteSCMRepo, SCMError):
# ignore stale info files
del result[rev]
return result
+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)
+208 -100
View File
@@ -3,16 +3,32 @@ 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__)
@@ -25,34 +41,49 @@ class ExpStatus(Enum):
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(
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,
):
) -> 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,
@@ -61,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,
@@ -73,14 +104,14 @@ def _collect_experiment_commit(
if not (out.is_metric or out.is_plot)
}
res["status"] = status.name
result["status"] = status.name
if status == ExpStatus.Running:
res["executor"] = running.get(exp_rev, {}).get("location", None)
result["executor"] = running.get(exp_rev, {}).get("location", None)
else:
res["executor"] = None
result["executor"] = None
if status == ExpStatus.Failed:
res["error"] = {
result["error"] = {
"msg": "Experiment run failed.",
"type": "",
}
@@ -89,96 +120,157 @@ def _collect_experiment_commit(
vals = _gather_metrics(
repo, targets=None, rev=rev, recursive=False, onerror=onerror
)
res["metrics"] = vals
result["metrics"] = vals
return res
return result
def _collect_experiment_branch(
res,
repo,
branch,
baseline,
onerror: Optional[Callable] = None,
running=None,
def _collect_complete_experiment(
repo: "Repo",
baseline: str,
exp_rev: str,
running: Dict[str, Any],
revs: List[str],
**kwargs,
):
from dvc.scm import resolve_rev
) -> 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:
status = ExpStatus.Running if rev in running else ExpStatus.Success
collected_exp = _collect_experiment_commit(
collected_exp = collect_experiment_commit(
repo,
rev,
onerror=onerror,
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 get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
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()
baseline_set = set()
for baseline in result:
for rev in result[baseline]:
if rev == "baseline":
rev = baseline
baseline_set.add(rev)
if rev != "workspace":
rev_set.add(rev)
names: Dict[str, Optional[str]] = {}
for base in ("refs/tags/", "refs/heads/"):
if rev_set:
names.update(
(rev, ref[len(base) :])
for rev, ref in repo.scm.describe(
baseline_set, base=base
).items()
if ref is not None
)
rev_set.difference_update(names.keys())
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 = names.get(rev, None)
name = name or exact_name[rev]
if name:
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
# flake8: noqa: C901
def _collect_active_experiment(
repo: "Repo",
found_revs: Dict[str, List[str]],
@@ -194,13 +286,16 @@ def _collect_active_experiment(
if entry.baseline_rev in found_revs and (
stash_rev not in running or not running[stash_rev].get("last")
):
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
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
@@ -214,13 +309,16 @@ def _collect_queued_experiment(
for entry in repo.experiments.celery_queue.iter_queued():
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs:
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
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
@@ -235,14 +333,16 @@ def _collect_failed_experiment(
entry = queue_done_result.entry
stash_rev = entry.stash_rev
if entry.baseline_rev in found_revs:
experiment = _collect_experiment_commit(
collected_exp = collect_experiment_commit(
repo,
stash_rev,
status=ExpStatus.Failed,
running=running,
**kwargs,
)
result[entry.baseline_rev][stash_rev] = experiment
if _is_scm_error(collected_exp):
continue
result[entry.baseline_rev][stash_rev] = collected_exp
return result
@@ -254,6 +354,40 @@ def update_new(
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(
repo: "Repo",
all_branches=False,
@@ -272,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)
@@ -286,6 +419,7 @@ 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: Dict[str, Dict] = repo.experiments.get_running_exps(
fetch_refs=fetch_running
@@ -316,7 +450,6 @@ def show(
repo,
found_revs,
running,
sha_only=sha_only,
param_deps=param_deps,
onerror=onerror,
)
@@ -324,49 +457,24 @@ def show(
else {}
)
for rev in found_revs:
status = ExpStatus.Running if rev in running else ExpStatus.Success
res[rev]["baseline"] = _collect_experiment_commit(
for baseline in found_revs:
res[baseline] = _collect_branch(
repo,
rev,
status=status,
param_deps=param_deps,
baseline,
running=running,
param_deps=param_deps,
onerror=onerror,
)
if rev == "workspace":
continue
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,
param_deps=param_deps,
running=running,
onerror=onerror,
)
if not hide_failed:
update_new(res, failed_experiments)
update_new(res, failed_experiments)
update_new(res, active_experiment)
if not hide_queued:
update_new(res, queued_experiment)
update_new(res, queued_experiment)
if not sha_only:
get_names(repo, res)
update_names(repo, branch_names, res)
move_properties_to_head(res)
return res
+2 -15
View File
@@ -17,20 +17,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _fetch_worktree(repo, remote):
from dvc_data.index import save
index = repo.index.data["repo"]
for key, entry in index.iteritems():
entry.fs = remote.fs
entry.path = remote.fs.path.join(
remote.path,
*key,
)
save(index)
return len(index)
@locked
def fetch(
self,
@@ -59,6 +45,7 @@ def fetch(
remote is configured
"""
from dvc.repo.imports import save_imports
from dvc.repo.worktree import fetch_worktree
from dvc_data.hashfile.transfer import TransferResult
if isinstance(targets, str):
@@ -67,7 +54,7 @@ def fetch(
with suppress(NoRemoteError):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _fetch_worktree(self, _remote)
return fetch_worktree(self, _remote)
failed_count = 0
+3
View File
@@ -97,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)
+2 -2
View File
@@ -44,7 +44,7 @@ def unfetched_view(
return True
unfetched = index.targets_view(targets, filter_fn=need_fetch, **kwargs)
unfetched = index.targets_view(targets, stage_filter=need_fetch, **kwargs)
return unfetched, changed_deps
@@ -53,7 +53,7 @@ def partial_view(
) -> "IndexView":
return index.targets_view(
targets,
filter_fn=lambda s: s.is_partial_import,
stage_filter=lambda s: s.is_partial_import,
**kwargs,
)
+59 -36
View File
@@ -7,8 +7,10 @@ from typing import (
Iterable,
Iterator,
List,
NamedTuple,
Optional,
Set,
Tuple,
)
from funcy import cached_property, nullcontext
@@ -150,25 +152,35 @@ class Index:
def targets_view(
self,
targets: "TargetType",
filter_fn: Optional[Callable[["Stage"], bool]] = None,
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
filter_fn: Optional stage filter to be applied after collecting
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 filter_fn or filter_fn(stage_info.stage)
if not stage_filter or stage_filter(stage_info.stage)
]
return IndexView(self, stage_infos)
return IndexView(self, stage_infos, outs_filter=outs_filter)
@property
def outs(self) -> Iterator["Output"]:
@@ -242,15 +254,8 @@ class Index:
workspace, key = out.index_key
data_index = by_workspace[workspace]
if out.files:
out.obj = out.get_obj()
entry = out.get_entry()
entry.key = key
if out.stage.is_import and not out.stage.is_repo_import:
entry.fs = out.stage.deps[0].fs
entry.path = out.stage.deps[0].fs_path
entry.meta = out.stage.deps[0].meta
data_index.add(entry)
return dict(by_workspace)
@@ -263,6 +268,7 @@ class Index:
force: bool = False,
recursive: bool = False,
jobs: int = None,
push: bool = False,
) -> "ObjectContainer":
from collections import defaultdict
@@ -276,6 +282,7 @@ class Index:
force=force,
jobs=jobs,
filter_info=filter_info,
push=push,
).items():
used[odb].update(objs)
return used
@@ -345,17 +352,26 @@ 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"]
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)
@@ -384,30 +400,32 @@ class IndexView:
yield from stage.deps
@property
def outs(self) -> Iterator["Output"]:
outs = set()
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):
outs.add(out)
yield from outs
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, Set["DataIndexKey"]]:
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
from collections import defaultdict
prefixes: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
for stage, filter_info in self._stage_infos:
for out in stage.filter_outs(filter_info):
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),
)
prefixes[workspace].add(key)
prefixes[workspace].update(
key[:i] for i in range(len(key), 0, -1)
)
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
@@ -418,14 +436,19 @@ class IndexView:
def key_filter(workspace: str, key: "DataIndexKey"):
try:
return key in self._data_prefixes[workspace]
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
return {
workspace: view(index, partial(key_filter, workspace))
for workspace, index in self._index.data.items()
}
data = {}
for workspace, data_index in self._index.data.items():
data_index.load()
data[workspace] = view(data_index, partial(key_filter, workspace))
return data
if __name__ == "__main__":
-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
+1 -6
View File
@@ -8,7 +8,6 @@ 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.collections import ensure_list
@@ -18,7 +17,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,10 +25,6 @@ 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
+21 -12
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
@@ -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):
@@ -444,16 +447,22 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = dvcfile.load().get("plots", {})
dvcfile_defs_dict: Dict[str, Union[Dict, None]] = {}
if isinstance(dvcfile_defs, list):
dvcfile_defs = {
k: v for elem in dvcfile_defs for k, v in elem.items()
}
for elem in dvcfile_defs:
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 = dvcfile_defs
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs,
dvcfile_defs_dict,
onerror=onerror,
)
dpath.util.merge(
@@ -546,7 +555,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)
@@ -555,7 +564,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.")
+4 -43
View File
@@ -9,48 +9,6 @@ if TYPE_CHECKING:
from dvc_objects.db.base import ObjectDB
def _push_worktree(repo, remote):
from dvc_data.index import checkout
from dvc_data.index.save import build_tree
index = repo.index.data["repo"]
checkout(index, remote.path, remote.fs)
for stage in repo.index.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
index = repo.index.data[workspace]
entry = index[key]
if out.isdir():
old_tree = out.get_obj()
entry.hash_info = old_tree.hash_info
entry.meta = out.meta
for subkey, entry in index.iteritems(key):
if entry.meta.isdir:
continue
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
_, hash_info = old_tree.get(
repo.fs.path.relparts(fs_path, out.fs_path)
)
entry.hash_info = hash_info
tree_meta, new_tree = build_tree(index, key)
out.obj = new_tree
out.hash_info = new_tree.hash_info
out.meta = tree_meta
else:
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage, with_files=True, update_pipeline=False)
return len(index)
@locked
def push(
self,
@@ -68,9 +26,11 @@ def push(
odb: Optional["ObjectDB"] = None,
include_imports=False,
):
from dvc.repo.worktree import push_worktree
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _push_worktree(self, _remote)
return push_worktree(self, _remote)
used_run_cache = (
self.stage_cache.push(remote, odb=odb) if run_cache else []
@@ -93,6 +53,7 @@ def push(
recursive=recursive,
used_run_cache=used_run_cache,
revs=revs,
push=True,
)
pushed = len(used_run_cache)
-6
View File
@@ -72,12 +72,6 @@ def _get_stage_files(stage: "Stage") -> typing.Iterator[str]:
for out in stage.outs:
if not out.use_scm_ignore and out.is_in_repo:
yield out.fs_path
if out.live:
from dvc.repo.live import summary_fs_path
summary = summary_fs_path(out)
if summary:
yield summary
def _track_stage(stage: "Stage") -> None:
+1
View File
@@ -89,6 +89,7 @@ def _cloud_status(
remote=remote,
jobs=jobs,
recursive=recursive,
push=True,
)
ret = {}
+121
View File
@@ -0,0 +1,121 @@
from typing import TYPE_CHECKING, Optional
from dvc.fs.callbacks import Callback
if TYPE_CHECKING:
from dvc.cloud import Remote
from dvc.index import Index, IndexView
from dvc.output import Output
from dvc.repo import Repo
from dvc.stage import Stage
from dvc.types import TargetType
def worktree_view(
index: "Index",
targets: Optional["TargetType"] = None,
push: bool = False,
**kwargs,
) -> "IndexView":
"""Return view of data that can be stored in worktree remotes.
Args:
targets: Optional targets.
push: Whether the view should be restricted to pushable data only.
Additional kwargs will be passed into target collection.
"""
def stage_filter(stage: "Stage") -> bool:
if push and stage.is_repo_import:
return False
return True
def outs_filter(out: "Output") -> bool:
if (
not out.is_in_repo
or not out.use_cache
or (push and not out.can_push)
):
return False
return True
return index.targets_view(
targets,
stage_filter=stage_filter,
outs_filter=outs_filter,
**kwargs,
)
def fetch_worktree(repo: "Repo", remote: "Remote") -> int:
from dvc_data.index import save
view = worktree_view(repo.index)
index = view.data["repo"]
for key, entry in index.iteritems():
entry.fs = remote.fs
entry.path = remote.fs.path.join(
remote.path,
*key,
)
total = len(index)
with Callback.as_tqdm_callback(
unit="file", desc="Fetch", disable=total == 0
) as cb:
cb.set_size(total)
return save(index, callback=cb)
def push_worktree(repo: "Repo", remote: "Remote") -> int:
from dvc_data.index import checkout
from dvc_data.index.save import build_tree
view = worktree_view(repo.index, push=True)
index = view.data["repo"]
total = len(index)
with Callback.as_tqdm_callback(
unit="file", desc="Push", disable=total == 0
) as cb:
cb.set_size(total)
pushed = checkout(
index,
remote.path,
remote.fs,
latest_only=False,
callback=cb,
)
for stage in view.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
index = repo.index.data[workspace]
entry = index[key]
if out.isdir():
old_tree = out.get_obj()
entry.hash_info = old_tree.hash_info
entry.meta = out.meta
for subkey, entry in index.iteritems(key):
if entry.meta.isdir:
continue
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
_, hash_info = old_tree.get(
repo.fs.path.relparts(fs_path, out.fs_path)
)
entry.hash_info = hash_info
tree_meta, new_tree = build_tree(index, key)
out.obj = new_tree
out.hash_info = new_tree.hash_info
out.meta = tree_meta
else:
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage, with_files=True, update_pipeline=False)
return pushed
+3 -10
View File
@@ -51,6 +51,7 @@ OUT_PSTAGE_DETAILED_SCHEMA = {
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Output.PARAM_REMOTE: str,
Output.PARAM_PUSH: bool,
}
}
@@ -67,13 +68,6 @@ PLOT_PROPS = {
PLOT_PROPS_SCHEMA = {**OUT_PSTAGE_DETAILED_SCHEMA[str], **PLOT_PROPS}
PLOT_PSTAGE_SCHEMA = {str: Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])}
LIVE_PROPS = {
Output.PARAM_LIVE_SUMMARY: bool,
Output.PARAM_LIVE_HTML: bool,
}
LIVE_PROPS_SCHEMA = {**PLOT_PROPS_SCHEMA, **LIVE_PROPS}
LIVE_PSTAGE_SCHEMA = {str: LIVE_PROPS_SCHEMA}
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
VARS_SCHEMA = [str, dict]
@@ -93,7 +87,6 @@ STAGE_DEFINITION = {
Any(str, OUT_PSTAGE_DETAILED_SCHEMA)
],
Optional(StageParams.PARAM_PLOTS): [Any(str, PLOT_PSTAGE_SCHEMA)],
Optional(StageParams.PARAM_LIVE): Any(str, LIVE_PSTAGE_SCHEMA),
}
@@ -111,7 +104,7 @@ def either_or(primary, fallback, fallback_includes=None):
PLOT_DEFINITION = {
Output.PARAM_PLOT_X: str,
Output.PARAM_PLOT_X: Any(str, {str: str}),
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
Output.PARAM_PLOT_X_LABEL: str,
Output.PARAM_PLOT_Y_LABEL: str,
@@ -127,7 +120,7 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
}
MULTI_STAGE_SCHEMA = {
PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]),
PLOTS: Any(SINGLE_PLOT_SCHEMA, [Any(str, SINGLE_PLOT_SCHEMA)]),
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
VARS_KWD: VARS_SCHEMA,
}
+36 -23
View File
@@ -113,10 +113,11 @@ def restore_fields(stage):
stage.meta = old.meta
stage.desc = old.desc
old_fields = {out.def_path: (out.annot, out.remote) for out in old.outs}
old_outs = {out.def_path: out for out in old.outs}
for out in stage.outs:
if out_fields := old_fields.get(out.def_path, None):
out.annot, out.remote = out_fields
old_out = old_outs.get(out.def_path, None)
if old_out is not None:
out.restore_fields(old_out)
class Stage(params.StageParams):
@@ -280,22 +281,7 @@ class Stage(params.StageParams):
def _read_env(self, out, checkpoint_func=None) -> Env:
env: Env = {}
if out.live:
from dvc.env import DVCLIVE_HTML, DVCLIVE_PATH, DVCLIVE_SUMMARY
from dvc.output import Output
from dvc.schema import LIVE_PROPS
env[DVCLIVE_PATH] = relpath(out.fs_path, self.wdir)
if isinstance(out.live, dict):
config = project(out.live, LIVE_PROPS)
env[DVCLIVE_SUMMARY] = str(
int(config.get(Output.PARAM_LIVE_SUMMARY, True))
)
env[DVCLIVE_HTML] = str(
int(config.get(Output.PARAM_LIVE_HTML, True))
)
elif out.checkpoint and checkpoint_func:
if out.checkpoint and checkpoint_func:
from dvc.env import DVC_CHECKPOINT
env.update({DVC_CHECKPOINT: "1"})
@@ -379,7 +365,7 @@ class Stage(params.StageParams):
def remove_outs(self, ignore_remove=False, force=False):
"""Used mainly for `dvc remove --outs` and :func:`Stage.reproduce`."""
for out in self.outs:
if (out.persist or out.checkpoint or out.live) and not force:
if (out.persist or out.checkpoint) and not force:
out.unprotect()
continue
@@ -477,10 +463,12 @@ class Stage(params.StageParams):
logger.debug("Computed %s md5: '%s'", self, m)
return m
def save(self, allow_missing=False):
def save(self, allow_missing: bool = False, merge_versioned: bool = False):
self.save_deps(allow_missing=allow_missing)
self.save_outs(allow_missing=allow_missing)
self.save_outs(
allow_missing=allow_missing, merge_versioned=merge_versioned
)
self.md5 = self.compute_md5()
self.repo.stage_cache.save(self)
@@ -495,15 +483,40 @@ class Stage(params.StageParams):
if not allow_missing:
raise
def save_outs(self, allow_missing=False):
def save_outs(
self, allow_missing: bool = False, merge_versioned: bool = False
):
from dvc.output import OutputDoesNotExistError
from .exceptions import StageFileDoesNotExistError, StageNotFound
if merge_versioned:
try:
old = self.reload()
old_outs = {out.def_path: out for out in old.outs}
merge_versioned = any(
(
out.files is not None
or (
out.meta is not None
and out.meta.version_id is not None
)
)
for out in old_outs.values()
)
except (StageFileDoesNotExistError, StageNotFound):
merge_versioned = False
for out in self.outs:
try:
out.save()
except OutputDoesNotExistError:
if not (allow_missing or out.checkpoint):
raise
if merge_versioned:
old_out = old_outs.get(out.def_path)
if old_out is not None:
out.merge_version_meta(old_out)
def ignore_outs(self):
for out in self.outs:
-1
View File
@@ -97,7 +97,6 @@ class StageLoader(Mapping):
stage.PARAM_OUTS,
stage.PARAM_METRICS,
stage.PARAM_PLOTS,
stage.PARAM_LIVE,
],
)
stage.outs = lcat(
-4
View File
@@ -20,10 +20,6 @@ class CheckpointKilledError(StageCmdFailedError):
pass
class LiveKilledError(StageCmdFailedError):
pass
@dataclass
class MonitorTask:
stage: "Stage"
-1
View File
@@ -12,4 +12,3 @@ class StageParams:
PARAM_METRICS = "metrics"
PARAM_PLOTS = "plots"
PARAM_DESC = "desc"
PARAM_LIVE = "live"
+6 -10
View File
@@ -29,6 +29,7 @@ PARAM_PERSIST = Output.PARAM_PERSIST
PARAM_CHECKPOINT = Output.PARAM_CHECKPOINT
PARAM_DESC = Annotation.PARAM_DESC
PARAM_REMOTE = Output.PARAM_REMOTE
PARAM_PUSH = Output.PARAM_PUSH
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
@@ -52,10 +53,10 @@ def _get_flags(out):
# `out.plot` is in the same order as is in the file when read
# and, should be dumped as-is without any sorting
yield from out.plot.items()
if out.live and isinstance(out.live, dict):
yield from out.live.items()
if out.remote:
yield PARAM_REMOTE, out.remote
if not out.can_push:
yield PARAM_PUSH, False
def _serialize_out(out):
@@ -65,19 +66,15 @@ def _serialize_out(out):
@no_type_check
def _serialize_outs(outputs: List[Output]):
outs, metrics, plots, live = [], [], [], None
outs, metrics, plots = [], [], []
for out in sort_by_path(outputs):
bucket = outs
if out.plot:
bucket = plots
elif out.metric:
bucket = metrics
elif out.live:
assert live is None
live = _serialize_out(out)
continue
bucket.append(_serialize_out(out))
return outs, metrics, plots, live
return outs, metrics, plots
def _serialize_params_keys(params):
@@ -126,7 +123,7 @@ def to_pipeline_file(stage: "PipelineStage"):
deps = sorted(d.def_path for d in deps)
params = _serialize_params_keys(params)
outs, metrics, plots, live = _serialize_outs(stage.outs)
outs, metrics, plots = _serialize_outs(stage.outs)
cmd = stage.cmd
assert cmd, (
@@ -142,7 +139,6 @@ def to_pipeline_file(stage: "PipelineStage"):
(stage.PARAM_OUTS, outs),
(stage.PARAM_METRICS, metrics),
(stage.PARAM_PLOTS, plots),
(stage.PARAM_LIVE, live),
(stage.PARAM_FROZEN, stage.frozen),
(stage.PARAM_ALWAYS_CHANGED, stage.always_changed),
(stage.PARAM_META, stage.meta),
+1 -43
View File
@@ -2,7 +2,7 @@ import os
import pathlib
from typing import TYPE_CHECKING, Union
from funcy import concat, first, lsplit, rpartial, without
from funcy import concat, first, lsplit, rpartial
from dvc.annotations import ANNOTATION_FIELDS
from dvc.exceptions import InvalidArgumentError
@@ -66,8 +66,6 @@ def fill_stage_outputs(stage, **kwargs):
stage.outs = []
stage.outs += _load_live_output(stage, **kwargs)
for key in keys:
stage.outs += loads_from(
stage,
@@ -80,34 +78,6 @@ def fill_stage_outputs(stage, **kwargs):
)
def _load_live_output(
stage,
live=None,
live_no_cache=None,
live_summary=False,
live_html=False,
**kwargs,
):
from dvc.output import Output, loads_from
outs = []
if live or live_no_cache:
assert bool(live) != bool(live_no_cache)
path = live or live_no_cache
outs += loads_from(
stage,
[path],
use_cache=not bool(live_no_cache),
live={
Output.PARAM_LIVE_SUMMARY: live_summary,
Output.PARAM_LIVE_HTML: live_html,
},
)
return outs
def fill_stage_dependencies(
stage, deps=None, erepo=None, params=None, fs_config=None
):
@@ -274,7 +244,6 @@ def prepare_file_path(kwargs):
kwargs.get("outs_persist", []),
kwargs.get("outs_persist_no_cache", []),
kwargs.get("checkpoints", []),
without([kwargs.get("live", None)], None),
)
)
@@ -332,15 +301,4 @@ def validate_kwargs(single_stage: bool = False, fname: str = None, **kwargs):
if single_stage:
kwargs.pop("name", None)
if kwargs.get("live") and kwargs.get("live_no_cache"):
raise InvalidArgumentError(
"cannot specify both `--live` and `--live-no-cache`"
)
kwargs.update(
{
"live_summary": not kwargs.pop("live_no_summary", False),
"live_html": not kwargs.pop("live_no_html", False),
}
)
return kwargs
+11 -3
View File
@@ -13,14 +13,22 @@ class TestAPI:
assert api.get_url("foo") == expected_url
def test_open(self, tmp_dir, dvc, remote):
tmp_dir.dvc_gen("foo", "foo-text")
tmp_dir.dvc_gen(
{
"foo": "foo-text",
"dir": {"bar": "bar-text"},
}
)
dvc.push()
# Remove cache to force download
remove(dvc.odb.local.path)
with api.open("foo") as fd:
assert fd.read() == "foo-text"
with api.open("foo") as fobj:
assert fobj.read() == "foo-text"
with api.open("dir/bar") as fobj:
assert fobj.read() == "bar-text"
@pytest.mark.parametrize(
"clear_cache", [True, False], ids=["cache", "no_cache"]
+1 -1
View File
@@ -65,7 +65,7 @@ class Cloud(ABC):
self._gen(struct)
return struct.keys()
def close(self):
def close(self): # noqa: B027
pass
@staticmethod
+3 -1
View File
@@ -270,7 +270,9 @@ class Console:
path = Path(file).resolve()
url = (
relpath(path) if "Microsoft" in uname().release else path.as_uri()
relpath(path)
if "microsoft" in uname().release.lower()
else path.as_uri()
)
opened = webbrowser.open(url)
+43 -10
View File
@@ -1,15 +1,24 @@
import sys
from pathlib import Path
from typing import TYPE_CHECKING, List
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from omegaconf import OmegaConf
try:
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from omegaconf import OmegaConf
from dvc.exceptions import InvalidArgumentError
hydra_compatible = True
except ValueError:
if sys.version_info >= (3, 11):
hydra_compatible = False
else:
raise
from dvc.exceptions import DvcException, InvalidArgumentError
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
from .serialize import DUMPERS, MODIFIERS
@@ -37,11 +46,20 @@ def compose_and_dump(
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
with initialize_config_dir(config_dir, version_base=None):
cfg = compose(config_name=config_name, overrides=overrides)
dumper = DUMPERS[Path(output_file).suffix.lower()]
dumper(output_file, OmegaConf.to_object(cfg))
suffix = Path(output_file).suffix.lower()
if suffix not in [".yml", ".yaml"]:
dumper = DUMPERS[suffix]
dumper(output_file, OmegaConf.to_object(cfg))
else:
Path(output_file).write_text(OmegaConf.to_yaml(cfg), encoding="utf-8")
def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
@@ -53,6 +71,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
"""
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
suffix = Path(path).suffix.lower()
hydra_errors = (ConfigCompositionException, OverrideParseException)
@@ -78,6 +101,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
def to_hydra_overrides(path_overrides):
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
parser = OverridesParser.create()
return parser.parse_overrides(overrides=path_overrides)
@@ -89,6 +117,11 @@ def dict_product(dicts):
def get_hydra_sweeps(path_overrides):
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
path_sweeps = {}
for path, overrides in path_overrides.items():
overrides = to_hydra_overrides(overrides)
+6 -2
View File
@@ -1,2 +1,6 @@
def get_plot_id(plot_id: str, config_file_path: str = ""):
return f"{config_file_path}::{plot_id}" if config_file_path else plot_id
def get_plot_id(config_plot_id: str, config_file_path: str = ""):
return (
f"{config_file_path}::{config_plot_id}"
if config_file_path
else config_plot_id
)
+9 -2
View File
@@ -1,10 +1,17 @@
"""Common utilities for serialize."""
import os
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Callable, ContextManager, Dict, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
ContextManager,
Dict,
Protocol,
Union,
)
from funcy import reraise
from typing_extensions import Protocol
from dvc.exceptions import DvcException
+1
View File
@@ -4,3 +4,4 @@ try:
from ._dvc_version import version_tuple # noqa: F401
except ImportError:
__version__ = "UNKNOWN"
version_tuple = (0, 0, __version__) # type: ignore[assignment]
+160 -2
View File
@@ -1,7 +1,140 @@
[build-system]
requires = ["setuptools>=48", "setuptools_scm[toml]>=7"]
requires = ["setuptools>=61", "setuptools_scm[toml]>=7"]
build-backend = "setuptools.build_meta"
[project]
name = "dvc"
description = "Git for data scientists - manage your code and data together"
readme = "README.rst"
keywords = [
"data-science",
"data-version-control",
"machine-learning",
"git",
"developer-tools",
"reproducibility",
"collaboration",
"ai",
]
license = { text = "Apache License 2.0" }
authors = [{ name = "Dmitry Petrov", email = "[email protected]" }]
maintainers = [{ name = "Iterative", email = "[email protected]" }]
requires-python = ">=3.8"
dependencies = [
"colorama>=0.3.9",
"configobj>=5.0.6",
"voluptuous>=0.11.7",
"requests>=2.22.0",
# See https://github.com/bdcht/grandalf/issues/26
"grandalf==0.6",
"distro>=1.3.0",
"appdirs>=1.4.3",
"ruamel.yaml>=0.17.11",
"tomlkit>=0.11.1",
"funcy>=1.14",
"pathspec>=0.9.0,<0.10.0",
"shortuuid>=0.5.0",
"tqdm>=4.63.1,<5",
"packaging>=19.0",
"zc.lockfile>=1.2.1",
"flufl.lock>=5",
"networkx>=2.5",
"psutil>=5.8.0",
"pydot>=1.2.4",
"importlib-resources>=5.2.2; python_version < '3.9'",
"flatten_dict>=0.4.1,<1",
"tabulate>=0.8.7",
"pygtrie>=2.3.2",
"dpath>=2.0.2,<3",
"shtab>=1.3.4,<2",
"rich>=10.13.0",
"pyparsing>=2.4.7",
"typing-extensions>=3.7.4",
"scmrepo==0.1.3",
"dvc-render==0.0.14",
"dvc-task==0.1.6",
"dvclive>=1.0",
"dvc-data==0.28.0",
"dvc-http==2.27.2",
"hydra-core>=1.1.0",
"iterative-telemetry==0.0.6",
]
dynamic = ["version"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
[project.optional-dependencies]
azure = ["dvc-azure==2.20.5"]
gdrive = ["dvc-gdrive==2.19.1"]
gs = ["dvc-gs==2.20.0"]
hdfs = ["dvc-hdfs==2.19.0; python_version < '3.11'"]
oss = ["dvc-oss==2.19.0"]
s3 = ["dvc-s3==2.21.0"]
ssh = ["dvc-ssh==2.20.0"]
ssh_gssapi = ["dvc-ssh[gssapi]==2.20.0"]
webdav = ["dvc-webdav==2.19.0"]
webhdfs = ["dvc-webhdfs==2.19.0"]
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19.0"]
terraform = ["tpi[ssh]>=2.1.0"]
testing = ["pytest-test-utils==0.0.8"]
tests = [
"tpi[ssh]>=2.1.0",
"dvc-ssh==2.20.0",
"pytest-test-utils==0.0.8",
# Test requirements
"pytest==7.2.0",
"pytest-cov==4.0.0",
"pytest-xdist==3.0.2",
"pytest-mock==3.10.0",
"pytest-lazy-fixture==0.6.3",
# https://github.com/docker/docker-py/issues/2902
"pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'",
"flaky==3.7.0",
"pytest-timeout==2.1.0",
"filelock==3.8.0",
# optional dependencies
"dvclive>=1.0",
"pywin32>=225; sys_platform == 'win32'",
"beautifulsoup4==4.11.1",
# pylint requirements
"pylint==2.15.5",
# we use this to suppress pytest-related false positives in our tests.
"pylint-pytest==1.1.2",
# we use this to suppress some messages in tests, eg: foo/bar naming,
# and, protected method calls in our tests
"pylint-plugin-utils==0.7",
# type-checking
"mypy==0.990",
"types-requests>=2.27.15",
"types-tabulate>=0.8.6",
"types-toml>=0.10.4",
]
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
dev = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs,tests]"]
[project.urls]
Documentation = "https://dvc.org/doc"
"Bug Tracker" = "https://github.com/iterative/dvc/issues"
Source = "https://github.com/iterative/dvc"
[project.scripts]
dvc = "dvc.cli:main"
[project.entry-points."fsspec.specs"]
dvc = "dvc.api:DVCFileSystem"
[tool.setuptools]
license-files = ["LICENSE"]
[tool.setuptools.packages]
find = {namespaces = false, exclude=["tests", "tests.*"]}
[tool.setuptools_scm]
write_to = "dvc/_dvc_version.py"
@@ -30,13 +163,34 @@ line_length = 79
[tool.pytest.ini_options]
log_level = "debug"
addopts = "-ra -p no:hydra_pytest"
addopts = "-ra -p no:hydra_pytest --cov-config pyproject.toml"
markers = [
"needs_internet: Might need network access for the tests",
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio",
]
xfail_strict = true
testpaths = ["tests"]
filterwarnings = ["error::pytest_mock.PytestMockWarning"]
[tool.coverage.run]
branch = true
source = ["dvc", "tests"]
[tool.coverage.paths]
source = ["dvc"]
[tool.coverage.report]
show_missing = true
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"if typing.TYPE_CHECKING:",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"raise AssertionError",
"@overload",
]
[tool.mypy]
# Error output
@@ -47,6 +201,7 @@ show_traceback = true
pretty = true
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
ignore_missing_imports = true
implicit_optional = true
check_untyped_defs = false
# Warnings
warn_no_return = true
@@ -81,3 +236,6 @@ signature-mutators = ["funcy.decorators.decorator"]
[tool.pylint.variables]
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs"
[tool.codespell]
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
+1 -1
View File
@@ -1,3 +1,3 @@
# NOTE: using strict version due to
# https://github.com/iterative/dvc/issues/7949
PyInstaller==5.0
PyInstaller==5.6.2
+2 -18
View File
@@ -1,7 +1,5 @@
import argparse
import importlib.util
import pathlib
import textwrap
from subprocess import STDOUT, check_call
path = pathlib.Path(__file__).parent.absolute()
@@ -25,22 +23,8 @@ args = parser.parse_args()
(dvc / "utils" / "build.py").write_text(f'PKG = "{args.pkg}"')
# Autogenerate version, similar to what we do in setup.py
spec = importlib.util.spec_from_file_location(
"dvc.version", dvc / "version.py"
)
dvc_version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dvc_version)
version = dvc_version.__version__
(dvc / "version.py").write_text(
textwrap.dedent(
f"""\
# AUTOGENERATED at build time by scripts/build.py
__version__ = "{version}"
"""
)
)
if not (dvc / "_dvc_version.py").exists():
raise Exception("no version info found")
check_call(
["python", "build.py"],
+3 -2
View File
@@ -10,5 +10,6 @@ fi
echo 'PKG = "pip"' >dvc/utils/build.py
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
python -m pip install build twine
python -m build
twine check dist/*
+4
View File
@@ -44,6 +44,10 @@ if args.pkg == "osxpkg":
]
)
else:
if args.pkg == "rpm":
# https://github.com/jordansissel/fpm/issues/1503
flags.extend(["--rpm-rpmbuild-define", "_build_id_links none"])
bash_dir = build / "etc" / "bash_completion.d"
dirs = ["usr", "etc"]
flags.extend(["--depends", "git >= 1.7.0"]) # needed for gitpython
+5 -5
View File
@@ -24,8 +24,8 @@ function addSymLinkPermissions($accountToAdd){
Write-Host "Account SID: $($sidstr)" -ForegroundColor DarkCyan
$tmp = [System.IO.Path]::GetTempFileName()
Write-Host "Export current Local Security Policy" -ForegroundColor DarkCyan
secedit.exe /export /cfg "$($tmp)"
$c = Get-Content -Path $tmp
secedit.exe /export /cfg "$($tmp)"
$c = Get-Content -Path $tmp
$currentSetting = ""
foreach($s in $c) {
if( $s -like "SECreateSymbolicLinkPrivilege*") {
@@ -35,7 +35,7 @@ function addSymLinkPermissions($accountToAdd){
}
if( $currentSetting -notlike "*$($sidstr)*" ) {
Write-Host "Need to add permissions to SymLink" -ForegroundColor Yellow
Write-Host "Modify Setting ""Create SymLink""" -ForegroundColor DarkCyan
if( [string]::IsNullOrEmpty($currentSetting) ) {
@@ -58,8 +58,8 @@ SECreateSymbolicLinkPrivilege = $($currentSetting)
$outfile | Set-Content -Path $tmp2 -Encoding Unicode -Force
Push-Location (Split-Path $tmp2)
try {
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
} finally {
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
} finally {
Pop-Location
}
} else {
@@ -3,4 +3,3 @@ from PyInstaller.utils.hooks import ( # pylint:disable=import-error
)
datas = copy_metadata("pydrive2")
datas += copy_metadata("google-api-python-client")
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
set -e
if [ ! -d "dvc" ]; then
echo "Please run this script from repository root"
exit 1
fi
rm -rf dist/
pip install twine
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
twine upload dist/*
-166
View File
@@ -1,166 +0,0 @@
[metadata]
description = Git for data scientists - manage your code and data together
name = dvc
long_description = file: README.rst
long_description_content_type = text/x-rst
license = Apache License 2.0
license_file = LICENSE
url = http://dvc.org
project_urls =
Documentation = https://dvc.org/doc
Source = https://github.com/iterative/dvc
download_url = https://github.com/iterative/dvc
author = Dmitry Petrov
author_email = [email protected]
maintainer = Iterative
maintainer_email = [email protected]
keywords = data-science, data-version-control, machine-learning, git
developer-tools, reproducibility, collaboration, ai
classifiers =
Development Status :: 4 - Beta
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options]
setup_requires =
setuptools>=48
setuptools_scm[toml]>=7
python_requires = >=3.8
zip_safe = False
packages = find:
include_package_data = True
install_requires =
colorama>=0.3.9
configobj>=5.0.6
voluptuous>=0.11.7
requests>=2.22.0
# See https://github.com/bdcht/grandalf/issues/26
grandalf==0.6
distro>=1.3.0
appdirs>=1.4.3
ruamel.yaml>=0.17.11
tomlkit>=0.11.1
funcy>=1.14
pathspec>=0.9.0,<0.10.0
shortuuid>=0.5.0
tqdm>=4.63.1,<5
packaging>=19.0
zc.lockfile>=1.2.1
flufl.lock>=5
networkx>=2.5
psutil>=5.8.0
pydot>=1.2.4
importlib-resources>=5.2.2; python_version < '3.9'
flatten_dict>=0.4.1,<1
tabulate>=0.8.7
pygtrie>=2.3.2
dpath>=2.0.2,<3
shtab>=1.3.4,<2
rich>=10.13.0
pyparsing>=2.4.7
typing-extensions>=3.7.4
scmrepo==0.1.2
dvc-render==0.0.12
dvc-task==0.1.4
dvclive>=0.10.0
dvc-data==0.22.0
dvc-http==2.27.2
hydra-core>=1.1.0
iterative-telemetry==0.0.5
[options.extras_require]
all =
%(azure)s
%(gdrive)s
%(gs)s
%(hdfs)s
%(oss)s
%(s3)s
%(ssh)s
%(webdav)s
%(webhdfs)s
dev =
%(all)s
%(tests)s
dvc-data[cli]
# Extra dependencies for remote integrations
azure = dvc-azure==2.20.4
gdrive = dvc-gdrive==2.19.0
gs = dvc-gs==2.19.1
hdfs =
dvc-hdfs==2.19.0; python_version < '3.11'
oss = dvc-oss==2.19.0
s3 = dvc-s3==2.20.1
ssh = dvc-ssh==2.20.0
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for Linux, so it will fail to compile if user doesn't have all the
# requirements, including kerberos itself.
ssh_gssapi = dvc-ssh[gssapi]==2.20.0
webdav = dvc-webdav==2.19.0
webhdfs = dvc-webhdfs==2.19.0
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
terraform = tpi[ssh]>=2.1.0
testing =
pytest-test-utils==0.0.8
tests =
%(terraform)s
dvc-ssh==2.20.0
# Test requirements
pytest==7.1.3
pytest-cov==4.0.0
pytest-xdist==2.5.0
pytest-mock==3.10.0
pytest-lazy-fixture==0.6.3
%(testing)s
# https://github.com/docker/docker-py/issues/2902
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
flaky==3.7.0
pytest-timeout==2.1.0
filelock==3.8.0
# pylint requirements
pylint==2.15.4
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.1.2
# we use this to suppress some messages in tests, eg: foo/bar naming,
# and, protected method calls in our tests
pylint-plugin-utils==0.7
# type-checking
mypy==0.982
types-requests>=2.27.15
types-tabulate>=0.8.6
types-toml>=0.10.4
# optional dependencies
pywin32>=225; sys_platform == 'win32'
dvclive[image]>=0.7.3
beautifulsoup4==4.11.1
[options.packages.find]
exclude =
dvc.testing
dvc.testing.*
tests
tests.*
[options.entry_points]
console_scripts =
dvc = dvc.cli:main
fsspec.specs =
dvc = dvc.api:DVCFileSystem
[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
@@ -1,3 +1,3 @@
from setuptools import setup
setup()
setup(name="dvc")
-28
View File
@@ -1,41 +1,13 @@
import os
import sys
# Increasing fd ulimit for tests
if os.name == "nt":
import subprocess
try:
import win32file # pylint: disable=import-error
except ImportError:
pass
else:
win32file._setmaxstdio(4096)
# Workaround for two bugs:
#
# 1) gitpython-developers/GitPython#546
# GitPython leaves git cat-file --batch/--batch-check processes that are
# not cleaned up correctly, so Popen._active list has their defunct
# process handles, that it is not able to cleanup because of bug 2)
#
# 2) https://bugs.python.org/issue37380
# subprocess.Popen._internal_poll on windows is getting
#
# OSError: [WinError 6] The handle is invalid
#
# exception, which it doesn't ignore and so Popen is not able to cleanup
# old processes and that prevents it from creating any new processes at
# all, which results in our tests failing whenever they try to use Popen.
# This patch was released in 3.9.0 and backported to some earlier
# versions.
if sys.version_info < (3, 9, 0):
def noop():
pass
subprocess._cleanup = noop
subprocess._active = None
else:
import resource # pylint: disable=import-error
+4 -18
View File
@@ -1,20 +1,6 @@
# Needs investigation, pylint was running tests
# Could be `pylint_pytest` plugin, skipping check for this file for now
# pylint: skip-file
if __name__ == "__main__":
import sys
import os
import sys
from subprocess import check_call
import pytest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(REPO_ROOT)
os.putenv(
"PATH", ":".join([os.path.join(REPO_ROOT, "bin"), os.getenv("PATH")])
)
os.putenv("DVC_HOME", REPO_ROOT)
opts = "-v -n=auto --dist loadscope --cov=dvc --durations=50"
params = " ".join(sys.argv[1:])
cmd = f"{sys.executable} -m pytest {opts} {params}"
check_call(cmd, shell=True)
sys.exit(pytest.main(["-v", "-n=auto", *sys.argv[1:]]))
-179
View File
@@ -1,179 +0,0 @@
import logging
import os
import tempfile
import warnings
from unittest import TestCase
import pytest
import shortuuid
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
logger = logging.getLogger("dvc")
class TestDirFixture:
DATA_DIR = "data_dir"
DATA_SUB_DIR = os.path.join(DATA_DIR, "data_sub_dir")
DATA = os.path.join(DATA_DIR, "data")
DATA_SUB = os.path.join(DATA_SUB_DIR, "data_sub")
DATA_CONTENTS = DATA
DATA_SUB_CONTENTS = DATA_SUB
FOO = "foo"
FOO_CONTENTS = FOO
BAR = "bar"
# NOTE: len(FOO_CONTENTS) must be != len(BAR_CONTENTS)
#
# Our state database relies on file mtime and file size to determine
# that a file has changed. Usually, mtime is enough by itself but on
# some filesystems like APFS on macOS mtime resolution is so bad,
# that our tests can overwrite a file in that time window without dvc
# being able to detect that, thus causing tests to fail. Usually,
# in tests, we replace foo with bar, so we need to make sure that when we
# modify a file in our tests, its content length changes.
BAR_CONTENTS = BAR + "r"
CODE = "code.py"
CODE_CONTENTS = (
"import sys\nimport shutil\n"
"shutil.copyfile(sys.argv[1], sys.argv[2])"
)
UNICODE = "тест"
UNICODE_CONTENTS = "проверка"
def __init__(self):
root_dir = self.mkdtemp()
self._root_dir = os.path.realpath(root_dir)
self._saved_dir = None
@property
def root_dir(self):
return self._root_dir
def _pushd(self, d):
if not self._saved_dir:
self._saved_dir = os.path.realpath(os.curdir)
os.chdir(d)
def _popd(self):
os.chdir(self._saved_dir)
self._saved_dir = None
def create(self, name, contents):
dname = os.path.dirname(name)
if len(dname) > 0 and not os.path.isdir(dname):
os.makedirs(dname)
with open(name, "a", encoding="utf-8") as f:
f.write(
contents
if isinstance(contents, str)
else contents.decode("utf-8")
)
@staticmethod
def mkdtemp(base_directory=None):
prefix = f"dvc-test.{os.getpid()}."
suffix = f".{shortuuid.uuid()}"
return tempfile.mkdtemp(
prefix=prefix, suffix=suffix, dir=base_directory
)
def setUp(self):
self._pushd(self._root_dir)
self.create(self.FOO, self.FOO_CONTENTS)
self.create(self.BAR, self.BAR_CONTENTS)
self.create(self.CODE, self.CODE_CONTENTS)
os.mkdir(self.DATA_DIR)
os.mkdir(self.DATA_SUB_DIR)
self.create(self.DATA, self.DATA_CONTENTS)
self.create(self.DATA_SUB, self.DATA_SUB_CONTENTS)
self.create(self.UNICODE, self.UNICODE_CONTENTS)
def tearDown(self):
self._popd()
try:
remove(self._root_dir)
except OSError as exc:
# pylint: disable=no-member
# We ignore this under Windows with a warning because it happened
# to be really hard to trace all not properly closed files.
#
# Best guess so far is that gitpython is the culprit:
# it opens files and uses __del__ to close them, which can happen
# late in current pythons. TestGitFixture and TestDvcFixture try
# to close that and it works on most of the tests, but not all.
# Repos and thus git repos are created all over the dvc ;)
if os.name == "nt" and exc.winerror == 32:
warnings.warn("Failed to remove test dir: " + str(exc))
else:
raise
class TestGitFixture(TestDirFixture):
N_RETRIES = 5
def setUp(self):
from scmrepo.git import Git
super().setUp()
self.git = Git.init(".")
self.git.add_commit(self.CODE, message="add code")
def tearDown(self):
self.git.close()
super().tearDown()
class TestDvcFixture(TestDirFixture):
def setUp(self):
super().setUp()
self.dvc = DvcRepo.init(self.root_dir, no_scm=True)
def tearDown(self):
self.dvc.close()
super().tearDown()
class TestDvcGitFixture(TestGitFixture):
def setUp(self):
super().setUp()
self.dvc = DvcRepo.init(self.root_dir)
self.dvc.scm.commit("init dvc")
def tearDown(self):
self.dvc.close()
super().tearDown()
# NOTE: Inheritance order in the classes below is important.
class TestDir(TestDirFixture, TestCase):
def __init__(self, methodName):
TestDirFixture.__init__(self)
TestCase.__init__(self, methodName)
class TestDvc(TestDvcFixture, TestCase):
def __init__(self, methodName):
TestDvcFixture.__init__(self)
TestCase.__init__(self, methodName)
self._caplog = None
self._capsys = None
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog, capsys):
self._caplog = caplog
self._capsys = capsys
class TestDvcGit(TestDvcGitFixture, TestCase):
def __init__(self, methodName):
TestDvcGitFixture.__init__(self)
TestCase.__init__(self, methodName)
self._caplog = None
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self._caplog = caplog
+19 -5
View File
@@ -1,3 +1,4 @@
import gc
import json
import os
import sys
@@ -9,7 +10,7 @@ from dvc.testing.fixtures import * # noqa, pylint: disable=wildcard-import
from .dir_helpers import * # noqa, pylint: disable=wildcard-import
from .remotes import * # noqa, pylint: disable=wildcard-import
from .utils.scriptify import scriptify
from .scripts import * # noqa, pylint: disable=wildcard-import
# Prevent updater and analytics from running their processes
os.environ["DVC_TEST"] = "true"
@@ -172,9 +173,6 @@ def custom_template(tmp_dir, dvc):
return template
scriptify_fixture = pytest.fixture(lambda: scriptify, name="scriptify")
@pytest.fixture(autouse=True)
def mocked_webbrowser_open(mocker):
mocker.patch("webbrowser.open")
@@ -218,7 +216,7 @@ defaultBranch=master
@pytest.fixture
def run_copy_metrics(tmp_dir, run_copy):
def run_copy_metrics(tmp_dir, copy_script):
def run(
file1,
file2,
@@ -262,3 +260,19 @@ def mock_hydra_conf(mocker):
# NOTE: using sentinel here so that any imports from `hydra.conf`
# return a mock.
sys.modules["hydra.conf"] = mocker.sentinel
@pytest.fixture(autouse=True)
def gc_collect_on_dvc_close_on_win_311(mocker):
if sys.version_info < (3, 11) and os.name != "nt":
return
from dvc.repo import Repo
close = Repo.close
def wrapped(repo):
close(repo)
gc.collect()
mocker.patch("dvc.repo.Repo.close", wrapped)
+2 -29
View File
@@ -47,7 +47,6 @@ from global repo template to creating everything inplace, which:
import os
import pathlib
from textwrap import dedent
import pytest
@@ -67,17 +66,7 @@ disable_other_loggers()
@pytest.fixture
def run_copy(tmp_dir, dvc):
tmp_dir.gen(
"copy.py",
(
"import sys, shutil, os\n"
"shutil.copyfile(sys.argv[1], sys.argv[2]) "
"if os.path.isfile(sys.argv[1]) "
"else shutil.copytree(sys.argv[1], sys.argv[2])"
),
)
def run_copy(tmp_dir, copy_script, dvc):
def run_copy(src, dst, **run_kwargs):
wdir = pathlib.Path(run_kwargs.get("wdir", "."))
wdir = pathlib.Path("../" * len(wdir.parts))
@@ -94,23 +83,7 @@ def run_copy(tmp_dir, dvc):
@pytest.fixture
def run_head(tmp_dir, dvc):
"""Output first line of each file to different file with '-1' appended.
Useful for tracking multiple outputs/dependencies which are not a copy
of each others.
"""
tmp_dir.gen(
{
"head.py": dedent(
"""
import sys
for file in sys.argv[1:]:
with open(file) as f, open(file +"-1","w+") as w:
w.write(f.readline())
"""
)
}
)
def run_head(tmp_dir, head_script, dvc):
script = os.path.abspath(tmp_dir / "head.py")
def run(*args, **run_kwargs):
+39 -9
View File
@@ -13,7 +13,7 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.scm import resolve_rev
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.utils.serialize import PythonFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
from tests.scripts import COPY_SCRIPT
@pytest.mark.parametrize("name", [None, "foo"])
@@ -209,8 +209,7 @@ def test_get_baseline(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.get_baseline(f"{CELERY_STASH}@{{1}}") == init_rev
def test_update_py_params(tmp_dir, scm, dvc, test_queue):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_update_py_params(tmp_dir, scm, dvc, test_queue, copy_script):
tmp_dir.gen("params.py", "INT = 1\n")
stage = dvc.run(
cmd="python copy.py params.py metrics.py",
@@ -371,8 +370,7 @@ def test_no_scm(tmp_dir):
getattr(dvc.experiments, cmd)()
def test_untracked(tmp_dir, scm, dvc, caplog, workspace):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_untracked(tmp_dir, scm, dvc, caplog, workspace, copy_script):
tmp_dir.scm_gen("params.yaml", "foo: 1", commit="track params")
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
@@ -419,7 +417,7 @@ def test_packed_args_exists(tmp_dir, scm, dvc, exp_stage, caplog):
assert "Temporary DVC file" in caplog.text
def test_list(tmp_dir, scm, dvc, exp_stage):
def _prepare_experiments(tmp_dir, scm, dvc, exp_stage):
baseline_a = scm.get_rev()
results = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp_a = first(results)
@@ -435,6 +433,18 @@ def test_list(tmp_dir, scm, dvc, exp_stage):
exp_c = first(results)
ref_info_c = first(exp_refs_by_rev(scm, exp_c))
return baseline_a, baseline_c, ref_info_a, ref_info_b, ref_info_c
def test_list(tmp_dir, scm, dvc, exp_stage):
(
baseline_a,
baseline_c,
ref_info_a,
ref_info_b,
ref_info_c,
) = _prepare_experiments(tmp_dir, scm, dvc, exp_stage)
assert dvc.experiments.ls() == {baseline_c: [ref_info_c.name]}
exp_list = dvc.experiments.ls(rev=ref_info_a.baseline_sha)
@@ -449,6 +459,25 @@ def test_list(tmp_dir, scm, dvc, exp_stage):
}
def test_list_cli(tmp_dir, scm, dvc, capsys, exp_stage):
from dvc.cli import main
baseline_a, _, ref_info_a, ref_info_b, ref_info_c = _prepare_experiments(
tmp_dir, scm, dvc, exp_stage
)
# Make sure that we prioritize the current branch name
scm.checkout("branch", True)
capsys.readouterr()
assert main(["exp", "list", "-A"]) == 0
cap = capsys.readouterr()
assert set(cap.out.split()) == set(
["branch:", baseline_a[:7] + ":"]
+ [ref_info_a.name, ref_info_b.name, ref_info_c.name]
)
def test_subdir(tmp_dir, scm, dvc, workspace):
subdir = tmp_dir / "dir"
subdir.gen("copy.py", COPY_SCRIPT)
@@ -538,7 +567,7 @@ def test_run_celery(tmp_dir, scm, dvc, exp_stage, mocker):
repro_spy = mocker.spy(dvc.experiments, "reproduce_celery")
results = dvc.experiments.run(run_all=True)
assert len(results) == 2
repro_spy.assert_called_once_with(entries=mocker.ANY, jobs=1)
repro_spy.assert_called_once_with(jobs=1)
expected = {"foo: 2", "foo: 3"}
metrics = set()
@@ -600,9 +629,10 @@ def test_fix_exp_head(tmp_dir, scm, tail):
"params, target",
itertools.product(("foo: 1", "foo: 2"), (True, False)),
)
def test_modified_data_dep(tmp_dir, scm, dvc, workspace, params, target):
def test_modified_data_dep(
tmp_dir, scm, dvc, workspace, params, target, copy_script
):
tmp_dir.dvc_gen("data", "data")
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
exp_stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
+4 -10
View File
@@ -1,12 +1,9 @@
import pytest
from funcy import first
from tests.func.test_repro_multistage import COPY_SCRIPT
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 1)])
def test_workspace(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_workspace(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -26,8 +23,7 @@ def test_workspace(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 2)])
def test_all_commits(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_commits(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -55,8 +51,7 @@ def test_all_commits(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
def test_all_branches(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_branches(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -91,8 +86,7 @@ def test_all_branches(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
def test_all_tags(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_tags(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
+8 -6
View File
@@ -345,11 +345,13 @@ def test_init_interactive_live(
"train": {
"cmd": "python script.py",
"deps": ["data", "script.py"],
"metrics": [{"dvclive.json": {"cache": False}}],
"metrics": [
{os.path.join("dvclive", "metrics.json"): {"cache": False}}
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("dvclive", "scalars"): {"cache": False}}
{os.path.join("dvclive", "plots"): {"cache": False}}
],
}
}
@@ -434,12 +436,12 @@ def test_init_with_type_default_and_live_provided(
"cmd": "cmd",
"deps": ["data", "src"],
"metrics": [
{"live.json": {"cache": False}},
{os.path.join("live", "metrics.json"): {"cache": False}},
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("live", "scalars"): {"cache": False}},
{os.path.join("live", "plots"): {"cache": False}},
],
}
}
@@ -477,13 +479,13 @@ def test_init_with_live_and_metrics_plots_provided(
"cmd": "cmd",
"deps": ["data", "src"],
"metrics": [
{"live.json": {"cache": False}},
{os.path.join("live", "metrics.json"): {"cache": False}},
{"metrics.json": {"cache": False}},
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("live", "scalars"): {"cache": False}},
{os.path.join("live", "plots"): {"cache": False}},
{"plots": {"cache": False}},
],
}
+40 -56
View File
@@ -11,64 +11,54 @@ def to_dict(tasks):
return status_dict
@pytest.fixture
def success_tasks(tmp_dir, dvc, scm, test_queue, exp_stage):
queue_length = 3
name_list = []
for i in range(queue_length):
name = f"success{i}"
name_list.append(name)
dvc.experiments.run(
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
)
dvc.experiments.run(run_all=True)
return name_list
@pytest.fixture
def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
queue_length = 3
name_list = []
for i in range(queue_length):
name = f"failed{i}"
name_list.append(name)
dvc.experiments.run(
failed_exp_stage.addressing,
params=[f"foo={i+queue_length}"],
queue=True,
name=name,
)
dvc.experiments.run(run_all=True)
return name_list
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
@pytest.mark.parametrize("follow", [True, False])
def test_celery_logs(
tmp_dir,
scm,
dvc,
failed_exp_stage,
test_queue,
follow,
capsys,
):
celery_queue = dvc.experiments.celery_queue
dvc.experiments.run(failed_exp_stage.addressing, queue=True)
dvc.experiments.run(run_all=True)
queue = dvc.experiments.celery_queue
done_result = first(queue.iter_done())
done_result = first(celery_queue.iter_done())
name = done_result.entry.stash_rev
captured = capsys.readouterr()
queue.logs(name, follow=follow)
celery_queue.logs(name, follow=follow)
captured = capsys.readouterr()
assert "failed to reproduce 'failed-copy-file'" in captured.out
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
def test_queue_remove_done(dvc, failed_tasks, success_tasks):
assert len(dvc.experiments.celery_queue.failed_stash) == 3
status = to_dict(dvc.experiments.celery_queue.status())
def test_queue_remove_done(
dvc,
exp_stage,
failed_exp_stage,
):
queue_length = 3
success_tasks = []
failed_tasks = []
celery_queue = dvc.experiments.celery_queue
for i in range(queue_length):
name = f"success{i}"
success_tasks.append(name)
dvc.experiments.run(
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
)
name_fail = f"failed{i}"
failed_tasks.append(name_fail)
dvc.experiments.run(
failed_exp_stage.addressing,
params=[f"foo={i+queue_length}"],
queue=True,
name=name_fail,
)
dvc.experiments.run(run_all=True)
assert len(celery_queue.failed_stash) == 3
status = to_dict(celery_queue.status())
assert len(status) == 6
for name in failed_tasks:
assert status[name] == "Failed"
@@ -76,27 +66,21 @@ def test_queue_remove_done(dvc, failed_tasks, success_tasks):
assert status[name] == "Success"
with pytest.raises(InvalidArgumentError):
dvc.experiments.celery_queue.remove(failed_tasks[:2] + ["non-exist"])
assert len(dvc.experiments.celery_queue.status()) == 6
celery_queue.remove(failed_tasks[:2] + ["non-exist"])
assert len(celery_queue.status()) == 6
to_remove = [failed_tasks[0], success_tasks[2]]
assert set(dvc.experiments.celery_queue.remove(to_remove)) == set(
to_remove
)
assert set(celery_queue.remove(to_remove)) == set(to_remove)
assert len(dvc.experiments.celery_queue.failed_stash) == 2
status = to_dict(dvc.experiments.celery_queue.status())
assert len(celery_queue.failed_stash) == 2
status = to_dict(celery_queue.status())
assert set(status) == set(failed_tasks[1:] + success_tasks[:2])
assert dvc.experiments.celery_queue.clear(failed=True) == failed_tasks[1:]
assert set(celery_queue.clear(failed=True)) == set(failed_tasks[1:])
assert len(dvc.experiments.celery_queue.failed_stash) == 0
assert set(to_dict(dvc.experiments.celery_queue.status())) == set(
success_tasks[:2]
)
assert len(celery_queue.failed_stash) == 0
assert set(to_dict(celery_queue.status())) == set(success_tasks[:2])
assert (
dvc.experiments.celery_queue.clear(success=True) == success_tasks[:2]
)
assert set(celery_queue.clear(success=True)) == set(success_tasks[:2])
assert dvc.experiments.celery_queue.status() == []
assert celery_queue.status() == []
+20 -2
View File
@@ -17,7 +17,7 @@ from ..utils.test_hydra import hydra_setup
def test_modify_params(params_repo, dvc, changes, expected):
dvc.experiments.run(params=changes)
# pylint: disable=unspecified-encoding
with open("params.yaml", mode="r") as fobj:
with open("params.yaml") as fobj:
assert fobj.read().strip() == expected
@@ -110,6 +110,11 @@ def test_hydra_compose_and_dump(
["foo=bar,baz"],
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
),
(
False,
[],
[{}],
),
],
)
def test_hydra_sweep(
@@ -139,5 +144,18 @@ def test_hydra_sweep(
def test_hydra_sweep_requires_queue(params_repo, dvc):
with pytest.raises(InvalidArgumentError):
with pytest.raises(
InvalidArgumentError,
match="Sweep overrides can't be used without `--queue`",
):
dvc.experiments.run(params=["db=mysql,postgresql"])
def test_hydra_sweep_cant_use_name(tmp_dir, params_repo, dvc):
with pytest.raises(
InvalidArgumentError,
match="Sweep overrides can't be used alongside `--name`",
):
dvc.experiments.run(
params=["db=mysql,postgresql"], queue=True, name="foo"
)
+131 -11
View File
@@ -1,10 +1,12 @@
import logging
import os
import random
from datetime import datetime
from unittest.mock import ANY
import pytest
from funcy import first, get_in
from scmrepo.exceptions import SCMError
from dvc.cli import main
from dvc.repo.experiments.executor.base import (
@@ -20,7 +22,6 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils import relpath
from dvc.utils.fs import makedirs
from dvc.utils.serialize import YAMLFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
LOCK_CONTENTS = {
"read": {
@@ -173,7 +174,6 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
@pytest.mark.vscode
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage):
baseline_rev = scm.get_rev()
timestamp = datetime.fromtimestamp(
@@ -313,10 +313,10 @@ def test_show_filter(
scm,
dvc,
capsys,
copy_script,
):
capsys.readouterr()
tmp_dir.gen("copy.py", COPY_SCRIPT)
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -646,6 +646,9 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
rev1 = first(result1)
ref_info1 = first(exp_refs_by_rev(scm, rev1))
# at least 1 second gap between these experiments to make sure
# the previous experiment to be regarded as branch_base
time.sleep(1)
result2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
rev2 = first(result2)
@@ -679,8 +682,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
)
def test_show_only_changed(tmp_dir, dvc, scm, capsys):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_show_only_changed(tmp_dir, dvc, scm, capsys, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -726,13 +728,14 @@ def test_show_only_changed(tmp_dir, dvc, scm, capsys):
assert "metrics.yaml:bar" in cap.out
def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
def test_show_parallel_coordinates(
tmp_dir, dvc, scm, mocker, capsys, copy_script
):
from dvc.commands.experiments import show
webbroser_open = mocker.patch("webbrowser.open")
show_experiments = mocker.spy(show, "show_experiments")
tmp_dir.gen("copy.py", COPY_SCRIPT)
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -807,8 +810,7 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
@pytest.mark.vscode
def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_show_outs(tmp_dir, dvc, scm, erepo_dir, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -889,8 +891,7 @@ def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
}
def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_metrics_renaming(tmp_dir, dvc, scm, capsys, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -975,3 +976,122 @@ def test_show_sorted_deps(tmp_dir, dvc, scm, capsys):
assert main(["exp", "show", "--csv"]) == 0
cap = capsys.readouterr()
assert "a,b,c,z" in cap.out
@pytest.mark.vscode
def test_show_queued_error(tmp_dir, scm, dvc, exp_stage, mocker):
baseline_rev = scm.get_rev()
dvc.experiments.run(
exp_stage.addressing, params=["foo=2"], queue=True, name="test_name"
)
exp_rev_2 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
commit_2 = scm.resolve_commit(exp_rev_2)
dvc.experiments.run(exp_stage.addressing, params=["foo=3"], queue=True)
exp_rev_3 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
def resolve_commit(rev):
if rev == exp_rev_3:
raise SCMError
else:
return commit_2
mocker.patch.object(
scm,
"resolve_commit",
side_effect=mocker.MagicMock(side_effect=resolve_commit),
)
results = dvc.experiments.show()[baseline_rev]
assert len(results) == 2
exp_2 = results[exp_rev_2]["data"]
assert exp_2["status"] == "Queued"
assert exp_2["params"]["params.yaml"] == {"data": {"foo": 2}}
@pytest.mark.vscode
def test_show_completed_error(tmp_dir, scm, dvc, exp_stage, mocker):
baseline_rev = scm.get_rev()
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp_rev_2 = first(result_2)
commit_2 = scm.resolve_commit(exp_rev_2)
result_3 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
exp_rev_3 = first(result_3)
def resolve_commit(rev):
if rev == exp_rev_3:
raise SCMError
else:
return commit_2
mocker.patch.object(
scm,
"resolve_commit",
side_effect=mocker.MagicMock(side_effect=resolve_commit),
)
experiments = dvc.experiments.show()[baseline_rev]
assert len(experiments) == 2
assert exp_rev_2 in experiments
@pytest.mark.vscode
def test_show_checkpoint_error(tmp_dir, scm, dvc, checkpoint_stage, mocker):
baseline_rev = scm.get_rev()
results = dvc.experiments.run(
checkpoint_stage.addressing, params=["foo=2"]
)
exp_rev = first(results)
exp_ref = str(first(exp_refs_by_rev(scm, exp_rev)))
results = dvc.experiments.show()[baseline_rev]
assert len(results) == checkpoint_stage.iterations + 1
checkpoints = {}
for rev in results:
if rev != "baseline":
checkpoints[rev] = scm.resolve_commit(rev)
checkpoints[exp_ref] = scm.resolve_commit(exp_ref)
checkpoints[baseline_rev] = scm.resolve_commit(baseline_rev)
failed_rev = random.choice(list(checkpoints.keys()))
def resolve_commit(rev):
if rev == failed_rev:
raise SCMError
return checkpoints[rev]
mocker.patch.object(
scm,
"resolve_commit",
side_effect=mocker.MagicMock(side_effect=resolve_commit),
)
results = dvc.experiments.show()[baseline_rev]
assert len(results) == 1
@pytest.mark.vscode
def test_show_baseline_error(tmp_dir, scm, dvc, exp_stage, mocker):
baseline_rev = scm.get_rev()
branch = scm.active_branch()
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp_rev_2 = first(result_2)
commit_2 = scm.resolve_commit(exp_rev_2)
def resolve_commit(rev):
if rev == baseline_rev:
raise SCMError
else:
return commit_2
mocker.patch.object(
scm,
"resolve_commit",
side_effect=mocker.MagicMock(side_effect=resolve_commit),
)
experiments = dvc.experiments.show()[baseline_rev]
assert len(experiments) == 1
assert experiments["baseline"]["data"] == {"name": branch}
assert isinstance(experiments["baseline"]["error"], SCMError)
+13 -7
View File
@@ -6,6 +6,7 @@ import pytest
from dvc.dependency import _merge_params
from dvc.parsing import DEFAULT_PARAMS_FILE, DataResolver
from dvc.parsing.context import recurse_not_a_node
from dvc.parsing.interpolate import escape_str
from . import (
CONTEXT_DATA,
@@ -272,14 +273,17 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
if list_config:
conf["parsing"]["list"] = list_config
string = "spaced string"
mixed_quote_string = "quote\"'d"
data = {
"dict": {
"foo": "foo",
"bar": 2,
"string": "spaced string",
"string": string,
"mixed_quote_string": mixed_quote_string,
"bool": True,
"bool-false": False,
"list": [1, 2, "foo"],
"list": [1, 2, "foo", mixed_quote_string],
"nested": {"foo": "foo"},
}
}
@@ -296,9 +300,10 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
bool_resolved = " --bool --no-bool-false"
if list_config is None or list_config == "nargs":
list_resolved = " --list 1 2 'foo'"
list_resolved = f" --list 1 2 foo {escape_str(mixed_quote_string)}"
else:
list_resolved = " --list 1 --list 2 --list 'foo'"
list_resolved = " --list 1 --list 2 --list foo"
list_resolved += f" --list {escape_str(mixed_quote_string)}"
assert_stage_equal(
resolver.resolve(),
@@ -306,11 +311,12 @@ def test_cmd_dict(tmp_dir, dvc, bool_config, list_config):
"stages": {
"stage1": {
"cmd": "python script.py"
" --foo 'foo' --bar 2"
" --string 'spaced string'"
" --foo foo --bar 2"
f" --string {escape_str(string)}"
f" --mixed_quote_string {escape_str(mixed_quote_string)}"
f"{bool_resolved}"
f"{list_resolved}"
" --nested.foo 'foo'"
" --nested.foo foo"
}
}
},
+23 -19
View File
@@ -257,16 +257,21 @@ def test_log_errors(
)
def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
with open("image.jpg", "wb") as fd:
@pytest.mark.parametrize("ext", ["jpg", "svg"])
def test_plots_binary(
tmp_dir, scm, dvc, run_copy_metrics, custom_template, ext
):
file1 = f"image.{ext}"
file2 = f"plot.{ext}"
with open(file1, "wb") as fd:
fd.write(b"content")
dvc.add(["image.jpg"])
dvc.add([file1])
run_copy_metrics(
"image.jpg",
"plot.jpg",
file1,
file2,
commit="run training",
plots=["plot.jpg"],
plots=[file2],
name="s2",
single_stage=False,
)
@@ -276,12 +281,12 @@ def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
scm.tag("v1")
with open("plot.jpg", "wb") as fd:
with open(file2, "wb") as fd:
fd.write(b"content2")
result = dvc.plots.show(revs=["v1", "workspace"])
assert get_plot(result, "v1", file="plot.jpg") == b"content"
assert get_plot(result, "workspace", file="plot.jpg") == b"content2"
assert get_plot(result, "v1", file=file2) == b"content"
assert get_plot(result, "workspace", file=file2) == b"content2"
def test_collect_non_existing_dir(tmp_dir, dvc, run_copy_metrics):
@@ -331,16 +336,15 @@ def test_collect_non_existing_dir(tmp_dir, dvc, run_copy_metrics):
@pytest.mark.parametrize(
"plot_config,expected_datafiles",
[
# TODO - enable providing data files for x
# (
# {
# "comparison": {
# "x": {"data1.json": "a"},
# "y": {"sub/dir/data2.json": "b"},
# }
# },
# ["data1.json", os.path.join("sub", "dir", "data2.json")],
# ),
(
{
"comparison": {
"x": {"data1.json": "a"},
"y": {"sub/dir/data2.json": "b"},
}
},
["data1.json", os.path.join("sub", "dir", "data2.json")],
),
(
{"data1.json": {"x": "c", "y": "a", "title": "File as key test"}},
["data1.json"],
+145 -194
View File
@@ -4,8 +4,7 @@ import os
import shutil
import stat
import textwrap
import time
from unittest.mock import call, patch
from unittest.mock import call
import colorama
import pytest
@@ -35,12 +34,11 @@ from dvc.stage.exceptions import (
StagePathNotFoundError,
)
from dvc.testing.workspace_tests import TestAdd
from dvc.utils import LARGE_DIR_SIZE, relpath
from dvc.utils import LARGE_DIR_SIZE
from dvc.utils.fs import path_isin
from dvc.utils.serialize import YAMLFileCorruptedError, load_yaml
from dvc.utils.serialize import YAMLFileCorruptedError
from dvc_data.hashfile.hash import file_md5
from dvc_data.hashfile.hash_info import HashInfo
from tests.basic_env import TestDvc
from tests.utils import get_gitignore_content
@@ -120,49 +118,38 @@ def test_add_directory(tmp_dir, dvc):
assert "\\" not in part
class TestAddDirectoryRecursive(TestDvc):
def test(self):
stages = self.dvc.add(self.DATA_DIR, recursive=True)
self.assertEqual(len(stages), 2)
def test_add_directory_recursive(tmp_dir, dvc):
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
stages = dvc.add("data", recursive=True)
assert len(stages) == 2
class TestAddCmdDirectoryRecursive(TestDvc):
def test(self):
ret = main(["add", "--recursive", self.DATA_DIR])
self.assertEqual(ret, 0)
def test_warn_about_large_directories(self):
warning = (
"You are adding a large directory 'large-dir' recursively."
"\nConsider tracking it as a whole instead with "
"`{cyan}dvc add large-dir{nc}`"
).format(
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
)
os.mkdir("large-dir")
# Create a lot of files
for iteration in range(LARGE_DIR_SIZE + 1):
path = os.path.join("large-dir", str(iteration))
with open(path, "w", encoding="utf-8") as fobj:
fobj.write(path)
assert main(["add", "--recursive", "large-dir"]) == 0
assert warning in self._capsys.readouterr()[1]
def test_add_cmd_directory_recursive(tmp_dir, dvc):
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
assert main(["add", "--recursive", "data"]) == 0
class TestAddDirectoryWithForwardSlash(TestDvc):
def test(self):
dname = "directory/"
os.mkdir(dname)
self.create(os.path.join(dname, "file"), "file")
stages = self.dvc.add(dname)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
self.assertEqual(os.path.abspath("directory.dvc"), stage.path)
def test_warn_about_large_directories_recursive_add(tmp_dir, dvc, capsys):
warning = (
"You are adding a large directory 'large-dir' recursively."
"\nConsider tracking it as a whole instead with "
"`{cyan}dvc add large-dir{nc}`"
).format(
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
)
tmp_dir.gen(
"large-dir", {f"{i}": f"{i}" for i in range(LARGE_DIR_SIZE + 1)}
)
assert main(["add", "--recursive", "large-dir"]) == 0
assert warning in capsys.readouterr()[1]
def test_add_directory_with_forward_slash(tmp_dir, dvc):
tmp_dir.gen("directory", {"file": "file"})
(stage,) = dvc.add("directory/")
assert stage.relpath == "directory.dvc"
def test_add_tracked_file(tmp_dir, scm, dvc):
@@ -178,33 +165,23 @@ def test_add_tracked_file(tmp_dir, scm, dvc):
dvc.add(path)
class TestAddDirWithExistingCache(TestDvc):
def test(self):
dname = "a"
fname = os.path.join(dname, "b")
os.mkdir(dname)
shutil.copyfile(self.FOO, fname)
def test_add_dir_with_existing_cache(tmp_dir, dvc):
tmp_dir.gen({"foo": "foo", "dir": {"file": "foo"}})
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
stages = self.dvc.add(dname)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
(stage,) = dvc.add("foo")
assert stage is not None
(stage,) = dvc.add("dir")
assert stage is not None
class TestAddModifiedDir(TestDvc):
def test(self):
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
os.unlink(self.DATA)
def test_add_modified_dir(tmp_dir, dvc):
tmp_dir.gen("data", {"foo": "foo", "sub": {"bar": "bar"}})
(stage,) = dvc.add("data")
assert stage is not None
time.sleep(2)
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
(tmp_dir / "data" / "foo").unlink()
(stage,) = dvc.add("data")
assert stage is not None
def test_add_file_in_dir(tmp_dir, dvc):
@@ -326,57 +303,47 @@ def test_add_external_relpath(tmp_dir, dvc, local_cloud):
assert dvc.status() == {}
class TestAddLocalRemoteFile(TestDvc):
def test(self):
"""
Making sure that 'remote' syntax is handled properly for local outs.
"""
cwd = os.getcwd()
remote = "myremote"
def test_add_local_remote_file(tmp_dir, dvc):
"""
Making sure that 'remote' syntax is handled properly for local outs.
"""
tmp_dir.gen({"foo": "foo", "bar": "bar"})
tmp_dir.add_remote(url=tmp_dir.fs_path, name="myremote")
ret = main(["remote", "add", remote, cwd])
self.assertEqual(ret, 0)
assert main(["add", "remote://myremote/foo"]) == 0
d = (tmp_dir / "foo.dvc").load_yaml()
assert d["outs"][0]["path"] == "remote://myremote/foo"
self.dvc.config.load()
foo = f"remote://{remote}/{self.FOO}"
ret = main(["add", foo])
self.assertEqual(ret, 0)
d = load_yaml("foo.dvc")
self.assertEqual(d["outs"][0]["path"], foo)
bar = os.path.join(cwd, self.BAR)
ret = main(["add", bar])
self.assertEqual(ret, 0)
d = load_yaml("bar.dvc")
self.assertEqual(d["outs"][0]["path"], self.BAR)
assert main(["add", (tmp_dir / "bar").fs_path]) == 0
d = (tmp_dir / "bar.dvc").load_yaml()
assert d["outs"][0]["path"] == "bar"
class TestCmdAdd(TestDvc):
def test(self):
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
def test_cmd_add(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo"])
assert ret == 0
ret = main(["add", "non-existing-file"])
self.assertNotEqual(ret, 0)
ret = main(["add", "non-existing-file"])
assert ret != 0
class TestDoubleAddUnchanged(TestDvc):
def test_file(self):
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
def test_double_add_unchanged_file(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
def test_dir(self):
ret = main(["add", self.DATA_DIR])
self.assertEqual(ret, 0)
ret = main(["add", self.DATA_DIR])
self.assertEqual(ret, 0)
def test_double_add_unchanged_dir(tmp_dir, dvc):
tmp_dir.gen("data", {"foo": "foo"})
ret = main(["add", "data"])
assert ret == 0
ret = main(["add", "data"])
assert ret == 0
def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
@@ -443,19 +410,17 @@ def test_should_update_state_entry_for_directory_after_add(
assert file_md5_counter.mock.call_count == 10
class TestAddCommit(TestDvc):
def test(self):
ret = main(["add", self.FOO, "--no-commit"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.isfile(self.FOO))
self.assertFalse(os.path.exists(self.dvc.odb.local.path))
def test_add_commit(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo", "--no-commit"])
assert ret == 0
assert os.path.isfile("foo")
assert not os.path.exists(dvc.odb.local.path)
ret = main(["commit", self.FOO + ".dvc"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.isfile(self.FOO))
self.assertTrue(
self.dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
)
ret = main(["commit", "foo" + ".dvc"])
assert ret == 0
assert os.path.isfile("foo")
assert dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
@@ -474,80 +439,66 @@ def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
assert counter.mock.call_count == 5
class TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink(TestDvc):
def test(self):
def is_symlink_true_below_dvc_root(path):
if path == os.path.dirname(self.dvc.root_dir):
return True
return False
def test_should_place_stage_in_data_dir_if_repository_below_symlink(
mocker, tmp_dir, dvc
):
def is_symlink_true_below_dvc_root(path):
return path == os.path.dirname(dvc.root_dir)
with patch.object(
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
):
tmp_dir.gen({"data": {"foo": "foo"}})
mocker.patch.object(
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
)
ret = main(["add", os.path.join("data", "foo")])
assert ret == 0
ret = main(["add", self.DATA])
self.assertEqual(0, ret)
stage_file_path_on_data_below_symlink = (
os.path.basename(self.DATA) + DVC_FILE_SUFFIX
)
self.assertFalse(
os.path.exists(stage_file_path_on_data_below_symlink)
)
stage_file_path = self.DATA + DVC_FILE_SUFFIX
self.assertTrue(os.path.exists(stage_file_path))
assert not (tmp_dir / "foo.dvc").exists()
assert (tmp_dir / "data" / "foo.dvc").exists()
class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc):
def test(self):
ret = main(["add", self.FOO])
assert 0 == ret
def test_should_throw_proper_exception_on_corrupted_stage_file(
caplog, tmp_dir, dvc
):
tmp_dir.gen({"foo": "foo", "bar": " bar"})
assert main(["add", "foo"]) == 0
foo_stage = relpath(self.FOO + DVC_FILE_SUFFIX)
with (tmp_dir / "foo.dvc").open("a+") as f:
f.write("this will break yaml file structure")
# corrupt stage file
with open(foo_stage, "a+", encoding="utf-8") as file:
file.write("this will break yaml file structure")
self._caplog.clear()
ret = main(["add", self.BAR])
assert 1 == ret
expected_error = (
f"unable to read: '{foo_stage}', YAML file structure is corrupted"
)
assert expected_error in self._caplog.text
caplog.clear()
assert main(["add", "bar"]) == 1
expected_error = (
"unable to read: 'foo.dvc', YAML file structure is corrupted"
)
assert expected_error in caplog.text
class TestAddFilename(TestDvc):
def test(self):
ret = main(["add", self.FOO, self.BAR, "--file", "error.dvc"])
self.assertNotEqual(0, ret)
def test_add_filename(tmp_dir, dvc):
tmp_dir.gen({"foo": "foo", "bar": "bar", "data": {"file": "file"}})
ret = main(["add", "foo", "bar", "--file", "error.dvc"])
assert ret != 0
ret = main(["add", "-R", self.DATA_DIR, "--file", "error.dvc"])
self.assertNotEqual(0, ret)
ret = main(["add", "-R", "data", "--file", "error.dvc"])
assert ret != 0
with self.assertRaises(RecursiveAddingWhileUsingFilename):
self.dvc.add(self.DATA_DIR, recursive=True, fname="error.dvc")
with pytest.raises(RecursiveAddingWhileUsingFilename):
dvc.add("data", recursive=True, fname="error.dvc")
ret = main(["add", self.DATA_DIR, "--file", "data_directory.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("data_directory.dvc"))
ret = main(["add", "data", "--file", "data_directory.dvc"])
assert ret == 0
assert (tmp_dir / "data_directory.dvc").exists()
ret = main(["add", self.FOO, "--file", "bar.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
self.assertFalse(os.path.exists("foo.dvc"))
ret = main(["add", "foo", "--file", "bar.dvc"])
assert ret == 0
assert (tmp_dir / "bar.dvc").exists()
assert not (tmp_dir / "foo.dvc").exists()
os.remove("bar.dvc")
(tmp_dir / "bar.dvc").unlink()
ret = main(["add", self.FOO, "--file", "bar.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
self.assertFalse(os.path.exists("foo.dvc"))
ret = main(["add", "foo", "--file", "bar.dvc"])
assert ret == 0
assert (tmp_dir / "bar.dvc").exists()
assert not (tmp_dir / "foo.dvc").exists()
def test_failed_add_cleanup(tmp_dir, scm, dvc):
@@ -577,25 +528,25 @@ def test_should_not_track_git_internal_files(mocker, dvc, tmp_dir):
assert ".git" not in fname
class TestAddUnprotected(TestDvc):
def test(self):
ret = main(["config", "cache.type", "hardlink"])
self.assertEqual(ret, 0)
def test_add_unprotected(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["config", "cache.type", "hardlink"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
self.assertFalse(os.access(self.FOO, os.W_OK))
self.assertTrue(system.is_hardlink(self.FOO))
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
ret = main(["unprotect", self.FOO])
self.assertEqual(ret, 0)
ret = main(["unprotect", "foo"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
self.assertFalse(os.access(self.FOO, os.W_OK))
self.assertTrue(system.is_hardlink(self.FOO))
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
@pytest.fixture

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