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

WIP accept manifest instead of path #17

Merged
merged 2 commits into from
Dec 9, 2023
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 📦 0.4.0

* Add ability to search a requirements file.
It is now possible to invoke `pip-abandoned search -r requirements.txt`

## 📦 0.3.2

* Tested on python 3.12
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ Some package registries like NPM and Packagist allow a user to mark a package as

## Usage

An example invocation of `pip-abandoned` looks like:

```bash
# Search a virtualenv path:
pip-abandoned search /home/alice/.virtualenvs/myproject/lib/python3.10/site-packages
```

```bash
# Search a requirements file:
pip-abandoned search -r /path/to/requirements.txt
```

When searching one or more requirements files, your packages will be installed into a temporary virtualenv. This means this search will include transitive dependencies.

## Exit Codes

`pip-abandoned search` exits with
Expand Down
45 changes: 39 additions & 6 deletions pip_abandoned/cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import argparse
import textwrap
from pathlib import Path

from . import lib
from .__version__ import __version__


def get_parser():
parser = argparse.ArgumentParser(
description="Search for abandoned and deprecated python packages"
description="Search for abandoned and deprecated python packages",
)
parser.add_argument(
"--version", action="version", version=f"%(prog)s {__version__}"
Expand All @@ -16,13 +18,35 @@ def get_parser():
)

search = subparsers.add_parser(
"search", help="Search for abandoned and deprecated python packages"
"search",
help="Search for abandoned and deprecated python packages",
epilog=textwrap.dedent(
"""\
Examples:
pip-abandoned search myproject/lib/python3.10/site-packages
pip-abandoned search -r requirements.txt
"""
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
search.add_argument(

dep_source_args = search.add_mutually_exclusive_group()
dep_source_args.add_argument(
"path",
type=str,
type=Path,
nargs="?",
help="Path to a virtualenv to search",
)
dep_source_args.add_argument(
"-r",
"--requirement",
type=argparse.FileType("r"),
metavar="REQUIREMENT",
action="append",
dest="requirements",
help="Install packages from the given requirements file into a temporary virtualenv. Then search that virtualenv. This option can be used multiple times.",
)

search.add_argument(
"-v",
"--verbose",
Expand All @@ -49,8 +73,17 @@ def cli():

args = parser.parse_args()

if args.subcommand == "search":
return lib.search(lib.get_token(), args.path, args.verbose, args.format)
if args.subcommand == "search" and args.path:
return lib.search_virtualenv_path(
lib.get_token(), args.path, args.verbose, args.format
)
elif args.subcommand == "search" and args.requirements:
return lib.search_requirements_files(
lib.get_token(),
[Path(req.name) for req in args.requirements],
args.verbose,
args.format,
)
elif args.subcommand == "set-token":
return lib.set_token()
else:
Expand Down
39 changes: 38 additions & 1 deletion pip_abandoned/lib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import json
import logging
import os
import subprocess
import sys
import venv
from pathlib import Path
from tempfile import TemporaryDirectory
from urllib.parse import urlparse, urlunparse

import keyring
Expand Down Expand Up @@ -62,6 +66,25 @@ def set_log_level(verbosity):
logger.setLevel(logging.DEBUG)


def get_python_version():
return f"python{sys.version_info.major}.{sys.version_info.minor}"


def create_temp_virtualenv(directory):
builder = venv.EnvBuilder(
system_site_packages=False,
clear=True,
symlinks=False,
upgrade=False,
with_pip=True,
)
builder.create(directory)

site_packages = Path(directory) / "lib" / get_python_version() / "site-packages"

return site_packages


def github_repo_url_or_none(url):
if url:
parsed_url = urlparse(url)
Expand Down Expand Up @@ -239,7 +262,7 @@ def output_json(inactive, unmaintained, archived):
)


def search(gh_token, path, verbosity, format_="text"):
def search_virtualenv_path(gh_token, path, verbosity, format_="text"):
set_log_level(verbosity)

dists = list(distributions(path=[path]))
Expand Down Expand Up @@ -274,3 +297,17 @@ def search(gh_token, path, verbosity, format_="text"):
):
return 0
return 9


def search_requirements_files(gh_token, requirements, verbosity, format_="text"):
with TemporaryDirectory() as tempdir:
site_packages = create_temp_virtualenv(tempdir)

command = [Path(tempdir) / "bin" / "pip", "install"]
for reqs in requirements:
command.append("-r")
command.append(reqs.absolute())

subprocess.run(command, capture_output=True)

return search_virtualenv_path(gh_token, site_packages, verbosity, format_)
1 change: 1 addition & 0 deletions tests/fixture_data/reqs-fail.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
commonmark
1 change: 1 addition & 0 deletions tests/fixture_data/reqs-pass.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
django
35 changes: 35 additions & 0 deletions tests/test_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest

from pip_abandoned.cli import get_parser


def test_virtualenv_path():
parser = get_parser()
args = parser.parse_args(["search", "foo/bar"])
assert str(args.path) == "foo/bar"
assert args.requirements is None


def test_one_requirements_file():
parser = get_parser()
file_ = "./tests/fixture_data/reqs-pass.txt"
args = parser.parse_args(["search", "-r", file_])
assert [r.name for r in args.requirements] == [file_]
assert args.path is None


def test_multiple_requirements_files():
parser = get_parser()
file1 = "./tests/fixture_data/reqs-pass.txt"
file2 = "./tests/fixture_data/reqs-fail.txt"
args = parser.parse_args(["search", "-r", file1, "-r", file2])
assert [r.name for r in args.requirements] == [file1, file2]
assert args.path is None


def test_invalid_combination():
parser = get_parser()
with pytest.raises(SystemExit):
parser.parse_args(
["search", "-r", "./tests/fixture_data/reqs-pass.txt", "foo/bar"]
)
70 changes: 43 additions & 27 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import re
import subprocess
import sys
import venv
from pathlib import Path
from tempfile import TemporaryDirectory

from pip_abandoned.cli import get_parser
from pip_abandoned.lib import create_temp_virtualenv

"""
Tests in this file call out to real services (PyPI, GitHub) without mocking
Expand All @@ -15,8 +14,8 @@
"""


def get_python_version():
return f"python{sys.version_info.major}.{sys.version_info.minor}"
def get_requirements_fixture(name):
return Path(".") / "tests" / "fixture_data" / name


def test_help():
Expand All @@ -33,44 +32,25 @@ def test_version():
assert re.match(r"pip-abandoned \d+.\d+.\d+", result.stdout.decode("utf-8").strip())


def test_search_pass():
def test_search_virtualenv_path_pass():
with TemporaryDirectory() as tempdir:
# create a temp virtualenv
builder = venv.EnvBuilder(
system_site_packages=False,
clear=True,
symlinks=False,
upgrade=False,
with_pip=True,
)
builder.create(tempdir)

site_packages = Path(tempdir) / "lib" / get_python_version() / "site-packages"
site_packages = create_temp_virtualenv(tempdir)
result = subprocess.run(
["pip-abandoned", "search", site_packages], capture_output=True
)
assert result.returncode == 0


def test_search_fail():
def test_search_virtualenv_path_fail():
with TemporaryDirectory() as tempdir:
# create a temp virtualenv
builder = venv.EnvBuilder(
system_site_packages=False,
clear=True,
symlinks=False,
upgrade=False,
with_pip=True,
)
builder.create(tempdir)
site_packages = create_temp_virtualenv(tempdir)

# install a known abandoned package into the env
subprocess.run(
[Path(tempdir) / "bin" / "pip", "install", "commonmark"],
capture_output=True,
)

site_packages = Path(tempdir) / "lib" / get_python_version() / "site-packages"
result = subprocess.run(
["pip-abandoned", "search", site_packages], capture_output=True
)
Expand All @@ -79,3 +59,39 @@ def test_search_fail():
in result.stdout
)
assert result.returncode == 9


def test_search_requirements_files_pass():
result = subprocess.run(
["pip-abandoned", "search", "-r", get_requirements_fixture("reqs-pass.txt")],
capture_output=True,
)
assert result.returncode == 0


def test_search_requirements_files_fail():
result = subprocess.run(
["pip-abandoned", "search", "-r", get_requirements_fixture("reqs-fail.txt")],
capture_output=True,
)
assert (
b"Packages associated with archived GitHub repos were found:" in result.stdout
)
assert result.returncode == 9


def test_search_requirements_files_multiple_files():
result = subprocess.run(
[
"pip-abandoned",
"search",
"-r",
get_requirements_fixture("reqs-pass.txt"),
"-r",
get_requirements_fixture("reqs-fail.txt"),
"-vv",
],
capture_output=True,
)
assert b"django" in result.stderr
assert b"commonmark" in result.stderr
Loading