Skip to content

-md markdown and -ws . options for comfort #1632

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 1 commit into from
Jun 3, 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
2 changes: 1 addition & 1 deletion winpython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
OTHER DEALINGS IN THE SOFTWARE.
"""

__version__ = '16.3.20250530'
__version__ = '16.4.20250603'
__license__ = __doc__
__project_url__ = 'http://winpython.github.io/'
14 changes: 4 additions & 10 deletions winpython/packagemetadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
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:
Expand Down Expand Up @@ -64,7 +63,7 @@ def extract_metadata_from_wheel(path: str) -> PackageMetadata:
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}")
raise ValueError(f"No METADATA found in {path}")

def extract_metadata_from_sdist(path: str) -> PackageMetadata:
import tarfile
Expand All @@ -78,6 +77,8 @@ def extract_metadata_from_sdist(path: str) -> PackageMetadata:
def parse_metadata_file(txt: str) -> PackageMetadata:
name = version = summary = description = ""
requires = []
description_lines = []
in_description = False
for line in txt.splitlines():
if line.startswith('Name: '):
name = line[6:].strip()
Expand All @@ -86,15 +87,9 @@ def parse_metadata_file(txt: str) -> PackageMetadata:
elif line.startswith('Summary: '):
summary = description = line[9:].strip()
elif line.startswith('Requires-Dist: '):
requires.append(line[14:].strip())
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
Expand All @@ -103,7 +98,6 @@ def main():
else:
# Installed packages mode
pkgs = get_installed_metadata()
build_dependency_tree(pkgs)

if __name__ == "__main__":
main()
81 changes: 3 additions & 78 deletions winpython/wheelhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
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 packagemetadata as pm
from . import utils

from packaging.utils import canonicalize_name, parse_wheel_filename, parse_sdist_filename
Expand Down Expand Up @@ -192,83 +190,10 @@ 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]]:
"Extract package metadata from a .whl file and validate it matches the filename"
wheel_name = filepath.name
try:
name, version, build, tags = parse_wheel_filename(wheel_name)
filename_name = canonicalize_name(name)
filename_version = str(version)
except Exception as e:
print(f"❌ Could not parse filename: {wheel_name}", e)
return None

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)
meta_name = canonicalize_name(str(metadata.get('Name', 'unknown'))) # Avoid Head type
meta_version = str(metadata.get('Version', 'unknown'))
summary = utils.sum_up(str(metadata.get('Summary', '')))
# Assert consistency
if meta_name != filename_name or meta_version != filename_version:
print(f"⚠️ Mismatch in {wheel_name}: filename says {filename_name}=={filename_version}, "
f"but METADATA says {meta_name}=={meta_version}")
return None
return meta_name, meta_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
sdist_name = filepath.name
try:
name, version = parse_sdist_filename(sdist_name)
filename_name = canonicalize_name(name)
filename_version = str(version)
except Exception as e:
print(f"❌ Could not parse filename: {sdist_name}", e)
return None

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'):
if isinstance(archive, tarfile.TarFile):
content = archive.extractfile(name)
else:
content = archive.open(name)
if content:
metadata = BytesParser(policy=default).parse(content)
meta_name = canonicalize_name(str(metadata.get('Name', 'unknown'))) # Avoid Head type
meta_version = str(metadata.get('Version', 'unknown'))
summary = utils.sum_up(str(metadata.get('Summary', '')))
# Assert consistency
if meta_name != filename_name or meta_version != filename_version:
print(f"⚠️ Mismatch in {sdist_name}: filename says {filename_name}=={filename_version}, "
f"but METADATA says {meta_name}=={meta_version}")
return None
return meta_name, meta_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}")
packages = pm.get_directory_metadata(directory)
results = [ (p.name, p.version, p.summary) for p in packages]
return results

def main() -> None:
Expand Down
24 changes: 16 additions & 8 deletions winpython/wppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,15 @@ def main(test=False):
# parser.add_argument( "--unregister_forall", action="store_true", help="un-Register distribution for all users")
parser.add_argument("--fix", action="store_true", help="make WinPython fix")
parser.add_argument("--movable", action="store_true", help="make WinPython movable")
parser.add_argument("-ws", dest="wheelsource", default=None, type=str, help="location to search wheels: wppm pylock.toml -ws source_of_wheels")
parser.add_argument("-wd", dest="wheeldrain" , default=None, type=str, help="location of found wheels: wppm pylock.toml -wd destination_of_wheels")
parser.add_argument("-ls", "--list", action="store_true", help="list installed packages matching the given [optional] package expression: wppm -ls, wppm -ls pand")
parser.add_argument("-lsa", dest="all", action="store_true",help=f"list details of package names matching given regular expression: wppm -lsa pandas -l1")
parser.add_argument("-p",dest="pipdown",action="store_true",help="show Package dependencies of the given package[option]: wppm -p pandas[test]")
parser.add_argument("-r", dest="pipup", action="store_true", help=f"show Reverse dependancies of the given package[option]: wppm -r pytest[test]")
parser.add_argument("-l", "--levels", type=int, default=2, help="show 'LEVELS' levels of dependencies (with -p, -r), default is 2: wppm -p pandas -l1")
parser.add_argument("-t", "--target", default=sys.prefix, help=f'path to target Python distribution (default: "{sys.prefix}")')
parser.add_argument("-ws", dest="wheelsource", default=None, type=str, help="wheels location, '.' = WheelHouse): wppm pylock.toml -ws source_of_wheels, wppm -ls -ws .")
parser.add_argument("-wd", dest="wheeldrain" , default=None, type=str, help="wheels destination: wppm pylock.toml -wd destination_of_wheels")
parser.add_argument("-ls", "--list", action="store_true", help="list installed packages matching [optional] expression: wppm -ls, wppm -ls pand")
parser.add_argument("-lsa", dest="all", action="store_true",help=f"list details of packages matching [optional] expression: wppm -lsa pandas -l1")
parser.add_argument("-md", dest="markdown", action="store_true",help=f"markdown summary if the installation")
parser.add_argument("-p",dest="pipdown",action="store_true",help="show Package dependencies of the given package[option], [.]=all: wppm -p pandas[.]")
parser.add_argument("-r", dest="pipup", action="store_true", help=f"show Reverse wppmdependancies of the given package[option]: wppm -r pytest[test]")
parser.add_argument("-l", dest="levels", type=int, default=2, help="show 'LEVELS' levels of dependencies (with -p, -r), default is 2: wppm -p pandas -l1")
parser.add_argument("-t", dest="target", default=sys.prefix, help=f'path to target Python distribution (default: "{sys.prefix}")')
parser.add_argument("-i", "--install", action="store_true", help="install a given package wheel or pylock file (use pip for more features)")
parser.add_argument("-u", "--uninstall", action="store_true", help="uninstall package (use pip for more features)")

Expand All @@ -290,6 +291,10 @@ def main(test=False):
targetpython = None
if args.target and args.target != sys.prefix:
targetpython = args.target if args.target.lower().endswith('.exe') else str(Path(args.target) / 'python.exe')
if args.wheelsource == ".": # play in default WheelHouse
if utils.is_python_distribution(args.target):
dist = Distribution(args.target)
args.wheelsource = dist.wheelhouse / 'included.wheels'
if args.install and args.uninstall:
raise RuntimeError("Incompatible arguments: --install and --uninstall")
if args.registerWinPython and args.unregisterWinPython:
Expand Down Expand Up @@ -360,6 +365,9 @@ def main(test=False):
if args.movable:
p = subprocess.Popen(["start", "cmd", "/k",dist.python_exe, "-c" , cmd_mov], shell = True, cwd=dist.target)
sys.exit()
if args.markdown:
print(dist.generate_package_index_markdown())
sys.exit()
if not args.install and not args.uninstall and args.fname.endswith(".toml"):
args.install = True # for Drag & Drop of .toml (and not wheel)
if args.fname == "" or (not args.install and not args.uninstall):
Expand Down