Skip to content

Commit

Permalink
Use Self type from PEP 673 (#327)
Browse files Browse the repository at this point in the history
* Use Self type from PEP 673

* Changelog

---------

Co-authored-by: Jay Qi <[email protected]>
  • Loading branch information
jayqi and jayqi authored Apr 16, 2023
1 parent a0dd927 commit 28b2eb5
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 39 deletions.
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

0 comments on commit 28b2eb5

Please sign in to comment.