Compare commits

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

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

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

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

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

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

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

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

Update tests/unit/command/test_plots.py

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

Update dvc/commands/plots.py

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

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

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

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

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

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

* deps: bump dvc-s3 to 0.0.2
2022-08-20 22:59:40 +05:45
skshetry 055641a51e deps: bump dvc-data to 0.1.21 (#8147) 2022-08-19 13:22:56 +05:45
Ruslan Kuprieiev fc509d4e4a Delete hook-googleapiclient.model.py
This is already provided by pyinstaller's standard hooks https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-googleapiclient.model.py
2022-08-18 21:07:57 +03:00
Ruslan Kuprieiev a67fea2235 doctor: ignore missing plugins 2022-08-18 21:02:41 +03:00
skshetry f062b52e05 Add python version label to the readme (#8146) 2022-08-18 18:34:58 +05:45
Paweł Redzyński 25166f3e02 plots: show-vega: adjust renderers before show vega
Fixes: iterative/cml#1112
2022-08-18 12:52:52 +02:00
Ruslan Kuprieiev a79ea15827 tests: webhdfs: fix linter 2022-08-18 12:43:12 +03:00
Ruslan Kuprieiev 8db9a2f70f deps: bump dvc-data to 0.1.19 2022-08-18 12:43:12 +03:00
David de la Iglesia Castro ff2f2e433f pyinstaller: Restrict to 5.0
5.2 didn't fix https://github.com/iterative/dvc/issues/7949
2022-08-18 12:42:11 +03:00
Ruslan Kuprieiev 3fc9f323a7 fs: don't import SSHFileSystem 2022-08-18 00:23:40 +03:00
Ruslan Kuprieiev bf0aa5e8aa repo: data: ignore pylint false-positives 2022-08-17 23:02:22 +03:00
Ruslan Kuprieiev 3b3866323e deps: bump dvc-data to 0.1.18 2022-08-17 23:02:22 +03:00
Ruslan Kuprieiev 3c3cbee7fd deps: bump dvc-http to 0.0.2 2022-08-16 22:31:05 +03:00
Ruslan Kuprieiev 4a95020155 gs: add endpointurl 2022-08-14 13:39:03 +03:00
Ruslan Kuprieiev 3a429a869e tests: lockfile: use fs_path 2022-08-13 23:09:21 +03:00
Ruslan Kuprieiev 99465b25b5 deps: bump dvc-data to 0.1.17 2022-08-13 22:43:46 +03:00
Ruslan Kuprieiev fcdad4d229 deps: bump dvc-data to 0.1.16 2022-08-13 22:25:24 +03:00
Ruslan Kuprieiev 2e8b463795 deps: bump dvc-data 2022-08-12 11:58:44 +03:00
Ivan Shcheklein 206e00ccbe guard rwlock file to prevent not locked repo edits 2022-08-11 15:54:42 +03:00
Peter Rowlands f13443477d exp run: account for repo root when staging git-deps 2022-08-11 13:48:28 +09:00
Dave Berenbaum 9837747eef exp run: ui tweak (#8124) 2022-08-11 09:14:43 +05:45
dependabot[bot] cf5dfc517a build(deps-dev): Bump filelock from 3.7.1 to 3.8.0 (#8125)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.7.1...3.8.0)

---
updated-dependencies:
- dependency-name: filelock
  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-08-11 09:14:12 +05:45
dependabot[bot] c134d4ab8e build(deps): Bump dvc-data from 0.1.13 to 0.1.14 (#8117)
Bumps [dvc-data](https://github.com/iterative/dvc-data) from 0.1.13 to 0.1.14.
- [Release notes](https://github.com/iterative/dvc-data/releases)
- [Commits](https://github.com/iterative/dvc-data/compare/0.1.13...0.1.14)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 09:17:12 +05:45
skshetry 7a0b3e9377 Update tests/func/experiments/test_queue.py 2022-08-09 17:50:48 +08:00
karajan1001 d6f8e5a507 Add flaky to a queue related test.
This is a flaky, the queue tasks status might remain "Running" even if
it had already been completed, beside sometimes I also noticed it raise
a pytest fixture error.
2022-08-09 17:50:48 +08:00
Alex HallandDavid de la Iglesia Castro 2ca1d6c7c1 refactor: extract load_path function from common code using LOADERS (#7983)
Following up on https://github.com/iterative/dvc/pull/7965#discussion_r912684074

Co-authored-by: David de la Iglesia Castro <[email protected]>
2022-08-09 09:09:43 +00:00
Ivan Shcheklein f7fbac1eed stage add: fix hint message consistency (#8112) 2022-08-09 11:18:37 +05:45
skshetry 7c1e339650 data status: handle path joining properly for tree leaves/items (#8107) 2022-08-09 09:34:15 +05:45
Ivan Shcheklein b7c0c41262 lock test name fix 2022-08-09 00:47:04 +03:00
karajan1001 9180a718c1 queue status: show final experiment name
fix: #8059

1. showing final experiment name in `queue status` for successful tasks.
2. Add a unit test for `celery_queue.status`
2022-08-08 15:49:51 +09:00
karajan1001 306456d453 queue status: ERROR: Invalid experiment
fix: #8014
> ERROR: Invalid experiment '{entry.stash_rev[:7]}'.

This happens when the queue task failed from a scm error.

{"exc_type": "GitMergeError", "exc_message": ["Cannot fast-forward HEAD
to '05100047a341f2fa4a02421289d48f84d8c45e86'"], "exc_module": "dvc.scm"}.

And the failed task neither create a infofile nor create a fail_stash.

1. Didn't raise DvcException if no infofile found for failed tasks.
   (success tasks still raise it)
2. Add a new unit test for this.
2022-08-08 15:07:11 +09:00
Ivan Shcheklein 95421a5605 fix logic for unlock on unlocked lock calls 2022-08-08 14:25:10 +09:00
David de la Iglesia Castro 31635b8312 exp run: Use hydra for parsing --set-param.
- Overriding a config value : foo.bar=value
- Appending a config value : +foo.bar=value
- Appending or overriding a config value : ++foo.bar=value
- Removing a config value : ~foo.bar, ~foo.bar=value

See https://hydra.cc/docs/advanced/override_grammar/basic/#modifying-the-config-object

---

Breaking changes:

To modify a list, `foo[0]=bar` must now be passed as `foo.0=bar`.

Modifying a nested list inside a dictionary is not supported by omegaconf.

---

Closes #4883
Closes #5868
Closes #6129
2022-08-05 17:53:24 +02:00
103 changed files with 2026 additions and 897 deletions
-1
View File
@@ -1 +0,0 @@
* @iterative/dvc
+50 -110
View File
@@ -6,152 +6,92 @@ on:
release:
types: [released, prereleased]
workflow_dispatch:
jobs:
deb:
runs-on: ubuntu-18.04
binary:
strategy:
matrix:
include:
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
name: ${{ matrix.pkg }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
cache: pip
cache-dependency-path: |
setup.cfg
scripts/build-requirements.txt
- name: Set up Ruby 2.6
uses: ruby/setup-ruby@v1
if: matrix.pkg != 'exe'
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build deb
run: python scripts/build.py deb
- name: Publish deb
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_content_type: binary/octet-stream
rpm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build rpm
run: python scripts/build.py rpm
- name: Publish rpm
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_content_type: binary/octet-stream
osxpkg:
runs-on: macos-10.15
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Set up Ruby 2.4
uses: actions/[email protected]
with:
ruby-version: 2.4
- name: Install deps
run: |
# available lxml (webdav dependency) wheels are built with an old
# SDK, which breaks osxpkg notarization. Building from scratch
# instead.
pip install .[all] --no-binary lxml
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build
run: python scripts/build.py osxpkg
- name: Publish
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
asset_content_type: binary/octet-stream
exe:
runs-on: windows-2019
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
ruby-version: '2.6'
- name: Install fpm
if: matrix.pkg != 'exe'
run: gem install --no-document fpm
- name: Install deps
run: |
pip install -U wheel pip setuptools
pip install .[all]
pip install -r scripts/build-requirements.txt
- name: Pull images
run: |
dvc pull
- name: Build
run: python scripts\build.py exe
shell: cmd
- name: Publish
run: dvc pull
- name: Build ${{ matrix.pkg }}
run: python scripts/build.py ${{ matrix.pkg }}
- name: Publish ${{ matrix.pkg }}
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/[email protected]
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
asset_name: dvc-${{ github.event.release.tag_name }}.exe
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
asset_name: ${{ matrix.asset }}
asset_content_type: binary/octet-stream
pip:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: setup.cfg
- name: Install
run: pip install ".[all,tests]"
run: |
pip install -U wheel pip build setuptools
pip install ".[all,tests]"
- name: Build packages
run: |
pip install -U pip
./scripts/build_package.sh
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
+5
View File
@@ -43,6 +43,9 @@ jobs:
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.8", "3.9", "3.10"]
include:
- {os: ubuntu-latest, pyv: "3.11-dev"}
steps:
- uses: actions/checkout@v3
with:
@@ -68,6 +71,8 @@ jobs:
run: |
pip install --upgrade pip setuptools wheel
pip install -e ".[dev]"
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.pyv == '3.11-dev' && '1' }}
- name: run tests
timeout-minutes: 40
run: >-
+5 -1
View File
@@ -11,7 +11,7 @@
• `Contributing`_
• `Community and Support`_
|CI| |Maintainability| |Coverage| |VS Code| |DOI|
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
@@ -248,6 +248,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
:target: https://pypi.org/project/dvc
:alt: Python Version
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
:target: https://codecov.io/gh/iterative/dvc
:alt: Codecov
+4
View File
@@ -37,6 +37,10 @@ def params_show(
stages to retrieve parameters from.
Defaults to `None`.
If `None`, all parameters from all stages will be retrieved.
If this method is called from a different location to the one where
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
be provided as a prefix with the syntax `{relpath}:{stage}`.
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
from.
Defaults to `None`.
+23 -21
View File
@@ -31,15 +31,22 @@ class CmdDataStatus(CmdBase):
"unchanged": "DVC unchanged files",
}
HINTS = {
"not_in_cache": 'use "dvc pull <file>..." '
"to update your local storage",
"committed": "git commit the corresponding dvc files "
"to update the repo",
"uncommitted": 'use "dvc commit <file>..." to track changes',
"untracked": 'use "git add <file> ..." or '
'dvc add <file>..." to commit to git or to dvc',
"git_dirty": "there are {}changes not tracked by dvc, "
'use "git status" to see',
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
"committed": (
"git commit the corresponding dvc files to update the repo",
),
"uncommitted": (
'use "dvc commit <file>..." to track changes',
'use "dvc checkout <file>..." to discard changes',
),
"untracked": (
'use "git add <file> ..." or '
'"dvc add <file>..." to commit to git or to dvc',
),
"git_dirty": (
"there are {}changes not tracked by dvc, "
'use "git status" to see',
),
}
@staticmethod
@@ -76,8 +83,9 @@ class CmdDataStatus(CmdBase):
color = cls.COLORS.get(stage, None)
ui.write(header)
if hint := cls.HINTS.get(stage):
ui.write(f" ({hint})")
if hints := cls.HINTS.get(stage):
for hint in hints:
ui.write(f" ({hint})")
if isinstance(stage_status, dict):
items = [
@@ -92,9 +100,10 @@ class CmdDataStatus(CmdBase):
out = "\n".join(tabs + item for item in chunk)
ui.write(colorize(out, color))
if (hint := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
message = hint.format("other " if result else "")
ui.write(f"[blue]({message})[/]", styled=True)
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
for hint in hints:
message = hint.format("other " if result else "")
ui.write(f"[blue]({message})[/]", styled=True)
return 0
def run(self) -> int:
@@ -102,7 +111,6 @@ class CmdDataStatus(CmdBase):
status = self.repo.data_status(
granular=self.args.granular,
untracked_files=self.args.untracked_files,
with_dirs=self.args.with_dirs,
)
if not self.args.unchanged:
@@ -172,10 +180,4 @@ def add_parser(subparsers, parent_parser):
nargs="?",
help="Show untracked files.",
)
data_status_parser.add_argument(
"--with-dirs",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
data_status_parser.set_defaults(func=CmdDataStatus)
+2 -1
View File
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
)
self.log_summary(stats)
except (CheckoutError, DvcException) as exc:
self.log_summary(getattr(exc, "stats", {}))
if stats := getattr(exc, "stats", {}):
self.log_summary(stats)
logger.exception("failed to pull data from the cloud")
return 1
+1 -1
View File
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
if self.args.json:
ui.write_json(diff)
else:
elif diff:
from dvc.compare import show_diff
precision = self.args.precision or DEFAULT_PRECISION
+11 -2
View File
@@ -19,6 +19,7 @@ class CmdImport(CmdBase):
fname=self.args.file,
rev=self.args.rev,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
desc=self.args.desc,
jobs=self.args.jobs,
)
@@ -69,11 +70,19 @@ def add_parser(subparsers, parent_parser):
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
)
import_parser.add_argument(
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create .dvc file without actually downloading it.",
help="Only create .dvc file without actually importing target data.",
)
no_download_exec_group.add_argument(
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
)
import_parser.add_argument(
"--desc",
+11 -2
View File
@@ -17,6 +17,7 @@ class CmdImportUrl(CmdBase):
out=self.args.out,
fname=self.args.file,
no_exec=self.args.no_exec,
no_download=self.args.no_download,
remote=self.args.remote,
to_remote=self.args.to_remote,
desc=self.args.desc,
@@ -66,11 +67,19 @@ def add_parser(subparsers, parent_parser):
help="Specify name of the .dvc file this command will generate.",
metavar="<filename>",
).complete = completion.DIR
import_parser.add_argument(
no_download_exec_group = import_parser.add_mutually_exclusive_group()
no_download_exec_group.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create .dvc file without actually downloading it.",
help="Only create .dvc file without actually importing target data.",
)
no_download_exec_group.add_argument(
"--no-download",
action="store_true",
default=False,
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
)
import_parser.add_argument(
"--to-remote",
+24 -32
View File
@@ -155,11 +155,6 @@ class CmdPlots(CmdBase):
out=renderers_out,
templates_dir=self.repo.plots.templates_dir,
)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
ui.write_json(json.loads(renderer.get_filled_template()))
return 0
if self.args.json:
renderers = _filter_unhandled_renderers(renderers)
_show_json(renderers, self.args.split)
@@ -167,6 +162,12 @@ class CmdPlots(CmdBase):
_adjust_vega_renderers(renderers)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
ui.write_json(json.loads(renderer.get_filled_template()))
return 0
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
if renderers:
@@ -222,31 +223,22 @@ class CmdPlotsModify(CmdPlots):
class CmdPlotsTemplates(CmdBase):
TEMPLATES_CHOICES = [
"simple",
"linear",
"confusion",
"confusion_normalized",
"scatter",
"smooth",
"bar_horizontal_sorted",
"bar_horizontal",
]
def run(self):
from dvc_render.vega_templates import dump_templates
from dvc.exceptions import InvalidArgumentError
from dvc_render.vega_templates import TEMPLATES
try:
out = (
os.path.join(os.getcwd(), self.args.out)
if self.args.out
else self.repo.plots.templates_dir
)
target = self.args.template
if target:
for template in TEMPLATES:
if target == template.DEFAULT_NAME:
ui.write_json(template.DEFAULT_CONTENT)
return 0
raise InvalidArgumentError(f"Unexpected template: {target}.")
targets = [self.args.target] if self.args.target else None
dump_templates(output=out, targets=targets)
templates_path = os.path.relpath(out, os.getcwd())
ui.write(f"Templates have been written into '{templates_path}'.")
else:
for template in TEMPLATES:
ui.write(template.DEFAULT_NAME)
return 0
except DvcException:
@@ -356,8 +348,7 @@ def add_parser(subparsers, parent_parser):
plots_modify_parser.set_defaults(func=CmdPlotsModify)
TEMPLATES_HELP = (
"Write built-in plots templates to a directory "
"(.dvc/plots by default)."
"List built-in plots templates or show JSON specification for one."
)
plots_templates_parser = plots_subparsers.add_parser(
"templates",
@@ -367,13 +358,14 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_templates_parser.add_argument(
"target",
"template",
default=None,
nargs="?",
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
help="Template to write. Writes all templates by default.",
help=(
"Template for which to show JSON specification. "
"List all template names by default."
),
)
_add_output_argument(plots_templates_parser, typ="templates")
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
+8
View File
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
rev=self.args.rev,
recursive=self.args.recursive,
to_remote=self.args.to_remote,
no_download=self.args.no_download,
remote=self.args.remote,
jobs=self.args.jobs,
)
@@ -55,6 +56,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Update all stages in the specified directory.",
)
update_parser.add_argument(
"--no-download",
action="store_true",
default=False,
help="Update .dvc file hash value(s)"
" but do not download the file(s).",
)
update_parser.add_argument(
"--to-remote",
action="store_true",
+1
View File
@@ -168,6 +168,7 @@ SCHEMA = {
"gs": {
"projectname": str,
"credentialpath": str,
"endpointurl": str,
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
+2 -5
View File
@@ -8,7 +8,7 @@ import dpath.util
from voluptuous import Any
from dvc.exceptions import DvcException
from dvc.utils.serialize import LOADERS, ParseError
from dvc.utils.serialize import ParseError, load_path
from dvc_data.hashfile.hash_info import HashInfo
from .base import Dependency
@@ -139,12 +139,9 @@ class ParamsDependency(Dependency):
)
def read_file(self):
_, ext = os.path.splitext(self.fs_path)
loader = LOADERS[ext]
self.validate_filepath()
try:
return loader(self.fs_path, fs=self.repo.fs)
return load_path(self.fs_path, self.repo.fs)
except ParseError as exc:
raise BadParamFileError(
f"Unable to read parameters from '{self}'"
+20 -22
View File
@@ -1,26 +1,18 @@
from urllib.parse import urlparse
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
# pylint: disable=unused-import
from dvc_objects.fs import utils # noqa: F401
from dvc_objects.fs import ( # noqa: F401
FS_MAP,
AzureFileSystem,
GDriveFileSystem,
GSFileSystem,
HDFSFileSystem,
HTTPFileSystem,
HTTPSFileSystem,
LocalFileSystem,
MemoryFileSystem,
OSSFileSystem,
S3FileSystem,
Schemes,
SSHFileSystem,
WebDAVFileSystem,
WebDAVSFileSystem,
WebHDFSFileSystem,
generic,
get_fs_cls,
known_implementations,
localfs,
registry,
system,
)
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
@@ -29,17 +21,26 @@ from dvc_objects.fs.errors import ( # noqa: F401
ConfigError,
RemoteMissingDepsError,
)
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
from dvc_objects.fs.implementations.local import localfs # noqa: F401
from dvc_objects.fs.implementations.ssh import ( # noqa: F401
DEFAULT_PORT as DEFAULT_SSH_PORT,
)
from dvc_objects.fs.path import Path # noqa: F401
from .data import DataFileSystem # noqa: F401
from .dvc import DvcFileSystem # noqa: F401
from .git import GitFileSystem # noqa: F401
known_implementations.update(
{
"dvc": {
"class": "dvc.fs.dvc.DvcFileSystem",
"err": "dvc is supported, but requires 'dvc' to be installed",
},
"git": {
"class": "dvc.fs.git.GitFileSystem",
"err": "git is supported, but requires 'dvc' to be installed",
},
}
)
# pylint: enable=unused-import
@@ -110,11 +111,8 @@ def get_cloud_fs(repo, **kwargs):
cls = get_fs_cls(remote_conf)
if cls == GDriveFileSystem and repo:
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
url = remote_conf.pop("url")
if issubclass(cls, WebDAVFileSystem):
if cls.protocol in ["webdav", "webdavs"]:
# For WebDAVFileSystem, provided url is the base path itself, so it
# should be treated as being a root path.
fs_path = cls.root_marker
+34 -35
View File
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
Key = Tuple[str, ...]
def as_posix(path: str) -> str:
@@ -33,6 +34,14 @@ def _wrap_walk(dvc_fs, *args, **kwargs):
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
# NOT the same as dvc.dvcfile.is_dvc_file()!
def _is_dvc_file(fname):
from dvc.dvcfile import is_valid_filename
from dvc.ignore import DvcIgnore
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
def _ls(fs, path):
dnames = []
fnames = []
@@ -137,12 +146,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
if hasattr(repo, "dvc_dir"):
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
def _get_key(self, path):
def _get_key(self, path) -> Key:
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
if parts == (".",):
parts = ()
return parts
def _get_key_from_relative(self, path) -> Key:
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
parts = parts[1:]
return parts
def _from_key(self, parts: Key) -> str:
return self.repo.fs.path.join(self.repo.root_dir, *parts)
@property
def repo_url(self):
if self.repo is None:
@@ -208,7 +226,7 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
) as repo:
return repo, factory
def _get_repo(self, path: str) -> "Repo":
def _get_repo(self, key: Key) -> "Repo":
"""Returns repo that the path falls in, using prefix.
If the path is already tracked/collected, it just returns the repo.
@@ -216,11 +234,6 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
Otherwise, it collects the repos that might be in the path's parents
and then returns the appropriate one.
"""
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
# outside of repo
return self.repo
key = self._get_key(path)
repo = self._subrepos_trie.get(key)
if repo:
return repo
@@ -231,6 +244,7 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
*prefix_key, # pylint: disable=not-an-iterable
)
path = self._from_key(key)
parents = (parent for parent in self.repo.fs.path.parents(path))
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
dirs.reverse()
@@ -277,24 +291,20 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
"""
Returns a pair of fss based on repo the path falls in, using prefix.
"""
parts = self.path.relparts(path, self.root_marker)
if parts and parts[0] == os.curdir:
parts = parts[1:]
key = self._get_key_from_relative(path)
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
repo = self._get_repo(fs_path)
fs_path = self._from_key(key)
repo = self._get_repo(key)
fs = repo.fs
if repo is self.repo:
dvc_parts = key
dvc_fs = self._datafss.get(())
else:
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
dvc_parts = key[len(repo_parts) :]
key = self._get_key(repo.root_dir)
dvc_fs = self._datafss.get(key)
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
if repo_parts[0] == os.curdir:
repo_parts = repo_parts[1:]
dvc_parts = parts[len(repo_parts) :]
if dvc_parts and dvc_parts[0] == os.curdir:
dvc_parts = dvc_parts[1:]
key = self._get_key(repo.root_dir)
dvc_fs = self._datafss.get(key)
if dvc_fs:
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
else:
@@ -345,19 +355,8 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
pass
dvcfiles = kwargs.get("dvcfiles", False)
def _func(fname):
from dvc.dvcfile import is_valid_filename
from dvc.ignore import DvcIgnore
if dvcfiles:
return True
return not (
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
)
names = filter(_func, names)
if not dvcfiles:
names = (name for name in names if not _is_dvc_file(name))
infos = []
paths = []
+31 -2
View File
@@ -8,12 +8,20 @@ import psutil
from dvc import __version__
from dvc.exceptions import NotDvcRepoError
from dvc.fs import FS_MAP, generic, get_fs_cls, get_fs_config
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
from dvc.repo import Repo
from dvc.scm import SCMError
from dvc.utils import error_link
from dvc.utils.pkg import PKG
SUBPROJECTS = (
"dvc_data",
"dvc_objects",
"dvc_render",
"dvc_task",
"dvclive",
"scmrepo",
)
package = "" if PKG is None else f"({PKG})"
@@ -23,6 +31,7 @@ def get_dvc_info():
"---------------------------------",
f"Platform: Python {platform.python_version()} on "
f"{platform.platform()}",
f"Subprojects:{_get_subprojects()}",
f"Supports:{_get_supported_remotes()}",
]
@@ -88,9 +97,29 @@ def _get_linktype_support_info(repo):
return ", ".join(links)
def _get_subprojects():
subprojects = []
for subproject in SUBPROJECTS:
try:
version = importlib_metadata.version(subproject)
subprojects.append(f"{subproject} = {version}")
except ImportError:
pass
return "\n\t" + "\n\t".join(subprojects)
def _get_supported_remotes():
supported_remotes = []
for scheme, fs_cls in FS_MAP.items():
for scheme in registry:
if scheme in [Schemes.LOCAL, Schemes.MEMORY, "dvc", "git"]:
continue
try:
fs_cls = registry[scheme]
except ImportError:
continue
if not fs_cls.get_missing_deps():
dependencies = []
for requirement in fs_cls.REQUIRES:
+11
View File
@@ -70,6 +70,8 @@ class LockNoop(LockBase):
self._lock = True
def unlock(self):
if not self.is_locked:
raise DvcException("Unlock called on an unlocked lock")
self._lock = False
@property
@@ -93,6 +95,7 @@ class Lock(LockBase):
super().__init__(lockfile)
self._friendly = friendly
self._lock = None
self._lock_failed = False
@property
def files(self):
@@ -100,6 +103,7 @@ class Lock(LockBase):
def _do_lock(self):
try:
self._lock_failed = False
with Tqdm(
bar_format="{desc}",
disable=not self._friendly,
@@ -111,6 +115,7 @@ class Lock(LockBase):
):
self._lock = zc.lockfile.LockFile(self._lockfile)
except zc.lockfile.LockError:
self._lock_failed = True
raise LockError(FAILED_TO_LOCK_MESSAGE)
def lock(self):
@@ -120,6 +125,12 @@ class Lock(LockBase):
lock_retry()
def unlock(self):
if self._lock_failed:
assert self._lock is None
return
if not self.is_locked:
raise DvcException("Unlock called on an unlocked lock")
self._lock.close()
self._lock = None
+2 -1
View File
@@ -3,7 +3,8 @@ from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator, Optional
if TYPE_CHECKING:
from dvc.fs import SSHFileSystem
from dvc_ssh import SSHFileSystem
from dvc.types import StrPath
+3 -2
View File
@@ -3,8 +3,9 @@ from contextlib import contextmanager
from functools import partial, partialmethod
from typing import TYPE_CHECKING, Iterator, Optional
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
from dvc.exceptions import DvcException
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
from dvc.utils.fs import makedirs
from .base import BaseMachineBackend
@@ -68,7 +69,7 @@ class TerraformBackend(BaseMachineBackend):
resource = tpi.default_resource(name)
return {
"host": resource["instance_ip"],
"port": DEFAULT_SSH_PORT,
"port": DEFAULT_PORT,
"username": "ubuntu",
"fs_factory": partial(_sshfs, dict(resource)),
}
+11 -11
View File
@@ -28,14 +28,7 @@ from dvc_data.hashfile.meta import Meta
from dvc_data.transfer import transfer as otransfer
from dvc_objects.errors import ObjectFormatError
from .fs import (
HDFSFileSystem,
LocalFileSystem,
RemoteMissingDepsError,
S3FileSystem,
Schemes,
get_cloud_fs,
)
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
from .utils import relpath
from .utils.fs import path_isin
@@ -65,10 +58,12 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
#
# so when a few types of outputs share the same name, we only need
# specify it once.
HDFS_PARAM_CHECKSUM = "checksum"
S3_PARAM_CHECKSUM = "etag"
CHECKSUMS_SCHEMA = {
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
}
@@ -380,6 +375,11 @@ class Output:
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
def clear(self):
self.hash_info = HashInfo.from_dict({})
self.meta = Meta.from_dict({})
self.obj = None
@property
def protocol(self):
return self.fs.protocol
@@ -737,7 +737,7 @@ class Output:
elif self.hash_info:
try:
obj = oload(self.odb, self.hash_info)
except FileNotFoundError:
except (FileNotFoundError, ObjectFormatError):
return None
else:
return None
+2 -6
View File
@@ -1,5 +1,4 @@
import logging
import os
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
@@ -357,17 +356,14 @@ class Context(CtxDict):
def load_from(
cls, fs, path: str, select_keys: List[str] = None
) -> "Context":
from dvc.utils.serialize import LOADERS
from dvc.utils.serialize import load_path
if not fs.exists(path):
raise ParamsLoadError(f"'{path}' does not exist")
if fs.isdir(path):
raise ParamsLoadError(f"'{path}' is a directory")
_, ext = os.path.splitext(path)
loader = LOADERS[ext]
data = loader(path, fs=fs)
data = load_path(path, fs)
if not isinstance(data, Mapping):
typ = type(data).__name__
raise ParamsLoadError(
+40 -1
View File
@@ -3,7 +3,7 @@ import os
from collections import defaultdict
from contextlib import contextmanager
from functools import wraps
from typing import TYPE_CHECKING, Callable, Optional
from typing import TYPE_CHECKING, Callable, List, Optional
from funcy import cached_property
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc.repo.scm_context import SCMContext
from dvc.scm import Base
from dvc.stage import Stage
logger = logging.getLogger(__name__)
@@ -444,6 +445,44 @@ class Repo:
return used
def partial_imports(
self,
targets=None,
all_branches=False,
all_tags=False,
all_commits=False,
all_experiments=False,
commit_date: Optional[str] = None,
recursive=False,
revs=None,
num=1,
) -> List["Stage"]:
"""Get the stages related to the given target and collect dependencies
which are missing outputs.
This is useful to retrieve files which have been imported to the repo
using --no-download.
Returns:
A list of partially imported stages
"""
from itertools import chain
partial_imports = chain.from_iterable(
self.index.partial_imports(targets, recursive=recursive)
for _ in self.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
all_experiments=all_experiments,
commit_date=commit_date,
num=num,
)
)
return list(partial_imports)
@property
def stages(self): # obsolete, only for backward-compatibility
return self.index.stages
+100 -71
View File
@@ -1,6 +1,15 @@
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, cast
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
Optional,
TypedDict,
cast,
)
from dvc.ui import ui
@@ -10,19 +19,16 @@ if TYPE_CHECKING:
from dvc.output import Output
from dvc.repo import Repo
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.obj import HashFile
def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
from dvc_objects.errors import ObjectFormatError
if not obj:
return False
if not obj.hash_info.value:
return False
assert obj.value
try:
cache.check(obj.hash_info.value)
cache.check(obj.value)
return True
except (FileNotFoundError, ObjectFormatError):
return False
@@ -30,31 +36,30 @@ def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
def _shallow_diff(
root: str,
old_obj: Optional["HashFile"],
new_obj: Optional["HashFile"],
old: Optional["HashInfo"],
new: Optional["HashInfo"],
cache: "HashFileDB",
) -> Dict[str, List[str]]:
# TODO: add support for shallow diff in dvc-data
# TODO: we may want to recursively do in_cache check
d = {}
from dvc_data.objects.tree import Tree
old_root = new_root = root
if old and old.isdir:
old_root = os.path.sep.join([root, ""])
if new and new.isdir:
new_root = os.path.sep.join([root, ""])
if isinstance(new_obj, Tree):
root = os.path.sep.join([root, ""])
if old and not _in_cache(old, cache):
d["not_in_cache"] = [old_root]
if not _in_cache(old_obj, cache):
d["not_in_cache"] = [root]
if old_obj is None and new_obj is None:
if not old and not new:
return d
if old_obj is None:
return {"added": [root], **d}
if new_obj is None:
return {"deleted": [root], **d}
if old_obj.hash_info != new_obj.hash_info:
return {"modified": [root], **d}
return {"unchanged": [root], **d}
if not new:
return {"deleted": [old_root], **d}
if not old:
return {"added": [new_root], **d}
if old != new:
return {"modified": [new_root], **d}
return {"unchanged": [new_root], **d}
def _granular_diff(
@@ -62,55 +67,60 @@ def _granular_diff(
old_obj: Optional["HashFile"],
new_obj: Optional["HashFile"],
cache: "HashFileDB",
with_dirs: bool = False,
) -> Dict[str, List[str]]:
from dvc_data.diff import ROOT
from dvc_data.diff import diff as odiff
from dvc_data.objects.tree import Tree
def path_join(root: str, *paths: str) -> str:
if not isinstance(new_obj, Tree):
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
if not isdir and paths == ROOT:
return root
return os.path.sep.join([root, *paths])
diff_data = odiff(old_obj, new_obj, cache)
drop_root = not with_dirs and isinstance(new_obj, Tree)
output: Dict[str, List[str]] = defaultdict(list)
for state in ("added", "deleted", "modified", "unchanged"):
items = getattr(diff_data, state)
output[state].extend(
path_join(root, *item.new.key)
for item in items
if not (drop_root and item.new.key == ROOT)
)
# TODO: PERF: diff is checking not_in_cache for each even if we only
# need it for the index.
# BUG: not_in_cache file also shows up as modified in staged and
# unstaged. We currently don't know if it is really modified.
output["not_in_cache"].extend(
path_join(root, *item.new.key)
for item in items
if not item.old.in_cache
and not (drop_root and item.new.key == ROOT)
and state != "added"
)
return output
for item in items: # pylint: disable=not-an-iterable
entry = item.old if state == "deleted" else item.new
isdir = entry.oid.isdir if entry.oid else False
path = path_join(root, *entry.key, isdir=isdir)
output[state].append(path)
if not item.old.in_cache and state != "added":
output["not_in_cache"].append(path)
return dict(output)
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
if not obj:
return []
from dvc_data.objects.tree import Tree
sep = os.path.sep
if isinstance(obj, Tree):
return [sep.join([root, *key]) for key, _, _ in obj]
return [root]
def _diff(
root: str,
old_oid: Optional["HashInfo"],
old_obj: Optional["HashFile"],
new_oid: Optional["HashInfo"],
new_obj: Optional["HashFile"],
cache: "HashFileDB",
odb: "HashFileDB",
granular: bool = False,
with_dirs: bool = False,
) -> Dict[str, List[str]]:
if granular:
return _granular_diff(
root, old_obj, new_obj, cache, with_dirs=with_dirs
)
return _shallow_diff(root, old_obj, new_obj, cache)
if not granular:
return _shallow_diff(root, old_oid, new_oid, odb)
if (old_oid and not old_obj) or (new_oid and not new_obj):
# we don't have enough information to give full details
unknown = _get_obj_items(root, new_obj)
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
return {**shallow_diff, "unknown": unknown}
return _granular_diff(root, old_obj, new_obj, odb)
class GitInfo(TypedDict, total=False):
@@ -171,8 +181,18 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
cache = repo.odb.repo
root = str(out)
old = out.get_obj()
with ui.status(f"Calculating diff for {root} between index/workspace"):
d = _diff(root, old, new, cache, **kwargs)
d = _diff(
root,
out.hash_info,
old,
new.hash_info if new else None,
new,
cache,
**kwargs,
)
for state, items in d.items():
if not items:
continue
@@ -194,14 +214,17 @@ def _diff_head_to_index(
root = str(out)
typ = "index" if rev == "workspace" else head
objs[root][typ] = out.get_obj()
objs[root][typ] = (out.get_obj(), out.hash_info)
cache = repo.odb.repo
for root, obj_d in objs.items():
old = obj_d.get(head, None)
new = obj_d.get("index", None)
old_obj, old_oid = obj_d.get(head, (None, None))
new_obj, new_oid = obj_d.get("index", (None, None))
with ui.status(f"Calculating diff for {root} between head/index"):
d = _diff(root, old, new, cache, **kwargs)
d = _diff(
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
)
for state, items in d.items():
if not items:
continue
@@ -212,28 +235,34 @@ def _diff_head_to_index(
class Status(TypedDict):
not_in_cache: List[str]
committed: Dict[str, Any]
uncommitted: Dict[str, Any]
committed: Dict[str, List[str]]
uncommitted: Dict[str, List[str]]
untracked: List[str]
unchanged: List[str]
git: GitInfo
def _transform_git_paths_to_dvc(repo: "Repo", files: List[str]):
def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]):
"""Transform files rel. to Git root to DVC root, and drop outside files."""
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
if rel in (os.curdir, ""):
return files
prefix = rel + os.sep
length = len(prefix)
return [file[length:] for file in files if file.startswith(prefix)]
# if we have repo root in a different location than scm's root,
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
# repo root and anything outside need to be filtered out.
if rel not in (os.curdir, ""):
prefix = rel + os.sep
length = len(prefix)
files = (file[length:] for file in files if file.startswith(prefix))
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
if start in (os.curdir, ""):
return list(files)
# we need to convert repo relative paths to curdir relative.
return [repo.fs.path.relpath(file, start) for file in files]
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
from scmrepo.exceptions import SCMError
from dvc.scm import NoSCMError
from dvc.scm import NoSCMError, SCMError
head = kwargs.pop("head", "HEAD")
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
+1 -1
View File
@@ -214,7 +214,7 @@ class Experiments:
for rev in revs:
name = self.get_exact_name(rev)
names.append(name if name else rev[:7])
ui.write("\nReproduced experiment(s): {}".format(", ".join(names)))
ui.write("\nRan experiment(s): {}".format(", ".join(names)))
if tmp_dir:
ui.write(
"To apply the results of an experiment to your workspace "
+1 -2
View File
@@ -5,10 +5,9 @@ import sys
from contextlib import contextmanager
from typing import TYPE_CHECKING, Callable, Iterable, Optional
from dvc_ssh import SSHFileSystem
from funcy import first
from dvc.fs import SSHFileSystem
from ..refs import (
EXEC_BRANCH,
EXEC_CHECKPOINT,
+33 -21
View File
@@ -1,5 +1,6 @@
import logging
import os
import sys
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import (
@@ -19,7 +20,6 @@ from typing import (
from funcy import cached_property
from dvc.dependency.param import MissingParamsError
from dvc.env import DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.ui import ui
@@ -185,9 +185,12 @@ class BaseStashQueue(ABC):
def _format_entry(
entry: QueueEntry,
exp_result: Optional[ExecutorResult] = None,
status: str = "Unknown",
) -> Dict[str, Any]:
name = entry.name
if not name and exp_result and exp_result.ref_info:
name = exp_result.ref_info.name
# NOTE: We fallback to Unknown status for experiments
# generated in prior (incompatible) DVC versions
return {
@@ -210,8 +213,8 @@ class BaseStashQueue(ABC):
for queue_entry, _ in self.iter_failed()
)
result.extend(
_format_entry(queue_entry, status="Success")
for queue_entry, _ in self.iter_success()
_format_entry(queue_entry, exp_result=exp_result, status="Success")
for queue_entry, exp_result in self.iter_success()
)
return result
@@ -283,7 +286,7 @@ class BaseStashQueue(ABC):
def _stash_exp(
self,
*args,
params: Optional[dict] = None,
params: Optional[Dict[str, List[str]]] = None,
resume_rev: Optional[str] = None,
baseline_rev: Optional[str] = None,
branch: Optional[str] = None,
@@ -292,10 +295,9 @@ class BaseStashQueue(ABC):
) -> QueueEntry:
"""Stash changes from the workspace as an experiment.
Arguments:
params: Optional dictionary of parameter values to be used.
Values take priority over any parameters specified in the
user's workspace.
Args:
params: Dict mapping paths to `Hydra Override`_ patterns,
provided via `exp run --set-param`.
resume_rev: Optional checkpoint resume rev.
baseline_rev: Optional baseline rev for this experiment, defaults
to the current SCM rev.
@@ -305,6 +307,9 @@ class BaseStashQueue(ABC):
name: Optional experiment name. If specified this will be used as
the human-readable name in the experiment branch ref. Has no
effect of branch is specified.
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
"""
with self.scm.detach_head(client="dvc") as orig_head:
stash_head = orig_head
@@ -508,22 +513,29 @@ class BaseStashQueue(ABC):
f"from '{config_path}': {param_list}"
)
def _update_params(self, params: dict):
"""Update experiment params files with the specified values."""
from dvc.utils.collections import NewParamsFound, merge_params
from dvc.utils.serialize import MODIFIERS
def _update_params(self, params: Dict[str, List[str]]):
"""Update param files with the provided `Hydra Override`_ patterns.
Args:
params: Dict mapping paths to `Hydra Override`_ patterns,
provided via `exp run --set-param`.
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
"""
logger.debug("Using experiment params '%s'", params)
for path in params:
suffix = self.repo.fs.path.suffix(path).lower()
modify_data = MODIFIERS[suffix]
with modify_data(path, fs=self.repo.fs) as data:
try:
merge_params(data, params[path], allow_new=False)
except NewParamsFound as e:
msg = self._format_new_params_msg(e.new_params, path)
raise MissingParamsError(msg)
try:
from dvc.utils.hydra import apply_overrides
except ValueError:
if sys.version_info >= (3, 11):
raise DvcException(
"--set-param is not supported in Python >= 3.11"
)
raise
for path, overrides in params.items():
apply_overrides(path, overrides)
# Force params file changes to be staged in git
# Otherwise in certain situations the changes to params file may be
+21 -24
View File
@@ -14,6 +14,7 @@ from typing import (
Set,
)
from celery.result import AsyncResult
from funcy import cached_property
from kombu.message import Message
@@ -23,7 +24,6 @@ from dvc.ui import ui
from ..exceptions import UnresolvedQueueExpNamesError
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
from ..stash import ExpStashEntry
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
from .tasks import run_exp
@@ -41,7 +41,7 @@ class _MessageEntry(NamedTuple):
class _TaskEntry(NamedTuple):
task_id: str
async_result: AsyncResult
entry: QueueEntry
@@ -192,30 +192,37 @@ class LocalCeleryQueue(BaseStashQueue):
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
def _iter_active_tasks(self) -> Generator[_TaskEntry, None, None]:
from celery.result import AsyncResult
for msg, entry in self._iter_processed():
task_id = msg.headers["id"]
result: AsyncResult = AsyncResult(task_id)
if not result.ready():
yield _TaskEntry(task_id, entry)
yield _TaskEntry(result, entry)
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
from celery.result import AsyncResult
for msg, entry in self._iter_processed():
task_id = msg.headers["id"]
result: AsyncResult = AsyncResult(task_id)
if result.ready():
yield _TaskEntry(task_id, entry)
yield _TaskEntry(result, entry)
def iter_active(self) -> Generator[QueueEntry, None, None]:
for _, entry in self._iter_active_tasks():
yield entry
def iter_done(self) -> Generator[QueueDoneResult, None, None]:
for _, entry in self._iter_done_tasks():
yield QueueDoneResult(entry, self.get_result(entry))
for result, entry in self._iter_done_tasks():
try:
exp_result = self.get_result(entry)
except FileNotFoundError:
if result.status == "SUCCESS":
raise DvcException(
f"Invalid experiment '{entry.stash_rev[:7]}'."
)
elif result.status == "FAILURE":
exp_result = None
yield QueueDoneResult(entry, exp_result)
def iter_success(self) -> Generator[QueueDoneResult, None, None]:
for queue_entry, exp_result in self.iter_done():
@@ -223,14 +230,8 @@ class LocalCeleryQueue(BaseStashQueue):
yield QueueDoneResult(queue_entry, exp_result)
def iter_failed(self) -> Generator[QueueDoneResult, None, None]:
failed_revs: Dict[str, ExpStashEntry] = (
dict(self.failed_stash.stash_revs)
if self.failed_stash is not None
else {}
)
for queue_entry, exp_result in self.iter_done():
if exp_result is None and queue_entry.stash_rev in failed_revs:
if exp_result is None:
yield QueueDoneResult(queue_entry, exp_result)
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
@@ -240,7 +241,6 @@ class LocalCeleryQueue(BaseStashQueue):
self, entry: QueueEntry, timeout: Optional[float] = None
) -> Optional[ExecutorResult]:
from celery.exceptions import TimeoutError as _CeleryTimeout
from celery.result import AsyncResult
def _load_info(rev: str) -> ExecutorInfo:
infofile = self.get_infofile_path(rev)
@@ -261,11 +261,12 @@ class LocalCeleryQueue(BaseStashQueue):
for queue_entry in self.iter_queued():
if entry.stash_rev == queue_entry.stash_rev:
raise DvcException("Experiment has not been started.")
for task_id, active_entry in self._iter_active_tasks():
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", task_id)
logger.debug(
"Waiting for exp task '%s' to complete", result.id
)
try:
result: AsyncResult = AsyncResult(task_id)
result.get(timeout=timeout)
except _CeleryTimeout as exc:
raise DvcException(
@@ -277,11 +278,7 @@ class LocalCeleryQueue(BaseStashQueue):
# 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.
try:
return _load_collected(entry.stash_rev)
except FileNotFoundError:
pass
raise DvcException(f"Invalid experiment '{entry.stash_rev[:7]}'.")
return _load_collected(entry.stash_rev)
def kill(self, revs: Collection[str]) -> None:
to_kill: Set[QueueEntry] = set()
+2 -2
View File
@@ -3,7 +3,7 @@ from typing import Dict, Iterable, Optional
from dvc.repo import locked
from dvc.ui import ui
from dvc.utils.cli_parse import loads_param_overrides
from dvc.utils.cli_parse import to_path_overrides
logger = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ def run(
return repo.experiments.reproduce_celery(entries, jobs=jobs)
if params:
params = loads_param_overrides(params)
params = to_path_overrides(params)
if queue:
if not kwargs.get("checkpoint_resume", None):
+3
View File
@@ -142,6 +142,9 @@ def show(
fetch_running: bool = True,
):
if repo.scm.no_commits:
return {}
if onerror is None:
onerror = onerror_collect
+42
View File
@@ -93,6 +93,18 @@ def fetch(
downloaded += d
failed += f
d, f = _fetch_partial_imports(
self,
targets,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
recursive=recursive,
revs=revs,
)
downloaded += d
failed += f
if failed:
raise DownloadError(failed)
@@ -107,3 +119,33 @@ def _fetch(repo, obj_ids, **kwargs):
except FileTransferError as exc:
failed += exc.amount
return downloaded, failed
def _fetch_partial_imports(repo, targets, **kwargs):
from dvc.stage.exceptions import DataSourceChanged
downloaded = 0
failed = 0
for stage in repo.partial_imports(targets, **kwargs):
dep = stage.deps[0]
out = stage.outs[0]
try:
if dep.changed_checksum():
raise DataSourceChanged(f"{stage} ({dep})")
logger.info("Importing '%s' -> '%s'", dep, out)
out.transfer(
dep.def_path, jobs=kwargs.get("jobs"), odb=repo.odb.local
)
except DataSourceChanged as exc:
logger.warning(f"{exc}")
failed += 1
continue
if not any(
kwargs.get(kw, None)
for kw in ("all_branches", "all_tags", "all_commits", "revs")
):
stage.dump()
downloaded += 1
return downloaded, failed
+4 -3
View File
@@ -17,6 +17,7 @@ def imp_url(
fname=None,
erepo=None,
frozen=True,
no_download=False,
no_exec=False,
remote=None,
to_remote=False,
@@ -31,9 +32,9 @@ def imp_url(
self, out, always_local=to_remote and not out
)
if to_remote and no_exec:
if to_remote and (no_exec or no_download):
raise InvalidArgumentError(
"--no-exec can't be combined with --to-remote"
"--no-exec/--no-download cannot be combined with --to-remote"
)
if not to_remote and remote:
@@ -80,7 +81,7 @@ def imp_url(
stage.save_deps()
stage.md5 = stage.compute_md5()
else:
stage.run(jobs=jobs)
stage.run(jobs=jobs, no_download=no_download)
stage.frozen = frozen
+22
View File
@@ -246,6 +246,28 @@ class Index:
used[odb].update(objs)
return used
def partial_imports(
self,
targets: "TargetType" = None,
recursive: bool = False,
) -> List["Stage"]:
from itertools import chain
from dvc.utils.collections import ensure_list
collect_targets: Sequence[Optional[str]] = (None,)
if targets:
collect_targets = ensure_list(targets)
pairs = chain.from_iterable(
self.stage_collector.collect_granular(
target, recursive=recursive, with_deps=True
)
for target in collect_targets
)
return [stage for stage, _ in pairs if stage.is_partial_import]
# Following methods help us treat the collection as a set-like structure
# and provides faux-immutability.
# These methods do not preserve stages order.
+11 -12
View File
@@ -1,5 +1,4 @@
import os
from itertools import chain
from typing import TYPE_CHECKING, Optional
from dvc.exceptions import PathMissingError
@@ -66,15 +65,16 @@ def _ls(
infos = {}
for root, dirs, files in fs.walk(
fs_path, dvcfiles=True, dvc_only=dvc_only
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
):
entries = chain(files, dirs) if not recursive else files
if not recursive:
files.update(dirs)
for entry in entries:
entry_fs_path = fs.path.join(root, entry)
for name, entry in files.items():
entry_fs_path = fs.path.join(root, name)
relparts = fs.path.relparts(entry_fs_path, fs_path)
name = os.path.join(*relparts)
infos[name] = fs.info(entry_fs_path)
infos[name] = entry
if not recursive:
break
@@ -85,11 +85,10 @@ def _ls(
ret = {}
for name, info in infos.items():
dvc_info = info.get("dvc_info", {})
if fs.isdvc(info["name"], recursive=True) or not dvc_only:
ret[name] = {
"isout": dvc_info.get("isout", False),
"isdir": info["type"] == "directory",
"isexec": info.get("isexec", False),
}
ret[name] = {
"isout": dvc_info.get("isout", False),
"isdir": info["type"] == "directory",
"isexec": info.get("isexec", False),
}
return ret
+8 -8
View File
@@ -12,7 +12,7 @@ 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
from dvc.utils.serialize import LOADERS
from dvc.utils.serialize import load_path
logger = logging.getLogger(__name__)
@@ -71,9 +71,7 @@ def _extract_metrics(metrics, path, rev):
@error_handler
def _read_metric(path, fs, rev, **kwargs):
suffix = fs.path.suffix(path).lower()
loader = LOADERS[suffix]
val = loader(path, fs=fs)
val = load_path(path, fs)
val = _extract_metrics(val, path, rev)
return val or {}
@@ -87,12 +85,14 @@ def _read_metrics(repo, metrics, rev, onerror=None):
res = {}
for metric in metrics:
rel_metric_path = os.path.join(relpath, *fs.path.parts(metric))
if not fs.isfile(metric):
continue
if fs.isfile(rel_metric_path):
metric = rel_metric_path
else:
continue
res[os.path.join(relpath, *fs.path.parts(metric))] = _read_metric(
metric, fs, rev, onerror=onerror
)
res[rel_metric_path] = _read_metric(metric, fs, rev, onerror=onerror)
return res
+5 -5
View File
@@ -22,7 +22,7 @@ from dvc.stage import PipelineStage
from dvc.ui import ui
from dvc.utils import error_handler, errored_revisions, onerror_collect
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import LOADERS
from dvc.utils.serialize import load_path
if TYPE_CHECKING:
from dvc.output import Output
@@ -56,15 +56,15 @@ def _collect_configs(
default_params
):
fs_paths.append(default_params)
if targets and (deps or stages) and not params:
# A target has been provided but it is not used in the stages
fs_paths = []
return params, fs_paths
@error_handler
def _read_fs_path(fs, fs_path, **kwargs):
suffix = fs.path.suffix(fs_path).lower()
loader = LOADERS[suffix]
return loader(fs_path, fs=fs)
return load_path(fs_path, fs)
def _read_params(
+73 -38
View File
@@ -386,30 +386,56 @@ def _collect_output_plots(
return result
def _adjust_definitions_to_cwd(fs, config_relpath, plots_definitions):
result = defaultdict(dict)
def _id_is_path(plot_props=None):
if not plot_props:
return True
config_dirname = fs.path.dirname(config_relpath)
y_def = plot_props.get("y")
return not isinstance(y_def, dict)
for plot_id, plot_def in plots_definitions.items():
y_def = plot_def.get("y", None) if plot_def else None
if y_def is None or not isinstance(y_def, dict):
# plot_id is filename
new_plot_id = _normpath(fs.path.join(config_dirname, plot_id))
result[new_plot_id] = plot_def or {}
def _adjust_sources(fs, plot_props, config_dir):
new_plot_props = deepcopy(plot_props)
old_y = new_plot_props.pop("y", {})
new_y = {}
for filepath, val in old_y.items():
new_y[_normpath(fs.path.join(config_dir, filepath))] = val
new_plot_props["y"] = new_y
return new_plot_props
def _resolve_definitions(
fs, targets, props, config_path, definitions, onerror=None
):
config_dir = fs.path.dirname(config_path)
result: Dict[str, Dict] = {}
for plot_id, plot_props in definitions.items():
if plot_props is None:
plot_props = {}
if _id_is_path(plot_props):
data_path = _normpath(fs.path.join(config_dir, plot_id))
if _matches(targets, config_path, plot_id):
unpacked = unpack_if_dir(
fs,
data_path,
props={**plot_props, **props},
onerror=onerror,
)
dpath.util.merge(
result,
unpacked,
)
else:
new_plot_def = deepcopy(plot_def)
old_y = new_plot_def.pop("y")
new_y = {}
for filepath, val in old_y.items():
new_y[_normpath(fs.path.join(config_dirname, filepath))] = val
new_plot_def["y"] = new_y
result[plot_id] = new_plot_def
return dict(result)
if _matches(targets, config_path, plot_id):
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
dpath.util.merge(
result, {"data": {plot_id: {**adjusted_props, **props}}}
)
return result
def _collect_pipeline_files(repo, targets: List[str], props):
def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
from dvc.dvcfile import PipelineFile
result: Dict[str, Dict] = {}
@@ -417,21 +443,19 @@ def _collect_pipeline_files(repo, targets: List[str], props):
for dvcfile in dvcfiles:
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = _adjust_definitions_to_cwd(
repo.fs, dvcfile_path, dvcfile.load().get("plots", {})
dvcfile_defs = dvcfile.load().get("plots", {})
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs,
onerror=onerror,
)
dpath.util.merge(
result,
{dvcfile_path: resolved},
)
for plot_id, plot_props in dvcfile_defs.items():
if plot_props is None:
plot_props = {}
if _matches(targets, dvcfile_path, plot_id):
dpath.util.merge(
result,
{
dvcfile_path: {
"data": {plot_id: {**plot_props, **props}}
}
},
)
return result
@@ -453,7 +477,10 @@ def _collect_definitions(
fs = DvcFileSystem(repo=repo)
if not config_files:
dpath.util.merge(result, _collect_pipeline_files(repo, targets, props))
dpath.util.merge(
result,
_collect_pipeline_files(repo, targets, props, onerror=onerror),
)
if targets or (not targets and not config_files):
dpath.util.merge(
@@ -463,12 +490,20 @@ def _collect_definitions(
if config_files:
for path in config_files:
definitions = parse(fs, path)
definitions = _adjust_definitions_to_cwd(
repo.fs, path, definitions
)
definitions = parse(fs, path).get("data", {})
if definitions:
dpath.util.merge(result, {path: definitions})
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
path,
definitions,
onerror=onerror,
)
dpath.util.merge(
result,
{path: resolved},
)
for target in targets:
if not result or fs.exists(target):
+2
View File
@@ -12,6 +12,8 @@ def _revisions(repo, revs, experiment):
def diff(repo, *args, revs=None, experiment=False, **kwargs):
if repo.scm.no_commits:
return {}
return repo.plots.show(
*args, revs=_revisions(repo, revs, experiment), **kwargs
)
+17 -1
View File
@@ -1,6 +1,13 @@
import logging
import typing
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageFileIsNotDvcFileError,
StageNotFound,
)
from . import locked
if typing.TYPE_CHECKING:
@@ -11,7 +18,16 @@ logger = logging.getLogger(__name__)
@locked
def remove(self: "Repo", target: str, outs: bool = False):
stages = self.stage.from_target(target)
try:
stages = self.stage.from_target(target)
except (StageNotFound, StageFileDoesNotExistError) as e:
# If the user specified a tracked file as a target instead of a stage,
# e.g. `data.csv` instead of `data.csv.dvc`,
# give a more helpful error message.
if self.fs.exists(target + DVC_FILE_SUFFIX):
raise StageFileIsNotDvcFileError(target) from e
else:
raise
for stage in stages:
stage.remove(remove_outs=outs, force=outs)
+3 -1
View File
@@ -64,7 +64,9 @@ def _get_stage_files(stage: "Stage") -> typing.Iterator[str]:
if (
not dep.use_scm_ignore
and dep.is_in_repo
and not stage.repo.dvcfs.isdvc(dep.fs_path)
and not stage.repo.dvcfs.isdvc(
stage.repo.dvcfs.from_os_path(str(dep))
)
):
yield dep.fs_path
for out in stage.outs:
+1 -1
View File
@@ -46,7 +46,7 @@ class SCMContext:
@staticmethod
def _make_git_add_cmd(paths: Union[str, Iterable[str]]) -> str:
files = " ".join(map(shlex.quote, ensure_list(paths)))
return f"\tgit add {files}".expandtabs(4)
return f"\tgit add {files}"
def add(self, paths: Union[str, Iterable[str]]) -> None:
from scmrepo.exceptions import UnsupportedIndexFormat
+13 -1
View File
@@ -10,6 +10,7 @@ def update(
rev=None,
recursive=False,
to_remote=False,
no_download=False,
remote=None,
jobs=None,
):
@@ -21,6 +22,11 @@ def update(
if isinstance(targets, str):
targets = [targets]
if to_remote and no_download:
raise InvalidArgumentError(
"--to-remote can't be used with --no-download"
)
if not to_remote and remote:
raise InvalidArgumentError(
"--remote can't be used without --to-remote"
@@ -31,7 +37,13 @@ def update(
stages.update(self.stage.collect(target, recursive=recursive))
for stage in stages:
stage.update(rev, to_remote=to_remote, remote=remote, jobs=jobs)
stage.update(
rev,
to_remote=to_remote,
remote=remote,
no_download=no_download,
jobs=jobs,
)
dvcfile = Dvcfile(self, stage.path)
dvcfile.dump(stage)
+27 -18
View File
@@ -7,6 +7,7 @@ from voluptuous import Invalid, Optional, Required, Schema
from .exceptions import DvcException
from .fs import localfs
from .lock import make_lock
from .utils import relpath
INFO_SCHEMA = {Required("pid"): int, Required("cmd"): str}
@@ -33,22 +34,29 @@ class RWLockFileFormatError(DvcException):
@contextmanager
def _edit_rwlock(lock_dir, fs):
def _edit_rwlock(lock_dir, fs, hardlink):
path = fs.path.join(lock_dir, "rwlock")
try:
with fs.open(path, encoding="utf-8") as fobj:
lock = SCHEMA(json.load(fobj))
except FileNotFoundError:
lock = SCHEMA({})
except json.JSONDecodeError as exc:
raise RWLockFileCorruptedError(path) from exc
except Invalid as exc:
raise RWLockFileFormatError(path) from exc
lock["read"] = defaultdict(list, lock["read"])
lock["write"] = defaultdict(dict, lock["write"])
yield lock
with fs.open(path, "w", encoding="utf-8") as fobj:
json.dump(lock, fobj)
rwlock_guard = make_lock(
fs.path.join(lock_dir, "rwlock.lock"),
tmp_dir=lock_dir,
hardlink_lock=hardlink,
)
with rwlock_guard:
try:
with fs.open(path, encoding="utf-8") as fobj:
lock = SCHEMA(json.load(fobj))
except FileNotFoundError:
lock = SCHEMA({})
except json.JSONDecodeError as exc:
raise RWLockFileCorruptedError(path) from exc
except Invalid as exc:
raise RWLockFileFormatError(path) from exc
lock["read"] = defaultdict(list, lock["read"])
lock["write"] = defaultdict(dict, lock["write"])
yield lock
with fs.open(path, "w", encoding="utf-8") as fobj:
json.dump(lock, fobj)
def _infos_to_str(infos):
@@ -133,7 +141,7 @@ def _release_read(lock, info, changes):
@contextmanager
def rwlock(tmp_dir, fs, cmd, read, write):
def rwlock(tmp_dir, fs, cmd, read, write, hardlink):
"""Create non-thread-safe RWLock for file paths.
Args:
@@ -142,6 +150,7 @@ def rwlock(tmp_dir, fs, cmd, read, write):
cmd (str): command that will be working on these file path.
read ([str]): file paths that are going to be read.
write ([str]): file paths that are going to be written.
hardlink (bool): use hardlink lock to guard rwlock file when on edit.
Raises:
LockError: raised if file paths we want to read is being written to by
@@ -153,7 +162,7 @@ def rwlock(tmp_dir, fs, cmd, read, write):
"""
info = {"pid": os.getpid(), "cmd": cmd}
with _edit_rwlock(tmp_dir, fs) as lock:
with _edit_rwlock(tmp_dir, fs, hardlink) as lock:
_check_blockers(lock, info, mode="write", waiters=read + write)
_check_blockers(lock, info, mode="read", waiters=write)
@@ -164,6 +173,6 @@ def rwlock(tmp_dir, fs, cmd, read, write):
try:
yield
finally:
with _edit_rwlock(tmp_dir, fs) as lock:
with _edit_rwlock(tmp_dir, fs, hardlink) as lock:
_release_write(lock, info, wchanges)
_release_read(lock, info, rchanges)
+1 -1
View File
@@ -160,7 +160,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
except InternalRevError as exc:
# `scm` will only resolve git branch and tag names,
# if rev is not a sha it may be an abbreviated experiment name
if not rev.startswith("refs/"):
if not (rev == "HEAD" or rev.startswith("refs/")):
from dvc.repo.experiments.utils import (
AmbiguousExpRefInfo,
resolve_name,
+37 -8
View File
@@ -78,6 +78,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
wdir = os.path.abspath(kwargs.get("wdir", None) or os.curdir)
path = os.path.abspath(path)
check_dvcfile_path(repo, path)
check_stage_path(repo, wdir, is_wdir=kwargs.get("wdir"))
check_stage_path(repo, os.path.dirname(path))
@@ -242,6 +243,14 @@ class Stage(params.StageParams):
"""Whether the DVC file was created with `dvc import`."""
return not self.cmd and len(self.deps) == 1 and len(self.outs) == 1
@property
def is_partial_import(self) -> bool:
"""
Whether the DVC file was created using `dvc import --no-download`
or `dvc import-url --no-download`.
"""
return self.is_import and (not self.outs[0].hash_info)
@property
def is_repo_import(self):
if not self.is_import:
@@ -433,11 +442,23 @@ class Stage(params.StageParams):
return self
def update(self, rev=None, to_remote=False, remote=None, jobs=None):
def update(
self,
rev=None,
to_remote=False,
remote=None,
no_download=None,
jobs=None,
):
if not (self.is_repo_import or self.is_import):
raise StageUpdateError(self.relpath)
update_import(
self, rev=rev, to_remote=to_remote, remote=remote, jobs=jobs
self,
rev=rev,
to_remote=to_remote,
remote=remote,
no_download=no_download,
jobs=jobs,
)
def reload(self):
@@ -457,6 +478,7 @@ class Stage(params.StageParams):
def save(self, allow_missing=False):
self.save_deps(allow_missing=allow_missing)
self.save_outs(allow_missing=allow_missing)
self.md5 = self.compute_md5()
@@ -525,14 +547,16 @@ class Stage(params.StageParams):
no_commit=False,
force=False,
allow_missing=False,
no_download=False,
**kwargs,
):
if (self.cmd or self.is_import) and not self.frozen and not dry:
self.remove_outs(ignore_remove=False, force=False)
if not self.frozen and self.is_import:
jobs = kwargs.get("jobs", None)
self._sync_import(dry, force, jobs)
if (not self.frozen and self.is_import) or self.is_partial_import:
self._sync_import(
dry, force, kwargs.get("jobs", None), no_download
)
elif not self.frozen and self.cmd:
self._run_stage(dry, force, **kwargs)
else:
@@ -544,7 +568,7 @@ class Stage(params.StageParams):
self._check_missing_outputs()
if not dry:
if kwargs.get("checkpoint_func", None):
if kwargs.get("checkpoint_func", None) or no_download:
allow_missing = True
self.save(allow_missing=allow_missing)
if not no_commit:
@@ -555,8 +579,8 @@ class Stage(params.StageParams):
return run_stage(self, dry, force, **kwargs)
@rwlocked(read=["deps"], write=["outs"])
def _sync_import(self, dry, force, jobs):
sync_import(self, dry, force, jobs)
def _sync_import(self, dry, force, jobs, no_download):
sync_import(self, dry, force, jobs, no_download)
@rwlocked(read=["outs"])
def _check_missing_outputs(self):
@@ -571,6 +595,8 @@ class Stage(params.StageParams):
@rwlocked(write=["outs"])
def checkout(self, allow_missing=False, **kwargs):
stats = defaultdict(list)
if self.is_partial_import:
return {}
for out in self.filter_outs(kwargs.get("filter_info")):
key, outs = self._checkout(
out,
@@ -658,6 +684,9 @@ class Stage(params.StageParams):
self, *args, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return set of object IDs used by this stage."""
if self.is_partial_import:
return {}
used_objs = defaultdict(set)
for out in self.filter_outs(kwargs.get("filter_info")):
for odb, objs in out.get_used_objs(*args, **kwargs).items():
+6 -1
View File
@@ -33,7 +33,12 @@ def rwlocked(call, read=None, write=None):
cmd = " ".join(sys.argv)
with rwlock(
stage.repo.tmp_dir, stage.repo.fs, cmd, _chain(read), _chain(write)
stage.repo.tmp_dir,
stage.repo.fs,
cmd,
_chain(read),
_chain(write),
stage.repo.config["core"].get("hardlink_lock", False),
):
return call()
+6 -1
View File
@@ -31,7 +31,7 @@ class StageFileIsNotDvcFileError(DvcException):
sname = fname + DVC_FILE_SUFFIX
if is_dvc_file(sname):
msg += f" Do you mean '{sname}'?"
msg += f". Do you mean '{sname}'?"
super().__init__(msg)
@@ -80,6 +80,11 @@ class MissingDataSource(DvcException):
super().__init__(msg)
class DataSourceChanged(DvcException):
def __init__(self, path: str):
super().__init__(f"data source changed: {path}")
class StageNotFound(DvcException, KeyError):
def __init__(self, file, name):
self.file = file.relpath
+24 -4
View File
@@ -17,20 +17,36 @@ def _update_import_on_remote(stage, remote, jobs):
stage.outs[0].transfer(url, odb=odb, jobs=jobs, update=True)
def update_import(stage, rev=None, to_remote=False, remote=None, jobs=None):
def update_import(
stage, rev=None, to_remote=False, remote=None, no_download=None, jobs=None
):
stage.deps[0].update(rev=rev)
outs = stage.outs
deps = stage.deps
frozen = stage.frozen
stage.frozen = False
if stage.outs:
stage.outs[0].clear()
try:
if to_remote:
_update_import_on_remote(stage, remote, jobs)
else:
stage.reproduce(jobs=jobs)
stage.reproduce(no_download=no_download, jobs=jobs)
finally:
if deps == stage.deps:
stage.outs = outs
stage.frozen = frozen
def sync_import(stage, dry=False, force=False, jobs=None):
def sync_import(
stage,
dry=False,
force=False,
jobs=None,
no_download=False,
):
"""Synchronize import's outs to the workspace."""
logger.info("Importing '%s' -> '%s'", stage.deps[0], stage.outs[0])
if dry:
@@ -40,4 +56,8 @@ def sync_import(stage, dry=False, force=False, jobs=None):
stage.outs[0].checkout()
else:
stage.save_deps()
stage.deps[0].download(stage.outs[0], jobs=jobs)
if not no_download:
stage.deps[0].download(
stage.outs[0],
jobs=jobs,
)
-26
View File
@@ -1,26 +0,0 @@
"""
Rollbacks monkeypatching of `json.encoder` by benedict.
It monkeypatches json to use Python-based encoder instead of C-based
during import.
We rollback that monkeypatch by keeping reference to that C-based
encoder and reinstate them after importing benedict.
See: https://github.com/iterative/dvc/issues/6423
https://github.com/fabiocaccamo/python-benedict/issues/62
and the source of truth:
https://github.com/fabiocaccamo/python-benedict/blob/c98c471065/benedict/dicts/__init__.py#L282-L285
"""
from json import encoder
try:
c_make_encoder = encoder.c_make_encoder # type: ignore[attr-defined]
except AttributeError:
c_make_encoder = None
from benedict import benedict # noqa: E402
encoder.c_make_encoder = c_make_encoder # type: ignore[attr-defined]
# Please import benedict from here lazily
__all__ = ["benedict"]
+13 -26
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Any, Dict, Iterable, List
from typing import Dict, Iterable, List
def parse_params(path_params: Iterable[str]) -> List[Dict[str, List[str]]]:
@@ -17,35 +17,22 @@ def parse_params(path_params: Iterable[str]) -> List[Dict[str, List[str]]]:
return [{path: params} for path, params in ret.items()]
def loads_param_overrides(
def to_path_overrides(
path_params: Iterable[str],
) -> Dict[str, Dict[str, Any]]:
"""Loads the content of params from the cli as Python object."""
from ruamel.yaml import YAMLError
) -> Dict[str, List[str]]:
"""Group overrides by path"""
from dvc.dependency.param import ParamsDependency
from dvc.exceptions import InvalidArgumentError
from .serialize import loads_yaml
ret: Dict[str, Dict[str, Any]] = defaultdict(dict)
path_overrides = defaultdict(list)
for path_param in path_params:
param_name, _, param_value = path_param.partition("=")
if not param_value:
raise InvalidArgumentError(
f"Must provide a value for parameter '{param_name}'"
)
path, _, param_name = param_name.partition(":")
if not param_name:
param_name = path
path_and_name = path_param.partition("=")[0]
if ":" not in path_and_name:
override = path_param
path = ParamsDependency.DEFAULT_PARAMS_FILE
else:
path, _, override = path_param.partition(":")
try:
ret[path][param_name] = loads_yaml(param_value)
except (ValueError, YAMLError):
raise InvalidArgumentError(
f"Invalid parameter value for '{param_name}': '{param_value}"
)
path_overrides[path].append(override)
return ret
return dict(path_overrides)
+47 -35
View File
@@ -3,16 +3,6 @@ from collections.abc import Mapping
from functools import wraps
from typing import Callable, Dict, Iterable, List, TypeVar, Union
from dvc.exceptions import DvcException
class NewParamsFound(DvcException):
"""Thrown if new params were found during merge_params"""
def __init__(self, new_params: List, *args):
self.new_params = new_params
super().__init__("New params found during merge", *args)
def apply_diff(src, dest):
"""Recursively apply changes from src to dest.
@@ -61,6 +51,53 @@ def apply_diff(src, dest):
)
def to_omegaconf(item):
"""
Some parsers return custom classes (i.e. parse_yaml_for_update)
that can mess up with omegaconf logic.
Cast the custom classes to Python primitives.
"""
if isinstance(item, dict):
item = {k: to_omegaconf(v) for k, v in item.items()}
elif isinstance(item, list):
item = [to_omegaconf(x) for x in item]
return item
def remove_missing_keys(src, to_update):
keys = list(src.keys())
for key in keys:
if key not in to_update:
del src[key]
elif isinstance(src[key], dict):
remove_missing_keys(src[key], to_update[key])
return src
def _merge_item(d, key, value):
if key in d:
item = d.get(key, None)
if isinstance(item, dict) and isinstance(value, dict):
merge_dicts(item, value)
else:
d[key] = value
else:
d[key] = value
def merge_dicts(src: Dict, to_update: Dict) -> Dict:
"""Recursively merges dictionaries.
Args:
src (dict): source dictionary of parameters
to_update (dict): dictionary of parameters to merge into src
"""
for key, value in to_update.items():
_merge_item(src, key, value)
return src
def ensure_list(item: Union[Iterable[str], str, None]) -> List[str]:
if item is None:
return []
@@ -79,31 +116,6 @@ def chunk_dict(d: Dict[_KT, _VT], size: int = 1) -> List[Dict[_KT, _VT]]:
return [{key: d[key] for key in chunk} for chunk in chunks(size, d)]
def merge_params(src: Dict, to_update: Dict, allow_new: bool = True) -> Dict:
"""
Recursively merges params with benedict's syntax support in-place.
Args:
src (dict): source dictionary of parameters
to_update (dict): dictionary of parameters to merge into src
allow_new (bool): if False, raises an error if new keys would be
added to src
"""
from ._benedict import benedict
data = benedict(src)
if not allow_new:
new_params = list(
set(to_update.keys()) - set(data.keypaths(indexes=True))
)
if new_params:
raise NewParamsFound(new_params)
data.merge(to_update, overwrite=True)
return src
class _NamespacedDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+53
View File
@@ -0,0 +1,53 @@
from pathlib import Path
from typing import TYPE_CHECKING, List
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.errors import ConfigCompositionException, OverrideParseException
from hydra.types import RunMode
from omegaconf import OmegaConf
from dvc.exceptions import InvalidArgumentError
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
from .serialize import MODIFIERS
if TYPE_CHECKING:
from dvc.types import StrPath
def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
"""Update `path` params with the provided `Hydra Override`_ patterns.
Args:
overrides: List of `Hydra Override`_ patterns.
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
"""
suffix = Path(path).suffix.lower()
hydra_errors = (ConfigCompositionException, OverrideParseException)
modify_data = MODIFIERS[suffix]
with modify_data(path) as original_data:
try:
parser = OverridesParser.create()
parsed = parser.parse_overrides(overrides=overrides)
ConfigLoaderImpl.validate_sweep_overrides_legal(
parsed, run_mode=RunMode.RUN, from_shell=True
)
new_data = OmegaConf.create(
to_omegaconf(original_data),
flags={"allow_objects": True},
)
OmegaConf.set_struct(new_data, True)
# pylint: disable=protected-access
ConfigLoaderImpl._apply_overrides_to_config(parsed, new_data)
new_data = OmegaConf.to_object(new_data)
except hydra_errors as e:
raise InvalidArgumentError("Invalid `--set-param` value") from e
merge_dicts(original_data, new_data)
remove_missing_keys(original_data, new_data)
+7
View File
@@ -14,6 +14,13 @@ LOADERS.update(
{".toml": load_toml, ".json": load_json, ".py": load_py} # noqa: F405
)
def load_path(fs_path, fs):
suffix = fs.path.suffix(fs_path).lower()
loader = LOADERS[suffix]
return loader(fs_path, fs=fs)
DUMPERS: DefaultDict[str, DumperFn] = defaultdict( # noqa: F405
lambda: dump_yaml # noqa: F405
)
+3 -3
View File
@@ -30,11 +30,11 @@ line_length = 79
[tool.pytest.ini_options]
log_level = "debug"
addopts = "-ra"
addopts = "-ra -p no:hydra_pytest"
markers = [
"needs_internet: Might need network access for the tests",
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio"
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio",
]
xfail_strict = true
+1 -1
View File
@@ -1,3 +1,3 @@
# NOTE: using strict version due to
# https://github.com/iterative/dvc/issues/7949
PyInstaller==5.2
PyInstaller==5.0
+1 -1
View File
@@ -1,5 +1,5 @@
import argparse
import importlib
import importlib.util
import pathlib
import textwrap
from subprocess import STDOUT, check_call
+20 -3
View File
@@ -1,6 +1,6 @@
import os
import pathlib
from subprocess import STDOUT, check_call
from subprocess import STDOUT, check_call, check_output
path = pathlib.Path(__file__).parent.absolute()
hooks = path / "hooks"
@@ -21,10 +21,27 @@ check_call(
stderr=STDOUT,
)
check_call(
out = check_output(
[
path / "dist" / "dvc" / "dvc",
"doctor",
],
stderr=STDOUT,
)
).decode()
remotes = [
"s3",
"oss",
"gdrive",
"gs",
"hdfs",
"http",
"webhdfs",
"azure",
"ssh",
"webdav",
]
print(out)
for remote in remotes:
assert f"\t{remote}" in out, f"Missing support for {remote}"
+12 -2
View File
@@ -16,5 +16,15 @@ datas += copy_metadata("webdav4")
datas += copy_metadata("aiohttp")
datas += copy_metadata("aiohttp_retry")
# https://github.com/pypa/setuptools/issues/1963
hiddenimports = ["pkg_resources.py2_warn"]
hiddenimports = [
"dvc_azure",
"dvc_gdrive",
"dvc_gs",
"dvc_hdfs",
"dvc_oss",
"dvc_s3",
"dvc_webdav",
"dvc_webhdfs",
# https://github.com/pypa/setuptools/issues/1963
"pkg_resources.py2_warn",
]
+1
View File
@@ -0,0 +1 @@
hiddenimports = ["fsspec.implementations.memory"]
@@ -1,8 +0,0 @@
"""Temporary fix for https://github.com/iterative/dvc/issues/5618."""
from PyInstaller.utils.hooks import ( # pylint:disable=import-error
collect_data_files,
copy_metadata,
)
datas = collect_data_files("googleapiclient.discovery")
datas += copy_metadata("google_api_python_client")
+17 -31
View File
@@ -60,16 +60,15 @@ install_requires =
dpath>=2.0.2,<3
shtab>=1.3.4,<2
rich>=10.13.0
python-benedict>=0.24.2
pyparsing>=2.4.7
typing-extensions>=3.7.4
fsspec[http]>=2021.10.1
aiohttp-retry>=2.4.5
scmrepo==0.0.25
dvc-render==0.0.9
dvc-task==0.1.2
dvclive>=0.10.0
dvc-data==0.1.13
dvc-data==0.2.0
dvc-http==2.19.1
hydra-core>=1.1.0
[options.extras_require]
all =
@@ -81,44 +80,31 @@ all =
%(s3)s
%(ssh)s
%(webdav)s
%(webhdfs)s
dev =
%(all)s
%(tests)s
dvc-data[cli]
# Extra dependencies for remote integrations
azure =
adlfs>=2021.10.0
azure-identity>=1.4.0
knack
gdrive = pydrive2[fsspec]>=1.10.1
gs = gcsfs>=2021.11.1
hdfs =
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
# explicitly specify pyarrow here
pyarrow>=1
fsspec[arrow]
oss = ossfs>=2021.8.0
s3 =
s3fs[boto3]>=2022.02.0
aiobotocore[boto3]>2
ssh =
# due to https://github.com/python-poetry/poetry/issues/4683, we have to
# explicitly specify bcrypt here
bcrypt
sshfs[bcrypt]>=2022.6.0
azure = dvc-azure==2.19.0
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.19.0
ssh = dvc-ssh==2.19.0
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for Linux, so it will fail to compile if user doesn't have all the
# requirements, including kerberos itself.
ssh_gssapi = sshfs[gssapi]>=2022.6.0
webdav = webdav4>=0.9.3
# not to break `dvc[webhdfs]`
webhdfs =
ssh_gssapi = dvc-ssh[gssapi]==2.19.0
webdav = dvc-webdav==2.19.0
webhdfs = dvc-webhdfs==2.19.0
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
webhdfs_kerberos = requests-kerberos==0.14.0
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
terraform = tpi[ssh]>=2.1.0
tests =
%(terraform)s
dvc_ssh==0.0.1a0
dvc-ssh==2.19.0
# Test requirements
pytest==7.1.2
pytest-cov==3.0.0
@@ -131,7 +117,7 @@ tests =
flaky==3.7.0
mock==4.0.3
pytest-timeout==2.1.0
filelock==3.7.1
filelock==3.8.0
# pylint requirements
pylint==2.14.5
# we use this to suppress pytest-related false positives in our tests.
+12
View File
@@ -250,3 +250,15 @@ def run_copy_metrics(tmp_dir, run_copy):
return stage
return run
@pytest.fixture(autouse=True)
def mock_hydra_conf(mocker):
if sys.version_info < (3, 11):
return
# `hydra.conf` fails to import in 3.11, it raises ValueError due to changes
# in dataclasses. See https://github.com/python/cpython/pull/29867.
# NOTE: using sentinel here so that any imports from `hydra.conf`
# return a mock.
sys.modules["hydra.conf"] = mocker.sentinel
+37
View File
@@ -1,3 +1,4 @@
import os
from textwrap import dedent
import pytest
@@ -67,6 +68,9 @@ def test_params_show_targets(params_repo):
"bar": 2,
"foobar": 3,
}
assert api.params_show("params.yaml", stages="stage-1") == {
"foo": 5,
}
def test_params_show_deps(params_repo):
@@ -92,6 +96,27 @@ def test_params_show_stages(params_repo):
api.params_show(stages="stage-0")
def test_params_show_stage_addressing(tmp_dir, dvc):
for subdir in {"subdir1", "subdir2"}:
subdir = tmp_dir / subdir
subdir.mkdir()
with subdir.chdir():
subdir.gen("params.yaml", "foo: 1")
dvc.run(name="stage-0", cmd="echo stage-0", params=["foo"])
for s in {"subdir1", "subdir2"}:
dvcyaml = os.path.join(s, "dvc.yaml")
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
with subdir.chdir():
nested = subdir / "nested"
nested.mkdir()
with nested.chdir():
dvcyaml = os.path.join("..", "dvc.yaml")
assert api.params_show(stages=f"{dvcyaml}:stage-0") == {"foo": 1}
def test_params_show_revs(params_repo):
assert api.params_show(rev="HEAD~1") == {
"params.yaml:foo": 1,
@@ -168,3 +193,15 @@ def test_params_show_stage_without_params(tmp_dir, dvc):
with pytest.raises(DvcException, match="No params found"):
api.params_show(deps=True)
def test_params_show_untracked_target(params_repo, tmp_dir):
tmp_dir.gen("params_foo.yaml", "foo: 1")
assert api.params_show("params_foo.yaml") == {"foo": 1}
with pytest.raises(DvcException, match="No params found"):
api.params_show("params_foo.yaml", stages="stage-0")
with pytest.raises(DvcException, match="No params found"):
api.params_show("params_foo.yaml", deps=True)
+14
View File
@@ -133,3 +133,17 @@ def http_auth_patch(mocker):
@pytest.fixture(params=[True, False])
def workspace(request, test_queue) -> bool: # noqa
return request.param
@pytest.fixture
def params_repo(tmp_dir, scm, dvc):
(tmp_dir / "params.yaml").dump(
{"foo": [{"bar": 1}, {"baz": 2}], "goo": {"bag": 3.0}, "lorem": False}
)
dvc.run(
cmd="echo foo",
params=["params.yaml:"],
name="foo",
)
scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml"])
scm.commit("init")
+1 -1
View File
@@ -4,9 +4,9 @@ from functools import partial
from urllib.parse import urlparse
import pytest
from dvc_ssh import SSHFileSystem
from dvc_ssh.tests.cloud import TEST_SSH_KEY_PATH, TEST_SSH_USER
from dvc.fs import SSHFileSystem
from dvc.repo.experiments.executor.base import ExecutorInfo, ExecutorResult
from dvc.repo.experiments.executor.ssh import SSHExecutor
from dvc.repo.experiments.refs import EXEC_HEAD, EXEC_MERGE
+11
View File
@@ -1,6 +1,17 @@
from funcy import first
def test_diff_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().experiments.diff() == {}
def test_diff_empty(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.diff() == {"params": {}, "metrics": {}}
+7 -59
View File
@@ -7,7 +7,7 @@ import pytest
from funcy import first
from dvc.dvcfile import PIPELINE_FILE
from dvc.exceptions import DvcException, ReproductionError
from dvc.exceptions import ReproductionError
from dvc.repo.experiments.queue.base import BaseStashQueue
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.scm import resolve_rev
@@ -107,69 +107,17 @@ def test_failed_exp_workspace(
@pytest.mark.parametrize(
"changes, expected",
[
[["foo=baz"], "{foo: baz, goo: {bag: 3}, lorem: false}"],
[["foo=baz", "goo=bar"], "{foo: baz, goo: bar, lorem: false}"],
[
["goo.bag=4"],
"{foo: [bar: 1, baz: 2], goo: {bag: 4}, lorem: false}",
],
[["foo[0]=bar"], "{foo: [bar, baz: 2], goo: {bag: 3}, lorem: false}"],
[
["foo[1].baz=3"],
"{foo: [bar: 1, baz: 3], goo: {bag: 3}, lorem: false}",
],
[
["foo[1]=[baz, goo]"],
"{foo: [bar: 1, [baz, goo]], goo: {bag: 3}, lorem: false}",
],
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
],
)
def test_modify_params(tmp_dir, scm, dvc, mocker, changes, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen(
"params.yaml", "{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: false}"
)
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
metrics_no_cache=["metrics.yaml"],
params=["foo", "goo", "lorem"],
name="copy-file",
)
scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
scm.commit("init")
new_mock = mocker.spy(dvc.experiments, "new")
results = dvc.experiments.run(stage.addressing, params=changes)
exp = first(results)
new_mock.assert_called_once()
fs = scm.get_fs(exp)
with fs.open("metrics.yaml", mode="r") as fobj:
def test_modify_params(params_repo, dvc, changes, expected):
dvc.experiments.run(params=changes)
# pylint: disable=unspecified-encoding
with open("params.yaml", mode="r") as fobj:
assert fobj.read().strip() == expected
@pytest.mark.parametrize(
"changes",
[["lorem.ipsum=3"], ["foo[0].bazar=3"]],
)
def test_add_params(tmp_dir, scm, dvc, changes):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen(
"params.yaml", "{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: false}"
)
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
metrics_no_cache=["metrics.yaml"],
params=["foo", "goo", "lorem"],
name="copy-file",
)
scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
scm.commit("init")
with pytest.raises(DvcException):
dvc.experiments.run(stage.addressing, params=changes)
def test_apply(tmp_dir, scm, dvc, exp_stage):
from dvc.exceptions import InvalidArgumentError
from dvc.repo.experiments.exceptions import ApplyConflictError
+2
View File
@@ -42,6 +42,7 @@ def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
return name_list
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
@pytest.mark.parametrize("follow", [True, False])
def test_celery_logs(
tmp_dir,
@@ -64,6 +65,7 @@ def test_celery_logs(
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())
+12
View File
@@ -35,6 +35,18 @@ def make_executor_info(**kwargs):
return ExecutorInfo(**kwargs)
@pytest.mark.vscode
def test_show_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().experiments.show() == {}
@pytest.mark.vscode
def test_show_simple(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.show()["workspace"] == {
+26
View File
@@ -21,6 +21,32 @@ def test_show_simple(tmp_dir, dvc, run_copy_metrics):
}
def test_show_simple_from_subdir(tmp_dir, dvc, run_copy_metrics):
subdir = tmp_dir / "subdir"
subdir.mkdir()
tmp_dir.gen("metrics_t.yaml", "1.1")
run_copy_metrics(
"metrics_t.yaml",
"subdir/metrics.yaml",
metrics=["subdir/metrics.yaml"],
)
expected_path = os.path.join("subdir", "metrics.yaml")
assert dvc.metrics.show() == {"": {"data": {expected_path: {"data": 1.1}}}}
expected_path = os.path.join("..", "subdir", "metrics.yaml")
with subdir.chdir():
assert dvc.metrics.show() == {
"": {"data": {expected_path: {"data": 1.1}}}
}
subdir2 = tmp_dir / "subdir2"
subdir2.mkdir()
with subdir2.chdir():
assert dvc.metrics.show() == {
"": {"data": {expected_path: {"data": 1.1}}}
}
def test_show(tmp_dir, dvc, run_copy_metrics):
tmp_dir.gen("metrics_t.yaml", "foo: 1.1")
run_copy_metrics(
+14
View File
@@ -1,3 +1,5 @@
import pytest
from tests.utils.plots import get_plot
@@ -46,3 +48,15 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
assert get_plot(
diff_result, "workspace", "definitions", file="", endkey="data"
) == {"metric.json": props}
@pytest.mark.vscode
def test_no_commits(tmp_dir):
from scmrepo.git import Git
from dvc.repo import Repo
git = Git.init(tmp_dir.fs_path)
assert git.no_commits
assert Repo.init().plots.diff() == {}
+14
View File
@@ -273,6 +273,20 @@ def test_pull_external_dvc_imports(tmp_dir, dvc, scm, erepo_dir):
assert (tmp_dir / "new_dir" / "bar").read_text() == "bar"
def test_pull_partial_import(tmp_dir, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
stage = dvc.imp_url(
"remote://workspace/file", os.fspath(dst), no_download=True
)
result = dvc.pull("file")
assert result["fetched"] == 1
assert dst.exists()
assert stage.outs[0].get_hash().value == "d10b4c3ff123b26dc068d43a8bef2d23"
def test_pull_external_dvc_imports_mixed(
tmp_dir, dvc, scm, erepo_dir, local_remote
):
+290 -53
View File
@@ -1,6 +1,9 @@
from os.path import join
import pytest
from dvc.repo import Repo
from dvc.repo.data import _transform_git_paths_to_dvc
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils.fs import remove
@@ -14,24 +17,55 @@ EMPTY_STATUS = {
}
def test_file(M, tmp_dir, scm, dvc):
@pytest.mark.parametrize("path", [None, ("sub", "repo")])
def test_git_to_dvc_path_wdir_transformation(tmp_dir, scm, path):
struct = {"dir": {"foo": "foo", "bar": "bar"}, "file": "file", "dir2": {}}
tmp_dir.gen(struct)
subdir = tmp_dir.joinpath(*path) if path else tmp_dir
make_subrepo(subdir, scm)
dvc = subdir.dvc
with subdir.chdir():
subdir.gen(struct)
_, _, untracked = scm.status(untracked_files="all")
# make order independent of the platforms for easier test assertions
untracked = sorted(untracked, reverse=True)
assert _transform_git_paths_to_dvc(dvc, untracked) == [
"file",
join("dir", "foo"),
join("dir", "bar"),
]
with (subdir / "dir").chdir():
assert _transform_git_paths_to_dvc(dvc, untracked) == [
join("..", "file"),
"foo",
"bar",
]
with (subdir / "dir2").chdir():
assert _transform_git_paths_to_dvc(dvc, untracked) == [
join("..", "file"),
join("..", "dir", "foo"),
join("..", "dir", "bar"),
]
def test_file(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen("foo", "foo", commit="add foo")
tmp_dir.dvc_gen("foo", "foobar")
remove(tmp_dir / "foo")
expected = {
**EMPTY_STATUS,
"committed": {"modified": ["foo"]},
"uncommitted": {"deleted": ["foo"]},
"git": M.instance_of(dict),
"not_in_cache": [],
"unchanged": [],
"untracked": [],
"git": M.dict(),
}
assert dvc.data_status() == expected
assert dvc.data_status(granular=True) == expected
def test_directory(M, tmp_dir, scm, dvc):
def test_directory(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar", "foobar": "foobar"}})
remove(tmp_dir / "dir")
@@ -39,12 +73,10 @@ def test_directory(M, tmp_dir, scm, dvc):
tmp_dir.gen("untracked", "untracked")
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"uncommitted": {"modified": [join("dir", "")]},
"git": M.instance_of(dict),
"not_in_cache": [],
"unchanged": [],
"untracked": [],
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
@@ -52,14 +84,15 @@ def test_directory(M, tmp_dir, scm, dvc):
"added": M.unordered(
join("dir", "bar"),
join("dir", "foobar"),
)
),
"modified": [join("dir", "")],
},
"uncommitted": {
"added": [join("dir", "baz")],
"modified": [join("dir", "bar")],
"modified": M.unordered(join("dir", ""), join("dir", "bar")),
"deleted": [join("dir", "foobar")],
},
"git": M.instance_of(dict),
"git": M.dict(),
"not_in_cache": [],
"unchanged": [join("dir", "foo")],
"untracked": ["untracked"],
@@ -81,52 +114,19 @@ def test_noscm_repo(dvc):
assert dvc.data_status() == EMPTY_STATUS
def test_unchanged(M, tmp_dir, scm, dvc):
def test_unchanged(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
assert dvc.data_status() == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"git": M.dict(),
"unchanged": M.unordered("bar", join("dir", "")),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo")),
}
def test_not_in_cache(M, tmp_dir, scm, dvc):
# TODO: investigation required, might return wrong results
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
remove(dvc.odb.local.path)
assert dvc.data_status() == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("bar", join("dir", "")),
"git": M.instance_of(dict),
"unchanged": ["bar"],
"uncommitted": {"added": [join("dir", "")]},
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("bar"),
"git": M.instance_of(dict),
"unchanged": [],
"committed": {"modified": ["bar"]},
"uncommitted": {"modified": ["bar"], "added": [join("dir", "foo")]},
}
def test_withdirs(M, tmp_dir, scm, dvc):
tmp_dir.dvc_gen({"dir": {"foo": "foo"}}, commit="add dir")
tmp_dir.dvc_gen("bar", "bar", commit="add foo")
assert dvc.data_status(granular=True, with_dirs=True) == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo"), join("dir", "")),
"git": M.dict(),
"unchanged": M.unordered("bar", join("dir", ""), join("dir", "foo")),
}
@@ -146,7 +146,8 @@ def test_skip_uncached_pipeline_outputs(tmp_dir, dvc, run_copy_metrics):
def test_output_with_newly_added_stage(tmp_dir, dvc):
dvc.stage.add(deps=["bar"], outs=["foo"], name="copy", cmd="cp foo bar")
assert dvc.data_status() == {**EMPTY_STATUS, "not_in_cache": ["foo"]}
assert dvc.data_status() == EMPTY_STATUS
assert dvc.data_status(granular=True) == EMPTY_STATUS
def test_subdir(M, tmp_dir, scm):
@@ -161,7 +162,243 @@ def test_subdir(M, tmp_dir, scm):
dvc = subrepo.dvc
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"git": M.instance_of(dict),
"unchanged": M.unordered("bar", join("dir", "foo")),
"git": M.dict(),
"unchanged": M.unordered(
"bar", join("dir", ""), join("dir", "foo")
),
"untracked": ["untracked"],
}
def test_untracked_newly_added_files(M, tmp_dir, dvc, scm):
tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.gen("foobar", "foobar")
expected = {
**EMPTY_STATUS,
"untracked": M.unordered(
join("dir", "foo"), join("dir", "bar"), "foobar"
),
"git": M.dict(),
}
assert dvc.data_status(untracked_files="all") == expected
assert dvc.data_status(granular=True, untracked_files="all") == expected
def test_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.dvc_gen("foobar", "foobar")
remove(dvc.odb.repo.path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"uncommitted": {
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
},
"not_in_cache": M.unordered(
"foobar",
join("dir", ""),
),
"git": M.dict(),
}
def test_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
tmp_dir.dvc_gen("foobar", "foobar")
for path in [dvc.odb.repo.path, "dir", "foobar"]:
remove(path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"untracked": M.unordered("foobar.dvc", "dir.dvc", ".gitignore"),
"uncommitted": {"deleted": M.unordered("foobar", join("dir", ""))},
"committed": {"added": M.unordered("foobar", join("dir", ""))},
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
}
def test_git_committed_missing_cache_workspace_exists(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
remove(dvc.odb.local.path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"not_in_cache": M.unordered("foobar", join("dir", "")),
"git": M.dict(),
"unchanged": M.unordered("foobar", join("dir", "")),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"not_in_cache": M.unordered(
"foobar",
join("dir", ""),
),
"uncommitted": {
"unknown": M.unordered(join("dir", "foo"), join("dir", "bar"))
},
"git": M.dict(),
"unchanged": M.unordered("foobar", join("dir", "")),
}
def test_git_committed_missing_cache_missing_workspace(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
tmp_dir.dvc_gen("foobar", "foobar", commit="add foobar")
for path in [dvc.odb.repo.path, "dir", "foobar"]:
remove(path)
assert dvc.data_status(untracked_files="all") == {
**EMPTY_STATUS,
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
"git": M.dict(),
}
assert dvc.data_status(granular=True, untracked_files="all") == {
**EMPTY_STATUS,
"committed": {},
"uncommitted": {"deleted": M.unordered(join("dir", ""), "foobar")},
"not_in_cache": M.unordered(join("dir", ""), "foobar"),
"git": M.dict(),
}
def test_partial_missing_cache(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
# remove "foo" from cache
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path("acbd18db4cc2f85cedef654fccc4a4d8"))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": [join("dir", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"added": M.unordered(
join("dir", ""), join("dir", "foo"), join("dir", "bar")
)
},
"not_in_cache": [join("dir", "foo")],
"git": M.dict(),
}
def test_missing_dir_object_from_head(M, tmp_dir, dvc, scm):
(stage,) = tmp_dir.dvc_gen(
{"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir"
)
remove("dir")
tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"modified": [join("dir", "")],
"unknown": [join("dir", "foobar")],
},
"git": M.dict(),
}
def test_missing_dir_object_from_index(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="add dir")
remove("dir")
(stage,) = tmp_dir.dvc_gen({"dir": {"foobar": "foobar"}})
odb = dvc.odb.repo
odb.fs.rm(odb.oid_to_path(stage.outs[0].hash_info.value))
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"modified": [join("dir", "")]},
"not_in_cache": [join("dir", "")],
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"modified": [join("dir", "")],
},
"uncommitted": {"unknown": [join("dir", "foobar")]},
"not_in_cache": [join("dir", "")],
"git": M.dict(),
}
def test_root_from_dir_to_file(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
remove("data")
tmp_dir.gen("data", "file")
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": [join("data", "")]},
"uncommitted": {"modified": ["data"]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {
"added": M.unordered(
join("data", ""), join("data", "foo"), join("data", "bar")
)
},
"uncommitted": {
"deleted": M.unordered(join("data", "foo"), join("data", "bar")),
"modified": ["data"],
},
"git": M.dict(),
}
def test_root_from_file_to_dir(M, tmp_dir, dvc, scm):
tmp_dir.dvc_gen("data", "file")
remove("data")
tmp_dir.gen({"data": {"foo": "foo", "bar": "bar"}})
assert dvc.data_status() == {
**EMPTY_STATUS,
"committed": {"added": ["data"]},
"uncommitted": {"modified": [join("data", "")]},
"git": M.dict(),
}
assert dvc.data_status(granular=True) == {
**EMPTY_STATUS,
"committed": {"added": ["data"]},
"uncommitted": {
"modified": [join("data", "")],
"added": M.unordered(join("data", "foo"), join("data", "bar")),
},
"git": M.dict(),
}
+22
View File
@@ -267,3 +267,25 @@ def test_import_url_to_remote_status(tmp_dir, dvc, local_cloud, local_remote):
status = dvc.status()
assert len(status) == 0
def test_import_url_no_download(tmp_dir, dvc, local_workspace):
local_workspace.gen("file", "file content")
dst = tmp_dir / "file"
stage = dvc.imp_url(
"remote://workspace/file", os.fspath(dst), no_download=True
)
assert stage.deps[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
assert not dst.exists()
out = stage.outs[0]
assert out.hash_info.value is None
assert out.hash_info.name is None
assert out.meta.size is None
status = dvc.status()
assert status["file.dvc"] == [
{"changed outs": {"file": "deleted"}},
]
+23
View File
@@ -1,6 +1,7 @@
import pytest
from dvc.cli import main
from dvc.exceptions import DvcException
from dvc.lock import Lock, LockError
@@ -14,6 +15,28 @@ def test_with(tmp_dir, dvc, mocker):
pass
def test_unlock_lock_failed(tmp_dir, dvc, mocker):
# patching to speedup tests
mocker.patch("dvc.lock.DEFAULT_TIMEOUT", 0.01)
lockfile = tmp_dir / dvc.tmp_dir / "lock"
lock = Lock(lockfile)
lock_ext = Lock(lockfile)
# It's a common scenario now to have lock unlocked and locked back (e.g. in
# repro of a stage) in with. We should see LockError exception here.
with pytest.raises(LockError), lock:
lock.unlock()
lock_ext.lock() # imitate an exernal process had time to lock it
lock.lock()
def test_unlock_unlocked_raises():
lock = Lock("lock")
with pytest.raises(DvcException):
lock.unlock()
def test_cli(tmp_dir, dvc, mocker, caplog):
# patching to speedup tests
mocker.patch("dvc.lock.DEFAULT_TIMEOUT", 0.01)
+1 -1
View File
@@ -207,7 +207,7 @@ def test_lockfile_invalid_versions(tmp_dir, dvc, version_info):
lockdata = {**version_info, "stages": {"foo": {"cmd": "echo foo"}}}
(tmp_dir / "dvc.lock").dump(lockdata)
with pytest.raises(YAMLValidationError) as exc_info:
Lockfile(dvc, tmp_dir / "dvc.lock").load()
Lockfile(dvc, (tmp_dir / "dvc.lock").fs_path).load()
rel = make_relpath("dvc.lock")
assert f"'{rel}' validation failed" in str(exc_info.value)
+16 -1
View File
@@ -4,7 +4,10 @@ import pytest
from dvc.cli import main
from dvc.fs import system
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.stage.exceptions import (
StageFileDoesNotExistError,
StageFileIsNotDvcFileError,
)
from dvc.utils.fs import remove
from dvc_objects.errors import ObjectDBError
from tests.utils import get_gitignore_content
@@ -32,6 +35,18 @@ def test_remove(tmp_dir, scm, dvc, run_copy, remove_outs):
assert not (tmp_dir / ".gitignore").exists()
def test_remove_file_target(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
with pytest.raises(
StageFileIsNotDvcFileError,
match="'foo' is not a .dvc file. Do you mean 'foo.dvc'?",
):
dvc.remove("foo")
dvc.remove("foo.dvc")
def test_remove_non_existent_file(tmp_dir, dvc):
with pytest.raises(StageFileDoesNotExistError):
dvc.remove("non_existent_dvc_file.dvc")
+27
View File
@@ -176,6 +176,33 @@ def test_update_import_url(tmp_dir, dvc, workspace):
assert dst.read_text() == "updated file content"
def test_update_import_url_no_download(tmp_dir, dvc, workspace):
workspace.gen("file", "file content")
dst = tmp_dir / "imported_file"
stage = dvc.imp_url(
"remote://workspace/file", os.fspath(dst), no_download=True
)
hash_info = stage.deps[0].hash_info
assert not dst.is_file()
assert stage.deps[0].hash_info.value == "d10b4c3ff123b26dc068d43a8bef2d23"
workspace.gen("file", "updated file content")
updated_stage = dvc.update([stage.path], no_download=True)[0]
assert not dst.exists()
updated_hash_info = updated_stage.deps[0].hash_info
assert updated_hash_info != hash_info
assert updated_hash_info.value == "6ffba511ce3aa40b8231d1b1f8c5fba5"
out = updated_stage.outs[0]
assert out.hash_info.value is None
assert out.hash_info.name is None
assert out.meta.size is None
def test_update_rev(tmp_dir, dvc, scm, git_dir):
with git_dir.chdir():
git_dir.scm_gen({"foo": "foo"}, commit="first")
+27
View File
@@ -4,6 +4,7 @@ from dvc.cli import main
from tests.unit.test_info import (
DVC_VERSION_REGEX,
PYTHON_VERSION_REGEX,
SUBPROJECTS,
find_supported_remotes,
)
@@ -14,8 +15,34 @@ def test_(tmp_dir, dvc, scm, capsys):
out, _ = capsys.readouterr()
assert re.search(rf"DVC version: {DVC_VERSION_REGEX}", out)
assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", out)
for subproject in SUBPROJECTS:
assert re.search(rf"{subproject} = {DVC_VERSION_REGEX}", out)
assert find_supported_remotes(out)
assert re.search(r"Cache types: .*", out)
assert re.search(r"Caches: local", out)
assert re.search(r"Remotes: None", out)
assert "Repo: dvc, git" in out
def test_import_error(tmp_dir, dvc, scm, capsys, monkeypatch):
import importlib.metadata as importlib_metadata
original = importlib_metadata.version
def _import_error(name):
if name == "dvclive":
raise ImportError
return original(name)
monkeypatch.setattr(importlib_metadata, "version", _import_error)
assert main(["version"]) == 0
out, _ = capsys.readouterr()
for subproject in SUBPROJECTS:
match = re.search(rf"{subproject} = {DVC_VERSION_REGEX}", out)
if subproject != "dvclive":
assert match
else:
assert match is None
+125
View File
@@ -0,0 +1,125 @@
import pytest
from dvc.exceptions import InvalidArgumentError
@pytest.mark.parametrize("suffix", ["yaml", "toml", "json"])
@pytest.mark.parametrize(
"overrides, expected",
[
# Overriding
(["foo=baz"], {"foo": "baz", "goo": {"bag": 3.0}, "lorem": False}),
(["foo=baz", "goo=bar"], {"foo": "baz", "goo": "bar", "lorem": False}),
(
["foo.0=bar"],
{"foo": ["bar", {"baz": 2}], "goo": {"bag": 3.0}, "lorem": False},
),
(
["foo.1.baz=3"],
{
"foo": [{"bar": 1}, {"baz": 3}],
"goo": {"bag": 3.0},
"lorem": False,
},
),
(
["goo.bag=4.0"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 4.0},
"lorem": False,
},
),
(
["++goo={bag: 1, b: 2}"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 1, "b": 2},
"lorem": False,
},
),
# 6129
(
["lorem="],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": "",
},
),
# 6129
(
["lorem=null"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": None,
},
),
# 5868
(
["lorem=1992-11-20"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": "1992-11-20",
},
),
# 5868
(
["lorem='1992-11-20'"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": "1992-11-20",
},
),
# Appending
(
["+a=1"],
{
"foo": [{"bar": 1}, {"baz": 2}],
"goo": {"bag": 3.0},
"lorem": False,
"a": 1,
},
),
# Removing
(["~foo"], {"goo": {"bag": 3.0}, "lorem": False}),
],
)
def test_apply_overrides(tmp_dir, suffix, overrides, expected):
from dvc.utils.hydra import apply_overrides
if suffix == "toml":
if overrides in [
["foo=baz"],
["foo.0=bar"],
["foo=baz", "goo=bar"],
["lorem=null"],
]:
# TOML dumper breaks when overriding a list/dict with other type
# or when handling `null` values.
pytest.xfail()
params_file = tmp_dir / f"params.{suffix}"
params_file.dump(
{"foo": [{"bar": 1}, {"baz": 2}], "goo": {"bag": 3.0}, "lorem": False}
)
apply_overrides(path=params_file.name, overrides=overrides)
assert params_file.parse() == expected
@pytest.mark.parametrize(
"overrides",
[["foobar=2"], ["lorem=3,2"], ["+lorem=3"], ["foo[0]=bar"]],
)
def test_invalid_overrides(tmp_dir, overrides):
from dvc.utils.hydra import apply_overrides
params_file = tmp_dir / "params.yaml"
params_file.dump(
{"foo": [{"bar": 1}, {"baz": 2}], "goo": {"bag": 3.0}, "lorem": False}
)
with pytest.raises(InvalidArgumentError):
apply_overrides(path=params_file.name, overrides=overrides)
+15 -6
View File
@@ -14,15 +14,21 @@ from tests.func.parsing.test_errors import escape_ansi
def mocked_status():
yield Status(
not_in_cache=["notincache"],
committed={"added": ["dir/bar", "dir/foo"]},
committed={
"added": ["dir/bar", "dir/foo"],
"deleted": ["dir/baz"],
"modified": ["dir/foobar"],
"unknown": ["dir/unknown1"],
},
uncommitted={
"added": ["dir/baz"],
"modified": ["dir/bar"],
"deleted": ["dir/foobar"],
"unknown": ["dir2/unknown2"],
},
untracked=["untracked"],
unchanged=["dir/foo"],
git={"is_dirty": True},
git={"is_dirty": True, "is_empty": False},
)
@@ -38,7 +44,6 @@ def test_cli(dvc, mocker, mocked_status):
"--json",
"--unchanged",
"--untracked-files",
"--with-dirs",
"--granular",
]
)
@@ -49,7 +54,6 @@ def test_cli(dvc, mocker, mocked_status):
status.assert_called_once_with(
untracked_files="all",
granular=True,
with_dirs=True,
)
@@ -106,24 +110,29 @@ def test_show_status(dvc, scm, mocker, capsys, mocked_status, args, is_dirty):
out, err = capsys.readouterr()
expected_out = """\
Not in cache:
(use "dvc pull <file>..." to update your local storage)
(use "dvc fetch <file>..." to download files)
notincache
DVC committed changes:
(git commit the corresponding dvc files to update the repo)
added: dir/bar
added: dir/foo
deleted: dir/baz
modified: dir/foobar
unknown: dir/unknown1
DVC uncommitted changes:
(use "dvc commit <file>..." to track changes)
(use "dvc checkout <file>..." to discard changes)
added: dir/baz
modified: dir/bar
deleted: dir/foobar
unknown: dir2/unknown2
"""
if "--untracked-files" in args:
expected_out += """
Untracked files:
(use "git add <file> ..." or dvc add <file>..." to commit to git or to dvc)
(use "git add <file> ..." or "dvc add <file>..." to commit to git or to dvc)
untracked
"""
if "--unchanged" in args:
+38
View File
@@ -34,6 +34,7 @@ def test_import(mocker):
fname="file",
rev="version",
no_exec=False,
no_download=False,
desc="description",
jobs=3,
)
@@ -69,6 +70,43 @@ def test_import_no_exec(mocker):
fname="file",
rev="version",
no_exec=True,
no_download=False,
desc="description",
jobs=None,
)
def test_import_no_download(mocker):
cli_args = parse_args(
[
"import",
"repo_url",
"src",
"--out",
"out",
"--file",
"file",
"--rev",
"version",
"--no-download",
"--desc",
"description",
]
)
cmd = cli_args.func(cli_args)
m = mocker.patch.object(cmd.repo, "imp", autospec=True)
assert cmd.run() == 0
m.assert_called_once_with(
"repo_url",
path="src",
out="out",
fname="file",
rev="version",
no_exec=False,
no_download=True,
desc="description",
jobs=None,
)
+24 -6
View File
@@ -1,5 +1,7 @@
import logging
import pytest
from dvc.cli import parse_args
from dvc.commands.imp_url import CmdImportUrl
from dvc.exceptions import DvcException
@@ -31,6 +33,7 @@ def test_import_url(mocker):
out="out",
fname="file",
no_exec=False,
no_download=False,
remote=None,
to_remote=False,
desc="description",
@@ -54,11 +57,19 @@ def test_failed_import_url(mocker, caplog):
assert expected_error in caplog.text
def test_import_url_no_exec(mocker):
@pytest.mark.parametrize(
"flag,expected",
[
("--no-exec", {"no_exec": True, "no_download": False}),
("--no-download", {"no_download": True, "no_exec": False}),
],
)
def test_import_url_no_exec_download_flags(mocker, flag, expected):
cli_args = parse_args(
[
"import-url",
"--no-exec",
flag,
"src",
"out",
"--file",
@@ -77,11 +88,11 @@ def test_import_url_no_exec(mocker):
"src",
out="out",
fname="file",
no_exec=True,
remote=None,
to_remote=False,
desc="description",
jobs=None,
**expected
)
@@ -110,6 +121,7 @@ def test_import_url_to_remote(mocker):
out="bar",
fname=None,
no_exec=False,
no_download=False,
remote="remote",
to_remote=True,
desc="description",
@@ -117,7 +129,8 @@ def test_import_url_to_remote(mocker):
)
def test_import_url_to_remote_invalid_combination(dvc, mocker, caplog):
@pytest.mark.parametrize("flag", ["--no-exec", "--no-download"])
def test_import_url_to_remote_invalid_combination(dvc, mocker, caplog, flag):
cli_args = parse_args(
[
"import-url",
@@ -126,7 +139,7 @@ def test_import_url_to_remote_invalid_combination(dvc, mocker, caplog):
"--to-remote",
"--remote",
"remote",
"--no-exec",
flag,
]
)
assert cli_args.func == CmdImportUrl
@@ -134,9 +147,14 @@ def test_import_url_to_remote_invalid_combination(dvc, mocker, caplog):
cmd = cli_args.func(cli_args)
with caplog.at_level(logging.ERROR, logger="dvc"):
assert cmd.run() == 1
expected_msg = "--no-exec can't be combined with --to-remote"
expected_msg = (
"--no-exec/--no-download cannot be combined with --to-remote"
)
assert expected_msg in caplog.text
def test_import_url_to_remote_flag(dvc, mocker, caplog):
cli_args = parse_args(
["import-url", "s3://bucket/foo", "bar", "--remote", "remote"]
)
+21 -23
View File
@@ -4,7 +4,6 @@ import posixpath
from pathlib import Path
import pytest
from funcy import pluck_attr
from dvc.cli import parse_args
from dvc.commands.plots import CmdPlotsDiff, CmdPlotsShow, CmdPlotsTemplates
@@ -348,39 +347,38 @@ def test_plots_diff_json(dvc, mocker, capsys):
render_mock.assert_not_called()
@pytest.mark.parametrize("target", (("t1"), (None)))
def test_plots_templates(tmp_dir, dvc, mocker, capsys, target):
assert not os.path.exists(dvc.plots.templates_dir)
mocker.patch(
"dvc.commands.plots.CmdPlotsTemplates.TEMPLATES_CHOICES",
["t1", "t2"],
)
@pytest.mark.parametrize(
"target,expected_out,expected_rtn",
(("t1", "\"{'t1'}\"", 0), (None, "t1\nt2", 0), ("t3", "", 1)),
)
def test_plots_templates(
dvc, mocker, capsys, target, expected_out, expected_rtn
):
t1 = mocker.Mock()
t1.DEFAULT_NAME = "t1"
t1.DEFAULT_CONTENT = "{'t1'}"
arguments = ["plots", "templates", "--out", "output"]
t2 = mocker.Mock()
t2.DEFAULT_NAME = "t2"
t2.DEFAULT_CONTENT = "{'t2'}"
mocker.patch("dvc_render.vega_templates.TEMPLATES", [t1, t2])
arguments = ["plots", "templates"]
if target:
arguments += [target]
cli_args = parse_args(arguments)
assert cli_args.func == CmdPlotsTemplates
dump_mock = mocker.patch("dvc_render.vega_templates.dump_templates")
cmd = cli_args.func(cli_args)
assert cmd.run() == 0
rtn = cmd.run()
out, _ = capsys.readouterr()
dump_mock.assert_called_once_with(
output=os.path.abspath("output"), targets=[target] if target else None
)
assert "Templates have been written into 'output'." in out
def test_plots_templates_choices(tmp_dir, dvc):
from dvc_render import TEMPLATES
assert CmdPlotsTemplates.TEMPLATES_CHOICES == list(
pluck_attr("DEFAULT_NAME", TEMPLATES)
)
assert out.strip() == expected_out
assert rtn == expected_rtn
@pytest.mark.parametrize("split", (True, False))
+2
View File
@@ -26,6 +26,7 @@ def test_update(dvc, mocker):
rev="REV",
recursive=True,
to_remote=False,
no_download=False,
remote=None,
jobs=8,
)
@@ -56,6 +57,7 @@ def test_update_to_remote(dvc, mocker):
rev=None,
recursive=True,
to_remote=True,
no_download=False,
remote="remote",
jobs=5,
)
+1 -2
View File
@@ -1,6 +1,5 @@
import pytest
from dvc.fs import AzureAuthError, AzureFileSystem
from dvc_azure import AzureAuthError, AzureFileSystem
container_name = "container-name"
connection_string = (
+28 -28
View File
@@ -1,36 +1,36 @@
import pytest
from dvc_http import HTTPFileSystem, HTTPSFileSystem
from dvc_s3 import S3FileSystem
from dvc_ssh import SSHFileSystem
from dvc.config import RemoteNotFoundError
from dvc.fs import (
HDFSFileSystem,
HTTPFileSystem,
HTTPSFileSystem,
LocalFileSystem,
S3FileSystem,
SSHFileSystem,
get_fs_cls,
get_fs_config,
)
from dvc.fs import LocalFileSystem, get_fs_cls, get_fs_config
url_cls_pairs = [
("s3://bucket/path", S3FileSystem),
("ssh://example.com:/dir/path", SSHFileSystem),
("http://example.com/path/to/file", HTTPFileSystem),
("https://example.com/path/to/file", HTTPSFileSystem),
("path/to/file", LocalFileSystem),
("path\\to\\file", LocalFileSystem),
("file", LocalFileSystem),
("./file", LocalFileSystem),
(".\\file", LocalFileSystem),
("../file", LocalFileSystem),
("..\\file", LocalFileSystem),
("unknown://path", LocalFileSystem),
]
@pytest.mark.parametrize(
"url, cls",
[
("s3://bucket/path", S3FileSystem),
("ssh://example.com:/dir/path", SSHFileSystem),
("hdfs://example.com/dir/path", HDFSFileSystem),
("http://example.com/path/to/file", HTTPFileSystem),
("https://example.com/path/to/file", HTTPSFileSystem),
("path/to/file", LocalFileSystem),
("path\\to\\file", LocalFileSystem),
("file", LocalFileSystem),
("./file", LocalFileSystem),
(".\\file", LocalFileSystem),
("../file", LocalFileSystem),
("..\\file", LocalFileSystem),
("unknown://path", LocalFileSystem),
],
)
try:
from dvc_hdfs import HDFSFileSystem
url_cls_pairs += [("hdfs://example.com/dir/path", HDFSFileSystem)]
except ImportError:
pass
@pytest.mark.parametrize("url, cls", url_cls_pairs)
def test_get_fs_cls(url, cls):
assert get_fs_cls({"url": url}) == cls
-16
View File
@@ -1,16 +0,0 @@
import pytest
from dvc.fs import HDFSFileSystem
@pytest.mark.parametrize(
"fs_args, expected_url",
[
({"host": "example.com"}, "hdfs://example.com"),
({"host": "example.com", "port": None}, "hdfs://example.com"),
({"host": "example.com", "port": 8020}, "hdfs://example.com:8020"),
],
)
def test_hdfs_unstrip_protocol(fs_args, expected_url):
fs = HDFSFileSystem(**fs_args)
assert fs.unstrip_protocol("/path") == expected_url + "/path"
+2 -1
View File
@@ -1,8 +1,9 @@
import os
import pytest
from dvc_s3 import S3FileSystem
from dvc.fs import ConfigError, S3FileSystem
from dvc.fs import ConfigError
bucket_name = "bucket-name"
prefix = "some/prefix"
+3 -6
View File
@@ -5,8 +5,7 @@ import os
from unittest.mock import mock_open, patch
import pytest
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
def test_get_kwargs_from_urls():
@@ -44,9 +43,7 @@ def test_init():
def test_ssh_ask_password(mocker):
mocker.patch(
"dvc_objects.fs.implementations.ssh.ask_password", return_value="fish"
)
mocker.patch("dvc_ssh.ask_password", return_value="fish")
fs = SSHFileSystem(user="test", host="2.2.2.2", ask_password=True)
assert fs.fs_args["password"] == fs.fs_args["passphrase"] == "fish"
@@ -101,7 +98,7 @@ def test_ssh_user(mock_file, config, expected_user):
[
({"host": "example.com"}, 1234),
({"host": "example.com", "port": 4321}, 4321),
({"host": "not_in_ssh_config.com"}, DEFAULT_SSH_PORT),
({"host": "not_in_ssh_config.com"}, DEFAULT_PORT),
({"host": "not_in_ssh_config.com", "port": 2222}, 2222),
],
)
+2 -1
View File
@@ -1,9 +1,10 @@
# pylint: disable=unsubscriptable-object
import pytest
from dvc_s3 import S3FileSystem
from dvc import output
from dvc.fs import LocalFileSystem, S3FileSystem
from dvc.fs import LocalFileSystem
from dvc.output import Output
from dvc.stage import Stage
+1 -1
View File
@@ -104,7 +104,7 @@ def test_get_used_objs(exists, expected_message, mocker, caplog):
assert first(caplog.messages) == expected_message
def test_remote_missing_depenency_on_dir_pull(tmp_dir, scm, dvc, mocker):
def test_remote_missing_dependency_on_dir_pull(tmp_dir, scm, dvc, mocker):
tmp_dir.dvc_gen({"dir": {"subfile": "file2 content"}}, commit="add dir")
with dvc.config.edit() as conf:
conf["remote"]["s3"] = {"url": "s3://bucket/name"}
+1 -1
View File
@@ -1,4 +1,4 @@
from dvc.fs import OSSFileSystem
from dvc_oss import OSSFileSystem
bucket_name = "bucket-name"
endpoint = "endpoint"
+3 -1
View File
@@ -1,8 +1,10 @@
import os
import pytest
from dvc_gs import GSFileSystem
from dvc_s3 import S3FileSystem
from dvc.fs import GSFileSystem, S3FileSystem, get_cloud_fs
from dvc.fs import get_cloud_fs
from dvc_data.db import get_index
+3 -2
View File
@@ -1,8 +1,9 @@
from unittest.mock import patch
import pytest
from dvc_webdav import WebDAVFileSystem, WebDAVSFileSystem
from dvc.fs import WebDAVFileSystem, WebDAVSFileSystem, get_cloud_fs
from dvc.fs import get_cloud_fs
from tests.utils.asserts import issubset
url_fmt = "{scheme}://{user}@example.com/public.php/webdav"
@@ -65,7 +66,7 @@ def test_token():
)
@patch("dvc_objects.fs.implementations.webdav.ask_password")
@patch("dvc_webdav.ask_password")
def test_ask_password(ask_password_mocked):
ask_password_mocked.return_value = "pass"
host = "host"
+3 -4
View File
@@ -2,8 +2,7 @@ from unittest.mock import Mock, create_autospec
import pytest
import requests
from dvc.fs import WebHDFSFileSystem
from dvc_webhdfs import WebHDFSFileSystem
host = "host"
kerberos = False
@@ -16,8 +15,8 @@ use_https = True
user = "test"
@pytest.fixture()
def webhdfs_config():
@pytest.fixture(name="webhdfs_config")
def fixture_webhdfs_config():
url = f"webhdfs://{user}@{host}:{port}"
url_config = WebHDFSFileSystem._get_kwargs_from_urls(url)
return {
+106 -1
View File
@@ -4,8 +4,10 @@ import pytest
from celery import shared_task
from flaky.flaky_decorator import flaky
from dvc.exceptions import DvcException
from dvc.repo.experiments.exceptions import UnresolvedExpNamesError
from dvc.repo.experiments.executor.local import TempDirExecutor
from dvc.repo.experiments.queue.base import QueueDoneResult
from dvc.repo.experiments.refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE
@@ -37,7 +39,7 @@ def test_shutdown_with_kill(test_queue, mocker):
mocker.patch.object(
test_queue,
"_iter_active_tasks",
return_value=[(result.id, mock_entry)],
return_value=[(result, mock_entry)],
)
kill_spy = mocker.patch.object(test_queue.proc, "kill")
@@ -120,3 +122,106 @@ def test_queue_clean_workspace_refs(git_dir, tmp_dir):
for ref in exec_heads:
assert git_dir.scm.get_ref(ref) is None
@pytest.mark.parametrize("status", ["FAILURE", "SUCCESS"])
def test_queue_iter_done_task(test_queue, mocker, status):
mock_entry = mocker.Mock(stash_rev=_foo.name)
result = mocker.Mock(status=status)
mocker.patch.object(
test_queue,
"_iter_done_tasks",
return_value=[(result, mock_entry)],
)
if status == "FAILURE":
assert list(test_queue.iter_failed()) == [
QueueDoneResult(mock_entry, None)
]
elif status == "SUCCESS":
with pytest.raises(DvcException):
assert list(test_queue.iter_success())
def test_queue_status(test_queue, scm, mocker):
from datetime import datetime
active_entry = mocker.Mock(stash_rev="active")
active_entry.name = "foo"
queued_entry = mocker.Mock(stash_rev="queued")
queued_entry.name = None
failed_entry = mocker.Mock(stash_rev="failed")
failed_entry.name = "bar"
success_entry = mocker.Mock(stash_rev="success")
success_entry.name = None
success_result = mocker.Mock(ref_info=mocker.Mock())
success_result.ref_info.name = "foobar"
def resolve_commit(rev):
if rev == "active":
commit_time = datetime(2022, 8, 7).timestamp()
elif rev == "queued":
commit_time = datetime(2022, 8, 6).timestamp()
elif rev == "failed":
commit_time = datetime(2022, 8, 5).timestamp()
elif rev == "success":
commit_time = datetime(2022, 8, 4).timestamp()
return mocker.Mock(commit_time=commit_time)
mocker.patch.object(
scm,
"resolve_commit",
side_effect=mocker.MagicMock(side_effect=resolve_commit),
)
mocker.patch.object(
test_queue,
"iter_active",
return_value=[active_entry],
)
mocker.patch.object(
test_queue,
"iter_queued",
return_value=[queued_entry],
)
mocker.patch.object(
test_queue,
"iter_failed",
return_value=[(failed_entry, None)],
)
mocker.patch.object(
test_queue,
"iter_success",
return_value=[(success_entry, success_result)],
)
assert test_queue.status() == [
{
"name": "foo",
"rev": "active",
"status": "Running",
"timestamp": datetime(2022, 8, 7, 0, 0, 0),
},
{
"name": None,
"rev": "queued",
"status": "Queued",
"timestamp": datetime(2022, 8, 6, 0, 0, 0),
},
{
"name": "bar",
"rev": "failed",
"status": "Failed",
"timestamp": datetime(2022, 8, 5, 0, 0, 0),
},
{
"name": "foobar",
"rev": "success",
"status": "Success",
"timestamp": datetime(2022, 8, 4, 0, 0, 0),
},
]
+44
View File
@@ -1,3 +1,8 @@
import os
from dvc.repo.reproduce import _get_stage_files
def test_number_reproduces(tmp_dir, dvc, mocker):
reproduce_stage_mock = mocker.patch(
"dvc.repo.reproduce._reproduce_stage", returns=[]
@@ -22,3 +27,42 @@ def test_number_reproduces(tmp_dir, dvc, mocker):
dvc.reproduce(all_pipelines=True)
assert reproduce_stage_mock.call_count == 5
def test_get_stage_files(tmp_dir, dvc):
tmp_dir.dvc_gen("dvc-dep", "dvc-dep")
tmp_dir.gen("other-dep", "other-dep")
stage = dvc.stage.add(
name="stage",
cmd="foo",
deps=["dvc-dep", "other-dep"],
outs=["dvc-out"],
outs_no_cache=["other-out"],
)
result = set(_get_stage_files(stage))
assert result == {
stage.dvcfile.relpath,
str(tmp_dir / "other-dep"),
str(tmp_dir / "other-out"),
}
def test_get_stage_files_wdir(tmp_dir, dvc):
tmp_dir.gen({"dir": {"dvc-dep": "dvc-dep", "other-dep": "other-dep"}})
dvc.add(os.path.join("dir", "dvc-dep"))
stage = dvc.stage.add(
name="stage",
cmd="foo",
wdir="dir",
deps=["dvc-dep", "other-dep"],
outs=["dvc-out"],
outs_no_cache=["other-out"],
)
result = set(_get_stage_files(stage))
assert result == {
stage.dvcfile.relpath,
str(tmp_dir / "dir" / "other-dep"),
str(tmp_dir / "dir" / "other-out"),
}
+6 -3
View File
@@ -4,7 +4,7 @@ import shutil
import pytest
from dvc.info import get_dvc_info
from dvc.info import SUBPROJECTS, get_dvc_info
# Python's version is in the shape of:
# <major>.<minor>.<patch>[{a|b|rc}N][.postN][.devN]
@@ -53,6 +53,9 @@ def test_info_in_repo(scm_init, tmp_dir):
assert re.search(rf"DVC version: {DVC_VERSION_REGEX}", dvc_info)
assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", dvc_info)
for subproject in SUBPROJECTS:
assert re.search(rf"{subproject} = {DVC_VERSION_REGEX}", dvc_info)
assert find_supported_remotes(dvc_info)
assert re.search(r"Cache types: .*", dvc_info)
@@ -141,10 +144,10 @@ def test_fs_info_outside_of_repo(tmp_dir, caplog):
def test_plugin_versions(tmp_dir, dvc):
from dvc.fs import FS_MAP
from dvc.fs import registry
dvc_info = get_dvc_info()
remotes = find_supported_remotes(dvc_info)
for remote, dependencies in remotes.items():
assert dependencies.keys() == FS_MAP[remote].REQUIRES.keys()
assert dependencies.keys() == registry[remote].REQUIRES.keys()
+34 -22
View File
@@ -16,19 +16,19 @@ def test_rwlock(tmp_path):
path = os.fspath(tmp_path)
foo = "foo"
with rwlock(path, localfs, "cmd1", [foo], []):
with rwlock(path, localfs, "cmd1", [foo], [], False):
with pytest.raises(LockError):
with rwlock(path, localfs, "cmd2", [], [foo]):
with rwlock(path, localfs, "cmd2", [], [foo], False):
pass
with rwlock(path, localfs, "cmd1", [], [foo]):
with rwlock(path, localfs, "cmd1", [], [foo], False):
with pytest.raises(LockError):
with rwlock(path, localfs, "cmd2", [foo], []):
with rwlock(path, localfs, "cmd2", [foo], [], False):
pass
with rwlock(path, localfs, "cmd1", [], [foo]):
with rwlock(path, localfs, "cmd1", [], [foo], False):
with pytest.raises(LockError):
with rwlock(path, localfs, "cmd2", [], [foo]):
with rwlock(path, localfs, "cmd2", [], [foo], False):
pass
@@ -36,47 +36,59 @@ def test_rwlock_reentrant(tmp_path):
path = os.fspath(tmp_path)
foo = "foo"
with rwlock(path, localfs, "cmd1", [], [foo]):
with rwlock(path, localfs, "cmd1", [], [foo]):
with rwlock(path, localfs, "cmd1", [], [foo], False):
with rwlock(path, localfs, "cmd1", [], [foo], False):
pass
with _edit_rwlock(path, localfs) as lock:
with _edit_rwlock(path, localfs, False) as lock:
assert lock == {
"read": {},
"write": {"foo": {"cmd": "cmd1", "pid": os.getpid()}},
}
with rwlock(path, localfs, "cmd", [foo], []):
with rwlock(path, localfs, "cmd", [foo], []):
with rwlock(path, localfs, "cmd", [foo], [], False):
with rwlock(path, localfs, "cmd", [foo], [], False):
pass
with _edit_rwlock(path, localfs) as lock:
with _edit_rwlock(path, localfs, False) as lock:
assert lock == {
"read": {"foo": [{"cmd": "cmd", "pid": os.getpid()}]},
"write": {},
}
def test_rwlock_edit_is_guarded(tmp_path, mocker):
# patching to speedup tests
mocker.patch("dvc.lock.DEFAULT_TIMEOUT", 0.01)
path = os.fspath(tmp_path)
with _edit_rwlock(path, localfs, False):
with pytest.raises(LockError):
with _edit_rwlock(path, localfs, False):
pass
def test_rwlock_subdirs(tmp_path):
path = os.fspath(tmp_path)
foo = "foo"
subfoo = os.path.join("foo", "subfoo")
with rwlock(path, localfs, "cmd1", [foo], []):
with rwlock(path, localfs, "cmd1", [foo], [], False):
with pytest.raises(LockError, match=r"subfoo(.|\n)*cmd1"):
with rwlock(path, localfs, "cmd2", [], [subfoo]):
with rwlock(path, localfs, "cmd2", [], [subfoo], False):
pass
with rwlock(path, localfs, "cmd1", [], [subfoo]):
with rwlock(path, localfs, "cmd1", [], [subfoo], False):
with pytest.raises(LockError, match=r"'foo'(.|\n)*cmd1"):
with rwlock(path, localfs, "cmd2", [foo], []):
with rwlock(path, localfs, "cmd2", [foo], [], False):
pass
with rwlock(path, localfs, "cmd1", [], [subfoo]):
with rwlock(path, localfs, "cmd1", [], [subfoo], False):
with pytest.raises(LockError):
with rwlock(path, localfs, "cmd2", [], [foo]):
with rwlock(path, localfs, "cmd2", [], [foo], False):
pass
with rwlock(path, localfs, "cmd1", [subfoo], []):
with rwlock(path, localfs, "cmd2", [foo], []):
with rwlock(path, localfs, "cmd1", [subfoo], [], False):
with rwlock(path, localfs, "cmd2", [foo], [], False):
pass
@@ -86,10 +98,10 @@ def test_broken_rwlock(tmp_path):
path.write_text('{"broken": "format"}', encoding="utf-8")
with pytest.raises(RWLockFileFormatError):
with _edit_rwlock(dir_path, localfs):
with _edit_rwlock(dir_path, localfs, False):
pass
path.write_text("{broken json", encoding="utf-8")
with pytest.raises(RWLockFileCorruptedError):
with _edit_rwlock(dir_path, localfs):
with _edit_rwlock(dir_path, localfs, False):
pass

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