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