Make the rest of the 'list' and 'diff' logic use PackageSet.

This commit is contained in:
Bryan Gardiner 2024-08-22 20:46:28 -07:00
parent 682570f054
commit e7d0ce0c89
No known key found for this signature in database
GPG key ID: 53EFBCA063E6183C

144
src/nvd
View file

@ -170,7 +170,7 @@ class Version:
if text is None: if text is None:
text = "" text = ""
self._text = text self._text: str = text
self._chunks: List[VersionChunk] = [] self._chunks: List[VersionChunk] = []
while text != "": while text != "":
@ -204,6 +204,10 @@ class Version:
return NotImplemented return NotImplemented
return self._chunks < other._chunks return self._chunks < other._chunks
def text(self) -> str:
"""Returns the version string, empty (not None) if there is no version."""
return self._text
class Package: class Package:
def __init__(self, *, pname: str, version: Version, store_path: StorePath): def __init__(self, *, pname: str, version: Version, store_path: StorePath):
assert isinstance(pname, str), f"Not a string: {pname!r}" assert isinstance(pname, str), f"Not a string: {pname!r}"
@ -223,20 +227,30 @@ class Package:
return self._store_path return self._store_path
class PackageSet: class PackageSet:
def __init__(self, packages: List[Package]): def __init__(self, packages: List[Package], store_paths: List[str]):
assert isinstance(packages, List) assert isinstance(packages, List)
assert all(isinstance(package, Package) for package in packages) assert all(isinstance(package, Package) for package in packages)
self._packages = packages self._packages = packages
self._store_paths = store_paths
# Unordered map from pnames to available package versions, in ascending
# version order.
self._packages_by_pname: Dict[str, List[Package]] = {} self._packages_by_pname: Dict[str, List[Package]] = {}
for entry in packages: for entry in packages:
self._packages_by_pname.setdefault(entry.pname(), []).append(entry) self._packages_by_pname.setdefault(entry.pname(), []).append(entry)
for pkgs in self._packages_by_pname.values():
pkgs.sort(key=lambda p: p.version())
@staticmethod @staticmethod
def from_direct_dependencies(path: Path) -> "PackageSet": def from_direct_dependencies(path: Path) -> "PackageSet":
return PackageSet.from_nix_query(["--references", str(path)]) return PackageSet.from_nix_query(["--references", str(path)])
@staticmethod
def from_closure(path: Path) -> "PackageSet":
return PackageSet.from_nix_query(["--requisites", str(path)])
@staticmethod @staticmethod
def from_nix_query(nix_query_args: List[str]) -> "PackageSet": def from_nix_query(nix_query_args: List[str]) -> "PackageSet":
result_paths_str: str = subprocess.run( result_paths_str: str = subprocess.run(
@ -258,7 +272,7 @@ class PackageSet:
store_path=StorePath(result_path), store_path=StorePath(result_path),
)) ))
return PackageSet(packages) return PackageSet(packages, result_paths)
def contains_pname(self, pname: str) -> bool: def contains_pname(self, pname: str) -> bool:
return pname in self._packages_by_pname return pname in self._packages_by_pname
@ -266,6 +280,13 @@ class PackageSet:
def all_pnames(self): def all_pnames(self):
return self._packages_by_pname.keys() return self._packages_by_pname.keys()
def all_store_paths(self):
return iter(self._store_paths)
def get_pname_versions(self, pname: str) -> List[Version]:
pkgs = self._packages_by_pname.get(pname, [])
return [pkg.version() for pkg in pkgs]
class PackageSetPair: class PackageSetPair:
def __init__( def __init__(
self, self,
@ -333,23 +354,8 @@ def parse_pname_version(path: str) -> Tuple[str, Optional[str]]:
return pname, version return pname, version
def closure_paths_to_map(paths: List[str]) -> Dict[str, List[Optional[str]]]:
result: Dict[str, List[Optional[str]]] = {}
for path in paths:
name, version = parse_pname_version(path)
if name not in result:
result[name] = [version]
else:
result[name].append(version)
for version_list in result.values():
version_list.sort(key=lambda ver: ver or "")
return result
def render_versions( def render_versions(
version_list: List[Optional[str]], version_list: List[Version],
*, *,
colour: bool = False, colour: bool = False,
highlight_from_pos: Optional[int] = None, highlight_from_pos: Optional[int] = None,
@ -367,23 +373,24 @@ def render_versions(
version_list.pop() version_list.pop()
count += 1 count += 1
if version is None: text = version.text()
version = "<none>" if text == "":
text = "<none>"
elif colour: # (Don't apply colour to <none>.) elif colour: # (Don't apply colour to <none>.)
if highlight_from_pos is None: if highlight_from_pos is None:
version = sgr(SGR_FG + SGR_YELLOW) + version + sgr(SGR_RESET) text = sgr(SGR_FG + SGR_YELLOW) + text + sgr(SGR_RESET)
else: else:
version = \ text = \
sgr(SGR_FG + SGR_YELLOW) \ sgr(SGR_FG + SGR_YELLOW) \
+ version[0:highlight_from_pos] \ + text[0:highlight_from_pos] \
+ sgr(SGR_BOLD) \ + sgr(SGR_BOLD) \
+ version[highlight_from_pos:] \ + text[highlight_from_pos:] \
+ sgr(SGR_RESET) + sgr(SGR_RESET)
if count == 1: if count == 1:
items.append(version) items.append(text)
else: else:
items.append(version + f" x{count}") items.append(text + f" x{count}")
items.reverse() items.reverse()
return ", ".join(items) return ", ".join(items)
@ -392,14 +399,14 @@ def print_package_list(
*, *,
pnames: List[str], pnames: List[str],
selected_sets: PackageSetPair, selected_sets: PackageSetPair,
left_versions_map: Dict[str, List[str]], left_package_set: PackageSet,
right_versions_map: Optional[Dict[str, List[str]]] = None, right_package_set: Optional[PackageSet] = None,
fixed_install_state: Optional[str] = None, fixed_install_state: Optional[str] = None,
no_version_suffix_highlight: bool = False, no_version_suffix_highlight: bool = False,
): ):
assert (right_versions_map is None) != (fixed_install_state is None), \ assert (right_package_set is None) != (fixed_install_state is None), \
"Expect either one versions map, or a fixed install state." "Expect either one package set, or a fixed install state."
have_single_version = right_versions_map is None have_single_version = right_package_set is None
if not pnames: if not pnames:
print("No packages to display.") print("No packages to display.")
@ -412,18 +419,18 @@ def print_package_list(
pname_format_str = "{:" + str(pname_width) + "}" pname_format_str = "{:" + str(pname_width) + "}"
for pname in pnames: for pname in pnames:
left_versions = left_versions_map[pname] left_versions = left_package_set.get_pname_versions(pname)
assert left_versions, f"No left versions available for pname: {pname!r}"
if have_single_version: if have_single_version:
install_state_str = fixed_install_state install_state_str = fixed_install_state
else: else:
right_versions = right_versions_map[pname] if right_versions_map is not None else None right_versions = right_package_set.get_pname_versions(pname)
# TODO Terrible - make this efficient. assert right_versions, f"No right versions available for pname: {pname!r}"
lv_parsed = [Version(v) for v in left_versions]
rv_parsed = [Version(v) for v in right_versions] if left_versions[-1] < right_versions[0]:
if all(lv < rv for lv in lv_parsed for rv in rv_parsed):
install_state_str = INST_UPGRADED install_state_str = INST_UPGRADED
elif all(lv > rv for lv in lv_parsed for rv in rv_parsed): elif left_versions[0] > right_versions[-1]:
install_state_str = INST_DOWNGRADED install_state_str = INST_DOWNGRADED
else: else:
install_state_str = INST_CHANGED install_state_str = INST_CHANGED
@ -447,8 +454,8 @@ def print_package_list(
versions_common_prefix_len = None versions_common_prefix_len = None
else: else:
versions_common_prefix_len = find_common_version_prefix_lists( versions_common_prefix_len = find_common_version_prefix_lists(
[x for x in left_versions if x is not None], [x.text() for x in left_versions if x.text() != ""],
[x for x in right_versions if x is not None], [x.text() for x in right_versions if x.text() != ""],
) )
status_str = "[{}{}]".format( status_str = "[{}{}]".format(
@ -620,19 +627,12 @@ def run_list(*, root, only_selected, name_patterns):
else: else:
selected_set = PackageSet.from_direct_dependencies(path / "sw") selected_set = PackageSet.from_direct_dependencies(path / "sw")
closure_paths: List[str] = subprocess.run( closure_set = PackageSet.from_closure(path)
[make_nix_bin_path("nix-store"), "-qR", str(path)],
stdout=PIPE,
text=True,
check=True,
).stdout.rstrip("\n").split("\n")
closure_map: Dict[str, List[str]] = closure_paths_to_map(closure_paths)
if only_selected: if only_selected:
target_pnames = list(selected_set.all_pnames()) target_pnames = list(selected_set.all_pnames())
else: else:
target_pnames = list(closure_map.keys()) target_pnames = list(closure_set.all_pnames())
target_pnames = [ target_pnames = [
pname pname
@ -645,8 +645,8 @@ def run_list(*, root, only_selected, name_patterns):
print_package_list( print_package_list(
pnames=target_pnames, pnames=target_pnames,
selected_sets=PackageSetPair(selected_set, selected_set), selected_sets=PackageSetPair(selected_set, selected_set),
left_versions_map=closure_map, left_package_set=closure_set,
right_versions_map=None, right_package_set=None,
fixed_install_state=INST_INSTALLED, fixed_install_state=INST_INSTALLED,
) )
@ -685,25 +685,11 @@ def run_diff(
selected_sets = PackageSetPair(left_selected_set, right_selected_set) selected_sets = PackageSetPair(left_selected_set, right_selected_set)
left_closure_paths: List[str] = subprocess.run( left_closure_set = PackageSet.from_closure(left_resolved)
[make_nix_bin_path("nix-store"), "-qR", str(left_resolved)], right_closure_set = PackageSet.from_closure(right_resolved)
stdout=PIPE,
text=True,
check=True,
).stdout.rstrip("\n").split("\n")
right_closure_paths: List[str] = subprocess.run(
[make_nix_bin_path("nix-store"), "-qR", (right_resolved)],
stdout=PIPE,
text=True,
check=True,
).stdout.rstrip("\n").split("\n")
# Maps from pname to lists of versions. left_package_names = set(left_closure_set.all_pnames())
left_closure_map: Dict[str, List[Optional[str]]] = closure_paths_to_map(left_closure_paths) right_package_names = set(right_closure_set.all_pnames())
right_closure_map: Dict[str, List[Optional[str]]] = closure_paths_to_map(right_closure_paths)
left_package_names = set(left_closure_map.keys())
right_package_names = set(right_closure_map.keys())
common_package_names = sorted(left_package_names & right_package_names) common_package_names = sorted(left_package_names & right_package_names)
left_only_package_names = sorted(left_package_names - right_package_names) left_only_package_names = sorted(left_package_names - right_package_names)
@ -713,7 +699,7 @@ def run_diff(
package_names_with_changed_versions = [] package_names_with_changed_versions = []
package_names_with_changed_selection_states = [] package_names_with_changed_selection_states = []
for pname in common_package_names: for pname in common_package_names:
if left_closure_map[pname] != right_closure_map[pname]: if left_closure_set.get_pname_versions(pname) != right_closure_set.get_pname_versions(pname):
package_names_with_changed_versions.append(pname) package_names_with_changed_versions.append(pname)
elif selected_sets.is_selection_state_changed(pname): elif selected_sets.is_selection_state_changed(pname):
package_names_with_changed_selection_states.append(pname) package_names_with_changed_selection_states.append(pname)
@ -727,8 +713,8 @@ def run_diff(
print_package_list( print_package_list(
pnames=package_names_with_changed_versions, pnames=package_names_with_changed_versions,
selected_sets=selected_sets, selected_sets=selected_sets,
left_versions_map=left_closure_map, left_package_set=left_closure_set,
right_versions_map=right_closure_map, right_package_set=right_closure_set,
no_version_suffix_highlight=no_version_suffix_highlight, no_version_suffix_highlight=no_version_suffix_highlight,
) )
@ -739,7 +725,7 @@ def run_diff(
print_package_list( print_package_list(
pnames=package_names_with_changed_selection_states, pnames=package_names_with_changed_selection_states,
selected_sets=selected_sets, selected_sets=selected_sets,
left_versions_map=left_closure_map, left_package_set=left_closure_set,
fixed_install_state=INST_CHANGED, fixed_install_state=INST_CHANGED,
) )
@ -750,7 +736,7 @@ def run_diff(
print_package_list( print_package_list(
pnames=right_only_package_names, pnames=right_only_package_names,
selected_sets=selected_sets, selected_sets=selected_sets,
left_versions_map=right_closure_map, # Yes, this is correct. left_package_set=right_closure_set, # Yes, this is correct.
fixed_install_state=INST_ADDED, fixed_install_state=INST_ADDED,
) )
@ -761,15 +747,15 @@ def run_diff(
print_package_list( print_package_list(
pnames=left_only_package_names, pnames=left_only_package_names,
selected_sets=selected_sets, selected_sets=selected_sets,
left_versions_map=left_closure_map, left_package_set=left_closure_set,
fixed_install_state=INST_REMOVED, fixed_install_state=INST_REMOVED,
) )
if not any_changes_displayed: if not any_changes_displayed:
print("No version or selection state changes.") print("No version or selection state changes.")
left_closure_paths_set = set(left_closure_paths) left_closure_paths_set = set(left_closure_set.all_store_paths())
right_closure_paths_set = set(right_closure_paths) right_closure_paths_set = set(right_closure_set.all_store_paths())
left_only_paths_count = len(left_closure_paths_set - right_closure_paths_set) left_only_paths_count = len(left_closure_paths_set - right_closure_paths_set)
right_only_paths_count = len(right_closure_paths_set - left_closure_paths_set) right_only_paths_count = len(right_closure_paths_set - left_closure_paths_set)
@ -785,7 +771,7 @@ def run_diff(
render_bytes(right_closure_disk_usage_bytes - left_closure_disk_usage_bytes) render_bytes(right_closure_disk_usage_bytes - left_closure_disk_usage_bytes)
print( print(
f"Closure size: {len(left_closure_paths)} -> {len(right_closure_paths)} " f"Closure size: {len(left_closure_paths_set)} -> {len(right_closure_paths_set)} "
f"({right_only_paths_count} paths added, {left_only_paths_count} paths removed, " f"({right_only_paths_count} paths added, {left_only_paths_count} paths removed, "
f"delta {right_only_paths_count - left_only_paths_count:+d}" f"delta {right_only_paths_count - left_only_paths_count:+d}"
f"{diff_closure_disk_usage_str})." f"{diff_closure_disk_usage_str})."