Compare commits

...
26 Commits
Author SHA1 Message Date
Ruslan Kuprieiev d39c0ce618 dvc: bump to 2.7.4 2021-09-18 00:30:41 +03:00
Saugat PachhaiandRuslan Kuprieiev 1a8680de7d tests: additional callbacks test for remaining filesystems (#6641)
* tests: additional callbacks test for remaining filesystems

* requirements: bump pydrive2 to 1.9.4

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-09-17 21:31:25 +03:00
Nikita Kodenko 696c704632 tests: replace several unittest.mock usages with mocker (#6544)
* tests: several unittest.mock usages replaced with mocker

* tests: fixes in tests/unit/repo/test_reproduce.py

* tests: rolled back WiP tests
2021-09-17 18:22:34 +03:00
Paweł Redzyński 3e68cd7105 plots: use dpath to explore data (#6639) 2021-09-17 15:09:28 +02:00
Saugat Pachhai 73088fc7b9 dvc.objects: remove unused callback (#6638) 2021-09-17 13:39:54 +03:00
Saugat PachhaiandDave Berenbaum 978d5ac227 exp init: add simple dvc exp init command (#6621)
* exp init: add simple dvc exp init command

* Apply suggestions from code review

Co-authored-by: Dave Berenbaum <[email protected]>

Co-authored-by: Dave Berenbaum <[email protected]>
2021-09-16 16:21:26 +05:45
David de la Iglesia Castro 21585d18ab Force sorted order in plots rendering (#6601)
* Force sorted order in plots

* Fix type hint

* Fixed mypy

* Updates test_plots

* Removed reduntant test

* Removed unused import
2021-09-16 14:25:03 +08:00
Saugat Pachhai 198cdeec06 tests: scriptify function into module (#6619)
so that we can write tests having larger side-effects than
the run_copy that we use as a hack
2021-09-15 22:01:10 +03:00
Saugat Pachhai e049c4bab1 Set timeout for tests back to 30
Reverts 93a200c
2021-09-15 19:10:52 +05:45
Saugat Pachhai e2ed671cc9 get rid of pytest-timeout (#6624)
Trying to see if getting rid of this dependency, our issue with timeout
gets solved or not.

We use timeout-minutes anyway on GHA, so we probably don't need it.
Locally, users can monitor this themselves.
2021-09-15 19:09:16 +05:45
Batuhan Taskaya 8a97b21b76 ssh: ask password before setting it (#6615) 2021-09-15 15:26:57 +03:00
Saugat Pachhai a113097817 fs.download: remove unused copy between same fs (#6623) 2021-09-15 17:36:37 +05:45
Saugat Pachhai fbcc3f389c TmpDir: add serialize utilities (#6620) 2021-09-15 16:13:06 +05:45
Saugat Pachhai 7521e91d05 Strip bucket on GdriveFileSystem (#6618) 2021-09-15 16:12:46 +05:45
GaoandPeter Rowlands 18d147e2f8 machine: add machine list&machine modify (#6602)
* machine: add `machine list`&`machine modify`

1. add new command `dvc machine list`
2. complete `dvc machine modify`
3. add new unit tests for this two commands to ensure the call.
4. add functionality tests for two commands

* Update dvc/config_schema.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Update tests/func/machine/test_machine_config.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Some reviewed problems

1. capitalize global variables.
2. remove precision restriction.
3. solve windows path seperator problem.

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-09-15 15:40:16 +09:00
Amey Kulkarni 39d5d36033 dvc cli: fixed error in 'dvc config --list' in non repo dir (#6587)
* dvc cli: fixed error in 'dvc config --list' in non repo dir

Fixes #6496

* Issue 6496: Added test that covers the issue

* dvc cli: fixes error in 'dvc config <name>' in non dvc repo dir

Fixes the specified error. Did some refactoring based on previous commits.
Also added a test case for setting config in non dvc repo dir.

Fixes #6496

* Issue-6496: dvc config --<level> and dvc config --list --<level> will now throw config error if ran in non dvc repo with repo only level

* Issue 6496: Segregated config levels into repo and system levels that helped in simplifying the logic to decide appropriate level at the time of cli operation.
2021-09-15 15:16:23 +09:00
Saugat Pachhai 091c59e663 fs.download_dir: add callback support (#6613) 2021-09-15 08:51:25 +05:45
Ruslan Kuprieiev f3010d1d65 requirements: require grandalf 0.6 2021-09-14 22:51:53 +03:00
Gao 36a2bf472b Fix ci failures (#6609)
The current ci failure was caused by wrong order of the experiments.
Because our test script runs so fast that they might share the same
commit timestamp, that makes them in an uncertain order. And then gives
a wrong branch_base.
2021-09-14 18:44:20 +03:00
Ruslan Kuprieiev 93a200cf5f gha: increase timeout 2021-09-14 18:37:10 +03:00
dae08255d0 output: use remote kwarg instead of odb (#6600)
* output: use remote kwarg instead of odb

Fixes #6596

* Update dvc/output.py

Co-authored-by: Dave Berenbaum <[email protected]>

* add test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: Dave Berenbaum <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-09-14 18:34:20 +03:00
Saugat Pachhai 237e9d4354 fs.download: add callback support (#6610)
* fs.download: add callback support

* fix lint issues

* compat for gdrive, use fs to upload text for tests

* fix typing issues

* skip gdrive test

* Update tests/func/test_fs.py
2021-09-14 15:53:16 +05:45
Saugat Pachhaiandpre-commit-ci[bot] 788b4d3bb5 fs.upload: add support for optional callback (#6546)
* fs.upload: add support for optional callback

Also removes _upload methods in other fses, and replaces them
with fsspec-compatible put_file which supports fsspec callback
even if they are not fsspec based filesystem.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-09-14 10:55:45 +05:45
Saugat Pachhai 40c61c4f62 Use GDriveURLInfo instead of CloudURLInfo (#6608)
* Use GDriveURLInfo instead of CloudURLInfo

* Update gdrive.py
2021-09-14 10:18:22 +05:45
dependabot[bot] 1a1294848b build(deps): Bump codecov/codecov-action from 2.0.3 to 2.1.0 (#6606)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.0.3 to 2.1.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2.0.3...v2.1.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-14 08:46:38 +05:45
dependabot[bot] 93dab26313 build(deps): Bump mock-ssh-server from 0.9.0 to 0.9.1 (#6607)
Bumps [mock-ssh-server](https://github.com/carletes/mock-ssh-server) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/carletes/mock-ssh-server/releases)
- [Commits](https://github.com/carletes/mock-ssh-server/compare/0.9.0...0.9.1)

---
updated-dependencies:
- dependency-name: mock-ssh-server
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-14 08:46:08 +05:45
93 changed files with 1295 additions and 651 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ jobs:
--cov-report=xml --cov-report=term
--all ${{ env.extra_test_args }}
- name: upload coverage report
uses: codecov/codecov-action@v2.0.3
uses: codecov/codecov-action@v2.1.0
with:
file: ./coverage.xml
fail_ci_if_error: false
+17 -4
View File
@@ -66,7 +66,8 @@ class CmdConfig(CmdBaseNoRepo):
)
return 1
levels = [self.args.level] if self.args.level else self.config.LEVELS
levels = self._get_appropriate_levels(self.args.level)
for level in levels:
conf = self.config.read(level)
prefix = self._config_file_prefix(
@@ -81,9 +82,7 @@ class CmdConfig(CmdBaseNoRepo):
def _get(self, remote, section, opt):
from dvc.config import ConfigError
levels = (
[self.args.level] if self.args.level else self.config.LEVELS[::-1]
)
levels = self._get_appropriate_levels(self.args.level)[::-1]
for level in levels:
conf = self.config.read(level)
@@ -136,6 +135,20 @@ class CmdConfig(CmdBaseNoRepo):
f"option '{opt}' doesn't exist in {name} '{section}'"
)
def _get_appropriate_levels(self, levels):
if levels:
self._validate_level_for_non_repo_operation(levels)
return [levels]
if self.config.dvc_dir is None:
return self.config.SYSTEM_LEVELS
return self.config.LEVELS
def _validate_level_for_non_repo_operation(self, level):
from dvc.config import ConfigError
if self.config.dvc_dir is None and level in self.config.REPO_LEVELS:
raise ConfigError("Not inside a DVC repo")
@staticmethod
def _format_config(config, prefix=""):
from dvc.utils.flatten import flatten
+119
View File
@@ -1,5 +1,6 @@
import argparse
import logging
import os
from collections import Counter, OrderedDict, defaultdict
from datetime import date, datetime
from fnmatch import fnmatch
@@ -789,6 +790,62 @@ class CmdExperimentsRemove(CmdBase):
return 0
class CmdExperimentsInit(CmdBase):
CODE = "src"
DATA = "data"
MODELS = "models"
DEFAULT_METRICS = "metrics.json"
DEFAULT_PARAMS = "params.yaml"
PLOTS = "plots"
DVCLIVE = "dvclive"
DEFAULT_NAME = "default"
def run(self):
from dvc.command.stage import parse_cmd
cmd = parse_cmd(self.args.cmd)
if not cmd:
raise InvalidArgumentError("command is not specified")
if self.args.interactive:
raise NotImplementedError(
"'-i/--interactive' is not implemented yet."
)
if self.args.explicit:
raise NotImplementedError("'--explicit' is not implemented yet.")
if self.args.template:
raise NotImplementedError("template is not supported yet.")
from dvc.utils.serialize import LOADERS
code = self.args.code or self.CODE
data = self.args.data or self.DATA
models = self.args.models or self.MODELS
metrics = self.args.metrics or self.DEFAULT_METRICS
params_path = self.args.params or self.DEFAULT_PARAMS
plots = self.args.plots or self.PLOTS
dvclive = self.args.live or self.DVCLIVE
_, ext = os.path.splitext(params_path)
params = list(LOADERS[ext](params_path))
name = self.args.name or self.DEFAULT_NAME
stage = self.repo.stage.add(
name=name,
cmd=cmd,
deps=[code, data],
outs=[models],
params=[{params_path: params}],
metrics_no_cache=[metrics],
plots_no_cache=[plots],
live=dvclive,
force=True,
)
if self.args.run:
return self.repo.experiments.run(targets=[stage.addressing])
return 0
def add_parser(subparsers, parent_parser):
EXPERIMENTS_HELP = "Commands to run and compare experiments."
@@ -1303,6 +1360,68 @@ def add_parser(subparsers, parent_parser):
)
experiments_remove_parser.set_defaults(func=CmdExperimentsRemove)
EXPERIMENTS_INIT_HELP = "Initialize experiments."
experiments_init_parser = experiments_subparsers.add_parser(
"init",
parents=[parent_parser],
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_init_parser.add_argument(
"cmd",
nargs=argparse.REMAINDER,
help="Command to execute.",
metavar="command",
)
experiments_init_parser.add_argument(
"--run",
action="store_true",
help="Run the experiment after initializing it",
)
experiments_init_parser.add_argument(
"--interactive",
"-i",
action="store_true",
help="Prompt for values that are not provided",
)
experiments_init_parser.add_argument(
"--template", help="Stage template to use to fill with provided values"
)
experiments_init_parser.add_argument(
"--explicit", help="Only use the path values explicitly provided"
)
experiments_init_parser.add_argument(
"--name", "-n", help="Name of the stage to create"
)
experiments_init_parser.add_argument(
"--code",
help="Path to the source file or directory "
"which your experiments depend",
)
experiments_init_parser.add_argument(
"--data",
help="Path to the data file or directory "
"which your experiments depend",
)
experiments_init_parser.add_argument(
"--models",
help="Path to the model file or directory for your experiments",
)
experiments_init_parser.add_argument(
"--params", help="Path to the parameters file for your experiments"
)
experiments_init_parser.add_argument(
"--metrics", help="Path to the metrics file for your experiments"
)
experiments_init_parser.add_argument(
"--plots",
help="Path to the plots file or directory for your experiments",
)
experiments_init_parser.add_argument(
"--live", help="Path to log dvclive outputs for your experiments"
)
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
def _add_run_common(parser):
"""Add common args for 'exp run' and 'exp resume'."""
+37
View File
@@ -69,6 +69,22 @@ class CmdMachineRemove(CmdMachineConfig):
return 0
class CmdMachineList(CmdMachineConfig):
def run(self):
levels = [self.args.level] if self.args.level else self.config.LEVELS
for level in levels:
conf = self.config.read(level)["machine"]
if self.args.name:
conf = conf.get(self.args.name, {})
prefix = self._config_file_prefix(
self.args.show_origin, self.config, level
)
configs = list(self._format_config(conf, prefix))
if configs:
ui.write("\n".join(configs))
return 0
class CmdMachineModify(CmdMachineConfig):
def run(self):
from dvc.config import merge
@@ -219,6 +235,27 @@ def add_parser(subparsers, parent_parser):
)
machine_default_parser.set_defaults(func=CmdMachineDefault)
machine_LIST_HELP = "List the configuration of one/all machines."
machine_list_parser = machine_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(machine_LIST_HELP, "machine/list"),
help=machine_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_list_parser.add_argument(
"--show-origin",
default=False,
action="store_true",
help="Show the source file containing each config value.",
)
machine_list_parser.add_argument(
"name",
nargs="?",
type=str,
help="name of machine to specify",
)
machine_list_parser.set_defaults(func=CmdMachineList)
machine_MODIFY_HELP = "Modify the configuration of an machine."
machine_modify_parser = machine_subparsers.add_parser(
"modify",
+4 -2
View File
@@ -76,8 +76,10 @@ class Config(dict):
APPNAME = "dvc"
APPAUTHOR = "iterative"
SYSTEM_LEVELS = ("system", "global")
REPO_LEVELS = ("repo", "local")
# In the order they shadow each other
LEVELS = ("system", "global", "repo", "local")
LEVELS = SYSTEM_LEVELS + REPO_LEVELS
CONFIG = "config"
CONFIG_LOCAL = "config.local"
@@ -286,7 +288,7 @@ class Config(dict):
def edit(self, level=None, validate=True):
# NOTE: we write to repo config by default, same as git config
level = level or "repo"
if level in {"repo", "local"} and self.dvc_dir is None:
if self.dvc_dir is None and level in self.REPO_LEVELS:
raise ConfigError("Not inside a DVC repo")
conf = self.load_one(level)
+10
View File
@@ -229,7 +229,17 @@ SCHEMA = {
"machine": {
str: {
"cloud": All(Lower, Choices("aws", "azure")),
"region": All(
Lower, Choices("us-west", "us-east", "eu-west", "eu-north")
),
"image": str,
"name": str,
"spot": Bool,
"spot_price": Coerce(float),
"instance_hdd_size": Coerce(int),
"instance_type": Lower,
"instance_gpu": Lower,
"ssh_private": str,
"startup_script": str,
},
},
+2 -2
View File
@@ -11,7 +11,7 @@ from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from dvc.utils import format_link
from .fsspec_wrapper import ObjectFSWrapper
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
logger = logging.getLogger(__name__)
_DEFAULT_CREDS_STEPS = (
@@ -39,7 +39,7 @@ def _az_config():
# pylint:disable=abstract-method
class AzureFileSystem(ObjectFSWrapper):
class AzureFileSystem(CallbackMixin, ObjectFSWrapper):
scheme = Schemes.AZURE
PATH_CLS = CloudURLInfo
PARAM_CHECKSUM = "etag"
+111 -69
View File
@@ -1,12 +1,18 @@
import contextlib
import logging
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial, partialmethod
from functools import partialmethod
from multiprocessing import cpu_count
from typing import Any, ClassVar, Dict, FrozenSet, Optional
from funcy import cached_property
from tqdm.utils import CallbackIOWrapper
from dvc.exceptions import DvcException
from dvc.path_info import URLInfo
from dvc.progress import Tqdm
from dvc.progress import DEFAULT_CALLBACK, FsspecCallback
from dvc.ui import ui
from dvc.utils import tmp_fname
from dvc.utils.fs import makedirs, move
@@ -217,134 +223,170 @@ class BaseFileSystem:
return False
return hash_.endswith(cls.CHECKSUM_DIR_SUFFIX)
@cached_property
def _local_fs(self):
from dvc.fs import LocalFileSystem
return LocalFileSystem()
def upload(
self,
from_info,
to_info,
total=None,
desc=None,
callback=None,
no_progress_bar=False,
**pbar_args,
):
is_file_obj = hasattr(from_info, "read")
method = "upload_fobj" if is_file_obj else "_upload"
method = "upload_fobj" if is_file_obj else "put_file"
if not hasattr(self, method):
raise RemoteActionNotImplemented(method, self.scheme)
if to_info.scheme != self.scheme:
raise NotImplementedError
logger.debug("Uploading '%s' to '%s'", from_info, to_info)
if is_file_obj:
with Tqdm.wrapattr(
from_info,
"read",
if not is_file_obj:
desc = desc or from_info.name
stack = contextlib.ExitStack()
if not callback:
pbar = ui.progress(
desc=desc,
disable=no_progress_bar,
bytes=True,
total=total,
desc=desc,
total=total or -1,
**pbar_args,
) as wrapped:
)
stack.enter_context(pbar)
callback = pbar.as_callback(self._local_fs, from_info)
if total:
callback.set_size(total)
with stack:
if is_file_obj:
wrapped = CallbackIOWrapper(
callback.relative_update, from_info
)
# `size` is used to provide hints to the WebdavFileSystem
# for legacy servers.
# pylint: disable=no-member
return self.upload_fobj(wrapped, to_info, size=total)
if from_info.scheme != "local":
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
name = desc or from_info.name
return self._upload( # noqa, pylint: disable=no-member
from_info.fspath,
to_info,
name=name,
no_progress_bar=no_progress_bar,
**pbar_args,
)
logger.debug("Uploading '%s' to '%s'", from_info, to_info)
# pylint: disable=no-member
return self.put_file(
os.fspath(from_info), to_info, callback=callback
)
def download(
self,
from_info,
to_info,
name=None,
callback=None,
no_progress_bar=False,
jobs=None,
_only_file=False,
**kwargs,
):
if not hasattr(self, "_download"):
raise RemoteActionNotImplemented("download", self.scheme)
if not hasattr(self, "get_file"):
raise RemoteActionNotImplemented("get_file", self.scheme)
if from_info.scheme != self.scheme:
raise NotImplementedError
if to_info.scheme == self.scheme != "local":
self.copy(from_info, to_info)
return 0
if to_info.scheme != "local":
raise NotImplementedError
if not _only_file and self.isdir(from_info):
return self._download_dir(
from_info, to_info, name, no_progress_bar, jobs, **kwargs
download_dir = not _only_file and self.isdir(from_info)
desc = name or to_info.name
stack = contextlib.ExitStack()
if not callback:
pbar_kwargs = {"unit": "Files"} if download_dir else {}
pbar = ui.progress(
total=-1,
desc="Downloading directory" if download_dir else desc,
bytes=not download_dir,
disable=no_progress_bar,
**pbar_kwargs,
)
return self._download_file(from_info, to_info, name, no_progress_bar)
stack.enter_context(pbar)
callback = pbar.as_callback(self, from_info)
with stack:
if download_dir:
return self._download_dir(
from_info, to_info, callback=callback, jobs=jobs, **kwargs
)
return self._download_file(from_info, to_info, callback=callback)
download_file = partialmethod(download, _only_file=True)
def _download_dir(
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs
self,
from_info,
to_info,
callback=DEFAULT_CALLBACK,
jobs=None,
**kwargs,
):
from_infos = list(self.walk_files(from_info, **kwargs))
if not from_infos:
makedirs(to_info, exist_ok=True)
return None
to_infos = (
to_info / info.relative_to(from_info) for info in from_infos
)
callback.set_size(len(from_infos))
with Tqdm(
total=len(from_infos),
desc="Downloading directory",
unit="Files",
disable=no_progress_bar,
) as pbar:
download_files = pbar.wrap_fn(
partial(self._download_file, name=name, no_progress_bar=True)
)
max_workers = jobs or self.jobs
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(download_files, from_info, to_info)
for from_info, to_info in zip(from_infos, to_infos)
]
download_files = FsspecCallback.wrap_fn(callback, self._download_file)
max_workers = jobs or self.jobs
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(download_files, from_info, to_info)
for from_info, to_info in zip(from_infos, to_infos)
]
# NOTE: unlike pulling/fetching cache, where we need to
# download everything we can, not raising an error here might
# turn very ugly, as the user might think that he has
# downloaded a complete directory, while having a partial one,
# which might cause unexpected results in his pipeline.
for future in as_completed(futures):
# NOTE: executor won't let us raise until all futures that
# it has are finished, so we need to cancel them ourselves
# before re-raising.
exc = future.exception()
if exc:
for entry in futures:
entry.cancel()
raise exc
# NOTE: unlike pulling/fetching cache, where we need to
# download everything we can, not raising an error here might
# turn very ugly, as the user might think that he has
# downloaded a complete directory, while having a partial one,
# which might cause unexpected results in his pipeline.
for future in as_completed(futures):
# NOTE: executor won't let us raise until all futures that
# it has are finished, so we need to cancel them ourselves
# before re-raising.
exc = future.exception()
if exc:
for entry in futures:
entry.cancel()
raise exc
def _download_file(self, from_info, to_info, name, no_progress_bar):
def _download_file(
self,
from_info,
to_info,
callback=DEFAULT_CALLBACK,
):
makedirs(to_info.parent, exist_ok=True)
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
name = name or to_info.name
tmp_file = tmp_fname(to_info)
self._download( # noqa, pylint: disable=no-member
from_info, tmp_file, name=name, no_progress_bar=no_progress_bar
)
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
try:
# noqa, pylint: disable=no-member
self.get_file(from_info, tmp_file, callback=callback)
except Exception: # pylint: disable=broad-except
# do we need to rollback makedirs for previously not-existing
# directories?
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_file)
raise
move(tmp_file, to_info)
+6 -3
View File
@@ -6,6 +6,7 @@ from dvc.exceptions import OutputNotFoundError
from dvc.path_info import PathInfo
from dvc.utils import relpath
from ..progress import DEFAULT_CALLBACK
from ._metadata import Metadata
from .base import BaseFileSystem
@@ -256,10 +257,12 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return ret
def _download(self, from_info, to_file, **kwargs):
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
fs, path = self._get_fs_path(from_info)
fs._download( # pylint: disable=protected-access
path, to_file, **kwargs
fs.get_file( # pylint: disable=protected-access
path, to_file, callback=callback, **kwargs
)
def checksum(self, path_info):
+47 -66
View File
@@ -3,8 +3,9 @@ import shutil
from functools import lru_cache
from funcy import cached_property
from tqdm.utils import CallbackIOWrapper
from dvc.progress import Tqdm
from dvc.progress import DEFAULT_CALLBACK
from .base import BaseFileSystem
from .local import LocalFileSystem
@@ -130,45 +131,26 @@ class FSSpecWrapper(BaseFileSystem):
self._with_bucket(path_info), exist_ok=kwargs.pop("exist_ok", True)
)
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
self.fs.put_file(
from_file, self._with_bucket(to_info), callback=callback, **kwargs
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def get_file(
self, from_info, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
self.fs.get_file(
self._with_bucket(from_info), to_info, callback=callback, **kwargs
)
def upload_fobj(self, fobj, to_info, **kwargs):
self.makedirs(to_info.parent)
with self.open(to_info, "wb") as fdest:
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **kwargs
):
self.makedirs(to_info.parent)
size = os.path.getsize(from_file)
with open(from_file, "rb") as fobj:
with Tqdm.wrapattr(
fobj,
"read",
disable=no_progress_bar,
bytes=True,
total=size,
desc=name,
) as wrapped:
self.upload_fobj(wrapped, to_info, size=size)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
):
total = self.getsize(self._with_bucket(from_info))
with self.open(from_info, "rb") as fobj:
with Tqdm.wrapattr(
fobj,
"read",
desc=name,
disable=no_progress_bar,
bytes=True,
total=total,
**pbar_args,
) as wrapped:
with open(to_file, "wb") as fdest:
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
# pylint: disable=abstract-method
class ObjectFSWrapper(FSSpecWrapper):
@@ -260,38 +242,37 @@ _LOCAL_FS = LocalFileSystem()
class CallbackMixin:
"""Use the native ``get_file()``/``put_file()`` APIs
if the target filesystem supports callbacks."""
"""Provides callback support for the filesystem that don't support yet."""
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
def put_file(
self,
from_file,
to_info,
callback=DEFAULT_CALLBACK,
**kwargs,
):
with Tqdm(
desc=name,
disable=no_progress_bar,
bytes=True,
total=-1,
**pbar_args,
) as pbar:
self.fs.put_file(
os.fspath(from_file),
self._with_bucket(to_info),
callback=pbar.as_callback(_LOCAL_FS, from_file),
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
"""Add compatibility support for Callback."""
# pylint: disable=protected-access
self.makedirs(to_info.parent)
size = os.path.getsize(from_file)
with open(from_file, "rb") as fobj:
callback.set_size(size)
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
self.upload_fobj(wrapped, to_info)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
def get_file(
self,
from_info,
to_info,
callback=DEFAULT_CALLBACK,
**kwargs,
):
with Tqdm(
desc=name,
disable=no_progress_bar,
bytes=True,
total=-1,
**pbar_args,
) as pbar:
self.fs.get_file(
self._with_bucket(from_info),
os.fspath(to_file),
callback=pbar.as_callback(self, from_info),
)
# pylint: disable=protected-access
total: int = self.getsize(from_info)
if total:
callback.set_size(total)
with self.open(from_info, "rb") as fobj, open(to_info, "wb") as fdest:
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
+11 -4
View File
@@ -12,7 +12,7 @@ from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from dvc.utils import format_link, tmp_fname
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
from .fsspec_wrapper import FSSpecWrapper
logger = logging.getLogger(__name__)
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
@@ -79,9 +79,7 @@ class GDriveURLInfo(CloudURLInfo):
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
class GDriveFileSystem(
CallbackMixin, FSSpecWrapper
): # pylint:disable=abstract-method
class GDriveFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
scheme = Schemes.GDRIVE
PATH_CLS = GDriveURLInfo
PARAM_CHECKSUM = "checksum"
@@ -298,6 +296,15 @@ class GDriveFileSystem(
return super()._with_bucket(path)
def _strip_bucket(self, entry):
try:
bucket, path = entry.split("/", 1)
except ValueError:
# If there is no path attached, only returns
# the bucket (top-level).
bucket, path = entry, None
return path or bucket
def upload_fobj(self, fobj, to_info, **kwargs):
rpath = self._with_bucket(to_info)
self.makedirs(os.path.dirname(rpath))
+14 -9
View File
@@ -1,8 +1,11 @@
import errno
import os
from tqdm.utils import CallbackIOWrapper
from dvc.utils import is_exec, relpath
from ..progress import DEFAULT_CALLBACK
from .base import BaseFileSystem
@@ -116,16 +119,18 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
# NOTE: os.path.join is ~5.5 times slower
yield f"{root}{os.sep}{file}"
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **kwargs
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
import shutil
from dvc.progress import Tqdm
total = self.getsize(from_info)
if total:
callback.set_size(total)
with open(to_file, "wb+") as to_fobj:
with Tqdm.wrapattr(
to_fobj, "write", desc=name, disable=no_progress_bar
) as wrapped:
with self.open(from_info, "rb", **kwargs) as from_fobj:
shutil.copyfileobj(from_fobj, wrapped)
with self.open(from_info, "rb", **kwargs) as from_fobj:
with open(to_file, "wb+") as to_fobj:
wrapped = CallbackIOWrapper(
callback.relative_update, from_fobj
)
shutil.copyfileobj(wrapped, to_fobj)
+4 -4
View File
@@ -6,11 +6,11 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from .fsspec_wrapper import ObjectFSWrapper
# pylint:disable=abstract-method
class GSFileSystem(ObjectFSWrapper):
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
class GSFileSystem(CallbackMixin, ObjectFSWrapper):
scheme = Schemes.GS
PATH_CLS = CloudURLInfo
REQUIRES = {"gcsfs": "gcsfs"}
+22 -25
View File
@@ -8,8 +8,10 @@ import subprocess
from collections import deque
from contextlib import closing, contextmanager
from tqdm.utils import CallbackIOWrapper
from dvc.hash_info import HashInfo
from dvc.progress import Tqdm
from dvc.progress import DEFAULT_CALLBACK
from dvc.scheme import Schemes
from dvc.utils import fix_env, tmp_fname
@@ -249,37 +251,32 @@ class HDFSFileSystem(BaseFileSystem):
with hdfs.open_output_stream(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest, self.BLOCK_SIZE)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
with self.hdfs(to_info) as hdfs:
hdfs.create_dir(to_info.parent.path)
tmp_file = tmp_fname(to_info.path)
total = os.path.getsize(from_file)
total: int = os.path.getsize(from_file)
callback.set_size(total)
with open(from_file, "rb") as fobj:
with Tqdm.wrapattr(
fobj,
"read",
desc=name,
total=total,
disable=no_progress_bar,
) as wrapped:
with hdfs.open_output_stream(tmp_file) as sobj:
shutil.copyfileobj(wrapped, sobj, self.BLOCK_SIZE)
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
with hdfs.open_output_stream(tmp_file) as sobj:
shutil.copyfileobj(wrapped, sobj, self.BLOCK_SIZE)
hdfs.move(tmp_file, to_info.path)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
with self.hdfs(from_info) as hdfs:
file_info = hdfs.get_file_info(from_info.path)
total = file_info.size
with open(to_file, "wb+") as fobj:
with Tqdm.wrapattr(
fobj,
"write",
desc=name,
total=total,
disable=no_progress_bar,
) as wrapped:
with hdfs.open_input_stream(from_info.path) as sobj:
shutil.copyfileobj(sobj, wrapped, self.BLOCK_SIZE)
if total:
callback.set_size(total)
with hdfs.open_input_stream(from_info.path) as sobj:
with open(to_file, "wb+") as fobj:
wrapped = CallbackIOWrapper(callback.relative_update, sobj)
shutil.copyfileobj(wrapped, fobj, self.BLOCK_SIZE)
+2 -2
View File
@@ -6,7 +6,7 @@ from dvc import prompt
from dvc.path_info import HTTPURLInfo
from dvc.scheme import Schemes
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper, NoDirectoriesMixin
from .fsspec_wrapper import FSSpecWrapper, NoDirectoriesMixin
@wrap_with(threading.Lock())
@@ -32,7 +32,7 @@ def make_context(ssl_verify):
# pylint: disable=abstract-method
class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
scheme = Schemes.HTTP
PATH_CLS = HTTPURLInfo
PARAM_CHECKSUM = "checksum"
+7 -12
View File
@@ -7,6 +7,7 @@ from dvc.system import System
from dvc.utils import is_exec, tmp_fname
from dvc.utils.fs import copy_fobj_to_file, copyfile, makedirs, move, remove
from ..progress import DEFAULT_CALLBACK
from .base import BaseFileSystem
logger = logging.getLogger(__name__)
@@ -158,21 +159,15 @@ class LocalFileSystem(BaseFileSystem):
def info(self, path_info):
return self.fs.info(path_info)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
makedirs(to_info.parent, exist_ok=True)
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
copyfile(from_file, tmp_file, callback=callback)
os.replace(tmp_file, to_info)
@staticmethod
def _download(
from_info, to_file, name=None, no_progress_bar=False, **_kwargs
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
copyfile(from_info, to_file, callback=callback)
+2 -2
View File
@@ -7,13 +7,13 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
from .fsspec_wrapper import ObjectFSWrapper
logger = logging.getLogger(__name__)
# pylint:disable=abstract-method
class OSSFileSystem(CallbackMixin, ObjectFSWrapper):
class OSSFileSystem(ObjectFSWrapper):
scheme = Schemes.OSS
PATH_CLS = CloudURLInfo
REQUIRES = {"ossfs": "ossfs"}
+8 -5
View File
@@ -9,6 +9,7 @@ from funcy import lfilter, wrap_with
from dvc.path_info import PathInfo
from ..progress import DEFAULT_CALLBACK
from .base import BaseFileSystem
from .dvc import DvcFileSystem
@@ -448,19 +449,21 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
for fname in files:
yield PathInfo(root) / fname
def _download(self, from_info, to_file, **kwargs):
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
fs, dvc_fs = self._get_fs_pair(from_info)
try:
fs._download( # pylint: disable=protected-access
from_info, to_file, **kwargs
fs.get_file( # pylint: disable=protected-access
from_info, to_file, callback=callback, **kwargs
)
return
except FileNotFoundError:
if not dvc_fs:
raise
dvc_fs._download( # pylint: disable=protected-access
from_info, to_file, **kwargs
dvc_fs.get_file( # pylint: disable=protected-access
from_info, to_file, callback=callback, **kwargs
)
def metadata(self, path):
+15 -28
View File
@@ -6,7 +6,7 @@ from collections import defaultdict
from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.progress import DEFAULT_CALLBACK
from dvc.scheme import Schemes
from .fsspec_wrapper import ObjectFSWrapper
@@ -204,36 +204,23 @@ class S3FileSystem(BaseS3FileSystem): # pylint:disable=abstract-method
return bucket.Object(path_info.path)
@_translate_exceptions
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
total = os.path.getsize(from_file)
with Tqdm(
disable=no_progress_bar,
total=total,
bytes=True,
desc=name,
**pbar_args,
) as pbar:
obj = self._get_obj(to_info)
obj.upload_file(
from_file,
Callback=pbar.update,
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
Config=self._transfer_config,
)
callback.set_size(os.path.getsize(from_file))
obj = self._get_obj(to_info)
obj.upload_file(
from_file,
Callback=callback.relative_update,
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
Config=self._transfer_config,
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
@_translate_exceptions
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
def get_file(
self, from_info, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
obj = self._get_obj(from_info)
with Tqdm(
disable=no_progress_bar,
total=obj.content_length,
bytes=True,
desc=name,
**pbar_args,
) as pbar:
obj.download_file(to_file, Callback=pbar.update)
callback.set_size(obj.content_length)
obj.download_file(to_info, Callback=callback.relative_update)
+12 -9
View File
@@ -8,7 +8,8 @@ from dvc import prompt
from dvc.scheme import Schemes
from dvc.utils.fs import as_atomic
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
from ..progress import DEFAULT_CALLBACK
from .fsspec_wrapper import FSSpecWrapper
_SSH_TIMEOUT = 60 * 30
_SSH_CONFIG_FILE = os.path.expanduser(os.path.join("~", ".ssh", "config"))
@@ -26,7 +27,7 @@ def ask_password(host, user, port):
# pylint:disable=abstract-method
class SSHFileSystem(CallbackMixin, FSSpecWrapper):
class SSHFileSystem(FSSpecWrapper):
scheme = Schemes.SSH
REQUIRES = {"sshfs": "sshfs"}
@@ -66,6 +67,11 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
or self.DEFAULT_PORT
)
if config.get("ask_password") and config.get("password") is None:
config["password"] = ask_password(
login_info["host"], login_info["username"], login_info["port"]
)
login_info["password"] = config.get("password")
login_info["passphrase"] = config.get("password")
@@ -100,11 +106,6 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
login_info["agent_forwarding"] = config.get("agent_forwarding", True)
login_info["proxy_command"] = user_ssh_config.get("ProxyCommand")
if config.get("ask_password") and login_info["password"] is None:
login_info["password"] = ask_password(
login_info["host"], login_info["username"], login_info["port"]
)
# We are going to automatically add stuff to known_hosts
# something like paramiko's AutoAddPolicy()
login_info["known_hosts"] = None
@@ -124,6 +125,8 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
with as_atomic(self, to_info) as tmp_file:
super().upload_fobj(fobj, tmp_file, **kwargs)
def _upload(self, from_file, to_info, *args, **kwargs):
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
with as_atomic(self, to_info) as tmp_file:
super()._upload(from_file, tmp_file, *args, **kwargs)
super().put_file(from_file, tmp_file, callback=callback, **kwargs)
+29 -20
View File
@@ -9,7 +9,7 @@ from funcy import cached_property, wrap_prop
from dvc.hash_info import HashInfo
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.progress import DEFAULT_CALLBACK
from dvc.scheme import Schemes
from .base import BaseFileSystem
@@ -29,6 +29,15 @@ def update_pbar(pbar, total):
return update
def update_callback(callback, total):
def update(_, bytes_transfered):
if bytes_transfered == -1:
return callback.absolute_update(total)
return callback.relative_update(bytes_transfered)
return update
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
scheme = Schemes.WEBHDFS
PATH_CLS = CloudURLInfo
@@ -145,27 +154,27 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
with self.hdfs_client.write(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
total = os.path.getsize(from_file)
with Tqdm(
desc=name, total=total, disable=no_progress_bar, bytes=True
) as pbar:
self.hdfs_client.upload(
to_info.path,
from_file,
overwrite=True,
progress=update_pbar(pbar, total),
)
callback.set_size(total)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
self.hdfs_client.makedirs(to_info.parent.path)
return self.hdfs_client.upload(
to_info.path,
from_file,
overwrite=True,
progress=update_callback(callback, total),
)
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
total = self.getsize(from_info)
with Tqdm(
desc=name, total=total, disable=no_progress_bar, bytes=True
) as pbar:
self.hdfs_client.download(
from_info.path, to_file, progress=update_pbar(pbar, total)
)
if total:
callback.set_size(total)
self.hdfs_client.download(
from_info.path, to_file, progress=update_callback(callback, total)
)
-5
View File
@@ -97,7 +97,6 @@ class ObjectDB:
hash_info: "HashInfo",
move: bool = True,
verify: Optional[bool] = None,
**kwargs,
):
if self.read_only:
raise ObjectDBPermissionError("Cannot add to read-only ODB")
@@ -121,10 +120,6 @@ class ObjectDB:
except (ObjectFormatError, FileNotFoundError):
pass
callback = kwargs.get("download_callback")
if callback:
callback(1)
def hash_to_path_info(self, hash_) -> "DvcPath":
return self.path_info / hash_[0:2] / hash_[2:]
+3 -2
View File
@@ -849,8 +849,9 @@ class Output:
try:
objects.check(self.odb, obj)
except FileNotFoundError:
remote = self.repo.cloud.get_remote_odb(self.remote)
self.repo.cloud.pull([obj.hash_info], odb=remote, **kwargs)
if self.remote:
kwargs["remote"] = self.remote
self.repo.cloud.pull([obj.hash_info], **kwargs)
if self.obj:
return self.obj
+29 -1
View File
@@ -1,5 +1,4 @@
"""Manages progress bars for DVC repo."""
import logging
import sys
from threading import RLock
@@ -182,9 +181,38 @@ class FsspecCallback(fsspec.Callback):
size = self.fs.info(self.path_info)["size"]
self.progress_bar.total = size
self.progress_bar.refresh()
super().set_size(size)
def relative_update(self, inc=1):
self.progress_bar.update(inc)
super().relative_update(inc)
def absolute_update(self, value):
self.progress_bar.update_to(value)
super().absolute_update(value)
@staticmethod
def wrap_fn(cb, fn):
def wrapped(*args, **kwargs):
res = fn(*args, **kwargs)
cb.relative_update()
return res
return wrapped
def tdqm_or_callback_wrapped(
fobj, method, total, callback=None, **pbar_kwargs
):
if callback:
from funcy import nullcontext
from tqdm.utils import CallbackIOWrapper
callback.set_size(total)
wrapper = CallbackIOWrapper(callback.relative_update, fobj, method)
return nullcontext(wrapper)
return Tqdm.wrapattr(fobj, method, total=total, bytes=True, **pbar_kwargs)
DEFAULT_CALLBACK = fsspec.callbacks.NoOpCallback()
+8 -28
View File
@@ -1,48 +1,28 @@
import os.path
from typing import Dict, List, Set
from typing import Dict, List
import dpath
def get_files(data: Dict) -> Set:
def get_files(data: Dict) -> List:
files = set()
for rev in data.keys():
for file in data[rev].get("data", {}).keys():
files.add(file)
return files
sorted_files = sorted(files)
return sorted_files
def group_by_filename(plots_data: Dict) -> List[Dict]:
# TODO use dpath.util.search once
# https://github.com/dpath-maintainers/dpath-python/issues/147 is released
# now cannot search when errors are present in data
files = get_files(plots_data)
grouped = []
for file in files:
tmp: Dict = {}
for revision, revision_data in plots_data.items():
if file in revision_data.get("data", {}):
if "data" not in tmp:
tmp[revision] = {"data": {}}
tmp[revision]["data"].update(
{file: revision_data["data"][file]}
)
grouped.append(tmp)
grouped.append(dpath.util.search(plots_data, ["*", "*", file]))
return grouped
def find_vega(repo, plots_data, target):
# TODO same as group_by_filename
grouped = group_by_filename(plots_data)
found = None
for plot_group in grouped:
files = get_files(plot_group)
assert len(files) == 1
file = files.pop()
if file == target:
found = plot_group
break
found = dpath.util.search(plots_data, ["*", "*", target])
from dvc.render.vega import VegaRenderer
if found and VegaRenderer.matches(found):
+15 -9
View File
@@ -193,33 +193,39 @@ def makedirs(path, exist_ok=False, mode=None):
logger.trace("failed to chmod '%o' '%s'", mode, path, exc_info=True)
def copyfile(src, dest, no_progress_bar=False, name=None):
def copyfile(src, dest, callback=None, no_progress_bar=False, name=None):
"""Copy file with progress bar"""
from dvc.progress import Tqdm
name = name if name else os.path.basename(dest)
total = os.stat(src).st_size
if os.path.isdir(dest):
dest = os.path.join(dest, os.path.basename(src))
if callback:
callback.set_size(total)
try:
System.reflink(src, dest)
except DvcException:
from dvc.progress import tdqm_or_callback_wrapped
with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
with Tqdm.wrapattr(
with tdqm_or_callback_wrapped(
fdest,
"write",
desc=name,
total,
callback=callback,
disable=no_progress_bar,
total=total,
bytes=True,
) as fdest_wrapped:
desc=name,
) as wrapped:
while True:
buf = fsrc.read(LOCAL_CHUNK_SIZE)
if not buf:
break
fdest_wrapped.write(buf)
wrapped.write(buf)
if callback:
callback.absolute_update(total)
def copy_fobj_to_file(fsrc, dest):
+7
View File
@@ -14,6 +14,13 @@ LOADERS.update(
{".toml": load_toml, ".json": load_json, ".py": load_py} # noqa: F405
)
DUMPERS: DefaultDict[str, DumperFn] = defaultdict( # noqa: F405
lambda: dump_yaml # noqa: F405
)
DUMPERS.update(
{".toml": dump_toml, ".json": dump_json, ".py": dump_py} # noqa: F405
)
MODIFIERS: DefaultDict[str, ModifierFn] = defaultdict( # noqa: F405
lambda: modify_yaml # noqa: F405
)
+8 -2
View File
@@ -63,10 +63,16 @@ def _load_data(path: "AnyPath", parser: ParserFn, fs: "BaseFileSystem" = None):
return parser(fd.read(), path)
def _dump_data(path, data: Any, dumper: DumperFn, fs: "BaseFileSystem" = None):
def _dump_data(
path,
data: Any,
dumper: DumperFn,
fs: "BaseFileSystem" = None,
**dumper_args,
):
open_fn = fs.open if fs else open
with open_fn(path, "w+", encoding="utf-8") as fd: # type: ignore
dumper(data, fd)
dumper(data, fd, **dumper_args)
@contextmanager
+2 -2
View File
@@ -19,8 +19,8 @@ def parse_json(text, path, **kwargs):
return json.loads(text, **kwargs) or {}
def dump_json(path, data, fs=None):
return _dump_data(path, data, dumper=json.dump, fs=fs)
def dump_json(path, data, fs=None, **kwargs):
return _dump_data(path, data, dumper=json.dump, fs=fs, **kwargs)
@contextmanager
+2 -2
View File
@@ -40,8 +40,8 @@ def _dump(data, stream):
return toml.dump(data, stream, encoder=toml.TomlPreserveCommentEncoder())
def dump_toml(path, data, fs=None):
return _dump_data(path, data, dumper=_dump, fs=fs)
def dump_toml(path, data, fs=None, **kwargs):
return _dump_data(path, data, dumper=_dump, fs=fs, **kwargs)
@contextmanager
+2 -2
View File
@@ -58,8 +58,8 @@ def _dump(data, stream):
return yaml.dump(data, stream)
def dump_yaml(path, data, fs=None):
return _dump_data(path, data, dumper=_dump, fs=fs)
def dump_yaml(path, data, fs=None, **kwargs):
return _dump_data(path, data, dumper=_dump, fs=fs, **kwargs)
def loads_yaml(s, typ="safe"):
+1 -1
View File
@@ -6,7 +6,7 @@
import os
import subprocess
_BASE_VERSION = "2.7.3"
_BASE_VERSION = "2.7.4"
def _generate_version(base_version):
+4 -3
View File
@@ -10,7 +10,8 @@ nanotime>=0.5.2
pyasn1>=0.4.1
voluptuous>=0.11.7
requests>=2.22.0
grandalf>=0.6
# See https://github.com/bdcht/grandalf/issues/26
grandalf==0.6
distro>=1.3.0
appdirs>=1.4.3
ruamel.yaml>=0.17.11
@@ -33,7 +34,7 @@ importlib-metadata>=1.4; python_version < '3.8'
flatten_dict>=0.4.1,<1
tabulate>=0.8.7
pygtrie>=2.3.2
dpath>=2.0.1,<3
dpath>=2.0.2,<3
shtab>=1.3.4,<2
rich>=10.0.0
dictdiffer>=0.8.1
@@ -42,4 +43,4 @@ pyparsing==2.4.7
typing_extensions>=3.10.0.2
fsspec[http]>=2021.8.1
aiohttp-retry==2.4.5
diskcache>=5.2.1
diskcache>=5.2.1
+1 -1
View File
@@ -1 +1 @@
pydrive2[fsspec]>=1.9.2
pydrive2[fsspec]>=1.9.4
+1 -2
View File
@@ -6,13 +6,12 @@ pytest-cov==2.12.1
pytest-xdist==2.3.0
pytest-mock==3.6.1
pytest-lazy-fixture==0.6.3
pytest-timeout==1.4.2
pytest-docker==0.10.3
flaky==3.7.0
mock==4.0.3
rangehttpserver==1.2.0
mock-ssh-server==0.9.0
mock-ssh-server==0.9.1
wget==3.2
filelock==3.0.12
wsgidav==3.1.1
-2
View File
@@ -17,8 +17,6 @@ known_first_party=dvc,tests
line_length=79
[tool:pytest]
timeout = 200
timeout_method = thread
log_level = debug
addopts = -ra
markers =
+4
View File
@@ -5,6 +5,7 @@ import pytest
from .dir_helpers import * # noqa, pylint: disable=wildcard-import
from .remotes import * # noqa, pylint: disable=wildcard-import
from .utils.scriptify import scriptify
# Prevent updater and analytics from running their processes
os.environ["DVC_TEST"] = "true"
@@ -164,3 +165,6 @@ def custom_template(tmp_dir, dvc):
template = tmp_dir / "custom_template.json"
shutil.copy(tmp_dir / ".dvc" / "plots" / "default.json", template)
return template
scriptify_fixture = pytest.fixture(lambda: scriptify, name="scriptify")
+18
View File
@@ -49,12 +49,14 @@ import os
import pathlib
import sys
from contextlib import contextmanager
from functools import partialmethod
from textwrap import dedent
import pytest
from funcy import lmap, retry
from dvc.logger import disable_other_loggers
from dvc.utils import serialize
from dvc.utils.fs import makedirs
__all__ = [
@@ -250,6 +252,22 @@ class TmpDir(pathlib.Path):
def hash_to_path_info(self, hash_):
return self / hash_[0:2] / hash_[2:]
def dump(self, *args, **kwargs):
return serialize.DUMPERS[self.suffix](self, *args, **kwargs)
def parse(self, *args, **kwargs):
return serialize.LOADERS[self.suffix](self, *args, **kwargs)
def modify(self, *args, **kwargs):
return serialize.MODIFIERS[self.suffix](self, *args, **kwargs)
load_yaml = partialmethod(serialize.load_yaml)
dump_yaml = partialmethod(serialize.dump_yaml)
load_json = partialmethod(serialize.load_json)
dump_json = partialmethod(serialize.dump_json)
load_toml = partialmethod(serialize.load_toml)
dump_toml = partialmethod(serialize.dump_toml)
def _coerce_filenames(filenames):
if isinstance(filenames, (str, bytes, pathlib.PurePath)):
+3 -3
View File
@@ -8,7 +8,7 @@ from funcy import first
from dvc.dvcfile import PIPELINE_FILE
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils.serialize import PythonFileCorruptedError, load_yaml
from dvc.utils.serialize import PythonFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
@@ -660,9 +660,9 @@ def test_modified_data_dep(tmp_dir, scm, dvc, workspace, params, target):
def test_exp_run_recursive(tmp_dir, scm, dvc, run_copy_metrics):
tmp_dir.dvc_gen("metric_t.json", "foo: 1")
tmp_dir.dvc_gen("metric_t.json", '{"foo": 1}')
run_copy_metrics(
"metric_t.json", "metric.json", metrics=["metric.json"], no_exec=True
)
assert dvc.experiments.run(".", recursive=True)
assert load_yaml(tmp_dir / "metric.json") == {"foo": 1}
assert (tmp_dir / "metric.json").parse() == {"foo": 1}
+34
View File
@@ -0,0 +1,34 @@
import os
from dvc.command.experiments import CmdExperimentsInit
from dvc.main import main
from dvc.utils.serialize import load_yaml
def test_init(tmp_dir, dvc):
tmp_dir.gen(
{
CmdExperimentsInit.CODE: {"copy.py": ""},
"data": "data",
"params.yaml": '{"foo": 1}',
"dvclive": {},
"plots": {},
}
)
code_path = os.path.join(CmdExperimentsInit.CODE, "copy.py")
script = f"python {code_path}"
assert main(["exp", "init", script]) == 0
assert load_yaml(tmp_dir / "dvc.yaml") == {
"stages": {
"default": {
"cmd": script,
"deps": ["data", "src"],
"live": {"dvclive": {"html": True, "summary": True}},
"metrics": [{"metrics.json": {"cache": False}}],
"outs": ["models"],
"params": ["foo"],
"plots": [{"plots": {"cache": False}}],
}
}
}
+8 -5
View File
@@ -11,7 +11,7 @@ from dvc.repo.experiments.base import EXPS_STASH, ExpRefInfo
from dvc.repo.experiments.executor.base import BaseExecutor, ExecutorInfo
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils.fs import makedirs
from dvc.utils.serialize import YAMLFileCorruptedError, dump_yaml
from dvc.utils.serialize import YAMLFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
@@ -286,7 +286,7 @@ def test_show_filter(
"train/bar": 1,
"nested": {"foo": 1, "bar": 1},
}
dump_yaml(params_file, params_data)
(tmp_dir / params_file).dump(params_data)
dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
@@ -386,7 +386,7 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
makedirs(pid_dir, True)
info = ExecutorInfo(None, None, None, BaseExecutor.DEFAULT_LOCATION)
pidfile = os.path.join(pid_dir, f"workspace{BaseExecutor.PIDFILE_EXT}")
dump_yaml(pidfile, info.to_dict())
(tmp_dir / pidfile).dump(info.to_dict())
assert dvc.experiments.show()["workspace"] == {
"baseline": {
@@ -417,7 +417,7 @@ def test_show_running_executor(tmp_dir, scm, dvc, exp_stage):
makedirs(pid_dir, True)
info = ExecutorInfo(None, None, None, BaseExecutor.DEFAULT_LOCATION)
pidfile = os.path.join(pid_dir, f"{exp_rev}{BaseExecutor.PIDFILE_EXT}")
dump_yaml(pidfile, info.to_dict())
(tmp_dir / pidfile).dump(info.to_dict())
results = dvc.experiments.show()
exp_data = get_in(results, [baseline_rev, exp_rev, "data"])
@@ -455,7 +455,7 @@ def test_show_running_checkpoint(
info = ExecutorInfo(123, "foo.git", baseline_rev, executor)
rev = "workspace" if workspace else stash_rev
pidfile = os.path.join(pid_dir, f"{rev}{BaseExecutor.PIDFILE_EXT}")
dump_yaml(pidfile, info.to_dict())
(tmp_dir / pidfile).dump(info.to_dict())
mocker.patch.object(
BaseExecutor, "fetch_exps", return_value=[str(exp_ref)]
@@ -495,6 +495,8 @@ def test_show_with_broken_repo(tmp_dir, scm, dvc, exp_stage, caplog):
def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
import time
baseline_rev = scm.get_rev()
def _get_rev_isotimestamp(rev):
@@ -505,6 +507,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
rev1 = first(result1)
ref_info1 = first(exp_refs_by_rev(scm, rev1))
time.sleep(1)
result2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
rev2 = first(result2)
ref_info2 = first(exp_refs_by_rev(scm, rev2))
View File
+123
View File
@@ -0,0 +1,123 @@
import os
import textwrap
import pytest
from dvc.main import main
CONFIG_TEXT = textwrap.dedent(
"""\
[feature]
machine = true
['machine \"foo\"']
cloud = aws
"""
)
@pytest.mark.parametrize(
"slot,value",
[
("region", "us-west"),
("image", "iterative-cml"),
("name", "iterative_test"),
("spot", "True"),
("spot_price", "1.2345"),
("spot_price", "12345"),
("instance_hdd_size", "10"),
("instance_type", "l"),
("instance_gpu", "tesla"),
("ssh_private", "secret"),
],
)
def test_machine_modify_susccess(tmp_dir, dvc, slot, value):
(tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
assert main(["machine", "modify", "foo", slot, value]) == 0
assert (
tmp_dir / ".dvc" / "config"
).read_text() == CONFIG_TEXT + f" {slot} = {value}\n"
assert main(["machine", "modify", "--unset", "foo", slot]) == 0
assert (tmp_dir / ".dvc" / "config").read_text() == CONFIG_TEXT
def test_machine_modify_startup_script(tmp_dir, dvc):
slot, value = "startup_script", "start.sh"
(tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
assert main(["machine", "modify", "foo", slot, value]) == 0
assert (
tmp_dir / ".dvc" / "config"
).read_text() == CONFIG_TEXT + f" {slot} = ../{value}\n"
assert main(["machine", "modify", "--unset", "foo", slot]) == 0
assert (tmp_dir / ".dvc" / "config").read_text() == CONFIG_TEXT
@pytest.mark.parametrize(
"slot,value,msg",
[
(
"region",
"other-west",
"expected one of us-west, us-east, eu-west, eu-north",
),
("spot_price", "NUM", "expected float"),
("instance_hdd_size", "BIG", "expected int"),
],
)
def test_machine_modify_fail(tmp_dir, dvc, caplog, slot, value, msg):
(tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
assert main(["machine", "modify", "foo", slot, value]) == 251
assert (tmp_dir / ".dvc" / "config").read_text() == CONFIG_TEXT
assert msg in caplog.text
FULL_CONFIG_TEXT = textwrap.dedent(
"""\
[feature]
machine = true
['machine \"bar\"']
cloud = azure
['machine \"foo\"']
cloud = aws
region = us-west
image = iterative-cml
name = iterative_test
spot = True
spot_price = 1.2345
instance_hdd_size = 10
instance_type = l
instance_gpu = tesla
ssh_private = secret
startup_script = {}
""".format(
os.path.join("..", "start.sh")
)
)
def test_machine_list(tmp_dir, dvc, capsys):
(tmp_dir / ".dvc" / "config").write_text(FULL_CONFIG_TEXT)
assert main(["machine", "list"]) == 0
cap = capsys.readouterr()
assert "cloud=azure" in cap.out
assert main(["machine", "list", "foo"]) == 0
cap = capsys.readouterr()
assert "cloud=azure" not in cap.out
assert "cloud=aws" in cap.out
assert "region=us-west" in cap.out
assert "image=iterative-cml" in cap.out
assert "name=iterative_test" in cap.out
assert "spot=True" in cap.out
assert "spot_price=1.2345" in cap.out
assert "instance_hdd_size=10" in cap.out
assert "instance_type=l" in cap.out
assert "instance_gpu=tesla" in cap.out
assert "ssh_private=secret" in cap.out
assert (
"startup_script={}".format(
os.path.join(tmp_dir, ".dvc", "..", "start.sh")
)
in cap.out
)
+6 -7
View File
@@ -1,7 +1,6 @@
import json
from dvc.main import main
from dvc.utils.serialize import dump_yaml
def test_metrics_diff_simple(tmp_dir, scm, dvc, run_copy_metrics):
@@ -22,7 +21,7 @@ def test_metrics_diff_simple(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_yaml(tmp_dir, scm, dvc, run_copy_metrics):
def _gen(val):
metrics = {"a": {"b": {"c": val, "d": 1, "e": str(val)}}}
dump_yaml("m_temp.yaml", metrics)
(tmp_dir / "m_temp.yaml").dump(metrics)
run_copy_metrics(
"m_temp.yaml", "m.yaml", metrics=["m.yaml"], commit=str(val)
)
@@ -39,7 +38,7 @@ def test_metrics_diff_yaml(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_json(tmp_dir, scm, dvc, run_copy_metrics):
def _gen(val):
metrics = {"a": {"b": {"c": val, "d": 1, "e": str(val)}}}
tmp_dir.gen({"m_temp.json": json.dumps(metrics)})
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
"m_temp.json", "m.json", metrics=["m.json"], commit=str(val)
)
@@ -55,7 +54,7 @@ def test_metrics_diff_json(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_json_unchanged(tmp_dir, scm, dvc, run_copy_metrics):
def _gen(val):
metrics = {"a": {"b": {"c": val, "d": 1, "e": str(val)}}}
tmp_dir.gen({"m_temp.json": json.dumps(metrics)})
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
"m_temp.json", "m.json", metrics=["m.json"], commit=str(val)
)
@@ -69,7 +68,7 @@ def test_metrics_diff_json_unchanged(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_broken_json(tmp_dir, scm, dvc, run_copy_metrics):
metrics = {"a": {"b": {"c": 1, "d": 1, "e": "3"}}}
tmp_dir.gen({"m_temp.json": json.dumps(metrics)})
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
"m_temp.json",
"m.json",
@@ -94,7 +93,7 @@ def test_metrics_diff_no_metrics(tmp_dir, scm, dvc):
def test_metrics_diff_new_metric(tmp_dir, scm, dvc, run_copy_metrics):
metrics = {"a": {"b": {"c": 1, "d": 1, "e": "3"}}}
tmp_dir.gen({"m_temp.json": json.dumps(metrics)})
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics("m_temp.json", "m.json", metrics_no_cache=["m.json"])
assert dvc.metrics.diff() == {
@@ -107,7 +106,7 @@ def test_metrics_diff_new_metric(tmp_dir, scm, dvc, run_copy_metrics):
def test_metrics_diff_deleted_metric(tmp_dir, scm, dvc, run_copy_metrics):
metrics = {"a": {"b": {"c": 1, "d": 1, "e": "3"}}}
tmp_dir.gen({"m_temp.json": json.dumps(metrics)})
(tmp_dir / "m_temp.json").dump(metrics)
run_copy_metrics(
"m_temp.json",
"m.json",
+5 -6
View File
@@ -6,10 +6,9 @@ from funcy import get_in
from dvc.dvcfile import PIPELINE_FILE
from dvc.exceptions import OverlappingOutputPathsError
from dvc.path_info import PathInfo
from dvc.repo import Repo
from dvc.utils.fs import remove
from dvc.utils.serialize import YAMLFileCorruptedError, dump_yaml, modify_yaml
from dvc.utils.serialize import YAMLFileCorruptedError
def test_show_simple(tmp_dir, dvc, run_copy_metrics):
@@ -227,10 +226,10 @@ def test_show_no_metrics_files(tmp_dir, dvc, caplog):
def test_metrics_show_overlap(
tmp_dir, dvc, run_copy_metrics, clear_before_run
):
data_dir = PathInfo("data")
(tmp_dir / data_dir).mkdir()
data_dir = tmp_dir / "data"
data_dir.mkdir()
dump_yaml(data_dir / "m1_temp.yaml", {"a": {"b": {"c": 2, "d": 1}}})
(data_dir / "m1_temp.yaml").dump({"a": {"b": {"c": 2, "d": 1}}})
run_copy_metrics(
str(data_dir / "m1_temp.yaml"),
str(data_dir / "m1.yaml"),
@@ -239,7 +238,7 @@ def test_metrics_show_overlap(
name="cp-m1",
metrics=[str(data_dir / "m1.yaml")],
)
with modify_yaml("dvc.yaml") as d:
with (tmp_dir / "dvc.yaml").modify() as d:
# trying to make an output overlaps error
d["stages"]["corrupted-stage"] = {
"cmd": "mkdir data",
+1 -1
View File
@@ -76,7 +76,7 @@ def test_partial_upload(tmp_dir, dvc, index, mocker):
tmp_dir.dvc_gen({"foo": "foo content"})
tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})
original = LocalFileSystem._upload
original = LocalFileSystem.upload
def unreliable_upload(self, from_file, to_info, name=None, **kwargs):
if "baz" in name:
+3 -4
View File
@@ -1,5 +1,4 @@
from dvc.utils import relpath
from dvc.utils.serialize import dump_yaml
def test_diff_no_params(tmp_dir, scm, dvc):
@@ -140,7 +139,7 @@ def test_no_commits(tmp_dir):
def test_vars_shows_on_params_diff(tmp_dir, scm, dvc):
params_file = tmp_dir / "test_params.yaml"
param_data = {"vars": {"model1": {"epoch": 15}, "model2": {"epoch": 35}}}
dump_yaml(params_file, param_data)
(tmp_dir / params_file).dump(param_data)
d = {
"vars": ["test_params.yaml"],
"stages": {
@@ -150,7 +149,7 @@ def test_vars_shows_on_params_diff(tmp_dir, scm, dvc):
}
},
}
dump_yaml("dvc.yaml", d)
(tmp_dir / "dvc.yaml").dump(d)
assert dvc.params.diff() == {
"test_params.yaml": {
"vars.model1.epoch": {"new": 15, "old": None},
@@ -161,7 +160,7 @@ def test_vars_shows_on_params_diff(tmp_dir, scm, dvc):
scm.commit("added stages")
param_data["vars"]["model1"]["epoch"] = 20
dump_yaml(params_file, param_data)
(tmp_dir / params_file).dump(param_data)
assert dvc.params.diff() == {
"test_params.yaml": {
"vars.model1.epoch": {"new": 20, "old": 15, "diff": 5}
+2 -3
View File
@@ -10,7 +10,6 @@ from dvc.parsing import ResolveError
from dvc.parsing.context import Context
from dvc.parsing.interpolate import embrace
from dvc.utils.humanize import join
from dvc.utils.serialize import dump_yaml
from . import make_entry_definition, make_foreach_def
@@ -133,7 +132,7 @@ def test_interpolate_non_string(tmp_dir, dvc):
def test_partial_vars_doesnot_exist(tmp_dir, dvc):
dump_yaml("test_params.yaml", {"sub1": "sub1", "sub2": "sub2"})
(tmp_dir / "test_params.yaml").dump({"sub1": "sub1", "sub2": "sub2"})
definition = make_entry_definition(
tmp_dir,
@@ -281,7 +280,7 @@ def test_item_key_in_generated_stage_vars(tmp_dir, dvc, redefine, from_file):
context = Context(foo="bar")
vars_ = [redefine]
if from_file:
dump_yaml("test_params.yaml", redefine)
(tmp_dir / "test_params.yaml").dump(redefine)
vars_ = ["test_params.yaml"]
definition = make_foreach_def(
+11 -15
View File
@@ -5,7 +5,6 @@ import pytest
from dvc.parsing import DEFAULT_PARAMS_FILE, DataResolver, ForeachDefinition
from dvc.parsing.context import Context
from dvc.utils.serialize import dump_yaml
def test_with_simple_list_data(tmp_dir, dvc):
@@ -111,7 +110,7 @@ def test_foreach_interpolate_with_composite_data(
def test_params_file_with_dict_tracked(tmp_dir, dvc):
foreach_data = {"model1": {"thresh": "foo"}, "model2": {"thresh": "bar"}}
params = {"models": foreach_data}
dump_yaml("params.yaml", params)
(tmp_dir / "params.yaml").dump(params)
resolver = DataResolver(dvc, tmp_dir, {})
data = {"foreach": "${models}", "do": {"cmd": "echo ${item.thresh}"}}
@@ -132,7 +131,7 @@ def test_params_file_with_dict_tracked(tmp_dir, dvc):
def test_params_file_tracked_for_composite_list(tmp_dir, dvc):
foreach_data = [{"thresh": "foo"}, {"thresh": "bar"}]
params = {"models": foreach_data}
dump_yaml("params.yaml", params)
(tmp_dir / "params.yaml").dump(params)
resolver = DataResolver(dvc, tmp_dir, {})
data = {"foreach": "${models}", "do": {"cmd": "echo ${item.thresh}"}}
@@ -178,8 +177,8 @@ def test_foreach_partial_interpolations(tmp_dir, dvc):
def test_mixed_vars_for_foreach_data(tmp_dir, dvc):
dump_yaml("params.yaml", {"models": {"model1": "foo"}})
dump_yaml("test_params.yaml", {"models": {"model2": "bar"}})
(tmp_dir / "params.yaml").dump({"models": {"model1": "foo"}})
(tmp_dir / "test_params.yaml").dump({"models": {"model2": "bar"}})
resolver = DataResolver(dvc, tmp_dir, {"vars": ["test_params.yaml"]})
data = {"foreach": "${models}", "do": {"cmd": "echo ${item}"}}
@@ -197,12 +196,10 @@ def test_mixed_vars_for_foreach_data(tmp_dir, dvc):
def test_mixed_vars_for_foreach_data_2(tmp_dir, dvc):
dump_yaml(
"params.yaml",
(tmp_dir / "params.yaml").dump(
{"models": {"model1": {"thresh": 10}, "model2": {"thresh": 15}}},
)
dump_yaml(
"test_params.yaml",
(tmp_dir / "test_params.yaml").dump(
{"models": {"model1": {"epochs": 5}, "model2": {"epochs": 10}}},
)
@@ -288,9 +285,9 @@ def test_foreach_with_local_vars(tmp_dir, dvc):
],
)
def test_foreach_with_imported_vars(tmp_dir, dvc, local_import):
dump_yaml("params.yaml", {"models": {"model1": {"thresh": "foo"}}})
dump_yaml(
"test_params.yaml", {"train": {"epochs": 10}, "prepare": {"nums": 25}}
(tmp_dir / "params.yaml").dump({"models": {"model1": {"thresh": "foo"}}})
(tmp_dir / "test_params.yaml").dump(
{"train": {"epochs": 10}, "prepare": {"nums": 25}}
)
resolver = DataResolver(dvc, tmp_dir, {})
foreach_data = ["foo", "bar"]
@@ -321,13 +318,12 @@ def test_foreach_with_imported_vars(tmp_dir, dvc, local_import):
def test_foreach_with_interpolated_wdir_and_local_vars(
tmp_dir, dvc, local_import
):
dump_yaml("params.yaml", {"models": {"model1": {"thresh": "foo"}}})
(tmp_dir / "params.yaml").dump({"models": {"model1": {"thresh": "foo"}}})
for i in range(5):
build_dir = tmp_dir / ("model-" + str(i))
build_dir.mkdir()
dump_yaml(
build_dir / "params.yaml",
(build_dir / "params.yaml").dump(
{"train": {"epochs": 1 + i}, "prepare": {"nums": 10 * i}},
)
+12 -13
View File
@@ -6,7 +6,6 @@ from dvc.dependency import _merge_params
from dvc.parsing import DEFAULT_PARAMS_FILE, DataResolver
from dvc.parsing.context import recurse_not_a_node
from dvc.path_info import PathInfo
from dvc.utils.serialize import dump_json, dump_yaml
from . import (
CONTEXT_DATA,
@@ -30,7 +29,7 @@ def assert_stage_equal(d1, d2):
def test_simple(tmp_dir, dvc):
dump_yaml(tmp_dir / DEFAULT_PARAMS_FILE, CONTEXT_DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(CONTEXT_DATA)
resolver = DataResolver(
dvc, PathInfo(str(tmp_dir)), deepcopy(TEMPLATED_DVC_YAML_DATA)
)
@@ -46,7 +45,7 @@ def test_vars_import(tmp_dir, dvc):
Test that different file can be loaded using `vars`
instead of default params.yaml.
"""
dump_yaml(tmp_dir / "params2.yaml", CONTEXT_DATA)
(tmp_dir / "params2.yaml").dump(CONTEXT_DATA)
d = deepcopy(TEMPLATED_DVC_YAML_DATA)
d["vars"] = ["params2.yaml"]
resolver = DataResolver(dvc, PathInfo(str(tmp_dir)), d)
@@ -68,7 +67,7 @@ def test_vars_and_params_import(tmp_dir, dvc):
"vars": [DEFAULT_PARAMS_FILE, {"dict": {"foo": "foobar"}}],
"stages": {"stage1": {"cmd": "echo ${dict.foo} ${dict.bar}"}},
}
dump_yaml(tmp_dir / DEFAULT_PARAMS_FILE, {"dict": {"bar": "bar"}})
(tmp_dir / DEFAULT_PARAMS_FILE).dump({"dict": {"bar": "bar"}})
resolver = DataResolver(dvc, PathInfo(str(tmp_dir)), d)
assert_stage_equal(
@@ -96,8 +95,8 @@ def test_stage_with_wdir(tmp_dir, dvc):
data_dir = tmp_dir / "data"
data_dir.mkdir()
dump_yaml(tmp_dir / DEFAULT_PARAMS_FILE, {"dict": {"bar": "bar"}})
dump_json(data_dir / DEFAULT_PARAMS_FILE, {"dict": {"foo": "foo"}})
(tmp_dir / DEFAULT_PARAMS_FILE).dump({"dict": {"bar": "bar"}})
(data_dir / DEFAULT_PARAMS_FILE).dump({"dict": {"foo": "foo"}})
resolver = DataResolver(dvc, PathInfo(str(tmp_dir)), d)
assert_stage_equal(
@@ -135,12 +134,12 @@ def test_with_templated_wdir(tmp_dir, dvc):
}
}
}
dump_yaml(
tmp_dir / DEFAULT_PARAMS_FILE, {"dict": {"bar": "bar", "ws": "data"}}
(tmp_dir / DEFAULT_PARAMS_FILE).dump(
{"dict": {"bar": "bar", "ws": "data"}}
)
data_dir = tmp_dir / "data"
data_dir.mkdir()
dump_json(data_dir / DEFAULT_PARAMS_FILE, {"dict": {"foo": "foo"}})
(data_dir / DEFAULT_PARAMS_FILE).dump({"dict": {"foo": "foo"}})
resolver = DataResolver(dvc, PathInfo(str(tmp_dir)), d)
assert_stage_equal(
@@ -167,7 +166,7 @@ def test_with_templated_wdir(tmp_dir, dvc):
def test_resolve_local_tries_to_load_globally_used_files(tmp_dir, dvc):
iterable = {"bar": "bar", "foo": "foo"}
dump_json(tmp_dir / "params.json", iterable)
(tmp_dir / "params.json").dump(iterable)
d = {
"vars": ["params.json"],
@@ -196,7 +195,7 @@ def test_resolve_local_tries_to_load_globally_used_files(tmp_dir, dvc):
def test_resolve_local_tries_to_load_globally_used_params_yaml(tmp_dir, dvc):
iterable = {"bar": "bar", "foo": "foo"}
dump_yaml(tmp_dir / "params.yaml", iterable)
(tmp_dir / "params.yaml").dump(iterable)
d = {
"stages": {
@@ -224,7 +223,7 @@ def test_resolve_local_tries_to_load_globally_used_params_yaml(tmp_dir, dvc):
def test_vars_relpath_overwrite(tmp_dir, dvc):
iterable = {"bar": "bar", "foo": "foo"}
dump_yaml(tmp_dir / "params.yaml", iterable)
(tmp_dir / "params.yaml").dump(iterable)
d = {
"vars": ["params.yaml"],
"stages": {
@@ -252,7 +251,7 @@ def test_vars_relpath_overwrite(tmp_dir, dvc):
)
def test_vars_load_partial(tmp_dir, dvc, local, vars_):
iterable = {"bar": "bar", "foo": "foo"}
dump_yaml(tmp_dir / "test_params.yaml", iterable)
(tmp_dir / "test_params.yaml").dump(iterable)
d = {"stages": {"build": {"cmd": "echo ${bar}"}}}
if local:
d["stages"]["build"]["vars"] = vars_
+12 -12
View File
@@ -5,7 +5,7 @@ import pytest
from dvc.parsing import DEFAULT_PARAMS_FILE, DataResolver, ResolveError
from dvc.parsing.context import Context
from dvc.utils.serialize import dump_json, dump_yaml, dumps_yaml
from dvc.utils.serialize import dumps_yaml
from . import CONTEXT_DATA, RESOLVED_DVC_YAML_DATA, TEMPLATED_DVC_YAML_DATA
@@ -50,16 +50,16 @@ def test_vars_interpolation_errors(tmp_dir, dvc, vars_):
"vars_", [{}, {"vars": []}, {"vars": [DEFAULT_PARAMS_FILE]}]
)
def test_default_params_file(tmp_dir, dvc, vars_):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
resolver = DataResolver(dvc, tmp_dir, vars_)
assert resolver.context == DATA
def test_load_vars_from_file(tmp_dir, dvc):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
datasets = {"datasets": ["foo", "bar"]}
dump_json("params.json", datasets)
(tmp_dir / "params.json").dump(datasets)
d = {"vars": [DEFAULT_PARAMS_FILE, "params.json"]}
resolver = DataResolver(dvc, tmp_dir, d)
@@ -82,7 +82,7 @@ def test_load_vars_with_relpath(tmp_dir, scm, dvc):
def test_partial_vars_doesnot_exist(tmp_dir, dvc):
dump_yaml("test_params.yaml", {"sub1": "sub1"})
(tmp_dir / "test_params.yaml").dump({"sub1": "sub1"})
with pytest.raises(ResolveError) as exc_info:
DataResolver(dvc, tmp_dir, {"vars": ["test_params.yaml:sub2"]})
@@ -94,8 +94,8 @@ def test_partial_vars_doesnot_exist(tmp_dir, dvc):
def test_global_overwrite_error_on_imports(tmp_dir, dvc):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
dump_json("params.json", DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
(tmp_dir / "params.json").dump(DATA)
d = {"vars": [DEFAULT_PARAMS_FILE, "params.json"]}
with pytest.raises(ResolveError) as exc_info:
@@ -109,7 +109,7 @@ def test_global_overwrite_error_on_imports(tmp_dir, dvc):
def test_global_overwrite_vars(tmp_dir, dvc):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
d = {"vars": [DATA]}
with pytest.raises(ResolveError) as exc_info:
@@ -123,7 +123,7 @@ def test_global_overwrite_vars(tmp_dir, dvc):
def test_local_declared_vars_overwrite(tmp_dir, dvc):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
d = {"vars": [DATA["models"], DATA["models"]]}
with pytest.raises(ResolveError) as exc_info:
@@ -158,7 +158,7 @@ def test_specified_params_file_not_exist(tmp_dir, dvc):
)
def test_vars_already_loaded_message(tmp_dir, dvc, local, vars_):
d = {"stages": {"build": {"cmd": "echo ${sub1} ${sub2}"}}}
dump_yaml("test_params.yaml", {"sub1": "sub1", "sub2": "sub2"})
(tmp_dir / "test_params.yaml").dump({"sub1": "sub1", "sub2": "sub2"})
if not local:
d["vars"] = vars_
else:
@@ -175,8 +175,8 @@ def test_vars_already_loaded_message(tmp_dir, dvc, local, vars_):
"vars_, loc", [(DATA, "build.vars[0]"), ("params.json", "params.json")]
)
def test_local_overwrite_error(tmp_dir, dvc, vars_, loc):
dump_yaml(DEFAULT_PARAMS_FILE, DATA)
dump_json("params.json", DATA)
(tmp_dir / DEFAULT_PARAMS_FILE).dump(DATA)
(tmp_dir / "params.json").dump(DATA)
d = {"stages": {"build": {"cmd": "echo ${models.foo}", "vars": [vars_]}}}
+4 -7
View File
@@ -1,8 +1,5 @@
from tests.func.plots.utils import _write_json
def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
_write_json(tmp_dir, [{"y": 2}, {"y": 3}], "metric_t.json")
(tmp_dir / "metric_t.json").dump([{"y": 2}, {"y": 3}], sort_keys=True)
run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -11,7 +8,7 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
)
metric_head = [{"y": 3}, {"y": 5}]
_write_json(tmp_dir, metric_head, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric_head, sort_keys=True)
run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -20,7 +17,7 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
)
metric_1 = [{"y": 5}, {"y": 6}]
_write_json(tmp_dir, metric_1, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric_1, sort_keys=True)
run_copy_metrics(
"metric_t.json", "metric.json", plots_no_cache=["metric.json"]
)
@@ -35,7 +32,7 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
},
}
metric_2 = [{"y": 7}, {"y": 8}]
_write_json(tmp_dir, metric_2, "metric.json")
(tmp_dir / "metric.json").dump_json(metric_2, sort_keys=True)
diff_result = dvc.plots.diff(props=props)
assert diff_result == {
+4 -5
View File
@@ -6,14 +6,13 @@ from dvc.dvcfile import PIPELINE_LOCK
from dvc.repo.plots import PropsNotFoundError
from dvc.repo.plots.template import TemplateNotFoundError
from dvc.utils import relpath
from tests.func.plots.utils import _write_json
def test_plots_modify_existing_template(
tmp_dir, dvc, run_copy_metrics, custom_template
):
metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
_write_json(tmp_dir, metric, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric, sort_keys=True)
stage = run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -31,7 +30,7 @@ def test_plots_modify_existing_template(
def test_plots_modify_should_not_change_lockfile(
tmp_dir, dvc, run_copy_metrics, custom_template
):
_write_json(tmp_dir, [{"a": 1, "b": 2}], "metric_t.json")
(tmp_dir / "metric_t.json").dump_json([{"a": 1, "b": 2}], sort_keys=True)
run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -56,7 +55,7 @@ def test_plots_modify_not_existing_template(dvc):
def test_unset_nonexistent(tmp_dir, dvc, run_copy_metrics, custom_template):
metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
_write_json(tmp_dir, metric, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric, sort_keys=True)
run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -76,7 +75,7 @@ def test_dir_plots(tmp_dir, dvc, run_copy_metrics):
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
fname = "file.json"
_write_json(tmp_dir, metric, fname)
(tmp_dir / fname).dump_json(metric, sort_keys=True)
p1 = os.path.join("subdir", "p1.json")
p2 = os.path.join("subdir", "p2.json")
+12 -19
View File
@@ -6,23 +6,16 @@ from funcy import get_in
from dvc.dvcfile import PIPELINE_FILE
from dvc.exceptions import OverlappingOutputPathsError
from dvc.main import main
from dvc.path_info import PathInfo
from dvc.render.vega import PlotMetricTypeError
from dvc.repo import Repo
from dvc.utils import onerror_collect
from dvc.utils.fs import remove
from dvc.utils.serialize import (
EncodingError,
YAMLFileCorruptedError,
dump_yaml,
modify_yaml,
)
from tests.func.plots.utils import _write_json
from dvc.utils.serialize import EncodingError, YAMLFileCorruptedError
def test_plot_cache_missing(tmp_dir, scm, dvc, caplog, run_copy_metrics):
metric1 = [{"y": 2}, {"y": 3}]
_write_json(tmp_dir, metric1, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric1, sort_keys=True)
run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -33,7 +26,7 @@ def test_plot_cache_missing(tmp_dir, scm, dvc, caplog, run_copy_metrics):
# Make a different plot and then remove its datafile
metric2 = [{"y": 3}, {"y": 4}]
_write_json(tmp_dir, metric2, "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(metric2, sort_keys=True)
stage = run_copy_metrics(
"metric_t.json",
"metric.json",
@@ -71,7 +64,7 @@ def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics):
@pytest.mark.parametrize("use_dvc", [True, False])
def test_show_non_plot(tmp_dir, scm, use_dvc):
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
_write_json(tmp_dir, metric, "metric.json")
(tmp_dir / "metric.json").dump_json(metric, sort_keys=True)
if use_dvc:
dvc = Repo.init()
@@ -87,7 +80,7 @@ def test_show_non_plot_and_plot_with_params(
tmp_dir, scm, dvc, run_copy_metrics
):
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
_write_json(tmp_dir, metric, "metric.json")
(tmp_dir / "metric.json").dump_json(metric, sort_keys=True)
run_copy_metrics(
"metric.json", "metric2.json", plots_no_cache=["metric2.json"]
)
@@ -105,7 +98,7 @@ def test_show_from_subdir(tmp_dir, dvc, capsys):
subdir.mkdir()
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
_write_json(subdir, metric, "metric.json")
(subdir / "metric.json").dump_json(metric, sort_keys=True)
with subdir.chdir():
assert main(["plots", "show", "metric.json"]) == 0
@@ -127,10 +120,10 @@ def test_plots_show_non_existing(tmp_dir, dvc, caplog):
@pytest.mark.parametrize("clear_before_run", [True, False])
def test_plots_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
data_dir = PathInfo("data")
(tmp_dir / data_dir).mkdir()
data_dir = tmp_dir / "data"
data_dir.mkdir()
dump_yaml(data_dir / "m1_temp.yaml", {"a": {"b": {"c": 2, "d": 1}}})
(data_dir / "m1_temp.yaml").dump({"a": {"b": {"c": 2, "d": 1}}})
run_copy_metrics(
str(data_dir / "m1_temp.yaml"),
str(data_dir / "m1.yaml"),
@@ -139,7 +132,7 @@ def test_plots_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
name="cp-m1",
plots=[str(data_dir / "m1.yaml")],
)
with modify_yaml("dvc.yaml") as d:
with (tmp_dir / "dvc.yaml").modify() as d:
# trying to make an output overlaps error
d["stages"]["corrupted-stage"] = {
"cmd": "mkdir data",
@@ -167,7 +160,7 @@ def test_dir_plots(tmp_dir, dvc, run_copy_metrics):
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
fname = "file.json"
_write_json(tmp_dir, metric, fname)
(tmp_dir / fname).dump_json(metric, sort_keys=True)
p1 = os.path.join("subdir", "p1.json")
p2 = os.path.join("subdir", "p2.json")
@@ -213,7 +206,7 @@ def test_log_errors(
tmp_dir, scm, dvc, run_copy_metrics, file, error_path, capsys
):
metric = [{"val": 2}, {"val": 3}]
dump_yaml("metric_t.yaml", metric)
(tmp_dir / "metric_t.yaml").dump(metric)
run_copy_metrics(
"metric_t.yaml",
"plot.yaml",
-12
View File
@@ -1,12 +0,0 @@
import json
from tests.utils import dump_sv
def _write_csv(metric, filename, header=True):
with open(filename, "w", newline="") as csvobj:
dump_sv(csvobj, metric, delimiter=",", header=header)
def _write_json(tmp_dir, metric, filename):
tmp_dir.gen(filename, json.dumps(metric, sort_keys=True))
+2 -2
View File
@@ -55,7 +55,7 @@ def test_add(tmp_dir, dvc):
assert stage.outs[0].hash_info == HashInfo("md5", md5)
assert stage.md5 is None
assert load_yaml("foo.dvc") == {
assert (tmp_dir / "foo.dvc").parse() == {
"outs": [
{
"md5": "acbd18db4cc2f85cedef654fccc4a4d8",
@@ -73,7 +73,7 @@ def test_add_executable(tmp_dir, dvc):
os.chmod("foo", st.st_mode | stat.S_IEXEC)
dvc.add("foo")
assert load_yaml("foo.dvc") == {
assert (tmp_dir / "foo.dvc").parse() == {
"outs": [
{
"md5": "acbd18db4cc2f85cedef654fccc4a4d8",
+7 -8
View File
@@ -1,5 +1,4 @@
import os
from unittest import mock
from unittest.mock import call
import pytest
@@ -9,17 +8,17 @@ from dvc.main import main
from dvc.repo import Repo
@mock.patch("dvc.analytics.send")
def test_daemon_analytics(mock_send, tmp_path):
def test_daemon_analytics(mocker, tmp_path):
mock_send = mocker.patch("dvc.analytics.send")
report = os.fspath(tmp_path)
assert 0 == main(["daemon", "analytics", report])
mock_send.assert_called_with(report)
@mock.patch("dvc.analytics.collect_and_send_report")
@mock.patch("dvc.analytics.is_enabled", return_value=True)
def test_main_analytics(mock_is_enabled, mock_report, tmp_dir, dvc):
def test_main_analytics(mocker, tmp_dir, dvc):
mock_is_enabled = mocker.patch("dvc.analytics.collect_and_send_report")
mock_report = mocker.patch("dvc.analytics.is_enabled", return_value=True)
tmp_dir.gen("foo", "text")
assert 0 == main(["add", "foo"])
assert mock_is_enabled.called
@@ -46,8 +45,8 @@ class ANY:
return isinstance(other, self.expected_type)
@mock.patch("requests.post")
def test_collect_and_send_report(mock_post, dvc, mock_daemon):
def test_collect_and_send_report(mocker, dvc, mock_daemon):
mock_post = mocker.patch("requests.post")
collect_and_send_report()
assert mock_daemon.call_count == 1
+2 -2
View File
@@ -867,9 +867,9 @@ def test_checkout_external_modified_file(tmp_dir, dvc, scm, mocker, workspace):
def test_checkout_executable(tmp_dir, dvc):
tmp_dir.dvc_gen("foo", "foo")
contents = load_yaml("foo.dvc")
contents = (tmp_dir / "foo.dvc").parse()
contents["outs"][0]["isexec"] = True
dump_yaml("foo.dvc", contents)
(tmp_dir / "foo.dvc").dump(contents)
dvc.checkout("foo")
+3 -4
View File
@@ -6,7 +6,6 @@ from dvc.dependency.base import DependencyDoesNotExistError
from dvc.dvcfile import PIPELINE_FILE
from dvc.output import OutputDoesNotExistError
from dvc.stage.exceptions import StageCommitError
from dvc.utils.serialize import dump_yaml, load_yaml
from tests.utils import clean_staging
@@ -65,9 +64,9 @@ def test_commit_changed_md5(tmp_dir, dvc):
tmp_dir.gen({"file": "file content"})
(stage,) = dvc.add("file", no_commit=True)
stage_file_content = load_yaml(stage.path)
stage_file_content = (tmp_dir / stage.path).parse()
stage_file_content["md5"] = "1111111111"
dump_yaml(stage.path, stage_file_content)
(tmp_dir / stage.path).dump(stage_file_content)
clean_staging()
@@ -75,7 +74,7 @@ def test_commit_changed_md5(tmp_dir, dvc):
dvc.commit(stage.path)
dvc.commit(stage.path, force=True)
assert "md5" not in load_yaml(stage.path)
assert "md5" not in (tmp_dir / stage.path).parse()
def test_commit_no_exec(tmp_dir, dvc):
+36
View File
@@ -77,6 +77,12 @@ def test_config_set_local(tmp_dir, dvc):
assert (tmp_dir / ".dvc" / "config.local").read_text() == "\n"
def test_config_set_in_non_dvc_repo(tmp_dir, caplog):
assert main(["config", "core.analytics", "true"]) != 0
out = caplog.text
assert "Not inside a DVC repo" in out
@pytest.mark.parametrize(
"args, ret, msg",
[
@@ -128,6 +134,21 @@ def test_config_get(tmp_dir, dvc, capsys, caplog, args, ret, msg):
assert msg in text
@pytest.mark.parametrize(
"args, ret",
[
(["--local", "core.remote"], 251),
(["--project", "core.remote"], 251),
(["core.remote"], 0),
],
)
def test_config_get_in_non_dvc_repo(tmp_dir, caplog, args, ret):
assert main(["config"] + args) == ret
if ret != 0:
out = caplog.text
assert "Not inside a DVC repo" in out
def test_config_list(tmp_dir, dvc, capsys):
(tmp_dir / ".dvc" / "config").write_text(
textwrap.dedent(
@@ -169,6 +190,21 @@ def test_config_list(tmp_dir, dvc, capsys):
assert "core.remote=myremote" in out
@pytest.mark.parametrize(
"args, ret",
[
(["--list", "--local"], 251),
(["--list", "--project"], 251),
(["--list"], 0),
],
)
def test_config_list_in_non_dvc_repo(tmp_dir, caplog, args, ret):
assert main(["config"] + args) == ret
if ret != 0:
out = caplog.text
assert "Not inside a DVC repo" in out
@pytest.mark.parametrize(
"args", [["core.analytics"], ["core.analytics", "false"], ["--unset"]]
)
+36 -7
View File
@@ -10,7 +10,6 @@ from dvc.external_repo import clean_repos
from dvc.main import main
from dvc.stage.exceptions import StageNotFound
from dvc.utils.fs import move, remove
from dvc.utils.serialize import dump_yaml, load_yaml
all_clouds = [
pytest.lazy_fixture(cloud)
@@ -204,9 +203,9 @@ def test_warn_on_outdated_stage(tmp_dir, dvc, local_remote, caplog):
assert main(["push"]) == 0
stage_file_path = stage.relpath
content = load_yaml(stage_file_path)
content = (tmp_dir / stage_file_path).parse()
del content["outs"][0]["md5"]
dump_yaml(stage_file_path, content)
(tmp_dir / stage_file_path).dump(content)
with caplog.at_level(logging.WARNING, logger="dvc"):
caplog.clear()
@@ -612,8 +611,6 @@ def test_pull_no_00_prefix(tmp_dir, dvc, remote, monkeypatch):
def test_output_remote(tmp_dir, dvc, make_remote):
from dvc.utils.serialize import modify_yaml
make_remote("default", default=True)
make_remote("for_foo", default=False)
make_remote("for_data", default=False)
@@ -622,10 +619,10 @@ def test_output_remote(tmp_dir, dvc, make_remote):
tmp_dir.dvc_gen("bar", "bar")
tmp_dir.dvc_gen("data", {"one": "one", "two": "two"})
with modify_yaml("foo.dvc") as d:
with (tmp_dir / "foo.dvc").modify() as d:
d["outs"][0]["remote"] = "for_foo"
with modify_yaml("data.dvc") as d:
with (tmp_dir / "data.dvc").modify() as d:
d["outs"][0]["remote"] = "for_data"
dvc.push()
@@ -653,3 +650,35 @@ def test_output_remote(tmp_dir, dvc, make_remote):
"6b18131dc289fd37006705affe961ef8.dir",
"b8a9f715dbb64fd5c56e7783c6820a61",
}
def test_target_remote(tmp_dir, dvc, make_remote):
make_remote("default", default=True)
make_remote("myremote", default=False)
tmp_dir.dvc_gen("foo", "foo")
tmp_dir.dvc_gen("data", {"one": "one", "two": "two"})
dvc.push(remote="myremote")
default = dvc.cloud.get_remote_odb("default")
myremote = dvc.cloud.get_remote_odb("myremote")
assert set(default.all()) == set()
assert set(myremote.all()) == {
"acbd18db4cc2f85cedef654fccc4a4d8",
"f97c5d29941bfb1b2fdab0874906ab82",
"6b18131dc289fd37006705affe961ef8.dir",
"b8a9f715dbb64fd5c56e7783c6820a61",
}
clean(["foo", "data"], dvc)
dvc.pull(remote="myremote")
assert set(dvc.odb.local.all()) == {
"acbd18db4cc2f85cedef654fccc4a4d8",
"f97c5d29941bfb1b2fdab0874906ab82",
"6b18131dc289fd37006705affe961ef8.dir",
"b8a9f715dbb64fd5c56e7783c6820a61",
}
+4 -5
View File
@@ -15,7 +15,6 @@ from dvc.stage.exceptions import (
StageFileFormatError,
)
from dvc.stage.loader import StageNotFound
from dvc.utils.serialize import dump_yaml
def test_run_load_one_for_multistage(tmp_dir, dvc):
@@ -245,14 +244,14 @@ def test_remove_stage_on_lockfile_format_error(tmp_dir, dvc, run_copy):
lock_data = lock_file.load()
lock_data["gibberish"] = True
data["gibberish"] = True
dump_yaml(lock_file.relpath, lock_data)
(tmp_dir / lock_file.relpath).dump(lock_data)
with pytest.raises(StageFileFormatError):
dvc_file.remove_stage(stage)
lock_file.remove()
dvc_file.dump(stage, update_pipeline=False)
dump_yaml(dvc_file.relpath, data)
(tmp_dir / dvc_file.relpath).dump(data)
with pytest.raises(StageFileFormatError):
dvc_file.remove_stage(stage)
@@ -335,7 +334,7 @@ def test_dvcfile_dump_preserves_desc(tmp_dir, dvc, run_copy):
data = dvcfile._load()[0]
data["stages"]["run_copy"]["outs"][0] = {"bar": {"desc": out_desc}}
dump_yaml(dvcfile.path, data)
(tmp_dir / dvcfile.path).dump(data)
assert stage.desc == stage_desc
stage.outs[0].desc = out_desc
@@ -376,7 +375,7 @@ def test_dvcfile_dump_preserves_comments(tmp_dir, dvc):
],
)
def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, name):
dump_yaml("dvc.yaml", {"stages": data, "vars": [{"foo": "foobar"}]})
(tmp_dir / "dvc.yaml").dump({"stages": data, "vars": [{"foo": "foobar"}]})
stage = dvc.stage.load_one(name=name)
dvcfile = stage.dvcfile
+10 -9
View File
@@ -13,7 +13,7 @@ from tests.unit.fs.test_repo import make_subrepo
from tests.utils import clean_staging
def test_external_repo(erepo_dir):
def test_external_repo(erepo_dir, mocker):
with erepo_dir.chdir():
with erepo_dir.branch("branch", new=True):
erepo_dir.dvc_gen("file", "branch", commit="create file on branch")
@@ -21,16 +21,17 @@ def test_external_repo(erepo_dir):
url = os.fspath(erepo_dir)
with patch.object(Git, "clone", wraps=Git.clone) as mock:
with external_repo(url) as repo:
with repo.open_by_relpath("file") as fd:
assert fd.read() == "master"
mock = mocker.patch.object(Git, "clone", wraps=Git.clone)
with external_repo(url, rev="branch") as repo:
with repo.open_by_relpath("file") as fd:
assert fd.read() == "branch"
with external_repo(url) as repo:
with repo.open_by_relpath("file") as fd:
assert fd.read() == "master"
assert mock.call_count == 1
with external_repo(url, rev="branch") as repo:
with repo.open_by_relpath("file") as fd:
assert fd.read() == "branch"
assert mock.call_count == 1
def test_source_change(erepo_dir):
+152
View File
@@ -3,6 +3,7 @@ import os
from operator import itemgetter
from os.path import join
import fsspec
import pytest
from dvc.fs import get_cloud_fs
@@ -413,3 +414,154 @@ def test_fs_makedirs_on_upload_and_copy(dvc, cloud):
fs.copy(cloud / "dir" / "foo", cloud / "dir2" / "foo")
assert fs.isdir(cloud / "dir2")
assert fs.exists(cloud / "dir2" / "foo")
@pytest.mark.needs_internet
@pytest.mark.parametrize(
"cloud",
[
pytest.lazy_fixture("azure"),
pytest.lazy_fixture("gs"),
pytest.lazy_fixture("gdrive"),
pytest.lazy_fixture("hdfs"),
pytest.lazy_fixture("local_cloud"),
pytest.lazy_fixture("oss"),
pytest.lazy_fixture("s3"),
pytest.lazy_fixture("ssh"),
pytest.lazy_fixture("webhdfs"),
pytest.lazy_fixture("webdav"),
pytest.lazy_fixture("http"),
],
)
def test_upload_callback(tmp_dir, dvc, cloud):
tmp_dir.gen("foo", "foo")
cls, config, _ = get_cloud_fs(dvc, **cloud.config)
fs = cls(**config)
expected_size = os.path.getsize(tmp_dir / "foo")
callback = fsspec.Callback()
fs.upload(tmp_dir / "foo", cloud / "foo", callback=callback)
assert callback.size == expected_size
assert callback.value == expected_size
@pytest.mark.needs_internet
@pytest.mark.parametrize(
"cloud",
[
pytest.lazy_fixture("azure"),
pytest.lazy_fixture("gs"),
pytest.lazy_fixture("gdrive"),
pytest.lazy_fixture("hdfs"),
pytest.lazy_fixture("local_cloud"),
pytest.lazy_fixture("oss"),
pytest.lazy_fixture("s3"),
pytest.lazy_fixture("ssh"),
pytest.lazy_fixture("webhdfs"),
pytest.lazy_fixture("webdav"),
pytest.lazy_fixture("http"),
],
)
def test_download_callback(tmp_dir, dvc, cloud, local_cloud):
cls, config, _ = get_cloud_fs(dvc, **cloud.config)
fs = cls(**config)
(tmp_dir / "to_upload").write_text("foo")
fs.upload(tmp_dir / "to_upload", cloud / "foo")
expected_size = fs.getsize(cloud / "foo")
callback = fsspec.Callback()
fs.download_file(cloud / "foo", tmp_dir / "foo", callback=callback)
assert callback.size == expected_size
assert callback.value == expected_size
assert (tmp_dir / "foo").read_text() == "foo"
@pytest.mark.needs_internet
@pytest.mark.parametrize(
"cloud",
[
pytest.lazy_fixture("azure"),
pytest.lazy_fixture("gs"),
pytest.lazy_fixture("hdfs"),
pytest.lazy_fixture("local_cloud"),
pytest.lazy_fixture("s3"),
pytest.param(
pytest.lazy_fixture("ssh"),
marks=pytest.mark.skipif(
os.name == "nt", reason="unsupported on Windows."
),
),
pytest.lazy_fixture("gdrive"),
pytest.lazy_fixture("webdav"),
],
)
def test_download_dir_callback(tmp_dir, dvc, cloud):
cls, config, _ = get_cloud_fs(dvc, **cloud.config)
fs = cls(**config)
cloud.gen({"dir": {"foo": "foo", "bar": "bar"}})
callback = fsspec.Callback()
fs.download(cloud / "dir", tmp_dir / "dir", callback=callback)
assert callback.size == 2
assert callback.value == 2
assert (tmp_dir / "dir").read_text() == {"foo": "foo", "bar": "bar"}
@pytest.mark.parametrize("fs_type", ["git", "dvc"])
def test_download_callbacks_on_dvc_git_fs(tmp_dir, dvc, scm, fs_type):
gen = tmp_dir.scm_gen if fs_type == "git" else tmp_dir.dvc_gen
gen({"dir": {"foo": "foo", "bar": "bar"}, "file": "file"}, commit="gen")
fs = dvc.dvcfs if fs_type == "dvc" else scm.get_fs("HEAD")
callback = fsspec.Callback()
fs.download(tmp_dir / "file", tmp_dir / "file2", callback=callback)
size = os.path.getsize(tmp_dir / "file")
assert (tmp_dir / "file2").read_text() == "file"
assert callback.size == size
assert callback.value == size
if fs_type == "git":
pytest.skip("gitfs does not support download_dir")
callback = fsspec.Callback()
fs.download(tmp_dir / "dir", tmp_dir / "dir2", callback=callback)
assert (tmp_dir / "dir2").read_text() == {"foo": "foo", "bar": "bar"}
assert callback.size == 2
assert callback.value == 2
def test_callback_on_repo_fs(tmp_dir, dvc, scm):
tmp_dir.dvc_gen({"dir": {"bar": "bar"}}, commit="dvc")
tmp_dir.scm_gen({"dir": {"foo": "foo"}}, commit="git")
fs = dvc.repo_fs
callback = fsspec.Callback()
fs.download(tmp_dir / "dir", tmp_dir / "dir2", callback=callback)
assert (tmp_dir / "dir2").read_text() == {"foo": "foo", "bar": "bar"}
assert callback.size == 2
assert callback.value == 2
callback = fsspec.Callback()
fs.download(tmp_dir / "dir" / "foo", tmp_dir / "foo", callback=callback)
size = os.path.getsize(tmp_dir / "dir" / "foo")
assert (tmp_dir / "foo").read_text() == "foo"
assert callback.size == size
assert callback.value == size
callback = fsspec.Callback()
fs.download(tmp_dir / "dir" / "bar", tmp_dir / "bar", callback=callback)
size = os.path.getsize(tmp_dir / "dir" / "bar")
assert (tmp_dir / "bar").read_text() == "bar"
assert callback.size == size
assert callback.value == size
+4 -9
View File
@@ -9,12 +9,7 @@ from dvc.dvcfile import PIPELINE_LOCK, Lockfile, LockfileCorruptedError
from dvc.hash_info import HashInfo
from dvc.stage.utils import split_params_deps
from dvc.utils.fs import remove
from dvc.utils.serialize import (
dump_yaml,
dumps_yaml,
load_yaml,
parse_yaml_for_update,
)
from dvc.utils.serialize import dumps_yaml, parse_yaml_for_update
from tests.func.test_run_multistage import supported_params
FS_STRUCTURE = {
@@ -183,7 +178,7 @@ def v1_repo_lock(tmp_dir, dvc):
}
dvc.run(cmd="echo foo", name="foo", no_exec=True)
dvc.run(cmd="echo bar>bar.txt", outs=["bar.txt"], name="bar", no_exec=True)
dump_yaml(tmp_dir / "dvc.lock", v1_lockdata)
(tmp_dir / "dvc.lock").dump(v1_lockdata)
yield v1_lockdata
@@ -202,7 +197,7 @@ def test_migrates_v1_lockfile_to_v2_during_dump(
assert dvc.reproduce()
assert "Migrating lock file 'dvc.lock' from v1 to v2" in caplog.messages
d = load_yaml(tmp_dir / "dvc.lock")
d = (tmp_dir / "dvc.lock").parse()
assert d == {"stages": v1_repo_lock, "schema": "2.0"}
@@ -211,7 +206,7 @@ def test_migrates_v1_lockfile_to_v2_during_dump(
)
def test_lockfile_invalid_versions(tmp_dir, dvc, version_info):
lockdata = {**version_info, "stages": {"foo": {"cmd": "echo foo"}}}
dump_yaml("dvc.lock", lockdata)
(tmp_dir / "dvc.lock").dump(lockdata)
with pytest.raises(LockfileCorruptedError) as exc_info:
Lockfile(dvc, tmp_dir / "dvc.lock").load()
+3 -3
View File
@@ -6,7 +6,7 @@ import pytest
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.exceptions import DvcException, MoveNotDataSourceError
from dvc.main import main
from dvc.utils.serialize import dump_yaml, load_yaml
from dvc.utils.serialize import load_yaml
from tests.basic_env import TestDvc, TestDvcGit
from tests.func.test_repro import TestRepro
from tests.utils import cd
@@ -249,9 +249,9 @@ def test_move_output_overlap(tmp_dir, dvc):
def test_move_meta(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo")
data = load_yaml(stage.path)
data = (tmp_dir / stage.path).parse()
data["meta"] = {"custom_key": 42}
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
dvc.move("foo", "bar")
res = (tmp_dir / "bar.dvc").read_text()
+3 -2
View File
@@ -7,7 +7,6 @@ from pygtrie import Trie
from dvc.repo.index import Index
from dvc.stage import PipelineStage, Stage
from dvc.utils import relpath
from tests.func.plots.utils import _write_json
def test_index(tmp_dir, scm, dvc, run_copy):
@@ -103,7 +102,9 @@ def test_deps_outs_getters(tmp_dir, dvc, run_copy_metrics):
params=["param"],
name="copy-metrics",
)
_write_json(tmp_dir, [{"a": 1, "b": 2}, {"a": 2, "b": 3}], "metric_t.json")
(tmp_dir / "metric_t.json").dump_json(
[{"a": 1, "b": 2}, {"a": 2, "b": 3}], sort_keys=True
)
run_stage2 = run_copy_metrics(
"metric_t.json",
"metric.json",
+1 -1
View File
@@ -1244,7 +1244,7 @@ def test_repro_when_cmd_changes(tmp_dir, dvc, run_copy, mocker):
data = SingleStageFile(dvc, stage.path)._load()[0]
data["cmd"] = " ".join(stage.cmd.split()) # change cmd spacing by two
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
assert dvc.status([stage.addressing]) == {
stage.addressing: ["changed checksum"]
+26 -27
View File
@@ -9,17 +9,17 @@ from dvc.dvcfile import PIPELINE_FILE, PIPELINE_LOCK
from dvc.exceptions import CyclicGraphError, ReproductionError
from dvc.main import main
from dvc.stage import PipelineStage
from dvc.utils.serialize import dump_yaml, load_yaml
from tests.func import test_repro
from tests.utils.scriptify import scriptify
COPY_SCRIPT_FORMAT = dedent(
"""\
import sys
def copy_script(src, dest):
import shutil
shutil.copyfile({}, {})
"""
)
COPY_SCRIPT = COPY_SCRIPT_FORMAT.format("sys.argv[1]", "sys.argv[2]")
shutil.copyfile(src, dest)
COPY_SCRIPT, _ = scriptify(copy_script)
class MultiStageRun:
@@ -317,7 +317,7 @@ def test_repro_when_new_deps_is_added_in_dvcfile(tmp_dir, dvc, run_copy):
dvcfile = Dvcfile(dvc, stage.path)
data, _ = dvcfile._load()
data["stages"]["copy-file"]["deps"] += ["copy.py"]
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
assert dvc.reproduce(target)[0] == stage
@@ -339,7 +339,7 @@ def test_repro_when_new_outs_is_added_in_dvcfile(tmp_dir, dvc):
dvcfile = Dvcfile(dvc, stage.path)
data, _ = dvcfile._load()
data["stages"]["copy-file"]["outs"] = ["foobar"]
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
assert dvc.reproduce(target)[0] == stage
@@ -358,7 +358,10 @@ def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
target = ":copy-file"
assert not dvc.reproduce(target)
tmp_dir.gen("copy.py", COPY_SCRIPT_FORMAT.format("'bar'", "'foobar'"))
# hardcode values in source code, ignore sys.argv
tmp_dir.gen(
"copy.py", COPY_SCRIPT.replace("func(*argv)", 'func("bar", "foobar")')
)
from shutil import move
move("foo", "bar")
@@ -366,7 +369,7 @@ def test_repro_when_new_deps_is_moved(tmp_dir, dvc):
dvcfile = Dvcfile(dvc, stage.path)
data, _ = dvcfile._load()
data["stages"]["copy-file"]["deps"] = ["bar"]
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
assert dvc.reproduce(target)[0] == stage
@@ -376,8 +379,7 @@ def test_repro_when_new_out_overlaps_others_stage_outs(tmp_dir, dvc):
tmp_dir.gen({"dir": {"file1": "file1"}, "foo": "foo"})
dvc.add("dir")
dump_yaml(
PIPELINE_FILE,
(tmp_dir / PIPELINE_FILE).dump(
{
"stages": {
"run-copy": {
@@ -395,8 +397,7 @@ def test_repro_when_new_out_overlaps_others_stage_outs(tmp_dir, dvc):
def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("foo", "foo")
dump_yaml(
PIPELINE_FILE,
(tmp_dir / PIPELINE_FILE).dump(
{
"stages": {
"run-copy": {
@@ -414,8 +415,7 @@ def test_repro_when_new_deps_added_does_not_exist(tmp_dir, dvc):
def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("foo", "foo")
dump_yaml(
PIPELINE_FILE,
(tmp_dir / PIPELINE_FILE).dump(
{
"stages": {
"run-copy": {
@@ -433,8 +433,7 @@ def test_repro_when_new_outs_added_does_not_exist(tmp_dir, dvc):
def test_repro_when_lockfile_gets_deleted(tmp_dir, dvc):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("foo", "foo")
dump_yaml(
PIPELINE_FILE,
(tmp_dir / PIPELINE_FILE).dump(
{
"stages": {
"run-copy": {
@@ -464,13 +463,13 @@ def test_cyclic_graph_error(tmp_dir, dvc, run_copy):
run_copy("bar", "baz", name="copy-bar-baz")
run_copy("baz", "foobar", name="copy-baz-foobar")
data = load_yaml(PIPELINE_FILE)
data = (tmp_dir / PIPELINE_FILE).parse()
data["stages"]["copy-baz-foo"] = {
"cmd": "echo baz > foo",
"deps": ["baz"],
"outs": ["foo"],
}
dump_yaml(PIPELINE_FILE, data)
(tmp_dir / PIPELINE_FILE).dump(data)
with pytest.raises(CyclicGraphError):
dvc.reproduce(":copy-baz-foo")
@@ -479,8 +478,8 @@ def test_repro_multiple_params(tmp_dir, dvc):
from dvc.stage.utils import split_params_deps
from tests.func.test_run_multistage import supported_params
dump_yaml(tmp_dir / "params2.yaml", supported_params)
dump_yaml(tmp_dir / "params.yaml", supported_params)
(tmp_dir / "params2.yaml").dump(supported_params)
(tmp_dir / "params.yaml").dump(supported_params)
(tmp_dir / "foo").write_text("foo")
stage = dvc.run(
@@ -522,7 +521,7 @@ def test_repro_multiple_params(tmp_dir, dvc):
assert not dvc.reproduce(stage.addressing)
params = deepcopy(supported_params)
params["answer"] = 43
dump_yaml(tmp_dir / "params.yaml", params)
(tmp_dir / "params.yaml").dump(params)
assert dvc.reproduce(stage.addressing) == [stage]
@@ -533,7 +532,7 @@ def test_repro_list_of_commands_in_order(tmp_dir, dvc, multiline):
if multiline:
cmd = "\n".join(cmd)
dump_yaml("dvc.yaml", {"stages": {"multi": {"cmd": cmd}}})
(tmp_dir / "dvc.yaml").dump({"stages": {"multi": {"cmd": cmd}}})
(tmp_dir / "dvc.yaml").write_text(
dedent(
@@ -559,7 +558,7 @@ def test_repro_list_of_commands_raise_and_stops_after_failure(
if multiline:
cmd = "\n".join(cmd)
dump_yaml("dvc.yaml", {"stages": {"multi": {"cmd": cmd}}})
(tmp_dir / "dvc.yaml").dump({"stages": {"multi": {"cmd": cmd}}})
with pytest.raises(ReproductionError):
dvc.reproduce(targets=["multi"])
+4 -7
View File
@@ -5,7 +5,6 @@ import pytest
from dvc.exceptions import InvalidArgumentError
from dvc.stage.exceptions import DuplicateStageName, InvalidStageName
from dvc.utils.serialize import dump_yaml, parse_yaml_for_update
def test_run_with_name(tmp_dir, dvc, run_copy):
@@ -232,7 +231,7 @@ supported_params = {
def test_run_params_default(tmp_dir, dvc):
from dvc.dependency import ParamsDependency
dump_yaml(tmp_dir / "params.yaml", supported_params)
(tmp_dir / "params.yaml").dump(supported_params)
stage = dvc.run(
name="read_params",
params=["nested.nested1.nested2"],
@@ -255,7 +254,7 @@ def test_run_params_default(tmp_dir, dvc):
def test_run_params_custom_file(tmp_dir, dvc):
from dvc.dependency import ParamsDependency
dump_yaml(tmp_dir / "params2.yaml", supported_params)
(tmp_dir / "params2.yaml").dump(supported_params)
stage = dvc.run(
name="read_params",
params=["params2.yaml:lists"],
@@ -278,7 +277,7 @@ def test_run_params_custom_file(tmp_dir, dvc):
def test_run_params_no_exec(tmp_dir, dvc):
from dvc.dependency import ParamsDependency
dump_yaml(tmp_dir / "params2.yaml", supported_params)
(tmp_dir / "params2.yaml").dump(supported_params)
stage = dvc.run(
name="read_params",
params=["params2.yaml:lists"],
@@ -318,9 +317,7 @@ def test_run_overwrite_order(tmp_dir, dvc, run_copy):
run_copy("foo1", "bar1", name="copy-foo-bar", force=True)
data = parse_yaml_for_update(
(tmp_dir / PIPELINE_FILE).read_text(), PIPELINE_FILE
)
data = (tmp_dir / PIPELINE_FILE).parse()
assert list(data["stages"].keys()) == ["copy-foo-bar", "copy-bar-foobar"]
+6 -6
View File
@@ -168,34 +168,34 @@ def test_meta_is_preserved(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo content")
# Add meta to DVC-file
data = load_yaml(stage.path)
data = (tmp_dir / stage.path).parse()
data["meta"] = {"custom_key": 42}
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
# Loading and dumping to test that it works and meta is retained
dvcfile = SingleStageFile(dvc, stage.path)
new_stage = dvcfile.stage
dvcfile.dump(new_stage)
new_data = load_yaml(stage.path)
new_data = (tmp_dir / stage.path).parse()
assert new_data["meta"] == data["meta"]
def test_desc_is_preserved(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo content")
data = load_yaml(stage.path)
data = (tmp_dir / stage.path).parse()
stage_desc = "test stage description"
out_desc = "test out description"
data["desc"] = stage_desc
data["outs"][0]["desc"] = out_desc
dump_yaml(stage.path, data)
(tmp_dir / stage.path).dump(data)
dvcfile = SingleStageFile(dvc, stage.path)
new_stage = dvcfile.stage
dvcfile.dump(new_stage)
new_data = load_yaml(stage.path)
new_data = (tmp_dir / stage.path).parse()
assert new_data["desc"] == stage_desc
assert new_data["outs"][0]["desc"] == out_desc
+2 -3
View File
@@ -16,7 +16,6 @@ from dvc.stage.exceptions import (
)
from dvc.utils import relpath
from dvc.utils.fs import remove
from dvc.utils.serialize import dump_yaml
def test_collect(tmp_dir, scm, dvc, run_copy):
@@ -149,7 +148,7 @@ def test_collect_generated(tmp_dir, dvc):
"build": {"foreach": "${vars}", "do": {"cmd": "echo ${item}"}}
},
}
dump_yaml("dvc.yaml", d)
(tmp_dir / "dvc.yaml").dump(d)
all_stages = set(dvc.index.stages)
assert len(all_stages) == 5
@@ -443,7 +442,7 @@ def test_collect_repo_callback(tmp_dir, dvc, mocker):
dvc.stage_collection_error_handler = mock
(stage,) = tmp_dir.dvc_gen("foo", "foo")
dump_yaml(tmp_dir / PIPELINE_FILE, {"stages": {"cmd": "echo hello world"}})
(tmp_dir / PIPELINE_FILE).dump({"stages": {"cmd": "echo hello world"}})
dvc._reset()
assert dvc.index.stages == [stage]
+2 -3
View File
@@ -7,8 +7,7 @@ from functools import partialmethod
import pytest
from funcy import cached_property, retry
from dvc.fs.gdrive import GDriveFileSystem
from dvc.path_info import CloudURLInfo
from dvc.fs.gdrive import GDriveFileSystem, GDriveURLInfo
from dvc.utils import tmp_fname
from .base import Base
@@ -44,7 +43,7 @@ def _gdrive_retry(func):
)(func)
class GDrive(Base, CloudURLInfo):
class GDrive(Base, GDriveURLInfo):
@staticmethod
def should_test():
return bool(os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA))
+2 -2
View File
@@ -6,13 +6,13 @@ from pathlib import Path
import pytest
from dvc.path_info import URLInfo
from dvc.path_info import CloudURLInfo
from .base import Base
from .hdfs import _hdfs_root, md5md5crc32c
class WebHDFS(Base, URLInfo): # pylint: disable=abstract-method
class WebHDFS(Base, CloudURLInfo): # pylint: disable=abstract-method
@contextmanager
def _webhdfs(self):
from hdfs import InsecureClient
+2 -3
View File
@@ -1,6 +1,5 @@
import collections
import os
from unittest import mock
import pytest
@@ -191,14 +190,14 @@ def test_show_json_hide_missing(mocker, capsys):
@pytest.mark.parametrize("show_hash", [None, True, False])
@mock.patch("dvc.command.diff._show_md")
def test_diff_show_md_and_hash(mock_show_md, mocker, show_hash):
def test_diff_show_md_and_hash(mocker, show_hash):
options = ["diff", "--show-md"] + (["--show-hash"] if show_hash else [])
args = parse_args(options)
cmd = args.func(args)
diff = {}
show_hash = show_hash if show_hash else False
mock_show_md = mocker.patch("dvc.command.diff._show_md")
mocker.patch("dvc.repo.Repo.diff", return_value=diff.copy())
assert 0 == cmd.run()
+36 -12
View File
@@ -5,10 +5,23 @@ from dvc.command.machine import (
CmdMachineAdd,
CmdMachineCreate,
CmdMachineDestroy,
CmdMachineList,
CmdMachineModify,
CmdMachineRemove,
CmdMachineSsh,
)
DATA = {
".dvc": {
"config": (
"[feature]\n"
" machine = true\n"
"['machine \"foo\"']\n"
" cloud = aws"
)
}
}
def test_add(tmp_dir):
tmp_dir.gen({".dvc": {"config": "[feature]\n machine = true"}})
@@ -21,18 +34,7 @@ def test_add(tmp_dir):
def test_remove(tmp_dir):
tmp_dir.gen(
{
".dvc": {
"config": (
"[feature]\n"
" machine = true\n"
"['machine \"foo\"']\n"
" cloud = aws"
)
}
}
)
tmp_dir.gen(DATA)
cli_args = parse_args(["machine", "remove", "foo"])
assert cli_args.func == CmdMachineRemove
cmd = cli_args.func(cli_args)
@@ -78,3 +80,25 @@ def test_ssh(tmp_dir, dvc, mocker):
assert cmd.run() == 0
m.assert_called_once_with("foo")
def test_list(tmp_dir, mocker):
from dvc.ui import ui
tmp_dir.gen(DATA)
cli_args = parse_args(["machine", "list", "foo"])
assert cli_args.func == CmdMachineList
cmd = cli_args.func(cli_args)
m = mocker.patch.object(ui, "write", autospec=True)
assert cmd.run() == 0
m.assert_called_once_with("cloud=aws")
def test_modified(tmp_dir):
tmp_dir.gen(DATA)
cli_args = parse_args(["machine", "modify", "foo", "cloud", "azure"])
assert cli_args.func == CmdMachineModify
cmd = cli_args.func(cli_args)
assert cmd.run() == 0
config = configobj.ConfigObj(str(tmp_dir / ".dvc" / "config"))
assert config['machine "foo"']["cloud"] == "azure"
+1 -1
View File
@@ -336,7 +336,7 @@ def make_subrepo(dir_, scm, config=None):
dir_.add_remote(config=config)
def test_subrepos(tmp_dir, scm, dvc):
def test_subrepos(tmp_dir, scm, dvc, mocker):
tmp_dir.scm_gen(
{"dir": {"repo.txt": "file to confuse RepoFileSystem"}},
commit="dir/repo.txt",
+6
View File
@@ -43,6 +43,12 @@ def test_init():
assert fs.fs_args["password"] == fs.fs_args["passphrase"] == "xxx"
def test_ssh_ask_password(mocker):
mocker.patch("dvc.fs.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"
mock_ssh_config = """
Host example.com
User ubuntu
+2 -2
View File
@@ -6,11 +6,11 @@ from mock import patch
from dvc.fs.http import HTTPFileSystem
def test_download_fails_on_error_code(dvc, http):
def test_download_fails_on_error_code(tmp_dir, http):
fs = HTTPFileSystem(**http.config)
with pytest.raises(FileNotFoundError):
fs._download(http / "missing.txt", "missing.txt")
fs.download_file(http / "missing.txt", tmp_dir / "missing.txt")
def test_public_auth_method(dvc):
+2 -2
View File
@@ -91,8 +91,8 @@ def test_walk_files(remote):
def test_copy_preserve_etag_across_buckets(cloud, dvc):
cloud.gen(FILE_WITH_CONTENTS)
rem = _get_odb(dvc, cloud.config)
s3 = rem.fs
s3.fs.mkdir("another/")
s3 = rem.fs.s3
s3.create_bucket(Bucket="another")
config = cloud.config.copy()
config["url"] = "s3://another"
@@ -1,5 +1,3 @@
from unittest import mock
import pytest
import dvc.objects.db.slow_link_detection
@@ -14,9 +12,9 @@ def timeout_immediately(monkeypatch):
@pytest.fixture
def make_remote():
def make_remote(mocker):
def _make_remote(cache_type=None, should_warn=True):
remote = mock.Mock()
remote = mocker.Mock()
remote.cache_types = cache_type
remote.slow_link_warning = should_warn
return remote
+1 -1
View File
@@ -305,7 +305,7 @@ def test_raise_on_no_template(tmp_dir, dvc):
def test_bad_template(tmp_dir, dvc):
metric = [{"val": 2}, {"val": 3}]
tmp_dir.gen("template.json", json.dumps({"a": "b", "c": "d"}))
(tmp_dir / "template.json").dump({"a": "b", "c": "d"})
props = {"template": "template.json"}
data = {
"workspace": {"data": {"file.json": {"data": metric, "props": props}}}
+4 -5
View File
@@ -1,8 +1,7 @@
from unittest import mock
@mock.patch("dvc.repo.reproduce._reproduce_stage", returns=[])
def test_number_reproduces(reproduce_stage_mock, tmp_dir, dvc):
def test_number_reproduces(tmp_dir, dvc, mocker):
reproduce_stage_mock = mocker.patch(
"dvc.repo.reproduce._reproduce_stage", returns=[]
)
tmp_dir.dvc_gen({"pre-foo": "pre-foo"})
dvc.run(
+18 -19
View File
@@ -2,7 +2,6 @@ import os
import signal
import subprocess
import threading
from unittest import mock
import pytest
@@ -18,35 +17,35 @@ TEST_STAGE_DICT = {
}
def test_stage_checksum():
def test_stage_checksum(mocker):
stage = Stage(None, "path", cmd="mycmd")
with mock.patch.object(stage, "dumpd", return_value=TEST_STAGE_DICT):
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
mocker.patch.object(stage, "dumpd", return_value=TEST_STAGE_DICT)
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
def test_wdir_default_ignored():
def test_wdir_default_ignored(mocker):
stage = Stage(None, "path", cmd="mycmd")
d = dict(TEST_STAGE_DICT, wdir=".")
with mock.patch.object(stage, "dumpd", return_value=d):
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
mocker.patch.object(stage, "dumpd", return_value=d)
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
def test_wdir_non_default_is_not_ignored():
def test_wdir_non_default_is_not_ignored(mocker):
stage = Stage(None, "path", cmd="mycmd")
d = dict(TEST_STAGE_DICT, wdir="..")
with mock.patch.object(stage, "dumpd", return_value=d):
assert stage.compute_md5() == "2ceba15e87f6848aa756502c1e6d24e9"
mocker.patch.object(stage, "dumpd", return_value=d)
assert stage.compute_md5() == "2ceba15e87f6848aa756502c1e6d24e9"
def test_meta_ignored():
def test_meta_ignored(mocker):
stage = Stage(None, "path", cmd="mycmd")
d = dict(TEST_STAGE_DICT, meta={"author": "Suor"})
with mock.patch.object(stage, "dumpd", return_value=d):
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
mocker.patch.object(stage, "dumpd", return_value=d)
assert stage.compute_md5() == "e9521a22111493406ea64a88cda63e0b"
def test_path_conversion(dvc):
@@ -124,21 +123,21 @@ def test_external_outs(tmp_path_factory, dvc):
create_stage(Stage, dvc, "path.dvc", outs=[os.fspath(foo)], external=True)
def test_env(dvc):
def test_env(dvc, mocker):
from dvc.env import DVC_ROOT
from dvc.exceptions import DvcException
from dvc.stage import create_stage
stage = create_stage(Stage, dvc, "path.dvc", outs=["foo", "bar"])
with mock.patch.object(stage, "_read_env", return_value={"foo": "foo"}):
env = stage.env()
assert env == {DVC_ROOT: dvc.root_dir, "foo": "foo"}
mocker.patch.object(stage, "_read_env", return_value={"foo": "foo"})
env = stage.env()
assert env == {DVC_ROOT: dvc.root_dir, "foo": "foo"}
def mock_read_env(out, **kwargs):
return {"foo": str(out)}
with pytest.raises(DvcException) as exc:
with mock.patch.object(stage, "_read_env", mock_read_env):
env = stage.env()
mocker.patch.object(stage, "_read_env", mock_read_env)
_ = stage.env()
assert exc.value == "Conflicting values for env variable"
+6 -7
View File
@@ -10,17 +10,16 @@ from dvc.cli import parse_args
@pytest.fixture
def tmp_global_dir(tmp_path):
def tmp_global_dir(mocker, tmp_path):
"""
Fixture to prevent modifying the actual global config
"""
with mock.patch("dvc.config.Config.get_dir", return_value=str(tmp_path)):
yield
mocker.patch("dvc.config.Config.get_dir", return_value=str(tmp_path))
@mock.patch("dvc.daemon._spawn")
@mock.patch("json.dump")
def test_collect_and_send_report(mock_json, mock_daemon, tmp_global_dir):
def test_collect_and_send_report(mocker, tmp_global_dir):
mock_json = mocker.patch("json.dump")
mock_daemon = mocker.patch("dvc.daemon._spawn")
analytics.collect_and_send_report()
report = mock_json.call_args[0][0]
@@ -67,7 +66,7 @@ def test_send(mock_post, tmp_path):
analytics.send(str(report_file))
assert mock_post.called
assert mock_post.call_args.args[0] == url
assert mock_post.call_args[0][0] == url
assert not report_file.exists()
+4 -4
View File
@@ -17,7 +17,7 @@ from dvc.parsing.context import (
recurse_not_a_node,
)
from dvc.utils import relpath
from dvc.utils.serialize import dump_yaml, dumps_yaml
from dvc.utils.serialize import dumps_yaml
def test_context():
@@ -290,7 +290,7 @@ def test_track(tmp_dir):
}
fs = LocalFileSystem()
path = tmp_dir / "params.yaml"
dump_yaml(path, d, fs)
path.dump(d, fs=fs)
context = Context.load_from(fs, path)
@@ -326,8 +326,8 @@ def test_track_from_multiple_files(tmp_dir):
fs = LocalFileSystem()
path1 = tmp_dir / "params.yaml"
path2 = tmp_dir / "params2.yaml"
dump_yaml(path1, d1, fs)
dump_yaml(path2, d2, fs)
path1.dump(d1, fs=fs)
path2.dump(d2, fs=fs)
context = Context.load_from(fs, path1)
c = Context.load_from(fs, path2)
+1 -2
View File
@@ -15,7 +15,6 @@ from dvc.stage.exceptions import (
StageFileIsNotDvcFileError,
)
from dvc.utils.fs import remove
from dvc.utils.serialize import dump_yaml
@pytest.mark.parametrize(
@@ -66,7 +65,7 @@ def test_stage_load_on_non_file(tmp_dir, dvc, file):
@pytest.mark.parametrize("file", ["stage.dvc", "dvc.yaml"])
def test_stage_load_on_invalid_data(tmp_dir, dvc, file):
data = {"is_this_a_valid_dvcfile": False}
dump_yaml(file, data)
(tmp_dir / file).dump(data)
dvcfile = Dvcfile(dvc, file)
with pytest.raises(StageFileFormatError):
assert dvcfile.stages
+3 -4
View File
@@ -2,7 +2,6 @@ import pytest
from dvc.dvcfile import FileIsGitIgnored, Lockfile, LockfileCorruptedError
from dvc.stage import PipelineStage
from dvc.utils.serialize import dump_yaml
def test_stage_dump_no_outs_deps(tmp_dir, dvc):
@@ -17,7 +16,7 @@ def test_stage_dump_no_outs_deps(tmp_dir, dvc):
def test_stage_dump_when_already_exists(tmp_dir, dvc):
data = {"s1": {"cmd": "command", "deps": [], "outs": []}}
dump_yaml("path.lock", data)
(tmp_dir / "path.lock").dump(data)
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
lockfile = Lockfile(dvc, "path.lock")
lockfile.dump(stage)
@@ -35,7 +34,7 @@ def test_stage_dump_with_deps_and_outs(tmp_dir, dvc):
"outs": [{"md5": "2.txt", "path": "checksum"}],
}
}
dump_yaml("path.lock", data)
(tmp_dir / "path.lock").dump(data)
lockfile = Lockfile(dvc, "path.lock")
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
lockfile.dump(stage)
@@ -78,7 +77,7 @@ def test_load_when_lockfile_does_not_exist(tmp_dir, dvc):
],
)
def test_load_when_lockfile_is_corrupted(tmp_dir, dvc, corrupt_data):
dump_yaml("Dvcfile.lock", corrupt_data)
(tmp_dir / "Dvcfile.lock").dump(corrupt_data)
lockfile = Lockfile(dvc, "Dvcfile.lock")
with pytest.raises(LockfileCorruptedError) as exc_info:
lockfile.load()
+3 -3
View File
@@ -28,9 +28,9 @@ def test_plots_order(tmp_dir, dvc):
name="stage2",
)
assert get_files(dvc.plots.show()) == {
assert get_files(dvc.plots.show()) == [
"p.json",
os.path.join("sub", "p4.json"),
"p1.json",
os.path.join("sub", "p3.json"),
}
os.path.join("sub", "p4.json"),
]
+17 -18
View File
@@ -1,6 +1,4 @@
import logging
import sys
from unittest import mock
from dvc.progress import Tqdm
from dvc.utils import env2bool
@@ -15,15 +13,15 @@ def test_quiet_logging(caplog, capsys):
assert out_err.err == ""
def test_quiet_logging_disable_false(caplog, capsys):
def test_quiet_logging_disable_false(caplog, capsys, mocker):
# simulate interactive terminal
mocker.patch("sys.stdout.isatty", return_value=True)
with caplog.at_level(logging.CRITICAL, logger="dvc"):
# simulate interactive terminal
with mock.patch.object(sys.stderr, "isatty", return_value=True):
for _ in Tqdm(range(10), disable=False):
pass
out_err = capsys.readouterr()
assert out_err.out == ""
assert out_err.err == ""
for _ in Tqdm(range(10), disable=False):
pass
out_err = capsys.readouterr()
assert out_err.out == ""
assert out_err.err == ""
def test_quiet_notty(caplog, capsys):
@@ -38,12 +36,13 @@ def test_quiet_notty(caplog, capsys):
assert out_err.err == ""
def test_default(caplog, capsys):
def test_default(caplog, capsys, mocker):
# simulate interactive terminal
mocker.patch("sys.stdout.isatty", return_value=True)
with caplog.at_level(logging.INFO, logger="dvc"):
# simulate interactive terminal
with mock.patch.object(sys.stderr, "isatty", return_value=True):
for _ in Tqdm(range(10)):
pass
out_err = capsys.readouterr()
assert out_err.out == ""
assert "0/10" in out_err.err
for _ in Tqdm(range(10)):
pass
out_err = capsys.readouterr()
assert out_err.out == ""
assert "0/10" in out_err.err
+8 -10
View File
@@ -2,7 +2,6 @@ import json
import logging
import os
import time
from unittest import mock
import pytest
@@ -12,12 +11,11 @@ from tests.func.parsing.test_errors import escape_ansi
@pytest.fixture
def tmp_global_dir(tmp_path):
def tmp_global_dir(mocker, tmp_path):
"""
Fixture to prevent modifying the actual global config
"""
with mock.patch("dvc.config.Config.get_dir", return_value=str(tmp_path)):
yield
mocker.patch("dvc.config.Config.get_dir", return_value=str(tmp_path))
@pytest.fixture(autouse=True)
@@ -36,8 +34,8 @@ def mock_tty(mocker):
return mocker.patch("sys.stdout.isatty", return_value=True)
@mock.patch("requests.get")
def test_fetch(mock_get, updater):
def test_fetch(mocker, updater):
mock_get = mocker.patch("requests.get")
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"version": __version__}
@@ -70,8 +68,8 @@ def test_is_enabled(dvc, updater, config, result):
@pytest.mark.parametrize("result", [True, False])
@mock.patch("dvc.updater.Updater._check")
def test_check_update_respect_config(mock_check, result, updater, mocker):
def test_check_update_respect_config(result, updater, mocker):
mock_check = mocker.patch("dvc.updater.Updater._check")
mocker.patch.object(updater, "is_enabled", return_value=result)
updater.check()
assert result == mock_check.called
@@ -139,8 +137,8 @@ def test_check_fetches_on_invalid_data_format(
fetch.assert_called_once()
@mock.patch("dvc.updater.Updater._check")
def test_check(mock_check, updater):
def test_check(mocker, updater):
mock_check = mocker.patch("dvc.updater.Updater._check")
updater.check()
updater.check()
updater.check()
+13 -13
View File
@@ -1,20 +1,20 @@
import subprocess
from unittest import mock
import dvc.version
def test_is_release():
with mock.patch.object(dvc.version.subprocess, "check_output") as m:
m.side_effect = subprocess.CalledProcessError(1, "cmd")
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret is False
def test_is_release(mocker):
m = mocker.patch.object(dvc.version.subprocess, "check_output")
m.side_effect = None
m.return_value = dvc.version._BASE_VERSION.encode("ascii")
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret
m.side_effect = subprocess.CalledProcessError(1, "cmd")
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret is False
m.return_value = b"630d1741c2d5dd89a3176bd15b63121b905d35c9"
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret is False
m.side_effect = None
m.return_value = dvc.version._BASE_VERSION.encode("ascii")
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret
m.return_value = b"630d1741c2d5dd89a3176bd15b63121b905d35c9"
ret = dvc.version._is_release(None, dvc.version._BASE_VERSION)
assert ret is False
+36
View File
@@ -0,0 +1,36 @@
import inspect
import sys
import textwrap
from pathlib import Path
SCRIPT_TEMPLATE = """\
#! {executable}
# AUTOGENERATED USING SCRIPTIFY FOR USE IN TESTS FOR DVC
{code}
if __name__ == "__main__":
import sys
_, *argv = sys.argv
func = {func!s}
func(*argv)
"""
def scriptify(func, path=None):
# you can use `breakpoint()` inside functions too
# just remember to run the test in `-s` mode.
code = textwrap.dedent(inspect.getsource(func))
executable = sys.executable
source = SCRIPT_TEMPLATE.format(
executable=executable, code=code, func=func.__name__
)
if not path:
return source, None
Path(path).write_text(source, encoding="utf-8")
return source, f"{executable} {path}"