cachebox 6.2.9__tar.gz → 6.2.10__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 (86) hide show
  1. {cachebox-6.2.9 → cachebox-6.2.10}/Cargo.lock +1 -1
  2. {cachebox-6.2.9 → cachebox-6.2.10}/Cargo.toml +1 -1
  3. {cachebox-6.2.9 → cachebox-6.2.10}/PKG-INFO +1 -1
  4. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/algorithms.md +17 -9
  5. {cachebox-6.2.9 → cachebox-6.2.10}/pyproject.toml +2 -2
  6. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/linked_list.rs +80 -50
  7. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/utils.rs +13 -3
  8. {cachebox-6.2.9 → cachebox-6.2.10}/src/macro_rules.rs +1 -1
  9. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/fifopolicy.rs +5 -4
  10. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/ttlpolicy.rs +9 -9
  11. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/vttlpolicy.rs +5 -4
  12. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/wrapped.rs +120 -37
  13. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/cache.rs +35 -37
  14. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/fifocache.rs +37 -39
  15. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/lfucache.rs +38 -40
  16. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/lrucache.rs +38 -40
  17. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/rrcache.rs +36 -38
  18. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/ttlcache.rs +47 -46
  19. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/vttlcache.rs +41 -43
  20. {cachebox-6.2.9 → cachebox-6.2.10}/src/typeref.rs +6 -3
  21. {cachebox-6.2.9 → cachebox-6.2.10}/tests/mixins.py +320 -0
  22. cachebox-6.2.10/tests/test_hashing.py +156 -0
  23. {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_impls.py +33 -0
  24. {cachebox-6.2.9 → cachebox-6.2.10}/uv.lock +28 -28
  25. cachebox-6.2.9/tests/test_hashing.py +0 -81
  26. {cachebox-6.2.9 → cachebox-6.2.10}/.github/dependabot.yaml +0 -0
  27. {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/CI.yml +0 -0
  28. {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/docs.yml +0 -0
  29. {cachebox-6.2.9 → cachebox-6.2.10}/.github/workflows/python-test.yml +0 -0
  30. {cachebox-6.2.9 → cachebox-6.2.10}/.gitignore +0 -0
  31. {cachebox-6.2.9 → cachebox-6.2.10}/AGENTS.md +0 -0
  32. {cachebox-6.2.9 → cachebox-6.2.10}/LICENSE +0 -0
  33. {cachebox-6.2.9 → cachebox-6.2.10}/LICENSE-THIRD-PARTY +0 -0
  34. {cachebox-6.2.9 → cachebox-6.2.10}/README.md +0 -0
  35. {cachebox-6.2.9 → cachebox-6.2.10}/build.rs +0 -0
  36. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/__init__.py +0 -0
  37. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_cachebox.py +0 -0
  38. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_core.pyi +0 -0
  39. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/_wrappers.py +0 -0
  40. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/py.typed +0 -0
  41. {cachebox-6.2.9 → cachebox-6.2.10}/cachebox/utils.py +0 -0
  42. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/impls.md +0 -0
  43. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/index.md +0 -0
  44. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/api/utils.md +0 -0
  45. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/faq.md +0 -0
  46. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/getting-started.md +0 -0
  47. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/index.md +0 -0
  48. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/installation.md +0 -0
  49. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/migration.md +0 -0
  50. {cachebox-6.2.9 → cachebox-6.2.10}/docs/docs/tips.md +0 -0
  51. {cachebox-6.2.9 → cachebox-6.2.10}/docs/mkdocs.yml +0 -0
  52. {cachebox-6.2.9 → cachebox-6.2.10}/rust-toolchain.toml +0 -0
  53. {cachebox-6.2.9 → cachebox-6.2.10}/rustfmt.toml +0 -0
  54. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/alloc.rs +0 -0
  55. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/bitmask.rs +2 -2
  56. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/generic.rs +0 -0
  57. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/lsx.rs +0 -0
  58. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/mod.rs +2 -2
  59. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/neon.rs +0 -0
  60. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/group/sse2.rs +0 -0
  61. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/mod.rs +0 -0
  62. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/control/tag.rs +0 -0
  63. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/mod.rs +0 -0
  64. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/raw.rs +0 -0
  65. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/scopeguard.rs +0 -0
  66. {cachebox-6.2.9 → cachebox-6.2.10}/src/hashbrown/util.rs +0 -0
  67. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/alias.rs +0 -0
  68. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/lazyheap.rs +0 -0
  69. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/mod.rs +0 -0
  70. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/onceinit.rs +0 -0
  71. {cachebox-6.2.9 → cachebox-6.2.10}/src/internal/pickle.rs +0 -0
  72. {cachebox-6.2.9 → cachebox-6.2.10}/src/lib.rs +0 -0
  73. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/common.rs +0 -0
  74. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/lfupolicy.rs +0 -0
  75. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/lrupolicy.rs +0 -0
  76. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/mod.rs +0 -0
  77. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/nopolicy.rs +0 -0
  78. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/rrpolicy.rs +0 -0
  79. {cachebox-6.2.9 → cachebox-6.2.10}/src/policies/traits.rs +0 -0
  80. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/base.rs +0 -0
  81. {cachebox-6.2.9 → cachebox-6.2.10}/src/pyclasses/mod.rs +0 -0
  82. {cachebox-6.2.9 → cachebox-6.2.10}/tests/__init__.py +0 -0
  83. {cachebox-6.2.9 → cachebox-6.2.10}/tests/conftest.py +0 -0
  84. {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_policy_model.py +0 -0
  85. {cachebox-6.2.9 → cachebox-6.2.10}/tests/test_utils.py +0 -0
  86. {cachebox-6.2.9 → cachebox-6.2.10}/typos.toml +0 -0
@@ -37,7 +37,7 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
37
37
 
38
38
  [[package]]
39
39
  name = "cachebox"
40
- version = "6.2.9"
40
+ version = "6.2.10"
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.9"
3
+ version = "6.2.10"
4
4
  edition = "2024"
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.9
3
+ Version: 6.2.10
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -15,7 +15,8 @@ in **when** and **which** entries are removed under pressure (or over time).
15
15
  | [`TTLCache`](#ttlcache) | Global TTL + FIFO among live entries | Uniform freshness windows | Per-key TTLs needed |
16
16
  | [`VTTLCache`](#vttlcache) | Per-item TTL (optional non-expiring) | Mixed lifetimes | All items share one TTL |
17
17
 
18
- Complexity (worst-case, amortized where marked with `~`):
18
+ Complexity (expected, as for any hash table; amortized where marked with `~`; `e` is the number of
19
+ expired entries the call removes):
19
20
 
20
21
  | Class | get | insert | delete | popitem |
21
22
  | ----------- | ---- | -------------- | -------------- | ------------- |
@@ -24,9 +25,13 @@ Complexity (worst-case, amortized where marked with `~`):
24
25
  | `RRCache` | O(1) | O(1)~ | O(1) | O(1) expected |
25
26
  | `LRUCache` | O(1) | O(1)~ | O(1) | O(1) |
26
27
  | `LFUCache` | O(1) | O(log n)~ | O(log n) | O(log n)~ |
27
- | `TTLCache` | O(1) | O(e)~ | O(n) | O(1)~ |
28
+ | `TTLCache` | O(1) | O(e+1)~ | O(n) | O(1)~ |
28
29
  | `VTTLCache` | O(1) | O((e+1) log n) | O((e+1) log n) | O(log n)~ |
29
30
 
31
+ `LFUCache.get` only bumps a counter; the heap is repaired on the next eviction, so a `popitem`
32
+ after `k` reads takes O(k log n), which amortizes to O(log n) per read. Iterating an `LFUCache` or
33
+ a `VTTLCache` re-sorts all entries, O(n log n), when anything changed since the last iteration.
34
+
30
35
  ## Decision Guide
31
36
 
32
37
  ```
@@ -56,7 +61,7 @@ print(cache["key"]) # value
56
61
 
57
62
  # Overflow when full
58
63
  cache.update({i: i for i in range(200)})
59
- # OverflowError: The cache has reached the bound.
64
+ # OverflowError: The cache has no algorithm to evict items
60
65
 
61
66
  # popitem always fails — there is no eviction order
62
67
  cache.popitem() # OverflowError
@@ -138,7 +143,7 @@ print(cache.least_recently_used()) # still 2 if nothing else was accessed
138
143
  ## LFUCache
139
144
 
140
145
  Least-Frequently-Used. The key with the lowest access count is evicted first. Ties are broken by
141
- recency (older first).
146
+ insertion order (inserted earlier first).
142
147
 
143
148
  ```python
144
149
  from cachebox import LFUCache
@@ -208,9 +213,10 @@ cache3.stop_sweeper() # stop the daemon thread when done
208
213
 
209
214
  !!! note "Lazy expiry"
210
215
 
211
- Without `sweep_interval`, expired entries linger until the next interaction
212
- (`insert`, `get`, iteration, `current_size`, …). See
213
- [TTL sweepers](tips.md#ttlcachevttlcache-background-thread).
216
+ Without `sweep_interval`, expired entries are misses for `get` and `[]` but stay in the
217
+ cache, and `len()` / `bool()` still count them, until an insert, iteration, `current_size()`
218
+ or `expire()` removes them. See
219
+ [TTL sweepers](tips.md#ttlcache-vttlcache-background-thread).
214
220
 
215
221
  ---
216
222
 
@@ -227,7 +233,8 @@ import time
227
233
  # ttl= here applies only to the initial iterable, not as a global default
228
234
  cache = VTTLCache(100, iterable={i: i for i in range(4)}, ttl=3)
229
235
  time.sleep(3)
230
- print(len(cache)) # 0 after interaction/expire
236
+ cache.expire()
237
+ print(len(cache)) # 0
231
238
 
232
239
  cache.insert("session", "tok", ttl=5) # lives 5 seconds
233
240
  cache.insert("config", {"theme": "dark"}) # never expires
@@ -275,6 +282,7 @@ Regardless of policy:
275
282
  - **`getsizeof`** — optional weighted capacity (see [Getting Started](getting-started.md#capacity-size-and-weighted-entries)).
276
283
  - **`insert` preferred over `[]=`** when you need the previous value or cross-policy consistency.
277
284
  - **Iterators are one-shot** — not live dict views; modifying the cache while iterating raises
278
- `RuntimeError`.
285
+ `RuntimeError`. In an `LRUCache`, `get` and `[]` count as modifications (they reorder entries);
286
+ in an `LFUCache`, starting a new iteration after a read invalidates iterators already open.
279
287
 
280
288
  Full method lists: [API Reference — Classes](api/impls.md).
@@ -55,8 +55,8 @@ lint = [
55
55
  "ruff>=0.16.9",
56
56
  ]
57
57
  maturin = [
58
- "maturin == 1.14.1 ; sys_platform != 'linux'",
59
- "maturin[patchelf] == 1.14.1 ; sys_platform == 'linux'",
58
+ "maturin == 1.15.0 ; sys_platform != 'linux'",
59
+ "maturin[patchelf] == 1.15.0 ; sys_platform == 'linux'",
60
60
  ]
61
61
  test = [
62
62
  "hypothesis >= 6.165.0",
@@ -1,11 +1,9 @@
1
+ use std::alloc::Layout;
1
2
  use std::alloc::alloc;
2
3
  use std::alloc::dealloc;
3
4
  use std::alloc::handle_alloc_error;
4
- use std::alloc::Layout;
5
5
  use std::marker::PhantomData;
6
- use std::mem;
7
- use std::ptr::NonNull;
8
- use std::ptr::{self};
6
+ use std::ptr;
9
7
 
10
8
  /// Maximum number of nodes parked on the free list
11
9
  const FREE_LIST_CAPACITY: usize = 256;
@@ -42,7 +40,7 @@ pub struct Node<T> {
42
40
 
43
41
  /// A doubly-linked list with an internal, bounded free-list for node reuse.
44
42
  pub struct LinkedList<T> {
45
- sentinel: NonNull<Links>,
43
+ sentinel: ptr::NonNull<Links>,
46
44
  free_head: *mut Links,
47
45
  free_len: usize,
48
46
  len: usize,
@@ -61,7 +59,7 @@ impl<T> LinkedList<T> {
61
59
  /// - The caller must not use `node` again as an "unlinked" node until it is
62
60
  /// unlinked again.
63
61
  #[inline]
64
- unsafe fn push_front_node(&mut self, node: NonNull<Links>) {
62
+ unsafe fn push_front_node(&mut self, node: ptr::NonNull<Links>) {
65
63
  let s = self.sentinel.as_ptr();
66
64
  let n = node.as_ptr();
67
65
  unsafe {
@@ -80,7 +78,7 @@ impl<T> LinkedList<T> {
80
78
  ///
81
79
  /// Same contract as [`push_front_node`](Self::push_front_node).
82
80
  #[inline]
83
- unsafe fn push_back_node(&mut self, node: NonNull<Links>) {
81
+ unsafe fn push_back_node(&mut self, node: ptr::NonNull<Links>) {
84
82
  let s = self.sentinel.as_ptr();
85
83
  let n = node.as_ptr();
86
84
  unsafe {
@@ -95,7 +93,7 @@ impl<T> LinkedList<T> {
95
93
 
96
94
  /// Removes and returns the node at the front of the list, if any.
97
95
  #[inline]
98
- fn pop_front_node(&mut self) -> Option<NonNull<Links>> {
96
+ fn pop_front_node(&mut self) -> Option<ptr::NonNull<Links>> {
99
97
  if self.len == 0 {
100
98
  return None;
101
99
  }
@@ -110,12 +108,12 @@ impl<T> LinkedList<T> {
110
108
  };
111
109
  self.len -= 1;
112
110
  // SAFETY: read from a valid node above, so non-null.
113
- Some(unsafe { NonNull::new_unchecked(node) })
111
+ Some(unsafe { ptr::NonNull::new_unchecked(node) })
114
112
  }
115
113
 
116
114
  /// Removes and returns the node at the back of the list, if any.
117
115
  #[inline]
118
- fn pop_back_node(&mut self) -> Option<NonNull<Links>> {
116
+ fn pop_back_node(&mut self) -> Option<ptr::NonNull<Links>> {
119
117
  if self.len == 0 {
120
118
  return None;
121
119
  }
@@ -130,7 +128,7 @@ impl<T> LinkedList<T> {
130
128
  };
131
129
  self.len -= 1;
132
130
  // SAFETY: read from a valid node above, so non-null.
133
- Some(unsafe { NonNull::new_unchecked(node) })
131
+ Some(unsafe { ptr::NonNull::new_unchecked(node) })
134
132
  }
135
133
 
136
134
  /// Unlinks `node` from the list, without deallocating or reading its element.
@@ -139,7 +137,7 @@ impl<T> LinkedList<T> {
139
137
  ///
140
138
  /// `node` must point to a node currently linked into this list (not the sentinel).
141
139
  #[inline]
142
- unsafe fn unlink_node(&mut self, node: NonNull<Links>) {
140
+ unsafe fn unlink_node(&mut self, node: ptr::NonNull<Links>) {
143
141
  let n = node.as_ptr();
144
142
  unsafe {
145
143
  // SAFETY: `n` linked per fn contract; `prev`/`next` valid.
@@ -182,7 +180,7 @@ impl<T> LinkedList<T> {
182
180
  /// `node` must point to a node whose `element` is initialized and which is
183
181
  /// no longer linked into the list (already unlinked by the caller).
184
182
  #[inline]
185
- unsafe fn take_element_and_recycle(&mut self, node: NonNull<Links>) -> T {
183
+ unsafe fn take_element_and_recycle(&mut self, node: ptr::NonNull<Links>) -> T {
186
184
  let node_ptr = node.as_ptr() as *mut Node<T>;
187
185
  // SAFETY: `element` initialized per fn contract; `ptr::read` doesn't drop.
188
186
  let element = unsafe { ptr::read(&(*node_ptr).element) };
@@ -197,7 +195,7 @@ impl<T> LinkedList<T> {
197
195
  ///
198
196
  /// `node` must point to a node currently linked into this list
199
197
  #[inline]
200
- unsafe fn remove_node(&mut self, node: NonNull<Links>) -> T {
198
+ unsafe fn remove_node(&mut self, node: ptr::NonNull<Links>) -> T {
201
199
  // SAFETY: caller guarantees `node` is linked.
202
200
  unsafe { self.unlink_node(node) };
203
201
  self.len -= 1;
@@ -212,20 +210,23 @@ impl<T> LinkedList<T> {
212
210
  fn drop_nodes(&mut self) {
213
211
  let s = self.sentinel.as_ptr();
214
212
  while self.len != 0 {
215
- unsafe {
213
+ let element = unsafe {
216
214
  // SAFETY: `self.len != 0`, so `(*s).next` points to a real node.
217
215
  let node = (*s).next;
218
216
  let second = (*node).next;
219
217
  (*second).prev = s;
220
218
  (*s).next = second;
221
219
 
222
- let node_ptr = node as *mut Node<T>;
223
220
  // SAFETY: layout-compatible (see `Node` docs); `element` still init.
224
- ptr::drop_in_place(&mut (*node_ptr).element);
225
- // SAFETY: element dropped above; node already unlinked.
221
+ let element = ptr::read(&(*(node as *mut Node<T>)).element);
222
+ // SAFETY: element moved out above; node already unlinked.
226
223
  self.recycle_node(node);
227
- }
224
+ element
225
+ };
228
226
  self.len -= 1;
227
+ // Dropped last: if this panics, the list is already consistent and
228
+ // the drop guard can carry on.
229
+ drop(element);
229
230
  }
230
231
  }
231
232
 
@@ -302,7 +303,7 @@ impl<T> LinkedList<T> {
302
303
  p
303
304
  };
304
305
  // SAFETY: non-null checked above (or diverged).
305
- let sentinel = unsafe { NonNull::new_unchecked(raw) };
306
+ let sentinel = unsafe { ptr::NonNull::new_unchecked(raw) };
306
307
  LinkedList {
307
308
  sentinel,
308
309
  free_head: ptr::null_mut(),
@@ -348,7 +349,7 @@ impl<T> LinkedList<T> {
348
349
  let node = unsafe { (*s).next };
349
350
  // SAFETY: non-null above; layout-compatible (see `Node` docs).
350
351
  Some(Cursor(unsafe {
351
- NonNull::new_unchecked(node as *mut Node<T>)
352
+ ptr::NonNull::new_unchecked(node as *mut Node<T>)
352
353
  }))
353
354
  }
354
355
 
@@ -364,7 +365,7 @@ impl<T> LinkedList<T> {
364
365
  let node = unsafe { (*s).prev };
365
366
  // SAFETY: same as `cursor_front`.
366
367
  Some(Cursor(unsafe {
367
- NonNull::new_unchecked(node as *mut Node<T>)
368
+ ptr::NonNull::new_unchecked(node as *mut Node<T>)
368
369
  }))
369
370
  }
370
371
 
@@ -373,13 +374,13 @@ impl<T> LinkedList<T> {
373
374
  pub fn push_front(&mut self, elt: T) -> Cursor<T> {
374
375
  let node = self.get_node(elt);
375
376
  // SAFETY: `get_node` never returns null.
376
- let links = unsafe { NonNull::new_unchecked(node as *mut Links) };
377
+ let links = unsafe { ptr::NonNull::new_unchecked(node as *mut Links) };
377
378
 
378
379
  // SAFETY: freshly obtained node, unlinked (see `push_front_node` contract).
379
380
  unsafe { self.push_front_node(links) };
380
381
  self.len += 1;
381
382
  // SAFETY: same pointer validated above.
382
- Cursor(unsafe { NonNull::new_unchecked(node) })
383
+ Cursor(unsafe { ptr::NonNull::new_unchecked(node) })
383
384
  }
384
385
 
385
386
  /// Removes and returns the front element, or `None` if the list is empty.
@@ -397,14 +398,14 @@ impl<T> LinkedList<T> {
397
398
  pub fn push_back(&mut self, elt: T) -> Cursor<T> {
398
399
  let node = self.get_node(elt);
399
400
  // SAFETY: `get_node` always returns a non-null, freshly-usable `Node<T>` pointer.
400
- let links = unsafe { NonNull::new_unchecked(node as *mut Links) };
401
+ let links = unsafe { ptr::NonNull::new_unchecked(node as *mut Links) };
401
402
 
402
403
  // SAFETY: `links` refers to a node that was just obtained and is
403
404
  // not linked into any list yet.
404
405
  unsafe { self.push_back_node(links) };
405
406
  self.len += 1;
406
407
  // SAFETY: `node` is the same non-null pointer validated above.
407
- Cursor(unsafe { NonNull::new_unchecked(node) })
408
+ Cursor(unsafe { ptr::NonNull::new_unchecked(node) })
408
409
  }
409
410
 
410
411
  /// Removes and returns the back element, or `None` if the list is empty.
@@ -439,17 +440,22 @@ impl<T> LinkedList<T> {
439
440
  }
440
441
  }
441
442
 
442
- // Guard ensures that if dropping an element panics, we still free
443
- // the free-list allocations instead of leaking them (the sentinel
444
- // is freed unconditionally below regardless of panics).
443
+ // Frees the rest of the list when it goes out of scope, which also covers an
444
+ // element destructor panicking in `LinkedList::drop`: the remaining elements,
445
+ // the free list and the sentinel are freed either way.
445
446
  struct DropGuard<'a, T>(&'a mut LinkedList<T>);
446
447
 
447
448
  impl<'a, T> Drop for DropGuard<'a, T> {
448
449
  fn drop(&mut self) {
449
450
  self.0.drop_nodes();
450
- // SAFETY: `self` is being torn down during unwind; no other
451
- // references to the free list exist, satisfying `drop_freelist`'s contract.
452
- unsafe { self.0.drop_freelist() };
451
+ unsafe {
452
+ // SAFETY: the list is being destroyed; no other references to the
453
+ // free list exist, satisfying `drop_freelist`'s contract.
454
+ self.0.drop_freelist();
455
+ // SAFETY: `sentinel` was allocated in `new` with exactly this
456
+ // layout and is never dereferenced after this point.
457
+ dealloc(self.0.sentinel.as_ptr() as *mut u8, Layout::new::<Links>());
458
+ }
453
459
  }
454
460
  }
455
461
 
@@ -457,16 +463,7 @@ impl<T> Drop for LinkedList<T> {
457
463
  fn drop(&mut self) {
458
464
  let guard = DropGuard(self);
459
465
  guard.0.drop_nodes();
460
- // SAFETY: `drop_nodes` completed without panicking; the free list is
461
- // only touched here and then never again (guard is forgotten next).
462
- unsafe { guard.0.drop_freelist() };
463
- mem::forget(guard);
464
- // SAFETY: `self.sentinel` was allocated in `new` with exactly this
465
- // layout (`Layout::new::<Links>()`), and after this point `self` is
466
- // being destroyed, so the pointer is never dereferenced again.
467
- unsafe {
468
- dealloc(self.sentinel.as_ptr() as *mut u8, Layout::new::<Links>());
469
- }
466
+ // `guard` drops here and frees the free list and the sentinel.
470
467
  }
471
468
  }
472
469
 
@@ -478,7 +475,7 @@ impl<T> Drop for LinkedList<T> {
478
475
  /// later access or reposition that node via [`Cursor::element`],
479
476
  /// [`Cursor::move_to_front`], [`Cursor::move_to_back`], or [`Cursor::unlink`].
480
477
  ///
481
- /// `#[repr(transparent)]` over `NonNull<Node<T>>` means a `Cursor<T>` has the
478
+ /// `#[repr(transparent)]` over `ptr::NonNull<Node<T>>` means a `Cursor<T>` has the
482
479
  /// exact same layout as the raw pointer it wraps — no extra state is tracked,
483
480
  /// so the cursor does *not* know which `LinkedList` it came from, whether the
484
481
  /// node is still linked, or whether other `Cursor`s alias the same node. All
@@ -489,9 +486,9 @@ impl<T> Drop for LinkedList<T> {
489
486
  /// equality (`PartialEq`/`Eq`) compares the underlying pointer, i.e. identity
490
487
  /// of the node, not the value of the element.
491
488
  #[repr(transparent)]
492
- pub struct Cursor<T>(NonNull<Node<T>>);
489
+ pub struct Cursor<T>(ptr::NonNull<Node<T>>);
493
490
 
494
- // `NonNull<Node<T>>` is just a pointer; copying it is always safe.
491
+ // `ptr::NonNull<Node<T>>` is just a pointer; copying it is always safe.
495
492
  impl<T> Clone for Cursor<T> {
496
493
  #[inline]
497
494
  fn clone(&self) -> Self {
@@ -516,10 +513,10 @@ impl<T> Cursor<T> {
516
513
  /// Relies on `#[repr(C)]` on `Node<T>` placing `links` at offset 0, so the
517
514
  /// cast is always valid regardless of `T`.
518
515
  #[inline]
519
- fn links(&self) -> NonNull<Links> {
516
+ fn links(&self) -> ptr::NonNull<Links> {
520
517
  let ptr = self.0.as_ptr() as *mut Links;
521
- // SAFETY: `self.0` is `NonNull`, so the reinterpreted pointer is non-null too.
522
- unsafe { NonNull::new_unchecked(ptr) }
518
+ // SAFETY: `self.0` is `ptr::NonNull`, so the reinterpreted pointer is non-null too.
519
+ unsafe { ptr::NonNull::new_unchecked(ptr) }
523
520
  }
524
521
 
525
522
  /// Returns a reference to the node's element, with a caller-chosen lifetime.
@@ -621,7 +618,7 @@ impl<T> Cursor<T> {
621
618
  pub struct RawIter<T> {
622
619
  next: *mut Links,
623
620
  len: usize,
624
- _marker: PhantomData<NonNull<Node<T>>>,
621
+ _marker: PhantomData<ptr::NonNull<Node<T>>>,
625
622
  }
626
623
 
627
624
  impl<T> Iterator for RawIter<T> {
@@ -641,7 +638,7 @@ impl<T> Iterator for RawIter<T> {
641
638
  self.next = unsafe { (*node).next };
642
639
  // SAFETY: `node` is non-null (came from a valid linked `Links`), and
643
640
  // by `#[repr(C)]` on `Node<T>` a `*mut Links` is a valid `*mut Node<T>`.
644
- let cursor = Cursor(unsafe { NonNull::new_unchecked(node as *mut Node<T>) });
641
+ let cursor = Cursor(unsafe { ptr::NonNull::new_unchecked(node as *mut Node<T>) });
645
642
  Some(cursor)
646
643
  }
647
644
 
@@ -659,3 +656,36 @@ unsafe impl<T: Send + Send> Send for RawIter<T> {}
659
656
  unsafe impl<T: Sync + Sync> Sync for RawIter<T> {}
660
657
  unsafe impl<T: Send + Send> Send for Cursor<T> {}
661
658
  unsafe impl<T: Sync + Sync> Sync for Cursor<T> {}
659
+
660
+ #[cfg(test)]
661
+ mod tests {
662
+ use super::LinkedList;
663
+
664
+ #[test]
665
+ fn a_panicking_element_drop_still_frees_every_node() {
666
+ struct Bomb {
667
+ panics: bool,
668
+ _label: String,
669
+ }
670
+ impl Drop for Bomb {
671
+ fn drop(&mut self) {
672
+ if self.panics {
673
+ panic!("boom");
674
+ }
675
+ }
676
+ }
677
+
678
+ let mut list = LinkedList::new();
679
+ for i in 0..6 {
680
+ list.push_back(Bomb {
681
+ panics: i == 2,
682
+ _label: i.to_string(),
683
+ });
684
+ }
685
+ list.pop_back(); // leave a node on the free list too
686
+ let dropped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(list)));
687
+ assert!(dropped.is_err());
688
+ // the other elements, the free list and the sentinel are freed; under
689
+ // Miri this also checks that nothing leaks
690
+ }
691
+ }
@@ -16,13 +16,23 @@ pub unsafe fn pyobject_hash(
16
16
  ) -> pyo3::PyResult<u64> {
17
17
  let py_hash = pyo3::ffi::PyObject_Hash(arg1);
18
18
  if crate::hashbrown::util::unlikely(py_hash == -1) {
19
- // SAFETY: PyObject_Hash never returns -1 on success.
20
- return Err(pyo3::PyErr::take(py).unwrap());
19
+ // PyObject_Hash never returns -1 on success.
20
+ return Err(take_error(py));
21
21
  }
22
22
 
23
23
  Ok(mix_hash(py_hash as u64))
24
24
  }
25
25
 
26
+ /// Takes the exception set by a failed C API call.
27
+ ///
28
+ /// A broken C type can report an error without setting one; like CPython, raise
29
+ /// `SystemError` then instead of panicking (release builds abort on panic).
30
+ #[cold]
31
+ fn take_error(py: pyo3::Python<'_>) -> pyo3::PyErr {
32
+ pyo3::PyErr::take(py)
33
+ .unwrap_or_else(|| new_py_error!(PySystemError, "error return without exception set"))
34
+ }
35
+
26
36
  /// Spreads entropy of a Python hash across all 64 bits.
27
37
  ///
28
38
  /// CPython hashes are frequently low-entropy in the high bits: `hash(n) == n` for
@@ -55,7 +65,7 @@ pub unsafe fn pyobject_equal(
55
65
  let boolean = pyo3::ffi::PyObject_RichCompareBool(arg1, arg2, pyo3::ffi::Py_EQ);
56
66
 
57
67
  if boolean < 0 {
58
- Err(pyo3::PyErr::take(py).unwrap_unchecked())
68
+ Err(take_error(py))
59
69
  } else {
60
70
  Ok(boolean == 1)
61
71
  }
@@ -54,7 +54,7 @@ macro_rules! implement_view_guard {
54
54
  let cache = self.cache.get();
55
55
  let inner = cache.0.get()?;
56
56
 
57
- let policy = inner.policy_py_attached(py);
57
+ let policy = inner.policy(py);
58
58
 
59
59
  if self.initial_gv == inner.shared().generation_version().get() {
60
60
  return Ok(policy);
@@ -166,10 +166,11 @@ impl FIFOPolicy {
166
166
 
167
167
  #[inline]
168
168
  fn decrement_indexes(&mut self, start: usize, end: usize) {
169
- const MAX_FRONT_OFFSET: usize = cfg_select! {
170
- feature = "use-small-offset" => u8::MAX as usize,
171
- not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
172
- };
169
+ const MAX_FRONT_OFFSET: usize =
170
+ cfg_select! {
171
+ feature = "use-small-offset" => u8::MAX as usize,
172
+ not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
173
+ };
173
174
 
174
175
  // Fast path: shifting the entire front is a single counter increment.
175
176
  // Guard against overflow; the full-normalization path below handles that case.
@@ -256,10 +256,11 @@ impl TTLPolicy {
256
256
 
257
257
  #[inline]
258
258
  fn decrement_indexes(&mut self, start: usize, end: usize) {
259
- const MAX_FRONT_OFFSET: usize = cfg_select! {
260
- feature = "use-small-offset" => u8::MAX as usize,
261
- not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
262
- };
259
+ const MAX_FRONT_OFFSET: usize =
260
+ cfg_select! {
261
+ feature = "use-small-offset" => u8::MAX as usize,
262
+ not(feature = "use-small-offset") => usize::MAX - isize::MAX as usize,
263
+ };
263
264
 
264
265
  // Fast path: shifting the entire front is a single counter increment.
265
266
  // Guard against overflow; the full-normalization path below handles that case.
@@ -422,12 +423,11 @@ impl PolicyExt for TTLPolicy {
422
423
  }
423
424
 
424
425
  fn evict(&mut self, shared: &Self::Shared) -> pyo3::PyResult<Self::Handle> {
425
- let front = self.entries.front();
426
- if front.is_none() {
427
- return Err(new_py_error!(PyKeyError, ()));
428
- }
426
+ self.expire(shared.generation_version());
429
427
 
430
- let front = unsafe { front.unwrap_unchecked() };
428
+ let Some(front) = self.entries.front() else {
429
+ return Err(new_py_error!(PyKeyError, ()));
430
+ };
431
431
 
432
432
  let eq = |index: &usize| Ok::<_, pyo3::PyErr>(*index - self.front_offset == 0);
433
433
  let removed_key_table = self.table.remove_entry(front.key().hash(), eq).unwrap();
@@ -391,11 +391,12 @@ impl PolicyExt for VTTLPolicy {
391
391
  }
392
392
 
393
393
  fn evict(&mut self, shared: &Self::Shared) -> pyo3::PyResult<Self::Handle> {
394
+ self.expire(shared.generation_version());
395
+
394
396
  {
395
- let front_cursor = self
396
- .heap
397
- .front()
398
- .ok_or_else(|| new_py_error!(PyKeyError, "cache is empty"))?;
397
+ let Some(front_cursor) = self.heap.front() else {
398
+ return Err(new_py_error!(PyKeyError, "cache is empty"));
399
+ };
399
400
 
400
401
  self.table
401
402
  .remove_entry(unsafe { front_cursor.element().key.hash() }, |x| {