Compare commits

..
57 Commits
Author SHA1 Message Date
Saugat Pachhai (सौगात) 2c3956bdb0 exp init: fix live metrics path 2022-11-07 12:45:32 +01:00
Saugat Pachhai (सौगात) a48a19ce31 tests: ignore versions for subprojects 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro 1de983f0de Remove live section support. 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro eb20a8e049 deps: Bump dvclive to >= 1.0 2022-11-07 12:45:32 +01:00
David de la Iglesia Castro 78e25747a4 exp init: Update DVCLive paths.
Closes #8531
2022-11-07 12:45:32 +01:00
dberenbaum 240f0ebdc3 fix hydra_sweeps referenced before assignment 2022-11-04 15:59:26 +01:00
skshetry 69f78d3bb6 ci: bench: use 3.11 in benchmarks 2022-11-04 18:43:02 +05:45
skshetry 740fe504c9 Update README.rst 2022-11-04 18:27:50 +05:45
skshetry 7bb84286f5 readme: show downloads badge 2022-11-04 18:27:50 +05:45
Peter Rowlands 3891e81bdd index: load dir entries before generating target views 2022-11-04 19:09:48 +09:00
Peter Rowlands bb62833897 index: move import specific entry building out of index.data() property 2022-11-04 19:09:48 +09:00
Peter Rowlands fb703150ce output: load precomputed md5 from PARAM_FILES for non-local fs 2022-11-04 19:09:48 +09:00
Peter Rowlands 33d4ead8b6 import-url: save files for version-aware dir 2022-11-04 19:09:48 +09:00
Peter Rowlands 76b1f8c907 deps: bump dvc-data to 0.25.3 2022-11-04 19:09:48 +09:00
Paweł Redzyński d7152e360e Update dvc/repo/plots/__init__.py 2022-11-04 09:54:45 +05:45
dberenbaum ab69b88a21 plots: allow top-level strings 2022-11-04 09:54:45 +05:45
dependabot[bot] 43a8eab2e0 build(deps-dev): Bump dvc-s3 from 2.20.1 to 2.21.0
Bumps [dvc-s3](https://github.com/iterative/dvc-s3) from 2.20.1 to 2.21.0.
- [Release notes](https://github.com/iterative/dvc-s3/releases)
- [Commits](https://github.com/iterative/dvc-s3/compare/2.20.1...2.21.0)

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

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

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

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

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

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

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

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

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

* ci: use pyproject.toml for cache key

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

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

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-31 21:23:57 +05:45
dependabot[bot] 4330f1a70f build(deps): Bump pyinstaller from 5.0 to 5.6.1 (#8475)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 5.0 to 5.6.1.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v5.0...v5.6.1)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-31 21:23:43 +05:45
dependabot[bot] 144e21b552 build(deps-dev): Bump pytest from 7.1.3 to 7.2.0 (#8479)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.3...7.2.0)

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

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-31 21:20:21 +05:45
dependabot[bot] 7945428e5e build(deps-dev): Bump pylint from 2.15.4 to 2.15.5 (#8463)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.4 to 2.15.5.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.4...v2.15.5)

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

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

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

* Apply suggestions from code review
2022-10-31 21:04:11 +05:45
skshetry 4ca2158b9d tests: retry celery queue on timeout (#8488)
retry celery queue on timeout
2022-10-31 19:38:05 +05:45
Ruslan Kuprieiev f87e78d754 deps: bump dvc-data to 0.25.2 2022-10-31 14:41:23 +02:00
103 changed files with 2870 additions and 4079 deletions
-10
View File
@@ -1,10 +0,0 @@
comment: false
coverage:
status:
project:
default:
target: 90
threshold: 10%
patch:
default:
threshold: 25%
-16
View File
@@ -1,16 +0,0 @@
[run]
branch = True
source = dvc
omit =
*__main__.py
[report]
exclude_lines =
if __name__ == .__main__.:
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
pass
raise NotImplementedError
# pragma: no cover
# pragma: no cover
\.\.\.$
-1
View File
@@ -1,4 +1,3 @@
/tests
/dvc
/.github
/bin
+12
View File
@@ -0,0 +1,12 @@
[flake8]
ignore=
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
P1, # unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
max_line_length = 79
max-complexity = 15
select = B,C,E,F,W,T4,B902,T,P
show_source = true
count = true
+1 -1
View File
@@ -8,7 +8,7 @@ about: Create a bug report to help us improve DVC
<!--
## Issue name
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
Example: `repro: doesn't detect input changes`
-->
@@ -2,4 +2,3 @@
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project
---
+12
View File
@@ -0,0 +1,12 @@
codecov:
notify:
wait_for_ci: true
coverage:
status:
project:
default:
target: auto
threshold: 2%
patch: off
github_checks:
annotations: false
+16
View File
@@ -0,0 +1,16 @@
changelog:
exclude:
labels: ["skip-changelog"]
categories:
- title: 🛠 Breaking Changes
labels: ["breaking-change"]
- title: 🚀 New Features and Enhancements
labels: ["feature", "enhancement", "ui"]
- title: ⚡ Optimizations
labels: ["optimize", "performance"]
- title: 🐛 Bug Fixes
labels: ["bugfix", "bug"]
- title: 🔨 Maintenance
labels: ["maintenance", "refactoring", "chore", "build", "ci"]
- title: Other Changes
labels: ["*"]
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
- uses: iterative/dvc-bench@main
with:
pytest_options: "-k 'test_init or test_help'"
+26 -11
View File
@@ -7,8 +7,13 @@ on:
types: [released, prereleased]
workflow_dispatch:
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
binary:
permissions:
contents: write # for actions/upload-release-asset to upload release asset
strategy:
matrix:
include:
@@ -24,13 +29,13 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: "3.10"
cache: pip
cache-dependency-path: |
setup.cfg
pyproject.toml
scripts/build-requirements.txt
- name: Set up Ruby 2.6
@@ -45,7 +50,7 @@ jobs:
- name: Install deps
run: |
pip install -U wheel pip setuptools
pip install --upgrade pip wheel setuptools
pip install .[all]
pip install -r scripts/build-requirements.txt
@@ -77,21 +82,31 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: setup.cfg
- name: Install
run: pip install --upgrade pip wheel
- name: Force version for Test PyPI uploads
if: ${{ !startsWith(github.ref, 'refs/tags') }}
run: |
pip install -U wheel pip build setuptools
pip install ".[all,tests]"
pip install setuptools_scm
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
- name: Build packages
run: |
pip install -U pip
./scripts/build_package.sh
run: ./scripts/build_package.sh
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
- name: Publish to Test PyPI
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
+23 -15
View File
@@ -17,6 +17,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
jobs:
lint:
timeout-minutes: 10
@@ -29,13 +32,12 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.8
cache: pip
cache-dependency-path: pyproject.toml
- name: Install requirements
run: |
pip install --upgrade pip setuptools wheel
pip install collective.checkdocs==0.2
pip install --upgrade pip wheel
pip install ".[dev]"
- name: Check README
run: python setup.py checkdocs
- uses: pre-commit/[email protected]
tests:
timeout-minutes: 45
@@ -43,13 +45,14 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
os: [ubuntu-20.04, windows-latest]
pyv: ["3.8", "3.9", "3.10"]
pytest-filter:
pytest-filter:
- "import or plot or live or experiment"
- "not (import or plot or live or experiment)"
include:
- {os: ubuntu-latest, pyv: "3.11-dev"}
- {os: ubuntu-latest, pyv: "3.11"}
- {os: macos-latest, pyv: "3.8"}
steps:
- uses: actions/checkout@v3
@@ -60,25 +63,30 @@ jobs:
with:
python-version: ${{ matrix.pyv }}
cache: pip
cache-dependency-path: setup.cfg
cache-dependency-path: pyproject.toml
- name: install
if: steps.cache.pip-cache-dir.cache-hit != 'true'
run: |
pip install --upgrade pip setuptools wheel
pip install --upgrade pip wheel
pip install -e ".[dev]"
- name: run tests
timeout-minutes: 40
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
timeout-minutes: 20
run: pytest -nauto --durations 100 --cov --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
- name: upload coverage report
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: false
check:
if: always()
needs: [lint, tests]
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
notify:
if: github.ref == 'refs/heads/main' && failure()
needs:
- lint
- tests
needs: [lint, tests]
runs-on: ubuntu-latest
steps:
- name: Slack Notification
+43 -28
View File
@@ -1,39 +1,62 @@
default_language_version:
python: python3
ci:
skip: [mypy, pylint, dvc-pre-commit]
repos:
- hooks:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 22.10.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
exclude: "tests/func/utils/test_strict_yaml.py"
args: ['--assume-in-merge']
- id: check-toml
- id: check-yaml
- hooks:
- id: codespell
args:
- --ignore-words-list
- ba,datas,fo,uptodate,cachable,falsy
repo: https://github.com/codespell-project/codespell
rev: v2.2.2
- hooks:
- id: isort
language_version: python3
repo: https://github.com/timothycrosley/isort
- id: debug-statements
exclude: "dvc/_debug.py"
- id: end-of-file-fixer
- id: mixed-line-ending
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/lovesegfault/beautysh
rev: v6.2.1
hooks:
- id: beautysh
args: ["-i", "2"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.0
hooks:
- id: pyupgrade
args:
- --py38-plus
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
- hooks:
hooks:
- id: isort
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
additional_dependencies: ["tomli"]
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-broken-line
- flake8-bugbear
- flake8-comprehensions
- flake8-debugger
- flake8-string-format
repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: local
hooks:
- id: mypy
@@ -47,14 +70,6 @@ repos:
entry: pylint
language: system
types: [python]
- hooks:
- args:
- -i
- "2"
id: beautysh
language_version: python3
repo: https://github.com/lovesegfault/beautysh
rev: v6.2.1
- hooks:
- args:
- git-hook
-1
View File
@@ -74,4 +74,3 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
-3
View File
@@ -1,3 +0,0 @@
include LICENSE
recursive-include requirements *.txt
recursive-include dvc/repo/plots/templates *.json
+6 -2
View File
@@ -13,7 +13,7 @@
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|PyPI| |PyPI Downloads| |Packages| |Brew| |Conda| |Choco| |Snap|
|
@@ -276,6 +276,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://pypi.org/project/dvc
:alt: PyPI
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/dvc.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold
:target: https://pypi.org/project/dvc
:alt: PyPI Downloads
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
:target: https://github.com/iterative/dvc/releases/latest
:alt: deb|pkg|rpm|exe
@@ -292,6 +296,6 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
:target: https://github.com/iterative/dvc/graphs/contributors
:alt: Contributors
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
:alt: VS Code Extension
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
PYTHONPATH=$DVC_HOME python -m dvc $@
-3
View File
@@ -1,3 +0,0 @@
@echo OFF
set PYTHONPATH=%DVC_HOME%
python -m dvc %*
+1 -1
View File
@@ -4,6 +4,6 @@ DVC
Make your data science projects reproducible and shareable.
"""
import dvc.logger
from dvc.version import __version__ # noqa: F401
from dvc.version import __version__, version_tuple # noqa: F401
dvc.logger.setup()
-20
View File
@@ -238,26 +238,6 @@ def _add_common_args(parser):
help="Declare output plot file (do not put into DVC cache).",
metavar="<path>",
)
parser.add_argument(
"--live", help="Declare output as dvclive.", metavar="<path>"
)
parser.add_argument(
"--live-no-cache",
help="Declare output as dvclive (do not put into DVC cache).",
metavar="<path>",
)
parser.add_argument(
"--live-no-summary",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--live-no-html",
action="store_true",
default=False,
help="Signal dvclive logger to not produce training report.",
)
parser.add_argument(
"-w",
"--wdir",
+6 -4
View File
@@ -1,7 +1,7 @@
from collections import defaultdict
from typing import Any, Mapping
from dvc.output import ARTIFACT_SCHEMA, Output
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
from .base import Dependency
from .param import ParamsDependency
@@ -15,10 +15,11 @@ SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**ParamsDependency.PARAM_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
def _get(stage, p, info, fs_config=None):
def _get(stage, p, info, **kwargs):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
@@ -27,14 +28,15 @@ def _get(stage, p, info, fs_config=None):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
return Dependency(stage, p, info, fs_config=fs_config)
return Dependency(stage, p, info, **kwargs)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(Output.PARAM_PATH, None)
ret.append(_get(stage, p, d))
files = d.pop(Output.PARAM_FILES, None)
ret.append(_get(stage, p, d, files=files))
return ret
+5
View File
@@ -62,3 +62,8 @@ class Dependency(Output):
self.fs_path = self.fs.path.version_path(
self.fs_path, self.meta.version_id
)
def dumpd(self, **kwargs):
if self.fs.version_aware:
kwargs["with_files"] = True
return super().dumpd(**kwargs)
-3
View File
@@ -2,9 +2,6 @@ DVC_CHECKPOINT = "DVC_CHECKPOINT"
DVC_DAEMON = "DVC_DAEMON"
DVC_PAGER = "DVC_PAGER"
DVC_ROOT = "DVC_ROOT"
DVCLIVE_PATH = "DVCLIVE_PATH"
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
DVCLIVE_HTML = "DVCLIVE_HTML"
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
+1 -1
View File
@@ -26,7 +26,7 @@ class BaseMachineBackend(ABC):
) -> Iterator[dict]:
"""Iterate over status of all instances of the specified machine."""
def close(self):
def close(self): # noqa: B027
pass
@abstractmethod
+56 -64
View File
@@ -87,7 +87,6 @@ def loadd_from(stage, d_list):
plot = d.pop(Output.PARAM_PLOT, False)
persist = d.pop(Output.PARAM_PERSIST, False)
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
live = d.pop(Output.PARAM_LIVE, False)
remote = d.pop(Output.PARAM_REMOTE, None)
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
files = d.pop(Output.PARAM_FILES, None)
@@ -101,7 +100,6 @@ def loadd_from(stage, d_list):
plot=plot,
persist=persist,
checkpoint=checkpoint,
live=live,
remote=remote,
**annot,
files=files,
@@ -118,7 +116,6 @@ def loads_from(
plot=False,
persist=False,
checkpoint=False,
live=False,
remote=None,
):
return [
@@ -131,7 +128,6 @@ def loads_from(
plot=plot,
persist=persist,
checkpoint=checkpoint,
live=live,
remote=remote,
)
for s in s_list
@@ -166,29 +162,21 @@ def load_from_pipeline(stage, data, typ="outs"):
stage.PARAM_OUTS,
stage.PARAM_METRICS,
stage.PARAM_PLOTS,
stage.PARAM_LIVE,
):
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
metric = typ == stage.PARAM_METRICS
plot = typ == stage.PARAM_PLOTS
live = typ == stage.PARAM_LIVE
if live:
# `live` is single object
data = [data]
d = _merge_data(data)
for path, flags in d.items():
plt_d, live_d = {}, {}
plt_d = {}
if plot:
from dvc.schema import PLOT_PROPS
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
if live:
from dvc.schema import LIVE_PROPS
live_d, flags = _split_dict(flags, keys=LIVE_PROPS.keys())
extra = project(
flags,
[
@@ -206,7 +194,6 @@ def load_from_pipeline(stage, data, typ="outs"):
info={},
plot=plt_d or plot,
metric=metric,
live=live_d or live,
**extra,
)
@@ -263,9 +250,6 @@ class Output:
PARAM_PLOT_TITLE = "title"
PARAM_PLOT_HEADER = "header"
PARAM_PERSIST = "persist"
PARAM_LIVE = "live"
PARAM_LIVE_SUMMARY = "summary"
PARAM_LIVE_HTML = "html"
PARAM_REMOTE = "remote"
METRIC_SCHEMA = Any(
@@ -292,7 +276,6 @@ class Output:
plot=False,
persist=False,
checkpoint=False,
live=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
@@ -355,7 +338,6 @@ class Output:
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.checkpoint = checkpoint
self.live = live
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj: Optional["HashFile"] = None
@@ -377,8 +359,12 @@ class Output:
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
if self.hash_info and self.hash_info.isdir:
if self.meta.nfiles or self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
if not self.hash_info and self.hash_name != "md5":
md5 = getattr(self.meta, "md5", None)
if md5:
self.hash_info = HashInfo("md5", md5)
def _parse_path(self, fs, fs_path):
parsed = urlparse(self.def_path)
@@ -523,7 +509,10 @@ class Output:
except NoRemoteError:
remote = None
return DataIndexEntry(
if self.files and not self.obj:
self.obj = self.get_obj()
entry = DataIndexEntry(
meta=self.meta,
obj=self.obj,
hash_info=self.hash_info,
@@ -531,6 +520,16 @@ class Output:
cache=self.odb,
remote=remote,
)
if self.stage.is_import and not self.stage.is_repo_import:
dep = self.stage.deps[0]
entry.fs = dep.fs
entry.path = dep.fs_path
entry.meta = dep.meta
if not entry.obj:
if not dep.obj:
dep.obj = dep.get_obj()
entry.obj = dep.obj
return entry
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -631,8 +630,16 @@ class Output:
if self.metric:
self.verify_metric()
if not self.use_cache:
_, self.meta, obj = build(
if self.use_cache:
_, self.meta, self.obj = build(
self.odb,
self.fs_path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
)
else:
_, self.meta, self.obj = build(
self.repo.odb.local,
self.fs_path,
self.fs,
@@ -640,23 +647,11 @@ class Output:
ignore=self.dvcignore,
dry_run=True,
)
self.hash_info = obj.hash_info
self.files = None
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
)
return
assert not self.IS_DEPENDENCY
_, self.meta, self.obj = build(
self.odb,
self.fs_path,
self.fs,
self.hash_name,
ignore=self.dvcignore,
)
self.hash_info = self.obj.hash_info
self.files = None
@@ -754,40 +749,37 @@ class Output:
ret[self.PARAM_PATH] = path
if self.IS_DEPENDENCY:
return ret
if not self.IS_DEPENDENCY:
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
if self.metric:
ret[self.PARAM_METRIC] = self.metric
if self.metric:
ret[self.PARAM_METRIC] = self.metric
if self.plot:
ret[self.PARAM_PLOT] = self.plot
if self.plot:
ret[self.PARAM_PLOT] = self.plot
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
if self.checkpoint:
ret[self.PARAM_CHECKPOINT] = self.checkpoint
if self.checkpoint:
ret[self.PARAM_CHECKPOINT] = self.checkpoint
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if self.live:
ret[self.PARAM_LIVE] = self.live
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if self.hash_info.isdir and (
kwargs.get("with_files") or self.files is not None
if (
(not self.IS_DEPENDENCY or self.stage.is_import)
and self.hash_info.isdir
and (kwargs.get("with_files") or self.files is not None)
):
if self.obj:
obj = self.obj
@@ -1182,11 +1174,11 @@ class Output:
@property
def is_metric(self) -> bool:
return bool(self.metric) or bool(self.live)
return bool(self.metric)
@property
def is_plot(self) -> bool:
return bool(self.plot) or bool(self.live)
return bool(self.plot)
META_SCHEMA = {
+1 -1
View File
@@ -36,7 +36,7 @@ from .interpolate import (
)
if TYPE_CHECKING:
from typing_extensions import NoReturn
from typing import NoReturn
from dvc.repo import Repo
+1 -2
View File
@@ -9,10 +9,9 @@ from dvc.exceptions import DvcException
from dvc.utils.flatten import flatten
if typing.TYPE_CHECKING:
from typing import List, Match
from typing import List, Match, NoReturn
from pyparsing import ParseException
from typing_extensions import NoReturn
from .context import Context
+1 -1
View File
@@ -269,7 +269,7 @@ class Repo:
except SCMError:
if self._uninitialized:
# might not be a git/dvc repo at all
# used in `params/metrics/plots/live` targets
# used in `params/metrics/plots` targets
return SCM(self.root_dir, no_scm=True)
raise
+6 -3
View File
@@ -56,10 +56,13 @@ def commit(
]
for stage_info in stages_info:
stage = stage_info.stage
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
if force:
stage.save(allow_missing=allow_missing)
else:
changes = stage.changed_entries()
if any(changes):
prompt_to_commit(stage, changes, force=force)
stage.save(allow_missing=allow_missing)
stage.commit(
filter_info=stage_info.filter_info, allow_missing=allow_missing
)
+10 -8
View File
@@ -248,11 +248,6 @@ def init(
except MissingParamsFile:
pass
models = context.get("models")
live_path = context.pop("live", None)
live_metrics = f"{live_path}.json" if live_path else None
live_plots = os.path.join(live_path, "scalars") if live_path else None
if type == "checkpoint":
outs_key = "checkpoints"
metrics_key = "metrics_persist_no_cache"
@@ -262,6 +257,13 @@ def init(
metrics_key = "metrics_no_cache"
plots_key = "plots_no_cache"
models = [context.get("models")]
metrics = [context.get("metrics")]
plots = [context.get("plots")]
if live_path := context.pop("live", None):
metrics.append(os.path.join(live_path, "metrics.json"))
plots.append(os.path.join(live_path, "plots"))
stage = repo.stage.create(
name=name,
cmd=context["cmd"],
@@ -269,9 +271,9 @@ def init(
params=[{params: None}] if params else None,
force=force,
**{
outs_key: compact([models]),
metrics_key: compact([context.get("metrics"), live_metrics]),
plots_key: compact([context.get("plots"), live_plots]),
outs_key: compact(models),
metrics_key: compact(metrics),
plots_key: compact(plots),
},
)
+3 -10
View File
@@ -1,6 +1,5 @@
import logging
import os
import sys
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import (
@@ -269,6 +268,7 @@ class BaseStashQueue(ABC):
finish any active experiments before shutting down.
"""
@abstractmethod
def logs(
self,
rev: str,
@@ -525,16 +525,9 @@ class BaseStashQueue(ABC):
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
logger.debug("Using experiment params '%s'", params)
from dvc.utils.hydra import apply_overrides, compose_and_dump
try:
from dvc.utils.hydra import apply_overrides, compose_and_dump
except ValueError:
if sys.version_info >= (3, 11):
raise DvcException(
"--set-param is not supported in Python >= 3.11"
)
raise
logger.debug("Using experiment params '%s'", params)
hydra_config = self.repo.config.get("hydra", {})
hydra_enabled = hydra_config.get("enabled", False)
+13 -13
View File
@@ -28,14 +28,25 @@ def run(
Returns a dict mapping new experiment SHAs to the results
of `repro` for that experiment.
"""
from dvc.utils.hydra import to_hydra_overrides
if run_all:
entries = list(repo.experiments.celery_queue.iter_queued())
return repo.experiments.reproduce_celery(entries, jobs=jobs)
hydra_sweep = None
if params:
from dvc.utils.hydra import to_hydra_overrides
path_overrides = to_path_overrides(params)
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
else:
path_overrides = {}
@@ -45,17 +56,6 @@ def run(
# Force `_update_params` even if `--set-param` was not used
path_overrides[hydra_output_file] = []
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
if not queue:
return repo.experiments.reproduce_one(
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
+9 -12
View File
@@ -242,15 +242,8 @@ class Index:
workspace, key = out.index_key
data_index = by_workspace[workspace]
if out.files:
out.obj = out.get_obj()
entry = out.get_entry()
entry.key = key
if out.stage.is_import and not out.stage.is_repo_import:
entry.fs = out.stage.deps[0].fs
entry.path = out.stage.deps[0].fs_path
entry.meta = out.stage.deps[0].meta
data_index.add(entry)
return dict(by_workspace)
@@ -418,14 +411,18 @@ class IndexView:
def key_filter(workspace: str, key: "DataIndexKey"):
try:
return key in self._data_prefixes[workspace]
return key in self._data_prefixes[workspace] or any(
key[: len(prefix)] == prefix
for prefix in self._data_prefixes[workspace]
)
except KeyError:
return False
return {
workspace: view(index, partial(key_filter, workspace))
for workspace, index in self._index.data.items()
}
data = {}
for workspace, data_index in self._index.data.items():
data_index.load()
data[workspace] = view(data_index, partial(key_filter, workspace))
return data
if __name__ == "__main__":
-19
View File
@@ -1,19 +0,0 @@
import logging
from typing import TYPE_CHECKING, Optional
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from dvc.output import Output
def summary_fs_path(out: "Output") -> Optional[str]:
from dvc.output import Output
assert out.live
has_summary = True
if isinstance(out.live, dict):
has_summary = out.live.get(Output.PARAM_LIVE_SUMMARY, True)
if has_summary:
return out.fs.path.with_suffix(out.fs_path, ".json")
return None
+1 -6
View File
@@ -8,7 +8,6 @@ from dvc.fs.dvc import DVCFileSystem
from dvc.output import Output
from dvc.repo import locked
from dvc.repo.collect import StrPaths, collect
from dvc.repo.live import summary_fs_path
from dvc.scm import NoSCMError
from dvc.utils import error_handler, errored_revisions, onerror_collect
from dvc.utils.collections import ensure_list
@@ -18,7 +17,7 @@ logger = logging.getLogger(__name__)
def _is_metric(out: Output) -> bool:
return bool(out.metric) or bool(out.live)
return bool(out.metric)
def _to_fs_paths(metrics: List[Output]) -> StrPaths:
@@ -26,10 +25,6 @@ def _to_fs_paths(metrics: List[Output]) -> StrPaths:
for out in metrics:
if out.metric:
result.append(out.repo.dvcfs.from_os_path(out.fs_path))
elif out.live:
fs_path = summary_fs_path(out)
if fs_path:
result.append(out.repo.dvcfs.from_os_path(fs_path))
return result
+13 -6
View File
@@ -14,6 +14,7 @@ from typing import (
List,
Optional,
Set,
Union,
)
import dpath.options
@@ -265,7 +266,7 @@ class Plots:
def _is_plot(out: "Output") -> bool:
return bool(out.plot) or bool(out.live)
return bool(out.plot)
def _resolve_data_sources(plots_data: Dict):
@@ -444,16 +445,22 @@ def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
if isinstance(dvcfile, PipelineFile):
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
dvcfile_defs = dvcfile.load().get("plots", {})
dvcfile_defs_dict: Dict[str, Union[Dict, None]] = {}
if isinstance(dvcfile_defs, list):
dvcfile_defs = {
k: v for elem in dvcfile_defs for k, v in elem.items()
}
for elem in dvcfile_defs:
if isinstance(elem, str):
dvcfile_defs_dict[elem] = None
else:
k, v = list(elem.items())[0]
dvcfile_defs_dict[k] = v
else:
dvcfile_defs_dict = dvcfile_defs
resolved = _resolve_definitions(
repo.dvcfs,
targets,
props,
dvcfile_path,
dvcfile_defs,
dvcfile_defs_dict,
onerror=onerror,
)
dpath.util.merge(
@@ -555,7 +562,7 @@ def parse(fs, path, props=None, **kwargs):
def _plot_props(out: "Output") -> Dict:
from dvc.schema import PLOT_PROPS
if not (out.plot or out.live):
if not (out.plot):
raise NotAPlotError(out)
if isinstance(out.plot, list):
raise DvcException("Multiple plots per data file not supported.")
-6
View File
@@ -72,12 +72,6 @@ def _get_stage_files(stage: "Stage") -> typing.Iterator[str]:
for out in stage.outs:
if not out.use_scm_ignore and out.is_in_repo:
yield out.fs_path
if out.live:
from dvc.repo.live import summary_fs_path
summary = summary_fs_path(out)
if summary:
yield summary
def _track_stage(stage: "Stage") -> None:
+1 -9
View File
@@ -67,13 +67,6 @@ PLOT_PROPS = {
PLOT_PROPS_SCHEMA = {**OUT_PSTAGE_DETAILED_SCHEMA[str], **PLOT_PROPS}
PLOT_PSTAGE_SCHEMA = {str: Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])}
LIVE_PROPS = {
Output.PARAM_LIVE_SUMMARY: bool,
Output.PARAM_LIVE_HTML: bool,
}
LIVE_PROPS_SCHEMA = {**PLOT_PROPS_SCHEMA, **LIVE_PROPS}
LIVE_PSTAGE_SCHEMA = {str: LIVE_PROPS_SCHEMA}
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
VARS_SCHEMA = [str, dict]
@@ -93,7 +86,6 @@ STAGE_DEFINITION = {
Any(str, OUT_PSTAGE_DETAILED_SCHEMA)
],
Optional(StageParams.PARAM_PLOTS): [Any(str, PLOT_PSTAGE_SCHEMA)],
Optional(StageParams.PARAM_LIVE): Any(str, LIVE_PSTAGE_SCHEMA),
}
@@ -127,7 +119,7 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
}
MULTI_STAGE_SCHEMA = {
PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]),
PLOTS: Any(SINGLE_PLOT_SCHEMA, [Any(str, SINGLE_PLOT_SCHEMA)]),
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
VARS_KWD: VARS_SCHEMA,
}
+2 -17
View File
@@ -280,22 +280,7 @@ class Stage(params.StageParams):
def _read_env(self, out, checkpoint_func=None) -> Env:
env: Env = {}
if out.live:
from dvc.env import DVCLIVE_HTML, DVCLIVE_PATH, DVCLIVE_SUMMARY
from dvc.output import Output
from dvc.schema import LIVE_PROPS
env[DVCLIVE_PATH] = relpath(out.fs_path, self.wdir)
if isinstance(out.live, dict):
config = project(out.live, LIVE_PROPS)
env[DVCLIVE_SUMMARY] = str(
int(config.get(Output.PARAM_LIVE_SUMMARY, True))
)
env[DVCLIVE_HTML] = str(
int(config.get(Output.PARAM_LIVE_HTML, True))
)
elif out.checkpoint and checkpoint_func:
if out.checkpoint and checkpoint_func:
from dvc.env import DVC_CHECKPOINT
env.update({DVC_CHECKPOINT: "1"})
@@ -379,7 +364,7 @@ class Stage(params.StageParams):
def remove_outs(self, ignore_remove=False, force=False):
"""Used mainly for `dvc remove --outs` and :func:`Stage.reproduce`."""
for out in self.outs:
if (out.persist or out.checkpoint or out.live) and not force:
if (out.persist or out.checkpoint) and not force:
out.unprotect()
continue
-1
View File
@@ -97,7 +97,6 @@ class StageLoader(Mapping):
stage.PARAM_OUTS,
stage.PARAM_METRICS,
stage.PARAM_PLOTS,
stage.PARAM_LIVE,
],
)
stage.outs = lcat(
-4
View File
@@ -20,10 +20,6 @@ class CheckpointKilledError(StageCmdFailedError):
pass
class LiveKilledError(StageCmdFailedError):
pass
@dataclass
class MonitorTask:
stage: "Stage"
-1
View File
@@ -12,4 +12,3 @@ class StageParams:
PARAM_METRICS = "metrics"
PARAM_PLOTS = "plots"
PARAM_DESC = "desc"
PARAM_LIVE = "live"
+3 -10
View File
@@ -52,8 +52,6 @@ def _get_flags(out):
# `out.plot` is in the same order as is in the file when read
# and, should be dumped as-is without any sorting
yield from out.plot.items()
if out.live and isinstance(out.live, dict):
yield from out.live.items()
if out.remote:
yield PARAM_REMOTE, out.remote
@@ -65,19 +63,15 @@ def _serialize_out(out):
@no_type_check
def _serialize_outs(outputs: List[Output]):
outs, metrics, plots, live = [], [], [], None
outs, metrics, plots = [], [], []
for out in sort_by_path(outputs):
bucket = outs
if out.plot:
bucket = plots
elif out.metric:
bucket = metrics
elif out.live:
assert live is None
live = _serialize_out(out)
continue
bucket.append(_serialize_out(out))
return outs, metrics, plots, live
return outs, metrics, plots
def _serialize_params_keys(params):
@@ -126,7 +120,7 @@ def to_pipeline_file(stage: "PipelineStage"):
deps = sorted(d.def_path for d in deps)
params = _serialize_params_keys(params)
outs, metrics, plots, live = _serialize_outs(stage.outs)
outs, metrics, plots = _serialize_outs(stage.outs)
cmd = stage.cmd
assert cmd, (
@@ -142,7 +136,6 @@ def to_pipeline_file(stage: "PipelineStage"):
(stage.PARAM_OUTS, outs),
(stage.PARAM_METRICS, metrics),
(stage.PARAM_PLOTS, plots),
(stage.PARAM_LIVE, live),
(stage.PARAM_FROZEN, stage.frozen),
(stage.PARAM_ALWAYS_CHANGED, stage.always_changed),
(stage.PARAM_META, stage.meta),
+1 -43
View File
@@ -2,7 +2,7 @@ import os
import pathlib
from typing import TYPE_CHECKING, Union
from funcy import concat, first, lsplit, rpartial, without
from funcy import concat, first, lsplit, rpartial
from dvc.annotations import ANNOTATION_FIELDS
from dvc.exceptions import InvalidArgumentError
@@ -66,8 +66,6 @@ def fill_stage_outputs(stage, **kwargs):
stage.outs = []
stage.outs += _load_live_output(stage, **kwargs)
for key in keys:
stage.outs += loads_from(
stage,
@@ -80,34 +78,6 @@ def fill_stage_outputs(stage, **kwargs):
)
def _load_live_output(
stage,
live=None,
live_no_cache=None,
live_summary=False,
live_html=False,
**kwargs,
):
from dvc.output import Output, loads_from
outs = []
if live or live_no_cache:
assert bool(live) != bool(live_no_cache)
path = live or live_no_cache
outs += loads_from(
stage,
[path],
use_cache=not bool(live_no_cache),
live={
Output.PARAM_LIVE_SUMMARY: live_summary,
Output.PARAM_LIVE_HTML: live_html,
},
)
return outs
def fill_stage_dependencies(
stage, deps=None, erepo=None, params=None, fs_config=None
):
@@ -274,7 +244,6 @@ def prepare_file_path(kwargs):
kwargs.get("outs_persist", []),
kwargs.get("outs_persist_no_cache", []),
kwargs.get("checkpoints", []),
without([kwargs.get("live", None)], None),
)
)
@@ -332,15 +301,4 @@ def validate_kwargs(single_stage: bool = False, fname: str = None, **kwargs):
if single_stage:
kwargs.pop("name", None)
if kwargs.get("live") and kwargs.get("live_no_cache"):
raise InvalidArgumentError(
"cannot specify both `--live` and `--live-no-cache`"
)
kwargs.update(
{
"live_summary": not kwargs.pop("live_no_summary", False),
"live_html": not kwargs.pop("live_no_html", False),
}
)
return kwargs
+1 -1
View File
@@ -65,7 +65,7 @@ class Cloud(ABC):
self._gen(struct)
return struct.keys()
def close(self):
def close(self): # noqa: B027
pass
@staticmethod
+37 -8
View File
@@ -1,15 +1,24 @@
import sys
from pathlib import Path
from typing import TYPE_CHECKING, List
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from omegaconf import OmegaConf
try:
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from omegaconf import OmegaConf
from dvc.exceptions import InvalidArgumentError
hydra_compatible = True
except ValueError:
if sys.version_info >= (3, 11):
hydra_compatible = False
else:
raise
from dvc.exceptions import DvcException, InvalidArgumentError
from .collections import merge_dicts, remove_missing_keys, to_omegaconf
from .serialize import DUMPERS, MODIFIERS
@@ -37,6 +46,11 @@ def compose_and_dump(
.. _Hydra Override:
https://hydra.cc/docs/advanced/override_grammar/basic/
"""
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
with initialize_config_dir(config_dir, version_base=None):
cfg = compose(config_name=config_name, overrides=overrides)
@@ -53,6 +67,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
.. _Hydra Override:
https://hydra.cc/docs/next/advanced/override_grammar/basic/
"""
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
suffix = Path(path).suffix.lower()
hydra_errors = (ConfigCompositionException, OverrideParseException)
@@ -78,6 +97,11 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
def to_hydra_overrides(path_overrides):
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
parser = OverridesParser.create()
return parser.parse_overrides(overrides=path_overrides)
@@ -89,6 +113,11 @@ def dict_product(dicts):
def get_hydra_sweeps(path_overrides):
if not hydra_compatible:
raise DvcException(
"hydra functionality is not supported in Python >= 3.11"
)
path_sweeps = {}
for path, overrides in path_overrides.items():
overrides = to_hydra_overrides(overrides)
+9 -2
View File
@@ -1,10 +1,17 @@
"""Common utilities for serialize."""
import os
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Callable, ContextManager, Dict, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
ContextManager,
Dict,
Protocol,
Union,
)
from funcy import reraise
from typing_extensions import Protocol
from dvc.exceptions import DvcException
+1
View File
@@ -4,3 +4,4 @@ try:
from ._dvc_version import version_tuple # noqa: F401
except ImportError:
__version__ = "UNKNOWN"
version_tuple = (0, 0, __version__) # type: ignore[assignment]
+159 -2
View File
@@ -1,7 +1,140 @@
[build-system]
requires = ["setuptools>=48", "setuptools_scm[toml]>=7"]
requires = ["setuptools>=61", "setuptools_scm[toml]>=7"]
build-backend = "setuptools.build_meta"
[project]
name = "dvc"
description = "Git for data scientists - manage your code and data together"
readme = "README.rst"
keywords = [
"data-science",
"data-version-control",
"machine-learning",
"git",
"developer-tools",
"reproducibility",
"collaboration",
"ai",
]
license = { text = "Apache License 2.0" }
authors = [{ name = "Dmitry Petrov", email = "[email protected]" }]
maintainers = [{ name = "Iterative", email = "[email protected]" }]
requires-python = ">=3.8"
dependencies = [
"colorama>=0.3.9",
"configobj>=5.0.6",
"voluptuous>=0.11.7",
"requests>=2.22.0",
# See https://github.com/bdcht/grandalf/issues/26
"grandalf==0.6",
"distro>=1.3.0",
"appdirs>=1.4.3",
"ruamel.yaml>=0.17.11",
"tomlkit>=0.11.1",
"funcy>=1.14",
"pathspec>=0.9.0,<0.10.0",
"shortuuid>=0.5.0",
"tqdm>=4.63.1,<5",
"packaging>=19.0",
"zc.lockfile>=1.2.1",
"flufl.lock>=5",
"networkx>=2.5",
"psutil>=5.8.0",
"pydot>=1.2.4",
"importlib-resources>=5.2.2; python_version < '3.9'",
"flatten_dict>=0.4.1,<1",
"tabulate>=0.8.7",
"pygtrie>=2.3.2",
"dpath>=2.0.2,<3",
"shtab>=1.3.4,<2",
"rich>=10.13.0",
"pyparsing>=2.4.7",
"typing-extensions>=3.7.4",
"scmrepo==0.1.3",
"dvc-render==0.0.12",
"dvc-task==0.1.4",
"dvclive>=1.0",
"dvc-data==0.25.3",
"dvc-http==2.27.2",
"hydra-core>=1.1.0",
"iterative-telemetry==0.0.5",
]
dynamic = ["version"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
[project.optional-dependencies]
azure = ["dvc-azure==2.20.4"]
gdrive = ["dvc-gdrive==2.19.0"]
gs = ["dvc-gs==2.19.1"]
hdfs = ["dvc-hdfs==2.19.0; python_version < '3.11'"]
oss = ["dvc-oss==2.19.0"]
s3 = ["dvc-s3==2.21.0"]
ssh = ["dvc-ssh==2.20.0"]
ssh_gssapi = ["dvc-ssh[gssapi]==2.20.0"]
webdav = ["dvc-webdav==2.19.0"]
webhdfs = ["dvc-webhdfs==2.19.0"]
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19.0"]
terraform = ["tpi[ssh]>=2.1.0"]
testing = ["pytest-test-utils==0.0.8"]
tests = [
"tpi[ssh]>=2.1.0",
"dvc-ssh==2.20.0",
"pytest-test-utils==0.0.8",
# Test requirements
"pytest==7.2.0",
"pytest-cov==4.0.0",
"pytest-xdist==3.0.2",
"pytest-mock==3.10.0",
"pytest-lazy-fixture==0.6.3",
# https://github.com/docker/docker-py/issues/2902
"pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'",
"flaky==3.7.0",
"pytest-timeout==2.1.0",
"filelock==3.8.0",
# optional dependencies
"dvclive>=1.0",
"pywin32>=225; sys_platform == 'win32'",
"beautifulsoup4==4.11.1",
# pylint requirements
"pylint==2.15.5",
# we use this to suppress pytest-related false positives in our tests.
"pylint-pytest==1.1.2",
# we use this to suppress some messages in tests, eg: foo/bar naming,
# and, protected method calls in our tests
"pylint-plugin-utils==0.7",
# type-checking
"mypy==0.982",
"types-requests>=2.27.15",
"types-tabulate>=0.8.6",
"types-toml>=0.10.4",
]
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
dev = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs,tests]"]
[project.urls]
Documentation = "https://dvc.org/doc"
"Bug Tracker" = "https://github.com/iterative/dvc/issues"
Source = "https://github.com/iterative/dvc"
[project.scripts]
dvc = "dvc.cli:main"
[project.entry-points."fsspec.specs"]
dvc = "dvc.api:DVCFileSystem"
[tool.setuptools]
license-files = ["LICENSE"]
[tool.setuptools.packages]
find = {namespaces = false, exclude=["tests", "tests.*"]}
[tool.setuptools_scm]
write_to = "dvc/_dvc_version.py"
@@ -30,13 +163,34 @@ line_length = 79
[tool.pytest.ini_options]
log_level = "debug"
addopts = "-ra -p no:hydra_pytest"
addopts = "-ra -p no:hydra_pytest --cov-config pyproject.toml"
markers = [
"needs_internet: Might need network access for the tests",
"vscode: Tests verifying contract between DVC and VSCode plugin",
"studio: Tests verifying contract between DVC and Studio",
]
xfail_strict = true
testpaths = ["tests"]
filterwarnings = ["error::pytest_mock.PytestMockWarning"]
[tool.coverage.run]
branch = true
source = ["dvc", "tests"]
[tool.coverage.paths]
source = ["dvc"]
[tool.coverage.report]
show_missing = true
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"if typing.TYPE_CHECKING:",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"raise AssertionError",
"@overload",
]
[tool.mypy]
# Error output
@@ -81,3 +235,6 @@ signature-mutators = ["funcy.decorators.decorator"]
[tool.pylint.variables]
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs"
[tool.codespell]
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
+1 -1
View File
@@ -1,3 +1,3 @@
# NOTE: using strict version due to
# https://github.com/iterative/dvc/issues/7949
PyInstaller==5.0
PyInstaller==5.6.2
+2 -18
View File
@@ -1,7 +1,5 @@
import argparse
import importlib.util
import pathlib
import textwrap
from subprocess import STDOUT, check_call
path = pathlib.Path(__file__).parent.absolute()
@@ -25,22 +23,8 @@ args = parser.parse_args()
(dvc / "utils" / "build.py").write_text(f'PKG = "{args.pkg}"')
# Autogenerate version, similar to what we do in setup.py
spec = importlib.util.spec_from_file_location(
"dvc.version", dvc / "version.py"
)
dvc_version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dvc_version)
version = dvc_version.__version__
(dvc / "version.py").write_text(
textwrap.dedent(
f"""\
# AUTOGENERATED at build time by scripts/build.py
__version__ = "{version}"
"""
)
)
if not (dvc / "_dvc_version.py").exists():
raise Exception("no version info found")
check_call(
["python", "build.py"],
+3 -2
View File
@@ -10,5 +10,6 @@ fi
echo 'PKG = "pip"' >dvc/utils/build.py
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
python -m pip install build twine
python -m build
twine check dist/*
+5 -5
View File
@@ -24,8 +24,8 @@ function addSymLinkPermissions($accountToAdd){
Write-Host "Account SID: $($sidstr)" -ForegroundColor DarkCyan
$tmp = [System.IO.Path]::GetTempFileName()
Write-Host "Export current Local Security Policy" -ForegroundColor DarkCyan
secedit.exe /export /cfg "$($tmp)"
$c = Get-Content -Path $tmp
secedit.exe /export /cfg "$($tmp)"
$c = Get-Content -Path $tmp
$currentSetting = ""
foreach($s in $c) {
if( $s -like "SECreateSymbolicLinkPrivilege*") {
@@ -35,7 +35,7 @@ function addSymLinkPermissions($accountToAdd){
}
if( $currentSetting -notlike "*$($sidstr)*" ) {
Write-Host "Need to add permissions to SymLink" -ForegroundColor Yellow
Write-Host "Modify Setting ""Create SymLink""" -ForegroundColor DarkCyan
if( [string]::IsNullOrEmpty($currentSetting) ) {
@@ -58,8 +58,8 @@ SECreateSymbolicLinkPrivilege = $($currentSetting)
$outfile | Set-Content -Path $tmp2 -Encoding Unicode -Force
Push-Location (Split-Path $tmp2)
try {
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
} finally {
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
} finally {
Pop-Location
}
} else {
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
set -e
if [ ! -d "dvc" ]; then
echo "Please run this script from repository root"
exit 1
fi
rm -rf dist/
pip install twine
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
twine upload dist/*
-166
View File
@@ -1,166 +0,0 @@
[metadata]
description = Git for data scientists - manage your code and data together
name = dvc
long_description = file: README.rst
long_description_content_type = text/x-rst
license = Apache License 2.0
license_file = LICENSE
url = http://dvc.org
project_urls =
Documentation = https://dvc.org/doc
Source = https://github.com/iterative/dvc
download_url = https://github.com/iterative/dvc
author = Dmitry Petrov
author_email = [email protected]
maintainer = Iterative
maintainer_email = [email protected]
keywords = data-science, data-version-control, machine-learning, git
developer-tools, reproducibility, collaboration, ai
classifiers =
Development Status :: 4 - Beta
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options]
setup_requires =
setuptools>=48
setuptools_scm[toml]>=7
python_requires = >=3.8
zip_safe = False
packages = find:
include_package_data = True
install_requires =
colorama>=0.3.9
configobj>=5.0.6
voluptuous>=0.11.7
requests>=2.22.0
# See https://github.com/bdcht/grandalf/issues/26
grandalf==0.6
distro>=1.3.0
appdirs>=1.4.3
ruamel.yaml>=0.17.11
tomlkit>=0.11.1
funcy>=1.14
pathspec>=0.9.0,<0.10.0
shortuuid>=0.5.0
tqdm>=4.63.1,<5
packaging>=19.0
zc.lockfile>=1.2.1
flufl.lock>=5
networkx>=2.5
psutil>=5.8.0
pydot>=1.2.4
importlib-resources>=5.2.2; python_version < '3.9'
flatten_dict>=0.4.1,<1
tabulate>=0.8.7
pygtrie>=2.3.2
dpath>=2.0.2,<3
shtab>=1.3.4,<2
rich>=10.13.0
pyparsing>=2.4.7
typing-extensions>=3.7.4
scmrepo==0.1.2
dvc-render==0.0.12
dvc-task==0.1.4
dvclive>=0.10.0
dvc-data==0.25.1
dvc-http==2.27.2
hydra-core>=1.1.0
iterative-telemetry==0.0.5
[options.extras_require]
all =
%(azure)s
%(gdrive)s
%(gs)s
%(hdfs)s
%(oss)s
%(s3)s
%(ssh)s
%(webdav)s
%(webhdfs)s
dev =
%(all)s
%(tests)s
dvc-data[cli]
# Extra dependencies for remote integrations
azure = dvc-azure==2.20.4
gdrive = dvc-gdrive==2.19.0
gs = dvc-gs==2.19.1
hdfs =
dvc-hdfs==2.19.0; python_version < '3.11'
oss = dvc-oss==2.19.0
s3 = dvc-s3==2.20.1
ssh = dvc-ssh==2.20.0
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for Linux, so it will fail to compile if user doesn't have all the
# requirements, including kerberos itself.
ssh_gssapi = dvc-ssh[gssapi]==2.20.0
webdav = dvc-webdav==2.19.0
webhdfs = dvc-webhdfs==2.19.0
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
terraform = tpi[ssh]>=2.1.0
testing =
pytest-test-utils==0.0.8
tests =
%(terraform)s
dvc-ssh==2.20.0
# Test requirements
pytest==7.1.3
pytest-cov==4.0.0
pytest-xdist==2.5.0
pytest-mock==3.10.0
pytest-lazy-fixture==0.6.3
%(testing)s
# https://github.com/docker/docker-py/issues/2902
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
flaky==3.7.0
pytest-timeout==2.1.0
filelock==3.8.0
# pylint requirements
pylint==2.15.4
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.1.2
# we use this to suppress some messages in tests, eg: foo/bar naming,
# and, protected method calls in our tests
pylint-plugin-utils==0.7
# type-checking
mypy==0.982
types-requests>=2.27.15
types-tabulate>=0.8.6
types-toml>=0.10.4
# optional dependencies
pywin32>=225; sys_platform == 'win32'
dvclive[image]>=0.7.3
beautifulsoup4==4.11.1
[options.packages.find]
exclude =
dvc.testing
dvc.testing.*
tests
tests.*
[options.entry_points]
console_scripts =
dvc = dvc.cli:main
fsspec.specs =
dvc = dvc.api:DVCFileSystem
[flake8]
ignore=
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
P1, # unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
max_line_length = 79
max-complexity = 15
select = B,C,E,F,W,T4,B902,T,P
show_source = true
count = true
+1 -1
View File
@@ -1,3 +1,3 @@
from setuptools import setup
setup()
setup(name="dvc")
-28
View File
@@ -1,41 +1,13 @@
import os
import sys
# Increasing fd ulimit for tests
if os.name == "nt":
import subprocess
try:
import win32file # pylint: disable=import-error
except ImportError:
pass
else:
win32file._setmaxstdio(4096)
# Workaround for two bugs:
#
# 1) gitpython-developers/GitPython#546
# GitPython leaves git cat-file --batch/--batch-check processes that are
# not cleaned up correctly, so Popen._active list has their defunct
# process handles, that it is not able to cleanup because of bug 2)
#
# 2) https://bugs.python.org/issue37380
# subprocess.Popen._internal_poll on windows is getting
#
# OSError: [WinError 6] The handle is invalid
#
# exception, which it doesn't ignore and so Popen is not able to cleanup
# old processes and that prevents it from creating any new processes at
# all, which results in our tests failing whenever they try to use Popen.
# This patch was released in 3.9.0 and backported to some earlier
# versions.
if sys.version_info < (3, 9, 0):
def noop():
pass
subprocess._cleanup = noop
subprocess._active = None
else:
import resource # pylint: disable=import-error
+4 -18
View File
@@ -1,20 +1,6 @@
# Needs investigation, pylint was running tests
# Could be `pylint_pytest` plugin, skipping check for this file for now
# pylint: skip-file
if __name__ == "__main__":
import sys
import os
import sys
from subprocess import check_call
import pytest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(REPO_ROOT)
os.putenv(
"PATH", ":".join([os.path.join(REPO_ROOT, "bin"), os.getenv("PATH")])
)
os.putenv("DVC_HOME", REPO_ROOT)
opts = "-v -n=auto --dist loadscope --cov=dvc --durations=50"
params = " ".join(sys.argv[1:])
cmd = f"{sys.executable} -m pytest {opts} {params}"
check_call(cmd, shell=True)
sys.exit(pytest.main(["-v", "-n=auto", *sys.argv[1:]]))
-179
View File
@@ -1,179 +0,0 @@
import logging
import os
import tempfile
import warnings
from unittest import TestCase
import pytest
import shortuuid
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
logger = logging.getLogger("dvc")
class TestDirFixture:
DATA_DIR = "data_dir"
DATA_SUB_DIR = os.path.join(DATA_DIR, "data_sub_dir")
DATA = os.path.join(DATA_DIR, "data")
DATA_SUB = os.path.join(DATA_SUB_DIR, "data_sub")
DATA_CONTENTS = DATA
DATA_SUB_CONTENTS = DATA_SUB
FOO = "foo"
FOO_CONTENTS = FOO
BAR = "bar"
# NOTE: len(FOO_CONTENTS) must be != len(BAR_CONTENTS)
#
# Our state database relies on file mtime and file size to determine
# that a file has changed. Usually, mtime is enough by itself but on
# some filesystems like APFS on macOS mtime resolution is so bad,
# that our tests can overwrite a file in that time window without dvc
# being able to detect that, thus causing tests to fail. Usually,
# in tests, we replace foo with bar, so we need to make sure that when we
# modify a file in our tests, its content length changes.
BAR_CONTENTS = BAR + "r"
CODE = "code.py"
CODE_CONTENTS = (
"import sys\nimport shutil\n"
"shutil.copyfile(sys.argv[1], sys.argv[2])"
)
UNICODE = "тест"
UNICODE_CONTENTS = "проверка"
def __init__(self):
root_dir = self.mkdtemp()
self._root_dir = os.path.realpath(root_dir)
self._saved_dir = None
@property
def root_dir(self):
return self._root_dir
def _pushd(self, d):
if not self._saved_dir:
self._saved_dir = os.path.realpath(os.curdir)
os.chdir(d)
def _popd(self):
os.chdir(self._saved_dir)
self._saved_dir = None
def create(self, name, contents):
dname = os.path.dirname(name)
if len(dname) > 0 and not os.path.isdir(dname):
os.makedirs(dname)
with open(name, "a", encoding="utf-8") as f:
f.write(
contents
if isinstance(contents, str)
else contents.decode("utf-8")
)
@staticmethod
def mkdtemp(base_directory=None):
prefix = f"dvc-test.{os.getpid()}."
suffix = f".{shortuuid.uuid()}"
return tempfile.mkdtemp(
prefix=prefix, suffix=suffix, dir=base_directory
)
def setUp(self):
self._pushd(self._root_dir)
self.create(self.FOO, self.FOO_CONTENTS)
self.create(self.BAR, self.BAR_CONTENTS)
self.create(self.CODE, self.CODE_CONTENTS)
os.mkdir(self.DATA_DIR)
os.mkdir(self.DATA_SUB_DIR)
self.create(self.DATA, self.DATA_CONTENTS)
self.create(self.DATA_SUB, self.DATA_SUB_CONTENTS)
self.create(self.UNICODE, self.UNICODE_CONTENTS)
def tearDown(self):
self._popd()
try:
remove(self._root_dir)
except OSError as exc:
# pylint: disable=no-member
# We ignore this under Windows with a warning because it happened
# to be really hard to trace all not properly closed files.
#
# Best guess so far is that gitpython is the culprit:
# it opens files and uses __del__ to close them, which can happen
# late in current pythons. TestGitFixture and TestDvcFixture try
# to close that and it works on most of the tests, but not all.
# Repos and thus git repos are created all over the dvc ;)
if os.name == "nt" and exc.winerror == 32:
warnings.warn("Failed to remove test dir: " + str(exc))
else:
raise
class TestGitFixture(TestDirFixture):
N_RETRIES = 5
def setUp(self):
from scmrepo.git import Git
super().setUp()
self.git = Git.init(".")
self.git.add_commit(self.CODE, message="add code")
def tearDown(self):
self.git.close()
super().tearDown()
class TestDvcFixture(TestDirFixture):
def setUp(self):
super().setUp()
self.dvc = DvcRepo.init(self.root_dir, no_scm=True)
def tearDown(self):
self.dvc.close()
super().tearDown()
class TestDvcGitFixture(TestGitFixture):
def setUp(self):
super().setUp()
self.dvc = DvcRepo.init(self.root_dir)
self.dvc.scm.commit("init dvc")
def tearDown(self):
self.dvc.close()
super().tearDown()
# NOTE: Inheritance order in the classes below is important.
class TestDir(TestDirFixture, TestCase):
def __init__(self, methodName):
TestDirFixture.__init__(self)
TestCase.__init__(self, methodName)
class TestDvc(TestDvcFixture, TestCase):
def __init__(self, methodName):
TestDvcFixture.__init__(self)
TestCase.__init__(self, methodName)
self._caplog = None
self._capsys = None
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog, capsys):
self._caplog = caplog
self._capsys = capsys
class TestDvcGit(TestDvcGitFixture, TestCase):
def __init__(self, methodName):
TestDvcGitFixture.__init__(self)
TestCase.__init__(self, methodName)
self._caplog = None
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self._caplog = caplog
+2 -5
View File
@@ -9,7 +9,7 @@ from dvc.testing.fixtures import * # noqa, pylint: disable=wildcard-import
from .dir_helpers import * # noqa, pylint: disable=wildcard-import
from .remotes import * # noqa, pylint: disable=wildcard-import
from .utils.scriptify import scriptify
from .scripts import * # noqa, pylint: disable=wildcard-import
# Prevent updater and analytics from running their processes
os.environ["DVC_TEST"] = "true"
@@ -172,9 +172,6 @@ def custom_template(tmp_dir, dvc):
return template
scriptify_fixture = pytest.fixture(lambda: scriptify, name="scriptify")
@pytest.fixture(autouse=True)
def mocked_webbrowser_open(mocker):
mocker.patch("webbrowser.open")
@@ -218,7 +215,7 @@ defaultBranch=master
@pytest.fixture
def run_copy_metrics(tmp_dir, run_copy):
def run_copy_metrics(tmp_dir, copy_script):
def run(
file1,
file2,
+2 -29
View File
@@ -47,7 +47,6 @@ from global repo template to creating everything inplace, which:
import os
import pathlib
from textwrap import dedent
import pytest
@@ -67,17 +66,7 @@ disable_other_loggers()
@pytest.fixture
def run_copy(tmp_dir, dvc):
tmp_dir.gen(
"copy.py",
(
"import sys, shutil, os\n"
"shutil.copyfile(sys.argv[1], sys.argv[2]) "
"if os.path.isfile(sys.argv[1]) "
"else shutil.copytree(sys.argv[1], sys.argv[2])"
),
)
def run_copy(tmp_dir, copy_script, dvc):
def run_copy(src, dst, **run_kwargs):
wdir = pathlib.Path(run_kwargs.get("wdir", "."))
wdir = pathlib.Path("../" * len(wdir.parts))
@@ -94,23 +83,7 @@ def run_copy(tmp_dir, dvc):
@pytest.fixture
def run_head(tmp_dir, dvc):
"""Output first line of each file to different file with '-1' appended.
Useful for tracking multiple outputs/dependencies which are not a copy
of each others.
"""
tmp_dir.gen(
{
"head.py": dedent(
"""
import sys
for file in sys.argv[1:]:
with open(file) as f, open(file +"-1","w+") as w:
w.write(f.readline())
"""
)
}
)
def run_head(tmp_dir, head_script, dvc):
script = os.path.abspath(tmp_dir / "head.py")
def run(*args, **run_kwargs):
+6 -7
View File
@@ -13,7 +13,7 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.scm import resolve_rev
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.utils.serialize import PythonFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
from tests.scripts import COPY_SCRIPT
@pytest.mark.parametrize("name", [None, "foo"])
@@ -209,8 +209,7 @@ def test_get_baseline(tmp_dir, scm, dvc, exp_stage):
assert dvc.experiments.get_baseline(f"{CELERY_STASH}@{{1}}") == init_rev
def test_update_py_params(tmp_dir, scm, dvc, test_queue):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_update_py_params(tmp_dir, scm, dvc, test_queue, copy_script):
tmp_dir.gen("params.py", "INT = 1\n")
stage = dvc.run(
cmd="python copy.py params.py metrics.py",
@@ -371,8 +370,7 @@ def test_no_scm(tmp_dir):
getattr(dvc.experiments, cmd)()
def test_untracked(tmp_dir, scm, dvc, caplog, workspace):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_untracked(tmp_dir, scm, dvc, caplog, workspace, copy_script):
tmp_dir.scm_gen("params.yaml", "foo: 1", commit="track params")
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
@@ -600,9 +598,10 @@ def test_fix_exp_head(tmp_dir, scm, tail):
"params, target",
itertools.product(("foo: 1", "foo: 2"), (True, False)),
)
def test_modified_data_dep(tmp_dir, scm, dvc, workspace, params, target):
def test_modified_data_dep(
tmp_dir, scm, dvc, workspace, params, target, copy_script
):
tmp_dir.dvc_gen("data", "data")
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
exp_stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
+4 -10
View File
@@ -1,12 +1,9 @@
import pytest
from funcy import first
from tests.func.test_repro_multistage import COPY_SCRIPT
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 1)])
def test_workspace(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_workspace(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -26,8 +23,7 @@ def test_workspace(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 0), (False, 2)])
def test_all_commits(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_commits(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -55,8 +51,7 @@ def test_all_commits(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
def test_all_branches(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_branches(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
@@ -91,8 +86,7 @@ def test_all_branches(tmp_dir, scm, dvc, queued, expected):
@pytest.mark.parametrize("queued, expected", [(True, 2), (False, 3)])
def test_all_tags(tmp_dir, scm, dvc, queued, expected):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_all_tags(tmp_dir, scm, dvc, queued, expected, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
+8 -6
View File
@@ -345,11 +345,13 @@ def test_init_interactive_live(
"train": {
"cmd": "python script.py",
"deps": ["data", "script.py"],
"metrics": [{"dvclive.json": {"cache": False}}],
"metrics": [
{os.path.join("dvclive", "metrics.json"): {"cache": False}}
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("dvclive", "scalars"): {"cache": False}}
{os.path.join("dvclive", "plots"): {"cache": False}}
],
}
}
@@ -434,12 +436,12 @@ def test_init_with_type_default_and_live_provided(
"cmd": "cmd",
"deps": ["data", "src"],
"metrics": [
{"live.json": {"cache": False}},
{os.path.join("live", "metrics.json"): {"cache": False}},
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("live", "scalars"): {"cache": False}},
{os.path.join("live", "plots"): {"cache": False}},
],
}
}
@@ -477,13 +479,13 @@ def test_init_with_live_and_metrics_plots_provided(
"cmd": "cmd",
"deps": ["data", "src"],
"metrics": [
{"live.json": {"cache": False}},
{os.path.join("live", "metrics.json"): {"cache": False}},
{"metrics.json": {"cache": False}},
],
"outs": ["models"],
"params": [{"params.yaml": None}],
"plots": [
{os.path.join("live", "scalars"): {"cache": False}},
{os.path.join("live", "plots"): {"cache": False}},
{"plots": {"cache": False}},
],
}
+6 -1
View File
@@ -17,7 +17,7 @@ from ..utils.test_hydra import hydra_setup
def test_modify_params(params_repo, dvc, changes, expected):
dvc.experiments.run(params=changes)
# pylint: disable=unspecified-encoding
with open("params.yaml", mode="r") as fobj:
with open("params.yaml") as fobj:
assert fobj.read().strip() == expected
@@ -110,6 +110,11 @@ def test_hydra_compose_and_dump(
["foo=bar,baz"],
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
),
(
False,
[],
[{}],
),
],
)
def test_hydra_sweep(
+7 -10
View File
@@ -20,7 +20,6 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils import relpath
from dvc.utils.fs import makedirs
from dvc.utils.serialize import YAMLFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
LOCK_CONTENTS = {
"read": {
@@ -313,10 +312,10 @@ def test_show_filter(
scm,
dvc,
capsys,
copy_script,
):
capsys.readouterr()
tmp_dir.gen("copy.py", COPY_SCRIPT)
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -679,8 +678,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
)
def test_show_only_changed(tmp_dir, dvc, scm, capsys):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_show_only_changed(tmp_dir, dvc, scm, capsys, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -726,13 +724,14 @@ def test_show_only_changed(tmp_dir, dvc, scm, capsys):
assert "metrics.yaml:bar" in cap.out
def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
def test_show_parallel_coordinates(
tmp_dir, dvc, scm, mocker, capsys, copy_script
):
from dvc.commands.experiments import show
webbroser_open = mocker.patch("webbrowser.open")
show_experiments = mocker.spy(show, "show_experiments")
tmp_dir.gen("copy.py", COPY_SCRIPT)
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -807,8 +806,7 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
@pytest.mark.vscode
def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_show_outs(tmp_dir, dvc, scm, erepo_dir, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
@@ -889,8 +887,7 @@ def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
}
def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_metrics_renaming(tmp_dir, dvc, scm, capsys, copy_script):
params_file = tmp_dir / "params.yaml"
params_data = {
"foo": 1,
+145 -194
View File
@@ -4,8 +4,7 @@ import os
import shutil
import stat
import textwrap
import time
from unittest.mock import call, patch
from unittest.mock import call
import colorama
import pytest
@@ -35,12 +34,11 @@ from dvc.stage.exceptions import (
StagePathNotFoundError,
)
from dvc.testing.workspace_tests import TestAdd
from dvc.utils import LARGE_DIR_SIZE, relpath
from dvc.utils import LARGE_DIR_SIZE
from dvc.utils.fs import path_isin
from dvc.utils.serialize import YAMLFileCorruptedError, load_yaml
from dvc.utils.serialize import YAMLFileCorruptedError
from dvc_data.hashfile.hash import file_md5
from dvc_data.hashfile.hash_info import HashInfo
from tests.basic_env import TestDvc
from tests.utils import get_gitignore_content
@@ -120,49 +118,38 @@ def test_add_directory(tmp_dir, dvc):
assert "\\" not in part
class TestAddDirectoryRecursive(TestDvc):
def test(self):
stages = self.dvc.add(self.DATA_DIR, recursive=True)
self.assertEqual(len(stages), 2)
def test_add_directory_recursive(tmp_dir, dvc):
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
stages = dvc.add("data", recursive=True)
assert len(stages) == 2
class TestAddCmdDirectoryRecursive(TestDvc):
def test(self):
ret = main(["add", "--recursive", self.DATA_DIR])
self.assertEqual(ret, 0)
def test_warn_about_large_directories(self):
warning = (
"You are adding a large directory 'large-dir' recursively."
"\nConsider tracking it as a whole instead with "
"`{cyan}dvc add large-dir{nc}`"
).format(
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
)
os.mkdir("large-dir")
# Create a lot of files
for iteration in range(LARGE_DIR_SIZE + 1):
path = os.path.join("large-dir", str(iteration))
with open(path, "w", encoding="utf-8") as fobj:
fobj.write(path)
assert main(["add", "--recursive", "large-dir"]) == 0
assert warning in self._capsys.readouterr()[1]
def test_add_cmd_directory_recursive(tmp_dir, dvc):
tmp_dir.gen("data", {"file1": "file1", "sub": {"file2": "file2"}})
assert main(["add", "--recursive", "data"]) == 0
class TestAddDirectoryWithForwardSlash(TestDvc):
def test(self):
dname = "directory/"
os.mkdir(dname)
self.create(os.path.join(dname, "file"), "file")
stages = self.dvc.add(dname)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
self.assertEqual(os.path.abspath("directory.dvc"), stage.path)
def test_warn_about_large_directories_recursive_add(tmp_dir, dvc, capsys):
warning = (
"You are adding a large directory 'large-dir' recursively."
"\nConsider tracking it as a whole instead with "
"`{cyan}dvc add large-dir{nc}`"
).format(
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
)
tmp_dir.gen(
"large-dir", {f"{i}": f"{i}" for i in range(LARGE_DIR_SIZE + 1)}
)
assert main(["add", "--recursive", "large-dir"]) == 0
assert warning in capsys.readouterr()[1]
def test_add_directory_with_forward_slash(tmp_dir, dvc):
tmp_dir.gen("directory", {"file": "file"})
(stage,) = dvc.add("directory/")
assert stage.relpath == "directory.dvc"
def test_add_tracked_file(tmp_dir, scm, dvc):
@@ -178,33 +165,23 @@ def test_add_tracked_file(tmp_dir, scm, dvc):
dvc.add(path)
class TestAddDirWithExistingCache(TestDvc):
def test(self):
dname = "a"
fname = os.path.join(dname, "b")
os.mkdir(dname)
shutil.copyfile(self.FOO, fname)
def test_add_dir_with_existing_cache(tmp_dir, dvc):
tmp_dir.gen({"foo": "foo", "dir": {"file": "foo"}})
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
stages = self.dvc.add(dname)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
(stage,) = dvc.add("foo")
assert stage is not None
(stage,) = dvc.add("dir")
assert stage is not None
class TestAddModifiedDir(TestDvc):
def test(self):
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
os.unlink(self.DATA)
def test_add_modified_dir(tmp_dir, dvc):
tmp_dir.gen("data", {"foo": "foo", "sub": {"bar": "bar"}})
(stage,) = dvc.add("data")
assert stage is not None
time.sleep(2)
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
(tmp_dir / "data" / "foo").unlink()
(stage,) = dvc.add("data")
assert stage is not None
def test_add_file_in_dir(tmp_dir, dvc):
@@ -326,57 +303,47 @@ def test_add_external_relpath(tmp_dir, dvc, local_cloud):
assert dvc.status() == {}
class TestAddLocalRemoteFile(TestDvc):
def test(self):
"""
Making sure that 'remote' syntax is handled properly for local outs.
"""
cwd = os.getcwd()
remote = "myremote"
def test_add_local_remote_file(tmp_dir, dvc):
"""
Making sure that 'remote' syntax is handled properly for local outs.
"""
tmp_dir.gen({"foo": "foo", "bar": "bar"})
tmp_dir.add_remote(url=tmp_dir.fs_path, name="myremote")
ret = main(["remote", "add", remote, cwd])
self.assertEqual(ret, 0)
assert main(["add", "remote://myremote/foo"]) == 0
d = (tmp_dir / "foo.dvc").load_yaml()
assert d["outs"][0]["path"] == "remote://myremote/foo"
self.dvc.config.load()
foo = f"remote://{remote}/{self.FOO}"
ret = main(["add", foo])
self.assertEqual(ret, 0)
d = load_yaml("foo.dvc")
self.assertEqual(d["outs"][0]["path"], foo)
bar = os.path.join(cwd, self.BAR)
ret = main(["add", bar])
self.assertEqual(ret, 0)
d = load_yaml("bar.dvc")
self.assertEqual(d["outs"][0]["path"], self.BAR)
assert main(["add", (tmp_dir / "bar").fs_path]) == 0
d = (tmp_dir / "bar.dvc").load_yaml()
assert d["outs"][0]["path"] == "bar"
class TestCmdAdd(TestDvc):
def test(self):
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
def test_cmd_add(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo"])
assert ret == 0
ret = main(["add", "non-existing-file"])
self.assertNotEqual(ret, 0)
ret = main(["add", "non-existing-file"])
assert ret != 0
class TestDoubleAddUnchanged(TestDvc):
def test_file(self):
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
def test_double_add_unchanged_file(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
def test_dir(self):
ret = main(["add", self.DATA_DIR])
self.assertEqual(ret, 0)
ret = main(["add", self.DATA_DIR])
self.assertEqual(ret, 0)
def test_double_add_unchanged_dir(tmp_dir, dvc):
tmp_dir.gen("data", {"foo": "foo"})
ret = main(["add", "data"])
assert ret == 0
ret = main(["add", "data"])
assert ret == 0
def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
@@ -443,19 +410,17 @@ def test_should_update_state_entry_for_directory_after_add(
assert file_md5_counter.mock.call_count == 10
class TestAddCommit(TestDvc):
def test(self):
ret = main(["add", self.FOO, "--no-commit"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.isfile(self.FOO))
self.assertFalse(os.path.exists(self.dvc.odb.local.path))
def test_add_commit(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo", "--no-commit"])
assert ret == 0
assert os.path.isfile("foo")
assert not os.path.exists(dvc.odb.local.path)
ret = main(["commit", self.FOO + ".dvc"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.isfile(self.FOO))
self.assertTrue(
self.dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
)
ret = main(["commit", "foo" + ".dvc"])
assert ret == 0
assert os.path.isfile("foo")
assert dvc.odb.local.exists("acbd18db4cc2f85cedef654fccc4a4d8")
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
@@ -474,80 +439,66 @@ def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
assert counter.mock.call_count == 5
class TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink(TestDvc):
def test(self):
def is_symlink_true_below_dvc_root(path):
if path == os.path.dirname(self.dvc.root_dir):
return True
return False
def test_should_place_stage_in_data_dir_if_repository_below_symlink(
mocker, tmp_dir, dvc
):
def is_symlink_true_below_dvc_root(path):
return path == os.path.dirname(dvc.root_dir)
with patch.object(
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
):
tmp_dir.gen({"data": {"foo": "foo"}})
mocker.patch.object(
system, "is_symlink", side_effect=is_symlink_true_below_dvc_root
)
ret = main(["add", os.path.join("data", "foo")])
assert ret == 0
ret = main(["add", self.DATA])
self.assertEqual(0, ret)
stage_file_path_on_data_below_symlink = (
os.path.basename(self.DATA) + DVC_FILE_SUFFIX
)
self.assertFalse(
os.path.exists(stage_file_path_on_data_below_symlink)
)
stage_file_path = self.DATA + DVC_FILE_SUFFIX
self.assertTrue(os.path.exists(stage_file_path))
assert not (tmp_dir / "foo.dvc").exists()
assert (tmp_dir / "data" / "foo.dvc").exists()
class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc):
def test(self):
ret = main(["add", self.FOO])
assert 0 == ret
def test_should_throw_proper_exception_on_corrupted_stage_file(
caplog, tmp_dir, dvc
):
tmp_dir.gen({"foo": "foo", "bar": " bar"})
assert main(["add", "foo"]) == 0
foo_stage = relpath(self.FOO + DVC_FILE_SUFFIX)
with (tmp_dir / "foo.dvc").open("a+") as f:
f.write("this will break yaml file structure")
# corrupt stage file
with open(foo_stage, "a+", encoding="utf-8") as file:
file.write("this will break yaml file structure")
self._caplog.clear()
ret = main(["add", self.BAR])
assert 1 == ret
expected_error = (
f"unable to read: '{foo_stage}', YAML file structure is corrupted"
)
assert expected_error in self._caplog.text
caplog.clear()
assert main(["add", "bar"]) == 1
expected_error = (
"unable to read: 'foo.dvc', YAML file structure is corrupted"
)
assert expected_error in caplog.text
class TestAddFilename(TestDvc):
def test(self):
ret = main(["add", self.FOO, self.BAR, "--file", "error.dvc"])
self.assertNotEqual(0, ret)
def test_add_filename(tmp_dir, dvc):
tmp_dir.gen({"foo": "foo", "bar": "bar", "data": {"file": "file"}})
ret = main(["add", "foo", "bar", "--file", "error.dvc"])
assert ret != 0
ret = main(["add", "-R", self.DATA_DIR, "--file", "error.dvc"])
self.assertNotEqual(0, ret)
ret = main(["add", "-R", "data", "--file", "error.dvc"])
assert ret != 0
with self.assertRaises(RecursiveAddingWhileUsingFilename):
self.dvc.add(self.DATA_DIR, recursive=True, fname="error.dvc")
with pytest.raises(RecursiveAddingWhileUsingFilename):
dvc.add("data", recursive=True, fname="error.dvc")
ret = main(["add", self.DATA_DIR, "--file", "data_directory.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("data_directory.dvc"))
ret = main(["add", "data", "--file", "data_directory.dvc"])
assert ret == 0
assert (tmp_dir / "data_directory.dvc").exists()
ret = main(["add", self.FOO, "--file", "bar.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
self.assertFalse(os.path.exists("foo.dvc"))
ret = main(["add", "foo", "--file", "bar.dvc"])
assert ret == 0
assert (tmp_dir / "bar.dvc").exists()
assert not (tmp_dir / "foo.dvc").exists()
os.remove("bar.dvc")
(tmp_dir / "bar.dvc").unlink()
ret = main(["add", self.FOO, "--file", "bar.dvc"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
self.assertFalse(os.path.exists("foo.dvc"))
ret = main(["add", "foo", "--file", "bar.dvc"])
assert ret == 0
assert (tmp_dir / "bar.dvc").exists()
assert not (tmp_dir / "foo.dvc").exists()
def test_failed_add_cleanup(tmp_dir, scm, dvc):
@@ -577,25 +528,25 @@ def test_should_not_track_git_internal_files(mocker, dvc, tmp_dir):
assert ".git" not in fname
class TestAddUnprotected(TestDvc):
def test(self):
ret = main(["config", "cache.type", "hardlink"])
self.assertEqual(ret, 0)
def test_add_unprotected(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["config", "cache.type", "hardlink"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
self.assertFalse(os.access(self.FOO, os.W_OK))
self.assertTrue(system.is_hardlink(self.FOO))
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
ret = main(["unprotect", self.FOO])
self.assertEqual(ret, 0)
ret = main(["unprotect", "foo"])
assert ret == 0
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", "foo"])
assert ret == 0
self.assertFalse(os.access(self.FOO, os.W_OK))
self.assertTrue(system.is_hardlink(self.FOO))
assert not os.access("foo", os.W_OK)
assert system.is_hardlink("foo")
@pytest.fixture
+2 -2
View File
@@ -12,7 +12,7 @@ from tests.utils import ANY
def test_daemon_analytics(mocker, tmp_path):
mock_send = mocker.patch("dvc.analytics.send")
report = os.fspath(tmp_path)
assert 0 == main(["daemon", "analytics", report])
assert main(["daemon", "analytics", report]) == 0
mock_send.assert_called_with(report)
@@ -21,7 +21,7 @@ def test_main_analytics(mocker, tmp_dir, dvc):
mock_is_enabled = mocker.patch("dvc.analytics.collect_and_send_report")
mock_report = mocker.patch("dvc.analytics.is_enabled", return_value=True)
tmp_dir.gen("foo", "text")
assert 0 == main(["add", "foo"])
assert main(["add", "foo"]) == 0
assert mock_is_enabled.called
assert mock_report.called
+305 -409
View File
@@ -1,16 +1,13 @@
import collections
import filecmp
import logging
import os
import shutil
import stat
import textwrap
from unittest.mock import patch
import pytest
from dvc.cli import main
from dvc.dvcfile import DVC_FILE_SUFFIX, PIPELINE_FILE, Dvcfile
from dvc.dvcfile import PIPELINE_FILE, Dvcfile
from dvc.exceptions import (
CheckoutError,
CheckoutErrorSuggestGit,
@@ -24,8 +21,7 @@ from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.utils import relpath
from dvc.utils.fs import remove
from dvc.utils.serialize import dump_yaml, load_yaml
from tests.basic_env import TestDvc, TestDvcGit
from tests.func.test_repro import TestRepro
from tests.utils import get_gitignore_content
logger = logging.getLogger("dvc")
@@ -36,465 +32,365 @@ def walk_files(directory):
yield os.path.join(root, f)
class TestCheckout(TestRepro):
def setUp(self):
super().setUp()
def test_checkout(tmp_dir, dvc, copy_script):
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
dvc.run(
fname="file1.dvc",
outs=["file1"],
deps=["foo", "copy.py"],
cmd="python copy.py foo file1",
single_stage=True,
)
remove(tmp_dir / "foo")
remove("data")
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.data_dir_stage = stages[0]
self.assertTrue(self.data_dir_stage is not None)
self.orig = "orig"
shutil.copy(self.FOO, self.orig)
os.unlink(self.FOO)
self.orig_dir = "orig_dir"
shutil.copytree(self.DATA_DIR, self.orig_dir)
shutil.rmtree(self.DATA_DIR)
def test(self):
self.dvc.checkout(force=True)
self._test_checkout()
def _test_checkout(self):
self.assertTrue(os.path.isfile(self.FOO))
self.assertTrue(filecmp.cmp(self.FOO, self.orig, shallow=False))
dvc.checkout(force=True)
assert (tmp_dir / "foo").read_text() == "foo"
assert (tmp_dir / "data").read_text() == {"file": "file"}
class TestCheckoutSingleStage(TestCheckout):
def test(self):
ret = main(["checkout", "--force", self.foo_stage.path])
self.assertEqual(ret, 0)
def test_checkout_cli(tmp_dir, dvc, copy_script):
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
dvc.run(
fname="file1.dvc",
outs=["file1"],
deps=["foo", "copy.py"],
cmd="python copy.py foo file1",
single_stage=True,
)
remove(tmp_dir / "foo")
remove("data")
ret = main(["checkout", "--force", self.data_dir_stage.path])
self.assertEqual(ret, 0)
assert main(["checkout", "--force"]) == 0
assert (tmp_dir / "foo").read_text() == "foo"
assert (tmp_dir / "data").read_text() == {"file": "file"}
self._test_checkout()
remove(tmp_dir / "foo")
remove("data")
assert main(["checkout", "--force", "foo.dvc"]) == 0
assert main(["checkout", "--force", "data.dvc"]) == 0
assert (tmp_dir / "foo").read_text() == "foo"
assert (tmp_dir / "data").read_text() == {"file": "file"}
class TestCheckoutCorruptedCacheFile(TestRepro):
def test(self):
cache = self.foo_stage.outs[0].cache_path
def test_checkout_corrupted_cache_file(tmp_dir, dvc):
(foo_stage,) = tmp_dir.dvc_gen("foo", "foo")
cache = foo_stage.outs[0].cache_path
os.chmod(cache, 0o644)
with open(cache, "a", encoding="utf-8") as fd:
fd.write("1")
os.chmod(cache, 0o644)
with open(cache, "a", encoding="utf-8") as fd:
fd.write("1")
with pytest.raises(CheckoutError):
self.dvc.checkout(force=True)
with pytest.raises(CheckoutError):
dvc.checkout(force=True)
self.assertFalse(os.path.isfile(self.FOO))
self.assertFalse(os.path.isfile(cache))
assert not os.path.isfile("foo")
assert not os.path.isfile(cache)
class TestCheckoutCorruptedCacheDir(TestDvc):
def test(self):
from dvc_data.hashfile import load
def test_checkout_corrupted_cache_dir(tmp_dir, dvc):
from dvc_data.hashfile import load
# NOTE: using 'copy' so that cache and link don't have same inode
ret = main(["config", "cache.type", "copy"])
self.assertEqual(ret, 0)
tmp_dir.gen("data", {"foo": "foo", "bar": "bar"})
# NOTE: using 'copy' so that cache and link don't have same inode
ret = main(["config", "cache.type", "copy"])
assert ret == 0
self.dvc.config.load()
dvc.config.load()
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertEqual(len(stages[0].outs), 1)
out = stages[0].outs[0]
stages = dvc.add("data")
assert len(stages) == 1
assert len(stages[0].outs) == 1
out = stages[0].outs[0]
# NOTE: modifying cache file for one of the files inside the directory
# to check if dvc will detect that the cache is corrupted.
obj = load(self.dvc.odb.local, out.hash_info)
_, _, entry_oid = list(obj)[0]
cache = self.dvc.odb.local.oid_to_path(entry_oid.value)
# NOTE: modifying cache file for one of the files inside the directory
# to check if dvc will detect that the cache is corrupted.
obj = load(dvc.odb.local, out.hash_info)
_, _, entry_oid = list(obj)[0]
cache = dvc.odb.local.oid_to_path(entry_oid.value)
os.chmod(cache, 0o644)
with open(cache, "w+", encoding="utf-8") as fobj:
fobj.write("1")
os.chmod(cache, 0o644)
with open(cache, "w+", encoding="utf-8") as fobj:
fobj.write("1")
with pytest.raises(CheckoutError):
self.dvc.checkout(force=True)
with pytest.raises(CheckoutError):
dvc.checkout(force=True)
self.assertFalse(os.path.exists(cache))
assert not os.path.exists(cache)
class TestCmdCheckout(TestCheckout):
def test(self):
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
self._test_checkout()
def test_remove_files_when_checkout(tmp_dir, dvc, scm):
# add the file into a separate branch
scm.checkout("branch", True)
ret = main(["checkout", "--force"])
assert ret == 0
tmp_dir.dvc_gen("file_in_a_branch", "random text", commit="add file")
# Checkout back in master
scm.checkout("master")
assert os.path.exists("file_in_a_branch")
# Make sure `dvc checkout` removes the file
# self.dvc.checkout()
ret = main(["checkout", "--force"])
assert ret == 0
assert not os.path.exists("file_in_a_branch")
class CheckoutBase(TestDvcGit):
GIT_IGNORE = ".gitignore"
def commit_data_file(self, fname, content="random text"):
with open(fname, "w", encoding="utf-8") as fd:
fd.write(content)
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
self.dvc.scm.add([fname + ".dvc", ".gitignore"])
self.dvc.scm.commit("adding " + fname)
def read_ignored(self):
with open(self.GIT_IGNORE, encoding="utf-8") as f:
return [s.strip("\n") for s in f.readlines()]
def outs_info(self, stage):
FileInfo = collections.namedtuple("FileInfo", "path inode")
paths = [
path
for output in stage["outs"]
for path in self.dvc.fs.find(output["path"])
]
return [
FileInfo(path=path, inode=system.inode(path)) for path in paths
]
class TestRemoveFilesWhenCheckout(CheckoutBase):
def test(self):
fname = "file_in_a_branch"
branch_master = "master"
branch_1 = "b1"
self.dvc.scm.add(self.dvc.scm.untracked_files())
self.dvc.scm.commit("add all files")
# add the file into a separate branch
self.dvc.scm.checkout(branch_1, True)
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
self.commit_data_file(fname)
# Checkout back in master
self.dvc.scm.checkout(branch_master)
self.assertTrue(os.path.exists(fname))
# Make sure `dvc checkout` removes the file
# self.dvc.checkout()
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists(fname))
class TestCheckoutCleanWorkingDir(CheckoutBase):
@patch("dvc.prompt.confirm", return_value=True)
def test(self, mock_prompt):
mock_prompt.return_value = True
stages = self.dvc.add(self.DATA_DIR)
stage = stages[0]
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
with open(working_dir_change, "w", encoding="utf-8") as f:
f.write("not_cached")
ret = main(["checkout", stage.relpath])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists(working_dir_change))
@patch("dvc.prompt.confirm", return_value=False)
def test_force(self, mock_prompt):
mock_prompt.return_value = False
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
stage = stages[0]
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
with open(working_dir_change, "w", encoding="utf-8") as f:
f.write("not_cached")
ret = main(["checkout", stage.relpath])
self.assertNotEqual(ret, 0)
class TestCheckoutCleanWorkingDir:
def test(self, mocker, tmp_dir, dvc): # pylint: disable=unused-argument
mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=True)
(stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"})
# change working directory
(tmp_dir / "data").gen("not_cached.txt", "not_cached")
assert main(["checkout", stage.relpath]) == 0
mock_prompt.assert_called()
self.assertNotEqual(ret, 0)
self.assertRaises(DvcException)
assert not (tmp_dir / "data" / "not_cached.txt").exists()
def test_force(self, mocker, tmp_dir, dvc):
mock_prompt = mocker.patch("dvc.prompt.confirm", return_value=False)
(stage,) = tmp_dir.dvc_gen("data", {"foo": "foo"})
# change working directory
(tmp_dir / "data").gen("not_cached.txt", "not_cached")
assert main(["checkout", stage.relpath]) != 0
mock_prompt.assert_called()
assert (tmp_dir / "data" / "not_cached.txt").exists()
def test_checkout_selective_remove(tmp_dir, dvc):
# Use copy to test for changes in the inodes
dvc.odb.local.cache_types = ["copy"]
tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
foo_inode = system.inode(os.path.join("data", "foo"))
bar_inode = system.inode(os.path.join("data", "bar"))
# move instead of remove, to lock inode assigned to stage_files[0].path
# if we were to use remove, we might end up with same inode assigned to
# newly checked out file
shutil.move(os.path.join("data", "foo"), "random_name")
assert main(["checkout", "--force", "data.dvc"]) == 0
assert (tmp_dir / "data").read_text() == {"foo": "foo", "bar": "bar"}
assert system.inode(os.path.join("data", "foo")) != foo_inode
assert system.inode(os.path.join("data", "bar")) == bar_inode
def test_gitignore_basic(tmp_dir, dvc, scm):
tmp_dir.gen("foo", "foo")
assert not os.path.exists(scm.GITIGNORE)
tmp_dir.dvc_gen("file1", "random text1", commit="add file1")
tmp_dir.dvc_gen("file2", "random text2", commit="add file2")
dvc.run(
single_stage=True,
cmd="cp foo file3",
deps=["foo"],
outs_no_cache=["file3"],
)
assert get_gitignore_content() == ["/file1", "/file2"]
def test_gitignore_when_checkout(tmp_dir, dvc, scm):
tmp_dir.dvc_gen("file_in_a_master", "master", commit="master")
class TestCheckoutSelectiveRemove(CheckoutBase):
def test(self):
# Use copy to test for changes in the inodes
ret = main(["config", "cache.type", "copy"])
self.assertEqual(ret, 0)
scm.checkout("branch", True)
ret = main(["checkout", "--force"])
assert ret == 0
tmp_dir.dvc_gen("file_in_a_branch", "branch", commit="branch")
scm.checkout("master")
ret = main(["checkout", "--force"])
assert ret == 0
ignored = get_gitignore_content()
assert len(ignored) == 1
assert "/file_in_a_master" in ignored
ret = main(["add", self.DATA_DIR])
self.assertEqual(0, ret)
scm.checkout("branch")
ret = main(["checkout", "--force"])
assert ret == 0
ignored = get_gitignore_content()
assert "/file_in_a_branch" in ignored
def test_checkout_missing_md5_in_stage_file(tmp_dir, dvc, copy_script):
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
stage = dvc.run(
fname="file1.dvc",
outs=["file1"],
deps=["foo", "copy.py"],
cmd="python copy.py foo file1",
single_stage=True,
)
d = load_yaml(stage.relpath)
del d[Stage.PARAM_OUTS][0][LocalFileSystem.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalFileSystem.PARAM_CHECKSUM]
dump_yaml(stage.relpath, d)
with pytest.raises(CheckoutError):
dvc.checkout(force=True)
def test_checkout_empty_dir(tmp_dir, dvc):
empty_dir = tmp_dir / "empty_dir"
empty_dir.mkdir()
(stage,) = dvc.add("empty_dir")
stage.outs[0].remove()
assert not empty_dir.exists()
stats = dvc.checkout(force=True)
assert stats["added"] == [os.path.join("empty_dir", "")]
assert empty_dir.is_dir()
assert not list(empty_dir.iterdir())
def test_checkout_not_cached_file(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
dvc.run(
cmd="cp foo bar",
deps=["foo"],
outs_no_cache=["bar"],
single_stage=True,
)
stats = dvc.checkout(force=True)
assert not any(stats.values())
def test_checkout_with_deps_cli(tmp_dir, dvc, copy_script):
tmp_dir.dvc_gen({"foo": "foo", "data": {"file": "file"}})
dvc.run(
fname="file1.dvc",
outs=["file1"],
deps=["foo", "copy.py"],
cmd="python copy.py foo file1",
single_stage=True,
)
remove("foo")
remove("file1")
assert not os.path.exists("foo")
assert not os.path.exists("file1")
stage_path = self.DATA_DIR + DVC_FILE_SUFFIX
stage = load_yaml(stage_path)
staged_files = self.outs_info(stage)
ret = main(["checkout", "--force", "file1.dvc", "--with-deps"])
assert ret == 0
assert os.path.exists("foo")
assert os.path.exists("file1")
# move instead of remove, to lock inode assigned to stage_files[0].path
# if we were to use remove, we might end up with same inode assigned to
# newly checked out file
shutil.move(staged_files[0].path, "random_name")
ret = main(["checkout", "--force", stage_path])
self.assertEqual(ret, 0)
def test_checkout_directory(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen({"data": {"foo": "foo", "bar": "bar"}})
remove("data")
assert not os.path.exists("data")
checkedout_files = self.outs_info(stage)
ret = main(["checkout", stage.path])
assert ret == 0
self.assertEqual(len(staged_files), len(checkedout_files))
self.assertEqual(staged_files[0].path, checkedout_files[0].path)
self.assertNotEqual(staged_files[0].inode, checkedout_files[0].inode)
self.assertEqual(staged_files[1].inode, checkedout_files[1].inode)
assert os.path.exists("data")
class TestGitIgnoreBasic(CheckoutBase):
def test(self):
fname1 = "file_1"
fname2 = "file_2"
fname3 = "file_3"
def test_checkout_hook(mocker, tmp_dir, dvc):
"""Test that dvc checkout handles EOFError gracefully, which is what
it will experience when running in a git hook.
"""
tmp_dir.dvc_gen({"data": {"foo": "foo"}})
mocker.patch("sys.stdout.isatty", return_value=True)
mocker.patch("dvc.prompt.input", side_effect=EOFError)
self.dvc.scm.add(self.dvc.scm.untracked_files())
self.dvc.scm.commit("add all files")
(tmp_dir / "data").gen("test", "test")
with pytest.raises(ConfirmRemoveError):
dvc.checkout()
self.assertFalse(os.path.exists(self.GIT_IGNORE))
self.commit_data_file(fname1)
self.commit_data_file(fname2)
self.dvc.run(
single_stage=True,
cmd=f"python {self.CODE} {self.FOO} {fname3}",
deps=[self.CODE, self.FOO],
outs_no_cache=[fname3],
)
def test_checkout_suggest_git(tmp_dir, dvc, scm):
# pylint: disable=no-member
tmp_dir.dvc_gen("foo", "foo")
try:
dvc.checkout(targets="gitbranch")
except DvcException as exc:
assert isinstance(exc, CheckoutErrorSuggestGit)
assert isinstance(exc.__cause__, NoOutputOrStageError)
assert isinstance(exc.__cause__.__cause__, StageFileDoesNotExistError)
try:
dvc.checkout(targets="foo")
except DvcException as exc:
assert isinstance(exc, CheckoutErrorSuggestGit)
assert isinstance(exc.__cause__, NoOutputOrStageError)
assert exc.__cause__.__cause__ is None
try:
dvc.checkout(targets="looks-like-dvcfile.dvc")
except DvcException as exc:
assert isinstance(exc, CheckoutErrorSuggestGit)
assert isinstance(exc.__cause__, StageFileDoesNotExistError)
assert exc.__cause__.__cause__ is None
self.assertTrue(os.path.exists(self.GIT_IGNORE))
ignored = self.read_ignored()
def test_checkout_target_recursive_should_not_remove_other_used_files(
tmp_dir, dvc
):
tmp_dir.dvc_gen({"foo": "foo", "bar": "bar", "data": {"file": "file"}})
assert main(["checkout", "-R", "data"]) == 0
assert (tmp_dir / "foo").exists()
assert (tmp_dir / "bar").exists()
self.assertEqual(len(ignored), 2)
self.assertIn("/" + fname1, ignored)
self.assertIn("/" + fname2, ignored)
def test_checkout_recursive_not_directory(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["add", "foo"])
assert ret == 0
stats = dvc.checkout(targets=["foo" + ".dvc"], recursive=True)
assert stats == {"added": [], "modified": [], "deleted": []}
class TestGitIgnoreWhenCheckout(CheckoutBase):
def test(self):
fname_master = "file_in_a_master"
branch_master = "master"
fname_branch = "file_in_a_branch"
branch_1 = "b1"
self.dvc.scm.add(self.dvc.scm.untracked_files())
self.dvc.scm.commit("add all files")
self.commit_data_file(fname_master)
def test_checkout_moved_cache_dir_with_symlinks(tmp_dir, dvc):
tmp_dir.gen({"foo": "foo", "data": {"file": "file"}})
ret = main(["config", "cache.type", "symlink"])
assert ret == 0
self.dvc.scm.checkout(branch_1, True)
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
self.commit_data_file(fname_branch)
ret = main(["add", "foo"])
assert ret == 0
self.dvc.scm.checkout(branch_master)
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
ret = main(["add", "data"])
assert ret == 0
ignored = self.read_ignored()
assert system.is_symlink("foo")
old_foo_link = os.path.realpath("foo")
self.assertEqual(len(ignored), 1)
self.assertIn("/" + fname_master, ignored)
assert system.is_symlink(os.path.join("data", "file"))
old_data_link = os.path.realpath(os.path.join("data", "file"))
self.dvc.scm.checkout(branch_1)
ret = main(["checkout", "--force"])
self.assertEqual(ret, 0)
ignored = self.read_ignored()
self.assertIn("/" + fname_branch, ignored)
old_cache_dir = dvc.odb.local.path
new_cache_dir = old_cache_dir + "_new"
os.rename(old_cache_dir, new_cache_dir)
ret = main(["cache", "dir", new_cache_dir])
assert ret == 0
class TestCheckoutMissingMd5InStageFile(TestRepro):
def test(self):
d = load_yaml(self.file1_stage)
del d[Stage.PARAM_OUTS][0][LocalFileSystem.PARAM_CHECKSUM]
del d[Stage.PARAM_DEPS][0][LocalFileSystem.PARAM_CHECKSUM]
dump_yaml(self.file1_stage, d)
ret = main(["checkout", "-f"])
assert ret == 0
with pytest.raises(CheckoutError):
self.dvc.checkout(force=True)
assert system.is_symlink("foo")
new_foo_link = os.path.realpath("foo")
assert system.is_symlink(os.path.join("data", "file"))
new_data_link = os.path.realpath(os.path.join("data", "file"))
class TestCheckoutEmptyDir(TestDvc):
def test(self):
dname = "empty_dir"
os.mkdir(dname)
assert relpath(old_foo_link, old_cache_dir) == relpath(
new_foo_link, new_cache_dir
)
stages = self.dvc.add(dname)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
self.assertEqual(len(stage.outs), 1)
stage.outs[0].remove()
self.assertFalse(os.path.exists(dname))
stats = self.dvc.checkout(force=True)
assert stats["added"] == [dname + os.sep]
self.assertTrue(os.path.isdir(dname))
self.assertEqual(len(os.listdir(dname)), 0)
class TestCheckoutNotCachedFile(TestDvc):
def test(self):
cmd = "python {} {} {}".format(self.CODE, self.FOO, "out")
self.dvc.add(self.FOO)
stage = self.dvc.run(
cmd=cmd,
deps=[self.FOO, self.CODE],
outs_no_cache=["out"],
single_stage=True,
)
self.assertTrue(stage is not None)
stats = self.dvc.checkout(force=True)
assert not any(stats.values())
class TestCheckoutWithDeps(TestRepro):
def test(self):
os.unlink(self.FOO)
os.unlink(self.file1)
self.assertFalse(os.path.exists(self.FOO))
self.assertFalse(os.path.exists(self.file1))
ret = main(["checkout", "--force", self.file1_stage, "--with-deps"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(self.FOO))
self.assertTrue(os.path.exists(self.file1))
class TestCheckoutDirectory(TestRepro):
def test(self):
stage = self.dvc.add(self.DATA_DIR)[0]
shutil.rmtree(self.DATA_DIR)
self.assertFalse(os.path.exists(self.DATA_DIR))
ret = main(["checkout", stage.path])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(self.DATA_DIR))
class TestCheckoutHook(TestDvc):
@patch("sys.stdout.isatty", return_value=True)
@patch("dvc.prompt.input", side_effect=EOFError)
def test(self, _mock_input, _mock_isatty):
"""Test that dvc checkout handles EOFError gracefully, which is what
it will experience when running in a git hook.
"""
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertNotEqual(stage, None)
self.create(os.path.join(self.DATA_DIR, "test"), "test")
with self.assertRaises(ConfirmRemoveError):
self.dvc.checkout()
class TestCheckoutSuggestGit(TestRepro):
def test(self):
# pylint: disable=no-member
try:
self.dvc.checkout(targets="gitbranch")
except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
self.assertIsInstance(
exc.__cause__.__cause__, StageFileDoesNotExistError
)
try:
self.dvc.checkout(targets=self.FOO)
except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, NoOutputOrStageError)
self.assertIsNone(exc.__cause__.__cause__)
try:
self.dvc.checkout(targets="looks-like-dvcfile.dvc")
except DvcException as exc:
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.__cause__, StageFileDoesNotExistError)
self.assertIsNone(exc.__cause__.__cause__)
class TestCheckoutTargetRecursiveShouldNotRemoveOtherUsedFiles(TestDvc):
def test(self):
ret = main(["add", self.DATA_DIR, self.FOO, self.BAR])
self.assertEqual(0, ret)
ret = main(["checkout", "-R", self.DATA_DIR])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists(self.FOO))
self.assertTrue(os.path.exists(self.BAR))
class TestCheckoutRecursiveNotDirectory(TestDvc):
def test(self):
ret = main(["add", self.FOO])
self.assertEqual(0, ret)
stats = self.dvc.checkout(targets=[self.FOO + ".dvc"], recursive=True)
assert stats == {"added": [], "modified": [], "deleted": []}
class TestCheckoutMovedCacheDirWithSymlinks(TestDvc):
def test(self):
ret = main(["config", "cache.type", "symlink"])
self.assertEqual(ret, 0)
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
ret = main(["add", self.DATA_DIR])
self.assertEqual(ret, 0)
self.assertTrue(system.is_symlink(self.FOO))
old_foo_link = os.path.realpath(self.FOO)
self.assertTrue(system.is_symlink(self.DATA))
old_data_link = os.path.realpath(self.DATA)
old_cache_dir = self.dvc.odb.local.path
new_cache_dir = old_cache_dir + "_new"
os.rename(old_cache_dir, new_cache_dir)
ret = main(["cache", "dir", new_cache_dir])
self.assertEqual(ret, 0)
ret = main(["checkout", "-f"])
self.assertEqual(ret, 0)
self.assertTrue(system.is_symlink(self.FOO))
new_foo_link = os.path.realpath(self.FOO)
self.assertTrue(system.is_symlink(self.DATA))
new_data_link = os.path.realpath(self.DATA)
self.assertEqual(
relpath(old_foo_link, old_cache_dir),
relpath(new_foo_link, new_cache_dir),
)
self.assertEqual(
relpath(old_data_link, old_cache_dir),
relpath(new_data_link, new_cache_dir),
)
assert relpath(old_data_link, old_cache_dir) == relpath(
new_data_link, new_cache_dir
)
def test_checkout_no_checksum(tmp_dir, dvc):
+137 -147
View File
@@ -1,5 +1,7 @@
import os
import pytest
from dvc.cli import DvcParserError, parse_args
from dvc.cli.command import CmdBase
from dvc.commands.add import CmdAdd
@@ -12,200 +14,188 @@ from dvc.commands.repro import CmdRepro
from dvc.commands.run import CmdRun
from dvc.commands.status import CmdDataStatus
from dvc.exceptions import DvcException
from tests.basic_env import TestDvc
class TestArgParse(TestDvc):
def test(self):
args = parse_args(["init"])
self.assertIsInstance(args.func(args), CmdInit)
def test_argparse(dvc):
args = parse_args(["init"])
assert isinstance(args.func(args), CmdInit)
class TestRun(TestDvc):
def test(self):
dep1 = "dep1"
dep2 = "dep2"
def test_run(dvc):
dep1 = "dep1"
dep2 = "dep2"
out1 = "out1"
out2 = "out2"
out1 = "out1"
out2 = "out2"
out_no_cache1 = "out_no_cache1"
out_no_cache2 = "out_no_cache2"
out_no_cache1 = "out_no_cache1"
out_no_cache2 = "out_no_cache2"
fname = "dvc.dvc"
cmd = "cmd"
arg1 = "arg1"
arg2 = "arg2"
fname = "dvc.dvc"
cmd = "cmd"
arg1 = "arg1"
arg2 = "arg2"
args = parse_args(
[
"run",
"-d",
dep1,
"--deps",
dep2,
"-o",
out1,
"--outs",
out2,
"-O",
out_no_cache1,
"--outs-no-cache",
out_no_cache2,
"--file",
fname,
cmd,
arg1,
arg2,
]
)
cmd_cls = args.func(args)
self.assertIsInstance(cmd_cls, CmdRun)
self.assertEqual(args.deps, [dep1, dep2])
self.assertEqual(args.outs, [out1, out2])
self.assertEqual(args.outs_no_cache, [out_no_cache1, out_no_cache2])
self.assertEqual(args.file, fname)
self.assertEqual(args.command, [cmd, arg1, arg2])
args = parse_args(
[
"run",
"-d",
dep1,
"--deps",
dep2,
"-o",
out1,
"--outs",
out2,
"-O",
out_no_cache1,
"--outs-no-cache",
out_no_cache2,
"--file",
fname,
cmd,
arg1,
arg2,
]
)
cmd_cls = args.func(args)
assert isinstance(cmd_cls, CmdRun)
assert args.deps == [dep1, dep2]
assert args.outs == [out1, out2]
assert args.outs_no_cache == [out_no_cache1, out_no_cache2]
assert args.file == fname
assert args.command == [cmd, arg1, arg2]
cmd_cls.repo.close()
cmd_cls.repo.close()
class TestPull(TestDvc):
def test(self):
args = parse_args(["pull"])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdDataPull)
def test_pull(dvc):
args = parse_args(["pull"])
cmd = args.func(args)
assert isinstance(cmd, CmdDataPull)
cmd.repo.close()
cmd.repo.close()
class TestPush(TestDvc):
def test(self):
args = parse_args(["push"])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdDataPush)
def test_push(dvc):
args = parse_args(["push"])
cmd = args.func(args)
assert isinstance(cmd, CmdDataPush)
cmd.repo.close()
cmd.repo.close()
class TestStatus(TestDvc):
def test(self):
args = parse_args(["status"])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdDataStatus)
def test_status(dvc):
args = parse_args(["status"])
cmd = args.func(args)
assert isinstance(cmd, CmdDataStatus)
cmd.repo.close()
cmd.repo.close()
class TestRepro(TestDvc):
def test(self):
target1 = "1"
target2 = "2"
def test_repro(dvc):
target1 = "1"
target2 = "2"
args = parse_args(
["repro", target1, target2, "-f", "--force", "-s", "--single-item"]
)
args = parse_args(
["repro", target1, target2, "-f", "--force", "-s", "--single-item"]
)
cmd = args.func(args)
self.assertIsInstance(cmd, CmdRepro)
self.assertEqual(args.targets, [target1, target2])
self.assertEqual(args.force, True)
self.assertEqual(args.single_item, True)
cmd = args.func(args)
assert isinstance(cmd, CmdRepro)
assert args.targets == [target1, target2]
assert args.force
assert args.single_item
cmd.repo.close()
cmd.repo.close()
class TestRemove(TestDvc):
def test(self):
target1 = "1"
target2 = "2"
def test_remove(dvc):
target1 = "1"
target2 = "2"
args = parse_args(["remove", target1, target2])
args = parse_args(["remove", target1, target2])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdRemove)
self.assertEqual(args.targets, [target1, target2])
cmd = args.func(args)
assert isinstance(cmd, CmdRemove)
assert args.targets == [target1, target2]
cmd.repo.close()
cmd.repo.close()
class TestAdd(TestDvc):
def test(self):
target1 = "1"
target2 = "2"
def test_add(dvc):
target1 = "1"
target2 = "2"
args = parse_args(["add", target1, target2])
args = parse_args(["add", target1, target2])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdAdd)
self.assertEqual(args.targets, [target1, target2])
cmd = args.func(args)
assert isinstance(cmd, CmdAdd)
assert args.targets == [target1, target2]
cmd.repo.close()
cmd.repo.close()
class TestConfig(TestDvc):
def test(self):
name = "section.option"
value = "1"
def test_config_unset(dvc):
name = "section.option"
value = "1"
args = parse_args(["config", "-u", "--unset", name, value])
args = parse_args(["config", "-u", "--unset", name, value])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdConfig)
self.assertEqual(args.unset, True)
self.assertEqual(args.name, (False, "section", "option"))
self.assertEqual(args.value, value)
def test_config_list(self):
args = parse_args(["config", "--list"])
self.assertTrue(args.list)
self.assertIsNone(args.name)
self.assertIsNone(args.value)
cmd = args.func(args)
assert isinstance(cmd, CmdConfig)
assert args.unset
assert args.name == (False, "section", "option")
assert args.value == value
class TestCheckout(TestDvc):
def test(self):
args = parse_args(["checkout"])
cmd = args.func(args)
self.assertIsInstance(cmd, CmdCheckout)
def test_config_list():
args = parse_args(["config", "--list"])
cmd.repo.close()
assert args.list
assert args.name is None
assert args.value is None
class TestFindRoot(TestDvc):
def test(self):
class Cmd(CmdBase):
def run(self):
pass
def test_checkout(dvc):
args = parse_args(["checkout"])
cmd = args.func(args)
assert isinstance(cmd, CmdCheckout)
class A:
quiet = False
verbose = True
cd = os.path.pardir
args = A()
with self.assertRaises(DvcException):
Cmd(args)
cmd.repo.close()
class TestCd(TestDvc):
def test(self):
class Cmd(CmdBase):
def run(self):
pass
def test_find_root(dvc):
class Cmd(CmdBase):
def run(self):
pass
class A:
quiet = False
verbose = True
cd = os.path.pardir
class A:
quiet = False
verbose = True
cd = os.path.pardir
parent_dir = os.path.realpath(os.path.pardir)
args = A()
with self.assertRaises(DvcException):
Cmd(args)
current_dir = os.path.realpath(os.path.curdir)
self.assertEqual(parent_dir, current_dir)
args = A()
with pytest.raises(DvcException):
Cmd(args)
def test_cd(dvc):
class Cmd(CmdBase):
def run(self):
pass
class A:
quiet = False
verbose = True
cd = os.path.pardir
parent_dir = os.path.realpath(os.path.pardir)
args = A()
with pytest.raises(DvcException):
Cmd(args)
current_dir = os.path.realpath(os.path.curdir)
assert parent_dir == current_dir
def test_unknown_command_help(capsys):
+3
View File
@@ -418,6 +418,7 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
"plots": {
"path/to/plot": {"x": "value", "y": "value"},
"path/to/another/plot": {"x": "value", "y": "value"},
"path/to/empty/plot": None,
},
"stages": STAGE_EXAMPLE,
},
@@ -425,6 +426,8 @@ def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
"plots": [
{"path/to/plot": {"x": "value", "y": "value"}},
{"path/to/another/plot": {"x": "value", "y": "value"}},
{"path/to/empty/plot": None},
"path/to/plot/str",
],
"stages": STAGE_EXAMPLE,
},
+56 -150
View File
@@ -2,186 +2,92 @@ import logging
import os
import shutil
import configobj
import pytest
from git import Repo
from dvc.cli import main
from dvc.exceptions import CollectCacheError
from dvc.fs import LocalFileSystem
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
from dvc_data.hashfile.db.local import LocalHashFileDB
from tests.basic_env import TestDir, TestDvcGit
class TestGC(TestDvcGit):
def setUp(self):
super().setUp()
@pytest.fixture
def good_and_bad_cache(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
(stage,) = tmp_dir.dvc_gen(
"data",
{"sub": {"data_sub": "data_sub", "data": "data", "тест": "проверка"}},
)
raw_dir_hash = stage.outs[0].hash_info.as_raw().value
odb = dvc.odb.local
self.dvc.add(self.FOO)
stages = self.dvc.add(self.DATA_DIR)
raw_dir_hash = stages[0].outs[0].hash_info.as_raw().value
bad_cache = {raw_dir_hash}
for i in ["123", "234", "345"]:
odb.add_bytes(i, i.encode("utf8"))
bad_cache.add(i)
self.good_cache = [
self.dvc.odb.local.oid_to_path(md5)
for md5 in self.dvc.odb.local.all()
if md5 != raw_dir_hash
]
self.bad_cache = [self.dvc.odb.local.oid_to_path(raw_dir_hash)]
for i in ["123", "234", "345"]:
path = os.path.join(self.dvc.odb.local.path, i[0:2], i[2:])
self.create(path, i)
self.bad_cache.append(path)
def test_api(self):
self.dvc.gc(workspace=True)
self._test_gc()
def test_cli(self):
ret = main(["gc", "-wf"])
self.assertEqual(ret, 0)
self._test_gc()
def _test_gc(self):
self.assertTrue(os.path.isdir(self.dvc.odb.local.path))
for c in self.bad_cache:
self.assertFalse(os.path.exists(c))
for c in self.good_cache:
self.assertTrue(os.path.exists(c))
good_cache = {md5 for md5 in odb.all() if md5 not in bad_cache}
return good_cache, bad_cache
class TestGCBranchesTags(TestDvcGit):
def _check_cache(self, num):
total = 0
for _, _, files in os.walk(os.path.join(".dvc", "cache")):
total += len(files)
self.assertEqual(total, num)
def test(self):
fname = "file"
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("v1.0")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
self.dvc.scm.add([".gitignore", stages[0].relpath])
self.dvc.scm.commit("v1.0")
self.dvc.scm.tag("v1.0")
self.dvc.scm.checkout("test", create_new=True)
self.dvc.remove(stages[0].relpath)
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("test")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
self.dvc.scm.add([".gitignore", stages[0].relpath])
self.dvc.scm.commit("test")
self.dvc.scm.checkout("master")
self.dvc.remove(stages[0].relpath)
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("trash")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
self.dvc.scm.add([".gitignore", stages[0].relpath])
self.dvc.scm.commit("trash")
self.dvc.remove(stages[0].relpath)
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("master")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
self.dvc.scm.add([".gitignore", stages[0].relpath])
self.dvc.scm.commit("master")
self._check_cache(4)
self.dvc.gc(all_tags=True, all_branches=True)
self._check_cache(3)
self.dvc.gc(all_tags=False, all_branches=True)
self._check_cache(2)
self.dvc.gc(all_tags=True, all_branches=False)
self._check_cache(1)
def test_gc_api(dvc, good_and_bad_cache):
dvc.gc(workspace=True)
odb = dvc.odb.local
good_cache, bad_cache = good_and_bad_cache
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
class TestGCMultipleDvcRepos(TestDvcGit):
def _check_cache(self, num):
total = 0
for _, _, files in os.walk(os.path.join(".dvc", "cache")):
total += len(files)
self.assertEqual(total, num)
def test_gc_cli(dvc, good_and_bad_cache):
assert main(["gc", "-wf"]) == 0
odb = dvc.odb.local
good_cache, bad_cache = good_and_bad_cache
assert set(odb.oids_exist([*good_cache, *bad_cache])) == good_cache
def setUp(self):
super().setUp()
self.additional_path = TestDir.mkdtemp()
self.additional_git = Repo.init(self.additional_path)
self.additional_dvc = DvcRepo.init(self.additional_path)
cache_path = os.path.join(self._root_dir, ".dvc", "cache")
config_path = os.path.join(
self.additional_path, ".dvc", "config.local"
)
cfg = configobj.ConfigObj()
cfg.filename = config_path
cfg["cache"] = {"dir": cache_path}
cfg.write()
def test_gc_branches_tags(tmp_dir, dvc, scm):
tmp_dir.dvc_gen("file", "v1.0", commit="v1.0")
scm.tag("v1.0")
self.additional_dvc = DvcRepo(self.additional_path)
with tmp_dir.branch("test", new=True):
dvc.remove("file.dvc")
tmp_dir.dvc_gen("file", "test", commit="test")
def test(self):
dvc.remove("file.dvc")
tmp_dir.dvc_gen("file", "trash", commit="trash")
# ADD FILE ONLY IN MAIN PROJECT
fname = "only_in_first"
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("only in main repo")
dvc.remove("file.dvc")
tmp_dir.dvc_gen("file", "master", commit="trash")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
odb = dvc.odb.local
assert len(list(odb.all())) == 4
# ADD FILE IN MAIN PROJECT THAT IS ALSO IN SECOND PROJECT
fname = "in_both"
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("in both repos")
dvc.gc(all_tags=True, all_branches=True)
assert len(list(odb.all())) == 3
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
dvc.gc(all_tags=False, all_branches=True)
assert len(list(odb.all())) == 2
cwd = os.getcwd()
os.chdir(self.additional_path)
# ADD FILE ONLY IN SECOND PROJECT
fname = "only_in_second"
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("only in additional repo")
dvc.gc(all_tags=True, all_branches=False)
assert len(list(odb.all())) == 1
stages = self.additional_dvc.add(fname)
self.assertEqual(len(stages), 1)
# ADD FILE IN SECOND PROJECT THAT IS ALSO IN MAIN PROJECT
fname = "in_both"
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("in both repos")
def test_gc_multiple_dvc_repos(tmp_dir, scm, dvc, erepo_dir):
tmp_dir.dvc_gen("only_in_first", "only in main repo")
tmp_dir.dvc_gen("in_both", "in both repos")
stages = self.additional_dvc.add(fname)
self.assertEqual(len(stages), 1)
erepo_dir.dvc.odb.local.path = dvc.odb.local.path
with erepo_dir.chdir():
erepo_dir.dvc_gen("in_both", "in both repos")
erepo_dir.dvc_gen("only_in_second", "only in additional repo")
os.chdir(cwd)
odb = dvc.odb.local
assert len(list(odb.all())) == 3
self._check_cache(3)
dvc.gc(repos=[erepo_dir], workspace=True)
assert len(list(odb.all())) == 3
self.dvc.gc(repos=[self.additional_path], workspace=True)
self._check_cache(3)
self.dvc.gc(workspace=True)
self._check_cache(2)
dvc.gc(workspace=True)
assert len(list(odb.all())) == 2
def test_all_commits(tmp_dir, scm, dvc):
+9 -9
View File
@@ -1,6 +1,5 @@
import filecmp
import os
from unittest.mock import patch
import pytest
from funcy import first
@@ -49,7 +48,7 @@ def test_import_git_file(tmp_dir, scm, dvc, git_dir, src_is_dvc):
}
def test_import_cached_file(erepo_dir, tmp_dir, dvc, scm, monkeypatch):
def test_import_cached_file(mocker, erepo_dir, tmp_dir, dvc, scm, monkeypatch):
src = "some_file"
dst = "some_file_imported"
@@ -60,10 +59,10 @@ def test_import_cached_file(erepo_dir, tmp_dir, dvc, scm, monkeypatch):
(tmp_dir / dst).unlink()
remote_exception = NoRemoteError("dvc import")
with patch.object(
mocker.patch.object(
dvc.cloud, "get_remote_odb", side_effect=remote_exception
):
tmp_dir.dvc.imp(os.fspath(erepo_dir), src, dst)
)
tmp_dir.dvc.imp(os.fspath(erepo_dir), src, dst)
assert (tmp_dir / dst).is_file()
assert filecmp.cmp(erepo_dir / src, tmp_dir / dst, shallow=False)
@@ -357,7 +356,9 @@ def test_push_wildcard_from_bare_git_repo(
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dir123")
def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
def test_download_error_pulling_imported_stage(
mocker, tmp_dir, dvc, erepo_dir
):
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
dvc.imp(os.fspath(erepo_dir), "foo", "foo_imported")
@@ -368,9 +369,8 @@ def test_download_error_pulling_imported_stage(tmp_dir, dvc, erepo_dir):
remove("foo_imported")
remove(dst_cache)
with patch(
"dvc_objects.fs.generic.transfer", side_effect=Exception
), pytest.raises(DownloadError):
mocker.patch("dvc_objects.fs.generic.transfer", side_effect=Exception)
with pytest.raises(DownloadError):
dvc.pull(["foo_imported.dvc"])
+33 -42
View File
@@ -13,37 +13,32 @@ from dvc.exceptions import InvalidArgumentError
from dvc.stage import Stage
from dvc.testing.workspace_tests import TestImport as _TestImport
from dvc.utils.fs import makedirs
from tests.basic_env import TestDvc
class TestCmdImport(TestDvc):
def test(self):
ret = main(["import-url", self.FOO, "import"])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists("import.dvc"))
def test_cmd_import(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
ret = main(["import-url", "foo", "import"])
assert ret == 0
assert os.path.exists("import.dvc")
ret = main(["import-url", "non-existing-file", "import"])
self.assertNotEqual(ret, 0)
def test_unsupported(self):
ret = main(["import-url", "unsupported://path", "import_unsupported"])
self.assertNotEqual(ret, 0)
ret = main(["import-url", "non-existing-file", "import"])
assert ret != 0
class TestDefaultOutput(TestDvc):
def test(self):
tmpdir = self.mkdtemp()
filename = str(uuid4())
tmpfile = os.path.join(tmpdir, filename)
def test_cmd_unsupported_scheme(dvc):
ret = main(["import-url", "unsupported://path", "import_unsupported"])
assert ret != 0
with open(tmpfile, "w", encoding="utf-8") as fd:
fd.write("content")
ret = main(["import-url", tmpfile])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(filename))
with open(filename, encoding="utf-8") as fd:
self.assertEqual(fd.read(), "content")
def test_default_output(tmp_dir, dvc, cloud):
filename = str(uuid4())
tmpfile = cloud / filename
tmpfile.write_bytes(b"content")
cloud.gen(filename, "content")
ret = main(["import-url", tmpfile.fs_path])
assert ret == 0
assert (tmp_dir / filename).read_bytes() == b"content"
def test_should_remove_outs_before_import(tmp_dir, dvc, mocker, erepo_dir):
@@ -56,26 +51,22 @@ def test_should_remove_outs_before_import(tmp_dir, dvc, mocker, erepo_dir):
assert remove_outs_call_counter.mock.call_count == 1
class TestImportFilename(TestDvc):
def setUp(self):
super().setUp()
tmp_dir = self.mkdtemp()
self.external_source = os.path.join(tmp_dir, "file")
with open(self.external_source, "w", encoding="utf-8") as fobj:
fobj.write("content")
def test_import_filename(tmp_dir, dvc, cloud):
external_source = cloud / "file"
(cloud / "file").write_text("content", encoding="utf-8")
ret = main(["import-url", "--file", "bar.dvc", external_source.fs_path])
assert ret == 0
assert (tmp_dir / "bar.dvc").exists()
def test(self):
ret = main(["import-url", "--file", "bar.dvc", self.external_source])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
(tmp_dir / "bar.dvc").unlink()
(tmp_dir / "sub").mkdir()
os.remove("bar.dvc")
os.mkdir("sub")
path = os.path.join("sub", "bar.dvc")
ret = main(["import-url", "--file", path, self.external_source, "out"])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists(path))
path = tmp_dir / "sub" / "bar.dvc"
ret = main(
["import-url", "--file", path.fs_path, external_source.fs_path, "out"]
)
assert ret == 0
assert path.exists()
@pytest.mark.parametrize("dname", [".", "dir", "dir/subdir"])
+1 -1
View File
@@ -64,7 +64,7 @@ def test_init_quiet_should_not_display_welcome_screen(tmp_dir, scm, caplog):
with caplog.at_level(logging.INFO, logger="dvc"):
ret = main(["init", "--quiet"])
assert 0 == ret
assert ret == 0
assert "" == caplog.text
-228
View File
@@ -1,228 +0,0 @@
from copy import deepcopy
from textwrap import dedent
import pytest
from funcy import first
from dvc import stage as stage_module
pytest.importorskip("dvclive", reason="no dvclive")
LIVE_SCRIPT = dedent(
"""
from dvclive import Live
from PIL import Image
import sys
r = 2
metrics_logger = Live()
for i in range(r):
metrics_logger.log("loss", 1-i/r)
metrics_logger.log("accuracy", i/r)
image = Image.new("RGB", (10,10), (i,i,i))
metrics_logger.log_image("image.jpg", image)
metrics_logger.next_step()"""
)
LIVE_CHECKPOINT_SCRIPT = dedent(
"""
import os
from dvclive import Live
def read(path):
value=0
if os.path.exists(path):
with open(path, 'r') as fobj:
try:
value = int(fobj.read())
except ValueError:
pass
return value
def dump(value, path):
with open(path, "w") as fobj:
fobj.write(str(value))
r = 3
checkpoint_file = "checkpoint"
metrics_logger = Live()
value = read(checkpoint_file)
for i in range(1,r):
m = i + value
dump(m, checkpoint_file)
metrics_logger.log("metric1", m)
metrics_logger.log("metric2", m * 2)
metrics_logger.next_step()"""
)
@pytest.fixture
def live_stage(tmp_dir, scm, dvc, mocker):
mocker.patch("dvc.stage.run.Monitor.AWAIT", 0.01)
def make(
html=True,
live=None,
live_no_cache=None,
code=LIVE_SCRIPT,
):
assert bool(live) != bool(live_no_cache)
tmp_dir.gen("train.py", code)
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
cmd="python train.py",
params=["foo"],
deps=["train.py"],
name="live_stage",
live=live,
live_no_cache=live_no_cache,
live_no_html=not html,
)
scm.add(["dvc.yaml", "dvc.lock", "train.py", "params.yaml"])
scm.commit("initial: live_stage")
return stage
yield make
@pytest.mark.parametrize("html", (True, False))
def test_export_config(tmp_dir, dvc, mocker, live_stage, html):
run_spy = mocker.spy(stage_module.run, "_run")
live_stage(html=html, live="logs")
assert run_spy.call_count == 1
_, kwargs = run_spy.call_args
assert "DVCLIVE_PATH" in kwargs["env"]
assert kwargs["env"]["DVCLIVE_PATH"] == "logs"
assert "DVCLIVE_HTML" in kwargs["env"]
assert kwargs["env"]["DVCLIVE_HTML"] == str(int(html))
def test_live_provides_metrics(tmp_dir, dvc, live_stage):
live_stage(live="logs")
assert (tmp_dir / "logs.json").is_file()
assert dvc.metrics.show() == {
"": {
"data": {
"logs.json": {
"data": {"accuracy": 0.5, "loss": 0.5, "step": 1}
}
}
}
}
assert (tmp_dir / "logs").is_dir()
plots_data = dvc.plots.show()
files = list(plots_data["workspace"]["sources"]["data"])
assert "logs/scalars/accuracy.tsv" in files
assert "logs/scalars/loss.tsv" in files
assert "logs/images/0/image.jpg" in files
assert "logs/images/1/image.jpg" in files
@pytest.mark.parametrize("typ", ("live", "live_no_cache"))
def test_experiments_track_summary(tmp_dir, scm, dvc, live_stage, typ):
live_stage(**{typ: "logs"})
baseline_rev = scm.get_rev()
experiments = dvc.experiments.run(targets=["live_stage"], params=["foo=2"])
assert len(experiments) == 1
((exp_rev, _),) = experiments.items()
res = dvc.experiments.show()
assert "logs.json" in res[baseline_rev][exp_rev]["data"]["metrics"].keys()
@pytest.mark.parametrize("html", [True, False])
def test_live_html(tmp_dir, dvc, live_stage, html):
live_stage(live="logs", html=html)
assert (tmp_dir / "logs_dvc_plots" / "index.html").is_file() == html
if html:
html_text = (tmp_dir / "logs_dvc_plots" / "index.html").read_text()
assert 'http-equiv="refresh"' in html_text
@pytest.fixture
def live_checkpoint_stage(tmp_dir, scm, dvc, mocker):
mocker.patch("dvc.stage.run.Monitor.AWAIT", 0.01)
def make(live=None, live_no_cache=None):
assert bool(live) != bool(live_no_cache)
tmp_dir.gen("train.py", LIVE_CHECKPOINT_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
cmd="python train.py",
params=["foo"],
deps=["train.py"],
name="live_stage",
live=live,
live_no_cache=live_no_cache,
checkpoints=["checkpoint"],
no_exec=True,
)
scm.add(["dvc.yaml", "train.py", "params.yaml", ".gitignore"])
scm.commit("initial: live_stage")
return stage
yield make
def checkpoints_metric(show_results, metric_file, metric_name):
tmp = deepcopy(show_results)
tmp.pop("workspace")
tmp = first(tmp.values())
tmp.pop("baseline")
return [
exp["data"]["metrics"][metric_file]["data"][metric_name]
for exp in tmp.values()
]
@pytest.mark.parametrize("typ", ("live", "live_no_cache"))
def test_live_checkpoints_resume(
tmp_dir, scm, dvc, live_checkpoint_stage, typ
):
stage = live_checkpoint_stage(**{typ: "logs"})
results = dvc.experiments.run(
stage.addressing, params=["foo=2"], tmp_dir=False
)
checkpoint_resume = first(results)
dvc.experiments.run(
stage.addressing, checkpoint_resume=checkpoint_resume, tmp_dir=False
)
results = dvc.experiments.show()
assert checkpoints_metric(results, "logs.json", "step") == [3, 2, 1, 0]
assert checkpoints_metric(results, "logs.json", "metric1") == [4, 3, 2, 1]
assert checkpoints_metric(results, "logs.json", "metric2") == [8, 6, 4, 2]
def test_dvclive_stage_with_different_wdir(tmp_dir, scm, dvc):
(tmp_dir / "subdir").gen("train.py", LIVE_SCRIPT)
(tmp_dir / "subdir" / "params.yaml").dump({"foo": 1})
dvc.stage.add(
cmd="python train.py",
params=["foo"],
deps=["train.py"],
name="live_stage",
live="dvclive",
live_no_html=True,
wdir="subdir",
)
dvc.reproduce()
assert (tmp_dir / "subdir" / "dvclive").is_dir()
assert (tmp_dir / "subdir" / "dvclive.json").is_file()
+87 -156
View File
@@ -4,198 +4,129 @@ import textwrap
import pytest
from dvc.cli import main
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.exceptions import DvcException, MoveNotDataSourceError
from dvc.utils.serialize import load_yaml
from tests.basic_env import TestDvc, TestDvcGit
from tests.func.test_repro import TestRepro
from tests.utils import cd
class TestMove(TestDvc):
def test(self):
dst = self.FOO + "1"
self.dvc.add(self.FOO)
self.dvc.move(self.FOO, dst)
def test_move(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
dvc.move("foo", "foo1")
self.assertFalse(os.path.isfile(self.FOO))
self.assertTrue(os.path.isfile(dst))
assert not (tmp_dir / "foo").is_file()
assert (tmp_dir / "foo1").is_file()
class TestMoveNonExistentFile(TestDvc):
def test(self):
with self.assertRaises(DvcException):
self.dvc.move("non_existent_file", "dst")
def test_move_non_existent_file(dvc):
with pytest.raises(DvcException):
dvc.move("non_existent_file", "dst")
class TestMoveDirectory(TestDvc):
def test(self):
dst = "dst"
stages = self.dvc.add(self.DATA_DIR)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
self.dvc.move(self.DATA_DIR, dst)
self.assertFalse(os.path.exists(self.DATA_DIR))
self.assertTrue(os.path.exists(dst))
def test_move_directory(tmp_dir, dvc):
tmp_dir.dvc_gen("data", {"foo": "foo", "bar": "bar"})
dvc.move("data", "dst")
assert not (tmp_dir / "data").is_dir()
assert (tmp_dir / "dst").is_dir()
class TestCmdMove(TestDvc):
def test(self):
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
self.assertTrue(stages[0] is not None)
ret = main(["move", self.FOO, self.FOO + "1"])
self.assertEqual(ret, 0)
ret = main(["move", "non-existing-file", "dst"])
self.assertNotEqual(ret, 0)
def test_cmd_move(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
assert main(["move", "foo", "foo" + "1"]) == 0
assert main(["move", "non-existing-file", "dst"]) != 0
class TestMoveNotDataSource(TestRepro):
def test(self):
from dvc.repo import Repo as DvcRepo
def test_move_not_data_source(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
dvc.run(
cmd="cp foo file1",
outs=["file1"],
deps=["foo"],
single_stage=True,
)
self.dvc = DvcRepo(self._root_dir)
with self.assertRaises(MoveNotDataSourceError):
self.dvc.move(self.file1, "dst")
with pytest.raises(MoveNotDataSourceError):
dvc.move("file1", "dst")
ret = main(["move", self.file1, "dst"])
self.assertNotEqual(ret, 0)
assert main(["move", "file1", "dst"]) != 0
assert (tmp_dir / "file1").exists()
class TestMoveFileWithExtension(TestDvc):
def test(self):
with open(
os.path.join(self.dvc.root_dir, "file.csv"), "w", encoding="utf-8"
) as fd:
fd.write("1,2,3\n")
def test_move_file_with_extension(tmp_dir, dvc):
tmp_dir.dvc_gen("file.csv", "1,2,3\n")
self.dvc.add("file.csv")
self.assertTrue(os.path.exists("file.csv"))
self.assertTrue(os.path.exists("file.csv.dvc"))
ret = main(["move", "file.csv", "other_name.csv"])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists("file.csv"))
self.assertFalse(os.path.exists("file.csv.dvc"))
self.assertTrue(os.path.exists("other_name.csv"))
self.assertTrue(os.path.exists("other_name.csv.dvc"))
assert main(["move", "file.csv", "other_name.csv"]) == 0
assert not (tmp_dir / "file.csv").exists()
assert not (tmp_dir / "file.csv.dvc").exists()
assert (tmp_dir / "other_name.csv").exists()
assert (tmp_dir / "other_name.csv.dvc").exists()
class TestMoveFileToDirectory(TestDvc):
def test(self):
foo_dvc_file = self.FOO + DVC_FILE_SUFFIX
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(foo_dvc_file))
def test_move_file_to_directory(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
tmp_dir.gen({"data": {"bar": "bar"}})
new_foo_path = os.path.join(self.DATA_DIR, self.FOO)
new_foo_dvc_path = new_foo_path + DVC_FILE_SUFFIX
ret = main(["move", self.FOO, new_foo_path])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists(self.FOO))
self.assertFalse(os.path.exists(foo_dvc_file))
self.assertTrue(os.path.exists(new_foo_path))
self.assertTrue(os.path.exists(new_foo_dvc_path))
assert main(["move", "foo", os.path.join("data", "foo")]) == 0
assert not (tmp_dir / "foo").exists()
assert not (tmp_dir / "foo.dvc").exists()
assert (tmp_dir / "data" / "foo").exists()
assert (tmp_dir / "data" / "foo.dvc").exists()
class TestMoveFileToDirectoryWithoutSpecifiedTargetName(TestDvc):
def test(self):
foo_stage_file_path = self.FOO + DVC_FILE_SUFFIX
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(foo_stage_file_path))
def test_move_file_to_directory_without_specified_target_name(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
tmp_dir.gen({"data": {"bar": "bar"}})
target_foo_path = os.path.join(self.DATA_DIR, self.FOO)
target_foo_stage_file_path = target_foo_path + DVC_FILE_SUFFIX
assert main(["move", "foo", "data"]) == 0
assert not (tmp_dir / "foo").exists()
assert not (tmp_dir / "foo.dvc").exists()
assert (tmp_dir / "data" / "foo").exists()
assert (tmp_dir / "data" / "foo.dvc").exists()
ret = main(["move", self.FOO, self.DATA_DIR])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists(self.FOO))
self.assertFalse(os.path.exists(foo_stage_file_path))
self.assertTrue(os.path.exists(target_foo_path))
self.assertTrue(os.path.exists(target_foo_stage_file_path))
new_stage = load_yaml(target_foo_stage_file_path)
self.assertEqual(self.FOO, new_stage["outs"][0]["path"])
new_stage = (tmp_dir / "data" / "foo.dvc").load_yaml()
assert new_stage["outs"][0]["path"] == "foo"
class TestMoveDirectoryShouldNotOverwriteExisting(TestDvcGit):
def test(self):
dir_name = "dir"
orig_listdir = set(os.listdir(self.DATA_DIR))
def test_move_directory_should_not_overwrite_existing(tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"data": {"foo": "foo"}})
new_dir = tmp_dir / "dir"
new_dir.mkdir()
self.dvc.add(self.DATA_DIR)
os.mkdir(dir_name)
new_dir_name = os.path.join(dir_name, self.DATA_DIR)
self.dvc.move(self.DATA_DIR, dir_name)
data_dir_stage = self.DATA_DIR + DVC_FILE_SUFFIX
self.assertFalse(os.path.exists(self.DATA_DIR))
self.assertFalse(os.path.exists(data_dir_stage))
self.assertTrue(os.path.exists(dir_name))
self.assertEqual(
set(os.listdir(dir_name)),
{".gitignore", data_dir_stage, self.DATA_DIR},
)
self.assertTrue(os.path.exists(new_dir_name))
self.assertTrue(os.path.isfile(new_dir_name + DVC_FILE_SUFFIX))
self.assertEqual(set(os.listdir(new_dir_name)), orig_listdir)
dvc.move("data", "dir")
assert not (tmp_dir / "data").exists()
assert not (tmp_dir / "data.dvc").exists()
assert set(new_dir.iterdir()) == {
new_dir / ".gitignore",
new_dir / "data.dvc",
new_dir / "data",
}
assert set((new_dir / "data").iterdir()) == {new_dir / "data" / "foo"}
class TestMoveFileBetweenDirectories(TestDvc):
def test(self):
data_stage_file = self.DATA + DVC_FILE_SUFFIX
ret = main(["add", self.DATA])
self.assertEqual(ret, 0)
self.assertTrue(os.path.exists(data_stage_file))
def test_move_file_between_directories(tmp_dir, dvc):
tmp_dir.gen({"data": {"foo": "foo"}})
dvc.add(os.path.join("data", "foo"))
new_data_dir = "data_dir2"
os.makedirs(new_data_dir)
(tmp_dir / "data2").mkdir()
ret = main(["move", self.DATA, new_data_dir])
self.assertEqual(ret, 0)
assert main(["move", os.path.join("data", "foo"), "data2"]) == 0
assert not (tmp_dir / "data" / "foo").exists()
assert not (tmp_dir / "data" / "foo.dvc").exists()
assert (tmp_dir / "data2" / "foo").exists()
assert (tmp_dir / "data2" / "foo.dvc").exists()
new_data_path = os.path.join(new_data_dir, os.path.basename(self.DATA))
new_data_stage_file = new_data_path + DVC_FILE_SUFFIX
self.assertFalse(os.path.exists(self.DATA))
self.assertFalse(os.path.exists(data_stage_file))
self.assertTrue(os.path.exists(new_data_path))
self.assertTrue(os.path.exists(new_data_stage_file))
new_stage_file = load_yaml(new_data_stage_file)
self.assertEqual(
os.path.basename(self.DATA), new_stage_file["outs"][0]["path"]
)
d = (tmp_dir / "data2" / "foo.dvc").load_yaml()
assert d["outs"][0]["path"] == "foo"
class TestMoveFileInsideDirectory(TestDvc):
def test(self):
ret = main(["add", self.DATA])
self.assertEqual(ret, 0)
def test_move_file_inside_directory(tmp_dir, dvc):
tmp_dir.gen({"data": {"foo": "foo"}})
file = tmp_dir / "data" / "foo"
dvc.add(file.fs_path)
with cd(self.DATA_DIR):
ret = main(["move", os.path.basename(self.DATA), "data.txt"])
self.assertEqual(ret, 0)
with (tmp_dir / "data").chdir():
assert main(["move", "foo", "data.txt"]) == 0
self.assertFalse(os.path.exists(self.DATA))
data_fullpath = os.path.join(self.DATA_DIR, "data.txt")
dvc_fullpath = os.path.join(self.DATA_DIR, "data.txt.dvc")
self.assertTrue(os.path.exists(data_fullpath))
self.assertTrue(os.path.exists(dvc_fullpath))
assert not file.exists()
assert (tmp_dir / "data" / "data.txt").exists()
assert (tmp_dir / "data" / "data.txt.dvc").exists()
def test_move_should_save_stage_info(tmp_dir, dvc):
+56 -65
View File
@@ -10,58 +10,51 @@ from dvc.cli import main
from dvc.config import Config
from dvc.exceptions import DownloadError, RemoteCacheRequiredError, UploadError
from dvc.utils.fs import remove
from tests.basic_env import TestDvc
class TestRemote(TestDvc):
def test(self):
remotes = ["a", "b", "c"]
def test_remote(dvc):
remotes = ["a", "b", "c"]
self.assertEqual(main(["remote", "list"]), 0)
self.assertNotEqual(main(["remote", "remove", remotes[0]]), 0)
assert main(["remote", "list"]) == 0
assert main(["remote", "remove", remotes[0]]) != 0
for r in remotes:
self.assertEqual(
main(["remote", "add", "--default", r, "s3://bucket/name"]), 0
)
for r in remotes:
assert main(["remote", "add", "--default", r, "s3://bucket/name"]) == 0
self.assertEqual(main(["remote", "list"]), 0)
assert main(["remote", "list"]) == 0
self.assertEqual(
main(["remote", "modify", remotes[0], "checksum_jobs", "1"]), 0
)
self.assertEqual(main(["remote", "remove", remotes[0]]), 0)
assert main(["remote", "modify", remotes[0], "checksum_jobs", "1"]) == 0
assert main(["remote", "remove", remotes[0]]) == 0
self.assertEqual(main(["remote", "list"]), 0)
assert main(["remote", "list"]) == 0
def test_relative_path(self):
dname = os.path.join("..", "path", "to", "dir")
ret = main(["remote", "add", "mylocal", dname])
self.assertEqual(ret, 0)
# NOTE: we are in the repo's root and config is in .dvc/, so
# dir path written to config should be just one level above.
rel = os.path.join("..", dname)
config = configobj.ConfigObj(self.dvc.config.files["repo"])
self.assertEqual(
config['remote "mylocal"']["url"], rel.replace("\\", "/")
)
def test_remote_add_relative_path(dvc):
dname = os.path.join("..", "path", "to", "dir")
ret = main(["remote", "add", "mylocal", dname])
assert ret == 0
def test_overwrite(self):
remote_name = "a"
remote_url = "s3://bucket/name"
self.assertEqual(main(["remote", "add", remote_name, remote_url]), 0)
self.assertEqual(main(["remote", "add", remote_name, remote_url]), 251)
self.assertEqual(
main(["remote", "add", "-f", remote_name, remote_url]), 0
)
# NOTE: we are in the repo's root and config is in .dvc/, so
# dir path written to config should be just one level above.
rel = os.path.join("..", dname)
config = configobj.ConfigObj(dvc.config.files["repo"])
assert config['remote "mylocal"']["url"] == rel.replace("\\", "/")
def test_referencing_other_remotes(self):
assert main(["remote", "add", "foo", "ssh://localhost/"]) == 0
assert main(["remote", "add", "bar", "remote://foo/dvc-storage"]) == 0
config = configobj.ConfigObj(self.dvc.config.files["repo"])
assert config['remote "bar"']["url"] == "remote://foo/dvc-storage"
def test_remote_overwrite(dvc):
remote_name = "a"
remote_url = "s3://bucket/name"
assert main(["remote", "add", remote_name, remote_url]) == 0
assert main(["remote", "add", remote_name, remote_url]) == 251
assert main(["remote", "add", "-f", remote_name, remote_url]) == 0
def test_referencing_other_remotes(dvc):
assert main(["remote", "add", "foo", "ssh://localhost/"]) == 0
assert main(["remote", "add", "bar", "remote://foo/dvc-storage"]) == 0
config = configobj.ConfigObj(dvc.config.files["repo"])
assert config['remote "bar"']["url"] == "remote://foo/dvc-storage"
def test_remove_default(tmp_dir, dvc):
@@ -85,37 +78,35 @@ def test_remove_default(tmp_dir, dvc):
assert local_config.get("core", {}).get("remote") is None
class TestRemoteRemove(TestDvc):
def test(self):
ret = main(["config", "core.checksum_jobs", "1"])
self.assertEqual(ret, 0)
def test_remote_remove(dvc):
ret = main(["config", "core.checksum_jobs", "1"])
assert ret == 0
remote = "mys3"
ret = main(["remote", "add", remote, "s3://bucket/name"])
self.assertEqual(ret, 0)
remote = "mys3"
ret = main(["remote", "add", remote, "s3://bucket/name"])
assert ret == 0
ret = main(["remote", "remove", remote])
self.assertEqual(ret, 0)
ret = main(["remote", "remove", remote])
assert ret == 0
class TestRemoteDefault(TestDvc):
def test(self):
remote = "mys3"
ret = main(["remote", "add", "mys3", "s3://bucket/path"])
self.assertEqual(ret, 0)
def test_remote_default_cmd(dvc):
remote = "mys3"
ret = main(["remote", "add", "mys3", "s3://bucket/path"])
assert ret == 0
ret = main(["remote", "default", "mys3"])
self.assertEqual(ret, 0)
config_file = os.path.join(self.dvc.dvc_dir, Config.CONFIG)
config = configobj.ConfigObj(config_file)
default = config["core"]["remote"]
self.assertEqual(default, remote)
ret = main(["remote", "default", "mys3"])
assert ret == 0
config_file = os.path.join(dvc.dvc_dir, Config.CONFIG)
config = configobj.ConfigObj(config_file)
default = config["core"]["remote"]
assert default == remote
ret = main(["remote", "default", "--unset"])
self.assertEqual(ret, 0)
config = configobj.ConfigObj(config_file)
default = config.get("core", {}).get("remote")
self.assertEqual(default, None)
ret = main(["remote", "default", "--unset"])
assert ret == 0
config = configobj.ConfigObj(config_file)
default = config.get("core", {}).get("remote")
assert default is None
def test_show_default(dvc, capsys):
+2 -2
View File
@@ -220,8 +220,8 @@ def test_unique_identifier(tmp_dir, dvc, scm, run_copy):
# we get "workspace" as well from the brancher by default
assert len(revs) + 1 == len(ids)
possible_ids = {
True: "2ba7c7c5b395d4211348d6274b869fc7",
False: "8406970ad2fcafaa84d9310330a67576",
True: "dd9cf3a3dc2caac5835e64f258ac2d7b",
False: "b446f3cdc35539884dcc474d90ac6286",
}
assert set(ids) == {possible_ids[os.name == "posix"]}
+731 -740
View File
File diff suppressed because it is too large Load Diff
+14 -156
View File
@@ -9,149 +9,6 @@ from dvc.cli import main
from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
from dvc.exceptions import CyclicGraphError, ReproductionError
from dvc.stage import PipelineStage
from tests.func import test_repro
from tests.utils.scriptify import scriptify
def copy_script(src, dest):
import shutil
shutil.copyfile(src, dest)
COPY_SCRIPT, _ = scriptify(copy_script)
class MultiStageRun:
def _run(self, **kwargs):
assert kwargs.get("name")
kwargs.pop("fname", None)
# ignore fname for now
return self.dvc.run(**kwargs) # noqa, pylint: disable=no-member
@staticmethod
def _get_stage_target(stage):
return stage.addressing
class TestReproFailMultiStage(MultiStageRun, test_repro.TestReproFail):
pass
class TestReproCyclicGraphMultiStage(
MultiStageRun, test_repro.TestReproCyclicGraph
):
pass
class TestReproUnderDirMultiStage(
MultiStageRun, test_repro.TestReproDepUnderDir
):
pass
class TestReproDepDirWithOutputsUnderItMultiStage(
MultiStageRun, test_repro.TestReproDepDirWithOutputsUnderIt
):
pass
class TestReproForceMultiStage(MultiStageRun, test_repro.TestReproForce):
pass
class TestReproChangedCodeMultiStage(
MultiStageRun, test_repro.TestReproChangedCode
):
pass
class TestReproChangedDataMultiStage(
MultiStageRun, test_repro.TestReproChangedData
):
pass
class TestReproDry(MultiStageRun, test_repro.TestReproDry):
pass
class TestReproUpToDateMultiStage(MultiStageRun, test_repro.TestReproUpToDate):
pass
class TestReproChangedDeepDataMultiStage(
MultiStageRun, test_repro.TestReproChangedDeepData
):
pass
class TestReproPipelineMultiStage(MultiStageRun, test_repro.TestReproPipeline):
pass
class TestReproPipelinesMultiStage(
MultiStageRun, test_repro.TestReproPipelines
):
pass
class TestReproFrozenMultiStage(MultiStageRun, test_repro.TestReproFrozen):
pass
class TestReproFrozenCallbackMultiStage(
MultiStageRun, test_repro.TestReproFrozenCallback
):
pass
class TestReproFrozenUnchangedMultiStage(
MultiStageRun, test_repro.TestReproFrozenUnchanged
):
pass
class TestReproPhonyMultiStage(MultiStageRun, test_repro.TestReproPhony):
pass
class TestCmdReproMultiStage(MultiStageRun, test_repro.TestCmdRepro):
pass
class TestReproAllPipelinesMultiStage(
MultiStageRun, test_repro.TestReproAllPipelines
):
pass
class TestReproNoCommit(MultiStageRun, test_repro.TestReproNoCommit):
pass
class TestNonExistingOutputMultiStage(
MultiStageRun, test_repro.TestNonExistingOutput
):
pass
class TestReproAlreadyCachedMultiStage(
MultiStageRun, test_repro.TestReproAlreadyCached
):
pass
class TestReproChangedDirMultiStage(
MultiStageRun, test_repro.TestReproChangedDir
):
pass
class TestReproChangedDirDataMultiStage(
MultiStageRun, test_repro.TestReproChangedDirData
):
pass
def test_non_existing_stage_name(tmp_dir, dvc, run_copy):
@@ -300,10 +157,11 @@ def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy, mocker):
)
def test_repro_when_new_deps_is_added_in_dvcfile(tmp_dir, dvc, run_copy):
def test_repro_when_new_deps_is_added_in_dvcfile(
tmp_dir, dvc, run_copy, copy_script
):
from dvc.dvcfile import Dvcfile
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen({"foo": "foo", "bar": "bar"})
stage = dvc.run(
cmd="python copy.py {} {}".format("foo", "foobar"),
@@ -322,10 +180,9 @@ def test_repro_when_new_deps_is_added_in_dvcfile(tmp_dir, dvc, run_copy):
assert dvc.reproduce(target)[0] == stage
def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc):
def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc, copy_script):
from dvc.dvcfile import Dvcfile
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen({"foo": "foo", "bar": "bar"})
stage = dvc.run(
cmd="python copy.py {} {}".format("foo", "foobar"),
@@ -344,10 +201,9 @@ def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc):
assert dvc.reproduce(target)[0] == stage
def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
def test_repro_when_new_deps_is_moved(tmp_dir, dvc, copy_script):
from dvc.dvcfile import Dvcfile
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen({"foo": "foo", "bar": "foo"})
stage = dvc.run(
cmd="python copy.py {} {}".format("foo", "foobar"),
@@ -360,7 +216,12 @@ def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
# hardcode values in source code, ignore sys.argv
tmp_dir.gen(
"copy.py", COPY_SCRIPT.replace("func(*argv)", 'func("bar", "foobar")')
"copy.py",
"""
import shutil
shutil.copyfile('bar', 'foobar')
""",
)
from shutil import move
@@ -394,8 +255,7 @@ def test_repro_when_new_out_overlaps_others_stage_outs(tmp_dir, dvc):
dvc.reproduce(":run-copy")
def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc, copy_script):
tmp_dir.gen("foo", "foo")
(tmp_dir / PIPELINE_FILE).dump(
{
@@ -412,8 +272,7 @@ def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
dvc.reproduce(":run-copy")
def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc, copy_script):
tmp_dir.gen("foo", "foo")
(tmp_dir / PIPELINE_FILE).dump(
{
@@ -430,8 +289,7 @@ def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
dvc.reproduce(":run-copy")
def test_repro_when_lockfile_gets_deleted(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def test_repro_when_lockfile_gets_deleted(tmp_dir, dvc, copy_script):
tmp_dir.gen("foo", "foo")
(tmp_dir / PIPELINE_FILE).dump(
{
File diff suppressed because it is too large Load Diff
+53 -72
View File
@@ -1,10 +1,8 @@
import os
import tempfile
import pytest
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dvcfile import SingleStageFile
from dvc.fs import LocalFileSystem
from dvc.output import Output
@@ -15,7 +13,6 @@ from dvc.stage.utils import compute_md5
from dvc.utils import dict_md5
from dvc.utils.serialize import dump_yaml, load_yaml
from dvc.utils.strictyaml import YAMLValidationError
from tests.basic_env import TestDvc
def test_cmd_obj():
@@ -71,90 +68,74 @@ def test_list():
SingleStageFile.validate(d)
class TestReload(TestDvc):
def test(self):
stages = self.dvc.add(self.FOO)
self.assertEqual(len(stages), 1)
stage = stages[0]
self.assertTrue(stage is not None)
def test_reload(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo")
d = load_yaml(stage.relpath)
d = load_yaml(stage.relpath)
# NOTE: checking that reloaded stage didn't change its checksum
md5 = "11111111111111111111111111111111"
d[stage.PARAM_MD5] = md5
dump_yaml(stage.relpath, d)
# NOTE: checking that reloaded stage didn't change its checksum
md5 = "11111111111111111111111111111111"
d[stage.PARAM_MD5] = md5
dump_yaml(stage.relpath, d)
dvcfile = SingleStageFile(dvc, stage.relpath)
stage = dvcfile.stage
dvcfile = SingleStageFile(self.dvc, stage.relpath)
stage = dvcfile.stage
assert stage is not None
dvcfile.dump(stage)
self.assertTrue(stage is not None)
dvcfile.dump(stage)
d = load_yaml(stage.relpath)
self.assertEqual(d[stage.PARAM_MD5], md5)
d = load_yaml(stage.relpath)
assert d[stage.PARAM_MD5] == md5
class TestDefaultWorkingDirectory(TestDvc):
def test_ignored_in_checksum(self):
stage = self.dvc.run(
cmd=f"echo test > {self.FOO}",
deps=[self.BAR],
outs=[self.FOO],
single_stage=True,
)
def test_default_wdir_ignored_in_checksum(tmp_dir, dvc):
tmp_dir.gen("bar", "bar")
stage = dvc.run(
cmd="cp bar foo",
deps=["bar"],
outs=["foo"],
single_stage=True,
)
d = stage.dumpd()
self.assertNotIn(Stage.PARAM_WDIR, d.keys())
d = stage.dumpd()
assert Stage.PARAM_WDIR not in d.keys()
d = load_yaml(stage.relpath)
self.assertNotIn(Stage.PARAM_WDIR, d.keys())
d = load_yaml(stage.relpath)
assert Stage.PARAM_WDIR not in d.keys()
with self.dvc.lock:
stage = SingleStageFile(self.dvc, stage.relpath).stage
self.assertFalse(stage.changed())
with dvc.lock:
stage = SingleStageFile(dvc, stage.relpath).stage
assert not stage.changed()
class TestExternalRemoteResolution(TestDvc):
def test_remote_output(self):
tmp_path = tempfile.mkdtemp()
storage = os.path.join(tmp_path, "storage")
file_path = os.path.join(storage, "file")
def test_external_remote_output_resolution(tmp_dir, dvc, make_remote):
tmp_path = make_remote("tmp", default=False)
tmp_dir.add_remote(
url="remote://tmp/storage", name="storage", default=False
)
storage = tmp_path / "storage"
storage.mkdir()
file_path = storage / "file"
os.makedirs(storage)
dvc.run(
cmd=f"echo file > {file_path}",
outs_no_cache=["remote://storage/file"],
single_stage=True,
)
assert os.path.exists(file_path)
assert main(["remote", "add", "tmp", tmp_path]) == 0
assert main(["remote", "add", "storage", "remote://tmp/storage"]) == 0
assert (
main(
[
"run",
"--single-stage",
"-O",
"remote://storage/file",
f"echo file > {file_path}",
]
)
== 0
)
assert os.path.exists(file_path)
def test_external_remote_dependency_resolution(tmp_dir, dvc, make_remote):
tmp_path = make_remote("tmp", default=False)
tmp_dir.add_remote(
url="remote://tmp/storage", name="storage", default=False
)
storage = tmp_path / "storage"
storage.mkdir()
file_path = storage / "file"
file_path.write_text("Isle of Dogs", encoding="utf-8")
def test_remote_dependency(self):
tmp_path = tempfile.mkdtemp()
storage = os.path.join(tmp_path, "storage")
file_path = os.path.join(storage, "file")
os.makedirs(storage)
with open(file_path, "w", encoding="utf-8") as fobj:
fobj.write("Isle of Dogs")
assert main(["remote", "add", "tmp", tmp_path]) == 0
assert main(["remote", "add", "storage", "remote://tmp/storage"]) == 0
assert main(["import-url", "remote://storage/file", "movie.txt"]) == 0
assert os.path.exists("movie.txt")
dvc.imp_url("remote://storage/file", "movie.txt")
assert (tmp_dir / "movie.txt").read_text() == "Isle of Dogs"
def test_md5_ignores_comments(tmp_dir, dvc):
+20 -30
View File
@@ -1,37 +1,27 @@
import os
from dvc.cli import main
from tests.basic_env import TestDvc
def test_unprotect(tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
class TestUnprotect(TestDvc):
def test(self):
ret = main(["config", "cache.type", "hardlink"])
self.assertEqual(ret, 0)
dvc.odb.local.cache_types = ["hardlink"]
dvc.add("foo")
cache = os.path.join(
".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
)
assert not os.access("foo", os.W_OK)
assert not os.access(cache, os.W_OK)
ret = main(["add", self.FOO])
self.assertEqual(ret, 0)
dvc.unprotect("foo")
assert os.access("foo", os.W_OK)
cache = os.path.join(
".dvc", "cache", "ac", "bd18db4cc2f85cedef654fccc4a4d8"
)
if os.name == "nt":
# NOTE: cache is now unprotected, because NTFS doesn't allow
# deleting read-only files, so we have to try to set write perms
# on files that we try to delete, which propagates to the cache
# file. But it should be restored after the next cache check, hence
# why we call `dvc status` here.
assert os.access(cache, os.W_OK)
dvc.status()
self.assertFalse(os.access(self.FOO, os.W_OK))
self.assertFalse(os.access(cache, os.W_OK))
ret = main(["unprotect", self.FOO])
self.assertEqual(ret, 0)
self.assertTrue(os.access(self.FOO, os.W_OK))
if os.name == "nt":
# NOTE: cache is now unprotected, because NTFS doesn't allow
# deleting read-only files, so we have to try to set write perms
# on files that we try to delete, which propagates to the cache
# file. But it should be restored after the next cache check, hence
# why we call `dvc status` here.
self.assertTrue(os.access(cache, os.W_OK))
ret = main(["status"])
self.assertEqual(ret, 0)
self.assertFalse(os.access(cache, os.W_OK))
assert not os.access(cache, os.W_OK)
+1 -1
View File
@@ -16,7 +16,7 @@ def test_(tmp_dir, dvc, scm, capsys):
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 re.search(rf"{subproject} = .*", out)
assert find_supported_remotes(out)
assert re.search(r"Cache types: .*", out)
+1 -1
View File
@@ -44,7 +44,7 @@ def extract_vega_specs(html_path, plots_ids):
result = {}
with open(html_path, "r", encoding="utf-8") as fd:
with open(html_path, encoding="utf-8") as fd:
content = fd.read()
reader = BeautifulSoup(content, features="html.parser")
+59
View File
@@ -0,0 +1,59 @@
import pytest
COPY_SCRIPT = """
import os
import shutil
import sys
if os.path.isfile(sys.argv[1]):
shutil.copyfile(sys.argv[1], sys.argv[2])
else:
shutil.copytree(sys.argv[1], sys.argv[2])
"""
def _add_script(tmp_dir, path, contents=""):
script, *_ = tmp_dir.gen(path, contents)
if hasattr(tmp_dir, "scm"):
tmp_dir.scm_add(path, commit=f"add {path}")
return script.fs_path
@pytest.fixture
def append_foo_script(tmp_dir):
return _add_script(
tmp_dir,
"append_foo.py",
"""
import sys
from pathlib import Path
with Path(sys.argv[1]).open("a+", encoding="utf-8") as f:
f.write("foo")
""",
)
@pytest.fixture
def copy_script(
tmp_dir,
):
return _add_script(tmp_dir, "copy.py", COPY_SCRIPT)
@pytest.fixture
def head_script(tmp_dir):
"""Output first line of each file to different file with '-1' appended.
Useful for tracking multiple outputs/dependencies which are not a copy
of each others.
"""
return _add_script(
tmp_dir,
"head.py",
"""
import sys
for file in sys.argv[1:]:
with open(file) as f, open(file +"-1","w+") as w:
w.write(f.readline())
""",
)
+8 -8
View File
@@ -39,7 +39,7 @@ def test_default(mocker, capsys):
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
assert (
"Added:\n"
" file\n"
@@ -77,7 +77,7 @@ def test_show_hash(mocker, capsys):
"not in cache": [],
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert (
@@ -115,7 +115,7 @@ def test_show_json(mocker, capsys):
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
assert '"deleted": []' in out
@@ -145,7 +145,7 @@ def test_show_json_and_hash(mocker, capsys):
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert (
'"added": [{"path": "file1", "hash": "11111111"}, '
@@ -180,7 +180,7 @@ def test_show_json_hide_missing(mocker, capsys):
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
assert '"deleted": []' in out
@@ -200,7 +200,7 @@ def test_diff_show_markdown_and_hash(mocker, show_hash):
mock_show_markdown = mocker.patch("dvc.commands.diff._show_markdown")
mocker.patch("dvc.repo.Repo.diff", return_value=diff.copy())
assert 0 == cmd.run()
assert cmd.run() == 0
mock_show_markdown.assert_called_once_with(diff, show_hash, False)
@@ -226,7 +226,7 @@ def test_no_changes(mocker, capsys, opts, show, expected):
cmd = args.func(args)
mocker.patch("dvc.repo.Repo.diff", return_value={})
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert expected == out.strip()
@@ -349,7 +349,7 @@ def test_hide_missing(mocker, capsys):
}
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
assert 0 == cmd.run()
assert cmd.run() == 0
out, _ = capsys.readouterr()
assert (
"Added:\n"
-18
View File
@@ -23,12 +23,6 @@ def test_run(mocker, dvc):
"plots",
"--plots-no-cache",
"plots-no-cache",
"--live",
"live",
"--live-no-cache",
"live-no-cache",
"--live-no-summary",
"--live-no-html",
"--file",
"file",
"--wdir",
@@ -74,10 +68,6 @@ def test_run(mocker, dvc):
outs_persist_no_cache=["outs-persist-no-cache"],
checkpoints=["checkpoints"],
params=["file:param1,param2", "param3"],
live="live",
live_no_cache="live-no-cache",
live_no_summary=True,
live_no_html=True,
fname="file",
wdir="wdir",
no_exec=True,
@@ -107,10 +97,6 @@ def test_run_args_from_cli(mocker, dvc):
metrics_no_cache=[],
plots=[],
plots_no_cache=[],
live=None,
live_no_cache=None,
live_no_summary=False,
live_no_html=False,
outs_persist=[],
outs_persist_no_cache=[],
checkpoints=[],
@@ -144,10 +130,6 @@ def test_run_args_with_spaces(mocker, dvc):
metrics_no_cache=[],
plots=[],
plots_no_cache=[],
live=None,
live_no_cache=None,
live_no_summary=False,
live_no_html=False,
outs_persist=[],
outs_persist_no_cache=[],
checkpoints=[],
-7
View File
@@ -35,10 +35,6 @@ def test_stage_add(mocker, dvc, command, parsed_command):
"plots",
"--plots-no-cache",
"plots-no-cache",
"--live",
"live",
"--live-no-summary",
"--live-no-html",
"--wdir",
"wdir",
"--force",
@@ -80,9 +76,6 @@ def test_stage_add(mocker, dvc, command, parsed_command):
metrics_no_cache=["metrics-no-cache"],
plots=["plots"],
plots_no_cache=["plots-no-cache"],
live="live",
live_no_summary=True,
live_no_html=True,
wdir="wdir",
outs_persist=["outs-persist"],
outs_persist_no_cache=["outs-persist-no-cache"],
+3 -3
View File
@@ -7,6 +7,6 @@ from dvc.stage import Stage
def test_save_missing(dvc, mocker):
stage = Stage(dvc)
dep = Dependency(stage, "path")
with mocker.patch.object(dep.fs, "exists", return_value=False):
with pytest.raises(dep.DoesNotExistError):
dep.save()
mocker.patch.object(dep.fs, "exists", return_value=False)
with pytest.raises(dep.DoesNotExistError):
dep.save()
+15 -20
View File
@@ -1,12 +1,10 @@
import os
from unittest.mock import patch
from dvc.output import Output
from dvc.stage import Stage
from dvc.utils import relpath
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.meta import Meta
from tests.basic_env import TestDvc
def test_str_workdir_outside_repo(tmp_dir, erepo_dir):
@@ -52,24 +50,21 @@ def test_str_on_external_absolute_path(dvc):
assert str(output) == abs_path
class TestGetFilesNumber(TestDvc):
def _get_output(self):
stage = Stage(self.dvc)
return Output(stage, "path")
def test_return_0_on_no_cache(dvc):
o = Output(Stage(dvc), "path")
o.use_cache = False
assert o.get_files_number() == 0
def test_return_0_on_no_cache(self):
o = self._get_output()
o.use_cache = False
self.assertEqual(0, o.get_files_number())
def test_return_multiple_for_dir(self):
o = self._get_output()
o.hash_info = HashInfo("md5", "12345678.dir")
o.meta = Meta(nfiles=2)
self.assertEqual(2, o.get_files_number())
def test_return_multiple_for_dir(dvc):
o = Output(Stage(dvc), "path")
o.hash_info = HashInfo("md5", "12345678.dir")
o.meta = Meta(nfiles=2)
assert o.get_files_number() == 2
@patch.object(Output, "is_dir_checksum", False)
def test_return_1_on_single_file_cache(self):
o = self._get_output()
o.hash_info = HashInfo("md5", "12345678")
self.assertEqual(1, o.get_files_number())
def test_return_1_on_single_file_cache(mocker, dvc):
mocker.patch.object(Output, "is_dir_checksum", False)
o = Output(Stage(dvc), "path")
o.hash_info = HashInfo("md5", "12345678")
assert o.get_files_number() == 1
+7 -7
View File
@@ -15,9 +15,9 @@ from dvc.utils.fs import remove
def test_save_missing(dvc, mocker):
stage = Stage(dvc)
out = Output(stage, "path", cache=False)
with mocker.patch.object(out.fs, "exists", return_value=False):
with pytest.raises(out.DoesNotExistError):
out.save()
mocker.patch.object(out.fs, "exists", return_value=False)
with pytest.raises(out.DoesNotExistError):
out.save()
@pytest.mark.parametrize(
@@ -115,9 +115,9 @@ def test_remote_missing_dependency_on_dir_pull(tmp_dir, scm, dvc, mocker):
remove("dir")
remove(dvc.odb.local.path)
with mocker.patch(
mocker.patch(
"dvc.data_cloud.DataCloud.get_remote_odb",
side_effect=RemoteMissingDepsError(dvc.fs, "azure", "azure://", []),
):
with pytest.raises(RemoteMissingDepsError):
dvc.pull()
)
with pytest.raises(RemoteMissingDepsError):
dvc.pull()
+2 -2
View File
@@ -50,11 +50,11 @@ def test_wait(tmp_dir, mocker):
proc._proc.wait = mocker.Mock(return_value=None)
proc._proc.returncode = 0
assert 0 == proc.wait()
assert proc.wait() == 0
proc._proc.wait.assert_called_once()
# once subprocess return code is set, future ManagedProcess.wait() calls
# should not block
proc._proc.wait.reset_mock()
assert 0 == proc.wait()
assert proc.wait() == 0
proc._proc.wait.assert_not_called()
+43 -53
View File
@@ -1,6 +1,5 @@
import math
import posixpath
from unittest import mock
from dvc.fs import FileSystem
from dvc_data.hashfile.db import ObjectDB
@@ -16,59 +15,54 @@ class _CallableOrNone:
CallableOrNone = _CallableOrNone()
@mock.patch.object(ObjectDB, "_list_oids_traverse")
@mock.patch.object(ObjectDB, "list_oids_exists")
def test_oids_exist(object_exists, traverse, dvc):
def test_oids_exist(mocker, dvc):
traverse = mocker.patch.object(ObjectDB, "_list_oids_traverse")
object_exists = mocker.patch.object(ObjectDB, "list_oids_exists")
odb = ObjectDB(FileSystem(), None)
# remote does not support traverse
odb.fs.CAN_TRAVERSE = False
with mock.patch.object(odb, "_list_oids", return_value=list(range(256))):
oids = set(range(1000))
odb.oids_exist(oids)
object_exists.assert_called_with(oids, None)
traverse.assert_not_called()
mocker.patch.object(odb, "_list_oids", return_value=list(range(256)))
oids = set(range(1000))
odb.oids_exist(oids)
object_exists.assert_called_with(oids, None)
traverse.assert_not_called()
odb.fs.CAN_TRAVERSE = True
# large remote, small local
object_exists.reset_mock()
traverse.reset_mock()
with mock.patch.object(odb, "_list_oids", return_value=list(range(2048))):
oids = list(range(1000))
odb.oids_exist(oids)
# verify that _odb_paths_with_max() short circuits
# before returning all 2048 remote oids
max_oids = math.ceil(
odb._max_estimation_size(oids)
/ pow(16, odb.fs.TRAVERSE_PREFIX_LEN)
)
assert max_oids < 2048
object_exists.assert_called_with(
frozenset(range(max_oids, 1000)), None
)
traverse.assert_not_called()
mocker.patch.object(odb, "_list_oids", return_value=list(range(2048)))
oids = list(range(1000))
odb.oids_exist(oids)
# verify that _odb_paths_with_max() short circuits
# before returning all 2048 remote oids
max_oids = math.ceil(
odb._max_estimation_size(oids) / pow(16, odb.fs.TRAVERSE_PREFIX_LEN)
)
assert max_oids < 2048
object_exists.assert_called_with(frozenset(range(max_oids, 1000)), None)
traverse.assert_not_called()
# large remote, large local
object_exists.reset_mock()
traverse.reset_mock()
odb.fs._JOBS = 16
with mock.patch.object(odb, "_list_oids", return_value=list(range(256))):
oids = list(range(1000000))
odb.oids_exist(oids)
object_exists.assert_not_called()
traverse.assert_called_with(
256 * pow(16, odb.fs.TRAVERSE_PREFIX_LEN),
set(range(256)),
jobs=None,
)
mocker.patch.object(odb, "_list_oids", return_value=list(range(256)))
oids = list(range(1000000))
odb.oids_exist(oids)
object_exists.assert_not_called()
traverse.assert_called_with(
256 * pow(16, odb.fs.TRAVERSE_PREFIX_LEN),
set(range(256)),
jobs=None,
)
@mock.patch.object(ObjectDB, "_list_oids", return_value=[])
@mock.patch.object(ObjectDB, "path_to_oid", side_effect=lambda x: x)
def test_list_oids_traverse(
path_to_oid, list_oids, dvc # pylint: disable=unused-argument
):
def test_list_oids_traverse(mocker, dvc): # pylint: disable=unused-argument
list_oids = mocker.patch.object(ObjectDB, "_list_oids", return_value=[])
odb = ObjectDB(FileSystem(), None)
odb.fs_path = "foo"
@@ -87,28 +81,24 @@ def test_list_oids_traverse(
list_oids.assert_called_with(None)
def test_list_oids(dvc):
def test_list_oids(mocker, dvc):
odb = ObjectDB(FileSystem(), None)
odb.fs_path = "foo"
with mock.patch.object(
odb, "_list_paths", return_value=["12/3456", "bar"]
):
oids = list(odb._list_oids())
assert oids == ["123456"]
mocker.patch.object(odb, "_list_paths", return_value=["12/3456", "bar"])
oids = list(odb._list_oids())
assert oids == ["123456"]
def test_list_paths(dvc):
def test_list_paths(mocker, dvc):
path = "foo"
odb = ObjectDB(FileSystem(), path)
with mock.patch.object(odb.fs, "find", return_value=[]) as walk_mock:
for _ in odb._list_paths():
pass
walk_mock.assert_called_with(path, prefix=False)
walk_mock = mocker.patch.object(odb.fs, "find", return_value=[])
for _ in odb._list_paths():
pass
walk_mock.assert_called_with(path, prefix=False)
for _ in odb._list_paths(prefix="000"):
pass
walk_mock.assert_called_with(
posixpath.join(path, "00", "0"), prefix=True
)
for _ in odb._list_paths(prefix="000"):
pass
walk_mock.assert_called_with(posixpath.join(path, "00", "0"), prefix=True)
+1 -2
View File
@@ -1,5 +1,4 @@
import ssl
from unittest.mock import patch
from dvc.fs import HTTPFileSystem
@@ -64,8 +63,8 @@ def test_ssl_verify_disable(dvc):
assert not fs.fs_args["client_kwargs"]["connector"]._ssl
@patch("ssl.SSLContext.load_verify_locations")
def test_ssl_verify_custom_cert(dvc, mocker):
mocker.patch("ssl.SSLContext.load_verify_locations")
config = {
"url": "http://example.com/",
"path": "file.html",
+4 -5
View File
@@ -1,5 +1,3 @@
from unittest.mock import patch
import pytest
from dvc_webdav import WebDAVFileSystem, WebDAVSFileSystem
@@ -66,9 +64,10 @@ def test_token():
)
@patch("dvc_webdav.ask_password")
def test_ask_password(ask_password_mocked):
ask_password_mocked.return_value = "pass"
def test_ask_password(mocker):
ask_password_mocked = mocker.patch(
"dvc_webdav.ask_password", return_value="pass"
)
host = "host"
# it should not ask for password as password is set
+5 -5
View File
@@ -1,5 +1,3 @@
from unittest.mock import Mock, create_autospec
import pytest
import requests
from dvc_webhdfs import WebHDFSFileSystem
@@ -42,9 +40,11 @@ def test_init(dvc, webhdfs_config):
assert fs.fs_args["use_https"] == use_https
def test_verify_ssl(dvc, webhdfs_config, monkeypatch):
mock_session = create_autospec(requests.Session)
monkeypatch.setattr(requests, "Session", Mock(return_value=mock_session))
def test_verify_ssl(dvc, webhdfs_config, monkeypatch, mocker):
mock_session = mocker.create_autospec(requests.Session)
monkeypatch.setattr(
requests, "Session", mocker.Mock(return_value=mock_session)
)
# can't have token at the same time as user or proxy_to
del webhdfs_config["token"]
fs = WebHDFSFileSystem(**webhdfs_config)
+26 -17
View File
@@ -1,10 +1,9 @@
from contextlib import contextmanager
from functools import partial
from textwrap import dedent
import pytest
from dvc.repo.experiments.queue.celery import LocalCeleryQueue
from tests.func.test_repro_multistage import COPY_SCRIPT
DEFAULT_ITERATIONS = 2
CHECKPOINT_SCRIPT_FORMAT = dedent(
@@ -44,8 +43,7 @@ CHECKPOINT_SCRIPT = CHECKPOINT_SCRIPT_FORMAT.format(
@pytest.fixture
def exp_stage(tmp_dir, scm, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def exp_stage(tmp_dir, scm, dvc, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
@@ -93,8 +91,7 @@ def checkpoint_stage(tmp_dir, scm, dvc, mocker):
@pytest.fixture
def failed_exp_stage(tmp_dir, scm, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
def failed_exp_stage(tmp_dir, scm, dvc, copy_script):
tmp_dir.gen("params.yaml", "foo: 1")
stage = dvc.stage.add(
cmd="python -c 'import sys; sys.exit(1)'",
@@ -117,15 +114,13 @@ def failed_exp_stage(tmp_dir, scm, dvc):
return stage
@contextmanager
def _thread_worker(app, **kwargs):
# Based on pytest-celery's celery_worker fixture but using thread pool
# instead of solo pool so that broadcast/control API is available
from celery.contrib.testing import worker
app.loader.import_task_module("celery.contrib.testing.tasks")
with worker.start_worker(app, pool="threads", **kwargs) as test_worker:
yield test_worker
return worker.start_worker(app, pool="threads", **kwargs)
@pytest.fixture
@@ -134,12 +129,26 @@ def test_queue(tmp_dir, dvc, scm, mocker) -> LocalCeleryQueue:
Test queue worker runs for the duration of the test in separate thread(s).
"""
celery_queue = dvc.experiments.celery_queue
mocker.patch.object(celery_queue, "spawn_worker")
with _thread_worker(
celery_queue.celery,
import celery
queue = dvc.experiments.celery_queue
mocker.patch.object(queue, "spawn_worker")
f = partial(
_thread_worker,
queue.celery,
concurrency=1,
ping_task_timeout=30,
) as worker:
mocker.patch.object(celery_queue, "worker", return_value=worker)
yield celery_queue
ping_task_timeout=20,
)
exc = None
for _ in range(3):
try:
with f() as worker:
mocker.patch.object(queue, "worker", return_value=worker)
yield queue
return
except celery.exceptions.TimeoutError as e:
exc = e
continue
assert exc
raise exc
+3 -3
View File
@@ -180,14 +180,14 @@ def test_shared_stage_cache(tmp_dir, dvc, run_copy):
run_copy("foo", "bar", name="copy-foo-bar")
parent_cache_dir = os.path.join(dvc.stage_cache.cache_dir, "88")
parent_cache_dir = os.path.join(dvc.stage_cache.cache_dir, "fd")
cache_dir = os.path.join(
parent_cache_dir,
"883395068439203a9de3d1e1649a16e9027bfd1ab5dab4f438d321c4a928b328",
"fdbe7847136ebe88f59a29ee5a568f893cab031f74f7d3ab050828b53fd0033a",
)
cache_file = os.path.join(
cache_dir,
"e42b7ebb9bc5ac4bccab769c8d1338914dad25d7ffecc8671dbd4581bad4aa15",
"8716052b2074f5acf1a379529d47d6ef1c1f50c2281a7489b8d7ce251f234f86",
)
# sanity check
+3 -3
View File
@@ -1,6 +1,5 @@
import json
import platform
from unittest import mock
import pytest
from voluptuous import Any, Schema
@@ -57,8 +56,9 @@ def test_runtime_info(tmp_global_dir):
assert schema(analytics._runtime_info())
@mock.patch("requests.post")
def test_send(mock_post, tmp_path):
def test_send(mocker, tmp_path):
mock_post = mocker.patch("requests.post")
import requests
url = "https://analytics.dvc.org"

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