diff --git a/diff.py b/diff.py
deleted file mode 100644
index 1fd01586..00000000
--- a/diff.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# WinPython diff.py script
-# Copyright © 2013 Pierre Raybaut
-# Copyright © 2014-2025+ The Winpython development team https://github.com/winpython/
-# Licensed under the terms of the MIT License
-# (see winpython/__init__.py for details)
-
-import os
-from pathlib import Path
-import re
-import shutil
-from packaging import version
-from winpython import utils
-
-CHANGELOGS_DIR = Path(__file__).parent / "changelogs"
-assert CHANGELOGS_DIR.is_dir()
-
-class Package:
- # SourceForge Wiki syntax:
- PATTERN = r"\[([a-zA-Z\-\:\/\.\_0-9]*)\]\(([^\]\ ]*)\) \| ([^\|]*) \| ([^\|]*)"
- # Google Code Wiki syntax:
- PATTERN_OLD = r"\[([a-zA-Z\-\:\/\.\_0-9]*) ([^\]\ ]*)\] \| ([^\|]*) \| ([^\|]*)"
-
- def __init__(self):
- self.name = self.version = self.description = self.url = None
-
- def __str__(self):
- return f"{self.name} {self.version}\r\n{self.description}\r\nWebsite: {self.url}"
-
- def from_text(self, text):
- match = re.match(self.PATTERN_OLD, text) or re.match(self.PATTERN, text)
- if not match:
- raise ValueError("Text does not match expected pattern")
- self.name, self.url, self.version, self.description = match.groups()
-
- def to_wiki(self):
- return f" * [{self.name}]({self.url}) {self.version} ({self.description})\r\n"
-
- def upgrade_wiki(self, other):
- assert self.name.replace("-", "_").lower() == other.name.replace("-", "_").lower()
- return f" * [{self.name}]({self.url}) {other.version} → {self.version} ({self.description})\r\n"
-
-class PackageIndex:
- WINPYTHON_PATTERN = r"\#\# WinPython\-*[0-9b-t]* ([0-9\.a-zA-Z]*)"
- TOOLS_LINE = "### Tools"
- PYTHON_PACKAGES_LINE = "### Python packages"
- HEADER_LINE1 = "Name | Version | Description"
- HEADER_LINE2 = "-----|---------|------------"
-
- def __init__(self, version, basedir=None, flavor="", architecture=64):
- self.version = version
- self.flavor = flavor
- self.basedir = basedir
- self.architecture = architecture
- self.other_packages = {}
- self.python_packages = {}
- self.from_file(basedir)
-
- def from_file(self, basedir):
- fname = CHANGELOGS_DIR / f"WinPython{self.flavor}-{self.architecture}bit-{self.version}.md"
- if not fname.exists():
- raise FileNotFoundError(f"Changelog file not found: {fname}")
- with open(fname, "r", encoding=utils.guess_encoding(fname)[0]) as fdesc:
- self.from_text(fdesc.read())
-
- def from_text(self, text):
- version = re.match(self.WINPYTHON_PATTERN + self.flavor, text).groups()[0]
- assert version == self.version
- tools_flag = python_flag = False
- for line in text.splitlines():
- if line:
- if line == self.TOOLS_LINE:
- tools_flag, python_flag = True, False
- continue
- elif line == self.PYTHON_PACKAGES_LINE:
- tools_flag, python_flag = False, True
- continue
- elif line in (self.HEADER_LINE1, self.HEADER_LINE2, "", " "):
- continue
- if tools_flag or python_flag:
- package = Package()
- package.from_text(line)
- if tools_flag:
- self.other_packages[package.name] = package
- else:
- self.python_packages[package.name] = package
-
-def diff_package_dicts(old_packages, new_packages):
- """Return difference between package old and package new"""
-
- # wheel replace '-' per '_' in key
- old = {k.replace("-", "_").lower(): v for k, v in old_packages.items()}
- new = {k.replace("-", "_").lower(): v for k, v in new_packages.items()}
- text = ""
-
- if new_keys := sorted(set(new) - set(old)):
- text += "New packages:\r\n\r\n" + "".join(new[k].to_wiki() for k in new_keys) + "\r\n"
-
- if upgraded := [new[k].upgrade_wiki(old[k]) for k in sorted(set(old) & set(new)) if old[k].version != new[k].version]:
- text += "Upgraded packages:\r\n\r\n" + f"{''.join(upgraded)}" + "\r\n"
-
- if removed_keys := sorted(set(old) - set(new)):
- text += "Removed packages:\r\n\r\n" + "".join(old[k].to_wiki() for k in removed_keys) + "\r\n"
- return text
-
-def find_closer_version(version1, basedir=None, flavor="", architecture=64):
- """Find version which is the closest to `version`"""
- builddir = Path(basedir) / f"bu{flavor}"
- pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-([0-9\.]*)\.(txt|md)")
- versions = [pattern.match(name).groups()[0] for name in os.listdir(builddir) if pattern.match(name)]
-
- if version1 not in versions:
- raise ValueError(f"Unknown version {version1}")
-
- version_below = '0.0.0.0'
- for v in versions:
- if version.parse(version_below) < version.parse(v) and version.parse(v) < version.parse(version1):
- version_below = v
-
- return version_below if version_below != '0.0.0.0' else version1
-
-def compare_package_indexes(version2, version1=None, basedir=None, flavor="", flavor1=None,architecture=64):
- """Compare two package index Wiki pages"""
- version1 = version1 if version1 else find_closer_version(version2, basedir, flavor, architecture)
- flavor1 = flavor1 if flavor1 else flavor
- pi1 = PackageIndex(version1, basedir, flavor1, architecture)
- pi2 = PackageIndex(version2, basedir, flavor, architecture)
-
- text = (
- f"## History of changes for WinPython-{architecture}bit {version2 + flavor}\r\n\r\n"
- f"The following changes were made to WinPython-{architecture}bit distribution since version {version1 + flavor1}.\r\n\r\n"
- "\r\n\r\n"
- )
-
- tools_text = diff_package_dicts(pi1.other_packages, pi2.other_packages)
- if tools_text:
- text += PackageIndex.TOOLS_LINE + "\r\n\r\n" + tools_text
-
- py_text = diff_package_dicts(pi1.python_packages, pi2.python_packages)
- if py_text:
- text += PackageIndex.PYTHON_PACKAGES_LINE + "\r\n\r\n" + py_text
-
- text += "\r\n \r\n* * *\r\n"
- return text
-
-def _copy_all_changelogs(version, basedir, flavor="", architecture=64):
- basever = ".".join(version.split(".")[:2])
- pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-{basever}([0-9\.]*)\.(txt|md)")
- for name in os.listdir(CHANGELOGS_DIR):
- if pattern.match(name):
- shutil.copyfile(CHANGELOGS_DIR / name, Path(basedir) / f"bu{flavor}" / name)
-
-def write_changelog(version2, version1=None, basedir=None, flavor="", architecture=64):
- """Write changelog between version1 and version2 of WinPython"""
- _copy_all_changelogs(version2, basedir, flavor, architecture)
- print("comparing_package_indexes", version2, basedir, flavor, architecture)
- changelog_text = compare_package_indexes(version2, version1, basedir, flavor, architecture=architecture)
- output_file = Path(basedir) / f"bu{flavor}" / f"WinPython{flavor}-{architecture}bit-{version2}_History.md"
-
- with open(output_file, "w", encoding="utf-8") as fdesc:
- fdesc.write(changelog_text)
- # Copy to winpython/changelogs
- shutil.copyfile(output_file, CHANGELOGS_DIR / output_file.name)
-
-if __name__ == "__main__":
- print(compare_package_indexes("3.7.4.0", "3.7.2.0", r"C:\WinP\bd37", "Zero", architecture=32))
- write_changelog("3.7.4.0", "3.7.2.0", r"C:\WinP\bd37", "Ps2", architecture=64)
diff --git a/generate_a_winpython_distro.bat b/generate_a_winpython_distro.bat
index 7fdbc12f..e9c73d9a 100644
--- a/generate_a_winpython_distro.bat
+++ b/generate_a_winpython_distro.bat
@@ -2,7 +2,7 @@ rem generate_a_winpython_distro.bat: to be launched from a winpython directory,
@echo on
REM Initialize variables
-if "%my_release_level%"=="" set my_release_level=b3
+if "%my_release_level%"=="" set my_release_level=b4
if "%my_create_installer%"=="" set my_create_installer=True
rem Set archive directory and log file
@@ -26,8 +26,8 @@ if "%target_python_exe%"=="" set target_python_exe=python.exe
rem Set Python target release based on my_python_target
if %my_python_target%==311 set my_python_target_release=3119& set my_release=1
-if %my_python_target%==312 set my_python_target_release=31210& set my_release=0
-if %my_python_target%==313 set my_python_target_release=3133& set my_release=0
+if %my_python_target%==312 set my_python_target_release=31210& set my_release=1
+if %my_python_target%==313 set my_python_target_release=3133& set my_release=1
if %my_python_target%==314 set my_python_target_release=3140& set my_release=0
echo -------------------------------------- >>%my_archive_log%
@@ -101,10 +101,102 @@ echo -------------------------------------- >>%my_archive_log%
python -m pip install -r %my_requirements% -c %my_constraints% --pre --no-index --trusted-host=None --find-links=%my_find_links% >>%my_archive_log%
python -c "from winpython import wppm;dist=wppm.Distribution(r'%WINPYDIR%');dist.patch_standard_packages('spyder', to_movable=True)"
+REM Add Wheelhouse (to replace per pip lock direct ? would allow paralellism)
+echo -------------------------------------- >>%my_archive_log%
+echo "(%date% %time%) Add lockfile wheels">>%my_archive_log%
+echo -------------------------------------- >>%my_archive_log%
+set path=%my_original_path%
+@echo on
+call %my_WINPYDIRBASE%\scripts\env.bat
+@echo on
+set WINPYVERLOCK=%WINPYVER2:.=_%
+set pylockinclude=%my_root_dir_for_builds%\bd%my_python_target%\bu%addlockfile%\pylock.%addlockfile%-%WINPYARCH%bit-%WINPYVERLOCK%.toml
+echo pylockinclude="%pylockinclude%"
+if not "Z%addlockfile%Z"=="ZZ" if exist "%pylockinclude%" (
+echo %my_WINPYDIRBASE%\python\scripts\wppm.exe "%pylockinclude%" -ws "%my_find_links%" -wd "%my_WINPYDIRBASE%\wheelhouse\included.wheels">>%my_archive_log%
+%my_WINPYDIRBASE%\python\scripts\wppm.exe "%pylockinclude%" -ws "%my_find_links%" -wd "%my_WINPYDIRBASE%\wheelhouse\included.wheels"
+)
+
+@echo on
+echo wheelhousereq=%wheelhousereq%
+set LOCKDIR=%WINPYDIRBASE%\..\
+set pip_lock_includedlocal=%LOCKDIR%pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_includedwheelslocal.toml
+set pip_lock_includedweb=%LOCKDIR%pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_includedwheels.toml
+set req_lock_includedlocal=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_includedwheelslocal.txt
+set req_lock_includedweb=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_includedwheels.txt
+
+if not "Z%wheelhousereq%Z"=="ZZ" if exist "%wheelhousereq%" (
+echo JOYYYwheelhousereq=%wheelhousereq%
+echo z%pip_lock_includedlocal%z=%pip_lock_includedlocal%
+rem no winpython in it naturally, with deps
+python.exe -m pip lock --no-index --trusted-host=None --find-links=%my_find_links% -c C:\WinP\constraints.txt -r "%wheelhousereq%" -o %pip_lock_includedlocal%
+rem generating also classic requirement with hash-256, from obtained pylock.toml
+python.exe -c "from winpython import wheelhouse as wh;wh.pylock_to_req(r'%pip_lock_includedlocal%', r'%req_lock_includedlocal%')"
+
+rem same with frozen web from local
+python.exe -m pip lock --no-deps --require-hashes -c C:\WinP\constraints.txt -r "%req_lock_includedlocal%" -o %pip_lock_includedweb%
+
+echo %my_WINPYDIRBASE%\python\scripts\wppm.exe "%pip_lock_includedweb%" -ws "%my_find_links%" -wd "%my_WINPYDIRBASE%\wheelhouse\included.wheels">>%my_archive_log%
+%my_WINPYDIRBASE%\python\scripts\wppm.exe "%pip_lock_includedweb%" -ws "%my_find_links%" -wd "%my_WINPYDIRBASE%\wheelhouse\included.wheels"
+)
+
+echo -------------------------------------- >>%my_archive_log%;
+echo "(%date% %time%) generate pylock.toml files and requirement.txt with hash files">>%my_archive_log%
+echo -------------------------------------- >>%my_archive_log%
+
+set path=%my_original_path%
+call %my_WINPYDIRBASE%\scripts\env.bat
+
+rem generate pip freeze requirements
+echo %date% %time%
+set LOCKDIR=%WINPYDIRBASE%\..\
+
+set WINPYVERLOCK=%WINPYVER2:.=_%
+set req=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_raw.txt
+set wanted_req=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%.txt
+set pip_lock_web=%LOCKDIR%pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%.toml
+set pip_lock_local=%LOCKDIR%pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_local.toml
+set req_lock_web=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%.txt
+set req_lock_local=%LOCKDIR%requirement.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_local.txt
+
+set pip_lock_web=%LOCKDIR%pylock.%WINPYARCH%-%WINPYVERLOCK%%my_flavor%%my_release_level%.toml
+set pip_lock_local=%LOCKDIR%pylock.%WINPYARCH%-%WINPYVERLOCK%%my_flavor%%my_release_level%_local.toml
+set req_lock_web=%LOCKDIR%requirement.%WINPYARCH%-%WINPYVERLOCK%%my_flavor%%my_release_level%.txt
+set req_lock_local=%LOCKDIR%requirement.%WINPYARCH%-%WINPYVERLOCK%%my_flavor%_local.txt
+
+
+set my_archive_lockfile=%my_archive_dir%\pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_%date:/=-%at_%my_time%.toml
+set my_archive_lockfile_local=%my_archive_dir%\pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%_%date:/=-%at_%my_time%.local.toml
+set my_changelog_lockfile=%~dp0changelogs\pylock.%my_flavor%-%WINPYARCH%bit-%WINPYVERLOCK%.toml
+
+python.exe -m pip freeze>%req%
+findstr /v "winpython" %req% > %wanted_req%
+
+
+rem pip lock from pypi, from the frozen req
+python.exe -m pip lock --no-deps -c C:\WinP\constraints.txt -r "%wanted_req%" -o %pip_lock_web%
+
+rem pip lock from local WheelHouse, from the frozen req
+python.exe -m pip lock --no-deps --no-index --trusted-host=None --find-links=C:\WinP\packages.srcreq -c C:\WinP\constraints.txt -r "%wanted_req%" -o %pip_lock_local%
+
+rem generating also classic requirement with hash-256, from obtained pylock.toml
+python.exe -c "from winpython import wheelhouse as wh;wh.pylock_to_req(r'%pip_lock_web%', r'%req_lock_web%')"
+python.exe -c "from winpython import wheelhouse as wh;wh.pylock_to_req(r'%pip_lock_local%', r'%req_lock_local%')"
+
+rem compare the two (result from pypi and local Wheelhouse must be equal)
+fc "%req_lock_web%" "%req_lock_local%"
+
+copy/Y %pip_lock_web% %my_archive_lockfile%
+copy/Y %pip_lock_web% %my_changelog_lockfile%
+
+
REM Archive success
echo -------------------------------------- >>%my_archive_log%
echo "(%date% %time%) Archive success">>%my_archive_log%
echo -------------------------------------- >>%my_archive_log%
+set path=%my_original_path%
+call %my_WINPYDIRBASE%\scripts\env.bat
+
%target_python_exe% -m pip freeze > %my_archive_log%.packages_versions.txt
REM Generate changelog and binaries
@@ -112,8 +204,10 @@ echo "(%date% %time%) Generate changelog and binaries">>%my_archive_log%
set path=%my_original_path%
cd /D %~dp0
call %my_buildenv%\scripts\env.bat
+
python.exe -c "from make import *;make_all(%my_release%, '%my_release_level%', pyver='%my_pyver%', basedir=r'%my_basedir%', verbose=True, architecture=%my_arch%, flavor='%my_flavor%', install_options=r'%my_install_options%', find_links=r'%my_find_links%', source_dirs=r'%my_source_dirs%', create_installer='%my_create_installer%', rebuild=False, python_target_release='%my_python_target_release%')" >> %my_archive_log%
+
echo -------------------------------------- >>%my_archive_log%
echo "(%date% %time%) END OF CREATION">>%my_archive_log%
echo -------------------------------------- >>%my_archive_log%
diff --git a/generate_winpython_distros313_wheel.bat b/generate_winpython_distros313_wheel.bat
new file mode 100644
index 00000000..02edeb99
--- /dev/null
+++ b/generate_winpython_distros313_wheel.bat
@@ -0,0 +1,40 @@
+rem this replace running manually from spyder the make.py
+rem to launch from a winpython module 'make' directory
+
+set my_original_path=%path%
+
+set my_root_dir_for_builds=C:\Winp
+set my_python_target=313
+set my_pyver=3.13
+set my_flavor=whl
+set my_arch=64
+
+rem settings delegated to generate_a_winpython_distro.bat
+set my_release=
+set my_release_level=
+
+rem list of installers to create separated per dot: False=none, .zip=zip, .7z=.7z, 7zip=auto-extractible 7z
+set my_create_installer=7zip-mx5.7z-mx7.zip
+set my_create_installer=.7z-mx7
+
+set my_preclear_build_directory=Yes
+
+set tmp_reqdir=%my_root_dir_for_builds%\bd%my_python_target%
+
+set my_requirements=C:\Winp\bd313\dot_requirements.txt
+set my_source_dirs=C:\Winp\bd313\packages.win-amd64
+
+set my_find_links=C:\Winp\packages.srcreq
+set my_toolsdirs=C:\Winp\bdTools\Tools.dot
+set my_docsdirs=C:\WinP\bdDocs\docs.dot
+
+set my_install_options=--no-index --pre --trusted-host=None
+
+rem set addlockfile=dot
+
+set wheelhousereq=C:\Winp\bd313\requirements64_whl.txt
+
+
+call %~dp0\generate_a_winpython_distro.bat
+
+pause
diff --git a/make.py b/make.py
index c1a6a93a..ec75c078 100644
--- a/make.py
+++ b/make.py
@@ -12,9 +12,7 @@
import subprocess
import sys
from pathlib import Path
-from winpython import wppm, utils
-# Local import
-import diff
+from winpython import wppm, utils, diff
# Define constant paths for clarity
CHANGELOGS_DIRECTORY = Path(__file__).parent / "changelogs"
@@ -102,26 +100,13 @@ def _get_python_zip_file(self) -> Path:
@property
def package_index_markdown(self) -> str:
"""Generates a Markdown formatted package index page."""
- return f"""## WinPython {self.winpyver2 + self.flavor}
-
-The following packages are included in WinPython-{self.architecture_bits}bit v{self.winpyver2 + self.flavor} {self.release_level}.
-
-
-
-### Tools
-
-Name | Version | Description
------|---------|------------
-{utils.get_installed_tools_markdown(utils.get_python_executable(self.python_executable_directory))}
-
-### Python packages
-
-Name | Version | Description
------|---------|------------
-{self.distribution.get_installed_packages_markdown()}
-
-
-"""
+ return self.distribution.generate_package_index_markdown(
+ self.python_executable_directory,
+ self.winpyver2,
+ self.flavor,
+ self.architecture_bits,
+ self.release_level
+ )
@property
def winpython_version_name(self) -> str:
@@ -146,17 +131,18 @@ def architecture_bits(self) -> int:
"""Returns the architecture (32 or 64 bits) of the distribution."""
return self.distribution.architecture if self.distribution else 64
- def create_installer_7zip(self, installer_type: str = ".exe"):
- """Creates a WinPython installer using 7-Zip: ".exe", ".7z", ".zip")"""
+ def create_installer_7zip(self, installer_type: str = "exe", compression= "mx5"):
+ """Creates a WinPython installer using 7-Zip: "exe", "7z", "zip")"""
self._print_action(f"Creating WinPython installer ({installer_type})")
- if installer_type not in [".exe", ".7z", ".zip"]:
- raise RuntimeError("installer_type {installer_type} is undefined")
+ if installer_type not in ["exe", "7z", "zip"]:
+ return
DISTDIR = self.winpython_directory
filename_stem = f"Winpython{self.architecture_bits}-{self.python_full_version}.{self.build_number}{self.flavor}{self.release_level}"
- fullfilename = DISTDIR.parent / (filename_stem + installer_type)
- sfx_option = "-sfx7z.sfx" if installer_type == ".exe" else ""
- zip_option = "-tzip" if installer_type == ".zip" else ""
- command = f'"{find_7zip_executable()}" {zip_option} -mx5 a "{fullfilename}" "{DISTDIR}" {sfx_option}'
+ fullfilename = DISTDIR.parent / (filename_stem + "." + installer_type)
+ sfx_option = "-sfx7z.sfx" if installer_type == "exe" else ""
+ zip_option = "-tzip" if installer_type == "zip" else ""
+ compress_level = "mx5" if compression == "" else compression
+ command = f'"{find_7zip_executable()}" {zip_option} -{compress_level} a "{fullfilename}" "{DISTDIR}" {sfx_option}'
print(f'Executing 7-Zip script: "{command}"')
try:
subprocess.run(command, shell=True, check=True, stderr=sys.stderr, stdout=sys.stderr)
@@ -208,6 +194,7 @@ def _create_initial_batch_scripts(self):
# Replacements for batch scripts (PyPy compatibility)
executable_name = self.distribution.short_exe if self.distribution else "python.exe" # default to python.exe if distribution is not yet set
init_variables = [('WINPYthon_exe', executable_name), ('WINPYthon_subdirectory_name', self.python_directory_name), ('WINPYVER', self.winpython_version_name)]
+ init_variables += [('WINPYVER2', f"{self.python_full_version}.{self.build_number}"), ('WINPYFLAVOR', self.flavor), ('WINPYARCH', self.architecture_bits)]
with open(self.winpython_directory / "scripts" / "env.ini", "w") as f:
f.writelines([f'{a}={b}\n' for a, b in init_variables])
@@ -221,10 +208,7 @@ def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirnam
if rebuild:
self._print_action(f"Creating WinPython {self.winpython_directory} base directory")
if self.winpython_directory.is_dir():
- try:
- shutil.rmtree(self.winpython_directory, onexc=utils.onerror)
- except TypeError: # before 3.12
- shutil.rmtree(self.winpython_directory, onerror=utils.onerror)
+ shutil.rmtree(self.winpython_directory)
os.makedirs(self.winpython_directory, exist_ok=True)
# preventive re-Creation of settings directory
(self.winpython_directory / "settings" / "AppData" / "Roaming").mkdir(parents=True, exist_ok=True)
@@ -260,14 +244,14 @@ def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirnam
self._print_action("Writing changelog")
shutil.copyfile(output_markdown_filename, str(Path(CHANGELOGS_DIRECTORY) / Path(output_markdown_filename).name))
- diff.write_changelog(self.winpyver2, None, self.base_directory, self.flavor, self.distribution.architecture)
+ diff.write_changelog(self.winpyver2, None, CHANGELOGS_DIRECTORY, self.flavor, self.distribution.architecture, basedir=self.winpython_directory.parent)
def rebuild_winpython_package(source_directory: Path, target_directory: Path, architecture: int = 64, verbose: bool = False):
"""Rebuilds the winpython package from source using flit."""
for file in target_directory.glob("winpython-*"):
if file.suffix in (".exe", ".whl", ".gz"):
file.unlink()
- utils.buildflit_wininst(source_directory, copy_to=target_directory, verbose=verbose)
+ utils.buildflit_wininst(source_directory, copy_to=target_directory, verbose=True)
def make_all(build_number: int, release_level: str, pyver: str, architecture: int, basedir: Path,
verbose: bool = False, rebuild: bool = True, create_installer: str = "True", install_options=["--no-index"],
@@ -333,9 +317,9 @@ def make_all(build_number: int, release_level: str, pyver: str, architecture: in
builder.build(rebuild=rebuild, requirements_files_list=requirements_files_list, winpy_dirname=winpython_dirname)
- for installer_type in [".zip", ".7z", ".exe"]:
- if installer_type in create_installer.lower().replace("7zip",".exe"):
- builder.create_installer_7zip(installer_type)
+ for commmand in create_installer.lower().replace("7zip",".exe").split('.'):
+ installer_type, compression = (commmand + "-").split("-")[:2]
+ builder.create_installer_7zip(installer_type, compression)
if __name__ == "__main__":
# DO create only one Winpython distribution at a time
diff --git a/portable/launchers_final/IDLE (Python GUI).exe b/portable/launchers_final/IDLE (Python GUI).exe
index c1e0c9dc..65523199 100644
Binary files a/portable/launchers_final/IDLE (Python GUI).exe and b/portable/launchers_final/IDLE (Python GUI).exe differ
diff --git a/portable/launchers_final/Spyder.exe b/portable/launchers_final/Spyder.exe
index 43874aa7..93b1a050 100644
Binary files a/portable/launchers_final/Spyder.exe and b/portable/launchers_final/Spyder.exe differ
diff --git a/portable/launchers_final/WinPython Control Panel.exe b/portable/launchers_final/WinPython Control Panel.exe
index 5795bf9c..72a57a67 100644
Binary files a/portable/launchers_final/WinPython Control Panel.exe and b/portable/launchers_final/WinPython Control Panel.exe differ
diff --git a/portable/launchers_final_proposed/IDLE (Python GUI).exe b/portable/launchers_final_proposed/IDLE (Python GUI).exe
index 01eed2ce..65523199 100644
Binary files a/portable/launchers_final_proposed/IDLE (Python GUI).exe and b/portable/launchers_final_proposed/IDLE (Python GUI).exe differ
diff --git a/portable/launchers_final_proposed/IDLE (Python GUI)_2025-05-09_not_ok.exe b/portable/launchers_final_proposed/IDLE (Python GUI)_2025-05-09_not_ok.exe
new file mode 100644
index 00000000..01eed2ce
Binary files /dev/null and b/portable/launchers_final_proposed/IDLE (Python GUI)_2025-05-09_not_ok.exe differ
diff --git a/portable/launchers_final_proposed/IDLE (Python GUI)_20250401.exe b/portable/launchers_final_proposed/IDLE (Python GUI)_20250401.exe
new file mode 100644
index 00000000..c1e0c9dc
Binary files /dev/null and b/portable/launchers_final_proposed/IDLE (Python GUI)_20250401.exe differ
diff --git a/portable/launchers_final_proposed/Spyder.exe b/portable/launchers_final_proposed/Spyder.exe
index f3729201..93b1a050 100644
Binary files a/portable/launchers_final_proposed/Spyder.exe and b/portable/launchers_final_proposed/Spyder.exe differ
diff --git a/portable/launchers_final_proposed/Spyder_2025-05-08_no_drag_and_drop.exe b/portable/launchers_final_proposed/Spyder_2025-05-08_no_drag_and_drop.exe
new file mode 100644
index 00000000..f3729201
Binary files /dev/null and b/portable/launchers_final_proposed/Spyder_2025-05-08_no_drag_and_drop.exe differ
diff --git a/portable/launchers_final_proposed/WinPython Control Panel.exe b/portable/launchers_final_proposed/WinPython Control Panel.exe
index 664ff576..72a57a67 100644
Binary files a/portable/launchers_final_proposed/WinPython Control Panel.exe and b/portable/launchers_final_proposed/WinPython Control Panel.exe differ
diff --git a/portable/launchers_final_proposed/WinPython Control Panel_2025-05-09.exe b/portable/launchers_final_proposed/WinPython Control Panel_2025-05-09.exe
new file mode 100644
index 00000000..664ff576
Binary files /dev/null and b/portable/launchers_final_proposed/WinPython Control Panel_2025-05-09.exe differ
diff --git a/portable/scripts/WinPython_Terminal.bat b/portable/scripts/WinPython_Terminal.bat
deleted file mode 100644
index 3e55a021..00000000
--- a/portable/scripts/WinPython_Terminal.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@echo off
-Powershell.exe -Command "& {Start-Process PowerShell.exe -ArgumentList '-ExecutionPolicy RemoteSigned -noexit -File ""%~dp0WinPython_PS_Prompt.ps1""'}"
-exit
\ No newline at end of file
diff --git a/portable/scripts/register_python_for_all.bat b/portable/scripts/register_python_for_all.bat
deleted file mode 100644
index 9f45aa70..00000000
--- a/portable/scripts/register_python_for_all.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@echo off
-call "%~dp0env.bat"
-"%WINPYDIR%\python.exe" "%WINPYDIR%\Lib\site-packages\winpython\associate.py" --all
diff --git a/portable/scripts/unregister_python_for_all.bat b/portable/scripts/unregister_python_for_all.bat
deleted file mode 100644
index b1600226..00000000
--- a/portable/scripts/unregister_python_for_all.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@echo off
-call "%~dp0env.bat"
-"%WINPYDIR%\python.exe" "%WINPYDIR%\Lib\site-packages\winpython\associate.py" --unregister --all
diff --git a/portable/scripts/winspyder.bat b/portable/scripts/winspyder.bat
index deae01c5..1a843ca0 100644
--- a/portable/scripts/winspyder.bat
+++ b/portable/scripts/winspyder.bat
@@ -1,3 +1,5 @@
@echo off
call "%~dp0env_for_icons.bat" %*
-"%WINPYDIR%\scripts\spyder.exe" %* -w "%WINPYWORKDIR1%"
\ No newline at end of file
+rem "%WINPYDIR%\scripts\spyder.exe" %* -w "%WINPYWORKDIR1%"
+"%WINPYDIR%\scripts\spyder.exe" %*
+
diff --git a/winpython/__init__.py b/winpython/__init__.py
index b865bf4c..468eed57 100644
--- a/winpython/__init__.py
+++ b/winpython/__init__.py
@@ -28,6 +28,6 @@
OTHER DEALINGS IN THE SOFTWARE.
"""
-__version__ = '15.4.20250507'
+__version__ = '16.4.20250603'
__license__ = __doc__
__project_url__ = 'http://winpython.github.io/'
diff --git a/winpython/associate.py b/winpython/associate.py
index 5d5854ff..894d8bda 100644
--- a/winpython/associate.py
+++ b/winpython/associate.py
@@ -10,9 +10,71 @@
from pathlib import Path
import importlib.util
import winreg
-from winpython import utils
+from . import utils
from argparse import ArgumentParser
+def get_special_folder_path(path_name):
+ """Return special folder path."""
+ from win32com.shell import shell, shellcon
+ try:
+ csidl = getattr(shellcon, path_name)
+ return shell.SHGetSpecialFolderPath(0, csidl, False)
+ except OSError:
+ print(f"{path_name} is an unknown path ID")
+
+def get_winpython_start_menu_folder(current=True):
+ """Return WinPython Start menu shortcuts folder."""
+ folder = get_special_folder_path("CSIDL_PROGRAMS")
+ if not current:
+ try:
+ folder = get_special_folder_path("CSIDL_COMMON_PROGRAMS")
+ except OSError:
+ pass
+ return str(Path(folder) / 'WinPython')
+
+def remove_winpython_start_menu_folder(current=True):
+ """Remove WinPython Start menu folder -- remove it if it already exists"""
+ path = get_winpython_start_menu_folder(current=current)
+ if Path(path).is_dir():
+ try:
+ shutil.rmtree(path, onexc=onerror)
+ except WindowsError:
+ print(f"Directory {path} could not be removed", file=sys.stderr)
+
+def create_winpython_start_menu_folder(current=True):
+ """Create WinPython Start menu folder."""
+ path = get_winpython_start_menu_folder(current=current)
+ if Path(path).is_dir():
+ try:
+ shutil.rmtree(path, onexc=onerror)
+ except WindowsError:
+ print(f"Directory {path} could not be removed", file=sys.stderr)
+ Path(path).mkdir(parents=True, exist_ok=True)
+ return path
+
+def create_shortcut(path, description, filename, arguments="", workdir="", iconpath="", iconindex=0, verbose=True):
+ """Create Windows shortcut (.lnk file)."""
+ import pythoncom
+ from win32com.shell import shell
+ ilink = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
+ ilink.SetPath(path)
+ ilink.SetDescription(description)
+ if arguments:
+ ilink.SetArguments(arguments)
+ if workdir:
+ ilink.SetWorkingDirectory(workdir)
+ if iconpath or iconindex:
+ ilink.SetIconLocation(iconpath, iconindex)
+ # now save it.
+ ipf = ilink.QueryInterface(pythoncom.IID_IPersistFile)
+ if not filename.endswith('.lnk'):
+ filename += '.lnk'
+ if verbose:
+ print(f'create menu *{filename}*')
+ try:
+ ipf.Save(filename, 0)
+ except:
+ print("a fail !")
# --- Helper functions for Registry ---
@@ -53,7 +115,7 @@ def _has_pywin32():
def _remove_start_menu_folder(target, current=True, has_pywin32=False):
"remove menu Folder for target WinPython if pywin32 exists"
if has_pywin32:
- utils.remove_winpython_start_menu_folder(current=current)
+ remove_winpython_start_menu_folder(current=current)
else:
print("Skipping start menu removal as pywin32 package is not installed.")
@@ -68,7 +130,7 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
bname, ext = Path(name).stem, Path(name).suffix
if ext.lower() == ".exe":
# Path for the shortcut file in the start menu folder
- shortcut_name = str(Path(utils.create_winpython_start_menu_folder(current=current)) / bname) + '.lnk'
+ shortcut_name = str(Path(create_winpython_start_menu_folder(current=current)) / bname) + '.lnk'
data.append(
(
str(Path(wpdir) / name), # Target executable path
@@ -133,8 +195,8 @@ def register_in_registery(target, current=True, reg_type=winreg.REG_SZ, verbose=
lost_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with IDLE", None, None))
if Path(spyder_exe).exists():
- dynamic_entries.append((rf"Software\Classes\Python.File\shell\Edit with Spyder\command", None, f'"{spyder_exe}" "%1"'))
- dynamic_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder\command", None, f'"{spyder_exe}" "%1"'))
+ dynamic_entries.append((rf"Software\Classes\Python.File\shell\Edit with Spyder\command", None, f'"{spyder_exe}" "%1" -w "%w"'))
+ dynamic_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder\command", None, f'"{spyder_exe}" "%1" -w "%w"'))
lost_entries.append((rf"Software\Classes\Python.File\shell\Edit with Spyder", None, None))
lost_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder", None, None))
@@ -180,9 +242,9 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
print(f'Creating WinPython menu for all icons in {target.parent}')
for path, desc, fname in _get_shortcut_data(target, current=current, has_pywin32=True):
try:
- utils.create_shortcut(path, desc, fname, verbose=verbose)
+ create_shortcut(path, desc, fname, verbose=verbose)
except Exception as e:
- print(f"Error creating shortcut for {desc} at {fname}: {e}", file=sys.stderr)
+ print(f"Error creating shortcut for {desc} at {fname}: {e}", file=sys.stderr)
else:
print("Skipping start menu shortcut creation as pywin32 package is needed.")
diff --git a/winpython/diff.py b/winpython/diff.py
new file mode 100644
index 00000000..a8e52e1d
--- /dev/null
+++ b/winpython/diff.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+#
+# WinPython diff.py script
+# Copyright © 2013 Pierre Raybaut
+# Copyright © 2014-2025+ The Winpython development team https://github.com/winpython/
+# Licensed under the terms of the MIT License
+# (see winpython/__init__.py for details)
+
+import os
+from pathlib import Path
+import re
+import shutil
+from packaging import version
+from . import utils
+
+CHANGELOGS_DIR = Path(__file__).parent.parent / "changelogs"
+assert CHANGELOGS_DIR.is_dir()
+
+class Package:
+ PATTERNS = [
+ r"\[([\w\-\:\/\.\_]+)\]\(([^)]+)\) \| ([^\|]*) \| ([^\|]*)", # SourceForge
+ r"\[([\w\-\:\/\.\_]+) ([^\]\ ]+)\] \| ([^\|]*) \| ([^\|]*)" # Google Code
+ ]
+
+ def __init__(self, text=None):
+ self.name = self.url = self.version = self.description = None
+ if text:
+ self.from_text(text)
+
+ def from_text(self, text):
+ for pattern in self.PATTERNS:
+ match = re.match(pattern, text)
+ if match:
+ self.name, self.url, self.version, self.description = match.groups()
+ return
+ raise ValueError(f"Unrecognized package line format: {text}")
+
+ def to_wiki(self):
+ return f" * [{self.name}]({self.url}) {self.version} ({self.description})\n"
+
+ def upgrade_wiki(self, other):
+ return f" * [{self.name}]({self.url}) {other.version} → {self.version} ({self.description})\n"
+
+class PackageIndex:
+ HEADERS = {"tools": "### Tools", "python": "### Python packages", "wheelhouse": "### WheelHouse packages"}
+ BLANKS = ["Name | Version | Description", "-----|---------|------------", "", "", " "]
+
+ def __init__(self, version, searchdir=None, flavor="", architecture=64):
+ self.version = version
+ self.flavor = flavor
+ self.searchdir = searchdir
+ self.architecture = architecture
+ self.packages = {"tools": {}, "python": {}, "wheelhouse": {}}
+ self._load_index()
+
+ def _load_index(self):
+ filename = self.searchdir / f"WinPython{self.flavor}-{self.architecture}bit-{self.version}.md"
+ if not filename.exists():
+ raise FileNotFoundError(f"Changelog not found: {filename}")
+
+ with open(filename, "r", encoding=utils.guess_encoding(filename)[0]) as f:
+ self._parse_index(f.read())
+
+ def _parse_index(self, text):
+ current = None
+ for line in text.splitlines():
+ if line in self.HEADERS.values():
+ current = [k for k, v in self.HEADERS.items() if v == line][0]
+ continue
+ if line.strip() in self.BLANKS:
+ continue
+ if current:
+ pkg = Package(line)
+ self.packages[current][pkg.name] = pkg
+
+def compare_packages(old, new):
+ """Return difference between package old and package new"""
+
+ # wheel replace '-' per '_' in key
+ def normalize(d): return {k.replace("-", "_").lower(): v for k, v in d.items()}
+ old, new = normalize(old), normalize(new)
+ output = ""
+
+ added = [new[k].to_wiki() for k in new if k not in old]
+ upgraded = [new[k].upgrade_wiki(old[k]) for k in new if k in old and new[k].version != old[k].version]
+ removed = [old[k].to_wiki() for k in old if k not in new]
+
+ if added:
+ output += "New packages:\n\n" + "".join(added) + "\n\n"
+ if upgraded:
+ output += "Upgraded packages:\n\n" + "".join(upgraded) + "\n\n"
+ if removed:
+ output += "Removed packages:\n\n" + "".join(removed) + "\n\n"
+ return output
+
+def find_previous_version(target_version, searchdir=None, flavor="", architecture=64):
+ """Find version which is the closest to `version`"""
+ search_dir = Path(searchdir) if searchdir else CHANGELOGS_DIR
+ pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-([0-9\.]+)\.(txt|md)")
+ versions = [pattern.match(f).group(1) for f in os.listdir(search_dir) if pattern.match(f)]
+ versions = [v for v in versions if version.parse(v) < version.parse(target_version)]
+ return max(versions, key=version.parse, default=target_version)
+
+def compare_package_indexes(version2, version1=None, searchdir=None, flavor="", flavor1=None, architecture=64):
+ version1 = version1 or find_previous_version(version2, searchdir, flavor, architecture)
+ flavor1 = flavor1 or flavor
+
+ pi1 = PackageIndex(version1, searchdir, flavor1, architecture)
+ pi2 = PackageIndex(version2, searchdir, flavor, architecture)
+
+ text = (
+ f"## History of changes for WinPython-{architecture}bit {version2 + flavor}\r\n\r\n"
+ f"The following changes were made to WinPython-{architecture}bit distribution since version {version1 + flavor1}.\n\n\n"
+ "\n\n"
+ )
+
+ for key in PackageIndex.HEADERS:
+ diff = compare_packages(pi1.packages[key], pi2.packages[key])
+ if diff:
+ text += f"\n{PackageIndex.HEADERS[key]}\n\n{diff}"
+
+ return text + "\n \n\n* * *\n"
+
+def copy_changelogs(version, searchdir, flavor="", architecture=64, basedir=None):
+ basever = ".".join(version.split(".")[:2])
+ pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-{basever}[0-9\.]*\.(txt|md)")
+ dest = Path(basedir)
+ for fname in os.listdir(searchdir):
+ if pattern.match(fname):
+ shutil.copyfile(searchdir / fname, dest / fname)
+
+def write_changelog(version2, version1=None, searchdir=None, flavor="", architecture=64, basedir=None):
+ """Write changelog between version1 and version2 of WinPython"""
+ if basedir:
+ copy_changelogs(version2, searchdir, flavor, architecture, basedir)
+ print("comparing_package_indexes", version2, searchdir, flavor, architecture)
+ changelog = compare_package_indexes(version2, version1, searchdir, flavor, architecture=architecture)
+ output_file = searchdir / f"WinPython{flavor}-{architecture}bit-{version2}_History.md"
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write(changelog)
+ # Copy to winpython/changelogs back to basedir
+ if basedir:
+ shutil.copyfile(output_file, basedir / output_file.name)
+
+if __name__ == "__main__":
+ print(compare_package_indexes("3.7.4.0", "3.7.2.0", r"C:\WinP\bd37\budot", "Zero", architecture=32))
+ write_changelog("3.7.4.0", "3.7.2.0", r"C:\WinP\bd37\budot", "Ps2", architecture=64)
diff --git a/winpython/packagemetadata.py b/winpython/packagemetadata.py
new file mode 100644
index 00000000..4e2a4989
--- /dev/null
+++ b/winpython/packagemetadata.py
@@ -0,0 +1,103 @@
+#!/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
+
+# --- 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 = []
+ description_lines = []
+ in_description = False
+ 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})
+
+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()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/winpython/piptree.py b/winpython/piptree.py
index 23a53ff3..e46bd465 100644
--- a/winpython/piptree.py
+++ b/winpython/piptree.py
@@ -17,6 +17,8 @@
from pip._vendor.packaging.markers import Marker
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__)
@@ -25,19 +27,10 @@ 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."""
- def __init__(self, target: Optional[str] = None):
+ def __init__(self, target: Optional[str] = None, wheelhouse = None):
"""
Initialize the PipData instance.
@@ -47,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:
@@ -75,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."""
@@ -287,5 +282,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]
diff --git a/winpython/utils.py b/winpython/utils.py
index 40961ec1..2d549f20 100644
--- a/winpython/utils.py
+++ b/winpython/utils.py
@@ -11,15 +11,11 @@
import stat
import shutil
import locale
-import tempfile
import subprocess
-import configparser as cp
from pathlib import Path
import re
import tarfile
import zipfile
-import atexit
-import winreg
# SOURCE_PATTERN defines what an acceptable source package name is
SOURCE_PATTERN = r'([a-zA-Z0-9\-\_\.]*)-([0-9\.\_]*[a-z]*[\-]?[0-9]*)(\.zip|\.tar\.gz|\-(py[2-7]*|py[2-7]*\.py[2-7]*)\-none\-any\.whl)'
@@ -43,25 +39,24 @@ def get_site_packages_path(path=None):
pypy_site_packages = base_dir / 'site-packages' # For PyPy
return str(pypy_site_packages if pypy_site_packages.is_dir() else site_packages)
-def get_installed_tools_markdown(path=None)-> str:
+def get_installed_tools(path=None)-> str:
"""Generates Markdown for installed tools section in package index."""
tool_lines = []
python_exe = Path(get_python_executable(path))
version = exec_shell_cmd(f'powershell (Get-Item {python_exe}).VersionInfo.FileVersion', python_exe.parent).splitlines()[0]
- tool_lines.append(f"[Python](http://www.python.org/) | {version} | Python programming language with standard library")
+ tool_lines.append(("Python" ,f"http://www.python.org/", version, "Python programming language with standard library"))
if (node_exe := python_exe.parent.parent / "n" / "node.exe").exists():
version = exec_shell_cmd(f'powershell (Get-Item {node_exe}).VersionInfo.FileVersion', node_exe.parent).splitlines()[0]
- tool_lines.append(f"[Nodejs](https://nodejs.org) | {version} | a JavaScript runtime built on Chrome's V8 JavaScript engine")
+ tool_lines.append("Nodejs", "https://nodejs.org", version, "a JavaScript runtime built on Chrome's V8 JavaScript engine")
if (pandoc_exe := python_exe.parent.parent / "t" / "pandoc.exe").exists():
version = exec_shell_cmd("pandoc -v", pandoc_exe.parent).splitlines()[0].split(" ")[-1]
- tool_lines.append(f"[Pandoc](https://pandoc.org) | {version} | an universal document converter")
+ tool_lines.append("Pandoc", "https://pandoc.org", version, "an universal document converter")
if (vscode_exe := python_exe.parent.parent / "t" / "VSCode" / "Code.exe").exists():
version = exec_shell_cmd(f'powershell (Get-Item {vscode_exe}).VersionInfo.FileVersion', vscode_exe.parent).splitlines()[0]
- tool_lines.append(f"[VSCode](https://code.visualstudio.com) | {version} | a source-code editor developed by Microsoft")
- return "\n".join(tool_lines)
-
+ tool_lines.append("VSCode","https://code.visualstudio.com", version, "a source-code editor developed by Microsoft")
+ return tool_lines
def onerror(function, path, excinfo):
"""Error handler for `shutil.rmtree`."""
@@ -71,93 +66,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 get_special_folder_path(path_name):
- """Return special folder path."""
- from win32com.shell import shell, shellcon
- try:
- csidl = getattr(shellcon, path_name)
- return shell.SHGetSpecialFolderPath(0, csidl, False)
- except OSError:
- print(f"{path_name} is an unknown path ID")
-
-def get_winpython_start_menu_folder(current=True):
- """Return WinPython Start menu shortcuts folder."""
- folder = get_special_folder_path("CSIDL_PROGRAMS")
- if not current:
- try:
- folder = get_special_folder_path("CSIDL_COMMON_PROGRAMS")
- except OSError:
- pass
- return str(Path(folder) / 'WinPython')
-
-def remove_winpython_start_menu_folder(current=True):
- """Remove WinPython Start menu folder -- remove it if it already exists"""
- path = get_winpython_start_menu_folder(current=current)
- if Path(path).is_dir():
- try:
- shutil.rmtree(path, onexc=onerror)
- except WindowsError:
- print(f"Directory {path} could not be removed", file=sys.stderr)
-
-def create_winpython_start_menu_folder(current=True):
- """Create WinPython Start menu folder."""
- path = get_winpython_start_menu_folder(current=current)
- if Path(path).is_dir():
- try:
- shutil.rmtree(path, onexc=onerror)
- except WindowsError:
- print(f"Directory {path} could not be removed", file=sys.stderr)
- Path(path).mkdir(parents=True, exist_ok=True)
- return path
-
-def create_shortcut(path, description, filename, arguments="", workdir="", iconpath="", iconindex=0, verbose=True):
- """Create Windows shortcut (.lnk file)."""
- import pythoncom
- from win32com.shell import shell
- ilink = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
- ilink.SetPath(path)
- ilink.SetDescription(description)
- if arguments:
- ilink.SetArguments(arguments)
- if workdir:
- ilink.SetWorkingDirectory(workdir)
- if iconpath or iconindex:
- ilink.SetIconLocation(iconpath, iconindex)
- # now save it.
- ipf = ilink.QueryInterface(pythoncom.IID_IPersistFile)
- if not filename.endswith('.lnk'):
- filename += '.lnk'
- if verbose:
- print(f'create menu *{filename}*')
- try:
- ipf.Save(filename, 0)
- except:
- print("a fail !")
+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 print_box(text):
"""Print text in a box"""
diff --git a/winpython/wheelhouse.py b/winpython/wheelhouse.py
new file mode 100644
index 00000000..f500dc31
--- /dev/null
+++ b/winpython/wheelhouse.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+"""
+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 . import packagemetadata as pm
+from . import utils
+
+from packaging.utils import canonicalize_name, parse_wheel_filename, parse_sdist_filename
+
+# Use tomllib if available (Python 3.11+), otherwise fall back to tomli
+try:
+ import tomllib # Python 3.11+
+except ImportError:
+ try:
+ import tomli as tomllib # For older Python versions
+ except ImportError:
+ print("Please install tomli for Python < 3.11: pip install tomli")
+ sys.exit(1)
+
+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])
+ package_hashes = defaultdict(lambda: {"version": "", "hashes": []})
+
+ for entry in data.get("packages", []):
+ name = entry["name"]
+ version = entry["version"]
+ all_hashes = []
+
+ # Handle wheels
+ for wheel in entry.get("wheels", []):
+ sha256 = wheel.get("hashes", {}).get("sha256")
+ if sha256:
+ all_hashes.append(sha256)
+
+ # Handle sdist (if present)
+ sdist = entry.get("sdist")
+ if sdist and "hashes" in sdist:
+ sha256 = sdist["hashes"].get("sha256")
+ if sha256:
+ all_hashes.append(sha256)
+
+ package_hashes[name]["version"] = version
+ package_hashes[name]["hashes"].extend(all_hashes)
+
+ return package_hashes
+
+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"]
+
+ if hashes:
+ f.write(f"{name}=={version} \\\n")
+ for i, h in enumerate(hashes):
+ end = " \\\n" if i < len(hashes) - 1 else "\n"
+ f.write(f" --hash=sha256:{h}{end}")
+ else:
+ f.write(f"{name}=={version}\n")
+
+ print(f"✅ requirements.txt written to {output_path}")
+
+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')
+ write_requirements_txt(pkgs, output_path)
+
+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, wheeldrain: Path, wheelorigin: Optional[Path] = None
+ , only_check: bool = True,post_install: bool = False) -> bool:
+ """Download or check Python wheels based on requirements."""
+ added = []
+ if wheelorigin:
+ added = ['--no-index', '--trusted-host=None', f'--find-links={wheelorigin}']
+ 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(wheeldrain)] + added
+ if wheeldrain:
+ added = ['--no-index', '--trusted-host=None', f'--find-links={wheeldrain}']
+ 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 wheels
+ if wheelorigin and wheelorigin != wheeldrain:
+ 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, wheelorigin: Optional[Path] = None, wheeldrain: Optional[Path] = None) -> None:
+ """Get wheels asked 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') + '.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 = Path(wheeldrain) if wheeldrain else wheelhouse / Path(lockfile).name.replace('.toml', '.wheels')
+ destination_wheelhouse.mkdir(parents=True, exist_ok=True)
+ # there can be an override
+
+
+ in_trusted = False
+
+ if wheelorigin 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, wheelorigin=trusted_wheelhouse, only_check=True)
+ if in_trusted:
+ print(f"\n\n*** We can install from Local WheelHouse: ***\n {trusted_wheelhouse}\n\n")
+ in_installed = get_wheels(filename_req, trusted_wheelhouse, wheelorigin=trusted_wheelhouse, only_check=False, post_install=True)
+
+ if not in_trusted:
+ post_install = True if wheelorigin and Path(wheelorigin).is_dir and Path(wheelorigin).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 wheelorigin or wheelorigin == '' else wheelorigin}\n\n")
+
+ in_pylock = get_wheels(filename_req, destination_wheelhouse, wheelorigin=wheelorigin, 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 list_packages_with_metadata(directory: str) -> List[Tuple[str, str, str]]:
+ "get metadata from a Wheelhouse directory"
+ packages = pm.get_directory_metadata(directory)
+ results = [ (p.name, p.version, p.summary) for p in packages]
+ return results
+
+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)
+
+ path = Path(sys.argv[1])
+ if not path.exists():
+ print(f"❌ File not found: {path}")
+ sys.exit(1)
+
+ pkgs = parse_pylock_toml(path)
+ dest = path.parent / (path.stem.replace('pylock', 'requirement_with_hash') + '.txt')
+ write_requirements_txt(pkgs, dest)
+
+if __name__ == "__main__":
+ main()
diff --git a/winpython/wppm.py b/winpython/wppm.py
index 659d14a4..01ed2c83 100644
--- a/winpython/wppm.py
+++ b/winpython/wppm.py
@@ -14,8 +14,9 @@
import json
from pathlib import Path
from argparse import ArgumentParser, RawTextHelpFormatter
-from winpython import utils, piptree, associate
-
+from . import utils, piptree, associate
+from . import wheelhouse as wh
+from operator import itemgetter
# Workaround for installing PyVISA on Windows from source:
os.environ["HOME"] = os.environ["USERPROFILE"]
@@ -23,8 +24,8 @@ 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.name, self.version = None, None
+ 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..."
infos = utils.get_source_package_infos(bname) # get name, version
@@ -47,6 +48,7 @@ def __init__(self, target: str = None, verbose: bool = False):
self.version, self.architecture = utils.get_python_infos(self.target)
self.python_exe = utils.get_python_executable(self.target)
self.short_exe = Path(self.python_exe).name
+ self.wheelhouse = Path(self.target).parent / "wheelhouse"
def create_file(self, package, name, dstdir, contents):
"""Generate data file -- path is relative to distribution root dir"""
@@ -67,13 +69,45 @@ def get_installed_packages(self, update: bool = False) -> list[Package]:
pip_list = self.pip.pip_list(full=True)
return [Package(f"{i[0].replace('-', '_').lower()}-{i[1]}-py3-none-any.whl", suggested_summary=i[2]) for i in pip_list]
- def get_installed_packages_markdown(self) -> str:
- """Generates Markdown for installed packages section in package index."""
- package_lines = [
- f"[{pkg.name}]({pkg.url}) | {pkg.version} | {pkg.description}"
- for pkg in sorted(self.get_installed_packages(), key=lambda p: p.name.lower())
- ]
- return "\n".join(package_lines)
+ def render_markdown_for_list(self, title, items):
+ """Generates a Markdown section; name, url, version, summary"""
+ md = f"### {title}\n\n"
+ md += "Name | Version | Description\n"
+ md += "-----|---------|------------\n"
+ for name, url, version, summary in sorted(items, key=lambda p: (p[0].lower(), p[2])):
+ md += f"[{name}]({url}) | {version} | {summary} \n"
+ md += "\n"
+ return md
+
+ def generate_package_index_markdown(self, python_executable_directory: str|None = None, winpyver2: str|None = None,
+ flavor: str|None = None, architecture_bits: int|None = None, release_level: str|None = None) -> str:
+ """Generates a Markdown formatted package index page."""
+ my_ver , my_arch = utils.get_python_infos(python_executable_directory or self.target)
+ # suppose we suite ourself (method will vary over time)
+ my_winpyver2 = winpyver2 or os.getenv("WINPYVER2","")
+ my_winpyver2 = my_winpyver2 if my_winpyver2 != "" else my_ver
+ my_flavor = flavor or os.getenv("WINPYFLAVOR", "")
+ my_release_level = release_level or os.getenv("WINPYVER", "").replace(my_winpyver2+my_flavor, "")
+
+ tools_list = utils.get_installed_tools(utils.get_python_executable(python_executable_directory))
+ package_list = [(pkg.name, pkg.url, pkg.version, pkg.description) for pkg in self.get_installed_packages()]
+ wheelhouse_list = []
+ wheeldir = self.wheelhouse / 'included.wheels'
+ if wheeldir.is_dir():
+ wheelhouse_list = [(name, f"https://pypi.org/project/{name}", version, summary)
+ for name, version, summary in wh.list_packages_with_metadata(str(wheeldir)) ]
+
+ return f"""## WinPython {my_winpyver2 + my_flavor}
+
+The following packages are included in WinPython-{my_arch}bit v{my_winpyver2 + my_flavor} {my_release_level}.
+
+
+
+{self.render_markdown_for_list("Tools", tools_list)}
+{self.render_markdown_for_list("Python packages", package_list)}
+{self.render_markdown_for_list("WheelHouse packages", wheelhouse_list)}
+
+"""
def find_package(self, name: str) -> Package | None:
"""Find installed package by name."""
@@ -91,7 +125,8 @@ def patch_all_shebang(self, to_movable: bool = True, max_exe_size: int = 999999,
def install(self, package: Package, install_options: list[str] = None):
"""Install package in distribution."""
- if package.fname.endswith((".whl", ".tar.gz", ".zip")): # Check extension with tuple
+ if package.fname.endswith((".whl", ".tar.gz", ".zip")) or (
+ ' ' not in package.fname and ';' not in package.fname and len(package.fname) >1): # Check extension with tuple
self.install_bdist_direct(package, install_options=install_options)
self.handle_specific_packages(package)
# minimal post-install actions
@@ -231,7 +266,7 @@ 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")
@@ -239,13 +274,16 @@ 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("-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("-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)")
@@ -253,22 +291,26 @@ 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:
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)
@@ -276,7 +318,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]])
@@ -323,22 +365,29 @@ 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 not args.install and not args.uninstall:
- args.install = True
- if not Path(args.fname).is_file() and args.install:
- if args.fname == "":
+ 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):
parser.print_help()
sys.exit()
- else:
- raise FileNotFoundError(f"File not found: {args.fname}")
+ else:
try:
+ filename = Path(args.fname).name
+ install_from_wheelhouse = ["--no-index", "--trusted-host=None", f"--find-links={dist.wheelhouse / 'included.wheels'}"]
+ if filename.split('.')[0] == "pylock" and filename.split('.')[-1] == 'toml':
+ print(' a lock file !', args.fname, dist.target)
+ wh.get_pylock_wheels(dist.wheelhouse, Path(args.fname), args.wheelsource, args.wheeldrain)
+ sys.exit()
if args.uninstall:
package = dist.find_package(args.fname)
dist.uninstall(package)
elif args.install:
package = Package(args.fname)
if args.install:
- dist.install(package)
+ dist.install(package, install_options=install_from_wheelhouse)
except NotImplementedError:
raise RuntimeError("Package is not (yet) supported by WPPM")
else: