Skip to content

at last... a glitch-less wheelhouse report #1620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions winpython/piptree.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pip._vendor.packaging.markers import Marker
from importlib.metadata import Distribution, distributions
from pathlib import Path
from winpython import utils

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Expand All @@ -25,15 +26,6 @@ class PipDataError(Exception):
"""Custom exception for PipData related errors."""
pass

def sum_up(text: str, max_length: int = 144, stop_at: str = ". ") -> str:
"""Summarize text to fit within max_length, ending at last complete sentence."""
summary = (text + os.linesep).splitlines()[0]
if len(summary) <= max_length:
return summary
if stop_at and stop_at in summary[:max_length]:
return summary[:summary.rfind(stop_at, 0, max_length)] + stop_at.rstrip()
return summary[:max_length].rstrip()

class PipData:
"""Manages package metadata and dependency relationships in a Python environment."""

Expand Down Expand Up @@ -287,5 +279,5 @@ def pip_list(self, full: bool = False, max_length: int = 144) -> List[Tuple[str,
"""
pkgs = sorted(self.distro.items())
if full:
return [(p, d["version"], sum_up(d["summary"], max_length)) for p, d in pkgs]
return [(p, d["version"], utils.sum_up(d["summary"], max_length)) for p, d in pkgs]
return [(p, d["version"]) for p, d in pkgs]
32 changes: 8 additions & 24 deletions winpython/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,14 @@ def onerror(function, path, excinfo):
else:
raise

def getFileProperties(fname):
"""Read all properties of the given file return them as a dictionary."""
import win32api
prop_names = ('ProductName', 'ProductVersion', 'FileDescription', 'FileVersion')
props = {'FixedFileInfo': None, 'StringFileInfo': None, 'FileVersion': None}

try:
fixed_info = win32api.GetFileVersionInfo(fname, '\\')
props['FixedFileInfo'] = fixed_info
props['FileVersion'] = "{}.{}.{}.{}".format(
fixed_info['FileVersionMS'] // 65536,
fixed_info['FileVersionMS'] % 65536,
fixed_info['FileVersionLS'] // 65536,
fixed_info['FileVersionLS'] % 65536
)
lang, codepage = win32api.GetFileVersionInfo(fname, '\\VarFileInfo\\Translation')[0]
props['StringFileInfo'] = {
prop_name: win32api.GetFileVersionInfo(fname, f'\\StringFileInfo\\{lang:04X}{codepage:04X}\\{prop_name}')
for prop_name in prop_names
}
except:
pass

return props
def sum_up(text: str, max_length: int = 144, stop_at: str = ". ") -> str:
"""Summarize text to fit within max_length, ending at last complete sentence."""
summary = (text + os.linesep).splitlines()[0].strip()
if len(summary) <= max_length:
return summary
if stop_at and stop_at in summary[:max_length]:
return summary[:summary.rfind(stop_at, 0, max_length)] + stop_at.strip()
return summary[:max_length].strip()

def get_special_folder_path(path_name):
"""Return special folder path."""
Expand Down
58 changes: 57 additions & 1 deletion winpython/wheelhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
"""
WheelHouse.py - manage WinPython local WheelHouse.
"""

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 email import message_from_bytes
from email.parser import BytesParser
from email.policy import default
from . import utils

from packaging.utils import canonicalize_name

# Use tomllib if available (Python 3.11+), otherwise fall back to tomli
try:
Expand Down Expand Up @@ -183,6 +192,53 @@ def get_pylock_wheels(wheelhouse: Path, lockfile: Path, wheelorigin: Optional[Pa
else:
print(f"\n\n*** We can't install {filename} ! ***\n\n")

def extract_metadata_from_wheel(filepath: Path) -> Optional[Tuple[str, str, str]]:
"get metadata from a wheel package"
with zipfile.ZipFile(filepath, 'r') as z:
# Locate *.dist-info/METADATA file inside but not in a vendored directory (flit-core)
for name in z.namelist():
if name.endswith(r'.dist-info/METADATA') and name.split("/")[1] == "METADATA":
with z.open(name) as meta_file:
metadata = BytesParser(policy=default).parse(meta_file)
name = canonicalize_name(str(metadata.get('Name', 'unknown'))) # Avoid Head type
version = str(metadata.get('Version', 'unknown'))
summary = utils.sum_up(str(metadata.get('Summary', '')))
return name, version, summary
return None

def extract_metadata_from_sdist(filepath: Path) -> Optional[Tuple[str, str, str]]:
"get metadata from a tar.gz or .zip package"
open_func = tarfile.open if filepath.suffixes[-2:] == ['.tar', '.gz'] else zipfile.ZipFile
with open_func(filepath, 'r') as archive:
namelist = archive.getnames() if isinstance(archive, tarfile.TarFile) else archive.namelist()
for name in namelist:
if name.endswith('PKG-INFO'):
content = archive.extractfile(name).read() if isinstance(archive, tarfile.TarFile) else archive.open(name).read()
metadata = message_from_bytes(content)
name = canonicalize_name(str(metadata.get('Name', 'unknown'))) # Avoid Head type
version = str(metadata.get('Version', 'unknown'))
summary = utils.sum_up(str(metadata.get('Summary', '')))
return name, version, summary
return None

def list_packages_with_metadata(directory: str) -> List[Tuple[str, str, str]]:
"get metadata from a Wheelhouse directory"
results = []
for file in os.listdir(directory):
path = Path(directory) / file
try:
if path.suffix == '.whl':
meta = extract_metadata_from_wheel(path)
elif path.suffix == '.zip' or path.name.endswith('.tar.gz'):
meta = extract_metadata_from_sdist(path)
else:
continue
if meta:
results.append(meta)
except OSError: #Exception as e: # need to see it
print(f"Skipping {file}: {e}")
return results

def main() -> None:
"""Main entry point for the script."""
if len(sys.argv) != 2:
Expand Down
6 changes: 3 additions & 3 deletions winpython/wppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
from argparse import ArgumentParser, RawTextHelpFormatter
from winpython import utils, piptree, associate
from winpython import wheelhouse as wh
from operator import itemgetter
# Workaround for installing PyVISA on Windows from source:
os.environ["HOME"] = os.environ["USERPROFILE"]

class Package:
"""Standardize a Package from filename or pip list."""
def __init__(self, fname: str, suggested_summary: str = None):
self.fname = fname
self.description = piptree.sum_up(suggested_summary) if suggested_summary else ""
self.description = (utils.sum_up(suggested_summary) if suggested_summary else "").strip()
self.name, self.version = fname, '?.?.?'
if fname.lower().endswith((".zip", ".tar.gz", ".whl")):
bname = Path(self.fname).name # e.g., "sqlite_bro-1.0.0..."
Expand Down Expand Up @@ -81,8 +82,7 @@ def get_wheelhouse_packages_markdown(self) -> str:
if wheeldir.is_dir():
package_lines = [
f"[{name}](https://pypi.org/project/{name}) | {version} | {summary}"
for name, version, summary in wh.list_packages_with_metadata(str(wheeldir))
#for pkg in sorted(wh.list_packages_with_metadata(str(wheeldir)), key=lambda p: p.name.lower())
for name, version, summary in sorted(wh.list_packages_with_metadata(str(wheeldir)), key=itemgetter(0 , 1)) # lambda p: p[0].lower())
]
return "\n".join(package_lines)
return ""
Expand Down