forceupdate 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forceupdate/__init__.py +9 -0
- forceupdate/__main__.py +5 -0
- forceupdate/advisories/__init__.py +30 -0
- forceupdate/advisories/cvss.py +95 -0
- forceupdate/advisories/osv.py +584 -0
- forceupdate/checks/__init__.py +49 -0
- forceupdate/checks/base.py +98 -0
- forceupdate/checks/deprecated.py +115 -0
- forceupdate/checks/outdated.py +183 -0
- forceupdate/checks/priority.py +82 -0
- forceupdate/checks/vulns.py +201 -0
- forceupdate/cli.py +309 -0
- forceupdate/detect/__init__.py +5 -0
- forceupdate/detect/languages.py +266 -0
- forceupdate/detect/manifests.py +478 -0
- forceupdate/engine.py +313 -0
- forceupdate/models.py +510 -0
- forceupdate/net/__init__.py +20 -0
- forceupdate/net/cache.py +217 -0
- forceupdate/net/http.py +270 -0
- forceupdate/parsers/__init__.py +43 -0
- forceupdate/parsers/base.py +22 -0
- forceupdate/parsers/json_spans.py +123 -0
- forceupdate/parsers/lockfiles.py +231 -0
- forceupdate/parsers/npm.py +1257 -0
- forceupdate/parsers/python.py +1421 -0
- forceupdate/parsers/toml_spans.py +297 -0
- forceupdate/registries/__init__.py +52 -0
- forceupdate/registries/base.py +294 -0
- forceupdate/registries/npm.py +153 -0
- forceupdate/registries/pypi.py +242 -0
- forceupdate/report/__init__.py +359 -0
- forceupdate/report/json.py +34 -0
- forceupdate/report/markdown.py +169 -0
- forceupdate/report/terminal.py +376 -0
- forceupdate/sources/__init__.py +1 -0
- forceupdate/sources/base.py +45 -0
- forceupdate/sources/local.py +183 -0
- forceupdate/update/__init__.py +8 -0
- forceupdate/update/constraints.py +317 -0
- forceupdate/update/planner.py +531 -0
- forceupdate/versions/__init__.py +40 -0
- forceupdate/versions/base.py +229 -0
- forceupdate/versions/npm.py +432 -0
- forceupdate/versions/pypi.py +220 -0
- forceupdate-0.1.0.dist-info/METADATA +79 -0
- forceupdate-0.1.0.dist-info/RECORD +50 -0
- forceupdate-0.1.0.dist-info/WHEEL +4 -0
- forceupdate-0.1.0.dist-info/entry_points.txt +2 -0
- forceupdate-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Réécrire une contrainte pour une nouvelle version, en gardant son style.
|
|
2
|
+
|
|
3
|
+
``^4.17.0`` devient ``^4.21.2``, pas ``4.21.2`` (brief, « Garder le style de contrainte ») :
|
|
4
|
+
seuls les numéros de version changent. Les opérateurs, les espaces, l'ordre, les préfixes
|
|
5
|
+
(``v``, ``=``) et le reste du texte sont gardés.
|
|
6
|
+
|
|
7
|
+
La règle : la nouvelle contrainte accepte la version visée, et sa borne basse monte jusqu'à
|
|
8
|
+
elle (``>=2.25,<3`` vers 3.1 : ``>=3.1,<4``). Par style :
|
|
9
|
+
|
|
10
|
+
- **version exacte** (``==1.2.3``, ``1.2.3`` npm, version nue Poetry) et **borne basse**
|
|
11
|
+
(``>=``, ``>``) : la version visée ; ``>`` devient ``>=``, sinon elle s'exclurait ;
|
|
12
|
+
- ``^`` et ``~`` (npm, Poetry) : la version visée derrière le même opérateur ;
|
|
13
|
+
- **borne haute** (``<``, ``<=``) : déplacée seulement si elle exclut la version visée, au même
|
|
14
|
+
niveau que celui qu'elle fixait (``<3`` vers 3.1 : ``<4`` ; ``<1.5`` vers 1.7.2 : ``<1.8``) ;
|
|
15
|
+
- ``!=`` est gardé tel quel ;
|
|
16
|
+
- **union** (``^1 || ^2``) : remplacée par le membre qui accepte la version visée, ou par le
|
|
17
|
+
dernier, réécrit ;
|
|
18
|
+
- **dépendance pair** (``widen``) : la plage dit avec quelles versions le paquet fonctionne ;
|
|
19
|
+
la remplacer retirerait les anciennes à ses utilisateurs. Un membre est ajouté à la fin
|
|
20
|
+
(``^18.0.0`` vers 19.1.0 : ``^18.0.0 || ^19.1.0`` ; ``^1 || ^2`` : ``^1 || ^2 || ^3.1.0``).
|
|
21
|
+
|
|
22
|
+
**Précision.** Le nombre de nombres écrits est gardé quand la version visée s'y écrit sans
|
|
23
|
+
rien perdre (``^1`` vers 2.0.0 : ``^2`` ; ``==1.2`` vers 1.3.0 : ``==1.3``). Sinon la version
|
|
24
|
+
visée est écrite en entier, sauf là où le nombre de nombres change le sens de la contrainte :
|
|
25
|
+
|
|
26
|
+
- ``~=1.4`` veut dire ``>=1.4, ==1.*``, mais ``~=1.4.2`` veut dire ``==1.4.*`` : ``~=`` garde
|
|
27
|
+
toujours sa précision (``~=1.4`` vers 1.9.2 : ``~=1.9``) ;
|
|
28
|
+
- les jokers (``1.2.*``, ``1.x``, la version partielle ``1.2`` de npm) aussi : ``2.3.*`` ;
|
|
29
|
+
- ``~1`` va jusqu'à 2, ``~1.3.4`` seulement jusqu'à 1.4 ; ``^0`` jusqu'à 1, ``^0.3.1`` seulement
|
|
30
|
+
jusqu'à 0.4 : ``~`` à un seul nombre, et ``^`` dont le premier nombre non nul de la version
|
|
31
|
+
visée tomberait hors de la précision écrite, gardent leur précision.
|
|
32
|
+
|
|
33
|
+
**Plancher** (``floor``) : pour une faille, la borne basse doit atteindre la version visée,
|
|
34
|
+
sinon la nouvelle contrainte accepterait encore des versions concernées. Quand le style ne le
|
|
35
|
+
permet pas (``~=1.9`` pour 1.9.2, ``2.3.*``), une borne ``>=`` est ajoutée :
|
|
36
|
+
``~=1.9,>=1.9.2``, ``2.x >=2.3.1``.
|
|
37
|
+
|
|
38
|
+
Le résultat est relu : il doit accepter la version visée (et respecter le plancher). Sinon, ou
|
|
39
|
+
pour un style qu'on ne sait pas réécrire (intervalle npm ``1.2.3 - 2.3.4`` dépassé, contrainte
|
|
40
|
+
sans version comme ``*`` ou une étiquette), ``rewrite`` rend ``None`` : à réécrire à la main.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
|
|
46
|
+
from forceupdate import versions
|
|
47
|
+
from forceupdate.models import Ecosystem
|
|
48
|
+
from forceupdate.versions import InvalidConstraint, InvalidVersion, VersionScheme
|
|
49
|
+
|
|
50
|
+
_NPM_OPERATORS = r"~>|<=|>=|[<>=~^]"
|
|
51
|
+
_PYPI_OPERATORS = r"===|==|!=|<>|<=|>=|~=|[<>=~^]"
|
|
52
|
+
_VERSION_TEXT = r"[vV]?(?:\d|[xX*])[0-9A-Za-z.!+*_-]*"
|
|
53
|
+
"""Une version telle qu'écrite dans une contrainte : complète, partielle, avec joker,
|
|
54
|
+
pré-version ou version locale. Le découpage fin est fait par ``_NUMBERS``."""
|
|
55
|
+
|
|
56
|
+
_NUMBERS = re.compile(
|
|
57
|
+
r"(?P<prefix>[vV=]*)(?P<epoch>\d+!)?(?P<numbers>(?:\d+|[xX*])(?:\.(?:\d+|[xX*]))*)"
|
|
58
|
+
r"(?P<rest>.*)",
|
|
59
|
+
re.ASCII,
|
|
60
|
+
)
|
|
61
|
+
_PLAIN = re.compile(r"(?P<epoch>\d+!)?(?P<numbers>\d+(?:\.\d+)*)(?P<rest>.*)", re.ASCII)
|
|
62
|
+
_HYPHEN = re.compile(r"(?P<first>\S+)(?P<middle>\s+-\s+)(?P<last>\S+)")
|
|
63
|
+
_UNION = re.compile(r"\s*\|\|?\s*")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class _Comparator:
|
|
68
|
+
"""Un comparateur tel qu'écrit dans un membre de contrainte."""
|
|
69
|
+
|
|
70
|
+
operator: str
|
|
71
|
+
operator_span: tuple[int, int]
|
|
72
|
+
version: str
|
|
73
|
+
version_span: tuple[int, int]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class _Target:
|
|
78
|
+
"""La version visée, découpée : ses nombres, et ce qui les suit (pré-version, ``.post0``…)."""
|
|
79
|
+
|
|
80
|
+
text: str
|
|
81
|
+
epoch: str
|
|
82
|
+
numbers: tuple[int, ...]
|
|
83
|
+
rest: str
|
|
84
|
+
|
|
85
|
+
def written(self, count: int) -> str:
|
|
86
|
+
"""Ses ``count`` premiers nombres (complétés par des zéros), sans ce qui les suit."""
|
|
87
|
+
numbers = (*self.numbers, *([0] * count))[:count]
|
|
88
|
+
return self.epoch + ".".join(str(n) for n in numbers)
|
|
89
|
+
|
|
90
|
+
def fits(self, count: int) -> bool:
|
|
91
|
+
"""Vrai si ``count`` nombres l'écrivent sans rien perdre : ``2.0.0`` en deux (``2.0``),
|
|
92
|
+
pas ``2.0.1`` ni ``2.0.0rc1``."""
|
|
93
|
+
return not self.rest and all(n == 0 for n in self.numbers[count:])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def rewrite(
|
|
97
|
+
ecosystem: Ecosystem, constraint: str, target: str, *, floor: bool = False, widen: bool = False
|
|
98
|
+
) -> str | None:
|
|
99
|
+
"""La contrainte ``constraint`` réécrite pour ``target``, dans le même style (voir la
|
|
100
|
+
docstring du module) ; ``None`` si on ne sait pas la réécrire.
|
|
101
|
+
|
|
102
|
+
``floor`` : la borne basse doit atteindre ``target`` (une faille). ``widen`` : la plage
|
|
103
|
+
s'élargit d'un membre au lieu d'être remplacée (une dépendance pair), et reste telle quelle
|
|
104
|
+
si elle accepte déjà ``target``.
|
|
105
|
+
|
|
106
|
+
La borne basse monte toujours jusqu'à ``target`` (ou ce que le style en garde) : quand la
|
|
107
|
+
contrainte l'accepte déjà et qu'un lockfile la fige, c'est au planificateur de ne pas
|
|
108
|
+
réécrire la contrainte (le lockfile seul suffit).
|
|
109
|
+
"""
|
|
110
|
+
scheme = versions.for_ecosystem(ecosystem)
|
|
111
|
+
parsed_target = _split_target(target)
|
|
112
|
+
if parsed_target is None:
|
|
113
|
+
return None
|
|
114
|
+
members = _members(constraint)
|
|
115
|
+
try:
|
|
116
|
+
if len(members) > 1 or widen:
|
|
117
|
+
new = _rewrite_union(scheme, constraint, members, parsed_target, floor, widen)
|
|
118
|
+
else:
|
|
119
|
+
new = _rewrite_member(scheme, constraint.strip(), parsed_target, floor)
|
|
120
|
+
if new is None or not scheme.satisfies(new, target):
|
|
121
|
+
return None
|
|
122
|
+
if floor and not _reaches(scheme, new, target):
|
|
123
|
+
return None
|
|
124
|
+
except (InvalidConstraint, InvalidVersion):
|
|
125
|
+
return None
|
|
126
|
+
return new
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _split_target(target: str) -> _Target | None:
|
|
130
|
+
match = _PLAIN.fullmatch(target.strip().lstrip("vV="))
|
|
131
|
+
if match is None:
|
|
132
|
+
return None
|
|
133
|
+
numbers = tuple(int(n) for n in match.group("numbers").split("."))
|
|
134
|
+
return _Target(target, match.group("epoch") or "", numbers, match.group("rest"))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _members(constraint: str) -> list[str]:
|
|
138
|
+
"""Les membres d'une union (``||`` de npm et Poetry, ``|`` de Poetry)."""
|
|
139
|
+
return [part for part in _UNION.split(constraint.strip())]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _rewrite_union(
|
|
143
|
+
scheme: VersionScheme,
|
|
144
|
+
constraint: str,
|
|
145
|
+
members: list[str],
|
|
146
|
+
target: _Target,
|
|
147
|
+
floor: bool,
|
|
148
|
+
widen: bool,
|
|
149
|
+
) -> str | None:
|
|
150
|
+
containing = [m for m in members if m and scheme.satisfies(m, target.text)]
|
|
151
|
+
if widen:
|
|
152
|
+
if containing:
|
|
153
|
+
return constraint
|
|
154
|
+
added = _rewrite_member(scheme, members[-1], target, floor=False)
|
|
155
|
+
if added is None:
|
|
156
|
+
return None
|
|
157
|
+
separator = _UNION.search(constraint)
|
|
158
|
+
return constraint.rstrip() + (separator.group() if separator else " || ") + added
|
|
159
|
+
member = containing[0] if containing else members[-1]
|
|
160
|
+
return _rewrite_member(scheme, member, target, floor)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _rewrite_member(scheme: VersionScheme, text: str, target: _Target, floor: bool) -> str | None:
|
|
164
|
+
npm = scheme.ecosystem is Ecosystem.NPM
|
|
165
|
+
hyphen = _HYPHEN.fullmatch(text) if npm else None
|
|
166
|
+
if hyphen is not None:
|
|
167
|
+
return _rewrite_hyphen(scheme, hyphen, target)
|
|
168
|
+
comparators = [(c, _kind(c, npm=npm)) for c in _comparators(text, npm=npm)]
|
|
169
|
+
if all(kind == "any" for _, kind in comparators):
|
|
170
|
+
return None # « * », « x », une étiquette : pas de version à réécrire
|
|
171
|
+
replacements: list[tuple[tuple[int, int], str]] = []
|
|
172
|
+
for comparator, kind in comparators:
|
|
173
|
+
if kind == "any":
|
|
174
|
+
continue
|
|
175
|
+
if kind == "upper":
|
|
176
|
+
if not scheme.satisfies(comparator.operator + comparator.version, target.text):
|
|
177
|
+
shifted = _shift_upper(comparator.version, target)
|
|
178
|
+
if shifted is None:
|
|
179
|
+
return None
|
|
180
|
+
replacements.append((comparator.version_span, shifted))
|
|
181
|
+
continue
|
|
182
|
+
if kind == "exclude":
|
|
183
|
+
continue
|
|
184
|
+
written = _written(comparator, kind, target)
|
|
185
|
+
if written is None:
|
|
186
|
+
return None
|
|
187
|
+
replacements.append((comparator.version_span, written))
|
|
188
|
+
if comparator.operator == ">":
|
|
189
|
+
replacements.append((comparator.operator_span, ">="))
|
|
190
|
+
new = _apply(text, replacements)
|
|
191
|
+
if floor and not _reaches(scheme, new, target.text):
|
|
192
|
+
new += f" >={target.text}" if npm else f",>={target.text}"
|
|
193
|
+
return new
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _comparators(text: str, *, npm: bool) -> list[_Comparator]:
|
|
197
|
+
operators = _NPM_OPERATORS if npm else _PYPI_OPERATORS
|
|
198
|
+
pattern = re.compile(rf"(?P<op>{operators})?\s*(?P<version>{_VERSION_TEXT})", re.ASCII)
|
|
199
|
+
found = []
|
|
200
|
+
for match in pattern.finditer(text):
|
|
201
|
+
operator = match.group("op") or ""
|
|
202
|
+
found.append(
|
|
203
|
+
_Comparator(
|
|
204
|
+
operator=operator,
|
|
205
|
+
operator_span=match.span("op") if operator else (match.start(), match.start()),
|
|
206
|
+
version=match.group("version"),
|
|
207
|
+
version_span=match.span("version"),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
return found
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _kind(comparator: _Comparator, *, npm: bool) -> str:
|
|
214
|
+
"""Le rôle d'un comparateur : ``exact``, ``lower``, ``upper``, ``caret``, ``tilde``,
|
|
215
|
+
``compatible`` (``~=``), ``wildcard``, ``exclude`` (``!=``) ou ``any`` (``*``)."""
|
|
216
|
+
operator = comparator.operator
|
|
217
|
+
numbers = _NUMBERS.fullmatch(comparator.version)
|
|
218
|
+
count, wildcard = _count(numbers.group("numbers")) if numbers else (0, True)
|
|
219
|
+
if count == 0:
|
|
220
|
+
return "any"
|
|
221
|
+
if operator in ("<", "<="):
|
|
222
|
+
return "upper"
|
|
223
|
+
if operator in (">", ">="):
|
|
224
|
+
return "lower"
|
|
225
|
+
if operator == "^":
|
|
226
|
+
return "caret"
|
|
227
|
+
if operator in ("~", "~>"):
|
|
228
|
+
return "tilde"
|
|
229
|
+
if operator == "~=":
|
|
230
|
+
return "compatible"
|
|
231
|
+
if operator in ("!=", "<>"):
|
|
232
|
+
return "exclude"
|
|
233
|
+
if wildcard:
|
|
234
|
+
return "wildcard"
|
|
235
|
+
if npm and count < 3:
|
|
236
|
+
return "wildcard" # « 1.2 » veut dire « 1.2.x » pour npm
|
|
237
|
+
return "exact"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _count(numbers: str) -> tuple[int, bool]:
|
|
241
|
+
"""Le nombre de nombres écrits avant un éventuel joker, et s'il y a un joker."""
|
|
242
|
+
count = 0
|
|
243
|
+
for part in numbers.split("."):
|
|
244
|
+
if not part.isdigit():
|
|
245
|
+
return count, True
|
|
246
|
+
count += 1
|
|
247
|
+
return count, False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _written(comparator: _Comparator, kind: str, target: _Target) -> str | None:
|
|
251
|
+
"""Le nouveau texte de la version d'un comparateur qui porte la borne basse."""
|
|
252
|
+
numbers = _NUMBERS.fullmatch(comparator.version)
|
|
253
|
+
if numbers is None:
|
|
254
|
+
return None
|
|
255
|
+
prefix = numbers.group("prefix")
|
|
256
|
+
count, _ = _count(numbers.group("numbers"))
|
|
257
|
+
if kind == "wildcard":
|
|
258
|
+
return prefix + target.written(count) + _joker(numbers.group("numbers"), count)
|
|
259
|
+
if kind == "compatible":
|
|
260
|
+
return prefix + target.written(max(count, 2))
|
|
261
|
+
if target.fits(count):
|
|
262
|
+
return prefix + target.written(count)
|
|
263
|
+
if kind == "tilde" and count < 2:
|
|
264
|
+
return prefix + target.written(count)
|
|
265
|
+
if kind == "caret" and not any(n != 0 for n in target.numbers[:count]):
|
|
266
|
+
return prefix + target.written(count)
|
|
267
|
+
return prefix + target.text
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _joker(numbers: str, count: int) -> str:
|
|
271
|
+
"""Ce qui suit les ``count`` premiers nombres : le joker (``.x``, ``.*``), ou rien."""
|
|
272
|
+
return numbers[len(".".join(numbers.split(".")[:count])) :]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _shift_upper(upper: str, target: _Target) -> str | None:
|
|
276
|
+
"""Une borne haute déplacée au-dessus de ``target``, au même niveau : le dernier nombre non
|
|
277
|
+
nul de la borne dit ce qu'elle fixait (``<3`` une majeure, ``<1.5`` une mineure)."""
|
|
278
|
+
match = _NUMBERS.fullmatch(upper)
|
|
279
|
+
if match is None:
|
|
280
|
+
return None
|
|
281
|
+
count, wildcard = _count(match.group("numbers"))
|
|
282
|
+
if wildcard or count == 0:
|
|
283
|
+
return None
|
|
284
|
+
written = [int(n) for n in match.group("numbers").split(".")]
|
|
285
|
+
level = max((i for i, n in enumerate(written) if n != 0), default=0)
|
|
286
|
+
numbers = [*target.numbers, *([0] * count)][: level + 1]
|
|
287
|
+
numbers[level] += 1
|
|
288
|
+
numbers += [0] * (count - level - 1)
|
|
289
|
+
return match.group("prefix") + ".".join(str(n) for n in numbers) + match.group("rest")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _rewrite_hyphen(scheme: VersionScheme, hyphen: re.Match[str], target: _Target) -> str | None:
|
|
293
|
+
"""``1.2.3 - 2.3.4`` (npm) : le début devient la version visée ; une fin partielle
|
|
294
|
+
(``- 2``, ``- 2.3``) suit la version visée à la même précision. Une fin complète dépassée
|
|
295
|
+
n'a pas de règle évidente : à réécrire à la main."""
|
|
296
|
+
last = hyphen.group("last")
|
|
297
|
+
if not scheme.satisfies(f"0.0.0 - {last}", target.text):
|
|
298
|
+
match = _NUMBERS.fullmatch(last)
|
|
299
|
+
if match is None:
|
|
300
|
+
return None
|
|
301
|
+
count, wildcard = _count(match.group("numbers"))
|
|
302
|
+
if count >= 3 and not wildcard:
|
|
303
|
+
return None
|
|
304
|
+
last = match.group("prefix") + target.written(count) + _joker(match.group("numbers"), count)
|
|
305
|
+
return f"{target.text}{hyphen.group('middle')}{last}"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _apply(text: str, replacements: list[tuple[tuple[int, int], str]]) -> str:
|
|
309
|
+
for (start, end), new in sorted(replacements, reverse=True):
|
|
310
|
+
text = text[:start] + new + text[end:]
|
|
311
|
+
return text
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _reaches(scheme: VersionScheme, constraint: str, target: str) -> bool:
|
|
315
|
+
"""Vrai si la borne basse de ``constraint`` atteint ``target``."""
|
|
316
|
+
lower = scheme.lower_bound(constraint)
|
|
317
|
+
return lower is not None and scheme.compare(lower, target) >= 0
|