Skip to content

Commit e33b583

Browse files
committed
weakref finalize instead of __del__
1 parent 6fc3a1e commit e33b583

File tree

3 files changed

+45
-28
lines changed

3 files changed

+45
-28
lines changed

cloudpathlib/cache_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pathlib import Path
2+
import shutil
3+
4+
5+
def _clear_cache(_local: Path) -> None:
6+
if _local.exists():
7+
if _local.is_file():
8+
_local.unlink()
9+
else:
10+
shutil.rmtree(_local)

cloudpathlib/client.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import shutil
66
from tempfile import TemporaryDirectory
77
from typing import Generic, Callable, Iterable, Optional, Tuple, TypeVar, Union
8+
import weakref
89

10+
from .cache_utils import _clear_cache
911
from .cloudpath import CloudImplementation, CloudPath, implementation_registry
1012
from .enums import FileCacheMode
1113
from .exceptions import InvalidConfigurationException
@@ -26,6 +28,18 @@ def decorator(cls: type) -> type:
2628
return decorator
2729

2830

31+
def _client_finalizer(file_cache_mode: FileCacheMode, _local_cache_dir) -> None:
32+
if file_cache_mode in [
33+
FileCacheMode.tmp_dir,
34+
FileCacheMode.close_file,
35+
FileCacheMode.cloudpath_object,
36+
]:
37+
_clear_cache(_local_cache_dir)
38+
39+
if _local_cache_dir.exists():
40+
_local_cache_dir.rmdir()
41+
42+
2943
class Client(abc.ABC, Generic[BoundedCloudPath]):
3044
_cloud_meta: CloudImplementation
3145
_default_client = None
@@ -82,18 +96,7 @@ def __init__(
8296

8397
self.file_cache_mode = file_cache_mode
8498

85-
def __del__(self) -> None:
86-
# remove containing dir, even if a more aggressive strategy
87-
# removed the actual files
88-
if getattr(self, "file_cache_mode", None) in [
89-
FileCacheMode.tmp_dir,
90-
FileCacheMode.close_file,
91-
FileCacheMode.cloudpath_object,
92-
]:
93-
self.clear_cache()
94-
95-
if self._local_cache_dir.exists():
96-
self._local_cache_dir.rmdir()
99+
weakref.finalize(self, _client_finalizer, self.file_cache_mode, self._local_cache_dir)
97100

98101
@classmethod
99102
def get_default_client(cls) -> "Client":

cloudpathlib/cloudpath.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
_PathParents,
1212
)
1313

14-
import shutil
1514
import sys
1615
from typing import (
1716
overload,
@@ -33,6 +32,7 @@
3332
)
3433
from urllib.parse import urlparse
3534
from warnings import warn
35+
import weakref
3636

3737
if sys.version_info >= (3, 10):
3838
from typing import TypeGuard
@@ -58,6 +58,7 @@ def _make_selector(pattern_parts, _flavour, case_sensitive=True):
5858

5959
from . import anypath
6060

61+
from .cache_utils import _clear_cache
6162
from .exceptions import (
6263
ClientMismatchError,
6364
CloudPathFileExistsError,
@@ -188,6 +189,20 @@ def __init__(cls, name: str, bases: Tuple[type, ...], dic: Dict[str, Any]) -> No
188189
getattr(cls, attr).fget.__doc__ = docstring
189190

190191

192+
def _cloudpath_finalizer(handle: Optional[IO], client: Optional["Client"], _no_prefix: str):
193+
"""Use weakref.finalizer instead of __del__ since it is more reliable (does
194+
not wait for garbage collection to actually run).
195+
"""
196+
# make sure that file handle to local path is closed
197+
if handle is not None:
198+
handle.close()
199+
200+
# ensure file removed from cache when cloudpath object deleted
201+
if client is not None:
202+
if getattr(client, "file_cache_mode", None) == FileCacheMode.cloudpath_object:
203+
_clear_cache(client._local_cache_dir / _no_prefix)
204+
205+
191206
# Abstract base class
192207
class CloudPath(metaclass=CloudPathMeta):
193208
"""Base class for cloud storage file URIs, in the style of the Python standard library's
@@ -242,23 +257,16 @@ def __init__(
242257
# track if local has been written to, if so it may need to be uploaded
243258
self._dirty = False
244259

260+
# register cache cleanup method when this object is marked for garbage collection
261+
weakref.finalize(self, _cloudpath_finalizer, self._handle, self._client, self._no_prefix)
262+
245263
@property
246264
def client(self):
247265
if getattr(self, "_client", None) is None:
248266
self._client = self._cloud_meta.client_class.get_default_client()
249267

250268
return self._client
251269

252-
def __del__(self) -> None:
253-
# make sure that file handle to local path is closed
254-
if self._handle is not None:
255-
self._handle.close()
256-
257-
# ensure file removed from cache when cloudpath object deleted
258-
client = getattr(self, "_client", None)
259-
if getattr(client, "file_cache_mode", None) == FileCacheMode.cloudpath_object:
260-
self.clear_cache()
261-
262270
def __getstate__(self) -> Dict[str, Any]:
263271
state = self.__dict__.copy()
264272

@@ -1088,11 +1096,7 @@ def copytree(self, destination, force_overwrite_to_cloud=None, ignore=None):
10881096

10891097
def clear_cache(self):
10901098
"""Removes cache if it exists"""
1091-
if self._local.exists():
1092-
if self._local.is_file():
1093-
self._local.unlink()
1094-
else:
1095-
shutil.rmtree(self._local)
1099+
_clear_cache(self._local)
10961100

10971101
# =========== private cloud methods ===============
10981102
@property

0 commit comments

Comments
 (0)