mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-19 07:53:00 +00:00
Add FrozenDict for caching parsed_scopes dicts
Since we need them to be immutable
This commit is contained in:
@@ -88,3 +88,50 @@ def lru_cache_key(key_func, maxsize=1024):
|
|||||||
return cached
|
return cached
|
||||||
|
|
||||||
return cache_func
|
return cache_func
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenDict(dict):
|
||||||
|
"""A frozen dictionary subclass
|
||||||
|
|
||||||
|
Immutable and hashable, so it can be used as a cache key
|
||||||
|
|
||||||
|
Values will be frozen with `.freeze(value)`
|
||||||
|
and must be hashable after freezing.
|
||||||
|
|
||||||
|
Not rigorous, but enough for our purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_hash = None
|
||||||
|
|
||||||
|
def __init__(self, d):
|
||||||
|
dict_set = dict.__setitem__
|
||||||
|
for key, value in d.items():
|
||||||
|
dict.__setitem__(self, key, self._freeze(value))
|
||||||
|
|
||||||
|
def _freeze(self, item):
|
||||||
|
"""Make values of a dict hashable
|
||||||
|
- list, set -> frozenset
|
||||||
|
- dict -> recursive _FrozenDict
|
||||||
|
- anything else: assumed hashable
|
||||||
|
"""
|
||||||
|
if isinstance(item, FrozenDict):
|
||||||
|
return item
|
||||||
|
elif isinstance(item, (set, list)):
|
||||||
|
return frozenset(item)
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
return FrozenDict(item)
|
||||||
|
else:
|
||||||
|
# any other type is assumed hashable
|
||||||
|
return item
|
||||||
|
|
||||||
|
def __setitem__(self, key):
|
||||||
|
raise RuntimeError("Cannot modify frozen {type(self).__name__}")
|
||||||
|
|
||||||
|
def update(self, other):
|
||||||
|
raise RuntimeError("Cannot modify frozen {type(self).__name__}")
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""Cache hash because we are immutable"""
|
||||||
|
if self._hash is None:
|
||||||
|
self._hash = hash(tuple((key, value) for key, value in self.items()))
|
||||||
|
return self._hash
|
||||||
|
@@ -24,7 +24,7 @@ from tornado import web
|
|||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
from . import orm, roles
|
from . import orm, roles
|
||||||
from ._memoize import DoNotCache, lru_cache_key
|
from ._memoize import DoNotCache, FrozenDict, lru_cache_key
|
||||||
|
|
||||||
"""when modifying the scope definitions, make sure that `docs/source/rbac/generate-scope-table.py` is run
|
"""when modifying the scope definitions, make sure that `docs/source/rbac/generate-scope-table.py` is run
|
||||||
so that changes are reflected in the documentation and REST API description."""
|
so that changes are reflected in the documentation and REST API description."""
|
||||||
@@ -698,13 +698,10 @@ def parse_scopes(scope_list):
|
|||||||
parsed_scopes[base_scope][key] = {value}
|
parsed_scopes[base_scope][key] = {value}
|
||||||
else:
|
else:
|
||||||
parsed_scopes[base_scope][key].add(value)
|
parsed_scopes[base_scope][key].add(value)
|
||||||
return parsed_scopes
|
return FrozenDict(parsed_scopes)
|
||||||
|
|
||||||
|
|
||||||
# Note: it doesn't make sense to cache unparse_scopes
|
|
||||||
# because computing the cache key is as expensive as the function itself
|
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache_key(FrozenDict)
|
||||||
def unparse_scopes(parsed_scopes):
|
def unparse_scopes(parsed_scopes):
|
||||||
"""Turn a parsed_scopes dictionary back into a expanded scopes set"""
|
"""Turn a parsed_scopes dictionary back into a expanded scopes set"""
|
||||||
expanded_scopes = set()
|
expanded_scopes = set()
|
||||||
@@ -722,7 +719,7 @@ def unparse_scopes(parsed_scopes):
|
|||||||
def reduce_scopes(expanded_scopes):
|
def reduce_scopes(expanded_scopes):
|
||||||
"""Reduce expanded scopes to minimal set
|
"""Reduce expanded scopes to minimal set
|
||||||
|
|
||||||
Eliminates redundancy, such as access:services and access:services!service=x
|
Eliminates overlapping scopes, such as access:services and access:services!service=x
|
||||||
"""
|
"""
|
||||||
return unparse_scopes(parse_scopes(expanded_scopes))
|
return unparse_scopes(parse_scopes(expanded_scopes))
|
||||||
|
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
from jupyterhub._memoize import DoNotCache, LRUCache, lru_cache_key
|
import pytest
|
||||||
|
|
||||||
|
from jupyterhub._memoize import DoNotCache, FrozenDict, LRUCache, lru_cache_key
|
||||||
|
|
||||||
|
|
||||||
def test_lru_cache():
|
def test_lru_cache():
|
||||||
@@ -73,3 +75,20 @@ def test_do_not_cache():
|
|||||||
before = call_count
|
before = call_count
|
||||||
assert is_even(1) == False
|
assert is_even(1) == False
|
||||||
assert call_count == before + 1
|
assert call_count == before + 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"d",
|
||||||
|
[
|
||||||
|
{"key": "value"},
|
||||||
|
{"key": ["list"]},
|
||||||
|
{"key": {"set"}},
|
||||||
|
{"key": ("tu", "ple")},
|
||||||
|
{"key": {"nested": ["dict"]}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_frozen_dict(d):
|
||||||
|
frozen_1 = FrozenDict(d)
|
||||||
|
frozen_2 = FrozenDict(d)
|
||||||
|
assert hash(frozen_1) == hash(frozen_2)
|
||||||
|
assert frozen_1 == frozen_2
|
||||||
|
Reference in New Issue
Block a user