dependabot-uv 0.299.1
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.
- checksums.yaml +7 -0
- data/helpers/build +34 -0
- data/helpers/lib/__init__.py +0 -0
- data/helpers/lib/hasher.py +36 -0
- data/helpers/lib/parser.py +270 -0
- data/helpers/requirements.txt +13 -0
- data/helpers/run.py +22 -0
- data/lib/dependabot/uv/authed_url_builder.rb +31 -0
- data/lib/dependabot/uv/file_fetcher.rb +328 -0
- data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
- data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
- data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
- data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
- data/lib/dependabot/uv/file_parser.rb +437 -0
- data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
- data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
- data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
- data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
- data/lib/dependabot/uv/file_updater.rb +105 -0
- data/lib/dependabot/uv/language.rb +76 -0
- data/lib/dependabot/uv/language_version_manager.rb +114 -0
- data/lib/dependabot/uv/metadata_finder.rb +186 -0
- data/lib/dependabot/uv/name_normaliser.rb +26 -0
- data/lib/dependabot/uv/native_helpers.rb +38 -0
- data/lib/dependabot/uv/package_manager.rb +54 -0
- data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
- data/lib/dependabot/uv/pipenv_runner.rb +108 -0
- data/lib/dependabot/uv/requirement.rb +163 -0
- data/lib/dependabot/uv/requirement_parser.rb +60 -0
- data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
- data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
- data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
- data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
- data/lib/dependabot/uv/update_checker.rb +317 -0
- data/lib/dependabot/uv/version.rb +321 -0
- data/lib/dependabot/uv.rb +35 -0
- metadata +306 -0
@@ -0,0 +1,391 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/uv/requirement_parser"
|
5
|
+
require "dependabot/uv/requirement"
|
6
|
+
require "dependabot/uv/update_checker"
|
7
|
+
require "dependabot/uv/version"
|
8
|
+
require "dependabot/requirements_update_strategy"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Uv
|
12
|
+
class UpdateChecker
|
13
|
+
class RequirementsUpdater
|
14
|
+
PYPROJECT_OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/
|
15
|
+
PYPROJECT_SEPARATOR = /#{PYPROJECT_OR_SEPARATOR}|,/
|
16
|
+
|
17
|
+
class UnfixableRequirement < StandardError; end
|
18
|
+
|
19
|
+
attr_reader :requirements
|
20
|
+
attr_reader :update_strategy
|
21
|
+
attr_reader :has_lockfile
|
22
|
+
attr_reader :latest_resolvable_version
|
23
|
+
|
24
|
+
def initialize(requirements:, update_strategy:, has_lockfile:,
|
25
|
+
latest_resolvable_version:)
|
26
|
+
@requirements = requirements
|
27
|
+
@update_strategy = update_strategy
|
28
|
+
@has_lockfile = has_lockfile
|
29
|
+
|
30
|
+
return unless latest_resolvable_version
|
31
|
+
|
32
|
+
@latest_resolvable_version =
|
33
|
+
Uv::Version.new(latest_resolvable_version)
|
34
|
+
end
|
35
|
+
|
36
|
+
def updated_requirements
|
37
|
+
return requirements if update_strategy.lockfile_only?
|
38
|
+
|
39
|
+
requirements.map do |req|
|
40
|
+
case req[:file]
|
41
|
+
when /setup\.(?:py|cfg)$/ then updated_setup_requirement(req)
|
42
|
+
when "pyproject.toml" then updated_pyproject_requirement(req)
|
43
|
+
when "Pipfile" then updated_pipfile_requirement(req)
|
44
|
+
when /\.txt$|\.in$/ then updated_requirement(req)
|
45
|
+
else raise "Unexpected filename: #{req[:file]}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
53
|
+
def updated_setup_requirement(req)
|
54
|
+
return req unless latest_resolvable_version
|
55
|
+
return req unless req.fetch(:requirement)
|
56
|
+
return req if new_version_satisfies?(req)
|
57
|
+
|
58
|
+
req_strings = req[:requirement].split(",").map(&:strip)
|
59
|
+
|
60
|
+
new_requirement =
|
61
|
+
if req_strings.any? { |r| requirement_class.new(r).exact? }
|
62
|
+
find_and_update_equality_match(req_strings)
|
63
|
+
elsif req_strings.any? { |r| r.start_with?("~=", "==") }
|
64
|
+
tw_req = req_strings.find { |r| r.start_with?("~=", "==") }
|
65
|
+
convert_to_range(tw_req, latest_resolvable_version)
|
66
|
+
else
|
67
|
+
update_requirements_range(req_strings)
|
68
|
+
end
|
69
|
+
|
70
|
+
req.merge(requirement: new_requirement)
|
71
|
+
rescue UnfixableRequirement
|
72
|
+
req.merge(requirement: :unfixable)
|
73
|
+
end
|
74
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
75
|
+
|
76
|
+
def updated_pipfile_requirement(req)
|
77
|
+
# For now, we just proxy to updated_requirement. In future this
|
78
|
+
# method may treat Pipfile requirements differently.
|
79
|
+
updated_requirement(req)
|
80
|
+
end
|
81
|
+
|
82
|
+
def updated_pyproject_requirement(req)
|
83
|
+
return req unless latest_resolvable_version
|
84
|
+
return req unless req.fetch(:requirement)
|
85
|
+
return req if new_version_satisfies?(req) && !has_lockfile
|
86
|
+
|
87
|
+
# If the requirement uses || syntax then we always want to widen it
|
88
|
+
return widen_pyproject_requirement(req) if req.fetch(:requirement).match?(PYPROJECT_OR_SEPARATOR)
|
89
|
+
|
90
|
+
# If the requirement is a development dependency we always want to
|
91
|
+
# bump it
|
92
|
+
return update_pyproject_version(req) if req.fetch(:groups).include?("dev-dependencies")
|
93
|
+
|
94
|
+
case update_strategy
|
95
|
+
when RequirementsUpdateStrategy::WidenRanges then widen_pyproject_requirement(req)
|
96
|
+
when RequirementsUpdateStrategy::BumpVersions then update_pyproject_version(req)
|
97
|
+
when RequirementsUpdateStrategy::BumpVersionsIfNecessary then update_pyproject_version_if_needed(req)
|
98
|
+
else raise "Unexpected update strategy: #{update_strategy}"
|
99
|
+
end
|
100
|
+
rescue UnfixableRequirement
|
101
|
+
req.merge(requirement: :unfixable)
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_pyproject_version_if_needed(req)
|
105
|
+
return req if new_version_satisfies?(req)
|
106
|
+
|
107
|
+
update_pyproject_version(req)
|
108
|
+
end
|
109
|
+
|
110
|
+
def update_pyproject_version(req)
|
111
|
+
requirement_strings = req[:requirement].split(",").map(&:strip)
|
112
|
+
|
113
|
+
new_requirement =
|
114
|
+
if requirement_strings.any? { |r| r.match?(/^=|^\d/) }
|
115
|
+
# If there is an equality operator, just update that. It must
|
116
|
+
# be binding and any other requirements will be being ignored
|
117
|
+
find_and_update_equality_match(requirement_strings)
|
118
|
+
elsif requirement_strings.any? { |r| r.start_with?("~", "^") }
|
119
|
+
# If a compatibility operator is being used, just bump its
|
120
|
+
# version (and remove any other requirements)
|
121
|
+
v_req = requirement_strings.find { |r| r.start_with?("~", "^") }
|
122
|
+
bump_version(v_req, latest_resolvable_version.to_s)
|
123
|
+
elsif new_version_satisfies?(req)
|
124
|
+
# Otherwise we're looking at a range operator. No change
|
125
|
+
# required if it's already satisfied
|
126
|
+
req.fetch(:requirement)
|
127
|
+
else
|
128
|
+
# But if it's not, update it
|
129
|
+
update_requirements_range(requirement_strings)
|
130
|
+
end
|
131
|
+
|
132
|
+
req.merge(requirement: new_requirement)
|
133
|
+
end
|
134
|
+
|
135
|
+
def widen_pyproject_requirement(req)
|
136
|
+
return req if new_version_satisfies?(req)
|
137
|
+
|
138
|
+
new_requirement =
|
139
|
+
if req[:requirement].match?(PYPROJECT_OR_SEPARATOR)
|
140
|
+
add_new_requirement_option(req[:requirement])
|
141
|
+
else
|
142
|
+
widen_requirement_range(req[:requirement])
|
143
|
+
end
|
144
|
+
|
145
|
+
req.merge(requirement: new_requirement)
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_new_requirement_option(req_string)
|
149
|
+
option_to_copy = req_string.split(PYPROJECT_OR_SEPARATOR).last
|
150
|
+
.split(PYPROJECT_SEPARATOR).first.strip
|
151
|
+
operator = option_to_copy.gsub(/\d.*/, "").strip
|
152
|
+
|
153
|
+
new_option =
|
154
|
+
case operator
|
155
|
+
when "", "==", "==="
|
156
|
+
find_and_update_equality_match([option_to_copy])
|
157
|
+
when "~=", "~", "^"
|
158
|
+
bump_version(option_to_copy, latest_resolvable_version.to_s)
|
159
|
+
else
|
160
|
+
# We don't expect to see OR conditions used with range
|
161
|
+
# operators. If / when we see it, we should handle it.
|
162
|
+
raise "Unexpected operator: #{operator}"
|
163
|
+
end
|
164
|
+
|
165
|
+
# TODO: Match source spacing
|
166
|
+
"#{req_string.strip} || #{new_option.strip}"
|
167
|
+
end
|
168
|
+
|
169
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
170
|
+
def widen_requirement_range(req_string)
|
171
|
+
requirement_strings = req_string.split(",").map(&:strip)
|
172
|
+
|
173
|
+
if requirement_strings.any? { |r| r.match?(/(^=|^\d)[^*]*$/) }
|
174
|
+
# If there is an equality operator, just update that.
|
175
|
+
# (i.e., assume it's being used deliberately)
|
176
|
+
find_and_update_equality_match(requirement_strings)
|
177
|
+
elsif requirement_strings.any? { |r| r.start_with?("~", "^") } ||
|
178
|
+
requirement_strings.any? { |r| r.include?("*") }
|
179
|
+
# If a compatibility operator is being used, widen its
|
180
|
+
# range to include the new version
|
181
|
+
v_req = requirement_strings
|
182
|
+
.find { |r| r.start_with?("~", "^") || r.include?("*") }
|
183
|
+
convert_to_range(v_req, latest_resolvable_version)
|
184
|
+
else
|
185
|
+
# Otherwise we have a range, and need to update the upper bound
|
186
|
+
update_requirements_range(requirement_strings)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
190
|
+
|
191
|
+
def updated_requirement(req)
|
192
|
+
return req unless latest_resolvable_version
|
193
|
+
return req unless req.fetch(:requirement)
|
194
|
+
|
195
|
+
case update_strategy
|
196
|
+
when RequirementsUpdateStrategy::WidenRanges
|
197
|
+
widen_requirement(req)
|
198
|
+
when RequirementsUpdateStrategy::BumpVersions
|
199
|
+
update_requirement(req)
|
200
|
+
when RequirementsUpdateStrategy::BumpVersionsIfNecessary
|
201
|
+
update_requirement_if_needed(req)
|
202
|
+
else
|
203
|
+
raise "Unexpected update strategy: #{update_strategy}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def update_requirement_if_needed(req)
|
208
|
+
return req if new_version_satisfies?(req)
|
209
|
+
|
210
|
+
update_requirement(req)
|
211
|
+
end
|
212
|
+
|
213
|
+
def update_requirement(req)
|
214
|
+
requirement_strings = req[:requirement].split(",").map(&:strip)
|
215
|
+
|
216
|
+
new_requirement =
|
217
|
+
if requirement_strings.any? { |r| r.match?(/^[=\d]/) }
|
218
|
+
find_and_update_equality_match(requirement_strings)
|
219
|
+
elsif requirement_strings.any? { |r| r.start_with?("~=") }
|
220
|
+
tw_req = requirement_strings.find { |r| r.start_with?("~=") }
|
221
|
+
bump_version(tw_req, latest_resolvable_version.to_s)
|
222
|
+
elsif new_version_satisfies?(req)
|
223
|
+
req.fetch(:requirement)
|
224
|
+
else
|
225
|
+
update_requirements_range(requirement_strings)
|
226
|
+
end
|
227
|
+
req.merge(requirement: new_requirement)
|
228
|
+
rescue UnfixableRequirement
|
229
|
+
req.merge(requirement: :unfixable)
|
230
|
+
end
|
231
|
+
|
232
|
+
def widen_requirement(req)
|
233
|
+
return req if new_version_satisfies?(req)
|
234
|
+
|
235
|
+
new_requirement = widen_requirement_range(req[:requirement])
|
236
|
+
|
237
|
+
req.merge(requirement: new_requirement)
|
238
|
+
end
|
239
|
+
|
240
|
+
def new_version_satisfies?(req)
|
241
|
+
requirement_class
|
242
|
+
.requirements_array(req.fetch(:requirement))
|
243
|
+
.any? { |r| r.satisfied_by?(latest_resolvable_version) }
|
244
|
+
end
|
245
|
+
|
246
|
+
def find_and_update_equality_match(requirement_strings)
|
247
|
+
if requirement_strings.any? { |r| requirement_class.new(r).exact? }
|
248
|
+
# True equality match
|
249
|
+
requirement_strings.find { |r| requirement_class.new(r).exact? }
|
250
|
+
.sub(
|
251
|
+
RequirementParser::VERSION,
|
252
|
+
latest_resolvable_version.to_s
|
253
|
+
)
|
254
|
+
else
|
255
|
+
# Prefix match
|
256
|
+
requirement_strings.find { |r| r.match?(/^(=+|\d)/) }
|
257
|
+
.sub(RequirementParser::VERSION) do |v|
|
258
|
+
at_same_precision(latest_resolvable_version.to_s, v)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def at_same_precision(new_version, old_version)
|
264
|
+
# return new_version unless old_version.include?("*")
|
265
|
+
|
266
|
+
count = old_version.split(".").count
|
267
|
+
precision = old_version.split(".").index("*") || count
|
268
|
+
|
269
|
+
new_version
|
270
|
+
.split(".")
|
271
|
+
.first(count)
|
272
|
+
.map.with_index { |s, i| i < precision ? s : "*" }
|
273
|
+
.join(".")
|
274
|
+
end
|
275
|
+
|
276
|
+
def update_requirements_range(requirement_strings)
|
277
|
+
ruby_requirements =
|
278
|
+
requirement_strings.map { |r| requirement_class.new(r) }
|
279
|
+
|
280
|
+
updated_requirement_strings = ruby_requirements.flat_map do |r|
|
281
|
+
next r.to_s if r.satisfied_by?(latest_resolvable_version)
|
282
|
+
|
283
|
+
case op = r.requirements.first.first
|
284
|
+
when "<"
|
285
|
+
"<" + update_greatest_version(r.requirements.first.last, latest_resolvable_version)
|
286
|
+
when "<="
|
287
|
+
"<=" + latest_resolvable_version.to_s
|
288
|
+
when "!=", ">", ">="
|
289
|
+
raise UnfixableRequirement
|
290
|
+
else
|
291
|
+
raise "Unexpected op for unsatisfied requirement: #{op}"
|
292
|
+
end
|
293
|
+
end.compact
|
294
|
+
|
295
|
+
updated_requirement_strings
|
296
|
+
.sort_by { |r| requirement_class.new(r).requirements.first.last }
|
297
|
+
.map(&:to_s).join(",").delete(" ")
|
298
|
+
end
|
299
|
+
|
300
|
+
# Updates the version in a constraint to be the given version
|
301
|
+
def bump_version(req_string, version_to_be_permitted)
|
302
|
+
old_version = req_string
|
303
|
+
.match(/(#{RequirementParser::VERSION})/o)
|
304
|
+
.captures.first
|
305
|
+
|
306
|
+
req_string.sub(
|
307
|
+
old_version,
|
308
|
+
at_same_precision(version_to_be_permitted, old_version)
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
def convert_to_range(req_string, version_to_be_permitted)
|
313
|
+
# Construct an upper bound at the same precision that the original
|
314
|
+
# requirement was at (taking into account ~ dynamics)
|
315
|
+
index_to_update = index_to_update_for(req_string)
|
316
|
+
ub_segments = version_to_be_permitted.segments
|
317
|
+
ub_segments << 0 while ub_segments.count <= index_to_update
|
318
|
+
ub_segments = ub_segments[0..index_to_update]
|
319
|
+
ub_segments[index_to_update] += 1
|
320
|
+
|
321
|
+
lb_segments = lower_bound_segments_for_req(req_string)
|
322
|
+
|
323
|
+
# Ensure versions have the same length as each other (cosmetic)
|
324
|
+
length = [lb_segments.count, ub_segments.count].max
|
325
|
+
lb_segments.fill(0, lb_segments.count...length)
|
326
|
+
ub_segments.fill(0, ub_segments.count...length)
|
327
|
+
|
328
|
+
">=#{lb_segments.join('.')},<#{ub_segments.join('.')}"
|
329
|
+
end
|
330
|
+
|
331
|
+
def lower_bound_segments_for_req(req_string)
|
332
|
+
requirement = requirement_class.new(req_string)
|
333
|
+
version = requirement.requirements.first.last
|
334
|
+
version = version.release if version.prerelease?
|
335
|
+
|
336
|
+
lb_segments = version.segments
|
337
|
+
lb_segments.pop while lb_segments.last.zero?
|
338
|
+
|
339
|
+
lb_segments
|
340
|
+
end
|
341
|
+
|
342
|
+
def index_to_update_for(req_string)
|
343
|
+
req = requirement_class.new(req_string.split(/[.\-]\*/).first)
|
344
|
+
version = req.requirements.first.last.release
|
345
|
+
|
346
|
+
if req_string.strip.start_with?("^")
|
347
|
+
version.segments.index { |i| i != 0 }
|
348
|
+
elsif req_string.include?("*")
|
349
|
+
version.segments.count - 1
|
350
|
+
elsif req_string.strip.start_with?("~=", "==")
|
351
|
+
version.segments.count - 2
|
352
|
+
elsif req_string.strip.start_with?("~")
|
353
|
+
req_string.split(".").count == 1 ? 0 : 1
|
354
|
+
else
|
355
|
+
raise "Don't know how to convert #{req_string} to range"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Updates the version in a "<" constraint to allow the given version
|
360
|
+
def update_greatest_version(version, version_to_be_permitted)
|
361
|
+
if version_to_be_permitted.is_a?(String)
|
362
|
+
version_to_be_permitted =
|
363
|
+
Uv::Version.new(version_to_be_permitted)
|
364
|
+
end
|
365
|
+
version = version.release if version.prerelease?
|
366
|
+
|
367
|
+
index_to_update = [
|
368
|
+
version.segments.map.with_index { |n, i| n.zero? ? 0 : i }.max,
|
369
|
+
version_to_be_permitted.segments.count - 1
|
370
|
+
].min
|
371
|
+
|
372
|
+
new_segments = version.segments.map.with_index do |_, index|
|
373
|
+
if index < index_to_update
|
374
|
+
version_to_be_permitted.segments[index]
|
375
|
+
elsif index == index_to_update
|
376
|
+
version_to_be_permitted.segments[index] + 1
|
377
|
+
else
|
378
|
+
0
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
new_segments.join(".")
|
383
|
+
end
|
384
|
+
|
385
|
+
def requirement_class
|
386
|
+
Uv::Requirement
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "excon"
|
5
|
+
require "toml-rb"
|
6
|
+
|
7
|
+
require "dependabot/dependency"
|
8
|
+
require "dependabot/errors"
|
9
|
+
require "dependabot/uv/name_normaliser"
|
10
|
+
require "dependabot/uv/requirement_parser"
|
11
|
+
require "dependabot/uv/requirement"
|
12
|
+
require "dependabot/registry_client"
|
13
|
+
require "dependabot/requirements_update_strategy"
|
14
|
+
require "dependabot/update_checkers"
|
15
|
+
require "dependabot/update_checkers/base"
|
16
|
+
|
17
|
+
module Dependabot
|
18
|
+
module Uv
|
19
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
20
|
+
require_relative "update_checker/pip_compile_version_resolver"
|
21
|
+
require_relative "update_checker/pip_version_resolver"
|
22
|
+
require_relative "update_checker/requirements_updater"
|
23
|
+
require_relative "update_checker/latest_version_finder"
|
24
|
+
|
25
|
+
MAIN_PYPI_INDEXES = %w(
|
26
|
+
https://pypi.python.org/simple/
|
27
|
+
https://pypi.org/simple/
|
28
|
+
).freeze
|
29
|
+
VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
|
30
|
+
|
31
|
+
def latest_version
|
32
|
+
@latest_version ||= fetch_latest_version
|
33
|
+
end
|
34
|
+
|
35
|
+
def latest_resolvable_version
|
36
|
+
@latest_resolvable_version ||=
|
37
|
+
if resolver_type == :requirements
|
38
|
+
resolver.latest_resolvable_version
|
39
|
+
elsif resolver_type == :pip_compile && resolver.resolvable?(version: latest_version)
|
40
|
+
latest_version
|
41
|
+
else
|
42
|
+
resolver.latest_resolvable_version(
|
43
|
+
requirement: unlocked_requirement_string
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def latest_resolvable_version_with_no_unlock
|
49
|
+
@latest_resolvable_version_with_no_unlock ||=
|
50
|
+
if resolver_type == :requirements
|
51
|
+
resolver.latest_resolvable_version_with_no_unlock
|
52
|
+
else
|
53
|
+
resolver.latest_resolvable_version(
|
54
|
+
requirement: current_requirement_string
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def lowest_security_fix_version
|
60
|
+
latest_version_finder.lowest_security_fix_version
|
61
|
+
end
|
62
|
+
|
63
|
+
def lowest_resolvable_security_fix_version
|
64
|
+
raise "Dependency not vulnerable!" unless vulnerable?
|
65
|
+
|
66
|
+
return @lowest_resolvable_security_fix_version if defined?(@lowest_resolvable_security_fix_version)
|
67
|
+
|
68
|
+
@lowest_resolvable_security_fix_version =
|
69
|
+
fetch_lowest_resolvable_security_fix_version
|
70
|
+
end
|
71
|
+
|
72
|
+
def updated_requirements
|
73
|
+
RequirementsUpdater.new(
|
74
|
+
requirements: requirements,
|
75
|
+
latest_resolvable_version: preferred_resolvable_version&.to_s,
|
76
|
+
update_strategy: requirements_update_strategy,
|
77
|
+
has_lockfile: requirements_text_file?
|
78
|
+
).updated_requirements
|
79
|
+
end
|
80
|
+
|
81
|
+
def requirements_unlocked_or_can_be?
|
82
|
+
!requirements_update_strategy.lockfile_only?
|
83
|
+
end
|
84
|
+
|
85
|
+
def requirements_update_strategy
|
86
|
+
# If passed in as an option (in the base class) honour that option
|
87
|
+
return @requirements_update_strategy if @requirements_update_strategy
|
88
|
+
|
89
|
+
# Otherwise, check if this is a library or not
|
90
|
+
library? ? RequirementsUpdateStrategy::WidenRanges : RequirementsUpdateStrategy::BumpVersions
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def latest_version_resolvable_with_full_unlock?
|
96
|
+
# Full unlock checks aren't implemented for Python (yet)
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def updated_dependencies_after_full_unlock
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
def fetch_lowest_resolvable_security_fix_version
|
105
|
+
fix_version = lowest_security_fix_version
|
106
|
+
return latest_resolvable_version if fix_version.nil?
|
107
|
+
|
108
|
+
return resolver.lowest_resolvable_security_fix_version if resolver_type == :requirements
|
109
|
+
|
110
|
+
resolver.resolvable?(version: fix_version) ? fix_version : nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def resolver
|
114
|
+
if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
|
115
|
+
Dependabot.logger.info("Python package resolver : #{resolver_type}")
|
116
|
+
end
|
117
|
+
|
118
|
+
case resolver_type
|
119
|
+
when :pip_compile then pip_compile_version_resolver
|
120
|
+
when :requirements then pip_version_resolver
|
121
|
+
else raise "Unexpected resolver type #{resolver_type}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def resolver_type
|
126
|
+
reqs = requirements
|
127
|
+
|
128
|
+
# If there are no requirements then this is a sub-dependency. It
|
129
|
+
# must come from one of Pipenv, Poetry or pip-tools, and can't come
|
130
|
+
# from the first two unless they have a lockfile.
|
131
|
+
return subdependency_resolver if reqs.none?
|
132
|
+
|
133
|
+
# Otherwise, this is a top-level dependency, and we can figure out
|
134
|
+
# which resolver to use based on the filename of its requirements
|
135
|
+
return :requirements if updating_pyproject?
|
136
|
+
return :pip_compile if updating_in_file?
|
137
|
+
|
138
|
+
if dependency.version && !exact_requirement?(reqs)
|
139
|
+
subdependency_resolver
|
140
|
+
else
|
141
|
+
:requirements
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def subdependency_resolver
|
146
|
+
return :pip_compile if pip_compile_files.any?
|
147
|
+
|
148
|
+
raise "Claimed to be a sub-dependency, but no lockfile exists!"
|
149
|
+
end
|
150
|
+
|
151
|
+
def exact_requirement?(reqs)
|
152
|
+
reqs = reqs.map { |r| r.fetch(:requirement) }
|
153
|
+
reqs = reqs.compact
|
154
|
+
reqs = reqs.flat_map { |r| r.split(",").map(&:strip) }
|
155
|
+
reqs.any? { |r| Uv::Requirement.new(r).exact? }
|
156
|
+
end
|
157
|
+
|
158
|
+
def pip_compile_version_resolver
|
159
|
+
@pip_compile_version_resolver ||=
|
160
|
+
PipCompileVersionResolver.new(**resolver_args)
|
161
|
+
end
|
162
|
+
|
163
|
+
def pip_version_resolver
|
164
|
+
@pip_version_resolver ||= PipVersionResolver.new(
|
165
|
+
dependency: dependency,
|
166
|
+
dependency_files: dependency_files,
|
167
|
+
credentials: credentials,
|
168
|
+
ignored_versions: ignored_versions,
|
169
|
+
raise_on_ignored: @raise_on_ignored,
|
170
|
+
security_advisories: security_advisories
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
def resolver_args
|
175
|
+
{
|
176
|
+
dependency: dependency,
|
177
|
+
dependency_files: dependency_files,
|
178
|
+
credentials: credentials,
|
179
|
+
repo_contents_path: repo_contents_path
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def current_requirement_string
|
184
|
+
reqs = requirements
|
185
|
+
return if reqs.none?
|
186
|
+
|
187
|
+
requirement = reqs.find do |r|
|
188
|
+
file = r[:file]
|
189
|
+
|
190
|
+
file == "Pipfile" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt")
|
191
|
+
end
|
192
|
+
|
193
|
+
requirement&.fetch(:requirement)
|
194
|
+
end
|
195
|
+
|
196
|
+
def unlocked_requirement_string
|
197
|
+
lower_bound_req = updated_version_req_lower_bound
|
198
|
+
|
199
|
+
# Add the latest_version as an upper bound. This means
|
200
|
+
# ignore conditions are considered when checking for the latest
|
201
|
+
# resolvable version.
|
202
|
+
#
|
203
|
+
# NOTE: This isn't perfect. If v2.x is ignored and v3 is out but
|
204
|
+
# unresolvable then the `latest_version` will be v3, and
|
205
|
+
# we won't be ignoring v2.x releases like we should be.
|
206
|
+
return lower_bound_req if latest_version.nil?
|
207
|
+
return lower_bound_req unless Uv::Version.correct?(latest_version)
|
208
|
+
|
209
|
+
lower_bound_req + ",<=#{latest_version}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def updated_version_req_lower_bound
|
213
|
+
return ">=#{dependency.version}" if dependency.version
|
214
|
+
|
215
|
+
version_for_requirement =
|
216
|
+
requirements.filter_map { |r| r[:requirement] }
|
217
|
+
.reject { |req_string| req_string.start_with?("<") }
|
218
|
+
.select { |req_string| req_string.match?(VERSION_REGEX) }
|
219
|
+
.map { |req_string| req_string.match(VERSION_REGEX).to_s }
|
220
|
+
.select { |version| Uv::Version.correct?(version) }
|
221
|
+
.max_by { |version| Uv::Version.new(version) }
|
222
|
+
|
223
|
+
">=#{version_for_requirement || 0}"
|
224
|
+
end
|
225
|
+
|
226
|
+
def fetch_latest_version
|
227
|
+
latest_version_finder.latest_version
|
228
|
+
end
|
229
|
+
|
230
|
+
def latest_version_finder
|
231
|
+
@latest_version_finder ||= LatestVersionFinder.new(
|
232
|
+
dependency: dependency,
|
233
|
+
dependency_files: dependency_files,
|
234
|
+
credentials: credentials,
|
235
|
+
ignored_versions: ignored_versions,
|
236
|
+
raise_on_ignored: @raise_on_ignored,
|
237
|
+
security_advisories: security_advisories
|
238
|
+
)
|
239
|
+
end
|
240
|
+
|
241
|
+
def library?
|
242
|
+
return false unless updating_pyproject?
|
243
|
+
|
244
|
+
return false if library_details["name"].nil?
|
245
|
+
|
246
|
+
# Hit PyPi and check whether there are details for a library with a
|
247
|
+
# matching name and description
|
248
|
+
index_response = Dependabot::RegistryClient.get(
|
249
|
+
url: "https://pypi.org/pypi/#{normalised_name(library_details['name'])}/json/"
|
250
|
+
)
|
251
|
+
|
252
|
+
return false unless index_response.status == 200
|
253
|
+
|
254
|
+
pypi_info = JSON.parse(index_response.body)["info"] || {}
|
255
|
+
pypi_info["summary"] == library_details["description"]
|
256
|
+
rescue Excon::Error::Timeout, Excon::Error::Socket
|
257
|
+
false
|
258
|
+
rescue URI::InvalidURIError
|
259
|
+
false
|
260
|
+
end
|
261
|
+
|
262
|
+
def updating_pyproject?
|
263
|
+
requirement_files.any?("pyproject.toml")
|
264
|
+
end
|
265
|
+
|
266
|
+
def updating_in_file?
|
267
|
+
requirement_files.any? { |f| f.end_with?(".in") }
|
268
|
+
end
|
269
|
+
|
270
|
+
def requirements_text_file?
|
271
|
+
requirement_files.any? { |f| f.end_with?("requirements.txt") }
|
272
|
+
end
|
273
|
+
|
274
|
+
def updating_requirements_file?
|
275
|
+
requirement_files.any? { |f| f =~ /\.txt$|\.in$/ }
|
276
|
+
end
|
277
|
+
|
278
|
+
def requirement_files
|
279
|
+
requirements.map { |r| r.fetch(:file) }
|
280
|
+
end
|
281
|
+
|
282
|
+
def requirements
|
283
|
+
dependency.requirements
|
284
|
+
end
|
285
|
+
|
286
|
+
def normalised_name(name)
|
287
|
+
NameNormaliser.normalise(name)
|
288
|
+
end
|
289
|
+
|
290
|
+
def pyproject
|
291
|
+
dependency_files.find { |f| f.name == "pyproject.toml" }
|
292
|
+
end
|
293
|
+
|
294
|
+
def library_details
|
295
|
+
@library_details ||= standard_details || build_system_details
|
296
|
+
end
|
297
|
+
|
298
|
+
def standard_details
|
299
|
+
@standard_details ||= toml_content["project"]
|
300
|
+
end
|
301
|
+
|
302
|
+
def build_system_details
|
303
|
+
@build_system_details ||= toml_content["build-system"]
|
304
|
+
end
|
305
|
+
|
306
|
+
def toml_content
|
307
|
+
@toml_content ||= TomlRB.parse(pyproject.content)
|
308
|
+
end
|
309
|
+
|
310
|
+
def pip_compile_files
|
311
|
+
dependency_files.select { |f| f.name.end_with?(".in") }
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
Dependabot::UpdateCheckers.register("uv", Dependabot::Uv::UpdateChecker)
|