From eb7548fd244c677a440cbcc0337fa3e59f9835bf Mon Sep 17 00:00:00 2001 From: stonebig Date: Sun, 1 Jun 2025 20:11:13 +0200 Subject: [PATCH] wppm -ls -ws ... works navigating into a WheelHouse.. --- winpython/packagemetadata.py | 109 +++++++++++++++++++++++++++++++++++ winpython/piptree.py | 13 +++-- winpython/wppm.py | 8 +-- 3 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 winpython/packagemetadata.py diff --git a/winpython/packagemetadata.py b/winpython/packagemetadata.py new file mode 100644 index 00000000..469925d6 --- /dev/null +++ b/winpython/packagemetadata.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +packagemetadata.py - get metadata from designated place +""" +import os +import re +import tarfile +import zipfile +import sys +from pathlib import Path +from collections import defaultdict +import shutil +import subprocess +from typing import Dict, List, Optional, Tuple +from . import utils + +from packaging.utils import canonicalize_name, parse_wheel_filename, parse_sdist_filename +# --- Abstract metadata accessor --- + +class PackageMetadata: + """A minimal abstraction for package metadata.""" + def __init__(self, name, version, requires, summary, description, metadata): + self.name = name + self.version = version + self.requires = requires # List[str] of dependencies + self.summary = summary + self.description = description + self.metadata = metadata + +def get_installed_metadata(path = None) -> List[PackageMetadata]: + # Use importlib.metadata or pkg_resources + import importlib.metadata + pkgs = [] + distro = importlib.metadata.distributions(path = path) if path else importlib.metadata.distributions() + for dist in distro: + name = dist.metadata['Name'] + version = dist.version + summary = dist.metadata.get("Summary", ""), + description = dist.metadata.get("Description", ""), + requires = dist.requires or [] + metadata = dist.metadata + pkgs.append(PackageMetadata(name, version, requires, summary, description, metadata)) + return pkgs + +def get_directory_metadata(directory: str) -> List[PackageMetadata]: + # For each .whl/.tar.gz file in directory, extract metadata + pkgs = [] + for fname in os.listdir(directory): + if fname.endswith('.whl'): + # Extract METADATA from wheel + meta = extract_metadata_from_wheel(os.path.join(directory, fname)) + pkgs.append(meta) + elif fname.endswith('.tar.gz'): + # Extract PKG-INFO from sdist + meta = extract_metadata_from_sdist(os.path.join(directory, fname)) + pkgs.append(meta) + return pkgs + +def extract_metadata_from_wheel(path: str) -> PackageMetadata: + import zipfile + with zipfile.ZipFile(path) as zf: + for name in zf.namelist(): + if name.endswith(r'.dist-info/METADATA') and name.split("/")[1] == "METADATA": + with zf.open(name) as f: + # Parse metadata (simple parsing for Name, Version, Requires-Dist) + return parse_metadata_file(f.read().decode()) + raise ValueError(f"No METADATA found in {path}") + +def extract_metadata_from_sdist(path: str) -> PackageMetadata: + import tarfile + with tarfile.open(path, "r:gz") as tf: + for member in tf.getmembers(): + if member.name.endswith('PKG-INFO'): + f = tf.extractfile(member) + return parse_metadata_file(f.read().decode()) + raise ValueError(f"No PKG-INFO found in {path}") + +def parse_metadata_file(txt: str) -> PackageMetadata: + name = version = summary = description = "" + requires = [] + for line in txt.splitlines(): + if line.startswith('Name: '): + name = line[6:].strip() + elif line.startswith('Version: '): + version = line[9:].strip() + elif line.startswith('Summary: '): + summary = description = line[9:].strip() + elif line.startswith('Requires-Dist: '): + requires.append(line[14:].strip()) + return PackageMetadata(name, version, requires, summary, description, {'Name': name, "Summary": summary, "Description": description}) + +# --- Main dependency tree logic --- + +def build_dependency_tree(pkgs: List[PackageMetadata]): + # Existing logic, but using our PackageMetadata objects + pass + +def main(): + if len(sys.argv) > 1: + # Directory mode + directory = sys.argv[1] + pkgs = get_directory_metadata(directory) + else: + # Installed packages mode + pkgs = get_installed_metadata() + build_dependency_tree(pkgs) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/winpython/piptree.py b/winpython/piptree.py index 1cc1d1ab..e46bd465 100644 --- a/winpython/piptree.py +++ b/winpython/piptree.py @@ -18,6 +18,7 @@ from importlib.metadata import Distribution, distributions from pathlib import Path from . import utils +from . import packagemetadata as pm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class PipDataError(Exception): class PipData: """Manages package metadata and dependency relationships in a Python environment.""" - def __init__(self, target: Optional[str] = None): + def __init__(self, target: Optional[str] = None, wheelhouse = None): """ Initialize the PipData instance. @@ -39,7 +40,7 @@ def __init__(self, target: Optional[str] = None): self.raw: Dict[str, Dict] = {} self.environment = self._get_environment() try: - packages = self._get_packages(target or sys.executable) + packages = self._get_packages(target or sys.executable, wheelhouse) self._process_packages(packages) self._populate_reverse_dependencies() except Exception as e: @@ -67,12 +68,14 @@ def _get_environment(self) -> Dict[str, str]: "sys_platform": sys.platform, } - def _get_packages(self, search_path: str) -> List[Distribution]: + def _get_packages(self, search_path: str, wheelhouse) -> List[Distribution]: """Retrieve installed packages from the specified path.""" + if wheelhouse: + return pm.get_directory_metadata(wheelhouse) if sys.executable == search_path: - return Distribution.discover() + return pm.get_installed_metadata() #Distribution.discover() else: - return distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')]) + return pm.get_installed_metadata(path=[str(Path(search_path).parent / 'lib' / 'site-packages')]) #distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')]) def _process_packages(self, packages: List[Distribution]) -> None: """Process packages metadata and store them in the distro dictionary.""" diff --git a/winpython/wppm.py b/winpython/wppm.py index 8b733b68..5b83988c 100644 --- a/winpython/wppm.py +++ b/winpython/wppm.py @@ -295,17 +295,17 @@ def main(test=False): if args.registerWinPython and args.unregisterWinPython: raise RuntimeError("Incompatible arguments: --install and --uninstall") if args.pipdown: - pip = piptree.PipData(targetpython) + pip = piptree.PipData(targetpython, args.wheelsource) pack, extra, *other = (args.fname + "[").replace("]", "[").split("[") print(pip.down(pack, extra, args.levels, verbose=args.verbose)) sys.exit() elif args.pipup: - pip = piptree.PipData(targetpython) + pip = piptree.PipData(targetpython, args.wheelsource) pack, extra, *other = (args.fname + "[").replace("]", "[").split("[") print(pip.up(pack, extra, args.levels, verbose=args.verbose)) sys.exit() elif args.list: - pip = piptree.PipData(targetpython) + pip = piptree.PipData(targetpython, args.wheelsource) todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))] titles = [['Package', 'Version', 'Summary'], ['_' * max(x, 6) for x in utils.columns_width(todo)]] listed = utils.formatted_list(titles + todo, max_width=70) @@ -313,7 +313,7 @@ def main(test=False): print(*p) sys.exit() elif args.all: - pip = piptree.PipData(targetpython) + pip = piptree.PipData(targetpython, args.wheelsource) todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))] for l in todo: # print(pip.distro[l[0]])