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,229 @@
|
|
|
1
|
+
"""L'interface commune des versions : lire, comparer, classer l'écart, tester une contrainte.
|
|
2
|
+
|
|
3
|
+
Chaque écosystème a ses règles (PEP 440 pour PyPI, semver pour npm…), et comparer
|
|
4
|
+
deux versions comme des chaînes donne ``"1.10" < "1.9"``. Les modèles gardent les
|
|
5
|
+
versions et les contraintes en chaînes, telles qu'écrites ; une ``VersionScheme``
|
|
6
|
+
les interprète. Toute comparaison de versions du code passe par elle.
|
|
7
|
+
|
|
8
|
+
Ce qui est commun à tous les écosystèmes est écrit ici une fois :
|
|
9
|
+
|
|
10
|
+
- l'écart entre deux versions (``gap``), avec la règle du 0.x ;
|
|
11
|
+
- les candidates : les pré-versions sont écartées, sauf celles de la version actuelle
|
|
12
|
+
quand c'en est une ;
|
|
13
|
+
- la plus haute version qui respecte une contrainte (``max_satisfying``).
|
|
14
|
+
|
|
15
|
+
Un écosystème n'écrit que la lecture d'une version et d'une contrainte.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
from typing import Any, Generic, Protocol, TypeVar
|
|
21
|
+
|
|
22
|
+
from forceupdate.models import Ecosystem, Gap
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvalidVersion(ValueError):
|
|
26
|
+
"""Une version illisible selon les règles de son écosystème."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InvalidConstraint(ValueError):
|
|
30
|
+
"""Une contrainte illisible, ou qu'on ne sait pas interpréter seule (``catalog:``)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Comparable(Protocol):
|
|
34
|
+
def __lt__(self, other: Any, /) -> bool: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
V = TypeVar("V", bound=_Comparable)
|
|
38
|
+
"""Une version lue : un objet comparable (``packaging.version.Version``, ``SemVer``…)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Constraint(ABC, Generic[V]):
|
|
42
|
+
"""Une contrainte lue : l'ensemble des versions qu'elle accepte."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def contains(self, version: V) -> bool:
|
|
46
|
+
"""Vrai si ``version`` respecte la contrainte."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def lower_bound(self) -> str | None:
|
|
50
|
+
"""La plus petite version que la contrainte accepte ; ``None`` si elle n'a pas de
|
|
51
|
+
borne basse (``*``, ``<2``, une étiquette npm)."""
|
|
52
|
+
|
|
53
|
+
def exact(self) -> str | None:
|
|
54
|
+
"""La version qu'impose une contrainte exacte (``==1.2.3``, ``1.2.3`` pour npm) ;
|
|
55
|
+
``None`` si elle en accepte plusieurs."""
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VersionScheme(ABC, Generic[V]):
|
|
60
|
+
"""Les règles de version d'un écosystème.
|
|
61
|
+
|
|
62
|
+
Les versions et les contraintes entrent et sortent en chaînes, comme dans les
|
|
63
|
+
modèles. Une version illisible lève ``InvalidVersion``, une contrainte illisible
|
|
64
|
+
``InvalidConstraint`` ; dans une liste de versions publiées, une version illisible
|
|
65
|
+
est sautée (un registre en garde parfois de très anciennes, hors normes).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
ecosystem: Ecosystem
|
|
69
|
+
|
|
70
|
+
# -- à écrire pour chaque écosystème ------------------------------------
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def parse(self, version: str) -> V:
|
|
74
|
+
"""La version lue, comparable avec ``<`` ; lève ``InvalidVersion``."""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def parse_constraint(self, constraint: str) -> Constraint[V]:
|
|
78
|
+
"""La contrainte lue, telle qu'écrite dans le manifeste ; lève ``InvalidConstraint``."""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _release(self, version: V) -> tuple[int, int, int]:
|
|
82
|
+
"""Les trois premiers nombres de la version (majeure, mineure, correctif)."""
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def _is_pre(self, version: V) -> bool:
|
|
86
|
+
"""Vrai pour une pré-version (``1.0.0-beta``, ``2.0.0rc1``)."""
|
|
87
|
+
|
|
88
|
+
def _same_release(self, a: V, b: V) -> bool:
|
|
89
|
+
"""Vrai si ``a`` et ``b`` sont deux états de la même version (``2.0.0rc1`` et
|
|
90
|
+
``2.0.0rc2``, ``2.0.0-beta.1`` et ``2.0.0``)."""
|
|
91
|
+
return self._release(a) == self._release(b)
|
|
92
|
+
|
|
93
|
+
# -- commun --------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def compare(self, a: str, b: str) -> int:
|
|
96
|
+
"""``-1`` si ``a`` est plus ancienne que ``b``, ``0`` si égales, ``1`` si plus récente."""
|
|
97
|
+
first, second = self.parse(a), self.parse(b)
|
|
98
|
+
return -1 if first < second else 1 if second < first else 0
|
|
99
|
+
|
|
100
|
+
def is_prerelease(self, version: str) -> bool:
|
|
101
|
+
"""Vrai pour une pré-version : ``1.0.0-beta`` (npm), ``2.0.0rc1`` ou ``1.0.dev1`` (PyPI)."""
|
|
102
|
+
return self._is_pre(self.parse(version))
|
|
103
|
+
|
|
104
|
+
def sort_key(self, version: str) -> tuple[int, Any]:
|
|
105
|
+
"""Une clé de tri qui ne lève jamais : les versions lisibles dans leur ordre, puis
|
|
106
|
+
les autres (une référence git…) dans l'ordre du texte."""
|
|
107
|
+
try:
|
|
108
|
+
return (0, self.parse(version))
|
|
109
|
+
except InvalidVersion:
|
|
110
|
+
return (1, version)
|
|
111
|
+
|
|
112
|
+
def gap(self, current: str, target: str) -> Gap | None:
|
|
113
|
+
"""L'écart pour passer de ``current`` à ``target`` ; ``None`` si ``target`` n'est
|
|
114
|
+
pas plus récente.
|
|
115
|
+
|
|
116
|
+
C'est le premier nombre qui change : de ``2.3.1``, ``2.3.4`` est un correctif,
|
|
117
|
+
``2.9.0`` une mineure, ``3.0.0`` une majeure. Deux règles s'y ajoutent :
|
|
118
|
+
|
|
119
|
+
- **0.x** : une majeure est ce qui sort de ``^current``. En ``0.y.z``, ``y`` joue
|
|
120
|
+
le rôle de la majeure (``0.2.3`` → ``0.3.0`` est majeure, ``0.2.3`` →
|
|
121
|
+
``0.2.4`` un correctif) ; en ``0.0.z``, tout changement est majeur ;
|
|
122
|
+
- **pré-version** : ``X.Y.Z-rc.1`` est juste sous ``X.Y.Z``. Vers sa version
|
|
123
|
+
finale, l'écart est celui de ``X.Y.Z`` : ``1.0.0-rc.1`` → ``1.0.0`` est majeure,
|
|
124
|
+
``1.2.0-rc.1`` → ``1.2.0`` mineure, ``1.2.3-rc.1`` → ``1.2.3`` un correctif.
|
|
125
|
+
Une pré-version de ``X.0.0`` vient d'avant ``X.0.0`` : tout ce qui suit est
|
|
126
|
+
majeur (``1.0.0-rc.1`` → ``1.1.1``). C'est la règle de ``semver.diff``.
|
|
127
|
+
"""
|
|
128
|
+
a, b = self.parse(current), self.parse(target)
|
|
129
|
+
if not a < b:
|
|
130
|
+
return None
|
|
131
|
+
return self._gap(a, b)
|
|
132
|
+
|
|
133
|
+
def _gap(self, a: V, b: V) -> Gap:
|
|
134
|
+
major, minor, patch = self._release(a)
|
|
135
|
+
target = self._release(b)
|
|
136
|
+
level: Gap
|
|
137
|
+
if self._is_pre(a) and ((minor, patch) == (0, 0) or self._same_release(a, b)):
|
|
138
|
+
if (minor, patch) == (0, 0):
|
|
139
|
+
level = Gap.MAJOR
|
|
140
|
+
else:
|
|
141
|
+
level = Gap.MINOR if patch == 0 else Gap.PATCH
|
|
142
|
+
elif target[0] != major:
|
|
143
|
+
level = Gap.MAJOR
|
|
144
|
+
elif target[1] != minor:
|
|
145
|
+
level = Gap.MINOR
|
|
146
|
+
else:
|
|
147
|
+
level = Gap.PATCH
|
|
148
|
+
if major == 0 and (minor == 0 or level is Gap.MINOR):
|
|
149
|
+
return Gap.MAJOR
|
|
150
|
+
return level
|
|
151
|
+
|
|
152
|
+
def satisfies(self, constraint: str, version: str) -> bool:
|
|
153
|
+
"""Vrai si ``version`` respecte ``constraint``."""
|
|
154
|
+
return self.parse_constraint(constraint).contains(self.parse(version))
|
|
155
|
+
|
|
156
|
+
def lower_bound(self, constraint: str) -> str | None:
|
|
157
|
+
"""La plus petite version que ``constraint`` accepte (``^1.2`` → ``1.2.0`` pour npm,
|
|
158
|
+
``1.2`` pour PyPI) ; ``None`` sans borne basse (``*``, ``<2``).
|
|
159
|
+
|
|
160
|
+
C'est la version actuelle approximative d'une dépendance sans lockfile (E07).
|
|
161
|
+
"""
|
|
162
|
+
return self.parse_constraint(constraint).lower_bound()
|
|
163
|
+
|
|
164
|
+
def exact(self, constraint: str) -> str | None:
|
|
165
|
+
"""La version qu'impose ``constraint`` si elle est exacte (``==1.2.3``) ; ``None``
|
|
166
|
+
sinon. Sans lockfile, c'est une version actuelle qui n'a rien d'approximatif."""
|
|
167
|
+
return self.parse_constraint(constraint).exact()
|
|
168
|
+
|
|
169
|
+
def candidates(self, versions: Iterable[str], current: str | None = None) -> list[str]:
|
|
170
|
+
"""Les versions qu'on peut proposer, dans leur ordre : les versions lisibles, sans
|
|
171
|
+
les pré-versions.
|
|
172
|
+
|
|
173
|
+
Si ``current`` est une pré-version, celles de la même version restent :
|
|
174
|
+
de ``2.0.0-rc.1``, on propose ``2.0.0-rc.2``, mais pas ``2.1.0-beta.1``.
|
|
175
|
+
"""
|
|
176
|
+
return [text for text, _ in self._candidates(versions, current)]
|
|
177
|
+
|
|
178
|
+
def _candidates(self, versions: Iterable[str], current: str | None) -> list[tuple[str, V]]:
|
|
179
|
+
line = None
|
|
180
|
+
if current is not None:
|
|
181
|
+
parsed = self.parse(current)
|
|
182
|
+
line = parsed if self._is_pre(parsed) else None
|
|
183
|
+
kept = []
|
|
184
|
+
for text in versions:
|
|
185
|
+
try:
|
|
186
|
+
version = self.parse(text)
|
|
187
|
+
except InvalidVersion:
|
|
188
|
+
continue
|
|
189
|
+
if self._is_pre(version) and (line is None or not self._same_release(version, line)):
|
|
190
|
+
continue
|
|
191
|
+
kept.append((text, version))
|
|
192
|
+
return kept
|
|
193
|
+
|
|
194
|
+
def latest(self, versions: Iterable[str], current: str | None = None) -> str | None:
|
|
195
|
+
"""La plus haute des candidates (voir ``candidates``) ; ``None`` s'il n'y en a pas."""
|
|
196
|
+
return _highest(self._candidates(versions, current))
|
|
197
|
+
|
|
198
|
+
def max_satisfying(
|
|
199
|
+
self, constraint: str, versions: Iterable[str], current: str | None = None
|
|
200
|
+
) -> str | None:
|
|
201
|
+
"""La plus haute des candidates (voir ``candidates``) qui respecte ``constraint`` :
|
|
202
|
+
celle-là se met à jour sans toucher au manifeste. ``None`` s'il n'y en a pas."""
|
|
203
|
+
parsed = self.parse_constraint(constraint)
|
|
204
|
+
return _highest(
|
|
205
|
+
(text, version)
|
|
206
|
+
for text, version in self._candidates(versions, current)
|
|
207
|
+
if parsed.contains(version)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def highest(self, versions: Iterable[str]) -> str | None:
|
|
211
|
+
"""La plus haute des versions lisibles, pré-versions comprises ; ``None`` s'il n'y
|
|
212
|
+
en a pas."""
|
|
213
|
+
readable = []
|
|
214
|
+
for text in versions:
|
|
215
|
+
try:
|
|
216
|
+
readable.append((text, self.parse(text)))
|
|
217
|
+
except InvalidVersion:
|
|
218
|
+
continue
|
|
219
|
+
return _highest(readable)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _highest(pairs: Iterable[tuple[str, V]]) -> str | None:
|
|
223
|
+
"""Le texte de la plus haute version ; la première rencontrée en cas d'égalité
|
|
224
|
+
(``1.0`` et ``1.0.0`` pour PyPI)."""
|
|
225
|
+
best: tuple[str, V] | None = None
|
|
226
|
+
for pair in pairs:
|
|
227
|
+
if best is None or best[1] < pair[1]:
|
|
228
|
+
best = pair
|
|
229
|
+
return None if best is None else best[0]
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Les versions npm : semver 2.0 et les plages de npm, écrits à la main (comme node-semver 7).
|
|
2
|
+
|
|
3
|
+
Les versions publiées sont du semver strict (``1.2.3``, ``1.0.0-beta.2``, ``v`` toléré
|
|
4
|
+
devant). Les plages suivent les règles de node-semver, en mode « lâche » comme npm :
|
|
5
|
+
|
|
6
|
+
- ``^``, ``~`` (et ``~>``), les jokers ``x``, ``X`` et ``*``, ``a - b``, les
|
|
7
|
+
comparateurs (``>=1.2 <2``), ``||`` ; ``v`` et ``=`` tolérés devant une version, et
|
|
8
|
+
des versions partielles (``1.2``) ;
|
|
9
|
+
- les règles du 0.x : ``^0.2.3`` veut dire ``>=0.2.3 <0.3.0-0``, ``^0.0.3``
|
|
10
|
+
``>=0.0.3 <0.0.4-0`` ;
|
|
11
|
+
- une pré-version ne respecte une plage que si un comparateur de la plage porte sur
|
|
12
|
+
le même ``major.minor.patch`` avec une pré-version : ``^1.2.3-beta.2`` accepte
|
|
13
|
+
``1.2.3-beta.4``, mais pas ``1.2.4-beta.1``.
|
|
14
|
+
|
|
15
|
+
Ce qu'un ``package.json`` peut écrire d'autre (E04) :
|
|
16
|
+
|
|
17
|
+
- ``""`` veut dire ``*`` ;
|
|
18
|
+
- une étiquette (``latest``, ``next``) suit le registre : elle accepte toutes les
|
|
19
|
+
versions et n'a pas de borne basse ;
|
|
20
|
+
- ``catalog:`` (pnpm) renvoie à ``pnpm-workspace.yaml`` : les parseurs y lisent la plage
|
|
21
|
+
(``Dependency.resolved_constraint``). Seule, elle lève ``InvalidConstraint``.
|
|
22
|
+
|
|
23
|
+
``str()`` d'une plage donne sa forme développée, celle de ``semver.validRange``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import functools
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Any, NamedTuple
|
|
30
|
+
|
|
31
|
+
from forceupdate.models import Ecosystem
|
|
32
|
+
from forceupdate.versions.base import Constraint, InvalidConstraint, InvalidVersion, VersionScheme
|
|
33
|
+
|
|
34
|
+
_NUMBER = r"0|[1-9]\d*"
|
|
35
|
+
_PRE_ID = r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)"
|
|
36
|
+
_LOOSE_ID = r"[0-9A-Za-z-]+"
|
|
37
|
+
|
|
38
|
+
_VERSION = re.compile(
|
|
39
|
+
rf"v?(?P<major>{_NUMBER})\.(?P<minor>{_NUMBER})\.(?P<patch>{_NUMBER})"
|
|
40
|
+
rf"(?:-(?P<pre>{_PRE_ID}(?:\.{_PRE_ID})*))?"
|
|
41
|
+
rf"(?:\+(?P<build>{_LOOSE_ID}(?:\.{_LOOSE_ID})*))?",
|
|
42
|
+
re.ASCII,
|
|
43
|
+
)
|
|
44
|
+
"""Une version publiée : semver strict."""
|
|
45
|
+
|
|
46
|
+
_X = r"\d+|[xX*]"
|
|
47
|
+
_PARTIAL = re.compile(
|
|
48
|
+
rf"[v=\s]*(?P<major>{_X})(?:\.(?P<minor>{_X})(?:\.(?P<patch>{_X})"
|
|
49
|
+
rf"(?:-?(?P<pre>{_LOOSE_ID}(?:\.{_LOOSE_ID})*))?"
|
|
50
|
+
rf"(?:\+{_LOOSE_ID}(?:\.{_LOOSE_ID})*)?)?)?",
|
|
51
|
+
re.ASCII,
|
|
52
|
+
)
|
|
53
|
+
"""Une version dans une plage : lâche (``v1``, ``=1.2``, ``01.2.3``, ``1.2.3beta``)
|
|
54
|
+
ou partielle (``1``, ``1.2.x``)."""
|
|
55
|
+
|
|
56
|
+
_HYPHEN = re.compile(r"(\S+)\s+-\s+(\S+)")
|
|
57
|
+
_LONE_OPERATOR = re.compile(r"[<>]=?|=|~>?|\^")
|
|
58
|
+
_COMPARATOR = re.compile(r"(?P<op>[<>]?=?)(?P<version>.*)")
|
|
59
|
+
_TAG = re.compile(r"[A-Za-z][0-9A-Za-z._-]*")
|
|
60
|
+
"""Une étiquette du registre (``latest``, ``next``, ``v4-lts``) : ce qui n'est pas une plage."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@functools.total_ordering
|
|
64
|
+
@dataclass(frozen=True, eq=False)
|
|
65
|
+
class SemVer:
|
|
66
|
+
"""Une version semver 2.0. Les métadonnées de build (``+sha.1``) ne comptent pas dans
|
|
67
|
+
l'ordre."""
|
|
68
|
+
|
|
69
|
+
major: int
|
|
70
|
+
minor: int
|
|
71
|
+
patch: int
|
|
72
|
+
prerelease: tuple[int | str, ...] = ()
|
|
73
|
+
build: tuple[str, ...] = ()
|
|
74
|
+
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
text = f"{self.major}.{self.minor}.{self.patch}"
|
|
77
|
+
if self.prerelease:
|
|
78
|
+
text += "-" + ".".join(str(part) for part in self.prerelease)
|
|
79
|
+
return text
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def release(self) -> tuple[int, int, int]:
|
|
83
|
+
return self.major, self.minor, self.patch
|
|
84
|
+
|
|
85
|
+
def _key(self) -> tuple[int, int, int, tuple[Any, ...]]:
|
|
86
|
+
# Sans pré-version, une version passe après toutes ses pré-versions. Entre deux
|
|
87
|
+
# identifiants : les nombres avant le texte, les nombres par valeur, le texte en
|
|
88
|
+
# ASCII ; à égalité, la liste la plus courte d'abord.
|
|
89
|
+
if not self.prerelease:
|
|
90
|
+
return self.major, self.minor, self.patch, (1,)
|
|
91
|
+
parts = tuple((0, p, "") if isinstance(p, int) else (1, 0, p) for p in self.prerelease)
|
|
92
|
+
return self.major, self.minor, self.patch, (0, *parts)
|
|
93
|
+
|
|
94
|
+
def __eq__(self, other: object) -> bool:
|
|
95
|
+
return isinstance(other, SemVer) and self._key() == other._key()
|
|
96
|
+
|
|
97
|
+
def __lt__(self, other: "SemVer") -> bool:
|
|
98
|
+
return self._key() < other._key()
|
|
99
|
+
|
|
100
|
+
def __hash__(self) -> int:
|
|
101
|
+
return hash(self._key())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _identifiers(text: str | None) -> tuple[int | str, ...]:
|
|
105
|
+
if not text:
|
|
106
|
+
return ()
|
|
107
|
+
return tuple(int(part) if part.isdigit() else part for part in text.split("."))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class _Comparator:
|
|
112
|
+
"""Un comparateur d'une plage développée : ``>=1.2.3``, ``<2.0.0-0``, ``1.2.3``."""
|
|
113
|
+
|
|
114
|
+
operator: str
|
|
115
|
+
"""``>=``, ``>``, ``<=``, ``<`` ou ``""`` (égal)."""
|
|
116
|
+
version: SemVer
|
|
117
|
+
|
|
118
|
+
def __str__(self) -> str:
|
|
119
|
+
return f"{self.operator}{self.version}"
|
|
120
|
+
|
|
121
|
+
def test(self, version: SemVer) -> bool:
|
|
122
|
+
if self.operator == ">=":
|
|
123
|
+
return not version < self.version
|
|
124
|
+
if self.operator == ">":
|
|
125
|
+
return self.version < version
|
|
126
|
+
if self.operator == "<=":
|
|
127
|
+
return not self.version < version
|
|
128
|
+
if self.operator == "<":
|
|
129
|
+
return version < self.version
|
|
130
|
+
return version == self.version
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
_NULL = _Comparator("<", SemVer(0, 0, 0, (0,)))
|
|
134
|
+
"""Le comparateur qu'aucune version ne respecte (``>*`` donne ça)."""
|
|
135
|
+
_ZERO = SemVer(0, 0, 0)
|
|
136
|
+
|
|
137
|
+
_Set = tuple[_Comparator, ...]
|
|
138
|
+
"""Un ensemble de comparateurs, tous à respecter ; vide, il accepte tout."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class NpmRange(Constraint[SemVer]):
|
|
142
|
+
"""Une plage npm développée : une union d'ensembles de comparateurs, ou une étiquette."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, sets: tuple[_Set, ...], tag: str | None = None) -> None:
|
|
145
|
+
self.sets = sets
|
|
146
|
+
self.tag = tag
|
|
147
|
+
"""L'étiquette (``latest``, ``next``) ; le registre dit vers quelle version elle pointe."""
|
|
148
|
+
|
|
149
|
+
def __str__(self) -> str:
|
|
150
|
+
if self.tag is not None:
|
|
151
|
+
return self.tag
|
|
152
|
+
return "||".join(" ".join(str(c) for c in comparators) for comparators in self.sets) or "*"
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return f"NpmRange({str(self)!r})"
|
|
156
|
+
|
|
157
|
+
def contains(self, version: SemVer) -> bool:
|
|
158
|
+
if self.tag is not None:
|
|
159
|
+
return True
|
|
160
|
+
return any(_test_set(comparators, version) for comparators in self.sets)
|
|
161
|
+
|
|
162
|
+
def lower_bound(self) -> str | None:
|
|
163
|
+
"""Comme ``semver.minVersion``, sauf qu'une plage dont un ensemble n'a pas de borne
|
|
164
|
+
basse (``*``, ``<2``, ``^0``) rend ``None`` au lieu de ``0.0.0``."""
|
|
165
|
+
if self.tag is not None:
|
|
166
|
+
return None
|
|
167
|
+
lowest: SemVer | None = None
|
|
168
|
+
for comparators in self.sets:
|
|
169
|
+
bounds = [_minimum(c) for c in comparators if c.operator in ("", ">=", ">")]
|
|
170
|
+
if not bounds:
|
|
171
|
+
return None
|
|
172
|
+
bound = max(bounds)
|
|
173
|
+
if lowest is None or bound < lowest:
|
|
174
|
+
lowest = bound
|
|
175
|
+
if lowest is None or not self.contains(lowest):
|
|
176
|
+
return None
|
|
177
|
+
return str(lowest)
|
|
178
|
+
|
|
179
|
+
def exact(self) -> str | None:
|
|
180
|
+
"""Une version complète seule (``1.2.3``, ``=1.2.3``, ``v1.2.3``) : elle-même. ``1.2``
|
|
181
|
+
est une plage (``>=1.2.0 <1.3.0-0``)."""
|
|
182
|
+
if self.tag is not None or len(self.sets) != 1 or len(self.sets[0]) != 1:
|
|
183
|
+
return None
|
|
184
|
+
(comparator,) = self.sets[0]
|
|
185
|
+
return str(comparator.version) if comparator.operator == "" else None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _minimum(comparator: _Comparator) -> SemVer:
|
|
189
|
+
"""La plus petite version qui respecte un comparateur ``>=``, ``>`` ou ``=`` :
|
|
190
|
+
``>1.2.3`` → ``1.2.4``, ``>1.2.3-beta`` → ``1.2.3-beta.0``."""
|
|
191
|
+
version = comparator.version
|
|
192
|
+
if comparator.operator != ">":
|
|
193
|
+
return version
|
|
194
|
+
if version.prerelease:
|
|
195
|
+
return SemVer(version.major, version.minor, version.patch, (*version.prerelease, 0))
|
|
196
|
+
return SemVer(version.major, version.minor, version.patch + 1)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _test_set(comparators: _Set, version: SemVer) -> bool:
|
|
200
|
+
if not all(comparator.test(version) for comparator in comparators):
|
|
201
|
+
return False
|
|
202
|
+
if not version.prerelease:
|
|
203
|
+
return True
|
|
204
|
+
# Une pré-version n'entre que par un comparateur qui en porte une sur la même version.
|
|
205
|
+
return any(c.version.prerelease and c.version.release == version.release for c in comparators)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class NpmVersions(VersionScheme[SemVer]):
|
|
209
|
+
"""semver 2.0, et les plages de npm."""
|
|
210
|
+
|
|
211
|
+
ecosystem = Ecosystem.NPM
|
|
212
|
+
|
|
213
|
+
def parse(self, version: str) -> SemVer:
|
|
214
|
+
return _parse(version)
|
|
215
|
+
|
|
216
|
+
def parse_constraint(self, constraint: str) -> NpmRange:
|
|
217
|
+
return _parse_range(constraint)
|
|
218
|
+
|
|
219
|
+
def _release(self, version: SemVer) -> tuple[int, int, int]:
|
|
220
|
+
return version.release
|
|
221
|
+
|
|
222
|
+
def _is_pre(self, version: SemVer) -> bool:
|
|
223
|
+
return bool(version.prerelease)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
NPM = NpmVersions()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@functools.lru_cache(maxsize=4096)
|
|
230
|
+
def _parse(version: str) -> SemVer:
|
|
231
|
+
match = _VERSION.fullmatch(version.strip())
|
|
232
|
+
if match is None:
|
|
233
|
+
raise InvalidVersion(f"version semver illisible : {version!r}")
|
|
234
|
+
build = match.group("build")
|
|
235
|
+
return SemVer(
|
|
236
|
+
int(match.group("major")),
|
|
237
|
+
int(match.group("minor")),
|
|
238
|
+
int(match.group("patch")),
|
|
239
|
+
_identifiers(match.group("pre")),
|
|
240
|
+
tuple(build.split(".")) if build else (),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Plages
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _RangeError(ValueError):
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _Partial(NamedTuple):
|
|
254
|
+
"""Une version de plage : ``None`` pour un nombre absent ou joker (``1.x``)."""
|
|
255
|
+
|
|
256
|
+
major: int | None
|
|
257
|
+
minor: int | None
|
|
258
|
+
patch: int | None
|
|
259
|
+
prerelease: tuple[int | str, ...]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@functools.lru_cache(maxsize=1024)
|
|
263
|
+
def _parse_range(constraint: str) -> NpmRange:
|
|
264
|
+
text = " ".join(constraint.split())
|
|
265
|
+
if text.lower().startswith("catalog:"):
|
|
266
|
+
raise InvalidConstraint(
|
|
267
|
+
f"{constraint!r} renvoie à un catalogue pnpm : la plage est dans pnpm-workspace.yaml"
|
|
268
|
+
)
|
|
269
|
+
try:
|
|
270
|
+
return NpmRange(_simplify(tuple(_parse_set(part.strip()) for part in text.split("||"))))
|
|
271
|
+
except _RangeError:
|
|
272
|
+
if _TAG.fullmatch(text):
|
|
273
|
+
return NpmRange((), tag=text)
|
|
274
|
+
raise InvalidConstraint(f"plage npm illisible : {constraint!r}") from None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _simplify(sets: tuple[_Set, ...]) -> tuple[_Set, ...]:
|
|
278
|
+
"""Comme node-semver : les ensembles vides de sens (``<0.0.0-0``) partent s'il en reste
|
|
279
|
+
d'autres, et un ensemble qui accepte tout absorbe l'union."""
|
|
280
|
+
if len(sets) == 1:
|
|
281
|
+
return sets
|
|
282
|
+
kept = tuple(s for s in sets if s != (_NULL,)) or sets[:1]
|
|
283
|
+
if () in kept:
|
|
284
|
+
return ((),)
|
|
285
|
+
return kept
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _parse_set(text: str) -> _Set:
|
|
289
|
+
"""Un membre de l'union : ``1.2.3 - 2.3.4``, ou des comparateurs séparés par des espaces."""
|
|
290
|
+
if not text:
|
|
291
|
+
return ()
|
|
292
|
+
hyphen = _HYPHEN.fullmatch(text)
|
|
293
|
+
if hyphen is not None and (first := _partial_or_none(hyphen[1])) is not None:
|
|
294
|
+
comparators = _hyphen(first, _partial(hyphen[2]))
|
|
295
|
+
else:
|
|
296
|
+
comparators = []
|
|
297
|
+
operator = ""
|
|
298
|
+
for token in text.split(" "):
|
|
299
|
+
if _LONE_OPERATOR.fullmatch(token):
|
|
300
|
+
# « >= 1.2.3 », « ^ 1.2 » : l'opérateur va avec ce qui suit.
|
|
301
|
+
if operator:
|
|
302
|
+
raise _RangeError(token)
|
|
303
|
+
operator = token
|
|
304
|
+
continue
|
|
305
|
+
comparators.extend(_comparators(operator + token))
|
|
306
|
+
operator = ""
|
|
307
|
+
if operator:
|
|
308
|
+
raise _RangeError(operator)
|
|
309
|
+
# >=0.0.0 n'est pas une borne : c'est « toutes les versions ».
|
|
310
|
+
kept = [c for c in comparators if not (c.operator == ">=" and c.version == _ZERO)]
|
|
311
|
+
if _NULL in kept:
|
|
312
|
+
return (_NULL,)
|
|
313
|
+
return tuple(dict.fromkeys(kept))
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _partial(text: str) -> _Partial:
|
|
317
|
+
partial = _partial_or_none(text)
|
|
318
|
+
if partial is None:
|
|
319
|
+
raise _RangeError(text)
|
|
320
|
+
return partial
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _partial_or_none(text: str) -> _Partial | None:
|
|
324
|
+
match = _PARTIAL.fullmatch(text)
|
|
325
|
+
if match is None:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def number(group: str) -> int | None:
|
|
329
|
+
value = match.group(group)
|
|
330
|
+
return None if value is None or value in ("x", "X", "*") else int(value)
|
|
331
|
+
|
|
332
|
+
return _Partial(
|
|
333
|
+
number("major"), number("minor"), number("patch"), _identifiers(match.group("pre"))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _comparators(token: str) -> list[_Comparator]:
|
|
338
|
+
"""Un comparateur tel qu'écrit, développé : ``^1.2.3`` → ``>=1.2.3 <2.0.0-0``."""
|
|
339
|
+
if token.startswith("^"):
|
|
340
|
+
return _caret(_partial(token[1:]))
|
|
341
|
+
if token.startswith("~"):
|
|
342
|
+
return _tilde(_partial(token[2:] if token.startswith("~>") else token[1:]))
|
|
343
|
+
match = _COMPARATOR.fullmatch(token)
|
|
344
|
+
if match is None: # pragma: no cover - l'expression accepte tout
|
|
345
|
+
raise _RangeError(token)
|
|
346
|
+
return _xrange(match.group("op"), _partial(match.group("version")))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _ge(major: int, minor: int, patch: int, pre: tuple[int | str, ...] = ()) -> _Comparator:
|
|
350
|
+
return _Comparator(">=", SemVer(major, minor, patch, pre))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _lt(major: int, minor: int, patch: int) -> _Comparator:
|
|
354
|
+
"""``<X.Y.Z-0`` : sous ``X.Y.Z`` et toutes ses pré-versions."""
|
|
355
|
+
return _Comparator("<", SemVer(major, minor, patch, (0,)))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _caret(p: _Partial) -> list[_Comparator]:
|
|
359
|
+
"""``^`` : ne change pas le premier nombre non nul (règles du 0.x)."""
|
|
360
|
+
major, minor, patch = p.major, p.minor, p.patch
|
|
361
|
+
if major is None:
|
|
362
|
+
return []
|
|
363
|
+
if minor is None:
|
|
364
|
+
return [_ge(major, 0, 0), _lt(major + 1, 0, 0)]
|
|
365
|
+
if patch is None:
|
|
366
|
+
upper = _lt(0, minor + 1, 0) if major == 0 else _lt(major + 1, 0, 0)
|
|
367
|
+
return [_ge(major, minor, 0), upper]
|
|
368
|
+
lower = _ge(major, minor, patch, p.prerelease)
|
|
369
|
+
if major > 0:
|
|
370
|
+
return [lower, _lt(major + 1, 0, 0)]
|
|
371
|
+
if minor > 0:
|
|
372
|
+
return [lower, _lt(0, minor + 1, 0)]
|
|
373
|
+
return [lower, _lt(0, 0, patch + 1)]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _tilde(p: _Partial) -> list[_Comparator]:
|
|
377
|
+
"""``~`` : les correctifs si la mineure est donnée, sinon les mineures."""
|
|
378
|
+
major, minor, patch = p.major, p.minor, p.patch
|
|
379
|
+
if major is None:
|
|
380
|
+
return []
|
|
381
|
+
if minor is None:
|
|
382
|
+
return [_ge(major, 0, 0), _lt(major + 1, 0, 0)]
|
|
383
|
+
if patch is None:
|
|
384
|
+
return [_ge(major, minor, 0), _lt(major, minor + 1, 0)]
|
|
385
|
+
return [_ge(major, minor, patch, p.prerelease), _lt(major, minor + 1, 0)]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _xrange(operator: str, p: _Partial) -> list[_Comparator]:
|
|
389
|
+
"""Un comparateur, avec ou sans joker : ``1.2.x``, ``>1.2``, ``<=1``, ``*``, ``1.2.3``."""
|
|
390
|
+
major, minor, patch = p.major, p.minor, p.patch
|
|
391
|
+
if operator == "=":
|
|
392
|
+
operator = ""
|
|
393
|
+
if major is None:
|
|
394
|
+
return [_NULL] if operator in (">", "<") else []
|
|
395
|
+
if minor is None or patch is None:
|
|
396
|
+
if not operator:
|
|
397
|
+
if minor is None:
|
|
398
|
+
return [_ge(major, 0, 0), _lt(major + 1, 0, 0)]
|
|
399
|
+
return [_ge(major, minor, 0), _lt(major, minor + 1, 0)]
|
|
400
|
+
# Comparateur sur une version partielle : on le ramène à une version complète.
|
|
401
|
+
bump_major = minor is None
|
|
402
|
+
minor = minor or 0
|
|
403
|
+
if operator == ">": # >1 → >=2.0.0 ; >1.2 → >=1.3.0
|
|
404
|
+
operator = ">="
|
|
405
|
+
major, minor = (major + 1, 0) if bump_major else (major, minor + 1)
|
|
406
|
+
elif operator == "<=": # <=1.2 → <1.3.0-0
|
|
407
|
+
operator = "<"
|
|
408
|
+
major, minor = (major + 1, 0) if bump_major else (major, minor + 1)
|
|
409
|
+
if operator == "<":
|
|
410
|
+
return [_lt(major, minor, 0)]
|
|
411
|
+
return [_Comparator(operator, SemVer(major, minor, 0))]
|
|
412
|
+
return [_Comparator(operator, SemVer(major, minor, patch, p.prerelease))]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _hyphen(first: _Partial, last: _Partial) -> list[_Comparator]:
|
|
416
|
+
"""``1.2 - 2.3.4`` → ``>=1.2.0 <=2.3.4`` ; ``1.2.3 - 2.3`` → ``>=1.2.3 <2.4.0-0``."""
|
|
417
|
+
comparators = []
|
|
418
|
+
if first.major is not None:
|
|
419
|
+
comparators.append(
|
|
420
|
+
_ge(first.major, first.minor or 0, first.patch or 0, first.prerelease)
|
|
421
|
+
if first.minor is not None and first.patch is not None
|
|
422
|
+
else _ge(first.major, first.minor or 0, 0)
|
|
423
|
+
)
|
|
424
|
+
if last.major is not None:
|
|
425
|
+
if last.minor is None:
|
|
426
|
+
comparators.append(_lt(last.major + 1, 0, 0))
|
|
427
|
+
elif last.patch is None:
|
|
428
|
+
comparators.append(_lt(last.major, last.minor + 1, 0))
|
|
429
|
+
else:
|
|
430
|
+
version = SemVer(last.major, last.minor, last.patch, last.prerelease)
|
|
431
|
+
comparators.append(_Comparator("<=", version))
|
|
432
|
+
return comparators
|