dependabot-conda 0.348.1 → 0.350.0
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 +4 -4
- data/lib/dependabot/conda/conda_registry_client.rb +220 -0
- data/lib/dependabot/conda/file_fetcher.rb +115 -66
- data/lib/dependabot/conda/file_parser.rb +40 -38
- data/lib/dependabot/conda/file_updater.rb +40 -16
- data/lib/dependabot/conda/metadata_finder.rb +8 -15
- data/lib/dependabot/conda/update_checker/latest_version_finder.rb +111 -6
- data/lib/dependabot/conda/update_checker/requirements_updater.rb +428 -0
- data/lib/dependabot/conda/update_checker.rb +16 -63
- data/lib/dependabot/conda/version.rb +317 -6
- data/lib/dependabot/conda.rb +4 -3
- metadata +11 -10
- data/lib/dependabot/conda/python_package_classifier.rb +0 -88
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/conda/requirement"
|
|
6
|
+
require "dependabot/conda/update_checker"
|
|
7
|
+
require "dependabot/conda/version"
|
|
8
|
+
require "dependabot/requirements_update_strategy"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Conda
|
|
12
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
13
|
+
class RequirementsUpdater
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
17
|
+
attr_reader :requirements
|
|
18
|
+
|
|
19
|
+
sig { returns(Dependabot::RequirementsUpdateStrategy) }
|
|
20
|
+
attr_reader :update_strategy
|
|
21
|
+
|
|
22
|
+
sig { returns(T.nilable(Dependabot::Conda::Version)) }
|
|
23
|
+
attr_reader :latest_resolvable_version
|
|
24
|
+
|
|
25
|
+
sig do
|
|
26
|
+
params(
|
|
27
|
+
requirements: T::Array[T::Hash[Symbol, T.untyped]],
|
|
28
|
+
update_strategy: Dependabot::RequirementsUpdateStrategy,
|
|
29
|
+
latest_resolvable_version: T.nilable(String)
|
|
30
|
+
).void
|
|
31
|
+
end
|
|
32
|
+
def initialize(requirements:, update_strategy:, latest_resolvable_version:)
|
|
33
|
+
@requirements = requirements
|
|
34
|
+
@update_strategy = update_strategy
|
|
35
|
+
@latest_resolvable_version = T.let(
|
|
36
|
+
(Conda::Version.new(latest_resolvable_version) if latest_resolvable_version),
|
|
37
|
+
T.nilable(Dependabot::Conda::Version)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
42
|
+
def updated_requirements
|
|
43
|
+
return requirements if update_strategy.lockfile_only?
|
|
44
|
+
return requirements unless latest_resolvable_version
|
|
45
|
+
|
|
46
|
+
requirements.map do |req|
|
|
47
|
+
case update_strategy
|
|
48
|
+
when RequirementsUpdateStrategy::WidenRanges
|
|
49
|
+
widen_requirement(req)
|
|
50
|
+
when RequirementsUpdateStrategy::BumpVersions
|
|
51
|
+
update_requirement(req)
|
|
52
|
+
when RequirementsUpdateStrategy::BumpVersionsIfNecessary
|
|
53
|
+
update_requirement_if_needed(req)
|
|
54
|
+
else
|
|
55
|
+
raise "Unexpected update strategy: #{update_strategy}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
63
|
+
def update_requirement_if_needed(req)
|
|
64
|
+
return req if new_version_satisfies?(req)
|
|
65
|
+
|
|
66
|
+
update_requirement(req)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
70
|
+
def update_requirement(req)
|
|
71
|
+
return req unless req[:requirement]
|
|
72
|
+
return req if ["", "*"].include?(req[:requirement])
|
|
73
|
+
|
|
74
|
+
requirement_strings = req[:requirement].split(",").map(&:strip)
|
|
75
|
+
new_req = calculate_updated_requirement(req, requirement_strings)
|
|
76
|
+
|
|
77
|
+
new_req == :unfixable ? req.merge(requirement: :unfixable) : req.merge(requirement: new_req)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(req: T::Hash[Symbol, T.untyped], requirement_strings: T::Array[String]).returns(T.any(String, Symbol)) }
|
|
81
|
+
def calculate_updated_requirement(req, requirement_strings)
|
|
82
|
+
# Step 1: Check for equality match first (e.g., "==1.21.0" or bare "1.21.0")
|
|
83
|
+
return handle_equality_match(requirement_strings) if equality_match?(requirement_strings)
|
|
84
|
+
|
|
85
|
+
# Step 2: Handle range requirements (e.g., ">=3.10,<3.12")
|
|
86
|
+
return handle_range_requirement(req, requirement_strings) if requirement_strings.length > 1
|
|
87
|
+
|
|
88
|
+
# Step 3: Handle single constraint (e.g., ">=3.10")
|
|
89
|
+
handle_single_constraint(req)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { params(requirement_strings: T::Array[String]).returns(T::Boolean) }
|
|
93
|
+
def equality_match?(requirement_strings)
|
|
94
|
+
requirement_strings.any? { |r| r.match?(/^[=\d]/) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig { params(requirement_strings: T::Array[String]).returns(T.any(String, Symbol)) }
|
|
98
|
+
def handle_equality_match(requirement_strings)
|
|
99
|
+
find_and_update_equality_match(requirement_strings, latest_resolvable_version)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig { params(req: T::Hash[Symbol, T.untyped], requirement_strings: T::Array[String]).returns(T.any(String, Symbol)) }
|
|
103
|
+
def handle_range_requirement(req, requirement_strings)
|
|
104
|
+
# Only skip update if using BumpVersionsIfNecessary strategy and version already satisfies
|
|
105
|
+
# For BumpVersions strategy, always update to the new version
|
|
106
|
+
if update_strategy == RequirementsUpdateStrategy::BumpVersionsIfNecessary &&
|
|
107
|
+
new_version_satisfies?(req)
|
|
108
|
+
return req[:requirement]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
update_requirements_range(requirement_strings)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T.any(String, Symbol)) }
|
|
115
|
+
def handle_single_constraint(req)
|
|
116
|
+
# Only skip update if using BumpVersionsIfNecessary strategy and version already satisfies
|
|
117
|
+
# For BumpVersions strategy, always update to the new version
|
|
118
|
+
if update_strategy == RequirementsUpdateStrategy::BumpVersionsIfNecessary &&
|
|
119
|
+
new_version_satisfies?(req)
|
|
120
|
+
return req[:requirement]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
bump_version_string(req[:requirement], T.must(latest_resolvable_version).to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig do
|
|
127
|
+
params(
|
|
128
|
+
requirement_strings: T::Array[String],
|
|
129
|
+
latest_version: T.nilable(Conda::Version)
|
|
130
|
+
).returns(T.any(String, Symbol))
|
|
131
|
+
end
|
|
132
|
+
def find_and_update_equality_match(requirement_strings, latest_version)
|
|
133
|
+
return :unfixable unless latest_version
|
|
134
|
+
|
|
135
|
+
current_requirement = requirement_strings.join(",")
|
|
136
|
+
|
|
137
|
+
# If dealing with a bare version number, treat it as exact match
|
|
138
|
+
if requirement_strings.length == 1 && T.must(requirement_strings.first).match?(/^\d/)
|
|
139
|
+
return "==#{latest_version}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Find the equality constraint (= or ==)
|
|
143
|
+
equality_req = requirement_strings.find { |r| r.match?(/^=+/) }
|
|
144
|
+
return current_requirement unless equality_req
|
|
145
|
+
|
|
146
|
+
# Extract version from equality constraint
|
|
147
|
+
version_string = equality_req.sub(/^=+\s*/, "")
|
|
148
|
+
|
|
149
|
+
# Preserve wildcard precision if present
|
|
150
|
+
return preserve_wildcard_precision(equality_req, latest_version.to_s) if version_string.include?("*")
|
|
151
|
+
|
|
152
|
+
# Determine operator (= or ==)
|
|
153
|
+
operator = equality_req.match?(/^==/) ? "==" : "="
|
|
154
|
+
|
|
155
|
+
# Standard equality update
|
|
156
|
+
"#{operator}#{latest_version}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
160
|
+
def widen_requirement(req)
|
|
161
|
+
return req unless req[:requirement]
|
|
162
|
+
return req if ["", "*"].include?(req[:requirement])
|
|
163
|
+
|
|
164
|
+
# For WidenRanges, always widen to ensure proper upper bounds
|
|
165
|
+
# Don't return early even if version satisfies - we want to add/update bounds
|
|
166
|
+
new_requirement = widen_requirement_string(req[:requirement])
|
|
167
|
+
req.merge(requirement: new_requirement)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
|
171
|
+
def new_version_satisfies?(req)
|
|
172
|
+
return false unless req[:requirement]
|
|
173
|
+
|
|
174
|
+
Conda::Requirement
|
|
175
|
+
.requirements_array(req[:requirement])
|
|
176
|
+
.all? { |r| r.satisfied_by?(T.must(latest_resolvable_version)) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
sig { params(req_string: String, new_version: String).returns(T.any(String, Symbol)) }
|
|
180
|
+
def bump_version_string(req_string, new_version)
|
|
181
|
+
# Strip whitespace for matching but preserve operator
|
|
182
|
+
stripped = req_string.strip
|
|
183
|
+
|
|
184
|
+
# Parse the current requirement to preserve the operator type
|
|
185
|
+
case stripped
|
|
186
|
+
when /^=\s*([0-9])/
|
|
187
|
+
# Conda exact version: =1.26 or =1.21.*
|
|
188
|
+
if stripped.include?("*")
|
|
189
|
+
# Wildcard: =1.21.* → =2.3.* (preserve wildcard pattern at new major.minor)
|
|
190
|
+
preserve_wildcard_precision(stripped, new_version)
|
|
191
|
+
else
|
|
192
|
+
# Exact: =1.26 → =2.3.4
|
|
193
|
+
"=#{new_version}"
|
|
194
|
+
end
|
|
195
|
+
when /^==\s*([0-9])/
|
|
196
|
+
# Pip exact version: ==1.26 or ==1.21.*
|
|
197
|
+
if stripped.include?("*")
|
|
198
|
+
preserve_wildcard_precision(stripped, new_version)
|
|
199
|
+
else
|
|
200
|
+
"==#{new_version}"
|
|
201
|
+
end
|
|
202
|
+
when /^>=\s*([0-9])/
|
|
203
|
+
# Range constraint: >=1.26 → >=2.3.4
|
|
204
|
+
# Check if version is too high (unfixable)
|
|
205
|
+
current_version_str = stripped[/>=\s*([\d.]+)/, 1]
|
|
206
|
+
if current_version_str && Conda::Version.new(current_version_str) > Conda::Version.new(new_version)
|
|
207
|
+
return :unfixable
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
">=#{new_version}"
|
|
211
|
+
when /^>\s*([0-9])/
|
|
212
|
+
# Greater than: >1.26 → >2.3.4
|
|
213
|
+
">#{new_version}"
|
|
214
|
+
when /^~=\s*([0-9])/
|
|
215
|
+
# Compatible release: ~=1.26 → ~=2.3.4
|
|
216
|
+
"~=#{new_version}"
|
|
217
|
+
when /^<=/, /^</, /^!=/
|
|
218
|
+
# Upper bound or not-equal constraints: keep unchanged
|
|
219
|
+
req_string
|
|
220
|
+
else
|
|
221
|
+
# Default to conda-style equality
|
|
222
|
+
"=#{new_version}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
sig { params(req_string: String, new_version: String).returns(String) }
|
|
227
|
+
def preserve_wildcard_precision(req_string, new_version)
|
|
228
|
+
# Count asterisks in original to preserve precision
|
|
229
|
+
# =1.21.* → =2.3.* (preserve major.minor.*)
|
|
230
|
+
# =1.* → =2.* (preserve major.*)
|
|
231
|
+
|
|
232
|
+
operator = req_string[/^[=~><!]+/] || "="
|
|
233
|
+
wildcard_parts = req_string.scan(/\d+|\*/)
|
|
234
|
+
new_parts = new_version.split(".")
|
|
235
|
+
|
|
236
|
+
# Build new requirement with same wildcard pattern
|
|
237
|
+
result_parts = []
|
|
238
|
+
wildcard_parts.each_with_index do |part, idx|
|
|
239
|
+
if part == "*"
|
|
240
|
+
result_parts << "*"
|
|
241
|
+
break # Stop after first wildcard
|
|
242
|
+
else
|
|
243
|
+
result_parts << (new_parts[idx] || "0")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
"#{operator}#{result_parts.join('.')}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
sig { params(req_string: String).returns(T.any(String, Symbol)) }
|
|
251
|
+
def widen_requirement_string(req_string)
|
|
252
|
+
# Convert wildcards and exact matches to ranges
|
|
253
|
+
# Order matters: check >= before = to avoid partial matches
|
|
254
|
+
|
|
255
|
+
if req_string.include?("*")
|
|
256
|
+
# Wildcard: =1.21.* → >=1.21,<3.0 (widen to major version range)
|
|
257
|
+
convert_wildcard_to_range(req_string)
|
|
258
|
+
elsif req_string.match?(/^>=/)
|
|
259
|
+
# Already a range (>=), update or add upper bound
|
|
260
|
+
result = update_range_upper_bound(req_string)
|
|
261
|
+
return result if result == :unfixable
|
|
262
|
+
|
|
263
|
+
result
|
|
264
|
+
elsif req_string.match?(/^(==?)\s*\d/)
|
|
265
|
+
# Exact match: =1.26 or ==1.26 → >=1.26,<3.0
|
|
266
|
+
convert_exact_to_range(req_string)
|
|
267
|
+
elsif req_string.match?(/^~=/)
|
|
268
|
+
# Compatible release: ~=1.3.0 → >=1.3,<3.0
|
|
269
|
+
convert_compatible_to_range(req_string)
|
|
270
|
+
elsif req_string.match?(/^(<=|<|!=)/)
|
|
271
|
+
# Upper bound or not-equal constraints: keep unchanged
|
|
272
|
+
req_string
|
|
273
|
+
else
|
|
274
|
+
# Unknown format, bump version as fallback
|
|
275
|
+
bump_version_string(req_string, T.must(latest_resolvable_version).to_s)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
sig { params(req_string: String).returns(String) }
|
|
280
|
+
def convert_wildcard_to_range(req_string)
|
|
281
|
+
# =1.21.* becomes >=1.21,<3.0 (or whatever major version latest is)
|
|
282
|
+
version_match = req_string.match(/(\d+(?:\.\d+)*)/)
|
|
283
|
+
return req_string unless version_match
|
|
284
|
+
|
|
285
|
+
lower_bound = version_match[1]
|
|
286
|
+
new_version = T.must(latest_resolvable_version)
|
|
287
|
+
upper_major = new_version.version_parts[0].to_i + 1
|
|
288
|
+
|
|
289
|
+
">=#{lower_bound},<#{upper_major}.0"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
sig { params(req_string: String).returns(String) }
|
|
293
|
+
def convert_exact_to_range(req_string)
|
|
294
|
+
# =1.26 becomes >=1.26,<3.0
|
|
295
|
+
version_match = req_string.match(/(\d+(?:\.\d+)*)/)
|
|
296
|
+
return req_string unless version_match
|
|
297
|
+
|
|
298
|
+
lower_bound = version_match[1]
|
|
299
|
+
new_version = T.must(latest_resolvable_version)
|
|
300
|
+
upper_major = new_version.version_parts[0].to_i + 1
|
|
301
|
+
|
|
302
|
+
">=#{lower_bound},<#{upper_major}.0"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
sig { params(req_string: String).returns(T.any(String, Symbol)) }
|
|
306
|
+
def update_range_upper_bound(req_string)
|
|
307
|
+
# >=1.26,<2.0 becomes >=1.26,<3.0
|
|
308
|
+
# Check if lower bound is too high (unfixable)
|
|
309
|
+
lower_bound_match = req_string.match(/>=\s*([\d.]+)/)
|
|
310
|
+
if lower_bound_match
|
|
311
|
+
lower_version = Conda::Version.new(lower_bound_match[1])
|
|
312
|
+
return :unfixable if lower_version > T.must(latest_resolvable_version)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
new_version = T.must(latest_resolvable_version)
|
|
316
|
+
upper_major = new_version.version_parts[0].to_i + 1
|
|
317
|
+
|
|
318
|
+
if req_string.include?(",<")
|
|
319
|
+
# Replace upper bound
|
|
320
|
+
req_string.sub(/,<[\d.]+/, ",<#{upper_major}.0")
|
|
321
|
+
else
|
|
322
|
+
# Add upper bound
|
|
323
|
+
"#{req_string},<#{upper_major}.0"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
sig { params(req_string: String).returns(String) }
|
|
328
|
+
def convert_compatible_to_range(req_string)
|
|
329
|
+
# ~=1.3.0 becomes >=1.3,<3.0
|
|
330
|
+
version_match = req_string.match(/~=\s*([\d.]+)/)
|
|
331
|
+
return req_string unless version_match
|
|
332
|
+
|
|
333
|
+
lower_bound = version_match[1]
|
|
334
|
+
# Extract major.minor for lower bound
|
|
335
|
+
parts = T.must(lower_bound).split(".")
|
|
336
|
+
lower_parts = parts.take(2)
|
|
337
|
+
lower_bound_simplified = lower_parts.join(".")
|
|
338
|
+
|
|
339
|
+
new_version = T.must(latest_resolvable_version)
|
|
340
|
+
upper_major = new_version.version_parts[0].to_i + 1
|
|
341
|
+
|
|
342
|
+
">=#{lower_bound_simplified},<#{upper_major}.0"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
sig { params(requirement_strings: T::Array[String]).returns(T.any(String, Symbol)) }
|
|
346
|
+
def update_requirements_range(requirement_strings)
|
|
347
|
+
# Handle comma-separated requirements like ">=3.10,<3.12"
|
|
348
|
+
# For BumpVersions strategy (matching Python's logic):
|
|
349
|
+
# - Keep constraints that already satisfy the new version
|
|
350
|
+
# - Update upper bounds (<, <=) that don't satisfy
|
|
351
|
+
# - Lower bounds (>=, >) that don't satisfy are UNFIXABLE
|
|
352
|
+
|
|
353
|
+
updated_parts = requirement_strings.map do |req_str|
|
|
354
|
+
stripped = req_str.strip
|
|
355
|
+
|
|
356
|
+
# Check if this individual constraint is satisfied by new version
|
|
357
|
+
if Conda::Requirement.requirements_array(stripped).any? { |r| r.satisfied_by?(T.must(latest_resolvable_version)) }
|
|
358
|
+
# Already satisfied - keep unchanged
|
|
359
|
+
stripped
|
|
360
|
+
elsif stripped.match?(/^</)
|
|
361
|
+
# Upper bound not satisfied - update to accommodate new version
|
|
362
|
+
update_upper_bound(stripped)
|
|
363
|
+
elsif stripped.match?(/^>=|^>/)
|
|
364
|
+
# Lower bound not satisfied - this is unfixable for BumpVersions
|
|
365
|
+
# (We don't lower the minimum version requirement)
|
|
366
|
+
return :unfixable
|
|
367
|
+
elsif stripped.match?(/^!=/)
|
|
368
|
+
# Exclusion not satisfied (new version equals excluded version) - unfixable
|
|
369
|
+
return :unfixable
|
|
370
|
+
else
|
|
371
|
+
# Unknown constraint - keep unchanged
|
|
372
|
+
stripped
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
updated_parts.join(",")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
sig { params(upper_bound_str: String).returns(String) }
|
|
380
|
+
def update_upper_bound(upper_bound_str)
|
|
381
|
+
# Update upper bound to accommodate new version using Python's algorithm
|
|
382
|
+
new_version = T.must(latest_resolvable_version)
|
|
383
|
+
|
|
384
|
+
if upper_bound_str.start_with?("<=")
|
|
385
|
+
# <= constraint: update to new version exactly
|
|
386
|
+
"<=#{new_version}"
|
|
387
|
+
elsif upper_bound_str.start_with?("<")
|
|
388
|
+
# < constraint: calculate appropriate next version
|
|
389
|
+
# Extract current upper bound version
|
|
390
|
+
current_upper = upper_bound_str.sub(/^<\s*/, "")
|
|
391
|
+
updated_version = calculate_next_version_bound(current_upper, new_version)
|
|
392
|
+
"<#{updated_version}"
|
|
393
|
+
else
|
|
394
|
+
# Shouldn't reach here, but return unchanged
|
|
395
|
+
upper_bound_str
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
sig { params(current_upper: String, new_version: Conda::Version).returns(String) }
|
|
400
|
+
def calculate_next_version_bound(current_upper, new_version)
|
|
401
|
+
# Python's algorithm: find the rightmost non-zero segment in current upper bound
|
|
402
|
+
# and increment the corresponding segment in the new version
|
|
403
|
+
current_segments = current_upper.split(".").map(&:to_i)
|
|
404
|
+
new_segments = new_version.version_parts.map(&:to_i)
|
|
405
|
+
|
|
406
|
+
# Find the index of the rightmost non-zero segment in current upper bound
|
|
407
|
+
index_to_update = current_segments.map.with_index { |n, i| n.to_i.zero? ? 0 : i }.max || 0
|
|
408
|
+
|
|
409
|
+
# Ensure we don't go beyond the new version's segment count
|
|
410
|
+
index_to_update = [index_to_update, new_segments.count - 1].min
|
|
411
|
+
|
|
412
|
+
# Build new upper bound
|
|
413
|
+
result_segments = new_segments.map.with_index do |_, index|
|
|
414
|
+
if index < index_to_update
|
|
415
|
+
new_segments[index]
|
|
416
|
+
elsif index == index_to_update
|
|
417
|
+
T.must(new_segments[index]) + 1
|
|
418
|
+
else
|
|
419
|
+
0
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
result_segments.join(".")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
@@ -6,7 +6,7 @@ require "dependabot/update_checkers"
|
|
|
6
6
|
require "dependabot/update_checkers/base"
|
|
7
7
|
require "dependabot/conda/version"
|
|
8
8
|
require "dependabot/conda/requirement"
|
|
9
|
-
require "dependabot/
|
|
9
|
+
require "dependabot/requirements_update_strategy"
|
|
10
10
|
|
|
11
11
|
module Dependabot
|
|
12
12
|
module Conda
|
|
@@ -50,28 +50,24 @@ module Dependabot
|
|
|
50
50
|
|
|
51
51
|
sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
|
|
52
52
|
def latest_version
|
|
53
|
+
return nil if dependency.requirements.all? { |req| req[:requirement].nil? || req[:requirement] == "*" }
|
|
54
|
+
|
|
53
55
|
@latest_version ||= fetch_latest_version
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
|
|
57
59
|
def latest_resolvable_version_with_no_unlock
|
|
58
|
-
# For now, same as latest_version since we're not doing full dependency resolution
|
|
59
60
|
latest_version
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
|
|
63
64
|
def latest_resolvable_version
|
|
64
|
-
# For Phase 3, delegate to latest_version_finder
|
|
65
|
-
# This will be enhanced with actual conda search and PyPI integration
|
|
66
65
|
latest_version
|
|
67
66
|
end
|
|
68
67
|
|
|
69
68
|
sig { override.returns(T::Boolean) }
|
|
70
69
|
def up_to_date?
|
|
71
70
|
return true if latest_version.nil?
|
|
72
|
-
|
|
73
|
-
# If dependency has no version (range constraint like >=2.0),
|
|
74
|
-
# we can't determine if it's up-to-date, so assume it needs checking
|
|
75
71
|
return false if dependency.version.nil?
|
|
76
72
|
|
|
77
73
|
T.must(latest_version) <= Dependabot::Conda::Version.new(dependency.version)
|
|
@@ -79,8 +75,6 @@ module Dependabot
|
|
|
79
75
|
|
|
80
76
|
sig { override.returns(T::Boolean) }
|
|
81
77
|
def requirements_unlocked_or_can_be?
|
|
82
|
-
# For conda, we don't have lock files, so requirements can always be updated
|
|
83
|
-
# This is unlike other ecosystems that have lock files (package-lock.json, Pipfile.lock, etc.)
|
|
84
78
|
true
|
|
85
79
|
end
|
|
86
80
|
|
|
@@ -102,17 +96,18 @@ module Dependabot
|
|
|
102
96
|
|
|
103
97
|
sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
104
98
|
def updated_requirements
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
RequirementsUpdater.new(
|
|
100
|
+
requirements: dependency.requirements,
|
|
101
|
+
update_strategy: requirements_update_strategy,
|
|
102
|
+
latest_resolvable_version: preferred_resolvable_version&.to_s
|
|
103
|
+
).updated_requirements
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { override.returns(Dependabot::RequirementsUpdateStrategy) }
|
|
107
|
+
def requirements_update_strategy
|
|
108
|
+
return @requirements_update_strategy if @requirements_update_strategy
|
|
109
|
+
|
|
110
|
+
RequirementsUpdateStrategy::BumpVersions
|
|
116
111
|
end
|
|
117
112
|
|
|
118
113
|
private
|
|
@@ -140,11 +135,8 @@ module Dependabot
|
|
|
140
135
|
|
|
141
136
|
sig { returns(T.nilable(Dependabot::Version)) }
|
|
142
137
|
def fetch_lowest_resolvable_security_fix_version
|
|
143
|
-
# Delegate to latest_version_finder for security fix resolution
|
|
144
|
-
# This leverages Python ecosystem's security advisory infrastructure
|
|
145
138
|
fix_version = latest_version_finder.lowest_security_fix_version
|
|
146
139
|
|
|
147
|
-
# If no security fix version is found, fall back to latest_resolvable_version
|
|
148
140
|
if fix_version.nil?
|
|
149
141
|
fallback = latest_resolvable_version
|
|
150
142
|
return fallback.is_a?(String) ? Dependabot::Conda::Version.new(fallback) : fallback
|
|
@@ -155,7 +147,6 @@ module Dependabot
|
|
|
155
147
|
|
|
156
148
|
sig { override.returns(T::Boolean) }
|
|
157
149
|
def latest_version_resolvable_with_full_unlock?
|
|
158
|
-
# No lock file support for Conda
|
|
159
150
|
false
|
|
160
151
|
end
|
|
161
152
|
|
|
@@ -163,49 +154,11 @@ module Dependabot
|
|
|
163
154
|
def updated_dependencies_after_full_unlock
|
|
164
155
|
raise NotImplementedError
|
|
165
156
|
end
|
|
166
|
-
|
|
167
|
-
sig { params(requirement_string: String, new_version: String).returns(String) }
|
|
168
|
-
def update_requirement_string(requirement_string, new_version)
|
|
169
|
-
# Parse the current requirement to preserve the operator type
|
|
170
|
-
case requirement_string
|
|
171
|
-
when /^=([0-9])/
|
|
172
|
-
# Conda exact version: =1.26 -> =2.3.2
|
|
173
|
-
"=#{new_version}"
|
|
174
|
-
when /^==([0-9])/
|
|
175
|
-
# Pip exact version: ==1.26 -> ==2.3.2
|
|
176
|
-
"==#{new_version}"
|
|
177
|
-
when /^>=([0-9])/
|
|
178
|
-
# Range constraint: preserve as range but update to new version
|
|
179
|
-
">=#{new_version}"
|
|
180
|
-
when /^>([0-9])/
|
|
181
|
-
# Greater than: >1.26 -> >2.3.2
|
|
182
|
-
">#{new_version}"
|
|
183
|
-
when /^<=([0-9])/
|
|
184
|
-
# Less than or equal: keep as is (shouldn't be updated)
|
|
185
|
-
requirement_string
|
|
186
|
-
when /^<([0-9])/
|
|
187
|
-
# Less than: keep as is (shouldn't be updated)
|
|
188
|
-
requirement_string
|
|
189
|
-
when /^!=([0-9])/
|
|
190
|
-
# Not equal: keep as is
|
|
191
|
-
requirement_string
|
|
192
|
-
when /^~=([0-9])/
|
|
193
|
-
# Compatible release: ~=1.26 -> ~=2.3.2
|
|
194
|
-
"~=#{new_version}"
|
|
195
|
-
else
|
|
196
|
-
# Default to conda-style equality for unknown patterns
|
|
197
|
-
"=#{new_version}"
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
sig { params(package_name: String).returns(T::Boolean) }
|
|
202
|
-
def python_package?(package_name)
|
|
203
|
-
PythonPackageClassifier.python_package?(package_name)
|
|
204
|
-
end
|
|
205
157
|
end
|
|
206
158
|
end
|
|
207
159
|
end
|
|
208
160
|
|
|
209
161
|
require_relative "update_checker/latest_version_finder"
|
|
162
|
+
require_relative "update_checker/requirements_updater"
|
|
210
163
|
|
|
211
164
|
Dependabot::UpdateCheckers.register("conda", Dependabot::Conda::UpdateChecker)
|