diff --git a/make.py b/make.py
index ec75c078..651cb458 100644
--- a/make.py
+++ b/make.py
@@ -12,7 +12,7 @@
import subprocess
import sys
from pathlib import Path
-from winpython import wppm, utils, diff
+from wppm import wppm, utils, diff
# Define constant paths for clarity
CHANGELOGS_DIRECTORY = Path(__file__).parent / "changelogs"
@@ -222,7 +222,7 @@ def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirnam
utils.python_execmodule("ensurepip", self.distribution.target)
self.distribution.patch_standard_packages("pip")
- essential_packages = ["pip", "setuptools", "wheel", "winpython"]
+ essential_packages = ["pip", "setuptools", "wheel", "wppm"]
for package_name in essential_packages:
actions = ["install", "--upgrade", "--pre", package_name] + self.install_options
self._print_action(f"Piping: {' '.join(actions)}")
@@ -247,8 +247,8 @@ def build(self, rebuild: bool = True, requirements_files_list=None, winpy_dirnam
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-*"):
+ """Rebuilds the winpython or wppm package from source using flit."""
+ for file in target_directory.glob("w*p*-*.*"):
if file.suffix in (".exe", ".whl", ".gz"):
file.unlink()
utils.buildflit_wininst(source_directory, copy_to=target_directory, verbose=True)
@@ -295,7 +295,7 @@ def make_all(build_number: int, release_level: str, pyver: str, architecture: in
os.makedirs(build_directory, exist_ok=True)
# use source_dirs as the directory to re-build Winpython wheel
winpython_source_dir = Path(__file__).resolve().parent
- rebuild_winpython_package(winpython_source_dir, Path(source_dirs), architecture, verbose)
+ # 2025-06-28 no more: rebuild_winpython_package(winpython_source_dir, Path(source_dirs), architecture, verbose)
builder = WinPythonDistributionBuilder(
build_number, release_level, build_directory, wheels_directory=source_dirs,
diff --git a/portable/scripts/make_working_directory_and_userprofile_be_winpython.bat b/portable/scripts/make_working_directory_and_userprofile_be_winpython.bat
index 2806dc14..489aef0b 100644
--- a/portable/scripts/make_working_directory_and_userprofile_be_winpython.bat
+++ b/portable/scripts/make_working_directory_and_userprofile_be_winpython.bat
@@ -1,3 +1,3 @@
call "%~dp0env_for_icons.bat"
-"%PYTHON%" -c "from winpython.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
-"%PYTHON%" -c "from winpython.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[inactive_environment_common]', '[active_environment_common]' )"
\ No newline at end of file
+"%PYTHON%" -c "from wppm.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
+"%PYTHON%" -c "from wppm.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[inactive_environment_common]', '[active_environment_common]' )"
\ No newline at end of file
diff --git a/portable/scripts/make_working_directory_be_not_winpython.bat b/portable/scripts/make_working_directory_be_not_winpython.bat
index 759de71d..7a726169 100644
--- a/portable/scripts/make_working_directory_be_not_winpython.bat
+++ b/portable/scripts/make_working_directory_be_not_winpython.bat
@@ -1,3 +1,3 @@
call "%~dp0env_for_icons.bat"
-"%PYTHON%" -c "from winpython.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
-"%PYTHON%" -c "from winpython.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[inactive_environment_per_user]', '[active_environment_per_user]' )"
\ No newline at end of file
+"%PYTHON%" -c "from wppm.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
+"%PYTHON%" -c "from wppm.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[inactive_environment_per_user]', '[active_environment_per_user]' )"
\ No newline at end of file
diff --git a/portable/scripts/make_working_directory_be_winpython.bat b/portable/scripts/make_working_directory_be_winpython.bat
index bcdd3c45..0f422a4e 100644
--- a/portable/scripts/make_working_directory_be_winpython.bat
+++ b/portable/scripts/make_working_directory_be_winpython.bat
@@ -1,2 +1,2 @@
call "%~dp0env_for_icons.bat"
-"%PYTHON%" -c "from winpython.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
+"%PYTHON%" -c "from wppm.utils import patch_sourcefile;patch_sourcefile(r'%~dp0..\settings\winpython.ini', '[active_environment', '[inactive_environment' )"
diff --git a/portable/scripts/upgrade_pip.bat b/portable/scripts/upgrade_pip.bat
index da212592..4443ed16 100644
--- a/portable/scripts/upgrade_pip.bat
+++ b/portable/scripts/upgrade_pip.bat
@@ -3,5 +3,5 @@ call "%~dp0env.bat"
echo this will upgrade pip with latest version, then patch it for WinPython portability ok ?
pause
"%WINPYDIR%\python.exe" -m pip install --upgrade pip
-"%WINPYDIR%\python.exe" -c "from winpython import wppm;dist=wppm.Distribution(r'%WINPYDIR%');dist.patch_standard_packages('pip', to_movable=True)
+"%WINPYDIR%\python.exe" -c "from wppm import wppm;dist=wppm.Distribution(r'%WINPYDIR%');dist.patch_standard_packages('pip', to_movable=True)
pause
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 2e96abec..4460ae5a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,13 +3,13 @@ requires = ["flit_core"]
build-backend = "flit_core.buildapi"
[project]
-name = "winpython"
+name = "wppm"
authors = [
{name = "Pierre Raybaut"},
{name = "stonebig"},
]
dependencies = []
-requires-python = ">=3.8"
+requires-python = ">=3.10"
readme = "README.rst"
license = {file = "LICENSE"}
classifiers=[
@@ -25,7 +25,7 @@ classifiers=[
'Topic :: Software Development :: Widget Sets',
]
dynamic = ["version",]
-description="WinPython distribution tools, including WPPM"
+description="WinPython Package Management"
keywords = ["Portable","Windows"]
[project.urls]
@@ -33,4 +33,4 @@ Documentation = "https://winpython.github.io/"
Source = "https://github.com/winpython/winpython"
[project.scripts]
-wppm = "winpython.wppm:main"
+wppm = "wppm.wppm:main"
diff --git a/winpython/__init__.py b/wppm/__init__.py
similarity index 95%
rename from winpython/__init__.py
rename to wppm/__init__.py
index b6c1d768..16dcd6d5 100644
--- a/winpython/__init__.py
+++ b/wppm/__init__.py
@@ -1,33 +1,33 @@
-# -*- coding: utf-8 -*-
-"""
-WinPython License Agreement (MIT License)
------------------------------------------
-
-Copyright (c) 2012-2013 Pierre Raybaut
-Copyright (c) 2014-2025+ The Winpython development team https://github.com/winpython/
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-"""
-
-__version__ = '16.6.20250620'
-__license__ = __doc__
-__project_url__ = 'http://winpython.github.io/'
+# -*- coding: utf-8 -*-
+"""
+WinPython License Agreement (MIT License)
+-----------------------------------------
+
+Copyright (c) 2012-2013 Pierre Raybaut
+Copyright (c) 2014-2025+ The Winpython development team https://github.com/winpython/
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+"""
+
+__version__ = '17.0.20250628'
+__license__ = __doc__
+__project_url__ = 'http://winpython.github.io/'
diff --git a/winpython/associate.py b/wppm/associate.py
similarity index 97%
rename from winpython/associate.py
rename to wppm/associate.py
index 894d8bda..9e02182b 100644
--- a/winpython/associate.py
+++ b/wppm/associate.py
@@ -1,300 +1,300 @@
-# -*- coding: utf-8 -*-
-#
-# associate.py = Register a Python distribution
-# Copyright © 2012 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see winpython/__init__.py for details)
-
-import sys
-import os
-from pathlib import Path
-import importlib.util
-import winreg
-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 ---
-
-def _set_reg_value(root, key_path, name, value, reg_type=winreg.REG_SZ, verbose=False):
- """Helper to create key and set a registry value using CreateKeyEx."""
- rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
- if verbose:
- print(f"{rootkey_name}\\{key_path}\\{name if name else ''}:{value}")
- try:
- # Use CreateKeyEx with context manager for automatic closing
- with winreg.CreateKeyEx(root, key_path, 0, winreg.KEY_WRITE) as key:
- winreg.SetValueEx(key, name, 0, reg_type, value)
- except OSError as e:
- print(f"Error creating/setting registry value {rootkey_name}\\{key_path}\\{name}: {e}", file=sys.stderr)
-
-def _delete_reg_key(root, key_path, verbose=False):
- """Helper to delete a registry key, ignoring if not found."""
- rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
- if verbose:
- print(f"{rootkey_name}\\{key_path}")
- try:
- # DeleteKey can only delete keys with no subkeys.
- # For keys with (still) subkeys, use DeleteKeyEx on the parent key if available
- winreg.DeleteKey(root, key_path)
- except FileNotFoundError:
- if verbose:
- print(f"Registry key not found (skipping deletion): {rootkey_name}\\{key_path}")
- except OSError as e: # Catch other potential errors like key not empty
- print(f"Error deleting registry key {rootkey_name}\\{key_path}: {e}", file=sys.stderr)
-
-
-# --- Helper functions for Start Menu Shortcuts ---
-
-def _has_pywin32():
- """Check if pywin32 (pythoncom) is installed."""
- return importlib.util.find_spec('pythoncom') is not None
-
-def _remove_start_menu_folder(target, current=True, has_pywin32=False):
- "remove menu Folder for target WinPython if pywin32 exists"
- if has_pywin32:
- remove_winpython_start_menu_folder(current=current)
- else:
- print("Skipping start menu removal as pywin32 package is not installed.")
-
-def _get_shortcut_data(target, current=True, has_pywin32=False):
- "get windows menu access data if pywin32 exists, otherwise empty list"
- if not has_pywin32:
- return []
-
- wpdir = str(Path(target).parent)
- data = []
- for name in os.listdir(wpdir):
- 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(create_winpython_start_menu_folder(current=current)) / bname) + '.lnk'
- data.append(
- (
- str(Path(wpdir) / name), # Target executable path
- bname, # Description/Name
- shortcut_name, # Shortcut file path
- )
- )
- return data
-
-# --- PythonCore entries (PEP-0514 and WinPython specific) ---
-
-
-def register_in_registery(target, current=True, reg_type=winreg.REG_SZ, verbose=True) -> tuple[list[any], ...]:
- """Register in Windows (like regedit)"""
-
- # --- Constants ---
- DROP_HANDLER_CLSID = "{60254CA5-953B-11CF-8C96-00AA00B8708C}"
-
- # --- CONFIG ---
- target_path = Path(target).resolve()
- python_exe = str(target_path / "python.exe")
- pythonw_exe = str(target_path / "pythonw.exe")
- spyder_exe = str(target_path.parent / "Spyder.exe")
- icon_py = str(target_path / "DLLs" / "py.ico")
- icon_pyc = str(target_path / "DLLs" / "pyc.ico")
- idle_path = str(target_path / "Lib" / "idlelib" / "idle.pyw")
- doc_path = str(target_path / "Doc" / "html" / "index.html")
- python_infos = utils.get_python_infos(target) # ('3.11', 64)
- short_version = python_infos[0] # e.g., '3.11'
- version = utils.get_python_long_version(target) # e.g., '3.11.5'
- arch = f'{python_infos[1]}bit' # e.g., '64bit'
- display = f"Python {version} ({arch})"
-
- permanent_entries = [] # key_path, name, value
- dynamic_entries = [] # key_path, name, value
- core_entries = [] # key_path, name, value
- lost_entries = [] # intermediate keys to remove later
- # --- File associations ---
- ext_map = {".py": "Python.File", ".pyw": "Python.NoConFile", ".pyc": "Python.CompiledFile"}
- ext_label = {".py": "Python File", ".pyw": "Python File (no console)", ".pyc": "Compiled Python File"}
- for ext, ftype in ext_map.items():
- permanent_entries.append((f"Software\\Classes\\{ext}", None, ftype))
- if ext in (".py", ".pyw"):
- permanent_entries.append((f"Software\\Classes\\{ext}", "Content Type", "text/plain"))
-
- # --- Descriptions, Icons, DropHandlers ---
- for ext, ftype in ext_map.items():
- dynamic_entries.append((f"Software\\Classes\\{ftype}", None, ext_label[ext]))
- dynamic_entries.append((f"Software\\Classes\\{ftype}\\DefaultIcon", None, icon_py if "Compiled" not in ftype else icon_pyc))
- dynamic_entries.append((f"Software\\Classes\\{ftype}\\shellex\\DropHandler", None, DROP_HANDLER_CLSID))
- lost_entries.append((f"Software\\Classes\\{ftype}\\shellex", None, None))
-
- # --- Shell commands ---
- for ext, ftype in ext_map.items():
- dynamic_entries.append((f"Software\\Classes\\{ftype}\\shell\\open\\command", None, f'''"{pythonw_exe if ftype=='Python.NoConFile' else python_exe}" "%1" %*'''))
- lost_entries.append((f"Software\\Classes\\{ftype}\\shell\\open", None, None))
- lost_entries.append((f"Software\\Classes\\{ftype}\\shell", None, None))
-
- dynamic_entries.append((rf"Software\Classes\Python.File\shell\Edit with IDLE\command", None, f'"{pythonw_exe}" "{idle_path}" -n -e "%1"'))
- dynamic_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with IDLE\command", None, f'"{pythonw_exe}" "{idle_path}" -n -e "%1"'))
- lost_entries.append((rf"Software\Classes\Python.File\shell\Edit with IDLE", None, None))
- 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" -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))
-
- # --- WinPython Core registry entries (PEP 514 style) ---
- base = f"Software\\Python\\WinPython\\{short_version}"
- core_entries.append((base, "DisplayName", display))
- core_entries.append((base, "SupportUrl", "https://winpython.github.io"))
- core_entries.append((base, "SysVersion", short_version))
- core_entries.append((base, "SysArchitecture", arch))
- core_entries.append((base, "Version", version))
-
- core_entries.append((f"{base}\\InstallPath", None, str(target)))
- core_entries.append((f"{base}\\InstallPath", "ExecutablePath", python_exe))
- core_entries.append((f"{base}\\InstallPath", "WindowedExecutablePath", pythonw_exe))
- core_entries.append((f"{base}\\InstallPath\\InstallGroup", None, f"Python {short_version}"))
-
- core_entries.append((f"{base}\\Modules", None, ""))
- core_entries.append((f"{base}\\PythonPath", None, f"{target}\\Lib;{target}\\DLLs"))
- core_entries.append((f"{base}\\Help\\Main Python Documentation", None, doc_path))
- lost_entries.append((f"{base}\\Help", None, None))
- lost_entries.append((f"Software\\Python\\WinPython", None, None))
-
- return permanent_entries, dynamic_entries, core_entries, lost_entries
-
-# --- Main Register/Unregister Functions ---
-
-def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
- """Register a Python distribution in Windows registry and create Start Menu shortcuts"""
- root = winreg.HKEY_CURRENT_USER if current else winreg.HKEY_LOCAL_MACHINE
- has_pywin32 = _has_pywin32()
-
- if verbose:
- print(f'Creating WinPython registry entries for {target}')
-
- permanent_entries, dynamic_entries, core_entries, lost_entries = register_in_registery(target)
- # Set registry entries for given target
- for key_path, name, value in permanent_entries + dynamic_entries + core_entries:
- _set_reg_value(root, key_path, name, value, verbose=verbose)
-
- # Create start menu entries
- if has_pywin32:
- if verbose:
- 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:
- create_shortcut(path, desc, fname, verbose=verbose)
- except Exception as e:
- 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.")
-
-def unregister(target, current=True, verbose=True):
- """Unregister a Python distribution from Windows registry and remove Start Menu shortcuts"""
- root = winreg.HKEY_CURRENT_USER if current else winreg.HKEY_LOCAL_MACHINE
- has_pywin32 = _has_pywin32()
-
- if verbose:
- print(f'Removing WinPython registry entries for {target}')
-
- permanent_entries, dynamic_entries, core_entries , lost_entries = register_in_registery(target)
-
- # List of keys to attempt to delete, ordered from most specific to general
- keys_to_delete = sorted(list(set(key_path for key_path , name, value in (dynamic_entries + core_entries + lost_entries))), key=len, reverse=True)
-
- rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
- for key_path in keys_to_delete:
- _delete_reg_key(root, key_path, verbose=verbose)
-
- # Remove start menu shortcuts
- if has_pywin32:
- if verbose:
- print(f'Removing WinPython menu for all icons in {target.parent}')
- _remove_start_menu_folder(target, current=current, has_pywin32=True)
- # The original code had commented out code to delete .lnk files individually.
- else:
- print("Skipping start menu removal as pywin32 package is needed.")
-
-
-if __name__ == "__main__":
- # Ensure we are running from the target WinPython environment
- parser = ArgumentParser(description="Register or Un-register Python file extensions, icons "\
- "and Windows explorer context menu to this "\
- "Python distribution.")
- parser.add_argument('--unregister', action="store_true",
- help='register to all users, requiring administrative '\
- 'privileges (default: register to current user only)')
- parser.add_argument('--all', action="store_true",
- help='action is to all users, requiring administrative '\
- 'privileges (default: to current user only)')
- args = parser.parse_args()
- expected_target = Path(sys.prefix)
- command = "unregister" if args.unregister else "register"
- users = "all" if args.all else "user"
- print(f"Attempting to {command} the Python environment for {users} at: {expected_target}")
-
- target_dir = sys.prefix # Or get from arguments
- is_current_user = True # Or get from arguments
- if command == "register":
- register(expected_target, current=not args.all)
- else:
- unregister(expected_target, current=not args.all)
+# -*- coding: utf-8 -*-
+#
+# associate.py = Register a Python distribution
+# Copyright © 2012 Pierre Raybaut
+# Licensed under the terms of the MIT License
+# (see winpython/__init__.py for details)
+
+import sys
+import os
+from pathlib import Path
+import importlib.util
+import winreg
+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 ---
+
+def _set_reg_value(root, key_path, name, value, reg_type=winreg.REG_SZ, verbose=False):
+ """Helper to create key and set a registry value using CreateKeyEx."""
+ rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
+ if verbose:
+ print(f"{rootkey_name}\\{key_path}\\{name if name else ''}:{value}")
+ try:
+ # Use CreateKeyEx with context manager for automatic closing
+ with winreg.CreateKeyEx(root, key_path, 0, winreg.KEY_WRITE) as key:
+ winreg.SetValueEx(key, name, 0, reg_type, value)
+ except OSError as e:
+ print(f"Error creating/setting registry value {rootkey_name}\\{key_path}\\{name}: {e}", file=sys.stderr)
+
+def _delete_reg_key(root, key_path, verbose=False):
+ """Helper to delete a registry key, ignoring if not found."""
+ rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
+ if verbose:
+ print(f"{rootkey_name}\\{key_path}")
+ try:
+ # DeleteKey can only delete keys with no subkeys.
+ # For keys with (still) subkeys, use DeleteKeyEx on the parent key if available
+ winreg.DeleteKey(root, key_path)
+ except FileNotFoundError:
+ if verbose:
+ print(f"Registry key not found (skipping deletion): {rootkey_name}\\{key_path}")
+ except OSError as e: # Catch other potential errors like key not empty
+ print(f"Error deleting registry key {rootkey_name}\\{key_path}: {e}", file=sys.stderr)
+
+
+# --- Helper functions for Start Menu Shortcuts ---
+
+def _has_pywin32():
+ """Check if pywin32 (pythoncom) is installed."""
+ return importlib.util.find_spec('pythoncom') is not None
+
+def _remove_start_menu_folder(target, current=True, has_pywin32=False):
+ "remove menu Folder for target WinPython if pywin32 exists"
+ if has_pywin32:
+ remove_winpython_start_menu_folder(current=current)
+ else:
+ print("Skipping start menu removal as pywin32 package is not installed.")
+
+def _get_shortcut_data(target, current=True, has_pywin32=False):
+ "get windows menu access data if pywin32 exists, otherwise empty list"
+ if not has_pywin32:
+ return []
+
+ wpdir = str(Path(target).parent)
+ data = []
+ for name in os.listdir(wpdir):
+ 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(create_winpython_start_menu_folder(current=current)) / bname) + '.lnk'
+ data.append(
+ (
+ str(Path(wpdir) / name), # Target executable path
+ bname, # Description/Name
+ shortcut_name, # Shortcut file path
+ )
+ )
+ return data
+
+# --- PythonCore entries (PEP-0514 and WinPython specific) ---
+
+
+def register_in_registery(target, current=True, reg_type=winreg.REG_SZ, verbose=True) -> tuple[list[any], ...]:
+ """Register in Windows (like regedit)"""
+
+ # --- Constants ---
+ DROP_HANDLER_CLSID = "{60254CA5-953B-11CF-8C96-00AA00B8708C}"
+
+ # --- CONFIG ---
+ target_path = Path(target).resolve()
+ python_exe = str(target_path / "python.exe")
+ pythonw_exe = str(target_path / "pythonw.exe")
+ spyder_exe = str(target_path.parent / "Spyder.exe")
+ icon_py = str(target_path / "DLLs" / "py.ico")
+ icon_pyc = str(target_path / "DLLs" / "pyc.ico")
+ idle_path = str(target_path / "Lib" / "idlelib" / "idle.pyw")
+ doc_path = str(target_path / "Doc" / "html" / "index.html")
+ python_infos = utils.get_python_infos(target) # ('3.11', 64)
+ short_version = python_infos[0] # e.g., '3.11'
+ version = utils.get_python_long_version(target) # e.g., '3.11.5'
+ arch = f'{python_infos[1]}bit' # e.g., '64bit'
+ display = f"Python {version} ({arch})"
+
+ permanent_entries = [] # key_path, name, value
+ dynamic_entries = [] # key_path, name, value
+ core_entries = [] # key_path, name, value
+ lost_entries = [] # intermediate keys to remove later
+ # --- File associations ---
+ ext_map = {".py": "Python.File", ".pyw": "Python.NoConFile", ".pyc": "Python.CompiledFile"}
+ ext_label = {".py": "Python File", ".pyw": "Python File (no console)", ".pyc": "Compiled Python File"}
+ for ext, ftype in ext_map.items():
+ permanent_entries.append((f"Software\\Classes\\{ext}", None, ftype))
+ if ext in (".py", ".pyw"):
+ permanent_entries.append((f"Software\\Classes\\{ext}", "Content Type", "text/plain"))
+
+ # --- Descriptions, Icons, DropHandlers ---
+ for ext, ftype in ext_map.items():
+ dynamic_entries.append((f"Software\\Classes\\{ftype}", None, ext_label[ext]))
+ dynamic_entries.append((f"Software\\Classes\\{ftype}\\DefaultIcon", None, icon_py if "Compiled" not in ftype else icon_pyc))
+ dynamic_entries.append((f"Software\\Classes\\{ftype}\\shellex\\DropHandler", None, DROP_HANDLER_CLSID))
+ lost_entries.append((f"Software\\Classes\\{ftype}\\shellex", None, None))
+
+ # --- Shell commands ---
+ for ext, ftype in ext_map.items():
+ dynamic_entries.append((f"Software\\Classes\\{ftype}\\shell\\open\\command", None, f'''"{pythonw_exe if ftype=='Python.NoConFile' else python_exe}" "%1" %*'''))
+ lost_entries.append((f"Software\\Classes\\{ftype}\\shell\\open", None, None))
+ lost_entries.append((f"Software\\Classes\\{ftype}\\shell", None, None))
+
+ dynamic_entries.append((rf"Software\Classes\Python.File\shell\Edit with IDLE\command", None, f'"{pythonw_exe}" "{idle_path}" -n -e "%1"'))
+ dynamic_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with IDLE\command", None, f'"{pythonw_exe}" "{idle_path}" -n -e "%1"'))
+ lost_entries.append((rf"Software\Classes\Python.File\shell\Edit with IDLE", None, None))
+ 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" -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))
+
+ # --- WinPython Core registry entries (PEP 514 style) ---
+ base = f"Software\\Python\\WinPython\\{short_version}"
+ core_entries.append((base, "DisplayName", display))
+ core_entries.append((base, "SupportUrl", "https://winpython.github.io"))
+ core_entries.append((base, "SysVersion", short_version))
+ core_entries.append((base, "SysArchitecture", arch))
+ core_entries.append((base, "Version", version))
+
+ core_entries.append((f"{base}\\InstallPath", None, str(target)))
+ core_entries.append((f"{base}\\InstallPath", "ExecutablePath", python_exe))
+ core_entries.append((f"{base}\\InstallPath", "WindowedExecutablePath", pythonw_exe))
+ core_entries.append((f"{base}\\InstallPath\\InstallGroup", None, f"Python {short_version}"))
+
+ core_entries.append((f"{base}\\Modules", None, ""))
+ core_entries.append((f"{base}\\PythonPath", None, f"{target}\\Lib;{target}\\DLLs"))
+ core_entries.append((f"{base}\\Help\\Main Python Documentation", None, doc_path))
+ lost_entries.append((f"{base}\\Help", None, None))
+ lost_entries.append((f"Software\\Python\\WinPython", None, None))
+
+ return permanent_entries, dynamic_entries, core_entries, lost_entries
+
+# --- Main Register/Unregister Functions ---
+
+def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
+ """Register a Python distribution in Windows registry and create Start Menu shortcuts"""
+ root = winreg.HKEY_CURRENT_USER if current else winreg.HKEY_LOCAL_MACHINE
+ has_pywin32 = _has_pywin32()
+
+ if verbose:
+ print(f'Creating WinPython registry entries for {target}')
+
+ permanent_entries, dynamic_entries, core_entries, lost_entries = register_in_registery(target)
+ # Set registry entries for given target
+ for key_path, name, value in permanent_entries + dynamic_entries + core_entries:
+ _set_reg_value(root, key_path, name, value, verbose=verbose)
+
+ # Create start menu entries
+ if has_pywin32:
+ if verbose:
+ 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:
+ create_shortcut(path, desc, fname, verbose=verbose)
+ except Exception as e:
+ 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.")
+
+def unregister(target, current=True, verbose=True):
+ """Unregister a Python distribution from Windows registry and remove Start Menu shortcuts"""
+ root = winreg.HKEY_CURRENT_USER if current else winreg.HKEY_LOCAL_MACHINE
+ has_pywin32 = _has_pywin32()
+
+ if verbose:
+ print(f'Removing WinPython registry entries for {target}')
+
+ permanent_entries, dynamic_entries, core_entries , lost_entries = register_in_registery(target)
+
+ # List of keys to attempt to delete, ordered from most specific to general
+ keys_to_delete = sorted(list(set(key_path for key_path , name, value in (dynamic_entries + core_entries + lost_entries))), key=len, reverse=True)
+
+ rootkey_name = "HKEY_CURRENT_USER" if root == winreg.HKEY_CURRENT_USER else "HKEY_LOCAL_MACHINE"
+ for key_path in keys_to_delete:
+ _delete_reg_key(root, key_path, verbose=verbose)
+
+ # Remove start menu shortcuts
+ if has_pywin32:
+ if verbose:
+ print(f'Removing WinPython menu for all icons in {target.parent}')
+ _remove_start_menu_folder(target, current=current, has_pywin32=True)
+ # The original code had commented out code to delete .lnk files individually.
+ else:
+ print("Skipping start menu removal as pywin32 package is needed.")
+
+
+if __name__ == "__main__":
+ # Ensure we are running from the target WinPython environment
+ parser = ArgumentParser(description="Register or Un-register Python file extensions, icons "\
+ "and Windows explorer context menu to this "\
+ "Python distribution.")
+ parser.add_argument('--unregister', action="store_true",
+ help='register to all users, requiring administrative '\
+ 'privileges (default: register to current user only)')
+ parser.add_argument('--all', action="store_true",
+ help='action is to all users, requiring administrative '\
+ 'privileges (default: to current user only)')
+ args = parser.parse_args()
+ expected_target = Path(sys.prefix)
+ command = "unregister" if args.unregister else "register"
+ users = "all" if args.all else "user"
+ print(f"Attempting to {command} the Python environment for {users} at: {expected_target}")
+
+ target_dir = sys.prefix # Or get from arguments
+ is_current_user = True # Or get from arguments
+ if command == "register":
+ register(expected_target, current=not args.all)
+ else:
+ unregister(expected_target, current=not args.all)
diff --git a/winpython/diff.py b/wppm/diff.py
similarity index 100%
rename from winpython/diff.py
rename to wppm/diff.py
diff --git a/winpython/hash.py b/wppm/hash.py
similarity index 100%
rename from winpython/hash.py
rename to wppm/hash.py
diff --git a/winpython/packagemetadata.py b/wppm/packagemetadata.py
similarity index 100%
rename from winpython/packagemetadata.py
rename to wppm/packagemetadata.py
diff --git a/winpython/piptree.py b/wppm/piptree.py
similarity index 100%
rename from winpython/piptree.py
rename to wppm/piptree.py
diff --git a/winpython/utils.py b/wppm/utils.py
similarity index 97%
rename from winpython/utils.py
rename to wppm/utils.py
index 0c83cb80..dcec0207 100644
--- a/winpython/utils.py
+++ b/wppm/utils.py
@@ -1,313 +1,313 @@
-# -*- coding: utf-8 -*-
-#
-# WinPython utilities
-# Copyright © 2012 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
-import sys
-import stat
-import shutil
-import locale
-import subprocess
-from pathlib import Path
-import re
-import tarfile
-import zipfile
-
-# 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)'
-
-# WHEELBIN_PATTERN defines what an acceptable binary wheel package is
-WHEELBIN_PATTERN = r'([a-zA-Z0-9\-\_\.]*)-([0-9\.\_]*[a-z0-9\+]*[0-9]?)-cp([0-9]*)\-[0-9|c|o|n|e|p|m]*\-(win32|win\_amd64)\.whl'
-
-def get_python_executable(path=None):
- """Return the path to the Python executable."""
- python_path = Path(path) if path else Path(sys.executable)
- base_dir = python_path if python_path.is_dir() else python_path.parent
- python_exe = base_dir / 'python.exe'
- pypy_exe = base_dir / 'pypy3.exe' # For PyPy
- return str(python_exe if python_exe.is_file() else pypy_exe)
-
-def get_site_packages_path(path=None):
- """Return the path to the Python site-packages directory."""
- python_path = Path(path) if path else Path(sys.executable)
- base_dir = python_path if python_path.is_dir() else python_path.parent
- site_packages = base_dir / 'Lib' / 'site-packages'
- 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(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(("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(("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(("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(("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`."""
- if not os.access(path, os.W_OK):
- os.chmod(path, stat.S_IWUSR)
- function(path)
- else:
- raise
-
-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"""
- line0 = "+" + ("-" * (len(text) + 2)) + "+"
- line1 = "| " + text + " |"
- print("\n\n" + "\n".join([line0, line1, line0]) + "\n")
-
-def is_python_distribution(path):
- """Return True if path is a Python distribution."""
- has_exec = Path(get_python_executable(path)).is_file()
- has_site = Path(get_site_packages_path(path)).is_dir()
- return has_exec and has_site
-
-def decode_fs_string(string):
- """Convert string from file system charset to unicode."""
- charset = sys.getfilesystemencoding() or locale.getpreferredencoding()
- return string.decode(charset)
-
-def exec_shell_cmd(args, path):
- """Execute shell command (*args* is a list of arguments) in *path*."""
- process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path, shell=True)
- return decode_fs_string(process.stdout.read())
-
-def exec_run_cmd(args, path=None):
- """Run a single command (*args* is a list of arguments) in optional *path*."""
- process = subprocess.run(args, capture_output=True, cwd=path, text=True)
- return process.stdout
-
-def python_query(cmd, path):
- """Execute Python command using the Python interpreter located in *path*."""
- the_exe = get_python_executable(path)
- return exec_shell_cmd(f'"{the_exe}" -c "{cmd}"', path).splitlines()[0]
-
-def python_execmodule(cmd, path):
- """Execute Python command using the Python interpreter located in *path*."""
- the_exe = get_python_executable(path)
- exec_shell_cmd(f'{the_exe} -m {cmd}', path)
-
-def get_python_infos(path):
- """Return (version, architecture) for the Python distribution located in *path*."""
- is_64 = python_query("import sys; print(sys.maxsize > 2**32)", path)
- arch = {"True": 64, "False": 32}.get(is_64, None)
- ver = python_query("import sys;print(f'{sys.version_info.major}.{sys.version_info.minor}')", path)
- return ver, arch
-
-def get_python_long_version(path):
- """Return long version (X.Y.Z) for the Python distribution located in *path*."""
- ver = python_query("import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')", path)
- return ver if re.match(r"([0-9]*)\.([0-9]*)\.([0-9]*)", ver) else None
-
-def patch_shebang_line(fname, pad=b" ", to_movable=True, targetdir=""):
- """Remove absolute path to python.exe in shebang lines in binary files, or re-add it."""
- target_dir = targetdir if to_movable else os.path.abspath(os.path.join(os.path.dirname(fname), r"..")) + "\\"
- executable = sys.executable
- shebang_line = re.compile(rb"""(#!.*pythonw?\.exe)"?""") # Python3+
- if "pypy3" in sys.executable:
- shebang_line = re.compile(rb"""(#!.*pypy3w?\.exe)"?""") # Pypy3+
- target_dir = target_dir.encode("utf-8")
-
- with open(fname, "rb") as fh:
- initial_content = fh.read()
- content = shebang_line.split(initial_content, maxsplit=1)
- if len(content) != 3:
- return
- exe = os.path.basename(content[1][2:])
- content[1] = b"#!" + target_dir + exe # + (pad * (len(content[1]) - len(exe) - 2))
- final_content = b"".join(content)
- if initial_content == final_content:
- return
- try:
- with open(fname, "wb") as fo:
- fo.write(final_content)
- print("patched", fname)
- except Exception:
- print("failed to patch", fname)
-
-def patch_shebang_line_py(fname, to_movable=True, targetdir=""):
- """Changes shebang line in '.py' file to relative or absolue path"""
- import fileinput
- exec_path = r'#!.\python.exe' if to_movable else '#!' + sys.executable
- if 'pypy3' in sys.executable:
- exec_path = r'#!.\pypy3.exe' if to_movable else exec_path
- for line in fileinput.input(fname, inplace=True):
- if re.match(r'^#\!.*python\.exe$', line) or re.match(r'^#\!.*pypy3\.exe$', line):
- print(exec_path)
- else:
- print(line, end='')
-
-def guess_encoding(csv_file):
- """guess the encoding of the given file"""
- with open(csv_file, "rb") as f:
- data = f.read(5)
- if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM" (normally no BOM in utf-8)
- return ["utf-8-sig"]
- try:
- with open(csv_file, encoding="utf-8") as f:
- preview = f.read(222222)
- return ["utf-8"]
- except:
- return [locale.getdefaultlocale()[1], "utf-8"]
-
-def replace_in_file(filepath: Path, replacements: list[tuple[str, str]], filedest: Path = None, verbose=False):
- """
- Replaces strings in a file
- Args:
- filepath: Path to the file to modify.
- replacements: A list of tuples of ('old string 'new string')
- filedest: optional output file, otherwise will be filepath
- """
- the_encoding = guess_encoding(filepath)[0]
- with open(filepath, "r", encoding=the_encoding) as f:
- content = f.read()
- new_content = content
- for old_text, new_text in replacements:
- new_content = new_content.replace(old_text, new_text)
- outfile = filedest if filedest else filepath
- if new_content != content or str(outfile) != str(filepath):
- with open(outfile, "w", encoding=the_encoding) as f:
- f.write(new_content)
- if verbose:
- print(f"patched from {Path(filepath).name} into {outfile} !")
-
-def patch_sourcefile(fname, in_text, out_text, silent_mode=False):
- """Replace a string in a source file."""
- if not silent_mode:
- print(f"patching {fname} from {in_text} to {out_text}")
- if Path(fname).is_file() and in_text != out_text:
- replace_in_file(Path(fname), [(in_text, out_text)])
-
-def extract_archive(fname, targetdir=None, verbose=False):
- """Extract .zip, .exe or .tar.gz archive to a temporary directory.
- Return the temporary directory path"""
- targetdir = targetdir or create_temp_dir()
- Path(targetdir).mkdir(parents=True, exist_ok=True)
- if Path(fname).suffix in ('.zip', '.exe'):
- obj = zipfile.ZipFile(fname, mode="r")
- elif fname.endswith('.tar.gz'):
- obj = tarfile.open(fname, mode='r:gz')
- else:
- raise RuntimeError(f"Unsupported archive filename {fname}")
- obj.extractall(path=targetdir)
- return targetdir
-
-def get_source_package_infos(fname):
- """Return a tuple (name, version) of the Python source package."""
- if fname.endswith('.whl'):
- return Path(fname).name.split("-")[:2]
- match = re.match(SOURCE_PATTERN, Path(fname).name)
- return match.groups()[:2] if match else None
-
-def buildflit_wininst(root, python_exe=None, copy_to=None, verbose=False):
- """Build Wheel from Python package located in *root* with flit."""
- python_exe = python_exe or sys.executable
- cmd = [python_exe, '-m', 'flit', 'build']
- if verbose:
- subprocess.call(cmd, cwd=root)
- else:
- subprocess.Popen(cmd, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
- distdir = Path(root) / 'dist'
- if not distdir.is_dir():
- raise RuntimeError(
- "Build failed: see package README file for further details regarding installation requirements.\n\n"
- "For more concrete debugging infos, please try to build the package from the command line:\n"
- "1. Open a WinPython command prompt\n"
- "2. Change working directory to the appropriate folder\n"
- "3. Type `python -m flit build`"
- )
- for distname in os.listdir(distdir):
- if re.match(SOURCE_PATTERN, distname) or re.match(WHEELBIN_PATTERN, distname):
- break
- else:
- raise RuntimeError(f"Build failed: not a pure Python package? {distdir}")
-
- src_fname = distdir / distname
- if copy_to:
- dst_fname = Path(copy_to) / distname
- shutil.move(src_fname, dst_fname)
- if verbose:
- print(f"Move: {src_fname} --> {dst_fname}")
-
-def direct_pip_install(fname, python_exe=None, verbose=False, install_options=None):
- """Direct install via python -m pip !"""
- python_exe = python_exe or sys.executable
- myroot = Path(python_exe).parent
- cmd = [python_exe, "-m", "pip", "install"] + (install_options or []) + [fname]
- if not verbose:
- process = subprocess.Popen(cmd, cwd=myroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- stdout, stderr = process.communicate()
- the_log = f"{stdout}\n {stderr}"
- if " not find " in the_log or " not found " in the_log:
- print(f"Failed to Install: \n {fname} \n msg: {the_log}")
- raise RuntimeError
- process.stdout.close()
- process.stderr.close()
- else:
- subprocess.call(cmd, cwd=myroot)
- print(f"Installed {fname} via {' '.join(cmd)}")
- return fname
-
-def do_script(this_script, python_exe=None, copy_to=None, verbose=False, install_options=None):
- """Execute a script (get-pip typically)."""
- python_exe = python_exe or sys.executable
- myroot = Path(python_exe).parent
- # cmd = [python_exe, myroot + r'\Scripts\pip-script.py', 'install']
- cmd = [python_exe] + (install_options or []) + ([this_script] if this_script else [])
- print("Executing ", cmd)
- if not verbose:
- subprocess.Popen(cmd, cwd=myroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
- else:
- subprocess.call(cmd, cwd=myroot)
- print("Executed ", cmd)
- return 'ok'
-
-def columns_width(list_of_lists):
- """Return the maximum string length of each column of a list of lists."""
- if not isinstance(list_of_lists, list):
- return [0]
- return [max(len(str(item)) for item in sublist) for sublist in zip(*list_of_lists)]
-
-def formatted_list(list_of_list, full=False, max_width=70):
- """Format a list_of_list to fixed length columns."""
- columns_size = columns_width(list_of_list)
- columns = range(len(columns_size))
- return [list(line[col].ljust(columns_size[col])[:max_width] for col in columns) for line in list_of_list]
-
-def normalize(this):
- """Apply PEP 503 normalization to the string."""
- return re.sub(r"[-_.]+", "-", this).lower()
-
-if __name__ == '__main__':
- print_box("Test")
- dname = sys.prefix
- print((dname + ':', '\n', get_python_infos(dname)))
-
- tmpdir = r'D:\Tests\winpython_tests'
- Path(tmpdir).mkdir(parents=True, exist_ok=True)
- print(extract_archive(str(Path(r'D:\WinP\bd37') / 'packages.win-amd64' / 'python-3.7.3.amd64.zip'), tmpdir))
+# -*- coding: utf-8 -*-
+#
+# WinPython utilities
+# Copyright © 2012 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
+import sys
+import stat
+import shutil
+import locale
+import subprocess
+from pathlib import Path
+import re
+import tarfile
+import zipfile
+
+# 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)'
+
+# WHEELBIN_PATTERN defines what an acceptable binary wheel package is
+WHEELBIN_PATTERN = r'([a-zA-Z0-9\-\_\.]*)-([0-9\.\_]*[a-z0-9\+]*[0-9]?)-cp([0-9]*)\-[0-9|c|o|n|e|p|m]*\-(win32|win\_amd64)\.whl'
+
+def get_python_executable(path=None):
+ """Return the path to the Python executable."""
+ python_path = Path(path) if path else Path(sys.executable)
+ base_dir = python_path if python_path.is_dir() else python_path.parent
+ python_exe = base_dir / 'python.exe'
+ pypy_exe = base_dir / 'pypy3.exe' # For PyPy
+ return str(python_exe if python_exe.is_file() else pypy_exe)
+
+def get_site_packages_path(path=None):
+ """Return the path to the Python site-packages directory."""
+ python_path = Path(path) if path else Path(sys.executable)
+ base_dir = python_path if python_path.is_dir() else python_path.parent
+ site_packages = base_dir / 'Lib' / 'site-packages'
+ 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(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(("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(("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(("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(("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`."""
+ if not os.access(path, os.W_OK):
+ os.chmod(path, stat.S_IWUSR)
+ function(path)
+ else:
+ raise
+
+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"""
+ line0 = "+" + ("-" * (len(text) + 2)) + "+"
+ line1 = "| " + text + " |"
+ print("\n\n" + "\n".join([line0, line1, line0]) + "\n")
+
+def is_python_distribution(path):
+ """Return True if path is a Python distribution."""
+ has_exec = Path(get_python_executable(path)).is_file()
+ has_site = Path(get_site_packages_path(path)).is_dir()
+ return has_exec and has_site
+
+def decode_fs_string(string):
+ """Convert string from file system charset to unicode."""
+ charset = sys.getfilesystemencoding() or locale.getpreferredencoding()
+ return string.decode(charset)
+
+def exec_shell_cmd(args, path):
+ """Execute shell command (*args* is a list of arguments) in *path*."""
+ process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path, shell=True)
+ return decode_fs_string(process.stdout.read())
+
+def exec_run_cmd(args, path=None):
+ """Run a single command (*args* is a list of arguments) in optional *path*."""
+ process = subprocess.run(args, capture_output=True, cwd=path, text=True)
+ return process.stdout
+
+def python_query(cmd, path):
+ """Execute Python command using the Python interpreter located in *path*."""
+ the_exe = get_python_executable(path)
+ return exec_shell_cmd(f'"{the_exe}" -c "{cmd}"', path).splitlines()[0]
+
+def python_execmodule(cmd, path):
+ """Execute Python command using the Python interpreter located in *path*."""
+ the_exe = get_python_executable(path)
+ exec_shell_cmd(f'{the_exe} -m {cmd}', path)
+
+def get_python_infos(path):
+ """Return (version, architecture) for the Python distribution located in *path*."""
+ is_64 = python_query("import sys; print(sys.maxsize > 2**32)", path)
+ arch = {"True": 64, "False": 32}.get(is_64, None)
+ ver = python_query("import sys;print(f'{sys.version_info.major}.{sys.version_info.minor}')", path)
+ return ver, arch
+
+def get_python_long_version(path):
+ """Return long version (X.Y.Z) for the Python distribution located in *path*."""
+ ver = python_query("import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')", path)
+ return ver if re.match(r"([0-9]*)\.([0-9]*)\.([0-9]*)", ver) else None
+
+def patch_shebang_line(fname, pad=b" ", to_movable=True, targetdir=""):
+ """Remove absolute path to python.exe in shebang lines in binary files, or re-add it."""
+ target_dir = targetdir if to_movable else os.path.abspath(os.path.join(os.path.dirname(fname), r"..")) + "\\"
+ executable = sys.executable
+ shebang_line = re.compile(rb"""(#!.*pythonw?\.exe)"?""") # Python3+
+ if "pypy3" in sys.executable:
+ shebang_line = re.compile(rb"""(#!.*pypy3w?\.exe)"?""") # Pypy3+
+ target_dir = target_dir.encode("utf-8")
+
+ with open(fname, "rb") as fh:
+ initial_content = fh.read()
+ content = shebang_line.split(initial_content, maxsplit=1)
+ if len(content) != 3:
+ return
+ exe = os.path.basename(content[1][2:])
+ content[1] = b"#!" + target_dir + exe # + (pad * (len(content[1]) - len(exe) - 2))
+ final_content = b"".join(content)
+ if initial_content == final_content:
+ return
+ try:
+ with open(fname, "wb") as fo:
+ fo.write(final_content)
+ print("patched", fname)
+ except Exception:
+ print("failed to patch", fname)
+
+def patch_shebang_line_py(fname, to_movable=True, targetdir=""):
+ """Changes shebang line in '.py' file to relative or absolue path"""
+ import fileinput
+ exec_path = r'#!.\python.exe' if to_movable else '#!' + sys.executable
+ if 'pypy3' in sys.executable:
+ exec_path = r'#!.\pypy3.exe' if to_movable else exec_path
+ for line in fileinput.input(fname, inplace=True):
+ if re.match(r'^#\!.*python\.exe$', line) or re.match(r'^#\!.*pypy3\.exe$', line):
+ print(exec_path)
+ else:
+ print(line, end='')
+
+def guess_encoding(csv_file):
+ """guess the encoding of the given file"""
+ with open(csv_file, "rb") as f:
+ data = f.read(5)
+ if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM" (normally no BOM in utf-8)
+ return ["utf-8-sig"]
+ try:
+ with open(csv_file, encoding="utf-8") as f:
+ preview = f.read(222222)
+ return ["utf-8"]
+ except:
+ return [locale.getdefaultlocale()[1], "utf-8"]
+
+def replace_in_file(filepath: Path, replacements: list[tuple[str, str]], filedest: Path = None, verbose=False):
+ """
+ Replaces strings in a file
+ Args:
+ filepath: Path to the file to modify.
+ replacements: A list of tuples of ('old string 'new string')
+ filedest: optional output file, otherwise will be filepath
+ """
+ the_encoding = guess_encoding(filepath)[0]
+ with open(filepath, "r", encoding=the_encoding) as f:
+ content = f.read()
+ new_content = content
+ for old_text, new_text in replacements:
+ new_content = new_content.replace(old_text, new_text)
+ outfile = filedest if filedest else filepath
+ if new_content != content or str(outfile) != str(filepath):
+ with open(outfile, "w", encoding=the_encoding) as f:
+ f.write(new_content)
+ if verbose:
+ print(f"patched from {Path(filepath).name} into {outfile} !")
+
+def patch_sourcefile(fname, in_text, out_text, silent_mode=False):
+ """Replace a string in a source file."""
+ if not silent_mode:
+ print(f"patching {fname} from {in_text} to {out_text}")
+ if Path(fname).is_file() and in_text != out_text:
+ replace_in_file(Path(fname), [(in_text, out_text)])
+
+def extract_archive(fname, targetdir=None, verbose=False):
+ """Extract .zip, .exe or .tar.gz archive to a temporary directory.
+ Return the temporary directory path"""
+ targetdir = targetdir or create_temp_dir()
+ Path(targetdir).mkdir(parents=True, exist_ok=True)
+ if Path(fname).suffix in ('.zip', '.exe'):
+ obj = zipfile.ZipFile(fname, mode="r")
+ elif fname.endswith('.tar.gz'):
+ obj = tarfile.open(fname, mode='r:gz')
+ else:
+ raise RuntimeError(f"Unsupported archive filename {fname}")
+ obj.extractall(path=targetdir)
+ return targetdir
+
+def get_source_package_infos(fname):
+ """Return a tuple (name, version) of the Python source package."""
+ if fname.endswith('.whl'):
+ return Path(fname).name.split("-")[:2]
+ match = re.match(SOURCE_PATTERN, Path(fname).name)
+ return match.groups()[:2] if match else None
+
+def buildflit_wininst(root, python_exe=None, copy_to=None, verbose=False):
+ """Build Wheel from Python package located in *root* with flit."""
+ python_exe = python_exe or sys.executable
+ cmd = [python_exe, '-m', 'flit', 'build']
+ if verbose:
+ subprocess.call(cmd, cwd=root)
+ else:
+ subprocess.Popen(cmd, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+ distdir = Path(root) / 'dist'
+ if not distdir.is_dir():
+ raise RuntimeError(
+ "Build failed: see package README file for further details regarding installation requirements.\n\n"
+ "For more concrete debugging infos, please try to build the package from the command line:\n"
+ "1. Open a WinPython command prompt\n"
+ "2. Change working directory to the appropriate folder\n"
+ "3. Type `python -m flit build`"
+ )
+ for distname in os.listdir(distdir):
+ if re.match(SOURCE_PATTERN, distname) or re.match(WHEELBIN_PATTERN, distname):
+ break
+ else:
+ raise RuntimeError(f"Build failed: not a pure Python package? {distdir}")
+
+ src_fname = distdir / distname
+ if copy_to:
+ dst_fname = Path(copy_to) / distname
+ shutil.move(src_fname, dst_fname)
+ if verbose:
+ print(f"Move: {src_fname} --> {dst_fname}")
+
+def direct_pip_install(fname, python_exe=None, verbose=False, install_options=None):
+ """Direct install via python -m pip !"""
+ python_exe = python_exe or sys.executable
+ myroot = Path(python_exe).parent
+ cmd = [python_exe, "-m", "pip", "install"] + (install_options or []) + [fname]
+ if not verbose:
+ process = subprocess.Popen(cmd, cwd=myroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = process.communicate()
+ the_log = f"{stdout}\n {stderr}"
+ if " not find " in the_log or " not found " in the_log:
+ print(f"Failed to Install: \n {fname} \n msg: {the_log}")
+ raise RuntimeError
+ process.stdout.close()
+ process.stderr.close()
+ else:
+ subprocess.call(cmd, cwd=myroot)
+ print(f"Installed {fname} via {' '.join(cmd)}")
+ return fname
+
+def do_script(this_script, python_exe=None, copy_to=None, verbose=False, install_options=None):
+ """Execute a script (get-pip typically)."""
+ python_exe = python_exe or sys.executable
+ myroot = Path(python_exe).parent
+ # cmd = [python_exe, myroot + r'\Scripts\pip-script.py', 'install']
+ cmd = [python_exe] + (install_options or []) + ([this_script] if this_script else [])
+ print("Executing ", cmd)
+ if not verbose:
+ subprocess.Popen(cmd, cwd=myroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+ else:
+ subprocess.call(cmd, cwd=myroot)
+ print("Executed ", cmd)
+ return 'ok'
+
+def columns_width(list_of_lists):
+ """Return the maximum string length of each column of a list of lists."""
+ if not isinstance(list_of_lists, list):
+ return [0]
+ return [max(len(str(item)) for item in sublist) for sublist in zip(*list_of_lists)]
+
+def formatted_list(list_of_list, full=False, max_width=70):
+ """Format a list_of_list to fixed length columns."""
+ columns_size = columns_width(list_of_list)
+ columns = range(len(columns_size))
+ return [list(line[col].ljust(columns_size[col])[:max_width] for col in columns) for line in list_of_list]
+
+def normalize(this):
+ """Apply PEP 503 normalization to the string."""
+ return re.sub(r"[-_.]+", "-", this).lower()
+
+if __name__ == '__main__':
+ print_box("Test")
+ dname = sys.prefix
+ print((dname + ':', '\n', get_python_infos(dname)))
+
+ tmpdir = r'D:\Tests\winpython_tests'
+ Path(tmpdir).mkdir(parents=True, exist_ok=True)
+ print(extract_archive(str(Path(r'D:\WinP\bd37') / 'packages.win-amd64' / 'python-3.7.3.amd64.zip'), tmpdir))
diff --git a/winpython/wheelhouse.py b/wppm/wheelhouse.py
similarity index 100%
rename from winpython/wheelhouse.py
rename to wppm/wheelhouse.py
diff --git a/winpython/wppm.py b/wppm/wppm.py
similarity index 98%
rename from winpython/wppm.py
rename to wppm/wppm.py
index 77a937ac..1f852236 100644
--- a/winpython/wppm.py
+++ b/wppm/wppm.py
@@ -1,406 +1,406 @@
-# -*- coding: utf-8 -*-
-#
-# WinPython Package Manager
-# Copyright © 2012 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
-import re
-import sys
-import shutil
-import subprocess
-import json
-from pathlib import Path
-from argparse import ArgumentParser, RawTextHelpFormatter
-from . import utils, piptree, associate, diff
-from . import wheelhouse as wh
-from operator import itemgetter
-# Workaround for installing PyVISA on Windows from source:
-os.environ["HOME"] = os.environ["USERPROFILE"]
-
-class Package:
- """Standardize a Package from filename or pip list."""
- def __init__(self, fname: str, suggested_summary: str = None):
- self.fname = fname
- self.description = (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
- if infos:
- self.name, self.version = utils.normalize(infos[0]), infos[1]
- self.url = f"https://pypi.org/project/{self.name}"
- self.files = []
-
- def __str__(self):
- return f"{self.name} {self.version}\r\n{self.description}\r\nWebsite: {self.url}"
-
-
-class Distribution:
- """Handles operations on a WinPython distribution."""
- def __init__(self, target: str = None, verbose: bool = False):
- self.target = target or str(Path(sys.executable).parent) # Default target more explicit
- self.verbose = verbose
- self.pip = None
- self.to_be_removed = []
- 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"""
- dst = Path(dstdir) / name
- if self.verbose:
- print(f"create: {dst}")
- full_dst = Path(self.target) / dst
- with open(full_dst, "w") as fd:
- fd.write(contents)
- package.files.append(str(dst))
-
- def get_installed_packages(self, update: bool = False) -> list[Package]:
- """Return installed packages."""
- if str(Path(sys.executable).parent) == self.target:
- self.pip = piptree.PipData()
- else:
- self.pip = piptree.PipData(utils.get_python_executable(self.target))
- 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 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, wheeldir: 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)
- 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 = []
- my_wheeldir = Path(wheeldir) if wheeldir else self.wheelhouse / 'included.wheels'
- if my_wheeldir.is_dir():
- wheelhouse_list = [(name, f"https://pypi.org/project/{name}", version, utils.sum_up(summary))
- for name, version, summary in wh.list_packages_with_metadata(str(my_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."""
- for pack in self.get_installed_packages():
- if utils.normalize(pack.name) == utils.normalize(name):
- return pack
-
- def patch_all_shebang(self, to_movable: bool = True, max_exe_size: int = 999999, targetdir: str = ""):
- """Make all python launchers relative."""
- for ffname in Path(self.target).glob("Scripts/*.exe"):
- if ffname.stat().st_size <= max_exe_size:
- utils.patch_shebang_line(ffname, to_movable=to_movable, targetdir=targetdir)
- for ffname in Path(self.target).glob("Scripts/*.py"):
- utils.patch_shebang_line_py(ffname, to_movable=to_movable, targetdir=targetdir)
-
- def install(self, package: Package, install_options: list[str] = None):
- """Install package in distribution."""
- 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
- self.patch_standard_packages(package.name)
-
- def do_pip_action(self, actions: list[str] = None, install_options: list[str] = None):
- """Execute pip action in the distribution."""
- my_list = install_options or []
- my_actions = actions or []
- executing = str(Path(self.target).parent / "scripts" / "env.bat")
- if Path(executing).is_file():
- complement = [r"&&", "cd", "/D", self.target, r"&&", utils.get_python_executable(self.target), "-m", "pip"]
- else:
- executing = utils.get_python_executable(self.target)
- complement = ["-m", "pip"]
- try:
- fname = utils.do_script(this_script=None, python_exe=executing, verbose=self.verbose, install_options=complement + my_actions + my_list)
- except RuntimeError as e:
- if not self.verbose:
- print("Failed!")
- raise
- else:
- print(f"Pip action failed with error: {e}") # Print error if verbose
-
- def patch_standard_packages(self, package_name="", to_movable=True):
- """patch Winpython packages in need"""
- import filecmp
-
- # 'pywin32' minimal post-install (pywin32_postinstall.py do too much)
- if package_name.lower() in ("", "pywin32"):
- origin = Path(self.target) / "site-packages" / "pywin32_system32"
- destin = Path(self.target)
- if origin.is_dir():
- for name in os.listdir(origin):
- here, there = origin / name, destin / name
- if not there.exists() or not filecmp.cmp(here, there):
- shutil.copyfile(here, there)
- # 'pip' to do movable launchers (around line 100) !!!!
- # rational: https://github.com/pypa/pip/issues/2328
- if package_name.lower() == "pip" or package_name == "":
- # ensure pip will create movable launchers
- # sheb_mov1 = classic way up to WinPython 2016-01
- # sheb_mov2 = tried way, but doesn't work for pip (at least)
- the_place = Path(self.target) / "lib" / "site-packages" / "pip" / "_vendor" / "distlib" / "scripts.py"
- sheb_fix = " executable = get_executable()"
- sheb_mov1 = " executable = os.path.join(os.path.basename(get_executable()))"
- sheb_mov2 = " executable = os.path.join('..',os.path.basename(get_executable()))"
- if to_movable:
- utils.patch_sourcefile(the_place, sheb_fix, sheb_mov1)
- utils.patch_sourcefile(the_place, sheb_mov2, sheb_mov1)
- else:
- utils.patch_sourcefile(the_place, sheb_mov1, sheb_fix)
- utils.patch_sourcefile(the_place, sheb_mov2, sheb_fix)
-
- # create movable launchers for previous package installations
- self.patch_all_shebang(to_movable=to_movable)
- if package_name.lower() in ("", "spyder"):
- # spyder don't goes on internet without you ask
- utils.patch_sourcefile(
- Path(self.target) / "lib" / "site-packages" / "spyder" / "config" / "main.py",
- "'check_updates_on_startup': True,",
- "'check_updates_on_startup': False,",
- )
-
-
- def handle_specific_packages(self, package):
- """Packages requiring additional configuration"""
- if package.name.lower() in ("pyqt4", "pyqt5", "pyside2"):
- # Qt configuration file (where to find Qt)
- name = "qt.conf"
- contents = """[Paths]\nPrefix = .\nBinaries = ."""
- self.create_file(package, name, str(Path("Lib") / "site-packages" / package.name), contents)
- self.create_file(package, name, ".", contents.replace(".", f"./Lib/site-packages/{package.name}"))
- # pyuic script
- if package.name.lower() == "pyqt5":
- # see http://code.activestate.com/lists/python-list/666469/
- tmp_string = r"""@echo off
-if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat"
-"%WINPYDIR%\python.exe" -m PyQt5.uic.pyuic %1 %2 %3 %4 %5 %6 %7 %8 %9"""
- else:
- tmp_string = r"""@echo off
-if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat"
-"%WINPYDIR%\python.exe" "%WINPYDIR%\Lib\site-packages\package.name\uic\pyuic.py" %1 %2 %3 %4 %5 %6 %7 %8 %9"""
- # PyPy adaption: python.exe or pypy3.exe
- my_exec = Path(utils.get_python_executable(self.target)).name
- tmp_string = tmp_string.replace("python.exe", my_exec).replace("package.name", package.name)
- self.create_file(package, f"pyuic{package.name[-1]}.bat", "Scripts", tmp_string)
- # Adding missing __init__.py files (fixes Issue 8)
- uic_path = str(Path("Lib") / "site-packages" / package.name / "uic")
- for dirname in ("Loader", "port_v2", "port_v3"):
- self.create_file(package, "__init__.py", str(Path(uic_path) / dirname), "")
-
- def _print(self, package: Package, action: str):
- """Print package-related action text."""
- text = f"{action} {package.name} {package.version}"
- if self.verbose:
- utils.print_box(text)
- else:
- print(f" {text}...", end=" ")
-
- def _print_done(self):
- """Print OK at the end of a process"""
- if not self.verbose:
- print("OK")
-
- def uninstall(self, package):
- """Uninstall package from distribution"""
- self._print(package, "Uninstalling")
- if package.name != "pip":
- # trick to get true target (if not current)
- this_exec = utils.get_python_executable(self.target) # PyPy !
- subprocess.call([this_exec, "-m", "pip", "uninstall", package.name, "-y"], cwd=self.target)
- self._print_done()
-
- def install_bdist_direct(self, package, install_options=None):
- """Install a package directly !"""
- self._print(package,f"Installing {package.fname.split('.')[-1]}")
- try:
- fname = utils.direct_pip_install(
- package.fname,
- python_exe=utils.get_python_executable(self.target), # PyPy !
- verbose=self.verbose,
- install_options=install_options,
- )
- except RuntimeError:
- if not self.verbose:
- print("Failed!")
- raise
- package = Package(fname)
- self._print_done()
-
-def main(test=False):
-
- registerWinPythonHelp = f"Register WinPython: associate file extensions, icons and context menu with this WinPython"
- unregisterWinPythonHelp = f"Unregister WinPython: de-associate file extensions, icons and context menu from this WinPython"
- parser = ArgumentParser(
- description="WinPython Package Manager: handle a WinPython Distribution and its packages",
- formatter_class=RawTextHelpFormatter,
- )
- parser.add_argument("fname", metavar="package(s) or lockfile", nargs="*", default=[""], type=str, help="optional package names, wheels, or lockfile")
- 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("--unregister", dest="unregisterWinPython", action="store_true", help=unregisterWinPythonHelp)
- 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="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 of 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 (!= constraining) dependancies of the given package[option]: wppm -r pytest![test]")
- parser.add_argument("-l", dest="levels", type=int, default=-1, help="show 'LEVELS' levels of dependencies (with -p, -r): 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)")
-
- args = parser.parse_args()
- 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, args.wheelsource)
- for args_fname in args.fname:
- pack, extra, *other = (args_fname + "[").replace("]", "[").split("[")
- print(pip.down(pack, extra, args.levels if args.levels>0 else 2, verbose=args.verbose))
- sys.exit()
- elif args.pipup:
- pip = piptree.PipData(targetpython, args.wheelsource)
- for args_fname in args.fname:
- pack, extra, *other = (args_fname + "[").replace("]", "[").split("[")
- print(pip.up(pack, extra, args.levels if args.levels>=0 else 1, verbose=args.verbose))
- sys.exit()
- elif args.list:
- pip = piptree.PipData(targetpython, args.wheelsource)
- todo= []
- for args_fname in args.fname:
- todo += [l for l in pip.pip_list(full=True) if bool(re.search(args_fname, l[0]))]
- todo = sorted(set(todo)) #, key=lambda p: (p[0].lower(), p[2])
- titles = [['Package', 'Version', 'Summary'], ['_' * max(x, 6) for x in utils.columns_width(todo)]]
- listed = utils.formatted_list(titles + todo, max_width=70)
- for p in listed:
- print(*p)
- sys.exit()
- elif args.all:
- pip = piptree.PipData(targetpython, args.wheelsource)
- for args_fname in args.fname:
- todo = [l for l in pip.pip_list(full=True) if bool(re.search(args_fname, l[0]))]
- for l in sorted(set(todo)):
- title = f"** Package: {l[0]} **"
- print("\n" + "*" * len(title), f"\n{title}", "\n" + "*" * len(title))
- for key, value in pip.raw[l[0]].items():
- rawtext = json.dumps(value, indent=2, ensure_ascii=False)
- lines = [l for l in rawtext.split(r"\n") if len(l.strip()) > 2]
- if key.lower() != 'description' or args.verbose:
- print(f"{key}: ", "\n".join(lines).replace('"', ""))
- sys.exit()
- if args.registerWinPython:
- print(registerWinPythonHelp)
- if utils.is_python_distribution(args.target):
- dist = Distribution(args.target)
- else:
- raise OSError(f"Invalid Python distribution {args.target}")
- print(f"registering {args.target}")
- print("continue ? Y/N")
- theAnswer = input()
- if theAnswer == "Y":
- associate.register(dist.target, verbose=args.verbose)
- sys.exit()
- if args.unregisterWinPython:
- print(unregisterWinPythonHelp)
- if utils.is_python_distribution(args.target):
- dist = Distribution(args.target)
- else:
- raise OSError(f"Invalid Python distribution {args.target}")
- print(f"unregistering {args.target}")
- print("continue ? Y/N")
- theAnswer = input()
- if theAnswer == "Y":
- associate.unregister(dist.target, verbose=args.verbose)
- sys.exit()
- if utils.is_python_distribution(args.target):
- dist = Distribution(args.target, verbose=True)
- cmd_fix = rf"from winpython import wppm;dist=wppm.Distribution(r'{dist.target}');dist.patch_standard_packages('pip', to_movable=False)"
- cmd_mov = rf"from winpython import wppm;dist=wppm.Distribution(r'{dist.target}');dist.patch_standard_packages('pip', to_movable=True)"
- if args.fix:
- # dist.patch_standard_packages('pip', to_movable=False) # would fail on wppm.exe
- p = subprocess.Popen(["start", "cmd", "/k",dist.python_exe, "-c" , cmd_fix], shell = True, cwd=dist.target)
- sys.exit()
- 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:
- default = dist.generate_package_index_markdown()
- if args.wheelsource:
- compare = dist.generate_package_index_markdown(wheeldir = args.wheelsource)
- print(diff.compare_markdown_sections(default, compare,'python', 'wheelhouse', 'installed', 'wheelhouse'))
- else:
- print(default)
- sys.exit()
- if not args.install and not args.uninstall and args.fname[0].endswith(".toml"):
- args.install = True # for Drag & Drop of .toml (and not wheel)
- if args.fname[0] == "" or (not args.install and not args.uninstall):
- parser.print_help()
- sys.exit()
- else:
- try:
- for args_fname in args.fname:
- 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, install_options=install_from_wheelhouse)
- except NotImplementedError:
- raise RuntimeError("Package is not (yet) supported by WPPM")
- else:
- raise OSError(f"Invalid Python distribution {args.target}")
-
-
-if __name__ == "__main__":
+# -*- coding: utf-8 -*-
+#
+# WinPython Package Manager
+# Copyright © 2012 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
+import re
+import sys
+import shutil
+import subprocess
+import json
+from pathlib import Path
+from argparse import ArgumentParser, RawTextHelpFormatter
+from . import utils, piptree, associate, diff
+from . import wheelhouse as wh
+from operator import itemgetter
+# Workaround for installing PyVISA on Windows from source:
+os.environ["HOME"] = os.environ["USERPROFILE"]
+
+class Package:
+ """Standardize a Package from filename or pip list."""
+ def __init__(self, fname: str, suggested_summary: str = None):
+ self.fname = fname
+ self.description = (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
+ if infos:
+ self.name, self.version = utils.normalize(infos[0]), infos[1]
+ self.url = f"https://pypi.org/project/{self.name}"
+ self.files = []
+
+ def __str__(self):
+ return f"{self.name} {self.version}\r\n{self.description}\r\nWebsite: {self.url}"
+
+
+class Distribution:
+ """Handles operations on a WinPython distribution."""
+ def __init__(self, target: str = None, verbose: bool = False):
+ self.target = target or str(Path(sys.executable).parent) # Default target more explicit
+ self.verbose = verbose
+ self.pip = None
+ self.to_be_removed = []
+ 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"""
+ dst = Path(dstdir) / name
+ if self.verbose:
+ print(f"create: {dst}")
+ full_dst = Path(self.target) / dst
+ with open(full_dst, "w") as fd:
+ fd.write(contents)
+ package.files.append(str(dst))
+
+ def get_installed_packages(self, update: bool = False) -> list[Package]:
+ """Return installed packages."""
+ if str(Path(sys.executable).parent) == self.target:
+ self.pip = piptree.PipData()
+ else:
+ self.pip = piptree.PipData(utils.get_python_executable(self.target))
+ 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 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, wheeldir: 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)
+ 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 = []
+ my_wheeldir = Path(wheeldir) if wheeldir else self.wheelhouse / 'included.wheels'
+ if my_wheeldir.is_dir():
+ wheelhouse_list = [(name, f"https://pypi.org/project/{name}", version, utils.sum_up(summary))
+ for name, version, summary in wh.list_packages_with_metadata(str(my_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."""
+ for pack in self.get_installed_packages():
+ if utils.normalize(pack.name) == utils.normalize(name):
+ return pack
+
+ def patch_all_shebang(self, to_movable: bool = True, max_exe_size: int = 999999, targetdir: str = ""):
+ """Make all python launchers relative."""
+ for ffname in Path(self.target).glob("Scripts/*.exe"):
+ if ffname.stat().st_size <= max_exe_size:
+ utils.patch_shebang_line(ffname, to_movable=to_movable, targetdir=targetdir)
+ for ffname in Path(self.target).glob("Scripts/*.py"):
+ utils.patch_shebang_line_py(ffname, to_movable=to_movable, targetdir=targetdir)
+
+ def install(self, package: Package, install_options: list[str] = None):
+ """Install package in distribution."""
+ 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
+ self.patch_standard_packages(package.name)
+
+ def do_pip_action(self, actions: list[str] = None, install_options: list[str] = None):
+ """Execute pip action in the distribution."""
+ my_list = install_options or []
+ my_actions = actions or []
+ executing = str(Path(self.target).parent / "scripts" / "env.bat")
+ if Path(executing).is_file():
+ complement = [r"&&", "cd", "/D", self.target, r"&&", utils.get_python_executable(self.target), "-m", "pip"]
+ else:
+ executing = utils.get_python_executable(self.target)
+ complement = ["-m", "pip"]
+ try:
+ fname = utils.do_script(this_script=None, python_exe=executing, verbose=self.verbose, install_options=complement + my_actions + my_list)
+ except RuntimeError as e:
+ if not self.verbose:
+ print("Failed!")
+ raise
+ else:
+ print(f"Pip action failed with error: {e}") # Print error if verbose
+
+ def patch_standard_packages(self, package_name="", to_movable=True):
+ """patch Winpython packages in need"""
+ import filecmp
+
+ # 'pywin32' minimal post-install (pywin32_postinstall.py do too much)
+ if package_name.lower() in ("", "pywin32"):
+ origin = Path(self.target) / "site-packages" / "pywin32_system32"
+ destin = Path(self.target)
+ if origin.is_dir():
+ for name in os.listdir(origin):
+ here, there = origin / name, destin / name
+ if not there.exists() or not filecmp.cmp(here, there):
+ shutil.copyfile(here, there)
+ # 'pip' to do movable launchers (around line 100) !!!!
+ # rational: https://github.com/pypa/pip/issues/2328
+ if package_name.lower() == "pip" or package_name == "":
+ # ensure pip will create movable launchers
+ # sheb_mov1 = classic way up to WinPython 2016-01
+ # sheb_mov2 = tried way, but doesn't work for pip (at least)
+ the_place = Path(self.target) / "lib" / "site-packages" / "pip" / "_vendor" / "distlib" / "scripts.py"
+ sheb_fix = " executable = get_executable()"
+ sheb_mov1 = " executable = os.path.join(os.path.basename(get_executable()))"
+ sheb_mov2 = " executable = os.path.join('..',os.path.basename(get_executable()))"
+ if to_movable:
+ utils.patch_sourcefile(the_place, sheb_fix, sheb_mov1)
+ utils.patch_sourcefile(the_place, sheb_mov2, sheb_mov1)
+ else:
+ utils.patch_sourcefile(the_place, sheb_mov1, sheb_fix)
+ utils.patch_sourcefile(the_place, sheb_mov2, sheb_fix)
+
+ # create movable launchers for previous package installations
+ self.patch_all_shebang(to_movable=to_movable)
+ if package_name.lower() in ("", "spyder"):
+ # spyder don't goes on internet without you ask
+ utils.patch_sourcefile(
+ Path(self.target) / "lib" / "site-packages" / "spyder" / "config" / "main.py",
+ "'check_updates_on_startup': True,",
+ "'check_updates_on_startup': False,",
+ )
+
+
+ def handle_specific_packages(self, package):
+ """Packages requiring additional configuration"""
+ if package.name.lower() in ("pyqt4", "pyqt5", "pyside2"):
+ # Qt configuration file (where to find Qt)
+ name = "qt.conf"
+ contents = """[Paths]\nPrefix = .\nBinaries = ."""
+ self.create_file(package, name, str(Path("Lib") / "site-packages" / package.name), contents)
+ self.create_file(package, name, ".", contents.replace(".", f"./Lib/site-packages/{package.name}"))
+ # pyuic script
+ if package.name.lower() == "pyqt5":
+ # see http://code.activestate.com/lists/python-list/666469/
+ tmp_string = r"""@echo off
+if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat"
+"%WINPYDIR%\python.exe" -m PyQt5.uic.pyuic %1 %2 %3 %4 %5 %6 %7 %8 %9"""
+ else:
+ tmp_string = r"""@echo off
+if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat"
+"%WINPYDIR%\python.exe" "%WINPYDIR%\Lib\site-packages\package.name\uic\pyuic.py" %1 %2 %3 %4 %5 %6 %7 %8 %9"""
+ # PyPy adaption: python.exe or pypy3.exe
+ my_exec = Path(utils.get_python_executable(self.target)).name
+ tmp_string = tmp_string.replace("python.exe", my_exec).replace("package.name", package.name)
+ self.create_file(package, f"pyuic{package.name[-1]}.bat", "Scripts", tmp_string)
+ # Adding missing __init__.py files (fixes Issue 8)
+ uic_path = str(Path("Lib") / "site-packages" / package.name / "uic")
+ for dirname in ("Loader", "port_v2", "port_v3"):
+ self.create_file(package, "__init__.py", str(Path(uic_path) / dirname), "")
+
+ def _print(self, package: Package, action: str):
+ """Print package-related action text."""
+ text = f"{action} {package.name} {package.version}"
+ if self.verbose:
+ utils.print_box(text)
+ else:
+ print(f" {text}...", end=" ")
+
+ def _print_done(self):
+ """Print OK at the end of a process"""
+ if not self.verbose:
+ print("OK")
+
+ def uninstall(self, package):
+ """Uninstall package from distribution"""
+ self._print(package, "Uninstalling")
+ if package.name != "pip":
+ # trick to get true target (if not current)
+ this_exec = utils.get_python_executable(self.target) # PyPy !
+ subprocess.call([this_exec, "-m", "pip", "uninstall", package.name, "-y"], cwd=self.target)
+ self._print_done()
+
+ def install_bdist_direct(self, package, install_options=None):
+ """Install a package directly !"""
+ self._print(package,f"Installing {package.fname.split('.')[-1]}")
+ try:
+ fname = utils.direct_pip_install(
+ package.fname,
+ python_exe=utils.get_python_executable(self.target), # PyPy !
+ verbose=self.verbose,
+ install_options=install_options,
+ )
+ except RuntimeError:
+ if not self.verbose:
+ print("Failed!")
+ raise
+ package = Package(fname)
+ self._print_done()
+
+def main(test=False):
+
+ registerWinPythonHelp = f"Register WinPython: associate file extensions, icons and context menu with this WinPython"
+ unregisterWinPythonHelp = f"Unregister WinPython: de-associate file extensions, icons and context menu from this WinPython"
+ parser = ArgumentParser(prog="wppm",
+ description="WinPython Package Manager: handle a WinPython Distribution and its packages",
+ formatter_class=RawTextHelpFormatter,
+ )
+ parser.add_argument("fname", metavar="package(s) or lockfile", nargs="*", default=[""], type=str, help="optional package names, wheels, or lockfile")
+ 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("--unregister", dest="unregisterWinPython", action="store_true", help=unregisterWinPythonHelp)
+ 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="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 of 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 (!= constraining) dependancies of the given package[option]: wppm -r pytest![test]")
+ parser.add_argument("-l", dest="levels", type=int, default=-1, help="show 'LEVELS' levels of dependencies (with -p, -r): 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)")
+
+ args = parser.parse_args()
+ 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, args.wheelsource)
+ for args_fname in args.fname:
+ pack, extra, *other = (args_fname + "[").replace("]", "[").split("[")
+ print(pip.down(pack, extra, args.levels if args.levels>0 else 2, verbose=args.verbose))
+ sys.exit()
+ elif args.pipup:
+ pip = piptree.PipData(targetpython, args.wheelsource)
+ for args_fname in args.fname:
+ pack, extra, *other = (args_fname + "[").replace("]", "[").split("[")
+ print(pip.up(pack, extra, args.levels if args.levels>=0 else 1, verbose=args.verbose))
+ sys.exit()
+ elif args.list:
+ pip = piptree.PipData(targetpython, args.wheelsource)
+ todo= []
+ for args_fname in args.fname:
+ todo += [l for l in pip.pip_list(full=True) if bool(re.search(args_fname, l[0]))]
+ todo = sorted(set(todo)) #, key=lambda p: (p[0].lower(), p[2])
+ titles = [['Package', 'Version', 'Summary'], ['_' * max(x, 6) for x in utils.columns_width(todo)]]
+ listed = utils.formatted_list(titles + todo, max_width=70)
+ for p in listed:
+ print(*p)
+ sys.exit()
+ elif args.all:
+ pip = piptree.PipData(targetpython, args.wheelsource)
+ for args_fname in args.fname:
+ todo = [l for l in pip.pip_list(full=True) if bool(re.search(args_fname, l[0]))]
+ for l in sorted(set(todo)):
+ title = f"** Package: {l[0]} **"
+ print("\n" + "*" * len(title), f"\n{title}", "\n" + "*" * len(title))
+ for key, value in pip.raw[l[0]].items():
+ rawtext = json.dumps(value, indent=2, ensure_ascii=False)
+ lines = [l for l in rawtext.split(r"\n") if len(l.strip()) > 2]
+ if key.lower() != 'description' or args.verbose:
+ print(f"{key}: ", "\n".join(lines).replace('"', ""))
+ sys.exit()
+ if args.registerWinPython:
+ print(registerWinPythonHelp)
+ if utils.is_python_distribution(args.target):
+ dist = Distribution(args.target)
+ else:
+ raise OSError(f"Invalid Python distribution {args.target}")
+ print(f"registering {args.target}")
+ print("continue ? Y/N")
+ theAnswer = input()
+ if theAnswer == "Y":
+ associate.register(dist.target, verbose=args.verbose)
+ sys.exit()
+ if args.unregisterWinPython:
+ print(unregisterWinPythonHelp)
+ if utils.is_python_distribution(args.target):
+ dist = Distribution(args.target)
+ else:
+ raise OSError(f"Invalid Python distribution {args.target}")
+ print(f"unregistering {args.target}")
+ print("continue ? Y/N")
+ theAnswer = input()
+ if theAnswer == "Y":
+ associate.unregister(dist.target, verbose=args.verbose)
+ sys.exit()
+ if utils.is_python_distribution(args.target):
+ dist = Distribution(args.target, verbose=True)
+ cmd_fix = rf"from winpython import wppm;dist=wppm.Distribution(r'{dist.target}');dist.patch_standard_packages('pip', to_movable=False)"
+ cmd_mov = rf"from winpython import wppm;dist=wppm.Distribution(r'{dist.target}');dist.patch_standard_packages('pip', to_movable=True)"
+ if args.fix:
+ # dist.patch_standard_packages('pip', to_movable=False) # would fail on wppm.exe
+ p = subprocess.Popen(["start", "cmd", "/k",dist.python_exe, "-c" , cmd_fix], shell = True, cwd=dist.target)
+ sys.exit()
+ 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:
+ default = dist.generate_package_index_markdown()
+ if args.wheelsource:
+ compare = dist.generate_package_index_markdown(wheeldir = args.wheelsource)
+ print(diff.compare_markdown_sections(default, compare,'python', 'wheelhouse', 'installed', 'wheelhouse'))
+ else:
+ print(default)
+ sys.exit()
+ if not args.install and not args.uninstall and args.fname[0].endswith(".toml"):
+ args.install = True # for Drag & Drop of .toml (and not wheel)
+ if args.fname[0] == "" or (not args.install and not args.uninstall):
+ parser.print_help()
+ sys.exit()
+ else:
+ try:
+ for args_fname in args.fname:
+ 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, install_options=install_from_wheelhouse)
+ except NotImplementedError:
+ raise RuntimeError("Package is not (yet) supported by WPPM")
+ else:
+ raise OSError(f"Invalid Python distribution {args.target}")
+
+
+if __name__ == "__main__":
main()
\ No newline at end of file