|
1 |
| -"""Check that your requirements.txt is up to date with the most recent packageversions. |
2 |
| -""" |
3 |
| -from __future__ import annotations |
| 1 | +import subprocess |
| 2 | +import sys |
4 | 3 |
|
5 |
| -import argparse |
6 |
| -import typing |
7 |
| -from pathlib import Path |
8 |
| -from sys import exit as sysexit |
9 |
| -from sys import stdout |
10 | 4 |
|
11 |
| -import requests |
12 |
| -import requirements |
13 |
| -from requirements.requirement import Requirement |
14 |
| - |
15 |
| -stdout.reconfigure(encoding="utf-8") |
16 |
| - |
17 |
| - |
18 |
| -class UpdateCompatible(typing.TypedDict): |
19 |
| - """UpdateCompatible type.""" |
20 |
| - |
21 |
| - ver: str |
22 |
| - compatible: bool |
23 |
| - |
24 |
| - |
25 |
| -class Dependency(typing.TypedDict): |
26 |
| - """Dependency type.""" |
27 |
| - |
28 |
| - name: str |
29 |
| - specs: tuple[str] |
30 |
| - ver: str |
31 |
| - compatible: bool |
32 |
| - |
33 |
| - |
34 |
| -def semver(version: str) -> list[str]: |
35 |
| - """Convert a semver/ python-ver string to a list in the form major, minor patch |
36 |
| -
|
37 |
| - Args: |
38 |
| - version (str): The version to convert |
39 |
| -
|
40 |
| - Returns: |
41 |
| - list[str]: A list in the form major, minor, patch |
42 |
| - """ |
43 |
| - return version.split(".") |
44 |
| - |
45 |
| - |
46 |
| -def semPad(ver: list[str], length: int) -> list[str]: |
47 |
| - """Pad a semver list to the required size. e.g. ["1", "0"] to ["1", "0", "0"]. |
48 |
| -
|
49 |
| - Args: |
50 |
| - ver (list[str]): the semver representation |
51 |
| - length (int): the new length |
52 |
| -
|
53 |
| - Returns: |
54 |
| - list[str]: the new semver |
55 |
| - """ |
56 |
| - char = "0" |
57 |
| - if ver[-1] == "*": |
58 |
| - char = "*" |
59 |
| - return ver + [char] * (length - len(ver)) |
60 |
| - |
61 |
| - |
62 |
| -def partCmp(verA: str, verB: str) -> int: |
63 |
| - """Compare parts of a semver. |
64 |
| -
|
65 |
| - Args: |
66 |
| - verA (str): lhs part to compare |
67 |
| - verB (str): rhs part to compare |
68 |
| -
|
69 |
| - Returns: |
70 |
| - int: 0 if equal, 1 if verA > verB and -1 if verA < verB |
71 |
| - """ |
72 |
| - if verA == verB or verA == "*" or verB == "*": |
73 |
| - return 0 |
74 |
| - if int(verA) > int(verB): |
75 |
| - return 1 |
76 |
| - return -1 |
77 |
| - |
78 |
| - |
79 |
| -def _doSemCmp(semA: list[str], semB: list[str], sign: str) -> bool: |
80 |
| - """Compare two semvers of equal length. e.g. 1.1.1 and 2.2.2. |
81 |
| -
|
82 |
| - Args: |
83 |
| - semA (list[str]): lhs to compare |
84 |
| - semB (list[str]): rhs to compare |
85 |
| - sign (str): string sign. one of ==, ~=, <=, >=, <, > |
86 |
| -
|
87 |
| - Raises: |
88 |
| - ValueError: if the sign is not one of the following. or the semvers |
89 |
| - have differing lengths |
90 |
| -
|
91 |
| - Returns: |
92 |
| - bool: true if the comparison is met. e.g. 1.1.1, 2.2.2, <= -> True |
93 |
| - """ |
94 |
| - if len(semA) != len(semB): |
95 |
| - raise ValueError |
96 |
| - # Equal. e.g. 1.1.1 == 1.1.1 |
97 |
| - if sign == "==": |
98 |
| - for index, _elem in enumerate(semA): |
99 |
| - if partCmp(semA[index], semB[index]) != 0: |
100 |
| - return False |
101 |
| - return True |
102 |
| - # Compatible. e.g. 1.1.2 ~= 1.1.1 |
103 |
| - if sign == "~=": |
104 |
| - for index, _elem in enumerate(semA[:-1]): |
105 |
| - if partCmp(semA[index], semB[index]) != 0: |
106 |
| - return False |
107 |
| - if partCmp(semA[-1], semB[-1]) < 0: |
108 |
| - return False |
109 |
| - return True |
110 |
| - # Greater than or equal. e.g. 1.1.2 >= 1.1.1 |
111 |
| - if sign == ">=": |
112 |
| - for index, _elem in enumerate(semA): |
113 |
| - cmp = partCmp(semA[index], semB[index]) |
114 |
| - if cmp > 0: |
115 |
| - return True |
116 |
| - if cmp < 0: |
117 |
| - return False |
118 |
| - return True |
119 |
| - # Less than or equal. e.g. 1.1.1 <= 1.1.2 |
120 |
| - if sign == "<=": |
121 |
| - for index, _elem in enumerate(semA): |
122 |
| - cmp = partCmp(semA[index], semB[index]) |
123 |
| - if cmp < 0: |
124 |
| - return True |
125 |
| - if cmp > 0: |
126 |
| - return False |
127 |
| - return True |
128 |
| - # Greater than. e.g. 1.1.2 > 1.1.1 |
129 |
| - if sign == ">": |
130 |
| - for index, _elem in enumerate(semA): |
131 |
| - cmp = partCmp(semA[index], semB[index]) |
132 |
| - if cmp > 0: |
133 |
| - return True |
134 |
| - if cmp < 0: |
135 |
| - return False |
136 |
| - return False |
137 |
| - # Less than. e.g. 1.1.1 < 1.1.2 |
138 |
| - if sign == "<": |
139 |
| - for index, _elem in enumerate(semA): |
140 |
| - cmp = partCmp(semA[index], semB[index]) |
141 |
| - if cmp < 0: |
142 |
| - return True |
143 |
| - if cmp > 0: |
144 |
| - return False |
145 |
| - return False |
146 |
| - raise ValueError |
147 |
| - |
148 |
| - |
149 |
| -def semCmp(versionA: str, versionB: str, sign: str) -> bool: |
150 |
| - """Compare two semvers of any length. e.g. 1.1 and 2.2.2. |
151 |
| -
|
152 |
| - Args: |
153 |
| - versionA (list[str]): lhs to compare |
154 |
| - versionB (list[str]): rhs to compare |
155 |
| - sign (str): string sign. one of ==, ~=, <=, >=, <, > |
156 |
| -
|
157 |
| - Raises: |
158 |
| - ValueError: if the sign is not one of the following. |
159 |
| -
|
160 |
| - Returns: |
161 |
| - bool: true if the comparison is met. e.g. 1.1.1, 2.2.2, <= -> True |
162 |
| - """ |
163 |
| - semA = semver(versionA) |
164 |
| - semB = semver(versionB) |
165 |
| - semLen = max(len(semA), len(semB)) |
166 |
| - return _doSemCmp(semPad(semA, semLen), semPad(semB, semLen), sign) |
167 |
| - |
168 |
| - |
169 |
| -def updateCompatible(req: Requirement) -> UpdateCompatible: |
170 |
| - """Check if the most recent version of a python requirement is compatible with |
171 |
| - the current version. |
172 |
| -
|
173 |
| - Args: |
174 |
| - req (Requirement): the requirement object as parsed by requirements_parser |
175 |
| -
|
176 |
| - Returns: |
177 |
| - UpdateCompatible: return a dict of the most recent version (ver) and |
178 |
| - is our requirement from requirements.txt or similar compatible |
179 |
| - with the new version per the version specifier (compatible) |
180 |
| - """ |
181 |
| - url = f"https://pypi.org/pypi/{req.name}/json" |
182 |
| - request = requests.get(url) |
183 |
| - updateVer = request.json()["info"]["version"] |
184 |
| - for spec in req.specs: |
185 |
| - if not semCmp(updateVer, spec[1], spec[0]): |
186 |
| - return {"ver": updateVer, "compatible": False} |
187 |
| - return {"ver": updateVer, "compatible": True} |
188 |
| - |
189 |
| - |
190 |
| -def checkRequirements(requirementsFile: str) -> list[Dependency]: |
191 |
| - """Check that your requirements.txt is up to date with the most recent package |
192 |
| - versions. Put in a function so dependants can use this function rather than |
193 |
| - reimplement it themselves. |
194 |
| -
|
195 |
| - Args: |
196 |
| - requirementsFile (str): file path to the requirements file |
197 |
| -
|
198 |
| - Returns: |
199 |
| - Dependency: dictionary containing info on each requirement such as the name, |
200 |
| - specs (from requirements_parser), ver (most recent version), compatible |
201 |
| - (is our version compatible with ver) |
202 |
| - """ |
203 |
| - reqsDict = [] |
204 |
| - for req in requirements.parse(Path(requirementsFile).read_text(encoding="utf-8")): # type: ignore |
205 |
| - reqsDict.append( |
206 |
| - {"name": req.name, "specs": req.specs, **updateCompatible(req)} |
207 |
| - ) # type: ignore |
208 |
| - return reqsDict |
| 5 | +def checkForOutdatedPackages(): |
| 6 | + cmd = ["poetry", "show", "--outdated"] |
| 7 | + result = subprocess.run(cmd, capture_output=True, text=True) |
| 8 | + return result.stdout.strip().split("\n")[1:] |
209 | 9 |
|
210 | 10 |
|
211 | 11 | def cli():
|
212 |
| - """CLI entry point.""" |
213 |
| - parser = argparse.ArgumentParser(description=__doc__) |
214 |
| - # yapf: disable |
215 |
| - parser.add_argument("--requirements-file", "-r", |
216 |
| - help="requirements file") |
217 |
| - parser.add_argument("--zero", "-0", |
218 |
| - help="Return non zero exit code if an incompatible license is found", action="store_true") |
219 |
| - # yapf: enable |
220 |
| - args = parser.parse_args() |
221 |
| - reqsDict = checkRequirements( |
222 |
| - args.requirements_file if args.requirements_file else "requirements.txt" |
223 |
| - ) |
224 |
| - if len(reqsDict) == 0: |
225 |
| - print("/ WARN: No requirements") |
226 |
| - incompat = False |
227 |
| - for req in reqsDict: |
228 |
| - name = req["name"] |
229 |
| - if req["compatible"]: |
230 |
| - print(f"+ OK: {name}") |
231 |
| - else: |
232 |
| - print(f"+ ERROR: {name}") |
233 |
| - incompat = True |
234 |
| - if incompat and args.zero: |
235 |
| - sysexit(1) |
236 |
| - sysexit(0) |
| 12 | + outdatedPackages = checkForOutdatedPackages() |
| 13 | + if outdatedPackages: |
| 14 | + print("Outdated packages (powered by poetry):") |
| 15 | + for package in outdatedPackages: |
| 16 | + print(package) |
| 17 | + else: |
| 18 | + print("No outdated packages.") |
| 19 | + |
| 20 | + sys.exit(1 if outdatedPackages else 0) |
0 commit comments