Skip to content
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

PR to bring pip-autoremove up-to-date #42

Merged
merged 26 commits into from
Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b76406
Show default help instead of exception, when no arguments passed.
Sep 8, 2018
67370a3
Make pip call more robust, for mixed Py2 and Py3 environments.
Sep 8, 2018
8705e12
Show default help instead of exception, when no arguments are passed.
Sep 8, 2018
4942e9e
Make pip call more robust, for mixed Py2 and Py3 environments.
Sep 8, 2018
8b80684
Add space for visual comfort
ccmorataya May 27, 2019
5a95e9e
Call `pip` only once
lucatrv Feb 4, 2020
e71f3ce
add py36
Apr 24, 2018
4d088d8
Fixing VersionConflict and DistributionNotFound exceptions
tresni May 1, 2018
116bcac
Catch addition exceptions
tresni May 3, 2018
5b3cf17
Allow `pip freeze` style output
tresni May 3, 2018
99cec06
Error messages to stderr
tresni May 3, 2018
00a3433
Fix import issues
tresni Apr 11, 2020
54a8d77
Merge pull request #25 from ccmorataya/patch-1
tresni Apr 11, 2020
1a468ac
Merge pull request #22 from bmjoan/robust_pip
tresni Apr 11, 2020
e4c4318
Merge pull request #21 from bmjoan/def_usage
tresni Apr 11, 2020
04fae1a
Merge pull request #28 from lucatrv/patch-1
tresni Apr 11, 2020
07bb6f4
Fix merge commit breaking sys.executable
tresni Apr 11, 2020
5afd272
Add pip-autoremove and wheel to whitelist
ThatXliner Oct 23, 2020
cd91465
update tox to test on py38 and py39
elingp May 5, 2021
6133c02
Merge pull request invl#33 from ThatXliner/patch-1
elingp May 5, 2021
0ea93e3
format with black
elingp May 5, 2021
0bb01e4
Merge pull request #1 from elingp/dev
tresni Sep 13, 2021
5e5f256
Trying Github actions
tresni Sep 13, 2021
e591a86
Move to pyproject.toml/setup.cfg
tresni Sep 13, 2021
659f06e
cleanup with pflake8, black, and friends
tresni Sep 13, 2021
08e73d0
Slightly better test case
tresni Sep 13, 2021
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
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Python application

on:
- push
- pull_request
# push:
# branches: [ dev ]
# pull_request:
# branches: [ dev ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: |
tox
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
/.tox
build/
dist/
*.egg-info/
.vscode
135 changes: 104 additions & 31 deletions pip_autoremove.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,61 @@

import optparse
import subprocess
import sys
from collections import defaultdict

from pkg_resources import working_set, get_distribution
import pip
from pkg_resources import (
DistributionNotFound,
VersionConflict,
get_distribution,
working_set,
)


__version__ = '0.9.1'
__version__ = "0.10.0"

try:
raw_input
except NameError:
raw_input = input

try:
ModuleNotFoundError
except NameError:
ModuleNotFoundError = ImportError

try:
# pip >= 10.0.0 hides main in pip._internal. We'll monkey patch what we need and hopefully this becomes available
# at some point.
from pip._internal import logger, main

WHITELIST = ['pip', 'setuptools']
pip.main = main
pip.logger = logger
except (ModuleNotFoundError, ImportError):
pass


WHITELIST = ["pip", "setuptools", "pip-autoremove", "wheel"]


def autoremove(names, yes=False):
dead = list_dead(names)
if dead and (yes or confirm("Uninstall (y/N)?")):
for d in dead:
remove_dist(d)
if dead and (yes or confirm("Uninstall (y/N)? ")):
remove_dists(dead)


def list_dead(names):
start = set(map(get_distribution, names))
start = set()
for name in names:
try:
start.add(get_distribution(name))
except DistributionNotFound:
print("%s is not an installed pip module, skipping" % name, file=sys.stderr)
except VersionConflict:
print(
"%s is not the currently installed version, skipping" % name,
file=sys.stderr,
)
graph = get_graph()
dead = exclude_whitelist(find_all_dead(graph, start))
for d in start:
Expand All @@ -34,7 +65,7 @@ def list_dead(names):


def exclude_whitelist(dists):
return set(dist for dist in dists if dist.project_name not in WHITELIST)
return {dist for dist in dists if dist.project_name not in WHITELIST}


def show_tree(dist, dead, indent=0, visited=None):
Expand All @@ -43,7 +74,7 @@ def show_tree(dist, dead, indent=0, visited=None):
if dist in visited:
return
visited.add(dist)
print(' ' * 4 * indent, end='')
print(" " * 4 * indent, end="")
show_dist(dist)
for req in requires(dist):
if req in dead:
Expand All @@ -55,7 +86,6 @@ def find_all_dead(graph, start):


def find_dead(graph, dead):

def is_killed_by_us(node):
succ = graph[node]
return succ and not (succ - dead)
Expand All @@ -72,70 +102,113 @@ def fixed_point(f, x):


def confirm(prompt):
return raw_input(prompt) == 'y'
return raw_input(prompt) == "y"


def show_dist(dist):
print('%s %s (%s)' % (dist.project_name, dist.version, dist.location))
print("%s %s (%s)" % (dist.project_name, dist.version, dist.location))


def remove_dist(dist):
subprocess.check_call(["pip", "uninstall", "-y", dist.project_name])
def show_freeze(dist):
print(dist.as_requirement())


def remove_dists(dists):
if sys.executable:
pip_cmd = [sys.executable, "-m", "pip"]
else:
pip_cmd = ["pip"]
subprocess.check_call(pip_cmd + ["uninstall", "-y"] + [d.project_name for d in dists])


def get_graph():
g = dict((dist, set()) for dist in working_set)
g = defaultdict(set)
for dist in working_set:
g[dist]
for req in requires(dist):
g[req].add(dist)
return g


def requires(dist):
return map(get_distribution, dist.requires())
required = []
for pkg in dist.requires():
try:
required.append(get_distribution(pkg))
except VersionConflict as e:
print(e.report(), file=sys.stderr)
print("Redoing requirement with just package name...", file=sys.stderr)
required.append(get_distribution(pkg.project_name))
except DistributionNotFound as e:
print(e.report(), file=sys.stderr)
print("Skipping %s" % pkg.project_name, file=sys.stderr)
return required


def main(argv=None):
parser = create_parser()
(opts, args) = parser.parse_args(argv)
if opts.leaves:
list_leaves()
if opts.leaves or opts.freeze:
list_leaves(opts.freeze)
elif opts.list:
list_dead(args)
elif len(args) == 0:
parser.print_help()
else:
autoremove(args, yes=opts.yes)


def get_leaves(graph):

def is_leaf(node):
return not graph[node]

return filter(is_leaf, graph)


def list_leaves():
def list_leaves(freeze=False):
graph = get_graph()
for node in get_leaves(graph):
show_dist(node)
if freeze:
show_freeze(node)
else:
show_dist(node)


def create_parser():
parser = optparse.OptionParser(
usage='usage: %prog [OPTION]... [NAME]...',
version='%prog ' + __version__,
usage="usage: %prog [OPTION]... [NAME]...",
version="%prog " + __version__,
)
parser.add_option(
"-l",
"--list",
action="store_true",
default=False,
help="list unused dependencies, but don't uninstall them.",
)
parser.add_option(
'-l', '--list', action='store_true', default=False,
help="list unused dependencies, but don't uninstall them.")
"-L",
"--leaves",
action="store_true",
default=False,
help="list leaves (packages which are not used by any others).",
)
parser.add_option(
'-L', '--leaves', action='store_true', default=False,
help="list leaves (packages which are not used by any others).")
"-y",
"--yes",
action="store_true",
default=False,
help="don't ask for confirmation of uninstall deletions.",
)
parser.add_option(
'-y', '--yes', action='store_true', default=False,
help="don't ask for confirmation of uninstall deletions.")
"-f",
"--freeze",
action="store_true",
default=False,
help="list leaves (packages which are not used by any others) in requirements.txt format",
)
return parser


if __name__ == '__main__':
if __name__ == "__main__":
main()
68 changes: 68 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[build-system]
requires = [
"setuptools >= 40.9.0",
"wheel",
]
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 120

[tool.flake8]
max-line-length = 120
extend-ignore = "SFS1,B014"

[tool.isort]
profile = "black"

[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py39-flake8, py27-flake8, py39-black, py26, py27, py36, py37, py38, py39, pypy2, pypy36, pypy37

[gh-actions]
python =
2.6: py26
2.7: py27
3.6: py36
3.7: py37
3.8: py38
3.9: py39
pypy-2.7: pypy2
pypy-3.6: pypy36
pypy-3.7: pypy37

[testenv]
commands = pytest
deps =
pytest

# Very basic syntax checking for py27
[testenv:py27-flake8]
commands =
flake8 --extend-ignore=E501
deps =
flake8

[testenv:py39-flake8]
commands =
pflake8
deps =
pyproject-flake8
pep8-naming
flake8-broken-line
flake8-bugbear
flake8-commas
flake8-comprehensions
flake8-eradicate
flake8-fixme
flake8-isort
flake8-sfs
flake8-type-annotations

[testenv:py39-black]
commands =
black --check --diff .
deps =
black
"""
29 changes: 29 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,31 @@
[metadata]
name = pip-autoremove
version = attr: pip_autoremove.__version__
description = Remove a package and its unused dependencies
long_description = file: README.rst, LICENSE
url = https://github.com/invl/pip-autoremove
license = Apache License 2.0
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: Implementation :: PyPy
data_files = pyproject.toml

[options]
scripts =
pip_autoremove.py
install_requires =
pip
setuptools

[options.entry_points]
console_scripts =
pip-autoremove = pip_autoremove:main

[wheel]
universal=1
Loading