objects: use object IDs and references instead of naive objs in status/transfer (#6360)

* objects.status: use object IDs instead of naive objects

* objects.transfer: use object IDs instead of naive objects

* objects: add ReferenceHashFile

* odb: add ReferenceObjectDB

* repo: collect used object IDs instead of naive objs

* support basic/naive fs serialization

* objects.stage: stage file objects in addition to trees

* objects: remove save(), update transfer()

* output/dep: update for osave removal

* path_info: support both url and path attributes

* status/gc: use object IDs instead of naive objs

* diff: update staging usage

* commit: update staging usage

* tests: clean staging when needed

* tests: update for save/transfer/stage changes

* dvcfs: don't allow config serialization (repofs should be used instead)

* utils: return mtime as str, size as int

* tests: update unit tests for save/stage/transfer changes

* objects: use Repo.open when needed in ReferenceHashFile

* fix ls test staging

* objects.stage: don't skip hash check when loading from state

- individual hashes will still be loaded from state if they are still
  valid

* ReferenceHashFile: serialize repo fs paths relative to repo root

* preserve state when staging

* objects.tree: skip hash computation in digest() when we have already
loaded hash from state

* always check file existence in referencehashfile

* update repofs test since stage no longer allows nonexistent files

* odb: only protect on check() if hash was verified

* staging: use unique memfs namespace for staging per dest ODB

* tests: clean full staging namespace

* refodb: preserve repofs cache dir

* repodependency: stage imports to local ODB and strict check circular
imports

* fetch: fetch staging last

* staging: make dest odb mandatory and preserve state

* refodb: cache nonlocal fs's

* fix rebase errors

* repofs: support full instantiation from fs config

* repofs: preserve subrepo factory for erepos

* tests: clarify repofs dirty file behavior
This commit is contained in:
Peter Rowlands (변기호)
2021-08-04 13:55:52 +03:00
committed by GitHub
parent 34697ef298
commit 114a07e7a4
42 changed files with 945 additions and 523 deletions
+6 -2
View File
@@ -35,7 +35,9 @@ def _changed(path_info, fs, obj, cache, state=None):
actual = state.get(path_info, fs) if state else None actual = state.get(path_info, fs) if state else None
if actual is None: if actual is None:
try: try:
actual = stage(cache, path_info, fs, obj.hash_info.name).hash_info actual = stage(
cache, path_info, fs, obj.hash_info.name, dry_run=True
)[1].hash_info
except FileNotFoundError: except FileNotFoundError:
logger.debug("'%s' doesn't exist.", path_info) logger.debug("'%s' doesn't exist.", path_info)
return True return True
@@ -61,7 +63,9 @@ def _remove(path_info, fs, cache, force=False):
fs.remove(path_info) fs.remove(path_info)
return return
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM).hash_info current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM, dry_run=True)[
1
].hash_info
try: try:
obj = load(cache, current) obj = load(cache, current)
check(cache, obj) check(cache, obj)
+4 -4
View File
@@ -6,8 +6,8 @@ from typing import TYPE_CHECKING, Iterable, Optional
from dvc.objects.db import get_index from dvc.objects.db import get_index
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from dvc.objects.db.base import ObjectDB from dvc.objects.db.base import ObjectDB
from dvc.objects.file import HashFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -64,7 +64,7 @@ class DataCloud:
def push( def push(
self, self,
objs: Iterable["HashFile"], objs: Iterable["HashInfo"],
jobs: Optional[int] = None, jobs: Optional[int] = None,
remote: Optional[str] = None, remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None, odb: Optional["ObjectDB"] = None,
@@ -93,7 +93,7 @@ class DataCloud:
def pull( def pull(
self, self,
objs: Iterable["HashFile"], objs: Iterable["HashInfo"],
jobs: Optional[int] = None, jobs: Optional[int] = None,
remote: Optional[str] = None, remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None, odb: Optional["ObjectDB"] = None,
@@ -123,7 +123,7 @@ class DataCloud:
def status( def status(
self, self,
objs: Iterable["HashFile"], objs: Iterable["HashInfo"],
jobs: Optional[int] = None, jobs: Optional[int] = None,
remote: Optional[str] = None, remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None, odb: Optional["ObjectDB"] = None,
+47 -21
View File
@@ -1,8 +1,8 @@
import os import os
from collections import defaultdict from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from funcy import first
from voluptuous import Required from voluptuous import Required
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
@@ -10,6 +10,7 @@ from dvc.path_info import PathInfo
from .base import Dependency from .base import Dependency
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from dvc.objects.db.base import ObjectDB from dvc.objects.db.base import ObjectDB
from dvc.objects.file import HashFile from dvc.objects.file import HashFile
@@ -91,23 +92,24 @@ class RepoDependency(Dependency):
def get_used_objs( def get_used_objs(
self, **kwargs self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]: ) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _ = self._get_used_and_obj(**kwargs) used, _ = self._get_used_and_obj(**kwargs)
return used return used
def _get_used_and_obj( def _get_used_and_obj(
self, obj_only=False, **kwargs self, obj_only=False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashFile"]], "HashFile"]: ) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
from dvc.config import NoRemoteError from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.objects.stage import get_staging, stage from dvc.objects.stage import stage
from dvc.objects.tree import Tree
local_odb = self.repo.odb.local local_odb = self.repo.odb.local
locked = kwargs.pop("locked", True) locked = kwargs.pop("locked", True)
with self._make_repo( with self._make_repo(
locked=locked, cache_dir=local_odb.cache_dir locked=locked, cache_dir=local_odb.cache_dir
) as repo: ) as repo:
used_objs = defaultdict(set) used_obj_ids = defaultdict(set)
rev = repo.get_rev() rev = repo.get_rev()
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None: if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev self.def_repo[self.PARAM_REV_LOCK] = rev
@@ -115,7 +117,7 @@ class RepoDependency(Dependency):
path_info = PathInfo(repo.root_dir) / str(self.def_path) path_info = PathInfo(repo.root_dir) / str(self.def_path)
if not obj_only: if not obj_only:
try: try:
for odb, objs in repo.used_objs( for odb, obj_ids in repo.used_objs(
[os.fspath(path_info)], [os.fspath(path_info)],
force=True, force=True,
jobs=kwargs.get("jobs"), jobs=kwargs.get("jobs"),
@@ -124,14 +126,14 @@ class RepoDependency(Dependency):
if odb is None: if odb is None:
odb = repo.cloud.get_remote_odb() odb = repo.cloud.get_remote_odb()
odb.read_only = True odb.read_only = True
self._check_circular_import(objs) self._check_circular_import(odb, obj_ids)
used_objs[odb].update(objs) used_obj_ids[odb].update(obj_ids)
except (NoRemoteError, NoOutputOrStageError): except (NoRemoteError, NoOutputOrStageError):
pass pass
try: try:
staged_obj = stage( staging, staged_obj = stage(
None, local_odb,
path_info, path_info,
repo.repo_fs, repo.repo_fs,
local_odb.fs.PARAM_CHECKSUM, local_odb.fs.PARAM_CHECKSUM,
@@ -140,27 +142,51 @@ class RepoDependency(Dependency):
raise PathMissingError( raise PathMissingError(
self.def_path, self.def_repo[self.PARAM_URL] self.def_path, self.def_repo[self.PARAM_URL]
) from exc ) from exc
staging = get_staging() staging = copy(staging)
staging.read_only = True staging.read_only = True
self._staged_objs[rev] = staged_obj self._staged_objs[rev] = staged_obj
used_objs[staging].add(staged_obj) used_obj_ids[staging].add(staged_obj.hash_info)
return used_objs, staged_obj if isinstance(staged_obj, Tree):
used_obj_ids[staging].update(
entry.hash_info for _, entry in staged_obj
)
return used_obj_ids, staged_obj
def _check_circular_import(self, objs): def _check_circular_import(self, odb, obj_ids):
from dvc.exceptions import CircularImportError from dvc.exceptions import CircularImportError
from dvc.fs.repo import RepoFileSystem from dvc.fs.repo import RepoFileSystem
from dvc.objects.db.reference import ReferenceObjectDB
from dvc.objects.tree import Tree from dvc.objects.tree import Tree
obj = first(objs) if not isinstance(odb, ReferenceObjectDB):
if isinstance(obj, Tree):
_, obj = first(obj)
if not isinstance(obj.fs, RepoFileSystem):
return return
self_url = self.repo.url or self.repo.root_dir def iter_objs():
if obj.fs.repo_url is not None and obj.fs.repo_url == self_url: for hash_info in obj_ids:
raise CircularImportError(self, obj.fs.repo_url, self_url) if hash_info.isdir:
tree = Tree.load(odb, hash_info)
yield from (odb.get(entry.hash_info) for _, entry in tree)
else:
yield odb.get(hash_info)
checked_urls = set()
for obj in iter_objs():
if not isinstance(obj.fs, RepoFileSystem):
continue
if (
obj.fs.repo_url in checked_urls
or obj.fs.root_dir in checked_urls
):
continue
self_url = self.repo.url or self.repo.root_dir
if (
obj.fs.repo_url is not None
and obj.fs.repo_url == self_url
or obj.fs.root_dir == self.repo.root_dir
):
raise CircularImportError(self, obj.fs.repo_url, self_url)
checked_urls.update([obj.fs.repo_url, obj.fs.root_dir])
def get_obj(self, filter_info=None, **kwargs): def get_obj(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True) locked = kwargs.get("locked", True)
+13 -9
View File
@@ -42,14 +42,6 @@ def external_repo(
config = _get_remote_config(url) if os.path.isdir(url) else {} config = _get_remote_config(url) if os.path.isdir(url) else {}
config.update(cache_config) config.update(cache_config)
def make_repo(path, **_kwargs):
_config = cache_config.copy()
if os.path.isdir(url):
rel = os.path.relpath(path, _kwargs["scm"].root_dir)
repo_path = os.path.join(url, rel)
_config.update(_get_remote_config(repo_path))
return Repo(path, config=_config, **_kwargs)
root_dir = path if for_write else os.path.realpath(path) root_dir = path if for_write else os.path.realpath(path)
repo_kwargs = dict( repo_kwargs = dict(
root_dir=root_dir, root_dir=root_dir,
@@ -57,7 +49,7 @@ def external_repo(
scm=None if for_write else Git(root_dir), scm=None if for_write else Git(root_dir),
rev=None if for_write else rev, rev=None if for_write else rev,
config=config, config=config,
repo_factory=make_repo, repo_factory=erepo_factory(url, cache_config),
**kwargs, **kwargs,
) )
@@ -87,6 +79,18 @@ def external_repo(
_remove(path) _remove(path)
def erepo_factory(url, cache_config, *args, **kwargs):
def make_repo(path, **_kwargs):
_config = cache_config.copy()
if os.path.isdir(url):
rel = os.path.relpath(path, _kwargs["scm"].root_dir)
repo_path = os.path.join(url, rel)
_config.update(_get_remote_config(repo_path))
return Repo(path, config=_config, **_kwargs)
return make_repo
CLONES: Dict[str, str] = {} CLONES: Dict[str, str] = {}
CACHE_DIRS: Dict[str, str] = {} CACHE_DIRS: Dict[str, str] = {}
+3 -2
View File
@@ -32,8 +32,9 @@ FS_MAP = {
} }
def get_fs_cls(remote_conf): def get_fs_cls(remote_conf, scheme=None):
scheme = urlparse(remote_conf["url"]).scheme if not scheme:
scheme = urlparse(remote_conf["url"]).scheme
return FS_MAP.get(scheme, LocalFileSystem) return FS_MAP.get(scheme, LocalFileSystem)
+5
View File
@@ -59,6 +59,11 @@ class BaseFileSystem:
self.jobs = kwargs.get("jobs") or self._JOBS self.jobs = kwargs.get("jobs") or self._JOBS
self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS
self._config = kwargs
@property
def config(self):
return self._config
@classmethod @classmethod
def _strip_protocol(cls, path: str): def _strip_protocol(cls, path: str):
+4
View File
@@ -32,6 +32,10 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
super().__init__(**kwargs) super().__init__(**kwargs)
self.repo = kwargs["repo"] self.repo = kwargs["repo"]
@property
def config(self):
raise NotImplementedError
def _find_outs(self, path, *args, **kwargs): def _find_outs(self, path, *args, **kwargs):
outs = self.repo.find_outs_by_path(path, *args, **kwargs) outs = self.repo.find_outs_by_path(path, *args, **kwargs)
+77 -1
View File
@@ -35,14 +35,29 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
scheme = "local" scheme = "local"
PARAM_CHECKSUM = "md5" PARAM_CHECKSUM = "md5"
PARAM_REPO_URL = "repo_url"
PARAM_REPO_ROOT = "repo_root"
PARAM_REV = "rev"
PARAM_CACHE_DIR = "cache_dir"
PARAM_CACHE_TYPES = "cache_types"
PARAM_SUBREPOS = "subrepos"
def __init__( def __init__(
self, repo=None, subrepos=False, repo_factory: RepoFactory = None self,
repo: Optional["Repo"] = None,
subrepos=False,
repo_factory: RepoFactory = None,
**kwargs,
): ):
super().__init__() super().__init__()
from dvc.utils.collections import PathStringTrie from dvc.utils.collections import PathStringTrie
if repo is None:
repo, repo_factory = self._repo_from_fs_config(
subrepos=subrepos, **kwargs
)
if not repo_factory: if not repo_factory:
from dvc.repo import Repo from dvc.repo import Repo
@@ -72,6 +87,67 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return None return None
return self._main_repo.url return self._main_repo.url
@property
def config(self):
return {
self.PARAM_REPO_URL: self.repo_url,
self.PARAM_REPO_ROOT: self.root_dir,
self.PARAM_REV: getattr(self._main_repo.fs, "rev", None),
self.PARAM_CACHE_DIR: os.path.abspath(
self._main_repo.odb.local.cache_dir
),
self.PARAM_CACHE_TYPES: self._main_repo.odb.local.cache_types,
self.PARAM_SUBREPOS: self._traverse_subrepos,
}
@classmethod
def _repo_from_fs_config(
cls, **config
) -> Tuple["Repo", Optional["RepoFactory"]]:
from dvc.external_repo import erepo_factory, external_repo
from dvc.repo import Repo
url = config.get(cls.PARAM_REPO_URL)
root = config.get(cls.PARAM_REPO_ROOT)
assert url or root
def _open(*args, **kwargs):
# NOTE: if original repo was an erepo (and has a URL),
# we cannot use Repo.open() since it will skip erepo
# cache/remote setup for local URLs
if url is None:
return Repo.open(*args, **kwargs)
return external_repo(*args, **kwargs)
cache_dir = config.get(cls.PARAM_CACHE_DIR)
cache_config = (
{}
if not cache_dir
else {
"cache": {
"dir": cache_dir,
"type": config.get(cls.PARAM_CACHE_TYPES),
}
}
)
repo_kwargs: dict = {
"rev": config.get(cls.PARAM_REV),
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
"uninitialized": True,
}
factory: Optional["RepoFactory"] = None
if url is None:
repo_kwargs["config"] = cache_config
else:
repo_kwargs["cache_dir"] = cache_dir
factory = erepo_factory(url, cache_config)
with _open(
url if url else root,
**repo_kwargs,
) as repo:
return repo, factory
def _get_repo(self, path: str) -> Optional["Repo"]: def _get_repo(self, path: str) -> Optional["Repo"]:
"""Returns repo that the path falls in, using prefix. """Returns repo that the path falls in, using prefix.
+1
View File
@@ -14,6 +14,7 @@ class HashInfo:
value: Optional[str] value: Optional[str]
size: Optional[int] = field(default=None, compare=False) size: Optional[int] = field(default=None, compare=False)
nfiles: Optional[int] = field(default=None, compare=False) nfiles: Optional[int] = field(default=None, compare=False)
obj_name: Optional[str] = field(default=None, compare=False)
def __bool__(self): def __bool__(self):
return bool(self.value) return bool(self.value)
+4 -45
View File
@@ -1,8 +1,5 @@
import logging import logging
from concurrent.futures import ThreadPoolExecutor, as_completed from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from dvc.progress import Tqdm
from .tree import Tree from .tree import Tree
@@ -15,50 +12,12 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def save( def check(odb: "ObjectDB", obj: "HashFile", **kwargs):
odb: "ObjectDB",
obj: "HashFile",
jobs: Optional[int] = None,
**kwargs,
):
if isinstance(obj, Tree):
from .stage import get_staging
with ThreadPoolExecutor(max_workers=jobs) as executor:
for future in Tqdm(
as_completed(
executor.submit(
odb.add,
entry.path_info,
entry.fs,
entry.hash_info,
**kwargs,
)
for _, entry in obj
),
desc="Saving files",
total=len(obj),
unit="file",
):
future.result()
# if dir cache for this tree has already been staged, move the
# staged object when possible
staging = get_staging(odb)
if staging.exists(obj.hash_info):
obj = staging.get(obj.hash_info)
kwargs["move"] = True
assert obj.path_info and obj.fs and obj.hash_info
odb.add(obj.path_info, obj.fs, obj.hash_info, **kwargs)
def check(odb: "ObjectDB", obj: "HashFile"):
if isinstance(obj, Tree): if isinstance(obj, Tree):
for _, entry in obj: for _, entry in obj:
odb.check(entry.hash_info) odb.check(entry.hash_info, **kwargs)
odb.check(obj.hash_info) odb.check(obj.hash_info, **kwargs)
def load(odb: "ObjectDB", hash_info: "HashInfo") -> "HashFile": def load(odb: "ObjectDB", hash_info: "HashInfo") -> "HashFile":
+24 -13
View File
@@ -13,7 +13,7 @@ from dvc.progress import Tqdm
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem from dvc.fs.base import BaseFileSystem
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.types import AnyPath from dvc.types import AnyPath, DvcPath
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -76,7 +76,14 @@ class ObjectDB:
hash_info, hash_info,
) )
def _add_file(self, from_fs, from_info, to_info, move): def _add_file(
self,
from_fs: "BaseFileSystem",
from_info: "AnyPath",
to_info: "DvcPath",
_hash_info: "HashInfo",
move: bool = False,
):
self.makedirs(to_info.parent) self.makedirs(to_info.parent)
use_move = isinstance(from_fs, type(self.fs)) and move use_move = isinstance(from_fs, type(self.fs)) and move
try: try:
@@ -131,8 +138,8 @@ class ObjectDB:
pass pass
cache_info = self.hash_to_path_info(hash_info.value) cache_info = self.hash_to_path_info(hash_info.value)
# using our makedirs to create dirs with proper permissions self._add_file(fs, path_info, cache_info, hash_info, move)
self._add_file(fs, path_info, cache_info, move)
try: try:
if verify: if verify:
self.check(hash_info, check_hash=True) self.check(hash_info, check_hash=True)
@@ -145,7 +152,7 @@ class ObjectDB:
if callback: if callback:
callback(1) callback(1)
def hash_to_path_info(self, hash_): def hash_to_path_info(self, hash_) -> "DvcPath":
return self.path_info / hash_[0:2] / hash_[2:] return self.path_info / hash_[0:2] / hash_[2:]
# Override to return path as a string instead of PathInfo for clouds # Override to return path as a string instead of PathInfo for clouds
@@ -199,9 +206,10 @@ class ObjectDB:
self.fs.remove(obj.path_info) self.fs.remove(obj.path_info)
raise raise
# making cache file read-only so we don't need to check it if check_hash:
# next time # making cache file read-only so we don't need to check it
self.protect(obj.path_info) # next time
self.protect(obj.path_info)
def _list_paths(self, prefix=None, progress_callback=None): def _list_paths(self, prefix=None, progress_callback=None):
if prefix: if prefix:
@@ -378,17 +386,20 @@ class ObjectDB:
def _remove_unpacked_dir(self, hash_): def _remove_unpacked_dir(self, hash_):
pass pass
def gc(self, used, jobs=None): def gc(self, used, jobs=None, cache_odb=None, shallow=True):
from ..tree import Tree from ..tree import Tree
if self.read_only: if self.read_only:
raise ObjectDBPermissionError("Cannot gc read-only ODB") raise ObjectDBPermissionError("Cannot gc read-only ODB")
if not cache_odb:
cache_odb = self
used_hashes = set() used_hashes = set()
for obj in used: for hash_info in used:
used_hashes.add(obj.hash_info.value) used_hashes.add(hash_info.value)
if isinstance(obj, Tree): if hash_info.isdir and not shallow:
tree = Tree.load(cache_odb, hash_info)
used_hashes.update( used_hashes.update(
entry_obj.hash_info.value for _, entry_obj in obj entry_obj.hash_info.value for _, entry_obj in tree
) )
removed = False removed = False
+77
View File
@@ -0,0 +1,77 @@
import io
import logging
import os
from typing import TYPE_CHECKING, Dict
from dvc.scheme import Schemes
from ..errors import ObjectFormatError
from ..reference import ReferenceHashFile
from .base import ObjectDB
if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem
from dvc.hash_info import HashInfo
from dvc.types import AnyPath, DvcPath
logger = logging.getLogger(__name__)
class ReferenceObjectDB(ObjectDB):
"""Reference ODB.
File objects are stored as ReferenceHashFiles which reference paths outside
of the staging ODB fs. Tree objects are stored natively.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._fs_cache: Dict[tuple, "BaseFileSystem"] = {}
def get(self, hash_info: "HashInfo"):
if hash_info.isdir:
return super().get(hash_info)
path_info = self.hash_to_path(hash_info.value)
try:
with self.fs.open(path_info, "rb") as fobj:
ref_file = ReferenceHashFile.from_bytes(
fobj.read(), fs_cache=self._fs_cache
)
except OSError:
raise FileNotFoundError
try:
ref_file.check(self, check_hash=False)
except ObjectFormatError:
self.fs.remove(path_info)
raise
return ref_file
def _add_file(
self,
from_fs: "BaseFileSystem",
from_info: "AnyPath",
to_info: "DvcPath",
hash_info: "HashInfo",
move: bool = False,
):
self.makedirs(to_info.parent)
if hash_info.isdir:
return super()._add_file(
from_fs, from_info, to_info, hash_info, move
)
ref_file = ReferenceHashFile(from_info, from_fs, hash_info)
ref_fobj = io.BytesIO(ref_file.to_bytes())
ref_fobj.seek(0)
try:
self.fs.upload_fobj(ref_fobj, to_info)
except OSError as exc:
if isinstance(exc, FileExistsError) or (
os.name == "nt"
and exc.__context__
and isinstance(exc.__context__, FileExistsError)
):
logger.debug("'%s' file already exists, skipping", to_info)
else:
raise
if from_fs.scheme != Schemes.LOCAL:
self._fs_cache[ReferenceHashFile.config_tuple(from_fs)] = from_fs
+11 -5
View File
@@ -8,7 +8,9 @@ from .errors import ObjectFormatError
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem from dvc.fs.base import BaseFileSystem
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.types import DvcPath from dvc.types import AnyPath
from .db.base import ObjectDB
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -16,7 +18,7 @@ logger = logging.getLogger(__name__)
class HashFile: class HashFile:
def __init__( def __init__(
self, self,
path_info: Optional["DvcPath"], path_info: Optional["AnyPath"],
fs: Optional["BaseFileSystem"], fs: Optional["BaseFileSystem"],
hash_info: "HashInfo", hash_info: "HashInfo",
name: Optional[str] = None, name: Optional[str] = None,
@@ -57,10 +59,9 @@ class HashFile:
) )
) )
def check(self, odb, check_hash=True): def check(self, odb: "ObjectDB", check_hash: bool = True):
from .stage import get_file_hash
if not check_hash: if not check_hash:
assert self.fs
if not self.fs.exists(self.path_info): if not self.fs.exists(self.path_info):
raise FileNotFoundError( raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), self.path_info errno.ENOENT, os.strerror(errno.ENOENT), self.path_info
@@ -68,6 +69,11 @@ class HashFile:
else: else:
return None return None
self._check_hash(odb)
def _check_hash(self, odb):
from .stage import get_file_hash
actual = get_file_hash( actual = get_file_hash(
self.path_info, self.fs, self.hash_info.name, odb.state self.path_info, self.fs, self.hash_info.name, odb.state
) )
+135
View File
@@ -0,0 +1,135 @@
import errno
import logging
import os
import pickle
from typing import TYPE_CHECKING, Optional, Tuple
from .errors import ObjectFormatError
from .file import HashFile
if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem
from dvc.hash_info import HashInfo
from dvc.types import AnyPath
from .db.base import ObjectDB
logger = logging.getLogger(__name__)
class ReferenceHashFile(HashFile):
PARAM_PATH = "path"
PARAM_HASH = "hash"
PARAM_MTIME = "mtime"
PARAM_SIZE = "size"
PARAM_FS_CONFIG = "fs_config"
def __init__(
self,
path_info: "AnyPath",
fs: "BaseFileSystem",
hash_info: "HashInfo",
mtime: Optional[str] = None,
size: Optional[int] = None,
**kwargs,
):
super().__init__(path_info, fs, hash_info, **kwargs)
cur_mtime, cur_size = self._get_mtime_and_size()
self.mtime = cur_mtime if mtime is None else mtime
self.hash_info.size = cur_size if size is None else size
def __str__(self):
return f"ref object {self.hash_info} -> {self.path_info}"
def check(self, odb: "ObjectDB", check_hash: bool = True):
assert self.fs
if not self.fs.exists(self.path_info):
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), self.path_info
)
if not (self.mtime, self.size) == self._get_mtime_and_size():
raise ObjectFormatError(f"{self} is changed")
if check_hash:
self._check_hash(odb)
def _get_mtime_and_size(self) -> Tuple[Optional[str], int]:
from dvc.utils.fs import get_mtime_and_size
assert self.fs
if hasattr(self.fs, "stat"):
try:
return get_mtime_and_size(self.path_info, self.fs)
except FileNotFoundError:
pass
return None, self.fs.getsize(self.path_info)
def to_bytes(self):
from dvc.fs.repo import RepoFileSystem
from dvc.path_info import PathInfo
# NOTE: dumping reference FS's this way is insecure, as the
# fully parsed remote FS config will include credentials
#
# ReferenceHashFiles should currently only be serialized in
# memory and not to disk
path_info = self.path_info
if isinstance(self.fs, RepoFileSystem):
path_info = PathInfo(path_info).relative_to(self.fs.root_dir)
dict_ = {
self.PARAM_PATH: path_info,
self.PARAM_HASH: self.hash_info,
self.PARAM_MTIME: self.mtime,
self.PARAM_SIZE: self.size,
self.PARAM_FS_CONFIG: self.config_tuple(self.fs),
}
try:
return pickle.dumps(dict_)
except pickle.PickleError as exc:
raise ObjectFormatError(f"Could not pickle {self}") from exc
@classmethod
def from_bytes(cls, data: bytes, fs_cache: Optional[dict] = None):
from dvc.fs import get_fs_cls
from dvc.fs.repo import RepoFileSystem
try:
dict_ = pickle.loads(data)
except pickle.PickleError as exc:
raise ObjectFormatError("ReferenceHashFile is corrupted") from exc
try:
path_info = dict_[cls.PARAM_PATH]
hash_info = dict_[cls.PARAM_HASH]
except KeyError as exc:
raise ObjectFormatError("ReferenceHashFile is corrupted") from exc
scheme, config_pairs = dict_.get(cls.PARAM_FS_CONFIG)
fs = fs_cache.get((scheme, config_pairs)) if fs_cache else None
if not fs:
config = dict(config_pairs)
if RepoFileSystem.PARAM_REPO_URL in config:
fs = RepoFileSystem(**config)
path_info = fs.root_dir / path_info
else:
fs_cls = get_fs_cls(config, scheme=path_info.scheme)
fs = fs_cls(**config)
return ReferenceHashFile(
path_info,
fs,
hash_info,
mtime=dict_.get(cls.PARAM_MTIME),
size=dict_.get(cls.PARAM_SIZE),
)
@staticmethod
def config_tuple(fs: "BaseFileSystem"):
return (
fs.scheme,
tuple(
(key, value)
for key, value in sorted(
fs.config.items(), key=lambda item: item[0]
)
),
)
+118 -80
View File
@@ -1,6 +1,10 @@
import hashlib
import logging
import os
import pathlib
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Dict, Tuple
from dvc.exceptions import DvcIgnoreInCollectedDirError from dvc.exceptions import DvcIgnoreInCollectedDirError
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
@@ -8,32 +12,34 @@ from dvc.ignore import DvcIgnore
from dvc.progress import Tqdm from dvc.progress import Tqdm
from dvc.utils import file_md5 from dvc.utils import file_md5
from .db.reference import ReferenceObjectDB
from .file import HashFile from .file import HashFile
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem from dvc.fs.base import BaseFileSystem
from dvc.types import DvcPath from dvc.types import AnyPath, DvcPath
from .db.base import ObjectDB from .db.base import ObjectDB
logger = logging.getLogger(__name__)
_STAGING_DIR = "staging"
_STAGING_MEMFS_PATH = f"dvc-{_STAGING_DIR}"
def _upload_file(path_info, fs, odb): _STAGING_MEMFS_PATH = "dvc-staging"
def _upload_file(path_info, fs, odb, upload_odb):
from dvc.utils import tmp_fname from dvc.utils import tmp_fname
from dvc.utils.stream import HashedStreamReader from dvc.utils.stream import HashedStreamReader
tmp_info = odb.path_info / tmp_fname() tmp_info = upload_odb.path_info / tmp_fname()
with fs.open(path_info, mode="rb", chunk_size=fs.CHUNK_SIZE) as stream: with fs.open(path_info, mode="rb", chunk_size=fs.CHUNK_SIZE) as stream:
stream = HashedStreamReader(stream) stream = HashedStreamReader(stream)
odb.fs.upload_fobj( upload_odb.fs.upload_fobj(
stream, tmp_info, desc=path_info.name, size=fs.getsize(path_info) stream, tmp_info, desc=path_info.name, size=fs.getsize(path_info)
) )
obj = HashFile(tmp_info, odb.fs, stream.hash_info) odb.add(tmp_info, upload_odb.fs, stream.hash_info)
return path_info, obj return path_info, odb.get(stream.hash_info)
def _get_file_hash(path_info, fs, name): def _get_file_hash(path_info, fs, name):
@@ -71,15 +77,20 @@ def get_file_hash(path_info, fs, name, state=None):
return hash_info return hash_info
def _get_file_obj(path_info, fs, name, odb=None, upload=False): def _get_file_obj(
if upload: path_info, fs, name, odb=None, upload_odb=None, dry_run=False
assert odb and name == "md5" ):
return _upload_file(path_info, fs, odb)
state = odb.state if odb else None state = odb.state if odb else None
obj = HashFile( hash_info = get_file_hash(path_info, fs, name, state=state)
path_info, fs, get_file_hash(path_info, fs, name, state=state) if upload_odb and not dry_run:
) assert odb and name == "md5"
return _upload_file(path_info, fs, odb, upload_odb)
if dry_run:
obj = HashFile(path_info, fs, hash_info)
else:
odb.add(path_info, fs, hash_info, move=False)
obj = odb.get(hash_info)
return path_info, obj return path_info, obj
@@ -87,9 +98,9 @@ def _build_objects(
path_info, path_info,
fs, fs,
name, name,
odb=None,
upload=False,
dvcignore=None, dvcignore=None,
jobs=None,
no_progress_bar=False,
**kwargs, **kwargs,
): ):
if dvcignore: if dvcignore:
@@ -99,33 +110,24 @@ def _build_objects(
with Tqdm( with Tqdm(
unit="md5", unit="md5",
desc="Computing file/dir hashes (only done once)", desc="Computing file/dir hashes (only done once)",
disable=kwargs.pop("no_progress_bar", False), disable=no_progress_bar,
) as pbar: ) as pbar:
worker = pbar.wrap_fn( worker = pbar.wrap_fn(
partial( partial(
_get_file_obj, _get_file_obj,
fs=fs, fs=fs,
name=name, name=name,
odb=odb, **kwargs,
upload=upload,
) )
) )
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=kwargs.pop("jobs", fs.hash_jobs) max_workers=jobs if jobs is not None else fs.hash_jobs
) as executor: ) as executor:
yield from executor.map(worker, walk_iterator) yield from executor.map(worker, walk_iterator)
def _iter_objects(path_info, fs, name, upload=False, **kwargs): def _iter_objects(path_info, fs, name, **kwargs):
if not upload and name in fs.DETAIL_FIELDS: yield from _build_objects(path_info, fs, name, **kwargs)
for details in fs.find(path_info, detail=True):
file_info = path_info.replace(path=details["name"])
hash_info = HashInfo(name, details[name], size=details.get("size"))
yield file_info, HashFile(file_info, fs, hash_info)
return None
yield from _build_objects(path_info, fs, name, upload=upload, **kwargs)
def _build_tree(path_info, fs, name, **kwargs): def _build_tree(path_info, fs, name, **kwargs):
@@ -145,14 +147,13 @@ def _build_tree(path_info, fs, name, **kwargs):
# Yes, this is a BUG, as long as we permit "/" in # Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix # filenames on Windows and "\" on Unix
tree.add(file_info.relative_to(path_info).parts, obj) tree.add(file_info.relative_to(path_info).parts, obj)
tree.digest()
return tree return tree
def _get_tree_obj(path_info, fs, name, odb=None, **kwargs): def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
from .tree import Tree from .tree import Tree
value = fs.info(path_info).get(name) value = fs_info.get(name)
if odb and value: if odb and value:
hash_info = HashInfo(name, value) hash_info = HashInfo(name, value)
try: try:
@@ -168,40 +169,59 @@ def _get_tree_obj(path_info, fs, name, odb=None, **kwargs):
pass pass
tree = _build_tree(path_info, fs, name, odb=odb, **kwargs) tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
state = odb.state if odb and odb.state else None
hash_info = None
if state:
hash_info = state.get( # pylint: disable=assignment-from-none
path_info, fs
)
tree.digest(hash_info=hash_info)
odb.add(tree.path_info, tree.fs, tree.hash_info, move=True)
raw = odb.get(tree.hash_info)
# cleanup unneeded memfs tmpfile and return tree based on the
# ODB fs/path
if odb.fs != tree.fs:
tree.fs.remove(tree.path_info)
tree.fs = raw.fs
tree.path_info = raw.path_info
return tree return tree
def get_staging(odb: Optional["ObjectDB"] = None) -> "ObjectDB": _url_cache: Dict[str, str] = {}
def _make_staging_url(path_info: "AnyPath"):
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
url = CloudURLInfo(f"{Schemes.MEMORY}://{_STAGING_MEMFS_PATH}")
if path_info:
if isinstance(path_info, (str, pathlib.PurePath)):
path = os.path.abspath(path_info)
else:
path = str(path_info)
if path not in _url_cache:
_url_cache[path] = hashlib.sha256(path.encode("utf-8")).hexdigest()
url /= _url_cache[path]
return url
def _get_staging(odb: "ObjectDB") -> "ObjectDB":
"""Return an ODB that can be used for staging objects. """Return an ODB that can be used for staging objects.
If odb.fs is local, .dvc/tmp/staging will be returned. Otherwise Staging will be a reference ODB stored in the the global memfs.
the the global (temporary) memfs ODB will be returned.
""" """
from dvc.fs.memory import MemoryFileSystem from dvc.fs.memory import MemoryFileSystem
from dvc.path_info import CloudURLInfo, PathInfo
from dvc.scheme import Schemes
from .db import get_odb fs = MemoryFileSystem()
path_info = _make_staging_url(odb.path_info)
if odb and odb.fs.scheme == Schemes.LOCAL and odb.tmp_dir: state = odb.state
fs = odb.fs return ReferenceObjectDB(fs, path_info, state=state)
path_info: "DvcPath" = PathInfo(odb.tmp_dir) / _STAGING_DIR
config = odb.config
else:
fs = MemoryFileSystem()
path_info = CloudURLInfo(f"{Schemes.MEMORY}://{_STAGING_MEMFS_PATH}")
config = {}
return get_odb(fs, path_info, **config)
def is_memfs_staging(odb: "ObjectDB"):
staging = get_staging()
return odb.fs == staging.fs and odb.path_info == staging.path_info
def _load_from_state(odb, staging, path_info, fs, name): def _load_from_state(odb, staging, path_info, fs, name):
from . import load from . import check, load
from .errors import ObjectFormatError from .errors import ObjectFormatError
from .tree import Tree from .tree import Tree
@@ -212,6 +232,7 @@ def _load_from_state(odb, staging, path_info, fs, name):
if odb_.exists(hash_info): if odb_.exists(hash_info):
try: try:
obj = load(odb_, hash_info) obj = load(odb_, hash_info)
check(odb_, obj, check_hash=False)
if isinstance(obj, Tree): if isinstance(obj, Tree):
obj.hash_info.nfiles = len(obj) obj.hash_info.nfiles = len(obj)
for key, entry in obj: for key, entry in obj:
@@ -222,8 +243,8 @@ def _load_from_state(odb, staging, path_info, fs, name):
obj.path_info = path_info obj.path_info = path_info
assert obj.hash_info.name == name assert obj.hash_info.name == name
obj.hash_info.size = hash_info.size obj.hash_info.size = hash_info.size
return obj return odb_, obj
except ObjectFormatError: except (ObjectFormatError, FileNotFoundError):
pass pass
raise FileNotFoundError raise FileNotFoundError
@@ -248,17 +269,31 @@ def _stage_external_tree_info(odb, tree, name):
def stage( def stage(
odb: Optional["ObjectDB"], odb: "ObjectDB",
path_info: "DvcPath", path_info: "DvcPath",
fs: "BaseFileSystem", fs: "BaseFileSystem",
name: str, name: str,
upload: bool = False, upload: bool = False,
dry_run: bool = False,
**kwargs, **kwargs,
) -> "HashFile": ) -> Tuple["ObjectDB", "HashFile"]:
"""Stage (prepare) objects from the given path for addition to an ODB.
Returns at tuple of (staging_odb, object) where addition to the ODB can
be completed by transferring the object from staging to the dest ODB.
If dry_run is True, object hashes will be computed and returned, but file
objects themselves will not be added to the staging ODB (i.e. the resulting
file objects cannot transferred from staging to another ODB).
If upload is True, files will be uploaded to a temporary path on the dest
ODB filesystem, and staged objects will reference the uploaded path rather
than the original source path.
"""
assert path_info and path_info.scheme == fs.scheme assert path_info and path_info.scheme == fs.scheme
details = fs.info(path_info) details = fs.info(path_info)
staging = get_staging(odb) staging = _get_staging(odb)
if odb: if odb:
try: try:
return _load_from_state(odb, staging, path_info, fs, name) return _load_from_state(odb, staging, path_info, fs, name)
@@ -267,26 +302,29 @@ def stage(
if details["type"] == "directory": if details["type"] == "directory":
obj = _get_tree_obj( obj = _get_tree_obj(
path_info, fs, name, odb=odb, upload=upload, **kwargs path_info,
fs,
details,
name,
odb=staging,
upload_odb=odb if upload else None,
dry_run=dry_run,
**kwargs,
) )
if name == "md5": logger.debug("staged tree '%s'", obj)
if odb and odb.exists(obj.hash_info): if name != "md5":
raw = odb.get(obj.hash_info)
else:
staging.add(obj.path_info, obj.fs, obj.hash_info)
raw = staging.get(obj.hash_info)
# cleanup unneeded memfs tmpfile and return obj based on staging
# ODB fs/path
if obj.fs != raw.fs:
obj.fs.remove(obj.path_info)
obj.fs = raw.fs
obj.path_info = raw.path_info
else:
obj = _stage_external_tree_info(odb, obj, name) obj = _stage_external_tree_info(odb, obj, name)
else: else:
_, obj = _get_file_obj(path_info, fs, name, odb=odb, upload=upload) _, obj = _get_file_obj(
path_info,
fs,
name,
odb=staging,
upload_odb=odb if upload else None,
dry_run=dry_run,
)
if odb and odb.state and obj.hash_info: if odb and odb.state and obj.hash_info:
odb.state.save(path_info, fs, obj.hash_info) odb.state.save(path_info, fs, obj.hash_info)
return obj return staging, obj
+55 -53
View File
@@ -2,6 +2,7 @@ import logging
from typing import TYPE_CHECKING, Dict, Iterable, NamedTuple, Optional, Set from typing import TYPE_CHECKING, Dict, Iterable, NamedTuple, Optional, Set
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.scheme import Schemes
from .tree import Tree from .tree import Tree
@@ -14,23 +15,21 @@ logger = logging.getLogger(__name__)
class StatusResult(NamedTuple): class StatusResult(NamedTuple):
exists: Set["HashFile"] exists: Set["HashInfo"]
missing: Set["HashFile"] missing: Set["HashInfo"]
class CompareStatusResult(NamedTuple): class CompareStatusResult(NamedTuple):
ok: Set["HashFile"] ok: Set["HashInfo"]
missing: Set["HashFile"] missing: Set["HashInfo"]
new: Set["HashFile"] new: Set["HashInfo"]
deleted: Set["HashFile"] deleted: Set["HashInfo"]
def _indexed_dir_hashes(odb, index, hash_infos, name, cache_odb): def _indexed_dir_hashes(odb, index, dir_objs, name, cache_odb):
from . import load
# Validate our index by verifying all indexed .dir hashes # Validate our index by verifying all indexed .dir hashes
# still exist on the remote # still exist on the remote
dir_hashes = {hash_info.value for hash_info in hash_infos} dir_hashes = set(dir_objs.keys())
indexed_dirs = set(index.dir_hashes()) indexed_dirs = set(index.dir_hashes())
indexed_dir_exists = set() indexed_dir_exists = set()
if indexed_dirs: if indexed_dirs:
@@ -51,10 +50,12 @@ def _indexed_dir_hashes(odb, index, hash_infos, name, cache_odb):
# If .dir hash exists in the ODB, assume directory contents # If .dir hash exists in the ODB, assume directory contents
# also exists # also exists
for dir_hash in dir_exists: for dir_hash in dir_exists:
try: tree = dir_objs.get(dir_hash)
tree = load(cache_odb, HashInfo(name, dir_hash)) if not tree:
except FileNotFoundError: try:
continue tree = Tree.load(cache_odb, HashInfo(name, dir_hash))
except FileNotFoundError:
continue
file_hashes = [entry.hash_info.value for _, entry in tree] file_hashes = [entry.hash_info.value for _, entry in tree]
if dir_hash not in index: if dir_hash not in index:
logger.debug( logger.debug(
@@ -67,21 +68,13 @@ def _indexed_dir_hashes(odb, index, hash_infos, name, cache_odb):
yield tree.hash_info.value yield tree.hash_info.value
def _status_staging(objs: Iterable["HashFile"]) -> "StatusResult":
exists: Set["HashFile"] = set()
for obj in objs:
if isinstance(obj, Tree):
exists.update(entry for _, entry in obj)
exists.add(obj)
return StatusResult(exists, set())
def status( def status(
odb: "ObjectDB", odb: "ObjectDB",
objs: Iterable["HashFile"], obj_ids: Iterable["HashInfo"],
name: Optional[str] = None, name: Optional[str] = None,
index: Optional["ObjectDBIndexBase"] = None, index: Optional["ObjectDBIndexBase"] = None,
cache_odb: Optional["ObjectDB"] = None, cache_odb: Optional["ObjectDB"] = None,
shallow: bool = True,
**kwargs, **kwargs,
) -> "StatusResult": ) -> "StatusResult":
"""Return status of whether or not the specified objects exist odb. """Return status of whether or not the specified objects exist odb.
@@ -93,39 +86,41 @@ def status(
exists: objs that exist in odb exists: objs that exist in odb
missing: objs that do not exist in ODB missing: objs that do not exist in ODB
""" """
from .stage import is_memfs_staging
logger.debug("Preparing to collect status from '%s'", odb.path_info) logger.debug("Preparing to collect status from '%s'", odb.path_info)
if not name: if not name:
name = odb.fs.PARAM_CHECKSUM name = odb.fs.PARAM_CHECKSUM
if is_memfs_staging(odb): if cache_odb is None:
# assume memfs staged objects already exist cache_odb = odb
return _status_staging(objs)
hash_objs: Dict[str, "HashFile"] = {} hash_infos: Dict[str, "HashInfo"] = {}
hashes: Set[str] = set() dir_objs: Dict[str, Optional["HashFile"]] = {}
dir_infos: Set["HashInfo"] = set() for hash_info in obj_ids:
exists: Set[str] = set() assert hash_info.value
for obj in objs: if hash_info.isdir:
assert obj.hash_info and obj.hash_info.value if shallow:
if isinstance(obj, Tree): tree = None
for _, entry in obj: else:
assert entry.hash_info and entry.hash_info.value tree = Tree.load(cache_odb, hash_info)
hash_objs[entry.hash_info.value] = entry for _, entry in tree:
hashes.add(entry.hash_info.value) assert entry.hash_info and entry.hash_info.value
hash_infos[entry.hash_info.value] = entry.hash_info
if index: if index:
dir_infos.add(obj.hash_info) dir_objs[hash_info.value] = tree
hashes.add(obj.hash_info.value) hash_infos[hash_info.value] = hash_info
hash_objs[obj.hash_info.value] = obj
if odb.fs.scheme == Schemes.MEMORY:
# assume memfs staged objects already exist
return StatusResult(set(hash_infos.values()), set())
hashes: Set[str] = set(hash_infos.keys())
exists: Set[str] = set()
logger.debug("Collecting status from '%s'", odb.path_info) logger.debug("Collecting status from '%s'", odb.path_info)
if index and hashes: if index and hashes:
if dir_infos: if dir_objs:
if cache_odb is None:
cache_odb = odb
exists = hashes.intersection( exists = hashes.intersection(
_indexed_dir_hashes(odb, index, dir_infos, name, cache_odb) _indexed_dir_hashes(odb, index, dir_objs, name, cache_odb)
) )
hashes.difference_update(exists) hashes.difference_update(exists)
if hashes: if hashes:
@@ -137,15 +132,15 @@ def status(
odb.hashes_exist(hashes, name=str(odb.path_info), **kwargs) odb.hashes_exist(hashes, name=str(odb.path_info), **kwargs)
) )
return StatusResult( return StatusResult(
{hash_objs[hash_] for hash_ in exists}, {hash_infos[hash_] for hash_ in exists},
{hash_objs[hash_] for hash_ in (hashes - exists)}, {hash_infos[hash_] for hash_ in (hashes - exists)},
) )
def compare_status( def compare_status(
src: "ObjectDB", src: "ObjectDB",
dest: "ObjectDB", dest: "ObjectDB",
objs: Iterable["HashFile"], obj_ids: Iterable["HashInfo"],
log_missing: bool = True, log_missing: bool = True,
check_deleted: bool = True, check_deleted: bool = True,
src_index: Optional["ObjectDBIndexBase"] = None, src_index: Optional["ObjectDBIndexBase"] = None,
@@ -160,11 +155,17 @@ def compare_status(
new: hashes that only exist in src new: hashes that only exist in src
deleted: hashes that only exist in dest deleted: hashes that only exist in dest
""" """
dest_exists, dest_missing = status(dest, objs, index=dest_index, **kwargs) if "cache_odb" not in kwargs:
kwargs["cache_odb"] = src
dest_exists, dest_missing = status(
dest, obj_ids, index=dest_index, **kwargs
)
# for transfer operations we can skip src status check when all objects # for transfer operations we can skip src status check when all objects
# already exist in dest # already exist in dest
if dest_missing or check_deleted: if dest_missing or check_deleted:
src_exists, src_missing = status(src, objs, index=src_index, **kwargs) src_exists, src_missing = status(
src, obj_ids, index=src_index, **kwargs
)
else: else:
src_exists = dest_exists src_exists = dest_exists
src_missing = set() src_missing = set()
@@ -176,7 +177,8 @@ def compare_status(
) )
if log_missing and result.missing: if log_missing and result.missing:
missing_desc = "\n".join( missing_desc = "\n".join(
f"name: {obj.name}, {obj.hash_info}" for obj in result.missing f"name: {hash_info.obj_name}, {hash_info}"
for hash_info in result.missing
) )
logger.warning( logger.warning(
"Some of the cache files do not exist neither locally " "Some of the cache files do not exist neither locally "
+82 -70
View File
@@ -4,14 +4,15 @@ import logging
from concurrent import futures from concurrent import futures
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial, wraps from functools import partial, wraps
from typing import TYPE_CHECKING, Iterable, Optional, Set from typing import TYPE_CHECKING, Callable, Iterable, Optional
from funcy import split
from dvc.progress import Tqdm from dvc.progress import Tqdm
from .file import HashFile
from .tree import Tree
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from .db.base import ObjectDB from .db.base import ObjectDB
from .db.index import ObjectDBIndexBase from .db.index import ObjectDBIndexBase
@@ -20,9 +21,9 @@ logger = logging.getLogger(__name__)
def _log_exceptions(func): def _log_exceptions(func):
@wraps(func) @wraps(func)
def wrapper(odb, obj, *args, **kwargs): def wrapper(path_info, *args, **kwargs):
try: try:
func(odb, obj, *args, **kwargs) func(path_info, *args, **kwargs)
return 0 return 0
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
# NOTE: this means we ran out of file descriptors and there is no # NOTE: this means we ran out of file descriptors and there is no
@@ -31,23 +32,17 @@ def _log_exceptions(func):
if isinstance(exc, OSError) and exc.errno == errno.EMFILE: if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
raise raise
logger.exception( logger.exception("failed to transfer '%s'", path_info)
"failed to transfer '%s' to '%s'",
obj.path_info,
odb.get(obj.hash_info).path_info,
)
return 1 return 1
return wrapper return wrapper
def _transfer(src, dest, dir_objs, file_objs, missing, jobs, verify, **kwargs): def _transfer(
from . import save src, dest, dir_ids, file_ids, missing_ids, jobs, verify, move, **kwargs
from .stage import is_memfs_staging ):
func = _log_exceptions(dest.add)
is_staged = is_memfs_staging(src) total = len(dir_ids) + len(file_ids)
func = _log_exceptions(save)
total = len(dir_objs) + len(file_objs)
if total == 0: if total == 0:
return 0 return 0
with Tqdm(total=total, unit="file", desc="Transferring") as pbar: with Tqdm(total=total, unit="file", desc="Transferring") as pbar:
@@ -59,37 +54,43 @@ def _transfer(src, dest, dir_objs, file_objs, missing, jobs, verify, **kwargs):
jobs, jobs,
func, func,
src, src,
dest,
is_staged,
verify, verify,
move,
) )
processor.save_func = func processor.add_func = func
_do_transfer( _do_transfer(
src, src,
dest, dest,
dir_objs, dir_ids,
file_objs, file_ids,
{obj.hash_info for obj in missing}, missing_ids,
processor, processor,
verify=verify,
move=move,
**kwargs, **kwargs,
) )
return total return total
def _create_tasks(executor, jobs, func, src, dest, is_staged, verify, objs): def _create_tasks(executor, jobs, func, src, verify, move, obj_ids):
fails = 0 fails = 0
obj_iter = iter(objs) hash_iter = iter(obj_ids)
def submit(hash_info):
obj = src.get(hash_info)
return executor.submit(
func,
obj.path_info,
obj.fs,
obj.hash_info,
verify=verify,
move=move,
)
def create_taskset(amount): def create_taskset(amount):
return { return {
executor.submit( submit(hash_info)
func, for hash_info in itertools.islice(hash_iter, amount)
dest,
_raw_obj(src, obj, is_staged),
move=False,
verify=verify,
)
for obj in itertools.islice(obj_iter, amount)
} }
tasks = create_taskset(jobs * 5) tasks = create_taskset(jobs * 5)
@@ -100,39 +101,46 @@ def _create_tasks(executor, jobs, func, src, dest, is_staged, verify, objs):
return fails return fails
def _raw_obj(odb, obj, is_staged=False):
if is_staged:
return HashFile(obj.path_info, obj.fs, obj.hash_info)
return odb.get(obj.hash_info)
def _do_transfer( def _do_transfer(
src, src: "ObjectDB",
dest, dest: "ObjectDB",
dir_objs, dir_ids: Iterable["HashInfo"],
file_objs, file_ids: Iterable["HashInfo"],
missing_hashes, missing_ids: Iterable["HashInfo"],
processor, processor: Callable,
src_index: Optional["ObjectDBIndexBase"] = None, src_index: Optional["ObjectDBIndexBase"] = None,
dest_index: Optional["ObjectDBIndexBase"] = None, dest_index: Optional["ObjectDBIndexBase"] = None,
cache_odb: Optional["ObjectDB"] = None,
**kwargs, **kwargs,
): ):
from dvc.exceptions import FileTransferError from dvc.exceptions import FileTransferError
from dvc.objects.errors import ObjectFormatError
total_fails = 0 total_fails = 0
succeeded_dir_objs = [] succeeded_dir_objs = []
all_file_objs = set(file_objs) all_file_ids = set(file_ids)
for dir_obj in dir_objs: for dir_hash in dir_ids:
bound_file_objs = set() from .tree import Tree
directory_hashes = {entry.hash_info for _, entry in dir_obj}
for file_obj in all_file_objs.copy(): bound_file_ids = set()
if file_obj.hash_info in directory_hashes: dir_obj: Optional["Tree"] = None
bound_file_objs.add(file_obj) for odb in (cache_odb, src):
all_file_objs.remove(file_obj) if odb is not None:
try:
dir_obj = Tree.load(odb, dir_hash)
break
except (FileNotFoundError, ObjectFormatError):
pass
assert dir_obj
entry_ids = {entry.hash_info for _, entry in dir_obj}
dir_fails = processor(bound_file_objs) for file_hash in all_file_ids.copy():
if file_hash in entry_ids:
bound_file_ids.add(file_hash)
all_file_ids.remove(file_hash)
dir_fails = processor(bound_file_ids)
if dir_fails: if dir_fails:
logger.debug( logger.debug(
"failed to upload full contents of '%s', " "failed to upload full contents of '%s', "
@@ -145,7 +153,7 @@ def _do_transfer(
dest.get(dir_obj.hash_info).path_info, dest.get(dir_obj.hash_info).path_info,
) )
total_fails += dir_fails + 1 total_fails += dir_fails + 1
elif directory_hashes.intersection(missing_hashes): elif entry_ids.intersection(missing_ids):
# if for some reason a file contained in this dir is # if for some reason a file contained in this dir is
# missing both locally and in the remote, we want to # missing both locally and in the remote, we want to
# push whatever file content we have, but should not # push whatever file content we have, but should not
@@ -156,15 +164,19 @@ def _do_transfer(
dir_obj.name, dir_obj.name,
) )
else: else:
is_dir_failed = processor.save_func( raw_obj = src.get(dir_obj.hash_info)
dest, src.get(dir_obj.hash_info), move=False is_dir_failed = processor.add_func( # type: ignore[attr-defined]
raw_obj.path_info,
raw_obj.fs,
raw_obj.hash_info,
**kwargs,
) )
total_fails += is_dir_failed total_fails += is_dir_failed
if not is_dir_failed: if not is_dir_failed:
succeeded_dir_objs.append(dir_obj) succeeded_dir_objs.append(dir_obj)
# insert the rest # insert the rest
total_fails += processor(all_file_objs) total_fails += processor(all_file_ids)
if total_fails: if total_fails:
if src_index: if src_index:
src_index.clear() src_index.clear()
@@ -179,15 +191,17 @@ def _do_transfer(
dir_obj.hash_info, dir_obj.hash_info,
len(file_hashes), len(file_hashes),
) )
assert dir_obj.hash_info and dir_obj.hash_info.value
dest_index.update([dir_obj.hash_info.value], file_hashes) dest_index.update([dir_obj.hash_info.value], file_hashes)
def transfer( def transfer(
src: "ObjectDB", src: "ObjectDB",
dest: "ObjectDB", dest: "ObjectDB",
objs: Iterable["HashFile"], obj_ids: Iterable["HashInfo"],
jobs: Optional[int] = None, jobs: Optional[int] = None,
verify: bool = False, verify: bool = False,
move: bool = False,
**kwargs, **kwargs,
) -> int: ) -> int:
"""Transfer (copy) the specified objects from one ODB to another. """Transfer (copy) the specified objects from one ODB to another.
@@ -201,27 +215,25 @@ def transfer(
src.path_info, src.path_info,
dest.path_info, dest.path_info,
) )
status = compare_status(src, dest, objs, check_deleted=False, **kwargs) if src == dest:
return 0
status = compare_status(src, dest, obj_ids, check_deleted=False, **kwargs)
if not status.new: if not status.new:
return 0 return 0
files: Set["HashFile"] = set() dir_ids, file_ids = split(lambda hash_info: hash_info.isdir, status.new)
dirs: Set["HashFile"] = set()
for obj in status.new:
if isinstance(obj, Tree):
dirs.add(obj)
else:
files.add(obj)
if jobs is None: if jobs is None:
jobs = dest.fs.jobs jobs = dest.fs.jobs
return _transfer( return _transfer(
src, src,
dest, dest,
dirs, set(dir_ids),
files, set(file_ids),
status.missing, status.missing,
jobs, jobs,
verify, verify,
move,
**kwargs, **kwargs,
) )
+11 -4
View File
@@ -1,7 +1,7 @@
import json import json
import logging import logging
import posixpath import posixpath
from typing import Dict, Optional, Tuple from typing import TYPE_CHECKING, Dict, Optional, Tuple
from funcy import cached_property from funcy import cached_property
@@ -9,6 +9,9 @@ from .errors import ObjectFormatError
from .file import HashFile from .file import HashFile
from .stage import get_file_hash from .stage import get_file_hash
if TYPE_CHECKING:
from dvc.hash_info import HashInfo
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,7 +32,7 @@ class Tree(HashFile):
self.__dict__.pop("trie", None) self.__dict__.pop("trie", None)
self._dict[key] = obj self._dict[key] = obj
def digest(self): def digest(self, hash_info: Optional["HashInfo"] = None):
from dvc.fs.memory import MemoryFileSystem from dvc.fs.memory import MemoryFileSystem
from dvc.path_info import CloudURLInfo from dvc.path_info import CloudURLInfo
from dvc.utils import tmp_fname from dvc.utils import tmp_fname
@@ -40,8 +43,12 @@ class Tree(HashFile):
fobj.write(self.as_bytes()) fobj.write(self.as_bytes())
self.fs = memfs self.fs = memfs
self.path_info = path_info self.path_info = path_info
self.hash_info = get_file_hash(path_info, memfs, "md5") if hash_info:
self.hash_info.value += ".dir" self.hash_info = hash_info
else:
self.hash_info = get_file_hash(path_info, memfs, "md5")
assert self.hash_info.value
self.hash_info.value += ".dir"
try: try:
self.hash_info.size = sum(obj.size for _, obj in self) self.hash_info.size = sum(obj.size for _, obj in self)
except TypeError: except TypeError:
+53 -34
View File
@@ -25,16 +25,15 @@ from .fs.s3 import S3FileSystem
from .hash_info import HashInfo from .hash_info import HashInfo
from .istextfile import istextfile from .istextfile import istextfile
from .objects import Tree from .objects import Tree
from .objects import save as osave
from .objects.errors import ObjectFormatError from .objects.errors import ObjectFormatError
from .objects.stage import stage as ostage from .objects.stage import stage as ostage
from .objects.transfer import transfer as otransfer
from .scheme import Schemes from .scheme import Schemes
from .utils import relpath from .utils import relpath
from .utils.fs import path_isin from .utils.fs import path_isin
if TYPE_CHECKING: if TYPE_CHECKING:
from .objects.db.base import ObjectDB from .objects.db.base import ObjectDB
from .objects.file import HashFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -404,21 +403,21 @@ class Output:
return self.odb.hash_to_path_info(self.hash_info.value).url return self.odb.hash_to_path_info(self.hash_info.value).url
def get_hash(self): def get_hash(self):
if not self.use_cache: if self.use_cache:
return ostage( odb = self.odb
self.repo.odb.local, name = self.odb.fs.PARAM_CHECKSUM
self.path_info, else:
self.fs, odb = self.repo.odb.local
self.fs.PARAM_CHECKSUM, name = self.fs.PARAM_CHECKSUM
dvcignore=self.dvcignore, _, obj = ostage(
).hash_info odb,
return ostage(
self.odb,
self.path_info, self.path_info,
self.fs, self.fs,
self.odb.fs.PARAM_CHECKSUM, name,
dvcignore=self.dvcignore, dvcignore=self.dvcignore,
).hash_info dry_run=not self.use_cache,
)
return obj.hash_info
@property @property
def is_dir_checksum(self): def is_dir_checksum(self):
@@ -545,7 +544,7 @@ class Output:
logger.debug("Output '%s' didn't change. Skipping saving.", self) logger.debug("Output '%s' didn't change. Skipping saving.", self)
return return
self.obj = ostage( _, self.obj = ostage(
self.odb, self.odb,
self.path_info, self.path_info,
self.fs, self.fs,
@@ -574,14 +573,20 @@ class Output:
if granular: if granular:
obj = self._commit_granular_dir(filter_info) obj = self._commit_granular_dir(filter_info)
else: else:
obj = ostage( staging, obj = ostage(
self.odb, self.odb,
filter_info or self.path_info, filter_info or self.path_info,
self.fs, self.fs,
self.odb.fs.PARAM_CHECKSUM, self.odb.fs.PARAM_CHECKSUM,
dvcignore=self.dvcignore, dvcignore=self.dvcignore,
) )
osave(self.odb, obj) otransfer(
staging,
self.odb,
{obj.hash_info},
shallow=False,
move=True,
)
checkout( checkout(
filter_info or self.path_info, filter_info or self.path_info,
self.fs, self.fs,
@@ -595,7 +600,7 @@ class Output:
def _commit_granular_dir(self, filter_info): def _commit_granular_dir(self, filter_info):
prefix = filter_info.relative_to(self.path_info).parts prefix = filter_info.relative_to(self.path_info).parts
save_obj = ostage( staging, save_obj = ostage(
self.odb, self.odb,
self.path_info, self.path_info,
self.fs, self.fs,
@@ -604,7 +609,13 @@ class Output:
) )
save_obj = save_obj.filter(prefix) save_obj = save_obj.filter(prefix)
checkout_obj = save_obj.get(prefix) checkout_obj = save_obj.get(prefix)
osave(self.odb, save_obj) otransfer(
staging,
self.odb,
{save_obj.hash_info} | {entry.hash_info for _, entry in save_obj},
shallow=True,
move=True,
)
return checkout_obj return checkout_obj
def dumpd(self): def dumpd(self):
@@ -786,7 +797,7 @@ class Output:
upload = not (update and from_fs.isdir(from_info)) upload = not (update and from_fs.isdir(from_info))
jobs = jobs or min((from_fs.jobs, odb.fs.jobs)) jobs = jobs or min((from_fs.jobs, odb.fs.jobs))
obj = ostage( staging, obj = ostage(
odb, odb,
from_info, from_info,
from_fs, from_fs,
@@ -795,7 +806,14 @@ class Output:
jobs=jobs, jobs=jobs,
no_progress_bar=no_progress_bar, no_progress_bar=no_progress_bar,
) )
osave(odb, obj, jobs=jobs, move=upload) otransfer(
staging,
odb,
{obj.hash_info},
jobs=jobs,
move=upload,
shallow=False,
)
self.hash_info = obj.hash_info self.hash_info = obj.hash_info
return obj return obj
@@ -825,7 +843,7 @@ class Output:
try: try:
objects.check(self.odb, obj) objects.check(self.odb, obj)
except FileNotFoundError: except FileNotFoundError:
self.repo.cloud.pull([obj], **kwargs) self.repo.cloud.pull([obj.hash_info], **kwargs)
if self.obj: if self.obj:
return self.obj return self.obj
@@ -839,8 +857,8 @@ class Output:
def collect_used_dir_cache( def collect_used_dir_cache(
self, remote=None, force=False, jobs=None, filter_info=None self, remote=None, force=False, jobs=None, filter_info=None
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]: ) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Fetch dir cache and return used objects for this out.""" """Fetch dir cache and return used object IDs for this out."""
try: try:
self.get_dir_cache(jobs=jobs, remote=remote) self.get_dir_cache(jobs=jobs, remote=remote)
@@ -866,13 +884,12 @@ class Output:
if filter_info and filter_info != self.path_info: if filter_info and filter_info != self.path_info:
prefix = filter_info.relative_to(self.path_info).parts prefix = filter_info.relative_to(self.path_info).parts
obj = obj.filter(prefix) obj = obj.filter(prefix)
self._set_obj_names(obj) return {None: set(self._named_obj_ids(obj))}
return {None: {obj}}
def get_used_objs( def get_used_objs(
self, **kwargs self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]: ) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return filtered set of used objects for this out.""" """Return filtered set of used object IDs for this out."""
if not self.use_cache: if not self.use_cache:
return {} return {}
@@ -905,19 +922,21 @@ class Output:
obj = self.get_obj(filter_info=kwargs.get("filter_info")) obj = self.get_obj(filter_info=kwargs.get("filter_info"))
if not obj: if not obj:
obj = self.odb.get(self.hash_info) obj = self.odb.get(self.hash_info)
self._set_obj_names(obj)
return {None: {obj}} return {None: set(self._named_obj_ids(obj))}
def _set_obj_names(self, obj): def _named_obj_ids(self, obj):
obj.name = str(self) name = str(self)
obj.hash_info.obj_name = name
yield obj.hash_info
if isinstance(obj, Tree): if isinstance(obj, Tree):
for key, entry_obj in obj: for key, entry_obj in obj:
entry_obj.name = self.fs.sep.join([obj.name, *key]) entry_obj.hash_info.obj_name = self.fs.sep.join([name, *key])
yield entry_obj.hash_info
def get_used_external( def get_used_external(
self, **kwargs self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]: ) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
if not self.use_cache or not self.stage.is_repo_import: if not self.use_cache or not self.stage.is_repo_import:
return {} return {}
+2
View File
@@ -64,6 +64,8 @@ class PathInfo(pathlib.PurePath, _BasePath):
url = fspath url = fspath
path = fspath
def relpath(self, other): def relpath(self, other):
return self.__class__(relpath(self, other)) return self.__class__(relpath(self, other))
+6 -2
View File
@@ -134,8 +134,12 @@ def _output_paths(repo, repo_fs, targets):
def _to_checksum(output): def _to_checksum(output):
if on_working_fs: if on_working_fs:
return ostage( return ostage(
repo.odb.local, output.path_info, repo.odb.local.fs, "md5" repo.odb.local,
).hash_info.value output.path_info,
repo.odb.local.fs,
"md5",
dry_run=True,
)[1].hash_info.value
return output.hash_info.value return output.hash_info.value
for stage in repo.stages: for stage in repo.stages:
+8 -9
View File
@@ -1,9 +1,8 @@
import logging import logging
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError, FileTransferError from dvc.exceptions import DownloadError, FileTransferError
from dvc.scheme import Schemes
from ..scheme import Schemes
from . import locked from . import locked
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -60,10 +59,13 @@ def fetch(
except DownloadError as exc: except DownloadError as exc:
failed += exc.amount failed += exc.amount
for odb, objs in used.items(): for odb, obj_ids in sorted(
used.items(),
key=lambda item: item[0] and item[0].fs.scheme == Schemes.MEMORY,
):
d, f = _fetch( d, f = _fetch(
self, self,
objs, obj_ids,
jobs=jobs, jobs=jobs,
remote=remote, remote=remote,
odb=odb, odb=odb,
@@ -77,14 +79,11 @@ def fetch(
return downloaded return downloaded
def _fetch(repo, objs, **kwargs): def _fetch(repo, obj_ids, **kwargs):
downloaded = 0 downloaded = 0
failed = 0 failed = 0
try: try:
downloaded += repo.cloud.pull(objs, **kwargs) downloaded += repo.cloud.pull(obj_ids, **kwargs)
except NoRemoteError:
if any(obj.fs.scheme == Schemes.LOCAL for obj in objs):
raise
except FileTransferError as exc: except FileTransferError as exc:
failed += exc.amount failed += exc.amount
return downloaded, failed return downloaded, failed
+5 -18
View File
@@ -1,14 +1,9 @@
import logging import logging
from typing import TYPE_CHECKING, Set
from dvc.exceptions import InvalidArgumentError from dvc.exceptions import InvalidArgumentError
from ..scheme import Schemes
from . import locked from . import locked
if TYPE_CHECKING:
from dvc.objects.file import HashFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,20 +45,19 @@ def gc(
from contextlib import ExitStack from contextlib import ExitStack
from dvc.objects.db import get_index from dvc.objects.db import get_index
from dvc.objects.stage import get_staging
from dvc.repo import Repo from dvc.repo import Repo
if not repos: if not repos:
repos = [] repos = []
all_repos = [Repo(path) for path in repos] all_repos = [Repo(path) for path in repos]
used_objs: Set["HashFile"] = set() used_obj_ids = set()
with ExitStack() as stack: with ExitStack() as stack:
for repo in all_repos: for repo in all_repos:
stack.enter_context(repo.lock) stack.enter_context(repo.lock)
for repo in all_repos + [self]: for repo in all_repos + [self]:
for objs in repo.used_objs( for obj_ids in repo.used_objs(
all_branches=all_branches, all_branches=all_branches,
with_deps=with_deps, with_deps=with_deps,
all_tags=all_tags, all_tags=all_tags,
@@ -73,28 +67,21 @@ def gc(
force=force, force=force,
jobs=jobs, jobs=jobs,
).values(): ).values():
used_objs.update(objs) used_obj_ids.update(obj_ids)
for scheme, odb in self.odb.by_scheme(): for scheme, odb in self.odb.by_scheme():
if not odb: if not odb:
continue continue
removed = odb.gc( removed = odb.gc(used_obj_ids, jobs=jobs)
{obj for obj in used_objs if obj.fs.scheme == scheme},
jobs=jobs,
)
if not removed: if not removed:
logger.info(f"No unused '{scheme}' cache to remove.") logger.info(f"No unused '{scheme}' cache to remove.")
get_staging(self.odb.local).gc(set(), jobs=jobs)
if not cloud: if not cloud:
return return
odb = self.cloud.get_remote_odb(remote, "gc -c") odb = self.cloud.get_remote_odb(remote, "gc -c")
removed = odb.gc( removed = odb.gc(used_obj_ids)
{obj for obj in used_objs if obj.fs.scheme == Schemes.LOCAL},
jobs=jobs,
)
if removed: if removed:
get_index(odb).clear() get_index(odb).clear()
else: else:
+2 -2
View File
@@ -41,11 +41,11 @@ def push(
) )
pushed = len(used_run_cache) pushed = len(used_run_cache)
for odb, objs in used.items(): for odb, obj_ids in used.items():
if odb and odb.read_only: if odb and odb.read_only:
continue continue
try: try:
pushed += self.cloud.push(objs, jobs, remote=remote, odb=odb) pushed += self.cloud.push(obj_ids, jobs, remote=remote, odb=odb)
except FileTransferError as exc: except FileTransferError as exc:
raise UploadError(exc.amount) raise UploadError(exc.amount)
return pushed return pushed
+4 -4
View File
@@ -89,16 +89,16 @@ def _cloud_status(
) )
ret = {} ret = {}
for odb, objs in used.items(): for odb, obj_ids in used.items():
if odb is not None: if odb is not None:
# ignore imported objects # ignore imported objects
continue continue
status_info = self.cloud.status( status_info = self.cloud.status(
objs, jobs, remote=remote, log_missing=False obj_ids, jobs, remote=remote, log_missing=False
) )
for status_ in ("deleted", "new", "missing"): for status_ in ("deleted", "new", "missing"):
for obj in getattr(status_info, status_, []): for hash_info in getattr(status_info, status_, []):
ret[obj.name] = status_ ret[hash_info.obj_name] = status_
return ret return ret
+3 -3
View File
@@ -36,8 +36,8 @@ from .utils import (
if TYPE_CHECKING: if TYPE_CHECKING:
from dvc.dvcfile import DVCFile from dvc.dvcfile import DVCFile
from dvc.hash_info import HashInfo
from dvc.objects.db.base import ObjectDB from dvc.objects.db.base import ObjectDB
from dvc.objects.file import HashFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Disallow all punctuation characters except hyphen and underscore # Disallow all punctuation characters except hyphen and underscore
@@ -639,8 +639,8 @@ class Stage(params.StageParams):
def get_used_objs( def get_used_objs(
self, *args, **kwargs self, *args, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]: ) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
"""Return set of objects used by this stage.""" """Return set of object IDs used by this stage."""
used_objs = defaultdict(set) used_objs = defaultdict(set)
for out in self.filter_outs(kwargs.get("filter_info")): for out in self.filter_outs(kwargs.get("filter_info")):
for odb, objs in out.get_used_objs(*args, **kwargs).items(): for odb, objs in out.get_used_objs(*args, **kwargs).items():
+8 -4
View File
@@ -89,10 +89,14 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
inode = get_inode(path_info) inode = get_inode(path_info)
logger.debug( logger.debug(
"state save (%s, %s, %s) %s", inode, mtime, size, hash_info.value "state save (%s, %s, %s) %s",
inode,
mtime,
str(size),
hash_info.value,
) )
self.md5s[inode] = (mtime, size, hash_info.value) self.md5s[inode] = (mtime, str(size), hash_info.value)
def get(self, path_info, fs): def get(self, path_info, fs):
"""Gets the hash for the specified path info. Hash will be """Gets the hash for the specified path info. Hash will be
@@ -122,10 +126,10 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
value = self.md5s.get(inode) value = self.md5s.get(inode)
if not value or value[0] != mtime or value[1] != size: if not value or value[0] != mtime or value[1] != str(size):
return None return None
return HashInfo("md5", value[2], size=int(size)) return HashInfo("md5", value[2], size=size)
def save_link(self, path_info, fs): def save_link(self, path_info, fs):
"""Adds the specified path to the list of links created by dvc. This """Adds the specified path to the list of links created by dvc. This
+1 -3
View File
@@ -65,9 +65,7 @@ def get_mtime_and_size(path, fs, dvcignore=None):
mtime = base_stat.st_mtime mtime = base_stat.st_mtime
mtime = int(nanotime.timestamp(mtime)) mtime = int(nanotime.timestamp(mtime))
# State of files handled by dvc is stored in db as TEXT. return str(mtime), size
# We cast results to string for later comparisons with stored values.
return str(mtime), str(size)
class BasePathNotInCheckedPathException(DvcException): class BasePathNotInCheckedPathException(DvcException):
+3
View File
@@ -4,6 +4,7 @@ from dvc.exceptions import DownloadError, UploadError
from dvc.fs.local import LocalFileSystem from dvc.fs.local import LocalFileSystem
from dvc.objects.db import get_index from dvc.objects.db import get_index
from dvc.utils.fs import remove from dvc.utils.fs import remove
from tests.utils import clean_staging
@pytest.fixture @pytest.fixture
@@ -16,6 +17,7 @@ def test_indexed_on_status(tmp_dir, dvc, index):
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0] foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0] bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
baz_hash = bar.obj.trie.get(("baz",)).hash_info baz_hash = bar.obj.trie.get(("baz",)).hash_info
clean_staging()
dvc.push() dvc.push()
index.clear() index.clear()
@@ -29,6 +31,7 @@ def test_indexed_on_push(tmp_dir, dvc, index):
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0] foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0] bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
baz_hash = bar.obj.trie.get(("baz",)).hash_info baz_hash = bar.obj.trie.get(("baz",)).hash_info
clean_staging()
dvc.push() dvc.push()
assert {bar.hash_info.value, baz_hash.value} == set(index.hashes()) assert {bar.hash_info.value, baz_hash.value} == set(index.hashes())
+17 -9
View File
@@ -7,6 +7,7 @@ from dvc.dvcfile import PIPELINE_FILE
from dvc.output import OutputDoesNotExistError from dvc.output import OutputDoesNotExistError
from dvc.stage.exceptions import StageCommitError from dvc.stage.exceptions import StageCommitError
from dvc.utils.serialize import dump_yaml, load_yaml from dvc.utils.serialize import dump_yaml, load_yaml
from tests.utils import clean_staging
def test_commit_recursive(tmp_dir, dvc): def test_commit_recursive(tmp_dir, dvc):
@@ -53,6 +54,8 @@ def test_commit_with_deps(tmp_dir, dvc, run_copy, run_kw):
assert foo_stage.outs[0].changed_cache() assert foo_stage.outs[0].changed_cache()
assert stage.outs[0].changed_cache() assert stage.outs[0].changed_cache()
clean_staging()
dvc.commit(stage.path, with_deps=True) dvc.commit(stage.path, with_deps=True)
assert not foo_stage.outs[0].changed_cache() assert not foo_stage.outs[0].changed_cache()
assert not stage.outs[0].changed_cache() assert not stage.outs[0].changed_cache()
@@ -66,6 +69,8 @@ def test_commit_changed_md5(tmp_dir, dvc):
stage_file_content["md5"] = "1111111111" stage_file_content["md5"] = "1111111111"
dump_yaml(stage.path, stage_file_content) dump_yaml(stage.path, stage_file_content)
clean_staging()
with pytest.raises(StageCommitError): with pytest.raises(StageCommitError):
dvc.commit(stage.path) dvc.commit(stage.path)
@@ -78,6 +83,8 @@ def test_commit_no_exec(tmp_dir, dvc):
stage = dvc.run( stage = dvc.run(
name="my", cmd="mycmd", deps=["dep"], outs=["out"], no_exec=True name="my", cmd="mycmd", deps=["dep"], outs=["out"], no_exec=True
) )
clean_staging()
assert dvc.status(stage.path) assert dvc.status(stage.path)
dvc.commit(stage.path, force=True) dvc.commit(stage.path, force=True)
assert dvc.status(stage.path) == {} assert dvc.status(stage.path) == {}
@@ -90,6 +97,7 @@ def test_commit_granular_output(tmp_dir, dvc):
outs=["foo", "bar"], outs=["foo", "bar"],
no_commit=True, no_commit=True,
) )
clean_staging()
cache = tmp_dir / ".dvc" / "cache" cache = tmp_dir / ".dvc" / "cache"
assert not list(cache.glob("*/*")) assert not list(cache.glob("*/*"))
@@ -103,6 +111,7 @@ def test_commit_granular_output(tmp_dir, dvc):
def test_commit_granular_output_file(tmp_dir, dvc): def test_commit_granular_output_file(tmp_dir, dvc):
tmp_dir.gen("foo", "foo") tmp_dir.gen("foo", "foo")
dvc.add("foo", no_commit=True) dvc.add("foo", no_commit=True)
clean_staging()
dvc.commit("foo") dvc.commit("foo")
assert dvc.status() == {} assert dvc.status() == {}
@@ -118,13 +127,12 @@ def test_commit_granular_output_dir(tmp_dir, dvc):
} }
) )
dvc.add("data", no_commit=True) dvc.add("data", no_commit=True)
clean_staging()
dvc.commit("data") dvc.commit("data")
assert dvc.status() == {} assert dvc.status() == {}
def test_commit_granular_dir(tmp_dir, dvc): def test_commit_granular_dir(tmp_dir, dvc):
from dvc.objects.stage import _STAGING_DIR
tmp_dir.gen( tmp_dir.gen(
{ {
"data": { "data": {
@@ -135,21 +143,18 @@ def test_commit_granular_dir(tmp_dir, dvc):
} }
) )
dvc.add("data", no_commit=True) dvc.add("data", no_commit=True)
clean_staging()
cache = tmp_dir / ".dvc" / "cache" cache = tmp_dir / ".dvc" / "cache"
staging = tmp_dir / ".dvc" / "tmp" / _STAGING_DIR
assert set(cache.glob("*/*")) == set() assert set(cache.glob("*/*")) == set()
assert set(staging.glob("*/*")) == {
staging / "1a" / "ca2c799df82929bbdd976557975546.dir"
}
dvc.commit(os.path.join("data", "foo")) dvc.commit(os.path.join("data", "foo"))
assert set(cache.glob("*/*")) == { assert set(cache.glob("*/*")) == {
cache / "1a" / "ca2c799df82929bbdd976557975546.dir", cache / "1a" / "ca2c799df82929bbdd976557975546.dir",
cache / "ac" / "bd18db4cc2f85cedef654fccc4a4d8", cache / "ac" / "bd18db4cc2f85cedef654fccc4a4d8",
} }
assert set(staging.glob("*/*")) == set() clean_staging()
dvc.commit(os.path.join("data", "subdir")) dvc.commit(os.path.join("data", "subdir"))
assert set(cache.glob("*/*")) == { assert set(cache.glob("*/*")) == {
@@ -158,7 +163,7 @@ def test_commit_granular_dir(tmp_dir, dvc):
cache / "4c" / "e8d2a2cf314a52fa7f315ca37ca445", cache / "4c" / "e8d2a2cf314a52fa7f315ca37ca445",
cache / "68" / "dde2c3c4e7953c2290f176bbdc9a54", cache / "68" / "dde2c3c4e7953c2290f176bbdc9a54",
} }
assert set(staging.glob("*/*")) == set() clean_staging()
dvc.commit(os.path.join("data")) dvc.commit(os.path.join("data"))
assert set(cache.glob("*/*")) == { assert set(cache.glob("*/*")) == {
@@ -168,13 +173,13 @@ def test_commit_granular_dir(tmp_dir, dvc):
cache / "68" / "dde2c3c4e7953c2290f176bbdc9a54", cache / "68" / "dde2c3c4e7953c2290f176bbdc9a54",
cache / "37" / "b51d194a7513e45b56f6524f2d51f2", cache / "37" / "b51d194a7513e45b56f6524f2d51f2",
} }
assert set(staging.glob("*/*")) == set()
def test_commit_no_exec_missing_dep(tmp_dir, dvc): def test_commit_no_exec_missing_dep(tmp_dir, dvc):
stage = dvc.run( stage = dvc.run(
name="my", cmd="mycmd", deps=["dep"], outs=["out"], no_exec=True name="my", cmd="mycmd", deps=["dep"], outs=["out"], no_exec=True
) )
clean_staging()
assert dvc.status(stage.path) assert dvc.status(stage.path)
with pytest.raises(DependencyDoesNotExistError): with pytest.raises(DependencyDoesNotExistError):
@@ -183,6 +188,7 @@ def test_commit_no_exec_missing_dep(tmp_dir, dvc):
def test_commit_no_exec_missing_out(tmp_dir, dvc): def test_commit_no_exec_missing_out(tmp_dir, dvc):
stage = dvc.run(name="my", cmd="mycmd", outs=["out"], no_exec=True) stage = dvc.run(name="my", cmd="mycmd", outs=["out"], no_exec=True)
clean_staging()
assert dvc.status(stage.path) assert dvc.status(stage.path)
with pytest.raises(OutputDoesNotExistError): with pytest.raises(OutputDoesNotExistError):
@@ -192,6 +198,7 @@ def test_commit_no_exec_missing_out(tmp_dir, dvc):
def test_commit_pipeline_stage(tmp_dir, dvc, run_copy): def test_commit_pipeline_stage(tmp_dir, dvc, run_copy):
tmp_dir.gen("foo", "foo") tmp_dir.gen("foo", "foo")
stage = run_copy("foo", "bar", no_commit=True, name="copy-foo-bar") stage = run_copy("foo", "bar", no_commit=True, name="copy-foo-bar")
clean_staging()
assert dvc.status(stage.addressing) assert dvc.status(stage.addressing)
assert dvc.commit(stage.addressing, force=True) == [stage] assert dvc.commit(stage.addressing, force=True) == [stage]
assert not dvc.status(stage.addressing) assert not dvc.status(stage.addressing)
@@ -205,6 +212,7 @@ def test_commit_pipeline_stage(tmp_dir, dvc, run_copy):
def test_imported_entries_unchanged(tmp_dir, dvc, erepo_dir): def test_imported_entries_unchanged(tmp_dir, dvc, erepo_dir):
with erepo_dir.chdir(): with erepo_dir.chdir():
erepo_dir.dvc_gen("file", "file content", "initial commit") erepo_dir.dvc_gen("file", "file content", "initial commit")
clean_staging()
stage = dvc.imp(os.fspath(erepo_dir), "file") stage = dvc.imp(os.fspath(erepo_dir), "file")
+22 -17
View File
@@ -50,7 +50,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
out = stage.outs[0] out = stage.outs[0]
cache = out.cache_path cache = out.cache_path
foo_hash = out.hash_info foo_hash = out.hash_info
foo_objs = out.get_used_objs().get(None, set()) foo_hashes = out.get_used_objs().get(None, set())
(stage_dir,) = tmp_dir.dvc_gen( (stage_dir,) = tmp_dir.dvc_gen(
{ {
@@ -63,7 +63,6 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
) )
out_dir = stage_dir.outs[0] out_dir = stage_dir.outs[0]
cache_dir = out_dir.cache_path cache_dir = out_dir.cache_path
dir_objs = {out_dir.obj}
dir_hash = out_dir.hash_info dir_hash = out_dir.hash_info
dir_hashes = {dir_hash} | { dir_hashes = {dir_hash} | {
entry_obj.hash_info for _, entry_obj in out_dir.obj entry_obj.hash_info for _, entry_obj in out_dir.obj
@@ -72,23 +71,23 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
def _check_status(status, **kwargs): def _check_status(status, **kwargs):
for key in ("ok", "missing", "new", "deleted"): for key in ("ok", "missing", "new", "deleted"):
expected = kwargs.get(key, set()) expected = kwargs.get(key, set())
assert expected == {obj.hash_info for obj in getattr(status, key)} assert expected == set(getattr(status, key))
# Check status # Check status
status = dvc.cloud.status(foo_objs) status = dvc.cloud.status(foo_hashes)
_check_status(status, new={foo_hash}) _check_status(status, new={foo_hash})
status_dir = dvc.cloud.status(dir_objs) status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, new=dir_hashes) _check_status(status_dir, new=dir_hashes)
# Move cache and check status # Move cache and check status
# See issue https://github.com/iterative/dvc/issues/4383 for details # See issue https://github.com/iterative/dvc/issues/4383 for details
backup_dir = dvc.odb.local.cache_dir + ".backup" backup_dir = dvc.odb.local.cache_dir + ".backup"
move(dvc.odb.local.cache_dir, backup_dir) move(dvc.odb.local.cache_dir, backup_dir)
status = dvc.cloud.status(foo_objs) status = dvc.cloud.status(foo_hashes)
_check_status(status, missing={foo_hash}) _check_status(status, missing={foo_hash})
status_dir = dvc.cloud.status(dir_objs) status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, missing=dir_hashes) _check_status(status_dir, missing=dir_hashes)
# Restore original cache: # Restore original cache:
@@ -96,42 +95,42 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
move(backup_dir, dvc.odb.local.cache_dir) move(backup_dir, dvc.odb.local.cache_dir)
# Push and check status # Push and check status
dvc.cloud.push(foo_objs) dvc.cloud.push(foo_hashes)
assert os.path.exists(cache) assert os.path.exists(cache)
assert os.path.isfile(cache) assert os.path.isfile(cache)
dvc.cloud.push(dir_objs) dvc.cloud.push(dir_hashes)
assert os.path.isfile(cache_dir) assert os.path.isfile(cache_dir)
status = dvc.cloud.status(foo_objs) status = dvc.cloud.status(foo_hashes)
_check_status(status, ok={foo_hash}) _check_status(status, ok={foo_hash})
status_dir = dvc.cloud.status(dir_objs) status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, ok=dir_hashes) _check_status(status_dir, ok=dir_hashes)
# Remove and check status # Remove and check status
remove(dvc.odb.local.cache_dir) remove(dvc.odb.local.cache_dir)
status = dvc.cloud.status(foo_objs) status = dvc.cloud.status(foo_hashes)
_check_status(status, deleted={foo_hash}) _check_status(status, deleted={foo_hash})
status_dir = dvc.cloud.status(dir_objs) status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, deleted=dir_hashes) _check_status(status_dir, deleted=dir_hashes)
# Pull and check status # Pull and check status
dvc.cloud.pull(foo_objs) dvc.cloud.pull(foo_hashes)
assert os.path.exists(cache) assert os.path.exists(cache)
assert os.path.isfile(cache) assert os.path.isfile(cache)
with open(cache) as fd: with open(cache) as fd:
assert fd.read() == "foo" assert fd.read() == "foo"
dvc.cloud.pull(dir_objs) dvc.cloud.pull(dir_hashes)
assert os.path.isfile(cache_dir) assert os.path.isfile(cache_dir)
status = dvc.cloud.status(foo_objs) status = dvc.cloud.status(foo_hashes)
_check_status(status, ok={foo_hash}) _check_status(status, ok={foo_hash})
status_dir = dvc.cloud.status(dir_objs) status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, ok=dir_hashes) _check_status(status_dir, ok=dir_hashes)
@@ -242,10 +241,13 @@ def test_hash_recalculation(mocker, dvc, tmp_dir, local_remote):
def test_missing_cache(tmp_dir, dvc, local_remote, caplog): def test_missing_cache(tmp_dir, dvc, local_remote, caplog):
from tests.utils import clean_staging
tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"}) tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"})
# purge cache # purge cache
remove(dvc.odb.local.cache_dir) remove(dvc.odb.local.cache_dir)
clean_staging()
header = ( header = (
"Some of the cache files do not exist " "Some of the cache files do not exist "
@@ -355,6 +357,8 @@ def test_pull_external_dvc_imports(tmp_dir, dvc, scm, erepo_dir):
def clean(outs, dvc=None): def clean(outs, dvc=None):
from tests.utils import clean_staging
if dvc: if dvc:
outs = outs + [dvc.odb.local.cache_dir] outs = outs + [dvc.odb.local.cache_dir]
for path in outs: for path in outs:
@@ -363,6 +367,7 @@ def clean(outs, dvc=None):
if dvc: if dvc:
os.makedirs(dvc.odb.local.cache_dir, exist_ok=True) os.makedirs(dvc.odb.local.cache_dir, exist_ok=True)
clean_repos() clean_repos()
clean_staging()
def recurse_list_dir(d): def recurse_list_dir(d):
+11 -3
View File
@@ -3,13 +3,14 @@ from unittest.mock import ANY, patch
from dvc.external_repo import CLONES, external_repo from dvc.external_repo import CLONES, external_repo
from dvc.fs.local import LocalFileSystem from dvc.fs.local import LocalFileSystem
from dvc.objects import save
from dvc.objects.stage import stage from dvc.objects.stage import stage
from dvc.objects.transfer import transfer
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
from dvc.scm.git import Git from dvc.scm.git import Git
from dvc.utils import relpath from dvc.utils import relpath
from dvc.utils.fs import makedirs, remove from dvc.utils.fs import makedirs, remove
from tests.unit.fs.test_repo import make_subrepo 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):
@@ -198,16 +199,23 @@ def test_subrepos_are_ignored(tmp_dir, erepo_dir):
# clear cache to test saving to cache # clear cache to test saving to cache
cache_dir = tmp_dir / repo.odb.local.cache_dir cache_dir = tmp_dir / repo.odb.local.cache_dir
remove(cache_dir) remove(cache_dir)
clean_staging()
makedirs(cache_dir) makedirs(cache_dir)
obj = stage( staging, obj = stage(
repo.odb.local, repo.odb.local,
PathInfo(repo.root_dir) / "dir", PathInfo(repo.root_dir) / "dir",
repo.repo_fs, repo.repo_fs,
"md5", "md5",
dvcignore=repo.dvcignore, dvcignore=repo.dvcignore,
) )
save(repo.odb.local, obj) transfer(
staging,
repo.odb.local,
{obj.hash_info},
shallow=False,
move=True,
)
assert set(cache_dir.glob("??/*")) == { assert set(cache_dir.glob("??/*")) == {
cache_dir / "e1" / "d9e8eae5374860ae025ec84cfd85c7.dir", cache_dir / "e1" / "d9e8eae5374860ae025ec84cfd85c7.dir",
cache_dir / "37" / "b51d194a7513e45b56f6524f2d51f2", cache_dir / "37" / "b51d194a7513e45b56f6524f2d51f2",
+4
View File
@@ -13,6 +13,7 @@ from dvc.stage.exceptions import StagePathNotFoundError
from dvc.system import System from dvc.system import System
from dvc.utils.fs import makedirs, remove from dvc.utils.fs import makedirs, remove
from tests.unit.fs.test_repo import make_subrepo from tests.unit.fs.test_repo import make_subrepo
from tests.utils import clean_staging
def test_import(tmp_dir, scm, dvc, erepo_dir): def test_import(tmp_dir, scm, dvc, erepo_dir):
@@ -290,6 +291,8 @@ def test_push_wildcard_from_bare_git_repo(
dvc_repo = make_tmp_dir("dvc-repo", scm=True, dvc=True) dvc_repo = make_tmp_dir("dvc-repo", scm=True, dvc=True)
with dvc_repo.chdir(): with dvc_repo.chdir():
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dirextra") dvc_repo.dvc.imp(os.fspath(tmp_dir), "dirextra")
clean_staging()
with pytest.raises(PathMissingError): with pytest.raises(PathMissingError):
dvc_repo.dvc.imp(os.fspath(tmp_dir), "dir123") dvc_repo.dvc.imp(os.fspath(tmp_dir), "dir123")
@@ -593,6 +596,7 @@ def test_circular_import(tmp_dir, dvc, scm, erepo_dir):
dvc.imp(os.fspath(erepo_dir), "dir", "dir_imported") dvc.imp(os.fspath(erepo_dir), "dir", "dir_imported")
scm.add("dir_imported.dvc") scm.add("dir_imported.dvc")
scm.commit("import") scm.commit("import")
clean_staging()
with erepo_dir.chdir(): with erepo_dir.chdir():
with pytest.raises(CircularImportError): with pytest.raises(CircularImportError):
+2
View File
@@ -512,6 +512,7 @@ def test_ls_target(erepo_dir, use_scm):
) )
def test_subrepo(dvc_top_level, erepo): def test_subrepo(dvc_top_level, erepo):
from tests.func.test_get import make_subrepo from tests.func.test_get import make_subrepo
from tests.utils import clean_staging
dvc_files = {"foo.txt": "foo.txt", "dvc_dir": {"lorem": "lorem"}} dvc_files = {"foo.txt": "foo.txt", "dvc_dir": {"lorem": "lorem"}}
scm_files = {"bar.txt": "bar.txt", "scm_dir": {"ipsum": "ipsum"}} scm_files = {"bar.txt": "bar.txt", "scm_dir": {"ipsum": "ipsum"}}
@@ -523,6 +524,7 @@ def test_subrepo(dvc_top_level, erepo):
repo.scm_gen(scm_files, commit=f"scm track for top {repo}") repo.scm_gen(scm_files, commit=f"scm track for top {repo}")
if hasattr(repo, "dvc"): if hasattr(repo, "dvc"):
repo.dvc_gen(dvc_files, commit=f"dvc track for {repo}") repo.dvc_gen(dvc_files, commit=f"dvc track for {repo}")
clean_staging()
def _list_files(repo, path=None): def _list_files(repo, path=None):
return set(map(itemgetter("path"), Repo.ls(os.fspath(repo), path))) return set(map(itemgetter("path"), Repo.ls(os.fspath(repo), path)))
+4 -6
View File
@@ -157,18 +157,16 @@ def test_dir_hash_should_be_key_order_agnostic(tmp_dir, dvc):
) )
tree.digest() tree.digest()
with patch("dvc.objects.stage._get_tree_obj", return_value=tree): with patch("dvc.objects.stage._get_tree_obj", return_value=tree):
hash1 = stage( _, obj = stage(dvc.odb.local, path_info, dvc.odb.local.fs, "md5")
dvc.odb.local, path_info, dvc.odb.local.fs, "md5" hash1 = obj.hash_info
).hash_info
tree = Tree.from_list( tree = Tree.from_list(
[{"md5": "1", "relpath": "1"}, {"md5": "2", "relpath": "2"}] [{"md5": "1", "relpath": "1"}, {"md5": "2", "relpath": "2"}]
) )
tree.digest() tree.digest()
with patch("dvc.objects.stage._get_tree_obj", return_value=tree): with patch("dvc.objects.stage._get_tree_obj", return_value=tree):
hash2 = stage( _, obj = stage(dvc.odb.local, path_info, dvc.odb.local.fs, "md5")
dvc.odb.local, path_info, dvc.odb.local.fs, "md5" hash2 = obj.hash_info
).hash_info
assert hash1 == hash2 assert hash1 == hash2
+3
View File
@@ -900,10 +900,13 @@ class TestShouldNotCheckoutUponCorruptedLocalHardlinkCache(TestDvc):
self.dvc = DvcRepo(".") self.dvc = DvcRepo(".")
def test(self): def test(self):
from tests.utils import clean_staging
cmd = f"python {self.CODE} {self.FOO} {self.BAR}" cmd = f"python {self.CODE} {self.FOO} {self.BAR}"
stage = self.dvc.run( stage = self.dvc.run(
deps=[self.FOO], outs=[self.BAR], cmd=cmd, single_stage=True deps=[self.FOO], outs=[self.BAR], cmd=cmd, single_stage=True
) )
clean_staging()
os.chmod(self.BAR, 0o644) os.chmod(self.BAR, 0o644)
with open(self.BAR, "w") as fd: with open(self.BAR, "w") as fd:
+12 -10
View File
@@ -243,15 +243,15 @@ def test_get_hash_granular(tmp_dir, dvc):
fs = DvcFileSystem(repo=dvc) fs = DvcFileSystem(repo=dvc)
subdir = PathInfo(tmp_dir) / "dir" / "subdir" subdir = PathInfo(tmp_dir) / "dir" / "subdir"
assert fs.info(subdir).get("md5") is None assert fs.info(subdir).get("md5") is None
assert stage(dvc.odb.local, subdir, fs, "md5").hash_info == HashInfo( _, obj = stage(dvc.odb.local, subdir, fs, "md5", dry_run=True)
assert obj.hash_info == HashInfo(
"md5", "af314506f1622d107e0ed3f14ec1a3b5.dir" "md5", "af314506f1622d107e0ed3f14ec1a3b5.dir"
) )
assert ( assert (
fs.info(subdir / "data")["md5"] == "8d777f385d3dfec8815d20f7496026dc" fs.info(subdir / "data")["md5"] == "8d777f385d3dfec8815d20f7496026dc"
) )
assert stage( _, obj = stage(dvc.odb.local, subdir / "data", fs, "md5", dry_run=True)
dvc.odb.local, subdir / "data", fs, "md5" assert obj.hash_info == HashInfo("md5", "8d777f385d3dfec8815d20f7496026dc")
).hash_info == HashInfo("md5", "8d777f385d3dfec8815d20f7496026dc")
def test_get_hash_dirty_file(tmp_dir, dvc): def test_get_hash_dirty_file(tmp_dir, dvc):
@@ -261,9 +261,10 @@ def test_get_hash_dirty_file(tmp_dir, dvc):
fs = DvcFileSystem(repo=dvc) fs = DvcFileSystem(repo=dvc)
expected = "8c7dd922ad47494fc02c388e12c00eac" expected = "8c7dd922ad47494fc02c388e12c00eac"
assert fs.info(PathInfo(tmp_dir) / "file").get("md5") == expected assert fs.info(PathInfo(tmp_dir) / "file").get("md5") == expected
assert stage( _, obj = stage(
dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5" dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5", dry_run=True
).hash_info == HashInfo("md5", expected) )
assert obj.hash_info == HashInfo("md5", expected)
def test_get_hash_dirty_dir(tmp_dir, dvc): def test_get_hash_dirty_dir(tmp_dir, dvc):
@@ -273,6 +274,7 @@ def test_get_hash_dirty_dir(tmp_dir, dvc):
fs = DvcFileSystem(repo=dvc) fs = DvcFileSystem(repo=dvc)
expected = "5ea40360f5b4ec688df672a4db9c17d1.dir" expected = "5ea40360f5b4ec688df672a4db9c17d1.dir"
assert fs.info(PathInfo(tmp_dir) / "dir").get("md5") == expected assert fs.info(PathInfo(tmp_dir) / "dir").get("md5") == expected
assert stage( _, obj = stage(
dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5" dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5", dry_run=True
).hash_info == HashInfo("md5", expected) )
assert obj.hash_info == HashInfo("md5", expected)
+57 -39
View File
@@ -8,6 +8,7 @@ from dvc.fs.repo import RepoFileSystem
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.objects.stage import stage from dvc.objects.stage import stage
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
from tests.utils import clean_staging
def test_exists(tmp_dir, dvc): def test_exists(tmp_dir, dvc):
@@ -512,9 +513,8 @@ def test_get_hash_cached_file(tmp_dir, dvc, mocker):
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
expected = "acbd18db4cc2f85cedef654fccc4a4d8" expected = "acbd18db4cc2f85cedef654fccc4a4d8"
assert fs.info(PathInfo(tmp_dir) / "foo").get("md5") is None assert fs.info(PathInfo(tmp_dir) / "foo").get("md5") is None
assert stage( _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "foo", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "foo", fs, "md5" assert obj.hash_info == HashInfo("md5", expected)
).hash_info == HashInfo("md5", expected)
(tmp_dir / "foo").unlink() (tmp_dir / "foo").unlink()
assert fs.info(PathInfo(tmp_dir) / "foo")["md5"] == expected assert fs.info(PathInfo(tmp_dir) / "foo")["md5"] == expected
@@ -526,15 +526,17 @@ def test_get_hash_cached_dir(tmp_dir, dvc, mocker):
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
expected = "8761c4e9acad696bee718615e23e22db.dir" expected = "8761c4e9acad696bee718615e23e22db.dir"
assert fs.info(PathInfo(tmp_dir) / "dir").get("md5") is None assert fs.info(PathInfo(tmp_dir) / "dir").get("md5") is None
assert stage( _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5" assert obj.hash_info == HashInfo(
).hash_info == HashInfo("md5", "8761c4e9acad696bee718615e23e22db.dir") "md5", "8761c4e9acad696bee718615e23e22db.dir"
)
shutil.rmtree(tmp_dir / "dir") shutil.rmtree(tmp_dir / "dir")
assert fs.info(PathInfo(tmp_dir) / "dir")["md5"] == expected assert fs.info(PathInfo(tmp_dir) / "dir")["md5"] == expected
assert stage( _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5" assert obj.hash_info == HashInfo(
).hash_info == HashInfo("md5", "8761c4e9acad696bee718615e23e22db.dir") "md5", "8761c4e9acad696bee718615e23e22db.dir"
)
def test_get_hash_cached_granular(tmp_dir, dvc, mocker): def test_get_hash_cached_granular(tmp_dir, dvc, mocker):
@@ -544,13 +546,13 @@ def test_get_hash_cached_granular(tmp_dir, dvc, mocker):
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
subdir = PathInfo(tmp_dir) / "dir" / "subdir" subdir = PathInfo(tmp_dir) / "dir" / "subdir"
assert fs.info(subdir).get("md5") is None assert fs.info(subdir).get("md5") is None
assert stage(dvc.odb.local, subdir, fs, "md5").hash_info == HashInfo( _, obj = stage(dvc.odb.local, subdir, fs, "md5")
assert obj.hash_info == HashInfo(
"md5", "af314506f1622d107e0ed3f14ec1a3b5.dir" "md5", "af314506f1622d107e0ed3f14ec1a3b5.dir"
) )
assert fs.info(subdir / "data").get("md5") is None assert fs.info(subdir / "data").get("md5") is None
assert stage( _, obj = stage(dvc.odb.local, subdir / "data", fs, "md5")
dvc.odb.local, subdir / "data", fs, "md5" assert obj.hash_info == HashInfo("md5", "8d777f385d3dfec8815d20f7496026dc")
).hash_info == HashInfo("md5", "8d777f385d3dfec8815d20f7496026dc")
(tmp_dir / "dir" / "subdir" / "data").unlink() (tmp_dir / "dir" / "subdir" / "data").unlink()
assert ( assert (
fs.info(subdir / "data")["md5"] == "8d777f385d3dfec8815d20f7496026dc" fs.info(subdir / "data")["md5"] == "8d777f385d3dfec8815d20f7496026dc"
@@ -568,50 +570,66 @@ def test_get_hash_mixed_dir(tmp_dir, scm, dvc):
] ]
) )
tmp_dir.scm.commit("add dir") tmp_dir.scm.commit("add dir")
clean_staging()
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
actual = stage( _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5" assert obj.hash_info == HashInfo(
).hash_info "md5", "e1d9e8eae5374860ae025ec84cfd85c7.dir"
expected = HashInfo("md5", "e1d9e8eae5374860ae025ec84cfd85c7.dir") )
assert actual == expected
def test_get_hash_dirty_file(tmp_dir, dvc): def test_get_hash_dirty_file(tmp_dir, dvc):
tmp_dir.dvc_gen("file", "file") from dvc.objects import check
(tmp_dir / "file").write_text("something") from dvc.objects.errors import ObjectFormatError
from dvc.objects.stage import get_file_hash
tmp_dir.dvc_gen("file", "file")
file_hash_info = HashInfo("md5", "8c7dd922ad47494fc02c388e12c00eac")
(tmp_dir / "file").write_text("something")
something_hash_info = HashInfo("md5", "437b930db84b8079c2dd804a71936b5f")
clean_staging()
# file is modified in workspace
# get_file_hash(file) should return workspace hash, not DVC cached hash
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
assert fs.info(PathInfo(tmp_dir) / "file").get("md5") is None assert fs.info(PathInfo(tmp_dir) / "file").get("md5") is None
actual = stage( staging, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5" assert obj.hash_info == something_hash_info
).hash_info check(staging, obj)
expected = HashInfo("md5", "437b930db84b8079c2dd804a71936b5f")
assert actual == expected
# file is removed in workspace
# any staged object referring to modified workspace obj is now invalid
(tmp_dir / "file").unlink() (tmp_dir / "file").unlink()
assert ( with pytest.raises(ObjectFormatError):
fs.info(PathInfo(tmp_dir) / "file")["md5"] check(staging, obj)
== "8c7dd922ad47494fc02c388e12c00eac"
# get_file_hash(file) should return DVC cached hash
assert fs.info(PathInfo(tmp_dir) / "file")["md5"] == file_hash_info.value
hash_info = get_file_hash(
PathInfo(tmp_dir) / "file", fs, "md5", state=dvc.state
) )
actual = stage( assert hash_info == file_hash_info
dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5"
).hash_info # tmp_dir/file can be staged even though it is missing in workspace since
expected = HashInfo("md5", "8c7dd922ad47494fc02c388e12c00eac") # repofs will use the DVC cached hash (and refer to the local cache object)
assert actual == expected _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "file", fs, "md5")
assert obj.hash_info == file_hash_info
def test_get_hash_dirty_dir(tmp_dir, dvc): def test_get_hash_dirty_dir(tmp_dir, dvc):
tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}) tmp_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}})
(tmp_dir / "dir" / "baz").write_text("baz") (tmp_dir / "dir" / "baz").write_text("baz")
clean_staging()
fs = RepoFileSystem(repo=dvc) fs = RepoFileSystem(repo=dvc)
actual = stage( _, obj = stage(dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5")
dvc.odb.local, PathInfo(tmp_dir) / "dir", fs, "md5" assert obj.hash_info == HashInfo(
).hash_info "md5", "ba75a2162ca9c29acecb7957105a0bc2.dir"
expected = HashInfo("md5", "ba75a2162ca9c29acecb7957105a0bc2.dir") )
assert actual == expected assert obj.hash_info.nfiles == 3
assert actual.nfiles == 3
@pytest.mark.parametrize("traverse_subrepos", [True, False]) @pytest.mark.parametrize("traverse_subrepos", [True, False])
+23 -40
View File
@@ -6,7 +6,6 @@ import pytest
from dvc.fs.local import LocalFileSystem from dvc.fs.local import LocalFileSystem
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.objects.db.local import LocalObjectDB from dvc.objects.db.local import LocalObjectDB
from dvc.objects.file import HashFile
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
@@ -18,21 +17,17 @@ def test_status_download_optimization(mocker, dvc):
from dvc.objects.status import compare_status from dvc.objects.status import compare_status
odb = LocalObjectDB(LocalFileSystem(), PathInfo(".")) odb = LocalObjectDB(LocalFileSystem(), PathInfo("."))
objs = { obj_ids = {
HashFile( HashInfo("md5", "acbd18db4cc2f85cedef654fccc4a4d8"),
None, odb.fs, HashInfo("md5", "acbd18db4cc2f85cedef654fccc4a4d8") HashInfo("md5", "37b51d194a7513e45b56f6524f2d51f2"),
),
HashFile(
None, odb.fs, HashInfo("md5", "37b51d194a7513e45b56f6524f2d51f2")
),
} }
local_exists = [obj.hash_info.value for obj in objs] local_exists = [hash_info.value for hash_info in obj_ids]
mocker.patch.object(odb, "hashes_exist", return_value=local_exists) mocker.patch.object(odb, "hashes_exist", return_value=local_exists)
src_odb = mocker.Mock() src_odb = mocker.Mock()
compare_status(src_odb, odb, objs, check_deleted=False) compare_status(src_odb, odb, obj_ids, check_deleted=False)
assert src_odb.hashes_exist.call_count == 0 assert src_odb.hashes_exist.call_count == 0
@@ -91,28 +86,24 @@ def test_set_exec_ignore_errors(tmp_dir, dvc, mocker, err):
def test_staging_file(tmp_dir, dvc): def test_staging_file(tmp_dir, dvc):
from dvc.objects import check, save from dvc.objects import check
from dvc.objects.stage import get_staging, stage from dvc.objects.stage import stage
from dvc.objects.transfer import transfer
tmp_dir.gen("foo", "foo") tmp_dir.gen("foo", "foo")
fs = LocalFileSystem() fs = LocalFileSystem()
local_odb = dvc.odb.local local_odb = dvc.odb.local
staging_odb = get_staging(local_odb) staging_odb, obj = stage(local_odb, tmp_dir / "foo", fs, "md5")
obj = stage(local_odb, tmp_dir / "foo", fs, "md5")
for odb in (local_odb, staging_odb): assert not local_odb.exists(obj.hash_info)
path_info = odb.hash_to_path_info(obj.hash_info.value) assert staging_odb.exists(obj.hash_info)
assert not odb.fs.exists(path_info)
# check for file after staging should fail since files are not added on
# stage()
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
check(local_odb, obj) check(local_odb, obj)
with pytest.raises(FileNotFoundError): check(staging_odb, obj)
check(staging_odb, obj)
save(local_odb, obj) transfer(staging_odb, local_odb, {obj.hash_info}, move=True)
check(local_odb, obj) check(local_odb, obj)
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
check(staging_odb, obj) check(staging_odb, obj)
@@ -122,35 +113,27 @@ def test_staging_file(tmp_dir, dvc):
def test_staging_dir(tmp_dir, dvc): def test_staging_dir(tmp_dir, dvc):
from dvc.objects import check, save from dvc.objects import check
from dvc.objects.stage import get_staging, stage from dvc.objects.stage import stage
from dvc.objects.transfer import transfer
tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}}) tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}})
fs = LocalFileSystem() fs = LocalFileSystem()
local_odb = dvc.odb.local local_odb = dvc.odb.local
staging_odb = get_staging(local_odb)
obj = stage(local_odb, tmp_dir / "dir", fs, "md5") staging_odb, obj = stage(local_odb, tmp_dir / "dir", fs, "md5")
path_info = local_odb.hash_to_path_info(obj.hash_info.value) assert not local_odb.exists(obj.hash_info)
assert not local_odb.fs.exists(path_info) assert staging_odb.exists(obj.hash_info)
path_info = staging_odb.hash_to_path_info(obj.hash_info.value)
assert staging_odb.fs.exists(path_info)
# check for raw object after staging should pass only when using the
# staging odb
raw = HashFile(obj.path_info, obj.fs, obj.hash_info)
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
check(local_odb, raw) check(local_odb, obj)
check(staging_odb, raw) check(staging_odb, obj)
# checking the entire tree should fail since individual file entries are transfer(staging_odb, local_odb, {obj.hash_info}, shallow=False, move=True)
# not added on stage() check(local_odb, obj)
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
check(staging_odb, obj) check(staging_odb, obj)
save(local_odb, obj)
check(local_odb, obj)
path_info = local_odb.hash_to_path_info(obj.hash_info.value) path_info = local_odb.hash_to_path_info(obj.hash_info.value)
assert fs.exists(path_info) assert fs.exists(path_info)
+2 -7
View File
@@ -45,8 +45,6 @@ def test_find_outs_by_path_does_graph_checks(tmp_dir, dvc):
[os.path.join("dir", "subdir", "file"), os.path.join("dir", "subdir")], [os.path.join("dir", "subdir", "file"), os.path.join("dir", "subdir")],
) )
def test_used_objs(tmp_dir, dvc, path): def test_used_objs(tmp_dir, dvc, path):
from dvc.objects.tree import Tree
tmp_dir.dvc_gen({"dir": {"subdir": {"file": "file"}, "other": "other"}}) tmp_dir.dvc_gen({"dir": {"subdir": {"file": "file"}, "other": "other"}})
expected = { expected = {
@@ -55,11 +53,8 @@ def test_used_objs(tmp_dir, dvc, path):
} }
used = set() used = set()
for _, objs in dvc.used_objs([path]).items(): for _, obj_ids in dvc.used_objs([path]).items():
for obj in objs: used.update(obj_ids)
used.add(obj.hash_info)
if isinstance(obj, Tree):
used.update(entry_obj.hash_info for _, entry_obj in obj)
assert used == expected assert used == expected
+4 -4
View File
@@ -38,11 +38,11 @@ class TestMtimeAndSize(TestDir):
) )
self.assertIs(type(file_time), str) self.assertIs(type(file_time), str)
self.assertIs(type(file_size), str) self.assertIs(type(file_size), int)
self.assertEqual(file_size, str(actual_file_size)) self.assertEqual(file_size, actual_file_size)
self.assertIs(type(dir_time), str) self.assertIs(type(dir_time), str)
self.assertIs(type(dir_size), str) self.assertIs(type(dir_size), int)
self.assertEqual(dir_size, str(actual_dir_size)) self.assertEqual(dir_size, actual_dir_size)
class TestContainsLink(TestCase): class TestContainsLink(TestCase):
+12
View File
@@ -42,3 +42,15 @@ def dump_sv(stream, metrics, delimiter=",", header=True):
writer = csv.writer(stream) writer = csv.writer(stream)
for d in metrics: for d in metrics:
writer.writerow(list(d.values())) writer.writerow(list(d.values()))
def clean_staging():
from dvc.fs.memory import MemoryFileSystem
from dvc.objects.stage import _STAGING_MEMFS_PATH
try:
MemoryFileSystem().fs.rm(
f"memory://{_STAGING_MEMFS_PATH}", recursive=True
)
except FileNotFoundError:
pass