dvc: use DirInfo instead of dicts/lists (#4851)

Currently we are converting dir_info to/from lists all the time.
The reason is that dir_info is stored as list of dicts in *.dir files,
but that makes it hard to work with. In addition to that, we will likely
be changing .dir file format in the near future #829, so we need to
abstract away dir_info into something that we won't care how it will be
stored on disk.

Related #3256
Related #4847
This commit is contained in:
Ruslan Kuprieiev
2020-11-07 02:55:05 +02:00
committed by GitHub
parent 80e181f2fc
commit 4531aa34ca
14 changed files with 468 additions and 183 deletions
+30 -114
View File
@@ -1,21 +1,18 @@
import json import json
import logging import logging
from copy import copy from copy import copy
from operator import itemgetter
from funcy import decorator from funcy import decorator
from shortuuid import uuid from shortuuid import uuid
import dvc.prompt as prompt import dvc.prompt as prompt
from dvc.dir_info import DirInfo
from dvc.exceptions import ( from dvc.exceptions import (
CacheLinkError, CacheLinkError,
CheckoutError, CheckoutError,
ConfirmRemoveError, ConfirmRemoveError,
DvcException, DvcException,
MergeError,
) )
from dvc.hash_info import HashInfo
from dvc.path_info import WindowsPathInfo
from dvc.progress import Tqdm from dvc.progress import Tqdm
from dvc.remote.slow_link_detection import slow_link_guard from dvc.remote.slow_link_detection import slow_link_guard
@@ -77,7 +74,7 @@ class CloudCache:
try: try:
dir_info = self.load_dir_cache(hash_info) dir_info = self.load_dir_cache(hash_info)
except DirCacheError: except DirCacheError:
dir_info = [] dir_info = DirInfo()
self._dir_info[hash_info.value] = dir_info self._dir_info[hash_info.value] = dir_info
return dir_info return dir_info
@@ -96,18 +93,9 @@ class CloudCache:
"dir cache file format error '%s' [skipping the file]", "dir cache file format error '%s' [skipping the file]",
path_info, path_info,
) )
return [] d = []
if self.tree.PATH_CLS == WindowsPathInfo: return DirInfo.from_list(d)
# only need to convert it for Windows
for info in d:
# NOTE: here is a BUG, see comment to .as_posix() below
relpath = info[self.tree.PARAM_RELPATH]
info[self.tree.PARAM_RELPATH] = relpath.replace(
"/", self.tree.PATH_CLS.sep
)
return d
def changed(self, path_info, hash_info): def changed(self, path_info, hash_info):
"""Checks if data has changed. """Checks if data has changed.
@@ -271,14 +259,9 @@ class CloudCache:
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
from dvc.utils import tmp_fname from dvc.utils import tmp_fname
# Sorting the list by path to ensure reproducibility
if isinstance(dir_info, dict):
dir_info = self._from_dict(dir_info)
dir_info = sorted(dir_info, key=itemgetter(self.tree.PARAM_RELPATH))
tmp = tempfile.NamedTemporaryFile(delete=False).name tmp = tempfile.NamedTemporaryFile(delete=False).name
with open(tmp, "w+") as fobj: with open(tmp, "w+") as fobj:
json.dump(dir_info, fobj, sort_keys=True) json.dump(dir_info.to_list(), fobj, sort_keys=True)
from_info = PathInfo(tmp) from_info = PathInfo(tmp)
to_info = self.tree.path_info / tmp_fname("") to_info = self.tree.path_info / tmp_fname("")
@@ -286,8 +269,8 @@ class CloudCache:
hash_info = self.tree.get_file_hash(to_info) hash_info = self.tree.get_file_hash(to_info)
hash_info.value += self.tree.CHECKSUM_DIR_SUFFIX hash_info.value += self.tree.CHECKSUM_DIR_SUFFIX
hash_info.dir_info = self._to_dict(dir_info) hash_info.dir_info = dir_info
hash_info.nfiles = len(dir_info) hash_info.nfiles = dir_info.nfiles
return hash_info, to_info return hash_info, to_info
@@ -312,14 +295,13 @@ class CloudCache:
def _save_dir(self, path_info, tree, hash_info, save_link=True, **kwargs): def _save_dir(self, path_info, tree, hash_info, save_link=True, **kwargs):
if not hash_info.dir_info: if not hash_info.dir_info:
hash_info.dir_info = self._to_dict( hash_info.dir_info = tree.cache.get_dir_cache(hash_info)
tree.cache.get_dir_cache(hash_info)
)
hi = self.save_dir_info(hash_info.dir_info, hash_info) hi = self.save_dir_info(hash_info.dir_info, hash_info)
for relpath, entry_hash in Tqdm( for entry_info, entry_hash in Tqdm(
hi.dir_info.items(), desc="Saving " + path_info.name, unit="file" hi.dir_info.items(path_info),
desc="Saving " + path_info.name,
unit="file",
): ):
entry_info = path_info / relpath
self._save_file( self._save_file(
entry_info, tree, entry_hash, save_link=False, **kwargs entry_info, tree, entry_hash, save_link=False, **kwargs
) )
@@ -404,13 +386,9 @@ class CloudCache:
if self.changed_cache_file(hash_info): if self.changed_cache_file(hash_info):
return True return True
for entry in self.get_dir_cache(hash_info): dir_info = self.get_dir_cache(hash_info)
entry_hash = HashInfo( for entry_info, entry_hash in dir_info.items(path_info):
self.tree.PARAM_CHECKSUM, entry[self.tree.PARAM_CHECKSUM]
)
if path_info and filter_info: if path_info and filter_info:
entry_info = path_info / entry[self.tree.PARAM_RELPATH]
if not entry_info.isin_or_eq(filter_info): if not entry_info.isin_or_eq(filter_info):
continue continue
@@ -488,16 +466,14 @@ class CloudCache:
logger.debug("Linking directory '%s'.", path_info) logger.debug("Linking directory '%s'.", path_info)
for entry in dir_info: for entry_info, entry_hash_info in dir_info.items(path_info):
relative_path = entry[self.tree.PARAM_RELPATH] entry_cache_info = self.tree.hash_to_path_info(
entry_hash = entry[self.tree.PARAM_CHECKSUM] entry_hash_info.value
entry_cache_info = self.tree.hash_to_path_info(entry_hash) )
entry_info = path_info / relative_path
if filter_info and not entry_info.isin_or_eq(filter_info): if filter_info and not entry_info.isin_or_eq(filter_info):
continue continue
entry_hash_info = HashInfo(self.tree.PARAM_CHECKSUM, entry_hash)
if relink or self.changed(entry_info, entry_hash_info): if relink or self.changed(entry_info, entry_hash_info):
modified = True modified = True
self.safe_remove(entry_info, force=force) self.safe_remove(entry_info, force=force)
@@ -520,9 +496,7 @@ class CloudCache:
def _remove_redundant_files(self, path_info, dir_info, force): def _remove_redundant_files(self, path_info, dir_info, force):
existing_files = set(self.tree.walk_files(path_info)) existing_files = set(self.tree.walk_files(path_info))
needed_files = { needed_files = {info for info, _ in dir_info.items(path_info)}
path_info / entry[self.tree.PARAM_RELPATH] for entry in dir_info
}
redundant_files = existing_files - needed_files redundant_files = existing_files - needed_files
for path in redundant_files: for path in redundant_files:
self.safe_remove(path, force) self.safe_remove(path, force)
@@ -623,77 +597,19 @@ class CloudCache:
return 1 return 1
if not filter_info: if not filter_info:
return len(self.get_dir_cache(hash_info)) return self.get_dir_cache(hash_info).nfiles
return ilen( return ilen(
filter_info.isin_or_eq(path_info / entry[self.tree.PARAM_CHECKSUM]) filter_info.isin_or_eq(path_info / relpath)
for entry in self.get_dir_cache(hash_info) for relpath, _ in self.get_dir_cache(hash_info).items()
)
def _to_dict(self, dir_info):
info = {}
for _entry in dir_info:
entry = _entry.copy()
relpath = entry.pop(self.tree.PARAM_RELPATH)
info[relpath] = HashInfo.from_dict(entry)
return info
def _from_dict(self, dir_dict):
return [
{self.tree.PARAM_RELPATH: relpath, **hash_info.to_dict()}
for relpath, hash_info in dir_dict.items()
]
@staticmethod
def _diff(ancestor, other, allow_removed=False):
from dictdiffer import diff
allowed = ["add"]
if allow_removed:
allowed.append("remove")
result = list(diff(ancestor, other))
for typ, _, _ in result:
if typ not in allowed:
raise MergeError(
"unable to auto-merge directories with diff that contains "
f"'{typ}'ed files"
)
return result
def _merge_dirs(self, ancestor_info, our_info, their_info):
from dictdiffer import patch
ancestor = self._to_dict(ancestor_info)
our = self._to_dict(our_info)
their = self._to_dict(their_info)
our_diff = self._diff(ancestor, our)
if not our_diff:
return self._from_dict(their)
their_diff = self._diff(ancestor, their)
if not their_diff:
return self._from_dict(our)
# make sure there are no conflicting files
self._diff(our, their, allow_removed=True)
merged = patch(our_diff + their_diff, ancestor, in_place=True)
# Sorting the list by path to ensure reproducibility
return sorted(
self._from_dict(merged), key=itemgetter(self.tree.PARAM_RELPATH),
) )
def _get_dir_size(self, dir_info): def _get_dir_size(self, dir_info):
def _getsize(entry):
return self.tree.getsize(
self.tree.hash_to_path_info(entry[self.tree.PARAM_CHECKSUM])
)
try: try:
return sum(_getsize(entry) for entry in dir_info) return sum(
self.tree.getsize(self.tree.hash_to_path_info(hi.value))
for _, hi in dir_info.items()
)
except FileNotFoundError: except FileNotFoundError:
return None return None
@@ -704,12 +620,12 @@ class CloudCache:
if ancestor_info: if ancestor_info:
ancestor = self.get_dir_cache(ancestor_info) ancestor = self.get_dir_cache(ancestor_info)
else: else:
ancestor = [] ancestor = DirInfo()
our = self.get_dir_cache(our_info) our = self.get_dir_cache(our_info)
their = self.get_dir_cache(their_info) their = self.get_dir_cache(their_info)
merged = self._merge_dirs(ancestor, our, their) merged = our.merge(ancestor, their)
hash_info = self.save_dir_info(merged) hash_info = self.save_dir_info(merged)
hash_info.size = self._get_dir_size(merged) hash_info.size = self._get_dir_size(merged)
return hash_info return hash_info
@@ -728,5 +644,5 @@ class CloudCache:
def set_dir_info(self, hash_info): def set_dir_info(self, hash_info):
assert hash_info.isdir assert hash_info.isdir
hash_info.dir_info = self._to_dict(self.get_dir_cache(hash_info)) hash_info.dir_info = self.get_dir_cache(hash_info)
hash_info.nfiles = len(hash_info.dir_info) hash_info.nfiles = hash_info.dir_info.nfiles
+102
View File
@@ -0,0 +1,102 @@
import posixpath
from operator import itemgetter
from pygtrie import Trie
from .hash_info import HashInfo
def _diff(ancestor, other, allow_removed=False):
from dictdiffer import diff
from dvc.exceptions import MergeError
allowed = ["add"]
if allow_removed:
allowed.append("remove")
result = list(diff(ancestor, other))
for typ, _, _ in result:
if typ not in allowed:
raise MergeError(
"unable to auto-merge directories with diff that contains "
f"'{typ}'ed files"
)
return result
def _merge(ancestor, our, their):
import copy
from dictdiffer import patch
our_diff = _diff(ancestor, our)
if not our_diff:
return copy.deepcopy(their)
their_diff = _diff(ancestor, their)
if not their_diff:
return copy.deepcopy(our)
# make sure there are no conflicting files
_diff(our, their, allow_removed=True)
return patch(our_diff + their_diff, ancestor)
class DirInfo:
PARAM_RELPATH = "relpath"
def __init__(self):
self.trie = Trie()
@property
def size(self):
try:
return sum(
hash_info.size
for _, hash_info in self.trie.iteritems() # noqa: B301
)
except TypeError:
return None
@property
def nfiles(self):
return len(self.trie)
def items(self, path_info=None):
for key, hash_info in self.trie.iteritems(): # noqa: B301
path = posixpath.sep.join(key)
if path_info is not None:
path = path_info / path
yield path, hash_info
@classmethod
def from_list(cls, lst):
ret = DirInfo()
for _entry in lst:
entry = _entry.copy()
relpath = entry.pop(cls.PARAM_RELPATH)
parts = tuple(relpath.split(posixpath.sep))
ret.trie[parts] = HashInfo.from_dict(entry)
return ret
def to_list(self):
# Sorting the list by path to ensure reproducibility
return sorted(
(
{
# NOTE: not using hash_info.to_dict() because we don't want
# size/nfiles fields at this point.
hash_info.name: hash_info.value,
self.PARAM_RELPATH: posixpath.sep.join(parts),
}
for parts, hash_info in self.trie.iteritems() # noqa: B301
),
key=itemgetter(self.PARAM_RELPATH),
)
def merge(self, ancestor, their):
merged = DirInfo()
merged.trie = _merge(ancestor.trie, self.trie, their.trie)
return merged
+2 -4
View File
@@ -454,9 +454,7 @@ class BaseOutput:
path = str(self.path_info) path = str(self.path_info)
filter_path = str(filter_info) if filter_info else None filter_path = str(filter_info) if filter_info else None
is_win = os.name == "nt" is_win = os.name == "nt"
for entry in self.dir_cache: for entry_relpath, entry_hash_info in self.dir_cache.items():
checksum = entry[self.tree.PARAM_CHECKSUM]
entry_relpath = entry[self.tree.PARAM_RELPATH]
if is_win: if is_win:
entry_relpath = entry_relpath.replace("/", os.sep) entry_relpath = entry_relpath.replace("/", os.sep)
entry_path = os.path.join(path, entry_relpath) entry_path = os.path.join(path, entry_relpath)
@@ -465,7 +463,7 @@ class BaseOutput:
or entry_path == filter_path or entry_path == filter_path
or entry_path.startswith(filter_path + os.sep) or entry_path.startswith(filter_path + os.sep)
): ):
cache.add(self.scheme, checksum, entry_path) cache.add(self.scheme, entry_hash_info.value, entry_path)
return cache return cache
+15 -25
View File
@@ -7,6 +7,7 @@ from urllib.parse import urlparse
from funcy import cached_property, decorator from funcy import cached_property, decorator
from dvc.dir_info import DirInfo
from dvc.exceptions import DvcException, DvcIgnoreInCollectedDirError from dvc.exceptions import DvcException, DvcIgnoreInCollectedDirError
from dvc.ignore import DvcIgnore from dvc.ignore import DvcIgnore
from dvc.path_info import URLInfo from dvc.path_info import URLInfo
@@ -50,7 +51,6 @@ class BaseTree:
PATH_CLS = URLInfo PATH_CLS = URLInfo
JOBS = 4 * cpu_count() JOBS = 4 * cpu_count()
PARAM_RELPATH = "relpath"
CHECKSUM_DIR_SUFFIX = ".dir" CHECKSUM_DIR_SUFFIX = ".dir"
HASH_JOBS = max(1, min(4, cpu_count() // 2)) HASH_JOBS = max(1, min(4, cpu_count() // 2))
DEFAULT_VERIFY = False DEFAULT_VERIFY = False
@@ -318,35 +318,25 @@ class BaseTree:
new_hash_infos = self._calculate_hashes(not_in_state) new_hash_infos = self._calculate_hashes(not_in_state)
hash_infos.update(new_hash_infos) hash_infos.update(new_hash_infos)
sizes = [hi.size for hi in hash_infos.values()] dir_info = DirInfo()
if None in sizes: for fi, hi in hash_infos.items():
size = None # NOTE: this is lossy transformation:
else: # "hey\there" -> "hey/there"
size = sum(sizes) # "hey/there" -> "hey/there"
# The latter is fine filename on Windows, which
# will transform to dir/file on back transform.
#
# Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix
dir_info.trie[fi.relative_to(path_info).parts] = hi
dir_info = [ return dir_info
{
self.PARAM_CHECKSUM: hash_infos[fi].value,
# NOTE: this is lossy transformation:
# "hey\there" -> "hey/there"
# "hey/there" -> "hey/there"
# The latter is fine filename on Windows, which
# will transform to dir/file on back transform.
#
# Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix
self.PARAM_RELPATH: fi.relative_to(path_info).as_posix(),
}
for fi in file_infos
]
return (dir_info, size)
@use_state @use_state
def get_dir_hash(self, path_info, **kwargs): def get_dir_hash(self, path_info, **kwargs):
dir_info, size = self._collect_dir(path_info, **kwargs) dir_info = self._collect_dir(path_info, **kwargs)
hash_info = self.repo.cache.local.save_dir_info(dir_info) hash_info = self.repo.cache.local.save_dir_info(dir_info)
hash_info.size = size hash_info.size = dir_info.size
return hash_info return hash_info
def upload(self, from_info, to_info, name=None, no_progress_bar=False): def upload(self, from_info, to_info, name=None, no_progress_bar=False):
+5 -11
View File
@@ -32,7 +32,6 @@ class DvcTree(BaseTree): # pylint:disable=abstract-method
scheme = "local" scheme = "local"
PARAM_CHECKSUM = "md5" PARAM_CHECKSUM = "md5"
_cached_dir_cache = None
_dir_entry_hashes = {} _dir_entry_hashes = {}
def __init__(self, repo, fetch=False, stream=False): def __init__(self, repo, fetch=False, stream=False):
@@ -60,11 +59,9 @@ class DvcTree(BaseTree): # pylint:disable=abstract-method
raise FileNotFoundError raise FileNotFoundError
# NOTE: use string paths here for performance reasons # NOTE: use string paths here for performance reasons
path_str = relpath(path_info, out.path_info) key = tuple(relpath(path_info, out.path_info).split(os.sep))
if os.name == "nt":
path_str = path_str.replace(os.sep, "/")
out.get_dir_cache(remote=remote) out.get_dir_cache(remote=remote)
file_hash = out.hash_info.dir_info.get(path_str) file_hash = out.hash_info.dir_info.trie.get(key)
if file_hash: if file_hash:
return file_hash return file_hash
raise FileNotFoundError raise FileNotFoundError
@@ -166,12 +163,9 @@ class DvcTree(BaseTree): # pylint:disable=abstract-method
self._fetch_dir(out, filter_info=top, **kwargs) self._fetch_dir(out, filter_info=top, **kwargs)
for entry in out.dir_cache: base = out.path_info.parts
entry_relpath = entry[out.tree.PARAM_RELPATH] for key in out.dir_cache.trie.iterkeys(): # noqa: B301
if os.name == "nt": trie[base + key] = None
entry_relpath = entry_relpath.replace("/", os.sep)
path_info = out.path_info / entry_relpath
trie[path_info.parts] = None
def _walk(self, root, trie, topdown=True, **kwargs): def _walk(self, root, trie, topdown=True, **kwargs):
dirs = set() dirs = set()
+8 -4
View File
@@ -32,14 +32,16 @@ def remote(tmp_dir, dvc, tmp_path_factory, mocker):
def test_indexed_on_status(tmp_dir, dvc, tmp_path_factory, remote): def test_indexed_on_status(tmp_dir, dvc, tmp_path_factory, remote):
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 = bar.dir_cache[0] baz_hash = bar.dir_cache.trie.get(("baz",))
dvc.push() dvc.push()
with remote.index: with remote.index:
remote.index.clear() remote.index.clear()
dvc.status(cloud=True) dvc.status(cloud=True)
with remote.index: with remote.index:
assert {bar.hash_info.value, baz["md5"]} == set(remote.index.hashes()) assert {bar.hash_info.value, baz_hash.value} == set(
remote.index.hashes()
)
assert [bar.hash_info.value] == list(remote.index.dir_hashes()) assert [bar.hash_info.value] == list(remote.index.dir_hashes())
assert foo.hash_info.value not in remote.index.hashes() assert foo.hash_info.value not in remote.index.hashes()
@@ -47,11 +49,13 @@ def test_indexed_on_status(tmp_dir, dvc, tmp_path_factory, remote):
def test_indexed_on_push(tmp_dir, dvc, tmp_path_factory, remote): def test_indexed_on_push(tmp_dir, dvc, tmp_path_factory, remote):
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 = bar.dir_cache[0] baz_hash = bar.dir_cache.trie.get(("baz",))
dvc.push() dvc.push()
with remote.index: with remote.index:
assert {bar.hash_info.value, baz["md5"]} == set(remote.index.hashes()) assert {bar.hash_info.value, baz_hash.value} == set(
remote.index.hashes()
)
assert [bar.hash_info.value] == list(remote.index.dir_hashes()) assert [bar.hash_info.value] == list(remote.index.dir_hashes())
assert foo.hash_info.value not in remote.index.hashes() assert foo.hash_info.value not in remote.index.hashes()
+2 -2
View File
@@ -80,8 +80,8 @@ def test_add_directory(tmp_dir, dvc):
hash_info = stage.outs[0].hash_info hash_info = stage.outs[0].hash_info
dir_info = dvc.cache.local.load_dir_cache(hash_info) dir_info = dvc.cache.local.load_dir_cache(hash_info)
for info in dir_info: for path, _ in dir_info.trie.items():
assert "\\" not in info["relpath"] assert "\\" not in path
class TestAddDirectoryRecursive(TestDvc): class TestAddDirectoryRecursive(TestDvc):
+5 -2
View File
@@ -6,6 +6,7 @@ import pytest
from dvc.cache import Cache from dvc.cache import Cache
from dvc.cache.base import DirCacheError from dvc.cache.base import DirCacheError
from dvc.dir_info import DirInfo
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.main import main from dvc.main import main
from dvc.utils import relpath from dvc.utils import relpath
@@ -60,9 +61,11 @@ class TestCacheLoadBadDirCache(TestDvc):
self.dvc.cache.local.tree.hash_to_path_info(dir_hash) self.dvc.cache.local.tree.hash_to_path_info(dir_hash)
) )
self.create(fname, '{"a": "b"}') self.create(fname, '{"a": "b"}')
self._do_test( dir_info = self.dvc.cache.local.load_dir_cache(
self.dvc.cache.local.load_dir_cache(HashInfo("md5", dir_hash)) HashInfo("md5", dir_hash)
) )
self.assertTrue(isinstance(dir_info, DirInfo))
self.assertEqual(dir_info.nfiles, 0)
class TestExternalCacheDir(TestDvc): class TestExternalCacheDir(TestDvc):
+4 -3
View File
@@ -100,10 +100,11 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
# NOTE: modifying cache file for one of the files inside the directory # NOTE: modifying cache file for one of the files inside the directory
# to check if dvc will detect that the cache is corrupted. # to check if dvc will detect that the cache is corrupted.
entry = self.dvc.cache.local.load_dir_cache(out.hash_info)[0] _, entry_hash = next(
entry_hash = entry[self.dvc.cache.local.tree.PARAM_CHECKSUM] self.dvc.cache.local.load_dir_cache(out.hash_info).items()
)
cache = os.fspath( cache = os.fspath(
self.dvc.cache.local.tree.hash_to_path_info(entry_hash) self.dvc.cache.local.tree.hash_to_path_info(entry_hash.value)
) )
os.chmod(cache, 0o644) os.chmod(cache, 0o644)
+11 -13
View File
@@ -7,6 +7,7 @@ import pytest
from mock import patch from mock import patch
from dvc.config import Config from dvc.config import Config
from dvc.dir_info import DirInfo
from dvc.exceptions import DownloadError, RemoteCacheRequiredError, UploadError from dvc.exceptions import DownloadError, RemoteCacheRequiredError, UploadError
from dvc.main import main from dvc.main import main
from dvc.path_info import PathInfo from dvc.path_info import PathInfo
@@ -149,23 +150,20 @@ def test_dir_hash_should_be_key_order_agnostic(tmp_dir, dvc):
tmp_dir.gen({"data": {"1": "1 content", "2": "2 content"}}) tmp_dir.gen({"data": {"1": "1 content", "2": "2 content"}})
path_info = PathInfo("data") path_info = PathInfo("data")
dir_info = DirInfo.from_list(
[{"relpath": "1", "md5": "1"}, {"relpath": "2", "md5": "2"}]
)
with patch.object( with patch.object(
BaseTree, BaseTree, "_collect_dir", return_value=dir_info,
"_collect_dir",
return_value=(
[{"relpath": "1", "md5": "1"}, {"relpath": "2", "md5": "2"}],
None,
),
): ):
hash1 = dvc.cache.local.tree.get_hash(path_info) hash1 = dvc.cache.local.tree.get_hash(path_info)
dir_info = DirInfo.from_list(
[{"md5": "1", "relpath": "1"}, {"md5": "2", "relpath": "2"}]
)
with patch.object( with patch.object(
BaseTree, BaseTree, "_collect_dir", return_value=dir_info,
"_collect_dir",
return_value=(
[{"md5": "1", "relpath": "1"}, {"md5": "2", "relpath": "2"}],
None,
),
): ):
hash2 = dvc.cache.local.tree.get_hash(path_info) hash2 = dvc.cache.local.tree.get_hash(path_info)
@@ -277,7 +275,7 @@ def test_push_order(tmp_dir, dvc, tmp_path_factory, mocker, local_remote):
remote = dvc.cloud.get_remote("upstream") remote = dvc.cloud.get_remote("upstream")
foo_path = remote.tree.hash_to_path_info(foo.hash_info.value) foo_path = remote.tree.hash_to_path_info(foo.hash_info.value)
bar_path = remote.tree.hash_to_path_info( bar_path = remote.tree.hash_to_path_info(
foo.hash_info.dir_info["bar"].value foo.hash_info.dir_info.trie[("bar",)].value
) )
paths = [args[1] for args, _ in mocked_upload.call_args_list] paths = [args[1] for args, _ in mocked_upload.call_args_list]
assert paths.index(foo_path) > paths.index(bar_path) assert paths.index(foo_path) > paths.index(bar_path)
+3 -3
View File
@@ -198,9 +198,9 @@ def test_repotree_walk_fetch(tmp_dir, dvc, scm, local_remote):
pass pass
assert os.path.exists(out.cache_path) assert os.path.exists(out.cache_path)
for entry in out.dir_cache: for _, hi in out.dir_cache.items():
hash_ = entry[out.tree.PARAM_CHECKSUM] assert hi.name == out.tree.PARAM_CHECKSUM
assert os.path.exists(dvc.cache.local.tree.hash_to_path_info(hash_)) assert os.path.exists(dvc.cache.local.tree.hash_to_path_info(hi.value))
def test_repotree_cache_save(tmp_dir, dvc, scm, erepo_dir, local_cloud): def test_repotree_cache_save(tmp_dir, dvc, scm, erepo_dir, local_cloud):
+7 -1
View File
@@ -3,6 +3,7 @@ import os
from mock import patch from mock import patch
from dvc.cache.local import LocalCache from dvc.cache.local import LocalCache
from dvc.dir_info import DirInfo
from dvc.hash_info import HashInfo from dvc.hash_info import HashInfo
from dvc.output import LocalOutput from dvc.output import LocalOutput
from dvc.stage import Stage from dvc.stage import Stage
@@ -81,7 +82,12 @@ class TestGetFilesNumber(TestDvc):
@patch.object( @patch.object(
LocalCache, LocalCache,
"get_dir_cache", "get_dir_cache",
return_value=[{"md5": "asdf"}, {"md5": "qwe"}], return_value=DirInfo.from_list(
[
{"relpath": "foo", "md5": "asdf"},
{"relpath": "bar", "md5": "qwe"},
]
),
) )
def test_return_multiple_for_dir(self, _mock_get_dir_cache): def test_return_multiple_for_dir(self, _mock_get_dir_cache):
o = self._get_output() o = self._get_output()
+273
View File
@@ -0,0 +1,273 @@
from operator import itemgetter
import pytest
from pygtrie import Trie
from dvc.dir_info import DirInfo, _merge
from dvc.hash_info import HashInfo
from dvc.path_info import PosixPathInfo, WindowsPathInfo
@pytest.mark.parametrize(
"lst, trie_dict",
[
([], {}),
(
[
{"md5": "def", "relpath": "zzz"},
{"md5": "123", "relpath": "foo"},
{"md5": "abc", "relpath": "aaa"},
{"md5": "456", "relpath": "bar"},
],
{
("zzz",): HashInfo("md5", "def"),
("foo",): HashInfo("md5", "123"),
("bar",): HashInfo("md5", "456"),
("aaa",): HashInfo("md5", "abc"),
},
),
(
[
{"md5": "123", "relpath": "dir/b"},
{"md5": "456", "relpath": "dir/z"},
{"md5": "789", "relpath": "dir/a"},
{"md5": "abc", "relpath": "b"},
{"md5": "def", "relpath": "a"},
{"md5": "ghi", "relpath": "z"},
{"md5": "jkl", "relpath": "dir/subdir/b"},
{"md5": "mno", "relpath": "dir/subdir/z"},
{"md5": "pqr", "relpath": "dir/subdir/a"},
],
{
("dir", "b"): HashInfo("md5", "123"),
("dir", "z"): HashInfo("md5", "456"),
("dir", "a"): HashInfo("md5", "789"),
("b",): HashInfo("md5", "abc"),
("a",): HashInfo("md5", "def"),
("z",): HashInfo("md5", "ghi"),
("dir", "subdir", "b"): HashInfo("md5", "jkl"),
("dir", "subdir", "z"): HashInfo("md5", "mno"),
("dir", "subdir", "a"): HashInfo("md5", "pqr"),
},
),
],
)
def test_list(lst, trie_dict):
dir_info = DirInfo.from_list(lst)
assert dir_info.trie == Trie(trie_dict)
assert dir_info.to_list() == sorted(lst, key=itemgetter("relpath"))
@pytest.mark.parametrize(
"trie_dict, size",
[
({}, 0),
(
{
("a",): HashInfo("md5", "abc", size=1),
("b",): HashInfo("md5", "def", size=2),
("c",): HashInfo("md5", "ghi", size=3),
("dir", "foo"): HashInfo("md5", "jkl", size=4),
("dir", "bar"): HashInfo("md5", "mno", size=5),
("dir", "baz"): HashInfo("md5", "pqr", size=6),
},
21,
),
(
{
("a",): HashInfo("md5", "abc", size=1),
("b",): HashInfo("md5", "def", size=None),
},
None,
),
],
)
def test_size(trie_dict, size):
dir_info = DirInfo()
dir_info.trie = Trie(trie_dict)
assert dir_info.size == size
@pytest.mark.parametrize(
"trie_dict, nfiles",
[
({}, 0),
(
{
("a",): HashInfo("md5", "abc", size=1),
("b",): HashInfo("md5", "def", size=2),
("c",): HashInfo("md5", "ghi", size=3),
("dir", "foo"): HashInfo("md5", "jkl", size=4),
("dir", "bar"): HashInfo("md5", "mno", size=5),
("dir", "baz"): HashInfo("md5", "pqr", size=6),
},
6,
),
(
{
("a",): HashInfo("md5", "abc", size=1),
("b",): HashInfo("md5", "def", size=None),
},
2,
),
],
)
def test_nfiles(trie_dict, nfiles):
dir_info = DirInfo()
dir_info.trie = Trie(trie_dict)
assert dir_info.nfiles == nfiles
@pytest.mark.parametrize(
"trie_dict, items",
[
({}, []),
(
{
("a",): HashInfo("md5", "abc"),
("b",): HashInfo("md5", "def"),
("c",): HashInfo("md5", "ghi"),
("dir", "foo"): HashInfo("md5", "jkl"),
("dir", "bar"): HashInfo("md5", "mno"),
("dir", "baz"): HashInfo("md5", "pqr"),
("dir", "subdir", "1"): HashInfo("md5", "stu"),
("dir", "subdir", "2"): HashInfo("md5", "vwx"),
("dir", "subdir", "3"): HashInfo("md5", "yz"),
},
[
("a", HashInfo("md5", "abc")),
("b", HashInfo("md5", "def")),
("c", HashInfo("md5", "ghi")),
("dir/foo", HashInfo("md5", "jkl")),
("dir/bar", HashInfo("md5", "mno")),
("dir/baz", HashInfo("md5", "pqr")),
("dir/subdir/1", HashInfo("md5", "stu")),
("dir/subdir/2", HashInfo("md5", "vwx")),
("dir/subdir/3", HashInfo("md5", "yz")),
],
),
],
)
def test_items(trie_dict, items):
dir_info = DirInfo()
dir_info.trie = Trie(trie_dict)
assert list(dir_info.items()) == items
@pytest.mark.parametrize(
"path_info, trie_dict, items",
[
(PosixPathInfo(), {}, []),
(WindowsPathInfo(), {}, []),
(
PosixPathInfo("/some/path"),
{
("a",): HashInfo("md5", "abc"),
("dir", "foo"): HashInfo("md5", "jkl"),
("dir", "sub", "1"): HashInfo("md5", "stu"),
},
[
(PosixPathInfo("/some/path/a"), HashInfo("md5", "abc")),
(PosixPathInfo("/some/path/dir/foo"), HashInfo("md5", "jkl")),
(
PosixPathInfo("/some/path/dir/sub/1"),
HashInfo("md5", "stu"),
),
],
),
(
WindowsPathInfo("C:\\some\\path"),
{
("a",): HashInfo("md5", "abc"),
("dir", "foo"): HashInfo("md5", "jkl"),
("dir", "sub", "1"): HashInfo("md5", "stu"),
},
[
(WindowsPathInfo("C:\\some\\path\\a"), HashInfo("md5", "abc")),
(
WindowsPathInfo("C:\\some\\path\\dir\\foo"),
HashInfo("md5", "jkl"),
),
(
WindowsPathInfo("C:\\some\\path\\dir\\sub\\1"),
HashInfo("md5", "stu"),
),
],
),
],
)
def test_items_with_path(path_info, trie_dict, items):
dir_info = DirInfo()
dir_info.trie = Trie(trie_dict)
assert list(dir_info.items(path_info)) == items
@pytest.mark.parametrize(
"ancestor_dict, our_dict, their_dict, merged_dict",
[
({}, {}, {}, {}),
(
{("foo",): HashInfo("md5", "123")},
{
("foo",): HashInfo("md5", "123"),
("bar",): HashInfo("md5", "345"),
},
{
("foo",): HashInfo("md5", "123"),
("baz",): HashInfo("md5", "678"),
},
{
("foo",): HashInfo("md5", "123"),
("bar",): HashInfo("md5", "345"),
("baz",): HashInfo("md5", "678"),
},
),
(
{
("common",): HashInfo("md5", "123"),
("subdir", "foo"): HashInfo("md5", "345"),
},
{
("common",): HashInfo("md5", "123"),
("subdir", "foo"): HashInfo("md5", "345"),
("subdir", "bar"): HashInfo("md5", "678"),
},
{
("common",): HashInfo("md5", "123"),
("subdir", "foo"): HashInfo("md5", "345"),
("subdir", "baz"): HashInfo("md5", "91011"),
},
{
("common",): HashInfo("md5", "123"),
("subdir", "foo"): HashInfo("md5", "345"),
("subdir", "bar"): HashInfo("md5", "678"),
("subdir", "baz"): HashInfo("md5", "91011"),
},
),
(
{},
{("foo",): HashInfo("md5", "123")},
{("bar",): HashInfo("md5", "456")},
{
("foo",): HashInfo("md5", "123"),
("bar",): HashInfo("md5", "456"),
},
),
(
{},
{},
{("bar",): HashInfo("md5", "123")},
{("bar",): HashInfo("md5", "123")},
),
(
{},
{("bar",): HashInfo("md5", "123")},
{},
{("bar",): HashInfo("md5", "123")},
),
],
)
def test_merge(ancestor_dict, our_dict, their_dict, merged_dict):
actual = _merge(Trie(ancestor_dict), Trie(our_dict), Trie(their_dict))
expected = Trie(merged_dict)
assert actual == expected
+1 -1
View File
@@ -560,7 +560,7 @@ def test_get_hash_dirty_dir(tmp_dir, dvc):
actual = tree.get_hash(PathInfo(tmp_dir) / "dir") actual = tree.get_hash(PathInfo(tmp_dir) / "dir")
expected = HashInfo("md5", "ba75a2162ca9c29acecb7957105a0bc2.dir") expected = HashInfo("md5", "ba75a2162ca9c29acecb7957105a0bc2.dir")
assert actual == expected assert actual == expected
assert len(actual.dir_info) == 3 assert actual.dir_info.nfiles == 3
@pytest.mark.parametrize("traverse_subrepos", [True, False]) @pytest.mark.parametrize("traverse_subrepos", [True, False])