Skip to content

flash- 2.5 beta complicated things, chatgpt + manual restored order #1565

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 93 additions & 222 deletions winpython/associate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,21 @@
import sys
import os
from pathlib import Path
import platform
import importlib.util
import winreg
from winpython import utils
from argparse import ArgumentParser

# --- Constants ---
KEY_C = r"Software\Classes\%s"
KEY_CP = r"Software\Classes"
KEY_S = r"Software\Python"
KEY_S0 = KEY_S + r"\WinPython" # was PythonCore before PEP-0514
EWI = "Edit with IDLE"
EWS = "Edit with Spyder"
DROP_HANDLER_CLSID = "{60254CA5-953B-11CF-8C96-00AA00B8708C}"

# --- 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
# KEY_WRITE access is needed to set values

if verbose:
print(f"{rootkey_name}\\{key_path}\\{name if name else ''}:{value}")
with winreg.CreateKeyEx(root, key_path, 0, winreg.KEY_WRITE) as key:
winreg.SetValueEx(key, name, 0, reg_type, value)
except OSError as e:
Expand All @@ -42,9 +31,9 @@ def _set_reg_value(root, key_path, name, value, reg_type=winreg.REG_SZ, verbose=
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:
if verbose:
print(f"{rootkey_name}\\{key_path}")
# 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)
Expand Down Expand Up @@ -79,7 +68,6 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
bname, ext = Path(name).stem, Path(name).suffix
if ext.lower() == ".exe":
# Path for the shortcut file in the start menu folder
# This depends on utils.create_winpython_start_menu_folder creating the right path
shortcut_name = str(Path(utils.create_winpython_start_menu_folder(current=current)) / bname) + '.lnk'
data.append(
(
Expand All @@ -90,128 +78,86 @@ def _get_shortcut_data(target, current=True, has_pywin32=False):
)
return data

# --- Registry Entry Definitions ---

# Structure: (key_path, value_name, value, reg_type)
# Use None for value_name to set the default value of the key
REGISTRY_ENTRIES = []

# --- Extensions ---
EXTENSIONS = {
".py": "Python.File",
".pyw": "Python.NoConFile",
".pyc": "Python.CompiledFile",
".pyo": "Python.CompiledFile",
}
for ext, file_type in EXTENSIONS.items():
REGISTRY_ENTRIES.append((KEY_C % ext, None, file_type))

# --- MIME types ---
MIME_TYPES = {
".py": "text/plain",
".pyw": "text/plain",
}
for ext, mime_type in MIME_TYPES.items():
REGISTRY_ENTRIES.append((KEY_C % ext, "Content Type", mime_type))

# --- Verbs (Open, Edit with IDLE, Edit with Spyder) ---
# These depend on the python/pythonw/spyder paths
def _get_verb_entries(target):
python = str((Path(target) / "python.exe").resolve())
pythonw = str((Path(target) / "pythonw.exe").resolve())
spyder_exe = str((Path(target).parent / "Spyder.exe").resolve())

# Command string for Spyder, fallback to script if exe not found
spyder_cmd = rf'"{spyder_exe}" "%1"' if Path(spyder_exe).is_file() else rf'"{pythonw}" "{target}\Scripts\spyder" "%1"'

verbs_data = [
# Open verb
(rf"{KEY_CP}\Python.File\shell\open\command", None, rf'"{python}" "%1" %*'),
(rf"{KEY_CP}\Python.NoConFile\shell\open\command", None, rf'"{pythonw}" "%1" %*'),
(rf"{KEY_CP}\Python.CompiledFile\shell\open\command", None, rf'"{python}" "%1" %*'),
# Edit with IDLE verb
(rf"{KEY_CP}\Python.File\shell\{EWI}\command", None, rf'"{pythonw}" "{target}\Lib\idlelib\idle.pyw" -n -e "%1"'),
(rf"{KEY_CP}\Python.NoConFile\shell\{EWI}\command", None, rf'"{pythonw}" "{target}\Lib\idlelib\idle.pyw" -n -e "%1"'),
# Edit with Spyder verb
(rf"{KEY_CP}\Python.File\shell\{EWS}\command", None, spyder_cmd),
(rf"{KEY_CP}\Python.NoConFile\shell\{EWS}\command", None, spyder_cmd),
]
return verbs_data

# --- Drop support ---
DROP_SUPPORT_FILE_TYPES = ["Python.File", "Python.NoConFile", "Python.CompiledFile"]
for file_type in DROP_SUPPORT_FILE_TYPES:
REGISTRY_ENTRIES.append((rf"{KEY_C % file_type}\shellex\DropHandler", None, DROP_HANDLER_CLSID))

# --- Icons ---
def _get_icon_entries(target):
dlls_path = str(Path(target) / "DLLs")
icon_data = [
(rf"{KEY_CP}\Python.File\DefaultIcon", None, rf"{dlls_path}\py.ico"),
(rf"{KEY_CP}\Python.NoConFile\DefaultIcon", None, rf"{dlls_path}\py.ico"),
(rf"{KEY_CP}\Python.CompiledFile\DefaultIcon", None, rf"{dlls_path}\pyc.ico"),
]
return icon_data

# --- Descriptions ---
DESCRIPTIONS = {
"Python.File": "Python File",
"Python.NoConFile": "Python File (no console)",
"Python.CompiledFile": "Compiled Python File",
}
for file_type, desc in DESCRIPTIONS.items():
REGISTRY_ENTRIES.append((KEY_C % file_type, None, desc))


# --- PythonCore entries (PEP-0514 and WinPython specific) ---
def _get_pythoncore_entries(target):
python_infos = utils.get_python_infos(target) # ('3.11', 64)
short_version = python_infos[0] # e.g., '3.11'
long_version = utils.get_python_long_version(target) # e.g., '3.11.5'

SupportUrl = "https://winpython.github.io"
SysArchitecture = f'{python_infos[1]}bit' # e.g., '64bit'
SysVersion = short_version # e.g., '3.11'
Version = long_version # e.g., '3.11.5'
DisplayName = f'Python {Version} ({SysArchitecture})'

python_exe = str((Path(target) / "python.exe").resolve())
pythonw_exe = str((Path(target) / "pythonw.exe").resolve())

core_entries = []

# Main version key (WinPython\3.11)
version_key = f"{KEY_S0}\\{short_version}"
core_entries.extend([
(version_key, 'DisplayName', DisplayName),
(version_key, 'SupportUrl', SupportUrl),
(version_key, 'SysVersion', SysVersion),
(version_key, 'SysArchitecture', SysArchitecture),
(version_key, 'Version', Version),
])

# InstallPath key (WinPython\3.11\InstallPath)
install_path_key = f"{version_key}\\InstallPath"
core_entries.extend([
(install_path_key, None, str(Path(target) / '')), # Default value is the install dir
(install_path_key, 'ExecutablePath', python_exe),
(install_path_key, 'WindowedExecutablePath', pythonw_exe),
])

# InstallGroup key (WinPython\3.11\InstallPath\InstallGroup)
core_entries.append((f"{install_path_key}\\InstallGroup", None, f"Python {short_version}"))
def register_in_registery(target, current=True, reg_type=winreg.REG_SZ, verbose=True) -> tuple[list[any], ...]:
"""Register in Windows (like regedit)"""

# Modules key (WinPython\3.11\Modules) - seems to be a placeholder key
core_entries.append((f"{version_key}\\Modules", None, ""))

# PythonPath key (WinPython\3.11\PythonPath)
core_entries.append((f"{version_key}\\PythonPath", None, rf"{target}\Lib;{target}\DLLs"))

# Help key (WinPython\3.11\Help\Main Python Documentation)
core_entries.append((f"{version_key}\\Help\\Main Python Documentation", None, rf"{target}\Doc\python{long_version}.chm"))

return core_entries
# --- 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 / "DLLs" / "py.ico")
icon_pyc = str(target / "DLLs" / "pyc.ico")
idle_path = str(target / "Lib" / "idlelib" / "idle.pyw")
doc_path = str(target / "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} if " "%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"'))
dynamic_entries.append((rf"Software\Classes\Python.NoConFile\shell\Edit with Spyder\command", None, f'"{spyder_exe}" "%1"'))
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 ---

Expand All @@ -223,19 +169,11 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
if verbose:
print(f'Creating WinPython registry entries for {target}')

# Set static registry entries
for key_path, name, value in REGISTRY_ENTRIES:
_set_reg_value(root, key_path, name, value, verbose=verbose)

# Set dynamic registry entries (verbs, icons, pythoncore)
dynamic_entries = []
dynamic_entries.extend(_get_verb_entries(target))
dynamic_entries.extend(_get_icon_entries(target))
dynamic_entries.extend(_get_pythoncore_entries(target))

for key_path, name, value in dynamic_entries:
_set_reg_value(root, key_path, name, value)

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:
Expand All @@ -246,8 +184,7 @@ def register(target, current=True, reg_type=winreg.REG_SZ, verbose=True):
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.")

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"""
Expand All @@ -256,92 +193,26 @@ def unregister(target, current=True, verbose=True):

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 = []

# Add dynamic keys first (helps DeleteKey succeed)
dynamic_entries = []
dynamic_entries.extend(_get_verb_entries(target))
dynamic_entries.extend(_get_icon_entries(target))
dynamic_entries.extend(_get_pythoncore_entries(target))

# Collect parent keys from dynamic entries
dynamic_parent_keys = {entry[0] for entry in dynamic_entries}
# Add keys from static entries
static_parent_keys = {entry[0] for entry in REGISTRY_ENTRIES}

# Combine and add the key templates that might become empty and should be removed
python_infos = utils.get_python_infos(target)
short_version = python_infos[0]
version_key_base = f"{KEY_S0}\\{short_version}"

# Keys from static REGISTRY_ENTRIES (mostly Class registrations)
keys_to_delete.extend([
KEY_C % file_type + rf"\shellex\DropHandler" for file_type in DROP_SUPPORT_FILE_TYPES
])
keys_to_delete.extend([
KEY_C % file_type + rf"\shellex" for file_type in DROP_SUPPORT_FILE_TYPES
])
#keys_to_delete.extend([
# KEY_C % file_type + rf"\DefaultIcon" for file_type in set(EXTENSIONS.values()) # Use values as file types
#])
keys_to_delete.extend([
KEY_C % file_type + rf"\shell\{EWI}\command" for file_type in ["Python.File", "Python.NoConFile"] # Specific types for IDLE verb
])
keys_to_delete.extend([
KEY_C % file_type + rf"\shell\{EWS}\command" for file_type in ["Python.File", "Python.NoConFile"] # Specific types for Spyder verb
])
# General open command keys (cover all file types)
keys_to_delete.extend([
KEY_C % file_type + rf"\shell\open\command" for file_type in ["Python.File", "Python.NoConFile", "Python.CompiledFile"]
])


# Keys from dynamic entries (Verbs, Icons, PythonCore) - add parents
# Verbs
keys_to_delete.extend([KEY_C % file_type + rf"\shell\{EWI}" for file_type in ["Python.File", "Python.NoConFile"]])
keys_to_delete.extend([KEY_C % file_type + rf"\shell\{EWS}" for file_type in ["Python.File", "Python.NoConFile"]])
keys_to_delete.extend([KEY_C % file_type + rf"\shell\open" for file_type in ["Python.File", "Python.NoConFile", "Python.CompiledFile"]])
keys_to_delete.extend([KEY_C % file_type + rf"\shell" for file_type in ["Python.File", "Python.NoConFile", "Python.CompiledFile"]]) # Shell parent

# Icons
keys_to_delete.extend([KEY_C % file_type + rf"\DefaultIcon" for file_type in set(EXTENSIONS.values())]) # Already added above? Check for duplicates or order
keys_to_delete.extend([KEY_C % file_type for file_type in set(EXTENSIONS.values())]) # Parent keys for file types

# Extensions/Descriptions parents
# keys_to_delete.extend([KEY_C % ext for ext in EXTENSIONS.keys()]) # e.g., .py, .pyw

# PythonCore keys (from most specific down to the base)
keys_to_delete.extend([
f"{version_key_base}\\InstallPath\\InstallGroup",
f"{version_key_base}\\InstallPath",
f"{version_key_base}\\Modules",
f"{version_key_base}\\PythonPath",
f"{version_key_base}\\Help\\Main Python Documentation",
f"{version_key_base}\\Help",
version_key_base, # e.g., Software\Python\WinPython\3.11
KEY_S0, # Software\Python\WinPython
#KEY_S, # Software\Python (only if WinPython key is the only subkey - risky to delete)
])

# Attempt to delete keys
# Use a set to avoid duplicates, then sort by length descending to try deleting children first
# (although DeleteKey only works on empty keys anyway, so explicit ordering is clearer)

for key in keys_to_delete:
_delete_reg_key(root, key, verbose=verbose)
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.
# remove_winpython_start_menu_folder is likely the intended method.
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 "\
Expand Down