├── .coverage ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── noxfile.py ├── pyproject.toml ├── src └── pip_check │ ├── __init__.py │ └── __main__.py └── uv.lock /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartTC/pip-check/6a8f9947ea391059e39d1632cce3bd534e5f19fc/.coverage -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testsuite 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Install uv 16 | uses: astral-sh/setup-uv@v5 17 | 18 | - name: Run nox with uv 19 | run: uv run nox 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | .venv 3 | build 4 | dist 5 | htmlcov -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | v3.1 (2025-04-14): 4 | 5 | - Adopted **uv** and **hatchling** as the primary build tools. 6 | - Implemented **nox** as the test runner. 7 | - Migrated all documentation from **reStructuredText** to **Markdown** for improved readability and accessibility. 8 | - Introduced **Ruff** as a linter and optimized code to align with the current set of Ruff rules. 9 | - Added type annotations throughout the codebase. 10 | - Test against **Python 3.14**. 11 | 12 | v3.0 (2025-04-11): 13 | 14 | - Added support for ``uv``. Use ``--cmd="uv pip"`` to utilize uv. 15 | 16 | v2.10 (2025-04-11): 17 | 18 | - Resolve the issue of missing line breaks when no packages are found. 19 | - Test against Python 3.13. 20 | 21 | v2.9 (2024-09-01): 22 | 23 | - Show current Python and pip version upon load. 24 | - Test against Python 3.12. 25 | 26 | v2.8.1 (2022-11-06): 27 | 28 | - Fixes issue with packages not correctly sorted into "Major" category. 29 | 30 | v2.8 (2022-11-06): 31 | 32 | - Added support for Python 3.11. 33 | - Replaced deprecated "distutils" with "packaging" module. 34 | 35 | v2.7 (2021-11-16): 36 | 37 | - Drop support for Python 2.7, 3.4 and 3.5 38 | - Added support for Python 3.9 and 3.10. 39 | - Removed 'colorclass' as a dependency and with that the shell argument 40 | `--disable-colors`. 41 | 42 | v2.6 (2019-12-12): 43 | 44 | - Requires Python 3.5 or higher. 45 | - Command error is shown if pip exits with a status code 1 (or larger). 46 | - Error message is shown if pip is not able to load packages in case of 47 | network problems. 48 | - Update instructions will now add ``--user`` in case the pip-check command 49 | should only show user packages as well. 50 | 51 | v2.5.2 (2019-08-08): 52 | 53 | - This is the last version that runs on Python 2.7. Install it with 54 | ``pip install pip-check==2.5.2`` 55 | - Windows color fixes. 56 | 57 | v2.5.1 (2019-08-08): 58 | 59 | - Windows script fixes. 60 | 61 | v2.5 (2019-08-08): 62 | 63 | - A more robust installation that installs pip-check as a proper console script. 64 | - Added new ``--disable-colors`` argument. 65 | - Added tests for Python 3.7 and 3.8. 66 | - Fixed Syntax warning happening with no outdated packages. 67 | - Cleanup of the entire codebase. 68 | 69 | v2.4 (2019-07-23): 70 | 71 | - Added support to only show packages from the ``user`` or ``local`` package 72 | namespace. 73 | 74 | v2.3.3 (2018-02-19): 75 | 76 | - Visual fixes around ``--show-update`` 77 | 78 | v2.3.2 (2018-02-18): 79 | 80 | - New ``--show-update`` argument. 81 | - Fixed ``--full-versions`` argument. 82 | - Minor UI improvements. 83 | 84 | v2.1 (2018-02-18): 85 | 86 | - Complete new architecture. It now calls ``pip`` directly and parses it output 87 | which should be more reliable. 88 | - It's also using distutils for the version comparision now, which is more 89 | reliable as well. 90 | - Lots of features and bug fixes. 91 | 92 | v0.2 (2016-02-09): 93 | 94 | - Fixes issues with older pip versions. 95 | - Truncates extremly long version numbers. 96 | 97 | v0.1 (2016-02-06): 98 | 99 | - Very first version, and yet with very limited features. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Martin Mahner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Testsuite](https://github.com/bartTC/pip-check/actions/workflows/test.yml/badge.svg)](https://github.com/bartTC/pip-check/actions/workflows/test.yml) 2 | [![PyPi Version](https://img.shields.io/pypi/v/pip-check 3 | )](https://pypi.org/project/pip-check/) 4 | ![Monthly Downloads](https://img.shields.io/pypi/dm/pip-check 5 | ) 6 | 7 | # pip-check 8 | 9 | pip-check gives you a quick overview of all installed packages and their 10 | update status. Under the hood it calls `pip list --outdated --format=columns` 11 | and transforms it into a more user-friendly table. 12 | 13 | pip-check also supports uv, or pip at any location. Pass the `pip` 14 | command using `--cmd`: 15 | 16 | ``` 17 | pip-check -c pip3 18 | pip-check -c ".venv/bin/pip" 19 | pip-check -c "uv pip" 20 | ``` 21 | 22 | ![Screenshot of pip-check in action](https://d.pr/i/ZDPuw5.png) 23 | 24 | ## Installation: 25 | 26 | ``` 27 | pip install pip-check 28 | ``` 29 | 30 | Or Install the last version that runs on Python 2.7 or 3.4: 31 | 32 | ``` 33 | pip install pip-check==2.5.2 34 | ``` 35 | 36 | ## Usage: 37 | 38 | ```bash 39 | $ pip-check -h 40 | usage: __init__.py [-h] [-a] [-c PIP_CMD] [-l] [-r] [-f] [-H] [-u] [-U] 41 | 42 | A quick overview of all installed packages and their update status. Supports `pip` or `uv pip`. 43 | 44 | options: 45 | -h, --help show this help message and exit 46 | -a, --ascii Display as ASCII Table 47 | -c, --cmd PIP_CMD The [uv] pip executable to run. E.g.: `/path/to/pip` or `uv pip`. Default: `pip` 48 | -l, --local Show only virtualenv installed packages. (pip only) 49 | -r, --not-required List only packages that are not dependencies of installed packages. (pip only) 50 | -f, --full-version Show full version strings. 51 | -H, --hide-unchanged Do not show "unchanged" packages. 52 | -u, --show-update Show update instructions for updatable packages. (pip only) 53 | -U, --user Show only user installed packages. (pip only) 54 | ``` 55 | 56 | ## Local Development 57 | 58 | pip-check uses `uv` for local development. 59 | 60 | ``` 61 | uv sync # Create a .venv and install dependencies 62 | uv build # Build distribution packages 63 | uvx uv-publish # Publish on Pypi 64 | ``` 65 | 66 | ### Testing: 67 | 68 | Test against a variation of Python versions: 69 | 70 | ```bash 71 | $ uv run nox 72 | ``` 73 | 74 | ## Recommended Similar Tools 75 | 76 | - [pip-date](https://github.com/E3V3A/pip-date) - Show the installation or modification times of all your pip packages 77 | - [pip-chill](https://github.com/rbanffy/pip-chill) - Lists only the dependencies (or not) of installed packages 78 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Basic tests around pip-check.""" 2 | 3 | from __future__ import annotations 4 | 5 | import nox 6 | 7 | nox.options.default_venv_backend = "uv" 8 | nox.options.sessions = [ 9 | "lint", 10 | "readme", 11 | "pip-check-test-py", 12 | "coverage", 13 | ] 14 | 15 | python_versions = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 16 | 17 | 18 | @nox.session(python=python_versions, name="pip-check-test-py") 19 | def tests(session: nox.Session) -> None: 20 | """pip-check smoke tests.""" 21 | session.install("--upgrade", "pip", "uv") 22 | session.install("html5lib==0.999999999", "django==1.10", "pyglet==2.0.dev23") 23 | session.install(".") 24 | 25 | response = session.run("pip-check", silent=True) 26 | 27 | # Make sure, packages are actually listed 28 | assert "1.10" in response 29 | assert "0.999999999" in response 30 | assert "2.0.dev23" in response 31 | 32 | session.run("pip-check", "--help") 33 | session.run("pip-check", "--version") 34 | session.run( 35 | "pip-check", 36 | "--ascii", 37 | "--not-required", 38 | "--full-version", 39 | "--hide-unchanged", 40 | "--show-update", 41 | ) 42 | session.run("pip-check", "--user") 43 | session.run("pip-check", "--local") 44 | 45 | session.run("pip-check", "--cmd=uv pip") 46 | session.run("pip-check", "--cmd=uv pip", "--help") 47 | session.run( 48 | "pip-check", "--cmd=uv pip", "--ascii", "--full-version", "--hide-unchanged" 49 | ) 50 | 51 | 52 | @nox.session 53 | def coverage(session: nox.Session) -> None: 54 | """Run the final coverage report.""" 55 | session.install("--upgrade", "coverage") 56 | session.install("-e", ".") 57 | session.run("coverage", "erase") 58 | session.run("coverage", "run", "--append", "-m", "pip_check") 59 | session.run("coverage", "run", "--append", "-m", "pip_check", "--version") 60 | session.run( 61 | "coverage", 62 | "run", 63 | "--append", 64 | "-m", 65 | "pip_check", 66 | "--ascii", 67 | "--not-required", 68 | "--full-version", 69 | "--hide-unchanged", 70 | "--show-update", 71 | ) 72 | session.run("coverage", "report") 73 | session.run("coverage", "html") 74 | 75 | 76 | @nox.session 77 | def readme(session: nox.Session) -> None: 78 | """Readme Validation.""" 79 | session.install("markdown-it-py") 80 | session.run("markdown-it", "README.md", "/dev/null", external=True) 81 | 82 | 83 | @nox.session 84 | def lint(session: nox.Session) -> None: 85 | """Ruff codebase linting.""" 86 | session.run("ruff", "check", "src", "noxfile.py", external=True) 87 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pip-check" 3 | version = "3.1" 4 | description = "Display installed pip packages and their update status." 5 | requires-python = ">=3.8" 6 | dynamic = ["readme"] 7 | license = { text = "MIT" } 8 | authors = [ 9 | { name = "Martin Mahner", email = "martin@mahner.org" } 10 | ] 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "Environment :: Console", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Programming Language :: Python :: 3.14", 24 | "Programming Language :: Python", 25 | ] 26 | dependencies = [ 27 | "terminaltables", 28 | "packaging", 29 | ] 30 | 31 | [dependency-groups] 32 | dev = [ 33 | "nox>=2022.1.7", 34 | "ruff>=0.11.5", 35 | ] 36 | 37 | [project.urls] 38 | Homepage = "https://github.com/bartTC/pip-check/" 39 | Issues = "https://github.com/bartTC/pip-check/issues" 40 | 41 | [project.scripts] 42 | pip-check = "pip_check:main" 43 | 44 | [build-system] 45 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 46 | build-backend = "hatchling.build" 47 | 48 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 49 | content-type = "text/markdown" 50 | 51 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 52 | path = "README.md" 53 | 54 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 55 | text = "\n\n" 56 | 57 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 58 | path = "CHANGELOG.md" 59 | 60 | [tool.ruff] 61 | target-version = "py38" 62 | lint.select = ["ALL"] 63 | lint.ignore = [ 64 | "D203", # No blank line before cclass 65 | "D212", # Multi-line summary first line 66 | "E501", # Line too long (>88) 67 | "COM812", # (ruff format) Checks for the absence of trailing commas 68 | "ISC001", # (ruff format) Checks for implicitly concatenated strings on a single line 69 | ] 70 | 71 | [tool.ruff.lint.extend-per-file-ignores] 72 | "noxfile.py" = [ 73 | "S101", # Use of assert detected 74 | ] 75 | -------------------------------------------------------------------------------- /src/pip_check/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | pip-check. 5 | 6 | pip-check gives you a quick overview of all installed packages and their 7 | update status. Under the hood it calls `pip list --outdated --format=columns` 8 | and transforms it into a more user-friendly table. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import argparse 14 | import contextlib 15 | import json 16 | import shlex 17 | import subprocess 18 | import sys 19 | from collections import OrderedDict 20 | from importlib.metadata import version 21 | 22 | import terminaltables 23 | from packaging.version import Version 24 | 25 | __version__ = version("pip-check") 26 | 27 | # The `pip` command to run. Normally `pip` but you can specify 28 | # it using the `--cmd=pip` argument. 29 | pip_cmd = "pip" 30 | 31 | # The complete command to run to get a JSON list of outdated packages 32 | pip_not_required_arg = "--not-required" 33 | pip_user_arg = "--user" 34 | pip_local_arg = "--local" 35 | pip_outdated_cmd = "{cmd} list --outdated --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}" 36 | pip_current_cmd = "{cmd} list --uptodate --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}" 37 | uv_outdated_cmd = "{cmd} list --outdated --format=json {notreq_arg}" 38 | uv_current_cmd = "{cmd} list --format=json {notreq_arg}" 39 | 40 | # Some pip packages such as pycryptopp have ridiculous long version 41 | # # 0.6.0.1206569328141510525648634803928199668821045408958 42 | # which messes up the table. These are capped by default to 10 characters. 43 | # Can be overridden with -f. 44 | max_version_length = 10 45 | 46 | err = sys.stderr.write 47 | out = sys.stdout.write 48 | 49 | 50 | def split_command(cmd: str) -> list[str]: 51 | """ 52 | Split a command string into a list of properly escaped and quoted substrings. 53 | 54 | This function takes a single command string as input, splits it into substrings, 55 | and ensures each substring is properly escaped and shell-quoted. It leverages 56 | the `shlex.split` method to perform parsing and tokenization. 57 | 58 | Args: 59 | cmd (str): The command string to be split and quoted. 60 | 61 | Returns: 62 | list[str]: A list of shell-quoted substrings derived from the input command. 63 | 64 | """ 65 | return [shlex.quote(s) for s in shlex.split(cmd)] 66 | 67 | 68 | def get_pip_version(options: argparse.Namespace) -> str: 69 | """ 70 | Retrieve the version of pip by executing the provided pip command. 71 | 72 | This function runs the pip command specified in the options parameter to fetch 73 | the current installed pip version. If the command execution fails or does not 74 | return a version string, the process will terminate with an error. 75 | 76 | Arguments: 77 | options (argparse.Namespace): A namespace object that encompasses the 78 | configurations needed for the command execution. This includes pip 79 | command to use, additional arguments for specifying package scope, 80 | and other related options. 81 | 82 | Returns: 83 | str: The output of the executed pip command, which includes the pip version. 84 | 85 | Raises: 86 | subprocess.CalledProcessError: If the pip command fails during execution. 87 | SystemExit: If the pip command execution fails or does not return a valid 88 | version string. 89 | 90 | """ 91 | cmd = f"{options.pip_cmd} --version" 92 | 93 | try: 94 | cmd_response = subprocess.run( # noqa: S603 95 | split_command(cmd), capture_output=True, check=True, text=True 96 | ) 97 | except subprocess.CalledProcessError as e: 98 | err(f"The pip command did not succeed: {e.stderr}") 99 | sys.exit(1) 100 | 101 | cmd_response_string = cmd_response.stdout.strip() 102 | 103 | if not cmd_response_string: 104 | err( 105 | "The pip command did not return a version string. " 106 | "Does `pip --version` work for you?" 107 | ) 108 | sys.exit(1) 109 | 110 | return cmd_response_string 111 | 112 | 113 | def get_package_versions( 114 | options: argparse.Namespace, *, outdated_only: bool = True 115 | ) -> dict: 116 | """ 117 | Fetch and parses the package version information using pip command. 118 | 119 | This function executes a pip command to retrieve the package versions, 120 | either limited to outdated packages or including all packages based on the 121 | outdated_only flag. The command is constructed based on the provided options 122 | and executed using subprocess. Results are captured and parsed from JSON 123 | for further processing. Errors during execution, connection issues, or JSON 124 | parsing errors are handled, and appropriate error messages are displayed 125 | to the user. The program will exit with relevant status codes upon encountering 126 | errors. 127 | 128 | Arguments: 129 | options (argparse.Namespace): A namespace object that encompasses the 130 | configurations needed for the command execution. This includes pip 131 | command to use, additional arguments for specifying package scope, 132 | and other related options. 133 | outdated_only (bool): Optional flag to determine whether to fetch only 134 | outdated packages (default is True) or all package versions. 135 | 136 | Returns: 137 | dict: A dictionary containing package version details parsed from the 138 | pip command output. 139 | 140 | Raises: 141 | SystemExit: Raised when execution of the pip command fails, HTTP 142 | connection issues are detected, or JSON parsing fails. 143 | 144 | """ 145 | if outdated_only: 146 | check_cmd = ( 147 | uv_outdated_cmd if options.pip_cmd.startswith("uv") else pip_outdated_cmd 148 | ) 149 | else: 150 | check_cmd = ( 151 | uv_current_cmd if options.pip_cmd.startswith("uv") else pip_current_cmd 152 | ) 153 | 154 | cmd = check_cmd.format( 155 | cmd=options.pip_cmd, 156 | notreq_arg=pip_not_required_arg if options.pip_not_required else "", 157 | user_arg=pip_user_arg if options.show_user else "", 158 | local_arg=pip_local_arg if options.show_local else "", 159 | ) 160 | 161 | try: 162 | cmd_response = subprocess.run( # noqa: S603 163 | split_command(cmd), check=False, capture_output=True, text=True 164 | ) 165 | 166 | except subprocess.CalledProcessError as e: 167 | err( 168 | "The pip command did not succeed: {stderr}\n".format( 169 | stderr=e.stderr.decode("utf-8") 170 | ) 171 | ) 172 | sys.exit(1) 173 | 174 | # The pip command exited with 0 but we have stderr content: 175 | if cmd_response.stderr and "NewConnectionError" in cmd_response.stderr: 176 | err( 177 | "\npip indicated that it has connection problems. " 178 | "Please check your network.\n" 179 | ) 180 | sys.exit(1) 181 | 182 | cmd_response_string = cmd_response.stdout.strip() 183 | 184 | if not cmd_response_string: 185 | err("No outdated packages. \\o/") 186 | sys.exit(0) 187 | 188 | try: 189 | pip_packages = json.loads(cmd_response_string) 190 | except json.JSONDecodeError: 191 | err( 192 | "Unable to parse the version list from pip. " 193 | "Does `pip list --format=json` work for you?\n" 194 | ) 195 | sys.exit(1) 196 | 197 | return pip_packages 198 | 199 | 200 | def run(options: argparse.Namespace) -> None: # noqa: PLR0912, PLR0915, C901 201 | """ 202 | Analyzes and prints information about Python packages. 203 | 204 | Categorizing them into major, minor, unchanged, or unknown updates. The function 205 | retrieves the current Python version, pip version, and package versions from the 206 | provided options. It prepares outputs, such as lists of packages grouped by their 207 | update types, and displays tabular output summarizing the analysis. 208 | 209 | Arguments: 210 | options (argparse.Namespace): Command-line options and flags passed to 211 | control specific behaviors of the function, such as whether to hide 212 | unchanged packages or show update commands. 213 | 214 | Returns: 215 | None 216 | 217 | Raises: 218 | None 219 | 220 | """ 221 | current_pip_version = get_pip_version(options) 222 | sys.stdout.write(f"Python {sys.version}\n") 223 | sys.stdout.write(f"{current_pip_version}\n") 224 | sys.stdout.write("\nLoading package versions...\n") 225 | sys.stdout.flush() 226 | 227 | # Unchanged Packages 228 | unchanged = [] 229 | if not options.hide_unchanged: 230 | unchanged = get_package_versions(options, outdated_only=False) 231 | 232 | packages = { 233 | "major": [], 234 | "minor": [], 235 | "unknown": [], 236 | "unchanged": unchanged, 237 | } 238 | 239 | # Fetch all outdated packages and sort them into major/minor/unknown. 240 | for package in get_package_versions(options, outdated_only=True): 241 | # No version info 242 | if "latest_version" not in package or "version" not in package: 243 | packages["unknown"].append(package) 244 | continue 245 | 246 | try: 247 | latest = Version(package["latest_version"]) 248 | current = Version(package["version"]) 249 | except ValueError: 250 | # Unable to parse the version into anything useful 251 | packages["unknown"].append(package) 252 | continue 253 | 254 | # If the current version is larger than the latest 255 | # (e.g. a pre-release is installed) put it into the unknown section. 256 | # Technically its 'unchanged' but I guess its better to have 257 | # pre-releases stand out more. 258 | if current > latest: 259 | packages["unknown"].append(package) 260 | continue 261 | 262 | # Current and latest package version is the same. If this happens, 263 | # it's likely a bug with the version parsing. 264 | if current == latest: 265 | packages["unchanged"].append(package) 266 | continue 267 | 268 | # Major upgrade (first version number) 269 | if current.major < latest.major: 270 | packages["major"].append(package) 271 | continue 272 | 273 | # Everything else is a minor update 274 | packages["minor"].append(package) 275 | 276 | table_data = OrderedDict() 277 | 278 | def cut_version(v: str) -> str: 279 | if not v or v == "Unknown": 280 | return v 281 | 282 | # Cut version to readable length 283 | if not options.show_long_versions and len(v) > max_version_length + 3: 284 | return f"{v[:max_version_length]}..." 285 | return v 286 | 287 | def columns(package_data: dict) -> list[str] | None: 288 | # Generate the columns for the table(s) for each package 289 | # Name | Current Version | Latest Version | pypi String 290 | 291 | name = package_data.get("name") 292 | current_version = package_data.get("version") 293 | latest_version = package_data.get("latest_version") 294 | help_string = "https://pypi.python.org/pypi/{}".format(package_data["name"]) 295 | 296 | if latest_version and options.show_update: 297 | help_string = "pip install {user}{name}=={version}".format( 298 | user="--user " if options.show_user is True else "", 299 | name=name, 300 | version=latest_version, 301 | ) 302 | 303 | return [ 304 | name, 305 | cut_version(current_version) or "Unknown", 306 | cut_version(latest_version) or current_version or "Unknown", 307 | help_string, 308 | ] 309 | 310 | for key, label, _ in [ 311 | ("major", "Major Release Update", "autored"), 312 | ("minor", "Minor Release Update", "autoyellow"), 313 | ("unchanged", "Unchanged Packages", "autogreen"), 314 | ("unknown", "Unknown Package Release Status", "autoblack"), 315 | ]: 316 | if packages[key]: 317 | if key not in table_data: 318 | table_data[key] = [] 319 | 320 | (table_data[key].append([label, "Version", "Latest"]),) 321 | for line_package in packages[key]: 322 | table_data[key].append(columns(line_package)) 323 | 324 | # Table output class 325 | table_class = ( 326 | terminaltables.AsciiTable if options.ascii_only else terminaltables.SingleTable 327 | ) 328 | 329 | for data in table_data.values(): 330 | out("\n") 331 | table = table_class(data) 332 | out(table.table) 333 | out("\n") 334 | sys.stdout.flush() 335 | 336 | if options.show_update: 337 | for label in ("major", "minor"): 338 | if packages[label]: 339 | out( 340 | "\nTo update all {label} releases run:\n\n" 341 | " {pip_cmd} install --upgrade {user}{packages}\n".format( 342 | label=label, 343 | pip_cmd=options.pip_cmd, 344 | user="--user " if options.show_user is True else "", 345 | packages=" ".join([p["name"] for p in packages[label]]), 346 | ) 347 | ) 348 | 349 | 350 | def main() -> None: 351 | """Parse command-line arguments and runs the application.""" 352 | parser = argparse.ArgumentParser( 353 | description="A quick overview of all installed packages " 354 | "and their update status. Supports `pip` or `uv pip`." 355 | ) 356 | parser.add_argument( 357 | "-a", 358 | "--ascii", 359 | action="store_true", 360 | dest="ascii_only", 361 | default=False, 362 | help="Display as ASCII Table", 363 | ) 364 | parser.add_argument( 365 | "-c", 366 | "--cmd", 367 | dest="pip_cmd", 368 | default=pip_cmd, 369 | help="The [uv] pip executable to run. E.g.: `/path/to/pip` or `uv pip`. Default: `pip`", 370 | ) 371 | parser.add_argument( 372 | "-l", 373 | "--local", 374 | action="store_true", 375 | dest="show_local", 376 | default=False, 377 | help="Show only virtualenv installed packages. (pip only)", 378 | ) 379 | parser.add_argument( 380 | "-r", 381 | "--not-required", 382 | action="store_true", 383 | dest="pip_not_required", 384 | default=False, 385 | help="List only packages that are not dependencies of installed packages. (pip only)", 386 | ) 387 | parser.add_argument( 388 | "-f", 389 | "--full-version", 390 | action="store_true", 391 | dest="show_long_versions", 392 | default=False, 393 | help="Show full version strings.", 394 | ) 395 | parser.add_argument( 396 | "-H", 397 | "--hide-unchanged", 398 | action="store_true", 399 | dest="hide_unchanged", 400 | default=False, 401 | help='Do not show "unchanged" packages.', 402 | ) 403 | parser.add_argument( 404 | "-u", 405 | "--show-update", 406 | action="store_true", 407 | dest="show_update", 408 | default=False, 409 | help="Show update instructions for updatable packages. (pip only)", 410 | ) 411 | parser.add_argument( 412 | "-U", 413 | "--user", 414 | action="store_true", 415 | dest="show_user", 416 | default=False, 417 | help="Show only user installed packages. (pip only)", 418 | ) 419 | parser.add_argument( 420 | "--version", 421 | action="store_true", 422 | dest="show_version", 423 | default=False, 424 | help="Show the version of pip-check and exit.", 425 | ) 426 | 427 | options = parser.parse_args() 428 | 429 | if options.show_version: 430 | sys.stdout.write(f"pip-check version {__version__}\n") 431 | sys.exit(0) 432 | 433 | run(options) 434 | 435 | 436 | if __name__ == "__main__": 437 | with contextlib.suppress(KeyboardInterrupt): 438 | main() 439 | -------------------------------------------------------------------------------- /src/pip_check/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Entry point for pip-check. 3 | 4 | This script provides a command-line interface to check 5 | installed Python packages and their update status. 6 | """ 7 | 8 | from . import main 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.8" 4 | resolution-markers = [ 5 | "python_full_version >= '3.9'", 6 | "python_full_version < '3.9'", 7 | ] 8 | 9 | [[package]] 10 | name = "argcomplete" 11 | version = "3.6.2" 12 | source = { registry = "https://pypi.org/simple" } 13 | sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403 } 14 | wheels = [ 15 | { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708 }, 16 | ] 17 | 18 | [[package]] 19 | name = "attrs" 20 | version = "25.3.0" 21 | source = { registry = "https://pypi.org/simple" } 22 | sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, 25 | ] 26 | 27 | [[package]] 28 | name = "colorama" 29 | version = "0.4.6" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 34 | ] 35 | 36 | [[package]] 37 | name = "colorlog" 38 | version = "6.9.0" 39 | source = { registry = "https://pypi.org/simple" } 40 | dependencies = [ 41 | { name = "colorama", marker = "sys_platform == 'win32'" }, 42 | ] 43 | sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, 46 | ] 47 | 48 | [[package]] 49 | name = "dependency-groups" 50 | version = "1.3.0" 51 | source = { registry = "https://pypi.org/simple" } 52 | dependencies = [ 53 | { name = "packaging" }, 54 | { name = "tomli", marker = "python_full_version < '3.11'" }, 55 | ] 56 | sdist = { url = "https://files.pythonhosted.org/packages/b4/57/cd53c3e335eafbb0894449af078e2b71db47e9939ce2b45013e5a9fe89b7/dependency_groups-1.3.0.tar.gz", hash = "sha256:5b9751d5d98fbd6dfd038a560a69c8382e41afcbf7ffdbcc28a2a3f85498830f", size = 9832 } 57 | wheels = [ 58 | { url = "https://files.pythonhosted.org/packages/99/2c/3e3afb1df3dc8a8deeb143f6ac41acbfdfae4f03a54c760871c56832a554/dependency_groups-1.3.0-py3-none-any.whl", hash = "sha256:1abf34d712deda5581e80d507512664d52b35d1c2d7caf16c85e58ca508547e0", size = 8597 }, 59 | ] 60 | 61 | [[package]] 62 | name = "distlib" 63 | version = "0.3.9" 64 | source = { registry = "https://pypi.org/simple" } 65 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } 66 | wheels = [ 67 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, 68 | ] 69 | 70 | [[package]] 71 | name = "filelock" 72 | version = "3.16.1" 73 | source = { registry = "https://pypi.org/simple" } 74 | resolution-markers = [ 75 | "python_full_version < '3.9'", 76 | ] 77 | sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } 78 | wheels = [ 79 | { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, 80 | ] 81 | 82 | [[package]] 83 | name = "filelock" 84 | version = "3.18.0" 85 | source = { registry = "https://pypi.org/simple" } 86 | resolution-markers = [ 87 | "python_full_version >= '3.9'", 88 | ] 89 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, 92 | ] 93 | 94 | [[package]] 95 | name = "nox" 96 | version = "2025.2.9" 97 | source = { registry = "https://pypi.org/simple" } 98 | dependencies = [ 99 | { name = "argcomplete" }, 100 | { name = "attrs" }, 101 | { name = "colorlog" }, 102 | { name = "dependency-groups" }, 103 | { name = "packaging" }, 104 | { name = "tomli", marker = "python_full_version < '3.11'" }, 105 | { name = "virtualenv" }, 106 | ] 107 | sdist = { url = "https://files.pythonhosted.org/packages/0d/22/84a2d3442cb33e6fb1af18172a15deb1eea3f970417f1f4c5fa1600143e8/nox-2025.2.9.tar.gz", hash = "sha256:d50cd4ca568bd7621c2e6cbbc4845b3b7f7697f25d5fb0190ce8f4600be79768", size = 4021103 } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/57/ca/64e634c056cba463cac743735660a772ab78eb26ec9759e88de735f2cd27/nox-2025.2.9-py3-none-any.whl", hash = "sha256:7d1e92d1918c6980d70aee9cf1c1d19d16faa71c4afe338fffd39e8a460e2067", size = 71315 }, 110 | ] 111 | 112 | [[package]] 113 | name = "packaging" 114 | version = "24.2" 115 | source = { registry = "https://pypi.org/simple" } 116 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 117 | wheels = [ 118 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 119 | ] 120 | 121 | [[package]] 122 | name = "pip-check" 123 | version = "3.1" 124 | source = { editable = "." } 125 | dependencies = [ 126 | { name = "packaging" }, 127 | { name = "terminaltables" }, 128 | ] 129 | 130 | [package.dev-dependencies] 131 | dev = [ 132 | { name = "nox" }, 133 | { name = "ruff" }, 134 | ] 135 | 136 | [package.metadata] 137 | requires-dist = [ 138 | { name = "packaging" }, 139 | { name = "terminaltables" }, 140 | ] 141 | 142 | [package.metadata.requires-dev] 143 | dev = [ 144 | { name = "nox", specifier = ">=2022.1.7" }, 145 | { name = "ruff", specifier = ">=0.11.5" }, 146 | ] 147 | 148 | [[package]] 149 | name = "platformdirs" 150 | version = "4.3.6" 151 | source = { registry = "https://pypi.org/simple" } 152 | resolution-markers = [ 153 | "python_full_version < '3.9'", 154 | ] 155 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } 156 | wheels = [ 157 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, 158 | ] 159 | 160 | [[package]] 161 | name = "platformdirs" 162 | version = "4.3.7" 163 | source = { registry = "https://pypi.org/simple" } 164 | resolution-markers = [ 165 | "python_full_version >= '3.9'", 166 | ] 167 | sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } 168 | wheels = [ 169 | { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, 170 | ] 171 | 172 | [[package]] 173 | name = "ruff" 174 | version = "0.11.5" 175 | source = { registry = "https://pypi.org/simple" } 176 | sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } 177 | wheels = [ 178 | { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, 179 | { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, 180 | { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, 181 | { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, 182 | { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, 183 | { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, 184 | { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, 185 | { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, 186 | { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, 187 | { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, 188 | { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, 189 | { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, 190 | { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, 191 | { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, 192 | { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, 193 | { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, 194 | { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, 195 | ] 196 | 197 | [[package]] 198 | name = "terminaltables" 199 | version = "3.1.10" 200 | source = { registry = "https://pypi.org/simple" } 201 | sdist = { url = "https://files.pythonhosted.org/packages/f5/fc/0b73d782f5ab7feba8d007573a3773c58255f223c5940a7b7085f02153c3/terminaltables-3.1.10.tar.gz", hash = "sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543", size = 12264 } 202 | wheels = [ 203 | { url = "https://files.pythonhosted.org/packages/c4/fb/ea621e0a19733e01fe4005d46087d383693c0f4a8f824b47d8d4122c87e0/terminaltables-3.1.10-py2.py3-none-any.whl", hash = "sha256:e4fdc4179c9e4aab5f674d80f09d76fa436b96fdc698a8505e0a36bf0804a874", size = 15155 }, 204 | ] 205 | 206 | [[package]] 207 | name = "tomli" 208 | version = "2.2.1" 209 | source = { registry = "https://pypi.org/simple" } 210 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 211 | wheels = [ 212 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 213 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 214 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 215 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 216 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 217 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 218 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 219 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 220 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 221 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 222 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 223 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 224 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 225 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 226 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 227 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 228 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 229 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 230 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 231 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 232 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 233 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 234 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 235 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 236 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 237 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 238 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 239 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 240 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 241 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 242 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 243 | ] 244 | 245 | [[package]] 246 | name = "virtualenv" 247 | version = "20.30.0" 248 | source = { registry = "https://pypi.org/simple" } 249 | dependencies = [ 250 | { name = "distlib" }, 251 | { name = "filelock", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 252 | { name = "filelock", version = "3.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 253 | { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, 254 | { name = "platformdirs", version = "4.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, 255 | ] 256 | sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } 257 | wheels = [ 258 | { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, 259 | ] 260 | --------------------------------------------------------------------------------