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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +34 -0
  3. data/helpers/lib/__init__.py +0 -0
  4. data/helpers/lib/hasher.py +36 -0
  5. data/helpers/lib/parser.py +270 -0
  6. data/helpers/requirements.txt +13 -0
  7. data/helpers/run.py +22 -0
  8. data/lib/dependabot/uv/authed_url_builder.rb +31 -0
  9. data/lib/dependabot/uv/file_fetcher.rb +328 -0
  10. data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
  11. data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
  12. data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
  13. data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
  14. data/lib/dependabot/uv/file_parser.rb +437 -0
  15. data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
  16. data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
  17. data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
  18. data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
  19. data/lib/dependabot/uv/file_updater.rb +105 -0
  20. data/lib/dependabot/uv/language.rb +76 -0
  21. data/lib/dependabot/uv/language_version_manager.rb +114 -0
  22. data/lib/dependabot/uv/metadata_finder.rb +186 -0
  23. data/lib/dependabot/uv/name_normaliser.rb +26 -0
  24. data/lib/dependabot/uv/native_helpers.rb +38 -0
  25. data/lib/dependabot/uv/package_manager.rb +54 -0
  26. data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
  27. data/lib/dependabot/uv/pipenv_runner.rb +108 -0
  28. data/lib/dependabot/uv/requirement.rb +163 -0
  29. data/lib/dependabot/uv/requirement_parser.rb +60 -0
  30. data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
  31. data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
  32. data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
  33. data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
  34. data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
  35. data/lib/dependabot/uv/update_checker.rb +317 -0
  36. data/lib/dependabot/uv/version.rb +321 -0
  37. data/lib/dependabot/uv.rb +35 -0
  38. 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)