Merge pull request 'clan_lib flake: fix handling of maybes and empty sets' (#4890) from select_fix into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4890
This commit is contained in:
lassulus
2025-08-22 22:31:29 +00:00
2 changed files with 67 additions and 16 deletions

View File

@@ -279,11 +279,15 @@ def parse_selector(selector: str) -> list[Selector]:
submode = "" submode = ""
acc_str = "" acc_str = ""
elif c == "}": elif c == "}":
if submode == "maybe": # Only append selector if we have accumulated content
set_select_type = SetSelectorType.MAYBE if acc_str != "" or submode != "":
else: if submode == "maybe":
set_select_type = SetSelectorType.STR set_select_type = SetSelectorType.MAYBE
acc_selectors.append(SetSelector(type=set_select_type, value=acc_str)) else:
set_select_type = SetSelectorType.STR
acc_selectors.append(
SetSelector(type=set_select_type, value=acc_str)
)
# Check for invalid multiselect patterns with outPath # Check for invalid multiselect patterns with outPath
for subselector in acc_selectors: for subselector in acc_selectors:
if subselector.value == "outPath": if subselector.value == "outPath":
@@ -554,16 +558,37 @@ class FlakeCacheEntry:
return self.value[selector.value].select(selectors[1:]) return self.value[selector.value].select(selectors[1:])
# if we are a MAYBE selector, we check if the key exists in the dict # if we are a MAYBE selector, we check if the key exists in the dict
if selector.type == SelectorType.MAYBE and isinstance(self.value, dict): if selector.type == SelectorType.MAYBE:
assert isinstance(selector.value, str) assert isinstance(selector.value, str)
if selector.value in self.value: if isinstance(self.value, dict):
if self.value[selector.value].exists: if selector.value in self.value:
return { if self.value[selector.value].exists:
selector.value: self.value[selector.value].select(selectors[1:]) return {
} selector.value: self.value[selector.value].select(
selectors[1:]
)
}
return {}
# Key not found, return empty dict for MAYBE selector
return {} return {}
if self.fetched_all: # Non-dict value (including None), return empty dict for MAYBE selector
return {}
# Handle SET selector on non-dict values
if selector.type == SelectorType.SET and not isinstance(self.value, dict):
assert isinstance(selector.value, list)
# Empty set or all sub-selectors are MAYBE
if len(selector.value) == 0:
# Empty set, return empty dict
return {} return {}
all_maybe = all(
subselector.type == SetSelectorType.MAYBE
for subselector in selector.value
)
if all_maybe:
# All sub-selectors are MAYBE, return empty dict for non-dict values
return {}
# Not all sub-selectors are MAYBE, fall through to raise KeyError
# otherwise we return a list or a dict # otherwise we return a list or a dict
if isinstance(self.value, dict): if isinstance(self.value, dict):
@@ -590,13 +615,24 @@ class FlakeCacheEntry:
# if we are a list, return a list # if we are a list, return a list
if self.is_list: if self.is_list:
result = [] result_list: list[Any] = []
for index in keys_to_select: for index in keys_to_select:
result.append(self.value[index].select(selectors[1:])) result_list.append(self.value[index].select(selectors[1:]))
return result return result_list
# otherwise return a dict # otherwise return a dict
return {k: self.value[k].select(selectors[1:]) for k in keys_to_select} result_dict: dict[str, Any] = {}
for key in keys_to_select:
value = self.value[key].select(selectors[1:])
if self.value[key].exists:
# Skip empty dicts when the original value is None
if not (
isinstance(value, dict)
and len(value) == 0
and self.value[key].value is None
):
result_dict[key] = value
return result_dict
# return a KeyError if we cannot fetch the key # return a KeyError if we cannot fetch the key
str_selector: str str_selector: str

View File

@@ -157,6 +157,21 @@ def test_select() -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
test_cache.select(selectors) test_cache.select(selectors)
testdict2 = {"x": {"y": {"a": 1, "b": 2}, "z": None}, "n": {}}
test_cache.insert(testdict2, parse_selector("testdict2"))
assert test_cache.select(parse_selector("testdict2.n")) == {}
assert test_cache.select(parse_selector("testdict2.?n")) == {"n": {}}
assert test_cache.select(parse_selector("testdict2.x.*.?a")) == {"y": {"a": 1}}
assert test_cache.select(parse_selector("testdict2.x.z.?a")) == {}
assert test_cache.select(parse_selector("testdict2.x.z.{?a}")) == {}
assert test_cache.select(parse_selector("testdict2.x.z.{}")) == {}
with pytest.raises(KeyError):
test_cache.select(parse_selector("testdict2.x.z.a"))
with pytest.raises(KeyError):
test_cache.select(parse_selector("testdict2.x.z.{a}"))
with pytest.raises(KeyError):
test_cache.select(parse_selector("testdict2.x.z.{a,?b}"))
def test_out_path() -> None: def test_out_path() -> None:
testdict = {"x": {"y": [123, 345, 456], "z": "/nix/store/abc-bla"}} testdict = {"x": {"y": [123, 345, 456], "z": "/nix/store/abc-bla"}}