-
Notifications
You must be signed in to change notification settings - Fork 34
Infra: allow for multiple install_hooks() running at the same time #448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
ab21939
7079a61
d283174
b34e082
229f1b0
351181b
973a4e6
fb8dc04
033211b
56e089b
ae90a43
df087b2
3d63f50
2ef659c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,16 @@ | ||
| import os | ||
| from dataclasses import dataclass | ||
| from os import PathLike | ||
| from pathlib import Path | ||
| from typing import Optional, TYPE_CHECKING | ||
| from typing import Optional, TYPE_CHECKING, Union, Tuple | ||
|
|
||
| try: | ||
| from functools import cached_property | ||
| except ImportError: | ||
| from cached_property import cached_property | ||
|
|
||
| if TYPE_CHECKING: | ||
| from dagshub.streaming import DagsHubFilesystem | ||
| from filesystem import DagsHubFilesystem | ||
|
|
||
| storage_schemas = ["s3", "gs", "azure"] | ||
|
|
||
|
|
@@ -24,15 +26,35 @@ class DagshubPath: | |
| relative_path (Optional[Path]): Path relative to the root of the encapsulating FileSystem. | ||
| If None, path is outside the FS | ||
| original_path (Path): Original path as it was accessed by the user | ||
| is_binary_path_requested (bool): For functions like scandir and listdir that have | ||
| different behaviour whether user requested a string or a binary path | ||
| """ | ||
|
|
||
| # TODO: this couples this class hard to the fs, need to decouple later | ||
| fs: "DagsHubFilesystem" # Actual type is DagsHubFilesystem, but imports are wonky | ||
| absolute_path: Optional[Path] | ||
| relative_path: Optional[Path] | ||
| original_path: Optional[Path] | ||
| def __init__(self, fs: "DagsHubFilesystem", file_path: Union[str, bytes, PathLike, "DagshubPath"]): | ||
|
guysmoilov marked this conversation as resolved.
|
||
| self.fs = fs | ||
| self.is_binary_path_requested = isinstance(file_path, bytes) | ||
| self.absolute_path, self.relative_path, self.original_path = self.parse_path(file_path) | ||
|
|
||
| def __post_init__(self): | ||
| def parse_path(self, file_path: Union[str, bytes, PathLike, "DagshubPath"]) -> Tuple[Path, Optional[Path], Path]: | ||
| if isinstance(file_path, DagshubPath): | ||
| self.is_binary_path_requested = file_path.is_binary_path_requested | ||
| if file_path.fs != self.fs: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the fs is the same, then we're just creating a copy of the input?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
| relativized = DagshubPath(self.fs, file_path.absolute_path) | ||
| return relativized.absolute_path, relativized.relative_path, relativized.original_path | ||
| return file_path.absolute_path, file_path.relative_path, file_path.original_path | ||
| if isinstance(file_path, bytes): | ||
| file_path = os.fsdecode(file_path) | ||
| orig_path = Path(file_path) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't want to save the original bytes representation of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to have the og requested path as a Path object because it's more convenient to use.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked the usages, and it's probably alright to actually just carry over the original path, because I'm not working with it that much
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed it to carry over as-is |
||
| abspath = Path(os.path.abspath(file_path)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should always be relative to PWD ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate on the question? Didn't understand it |
||
| try: | ||
| relpath = abspath.relative_to(os.path.abspath(self.fs.project_root)) | ||
| if str(relpath).startswith("<"): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this condition mean?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly don't remember. Probably an old leftover, so maybe should delete it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a commit where it got added: 9174106
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's some IPython magic, then it's probably better to leave it, but could be useful to know how to trigger it at least 🤔 |
||
| return abspath, None, orig_path | ||
| return abspath, relpath, orig_path | ||
| except ValueError: | ||
| return abspath, None, orig_path | ||
|
|
||
| def handle_storages(self): | ||
| # Handle storage paths - translate s3:/bla-bla to .dagshub/storage/s3/bla-bla | ||
| if self.relative_path is not None: | ||
| str_path = self.relative_path.as_posix() | ||
|
|
@@ -41,9 +63,11 @@ def __post_init__(self): | |
| str_path = str_path[len(storage_schema) + 2 :] | ||
| self.relative_path = Path(".dagshub/storage") / storage_schema / str_path | ||
| self.absolute_path = self.fs.project_root / self.relative_path | ||
| break | ||
|
|
||
| @cached_property | ||
| def name(self): | ||
| assert self.absolute_path is not None | ||
| return self.absolute_path.name | ||
|
|
||
| @cached_property | ||
|
|
@@ -56,10 +80,11 @@ def is_storage_path(self): | |
| Is path a storage path (stored in a bucket) | ||
| Those paths are accessible via a path like `.dagshub/storage/s3/bucket/...` | ||
| """ | ||
| if self.relative_path is None: | ||
| return False | ||
| return self.relative_path.as_posix().startswith(".dagshub/storage") | ||
|
|
||
| @cached_property | ||
| def is_passthrough_path(self): | ||
| def is_passthrough_path(self, fs: "DagsHubFilesystem"): | ||
| """ | ||
| Is path a "passthrough" path | ||
| A passthrough path is a path that the FS ignores when trying to look up if the file exists on DagsHub | ||
|
|
@@ -68,17 +93,144 @@ def is_passthrough_path(self): | |
| If you need to read with streaming from a .dvc folder (to read config for example), please pull the repo | ||
| - Any /site-packages/ folder - if you have a venv in your repo, python will try to find packages there. | ||
| """ | ||
| if self.relative_path is None: | ||
| return True | ||
| str_path = self.relative_path.as_posix() | ||
| if "/site-packages/" in str_path or str_path.endswith("/site-packages"): | ||
| return True | ||
| if str_path.startswith((".git/", ".dvc/")) or str_path in (".git", ".dvc"): | ||
| return True | ||
| return any((self.relative_path.match(glob) for glob in self.fs.exclude_globs)) | ||
| return any((self.relative_path.match(glob) for glob in fs.exclude_globs)) | ||
|
|
||
| def __truediv__(self, other): | ||
| return DagshubPath( | ||
| absolute_path=self.absolute_path / other, | ||
| relative_path=self.relative_path / other, | ||
| original_path=self.original_path / other, | ||
| fs=self.fs, | ||
| new = DagshubPath( | ||
| self.fs, | ||
| self.original_path / other, | ||
| ) | ||
| new.is_binary_path_requested = self.is_binary_path_requested | ||
| return new | ||
|
|
||
|
|
||
| class DagshubScandirIterator: | ||
| def __init__(self, iterator): | ||
| self._iterator = iterator | ||
|
|
||
| def __iter__(self): | ||
| return self._iterator | ||
|
|
||
| def __next__(self): | ||
| return self._iterator.__next__() | ||
|
|
||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, *args): | ||
| return self | ||
|
|
||
|
|
||
| class DagshubStatResult: | ||
| def __init__( | ||
| self, fs: "DagsHubFilesystem", path: DagshubPath, is_directory: bool, custom_size: Optional[int] = None | ||
| ): | ||
| self._fs = fs | ||
| self._path = path | ||
| self._is_directory = is_directory | ||
| self._custom_size = custom_size | ||
| self._true_stat: Optional[os.stat_result] = None | ||
| assert not self._is_directory # TODO make folder stats lazy? | ||
|
|
||
| def __getattr__(self, name: str): | ||
| if not name.startswith("st_"): | ||
| raise AttributeError | ||
| if self._true_stat is not None: | ||
| return os.stat_result.__getattribute__(self._true_stat, name) | ||
| if name == "st_uid": | ||
| return os.getuid() | ||
| elif name == "st_gid": | ||
| return os.getgid() | ||
| elif name == "st_atime" or name == "st_mtime" or name == "st_ctime": | ||
| return 0 | ||
| elif name == "st_mode": | ||
| return 0o100644 | ||
| elif name == "st_size": | ||
| if self._custom_size is not None: | ||
| return self._custom_size | ||
| return 1100 # hardcoded size because size requests take a disproportionate amount of time | ||
| self._fs.open(self._path) | ||
| self._true_stat = self._fs.original_stat(self._path.absolute_path) | ||
| return os.stat_result.__getattribute__(self._true_stat, name) | ||
|
|
||
| def __repr__(self): | ||
| inner = repr(self._true_stat) if self._true_stat is not None else "pending..." | ||
| return f"dagshub_stat_result({inner}, path={self._path})" | ||
|
|
||
|
|
||
| class DagshubDirEntry: | ||
| def __init__(self, fs: "DagsHubFilesystem", path: DagshubPath, is_directory: bool = False, is_binary: bool = False): | ||
| self._fs = fs | ||
| self._path = path | ||
| self._is_directory = is_directory | ||
| self._is_binary = is_binary | ||
| self._true_direntry: Optional[os.DirEntry] = None | ||
|
|
||
| @property | ||
| def name(self): | ||
| if self._true_direntry is not None: | ||
| name = self._true_direntry.name | ||
| else: | ||
| name = self._path.name | ||
| return os.fsencode(name) if self._is_binary else name | ||
|
|
||
| @property | ||
| def path(self): | ||
| if self._true_direntry is not None: | ||
| path = self._true_direntry.path | ||
| else: | ||
| path = str(self._path.original_path) | ||
| return os.fsencode(path) if self._is_binary else path | ||
|
|
||
| def is_dir(self): | ||
| if self._true_direntry is not None: | ||
| return self._true_direntry.is_dir() | ||
| else: | ||
| return self._is_directory | ||
|
|
||
| def is_file(self): | ||
| if self._true_direntry is not None: | ||
| return self._true_direntry.is_file() | ||
| else: | ||
| # TODO: Symlinks should return false | ||
| return not self._is_directory | ||
|
|
||
| def stat(self): | ||
| if self._true_direntry is not None: | ||
| return self._true_direntry.stat() | ||
| else: | ||
| return self._fs.stat(self._path.original_path) | ||
|
|
||
| def __getattr__(self, name: str): | ||
| if name == "_true_direntry": | ||
| raise AttributeError | ||
| if self._true_direntry is not None: | ||
| return os.DirEntry.__getattribute__(self._true_direntry, name) | ||
|
|
||
| # Either create a dir, or download the file | ||
| if self._is_directory: | ||
| self._fs.mkdirs(self._path.absolute_path) | ||
| else: | ||
| self._fs.open(self._path.absolute_path) | ||
|
|
||
| for direntry in self._fs.original_stat(self._path.original_path): | ||
| if direntry.name == self._path.name: | ||
| self._true_direntry = direntry | ||
| return os.DirEntry.__getattribute__(self._true_direntry, name) | ||
| else: | ||
| raise FileNotFoundError | ||
|
|
||
| def __repr__(self): | ||
| cached = " (cached)" if self._true_direntry is not None else "" | ||
| return f"<dagshub_DirEntry '{self.name}'{cached}>" | ||
|
|
||
|
|
||
| PathType = Union[str, int, bytes, PathLike] | ||
| PathTypeWithDagshubPath = Union[PathType, DagshubPath] | ||
Uh oh!
There was an error while loading. Please reload this page.