api: open scm controlled files (#2566)

* tree: make open method use mode and encoding

* api: try to open using self.tree if output not found

* repo: rename OutputMissingException

* test: api: test adding not cached output

* api: open: exists test instead of try

* repo: open: try opening from tree first

* repo: open: handle missing and not cached files

* repo: open: separate find out logic from opening
This commit is contained in:
Paweł Redzyński
2019-10-08 15:23:42 +03:00
committed by Ruslan Kuprieiev
parent 156fc9d4e1
commit d2f8cb0520
5 changed files with 65 additions and 21 deletions
+5 -4
View File
@@ -259,10 +259,11 @@ class ETagMismatchError(DvcException):
)
class OutputFileMissingError(DvcException):
def __init__(self, path):
super(OutputFileMissingError, self).__init__(
"Can't find {} neither locally nor on remote".format(path)
class FileMissingError(DvcException):
def __init__(self, path, cause=None):
super(FileMissingError, self).__init__(
"Can't find '{}' neither locally nor on remote".format(path),
cause=cause,
)
+22 -7
View File
@@ -12,7 +12,7 @@ from dvc.config import Config
from dvc.exceptions import (
NotDvcRepoError,
OutputNotFoundError,
OutputFileMissingError,
FileMissingError,
)
from dvc.ignore import DvcIgnoreFilter
from dvc.path_info import PathInfo
@@ -454,14 +454,29 @@ class Repo(object):
@contextmanager
def open(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
cause = None
try:
with self._open(path, remote, mode, encoding) as fd:
yield fd
except FileNotFoundError:
raise OutputFileMissingError(relpath(path, self.root_dir))
out, = self.find_outs_by_path(path)
except OutputNotFoundError as e:
out = None
cause = e
def _open(self, path, remote=None, mode="r", encoding=None):
out, = self.find_outs_by_path(path)
if out and out.use_cache:
try:
with self._open_cached(out, remote, mode, encoding) as fd:
yield fd
return
except FileNotFoundError as e:
raise FileMissingError(relpath(path, self.root_dir), cause=e)
if self.tree.exists(path):
with self.tree.open(path, mode, encoding) as fd:
yield fd
return
raise FileMissingError(relpath(path, self.root_dir), cause=cause)
def _open_cached(self, out, remote=None, mode="r", encoding=None):
if out.isdir():
raise ValueError("Can't open a dir")
+4 -3
View File
@@ -45,7 +45,8 @@ class GitTree(BaseTree):
def tree_root(self):
return self.git.working_dir
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
assert mode in {"r", "rb"}
relative_path = relpath(path, self.git.working_dir)
@@ -61,9 +62,9 @@ class GitTree(BaseTree):
# the `open()` behavior (since data_stream.read() returns bytes,
# and `open` with default "r" mode returns str)
data = obj.data_stream.read()
if binary:
if mode == "rb":
return BytesIO(data)
return StringIO(data.decode("utf-8"))
return StringIO(data.decode(encoding))
def exists(self, path):
return self.git_object_by_path(path) is not None
+3 -5
View File
@@ -11,7 +11,7 @@ class BaseTree(object):
def tree_root(self):
pass
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
"""Open file and return a stream."""
def exists(self, path):
@@ -42,11 +42,9 @@ class WorkingTree(BaseTree):
def tree_root(self):
return self.repo_root
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
"""Open file and return a stream."""
if binary:
return open(path, "rb")
return open(path, encoding="utf-8")
return open(path, mode=mode, encoding=encoding)
def exists(self, path):
"""Test whether a path exists."""
+31 -2
View File
@@ -1,8 +1,10 @@
import os
import pytest
import shutil
from dvc import api
from dvc.exceptions import OutputFileMissingError
from dvc.exceptions import FileMissingError
from dvc.main import main
from dvc.path_info import URLInfo
from dvc.remote.config import RemoteConfig
@@ -134,7 +136,7 @@ def test_missing(repo_dir, dvc_repo, remote_url):
# Remove cache to make foo missing
shutil.rmtree(dvc_repo.cache.local.cache_dir)
with pytest.raises(OutputFileMissingError):
with pytest.raises(FileMissingError):
api.read(repo_dir.FOO)
@@ -143,3 +145,30 @@ def _set_remote_url_and_commit(repo, remote_url):
rconfig.modify("upstream", "url", remote_url)
repo.scm.add([repo.config.config_file])
repo.scm.commit("modify remote")
def test_open_scm_controlled(dvc_repo, repo_dir):
stage, = dvc_repo.add(repo_dir.FOO)
stage_content = open(stage.path, "r").read()
with api.open(stage.path) as fd:
assert fd.read() == stage_content
def test_open_not_cached(dvc_repo):
metric_file = "metric.txt"
metric_content = "0.6"
metric_code = "open('{}', 'w').write('{}')".format(
metric_file, metric_content
)
dvc_repo.run(
metrics_no_cache=[metric_file],
cmd=('python -c "{}"'.format(metric_code)),
)
with api.open(metric_file) as fd:
assert fd.read() == metric_content
os.remove(metric_file)
with pytest.raises(FileMissingError):
api.read(metric_file)