mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
repofs: use / as root_marker
This makes dvcfs/repofs much easier to work with and allows it to support isabs/abspath/etc correctly, as previously any full path (with root_marker "") would be impossible to distinguish from a relative one. This also detaches external workspaces from the main repo workspace in DvcFileSystem, which was necessary to be able to distinguish dvcfs abspaths from local external output paths. Note that currently we still have limited support for those (e.g. get/api supports them, but import doesn't) and this change keeps it that way.
This commit is contained in:
+2
-1
@@ -19,8 +19,9 @@ def get_url(path, repo=None, rev=None, remote=None):
|
|||||||
directory in the remote storage.
|
directory in the remote storage.
|
||||||
"""
|
"""
|
||||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||||
|
fs_path = _repo.repo_fs.from_os_path(path)
|
||||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||||
info = _repo.repo_fs.info(path)
|
info = _repo.repo_fs.info(fs_path)
|
||||||
|
|
||||||
dvc_info = info.get("dvc_info")
|
dvc_info = info.get("dvc_info")
|
||||||
if not dvc_info:
|
if not dvc_info:
|
||||||
|
|||||||
+4
-1
@@ -83,7 +83,10 @@ class FileSystem:
|
|||||||
def path(self):
|
def path(self):
|
||||||
from .path import Path
|
from .path import Path
|
||||||
|
|
||||||
return Path(self.sep)
|
def _getcwd():
|
||||||
|
return self.fs.root_marker
|
||||||
|
|
||||||
|
return Path(self.sep, getcwd=_getcwd)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _strip_protocol(cls, path: str) -> str:
|
def _strip_protocol(cls, path: str) -> str:
|
||||||
|
|||||||
+24
-13
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
@@ -22,31 +21,43 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
|||||||
repo: DVC repo.
|
repo: DVC repo.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
root_marker = "/"
|
||||||
|
|
||||||
|
def __init__(self, workspace=None, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.repo = kwargs["repo"]
|
self.repo = kwargs["repo"]
|
||||||
|
self.workspace = workspace or "repo"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def path(self):
|
||||||
|
from .path import Path
|
||||||
|
|
||||||
|
def _getcwd():
|
||||||
|
return self.root_marker
|
||||||
|
|
||||||
|
return Path(self.sep, getcwd=_getcwd)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _get_key(self, path):
|
def _get_key(self, path):
|
||||||
from dvc.fs.local import LocalFileSystem
|
if self.workspace != "repo":
|
||||||
|
from . import get_cloud_fs
|
||||||
|
|
||||||
from . import get_cloud_fs
|
cls, kwargs, fs_path = get_cloud_fs(None, url=path)
|
||||||
|
|
||||||
cls, kwargs, fs_path = get_cloud_fs(None, url=path)
|
|
||||||
|
|
||||||
if cls != LocalFileSystem or os.path.isabs(path):
|
|
||||||
fs = cls(**kwargs)
|
fs = cls(**kwargs)
|
||||||
return (cls.scheme, *fs.path.parts(fs_path))
|
return (self.workspace, *fs.path.parts(fs_path))
|
||||||
|
|
||||||
fs_key = "repo"
|
path = self.path.abspath(path)
|
||||||
key = path.split(self.sep)
|
if path == self.root_marker:
|
||||||
if key == ["."] or key == [""]:
|
return (self.workspace,)
|
||||||
|
|
||||||
|
key = self.path.relparts(path, self.root_marker)
|
||||||
|
if key == (".") or key == (""):
|
||||||
key = ()
|
key = ()
|
||||||
|
|
||||||
return (fs_key, *key)
|
return (self.workspace, *key)
|
||||||
|
|
||||||
def _get_fs_path(self, path: "AnyPath", remote=None):
|
def _get_fs_path(self, path: "AnyPath", remote=None):
|
||||||
from dvc.config import NoRemoteError
|
from dvc.config import NoRemoteError
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def path(self):
|
||||||
|
from .path import Path
|
||||||
|
|
||||||
|
return Path(self.sep, getcwd=os.getcwd)
|
||||||
|
|
||||||
@wrap_prop(threading.Lock())
|
@wrap_prop(threading.Lock())
|
||||||
@cached_property
|
@cached_property
|
||||||
def fs(self) -> "FsspecGitFileSystem":
|
def fs(self) -> "FsspecGitFileSystem":
|
||||||
|
|||||||
+5
-1
@@ -105,7 +105,11 @@ class Path:
|
|||||||
return self.isin_or_eq(left, right) or self.isin(right, left)
|
return self.isin_or_eq(left, right) or self.isin(right, left)
|
||||||
|
|
||||||
def relpath(self, path, start):
|
def relpath(self, path, start):
|
||||||
return self.flavour.relpath(path, start=start)
|
if not start:
|
||||||
|
start = "."
|
||||||
|
return self.flavour.relpath(
|
||||||
|
self.abspath(path), start=self.abspath(start)
|
||||||
|
)
|
||||||
|
|
||||||
def relparts(self, path, base):
|
def relparts(self, path, base):
|
||||||
return self.parts(self.relpath(path, base))
|
return self.parts(self.relpath(path, base))
|
||||||
|
|||||||
+15
-12
@@ -73,6 +73,8 @@ class _RepoFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
|||||||
kwargs: Additional keyword arguments passed to the `DvcFileSystem()`.
|
kwargs: Additional keyword arguments passed to the `DvcFileSystem()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
root_marker = "/"
|
||||||
|
|
||||||
PARAM_REPO_URL = "repo_url"
|
PARAM_REPO_URL = "repo_url"
|
||||||
PARAM_REPO_ROOT = "repo_root"
|
PARAM_REPO_ROOT = "repo_root"
|
||||||
PARAM_REV = "rev"
|
PARAM_REV = "rev"
|
||||||
@@ -103,7 +105,10 @@ class _RepoFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
|||||||
else:
|
else:
|
||||||
self.repo_factory = repo_factory
|
self.repo_factory = repo_factory
|
||||||
|
|
||||||
self.path = Path(self.sep)
|
def _getcwd():
|
||||||
|
return self.root_marker
|
||||||
|
|
||||||
|
self.path = Path(self.sep, getcwd=_getcwd)
|
||||||
self.repo = repo
|
self.repo = repo
|
||||||
self.hash_jobs = repo.fs.hash_jobs
|
self.hash_jobs = repo.fs.hash_jobs
|
||||||
self._traverse_subrepos = subrepos
|
self._traverse_subrepos = subrepos
|
||||||
@@ -258,17 +263,7 @@ class _RepoFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
|||||||
"""
|
"""
|
||||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||||
"""
|
"""
|
||||||
from dvc.utils import as_posix
|
parts = self.path.relparts(path, self.root_marker)
|
||||||
|
|
||||||
if os.path.isabs(path):
|
|
||||||
if self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
|
||||||
path = self.repo.fs.path.relpath(path, self.repo.root_dir)
|
|
||||||
else:
|
|
||||||
return None, None, self.repo.dvcfs, path
|
|
||||||
|
|
||||||
path = as_posix(path)
|
|
||||||
|
|
||||||
parts = self.path.parts(path)
|
|
||||||
if parts and parts[0] == os.curdir:
|
if parts and parts[0] == os.curdir:
|
||||||
parts = parts[1:]
|
parts = parts[1:]
|
||||||
|
|
||||||
@@ -461,3 +456,11 @@ class RepoFileSystem(FileSystem):
|
|||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
return self.fs.config
|
return self.fs.config
|
||||||
|
|
||||||
|
def from_os_path(self, path):
|
||||||
|
from dvc.utils import as_posix
|
||||||
|
|
||||||
|
if os.path.isabs(path):
|
||||||
|
path = os.path.relpath(path, self.repo.root_dir)
|
||||||
|
|
||||||
|
return as_posix(path)
|
||||||
|
|||||||
+12
-2
@@ -491,12 +491,22 @@ class Repo:
|
|||||||
@contextmanager
|
@contextmanager
|
||||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||||
"""Opens a specified resource as a file descriptor"""
|
"""Opens a specified resource as a file descriptor"""
|
||||||
|
from dvc.fs.dvc import DvcFileSystem
|
||||||
from dvc.fs.repo import RepoFileSystem
|
from dvc.fs.repo import RepoFileSystem
|
||||||
|
|
||||||
fs = RepoFileSystem(repo=self, subrepos=True)
|
if os.path.isabs(path):
|
||||||
|
fs = DvcFileSystem(repo=self, workspace="local")
|
||||||
|
fs_path = path
|
||||||
|
else:
|
||||||
|
fs = RepoFileSystem(repo=self, subrepos=True)
|
||||||
|
fs_path = fs.from_os_path(path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with fs.open(
|
with fs.open(
|
||||||
path, mode=mode, encoding=encoding, remote=remote
|
fs_path,
|
||||||
|
mode=mode,
|
||||||
|
encoding=encoding,
|
||||||
|
remote=remote,
|
||||||
) as fobj:
|
) as fobj:
|
||||||
yield fobj
|
yield fobj
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Tuple
|
from typing import TYPE_CHECKING, Callable, Iterable, List, Tuple
|
||||||
|
|
||||||
from dvc.types import AnyPath
|
from dvc.types import AnyPath
|
||||||
@@ -34,8 +33,8 @@ def _collect_paths(
|
|||||||
from dvc.fs.repo import RepoFileSystem
|
from dvc.fs.repo import RepoFileSystem
|
||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
fs_paths = [os.path.abspath(target) for target in targets]
|
|
||||||
fs = RepoFileSystem(repo=repo)
|
fs = RepoFileSystem(repo=repo)
|
||||||
|
fs_paths = [fs.from_os_path(target) for target in targets]
|
||||||
|
|
||||||
target_paths: StrPaths = []
|
target_paths: StrPaths = []
|
||||||
for fs_path in fs_paths:
|
for fs_path in fs_paths:
|
||||||
@@ -60,10 +59,11 @@ def _filter_duplicates(
|
|||||||
fs_res_paths = fs_paths
|
fs_res_paths = fs_paths
|
||||||
|
|
||||||
for out in outs:
|
for out in outs:
|
||||||
if out.fs_path in fs_paths:
|
fs_path = out.repo.repo_fs.from_os_path(out.fs_path)
|
||||||
|
if fs_path in fs_paths:
|
||||||
res_outs.append(out)
|
res_outs.append(out)
|
||||||
# MUTATING THE SAME LIST!!
|
# MUTATING THE SAME LIST!!
|
||||||
fs_res_paths.remove(out.fs_path)
|
fs_res_paths.remove(fs_path)
|
||||||
|
|
||||||
return res_outs, fs_res_paths
|
return res_outs, fs_res_paths
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -186,8 +186,9 @@ def _dir_output_paths(fs, fs_path, obj, targets=None):
|
|||||||
|
|
||||||
def _filter_missing(repo_fs, paths):
|
def _filter_missing(repo_fs, paths):
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
fs_path = repo_fs.from_os_path(path)
|
||||||
try:
|
try:
|
||||||
info = repo_fs.info(path)
|
info = repo_fs.info(fs_path)
|
||||||
dvc_info = info.get("dvc_info")
|
dvc_info = info.get("dvc_info")
|
||||||
if (
|
if (
|
||||||
dvc_info
|
dvc_info
|
||||||
|
|||||||
+14
-2
@@ -50,14 +50,26 @@ def get(url, path, out=None, rev=None, jobs=None):
|
|||||||
with external_repo(
|
with external_repo(
|
||||||
url=url, rev=rev, cache_dir=tmp_dir, cache_types=cache_types
|
url=url, rev=rev, cache_dir=tmp_dir, cache_types=cache_types
|
||||||
) as repo:
|
) as repo:
|
||||||
fs = repo.repo_fs
|
|
||||||
|
if os.path.isabs(path):
|
||||||
|
from dvc.fs.dvc import DvcFileSystem
|
||||||
|
|
||||||
|
fs = DvcFileSystem(repo=repo, workspace="local")
|
||||||
|
fs_path = path
|
||||||
|
else:
|
||||||
|
fs = repo.repo_fs
|
||||||
|
fs_path = fs.from_os_path(path)
|
||||||
|
|
||||||
with FsspecCallback.as_tqdm_callback(
|
with FsspecCallback.as_tqdm_callback(
|
||||||
total=-1,
|
total=-1,
|
||||||
desc=f"Downloading {fs.path.name(path)}",
|
desc=f"Downloading {fs.path.name(path)}",
|
||||||
unit="files",
|
unit="files",
|
||||||
) as cb:
|
) as cb:
|
||||||
fs.get(
|
fs.get(
|
||||||
path, os.path.abspath(out), batch_size=jobs, callback=cb
|
fs_path,
|
||||||
|
os.path.abspath(out),
|
||||||
|
batch_size=jobs,
|
||||||
|
callback=cb,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
remove(tmp_dir)
|
remove(tmp_dir)
|
||||||
|
|||||||
+3
-1
@@ -45,8 +45,10 @@ def ls(url, path=None, rev=None, recursive=None, dvc_only=False):
|
|||||||
|
|
||||||
|
|
||||||
def _ls(fs, path, recursive=None, dvc_only=False):
|
def _ls(fs, path, recursive=None, dvc_only=False):
|
||||||
|
fs_path = fs.from_os_path(path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fs_path = fs.info(path)["name"]
|
fs_path = fs.info(fs_path)["name"]
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ def _to_fs_paths(metrics: List[Output]) -> StrPaths:
|
|||||||
result = []
|
result = []
|
||||||
for out in metrics:
|
for out in metrics:
|
||||||
if out.metric:
|
if out.metric:
|
||||||
result.append(out.fs_path)
|
result.append(out.repo.repo_fs.from_os_path(out.fs_path))
|
||||||
elif out.live:
|
elif out.live:
|
||||||
fs_path = summary_fs_path(out)
|
fs_path = summary_fs_path(out)
|
||||||
if fs_path:
|
if fs_path:
|
||||||
result.append(fs_path)
|
result.append(out.repo.repo_fs.from_os_path(fs_path))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -78,12 +78,16 @@ def _read_metric(path, fs, rev, **kwargs):
|
|||||||
def _read_metrics(repo, metrics, rev, onerror=None):
|
def _read_metrics(repo, metrics, rev, onerror=None):
|
||||||
fs = RepoFileSystem(repo=repo)
|
fs = RepoFileSystem(repo=repo)
|
||||||
|
|
||||||
|
relpath = ""
|
||||||
|
if repo.root_dir != repo.fs.path.getcwd():
|
||||||
|
relpath = repo.fs.path.relpath(repo.root_dir, repo.fs.path.getcwd())
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
for metric in metrics:
|
for metric in metrics:
|
||||||
if not fs.isfile(metric):
|
if not fs.isfile(metric):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
res[os.path.relpath(metric, os.getcwd())] = _read_metric(
|
res[os.path.join(relpath, *fs.path.parts(metric))] = _read_metric(
|
||||||
metric, fs, rev, onerror=onerror
|
metric, fs, rev, onerror=onerror
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,10 @@ def _collect_plots(
|
|||||||
recursive=recursive,
|
recursive=recursive,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {plot.fs_path: _plot_props(plot) for plot in plots}
|
result = {
|
||||||
|
repo.repo_fs.from_os_path(plot.fs_path): _plot_props(plot)
|
||||||
|
for plot in plots
|
||||||
|
}
|
||||||
result.update({fs_path: {} for fs_path in fs_paths})
|
result.update({fs_path: {} for fs_path in fs_paths})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from dvc.utils.fs import remove
|
|||||||
[
|
[
|
||||||
("", ("repo",)),
|
("", ("repo",)),
|
||||||
(".", ("repo",)),
|
(".", ("repo",)),
|
||||||
|
("/", ("repo",)),
|
||||||
("foo", ("repo", "foo")),
|
("foo", ("repo", "foo")),
|
||||||
("dir/foo", ("repo", "dir", "foo")),
|
("dir/foo", ("repo", "dir", "foo")),
|
||||||
],
|
],
|
||||||
|
|||||||
+12
-12
@@ -474,16 +474,16 @@ def test_repo_fs_no_subrepos(tmp_dir, dvc, scm):
|
|||||||
dvc._reset()
|
dvc._reset()
|
||||||
fs = RepoFileSystem(repo=dvc)
|
fs = RepoFileSystem(repo=dvc)
|
||||||
expected = [
|
expected = [
|
||||||
".dvcignore",
|
"/.dvcignore",
|
||||||
".gitignore",
|
"/.gitignore",
|
||||||
"lorem",
|
"/lorem",
|
||||||
"lorem.dvc",
|
"/lorem.dvc",
|
||||||
"dir",
|
"/dir",
|
||||||
"dir/repo.txt",
|
"/dir/repo.txt",
|
||||||
]
|
]
|
||||||
|
|
||||||
actual = []
|
actual = []
|
||||||
for root, dirs, files in fs.walk("", dvcfiles=True):
|
for root, dirs, files in fs.walk("/", dvcfiles=True):
|
||||||
for entry in dirs + files:
|
for entry in dirs + files:
|
||||||
actual.append(posixpath.join(root, entry))
|
actual.append(posixpath.join(root, entry))
|
||||||
|
|
||||||
@@ -657,9 +657,9 @@ def test_walk_nested_subrepos(tmp_dir, dvc, scm, traverse_subrepos):
|
|||||||
|
|
||||||
if traverse_subrepos or repo_dir == tmp_dir:
|
if traverse_subrepos or repo_dir == tmp_dir:
|
||||||
repo_dir_path = (
|
repo_dir_path = (
|
||||||
repo_dir.relative_to(tmp_dir).as_posix()
|
"/" + repo_dir.relative_to(tmp_dir).as_posix()
|
||||||
if repo_dir != tmp_dir
|
if repo_dir != tmp_dir
|
||||||
else ""
|
else "/"
|
||||||
)
|
)
|
||||||
expected[repo_dir_path] = set(
|
expected[repo_dir_path] = set(
|
||||||
scm_files.keys() | dvc_files.keys() | extras
|
scm_files.keys() | dvc_files.keys() | extras
|
||||||
@@ -675,13 +675,13 @@ def test_walk_nested_subrepos(tmp_dir, dvc, scm, traverse_subrepos):
|
|||||||
|
|
||||||
if traverse_subrepos:
|
if traverse_subrepos:
|
||||||
# update subrepos
|
# update subrepos
|
||||||
expected[""].update(["subrepo1", "subrepo2"])
|
expected["/"].update(["subrepo1", "subrepo2"])
|
||||||
expected["subrepo1"].add("subrepo3")
|
expected["/subrepo1"].add("subrepo3")
|
||||||
|
|
||||||
actual = {}
|
actual = {}
|
||||||
fs = RepoFileSystem(repo=dvc)
|
fs = RepoFileSystem(repo=dvc)
|
||||||
for root, dirs, files in fs.walk(
|
for root, dirs, files in fs.walk(
|
||||||
"", ignore_subrepos=not traverse_subrepos
|
"/", ignore_subrepos=not traverse_subrepos
|
||||||
):
|
):
|
||||||
actual[root] = set(dirs + files)
|
actual[root] = set(dirs + files)
|
||||||
assert expected == actual
|
assert expected == actual
|
||||||
|
|||||||
@@ -137,10 +137,3 @@ def test_info_on_subrepos(make_tmp_dir, tmp_dir, dvc, scm, repo_fs):
|
|||||||
assert info["repo"].root_dir == str(
|
assert info["repo"].root_dir == str(
|
||||||
subrepo
|
subrepo
|
||||||
), f"repo root didn't match for {path}"
|
), f"repo root didn't match for {path}"
|
||||||
|
|
||||||
# supports external outputs on top-level DVC repo
|
|
||||||
external_dir = make_tmp_dir("external-output")
|
|
||||||
external_dir.gen("bar", "bar")
|
|
||||||
dvc.add(str(external_dir / "bar"), external=True)
|
|
||||||
info = repo_fs.info((external_dir / "bar").fs_path)
|
|
||||||
assert info["repo"].root_dir == str(tmp_dir)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user