dependabot-uv 0.393.0 → 0.395.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: c5e87aed24a3d0d1e38f69d064fa0d05ea3aa5db358ffe8b8aca51b0ba814f41
4
+ data.tar.gz: a79f5e14f753586b9a66310917f590bf7ac1065318cbd9f18cecf43c54a4ae24
5
5
  SHA512:
6
- metadata.gz: 3117ccb8929d12b1caff2b31607e8b459fbb61008495f3f9c5b195e7d4b888cabd0d2b47de2535d130660db0b4fe51fc2aede86b856bcd237d5f3e2eb20da37b
7
- data.tar.gz: 162286da713782ed33c25c4fcea096c4365edcf219ad4da6bb93812fd64792a569e92e86b3c20d4f8814f10f141b1bf010ba0c3cf44a3c6e553c814e1006aad2
6
+ metadata.gz: e5c993116408e7948b8d4622ad7d76e1a9a4a9fde594072883d0dd1d8e8275531a5f4d2be045fb599b61f26577d72bee2e1111a226fccb39f73058d826f66e1f
7
+ data.tar.gz: 3fb63ce5867dc87ee0ee729cb97d67c88069517f31b046d46ca9db30e27138d617af03499392543360bab156018b2ef67550d3b49c7f041468611acf50ad0097
@@ -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.7
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(
@@ -18,7 +18,7 @@ require "dependabot/uv/authed_url_builder"
18
18
  module Dependabot
19
19
  module Uv
20
20
  class FileUpdater
21
- # rubocop:disable Metrics/ClassLength
21
+ # rubocop:disable-next Metrics/ClassLength
22
22
  class CompileFileUpdater
23
23
  extend T::Sig
24
24
 
@@ -434,12 +434,17 @@ module Dependabot
434
434
  args << index_url if index_url
435
435
 
436
436
  begin
437
+ helper_result = SharedHelpers.run_helper_subprocess(
438
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
439
+ function: "get_dependency_hash",
440
+ args: args
441
+ )
442
+
443
+ # Skip this index if helper returned nil (can happen with unavailable registries)
444
+ next if helper_result.nil?
445
+
437
446
  native_helper_hashes = T.cast(
438
- SharedHelpers.run_helper_subprocess(
439
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
440
- function: "get_dependency_hash",
441
- args: args
442
- ),
447
+ helper_result,
443
448
  T::Array[T::Hash[String, String]]
444
449
  ).map { |h| "--hash=#{algorithm}:#{h['hash']}" }
445
450
 
@@ -451,6 +456,8 @@ module Dependabot
451
456
  end
452
457
  end
453
458
 
459
+ raise DependencyFileNotResolvable, "Unable to find hashes for package #{name}" if hashes.empty?
460
+
454
461
  hashes
455
462
  end
456
463
 
@@ -665,7 +672,6 @@ module Dependabot
665
672
  dependency_files.select { |f| f.name.end_with?(".txt") }
666
673
  end
667
674
  end
668
- # rubocop:enable Metrics/ClassLength
669
675
  end
670
676
  end
671
677
  end
@@ -18,7 +18,7 @@ require "dependabot/uv/requirement_suffix_helper"
18
18
  module Dependabot
19
19
  module Uv
20
20
  class FileUpdater
21
- # rubocop:disable Metrics/ClassLength
21
+ # rubocop:disable-next Metrics/ClassLength
22
22
  class LockFileUpdater
23
23
  extend T::Sig
24
24
 
@@ -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}"
@@ -777,7 +798,6 @@ module Dependabot
777
798
  )
778
799
  end
779
800
  end
780
- # rubocop:enable Metrics/ClassLength
781
801
  end
782
802
  end
783
803
  end
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -13,6 +13,8 @@ module Dependabot
13
13
  class VersionConfigParser
14
14
  extend T::Sig
15
15
 
16
+ ObjectHash = T.type_alias { T::Hash[String, Object] }
17
+
16
18
  class VersionConfig < T::Struct
17
19
  extend T::Sig
18
20
 
@@ -32,7 +34,7 @@ module Dependabot
32
34
  @pyproject_content = pyproject_content
33
35
  @base_path = base_path
34
36
  @repo_root = repo_root
35
- @parsed_pyproject = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
37
+ @parsed_pyproject = T.let(nil, T.nilable(ObjectHash))
36
38
  end
37
39
 
38
40
  sig { returns(VersionConfig) }
@@ -56,15 +58,46 @@ module Dependabot
56
58
  sig { returns(String) }
57
59
  attr_reader :repo_root
58
60
 
59
- sig { returns(T::Hash[String, T.untyped]) }
61
+ sig { returns(ObjectHash) }
60
62
  def parsed_pyproject
61
63
  return @parsed_pyproject unless @parsed_pyproject.nil?
62
64
 
63
- @parsed_pyproject = TomlRB.parse(pyproject_content)
65
+ @parsed_pyproject = object_hash(T.cast(TomlRB.parse(pyproject_content), Object))
64
66
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
65
67
  @parsed_pyproject = {}
66
68
  end
67
69
 
70
+ sig { params(value: Object).returns(ObjectHash) }
71
+ def object_hash(value)
72
+ raise TypeError, "Version configuration must be a table" unless value.is_a?(Hash)
73
+
74
+ result = T.let({}, ObjectHash)
75
+ value.each do |raw_key, raw_value|
76
+ key = T.cast(raw_key, Object)
77
+ raise TypeError, "Version configuration keys must be strings" unless key.is_a?(String)
78
+
79
+ result[key] = T.cast(raw_value, Object)
80
+ end
81
+ result
82
+ end
83
+
84
+ sig { params(path: T::Array[String]).returns(Object) }
85
+ def config_value(path)
86
+ value = T.let(parsed_pyproject, Object)
87
+ path.each do |key|
88
+ return nil if value.nil?
89
+
90
+ value = object_hash(value)[key]
91
+ end
92
+ value
93
+ end
94
+
95
+ sig { params(path: T::Array[String]).returns(T.nilable(ObjectHash)) }
96
+ def config_table(path)
97
+ value = config_value(path)
98
+ object_hash(value) if value.is_a?(Hash)
99
+ end
100
+
68
101
  sig { returns(T::Array[String]) }
69
102
  def collect_write_paths
70
103
  paths = []
@@ -82,8 +115,8 @@ module Dependabot
82
115
 
83
116
  sig { returns(T::Array[String]) }
84
117
  def setuptools_scm_write_paths
85
- scm_config = parsed_pyproject.dig("tool", "setuptools_scm")
86
- return [] unless scm_config.is_a?(Hash)
118
+ scm_config = config_table(%w(tool setuptools_scm))
119
+ return [] unless scm_config
87
120
 
88
121
  paths = []
89
122
 
@@ -98,8 +131,8 @@ module Dependabot
98
131
 
99
132
  sig { returns(T::Array[String]) }
100
133
  def hatch_vcs_build_hook_write_paths
101
- vcs_hook = parsed_pyproject.dig("tool", "hatch", "build", "hooks", "vcs")
102
- return [] unless vcs_hook.is_a?(Hash)
134
+ vcs_hook = config_table(%w(tool hatch build hooks vcs))
135
+ return [] unless vcs_hook
103
136
 
104
137
  paths = []
105
138
 
@@ -111,8 +144,8 @@ module Dependabot
111
144
 
112
145
  sig { returns(T::Array[String]) }
113
146
  def hatch_version_source_paths
114
- hatch_version = parsed_pyproject.dig("tool", "hatch", "version")
115
- return [] unless hatch_version.is_a?(Hash)
147
+ hatch_version = config_table(%w(tool hatch version))
148
+ return [] unless hatch_version
116
149
 
117
150
  paths = []
118
151
 
@@ -124,14 +157,14 @@ module Dependabot
124
157
 
125
158
  sig { returns(T.nilable(String)) }
126
159
  def extract_fallback_version
127
- scm_config = parsed_pyproject.dig("tool", "setuptools_scm")
128
- if scm_config.is_a?(Hash)
160
+ scm_config = config_table(%w(tool setuptools_scm))
161
+ if scm_config
129
162
  fallback = scm_config["fallback_version"]
130
163
  return fallback if fallback.is_a?(String)
131
164
  end
132
165
 
133
- raw_options = parsed_pyproject.dig("tool", "hatch", "version", "raw-options")
134
- if raw_options.is_a?(Hash)
166
+ raw_options = config_table(%w(tool hatch version raw-options))
167
+ if raw_options
135
168
  fallback = raw_options["fallback_version"]
136
169
  return fallback if fallback.is_a?(String)
137
170
  end
@@ -141,7 +174,7 @@ module Dependabot
141
174
 
142
175
  sig { returns(T.nilable(String)) }
143
176
  def extract_package_name
144
- name = parsed_pyproject.dig("project", "name")
177
+ name = config_value(%w(project name))
145
178
  return name if name.is_a?(String)
146
179
 
147
180
  nil
@@ -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
@@ -24,7 +24,7 @@ module Dependabot
24
24
  # This class does version resolution for pip-compile. Its approach is:
25
25
  # - Unlock the dependency we're checking in the requirements.in file
26
26
  # - Run `pip-compile` and see what the result is
27
- # rubocop:disable Metrics/ClassLength
27
+ # rubocop:disable-next Metrics/ClassLength
28
28
  class PipCompileVersionResolver
29
29
  extend T::Sig
30
30
 
@@ -595,7 +595,6 @@ module Dependabot
595
595
  dependency_files.select { |f| f.name.end_with?("setup.cfg") }
596
596
  end
597
597
  end
598
- # rubocop:enable Metrics/ClassLength
599
598
  end
600
599
 
601
600
  class PipCompileErrorHandler
@@ -228,11 +228,11 @@ module Dependabot
228
228
  requirement_files.any? { |f| f.end_with?("requirements.txt") }
229
229
  end
230
230
 
231
- sig { override.returns(T.nilable(T::Hash[String, T.untyped])) }
231
+ sig { override.returns(T.nilable(PyprojectDocument::ProjectMetadata)) }
232
232
  def library_details
233
233
  @library_details ||= T.let(
234
- standard_details || build_system_details,
235
- T.nilable(T::Hash[String, T.untyped])
234
+ pyproject_document.project_metadata || pyproject_document.build_system_metadata,
235
+ T.nilable(PyprojectDocument::ProjectMetadata)
236
236
  )
237
237
  end
238
238
 
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.395.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.395.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.395.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.395.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.395.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -183,28 +183,28 @@ 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
- name: turbo_tests
195
+ name: turbo_tests2
196
196
  requirement: !ruby/object:Gem::Requirement
197
197
  requirements:
198
198
  - - "~>"
199
199
  - !ruby/object:Gem::Version
200
- version: 2.2.5
200
+ version: 3.2.7
201
201
  type: :development
202
202
  prerelease: false
203
203
  version_requirements: !ruby/object:Gem::Requirement
204
204
  requirements:
205
205
  - - "~>"
206
206
  - !ruby/object:Gem::Version
207
- version: 2.2.5
207
+ version: 3.2.7
208
208
  - !ruby/object:Gem::Dependency
209
209
  name: vcr
210
210
  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.395.0
305
305
  rdoc_options: []
306
306
  require_paths:
307
307
  - lib