diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 8ee0f687..dabb97ef 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -198,7 +198,28 @@ class App(Base): except FileNotFoundError: return None - def __get_project_refs( + def read_dep_edges( + self, + name: str, + sections: list[str], + key: str, + ) -> list[str]: + """The deduplicated dependency values of name for the given + sections and key (flavour), in section and value order. + """ + ret: list[str] = [] + for section in sections: + vals = self.get_value(name, section, key) + log(DEBUG, f'name={name}, section={section}, key={key}, deps={vals}') + if not vals: + continue + for val in vals.split(','): + val = val.strip() + if (len(val)) and (val not in ret): + ret.append(val) + return ret + + def walk_project_deps( self, buf: list[str], visited: set[str], @@ -208,7 +229,21 @@ class App(Base): add_self: bool, scope: Scope, names_only: bool, + *, + check_installed: bool = True, + exclude: set[str] = set(), + recurse: bool = True, ) -> None: + """Walk the dependency edges of sections and key (flavour) + starting at spec, appending the visited specs to buf in + postorder; buf and visited are updated in place. + + check_installed applies the installed-package check, which + skips the dependencies of an installed run package and fails + on an unmet dependency. exclude names specs that are neither + traversed nor appended. recurse = False stops the walk at the + starting spec. + """ dep = Dependency(spec) name = dep.base_name mod = dep.full_name @@ -216,52 +251,44 @@ class App(Base): spec = name if spec in buf: return + if spec in exclude: + return if spec in visited: if add_self: buf.append(spec) return visited.add(spec) - needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' - if not self.is_installed(name, devel = True): - if not mod.endswith(('-dev', '-devel')): - if self.is_installed(name, devel = False): - return - raise Exception( - f'Unmet dependency on {mod}: the -{needed_subpackage} package ' - f'of project {name} is not installed' - ) - 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) + if check_installed: + needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' + if not self.is_installed(name, devel = True): + if not mod.endswith(('-dev', '-devel')): + if self.is_installed(name, devel = False): + return + raise Exception( + f'Unmet dependency on {mod}: the -{needed_subpackage} package ' + f'of project {name} is not installed' + ) + vals_list = self.read_dep_edges(name, sections, key) 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: - self.__get_project_refs( - buf, - visited, - val, - sections, - key, - add_self = True, - scope = subscope, - names_only = names_only, - ) + if recurse: + subscope = scope.Self if scope == Scope.One else scope + for val in vals_list: + self.walk_project_deps( + buf, + visited, + val, + sections, + key, + add_self = True, + scope = subscope, + names_only = names_only, + check_installed = check_installed, + exclude = exclude, + recurse = recurse, + ) if add_self: buf.append(spec) @@ -567,7 +594,7 @@ class App(Base): visited: set[str] = set() for name in projects: rr: list[str] = [] - self.__get_project_refs( + self.walk_project_deps( rr, visited, name, sections, key, add_self, scope, names_only ) # TODO: this looks like a performance hogger