Skip to content

Commit 62540f0

Browse files
authored
Merge pull request winpython#1630 from stonebig/master
wppm -ls -ws ... works
2 parents dff57f8 + eb7548f commit 62540f0

File tree

3 files changed

+121
-9
lines changed

3 files changed

+121
-9
lines changed

winpython/packagemetadata.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""
3+
packagemetadata.py - get metadata from designated place
4+
"""
5+
import os
6+
import re
7+
import tarfile
8+
import zipfile
9+
import sys
10+
from pathlib import Path
11+
from collections import defaultdict
12+
import shutil
13+
import subprocess
14+
from typing import Dict, List, Optional, Tuple
15+
from . import utils
16+
17+
from packaging.utils import canonicalize_name, parse_wheel_filename, parse_sdist_filename
18+
# --- Abstract metadata accessor ---
19+
20+
class PackageMetadata:
21+
"""A minimal abstraction for package metadata."""
22+
def __init__(self, name, version, requires, summary, description, metadata):
23+
self.name = name
24+
self.version = version
25+
self.requires = requires # List[str] of dependencies
26+
self.summary = summary
27+
self.description = description
28+
self.metadata = metadata
29+
30+
def get_installed_metadata(path = None) -> List[PackageMetadata]:
31+
# Use importlib.metadata or pkg_resources
32+
import importlib.metadata
33+
pkgs = []
34+
distro = importlib.metadata.distributions(path = path) if path else importlib.metadata.distributions()
35+
for dist in distro:
36+
name = dist.metadata['Name']
37+
version = dist.version
38+
summary = dist.metadata.get("Summary", ""),
39+
description = dist.metadata.get("Description", ""),
40+
requires = dist.requires or []
41+
metadata = dist.metadata
42+
pkgs.append(PackageMetadata(name, version, requires, summary, description, metadata))
43+
return pkgs
44+
45+
def get_directory_metadata(directory: str) -> List[PackageMetadata]:
46+
# For each .whl/.tar.gz file in directory, extract metadata
47+
pkgs = []
48+
for fname in os.listdir(directory):
49+
if fname.endswith('.whl'):
50+
# Extract METADATA from wheel
51+
meta = extract_metadata_from_wheel(os.path.join(directory, fname))
52+
pkgs.append(meta)
53+
elif fname.endswith('.tar.gz'):
54+
# Extract PKG-INFO from sdist
55+
meta = extract_metadata_from_sdist(os.path.join(directory, fname))
56+
pkgs.append(meta)
57+
return pkgs
58+
59+
def extract_metadata_from_wheel(path: str) -> PackageMetadata:
60+
import zipfile
61+
with zipfile.ZipFile(path) as zf:
62+
for name in zf.namelist():
63+
if name.endswith(r'.dist-info/METADATA') and name.split("/")[1] == "METADATA":
64+
with zf.open(name) as f:
65+
# Parse metadata (simple parsing for Name, Version, Requires-Dist)
66+
return parse_metadata_file(f.read().decode())
67+
raise ValueError(f"No METADATA found in {path}")
68+
69+
def extract_metadata_from_sdist(path: str) -> PackageMetadata:
70+
import tarfile
71+
with tarfile.open(path, "r:gz") as tf:
72+
for member in tf.getmembers():
73+
if member.name.endswith('PKG-INFO'):
74+
f = tf.extractfile(member)
75+
return parse_metadata_file(f.read().decode())
76+
raise ValueError(f"No PKG-INFO found in {path}")
77+
78+
def parse_metadata_file(txt: str) -> PackageMetadata:
79+
name = version = summary = description = ""
80+
requires = []
81+
for line in txt.splitlines():
82+
if line.startswith('Name: '):
83+
name = line[6:].strip()
84+
elif line.startswith('Version: '):
85+
version = line[9:].strip()
86+
elif line.startswith('Summary: '):
87+
summary = description = line[9:].strip()
88+
elif line.startswith('Requires-Dist: '):
89+
requires.append(line[14:].strip())
90+
return PackageMetadata(name, version, requires, summary, description, {'Name': name, "Summary": summary, "Description": description})
91+
92+
# --- Main dependency tree logic ---
93+
94+
def build_dependency_tree(pkgs: List[PackageMetadata]):
95+
# Existing logic, but using our PackageMetadata objects
96+
pass
97+
98+
def main():
99+
if len(sys.argv) > 1:
100+
# Directory mode
101+
directory = sys.argv[1]
102+
pkgs = get_directory_metadata(directory)
103+
else:
104+
# Installed packages mode
105+
pkgs = get_installed_metadata()
106+
build_dependency_tree(pkgs)
107+
108+
if __name__ == "__main__":
109+
main()

winpython/piptree.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from importlib.metadata import Distribution, distributions
1919
from pathlib import Path
2020
from . import utils
21+
from . import packagemetadata as pm
2122

2223
logging.basicConfig(level=logging.INFO)
2324
logger = logging.getLogger(__name__)
@@ -29,7 +30,7 @@ class PipDataError(Exception):
2930
class PipData:
3031
"""Manages package metadata and dependency relationships in a Python environment."""
3132

32-
def __init__(self, target: Optional[str] = None):
33+
def __init__(self, target: Optional[str] = None, wheelhouse = None):
3334
"""
3435
Initialize the PipData instance.
3536
@@ -39,7 +40,7 @@ def __init__(self, target: Optional[str] = None):
3940
self.raw: Dict[str, Dict] = {}
4041
self.environment = self._get_environment()
4142
try:
42-
packages = self._get_packages(target or sys.executable)
43+
packages = self._get_packages(target or sys.executable, wheelhouse)
4344
self._process_packages(packages)
4445
self._populate_reverse_dependencies()
4546
except Exception as e:
@@ -67,12 +68,14 @@ def _get_environment(self) -> Dict[str, str]:
6768
"sys_platform": sys.platform,
6869
}
6970

70-
def _get_packages(self, search_path: str) -> List[Distribution]:
71+
def _get_packages(self, search_path: str, wheelhouse) -> List[Distribution]:
7172
"""Retrieve installed packages from the specified path."""
73+
if wheelhouse:
74+
return pm.get_directory_metadata(wheelhouse)
7275
if sys.executable == search_path:
73-
return Distribution.discover()
76+
return pm.get_installed_metadata() #Distribution.discover()
7477
else:
75-
return distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')])
78+
return pm.get_installed_metadata(path=[str(Path(search_path).parent / 'lib' / 'site-packages')]) #distributions(path=[str(Path(search_path).parent / 'lib' / 'site-packages')])
7679

7780
def _process_packages(self, packages: List[Distribution]) -> None:
7881
"""Process packages metadata and store them in the distro dictionary."""

winpython/wppm.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,25 +295,25 @@ def main(test=False):
295295
if args.registerWinPython and args.unregisterWinPython:
296296
raise RuntimeError("Incompatible arguments: --install and --uninstall")
297297
if args.pipdown:
298-
pip = piptree.PipData(targetpython)
298+
pip = piptree.PipData(targetpython, args.wheelsource)
299299
pack, extra, *other = (args.fname + "[").replace("]", "[").split("[")
300300
print(pip.down(pack, extra, args.levels, verbose=args.verbose))
301301
sys.exit()
302302
elif args.pipup:
303-
pip = piptree.PipData(targetpython)
303+
pip = piptree.PipData(targetpython, args.wheelsource)
304304
pack, extra, *other = (args.fname + "[").replace("]", "[").split("[")
305305
print(pip.up(pack, extra, args.levels, verbose=args.verbose))
306306
sys.exit()
307307
elif args.list:
308-
pip = piptree.PipData(targetpython)
308+
pip = piptree.PipData(targetpython, args.wheelsource)
309309
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))]
310310
titles = [['Package', 'Version', 'Summary'], ['_' * max(x, 6) for x in utils.columns_width(todo)]]
311311
listed = utils.formatted_list(titles + todo, max_width=70)
312312
for p in listed:
313313
print(*p)
314314
sys.exit()
315315
elif args.all:
316-
pip = piptree.PipData(targetpython)
316+
pip = piptree.PipData(targetpython, args.wheelsource)
317317
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))]
318318
for l in todo:
319319
# print(pip.distro[l[0]])

0 commit comments

Comments
 (0)