Skip to content
Open
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 @@ -2,6 +2,7 @@

## UNRELEASED
- Added support for Pydantic serialization (Issue [#537](https://github.com/drivendataorg/cloudpathlib/issues/537), PR [#538](https://github.com/drivendataorg/cloudpathlib/pull/538))
- Improved import time by lazy-loading cloud provider modules (Issue [#544](https://github.com/drivendataorg/cloudpathlib/issues/544), PR [#TBD](https://github.com/drivendataorg/cloudpathlib/pull/TBD))

## v0.23.0 (2025-10-07)

Expand Down
95 changes: 80 additions & 15 deletions cloudpathlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import os
import sys
from typing import TYPE_CHECKING

from .anypath import AnyPath
from .azure.azblobclient import AzureBlobClient
from .azure.azblobpath import AzureBlobPath
from .cloudpath import CloudPath, implementation_registry
from .patches import patch_open, patch_os_functions, patch_glob, patch_all_builtins
from .gs.gsclient import GSClient
from .gs.gspath import GSPath
from .http.httpclient import HttpClient, HttpsClient
from .http.httppath import HttpPath, HttpsPath
from .s3.s3client import S3Client
from .s3.s3path import S3Path
# Lazy imports for cloud providers to avoid loading heavy SDKs at import time
# Google Cloud SDK alone adds ~200ms to import time

if TYPE_CHECKING:
from .anypath import AnyPath as AnyPath
from .azure.azblobclient import AzureBlobClient as AzureBlobClient
from .azure.azblobpath import AzureBlobPath as AzureBlobPath
from .cloudpath import (
CloudPath as CloudPath,
implementation_registry as implementation_registry,
)
from .patches import (
patch_open as patch_open,
patch_os_functions as patch_os_functions,
patch_glob as patch_glob,
patch_all_builtins as patch_all_builtins,
)
from .gs.gsclient import GSClient as GSClient
from .gs.gspath import GSPath as GSPath
from .http.httpclient import HttpClient as HttpClient, HttpsClient as HttpsClient
from .http.httppath import HttpPath as HttpPath, HttpsPath as HttpsPath
from .s3.s3client import S3Client as S3Client
from .s3.s3path import S3Path as S3Path

if sys.version_info[:2] >= (3, 8):
import importlib.metadata as importlib_metadata
Expand Down Expand Up @@ -43,14 +56,66 @@
]


# Lazy loading implementation
_LAZY_IMPORTS = {
# Core
"AnyPath": ".anypath",
"CloudPath": ".cloudpath",
"implementation_registry": ".cloudpath",
# Patches
"patch_open": ".patches",
"patch_os_functions": ".patches",
"patch_glob": ".patches",
"patch_all_builtins": ".patches",
# S3
"S3Client": ".s3.s3client",
"S3Path": ".s3.s3path",
# GCS
"GSClient": ".gs.gsclient",
"GSPath": ".gs.gspath",
# Azure
"AzureBlobClient": ".azure.azblobclient",
"AzureBlobPath": ".azure.azblobpath",
# HTTP
"HttpClient": ".http.httpclient",
"HttpsClient": ".http.httpclient",
"HttpPath": ".http.httppath",
"HttpsPath": ".http.httppath",
}


def __getattr__(name: str):
if name in _LAZY_IMPORTS:
import importlib

module_path = _LAZY_IMPORTS[name]
module = importlib.import_module(module_path, __name__)
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return __all__


# Handle environment-variable-based patching
# These need to be checked at import time, so we do them lazily only if env vars are set
if bool(os.environ.get("CLOUDPATHLIB_PATCH_OPEN", "")):
patch_open()
from .patches import patch_open as _patch_open

_patch_open()

if bool(os.environ.get("CLOUDPATHLIB_PATCH_OS", "")):
patch_os_functions()
from .patches import patch_os_functions as _patch_os_functions

_patch_os_functions()

if bool(os.environ.get("CLOUDPATHLIB_PATCH_GLOB", "")):
patch_glob()
from .patches import patch_glob as _patch_glob

_patch_glob()

if bool(os.environ.get("CLOUDPATHLIB_PATCH_ALL", "")):
patch_all_builtins()
from .patches import patch_all_builtins as _patch_all_builtins

_patch_all_builtins()
19 changes: 17 additions & 2 deletions cloudpathlib/azure/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
from .azblobclient import AzureBlobClient
from .azblobpath import AzureBlobPath
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .azblobclient import AzureBlobClient as AzureBlobClient
from .azblobpath import AzureBlobPath as AzureBlobPath

__all__ = [
"AzureBlobClient",
"AzureBlobPath",
]


def __getattr__(name: str):
if name == "AzureBlobClient":
from .azblobclient import AzureBlobClient

return AzureBlobClient
if name == "AzureBlobPath":
from .azblobpath import AzureBlobPath

return AzureBlobPath
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
11 changes: 9 additions & 2 deletions cloudpathlib/cloudpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ def _make_selector(pattern_parts, _flavour, case_sensitive=True): # noqa: F811
from .legacy.glob import _make_selector # noqa: F811


from cloudpathlib.enums import FileCacheMode
from .enums import FileCacheMode

from . import anypath
from .exceptions import (
ClientMismatchError,
CloudPathFileExistsError,
Expand Down Expand Up @@ -1143,6 +1142,8 @@ def _copy(
force_overwrite_to_cloud: Optional[bool] = None,
remove_src: bool = False,
) -> Union[Path, Self]:
from . import anypath

if not self.exists():
raise ValueError(f"Path {self} must exist to copy.")

Expand Down Expand Up @@ -1275,6 +1276,8 @@ def copy_into(
force_overwrite_to_cloud: Optional[bool] = None,
) -> Union[Path, Self]:
"""Copy self into target directory, preserving the filename."""
from . import anypath

target_path = anypath.to_anypath(target_dir) / self.name

result = self._copy(
Expand Down Expand Up @@ -1312,6 +1315,8 @@ def copytree(

def copytree(self, destination, force_overwrite_to_cloud=None, ignore=None):
"""Copy self to a directory, if self is a directory."""
from . import anypath

if not self.is_dir():
raise CloudPathNotADirectoryError(
f"Origin path {self} must be a directory. To copy a single file use the method copy."
Expand Down Expand Up @@ -1427,6 +1432,8 @@ def move_into(
force_overwrite_to_cloud: Optional[bool] = None,
) -> Union[Path, Self]:
"""Move self into target directory, preserving the filename and removing the source."""
from . import anypath

target_path = anypath.to_anypath(target_dir) / self.name

result = self._copy(
Expand Down
19 changes: 17 additions & 2 deletions cloudpathlib/gs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
from .gsclient import GSClient
from .gspath import GSPath
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .gsclient import GSClient as GSClient
from .gspath import GSPath as GSPath

__all__ = [
"GSClient",
"GSPath",
]


def __getattr__(name: str):
if name == "GSClient":
from .gsclient import GSClient

return GSClient
if name == "GSPath":
from .gspath import GSPath

return GSPath
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19 changes: 17 additions & 2 deletions cloudpathlib/s3/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
from .s3client import S3Client
from .s3path import S3Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .s3client import S3Client as S3Client
from .s3path import S3Path as S3Path

__all__ = [
"S3Client",
"S3Path",
]


def __getattr__(name: str):
if name == "S3Client":
from .s3client import S3Client

return S3Client
if name == "S3Path":
from .s3path import S3Path

return S3Path
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Loading