cachebox 6.2.1__tar.gz → 6.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {cachebox-6.2.1 → cachebox-6.2.2}/Cargo.lock +1 -1
  2. {cachebox-6.2.1 → cachebox-6.2.2}/Cargo.toml +1 -1
  3. {cachebox-6.2.1 → cachebox-6.2.2}/PKG-INFO +2 -1
  4. {cachebox-6.2.1 → cachebox-6.2.2}/README.md +1 -0
  5. {cachebox-6.2.1 → cachebox-6.2.2}/cachebox/_wrappers.py +19 -35
  6. {cachebox-6.2.1 → cachebox-6.2.2}/cachebox/utils.py +34 -9
  7. {cachebox-6.2.1 → cachebox-6.2.2}/tests/test_utils.py +121 -0
  8. {cachebox-6.2.1 → cachebox-6.2.2}/.github/workflows/CI.yml +0 -0
  9. {cachebox-6.2.1 → cachebox-6.2.2}/.github/workflows/docs.yml +0 -0
  10. {cachebox-6.2.1 → cachebox-6.2.2}/.github/workflows/python-test.yml +0 -0
  11. {cachebox-6.2.1 → cachebox-6.2.2}/.gitignore +0 -0
  12. {cachebox-6.2.1 → cachebox-6.2.2}/LICENSE +0 -0
  13. {cachebox-6.2.1 → cachebox-6.2.2}/LICENSE-THIRD-PARTY +0 -0
  14. {cachebox-6.2.1 → cachebox-6.2.2}/build.rs +0 -0
  15. {cachebox-6.2.1 → cachebox-6.2.2}/cachebox/__init__.py +0 -0
  16. {cachebox-6.2.1 → cachebox-6.2.2}/cachebox/_cachebox.py +0 -0
  17. {cachebox-6.2.1 → cachebox-6.2.2}/cachebox/_core.pyi +0 -0
  18. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/api/impls.md +0 -0
  19. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/api/index.md +0 -0
  20. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/api/utils.md +0 -0
  21. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/getting-started.md +0 -0
  22. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/index.md +0 -0
  23. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/installation.md +0 -0
  24. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/migration.md +0 -0
  25. {cachebox-6.2.1 → cachebox-6.2.2}/docs/docs/tips.md +0 -0
  26. {cachebox-6.2.1 → cachebox-6.2.2}/docs/mkdocs.yml +0 -0
  27. {cachebox-6.2.1 → cachebox-6.2.2}/pyproject.toml +0 -0
  28. {cachebox-6.2.1 → cachebox-6.2.2}/requirements-dev.txt +0 -0
  29. {cachebox-6.2.1 → cachebox-6.2.2}/rustfmt.toml +0 -0
  30. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/alloc.rs +0 -0
  31. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/bitmask.rs +0 -0
  32. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/group/generic.rs +0 -0
  33. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/group/lsx.rs +0 -0
  34. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/group/mod.rs +0 -0
  35. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/group/neon.rs +0 -0
  36. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/group/sse2.rs +0 -0
  37. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/mod.rs +0 -0
  38. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/control/tag.rs +0 -0
  39. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/mod.rs +0 -0
  40. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/raw.rs +0 -0
  41. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/scopeguard.rs +0 -0
  42. {cachebox-6.2.1 → cachebox-6.2.2}/src/hashbrown/util.rs +0 -0
  43. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/alias.rs +0 -0
  44. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/lazyheap.rs +0 -0
  45. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/linked_list.rs +0 -0
  46. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/mod.rs +0 -0
  47. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/onceinit.rs +0 -0
  48. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/pickle.rs +0 -0
  49. {cachebox-6.2.1 → cachebox-6.2.2}/src/internal/utils.rs +0 -0
  50. {cachebox-6.2.1 → cachebox-6.2.2}/src/lib.rs +0 -0
  51. {cachebox-6.2.1 → cachebox-6.2.2}/src/macro_rules.rs +0 -0
  52. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/common.rs +0 -0
  53. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/fifopolicy.rs +0 -0
  54. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/lfupolicy.rs +0 -0
  55. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/lrupolicy.rs +0 -0
  56. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/mod.rs +0 -0
  57. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/nopolicy.rs +0 -0
  58. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/rrpolicy.rs +0 -0
  59. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/traits.rs +0 -0
  60. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/ttlpolicy.rs +0 -0
  61. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/vttlpolicy.rs +0 -0
  62. {cachebox-6.2.1 → cachebox-6.2.2}/src/policies/wrapped.rs +0 -0
  63. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/base.rs +0 -0
  64. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/cache.rs +0 -0
  65. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/fifocache.rs +0 -0
  66. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/lfucache.rs +0 -0
  67. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/lrucache.rs +0 -0
  68. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/mod.rs +0 -0
  69. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/rrcache.rs +0 -0
  70. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/ttlcache.rs +0 -0
  71. {cachebox-6.2.1 → cachebox-6.2.2}/src/pyclasses/vttlcache.rs +0 -0
  72. {cachebox-6.2.1 → cachebox-6.2.2}/src/typeref.rs +0 -0
  73. {cachebox-6.2.1 → cachebox-6.2.2}/tests/__init__.py +0 -0
  74. {cachebox-6.2.1 → cachebox-6.2.2}/tests/conftest.py +0 -0
  75. {cachebox-6.2.1 → cachebox-6.2.2}/tests/mixins.py +0 -0
  76. {cachebox-6.2.1 → cachebox-6.2.2}/tests/test_impls.py +0 -0
@@ -37,7 +37,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
37
37
 
38
38
  [[package]]
39
39
  name = "cachebox"
40
- version = "6.2.1"
40
+ version = "6.2.2"
41
41
  dependencies = [
42
42
  "allocator-api2",
43
43
  "cfg-if",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cachebox"
3
- version = "6.2.1"
3
+ version = "6.2.2"
4
4
  edition = "2021"
5
5
  description = "The fastest memoizing and caching Python library written in Rust"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachebox
3
- Version: 6.2.1
3
+ Version: 6.2.2
4
4
  Classifier: Programming Language :: Python :: Implementation :: CPython
5
5
  Classifier: Programming Language :: Python :: Implementation :: PyPy
6
6
  Classifier: Programming Language :: Python :: 3
@@ -41,6 +41,7 @@ Project-URL: Homepage, https://github.com/awolverp/cachebox
41
41
 
42
42
  [![License](https://img.shields.io/github/license/awolverp/cachebox.svg?style=flat-square)](https://github.com/awolverp/cachebox/blob/main/LICENSE)
43
43
  [![Downloads](https://img.shields.io/pypi/dm/cachebox?style=flat-square&color=%23314bb5)](https://pepy.tech/projects/cachebox)
44
+ [![Donate](https://img.shields.io/badge/donate-PayRequest-blue)](https://payrequest.me/ali-pooralijan-awolverp)
44
45
 
45
46
  </div>
46
47
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  [![License](https://img.shields.io/github/license/awolverp/cachebox.svg?style=flat-square)](https://github.com/awolverp/cachebox/blob/main/LICENSE)
12
12
  [![Downloads](https://img.shields.io/pypi/dm/cachebox?style=flat-square&color=%23314bb5)](https://pepy.tech/projects/cachebox)
13
+ [![Donate](https://img.shields.io/badge/donate-PayRequest-blue)](https://payrequest.me/ali-pooralijan-awolverp)
13
14
 
14
15
  </div>
15
16
 
@@ -18,8 +18,10 @@ class _Lock:
18
18
 
19
19
  def __enter__(self) -> None:
20
20
  self.waiters += 1
21
- self._lock.__enter__()
22
- self.waiters -= 1
21
+ try:
22
+ self._lock.__enter__()
23
+ finally:
24
+ self.waiters -= 1
23
25
 
24
26
  def __exit__(self, *_) -> None:
25
27
  self._lock.__exit__(*_)
@@ -34,8 +36,10 @@ class _AsyncLock:
34
36
 
35
37
  async def __aenter__(self) -> None:
36
38
  self.waiters += 1
37
- await self._lock.__aenter__()
38
- self.waiters -= 1
39
+ try:
40
+ await self._lock.__aenter__()
41
+ finally:
42
+ self.waiters -= 1
39
43
 
40
44
  async def __aexit__(self, *_) -> None:
41
45
  await self._lock.__aexit__(*_)
@@ -102,7 +106,7 @@ def _cached_wrapper_without_lock(
102
106
 
103
107
  result = func(*args, **kwds)
104
108
  _cache.insert(key, result)
105
- hits += 1
109
+ misses += 1
106
110
  if callback is not None:
107
111
  callback(EVENT_MISS, key, result)
108
112
 
@@ -159,7 +163,7 @@ def _async_cached_wrapper_without_lock(
159
163
  # Passing `cachebox__ignore=True` bypasses the cache and
160
164
  # calls the function directly.
161
165
  if kwds.pop("cachebox__ignore", False):
162
- return func(*args, **kwds)
166
+ return await func(*args, **kwds)
163
167
 
164
168
  _cache: BaseCacheImpl = cache(args[0]) if cache_is_fn else cache # type: ignore[arg-type]
165
169
  key = _make_key(args, kwds)
@@ -266,17 +270,19 @@ def _cached_wrapper(
266
270
  except KeyError:
267
271
  try:
268
272
  result = func(*args, **kwds)
269
- except Exception as exc:
273
+ except BaseException as exc:
270
274
  if lock.waiters > 0:
271
275
  pending_errors[key] = exc
276
+
272
277
  raise
273
278
  else:
274
279
  _cache[key] = result
275
280
  misses += 1
276
281
  event = EVENT_MISS
277
282
 
278
- if lock.waiters == 0:
279
- locks.pop(key, None)
283
+ finally:
284
+ if lock.waiters == 0:
285
+ locks.pop(key, None)
280
286
 
281
287
  if callback is not None:
282
288
  callback(event, key, result)
@@ -319,29 +325,7 @@ def _async_cached_wrapper(
319
325
  hits = 0
320
326
  misses = 0
321
327
 
322
- # See _cached_wrapper
323
328
  locks: Cache[typing.Hashable, _AsyncLock] = Cache(0)
324
-
325
- # if lock_type is asyncio.Lock:
326
-
327
- # async def _get_lock(key: typing.Hashable):
328
- # return locks.setdefault(key, _AsyncLock(lock_type()))
329
-
330
- # else:
331
- # _lock_creation_guard = asyncio.Lock()
332
-
333
- # async def _get_lock(key: typing.Hashable):
334
- # lock = locks.get(key)
335
- # if lock is not None:
336
- # return lock
337
-
338
- # async with _lock_creation_guard:
339
- # lock = locks.get(key)
340
- # if lock is None:
341
- # locks[key] = lock = _AsyncLock(lock_type())
342
-
343
- # return lock
344
-
345
329
  pending_errors: dict[typing.Hashable, BaseException] = {}
346
330
 
347
331
  async def _wrapped(*args, **kwds):
@@ -381,7 +365,7 @@ def _async_cached_wrapper(
381
365
  except KeyError:
382
366
  try:
383
367
  result = await func(*args, **kwds)
384
- except Exception as exc:
368
+ except BaseException as exc:
385
369
  if lock.waiters > 0:
386
370
  pending_errors[key] = exc
387
371
  raise
@@ -389,9 +373,9 @@ def _async_cached_wrapper(
389
373
  _cache[key] = result
390
374
  misses += 1
391
375
  event = EVENT_MISS
392
-
393
- if lock.waiters == 0:
394
- locks.pop(key, None)
376
+ finally:
377
+ if lock.waiters == 0:
378
+ locks.pop(key, None)
395
379
 
396
380
  await _call_async_callback(callback, event, key, result)
397
381
 
@@ -73,12 +73,14 @@ def make_key(*args, **kwds) -> typing.Hashable:
73
73
  if not kwds:
74
74
  if len(args) == 1 and type(args[0]) in _FAST_TYPES:
75
75
  return args[0]
76
+
76
77
  return args
77
78
 
78
79
  key = args + (_KWDS_MARK,)
79
80
  for item in kwds.items():
80
81
  key += item
81
- return key[0] if len(key) == 1 and type(key[0]) in _FAST_TYPES else key
82
+
83
+ return key
82
84
 
83
85
 
84
86
  def make_hash_key(*args, **kwds) -> int:
@@ -155,8 +157,13 @@ class Frozen(BaseCacheImpl[KT, VT]): # pragma: no cover
155
157
  ignore: If ``True``, silently ignores modification attempts; if ``False``, raises
156
158
  ``TypeError`` when modification is attempted. Default is ``False``.
157
159
  """
158
- assert isinstance(cls, BaseCacheImpl)
159
- assert type(cls) is not Frozen
160
+ if not isinstance(cls, BaseCacheImpl):
161
+ raise TypeError(
162
+ f"expected a BaseCacheImpl instance, got {type(cls).__name__!r}"
163
+ )
164
+
165
+ if type(cls) is Frozen:
166
+ raise TypeError("cannot wrap an already-frozen cache")
160
167
 
161
168
  self.__cache = cls
162
169
  self.ignore = ignore
@@ -366,9 +373,16 @@ class Frozen(BaseCacheImpl[KT, VT]): # pragma: no cover
366
373
  def _cast_lock(
367
374
  iscoroutinefunction: bool,
368
375
  lock: (
369
- typing.Type[AbstractContextManager] | typing.Type[AbstractAsyncContextManager] | bool | None
376
+ typing.Type[AbstractContextManager]
377
+ | typing.Type[AbstractAsyncContextManager]
378
+ | bool
379
+ | None
370
380
  ) = True,
371
- ) -> typing.Type[AbstractContextManager] | typing.Type[AbstractAsyncContextManager] | None:
381
+ ) -> (
382
+ typing.Type[AbstractContextManager]
383
+ | typing.Type[AbstractAsyncContextManager]
384
+ | None
385
+ ):
372
386
  import _thread
373
387
  import asyncio
374
388
  import threading
@@ -381,12 +395,18 @@ def _cast_lock(
381
395
 
382
396
  if iscoroutinefunction:
383
397
  if not hasattr(lock, "__aenter__"):
384
- raise TypeError("For async functions, you cannot use a regular synchronous lock.")
398
+ raise TypeError(
399
+ "For async functions, you cannot use a regular synchronous lock."
400
+ )
385
401
 
386
402
  return typing.cast(typing.Type[AbstractAsyncContextManager], lock)
387
403
 
388
404
  # threading.Lock, threading.RLock and _thread.allocate_lock are function
389
- if lock is threading.Lock or lock is threading.RLock or lock is _thread.allocate_lock:
405
+ if (
406
+ lock is threading.Lock
407
+ or lock is threading.RLock
408
+ or lock is _thread.allocate_lock
409
+ ):
390
410
  return typing.cast(typing.Type[AbstractContextManager], lock)
391
411
 
392
412
  if not hasattr(lock, "__enter__"):
@@ -403,7 +423,10 @@ def cached(
403
423
  copy_level: int = 1,
404
424
  postprocess: _PostProcess | None = postprocess_copy_mutables,
405
425
  lock: (
406
- typing.Type[AbstractContextManager] | typing.Type[AbstractAsyncContextManager] | bool | None
426
+ typing.Type[AbstractContextManager]
427
+ | typing.Type[AbstractAsyncContextManager]
428
+ | bool
429
+ | None
407
430
  ) = True,
408
431
  ) -> typing.Callable[[FT], FT]:
409
432
  """
@@ -480,7 +503,9 @@ def cached(
480
503
  lock_type = _cast_lock(iscoroutinefunction, lock)
481
504
 
482
505
  if not iscoroutinefunction and inspect.iscoroutinefunction(callback):
483
- raise TypeError("For sync functions, you cannot use a asynchronous callback")
506
+ raise TypeError(
507
+ "For sync functions, you cannot use a asynchronous callback"
508
+ )
484
509
 
485
510
  if lock_type:
486
511
  builder = _async_cached_wrapper if iscoroutinefunction else _cached_wrapper
@@ -742,3 +742,124 @@ async def test_async_handling_pending_errors():
742
742
  await calc(1, 2, 3, exception=True)
743
743
 
744
744
  assert await calc(1, 2, 3, exception=False) == 6
745
+
746
+
747
+ def test_misses_should_increment_without_lock():
748
+ calls = {"n": 0}
749
+
750
+ @cachebox.cached(cachebox.LRUCache(0), lock=False)
751
+ def f(x):
752
+ calls["n"] += 1
753
+ return x * 2
754
+
755
+ assert f(1) == 2 # miss
756
+ assert f(1) == 2 # hit
757
+ assert f(2) == 4 # miss
758
+
759
+ info = cachebox.get_cached_cache_info(f)
760
+ print(info)
761
+
762
+ assert info.misses == 2
763
+ assert info.hits == 1
764
+
765
+
766
+ @pytest.mark.asyncio
767
+ async def test_ignore_path_should_return_awaited_value():
768
+ @cachebox.cached(cachebox.LRUCache(0), lock=False)
769
+ async def f(x):
770
+ await asyncio.sleep(0)
771
+ return x * 2
772
+
773
+ result = await f(
774
+ 5,
775
+ cachebox__ignore=True, # type: ignore
776
+ )
777
+ assert not asyncio.iscoroutine(result)
778
+ assert result == 10
779
+
780
+
781
+ def test_locks_should_not_leak_on_exception():
782
+ @cachebox.cached(cachebox.LRUCache(0)) # lock پیش‌فرض True است
783
+ def f(x):
784
+ raise ValueError("boom")
785
+
786
+ for i in range(50):
787
+ with pytest.raises(ValueError):
788
+ f(i)
789
+
790
+ locks_cache = None
791
+ for cell in f.__closure__: # type: ignore
792
+ try:
793
+ val = cell.cell_contents
794
+ except ValueError:
795
+ continue
796
+
797
+ if isinstance(val, cachebox.Cache):
798
+ locks_cache = val
799
+ break
800
+
801
+ assert locks_cache is not None, "We couldn't find locks_cache"
802
+ print("locks length after 50 failing calls:", len(locks_cache))
803
+
804
+ assert len(locks_cache) == 0
805
+
806
+
807
+ @pytest.mark.asyncio
808
+ async def test_waiters_should_not_leak_on_cancel():
809
+ started = asyncio.Event()
810
+ release = asyncio.Event()
811
+
812
+ @cachebox.cached(cachebox.LRUCache(0))
813
+ async def f(x):
814
+ started.set()
815
+ await release.wait()
816
+ return x
817
+
818
+ first = asyncio.create_task(f(1))
819
+ await started.wait()
820
+
821
+ second = asyncio.create_task(f(1))
822
+ await asyncio.sleep(0.05)
823
+
824
+ second.cancel()
825
+ with pytest.raises(asyncio.CancelledError):
826
+ await second
827
+
828
+ release.set()
829
+ result = await first
830
+ assert result == 1
831
+
832
+ locks_cache = None
833
+ for cell in f.__closure__: # type: ignore
834
+ try:
835
+ val = cell.cell_contents
836
+ except ValueError:
837
+ continue
838
+ if isinstance(val, cachebox.Cache):
839
+ locks_cache = val
840
+ break
841
+ assert locks_cache is not None
842
+
843
+ print("locks length after cancel scenario:", len(locks_cache))
844
+ assert len(locks_cache) == 0
845
+
846
+
847
+ @pytest.mark.asyncio
848
+ async def test_cancelled_error_should_propagate_to_waiters_too():
849
+ gate = asyncio.Event()
850
+
851
+ @cachebox.cached(cachebox.LRUCache(0))
852
+ async def f(x):
853
+ await gate.wait()
854
+ raise asyncio.CancelledError()
855
+
856
+ t1 = asyncio.create_task(f(1))
857
+ t2 = asyncio.create_task(f(1))
858
+ await asyncio.sleep(0.01)
859
+ gate.set()
860
+
861
+ with pytest.raises(asyncio.CancelledError):
862
+ await t1
863
+
864
+ with pytest.raises(asyncio.CancelledError):
865
+ await t2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes