@@ -428,6 +428,42 @@ def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
428428 return rv
429429
430430
431+ class _ImmutableKey :
432+ """Wrapper that makes any key hashable for storage in the deque.
433+
434+ Python 3.14 raises TypeError when attempting to use an unhashable
435+ type (e.g. a dict) inside a tuple as a dict key, because
436+ ``deque.remove()`` calls ``__hash__`` during the linear search.
437+ This wrapper uses ``object.__hash__`` (identity-based hashing),
438+ sidestepping the issue entirely.
439+ """
440+
441+ __slots__ = ("_key" , "_hash" )
442+
443+ def __init__ (self , key : t .Any ) -> None :
444+ self ._key = key
445+ self ._hash = object .__hash__ (key )
446+
447+ def __hash__ (self ) -> int :
448+ return self ._hash
449+
450+ def __eq__ (self , other : object ) -> bool :
451+ if isinstance (other , _ImmutableKey ):
452+ return self ._key is other ._key
453+ return NotImplemented
454+
455+ def __getstate__ (self ) -> dict [str , t .Any ]:
456+ return {"_key" : self ._key }
457+
458+ def __setstate__ (self , state : dict [str , t .Any ]) -> None :
459+ self ._key = state ["_key" ]
460+ self ._hash = object .__hash__ (self ._key )
461+
462+ @property
463+ def key (self ) -> t .Any :
464+ return self ._key
465+
466+
431467@abc .MutableMapping .register
432468class LRUCache :
433469 """A simple LRU Cache implementation."""
@@ -438,8 +474,8 @@ class LRUCache:
438474
439475 def __init__ (self , capacity : int ) -> None :
440476 self .capacity = capacity
441- self ._mapping : dict [t . Any , t .Any ] = {}
442- self ._queue : deque [t . Any ] = deque ()
477+ self ._mapping : dict [_ImmutableKey , t .Any ] = {}
478+ self ._queue : deque [_ImmutableKey ] = deque ()
443479 self ._postinit ()
444480
445481 def _postinit (self ) -> None :
@@ -496,14 +532,14 @@ def clear(self) -> None:
496532
497533 def __contains__ (self , key : t .Any ) -> bool :
498534 """Check if a key exists in this cache."""
499- return key in self ._mapping
535+ return _ImmutableKey ( key ) in self ._mapping
500536
501537 def __len__ (self ) -> int :
502538 """Return the current size of the cache."""
503539 return len (self ._mapping )
504540
505541 def __repr__ (self ) -> str :
506- return f"<{ type (self ).__name__ } { self . _mapping !r } >"
542+ return f"<{ type (self ).__name__ } {{ { ', ' . join ( f' { k !r } : { v !r } ' for k , v in self . items ()) } } }>"
507543
508544 def __getitem__ (self , key : t .Any ) -> t .Any :
509545 """Get an item from the cache. Moves the item up so that it has the
@@ -512,18 +548,12 @@ def __getitem__(self, key: t.Any) -> t.Any:
512548 Raise a `KeyError` if it does not exist.
513549 """
514550 with self ._wlock :
515- rv = self ._mapping [key ]
516-
517- if self ._queue [- 1 ] != key :
518- try :
519- self ._remove (key )
520- except ValueError :
521- # if something removed the key from the container
522- # when we read, ignore the ValueError that we would
523- # get otherwise.
524- pass
551+ ikey = _ImmutableKey (key )
552+ rv = self ._mapping [ikey ]
525553
526- self ._append (key )
554+ if self ._queue [- 1 ] != ikey :
555+ self ._queue .remove (ikey )
556+ self ._append (ikey )
527557
528558 return rv
529559
@@ -532,48 +562,45 @@ def __setitem__(self, key: t.Any, value: t.Any) -> None:
532562 has the highest priority then.
533563 """
534564 with self ._wlock :
535- if key in self ._mapping :
536- self ._remove (key )
565+ ikey = _ImmutableKey (key )
566+ if ikey in self ._mapping :
567+ self ._queue .remove (ikey )
537568 elif len (self ._mapping ) == self .capacity :
538569 del self ._mapping [self ._popleft ()]
539570
540- self ._append (key )
541- self ._mapping [key ] = value
571+ self ._append (ikey )
572+ self ._mapping [ikey ] = value
542573
543574 def __delitem__ (self , key : t .Any ) -> None :
544575 """Remove an item from the cache dict.
545576 Raise a `KeyError` if it does not exist.
546577 """
547578 with self ._wlock :
548- del self ._mapping [key ]
549-
550- try :
551- self ._remove (key )
552- except ValueError :
553- pass
579+ ikey = _ImmutableKey (key )
580+ del self ._mapping [ikey ]
581+ self ._queue .remove (ikey )
554582
555583 def items (self ) -> t .Iterable [tuple [t .Any , t .Any ]]:
556584 """Return a list of items."""
557- result = [(key , self ._mapping [key ]) for key in list (self ._queue )]
558- result .reverse ()
585+ result = [(ikey .key , self ._mapping [ikey ]) for ikey in reversed (self ._queue )]
559586 return result
560587
561588 def values (self ) -> t .Iterable [t .Any ]:
562589 """Return a list of all values."""
563- return [x [ 1 ] for x in self .items ()]
590+ return [v for _ , v in self .items ()]
564591
565592 def keys (self ) -> t .Iterable [t .Any ]:
566593 """Return a list of all keys ordered by most recent usage."""
567594 return list (self )
568595
569596 def __iter__ (self ) -> t .Iterator [t .Any ]:
570- return reversed (tuple (self ._queue ))
597+ return ( ikey . key for ikey in reversed (tuple (self ._queue ) ))
571598
572599 def __reversed__ (self ) -> t .Iterator [t .Any ]:
573600 """Iterate over the keys in the cache dict, oldest items
574601 coming first.
575602 """
576- return iter ( tuple (self ._queue ))
603+ return ( ikey . key for ikey in tuple (self ._queue ))
577604
578605 __copy__ = copy
579606
0 commit comments