11#!/usr/bin/env python
22
3- """
4- pip-check gives you a quick overview of all installed packages and their
5- update status. Under the hood it calls
6-
7- `pip list --outdated --format=columns`
3+ """pip-check.
84
5+ pip-check gives you a quick overview of all installed packages and their
6+ update status. Under the hood it calls `pip list --outdated --format=columns`
97and transforms it into a more user friendly table.
10-
11- Requires ``pip`` Version 9 or higher!
12-
13- Installation::
14-
15- pip install pip-check
16-
17- Usage::
18-
19- $ pip-check -h
20-
21- usage: pip-check [-h] [-a] [-c PIP_CMD] [-l] [-r] [-f] [-H] [-u] [-U]
22-
23- A quick overview of all installed packages and their update status.
24-
25- optional arguments:
26- -h, --help show this help message and exit
27- -a, --ascii Display as ASCII Table
28- -c PIP_CMD, --cmd PIP_CMD
29- The pip executable to run. Default: `pip`
30- -l, --local Show only virtualenv installed packages.
31- -r, --not-required List only packages that are not dependencies of installed packages.
32- -f, --full-version Show full version strings.
33- -H, --hide-unchanged Do not show "unchanged" packages.
34- -u, --show-update Show update instructions for updatable packages.
35- -U, --user Show only user installed packages.
368"""
379
10+ from __future__ import annotations
11+
3812import argparse
13+ import contextlib
3914import json
15+ import shlex
4016import subprocess
4117import sys
4218from collections import OrderedDict
5026
5127# The `pip` command to run. Normally `pip` but you can specify
5228# it using the `--cmd=pip` argument.
53- #
54- # pip-check --cmd=pip3
5529pip_cmd = "pip"
5630
57-
5831# The complete command to run to get a JSON list of outdated packages
5932pip_not_required_arg = "--not-required"
6033pip_user_arg = "--user"
6134pip_local_arg = "--local"
62-
6335pip_outdated_cmd = "{cmd} list --outdated --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}"
6436pip_current_cmd = "{cmd} list --uptodate --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}"
6537uv_outdated_cmd = "{cmd} list --outdated --format=json {notreq_arg}"
7446err = sys .stderr .write
7547out = sys .stdout .write
7648
49+
50+ # ------------------------------------------------------------------------------
51+ # Functions
7752# ------------------------------------------------------------------------------
7853
7954
80- def check_pip_version (options ):
55+ def split_command (cmd : str ) -> list [str ]:
56+ """Split a command string into a list of properly escaped and quoted substrings.
57+
58+ This function takes a single command string as input, splits it into substrings,
59+ and ensures each substring is properly escaped and shell-quoted. It leverages
60+ the `shlex.split` method to perform parsing and tokenization.
61+
62+ Args:
63+ cmd (str): The command string to be split and quoted.
64+
65+ Returns:
66+ list[str]: A list of shell-quoted substrings derived from the input command.
67+
8168 """
82- Make sure minimum pip version is met.
69+ return [shlex .quote (s ) for s in shlex .split (cmd )]
70+
71+
72+ def get_pip_version (options : argparse .Namespace ) -> str :
73+ """Retrieve the version of pip by executing the provided pip command.
74+
75+ This function runs the pip command specified in the options parameter to fetch
76+ the current installed pip version. If the command execution fails or does not
77+ return a version string, the process will terminate with an error.
78+
79+ Arguments:
80+ options (argparse.Namespace): A namespace object that encompasses the
81+ configurations needed for the command execution. This includes pip
82+ command to use, additional arguments for specifying package scope,
83+ and other related options.
84+
85+ Returns:
86+ str: The output of the executed pip command, which includes the pip version.
87+
88+ Raises:
89+ subprocess.CalledProcessError: If the pip command fails during execution.
90+ SystemExit: If the pip command execution fails or does not return a valid
91+ version string.
92+
8393 """
84- cmd = "{ pip_cmd} --version". format ( pip_cmd = options . pip_cmd )
94+ cmd = f" { options . pip_cmd } --version"
8595
8696 try :
87- cmd_response = subprocess .run (
88- cmd ,
89- shell = True ,
90- stdout = subprocess .PIPE ,
91- stderr = subprocess .PIPE ,
92- check = True ,
97+ cmd_response = subprocess .run ( # noqa: S603
98+ split_command (cmd ), capture_output = True , check = True , text = True
9399 )
94100 except subprocess .CalledProcessError as e :
95- err (
96- "The pip command did not succeed: {stderr}" .format (
97- stderr = e .stderr .decode ("utf-8" )
98- )
99- )
101+ err (f"The pip command did not succeed: { e .stderr } " )
100102 sys .exit (1 )
101103
102- cmd_response_string = cmd_response .stdout .decode ( "utf-8" ). strip ()
104+ cmd_response_string = cmd_response .stdout .strip ()
103105
104106 if not cmd_response_string :
105107 err (
@@ -111,11 +113,36 @@ def check_pip_version(options):
111113 return cmd_response_string
112114
113115
114- def get_package_versions (options , outdated_only = True ):
115- """
116- Retrieve a list of outdated packages from pip. Calls:
116+ def get_package_versions (
117+ options : argparse .Namespace , * , outdated_only : bool = True
118+ ) -> dict :
119+ """Fetch and parses the package version information using pip command.
120+
121+ This function executes a pip command to retrieve the package versions,
122+ either limited to outdated packages or including all packages based on the
123+ outdated_only flag. The command is constructed based on the provided options
124+ and executed using subprocess. Results are captured and parsed from JSON
125+ for further processing. Errors during execution, connection issues, or JSON
126+ parsing errors are handled, and appropriate error messages are displayed
127+ to the user. The program will exit with relevant status codes upon encountering
128+ errors.
129+
130+ Arguments:
131+ options (argparse.Namespace): A namespace object that encompasses the
132+ configurations needed for the command execution. This includes pip
133+ command to use, additional arguments for specifying package scope,
134+ and other related options.
135+ outdated_only (bool): Optional flag to determine whether to fetch only
136+ outdated packages (default is True) or all package versions.
137+
138+ Returns:
139+ dict: A dictionary containing package version details parsed from the
140+ pip command output.
141+
142+ Raises:
143+ SystemExit: Raised when execution of the pip command fails, HTTP
144+ connection issues are detected, or JSON parsing fails.
117145
118- [uv] pip list [--outdated|--uptodate] --format=json [--not-required] [--user] [--local]
119146 """
120147 if outdated_only :
121148 check_cmd = (
@@ -134,8 +161,8 @@ def get_package_versions(options, outdated_only=True):
134161 )
135162
136163 try :
137- cmd_response = subprocess .run (
138- cmd , shell = True , stdout = subprocess . PIPE , stderr = subprocess . PIPE
164+ cmd_response = subprocess .run ( # noqa: S603
165+ split_command ( cmd ), check = False , capture_output = True , text = True
139166 )
140167
141168 except subprocess .CalledProcessError as e :
@@ -147,23 +174,22 @@ def get_package_versions(options, outdated_only=True):
147174 sys .exit (1 )
148175
149176 # The pip command exited with 0 but we have stderr content:
150- if cmd_response .stderr :
151- if "NewConnectionError" in cmd_response .stderr .decode ("utf-8" ).strip ():
152- err (
153- "\n pip indicated that it has connection problems. "
154- "Please check your network.\n "
155- )
156- sys .exit (1 )
177+ if cmd_response .stderr and "NewConnectionError" in cmd_response .stderr :
178+ err (
179+ "\n pip indicated that it has connection problems. "
180+ "Please check your network.\n "
181+ )
182+ sys .exit (1 )
157183
158- cmd_response_string = cmd_response .stdout .decode ( "utf-8" ). strip ()
184+ cmd_response_string = cmd_response .stdout .strip ()
159185
160186 if not cmd_response_string :
161187 err ("No outdated packages. \\ o/" )
162188 sys .exit (0 )
163189
164190 try :
165191 pip_packages = json .loads (cmd_response_string )
166- except Exception : # Py2 raises ValueError, Py3 JSONEexception
192+ except json . JSONDecodeError :
167193 err (
168194 "Unable to parse the version list from pip. "
169195 "Does `pip list --format=json` work for you?\n "
@@ -173,7 +199,15 @@ def get_package_versions(options, outdated_only=True):
173199 return pip_packages
174200
175201
176- def main ():
202+ def main () -> None : # noqa: C901 PLR0912 PLR0915 # Ignore too complex warning
203+ """Parse command-line arguments and show package list.
204+
205+ Provides functionality for displaying and categorizing installed Python packages
206+ along with their update statuses. The program supports options to filter
207+ packages, show update instructions, and display formatted tables. Package
208+ classifications include major updates, minor updates, unchanged, and unknown
209+ statuses.
210+ """
177211 parser = argparse .ArgumentParser (
178212 description = "A quick overview of all installed packages "
179213 "and their update status. Supports `pip` or `uv pip`."
@@ -244,12 +278,12 @@ def main():
244278 options = parser .parse_args ()
245279
246280 # The pip check factory
247- current_pip_version = check_pip_version (options )
281+ current_pip_version = get_pip_version (options )
248282
249283 # --------------------------------------------------------------------------
250284
251- sys .stdout .write ("Python %s \n " % sys .version )
252- sys .stdout .write ("%s \n " % current_pip_version )
285+ sys .stdout .write (f "Python { sys .version } \n " )
286+ sys .stdout .write (f" { current_pip_version } \n " )
253287
254288 sys .stdout .write ("\n Loading package versions...\n " )
255289
@@ -306,23 +340,23 @@ def main():
306340
307341 table_data = OrderedDict ()
308342
309- def cut_version (version ) :
343+ def cut_version (version : str ) -> str :
310344 if not version or version == "Unknown" :
311345 return version
312346
313347 # Cut version to readable length
314348 if not options .show_long_versions and len (version ) > version_length + 3 :
315- return "{0}..." . format ( version [:version_length ])
349+ return f" { version [:version_length ]} ..."
316350 return version
317351
318- def columns (package ) :
319- # Generate the columsn for the table(s) for each package
352+ def columns (package_data : dict ) -> list [ str ] | None :
353+ # Generate the columns for the table(s) for each package
320354 # Name | Current Version | Latest Version | pypi String
321355
322- name = package .get ("name" )
323- current_version = package .get ("version" , None )
324- latest_version = package .get ("latest_version" , None )
325- help_string = "https://pypi.python.org/pypi/{}" .format (package ["name" ])
356+ name = package_data .get ("name" )
357+ current_version = package_data .get ("version" )
358+ latest_version = package_data .get ("latest_version" )
359+ help_string = "https://pypi.python.org/pypi/{}" .format (package_data ["name" ])
326360
327361 if latest_version and options .show_update :
328362 help_string = "pip install {user}{name}=={version}" .format (
@@ -338,7 +372,7 @@ def columns(package):
338372 help_string ,
339373 ]
340374
341- for key , label , color in [
375+ for key , label , _ in [
342376 ("major" , "Major Release Update" , "autored" ),
343377 ("minor" , "Minor Release Update" , "autoyellow" ),
344378 ("unchanged" , "Unchanged Packages" , "autogreen" ),
@@ -349,17 +383,17 @@ def columns(package):
349383 table_data [key ] = []
350384
351385 (table_data [key ].append ([label , "Version" , "Latest" ]),)
352- for package in packages [key ]:
353- table_data [key ].append (columns (package ))
386+ for line_package in packages [key ]:
387+ table_data [key ].append (columns (line_package ))
354388
355389 # Table output class
356- Table = (
390+ table_class = (
357391 terminaltables .AsciiTable if options .ascii_only else terminaltables .SingleTable
358392 )
359393
360- for key , data in table_data .items ():
394+ for data in table_data .values ():
361395 out ("\n " )
362- table = Table (data )
396+ table = table_class (data )
363397 out (table .table )
364398 out ("\n " )
365399 sys .stdout .flush ()
@@ -381,7 +415,5 @@ def columns(package):
381415# ------------------------------------------------------------------------------
382416
383417if __name__ == "__main__" :
384- try :
418+ with contextlib . suppress ( KeyboardInterrupt ) :
385419 main ()
386- except KeyboardInterrupt :
387- pass
0 commit comments