Skip to content

Instantly share code, notes, and snippets.

@awbacker
Created March 15, 2022 18:54
Show Gist options
  • Save awbacker/504919b146ef5566b816d002facb3083 to your computer and use it in GitHub Desktop.
Save awbacker/504919b146ef5566b816d002facb3083 to your computer and use it in GitHub Desktop.
#!/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