-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
version.clj
470 lines (415 loc) · 14.5 KB
/
version.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
(ns pip-license-checker.version
"Version parsing and comparing"
(:gen-class)
(:require
;;[clojure.spec.test.alpha :refer [instrument]]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[pip-license-checker.spec :as sp]))
;; Parse version
(def regex-split-comma #",")
(def regex-specifier #"(?<op>(===|==|~=|!=|>=|<=|<|>))(?<version>(.*))")
(def regex-version #"v?(?:(?:(?<epoch>[0-9]+)!)?(?<release>[0-9]+(?:\.[0-9]+)*)(?<pre>[-_\.]?(?<prel>(a|b|c|rc|alpha|beta|pre|preview))[-_\.]?(?<pren>[0-9]+)?)?(?<post>(?:-(?<postn1>[0-9]+))|(?:[-_\.]?(?<postl>post|rev|r)[-_\.]?(?<postn2>[0-9]+)?))?(?<dev>[-_\.]?(?<devl>dev)[-_\.]?(?<devn>[0-9]+)?)?)(?:\+(?<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?")
(defn parse-number
"Parse number string into integer or return 0"
[number]
(if (not number) 0 (Integer/parseInt number)))
(s/fdef parse-letter-version
:args (s/cat :letter ::sp/matched-version-part
:number ::sp/matched-version-part)
:ret ::sp/opt-version-letter)
(defn parse-letter-version
"Parse letter part of version"
[letter number]
(let [result
(cond
letter
(let [sanitized-letter (str/lower-case letter)
canonical-letter
(cond
(= sanitized-letter "alpha") "a"
(= sanitized-letter "beta") "b"
(contains? #{"c" "pre" "preview"} sanitized-letter) "rc"
(contains? #{"rev" "r"} sanitized-letter) "post"
:else sanitized-letter)
canonical-number (parse-number number)]
[canonical-letter canonical-number])
(and (not letter) number)
(let [canonical-letter "post"
canonical-number (parse-number number)]
[canonical-letter canonical-number])
:else nil)]
result))
(s/fdef parse-local-version
:args (s/cat :local ::sp/matched-version-part)
:ret ::sp/opt-version-local)
(defn parse-local-version
"Parse strings into vec with string parsed into ints if possible"
[local]
(let [lowered (if local (str/lower-case local) nil)
splitted (if lowered (str/split lowered #"[\._-]") nil)
parsed
(vec (map
#(try
(Integer/parseInt %)
(catch NumberFormatException _ %))
splitted))]
(if (= parsed []) nil parsed)))
(defn validate-version
[version-map]
(if version-map
(let [{:keys
[orig
epoch
release
prel pren
postn1 postl postn2
devl devn
local]} version-map
result
{:orig orig
:epoch (if epoch (Integer/parseInt epoch) 0)
:release (vec (map #(Integer/parseInt %) (str/split release #"\.")))
:pre (parse-letter-version prel pren)
:post (parse-letter-version postl (or postn1 postn2))
:dev (parse-letter-version devl devn)
:local (parse-local-version local)}]
result)
nil))
(s/fdef parse-version
:args (s/cat :version-str ::sp/version-str)
:ret ::sp/version)
(defn parse-version
"Return a hash-map of regex groups"
[version-str]
(let [sanitized-version (str/lower-case version-str)
matcher (re-matcher regex-version sanitized-version)
version-map
(if (.matches matcher)
{:orig version-str
:epoch (.group matcher "epoch")
:release (.group matcher "release")
:pre (.group matcher "pre")
:prel (.group matcher "prel")
:pren (.group matcher "pren")
:post (.group matcher "post")
:postn1 (.group matcher "postn1")
:postl (.group matcher "postl")
:postn2 (.group matcher "postn2")
:dev (.group matcher "dev")
:devl (.group matcher "devl")
:devn (.group matcher "devn")
:local (.group matcher "local")}
nil)]
(validate-version version-map)))
;; Comparison of parsed versions
;; https://clojuredocs.org/clojure.core/compare
;; https://clojure.org/guides/comparators
(defn truncate-release
"Return release vector with trailing zero parts dropped"
[release]
(let [release-truncated
(vec (reverse (drop-while #(= % 0) (reverse release))))]
release-truncated))
(defn get-comparable-version
"Get parsed version map formatted to be comparable
See more details in pypa/packaging:
https://github.com/pypa/packaging/blob/20.8/packaging/version.py#L505"
[version-map]
(let [{:keys [orig epoch release pre post dev local]} version-map
release (truncate-release release)
pre
(cond
(and (not pre) (not post) dev) (Double/NEGATIVE_INFINITY)
(not pre) (Double/POSITIVE_INFINITY)
:else pre)
post
(cond
(not post) (Double/NEGATIVE_INFINITY)
:else post)
dev
(cond
(not dev) (Double/POSITIVE_INFINITY)
:else dev)
local
(cond
(not local) [[(Double/NEGATIVE_INFINITY) ""]]
:else
(vec (map #(if (integer? %)
[% ""]
[(Double/NEGATIVE_INFINITY) %]) local)))]
{:orig orig
:epoch epoch
:release release
:pre pre
:post post
:dev dev
:local local}))
(defn pad-vector
"Append padding values to a given vector to make it of the specified len
Used to compare vectors of numbers"
[vec-val len pad-val]
(let [size (count vec-val)
left (- len size)
prepend (vec (repeat left pad-val))]
(vec (concat vec-val prepend))))
(defn compare-letter-version
"Compare vectors of [name version] shape with possible fallbacks to +/- Inf
NB! Comparator will break if assumed shape of vectors is violated"
[a b]
(cond
(or
(and (vector? a) (vector? b))
(and (number? a) (number? b))) (compare a b)
(and (number? a) (not (number? b))) (compare a 0)
(and (not (number? a)) (number? b)) (compare 0 b)
:else
(throw
(ex-info
(format "Cannot compare letter-version vectors")
{:a a :b b}))))
(defn compare-version
"Compare version maps"
[a b]
(let [;; compare epochs
a (get-comparable-version a)
b (get-comparable-version b)
c-epoch (compare (:epoch a) (:epoch b))
;; compare releases
release-a (:release a)
release-b (:release b)
max-release-len (max (count release-a) (count release-b))
release-a-padded (pad-vector release-a max-release-len 0)
release-b-padded (pad-vector release-b max-release-len 0)
c-release (compare release-a-padded release-b-padded)
;; compare pre, post, dev parts
c-pre (compare-letter-version (:pre a) (:pre b))
c-post (compare-letter-version (:post a) (:post b))
c-dev (compare-letter-version (:dev a) (:dev b))
;; compare local
local-a (:local a)
local-b (:local b)
max-local-len (max (count local-a) (count local-b))
local-a-padded (pad-vector local-a max-local-len [0 ""])
local-b-padded (pad-vector local-b max-local-len [0 ""])
c-local (compare local-a-padded local-b-padded)
;; get all comparators
c-all [c-epoch c-release c-pre c-post c-dev c-local]
result (some #(if (not= % 0) % nil) c-all)]
(or result 0)))
(defn eq
"Return true if versions a and b are equal"
[a b]
(let [comparator (compare-version a b)]
(= comparator 0)))
(defn neq
"Return true if versions a and b are not equal"
[a b]
(let [comparator (compare-version a b)]
(not= comparator 0)))
(defn lt
"Return true if version a less than b"
[a b]
(let [comparator (compare-version a b)]
(neg? comparator)))
(defn le
"Return true if version a less than or equal to b"
[a b]
(let [comparator (compare-version a b)]
(<= comparator 0)))
(defn gt
"Return true if version a greater than b"
[a b]
(let [comparator (compare-version a b)]
(pos? comparator)))
(defn ge
"Return true if version a greater than or equal to b"
[a b]
(let [comparator (compare-version a b)]
(>= comparator 0)))
(defn compatible
"Return true if version a is compatible with b
Compatible releases have an equivalent combination of >= and ==.
That is that ~=2.2 is equivalent to >=2.2,==2.*.
See more:
https://www.python.org/dev/peps/pep-0440/#compatible-release"
[a b]
(let [b-release-trunc (vec (take (- (count (:release b)) 1) (:release b)))
a-release-trunc (vec (take (count b-release-trunc) (:release a)))]
(and (ge a b) (= a-release-trunc b-release-trunc))))
(defn arbitrary-eq
"Return true if string representation of version a equal to b
See more:
https://www.python.org/dev/peps/pep-0440/#arbitrary-equality"
[a b]
(let [a-str (:orig a)
b-str (:orig b)]
(= a-str b-str)))
;; Parse specifier string
(defn get-comparison-op
"Get comparison function for operator string"
[op]
(case op
"===" arbitrary-eq
"==" eq
"~=" compatible
"!=" neq
"<=" le
">=" ge
"<" lt
">" gt))
(s/fdef parse-specifier
:args (s/cat :specifier-str ::sp/specifier-str)
:ret ::sp/specifier)
(defn parse-specifier
"Parse single specifier string into a vec of operator function and version map.
E.g. '>=1.2.3 parsed into [parsed-op parsed-version]"
[specifier-str]
(let [matcher (re-matcher regex-specifier specifier-str)
specifier-pair
(if (.matches matcher)
[(get-comparison-op (.group matcher "op"))
(parse-version (.group matcher "version"))]
nil)]
specifier-pair))
(s/fdef parse-specifiers
:args (s/cat :specifiers-str string?)
:ret ::sp/specifiers)
(defn parse-specifiers
"Parse a string of specifiers into a vec of parsed specifier vecs/
E.g. '>=1.2.3,<2' parsed into [[>=' 1.2.3'] [<' 2']]"
[specifiers-str]
(let [specifiers-vec (str/split specifiers-str regex-split-comma)
result (vec (map parse-specifier specifiers-vec))]
result))
;; Versions filtering helpers
(defn same-release-with-post-or-local?
"Check if version to be excluded from >V comparison as per
https://www.python.org/dev/peps/pep-0440/#exclusive-ordered-comparison"
[a b]
(let [a-post? (not (nil? (:post a)))
a-local? (not (nil? (:local a)))
a-release (:release a)
b-post? (not (nil? (:post b)))
b-local? (not (nil? (:local b)))
b-release (:release b)
max-release-len
(max (count a-release) (count b-release))
a-release* (pad-vector a-release max-release-len 0)
b-release* (pad-vector b-release max-release-len 0)
result
(and
(= a-release* b-release*)
(or a-post? a-local?)
(not (or b-post? b-local?)))]
result))
(defn same-release-with-pre-or-local?
"Check if version to be excluded from <V comparison as per
https://www.python.org/dev/peps/pep-0440/#exclusive-ordered-comparison"
[a b]
(let [a-pre? (not (nil? (:pre a)))
a-local? (not (nil? (:local a)))
a-release (:release a)
b-pre? (not (nil? (:pre b)))
b-local? (not (nil? (:local b)))
b-release (:release b)
max-release-len
(max (count b-release) (count a-release))
a-release* (pad-vector a-release max-release-len 0)
b-release* (pad-vector b-release max-release-len 0)
result
(and
(= a-release* b-release*)
(or a-pre? a-local?)
(not (or b-pre? b-local?)))]
result))
;; Check version against specifiers
(s/fdef version-ok?
:args (s/cat :specifiers ::sp/specifiers :version ::sp/version)
:ret boolean?)
(defn version-ok?
"Return true if a parsed version satisfies each specifier
Specifiers is a collection of vec [specifier-op specifier-version]"
[specifiers version]
(every?
true?
(map
(fn [[spec-op spec-version]]
(and (spec-op version spec-version)
(not
(and (= spec-op gt)
(same-release-with-post-or-local? version spec-version)))
(not
(and (= spec-op lt)
(same-release-with-pre-or-local? version spec-version)))))
specifiers)))
(s/fdef version-stable?
:args (s/cat :version ::sp/version)
:ret boolean?)
(defn version-stable?
"Return true if version is neither pre-release or development version"
[version]
(let [version-not-pre? (nil? (:pre version))
version-not-dev? (nil? (:dev version))
result (and version-not-pre? version-not-dev?)]
result))
(s/def ::pre boolean?)
(s/fdef filter-versions
:args (s/cat :specifiers ::sp/specifiers
:versions ::sp/versions
:pre (s/? keyword?)
:value (s/? boolean?))
:ret ::sp/versions)
(defn filter-versions
"Return lazy seq of parsed versions that satisfy specifiers"
[specifiers versions & {:keys [pre] :or {pre true}}]
(let [exclude-pre-releases (false? pre)
versions-with-pre (filter #(version-ok? specifiers %) versions)
versions-stable (filter version-stable? versions-with-pre)
result
(cond
pre versions-with-pre
(and exclude-pre-releases (seq versions-stable)) versions-stable
:else versions-with-pre)]
result))
(s/fdef sort-versions
:args (s/cat :versions ::sp/versions
:order (s/? keyword?)
:value (s/? keyword?))
:ret ::sp/versions)
(defn sort-versions
"Sort a vector of parsed versions.
Ascending sort order is used by default."
[versions & {:keys [order] :or {order :asc}}]
(let [comparator-fn
(if (= order :asc)
#(compare-version %1 %2)
#(compare-version %2 %1))]
(sort comparator-fn versions)))
(s/fdef get-version
:args (s/cat :specifiers ::sp/specifiers
:versions ::sp/versions
:pre (s/? keyword?)
:value (s/? boolean?))
:ret ::sp/version-str)
(defn get-version
"Get the most recent version from given versions that satisfies specifiers"
[specifiers versions & {:keys [pre] :or {pre true}}]
(let [versions-ok (filter-versions specifiers versions :pre pre)
versions-sorted (sort-versions versions-ok)
version-latest (last versions-sorted)
version (:orig version-latest)]
version))
;;
;; Instrumented functions - uncomment only while testing
;;
;; (instrument `parse-letter-version)
;; (instrument `parse-local-version)
;; (instrument `parse-version)
;; (instrument `parse-specifier)
;; (instrument `parse-specifiers)
;; (instrument `version-ok?)
;; (instrument `version-stable?)
;; (instrument `filter-versions)
;; (instrument `sort-versions)
;; (instrument `get-version)