|
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