Skip to content

Commit f9becc9

Browse files
committed
Improve comments
1 parent 5045fe0 commit f9becc9

1 file changed

Lines changed: 118 additions & 86 deletions

File tree

src/pip_check/__init__.py

100644100755
Lines changed: 118 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,18 @@
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`
97
and 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+
3812
import argparse
13+
import contextlib
3914
import json
15+
import shlex
4016
import subprocess
4117
import sys
4218
from collections import OrderedDict
@@ -50,16 +26,12 @@
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
5529
pip_cmd = "pip"
5630

57-
5831
# The complete command to run to get a JSON list of outdated packages
5932
pip_not_required_arg = "--not-required"
6033
pip_user_arg = "--user"
6134
pip_local_arg = "--local"
62-
6335
pip_outdated_cmd = "{cmd} list --outdated --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}"
6436
pip_current_cmd = "{cmd} list --uptodate --retries=1 --disable-pip-version-check --format=json {notreq_arg} {user_arg} {local_arg}"
6537
uv_outdated_cmd = "{cmd} list --outdated --format=json {notreq_arg}"
@@ -74,32 +46,62 @@
7446
err = sys.stderr.write
7547
out = 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-
"\npip 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+
"\npip 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("\nLoading 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

383417
if __name__ == "__main__":
384-
try:
418+
with contextlib.suppress(KeyboardInterrupt):
385419
main()
386-
except KeyboardInterrupt:
387-
pass

0 commit comments

Comments
 (0)