From 63bc5efdc3f9fbed5bade2366d2e59c6009c2bd8 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 21 Sep 2026 05:31:38 +0200 Subject: [PATCH] App.get_project_refs(): Fix multi-section results get_project_refs() takes a list of sections to look for references in. To now, that has never been exercised, i.e. only one section had ever been passed in. With the introduction of pkg.recommends, that could change now, and it has a subtle bug. Say, a caller wants to retrieve all requires and recommends of package A with this graph: A recommends B, requires nothing B requires C then A, B, C should be returned, but the current implementation only returns A and B. The cause is that every section is recursed into in isolation, and the union of all results is returned. Since A doesn't require anything, the requires-branch never reaches B, and C is missed. The fix is to make __get_project_refs(), the recursing method, take a list of sections instead of one section, and decide if it recurses deeper based on all of its entries at every depth. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.86.1 --- src/python/jw/pkg/App.py | 55 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 32a4db97..8ee0f687 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -203,7 +203,7 @@ class App(Base): buf: list[str], visited: set[str], spec: str, - section: str, + sections: list[str], key: str, add_self: bool, scope: Scope, @@ -230,29 +230,33 @@ class App(Base): f'Unmet dependency on {mod}: the -{needed_subpackage} package ' f'of project {name} is not installed' ) - vals = self.get_value(name, section, key) - log( - DEBUG, - ( - f'name={name}, section={section}, key={key}, deps={vals}, ' - f'scope={scope.name}, visited={visited}' - ), - ) - vals_list = vals.split(',') if vals else [] + vals_list: list[str] = [] + for section in sections: + vals = self.get_value(name, section, key) + log( + DEBUG, + ( + f'name={name}, section={section}, key={key}, deps={vals}, ' + f'scope={scope.name}, visited={visited}' + ), + ) + if not vals: + continue + for val in vals.split(','): + val = val.strip() + if (len(val)) and (val not in vals_list): + vals_list.append(val) match scope: case Scope.Self: buf += vals_list case Scope.One | Scope.Subtree: subscope = scope.Self if scope == Scope.One else scope for val in vals_list: - val = val.strip() - if not (len(val)): - continue self.__get_project_refs( buf, visited, val, - section, + sections, key, add_self = True, scope = subscope, @@ -559,18 +563,17 @@ class App(Base): if isinstance(keys, str): keys = [keys] ret: list[str] = [] - for section in sections: - for key in keys: - visited: set[str] = set() - for name in projects: - rr: list[str] = [] - self.__get_project_refs( - rr, visited, name, section, key, add_self, scope, names_only - ) - # TODO: this looks like a performance hogger - for m in rr: - if m not in ret: - ret.append(m) + for key in keys: + visited: set[str] = set() + for name in projects: + rr: list[str] = [] + self.__get_project_refs( + rr, visited, name, sections, key, add_self, scope, names_only + ) + # TODO: this looks like a performance hogger + for m in rr: + if m not in ret: + ret.append(m) return ret def get_libname(self, spec: str) -> str | None: