From 4b764060fcd11c93e092c41678cd85a19bcd8db4 Mon Sep 17 00:00:00 2001 From: torimus Date: Sat, 8 Sep 2018 12:37:30 +0200 Subject: [PATCH 01/20] Show default help instead of exception, when no arguments passed. --- pip_autoremove.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..2194fa0 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -102,6 +102,8 @@ def main(argv=None): list_leaves() elif opts.list: list_dead(args) + elif len(args) == 0: + parser.print_help() else: autoremove(args, yes=opts.yes) From 67370a34e2aae121dcf072ff9f0db52772aac31b Mon Sep 17 00:00:00 2001 From: torimus Date: Sat, 8 Sep 2018 12:46:26 +0200 Subject: [PATCH 02/20] Make pip call more robust, for mixed Py2 and Py3 environments. --- pip_autoremove.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 2194fa0..a00f751 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -80,7 +80,11 @@ def show_dist(dist): def remove_dist(dist): - subprocess.check_call(["pip", "uninstall", "-y", dist.project_name]) + if sys.executable: + pip_cmd = [sys.executable, '-m', 'pip'] + else: + pip_cmd = ['pip'] + subprocess.check_call(pip_cmd + ["uninstall", "-y", dist.project_name]) def get_graph(): From 8705e12ff8823ddcd7ae0e79cff890bb3fcac572 Mon Sep 17 00:00:00 2001 From: bmjoan Date: Sat, 8 Sep 2018 15:58:07 +0200 Subject: [PATCH 03/20] Show default help instead of exception, when no arguments are passed. --- pip_autoremove.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..2194fa0 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -102,6 +102,8 @@ def main(argv=None): list_leaves() elif opts.list: list_dead(args) + elif len(args) == 0: + parser.print_help() else: autoremove(args, yes=opts.yes) From 4942e9e22bfb66d9ac3c43efa1fd7186ed35b2a8 Mon Sep 17 00:00:00 2001 From: bmjoan Date: Sat, 8 Sep 2018 16:15:37 +0200 Subject: [PATCH 04/20] Make pip call more robust, for mixed Py2 and Py3 environments. --- pip_autoremove.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index a00f751..0ead99f 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -2,6 +2,7 @@ import optparse import subprocess +import sys from pkg_resources import working_set, get_distribution @@ -106,8 +107,6 @@ def main(argv=None): list_leaves() elif opts.list: list_dead(args) - elif len(args) == 0: - parser.print_help() else: autoremove(args, yes=opts.yes) From 8b806848a00195176fcac1e163ffe5a536d94fbc Mon Sep 17 00:00:00 2001 From: Cristian Morataya Date: Mon, 27 May 2019 08:52:53 -0600 Subject: [PATCH 05/20] Add space for visual comfort --- pip_autoremove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..8e96b19 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -19,7 +19,7 @@ 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)? ")): for d in dead: remove_dist(d) From 5a95e9ed695cc59e7babf2fbe62013fb463c54ab Mon Sep 17 00:00:00 2001 From: Luca Trevisani Date: Tue, 4 Feb 2020 23:52:05 +0100 Subject: [PATCH 06/20] Call `pip` only once --- pip_autoremove.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..3efe08c 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -20,8 +20,7 @@ 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) + remove_dists(dead) def list_dead(names): @@ -79,8 +78,8 @@ def show_dist(dist): 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 remove_dists(dists): + subprocess.check_call(["pip", "uninstall", "-y"] + [d.project_name for d in dists]) def get_graph(): From e71f3cefe0bfdd03e27fc96bd3f0d2e7ce394f20 Mon Sep 17 00:00:00 2001 From: Oren Shklarsky Date: Tue, 24 Apr 2018 13:31:36 -0700 Subject: [PATCH 07/20] add py36 --- pip_autoremove.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..adb7ba4 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -13,6 +13,15 @@ except NameError: raw_input = input +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 main, logger + pip.main = main + pip.logger = logger +except ModuleNotFoundError: + pass + WHITELIST = ['pip', 'setuptools'] From 4d088d84250556455020e29fa65c50d47f4abab4 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Tue, 1 May 2018 10:27:28 -0600 Subject: [PATCH 08/20] Fixing VersionConflict and DistributionNotFound exceptions These are due to packages being upgraded beyond what some depending package specifies (e.g. if you tend to do `pip list --outdated | xargs pip install --upgrade` or anything similar.) To deal with this, we ignore version requirement if VersionConflict. Not sure how we can end up in a DistributionNotFound scenario, but plenty of bug reports about it, so just ignoring those outright. Fixes invl#7, invl#9, invl#10. --- pip_autoremove.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index adb7ba4..ce0718a 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -3,7 +3,8 @@ import optparse import subprocess -from pkg_resources import working_set, get_distribution +import pip +from pkg_resources import working_set, get_distribution, VersionConflict, DistributionNotFound __version__ = '0.9.1' @@ -101,7 +102,18 @@ def get_graph(): 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()) + print("Redoing requirement with just package name...") + required.append(get_distribution(pkg.project_name)) + except DistributionNotFound as e: + print(e.report()) + print("Skipping %s", pkg.project_name) + return required def main(argv=None): From 116bcacc2a9d7818e8426e27d48a9535b89e51a6 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Thu, 3 May 2018 14:25:01 -0600 Subject: [PATCH 09/20] Catch addition exceptions --- pip_autoremove.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index ce0718a..5114059 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -35,7 +35,14 @@ def autoremove(names, yes=False): 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) + except VersionConflict: + print("%s is not the currently installed version, skipping" % name) graph = get_graph() dead = exclude_whitelist(find_all_dead(graph, start)) for d in start: @@ -112,7 +119,7 @@ def requires(dist): required.append(get_distribution(pkg.project_name)) except DistributionNotFound as e: print(e.report()) - print("Skipping %s", pkg.project_name) + print("Skipping %s" % pkg.project_name) return required From 5b3cf178678c16b78d0d489066e4dd955d21e535 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Thu, 3 May 2018 11:33:52 -0600 Subject: [PATCH 10/20] Allow `pip freeze` style output Use `--freeze` option on its own or with `--list`. Fixes invl#12 --- pip_autoremove.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 5114059..2bd204e 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -96,6 +96,10 @@ def show_dist(dist): print('%s %s (%s)' % (dist.project_name, dist.version, dist.location)) +def show_freeze(dist): + print(dist.as_requirement()) + + def remove_dist(dist): subprocess.check_call(["pip", "uninstall", "-y", dist.project_name]) @@ -126,8 +130,8 @@ def requires(dist): 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) else: @@ -142,10 +146,13 @@ def is_leaf(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(): @@ -162,6 +169,9 @@ def create_parser(): parser.add_option( '-y', '--yes', action='store_true', default=False, help="don't ask for confirmation of uninstall deletions.") + parser.add_option( + '-f', '--freeze', action='store_true', default=False, + help="list leaves (packages which are not used by any others) in requirements.txt format") return parser From 99cec06215550452963b9a5efbea96954cd82407 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Thu, 3 May 2018 15:47:42 -0600 Subject: [PATCH 11/20] Error messages to stderr --- pip_autoremove.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 2bd204e..d249c3d 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -40,9 +40,9 @@ def list_dead(names): try: start.add(get_distribution(name)) except DistributionNotFound: - print("%s is not an installed pip module, skipping" % name) + 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) + 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: @@ -118,12 +118,12 @@ def requires(dist): try: required.append(get_distribution(pkg)) except VersionConflict as e: - print(e.report()) - print("Redoing requirement with just package name...") + 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()) - print("Skipping %s" % pkg.project_name) + print(e.report(), file=sys.stderr) + print("Skipping %s" % pkg.project_name, file=sys.stderr) return required From 00a3433c4a1ed1b73c59a843c430749d49e409c5 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Sat, 11 Apr 2020 16:36:31 -0600 Subject: [PATCH 12/20] Fix import issues Add py37 to tox --- pip_autoremove.py | 8 +++++++- tox.ini | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index d249c3d..985241f 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -2,6 +2,7 @@ import optparse import subprocess +import sys import pip from pkg_resources import working_set, get_distribution, VersionConflict, DistributionNotFound @@ -14,13 +15,18 @@ 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 main, logger pip.main = main pip.logger = logger -except ModuleNotFoundError: +except (ModuleNotFoundError, ImportError): pass diff --git a/tox.ini b/tox.ini index 6342633..a05dd1a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py26, py34, py36, pypy +envlist = py27, py26, py34, py36, py37, pypy [testenv] commands = py.test From 07bb6f43791bbcd8af7953dd0ef7d84407904336 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Sat, 11 Apr 2020 17:15:15 -0600 Subject: [PATCH 13/20] Fix merge commit breaking sys.executable --- pip_autoremove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index cf928f8..0def4fb 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -110,7 +110,7 @@ def remove_dists(dists): pip_cmd = [sys.executable, '-m', 'pip'] else: pip_cmd = ['pip'] - subprocess.check_call(["pip", "uninstall", "-y"] + [d.project_name for d in dists]) + subprocess.check_call(pip_cmd + ["uninstall", "-y"] + [d.project_name for d in dists]) def get_graph(): From 5afd272a847cdeb9d485dc330c2f8f50f3e1e2b2 Mon Sep 17 00:00:00 2001 From: ThatXliner <66848002+ThatXliner@users.noreply.github.com> Date: Thu, 22 Oct 2020 17:41:51 -0700 Subject: [PATCH 14/20] Add pip-autoremove and wheel to whitelist Because we need wheel --- pip_autoremove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index 9933c5f..e90aa9b 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -14,7 +14,7 @@ raw_input = input -WHITELIST = ['pip', 'setuptools'] +WHITELIST = ['pip', 'setuptools', "pip-autoremove", "wheel"] def autoremove(names, yes=False): From cd91465d8b69ab317a0d39811af2ddd3785b7ceb Mon Sep 17 00:00:00 2001 From: Eling Pramuatmaja Date: Thu, 6 May 2021 01:20:56 +0800 Subject: [PATCH 15/20] update tox to test on py38 and py39 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a05dd1a..8003d6f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py26, py34, py36, py37, pypy +envlist = py27, py26, py34, py36, py37, py38, py39, pypy [testenv] commands = py.test From 0ea93e3caf40882686991165bac1b46e928a7135 Mon Sep 17 00:00:00 2001 From: Eling Pramuatmaja Date: Thu, 6 May 2021 01:39:40 +0800 Subject: [PATCH 16/20] format with black --- pip_autoremove.py | 71 ++++++++++++++++++++++++++++-------------- setup.py | 22 ++++++------- test_pip_autoremove.py | 21 ++++++------- tox.ini | 2 +- 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/pip_autoremove.py b/pip_autoremove.py index c6ff7b4..e90bc26 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -5,10 +5,15 @@ import sys import pip -from pkg_resources import working_set, get_distribution, VersionConflict, DistributionNotFound +from pkg_resources import ( + working_set, + get_distribution, + VersionConflict, + DistributionNotFound, +) -__version__ = '0.9.1' +__version__ = "0.9.1" try: raw_input @@ -24,13 +29,14 @@ # 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 main, logger + pip.main = main pip.logger = logger except (ModuleNotFoundError, ImportError): pass -WHITELIST = ['pip', 'setuptools', "pip-autoremove", "wheel"] +WHITELIST = ["pip", "setuptools", "pip-autoremove", "wheel"] def autoremove(names, yes=False): @@ -47,7 +53,10 @@ def list_dead(names): 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) + 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: @@ -65,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: @@ -77,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) @@ -94,11 +102,11 @@ 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 show_freeze(dist): @@ -107,10 +115,12 @@ def show_freeze(dist): def remove_dists(dists): if sys.executable: - pip_cmd = [sys.executable, '-m', 'pip'] + pip_cmd = [sys.executable, "-m", "pip"] else: - pip_cmd = ['pip'] - subprocess.check_call(pip_cmd + ["uninstall", "-y"] + [d.project_name for d in dists]) + pip_cmd = ["pip"] + subprocess.check_call( + pip_cmd + ["uninstall", "-y"] + [d.project_name for d in dists] + ) def get_graph(): @@ -150,7 +160,6 @@ def main(argv=None): def get_leaves(graph): - def is_leaf(node): return not graph[node] @@ -168,23 +177,39 @@ def list_leaves(freeze=False): 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.") + "-l", + "--list", + action="store_true", + default=False, + help="list unused dependencies, but don't uninstall them.", + ) parser.add_option( - '-L', '--leaves', action='store_true', default=False, - help="list leaves (packages which are not used by any others).") + "-L", + "--leaves", + action="store_true", + default=False, + help="list leaves (packages which are not used by any others).", + ) parser.add_option( - '-y', '--yes', action='store_true', default=False, - help="don't ask for confirmation of uninstall deletions.") + "-y", + "--yes", + action="store_true", + default=False, + help="don't ask for confirmation of uninstall deletions.", + ) parser.add_option( - '-f', '--freeze', action='store_true', default=False, - help="list leaves (packages which are not used by any others) in requirements.txt format") + "-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() diff --git a/setup.py b/setup.py index 0fe4e70..64492e6 100644 --- a/setup.py +++ b/setup.py @@ -7,22 +7,22 @@ name="pip-autoremove", version=pip_autoremove.__version__, description="Remove a package and its unused dependencies", - long_description=open('README.rst').read(), + long_description=open("README.rst").read(), py_modules=["pip_autoremove"], - license='Apache License 2.0', - url='https://github.com/invl/pip-autoremove', + license="Apache License 2.0", + url="https://github.com/invl/pip-autoremove", entry_points=""" [console_scripts] pip-autoremove = pip_autoremove:main """, 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', + "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", - ] + ], ) diff --git a/test_pip_autoremove.py b/test_pip_autoremove.py index 2af7d43..7d797fa 100644 --- a/test_pip_autoremove.py +++ b/test_pip_autoremove.py @@ -6,17 +6,16 @@ def test_find_all_dead(): graph = { - 'Flask': set([]), - 'Jinja2': set(['Flask']), - 'MarkupSafe': set(['Jinja2']), - 'Werkzeug': set(['Flask']), - 'itsdangerous': set(['Flask']), - 'pip': set([]), - 'setuptools': set([]), + "Flask": set([]), + "Jinja2": set(["Flask"]), + "MarkupSafe": set(["Jinja2"]), + "Werkzeug": set(["Flask"]), + "itsdangerous": set(["Flask"]), + "pip": set([]), + "setuptools": set([]), } start = set(["Flask"]) - expected = set( - ["Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"]) + expected = set(["Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"]) dead = pip_autoremove.find_all_dead(graph, start) assert dead == expected @@ -34,10 +33,10 @@ def has_dist(req): def test_main(): expected = ["Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"] - install_dist('Flask') + install_dist("Flask") for name in expected: assert has_dist(name) - pip_autoremove.main(['-y', 'Flask']) + pip_autoremove.main(["-y", "Flask"]) for name in expected: assert not has_dist(name) diff --git a/tox.ini b/tox.ini index 8003d6f..af50a08 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py26, py34, py36, py37, py38, py39, pypy +envlist = py26, py27, py34, py36, py37, py38, py39, pypy [testenv] commands = py.test From 5e5f256d4885e66e3d2c6d1d40b6e8ff7c08b4d4 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Sun, 12 Sep 2021 18:34:10 -0600 Subject: [PATCH 17/20] Trying Github actions --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++++++++ tox.ini | 18 +++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f08dea9 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/tox.ini b/tox.ini index af50a08..35dddd5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,24 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] -envlist = py26, py27, py34, py36, py37, py38, py39, pypy +envlist = 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 = py.test +commands = pytest deps = pytest From e591a86cdc47a69a3b66242edc0253fa3f22ea57 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Mon, 13 Sep 2021 00:42:57 -0600 Subject: [PATCH 18/20] Move to pyproject.toml/setup.cfg --- tox.ini => pyproject.toml | 10 ++++++++++ setup.cfg | 27 +++++++++++++++++++++++++++ setup.py | 27 +-------------------------- 3 files changed, 38 insertions(+), 26 deletions(-) rename tox.ini => pyproject.toml (78%) diff --git a/tox.ini b/pyproject.toml similarity index 78% rename from tox.ini rename to pyproject.toml index 35dddd5..b5f8625 100644 --- a/tox.ini +++ b/pyproject.toml @@ -1,3 +1,12 @@ +[build-system] +requires = [ + "setuptools >= 40.9.0", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.tox] +legacy_tox_ini = """ # Tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" @@ -22,3 +31,4 @@ python = commands = pytest deps = pytest +""" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 7c964b4..f841f11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,29 @@ +[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] +#packages = find: +scripts = + pip_autoremove.py + +[options.entry_points] +console_scripts = + pip-autoremove = pip_autoremove:main + [wheel] universal=1 diff --git a/setup.py b/setup.py index 64492e6..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,3 @@ from setuptools import setup -import pip_autoremove - - -setup( - name="pip-autoremove", - version=pip_autoremove.__version__, - description="Remove a package and its unused dependencies", - long_description=open("README.rst").read(), - py_modules=["pip_autoremove"], - license="Apache License 2.0", - url="https://github.com/invl/pip-autoremove", - entry_points=""" - [console_scripts] - pip-autoremove = pip_autoremove:main - """, - 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", - ], -) +setup() From 659f06e8c15103bcd41bcd2d0be1a53d97a49221 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Mon, 13 Sep 2021 00:46:48 -0600 Subject: [PATCH 19/20] cleanup with pflake8, black, and friends --- .gitignore | 7 +++++++ pip_autoremove.py | 21 +++++++++---------- pyproject.toml | 46 ++++++++++++++++++++++++++++++++++++------ setup.cfg | 4 +++- test_pip_autoremove.py | 21 ++++++++++--------- 5 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..909ae9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +/.tox +build/ +dist/ +*.egg-info/ +.vscode diff --git a/pip_autoremove.py b/pip_autoremove.py index e90bc26..6fb80ad 100644 --- a/pip_autoremove.py +++ b/pip_autoremove.py @@ -3,17 +3,17 @@ import optparse import subprocess import sys +from collections import defaultdict import pip from pkg_resources import ( - working_set, - get_distribution, - VersionConflict, DistributionNotFound, + VersionConflict, + get_distribution, + working_set, ) - -__version__ = "0.9.1" +__version__ = "0.10.0" try: raw_input @@ -28,7 +28,7 @@ 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 main, logger + from pip._internal import logger, main pip.main = main pip.logger = logger @@ -65,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): @@ -118,14 +118,13 @@ def remove_dists(dists): pip_cmd = [sys.executable, "-m", "pip"] else: pip_cmd = ["pip"] - subprocess.check_call( - pip_cmd + ["uninstall", "-y"] + [d.project_name for d in dists] - ) + 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 diff --git a/pyproject.toml b/pyproject.toml index b5f8625..4c0f780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,15 +5,20 @@ requires = [ ] 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 (https://tox.readthedocs.io/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - [tox] -envlist = py26, py27, py36, py37, py38, py39, pypy2, pypy36, pypy37 +envlist = py39-flake8, py27-flake8, py39-black, py26, py27, py36, py37, py38, py39, pypy2, pypy36, pypy37 [gh-actions] python = @@ -31,4 +36,33 @@ python = 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 """ \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index f841f11..1d106bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,9 +17,11 @@ classifiers = data_files = pyproject.toml [options] -#packages = find: scripts = pip_autoremove.py +install_requires = + pip + setuptools [options.entry_points] console_scripts = diff --git a/test_pip_autoremove.py b/test_pip_autoremove.py index 7d797fa..2bed85a 100644 --- a/test_pip_autoremove.py +++ b/test_pip_autoremove.py @@ -1,21 +1,22 @@ -import pkg_resources import subprocess +import pkg_resources + import pip_autoremove def test_find_all_dead(): graph = { - "Flask": set([]), - "Jinja2": set(["Flask"]), - "MarkupSafe": set(["Jinja2"]), - "Werkzeug": set(["Flask"]), - "itsdangerous": set(["Flask"]), - "pip": set([]), - "setuptools": set([]), + "Flask": set(), + "Jinja2": {"Flask"}, + "MarkupSafe": {"Jinja2"}, + "Werkzeug": {"Flask"}, + "itsdangerous": {"Flask"}, + "pip": set(), + "setuptools": set(), } - start = set(["Flask"]) - expected = set(["Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"]) + start = {"Flask"} + expected = {"Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"} dead = pip_autoremove.find_all_dead(graph, start) assert dead == expected From 08e73d08b306ea88367977bb6e4fbfc1ea2f26c7 Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Mon, 13 Sep 2021 02:44:47 -0600 Subject: [PATCH 20/20] Slightly better test case --- test_pip_autoremove.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test_pip_autoremove.py b/test_pip_autoremove.py index 2bed85a..6f5843f 100644 --- a/test_pip_autoremove.py +++ b/test_pip_autoremove.py @@ -8,18 +8,29 @@ def test_find_all_dead(): graph = { "Flask": set(), - "Jinja2": {"Flask"}, + "Jinja2": {"Flask", "Sphinx"}, "MarkupSafe": {"Jinja2"}, "Werkzeug": {"Flask"}, "itsdangerous": {"Flask"}, + "Sphinx": set(), "pip": set(), "setuptools": set(), } start = {"Flask"} - expected = {"Flask", "Jinja2", "MarkupSafe", "Werkzeug", "itsdangerous"} + expected = {"Flask", "Werkzeug", "itsdangerous"} dead = pip_autoremove.find_all_dead(graph, start) assert dead == expected + start = {"Sphinx"} + dead = pip_autoremove.find_all_dead(graph, start) + assert dead == start + + start = {"Sphinx", "Flask"} + expected = {"Flask", "Werkzeug", "itsdangerous", "Sphinx", "Jinja2", "MarkupSafe"} + dead = pip_autoremove.find_all_dead(graph, start) + print(dead) + assert dead == expected + def install_dist(req): subprocess.check_call(["pip", "install", req])