From 093644de58d7e9594cbb6e727962b6c43c317121 Mon Sep 17 00:00:00 2001 From: Ryan McGinty Date: Wed, 23 Apr 2025 15:28:28 -0700 Subject: [PATCH 1/2] Update .gitignore and Makefile for improved installation commands --- .gitignore | 1 + Makefile | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8363e9b8..16dfb361 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,4 @@ ENV/ # IDE settings .vscode/ +*.code-workspace \ No newline at end of file diff --git a/Makefile b/Makefile index 9464111b..7c2f31c3 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,10 @@ help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) install: clean ## install the package to the active Python's site-packages - python setup.py install + pip install . + +install-dev: clean ## install the package and development requirements + pip install -e . lint: ## check style with black, flake8, and mypy black --check cloudpathlib tests docs From 94d3508654db96e653725845670d61e50c6aa01c Mon Sep 17 00:00:00 2001 From: Ryan McGinty Date: Wed, 30 Apr 2025 10:52:54 -0700 Subject: [PATCH 2/2] Enhance cloudpathlib with PathBase integration and CloudPathInfo class - Added PathBase class for path-like objects in cloud storage. - Introduced CloudPathInfo class to provide information about cloud paths. - Updated AnyPath and CloudPath to utilize PathBase. - Improved compatibility with pathlib_abc for Python versions. - Added tests for AnyPath subclassing and PathBase functionality. --- cloudpathlib/anypath.py | 21 +++++++++++++ cloudpathlib/cloudpath.py | 63 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +- tests/test_anypath.py | 14 +++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/cloudpathlib/anypath.py b/cloudpathlib/anypath.py index dbab9db9..8d0a8476 100644 --- a/cloudpathlib/anypath.py +++ b/cloudpathlib/anypath.py @@ -1,8 +1,17 @@ import os +import sys from abc import ABC from pathlib import Path from typing import Any, Union +if sys.version_info >= (3, 14, 0): + if sys.version_info >= (3, 14, 4): + from pathlib._abc import ReadablePath, WritablePath + else: + from pathlib._abc import PathBase as ReadablePath, PathBase as WritablePath +else: + from pathlib_abc import ReadablePath, WritablePath + from .cloudpath import InvalidPrefixError, CloudPath from .exceptions import AnyPathTypeError from .url_utils import path_from_fileurl @@ -90,3 +99,15 @@ def to_anypath(s: Union[str, os.PathLike]) -> Union[CloudPath, Path]: return s return AnyPath(s) # type: ignore + + +class PathBase(ReadablePath, WritablePath): + """ + A base class for path-like objects that can be used with cloud storage. + This class is a subclass of Readable + and WritablePath from pathlib_abc, which provides a common interface + for path-like objects that can be read from and written to. + """ + + +PathBase.register(Path) diff --git a/cloudpathlib/cloudpath.py b/cloudpathlib/cloudpath.py index 5845e929..fb86ef46 100644 --- a/cloudpathlib/cloudpath.py +++ b/cloudpathlib/cloudpath.py @@ -149,6 +149,35 @@ def decorator(cls: Type[CloudPathT]) -> Type[CloudPathT]: return decorator +class CloudPathInfo: + """A class that provides information about a cloud path + + This class is an implementation of `pathlib_abc.PathInfo` (>=0.4.0), which provides + a common interface for getting information from path-like objects that can be read from + and written to. + + """ + + def __init__(self, path: "CloudPath") -> None: + self._cloud_path = path + + @property + def cloud_path(self) -> "CloudPath": + return self._cloud_path + + def exists(self, *, follow_symlinks: bool = True) -> bool: + return self.cloud_path.exists() + + def is_dir(self, *, follow_symlinks: bool = True) -> bool: + return self.cloud_path.is_dir(follow_symlinks=follow_symlinks) + + def is_file(self, *, follow_symlinks: bool = True) -> bool: + return self.cloud_path.is_file(follow_symlinks=follow_symlinks) + + def is_symlink(self) -> bool: + return self.cloud_path.is_symlink() + + class CloudPathMeta(abc.ABCMeta): @overload def __call__( @@ -215,7 +244,7 @@ def __init__(cls, name: str, bases: Tuple[type, ...], dic: Dict[str, Any]) -> No # Abstract base class -class CloudPath(metaclass=CloudPathMeta): +class CloudPath(anypath.PathBase, metaclass=CloudPathMeta): """Base class for cloud storage file URIs, in the style of the Python standard library's [`pathlib` module](https://docs.python.org/3/library/pathlib.html). Instances represent a path in cloud storage with filesystem path semantics, and convenient methods allow for basic @@ -365,6 +394,38 @@ def __ge__(self, other: Any) -> bool: return NotImplemented return self.parts >= other.parts + # ====================== PathBase IMPLEMENTATIONS ====================== + + @property + def info(self) -> CloudPathInfo: + """Return a PathInfo object with information about the path.""" + return CloudPathInfo(self) + + def __open_rb__(self, buffering: int = -1) -> BinaryIO: + """Open the file in binary read mode.""" + return self.open(mode="rb", buffering=buffering) + + def __open_wb__(self, buffering: int = -1) -> BinaryIO: + """Open the file in binary write mode.""" + return self.open(mode="wb", buffering=buffering) + + def symlink_to(self, target, target_is_directory: bool = False): + raise IncompleteImplementationError("symlink_to is not implemented for cloud paths.") + + def is_symlink(self) -> bool: + """Check if the path is a symlink + + For cloud paths, this is always False since cloud paths do not support symlinks. + + Returns: + bool: False always, since cloud paths do not support symlinks. + """ + return False + + def readlink(self): + # ASSUMPTION: cloud paths are not symlinks + return self + # ====================== NOT IMPLEMENTED ====================== # as_posix - no cloud equivalent; not needed since we assume url separator # chmod - permission changing should be explicitly done per client with methods diff --git a/pyproject.toml b/pyproject.toml index 77c28336..9efb09cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "typing-extensions>4 ; python_version < '3.11'", + "typing_extensions>4 ; python_version < '3.11'", + "pathlib-abc ~= ; python_version < '3.14'", ] [project.optional-dependencies] diff --git a/tests/test_anypath.py b/tests/test_anypath.py index 04fc5070..4750d6e2 100644 --- a/tests/test_anypath.py +++ b/tests/test_anypath.py @@ -64,3 +64,17 @@ def test_anypath_bad_input(): def test_anypath_subclass_anypath(): assert issubclass(AnyPath, AnyPath) + + +def test__anypathbase__subclassing_works_correctly(rig): + from cloudpathlib.anypathbase import PathBase + + cloudpath = rig.create_cloud_path("a/b/c") + + localpath = Path("a/b/c") + + assert isinstance(cloudpath, PathBase) + assert isinstance(localpath, PathBase) + + assert isinstance(cloudpath, os.PathLike) + assert isinstance(localpath, os.PathLike)