Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## v0.14.0 (Unreleased)

- Changed to pyproject.toml-based build.
- Changed type hints from custom type variable `DerivedCloudPath` to [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) ([PEP 673](https://docs.python.org/3/library/typing.html#typing.Self)). This adds a dependency on the [typing-extensions](https://pypi.org/project/typing-extensions/) backport package from Python versions lower than 3.11.

## v0.13.0 (2023-02-15)

Expand Down
75 changes: 37 additions & 38 deletions cloudpathlib/cloudpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_PathParents,
)
import shutil
import sys
from typing import (
overload,
Any,
Expand All @@ -34,6 +35,11 @@
from urllib.parse import urlparse
from warnings import warn

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self

from cloudpathlib.enums import FileCacheMode

from . import anypath
Expand Down Expand Up @@ -152,9 +158,6 @@ def __init__(cls, name: str, bases: Tuple[type, ...], dic: Dict[str, Any]) -> No
getattr(cls, attr).fget.__doc__ = docstring


DerivedCloudPath = TypeVar("DerivedCloudPath", bound="CloudPath")


# Abstract base class
class CloudPath(metaclass=CloudPathMeta):
"""Base class for cloud storage file URIs, in the style of the Python standard library's
Expand All @@ -175,8 +178,8 @@ class CloudPath(metaclass=CloudPathMeta):
cloud_prefix: str

def __init__(
self: DerivedCloudPath,
cloud_path: Union[str, DerivedCloudPath],
self,
cloud_path: Union[str, Self],
client: Optional["Client"] = None,
) -> None:
# handle if local file gets opened. must be set at the top of the method in case any code
Expand Down Expand Up @@ -372,9 +375,7 @@ def _glob_checks(self, pattern: str) -> None:
".glob is only supported within a bucket or container; you can use `.iterdir` to list buckets; for example, CloudPath('s3://').iterdir()"
)

def _glob(
self: DerivedCloudPath, selector, recursive: bool
) -> Generator[DerivedCloudPath, None, None]:
def _glob(self, selector, recursive: bool) -> Generator[Self, None, None]:
# build a tree structure for all files out of default dicts
Tree: Callable = lambda: defaultdict(Tree)

Expand Down Expand Up @@ -413,7 +414,7 @@ def _build_tree(trunk, branch, nodes, is_dir):
# select_from returns self.name/... so strip before joining
yield (self / str(p)[len(self.name) + 1 :])

def glob(self: DerivedCloudPath, pattern: str) -> Generator[DerivedCloudPath, None, None]:
def glob(self, pattern: str) -> Generator[Self, None, None]:
self._glob_checks(pattern)

pattern_parts = PurePosixPath(pattern).parts
Expand All @@ -426,15 +427,15 @@ def glob(self: DerivedCloudPath, pattern: str) -> Generator[DerivedCloudPath, No
in pattern, # recursive listing needed if explicit ** or any sub folder in pattern
)

def rglob(self: DerivedCloudPath, pattern: str) -> Generator[DerivedCloudPath, None, None]:
def rglob(self, pattern: str) -> Generator[Self, None, None]:
self._glob_checks(pattern)

pattern_parts = PurePosixPath(pattern).parts
selector = _make_selector(("**",) + tuple(pattern_parts), _posix_flavour)

yield from self._glob(selector, True)

def iterdir(self: DerivedCloudPath) -> Generator[DerivedCloudPath, None, None]:
def iterdir(self) -> Generator[Self, None, None]:
for f, _ in self.client._list_dir(self, recursive=False):
if f != self: # iterdir does not include itself in pathlib
yield f
Expand Down Expand Up @@ -526,7 +527,7 @@ def _patched_close_empty_cache(*args, **kwargs):

return buffer

def replace(self: DerivedCloudPath, target: DerivedCloudPath) -> DerivedCloudPath:
def replace(self, target: Self) -> Self:
if type(self) != type(target):
raise TypeError(
f"The target based to rename must be an instantiated class of type: {type(self)}"
Expand All @@ -547,7 +548,7 @@ def replace(self: DerivedCloudPath, target: DerivedCloudPath) -> DerivedCloudPat
self.client._move_file(self, target)
return target

def rename(self: DerivedCloudPath, target: DerivedCloudPath) -> DerivedCloudPath:
def rename(self, target: Self) -> Self:
# for cloud services replace == rename since we don't just rename,
# we actually move files
return self.replace(target)
Expand Down Expand Up @@ -652,25 +653,25 @@ def _dispatch_to_path(self, func: str, *args, **kwargs) -> Any:
else:
return path_version

def __truediv__(self: DerivedCloudPath, other: Union[str, PurePosixPath]) -> DerivedCloudPath:
def __truediv__(self, other: Union[str, PurePosixPath]) -> Self:
if not isinstance(other, (str, PurePosixPath)):
raise TypeError(f"Can only join path {repr(self)} with strings or posix paths.")

return self._dispatch_to_path("__truediv__", other)

def joinpath(self: DerivedCloudPath, *args: Union[str, os.PathLike]) -> DerivedCloudPath:
def joinpath(self, *args: Union[str, os.PathLike]) -> Self:
return self._dispatch_to_path("joinpath", *args)

def absolute(self: DerivedCloudPath) -> DerivedCloudPath:
def absolute(self) -> Self:
return self

def is_absolute(self) -> bool:
return True

def resolve(self: DerivedCloudPath, strict: bool = False) -> DerivedCloudPath:
def resolve(self, strict: bool = False) -> Self:
return self

def relative_to(self: DerivedCloudPath, other: DerivedCloudPath) -> PurePosixPath:
def relative_to(self, other: Self) -> PurePosixPath:
# We don't dispatch regularly since this never returns a cloud path (since it is relative, and cloud paths are
# absolute)
if not isinstance(other, CloudPath):
Expand All @@ -681,7 +682,7 @@ def relative_to(self: DerivedCloudPath, other: DerivedCloudPath) -> PurePosixPat
)
return self._path.relative_to(other._path)

def is_relative_to(self: DerivedCloudPath, other: DerivedCloudPath) -> bool:
def is_relative_to(self, other: Self) -> bool:
try:
self.relative_to(other)
return True
Expand All @@ -700,11 +701,11 @@ def match(self, path_pattern: str) -> bool:
return self._dispatch_to_path("match", path_pattern)

@property
def parent(self: DerivedCloudPath) -> DerivedCloudPath:
def parent(self) -> Self:
return self._dispatch_to_path("parent")

@property
def parents(self: DerivedCloudPath) -> Sequence[DerivedCloudPath]:
def parents(self) -> Sequence[Self]:
return self._dispatch_to_path("parents")

@property
Expand All @@ -727,10 +728,10 @@ def suffix(self) -> str:
def suffixes(self) -> List[str]:
return self._dispatch_to_path("suffixes")

def with_name(self: DerivedCloudPath, name: str) -> DerivedCloudPath:
def with_name(self, name: str) -> Self:
return self._dispatch_to_path("with_name", name)

def with_suffix(self: DerivedCloudPath, suffix: str) -> DerivedCloudPath:
def with_suffix(self, suffix: str) -> Self:
return self._dispatch_to_path("with_suffix", suffix)

# ====================== DISPATCHED TO LOCAL CACHE FOR CONCRETE PATHS ======================
Expand Down Expand Up @@ -793,10 +794,10 @@ def rmtree(self) -> None:
self.client._remove(self)

def upload_from(
self: DerivedCloudPath,
self,
source: Union[str, os.PathLike],
force_overwrite_to_cloud: bool = False,
) -> DerivedCloudPath:
) -> Self:
"""Upload a file or directory to the cloud path."""
source = Path(source)

Expand All @@ -819,9 +820,9 @@ def upload_from(
@overload
def copy(
self,
destination: DerivedCloudPath,
destination: Self,
force_overwrite_to_cloud: bool = False,
) -> DerivedCloudPath:
) -> Self:
...

@overload
Expand Down Expand Up @@ -885,10 +886,10 @@ def copy(self, destination, force_overwrite_to_cloud=False):
@overload
def copytree(
self,
destination: DerivedCloudPath,
destination: Self,
force_overwrite_to_cloud: bool = False,
ignore: Optional[Callable[[str, Iterable[str]], Container[str]]] = None,
) -> DerivedCloudPath:
) -> Self:
...

@overload
Expand Down Expand Up @@ -964,7 +965,7 @@ def _local(self) -> Path:
"""Cached local version of the file."""
return self.client._local_cache_dir / self._no_prefix

def _new_cloudpath(self: DerivedCloudPath, path: Union[str, os.PathLike]) -> DerivedCloudPath:
def _new_cloudpath(self, path: Union[str, os.PathLike]) -> Self:
"""Use the scheme, client, cache dir of this cloudpath to instantiate
a new cloudpath of the same type with the path passed.

Expand Down Expand Up @@ -1022,9 +1023,9 @@ def _refresh_cache(self, force_overwrite_from_cloud: bool = False) -> None:
)

def _upload_local_to_cloud(
self: DerivedCloudPath,
self,
force_overwrite_to_cloud: bool = False,
) -> DerivedCloudPath:
) -> Self:
"""Uploads cache file at self._local to the cloud"""
# We should never try to be syncing entire directories; we should only
# cache and upload individual files.
Expand All @@ -1046,10 +1047,10 @@ def _upload_local_to_cloud(
return uploaded

def _upload_file_to_cloud(
self: DerivedCloudPath,
self,
local_path: Path,
force_overwrite_to_cloud: bool = False,
) -> DerivedCloudPath:
) -> Self:
"""Uploads file at `local_path` to the cloud if there is not a newer file
already there.
"""
Expand Down Expand Up @@ -1081,15 +1082,13 @@ def _upload_file_to_cloud(

# =========== pydantic integration special methods ===============
@classmethod
def __get_validators__(
cls: Type[DerivedCloudPath],
) -> Generator[Callable[[Any], DerivedCloudPath], None, None]:
def __get_validators__(cls) -> Generator[Callable[[Any], Self], None, None]:
"""Pydantic special method. See
https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types"""
yield cls._validate

@classmethod
def _validate(cls: Type[DerivedCloudPath], value: Any) -> DerivedCloudPath:
def _validate(cls, value: Any) -> Self:
"""Used as a Pydantic validator. See
https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types"""
return cls(value)
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]
requires-python = ">=3.7"
dependencies = ["importlib_metadata ; python_version < '3.8'"]
dependencies = [
"importlib_metadata ; python_version < '3.8'",
"typing_extensions>4 ; python_version < '3.11'",
]

[project.optional-dependencies]
azure = ["azure-storage-blob>=12"]
Expand Down