Skip to content
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
173 changes: 56 additions & 117 deletions winpython/wppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
os.environ["HOME"] = os.environ["USERPROFILE"]

class Package:
"standardize a Package from filename or pip list"
def __init__(self, fname, suggested_summary=None):
"""Standardize a Package from filename or pip list."""
def __init__(self, fname: str, suggested_summary: str = None):
self.fname = fname
self.description = piptree.sum_up(suggested_summary) if suggested_summary else ""
self.name, self.version = None, None
Expand All @@ -40,68 +40,30 @@ def __str__(self):


class Distribution:
def __init__(self, target=None, verbose=False):
# if no target path given, take the current python interpreter one
self.target = target or os.path.dirname(sys.executable)
"""Handles operations on a WinPython distribution."""
def __init__(self, target: str = None, verbose: bool = False):
self.target = target or os.path.dirname(sys.executable) # Default target more explicit
self.verbose = verbose
self.pip = None
self.to_be_removed = [] # list of directories to be removed later
self.version, self.architecture = utils.get_python_infos(target)
# name of the exe (python.exe or pypy3.exe)
self.to_be_removed = []
self.version, self.architecture = utils.get_python_infos(self.target)
self.short_exe = Path(utils.get_python_executable(self.target)).name

def clean_up(self):
"""Remove directories which couldn't be removed when building"""
"""Remove directories that were marked for removal."""
for path in self.to_be_removed:
try:
shutil.rmtree(path, onexc=utils.onerror)
except WindowsError:
print(f"Directory {path} could not be removed", file=sys.stderr)
except OSError as e:
print(f"Error: Could not remove directory {path}: {e}", file=sys.stderr)

def remove_directory(self, path):
"""Try to remove directory -- on WindowsError, remove it later"""
def remove_directory(self, path: str):
"""Try to remove a directory, add to removal list on failure."""
try:
shutil.rmtree(path)
except WindowsError:
except OSError:
self.to_be_removed.append(path)

def copy_files(self, package, targetdir, srcdir, dstdir, create_bat_files=False):
"""Add copy task"""
srcdir = str(Path(targetdir) / srcdir)
if not Path(srcdir).is_dir():
return
offset = len(srcdir) + len(os.pathsep)
for dirpath, dirnames, filenames in os.walk(srcdir):
for dname in dirnames:
t_dname = str(Path(dirpath) / dname)[offset:]
src = str(Path(srcdir) / t_dname)
dst = str(Path(dstdir) / t_dname)
if self.verbose:
print(f"mkdir: {dst}")
full_dst = str(Path(self.target) / dst)
if not Path(full_dst).exists():
os.mkdir(full_dst)
package.files.append(dst)
for fname in filenames:
t_fname = str(Path(dirpath) / fname)[offset:]
src = str(Path(srcdir) / t_fname)
dst = fname if dirpath.endswith("_system32") else str(Path(dstdir) / t_fname)
if self.verbose:
print(f"file: {dst}")
full_dst = str(Path(self.target) / dst)
shutil.move(src, full_dst)
package.files.append(dst)
name, ext = Path(dst).stem, Path(dst).suffix
if create_bat_files and ext in ("", ".py"):
dst = name + ".bat"
if self.verbose:
print(f"file: {dst}")
full_dst = str(Path(self.target) / dst)
fd = open(full_dst, "w")
fd.write(f"""@echo off\npython "%~dpn0{ext}" %*""")
fd.close()
package.files.append(dst)

def create_file(self, package, name, dstdir, contents):
"""Generate data file -- path is relative to distribution root dir"""
dst = str(Path(dstdir) / name)
Expand All @@ -112,8 +74,8 @@ def create_file(self, package, name, dstdir, contents):
fd.write(contents)
package.files.append(dst)

def get_installed_packages(self, update=False):
"""Return installed packages"""
def get_installed_packages(self, update: bool = False) -> list[Package]:
"""Return installed packages."""

# Include package installed via pip (not via WPPM)
wppm = []
Expand All @@ -133,14 +95,14 @@ def get_installed_packages(self, update=False):
]
return sorted(wppm, key=lambda tup: tup.name.lower())

def find_package(self, name):
"""Find installed package"""
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=True, max_exe_size=999999, targetdir=""):
"""make all python launchers relatives"""
def patch_all_shebang(self, to_movable: bool = True, max_exe_size: int = 999999, targetdir: str = ""):
"""Make all python launchers relative."""
import glob

for ffname in glob.glob(r"%s\Scripts\*.exe" % self.target):
Expand All @@ -150,17 +112,16 @@ def patch_all_shebang(self, to_movable=True, max_exe_size=999999, targetdir=""):
for ffname in glob.glob(r"%s\Scripts\*.py" % self.target):
utils.patch_shebang_line_py(ffname, to_movable=to_movable, targetdir=targetdir)

def install(self, package, install_options=None):
"""Install package in distribution"""
# wheel addition
if package.fname.endswith((".whl", ".tar.gz", ".zip")):
def install(self, package: Package, install_options: list[str] = None): # Type hint install_options
"""Install package in distribution."""
if package.fname.endswith((".whl", ".tar.gz", ".zip")): # 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=None, install_options=None):
"""Do pip action in a distribution"""
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")
Expand All @@ -171,10 +132,12 @@ def do_pip_action(self, actions=None, install_options=None):
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:
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"""
Expand Down Expand Up @@ -206,9 +169,7 @@ def patch_standard_packages(self, package_name="", to_movable=True):
# sheb_mov2 = tried way, but doesn't work for pip (at least)
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()))"
)
sheb_mov2 = " executable = os.path.join('..',os.path.basename(get_executable()))"

# Adpating to PyPy
the_place = site_package_place + r"pip\_vendor\distlib\scripts.py"
Expand Down Expand Up @@ -240,41 +201,33 @@ def patch_standard_packages(self, package_name="", to_movable=True):
else:
self.create_pybat(package_name.lower())

def create_pybat(
self,
names="",
contents=r"""@echo off

def create_pybat(self, names="", contents=r"""@echo off
..\python "%~dpn0" %*""",
):
"""Create launcher batch script when missing"""

scriptpy = str(Path(self.target) / "Scripts") # std Scripts of python

# PyPy has no initial Scipts directory
if not Path(scriptpy).is_dir():
os.mkdir(scriptpy)
scriptpy = Path(self.target) / "Scripts" # std Scripts of python
os.makedirs(scriptpy, exist_ok=True)
if not list(names) == names:
my_list = [f for f in os.listdir(scriptpy) if "." not in f and f.startswith(names)]
else:
my_list = names
for name in my_list:
if Path(scriptpy).is_dir() and (Path(scriptpy) / name).is_file():
if scriptpy.is_dir() and (scriptpy / name).is_file():
if (
not (Path(scriptpy) / (name + ".exe")).is_file()
and not (Path(scriptpy) / (name + ".bat")).is_file()
not (scriptpy / (name + ".exe")).is_file()
and not (scriptpy / (name + ".bat")).is_file()
):
with open(Path(scriptpy) / (name + ".bat"), "w") as fd:
with open(scriptpy / (name + ".bat"), "w") as fd:
fd.write(contents)
fd.close()

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]
Prefix = .
Binaries = ."""
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
Expand All @@ -296,13 +249,14 @@ def handle_specific_packages(self, package):
for dirname in ("Loader", "port_v2", "port_v3"):
self.create_file(package, "__init__.py", str(Path(uic_path) / dirname), "")

def _print(self, package, action):
"""Print package-related action text (e.g. 'Installing')"""

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(" " + text + "...", end=" ")
print(f" {text}...", end=" ")

def _print_done(self):
"""Print OK at the end of a process"""
Expand All @@ -312,12 +266,13 @@ def _print_done(self):
def uninstall(self, package):
"""Uninstall package from distribution"""
self._print(package, "Uninstalling")
if not package.name == "pip":
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]}")
Expand All @@ -335,19 +290,6 @@ def install_bdist_direct(self, package, install_options=None):
package = Package(fname)
self._print_done()

def install_script(self, script, install_options=None):
try:
fname = utils.do_script(
script,
python_exe=utils.get_python_executable(self.target), # PyPy3 !
verbose=self.verbose,
install_options=install_options,
)
except RuntimeError:
if not self.verbose:
print("Failed!")
raise


def main(test=False):
if test:
Expand Down Expand Up @@ -415,7 +357,7 @@ def main(test=False):
const=True,
default=False,
help=f"list packages matching the given [optionnal] package expression: wppm -ls, wppm -ls pand",
)
)
parser.add_argument(
"-p",
dest="pipdown",
Expand Down Expand Up @@ -473,9 +415,8 @@ def main(test=False):
)
args = parser.parse_args()
targetpython = None
if args.target and not args.target==sys.prefix:
targetpython = args.target if args.target[-4:] == '.exe' else str(Path(args.target) / 'python.exe')
# print(targetpython.resolve() to check)
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.install and args.uninstall:
raise RuntimeError("Incompatible arguments: --install and --uninstall")
if args.registerWinPython and args.unregisterWinPython:
Expand All @@ -492,51 +433,49 @@ def main(test=False):
sys.exit()
elif args.list:
pip = piptree.PipData(targetpython)
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0])) ]
titles = [['Package', 'Version', 'Summary'],['_' * max(x, 6) for x in utils.columns_width(todo)]]
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))]
titles = [['Package', 'Version', 'Summary'], ['_' * max(x, 6) for x in utils.columns_width(todo)]]
listed = utils.formatted_list(titles + todo, max_width=70)
for p in listed:
print(*p)
sys.exit()
elif args.all:
pip = piptree.PipData(targetpython)
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0])) ]
todo = [l for l in pip.pip_list(full=True) if bool(re.search(args.fname, l[0]))]
for l in todo:
# print(pip.distro[l[0]])
title = f"** Package: {l[0]} **"
print("\n"+"*"*len(title), f"\n{title}", "\n"+"*"*len(title) )
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==True:
if key.lower() != 'description' or args.verbose:
print(f"{key}: ", "\n".join(lines).replace('"', ""))
sys.exit()
sys.exit()
if args.registerWinPython:
print(registerWinPythonHelp)
if utils.is_python_distribution(args.target):
dist = Distribution(args.target)
else:
raise WindowsError(f"Invalid Python distribution {args.target}")
raise OSError(f"Invalid Python distribution {args.target}")
print(f"registering {args.target}")
print("continue ? Y/N")
theAnswer = input()
if theAnswer == "Y":
from winpython import associate

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 WindowsError(f"Invalid Python distribution {args.target}")
raise OSError(f"Invalid Python distribution {args.target}")
print(f"unregistering {args.target}")
print("continue ? Y/N")
theAnswer = input()
if theAnswer == "Y":
from winpython import associate

associate.unregister(dist.target, verbose=args.verbose)
sys.exit()
elif not args.install and not args.uninstall:
Expand All @@ -546,7 +485,7 @@ def main(test=False):
parser.print_help()
sys.exit()
else:
raise IOError(f"File not found: {args.fname}")
raise FileNotFoundError(f"File not found: {args.fname}")
if utils.is_python_distribution(args.target):
dist = Distribution(args.target, verbose=True)
try:
Expand All @@ -560,7 +499,7 @@ def main(test=False):
except NotImplementedError:
raise RuntimeError("Package is not (yet) supported by WPPM")
else:
raise WindowsError(f"Invalid Python distribution {args.target}")
raise OSError(f"Invalid Python distribution {args.target}")


if __name__ == "__main__":
Expand Down