Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions autoconf/json_prior/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ def path_for_class(cls) -> List[str]:
return f"{cls.__module__}.{cls.__name__}".split(".")


# Sentinels for the per-instance lookup cache: a query can legitimately resolve to
# None, and the class-family probe in for_class_and_suffix_path relies on repeated
# expected misses, so both found-None and not-found must be cacheable.
_UNCACHED = object()
_NOT_FOUND = object()


class JSONPriorConfig:
def __init__(self, config_dict: dict, directory=None):
"""
Expand All @@ -79,6 +86,8 @@ def __init__(self, config_dict: dict, directory=None):
self.obj = config_dict
self.directory = directory
self._path_value_map = None
self._path_value_tuples = None
self._lookup_cache = {}

@property
def paths(self):
Expand Down Expand Up @@ -109,12 +118,18 @@ def path_value_tuples(self) -> List[Tuple[str, object]]:
"""
Tuple pairs matching every possible path to the configuration it points to.
These are ordered by key length with the longest key first.

Cached on the instance — this is consulted for every prior of every model
construction, and re-sorting the flattened configuration per lookup
dominated model deserialization.
"""
return sorted(
list(self.path_value_map.items()),
key=lambda item: len(item[0]),
reverse=True,
)
if self._path_value_tuples is None:
self._path_value_tuples = sorted(
list(self.path_value_map.items()),
key=lambda item: len(item[0]),
reverse=True,
)
return self._path_value_tuples

@classmethod
def from_directory(cls, directory: str) -> "JSONPriorConfig":
Expand Down Expand Up @@ -209,9 +224,20 @@ def __call__(self, config_path: List[str]):
If no configuration is found.
"""
key = ".".join(config_path)
cached = self._lookup_cache.get(key, _UNCACHED)
if cached is _NOT_FOUND:
raise KeyError(
f"No configuration was found for the path {config_path}"
+ ("" if self.directory is None else f" ({self.directory})")
)
if cached is not _UNCACHED:
return cached

for path, value in self.path_value_tuples:
if key.endswith(path):
self._lookup_cache[key] = value
return value
self._lookup_cache[key] = _NOT_FOUND
raise KeyError(
f"No configuration was found for the path {config_path}"
+ ("" if self.directory is None else f" ({self.directory})")
Expand Down
19 changes: 19 additions & 0 deletions test_autoconf/json_prior/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@


def test_lookup_cache_repeats_and_misses():
"""
Lookups are memoised per instance — including misses, which the class-family
probe in for_class_and_suffix_path performs repeatedly by design.
"""
import pytest
from autoconf.json_prior.config import JSONPriorConfig

config = JSONPriorConfig({"module.Class": {"value": 1}})

assert config(["pkg", "module", "Class"]) == {"value": 1}
assert config(["pkg", "module", "Class"]) == {"value": 1}

with pytest.raises(KeyError):
config(["pkg", "module", "Missing"])
with pytest.raises(KeyError):
config(["pkg", "module", "Missing"])
Loading