Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion winpython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
OTHER DEALINGS IN THE SOFTWARE.
"""

__version__ = '12.0.20250201'
__version__ = '13.0.20250209'
__license__ = __doc__
__project_url__ = 'http://winpython.github.io/'
56 changes: 27 additions & 29 deletions winpython/piptree.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
This script provides functionality to inspect and display package dependencies
This script provides functionality to inspect and display package dependencies
in a Python environment, including both downward and upward dependencies.
Requires Python 3.8+ due to importlib.metadata.
"""
Expand All @@ -11,20 +11,19 @@
import platform
import os
from collections import OrderedDict
from typing import Dict, List, Optional, Tuple, Union
from pip._vendor.packaging.markers import Marker, InvalidMarker
from importlib.metadata import Distribution, distributions
from pathlib import Path


def normalize(name):
def normalize(name: str) -> str:
"""Normalize package name according to PEP 503."""
return re.sub(r"[-_.]+", "-", name).lower()


def sum_up(text, max_length=144, stop_at=". "):
def sum_up(text: str, max_length: int = 144, stop_at: str = ". ") -> str:
"""
Summarize text to fit within max_length characters, ending at the last complete sentence if possible.

:param text: The text to summarize
:param max_length: Maximum length for summary
:param stop_at: String to stop summarization at
Expand All @@ -37,14 +36,14 @@ def sum_up(text, max_length=144, stop_at=". "):
summary = summary[:max_length]
return summary

class pipdata:
class PipData:
"""
Wrapper around Distribution.discover() or Distribution.distributions() to manage package metadata.
"""

def __init__(self, target=None):
self.distro = {}
self.raw = {}
def __init__(self, target: Optional[str] = None):
self.distro: Dict[str, Dict] = {}
self.raw: Dict[str, Dict] = {}
self.environment = self._get_environment()

search_path = target or sys.executable
Expand All @@ -57,18 +56,18 @@ def __init__(self, target=None):
for package in packages:
self._process_package(package)

# On a second pass, complement dependancies in reverse mode with 'wanted-per':
# On a second pass, complement dependencies in reverse mode with 'wanted-per':
self._populate_reverse_dependencies()

def _get_environment(self):
def _get_environment(self) -> Dict[str, str]:
"""
Collect environment details for dependency evaluation.

:return: Dictionary containing system and Python environment information
"""
return {
"implementation_name": sys.implementation.name,
"implementation_version": "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version),
"implementation_version": f"{sys.implementation.version.major}.{sys.implementation.version.minor}.{sys.implementation.version.micro}",
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
Expand All @@ -80,7 +79,7 @@ def _get_environment(self):
"sys_platform": sys.platform,
}

def _process_package(self, package):
def _process_package(self, package: Distribution) -> None:
"""Process package metadata and store it in the distro dictionary."""
meta = package.metadata
name = meta['Name']
Expand All @@ -99,7 +98,7 @@ def _process_package(self, package):
"provided": {'': None} # Placeholder for extras provided by this package
}

def _get_requires(self, package):
def _get_requires(self, package: Distribution) -> List[Dict[str, str]]:
"""Extract and normalize requirements for a package."""
# requires = list of dict with 1 level need downward
# req_key = package_key requires
Expand Down Expand Up @@ -136,7 +135,7 @@ def _get_requires(self, package):
requires.append(req_add)
return requires

def _get_provides(self, package):
def _get_provides(self, package: Distribution) -> Dict[str, None]:
"""Get the list of extras provided by this package."""
provides = {'': None}
if package.requires:
Expand All @@ -147,7 +146,7 @@ def _get_provides(self, package):
provides[req_marker.split('extra == ')[1].translate(remove_list)] = None
return provides

def _populate_reverse_dependencies(self):
def _populate_reverse_dependencies(self) -> None:
"""Add reverse dependencies to each package."""
# - get all downward links in 'requires_dist' of each package
# - feed the required packages 'reverse_dependencies' as a reverse dict of dict
Expand All @@ -167,11 +166,11 @@ def _populate_reverse_dependencies(self):
if "req_marker" in requirement:
want_add["req_marker"] = requirement["req_marker"]
if 'extra == ' in requirement["req_marker"]:
remove_list = {ord("'"):None, ord('"'):None}
remove_list = {ord("'"): None, ord('"'): None}
self.distro[requirement["req_key"]]["provided"][requirement["req_marker"].split('extra == ')[1].translate(remove_list)] = None
self.distro[requirement["req_key"]]["reverse_dependencies"].append(want_add)

def _get_dependency_tree(self, package_name, extra="", version_req="", depth=20, path=None, verbose=False, upward=False):
def _get_dependency_tree(self, package_name: str, extra: str = "", version_req: str = "", depth: int = 20, path: Optional[List[str]] = None, verbose: bool = False, upward: bool = False) -> List[List[str]]:
"""Recursive function to build dependency tree."""
path = path or []
extras = extra.split(",")
Expand Down Expand Up @@ -235,7 +234,7 @@ def _get_dependency_tree(self, package_name, extra="", version_req="", depth=20,
ret_all.append(ret)
return ret_all

def down(self, pp="", extra="", depth=20, indent=5, version_req="", verbose=False):
def down(self, pp: str = "", extra: str = "", depth: int = 20, indent: int = 5, version_req: str = "", verbose: bool = False) -> str:
"""Print the downward requirements for the package or all packages."""
if pp == ".":
results = [self.down(one_pp, extra, depth, indent, version_req, verbose=verbose) for one_pp in sorted(self.distro)]
Expand All @@ -255,9 +254,8 @@ def down(self, pp="", extra="", depth=20, indent=5, version_req="", verbose=Fals
lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2]
return "\n".join(lines).replace('"', "")

def up(self, pp, extra="", depth=20, indent=5, version_req="", verbose=False):
def up(self, pp: str, extra: str = "", depth: int = 20, indent: int = 5, version_req: str = "", verbose: bool = False) -> str:
"""Print the upward needs for the package."""

if pp == ".":
results = [self.up(one_pp, extra, depth, indent, version_req, verbose) for one_pp in sorted(self.distro)]
return '\n'.join(filter(None, results))
Expand All @@ -274,22 +272,22 @@ def up(self, pp, extra="", depth=20, indent=5, version_req="", verbose=False):

rawtext = json.dumps(self._get_dependency_tree(pp, extra, version_req, depth, verbose=verbose, upward=True), indent=indent)
lines = [l for l in rawtext.split("\n") if len(l.strip()) > 2]
return '\n'.join(filter(None, lines)).replace('"', "")
return "\n".join(filter(None, lines)).replace('"', "")

def description(self, pp):
def description(self, pp: str) -> None:
"""Return description of the package."""
if pp in self.distro:
return print("\n".join(self.distro[pp]["description"].split(r"\n")))

def summary(self, pp):
"""Return summary of the package."""
if pp in self.distro:
return self.distro[pp]["summary"]
return self.distro[pp]["summary"]
return ""

def pip_list(self, full=False, max_length=144):
def pip_list(self, full: bool = False, max_length: int = 144) -> List[Tuple[str, Union[str, Tuple[str, str]]]]:
"""List installed packages similar to pip list."""
if full:
return [(p, self.distro[p]["version"], sum_up(self.distro[p]["summary"]), max_length) for p in sorted(self.distro)]
return [(p, self.distro[p]["version"], sum_up(self.distro[p]["summary"], max_length)) for p in sorted(self.distro)]
else:
return [(p, sum_up(self.distro[p]["version"], max_length)) for p in sorted(self.distro)]

12 changes: 6 additions & 6 deletions winpython/wppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ def get_installed_packages(self, update=False):
# Include package installed via pip (not via WPPM)
wppm = []
if str(Path(sys.executable).parent) == self.target:
self.pip = piptree.pipdata()
self.pip = piptree.PipData()
else:
self.pip = piptree.pipdata(utils.get_python_executable(self.target))
self.pip = piptree.PipData(utils.get_python_executable(self.target))
pip_list = self.pip.pip_list()

# create pip package list
Expand Down Expand Up @@ -571,25 +571,25 @@ def main(test=False):
if args.registerWinPython and args.unregisterWinPython:
raise RuntimeError("Incompatible arguments: --install and --uninstall")
if args.pipdown:
pip = piptree.pipdata(targetpython)
pip = piptree.PipData(targetpython)
pack, extra, *other = (args.fname + "[").replace("]", "[").split("[")
print(pip.down(pack, extra, args.levels, verbose=args.verbose))
sys.exit()
elif args.pipup:
pip = piptree.pipdata(targetpython)
pip = piptree.PipData(targetpython)
pack, extra, *other = (args.fname + "[").replace("]", "[").split("[")
print(pip.up(pack, extra, args.levels, verbose=args.verbose))
sys.exit()
elif args.list:
pip = piptree.pipdata(targetpython)
pip = piptree.PipData(targetpython)
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)
pip = piptree.PipData(targetpython)
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]])
Expand Down