dvc.objects: move hashing functions to dvc.objects

This commit moves `HashedStreamReader`/`get_hash_file`/`file_md5`
into `dvc.objects.hash`.

The `get_hash_file` has been renamed to `hash_file`.
This also copies `relpath` and `is_exec` to the `dvc.fs.utils`.
This commit is contained in:
Saugat Pachhai (सौगात)
2022-05-12 20:33:18 +03:00
committed by Ruslan Kuprieiev
parent 7a795f79db
commit 3f5757a0a5
25 changed files with 257 additions and 167 deletions
+5 -54
View File
@@ -9,10 +9,10 @@ from typing import TYPE_CHECKING, Dict, Optional, Tuple
from dvc.exceptions import DvcIgnoreInCollectedDirError
from dvc.ignore import DvcIgnore
from dvc.objects.file import HashFile
from dvc.objects.hash import hash_file
from dvc.objects.hash_info import HashInfo
from dvc.objects.meta import Meta
from dvc.progress import Tqdm
from dvc.utils import file_md5, is_exec
from .db.reference import ReferenceObjectDB
@@ -32,7 +32,8 @@ _STAGING_MEMFS_PATH = "dvc-staging"
def _upload_file(from_fs_path, fs, odb, upload_odb, callback=None):
from dvc.fs._callback import FsspecCallback
from dvc.utils import tmp_fname
from dvc.utils.stream import HashedStreamReader
from .stream import HashedStreamReader
fs_path = upload_odb.fs.path
tmp_info = fs_path.join(upload_odb.fs_path, tmp_fname())
@@ -52,59 +53,9 @@ def _upload_file(from_fs_path, fs, odb, upload_odb, callback=None):
return from_fs_path, meta, odb.get(stream.hash_info)
def _adapt_info(info, scheme):
if scheme == "s3" and "ETag" in info:
info["etag"] = info["ETag"].strip('"')
elif scheme == "gs" and "etag" in info:
import base64
info["etag"] = base64.b64decode(info["etag"]).hex()
elif scheme.startswith("http") and (
"ETag" in info or "Content-MD5" in info
):
info["checksum"] = info.get("ETag") or info.get("Content-MD5")
return info
def _get_file_hash(fs_path, fs, name):
info = _adapt_info(fs.info(fs_path), fs.scheme)
if name in info:
assert not info[name].endswith(".dir")
hash_value = info[name]
elif hasattr(fs, name):
func = getattr(fs, name)
hash_value = func(fs_path)
elif name == "md5":
hash_value = file_md5(fs_path, fs)
else:
raise NotImplementedError
meta = Meta(size=info["size"], isexec=is_exec(info.get("mode", 0)))
hash_info = HashInfo(name, hash_value)
return meta, hash_info
def get_file_hash(fs_path, fs, name, state=None):
if state:
meta, hash_info = state.get( # pylint: disable=assignment-from-none
fs_path, fs
)
if hash_info:
return meta, hash_info
meta, hash_info = _get_file_hash(fs_path, fs, name)
if state:
assert ".dir" not in hash_info.value
state.save(fs_path, fs, hash_info)
return meta, hash_info
def _stage_file(fs_path, fs, name, odb=None, upload_odb=None, dry_run=False):
state = odb.state if odb else None
meta, hash_info = get_file_hash(fs_path, fs, name, state=state)
meta, hash_info = hash_file(fs_path, fs, name, state=state)
if upload_odb and not dry_run:
assert odb and name == "md5"
return _upload_file(fs_path, fs, odb, upload_odb)
@@ -346,7 +297,7 @@ def _stage_external_tree_info(odb, tree, name):
odb.add(tree.fs_path, tree.fs, tree.hash_info)
raw = odb.get(tree.hash_info)
_, hash_info = get_file_hash(raw.fs_path, raw.fs, name, state=odb.state)
_, hash_info = hash_file(raw.fs_path, raw.fs, name, state=odb.state)
tree.fs_path = raw.fs_path
tree.fs = raw.fs
tree.hash_info.name = hash_info.name
+1 -1
View File
@@ -3,9 +3,9 @@ import io
from funcy import cached_property
from dvc.objects.hash import dos2unix
from dvc.objects.hash_info import HashInfo
from dvc.objects.istextfile import DEFAULT_CHUNK_SIZE, istextblock
from dvc.utils import dos2unix
class HashedStreamReader(io.IOBase):
+2 -3
View File
@@ -7,8 +7,7 @@ from funcy import cached_property
from dvc.objects.errors import ObjectFormatError
from dvc.objects.file import HashFile
from .stage import get_file_hash
from dvc.objects.hash import hash_file
if TYPE_CHECKING:
from dvc.objects.db import ObjectDB
@@ -67,7 +66,7 @@ class Tree(HashFile):
if hash_info:
self.hash_info = hash_info
else:
_, self.hash_info = get_file_hash(fs_path, memfs, "md5")
_, self.hash_info = hash_file(fs_path, memfs, "md5")
assert self.hash_info.value
self.hash_info.value += ".dir"
+14 -4
View File
@@ -1,12 +1,12 @@
from contextlib import ExitStack
from functools import wraps
from typing import IO, TYPE_CHECKING, Any, Dict, Optional, TypeVar, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, TypeVar, overload
import fsspec
from funcy import cached_property
if TYPE_CHECKING:
from typing import Callable
from typing import BinaryIO, Callable, TextIO, Union
from typing_extensions import ParamSpec
@@ -20,11 +20,21 @@ if TYPE_CHECKING:
class FsspecCallback(fsspec.Callback):
"""FsspecCallback usable as a context manager, and a few helper methods."""
def wrap_attr(self, fobj: IO, method: str = "read") -> IO:
@overload
def wrap_attr(self, fobj: "BinaryIO", method: str = "read") -> "BinaryIO":
...
@overload
def wrap_attr(self, fobj: "TextIO", method: str = "read") -> "TextIO":
...
def wrap_attr(
self, fobj: "Union[TextIO, BinaryIO]", method: str = "read"
) -> "Union[TextIO, BinaryIO]":
from tqdm.utils import CallbackIOWrapper
wrapped = CallbackIOWrapper(self.relative_update, fobj, method)
return cast(IO, wrapped)
return wrapped
def wrap_fn(self, fn: "Callable[_P, _R]") -> "Callable[_P, _R]":
@wraps(fn)
+24 -4
View File
@@ -25,6 +25,8 @@ from dvc.utils.threadpool import ThreadPoolExecutor
from ._callback import DEFAULT_CALLBACK, FsspecCallback
if TYPE_CHECKING:
from typing import BinaryIO, TextIO
from fsspec.spec import AbstractFileSystem
from typing_extensions import Literal
@@ -179,12 +181,30 @@ class FileSystem:
return not self.fs.ls(path)
return entry["size"] == 0
@overload
def open(
self,
path: AnyFSPath,
mode: "Literal['rb', 'br', 'wb']",
**kwargs: Any,
) -> "BinaryIO": # pylint: disable=arguments-differ
return self.open(path, mode, **kwargs)
@overload
def open(
self,
path: AnyFSPath,
mode: "Literal['r', 'rt', 'w']",
**kwargs: Any,
) -> "TextIO": # pylint: disable=arguments-differ
...
def open(
self,
path: AnyFSPath,
mode: str = "r",
**kwargs,
) -> "IO": # pylint: disable=arguments-differ
**kwargs: Any,
) -> "IO[Any]": # pylint: disable=arguments-differ
if "b" in mode:
kwargs.pop("encoding", None)
return self.fs.open(path, mode=mode, **kwargs)
@@ -354,7 +374,7 @@ class FileSystem:
def put_file(
self,
from_file: Union[AnyFSPath, IO],
from_file: Union[AnyFSPath, "BinaryIO"],
to_info: AnyFSPath,
callback: FsspecCallback = DEFAULT_CALLBACK,
size: int = None,
@@ -363,7 +383,7 @@ class FileSystem:
if size:
callback.set_size(size)
if hasattr(from_file, "read"):
stream = callback.wrap_attr(cast("IO", from_file))
stream = callback.wrap_attr(cast("BinaryIO", from_file))
self.upload_fobj(stream, to_info, size=size)
else:
assert isinstance(from_file, str)
+1 -1
View File
@@ -41,7 +41,7 @@ def _ls(fs, path):
def _merge_info(repo, fs_info, dvc_info):
from dvc.utils import is_exec
from dvc.fs.utils import is_exec
ret = {"repo": repo}
+2 -2
View File
@@ -1,5 +1,5 @@
import threading
from typing import IO, Union
from typing import BinaryIO, Union
from funcy import cached_property, memoize, wrap_with
@@ -141,7 +141,7 @@ class HTTPFileSystem(FileSystem):
def put_file(
self,
from_file: Union[AnyFSPath, IO],
from_file: Union[AnyFSPath, BinaryIO],
to_info: AnyFSPath,
callback: FsspecCallback = DEFAULT_CALLBACK,
size: int = None,
+31
View File
@@ -0,0 +1,31 @@
import os
import stat
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .base import AnyFSPath
def is_exec(mode: int) -> bool:
return bool(mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
def relpath(path: "AnyFSPath", start: "AnyFSPath" = os.curdir) -> "AnyFSPath":
path = os.fspath(path)
start = os.path.abspath(os.fspath(start))
# Windows path on different drive than curdir doesn't have relpath
if os.name == "nt":
# Since python 3.8 os.realpath resolves network shares to their UNC
# path. So, to be certain that relative paths correctly captured,
# we need to resolve to UNC path first. We resolve only the drive
# name so that we don't follow any 'real' symlinks on the path
def resolve_network_drive_windows(path_to_resolve):
drive, tail = os.path.splitdrive(path_to_resolve)
return os.path.join(os.path.realpath(drive), tail)
path = resolve_network_drive_windows(os.path.abspath(path))
start = resolve_network_drive_windows(start)
if not os.path.commonprefix([start, path]):
return path
return os.path.relpath(path, start)
+1 -1
View File
@@ -57,5 +57,5 @@ class Cache(diskcache.Cache):
) -> None:
settings.setdefault("disk_pickle_protocol", 4)
super().__init__(
directory=directory, timeout=timeout, disk=Disk, **settings
directory=directory, timeout=timeout, disk=disk, **settings
)
+3 -3
View File
@@ -65,10 +65,10 @@ class HashFile:
self._check_hash(odb)
def _check_hash(self, odb):
from dvc.data.stage import get_file_hash
from dvc.objects.errors import ObjectFormatError
from .errors import ObjectFormatError
from .hash import hash_file
_, actual = get_file_hash(
_, actual = hash_file(
self.fs_path, self.fs, self.hash_info.name, odb.state
)
+136
View File
@@ -0,0 +1,136 @@
import hashlib
import logging
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Tuple
from dvc.fs._callback import DEFAULT_CALLBACK, FsspecCallback, TqdmCallback
from dvc.fs.utils import is_exec
from .hash_info import HashInfo
from .istextfile import istextfile
from .meta import Meta
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from dvc.fs.base import AnyFSPath, FileSystem
from .state import StateBase
def _adapt_info(info: Dict[str, Any], scheme: str) -> Dict[str, Any]:
if scheme == "s3" and "ETag" in info:
info["etag"] = info["ETag"].strip('"')
elif scheme == "gs" and "etag" in info:
import base64
info["etag"] = base64.b64decode(info["etag"]).hex()
elif scheme.startswith("http") and (
"ETag" in info or "Content-MD5" in info
):
info["checksum"] = info.get("ETag") or info.get("Content-MD5")
return info
def dos2unix(data: bytes) -> bytes:
return data.replace(b"\r\n", b"\n")
def _fobj_md5(
fobj: BinaryIO,
hash_md5: "hashlib._Hash",
binary: bool,
chunk_size: int = 2**20,
) -> None:
while True:
data = fobj.read(chunk_size)
if not data:
break
chunk = data if binary else dos2unix(data)
hash_md5.update(chunk)
def file_md5(
fname: "AnyFSPath",
fs: "FileSystem",
callback: "FsspecCallback" = DEFAULT_CALLBACK,
) -> str:
"""get the (md5 hexdigest, md5 digest) of a file"""
hash_md5 = hashlib.md5()
binary = not istextfile(fname, fs=fs)
size = fs.size(fname) or 0
callback.set_size(size)
with fs.open(fname, "rb") as fobj:
_fobj_md5(callback.wrap_attr(fobj), hash_md5, binary)
return hash_md5.hexdigest()
def _hash_file(
fs_path: "AnyFSPath",
fs: "FileSystem",
name: str,
callback: "FsspecCallback" = DEFAULT_CALLBACK,
) -> Tuple["str", Dict[str, Any]]:
info = _adapt_info(fs.info(fs_path), fs.scheme)
if name in info:
assert not info[name].endswith(".dir")
return info[name], info
if hasattr(fs, name):
func = getattr(fs, name)
return func(fs_path), info
if name == "md5":
return file_md5(fs_path, fs, callback=callback), info
raise NotImplementedError
class LargeFileHashingCallback(TqdmCallback):
"""Callback that only shows progress bar if self.size > LARGE_FILE_SIZE."""
LARGE_FILE_SIZE = 2**30
def __init__(self, *args, **kwargs):
kwargs.setdefault("bytes", True)
super().__init__(*args, **kwargs)
self._logged = False
# TqdmCallback force renders progress bar on `set_size`.
set_size = FsspecCallback.set_size
def call(self, hook_name=None, **kwargs):
if self.size and self.size > self.LARGE_FILE_SIZE:
if not self._logged:
desc = self.progress_bar.desc
logger.info(
f"Computing md5 for a large file '{desc}'. "
"This is only done once."
)
self._logged = True
super().call()
def hash_file(
fs_path: "AnyFSPath",
fs: "FileSystem",
name: str,
state: "StateBase" = None,
callback: "FsspecCallback" = None,
) -> Tuple["Meta", "HashInfo"]:
if state:
meta, hash_info = state.get(fs_path, fs)
if hash_info:
return meta, hash_info
cb = callback or LargeFileHashingCallback(desc=fs_path)
with cb:
hash_value, info = _hash_file(fs_path, fs, name, callback=cb)
hash_info = HashInfo(name, hash_value)
if state:
assert ".dir" not in hash_info.value
state.save(fs_path, fs, hash_info)
meta = Meta(size=info["size"], isexec=is_exec(info.get("mode", 0)))
return meta, hash_info
+1 -1
View File
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
from dvc.fs.local import LocalFileSystem
from dvc.fs.system import inode as get_inode
from dvc.utils import relpath
from dvc.fs.utils import relpath
from .hash_info import HashInfo
from .utils import get_mtime_and_size
+1 -59
View File
@@ -6,7 +6,6 @@ import logging
import math
import os
import re
import stat
import sys
import time
from typing import Dict, List, Optional, Tuple
@@ -15,62 +14,10 @@ import colorama
logger = logging.getLogger(__name__)
LOCAL_CHUNK_SIZE = 2**20 # 1 MB
LARGE_FILE_SIZE = 2**30 # 1 GB
LARGE_DIR_SIZE = 100
TARGET_REGEX = re.compile(r"(?P<path>.*?)(:(?P<name>[^\\/:]*))??$")
def dos2unix(data):
return data.replace(b"\r\n", b"\n")
def _fobj_md5(fobj, hash_md5, binary, progress_func=None):
while True:
data = fobj.read(LOCAL_CHUNK_SIZE)
if not data:
break
if binary:
chunk = data
else:
chunk = dos2unix(data)
hash_md5.update(chunk)
if progress_func:
progress_func(len(data))
def file_md5(fname, fs):
"""get the (md5 hexdigest, md5 digest) of a file"""
from dvc.objects.istextfile import istextfile
from dvc.progress import Tqdm
hash_md5 = hashlib.md5()
binary = not istextfile(fname, fs=fs)
size = fs.size(fname) or 0
no_progress_bar = True
if size >= LARGE_FILE_SIZE:
no_progress_bar = False
msg = (
f"Computing md5 for a large file '{fname}'. "
"This is only done once."
)
logger.info(msg)
with Tqdm(
desc=str(fname),
disable=no_progress_bar,
total=size,
bytes=True,
leave=False,
) as pbar:
with fs.open(fname, "rb") as fobj:
_fobj_md5(fobj, hash_md5, binary, pbar.update)
return hash_md5.hexdigest()
def bytes_hash(byts, typ):
hasher = getattr(hashlib, typ)()
hasher.update(byts)
@@ -370,7 +317,6 @@ def resolve_output(inp, out):
def resolve_paths(repo, out, always_local=False):
from urllib.parse import urlparse
from dvc.fs import system
from dvc.fs.local import localfs
from ..dvcfile import DVC_FILE_SUFFIX
@@ -390,7 +336,7 @@ def resolve_paths(repo, out, always_local=False):
if scheme or not localfs.path.isin_or_eq(abspath, repo.root_dir):
wdir = os.getcwd()
elif contains_symlink_up_to(dirname, repo.root_dir) or (
os.path.isdir(abspath) and system.is_symlink(abspath)
os.path.isdir(abspath) and localfs.is_symlink(abspath)
):
msg = (
"Cannot add files inside symlinked directories to DVC. "
@@ -471,10 +417,6 @@ def parse_target(
return path or default, name
def is_exec(mode):
return bool(mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
def glob_targets(targets, glob=True, recursive=True):
from ..exceptions import DvcException
+16
View File
@@ -0,0 +1,16 @@
from dvc.fs import LocalFileSystem
from dvc.objects.hash import file_md5
def test_file_md5(tmp_dir):
tmp_dir.gen("foo", "foo content")
fs = LocalFileSystem()
assert file_md5("foo", fs) == file_md5("foo", fs)
def test_file_md5_crlf(tmp_dir):
fs = LocalFileSystem()
tmp_dir.gen("cr", b"a\nb\nc")
tmp_dir.gen("crlf", b"a\r\nb\r\nc")
assert file_md5("cr", fs) == file_md5("crlf", fs)
+4 -3
View File
@@ -23,6 +23,7 @@ from dvc.exceptions import (
)
from dvc.fs import system
from dvc.fs.local import LocalFileSystem
from dvc.objects.hash import file_md5
from dvc.objects.hash_info import HashInfo
from dvc.output import (
OutputAlreadyTrackedError,
@@ -35,7 +36,7 @@ from dvc.stage.exceptions import (
StagePathNotFoundError,
)
from dvc.testing.test_workspace import TestAdd
from dvc.utils import LARGE_DIR_SIZE, file_md5, relpath
from dvc.utils import LARGE_DIR_SIZE, relpath
from dvc.utils.fs import path_isin
from dvc.utils.serialize import YAMLFileCorruptedError, load_yaml
from tests.basic_env import TestDvc
@@ -378,7 +379,7 @@ class TestDoubleAddUnchanged(TestDvc):
def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
file_md5_counter = mocker.spy(dvc_module.data.stage, "file_md5")
file_md5_counter = mocker.spy(dvc_module.objects.hash, "file_md5")
tmp_dir.gen("foo", "foo")
ret = main(["config", "cache.type", "copy"])
@@ -409,7 +410,7 @@ def test_should_update_state_entry_for_file_after_add(mocker, dvc, tmp_dir):
def test_should_update_state_entry_for_directory_after_add(
mocker, dvc, tmp_dir
):
file_md5_counter = mocker.spy(dvc_module.data.stage, "file_md5")
file_md5_counter = mocker.spy(dvc_module.objects.hash, "file_md5")
tmp_dir.gen({"data/data": "foo", "data/data_sub/sub_data": "foo"})
+2 -2
View File
@@ -149,7 +149,7 @@ def test_warn_on_outdated_stage(tmp_dir, dvc, local_remote, caplog):
def test_hash_recalculation(mocker, dvc, tmp_dir, local_remote):
tmp_dir.gen({"foo": "foo"})
test_file_md5 = mocker.spy(dvc_module.data.stage, "file_md5")
test_file_md5 = mocker.spy(dvc_module.objects.hash, "file_md5")
ret = main(["config", "cache.type", "hardlink"])
assert ret == 0
ret = main(["add", "foo"])
@@ -211,7 +211,7 @@ def test_verify_hashes(
remove("dir")
remove(dvc.odb.local.cache_dir)
hash_spy = mocker.spy(dvc_module.data.stage, "file_md5")
hash_spy = mocker.spy(dvc_module.objects.hash, "file_md5")
dvc.pull()
assert hash_spy.call_count == 0
+1 -1
View File
@@ -6,7 +6,7 @@ import pytest
from git import GitCommandError
from dvc.exceptions import DvcException
from dvc.utils import file_md5
from dvc.objects.hash import file_md5
from tests.func.parsing.test_errors import escape_ansi
+2 -1
View File
@@ -16,10 +16,11 @@ from dvc.exceptions import (
)
from dvc.fs import system
from dvc.fs.local import LocalFileSystem
from dvc.objects.hash import file_md5
from dvc.output import Output
from dvc.stage import Stage
from dvc.stage.exceptions import StageFileDoesNotExistError
from dvc.utils import file_md5, relpath
from dvc.utils import relpath
from dvc.utils.fs import remove
from dvc.utils.serialize import dump_yaml, load_yaml
from tests.basic_env import TestDvc
+1 -1
View File
@@ -20,6 +20,7 @@ from dvc.exceptions import (
StagePathAsOutputError,
)
from dvc.fs import system
from dvc.objects.hash import file_md5
from dvc.output import Output, OutputIsStageFileError
from dvc.repo import Repo as DvcRepo
from dvc.stage import Stage
@@ -30,7 +31,6 @@ from dvc.stage.exceptions import (
StagePathNotFoundError,
StagePathOutsideError,
)
from dvc.utils import file_md5
from dvc.utils.serialize import load_yaml
from tests.basic_env import TestDvc, TestDvcGit
+1 -1
View File
@@ -1,10 +1,10 @@
import os
import re
from dvc.objects.hash import file_md5
from dvc.objects.hash_info import HashInfo
from dvc.objects.state import State
from dvc.repo import Repo
from dvc.utils import file_md5
def test_state(tmp_dir, dvc):
-8
View File
@@ -4,14 +4,6 @@ import pytest
from dvc import utils
from dvc.exceptions import DvcException
from dvc.fs.local import LocalFileSystem
def test_file_md5_crlf(tmp_dir):
fs = LocalFileSystem()
tmp_dir.gen("cr", b"a\nb\nc")
tmp_dir.gen("crlf", b"a\r\nb\r\nc")
assert utils.file_md5("cr", fs) == utils.file_md5("crlf", fs)
def test_dict_md5():
+2 -2
View File
@@ -232,9 +232,9 @@ def test_get_hash_dir(tmp_dir, dvc, mocker):
{"dir": {"foo": "foo", "bar": "bar", "subdir": {"data": "data"}}}
)
fs = DataFileSystem(repo=dvc)
get_file_hash_spy = mocker.spy(dvc_module.data.stage, "get_file_hash")
hash_file_spy = mocker.spy(dvc_module.objects.hash, "hash_file")
assert fs.info("dir")["md5"] == "8761c4e9acad696bee718615e23e22db.dir"
assert not get_file_hash_spy.called
assert not hash_file_spy.called
def test_get_hash_granular(tmp_dir, dvc):
+4 -4
View File
@@ -576,8 +576,8 @@ def test_get_hash_mixed_dir(tmp_dir, scm, dvc):
def test_get_hash_dirty_file(tmp_dir, dvc):
from dvc.data import check
from dvc.data.stage import get_file_hash
from dvc.objects.errors import ObjectFormatError
from dvc.objects.hash import hash_file
tmp_dir.dvc_gen("file", "file")
file_hash_info = HashInfo("md5", "8c7dd922ad47494fc02c388e12c00eac")
@@ -588,7 +588,7 @@ def test_get_hash_dirty_file(tmp_dir, dvc):
clean_staging()
# file is modified in workspace
# get_file_hash(file) should return workspace hash, not DVC cached hash
# hash_file(file) should return workspace hash, not DVC cached hash
fs = DvcFileSystem(repo=dvc)
assert fs.info("file").get("md5") is None
staging, _, obj = stage(dvc.odb.local, "file", fs, "md5")
@@ -601,9 +601,9 @@ def test_get_hash_dirty_file(tmp_dir, dvc):
with pytest.raises(ObjectFormatError):
check(staging, obj)
# get_file_hash(file) should return DVC cached hash
# hash_file(file) should return DVC cached hash
assert fs.info("file")["md5"] == file_hash_info.value
_, hash_info = get_file_hash("file", fs, "md5", state=dvc.state)
_, hash_info = hash_file("file", fs, "md5", state=dvc.state)
assert hash_info == file_hash_info
# tmp_dir/file can be staged even though it is missing in workspace since
+2 -2
View File
@@ -1,9 +1,9 @@
import pytest
from dvc.data.stream import HashedStreamReader
from dvc.fs.local import LocalFileSystem
from dvc.objects.hash import file_md5
from dvc.objects.istextfile import DEFAULT_CHUNK_SIZE, istextfile
from dvc.utils import file_md5
from dvc.utils.stream import HashedStreamReader
def test_hashed_stream_reader(tmp_dir):
-9
View File
@@ -3,10 +3,8 @@ import re
import pytest
from dvc.fs.local import LocalFileSystem
from dvc.utils import (
dict_sha256,
file_md5,
fix_env,
parse_target,
relpath,
@@ -83,13 +81,6 @@ def test_fix_env_pyenv(path, orig):
assert fix_env(env)["PATH"] == orig
def test_file_md5(tmp_dir):
tmp_dir.gen("foo", "foo content")
fs = LocalFileSystem()
assert file_md5("foo", fs) == file_md5("foo", fs)
def test_tmp_fname():
file_path = os.path.join("path", "to", "file")