Skip to content

Commit

Permalink
complete the function of uninstalling package and its dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Abeautifulsnow committed Mar 6, 2024
1 parent ab19e69 commit b47de95
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 34 deletions.
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ pip3 install pkgu

after the installation is complete, `pkgu` executable file will be written to the python bin directory and you can enter `pkgu -h` command on your terminal to learn how to use it.

```bash
# Usage
usage: pkgu [-h] {update,remove} ...

Upgrade and uninstall python package.

options:
-h, --help show this help message and exit

Available commands:
{update,remove} Available commands
update Update python package.
remove remove python package with its dependencies.

# ------ update ------
options:
-h, --help show this help message and exit
-a, --async_upgrade Update the library asynchronously. Default: False
-d CACHE_FOLDER, --cache_folder CACHE_FOLDER
The cache.db file. Default: ~/.cache/cache.db
-e EXPIRE_TIME, --expire_time EXPIRE_TIME
The expiration time. Default: 43200
--no-cache Whether to use db cache. Default: False
-v, --version Display pkgu update version and information

# ------ remove ------
options:
-h, --help show this help message and exit
-l, --list list unused dependencies, but don't uninstall them.
-L, --leaves list leaves (packages which are not used by any others).
-y, --yes don't ask for confirmation of uninstall deletions.
-f, --freeze list leaves (packages which are not used by any others) in requirements.txt format
```

## ScreenShoot

> 1. No packages need to be upgraded.
Expand Down Expand Up @@ -64,6 +98,12 @@ So now it can support to update the python libraries asynchronously. 🥳

![img_7.png](https://raw.githubusercontent.com/Abeautifulsnow/pkgu/main/screenshoot/img_7.png)

> 6. !!!New - Support to use cache result from sqlite db file.
> 6. Support to use cache result from sqlite db file.
> 7. !!!New - Support to uninstall package with its dependencies together.
```bash

```

This improve the expirence that how we list the out-dated packages when they are huge to collect, and then there also is a cli flag `--no-cache` to control whether should to use cache.
91 changes: 58 additions & 33 deletions pkgu.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
from loguru import logger

try:
from pkg_resources import Distribution, get_distribution, working_set
from pkg_resources import Distribution
from pkg_resources import DistributionNotFound as _DistributionNotFound
from pkg_resources import VersionConflict as _VersionConflict
from pkg_resources import get_distribution, working_set
except (DeprecationWarning, ModuleNotFoundError, ImportError):
# TODO: Need to develop a backward-compatible version and use other way instead.
# https://setuptools.pypa.io/en/latest/pkg_resources.html
Expand Down Expand Up @@ -123,7 +124,7 @@ class VersionConflict(ResolutionError):
Requirement.
"""

_template = "{self.dist} is installed but {self.req} is required"
_template = "🦀 {self.dist} is installed but {self.req} is required"

@property
def dist(self):
Expand Down Expand Up @@ -153,7 +154,7 @@ class ContextualVersionConflict(VersionConflict):
requirements that required the installed Distribution.
"""

_template = VersionConflict._template + " by {self.required_by}"
_template = "🦏 " + VersionConflict._template + " by {self.required_by}"

@property
def required_by(self):
Expand All @@ -164,7 +165,7 @@ class DistributionNotFound(ResolutionError):
"""A requested distribution was not found"""

_template = (
"The '{self.req}' distribution was not found "
"🦀 The '{self.req}' distribution was not found "
"and is required by {self.requirers_str}"
)

Expand Down Expand Up @@ -756,9 +757,9 @@ def get_result_with_cache(self, cache_key: str):
def get_result(
self,
key: str,
nocache_fn: Callable[[Union[str, list]], Tuple[str, bool]],
nocache_fn: Callable[[Union[str, list]], tuple[str, bool]],
param: Union[str, list],
) -> Tuple[str]:
) -> tuple[str]:
cache_key = self.get_cache_key(key)

if self.no_cache:
Expand All @@ -776,21 +777,27 @@ def get_result(
##############################################
def autoremove(names, yes=False):
dead = list_dead(names)
if dead and (yes or confirm("Uninstall (y/N)? ")):
if dead and (yes or confirm("🙉 Uninstall (y/N)? ")):
remove_dists(dead)


def list_dead(names: list[str]):
start: set[Distribution] = set()
for name in names:
try:
print(f"get_distribution(name)={get_distribution(name)}")
start.add(get_distribution(name))
except _DistributionNotFound:
print("%s is not an installed pip module, skipping" % name, file=sys.stderr)
print(
Fore.RED
+ "🐙 「%s is not an installed pip module, skipping 」" % name
+ Style.RESET_ALL,
file=sys.stderr,
)
except _VersionConflict:
print(
"%s is not the currently installed version, skipping" % name,
Fore.RED
+ "🐙 「%s is not the currently installed version, skipping 」" % name
+ Style.RESET_ALL,
file=sys.stderr,
)

Expand All @@ -800,6 +807,7 @@ def list_dead(names: list[str]):
graph = get_graph()
dead = exclude_whitelist(find_all_dead(graph, start))
for d in start:
print("[🐬 ⬇️-Target And Its Dependencies-⬇️⎯🐬]")
show_tree(d, dead)
return dead

Expand All @@ -808,27 +816,29 @@ def exclude_whitelist(dists: list[Distribution]):
return {dist for dist in dists if dist.project_name not in WHITELIST}


def show_tree(dist: Distribution, dead, indent=0, visited=None):
def show_tree(dist: Distribution, dead, padding="", visited=None, last=False):
if visited is None:
visited = set()
if dist in visited:
return
visited.add(dist)

out_display = None

if indent == 0:
out_display = "."
elif indent > 1:
out_display = " " * indent * 2 + " │" + ("——" * indent)
if last: # 最后一个条目
prefix = "└─"
child_padding = padding + " "
else:
out_display = "│" + ("——" * indent * 2)
prefix = "├─"
child_padding = padding + "│ "

print(out_display, end="", file=sys.stderr)
print(f"{padding}{prefix}", end="", file=sys.stderr)
show_dist(dist)
for req in requires(dist, False):
if req in dead:
show_tree(req, dead, indent + 1, visited)

valid_req = [req for req in requires(dist, False) if req in dead]
for i, req in enumerate(valid_req):
if i == len(valid_req) - 1:
show_tree(req, dead, child_padding, visited, True)
else:
show_tree(req, dead, child_padding, visited)


def find_all_dead(graph, start):
Expand Down Expand Up @@ -858,18 +868,23 @@ def confirm(prompt: str):

def show_dist(dist: Distribution):
print(
"{} {} ({})".format(dist.project_name, dist.version, dist.location),
"{} {} {}".format(
Fore.RED + dist.project_name,
Fore.MAGENTA + dist.version,
Fore.GREEN + "(" + dist.location + ")" + Style.RESET_ALL,
),
file=sys.stderr,
)


def show_freeze(dist: Distribution):
print(dist.as_requirement())
print("🐳 ", dist.as_requirement())


def remove_dists(dists: list[Distribution]):
if sys.executable:
pip_cmd = [sys.executable, "-m", "pip"]
"""删除指定的包"""
if py_exe := get_python():
pip_cmd = [py_exe, "-m", "pip"]
else:
pip_cmd = ["pip"]
subprocess.check_call(
Expand All @@ -894,14 +909,18 @@ def requires(dist: Distribution, output=True):
required.append(get_distribution(pkg))
except _VersionConflict as e:
if output:
print("{} by {}".format(e.report(), dist.project_name), file=sys.stderr)
print("Redoing requirement with just package name...", file=sys.stderr)
print(
"🐡 {} by {}".format(e.report(), dist.project_name), file=sys.stderr
)
print(
"🐸 Redoing requirement with just package name...", file=sys.stderr
)
required.append(get_distribution(pkg.project_name))
except _DistributionNotFound as e:
if output:
print(e.report(), file=sys.stderr)
print("🐙", e.report(), file=sys.stderr)
print(
"%s is not installed, but required by %s, skipping"
"🐙 %s is not installed, but required by %s, skipping"
% (pkg.project_name, dist.project_name),
file=sys.stderr,
)
Expand All @@ -918,10 +937,16 @@ def remove_package_and_dependencies(args: "argparse.Namespace"):
list_leaves(args.freeze)
elif args.list:
dead = list_dead(args.pkg_name)
print(" ".join([d.project_name for d in dead]))

if dead:
print(
"🔴",
Fore.BLUE
+ " | ".join([d.project_name for d in dead])
+ Style.RESET_ALL,
)
else:
print("remove package.")
# autoremove(args.pkg_name, yes=args.yes)
autoremove(args.pkg_name, yes=args.yes)


def get_leaves(graph):
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ build-backend = "poetry.core.masonry.api"

[tool.isort]
profile = "black"

[tool.pyright]
reportGeneralTypeIssues = false

0 comments on commit b47de95

Please sign in to comment.