Skip to content

WheelHouse integrated management #1614

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
May 24, 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.0.20250513'
__version__ = '16.1.20250524'
__license__ = __doc__
__project_url__ = 'http://winpython.github.io/'
140 changes: 127 additions & 13 deletions winpython/wheelhouse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#
# WheelHouse.py
#!/usr/bin/env python3
"""
WheelHouse.py - manage WinPython local WheelHouse.
"""

import sys
from pathlib import Path
from collections import defaultdict
import shutil
import subprocess
from typing import Dict, List, Optional, Tuple

# Use tomllib if available (Python 3.11+), otherwise fall back to tomli
try:
Expand All @@ -14,10 +20,9 @@
print("Please install tomli for Python < 3.11: pip install tomli")
sys.exit(1)



def parse_pylock_toml(path):
with open(Path(path), "rb") as f:
def parse_pylock_toml(path: Path) -> Dict[str, Dict[str, str | List[str]]]:
"""Parse a pylock.toml file and extract package information."""
with open(path, "rb") as f:
data = tomllib.load(f)

# This dictionary maps package names to (version, [hashes])
Expand Down Expand Up @@ -46,9 +51,9 @@ def parse_pylock_toml(path):

return package_hashes


def write_requirements_txt(package_hashes, output_path="requirements.txt"):
with open(Path(output_path), "w") as f:
def write_requirements_txt(package_hashes: Dict[str, Dict[str, str | List[str]]], output_path: Path) -> None:
"""Write package requirements to a requirements.txt file."""
with open(output_path, "w") as f:
for name, data in sorted(package_hashes.items()):
version = data["version"]
hashes = data["hashes"]
Expand All @@ -63,13 +68,119 @@ def write_requirements_txt(package_hashes, output_path="requirements.txt"):

print(f"✅ requirements.txt written to {output_path}")

def pylock_to_req(path, output_path=None):
def pylock_to_req(path: Path, output_path: Optional[Path] = None) -> None:
"""Convert a pylock.toml file to requirements.txt."""
pkgs = parse_pylock_toml(path)
if not output_path:
output_path = path.parent / (path.stem.replace('pylock','requirement_with_hash')+ '.txt')
output_path = path.parent / (path.stem.replace('pylock', 'requirement_with_hash') + '.txt')
write_requirements_txt(pkgs, output_path)

if __name__ == "__main__":
def run_pip_command(command: List[str], check: bool = True, capture_output=True) -> Tuple[bool, Optional[str]]:
"""Run a pip command and return the result."""
print('\n', ' '.join(command),'\n')
try:
result = subprocess.run(
command,
capture_output=capture_output,
text=True,
check=check
)
return (result.returncode == 0), (result.stderr or result.stdout)
except subprocess.CalledProcessError as e:
return False, e.stderr
except FileNotFoundError:
return False, "pip or Python not found."
except Exception as e:
return False, f"Unexpected error: {e}"

def get_wheels(requirements: Path, wheeldir: Path, from_local: Optional[Path] = None
, only_check: bool = True,post_install: bool = False) -> bool:
"""Download or check Python wheels based on requirements."""
added = []
if from_local:
added += ['--no-index', '--trusted-host=None', f'--find-links={from_local}']
pre_checks = [sys.executable, "-m", "pip", "install", "--dry-run", "--no-deps", "--require-hashes", "-r", str(requirements)] + added
instruction = [sys.executable, "-m", "pip", "download", "--no-deps", "--require-hashes", "-r", str(requirements), "--dest", str(wheeldir)] + added
post_install_cmd = [sys.executable, "-m", "pip", "install", "--no-deps", "--require-hashes", "-r", str(requirements)] + added

# Run pip dry-run, only if a move of directory
if from_local and from_local != wheeldir:
success, output = run_pip_command(pre_checks, check=False)
if not success:
print("❌ Dry-run failed. Here's the output:\n")
print(output or "")
return False

print("✅ Requirements can be installed successfully (dry-run passed).\n")

# All ok
if only_check and not post_install:
return True

# Want to install
if not only_check and post_install:
success, output = run_pip_command(post_install_cmd, check=False, capture_output=False)
if not success:
print("❌ Installation failed. Here's the output:\n")
print(output or "")
return False
return True

# Otherwise download also, but not install direct
success, output = run_pip_command(instruction)
if not success:
print("❌ Download failed. Here's the output:\n")
print(output or "")
return False

return True

def get_pylock_wheels(wheelhouse: Path, lockfile: Path, from_local: Optional[Path] = None) -> None:
"""Get wheels for a pylock file."""
filename = Path(lockfile).name
wheelhouse.mkdir(parents=True, exist_ok=True)
trusted_wheelhouse = wheelhouse / "included.wheels"
trusted_wheelhouse.mkdir(parents=True, exist_ok=True)

filename_lock = wheelhouse / filename
filename_req = wheelhouse / (Path(lockfile).stem.replace('pylock', 'requirement_with_hash') + '.txt')

pylock_to_req(Path(lockfile), filename_req)

if not str(Path(lockfile)) == str(filename_lock):
shutil.copy2(lockfile, filename_lock)

# We create a destination for wheels that is specific, so we can check all is there
destination_wheelhouse = wheelhouse / Path(lockfile).name.replace('.toml', '.wheels')
in_trusted = False

if from_local is None:
# Try from trusted WheelHouse
print(f"\n\n*** Checking if we can install from our Local WheelHouse: ***\n {trusted_wheelhouse}\n\n")
in_trusted = get_wheels(filename_req, destination_wheelhouse, from_local=trusted_wheelhouse, only_check=True)
if in_trusted:
print(f"\n\n*** We can install from Local WheelHouse: ***\n {trusted_wheelhouse}\n\n")
user_input = input("Do you want to continue and install from {trusted_wheelhouse} ? (yes/no):")
if user_input.lower() == "yes":
in_installed = get_wheels(filename_req, trusted_wheelhouse, from_local=trusted_wheelhouse, only_check=True, post_install=True)

if not in_trusted:
post_install = True if from_local and Path(from_local).is_dir and Path(from_local).samefile(destination_wheelhouse) else False
if post_install:
print(f"\n\n*** Installing from Local WheelHouse: ***\n {destination_wheelhouse}\n\n")
else:
print(f"\n\n*** Re-Checking if we can install from: {'pypi.org' if not from_local or from_local == '' else from_local}\n\n")

in_pylock = get_wheels(filename_req, destination_wheelhouse, from_local=from_local, only_check=False, post_install=post_install)
if in_pylock:
if not post_install:
print(f"\n\n*** You can now install from this dedicated WheelHouse: ***\n {destination_wheelhouse}")
print(f"\n via:\n wppm {filename_lock} -wh {destination_wheelhouse}\n")
else:
print(f"\n\n*** We can't install {filename} ! ***\n\n")

def main() -> None:
"""Main entry point for the script."""
if len(sys.argv) != 2:
print("Usage: python pylock_to_requirements.py pylock.toml")
sys.exit(1)
Expand All @@ -80,5 +191,8 @@ def pylock_to_req(path, output_path=None):
sys.exit(1)

pkgs = parse_pylock_toml(path)
dest = path.parent / (path.stem.replace('pylock','requirement_with_hash')+ '.txt')
dest = path.parent / (path.stem.replace('pylock', 'requirement_with_hash') + '.txt')
write_requirements_txt(pkgs, dest)

if __name__ == "__main__":
main()
12 changes: 10 additions & 2 deletions winpython/wppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,21 +231,22 @@ def main(test=False):
description="WinPython Package Manager: handle a WinPython Distribution and its packages",
formatter_class=RawTextHelpFormatter,
)
parser.add_argument("fname", metavar="package", nargs="?", default="", type=str, help="optional package name or package wheel")
parser.add_argument("fname", metavar="package or lockfile", nargs="?", default="", type=str, help="optional package name or package wheel")
parser.add_argument("-v", "--verbose", action="store_true", help="show more details on packages and actions")
parser.add_argument( "--register", dest="registerWinPython", action="store_true", help=registerWinPythonHelp)
# parser.add_argument( "--register_forall", action="store_true", help="Register distribution for all users")
parser.add_argument("--unregister", dest="unregisterWinPython", action="store_true", help=unregisterWinPythonHelp)
# 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("-wh", "--wheelhouse", default=None, type=str, help="wheelhouse location to search for wheels: wppm pylock.toml -wh directory_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("-i", "--install", action="store_true", help="install a given package wheel (use pip for more features)")
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 Down Expand Up @@ -331,7 +332,14 @@ def main(test=False):
sys.exit()
else:
raise FileNotFoundError(f"File not found: {args.fname}")
else:
try:
filename = Path(args.fname).name
if filename.split('.')[0] == "pylock" and filename.split('.')[-1] == 'toml':
print(' a lock file !', args.fname, dist.target)
from winpython import wheelhouse as wh
wh.get_pylock_wheels(Path(dist.target).parent/ "WheelHouse", Path(args.fname), args.wheelhouse)
sys.exit()
if args.uninstall:
package = dist.find_package(args.fname)
dist.uninstall(package)
Expand Down