App.walk_project_deps() / read_dep_edges(): Add methods

App.get_project_refs() uses __get_project_refs() to walk the [pkg.*.jw]
sections and pkg_relations() walks the same edges with its own
implementation. In an attempt to unify the walks, this commit tries to make
the first walk implementation so generally useful that it can be used by
the second.

- Make __get_project_refs() public as walk_project_deps(). Name changed
  because it sounds less awkward
- Add three parameters needed from the other call site:
  - check_installed applies the installed-package check that 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
- Wrap the per-project dependency reading into App.read_dep_edges(),
  because the other call site does the same thing

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.86.1
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-09-21 19:03:19 +02:00
commit c6d6987e54
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -198,7 +198,28 @@ class App(Base):
except FileNotFoundError: except FileNotFoundError:
return None 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, self,
buf: list[str], buf: list[str],
visited: set[str], visited: set[str],
@ -208,7 +229,21 @@ class App(Base):
add_self: bool, add_self: bool,
scope: Scope, scope: Scope,
names_only: bool, names_only: bool,
*,
check_installed: bool = True,
exclude: set[str] = set(),
recurse: bool = True,
) -> None: ) -> 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) dep = Dependency(spec)
name = dep.base_name name = dep.base_name
mod = dep.full_name mod = dep.full_name
@ -216,52 +251,44 @@ class App(Base):
spec = name spec = name
if spec in buf: if spec in buf:
return return
if spec in exclude:
return
if spec in visited: if spec in visited:
if add_self: if add_self:
buf.append(spec) buf.append(spec)
return return
visited.add(spec) visited.add(spec)
needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' if check_installed:
if not self.is_installed(name, devel = True): needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run'
if not mod.endswith(('-dev', '-devel')): if not self.is_installed(name, devel = True):
if self.is_installed(name, devel = False): if not mod.endswith(('-dev', '-devel')):
return if self.is_installed(name, devel = False):
raise Exception( return
f'Unmet dependency on {mod}: the -{needed_subpackage} package ' raise Exception(
f'of project {name} is not installed' 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_list = self.read_dep_edges(name, sections, key)
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: match scope:
case Scope.Self: case Scope.Self:
buf += vals_list buf += vals_list
case Scope.One | Scope.Subtree: case Scope.One | Scope.Subtree:
subscope = scope.Self if scope == Scope.One else scope if recurse:
for val in vals_list: subscope = scope.Self if scope == Scope.One else scope
self.__get_project_refs( for val in vals_list:
buf, self.walk_project_deps(
visited, buf,
val, visited,
sections, val,
key, sections,
add_self = True, key,
scope = subscope, add_self = True,
names_only = names_only, scope = subscope,
) names_only = names_only,
check_installed = check_installed,
exclude = exclude,
recurse = recurse,
)
if add_self: if add_self:
buf.append(spec) buf.append(spec)
@ -567,7 +594,7 @@ class App(Base):
visited: set[str] = set() visited: set[str] = set()
for name in projects: for name in projects:
rr: list[str] = [] rr: list[str] = []
self.__get_project_refs( self.walk_project_deps(
rr, visited, name, sections, key, add_self, scope, names_only rr, visited, name, sections, key, add_self, scope, names_only
) )
# TODO: this looks like a performance hogger # TODO: this looks like a performance hogger