Created
March 15, 2022 18:54
-
-
Save awbacker/504919b146ef5566b816d002facb3083 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# ------------------------------------------------------- | |
# Place this somewhere in your path, and make it executable. | |
# Inside any venv, run `outdated.py path/to/requirements.txt` | |
# By default it will attempt to read ~/requirements.txt | |
# | |
# $> cd project && source .venv/bin/activate | |
# $> outdated.py requirements/common.txt | |
# >> Reading requirements (/home/user/project/requirements/common.txt) | |
# >> Excluding packages not listed in requirements | |
# >> Getting latest package info for comparison | |
# Package Version Latest Type | |
# ----------------------------- ------- ----------- ----- | |
# asn1crypto 1.4.0 1.5.0 wheel | |
# boto3 1.21.8 1.21.14 wheel | |
# boto3-stubs 1.21.8 1.21.14 wheel | |
# ddtrace 0.58.5 0.59.0 wheel | |
# Django 4.0.2 4.0.3 wheel | |
from argparse import ArgumentParser | |
from optparse import Values | |
from pathlib import Path | |
import re | |
from typing import List | |
from pip._internal.commands.list import ListCommand | |
from pip._vendor.pkg_resources import DistInfoDistribution | |
from pkg_resources import Requirement | |
def main(): | |
parser = ArgumentParser() | |
parser.add_argument( | |
"file", | |
nargs="?", # a bit confusing, but regex style 1 or None (optional) | |
default=Path("./requirements.txt"), | |
type=Path, | |
help="Requirements file to filter by. Defaults to ./requirements.txt" | |
) | |
args = parser.parse_args() | |
if not args.file.exists(): | |
print("Requirements file not found:") | |
print(f" > {args.file.absolute()}") | |
exit(1) | |
reqs_map = { | |
r.project_name: r | |
for r in read_requirements(args.file) | |
} | |
cmd = ListOutdatedWithFilter("List v2", "List with requirements", requirements_map=reqs_map) | |
cmd.main() | |
def read_requirements(file: Path) -> List[Requirement]: | |
print(f">> Reading requirements ({file.absolute()})") | |
lines = file.read_text().splitlines(False) | |
lines = [r.strip() for r in lines if r.strip() and not r.startswith("#")] | |
reqs = [] | |
for line in lines: | |
if line.startswith('-r') or line.startswith('--requirement'): | |
reqs += read_requirements(file.parent / line.split()[1]) | |
else: | |
reqs.append(Requirement.parse(line)) | |
return reqs | |
class ListOutdatedWithFilter(ListCommand): | |
def __init__(self, *args, requirements_map=None, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.reqs_map = requirements_map | |
def main(self, args: List[str] = None) -> int: | |
return super().main(["--outdated"]) | |
def _name(self, pkg): # older versions used "project_name", so support both (pip < 21.2) | |
return pkg.canonical_name if hasattr(pkg, "canonical_name") else pkg.project_name | |
def _current_version(self, dist): | |
return dist.parsed_version if hasattr(dist, "parsed_version") else dist.version | |
def get_outdated(self, packages, options: Values): | |
print(f">> Excluding packages not listed in requirements") | |
packages: List[DistInfoDistribution] = [ | |
p for p in packages if self._name(p) in self.reqs_map | |
] | |
print(f">> Getting latest package info for comparison") | |
distributions = sorted(self.iter_packages_latest_infos(packages, options), key=self._name) | |
# see if the latest > the parsed (from ) | |
return [d for d in distributions if d.latest_version > self._current_version(d)] | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment