├── tests ├── __init__.py ├── test_pytz.py └── test_parse_cfg.py ├── pyrightconfig.json ├── setup.py ├── .gitignore ├── pip_outdated ├── __main__.py ├── verbose.py ├── session.py ├── __init__.py ├── print_outdated.py ├── check_outdated.py └── find_require.py ├── .editorconfig ├── requirements.txt ├── .pylintrc ├── .github └── workflows │ └── build.yml ├── cute.py ├── setup.cfg ├── requirements-lock.txt └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "venv": ".venv", 3 | "venvPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .venv 6 | .coverage 7 | -------------------------------------------------------------------------------- /pip_outdated/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from . import main 3 | main() 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | 5 | [*.py] 6 | indent_size = 4 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pygments==2.19.2 2 | build==1.3.0 3 | docutils==0.22 4 | pylint==3.3.7 5 | pytest-asyncio==1.1.0 6 | pytest-cov==6.2.1 7 | pytest==8.4.1 8 | pyxcute==0.8.1 9 | twine==6.1.0 10 | -------------------------------------------------------------------------------- /pip_outdated/verbose.py: -------------------------------------------------------------------------------- 1 | """Simple namespace to share verbose state.""" 2 | 3 | VERBOSE: bool = None 4 | 5 | def set_verbose(value: bool) -> None: 6 | global VERBOSE 7 | VERBOSE = value 8 | 9 | def verbose() -> bool: 10 | return VERBOSE 11 | -------------------------------------------------------------------------------- /pip_outdated/session.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | 4 | def get_session() -> aiohttp.ClientSession: 5 | headers = {"User-Agent": "pip-outdated"} 6 | connector = aiohttp.TCPConnector(limit=5) 7 | return aiohttp.ClientSession(headers=headers, connector=connector) 8 | -------------------------------------------------------------------------------- /tests/test_pytz.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pip_outdated.check_outdated import get_pypi_versions 3 | from pip_outdated.session import get_session 4 | 5 | @pytest.mark.asyncio 6 | async def test_pytz(): 7 | async with get_session() as session: 8 | await get_pypi_versions("pytz", session) 9 | 10 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable= 3 | design, 4 | global-statement, 5 | missing-docstring, 6 | missing-final-newline, 7 | trailing-whitespace, 8 | fixme 9 | 10 | [BASIC] 11 | variable-rgx=^[a-z_]{1,}$ 12 | function-rgx=^[a-z_]{1,}$ 13 | const-rgx=^(?P[a-z_]{3,})|(?P[A-Z_]{3,})$ 14 | 15 | [FORMAT] 16 | indent-string=" " 17 | -------------------------------------------------------------------------------- /tests/test_parse_cfg.py: -------------------------------------------------------------------------------- 1 | from pip_outdated.find_require import parse_cfg 2 | 3 | def test_parse_cfg(tmp_path): 4 | file = tmp_path / "setup.cfg" 5 | file.write_text(""" 6 | [options] 7 | setup_requires = 8 | setuptools_scm >= 1.15.0 9 | setuptools_scm_git_archive >= 1.0 10 | 11 | install_requires = 12 | ansible >= 2.5 13 | ansible-lint >= 4.0.2, < 5 14 | 15 | [options.extras_require] 16 | docs = 17 | alabaster 18 | Sphinx""") 19 | 20 | names = [r.name for r in parse_cfg(str(file))] 21 | assert names == [ 22 | "setuptools_scm", 23 | "setuptools_scm_git_archive", 24 | "ansible", 25 | "ansible-lint", 26 | "alabaster", 27 | "Sphinx" 28 | ] 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | build: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: actions/setup-python@v2 8 | with: 9 | python-version: '3.12' 10 | - run: pip install wheel 11 | - run: pip install . 12 | - run: pip install -r requirements.txt 13 | # can't use the lock file, some linux-only are not in the lock file. 14 | # - run: cat requirements-lock.txt | xargs -n 1 pip install --no-deps || exit 0 15 | - run: python cute.py test 16 | - uses: codecov/codecov-action@v4 17 | with: 18 | fail_ci_if_error: true # optional (default = false) 19 | token: ${{ secrets.CODECOV_TOKEN }} # required 20 | verbose: true # optional (default = false) 21 | -------------------------------------------------------------------------------- /cute.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | from xcute import cute, LiveReload 3 | 4 | cute( 5 | pkg_name = 'pip_outdated', 6 | lint = 'pylint cute.py tests {pkg_name}', 7 | test = ["lint", 'readme_build', "pytest --cov={pkg_name}"], 8 | bump_pre = 'test', 9 | bump_post = ['dist', 'release', 'publish', 'install'], 10 | # https://stackoverflow.com/q/26545668/3413125 11 | clean = 'x-clean build dist *.egg-info', 12 | dist_pre = 'clean', 13 | dist = 'python -m build', 14 | release = [ 15 | 'git add .', 16 | 'git commit -m "Release v{version}"', 17 | 'git tag -a v{version} -m "Release v{version}"' 18 | ], 19 | publish = [ 20 | 'twine upload dist/*', 21 | 'git push --follow-tags' 22 | ], 23 | install = 'pip install -e .', 24 | readme_build = [ 25 | ('rst2html5 --no-raw --exit-status=1 --verbose ' 26 | 'README.rst | x-pipe build/README.html') 27 | ], 28 | readme_pre = "readme_build", 29 | readme = LiveReload("README.rst", "readme_build", "build/README.html") 30 | ) 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pip-outdated 3 | version = 0.8.0 4 | description = Find outdated dependencies in your requirements.txt or setup.cfg 5 | author = eight 6 | author_email = eight04@gmail.com 7 | 8 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 9 | classifiers = 10 | Development Status :: 4 - Beta 11 | Environment :: Console 12 | Intended Audience :: Developers 13 | Programming Language :: Python :: 3.7 14 | Topic :: Software Development :: Build Tools 15 | 16 | keywords = pip, check, outdate, npm 17 | license = MIT 18 | long_description = file: README.rst 19 | url = https://github.com/eight04/pip-outdated 20 | 21 | [options] 22 | zip_safe = True 23 | packages = find: 24 | 25 | install_requires = 26 | aiohttp~=3.12 27 | colorama~=0.4.6 28 | packaging~=25.0 29 | termcolor~=3.1 30 | terminaltables~=3.1 31 | 32 | [options.packages.find] 33 | exclude = tests 34 | 35 | [options.entry_points] 36 | console_scripts = 37 | pip-outdated = pip_outdated:main 38 | 39 | [vpip] 40 | command_fallback = python cute.py 41 | -------------------------------------------------------------------------------- /requirements-lock.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.6.1 2 | aiohttp==3.12.15 3 | aiosignal==1.4.0 4 | astroid==3.3.11 5 | attrs==25.3.0 6 | build==1.3.0 7 | certifi==2025.8.3 8 | charset-normalizer==3.4.2 9 | colorama==0.4.6 10 | coverage==7.10.2 11 | dill==0.4.0 12 | docutils==0.22 13 | frozenlist==1.7.0 14 | id==1.5.0 15 | idna==3.10 16 | iniconfig==2.1.0 17 | isort==6.0.1 18 | jaraco.classes==3.4.0 19 | jaraco.context==6.0.1 20 | jaraco.functools==4.2.1 21 | keyring==25.6.0 22 | livereload==2.7.1 23 | markdown-it-py==3.0.0 24 | mccabe==0.7.0 25 | mdurl==0.1.2 26 | more-itertools==10.7.0 27 | multidict==6.6.3 28 | nh3==0.3.0 29 | ordered-set==3.1.1 30 | packaging==25.0 31 | platformdirs==4.3.8 32 | pluggy==1.6.0 33 | propcache==0.3.2 34 | Pygments==2.19.2 35 | pylint==3.3.7 36 | pyproject_hooks==1.2.0 37 | pytest==8.4.1 38 | pytest-asyncio==1.1.0 39 | pytest-cov==6.2.1 40 | pywin32-ctypes==0.2.3 41 | pyxcute==0.8.1 42 | readme_renderer==44.0 43 | requests==2.32.4 44 | requests-toolbelt==1.0.0 45 | rfc3986==2.0.0 46 | rich==14.1.0 47 | semver==2.13.0 48 | Send2Trash==1.8.3 49 | termcolor==3.1.0 50 | terminaltables==3.1.10 51 | tomlkit==0.13.3 52 | tornado==6.5.1 53 | twine==6.1.0 54 | urllib3==2.5.0 55 | yarl==1.20.1 -------------------------------------------------------------------------------- /pip_outdated/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | 4 | __version__ = "0.8.0" 5 | 6 | def parse_args(): 7 | parser = argparse.ArgumentParser( 8 | prog="pip-outdated", 9 | description="Find outdated dependencies in your requirements.txt or " 10 | "setup.cfg file.", 11 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 12 | parser.add_argument( 13 | "-v", "--verbose", action="store_true", help="Print verbose information.") 14 | parser.add_argument( 15 | "-q", "--quiet", action="store_true", 16 | help="Don't return exit code 1 if not everything is up to date.") 17 | parser.add_argument( 18 | "file", nargs="*", default=["requirements.txt", "setup.cfg"], metavar="", 19 | help="Read dependencies from requirements files. This option accepts " 20 | "glob pattern.") 21 | return parser.parse_args() 22 | 23 | def main(): 24 | # FIXME: we can't use asyncio.run since it closes the event loop 25 | # https://github.com/aio-libs/aiohttp/issues/1925 26 | # asyncio.run(_main()) 27 | asyncio.get_event_loop().run_until_complete(_main()) 28 | 29 | async def _main(): 30 | # pylint: disable=import-outside-toplevel 31 | args = parse_args() 32 | 33 | from .verbose import set_verbose 34 | set_verbose(args.verbose) 35 | 36 | from .find_require import find_require 37 | from .check_outdated import check_outdated 38 | from .print_outdated import print_outdated 39 | from .session import get_session 40 | 41 | requires = find_require(args.file) 42 | async with get_session() as session: 43 | outdated_results = [asyncio.create_task(check_outdated(r, session)) for r in requires] 44 | await print_outdated(outdated_results, args.quiet) 45 | -------------------------------------------------------------------------------- /pip_outdated/print_outdated.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Awaitable, List, Optional 3 | 4 | import colorama 5 | 6 | from termcolor import colored 7 | from terminaltables import AsciiTable as Table 8 | 9 | from .check_outdated import OutdateResult 10 | 11 | 12 | def make_row(outdate: OutdateResult) -> Optional[List[str]]: 13 | if not outdate.outdated(): 14 | return None 15 | 16 | def colored_current(): 17 | if outdate.install_not_found() or outdate.install_not_wanted(): 18 | return colored(str(outdate.version), "red", attrs=["bold"]) 19 | return str(outdate.version) 20 | 21 | def colored_wanted(): 22 | if outdate.pypi_not_found() or not outdate.wanted: 23 | return colored("None", "red", attrs=["bold"]) 24 | if not outdate.install_not_found() and outdate.version < outdate.wanted: 25 | return colored(str(outdate.wanted), "green", attrs=["bold"]) 26 | return str(outdate.wanted) 27 | 28 | def colored_latest(): 29 | if outdate.pypi_not_found(): 30 | return colored("None", "red", attrs=["bold"]) 31 | if not outdate.install_not_found() and outdate.version < outdate.latest: 32 | return colored(str(outdate.latest), "green", attrs=["bold"]) 33 | return str(outdate.latest) 34 | 35 | return [ 36 | outdate.name, 37 | colored_current(), 38 | colored_wanted(), 39 | colored_latest() 40 | ] 41 | 42 | async def print_outdated(outdates: List[Awaitable[OutdateResult]], quiet: bool): 43 | colorama.init() 44 | 45 | data = [["Name", "Installed", "Wanted", "Latest"]] 46 | count = 0 47 | for count, outdate in enumerate(outdates, 1): 48 | row = make_row(await outdate) 49 | if row: 50 | data.append(row) 51 | 52 | if not count: 53 | print(colored("No requirements found.", "red")) 54 | return 55 | 56 | if len(data) == 1: 57 | print(colored("Everything is up-to-date!", "cyan", attrs=["bold"])) 58 | return 59 | 60 | print(colored("Red = unavailable/outdated/out of version specifier", "red", attrs=["bold"])) 61 | print(colored("Green = updatable", "green", attrs=["bold"])) 62 | table = Table(data) 63 | print(table.table) 64 | if not quiet: 65 | sys.exit(1) 66 | -------------------------------------------------------------------------------- /pip_outdated/check_outdated.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from dataclasses import dataclass 3 | from typing import List, Optional 4 | 5 | from packaging.requirements import Requirement 6 | from packaging.utils import canonicalize_name 7 | from packaging.version import Version, InvalidVersion 8 | from packaging.version import parse as parse_version 9 | 10 | from .verbose import verbose 11 | 12 | 13 | @dataclass 14 | class OutdateResult: 15 | requirement: Requirement 16 | version: Optional[Version] 17 | all_versions: List[Version] 18 | 19 | wanted: Optional[Version] = None 20 | latest: Optional[Version] = None 21 | 22 | def __post_init__(self): 23 | if self.all_versions: 24 | try: 25 | self.wanted = next(v for v in reversed(self.all_versions) 26 | if v in self.requirement.specifier) 27 | except StopIteration: 28 | pass 29 | self.latest = self.all_versions[-1] 30 | 31 | @property 32 | def name(self) -> str: 33 | return self.requirement.name 34 | 35 | def install_not_found(self) -> bool: 36 | return self.version is None 37 | 38 | def install_not_wanted(self) -> bool: 39 | if self.version is None: 40 | return False 41 | return self.version not in self.requirement.specifier 42 | 43 | def pypi_not_found(self) -> bool: 44 | return self.latest is None 45 | 46 | def outdated(self) -> bool: 47 | return self.version != self.wanted or self.version != self.latest 48 | 49 | async def get_local_version(name: str) -> Optional[Version]: 50 | p = await asyncio.create_subprocess_shell( 51 | f"pip show {name}", 52 | stdout=asyncio.subprocess.PIPE, 53 | stderr=asyncio.subprocess.PIPE, 54 | ) 55 | stdout, _ = await p.communicate() 56 | if p.returncode != 0: 57 | return None 58 | for line in stdout.decode().splitlines(): 59 | if line.startswith("Version: "): 60 | return parse_version(line[9:]) 61 | 62 | async def get_pypi_versions(name: str, session) -> List[Version]: 63 | async with session.get(f"https://pypi.org/pypi/{name}/json") as r: 64 | r.raise_for_status() 65 | keys = [] 66 | for s in (await r.json())["releases"].keys(): 67 | try: 68 | version = parse_version(s) 69 | except InvalidVersion: 70 | continue 71 | if version.is_prerelease: 72 | continue 73 | keys.append(version) 74 | keys.sort() 75 | return keys 76 | 77 | async def check_outdated(require, session) -> OutdateResult: 78 | if verbose(): 79 | print(f"Checking: {require.name} {require.specifier}") 80 | name = canonicalize_name(require.name) 81 | current_version = await get_local_version(name) 82 | pypi_versions = await get_pypi_versions(name, session) 83 | return OutdateResult(require, current_version, pypi_versions) 84 | -------------------------------------------------------------------------------- /pip_outdated/find_require.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find requirements. 3 | https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format 4 | """ 5 | 6 | from collections.abc import Iterable 7 | from configparser import ConfigParser 8 | import pathlib 9 | import re 10 | 11 | from packaging.requirements import Requirement, InvalidRequirement 12 | from .verbose import verbose 13 | 14 | def iter_files(patterns): 15 | """Yield path.Path(file) from multiple glob patterns.""" 16 | for pattern in patterns: 17 | if pathlib.Path(pattern).is_file(): 18 | yield pathlib.Path(pattern) 19 | else: 20 | yield from pathlib.Path(".").glob(pattern) 21 | 22 | def file_to_lines(file): 23 | """Yield line from a file. Handle '#' comment and '\' continuation escape. 24 | """ 25 | if verbose(): 26 | print(f"Parse: {file}") 27 | with file.open("r", encoding="utf-8") as f: 28 | yield from parse_lines(f) 29 | 30 | def parse_lines(lines: Iterable[str]): 31 | pre_line = "" 32 | for line in lines: 33 | match = re.match(r"(.*?)(^|\s)#", line) 34 | if match: 35 | yield pre_line + match.group(1) 36 | pre_line = "" 37 | continue 38 | if line.endswith("\\\n"): 39 | pre_line += line[0:-2] 40 | continue 41 | if line.endswith("\n"): 42 | yield pre_line + line[0:-1] 43 | pre_line = "" 44 | continue 45 | yield pre_line + line 46 | pre_line = "" 47 | 48 | def parse_requirements(file): 49 | for line in file_to_lines(file): 50 | require = parse_require(line) 51 | if require: 52 | yield require 53 | 54 | def parse_requirements_text(text: str): 55 | for line in parse_lines(text.splitlines(True)): 56 | require = parse_require(line) 57 | if require: 58 | yield require 59 | 60 | def parse_cfg(file): 61 | conf = ConfigParser() 62 | conf.read(file, encoding="utf-8") 63 | 64 | def get_texts(): 65 | try: 66 | yield conf["options"]["setup_requires"] 67 | except KeyError: 68 | pass 69 | try: 70 | yield conf["options"]["install_requires"] 71 | except KeyError: 72 | pass 73 | try: 74 | for key in conf["options.extras_require"]: 75 | yield conf["options.extras_require"][key] 76 | except KeyError: 77 | pass 78 | 79 | for text in get_texts(): 80 | if not text: 81 | continue 82 | yield from parse_requirements_text(text) 83 | 84 | def find_require(files): 85 | for file in iter_files(files): 86 | requires = parse_cfg(file) if file.suffix == ".cfg" else parse_requirements(file) 87 | yield from requires 88 | 89 | def parse_require(text): 90 | # strip options 91 | match = re.match(r"(.*?)\s--?[a-z]", text) 92 | if match: 93 | text = match.group(1) 94 | try: 95 | return Requirement(text) 96 | except InvalidRequirement: 97 | return None 98 | 99 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pip-outdated 2 | ============ 3 | 4 | .. image:: https://travis-ci.com/eight04/pip-outdated.svg?branch=master 5 | :target: https://travis-ci.com/eight04/pip-outdated 6 | 7 | .. image:: https://codecov.io/gh/eight04/pip-outdated/branch/master/graph/badge.svg 8 | :target: https://codecov.io/gh/eight04/pip-outdated 9 | 10 | Find outdated dependencies in your requirements.txt or setup.cfg file. Report missing/outdated/incompatible packages with table and colors. 11 | 12 | This tool compares the version number with the version specifier in ``requirements.txt`` or ``setup.cfg``. If you just want to list all updatable package, simply use ``pip list --outdated`` command. 13 | 14 | Installation 15 | ------------ 16 | 17 | From `pypi `__ 18 | 19 | :: 20 | 21 | pip install pip-outdated 22 | 23 | Usage 24 | ----- 25 | 26 | :: 27 | 28 | usage: pip-outdated [-h] [-v] [-q] [ [ ...]] 29 | 30 | Find outdated dependencies in your requirements.txt or setup.cfg file. 31 | 32 | positional arguments: 33 | Read dependencies from requirements files. This option 34 | accepts glob pattern. (default: ['requirements.txt', 35 | 'setup.cfg']) 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | -v, --verbose Print verbose information. (default: False) 40 | -q, --quiet Don't return exit code 1 if not everything is up to date. 41 | (default: False) 42 | 43 | Check multiple files e.g. ``test-requirements.txt`` and ``dev-requirements.txt``:: 44 | 45 | pip-outdated *-requirements.txt 46 | 47 | Check files under ``requirements`` folder:: 48 | 49 | pip-outdated requirements/*.txt 50 | 51 | Todos 52 | ----- 53 | 54 | * Add options to update the package? 55 | * Add options to update the requirements.txt/setup.cfg file? 56 | * Add options to list all packages? (e.g. ``-g, --global``) 57 | 58 | Changelog 59 | --------- 60 | 61 | * 0.8.0 (Aug 9, 2025) 62 | 63 | - Change: drop setuptools dependency, switch to configparse for reading setup.cfg. 64 | 65 | * 0.7.0 (Aug 13, 2024) 66 | 67 | - Change: ignore ``InvalidVersion`` error. 68 | 69 | * 0.6.0 (Jan 1, 2023) 70 | 71 | - Bump dependencies. 72 | - Drop cchardet. 73 | - Change: fetch package version from ``pip`` CLI, so we can get package version in venv. 74 | 75 | * 0.5.0 (Jan 12, 2022) 76 | 77 | - Bump dependencies. 78 | - Add: typehint. 79 | 80 | * 0.4.0 (Jan 30, 2020) 81 | 82 | - **Breaking: bump Python to 3.7** 83 | - Add: request in parallel. 84 | - Add: ``--quiet`` option. 85 | 86 | * 0.3.0 (Oct 13, 2019) 87 | 88 | - **Breaking: set exit code to 1 if not all good.** 89 | - Fix: don't check prereleases. 90 | - Add: check ``setup_requires`` and ``extras_require`` in cfg files. 91 | 92 | * 0.2.0 (Feb 10, 2019) 93 | 94 | - Bump dependencies: 95 | 96 | - colorama@0.4.x 97 | - packaging@19.x 98 | - requests@2.x 99 | - termcolor@1.x 100 | - terminaltables@3.x 101 | 102 | * 0.1.0 (May 12, 2018) 103 | 104 | - First release. 105 | 106 | --------------------------------------------------------------------------------