dependabot-uv 0.393.0 → 0.394.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0b2c86fecd6361c11d682fd96da7135b08179f251f7a524d045824341ccbe9e
4
- data.tar.gz: df43361f2dd0a76a17da2a6ea510086da7adadbe859d1d91975be7ebe84a61bc
3
+ metadata.gz: f9eee59202ca318e612e23d7303309e498d51999cda642a1660d41390a2bd4a8
4
+ data.tar.gz: 3b3bb2ea60f190db5d8c9bd36f1ff01531097788c3755c6638d5a4e8e3ebf923
5
5
  SHA512:
6
- metadata.gz: 3117ccb8929d12b1caff2b31607e8b459fbb61008495f3f9c5b195e7d4b888cabd0d2b47de2535d130660db0b4fe51fc2aede86b856bcd237d5f3e2eb20da37b
7
- data.tar.gz: 162286da713782ed33c25c4fcea096c4365edcf219ad4da6bb93812fd64792a569e92e86b3c20d4f8814f10f141b1bf010ba0c3cf44a3c6e553c814e1006aad2
6
+ metadata.gz: 2f6d9c0f1ea2c05bab8acea5563a4a74c016ab9b8bd77639afe9468a8f7b8cb18d238585b5bdc11b255bf8e8d0af01d7d470c07ab9b83792d8001156bb73a199
7
+ data.tar.gz: '09c83e67c0b94adf7ad3f848f17521b57e282ef33db9d4ac0b5991d0ea8135024fc9eb5e78a1515a7f9f4c826f8db3347e25b24ed842ed85681a0b3dd8289e94'
@@ -1,13 +1,13 @@
1
- pip==26.1.1
2
- pip-tools==7.5.3
1
+ pip==26.2.1
2
+ pip-tools==7.6.1
3
3
  flake8==7.3.0
4
4
  hashin==1.0.5
5
5
  pipenv==2024.4.1
6
6
  plette==2.2.1
7
- poetry==2.3.4
7
+ poetry==2.4.1
8
8
  # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
9
9
  tomli==2.4.1
10
- uv==0.12.1
10
+ uv==0.12.5
11
11
 
12
12
  # Some dependencies will only install if Cython is present
13
- Cython==3.2.4
13
+ Cython==3.3.0
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -9,6 +9,9 @@ require "dependabot/uv/file_parser"
9
9
  require "dependabot/uv/requirement"
10
10
  require "dependabot/errors"
11
11
  require "dependabot/uv/name_normaliser"
12
+ require "dependabot/python/file_parser/pep_dependency"
13
+ require "dependabot/python/file_parser/poetry_lock"
14
+ require "dependabot/python/file_parser/pyproject_document"
12
15
 
13
16
  module Dependabot
14
17
  module Uv
@@ -16,6 +19,10 @@ module Dependabot
16
19
  class PyprojectFilesParser
17
20
  extend T::Sig
18
21
 
22
+ PepDependency = Dependabot::Python::FileParser::PepDependency
23
+ PoetryLock = Dependabot::Python::FileParser::PoetryLock
24
+ PyprojectDocument = Dependabot::Python::FileParser::PyprojectDocument
25
+
19
26
  POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze
20
27
 
21
28
  # https://python-poetry.org/docs/dependency-specification/
@@ -24,6 +31,8 @@ module Dependabot
24
31
  sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
25
32
  def initialize(dependency_files:)
26
33
  @dependency_files = dependency_files
34
+ @pyproject_document = T.let(nil, T.nilable(PyprojectDocument))
35
+ @poetry_lock_document = T.let(nil, T.nilable(PoetryLock))
27
36
  end
28
37
 
29
38
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
@@ -61,13 +70,11 @@ module Dependabot
61
70
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
62
71
 
63
72
  POETRY_DEPENDENCY_TYPES.each do |type|
64
- deps_hash = T.must(poetry_root)[type] || {}
65
- dependencies += parse_poetry_dependency_group(type, deps_hash)
73
+ dependencies += parse_poetry_dependency_group(type, pyproject_document.poetry_dependencies(type))
66
74
  end
67
75
 
68
- groups = T.must(poetry_root)["group"] || {}
69
- groups.each do |group, group_spec|
70
- dependencies += parse_poetry_dependency_group(group, group_spec["dependencies"])
76
+ pyproject_document.poetry_groups.each do |group, dependencies_by_name|
77
+ dependencies += parse_poetry_dependency_group(group, dependencies_by_name)
71
78
  end
72
79
  dependencies
73
80
  end
@@ -85,20 +92,20 @@ module Dependabot
85
92
  parse_pep621_pep735_dependencies(pyproject_file).each do |dep|
86
93
  # If a requirement has a `<` or `<=` marker then updating it is
87
94
  # probably blocked. Ignore it.
88
- next if dep["markers"]&.include?("<")
95
+ next if dep.markers&.include?("<")
89
96
 
90
97
  # In uv no constraint means any version is acceptable
91
- requirement_value = dep["requirement"] && dep["requirement"].empty? ? "*" : dep["requirement"]
98
+ requirement_value = dep.requirement == "" ? "*" : dep.requirement
92
99
 
93
100
  dependencies <<
94
101
  Dependency.new(
95
- name: normalised_name(dep["name"], dep["extras"]),
96
- version: dep["version"]&.include?("*") ? nil : dep["version"],
102
+ name: normalised_name(dep.name, dep.extras),
103
+ version: dep.version&.include?("*") ? nil : dep.version,
97
104
  requirements: [{
98
105
  requirement: requirement_value,
99
- file: Pathname.new(dep["file"]).cleanpath.to_path,
106
+ file: Pathname.new(dep.file).cleanpath.to_path,
100
107
  source: nil,
101
- groups: [dep["requirement_type"]].compact
108
+ groups: [dep.requirement_type].compact
102
109
  }],
103
110
  package_manager: "uv"
104
111
  )
@@ -139,8 +146,7 @@ module Dependabot
139
146
  def workspace_globs(key)
140
147
  return [] unless pyproject
141
148
 
142
- globs = parsed_pyproject.dig("tool", "uv", "workspace", key)
143
- globs.is_a?(Array) ? globs.grep(String) : []
149
+ pyproject_document.workspace_globs(key)
144
150
  end
145
151
 
146
152
  sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
@@ -172,17 +178,16 @@ module Dependabot
172
178
  sig do
173
179
  params(
174
180
  type: String,
175
- deps_hash: T::Hash[String,
176
- T.untyped]
181
+ deps_hash: PyprojectDocument::PoetryDependencyMap
177
182
  ).returns(Dependabot::FileParsers::Base::DependencySet)
178
183
  end
179
184
  def parse_poetry_dependency_group(type, deps_hash)
180
185
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
181
186
 
182
- deps_hash.each do |name, req|
187
+ deps_hash.each do |name, requirements_data|
183
188
  next if normalise(name) == "python"
184
189
 
185
- requirements = parse_requirements_from(req, type)
190
+ requirements = parse_requirements_from(requirements_data, type)
186
191
  next if requirements.empty?
187
192
 
188
193
  dependencies << Dependency.new(
@@ -200,57 +205,62 @@ module Dependabot
200
205
  NameNormaliser.normalise_including_extras(name, extras)
201
206
  end
202
207
 
203
- # @param req can be an Array, Hash or String that represents the constraints for a dependency
204
- sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
205
- def parse_requirements_from(req, type)
206
- [req].flatten.compact.filter_map do |requirement|
207
- next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
208
+ sig do
209
+ params(
210
+ requirements_data: T::Array[PyprojectDocument::PoetryRequirement],
211
+ type: String
212
+ ).returns(T::Array[Dependabot::Dependency::RequirementInput])
213
+ end
214
+ def parse_requirements_from(requirements_data, type)
215
+ requirements_data.filter_map { |requirement| poetry_requirement(requirement, type) }
216
+ end
208
217
 
218
+ sig do
219
+ params(
220
+ requirement: PyprojectDocument::PoetryRequirement,
221
+ type: String
222
+ ).returns(T.nilable(Dependabot::Dependency::RequirementInput))
223
+ end
224
+ def poetry_requirement(requirement, type)
225
+ if requirement.is_a?(String)
209
226
  check_requirements(requirement)
210
-
211
- if requirement.is_a?(String)
212
- {
213
- requirement: requirement,
214
- file: T.must(pyproject).name,
215
- source: nil,
216
- groups: [type]
217
- }
218
- else
219
- {
220
- requirement: requirement["version"],
221
- file: T.must(pyproject).name,
222
- source: requirement.fetch("source", nil),
223
- groups: [type]
224
- }
225
- end
227
+ return {
228
+ requirement: requirement,
229
+ file: T.must(pyproject).name,
230
+ source: nil,
231
+ groups: [type]
232
+ }
226
233
  end
234
+
235
+ return if UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
236
+
237
+ check_requirements(requirement)
238
+ {
239
+ requirement: requirement_string(requirement, "version"),
240
+ file: T.must(pyproject).name,
241
+ source: poetry_source(requirement["source"]),
242
+ groups: [type]
243
+ }
227
244
  end
228
245
 
229
- sig { returns(T.nilable(T::Boolean)) }
246
+ sig { returns(T::Boolean) }
230
247
  def using_poetry?
231
- !poetry_root.nil?
248
+ pyproject_document.poetry?
232
249
  end
233
250
 
234
251
  sig { returns(T::Boolean) }
235
252
  def using_pep621?
236
- !parsed_pyproject.dig("project", "dependencies").nil? ||
237
- !parsed_pyproject.dig("project", "optional-dependencies").nil? ||
238
- !parsed_pyproject.dig("build-system", "requires").nil?
239
- end
240
-
241
- sig { returns(T.nilable(T::Hash[String, T.untyped])) }
242
- def poetry_root
243
- parsed_pyproject.dig("tool", "poetry")
253
+ pyproject_document.pep621?
244
254
  end
245
255
 
246
256
  sig { returns(T::Boolean) }
247
257
  def using_pep735?
248
- parsed_pyproject.key?("dependency-groups")
258
+ pyproject_document.pep735?
249
259
  end
250
260
 
251
- sig { returns(T.untyped) }
261
+ sig { returns(T::Boolean) }
252
262
  def using_pdm?
253
- using_pep621? && pdm_lock
263
+ using_pep621? && !pdm_lock.nil?
254
264
  end
255
265
 
256
266
  # Create a DependencySet where each element has no requirement. Any
@@ -261,15 +271,15 @@ module Dependabot
261
271
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
262
272
 
263
273
  source_types = %w(directory git url)
264
- parsed_lockfile.fetch("package", []).each do |details|
265
- next if source_types.include?(details.dig("source", "type"))
274
+ poetry_lock_document.packages.each do |package|
275
+ next if source_types.include?(package.source_type)
266
276
 
267
- name = normalise(details.fetch("name"))
277
+ name = normalise(package.name)
268
278
 
269
279
  dependencies <<
270
280
  Dependency.new(
271
281
  name: name,
272
- version: details.fetch("version"),
282
+ version: package.version,
273
283
  requirements: [],
274
284
  package_manager: "uv",
275
285
  subdependency_metadata: [{
@@ -292,8 +302,10 @@ module Dependabot
292
302
  sig { returns(T::Array[T.nilable(String)]) }
293
303
  def parse_production_dependency_names
294
304
  SharedHelpers.in_a_temporary_directory do
295
- File.write(T.must(pyproject).name, T.must(pyproject).content)
296
- File.write(lockfile.name, lockfile.content)
305
+ pyproject_file = T.must(pyproject)
306
+ lockfile_file = T.must(lockfile)
307
+ File.write(pyproject_file.name, pyproject_file.content)
308
+ File.write(lockfile_file.name, lockfile_file.content)
297
309
 
298
310
  begin
299
311
  output = SharedHelpers.run_shell_command("pyenv exec poetry show --only main")
@@ -312,18 +324,19 @@ module Dependabot
312
324
  end
313
325
  end
314
326
 
315
- sig { params(dep_name: String).returns(T.untyped) }
327
+ sig { params(dep_name: String).returns(T.nilable(String)) }
316
328
  def version_from_lockfile(dep_name)
317
- return unless parsed_lockfile
329
+ return unless poetry_lock
318
330
 
319
- parsed_lockfile.fetch("package", [])
320
- .find { |p| normalise(p.fetch("name")) == normalise(dep_name) }
321
- &.fetch("version", nil)
331
+ poetry_lock_document.version_for(dep_name) { |name| normalise(name) }
322
332
  end
323
333
 
324
- sig { params(req: T.untyped).returns(T::Array[Dependabot::Uv::Requirement]) }
334
+ sig do
335
+ params(req: PyprojectDocument::PoetryRequirement)
336
+ .returns(T::Array[Dependabot::Uv::Requirement])
337
+ end
325
338
  def check_requirements(req)
326
- requirement = req.is_a?(String) ? req : req["version"]
339
+ requirement = req.is_a?(String) ? req : requirement_string(req, "version")
327
340
  Uv::Requirement.requirements_array(requirement)
328
341
  rescue Gem::Requirement::BadRequirementError => e
329
342
  raise Dependabot::DependencyFileNotEvaluatable, e.message
@@ -334,21 +347,14 @@ module Dependabot
334
347
  NameNormaliser.normalise(name)
335
348
  end
336
349
 
337
- sig { returns(T::Hash[String, T.untyped]) }
338
- def parsed_pyproject
339
- @parsed_pyproject ||= T.let(TomlRB.parse(T.must(pyproject).content), T.nilable(T::Hash[String, T.untyped]))
340
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
341
- raise Dependabot::DependencyFileNotParseable, T.must(pyproject).path
350
+ sig { returns(PyprojectDocument) }
351
+ def pyproject_document
352
+ @pyproject_document ||= PyprojectDocument.from_file(T.must(pyproject))
342
353
  end
343
354
 
344
- sig { returns(T::Hash[String, T.untyped]) }
345
- def parsed_poetry_lock
346
- @parsed_poetry_lock ||= T.let(
347
- TomlRB.parse(T.must(poetry_lock).content),
348
- T.nilable(T::Hash[String, T.untyped])
349
- )
350
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
351
- raise Dependabot::DependencyFileNotParseable, T.must(poetry_lock).path
355
+ sig { returns(PoetryLock) }
356
+ def poetry_lock_document
357
+ @poetry_lock_document ||= PoetryLock.from_file(T.must(poetry_lock))
352
358
  end
353
359
 
354
360
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -359,20 +365,22 @@ module Dependabot
359
365
  )
360
366
  end
361
367
 
362
- sig { returns(T.untyped) }
368
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
363
369
  def lockfile
364
370
  poetry_lock
365
371
  end
366
372
 
367
- sig { params(pyproject_file: Dependabot::DependencyFile).returns(T.untyped) }
373
+ sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[PepDependency]) }
368
374
  def parse_pep621_pep735_dependencies(pyproject_file)
369
375
  SharedHelpers.in_a_temporary_directory do
370
376
  write_temporary_pyproject(pyproject_file)
371
377
 
372
- SharedHelpers.run_helper_subprocess(
373
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
374
- function: "parse_pep621_pep735_dependencies",
375
- args: [pyproject_file.name]
378
+ PepDependency.from_helper_result(
379
+ SharedHelpers.run_helper_subprocess(
380
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
381
+ function: "parse_pep621_pep735_dependencies",
382
+ args: [pyproject_file.name]
383
+ )
376
384
  )
377
385
  end
378
386
  end
@@ -384,11 +392,6 @@ module Dependabot
384
392
  File.write(path, pyproject_file.content)
385
393
  end
386
394
 
387
- sig { returns(T.untyped) }
388
- def parsed_lockfile
389
- parsed_poetry_lock if poetry_lock
390
- end
391
-
392
395
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
393
396
  def poetry_lock
394
397
  @poetry_lock ||= T.let(
@@ -397,6 +400,36 @@ module Dependabot
397
400
  )
398
401
  end
399
402
 
403
+ sig do
404
+ params(
405
+ requirement: T::Hash[String, Object],
406
+ key: String
407
+ ).returns(T.nilable(String))
408
+ end
409
+ def requirement_string(requirement, key)
410
+ value = requirement[key]
411
+ return if value.nil?
412
+ return value if value.is_a?(String)
413
+
414
+ raise TypeError, "Poetry dependency #{key} must be a string or nil"
415
+ end
416
+
417
+ sig { params(value: T.nilable(Object)).returns(T.nilable(Dependabot::DependencyRequirement::Source)) }
418
+ def poetry_source(value)
419
+ return if value.nil?
420
+ return value if value.is_a?(String)
421
+ raise TypeError, "Poetry dependency source must be a string, object, or nil" unless value.is_a?(Hash)
422
+
423
+ source = T.let({}, Dependabot::DependencyRequirement::ObjectHash)
424
+ value.each do |raw_key, raw_value|
425
+ key = T.cast(raw_key, Object)
426
+ raise TypeError, "Poetry dependency source keys must be strings" unless key.is_a?(String)
427
+
428
+ source[key] = T.cast(raw_value, Object)
429
+ end
430
+ source
431
+ end
432
+
400
433
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
401
434
  def pdm_lock
402
435
  @pdm_lock ||= T.let(
@@ -44,21 +44,33 @@ module Dependabot
44
44
  sig { returns(T.nilable(String)) }
45
45
  attr_reader :repo_contents_path
46
46
 
47
+ sig { returns(T.nilable(String)) }
48
+ attr_reader :target_requirement
49
+
47
50
  sig do
48
51
  params(
49
52
  dependencies: T::Array[Dependency],
50
53
  dependency_files: T::Array[DependencyFile],
51
54
  credentials: T::Array[Dependabot::Credential],
52
55
  index_urls: T.nilable(T::Array[T.nilable(String)]),
53
- repo_contents_path: T.nilable(String)
56
+ repo_contents_path: T.nilable(String),
57
+ target_requirement: T.nilable(String)
54
58
  ).void
55
59
  end
56
- def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil, repo_contents_path: nil)
60
+ def initialize(
61
+ dependencies:,
62
+ dependency_files:,
63
+ credentials:,
64
+ index_urls: nil,
65
+ repo_contents_path: nil,
66
+ target_requirement: nil
67
+ )
57
68
  @dependencies = dependencies
58
69
  @dependency_files = dependency_files
59
70
  @credentials = credentials
60
71
  @index_urls = index_urls
61
72
  @repo_contents_path = repo_contents_path
73
+ @target_requirement = target_requirement
62
74
  @prepared_pyproject = T.let(nil, T.nilable(String))
63
75
  @updated_lockfile_content = T.let(nil, T.nilable(String))
64
76
  @pyproject = T.let(nil, T.nilable(Dependabot::DependencyFile))
@@ -112,7 +124,9 @@ module Dependabot
112
124
  # Use updated_lockfile_content which might raise if the lockfile doesn't change
113
125
  new_content = updated_lockfile_content
114
126
 
115
- raise "Expected lockfile to change!" if T.must(lockfile).content == new_content
127
+ if T.must(lockfile).content == new_content
128
+ raise DependencyFileContentNotChanged, "Expected lockfile to change!"
129
+ end
116
130
 
117
131
  updated_files << updated_file(file: T.must(lockfile), content: new_content)
118
132
  end
@@ -291,7 +305,14 @@ module Dependabot
291
305
  # Strip extras from the package name for the uv lock command
292
306
  # uv lock --upgrade-package expects the base package name without extras
293
307
  base_dep_name = normalise(dep_name)
294
- package_spec = dep_version ? "#{base_dep_name}==#{dep_version}" : base_dep_name
308
+ package_spec =
309
+ if target_requirement
310
+ "#{base_dep_name}#{target_requirement}"
311
+ elsif dep_version
312
+ "#{base_dep_name}==#{dep_version}"
313
+ else
314
+ base_dep_name
315
+ end
295
316
 
296
317
  command = "pyenv exec uv lock --upgrade-package #{package_spec} #{options}"
297
318
  fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
@@ -2,9 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "toml-rb"
5
6
 
7
+ require "dependabot/errors"
6
8
  require "dependabot/package/release_cooldown_options"
9
+ require "dependabot/update_checkers/version_filters"
7
10
  require "dependabot/uv/version"
11
+ require "dependabot/uv/file_updater/lock_file_error_handler"
12
+ require "dependabot/uv/file_updater/lock_file_updater"
13
+ require "dependabot/uv/file_parser/python_requirement_parser"
14
+ require "dependabot/uv/language_version_manager"
15
+ require "dependabot/uv/name_normaliser"
8
16
  require "dependabot/uv/requirement"
9
17
  require "dependabot/uv/update_checker"
10
18
  require "dependabot/uv/update_checker/latest_version_finder"
@@ -42,33 +50,42 @@ module Dependabot
42
50
  @security_advisories = security_advisories
43
51
  @ignored_versions = ignored_versions
44
52
  @update_cooldown = update_cooldown
53
+ @latest_resolvable_versions = T.let(
54
+ {},
55
+ T::Hash[String, T.nilable(Dependabot::Uv::Version)]
56
+ )
57
+ @resolved_versions = T.let(
58
+ {},
59
+ T::Hash[String, T.nilable(Dependabot::Uv::Version)]
60
+ )
61
+ @resolvable_versions = T.let({}, T::Hash[String, T::Boolean])
45
62
  end
46
63
 
47
64
  sig { params(requirement: T.nilable(String)).returns(T.nilable(Dependabot::Uv::Version)) }
48
65
  def latest_resolvable_version(requirement:)
49
66
  return nil unless requirement
67
+ return @latest_resolvable_versions[requirement] if @latest_resolvable_versions.key?(requirement)
50
68
 
51
- req = Uv::Requirement.new(requirement)
52
-
53
- # Get the version from the dependency if available
54
- version_from_dependency = dependency.version && Uv::Version.new(dependency.version)
55
- return version_from_dependency if version_from_dependency && req.satisfied_by?(version_from_dependency)
56
-
57
- nil
69
+ @latest_resolvable_versions[requirement] = fetch_latest_resolvable_version(requirement)
58
70
  end
59
71
 
60
- sig { params(_version: T.anything).returns(T::Boolean) }
61
- def resolvable?(_version)
62
- # Always return true since we don't actually attempt resolution
63
- # This is just a placeholder implementation
64
- true
72
+ sig { params(version: Gem::Version).returns(T::Boolean) }
73
+ def resolvable?(version:)
74
+ target_version = Uv::Version.new(version.to_s)
75
+ key = target_version.to_s
76
+ return T.must(@resolvable_versions[key]) if @resolvable_versions.key?(key)
77
+
78
+ @resolvable_versions[key] = resolved_version("==#{key}", target_version)&.to_s == key
65
79
  end
66
80
 
67
81
  sig { returns(T.nilable(Dependabot::Uv::Version)) }
68
82
  def lowest_resolvable_security_fix_version
69
83
  # Delegate to LatestVersionFinder which handles security advisory filtering
70
- fix_version = latest_version_finder.lowest_security_fix_version
71
- return nil if fix_version.nil?
84
+ fix_version = latest_version_finder.lowest_security_fix_version(
85
+ language_version: python_version
86
+ )
87
+ return nil unless fix_version
88
+ return nil unless resolvable?(version: fix_version)
72
89
 
73
90
  # Return the fix version cast to Uv::Version
74
91
  Uv::Version.new(fix_version.to_s)
@@ -97,6 +114,191 @@ module Dependabot
97
114
  sig { returns(T.nilable(Dependabot::Package::ReleaseCooldownOptions)) }
98
115
  attr_reader :update_cooldown
99
116
 
117
+ sig { params(requirement_string: String).returns(T.nilable(Dependabot::Uv::Version)) }
118
+ def fetch_latest_resolvable_version(requirement_string)
119
+ requirement = Uv::Requirement.new(requirement_string)
120
+ current_version = dependency.version && Uv::Version.new(dependency.version)
121
+ eligible_versions = eligible_versions(requirement, current_version)
122
+ return current_version if no_newer_eligible_version?(requirement, current_version, eligible_versions)
123
+
124
+ native_version = resolved_version(
125
+ policy_requirement(requirement_string, requirement, current_version, eligible_versions),
126
+ nil
127
+ )
128
+ if eligible_native_version?(requirement, native_version, eligible_versions) &&
129
+ resolvable?(version: T.must(native_version))
130
+ return native_version
131
+ end
132
+
133
+ current_version if current_version && requirement.satisfied_by?(current_version)
134
+ end
135
+
136
+ sig do
137
+ params(
138
+ requirement: Dependabot::Uv::Requirement,
139
+ current_version: T.nilable(Dependabot::Uv::Version),
140
+ eligible_versions: T.nilable(T::Array[Dependabot::Uv::Version])
141
+ ).returns(T::Boolean)
142
+ end
143
+ def no_newer_eligible_version?(requirement, current_version, eligible_versions)
144
+ return false unless current_version && eligible_versions
145
+
146
+ requirement.satisfied_by?(current_version) && eligible_versions.empty?
147
+ end
148
+
149
+ sig do
150
+ params(
151
+ requirement: Dependabot::Uv::Requirement,
152
+ native_version: T.nilable(Dependabot::Uv::Version),
153
+ eligible_versions: T.nilable(T::Array[Dependabot::Uv::Version])
154
+ ).returns(T::Boolean)
155
+ end
156
+ def eligible_native_version?(requirement, native_version, eligible_versions)
157
+ return false unless native_version && requirement.satisfied_by?(native_version)
158
+
159
+ !eligible_versions || eligible_versions.include?(native_version)
160
+ end
161
+
162
+ sig do
163
+ params(
164
+ requirement: Dependabot::Uv::Requirement,
165
+ current_version: T.nilable(Dependabot::Uv::Version)
166
+ ).returns(T.nilable(T::Array[Dependabot::Uv::Version]))
167
+ end
168
+ def eligible_versions(requirement, current_version)
169
+ releases = latest_version_finder.eligible_releases(
170
+ language_version: python_version
171
+ )
172
+ return unless releases
173
+
174
+ releases = Dependabot::UpdateCheckers::VersionFilters
175
+ .filter_vulnerable_versions(releases, security_advisories)
176
+ releases
177
+ .map { |release| Uv::Version.new(release.version.to_s) }
178
+ .select { |version| requirement.satisfied_by?(version) && (!current_version || version > current_version) }
179
+ end
180
+
181
+ sig do
182
+ params(
183
+ requirement_string: String,
184
+ requirement: Dependabot::Uv::Requirement,
185
+ current_version: T.nilable(Dependabot::Uv::Version),
186
+ eligible_versions: T.nilable(T::Array[Dependabot::Uv::Version])
187
+ ).returns(String)
188
+ end
189
+ def policy_requirement(requirement_string, requirement, current_version, eligible_versions)
190
+ releases = latest_version_finder.available_versions
191
+ return requirement_string unless releases && eligible_versions
192
+
193
+ excluded_versions = releases
194
+ .map { |release| Uv::Version.new(release.version.to_s) }
195
+ .select do |version|
196
+ requirement.satisfied_by?(version) &&
197
+ (!current_version || version > current_version) &&
198
+ !eligible_versions.include?(version)
199
+ end
200
+
201
+ ([requirement_string] + excluded_versions.map { |version| "!=#{version}" }).join(",")
202
+ end
203
+
204
+ sig do
205
+ params(
206
+ requirement_string: String,
207
+ target_version: T.nilable(Dependabot::Uv::Version)
208
+ )
209
+ .returns(T.nilable(Dependabot::Uv::Version))
210
+ end
211
+ def resolved_version(requirement_string, target_version)
212
+ return @resolved_versions[requirement_string] if @resolved_versions.key?(requirement_string)
213
+
214
+ @resolved_versions[requirement_string] = run_resolution(requirement_string, target_version)
215
+ end
216
+
217
+ sig do
218
+ params(
219
+ requirement_string: String,
220
+ target_version: T.nilable(Dependabot::Uv::Version)
221
+ )
222
+ .returns(T.nilable(Dependabot::Uv::Version))
223
+ end
224
+ def run_resolution(requirement_string, target_version)
225
+ updated_dependency = Dependabot::Dependency.new(
226
+ name: dependency.name,
227
+ version: nil,
228
+ previous_version: dependency.version,
229
+ requirements: [],
230
+ previous_requirements: [],
231
+ package_manager: dependency.package_manager
232
+ )
233
+
234
+ updated_files = FileUpdater::LockFileUpdater.new(
235
+ dependencies: [updated_dependency],
236
+ dependency_files: dependency_files,
237
+ credentials: credentials,
238
+ repo_contents_path: repo_contents_path,
239
+ target_requirement: requirement_string
240
+ ).updated_dependency_files
241
+
242
+ lockfile = updated_files.find { |file| file.name == "uv.lock" } || original_lockfile
243
+ resolved_locked_version(T.must(lockfile), target_version)
244
+ rescue Dependabot::DependencyFileContentNotChanged
245
+ resolved_locked_version(T.must(original_lockfile), target_version)
246
+ rescue Dependabot::UpdateNotPossible
247
+ nil
248
+ rescue Dependabot::DependencyFileNotResolvable => e
249
+ conflict = e.message.match?(FileUpdater::LockFileErrorHandler::UV_UNRESOLVABLE_REGEX) ||
250
+ e.message.include?(FileUpdater::LockFileErrorHandler::RESOLUTION_IMPOSSIBLE_ERROR)
251
+ raise unless conflict
252
+
253
+ nil
254
+ end
255
+
256
+ sig do
257
+ params(
258
+ lockfile: Dependabot::DependencyFile,
259
+ target_version: T.nilable(Dependabot::Uv::Version)
260
+ ).returns(T.nilable(Dependabot::Uv::Version))
261
+ end
262
+ def resolved_locked_version(lockfile, target_version)
263
+ original_versions = locked_versions(T.must(original_lockfile))
264
+ updated_versions = locked_versions(lockfile)
265
+ current_version = dependency.version && Uv::Version.new(dependency.version)
266
+ return nil unless current_version && original_versions.min == current_version
267
+ return current_version if updated_versions.include?(current_version)
268
+
269
+ updated_version = updated_versions.min
270
+ return target_version if target_version && updated_version == target_version
271
+ return nil if target_version
272
+
273
+ updated_version
274
+ end
275
+
276
+ sig do
277
+ params(lockfile: Dependabot::DependencyFile)
278
+ .returns(T::Array[Dependabot::Uv::Version])
279
+ end
280
+ def locked_versions(lockfile)
281
+ parsed = T.cast(TomlRB.parse(T.must(lockfile.content)), T::Hash[String, Object])
282
+ packages = T.cast(parsed["package"], T.nilable(T::Array[T::Hash[String, Object]])) || []
283
+ dependency_name = NameNormaliser.normalise(dependency.name)
284
+ versions = T.let([], T::Array[Dependabot::Uv::Version])
285
+ packages.each do |package|
286
+ package_name = T.cast(package["name"], T.nilable(String))
287
+ package_version = T.cast(package["version"], T.nilable(String))
288
+ next unless package_name && NameNormaliser.normalise(package_name) == dependency_name
289
+ next unless Uv::Version.correct?(package_version)
290
+
291
+ versions << Uv::Version.new(package_version)
292
+ end
293
+
294
+ versions
295
+ end
296
+
297
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
298
+ def original_lockfile
299
+ dependency_files.find { |file| file.name == "uv.lock" }
300
+ end
301
+
100
302
  sig { returns(LatestVersionFinder) }
101
303
  def latest_version_finder
102
304
  @latest_version_finder ||= T.let(
@@ -112,6 +314,18 @@ module Dependabot
112
314
  T.nilable(LatestVersionFinder)
113
315
  )
114
316
  end
317
+
318
+ sig { returns(String) }
319
+ def python_version
320
+ @python_version ||= T.let(
321
+ LanguageVersionManager.new(
322
+ python_requirement_parser: FileParser::PythonRequirementParser.new(
323
+ dependency_files: dependency_files
324
+ )
325
+ ).python_version,
326
+ T.nilable(String)
327
+ )
328
+ end
115
329
  end
116
330
  end
117
331
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-uv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.393.0
4
+ version: 0.394.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.393.0
18
+ version: 0.394.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.393.0
25
+ version: 0.394.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dependabot-python
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.393.0
32
+ version: 0.394.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.393.0
39
+ version: 0.394.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -183,14 +183,14 @@ dependencies:
183
183
  requirements:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
- version: '0.22'
186
+ version: '1.1'
187
187
  type: :development
188
188
  prerelease: false
189
189
  version_requirements: !ruby/object:Gem::Requirement
190
190
  requirements:
191
191
  - - "~>"
192
192
  - !ruby/object:Gem::Version
193
- version: '0.22'
193
+ version: '1.1'
194
194
  - !ruby/object:Gem::Dependency
195
195
  name: turbo_tests
196
196
  requirement: !ruby/object:Gem::Requirement
@@ -301,7 +301,7 @@ licenses:
301
301
  - MIT
302
302
  metadata:
303
303
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
304
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.393.0
304
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.394.0
305
305
  rdoc_options: []
306
306
  require_paths:
307
307
  - lib