diff --git a/dvc/exceptions.py b/dvc/exceptions.py index c40677170e..ee860fe71b 100644 --- a/dvc/exceptions.py +++ b/dvc/exceptions.py @@ -1,6 +1,6 @@ """Exceptions raised by the dvc.""" -from dvc.utils import relpath +from dvc.utils import relpath, format_link class DvcException(Exception): @@ -310,3 +310,18 @@ def __init__(self, path, repo): " neither as an output nor a git-handled file." ) super().__init__(msg.format(path, repo)) + + +class RemoteCacheRequiredError(DvcException): + def __init__(self, path_info): + super().__init__( + ( + "Current operation was unsuccessful because '{}' requires " + "existing cache on '{}' remote. See {} for information on how " + "to set up remote cache." + ).format( + path_info, + path_info.scheme, + format_link("https://man.dvc.org/config#cache"), + ) + ) diff --git a/dvc/output/base.py b/dvc/output/base.py index 0fca876c4a..ee69e30bcd 100644 --- a/dvc/output/base.py +++ b/dvc/output/base.py @@ -6,7 +6,7 @@ import dvc.prompt as prompt from dvc.cache import NamedCache -from dvc.exceptions import CollectCacheError +from dvc.exceptions import CollectCacheError, RemoteCacheRequiredError from dvc.exceptions import DvcException from dvc.remote.base import RemoteBASE @@ -98,14 +98,9 @@ def __init__( self.persist = persist self.tags = None if self.IS_DEPENDENCY else (tags or {}) - if self.use_cache and self.cache is None: - raise DvcException( - "no cache location setup for '{}' outputs.".format( - self.REMOTE.scheme - ) - ) - self.path_info = self._parse_path(remote, path) + if self.use_cache and self.cache is None: + raise RemoteCacheRequiredError(self.path_info) def _parse_path(self, remote, path): if remote: diff --git a/dvc/remote/base.py b/dvc/remote/base.py index 1f466dad23..33ec566bb7 100644 --- a/dvc/remote/base.py +++ b/dvc/remote/base.py @@ -18,6 +18,7 @@ DvcException, ConfirmRemoveError, DvcIgnoreInCollectedDirError, + RemoteCacheRequiredError, ) from dvc.ignore import DvcIgnore from dvc.path_info import PathInfo, URLInfo @@ -233,6 +234,9 @@ def _collect_dir(self, path_info): return sorted(result, key=itemgetter(self.PARAM_RELPATH)) def get_dir_checksum(self, path_info): + if not self.cache: + raise RemoteCacheRequiredError(path_info) + dir_info = self._collect_dir(path_info) checksum, tmp_info = self._get_dir_info_checksum(dir_info) new_info = self.cache.checksum_to_path_info(checksum) diff --git a/tests/func/test_remote.py b/tests/func/test_remote.py index 4a4ce4b306..97a471a51a 100644 --- a/tests/func/test_remote.py +++ b/tests/func/test_remote.py @@ -11,7 +11,7 @@ from dvc.main import main from dvc.path_info import PathInfo from dvc.remote import RemoteLOCAL, RemoteConfig -from dvc.remote.base import RemoteBASE +from dvc.remote.base import RemoteBASE, RemoteCacheRequiredError from dvc.compat import fspath from tests.basic_env import TestDvc from tests.remotes import Local @@ -257,3 +257,14 @@ def test_modify_missing_remote(dvc): with pytest.raises(ConfigError, match=r"Unable to find remote section"): remote_config.modify("myremote", "gdrive_client_id", "xxx") + + +def test_external_dir_resource_on_no_cache(tmp_dir, dvc, tmp_path_factory): + # https://github.com/iterative/dvc/issues/2647, is some situations + # (external dir dependency) cache is required to calculate dir md5 + external_dir = tmp_path_factory.mktemp("external_dir") + (external_dir / "file").write_text("content") + + dvc.cache.local = None + with pytest.raises(RemoteCacheRequiredError): + dvc.run(deps=[fspath(external_dir)])