mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-03 15:28:29 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc689954f | ||
|
|
076c48532d | ||
|
|
e4ea463c73 | ||
|
|
87d8269365 | ||
|
|
a753ee215f | ||
|
|
014a38fae2 | ||
|
|
8913f5a6fe | ||
|
|
4ce9825c2b | ||
|
|
e9c5b5fa84 | ||
|
|
fa54c1af7c | ||
|
|
9fbbfd07a6 | ||
|
|
929de7c651 | ||
|
|
49af6e1435 | ||
|
|
a6bfc7db4e | ||
|
|
872a0d663e | ||
|
|
c01583f3dd | ||
|
|
e0d41c7012 | ||
|
|
628b992442 | ||
|
|
99e7f52bbd | ||
|
|
b2a275ee9e | ||
|
|
7e423b668f | ||
|
|
2c3956bdb0 | ||
|
|
a48a19ce31 | ||
|
|
1de983f0de | ||
|
|
eb20a8e049 | ||
|
|
78e25747a4 | ||
|
|
240f0ebdc3 | ||
|
|
69f78d3bb6 | ||
|
|
740fe504c9 | ||
|
|
7bb84286f5 | ||
|
|
3891e81bdd | ||
|
|
bb62833897 | ||
|
|
fb703150ce | ||
|
|
33d4ead8b6 | ||
|
|
76b1f8c907 | ||
|
|
d7152e360e | ||
|
|
ab69b88a21 | ||
|
|
43a8eab2e0 | ||
|
|
15f35e827e | ||
|
|
4d17c171f1 | ||
|
|
c27005a8b7 | ||
|
|
ecd371f8ae | ||
|
|
bda1388bff | ||
|
|
c02bd7f318 | ||
|
|
fe1d805c80 | ||
|
|
f9b0de883f | ||
|
|
1cad8ea7fd | ||
|
|
08a643e005 | ||
|
|
97a8b6978c | ||
|
|
b44099367e | ||
|
|
29b3dc1f9e | ||
|
|
6e73bd0b5b | ||
|
|
fa5a9abdeb | ||
|
|
30775f7b31 | ||
|
|
e07aca9fd3 | ||
|
|
d2265f36ba | ||
|
|
d548618291 | ||
|
|
48ff332f4a | ||
|
|
97f5ffc3c6 | ||
|
|
f6849d2f06 | ||
|
|
a172240536 | ||
|
|
129a4fbb92 | ||
|
|
c101514677 | ||
|
|
5e883bc00c | ||
|
|
4b720e5820 | ||
|
|
c0054fa57e | ||
|
|
6049494cef | ||
|
|
120a5879bb | ||
|
|
c8e39523c3 | ||
|
|
7648e715f8 | ||
|
|
ca5ad6457f | ||
|
|
de848d9598 | ||
|
|
4330f1a70f | ||
|
|
144e21b552 | ||
|
|
7945428e5e | ||
|
|
6df70d388e | ||
|
|
4ca2158b9d | ||
|
|
f87e78d754 | ||
|
|
5899440b46 |
@@ -1,10 +0,0 @@
|
||||
comment: false
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90
|
||||
threshold: 10%
|
||||
patch:
|
||||
default:
|
||||
threshold: 25%
|
||||
-16
@@ -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,4 +1,3 @@
|
||||
/tests
|
||||
/dvc
|
||||
/.github
|
||||
/bin
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
codecov:
|
||||
notify:
|
||||
wait_for_ci: true
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 2%
|
||||
patch: off
|
||||
github_checks:
|
||||
annotations: false
|
||||
@@ -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: ["*"]
|
||||
@@ -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'"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '32 19 * * 2'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['python']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -7,8 +7,13 @@ on:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -24,13 +29,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
setup.cfg
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
@@ -45,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install -U wheel pip setuptools
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
@@ -77,21 +82,31 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
|
||||
- name: Install
|
||||
run: pip install --upgrade pip wheel
|
||||
|
||||
- name: Force version for Test PyPI uploads
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
pip install -U wheel pip build setuptools
|
||||
pip install ".[all,tests]"
|
||||
pip install setuptools_scm
|
||||
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
run: ./scripts/build_package.sh
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
|
||||
@@ -17,6 +17,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
timeout-minutes: 10
|
||||
@@ -29,13 +32,12 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install collective.checkdocs==0.2
|
||||
pip install --upgrade pip wheel
|
||||
pip install ".[dev]"
|
||||
- name: Check README
|
||||
run: python setup.py checkdocs
|
||||
- uses: pre-commit/[email protected]
|
||||
tests:
|
||||
timeout-minutes: 45
|
||||
@@ -43,13 +45,16 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
pytest-filter:
|
||||
os: [ubuntu-20.04, windows-latest]
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11"]
|
||||
pytest-filter:
|
||||
- "import or plot or live or experiment"
|
||||
- "not (import or plot or live or experiment)"
|
||||
include:
|
||||
- {os: ubuntu-latest, pyv: "3.11-dev"}
|
||||
- {os: macos-latest, pyv: "3.8"}
|
||||
- {os: macos-latest, pyv: "3.9"}
|
||||
- {os: macos-latest, pyv: "3.10"}
|
||||
- {os: macos-latest, pyv: "3.11"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -60,25 +65,30 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: install
|
||||
if: steps.cache.pip-cache-dir.cache-hit != 'true'
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install --upgrade pip wheel
|
||||
pip install -e ".[dev]"
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
|
||||
timeout-minutes: 20
|
||||
run: pytest -nauto --durations 100 --cov --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
check:
|
||||
if: always()
|
||||
needs: [lint, tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
notify:
|
||||
if: github.ref == 'refs/heads/main' && failure()
|
||||
needs:
|
||||
- lint
|
||||
- tests
|
||||
needs: [lint, tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
|
||||
+43
-28
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
include LICENSE
|
||||
recursive-include requirements *.txt
|
||||
recursive-include dvc/repo/plots/templates *.json
|
||||
+6
-2
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo OFF
|
||||
set PYTHONPATH=%DVC_HOME%
|
||||
python -m dvc %*
|
||||
+1
-1
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,9 +2,6 @@ DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_PATH = "DVCLIVE_PATH"
|
||||
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
|
||||
DVCLIVE_HTML = "DVCLIVE_HTML"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
|
||||
+10
-3
@@ -132,10 +132,17 @@ class NotDvcRepoError(DvcException):
|
||||
class CyclicGraphError(DvcException):
|
||||
def __init__(self, stages):
|
||||
assert isinstance(stages, list)
|
||||
msg = "Pipeline has a cycle involving: {}.".format(
|
||||
", ".join(s.addressing for s in stages)
|
||||
stage_part = "stage" if len(stages) == 1 else "stages"
|
||||
msg = (
|
||||
"Same item(s) are defined as both a dependency and an output "
|
||||
"in {stage_part}: {stage}."
|
||||
)
|
||||
super().__init__(
|
||||
msg.format(
|
||||
stage_part=stage_part,
|
||||
stage=", ".join(s.addressing for s in stages),
|
||||
)
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ConfirmRemoveError(DvcException):
|
||||
|
||||
@@ -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
@@ -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 and dep.files:
|
||||
dep.obj = dep.get_obj()
|
||||
entry.obj = dep.obj
|
||||
return entry
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
@@ -631,8 +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 = {
|
||||
|
||||
@@ -36,7 +36,7 @@ from .interpolate import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import NoReturn
|
||||
from typing import NoReturn
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -56,10 +56,13 @@ def commit(
|
||||
]
|
||||
for stage_info in stages_info:
|
||||
stage = stage_info.stage
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info, allow_missing=allow_missing
|
||||
)
|
||||
|
||||
+5
-3
@@ -12,6 +12,7 @@ from typing import (
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
@@ -19,10 +20,9 @@ from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.base import Base
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
@@ -140,7 +140,9 @@ class GitInfo(TypedDict, total=False):
|
||||
is_dirty: bool
|
||||
|
||||
|
||||
def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
def _git_info(
|
||||
scm: Union["Git", "NoSCM"], untracked_files: str = "all"
|
||||
) -> GitInfo:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
import time
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
from funcy import cached_property, chain, first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -171,7 +171,18 @@ class Experiments:
|
||||
) -> Dict[str, str]:
|
||||
results: Dict[str, str] = {}
|
||||
if entries is None:
|
||||
entries = list(self.celery_queue.iter_queued())
|
||||
entries = list(
|
||||
chain(
|
||||
self.celery_queue.iter_active(),
|
||||
self.celery_queue.iter_queued(),
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"reproduce all these entries '%s'",
|
||||
entries,
|
||||
)
|
||||
|
||||
if not entries:
|
||||
return results
|
||||
|
||||
@@ -189,7 +200,10 @@ class Experiments:
|
||||
time.sleep(1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
result = self.celery_queue.get_result(entry)
|
||||
try:
|
||||
result = self.celery_queue.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
result = None
|
||||
if result is None or result.exp_hash is None:
|
||||
name = entry.name or entry.stash_rev[:7]
|
||||
failed.append(name)
|
||||
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
from dvc.repo.experiments.show import _collect_experiment_commit
|
||||
from dvc.repo.experiments.show import collect_experiment_commit
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if repo.scm.no_commits:
|
||||
@@ -15,15 +15,15 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
|
||||
if a_rev:
|
||||
rev = resolve_rev(repo.scm, a_rev)
|
||||
old = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
old = _collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
|
||||
if b_rev:
|
||||
rev = resolve_rev(repo.scm, b_rev)
|
||||
new = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
new = _collect_experiment_commit(
|
||||
new = collect_experiment_commit(
|
||||
repo, "workspace", param_deps=param_deps
|
||||
)
|
||||
|
||||
|
||||
@@ -602,29 +602,22 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
finally:
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
super().cleanup(infofile)
|
||||
|
||||
def collect_cache(
|
||||
self, repo: "Repo", exp_ref: "ExpRefInfo", run_cache: bool = True
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,12 +23,7 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import UnresolvedQueueExpNamesError
|
||||
from ..executor.base import (
|
||||
EXEC_TMP_DIR,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
from .tasks import run_exp
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
@@ -187,6 +182,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
continue
|
||||
args, kwargs, _embed = msg.decode()
|
||||
entry_dict = kwargs.get("entry_dict", args[0])
|
||||
logger.debug("Found queued task %s", entry_dict["stash_rev"])
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
|
||||
@@ -203,6 +199,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
@@ -211,6 +208,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -243,48 +241,50 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_result(
|
||||
def _load_info(self, rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _get_done_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional[ExecutorResult]:
|
||||
) -> Optional["ExecutorResult"]:
|
||||
from celery.exceptions import TimeoutError as _CeleryTimeout
|
||||
|
||||
def _load_info(rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _load_collected(rev: str) -> Optional[ExecutorResult]:
|
||||
executor_info = _load_info(rev)
|
||||
if executor_info.status > TaskStatus.SUCCESS:
|
||||
for msg, processed_entry in self._iter_processed():
|
||||
if entry.stash_rev == processed_entry.stash_rev:
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = self._load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
raise FileNotFoundError
|
||||
raise FileNotFoundError
|
||||
|
||||
def get_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional["ExecutorResult"]:
|
||||
|
||||
try:
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
except FileNotFoundError:
|
||||
# Infofile will not be created until execution begins
|
||||
pass
|
||||
|
||||
for queue_entry in self.iter_queued():
|
||||
if entry.stash_rev == queue_entry.stash_rev:
|
||||
raise DvcException("Experiment has not been started.")
|
||||
for result, active_entry in self._iter_active_tasks():
|
||||
if entry.stash_rev == active_entry.stash_rev:
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = _load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
|
||||
# NOTE: It's possible for an exp to complete while iterating through
|
||||
# other queued and active tasks, in which case the exp will get moved
|
||||
# out of the active task list, and needs to be loaded here.
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
def kill(self, revs: Collection[str]) -> None:
|
||||
to_kill: Set[QueueEntry] = set()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from ..executor.base import ExecutorInfo, TaskStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -52,7 +54,7 @@ def fetch_running_exp_from_temp_dir(
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
except (InvalidRemoteSCMRepo, SCMError):
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
return result
|
||||
|
||||
+18
-15
@@ -28,14 +28,24 @@ def run(
|
||||
Returns a dict mapping new experiment SHAs to the results
|
||||
of `repro` for that experiment.
|
||||
"""
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
if run_all:
|
||||
entries = list(repo.experiments.celery_queue.iter_queued())
|
||||
return repo.experiments.reproduce_celery(entries, jobs=jobs)
|
||||
return repo.experiments.reproduce_celery(jobs=jobs)
|
||||
|
||||
hydra_sweep = None
|
||||
if params:
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
path_overrides = to_path_overrides(params)
|
||||
hydra_sweep = any(
|
||||
x.is_sweep_override()
|
||||
for param_file in path_overrides
|
||||
for x in to_hydra_overrides(path_overrides[param_file])
|
||||
)
|
||||
|
||||
if hydra_sweep and not queue:
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used without `--queue`"
|
||||
)
|
||||
else:
|
||||
path_overrides = {}
|
||||
|
||||
@@ -45,23 +55,16 @@ def run(
|
||||
# Force `_update_params` even if `--set-param` was not used
|
||||
path_overrides[hydra_output_file] = []
|
||||
|
||||
hydra_sweep = any(
|
||||
x.is_sweep_override()
|
||||
for param_file in path_overrides
|
||||
for x in to_hydra_overrides(path_overrides[param_file])
|
||||
)
|
||||
|
||||
if hydra_sweep and not queue:
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used without `--queue`"
|
||||
)
|
||||
|
||||
if not queue:
|
||||
return repo.experiments.reproduce_one(
|
||||
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
|
||||
)
|
||||
|
||||
if hydra_sweep:
|
||||
if kwargs.get("name"):
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used alongside `--name`"
|
||||
)
|
||||
from dvc.utils.hydra import get_hydra_sweeps
|
||||
|
||||
sweeps = get_hydra_sweeps(path_overrides)
|
||||
|
||||
+137
-83
@@ -3,16 +3,29 @@ from collections import OrderedDict, defaultdict
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from scmrepo.exceptions import SCMError as InnerScmError
|
||||
|
||||
from dvc.repo.metrics.show import _gather_metrics
|
||||
from dvc.repo.params.show import _gather_params
|
||||
from dvc.scm import iter_revs
|
||||
from dvc.scm import SCMError, iter_revs, resolve_rev
|
||||
from dvc.utils import error_handler, onerror_collect, relpath
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git.objects import GitCommit
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,34 +38,49 @@ class ExpStatus(Enum):
|
||||
Failed = 3
|
||||
|
||||
|
||||
def _is_scm_error(collected_exp: Dict[str, Any]) -> bool:
|
||||
if "error" in collected_exp and (
|
||||
isinstance(collected_exp["error"], SCMError)
|
||||
or isinstance(collected_exp["error"], InnerScmError)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _show_onerror_collect(result: Dict, exception: Exception, *args, **kwargs):
|
||||
onerror_collect(result, exception, *args, **kwargs)
|
||||
result["data"] = {}
|
||||
|
||||
|
||||
@error_handler
|
||||
def _collect_experiment_commit(
|
||||
def collect_experiment_commit(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
param_deps=False,
|
||||
running=None,
|
||||
running: Optional[Dict[str, Any]] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
):
|
||||
) -> Dict[str, Any]:
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
|
||||
res: Dict[str, Optional[Any]] = defaultdict(dict)
|
||||
result: Dict[str, Any] = defaultdict(dict)
|
||||
running = running or {}
|
||||
for rev in repo.brancher(revs=[exp_rev]):
|
||||
if rev == "workspace":
|
||||
if exp_rev != "workspace":
|
||||
continue
|
||||
res["timestamp"] = None
|
||||
result["timestamp"] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
res["timestamp"] = datetime.fromtimestamp(commit.commit_time)
|
||||
result["timestamp"] = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
params = _gather_params(
|
||||
repo, rev=rev, targets=None, deps=param_deps, onerror=onerror
|
||||
)
|
||||
if params:
|
||||
res["params"] = params
|
||||
result["params"] = params
|
||||
|
||||
res["deps"] = {
|
||||
result["deps"] = {
|
||||
relpath(dep.fs_path, repo.root_dir): {
|
||||
"hash": dep.hash_info.value,
|
||||
"size": dep.meta.size,
|
||||
@@ -61,7 +89,7 @@ def _collect_experiment_commit(
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
}
|
||||
res["outs"] = {
|
||||
result["outs"] = {
|
||||
relpath(out.fs_path, repo.root_dir): {
|
||||
"hash": out.hash_info.value,
|
||||
"size": out.meta.size,
|
||||
@@ -73,14 +101,14 @@ def _collect_experiment_commit(
|
||||
if not (out.is_metric or out.is_plot)
|
||||
}
|
||||
|
||||
res["status"] = status.name
|
||||
result["status"] = status.name
|
||||
if status == ExpStatus.Running:
|
||||
res["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
result["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
else:
|
||||
res["executor"] = None
|
||||
result["executor"] = None
|
||||
|
||||
if status == ExpStatus.Failed:
|
||||
res["error"] = {
|
||||
result["error"] = {
|
||||
"msg": "Experiment run failed.",
|
||||
"type": "",
|
||||
}
|
||||
@@ -89,54 +117,102 @@ def _collect_experiment_commit(
|
||||
vals = _gather_metrics(
|
||||
repo, targets=None, rev=rev, recursive=False, onerror=onerror
|
||||
)
|
||||
res["metrics"] = vals
|
||||
result["metrics"] = vals
|
||||
|
||||
return res
|
||||
return result
|
||||
|
||||
|
||||
def _collect_experiment_branch(
|
||||
res,
|
||||
repo,
|
||||
branch,
|
||||
baseline,
|
||||
onerror: Optional[Callable] = None,
|
||||
running=None,
|
||||
def _collect_complete_experiment(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
exp_rev: str,
|
||||
running: Dict[str, Any],
|
||||
revs: List[str],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.scm import resolve_rev
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
||||
|
||||
exp_rev = resolve_rev(repo.scm, branch)
|
||||
prev = None
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
checkpoint: bool = True if len(revs) > 1 else False
|
||||
prev = ""
|
||||
for rev in revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
collected_exp = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
rev,
|
||||
onerror=onerror,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if len(revs) > 1:
|
||||
if _is_scm_error(collected_exp):
|
||||
return {}
|
||||
if checkpoint:
|
||||
exp = {"checkpoint_tip": exp_rev}
|
||||
if prev:
|
||||
res[prev]["data"][ # type: ignore[unreachable]
|
||||
results[prev]["data"][ # type: ignore[unreachable]
|
||||
"checkpoint_parent"
|
||||
] = rev
|
||||
if rev in res:
|
||||
res[rev]["data"].update(exp)
|
||||
res.move_to_end(rev)
|
||||
if rev in results:
|
||||
results[rev]["data"].update(exp)
|
||||
results.move_to_end(rev)
|
||||
else:
|
||||
exp.update(collected_exp["data"])
|
||||
else:
|
||||
exp = collected_exp["data"]
|
||||
if rev not in res:
|
||||
res[rev] = {"data": exp}
|
||||
if rev not in results:
|
||||
results[rev] = {"data": exp}
|
||||
prev = rev
|
||||
if len(revs) > 1:
|
||||
res[prev]["data"]["checkpoint_parent"] = baseline
|
||||
return res
|
||||
if checkpoint and prev:
|
||||
results[prev]["data"]["checkpoint_parent"] = baseline
|
||||
return results
|
||||
|
||||
|
||||
def _collect_branch(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: Dict[str, Dict[str, Any]] = OrderedDict()
|
||||
status = ExpStatus.Running if baseline in running else ExpStatus.Success
|
||||
results["baseline"] = collect_experiment_commit(
|
||||
repo,
|
||||
baseline,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if baseline == "workspace" or _is_scm_error(results["baseline"]):
|
||||
return results
|
||||
|
||||
ref_info = ExpRefInfo(baseline_sha=baseline)
|
||||
commits: List[Tuple[str, "GitCommit", str, List[str]]] = []
|
||||
|
||||
for ref in repo.scm.iter_refs(base=str(ref_info)):
|
||||
try:
|
||||
commit = repo.scm.resolve_commit(ref)
|
||||
exp_rev = resolve_rev(repo.scm, ref)
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
except (SCMError, InnerScmError):
|
||||
continue
|
||||
commits.append((ref, commit, exp_rev, revs))
|
||||
|
||||
for exp_ref, _, exp_rev, revs in sorted(
|
||||
commits, key=lambda x: x[1].commit_time, reverse=True
|
||||
):
|
||||
ref_info = ExpRefInfo.from_ref(exp_ref)
|
||||
assert ref_info.baseline_sha == baseline
|
||||
collected_exp = _collect_complete_experiment(
|
||||
repo,
|
||||
baseline=baseline,
|
||||
exp_rev=exp_rev,
|
||||
running=running,
|
||||
revs=revs,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
results.update(collected_exp)
|
||||
return results
|
||||
|
||||
|
||||
def get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
|
||||
@@ -172,13 +248,11 @@ def get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
|
||||
rev = baseline
|
||||
if rev == "workspace":
|
||||
continue
|
||||
name = names.get(rev, None)
|
||||
name = name or exact_name[rev]
|
||||
name = names.get(rev, None) or exact_name[rev]
|
||||
if name:
|
||||
rev_result["data"]["name"] = name
|
||||
|
||||
|
||||
# flake8: noqa: C901
|
||||
def _collect_active_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
@@ -194,13 +268,16 @@ def _collect_active_experiment(
|
||||
if entry.baseline_rev in found_revs and (
|
||||
stash_rev not in running or not running[stash_rev].get("last")
|
||||
):
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Running,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -214,13 +291,16 @@ def _collect_queued_experiment(
|
||||
for entry in repo.experiments.celery_queue.iter_queued():
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Queued,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -235,14 +315,16 @@ def _collect_failed_experiment(
|
||||
entry = queue_done_result.entry
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
experiment = _collect_experiment_commit(
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Failed,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
result[entry.baseline_rev][stash_rev] = experiment
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
@@ -273,7 +355,7 @@ def show(
|
||||
return {}
|
||||
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
onerror = _show_onerror_collect
|
||||
|
||||
res: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
|
||||
@@ -316,7 +398,6 @@ def show(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
sha_only=sha_only,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
@@ -324,47 +405,20 @@ def show(
|
||||
else {}
|
||||
)
|
||||
|
||||
for rev in found_revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
res[rev]["baseline"] = _collect_experiment_commit(
|
||||
for baseline in found_revs:
|
||||
res[baseline] = _collect_branch(
|
||||
repo,
|
||||
rev,
|
||||
status=status,
|
||||
param_deps=param_deps,
|
||||
baseline,
|
||||
running=running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
if rev == "workspace":
|
||||
continue
|
||||
|
||||
ref_info = ExpRefInfo(baseline_sha=rev)
|
||||
commits = [
|
||||
(ref, repo.scm.resolve_commit(ref))
|
||||
for ref in repo.scm.iter_refs(base=str(ref_info))
|
||||
]
|
||||
for exp_ref, _ in sorted(
|
||||
commits, key=lambda x: x[1].commit_time, reverse=True
|
||||
):
|
||||
ref_info = ExpRefInfo.from_ref(exp_ref)
|
||||
assert ref_info.baseline_sha == rev
|
||||
_collect_experiment_branch(
|
||||
res[rev],
|
||||
repo,
|
||||
exp_ref,
|
||||
rev,
|
||||
param_deps=param_deps,
|
||||
running=running,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
if not hide_failed:
|
||||
update_new(res, failed_experiments)
|
||||
update_new(res, failed_experiments)
|
||||
|
||||
update_new(res, active_experiment)
|
||||
|
||||
if not hide_queued:
|
||||
update_new(res, queued_experiment)
|
||||
update_new(res, queued_experiment)
|
||||
|
||||
if not sha_only:
|
||||
get_names(repo, res)
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ def unfetched_view(
|
||||
|
||||
return True
|
||||
|
||||
unfetched = index.targets_view(targets, filter_fn=need_fetch, **kwargs)
|
||||
unfetched = index.targets_view(targets, stage_filter=need_fetch, **kwargs)
|
||||
return unfetched, changed_deps
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def partial_view(
|
||||
) -> "IndexView":
|
||||
return index.targets_view(
|
||||
targets,
|
||||
filter_fn=lambda s: s.is_partial_import,
|
||||
stage_filter=lambda s: s.is_partial_import,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
+56
-35
@@ -7,8 +7,10 @@ from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from funcy import cached_property, nullcontext
|
||||
@@ -151,24 +153,34 @@ class Index:
|
||||
def targets_view(
|
||||
self,
|
||||
targets: "TargetType",
|
||||
filter_fn: Optional[Callable[["Stage"], bool]] = None,
|
||||
stage_filter: Optional[Callable[["Stage"], bool]] = None,
|
||||
outs_filter: Optional[Callable[["Output"], bool]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "IndexView":
|
||||
"""Return read-only view of index for the specified targets.
|
||||
|
||||
Args:
|
||||
targets: Targets to collect
|
||||
filter_fn: Optional stage filter to be applied after collecting
|
||||
stage_filter: Optional stage filter to be applied after collecting
|
||||
targets.
|
||||
outs_filter: Optional output filter to be applied after collecting
|
||||
targets.
|
||||
|
||||
Additional kwargs will be passed into the stage collector.
|
||||
|
||||
Note:
|
||||
If both stage_filter and outs_filter are provided, stage_filter
|
||||
will be applied first, and the resulting view will only contain
|
||||
outputs from stages that matched stage_filter. Outputs from stages
|
||||
that did not match will be excluded from the view (whether or not
|
||||
the output would have matched outs_filter).
|
||||
"""
|
||||
stage_infos = [
|
||||
stage_info
|
||||
for stage_info in self._collect_targets(targets, **kwargs)
|
||||
if not filter_fn or filter_fn(stage_info.stage)
|
||||
if not stage_filter or stage_filter(stage_info.stage)
|
||||
]
|
||||
return IndexView(self, stage_infos)
|
||||
return IndexView(self, stage_infos, outs_filter=outs_filter)
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
@@ -242,15 +254,8 @@ class Index:
|
||||
workspace, key = out.index_key
|
||||
data_index = by_workspace[workspace]
|
||||
|
||||
if out.files:
|
||||
out.obj = out.get_obj()
|
||||
|
||||
entry = out.get_entry()
|
||||
entry.key = key
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
entry.fs = out.stage.deps[0].fs
|
||||
entry.path = out.stage.deps[0].fs_path
|
||||
entry.meta = out.stage.deps[0].meta
|
||||
data_index.add(entry)
|
||||
|
||||
return dict(by_workspace)
|
||||
@@ -345,17 +350,26 @@ class Index:
|
||||
return dict_md5(self.dumpd())
|
||||
|
||||
|
||||
class _DataPrefixes(NamedTuple):
|
||||
explicit: Set["DataIndexKey"]
|
||||
recursive: Set["DataIndexKey"]
|
||||
|
||||
|
||||
class IndexView:
|
||||
"""Read-only view of Index.data using filtered stages."""
|
||||
|
||||
def __init__( # pylint: disable=redefined-outer-name
|
||||
self, index: Index, stage_infos: Iterable["StageInfo"]
|
||||
self,
|
||||
index: Index,
|
||||
stage_infos: Iterable["StageInfo"],
|
||||
outs_filter: Optional[Callable[["Output"], bool]],
|
||||
):
|
||||
self._index = index
|
||||
self._stage_infos = stage_infos
|
||||
# NOTE: stage_infos might have the same stage multiple times but with
|
||||
# different filter_info
|
||||
self._stages = list({stage for stage, _ in stage_infos})
|
||||
self._outs_filter = outs_filter
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._stages)
|
||||
@@ -384,30 +398,32 @@ class IndexView:
|
||||
yield from stage.deps
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
outs = set()
|
||||
def _filtered_outs(self) -> Iterator[Tuple["Output", Optional[str]]]:
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
outs.add(out)
|
||||
yield from outs
|
||||
if not self._outs_filter or self._outs_filter(out):
|
||||
yield out, filter_info
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
yield from {out for (out, _) in self._filtered_outs}
|
||||
|
||||
@cached_property
|
||||
def _data_prefixes(self) -> Dict[str, Set["DataIndexKey"]]:
|
||||
def _data_prefixes(self) -> Dict[str, "_DataPrefixes"]:
|
||||
from collections import defaultdict
|
||||
|
||||
prefixes: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = (
|
||||
*key,
|
||||
out.fs.path.relparts(filter_info, out.fs_path),
|
||||
)
|
||||
prefixes[workspace].add(key)
|
||||
prefixes[workspace].update(
|
||||
key[:i] for i in range(len(key), 0, -1)
|
||||
)
|
||||
prefixes: Dict[str, "_DataPrefixes"] = defaultdict(
|
||||
lambda: _DataPrefixes(set(), set())
|
||||
)
|
||||
for out, filter_info in self._filtered_outs:
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.path.relparts(filter_info, out.fs_path)
|
||||
if out.meta.isdir:
|
||||
prefixes[workspace].recursive.add(key)
|
||||
prefixes[workspace].explicit.update(
|
||||
key[:i] for i in range(len(key), 0, -1)
|
||||
)
|
||||
return prefixes
|
||||
|
||||
@cached_property
|
||||
@@ -418,14 +434,19 @@ class IndexView:
|
||||
|
||||
def key_filter(workspace: str, key: "DataIndexKey"):
|
||||
try:
|
||||
return key in self._data_prefixes[workspace]
|
||||
prefixes = self._data_prefixes[workspace]
|
||||
return key in prefixes.explicit or any(
|
||||
key[: len(prefix)] == prefix
|
||||
for prefix in prefixes.recursive
|
||||
)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return {
|
||||
workspace: view(index, partial(key_filter, workspace))
|
||||
for workspace, index in self._index.data.items()
|
||||
}
|
||||
data = {}
|
||||
for workspace, data_index in self._index.data.items():
|
||||
data_index.load()
|
||||
data[workspace] = view(data_index, partial(key_filter, workspace))
|
||||
return data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -546,7 +553,7 @@ def parse(fs, path, props=None, **kwargs):
|
||||
return _load_sv(path=path, fs=fs, delimiter="\t", header=header)
|
||||
if extension in LOADERS or extension in (".yml", ".yaml"):
|
||||
return LOADERS[extension](path=path, fs=fs)
|
||||
if extension in (".jpeg", ".jpg", ".gif", ".png"):
|
||||
if extension in (".jpeg", ".jpg", ".gif", ".png", ".svg"):
|
||||
with fs.open(path, "rb") as fd:
|
||||
return fd.read()
|
||||
raise PlotMetricTypeError(path)
|
||||
@@ -555,7 +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.")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@ class StageLoader(Mapping):
|
||||
stage.PARAM_OUTS,
|
||||
stage.PARAM_METRICS,
|
||||
stage.PARAM_PLOTS,
|
||||
stage.PARAM_LIVE,
|
||||
],
|
||||
)
|
||||
stage.outs = lcat(
|
||||
|
||||
@@ -20,10 +20,6 @@ class CheckpointKilledError(StageCmdFailedError):
|
||||
pass
|
||||
|
||||
|
||||
class LiveKilledError(StageCmdFailedError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorTask:
|
||||
stage: "Stage"
|
||||
|
||||
@@ -12,4 +12,3 @@ class StageParams:
|
||||
PARAM_METRICS = "metrics"
|
||||
PARAM_PLOTS = "plots"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
|
||||
+3
-10
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ try:
|
||||
from ._dvc_version import version_tuple # noqa: F401
|
||||
except ImportError:
|
||||
__version__ = "UNKNOWN"
|
||||
version_tuple = (0, 0, __version__) # type: ignore[assignment]
|
||||
|
||||
+160
-2
@@ -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.5",
|
||||
"dvclive>=1.0",
|
||||
"dvc-data==0.25.3",
|
||||
"dvc-http==2.27.2",
|
||||
"hydra-core>=1.1.0",
|
||||
"iterative-telemetry==0.0.6",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
azure = ["dvc-azure==2.20.4"]
|
||||
gdrive = ["dvc-gdrive==2.19.1"]
|
||||
gs = ["dvc-gs==2.20.0"]
|
||||
hdfs = ["dvc-hdfs==2.19.0; python_version < '3.11'"]
|
||||
oss = ["dvc-oss==2.19.0"]
|
||||
s3 = ["dvc-s3==2.21.0"]
|
||||
ssh = ["dvc-ssh==2.20.0"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]==2.20.0"]
|
||||
webdav = ["dvc-webdav==2.19.0"]
|
||||
webhdfs = ["dvc-webhdfs==2.19.0"]
|
||||
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19.0"]
|
||||
terraform = ["tpi[ssh]>=2.1.0"]
|
||||
testing = ["pytest-test-utils==0.0.8"]
|
||||
tests = [
|
||||
"tpi[ssh]>=2.1.0",
|
||||
"dvc-ssh==2.20.0",
|
||||
"pytest-test-utils==0.0.8",
|
||||
# Test requirements
|
||||
"pytest==7.2.0",
|
||||
"pytest-cov==4.0.0",
|
||||
"pytest-xdist==3.0.2",
|
||||
"pytest-mock==3.10.0",
|
||||
"pytest-lazy-fixture==0.6.3",
|
||||
# https://github.com/docker/docker-py/issues/2902
|
||||
"pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'",
|
||||
"flaky==3.7.0",
|
||||
"pytest-timeout==2.1.0",
|
||||
"filelock==3.8.0",
|
||||
# optional dependencies
|
||||
"dvclive>=1.0",
|
||||
"pywin32>=225; sys_platform == 'win32'",
|
||||
"beautifulsoup4==4.11.1",
|
||||
# pylint requirements
|
||||
"pylint==2.15.5",
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
"pylint-pytest==1.1.2",
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
# and, protected method calls in our tests
|
||||
"pylint-plugin-utils==0.7",
|
||||
# type-checking
|
||||
"mypy==0.990",
|
||||
"types-requests>=2.27.15",
|
||||
"types-tabulate>=0.8.6",
|
||||
"types-toml>=0.10.4",
|
||||
]
|
||||
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
|
||||
dev = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs,tests]"]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://dvc.org/doc"
|
||||
"Bug Tracker" = "https://github.com/iterative/dvc/issues"
|
||||
Source = "https://github.com/iterative/dvc"
|
||||
|
||||
[project.scripts]
|
||||
dvc = "dvc.cli:main"
|
||||
|
||||
[project.entry-points."fsspec.specs"]
|
||||
dvc = "dvc.api:DVCFileSystem"
|
||||
|
||||
[tool.setuptools]
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[tool.setuptools.packages]
|
||||
find = {namespaces = false, exclude=["tests", "tests.*"]}
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "dvc/_dvc_version.py"
|
||||
|
||||
@@ -30,13 +163,34 @@ line_length = 79
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_level = "debug"
|
||||
addopts = "-ra -p no:hydra_pytest"
|
||||
addopts = "-ra -p no:hydra_pytest --cov-config pyproject.toml"
|
||||
markers = [
|
||||
"needs_internet: Might need network access for the tests",
|
||||
"vscode: Tests verifying contract between DVC and VSCode plugin",
|
||||
"studio: Tests verifying contract between DVC and Studio",
|
||||
]
|
||||
xfail_strict = true
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = ["error::pytest_mock.PytestMockWarning"]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["dvc", "tests"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["dvc"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if __name__ == .__main__.:",
|
||||
"if typing.TYPE_CHECKING:",
|
||||
"if TYPE_CHECKING:",
|
||||
"raise NotImplementedError",
|
||||
"raise AssertionError",
|
||||
"@overload",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
# Error output
|
||||
@@ -47,6 +201,7 @@ show_traceback = true
|
||||
pretty = true
|
||||
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
|
||||
ignore_missing_imports = true
|
||||
implicit_optional = true
|
||||
check_untyped_defs = false
|
||||
# Warnings
|
||||
warn_no_return = true
|
||||
@@ -81,3 +236,6 @@ signature-mutators = ["funcy.decorators.decorator"]
|
||||
[tool.pylint.variables]
|
||||
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
|
||||
ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs"
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
|
||||
|
||||
@@ -1,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
@@ -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"],
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -24,8 +24,8 @@ function addSymLinkPermissions($accountToAdd){
|
||||
Write-Host "Account SID: $($sidstr)" -ForegroundColor DarkCyan
|
||||
$tmp = [System.IO.Path]::GetTempFileName()
|
||||
Write-Host "Export current Local Security Policy" -ForegroundColor DarkCyan
|
||||
secedit.exe /export /cfg "$($tmp)"
|
||||
$c = Get-Content -Path $tmp
|
||||
secedit.exe /export /cfg "$($tmp)"
|
||||
$c = Get-Content -Path $tmp
|
||||
$currentSetting = ""
|
||||
foreach($s in $c) {
|
||||
if( $s -like "SECreateSymbolicLinkPrivilege*") {
|
||||
@@ -35,7 +35,7 @@ function addSymLinkPermissions($accountToAdd){
|
||||
}
|
||||
if( $currentSetting -notlike "*$($sidstr)*" ) {
|
||||
Write-Host "Need to add permissions to SymLink" -ForegroundColor Yellow
|
||||
|
||||
|
||||
Write-Host "Modify Setting ""Create SymLink""" -ForegroundColor DarkCyan
|
||||
|
||||
if( [string]::IsNullOrEmpty($currentSetting) ) {
|
||||
@@ -58,8 +58,8 @@ SECreateSymbolicLinkPrivilege = $($currentSetting)
|
||||
$outfile | Set-Content -Path $tmp2 -Encoding Unicode -Force
|
||||
Push-Location (Split-Path $tmp2)
|
||||
try {
|
||||
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
|
||||
} finally {
|
||||
secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,4 +3,3 @@ from PyInstaller.utils.hooks import ( # pylint:disable=import-error
|
||||
)
|
||||
|
||||
datas = copy_metadata("pydrive2")
|
||||
datas += copy_metadata("google-api-python-client")
|
||||
|
||||
@@ -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/*
|
||||
@@ -1,166 +0,0 @@
|
||||
[metadata]
|
||||
description = Git for data scientists - manage your code and data together
|
||||
name = dvc
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
license = Apache License 2.0
|
||||
license_file = LICENSE
|
||||
url = http://dvc.org
|
||||
project_urls =
|
||||
Documentation = https://dvc.org/doc
|
||||
Source = https://github.com/iterative/dvc
|
||||
download_url = https://github.com/iterative/dvc
|
||||
author = Dmitry Petrov
|
||||
author_email = [email protected]
|
||||
maintainer = Iterative
|
||||
maintainer_email = [email protected]
|
||||
keywords = data-science, data-version-control, machine-learning, git
|
||||
developer-tools, reproducibility, collaboration, ai
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
|
||||
[options]
|
||||
setup_requires =
|
||||
setuptools>=48
|
||||
setuptools_scm[toml]>=7
|
||||
|
||||
python_requires = >=3.8
|
||||
zip_safe = False
|
||||
packages = find:
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
colorama>=0.3.9
|
||||
configobj>=5.0.6
|
||||
voluptuous>=0.11.7
|
||||
requests>=2.22.0
|
||||
# See https://github.com/bdcht/grandalf/issues/26
|
||||
grandalf==0.6
|
||||
distro>=1.3.0
|
||||
appdirs>=1.4.3
|
||||
ruamel.yaml>=0.17.11
|
||||
tomlkit>=0.11.1
|
||||
funcy>=1.14
|
||||
pathspec>=0.9.0,<0.10.0
|
||||
shortuuid>=0.5.0
|
||||
tqdm>=4.63.1,<5
|
||||
packaging>=19.0
|
||||
zc.lockfile>=1.2.1
|
||||
flufl.lock>=5
|
||||
networkx>=2.5
|
||||
psutil>=5.8.0
|
||||
pydot>=1.2.4
|
||||
importlib-resources>=5.2.2; python_version < '3.9'
|
||||
flatten_dict>=0.4.1,<1
|
||||
tabulate>=0.8.7
|
||||
pygtrie>=2.3.2
|
||||
dpath>=2.0.2,<3
|
||||
shtab>=1.3.4,<2
|
||||
rich>=10.13.0
|
||||
pyparsing>=2.4.7
|
||||
typing-extensions>=3.7.4
|
||||
scmrepo==0.1.2
|
||||
dvc-render==0.0.12
|
||||
dvc-task==0.1.4
|
||||
dvclive>=0.10.0
|
||||
dvc-data==0.22.0
|
||||
dvc-http==2.27.2
|
||||
hydra-core>=1.1.0
|
||||
iterative-telemetry==0.0.5
|
||||
|
||||
[options.extras_require]
|
||||
all =
|
||||
%(azure)s
|
||||
%(gdrive)s
|
||||
%(gs)s
|
||||
%(hdfs)s
|
||||
%(oss)s
|
||||
%(s3)s
|
||||
%(ssh)s
|
||||
%(webdav)s
|
||||
%(webhdfs)s
|
||||
dev =
|
||||
%(all)s
|
||||
%(tests)s
|
||||
dvc-data[cli]
|
||||
# Extra dependencies for remote integrations
|
||||
azure = dvc-azure==2.20.4
|
||||
gdrive = dvc-gdrive==2.19.0
|
||||
gs = dvc-gs==2.19.1
|
||||
hdfs =
|
||||
dvc-hdfs==2.19.0; python_version < '3.11'
|
||||
oss = dvc-oss==2.19.0
|
||||
s3 = dvc-s3==2.20.1
|
||||
ssh = dvc-ssh==2.20.0
|
||||
# gssapi should not be included in all_remotes, because it doesn't have wheels
|
||||
# for Linux, so it will fail to compile if user doesn't have all the
|
||||
# requirements, including kerberos itself.
|
||||
ssh_gssapi = dvc-ssh[gssapi]==2.20.0
|
||||
webdav = dvc-webdav==2.19.0
|
||||
webhdfs = dvc-webhdfs==2.19.0
|
||||
# requests-kerberos requires krb5 & gssapi, which does not provide wheels Linux
|
||||
webhdfs_kerberos = dvc-webhdfs[kerberos]==2.19.0
|
||||
terraform = tpi[ssh]>=2.1.0
|
||||
testing =
|
||||
pytest-test-utils==0.0.8
|
||||
tests =
|
||||
%(terraform)s
|
||||
dvc-ssh==2.20.0
|
||||
# Test requirements
|
||||
pytest==7.1.3
|
||||
pytest-cov==4.0.0
|
||||
pytest-xdist==2.5.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-lazy-fixture==0.6.3
|
||||
%(testing)s
|
||||
# https://github.com/docker/docker-py/issues/2902
|
||||
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
|
||||
flaky==3.7.0
|
||||
pytest-timeout==2.1.0
|
||||
filelock==3.8.0
|
||||
# pylint requirements
|
||||
pylint==2.15.4
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
pylint-pytest==1.1.2
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
# and, protected method calls in our tests
|
||||
pylint-plugin-utils==0.7
|
||||
# type-checking
|
||||
mypy==0.982
|
||||
types-requests>=2.27.15
|
||||
types-tabulate>=0.8.6
|
||||
types-toml>=0.10.4
|
||||
# optional dependencies
|
||||
pywin32>=225; sys_platform == 'win32'
|
||||
dvclive[image]>=0.7.3
|
||||
beautifulsoup4==4.11.1
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
dvc.testing
|
||||
dvc.testing.*
|
||||
tests
|
||||
tests.*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
dvc = dvc.cli:main
|
||||
fsspec.specs =
|
||||
dvc = dvc.api:DVCFileSystem
|
||||
|
||||
[flake8]
|
||||
ignore=
|
||||
E203, # Whitespace before ':'
|
||||
E266, # Too many leading '#' for block comment
|
||||
W503, # Line break occurred before a binary operator
|
||||
P1, # unindexed parameters in the str.format, see:
|
||||
# https://pypi.org/project/flake8-string-format/
|
||||
max_line_length = 79
|
||||
max-complexity = 15
|
||||
select = B,C,E,F,W,T4,B902,T,P
|
||||
show_source = true
|
||||
count = true
|
||||
@@ -1,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
@@ -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:]]))
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
@@ -538,7 +536,7 @@ def test_run_celery(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
repro_spy = mocker.spy(dvc.experiments, "reproduce_celery")
|
||||
results = dvc.experiments.run(run_all=True)
|
||||
assert len(results) == 2
|
||||
repro_spy.assert_called_once_with(entries=mocker.ANY, jobs=1)
|
||||
repro_spy.assert_called_once_with(jobs=1)
|
||||
|
||||
expected = {"foo: 2", "foo: 3"}
|
||||
metrics = set()
|
||||
@@ -600,9 +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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -11,64 +11,54 @@ def to_dict(tasks):
|
||||
return status_dict
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def success_tasks(tmp_dir, dvc, scm, test_queue, exp_stage):
|
||||
queue_length = 3
|
||||
name_list = []
|
||||
for i in range(queue_length):
|
||||
name = f"success{i}"
|
||||
name_list.append(name)
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
return name_list
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def failed_tasks(tmp_dir, dvc, scm, test_queue, failed_exp_stage):
|
||||
queue_length = 3
|
||||
name_list = []
|
||||
for i in range(queue_length):
|
||||
name = f"failed{i}"
|
||||
name_list.append(name)
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing,
|
||||
params=[f"foo={i+queue_length}"],
|
||||
queue=True,
|
||||
name=name,
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
return name_list
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
@pytest.mark.parametrize("follow", [True, False])
|
||||
def test_celery_logs(
|
||||
tmp_dir,
|
||||
scm,
|
||||
dvc,
|
||||
failed_exp_stage,
|
||||
test_queue,
|
||||
follow,
|
||||
capsys,
|
||||
):
|
||||
celery_queue = dvc.experiments.celery_queue
|
||||
dvc.experiments.run(failed_exp_stage.addressing, queue=True)
|
||||
dvc.experiments.run(run_all=True)
|
||||
|
||||
queue = dvc.experiments.celery_queue
|
||||
done_result = first(queue.iter_done())
|
||||
done_result = first(celery_queue.iter_done())
|
||||
|
||||
name = done_result.entry.stash_rev
|
||||
captured = capsys.readouterr()
|
||||
queue.logs(name, follow=follow)
|
||||
celery_queue.logs(name, follow=follow)
|
||||
captured = capsys.readouterr()
|
||||
assert "failed to reproduce 'failed-copy-file'" in captured.out
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
def test_queue_remove_done(dvc, failed_tasks, success_tasks):
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 3
|
||||
status = to_dict(dvc.experiments.celery_queue.status())
|
||||
def test_queue_remove_done(
|
||||
dvc,
|
||||
exp_stage,
|
||||
failed_exp_stage,
|
||||
):
|
||||
queue_length = 3
|
||||
success_tasks = []
|
||||
failed_tasks = []
|
||||
celery_queue = dvc.experiments.celery_queue
|
||||
for i in range(queue_length):
|
||||
name = f"success{i}"
|
||||
success_tasks.append(name)
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=[f"foo={i}"], queue=True, name=name
|
||||
)
|
||||
name_fail = f"failed{i}"
|
||||
failed_tasks.append(name_fail)
|
||||
dvc.experiments.run(
|
||||
failed_exp_stage.addressing,
|
||||
params=[f"foo={i+queue_length}"],
|
||||
queue=True,
|
||||
name=name_fail,
|
||||
)
|
||||
dvc.experiments.run(run_all=True)
|
||||
assert len(celery_queue.failed_stash) == 3
|
||||
status = to_dict(celery_queue.status())
|
||||
assert len(status) == 6
|
||||
for name in failed_tasks:
|
||||
assert status[name] == "Failed"
|
||||
@@ -76,27 +66,21 @@ def test_queue_remove_done(dvc, failed_tasks, success_tasks):
|
||||
assert status[name] == "Success"
|
||||
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
dvc.experiments.celery_queue.remove(failed_tasks[:2] + ["non-exist"])
|
||||
assert len(dvc.experiments.celery_queue.status()) == 6
|
||||
celery_queue.remove(failed_tasks[:2] + ["non-exist"])
|
||||
assert len(celery_queue.status()) == 6
|
||||
|
||||
to_remove = [failed_tasks[0], success_tasks[2]]
|
||||
assert set(dvc.experiments.celery_queue.remove(to_remove)) == set(
|
||||
to_remove
|
||||
)
|
||||
assert set(celery_queue.remove(to_remove)) == set(to_remove)
|
||||
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 2
|
||||
status = to_dict(dvc.experiments.celery_queue.status())
|
||||
assert len(celery_queue.failed_stash) == 2
|
||||
status = to_dict(celery_queue.status())
|
||||
assert set(status) == set(failed_tasks[1:] + success_tasks[:2])
|
||||
|
||||
assert dvc.experiments.celery_queue.clear(failed=True) == failed_tasks[1:]
|
||||
assert set(celery_queue.clear(failed=True)) == set(failed_tasks[1:])
|
||||
|
||||
assert len(dvc.experiments.celery_queue.failed_stash) == 0
|
||||
assert set(to_dict(dvc.experiments.celery_queue.status())) == set(
|
||||
success_tasks[:2]
|
||||
)
|
||||
assert len(celery_queue.failed_stash) == 0
|
||||
assert set(to_dict(celery_queue.status())) == set(success_tasks[:2])
|
||||
|
||||
assert (
|
||||
dvc.experiments.celery_queue.clear(success=True) == success_tasks[:2]
|
||||
)
|
||||
assert set(celery_queue.clear(success=True)) == set(success_tasks[:2])
|
||||
|
||||
assert dvc.experiments.celery_queue.status() == []
|
||||
assert celery_queue.status() == []
|
||||
|
||||
@@ -17,7 +17,7 @@ from ..utils.test_hydra import hydra_setup
|
||||
def test_modify_params(params_repo, dvc, changes, expected):
|
||||
dvc.experiments.run(params=changes)
|
||||
# pylint: disable=unspecified-encoding
|
||||
with open("params.yaml", mode="r") as fobj:
|
||||
with open("params.yaml") as fobj:
|
||||
assert fobj.read().strip() == expected
|
||||
|
||||
|
||||
@@ -110,6 +110,11 @@ def test_hydra_compose_and_dump(
|
||||
["foo=bar,baz"],
|
||||
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
|
||||
),
|
||||
(
|
||||
False,
|
||||
[],
|
||||
[{}],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_hydra_sweep(
|
||||
@@ -139,5 +144,18 @@ def test_hydra_sweep(
|
||||
|
||||
|
||||
def test_hydra_sweep_requires_queue(params_repo, dvc):
|
||||
with pytest.raises(InvalidArgumentError):
|
||||
with pytest.raises(
|
||||
InvalidArgumentError,
|
||||
match="Sweep overrides can't be used without `--queue`",
|
||||
):
|
||||
dvc.experiments.run(params=["db=mysql,postgresql"])
|
||||
|
||||
|
||||
def test_hydra_sweep_cant_use_name(tmp_dir, params_repo, dvc):
|
||||
with pytest.raises(
|
||||
InvalidArgumentError,
|
||||
match="Sweep overrides can't be used alongside `--name`",
|
||||
):
|
||||
dvc.experiments.run(
|
||||
params=["db=mysql,postgresql"], queue=True, name="foo"
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY
|
||||
|
||||
import pytest
|
||||
from funcy import first, get_in
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.cli import main
|
||||
from dvc.repo.experiments.executor.base import (
|
||||
@@ -20,7 +22,6 @@ from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import makedirs
|
||||
from dvc.utils.serialize import YAMLFileCorruptedError
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
|
||||
LOCK_CONTENTS = {
|
||||
"read": {
|
||||
@@ -173,7 +174,6 @@ def test_show_queued(tmp_dir, scm, dvc, exp_stage):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
@pytest.mark.xfail(strict=False, reason="pytest-celery flaky")
|
||||
def test_show_failed_experiment(tmp_dir, scm, dvc, failed_exp_stage):
|
||||
baseline_rev = scm.get_rev()
|
||||
timestamp = datetime.fromtimestamp(
|
||||
@@ -313,10 +313,10 @@ def test_show_filter(
|
||||
scm,
|
||||
dvc,
|
||||
capsys,
|
||||
copy_script,
|
||||
):
|
||||
capsys.readouterr()
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -646,6 +646,9 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
rev1 = first(result1)
|
||||
ref_info1 = first(exp_refs_by_rev(scm, rev1))
|
||||
|
||||
# at least 1 second gap between these experiments to make sure
|
||||
# the previous experiment to be regarded as branch_base
|
||||
time.sleep(1)
|
||||
result2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
rev2 = first(result2)
|
||||
@@ -679,8 +682,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_show_only_changed(tmp_dir, dvc, scm, capsys):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_show_only_changed(tmp_dir, dvc, scm, capsys, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -726,13 +728,14 @@ def test_show_only_changed(tmp_dir, dvc, scm, capsys):
|
||||
assert "metrics.yaml:bar" in cap.out
|
||||
|
||||
|
||||
def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
|
||||
def test_show_parallel_coordinates(
|
||||
tmp_dir, dvc, scm, mocker, capsys, copy_script
|
||||
):
|
||||
from dvc.commands.experiments import show
|
||||
|
||||
webbroser_open = mocker.patch("webbrowser.open")
|
||||
show_experiments = mocker.spy(show, "show_experiments")
|
||||
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -807,8 +810,7 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_show_outs(tmp_dir, dvc, scm, erepo_dir, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -889,8 +891,7 @@ def test_show_outs(tmp_dir, dvc, scm, erepo_dir):
|
||||
}
|
||||
|
||||
|
||||
def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
def test_metrics_renaming(tmp_dir, dvc, scm, capsys, copy_script):
|
||||
params_file = tmp_dir / "params.yaml"
|
||||
params_data = {
|
||||
"foo": 1,
|
||||
@@ -975,3 +976,122 @@ def test_show_sorted_deps(tmp_dir, dvc, scm, capsys):
|
||||
assert main(["exp", "show", "--csv"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert "a,b,c,z" in cap.out
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_queued_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
|
||||
dvc.experiments.run(
|
||||
exp_stage.addressing, params=["foo=2"], queue=True, name="test_name"
|
||||
)
|
||||
exp_rev_2 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
|
||||
dvc.experiments.run(exp_stage.addressing, params=["foo=3"], queue=True)
|
||||
exp_rev_3 = dvc.experiments.scm.resolve_rev(f"{CELERY_STASH}@{{0}}")
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == exp_rev_3:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == 2
|
||||
exp_2 = results[exp_rev_2]["data"]
|
||||
assert exp_2["status"] == "Queued"
|
||||
assert exp_2["params"]["params.yaml"] == {"data": {"foo": 2}}
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_completed_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
|
||||
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp_rev_2 = first(result_2)
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
result_3 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
exp_rev_3 = first(result_3)
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == exp_rev_3:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
experiments = dvc.experiments.show()[baseline_rev]
|
||||
assert len(experiments) == 2
|
||||
assert exp_rev_2 in experiments
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_checkpoint_error(tmp_dir, scm, dvc, checkpoint_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
results = dvc.experiments.run(
|
||||
checkpoint_stage.addressing, params=["foo=2"]
|
||||
)
|
||||
exp_rev = first(results)
|
||||
exp_ref = str(first(exp_refs_by_rev(scm, exp_rev)))
|
||||
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == checkpoint_stage.iterations + 1
|
||||
|
||||
checkpoints = {}
|
||||
for rev in results:
|
||||
if rev != "baseline":
|
||||
checkpoints[rev] = scm.resolve_commit(rev)
|
||||
checkpoints[exp_ref] = scm.resolve_commit(exp_ref)
|
||||
checkpoints[baseline_rev] = scm.resolve_commit(baseline_rev)
|
||||
|
||||
failed_rev = random.choice(list(checkpoints.keys()))
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == failed_rev:
|
||||
raise SCMError
|
||||
return checkpoints[rev]
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
results = dvc.experiments.show()[baseline_rev]
|
||||
assert len(results) == 1
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_baseline_error(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
baseline_rev = scm.get_rev()
|
||||
branch = scm.active_branch()
|
||||
|
||||
result_2 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp_rev_2 = first(result_2)
|
||||
commit_2 = scm.resolve_commit(exp_rev_2)
|
||||
|
||||
def resolve_commit(rev):
|
||||
if rev == baseline_rev:
|
||||
raise SCMError
|
||||
else:
|
||||
return commit_2
|
||||
|
||||
mocker.patch.object(
|
||||
scm,
|
||||
"resolve_commit",
|
||||
side_effect=mocker.MagicMock(side_effect=resolve_commit),
|
||||
)
|
||||
experiments = dvc.experiments.show()[baseline_rev]
|
||||
assert len(experiments) == 1
|
||||
assert experiments["baseline"]["data"] == {"name": branch}
|
||||
assert isinstance(experiments["baseline"]["error"], SCMError)
|
||||
|
||||
@@ -257,16 +257,21 @@ def test_log_errors(
|
||||
)
|
||||
|
||||
|
||||
def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
|
||||
with open("image.jpg", "wb") as fd:
|
||||
@pytest.mark.parametrize("ext", ["jpg", "svg"])
|
||||
def test_plots_binary(
|
||||
tmp_dir, scm, dvc, run_copy_metrics, custom_template, ext
|
||||
):
|
||||
file1 = f"image.{ext}"
|
||||
file2 = f"plot.{ext}"
|
||||
with open(file1, "wb") as fd:
|
||||
fd.write(b"content")
|
||||
|
||||
dvc.add(["image.jpg"])
|
||||
dvc.add([file1])
|
||||
run_copy_metrics(
|
||||
"image.jpg",
|
||||
"plot.jpg",
|
||||
file1,
|
||||
file2,
|
||||
commit="run training",
|
||||
plots=["plot.jpg"],
|
||||
plots=[file2],
|
||||
name="s2",
|
||||
single_stage=False,
|
||||
)
|
||||
@@ -276,12 +281,12 @@ def test_plots_binary(tmp_dir, scm, dvc, run_copy_metrics, custom_template):
|
||||
|
||||
scm.tag("v1")
|
||||
|
||||
with open("plot.jpg", "wb") as fd:
|
||||
with open(file2, "wb") as fd:
|
||||
fd.write(b"content2")
|
||||
|
||||
result = dvc.plots.show(revs=["v1", "workspace"])
|
||||
assert get_plot(result, "v1", file="plot.jpg") == b"content"
|
||||
assert get_plot(result, "workspace", file="plot.jpg") == b"content2"
|
||||
assert get_plot(result, "v1", file=file2) == b"content"
|
||||
assert get_plot(result, "workspace", file=file2) == b"content2"
|
||||
|
||||
|
||||
def test_collect_non_existing_dir(tmp_dir, dvc, run_copy_metrics):
|
||||
|
||||
+145
-194
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -492,7 +492,7 @@ def test_ls_granular(erepo_dir):
|
||||
assert entries == [
|
||||
{"isout": True, "isdir": False, "isexec": False, "path": "1"},
|
||||
{"isout": True, "isdir": False, "isexec": False, "path": "2"},
|
||||
{"isout": False, "isdir": True, "isexec": False, "path": "subdir"},
|
||||
{"isout": True, "isdir": True, "isexec": False, "path": "subdir"},
|
||||
]
|
||||
|
||||
|
||||
|
||||
+87
-156
@@ -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
@@ -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):
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -303,3 +303,85 @@ def test_getitem(tmp_dir, dvc, run_copy):
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_ = index["no-valid-stage-name"]
|
||||
|
||||
|
||||
def test_view_granular_dir(tmp_dir, scm, dvc, run_copy):
|
||||
tmp_dir.dvc_gen(
|
||||
{"dir": {"subdir": {"in-subdir": "in-subdir"}, "in-dir": "in-dir"}},
|
||||
commit="init",
|
||||
)
|
||||
index = Index(dvc)
|
||||
|
||||
# view should include the specific target, parent dirs, and children
|
||||
# view should exclude any siblings of the target
|
||||
view = index.targets_view("dir/subdir")
|
||||
data_index = view.data["repo"]
|
||||
assert ("dir",) in data_index
|
||||
assert (
|
||||
"dir",
|
||||
"subdir",
|
||||
) in data_index
|
||||
assert ("dir", "subdir", "in-subdir") in data_index
|
||||
assert (
|
||||
"dir",
|
||||
"in-dir",
|
||||
) not in data_index
|
||||
|
||||
|
||||
def test_view_stage_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(None)
|
||||
assert set(view.stages) == {stage1, stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in (stage1.outs + stage2.outs)
|
||||
}
|
||||
|
||||
view = index.targets_view(
|
||||
None, stage_filter=lambda s: getattr(s, "name", "").startswith("copy")
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage2.outs
|
||||
}
|
||||
|
||||
|
||||
def test_view_outs_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(None, outs_filter=lambda o: o.def_path == "foo")
|
||||
assert set(view.stages) == {stage1, stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage1.outs
|
||||
}
|
||||
|
||||
|
||||
def test_view_combined_filter(tmp_dir, scm, dvc, run_copy):
|
||||
(stage1,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
stage2 = run_copy("foo", "bar", name="copy-foo-bar")
|
||||
tmp_dir.commit([s.outs[0].fspath for s in (stage1, stage2)], msg="add")
|
||||
index = Index(dvc)
|
||||
|
||||
view = index.targets_view(
|
||||
None,
|
||||
stage_filter=lambda s: getattr(s, "name", "").startswith("copy"),
|
||||
outs_filter=lambda o: o.def_path == "foo",
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert set(view.outs) == set()
|
||||
|
||||
view = index.targets_view(
|
||||
None,
|
||||
stage_filter=lambda s: getattr(s, "name", "").startswith("copy"),
|
||||
outs_filter=lambda o: o.def_path == "bar",
|
||||
)
|
||||
assert set(view.stages) == {stage2}
|
||||
assert {out.fs_path for out in view.outs} == {
|
||||
out.fs_path for out in stage2.outs
|
||||
}
|
||||
|
||||
+731
-740
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
{
|
||||
|
||||
+498
-605
File diff suppressed because it is too large
Load Diff
+53
-72
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user