dependabot-python 0.392.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.
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -25,7 +25,8 @@ module Dependabot
25
25
  sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
26
26
  def initialize(dependency_files:)
27
27
  @dependency_files = dependency_files
28
- @dynamic_fields = T.let(nil, T.nilable(T::Array[String]))
28
+ @pyproject_document = T.let(nil, T.nilable(PyprojectDocument))
29
+ @poetry_lock_document = T.let(nil, T.nilable(PoetryLock))
29
30
  end
30
31
 
31
32
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
@@ -63,16 +64,11 @@ module Dependabot
63
64
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
64
65
 
65
66
  POETRY_DEPENDENCY_TYPES.each do |type|
66
- deps_hash = T.must(poetry_root)[type] || {}
67
- dependencies += parse_poetry_dependency_group(type, deps_hash)
67
+ dependencies += parse_poetry_dependency_group(type, pyproject_document.poetry_dependencies(type))
68
68
  end
69
69
 
70
- groups = T.must(poetry_root)["group"] || {}
71
- groups.each do |group, group_spec|
72
- deps = group_spec["dependencies"]
73
- next unless deps
74
-
75
- dependencies += parse_poetry_dependency_group(group, deps)
70
+ pyproject_document.poetry_groups.each do |group, dependencies_by_name|
71
+ dependencies += parse_poetry_dependency_group(group, dependencies_by_name)
76
72
  end
77
73
  dependencies
78
74
  end
@@ -92,17 +88,17 @@ module Dependabot
92
88
 
93
89
  dependencies <<
94
90
  Dependency.new(
95
- name: normalise(dep["name"]),
96
- version: dep["version"]&.include?("*") ? nil : dep["version"],
91
+ name: normalise(dep.name),
92
+ version: dep.version&.include?("*") ? nil : dep.version,
97
93
  requirements: [{
98
- requirement: dep["requirement"],
99
- file: Pathname.new(dep["file"]).cleanpath.to_path,
94
+ requirement: dep.requirement,
95
+ file: Pathname.new(dep.file).cleanpath.to_path,
100
96
  source: nil,
101
- groups: [dep["requirement_type"]].compact
97
+ groups: [dep.requirement_type].compact
102
98
  }],
103
99
  package_manager: "pip",
104
- metadata: extras_metadata(dep["extras"]).merge(
105
- source_requirement: dep["source_requirement"]
100
+ metadata: extras_metadata(dep.extras).merge(
101
+ source_requirement: dep.source_requirement
106
102
  ).compact
107
103
  )
108
104
  end
@@ -113,17 +109,16 @@ module Dependabot
113
109
  sig do
114
110
  params(
115
111
  type: String,
116
- deps_hash: T.nilable(T::Hash[String, T.untyped])
112
+ deps_hash: PyprojectDocument::PoetryDependencyMap
117
113
  ).returns(Dependabot::FileParsers::Base::DependencySet)
118
114
  end
119
115
  def parse_poetry_dependency_group(type, deps_hash)
120
116
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
121
- return dependencies if deps_hash.nil?
122
117
 
123
- deps_hash.each do |name, req|
118
+ deps_hash.each do |name, requirements_data|
124
119
  next if normalise(name) == "python"
125
120
 
126
- requirements = parse_requirements_from(req, type)
121
+ requirements = parse_requirements_from(requirements_data, type)
127
122
  next if requirements.empty?
128
123
 
129
124
  dependencies << Dependency.new(
@@ -151,102 +146,108 @@ module Dependabot
151
146
  { extras: extras.join(",") }
152
147
  end
153
148
 
154
- # @param req can be an Array, Hash or String that represents the constraints for a dependency
155
- sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
156
- # rubocop:disable Metrics/PerceivedComplexity
157
- def parse_requirements_from(req, type)
158
- [req].flatten.compact.filter_map do |requirement|
159
- # Skip unsupported dependency types (path, url), but allow git
160
- next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
161
- # Skip git dependencies without tags (e.g., with branch, rev)
162
- next if requirement.is_a?(Hash) && requirement["git"] && !requirement["tag"]
163
-
164
- # Handle git dependencies with tags
165
- if requirement.is_a?(Hash) && requirement["git"] && requirement["tag"]
166
- {
167
- requirement: nil,
168
- file: T.must(pyproject).name,
169
- source: {
170
- type: "git",
171
- url: requirement["git"],
172
- ref: requirement["tag"],
173
- branch: nil
174
- },
175
- groups: [type]
176
- }
177
- elsif requirement.is_a?(String)
178
- check_requirements(requirement)
179
- {
180
- requirement: requirement,
181
- file: T.must(pyproject).name,
182
- source: nil,
183
- groups: [type]
184
- }
185
- else
186
- check_requirements(requirement)
187
- # String sources are registry name references (e.g., "custom") that reference
188
- # [[tool.poetry.source]] definitions. Resolve them to proper hashes.
189
- source_value = requirement.fetch("source", nil)
190
- source = resolve_source(source_value)
191
- {
192
- requirement: requirement["version"],
193
- file: T.must(pyproject).name,
194
- source: source,
195
- groups: [type]
196
- }
197
- end
149
+ sig do
150
+ params(
151
+ requirements_data: T::Array[PyprojectDocument::PoetryRequirement],
152
+ type: String
153
+ ).returns(T::Array[Dependabot::Dependency::RequirementInput])
154
+ end
155
+ def parse_requirements_from(requirements_data, type)
156
+ requirements_data.filter_map { |requirement| poetry_requirement(requirement, type) }
157
+ end
158
+
159
+ sig do
160
+ params(
161
+ requirement: PyprojectDocument::PoetryRequirement,
162
+ type: String
163
+ ).returns(T.nilable(Dependabot::Dependency::RequirementInput))
164
+ end
165
+ def poetry_requirement(requirement, type)
166
+ if requirement.is_a?(String)
167
+ check_requirements(requirement)
168
+ return {
169
+ requirement: requirement,
170
+ file: T.must(pyproject).name,
171
+ source: nil,
172
+ groups: [type]
173
+ }
198
174
  end
175
+
176
+ return if UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
177
+
178
+ git = requirement_string(requirement, "git")
179
+ tag = requirement_string(requirement, "tag")
180
+ return if git && !tag
181
+
182
+ return git_requirement(git, tag, type) if git && tag
183
+
184
+ check_requirements(requirement)
185
+ {
186
+ requirement: requirement_string(requirement, "version"),
187
+ file: T.must(pyproject).name,
188
+ source: resolve_source(requirement["source"]),
189
+ groups: [type]
190
+ }
191
+ end
192
+
193
+ sig { params(git: String, tag: String, type: String).returns(Dependabot::Dependency::RequirementInput) }
194
+ def git_requirement(git, tag, type)
195
+ {
196
+ requirement: nil,
197
+ file: T.must(pyproject).name,
198
+ source: {
199
+ type: "git",
200
+ url: git,
201
+ ref: tag,
202
+ branch: nil
203
+ },
204
+ groups: [type]
205
+ }
199
206
  end
200
- # rubocop:enable Metrics/PerceivedComplexity
201
207
 
202
- sig { returns(T.nilable(T::Boolean)) }
208
+ sig { returns(T::Boolean) }
203
209
  def using_poetry?
204
- !poetry_root.nil?
210
+ pyproject_document.poetry?
205
211
  end
206
212
 
207
213
  sig { returns(T::Boolean) }
208
214
  def using_pep621?
209
- !parsed_pyproject.dig("project", "dependencies").nil? ||
210
- !parsed_pyproject.dig("project", "optional-dependencies").nil? ||
211
- !parsed_pyproject.dig("build-system", "requires").nil?
215
+ pyproject_document.pep621?
212
216
  end
213
217
 
214
218
  sig { returns(T::Boolean) }
215
219
  def using_pep735?
216
- parsed_pyproject.key?("dependency-groups")
217
- end
218
-
219
- sig { returns(T.nilable(T::Hash[String, T.untyped])) }
220
- def poetry_root
221
- parsed_pyproject.dig("tool", "poetry")
220
+ pyproject_document.pep735?
222
221
  end
223
222
 
224
- sig { returns(T.untyped) }
223
+ sig { returns(T::Boolean) }
225
224
  def using_pdm?
226
- using_pep621? && pdm_lock
225
+ using_pep621? && !pdm_lock.nil?
227
226
  end
228
227
 
229
228
  sig { returns(T::Array[String]) }
230
229
  def dynamic_fields
231
- @dynamic_fields ||= parsed_pyproject.dig("project", "dynamic") || []
230
+ pyproject_document.dynamic_fields
232
231
  end
233
232
 
234
- sig { params(dep: T::Hash[String, T.untyped]).returns(T::Boolean) }
233
+ sig { params(dep: PepDependency).returns(T::Boolean) }
235
234
  def skip_pep621_dep?(dep)
236
235
  # If a requirement has a `<` or `<=` marker then updating it is
237
236
  # probably blocked. Ignore it.
238
- return true if dep["markers"]&.include?("<")
237
+ return true if dep.markers&.include?("<")
239
238
 
240
239
  # If no requirement, don't add it
241
- return true if dep["requirement"].empty?
240
+ requirement = dep.requirement
241
+ raise TypeError, "Python PEP dependency requirement must be a string" unless requirement
242
+ return true if requirement.empty?
242
243
 
243
244
  # Skip build-system.requires dependencies when using Poetry
244
245
  # Poetry manages its own build system dependencies
245
- return true if using_poetry? && dep["requirement_type"] == "build-system.requires"
246
+ return true if using_poetry? && dep.requirement_type == "build-system.requires"
246
247
 
247
248
  # When dependencies or optional-dependencies are listed in project.dynamic,
248
249
  # they are managed by the build backend (e.g. Poetry) — skip the PEP 621 path
249
- dynamic_pep621_dep?(dep["requirement_type"])
250
+ dynamic_pep621_dep?(dep.requirement_type)
250
251
  end
251
252
 
252
253
  sig { params(requirement_type: T.nilable(String)).returns(T::Boolean) }
@@ -256,7 +257,7 @@ module Dependabot
256
257
 
257
258
  if requirement_type == "dependencies"
258
259
  dynamic_fields.include?("dependencies")
259
- elsif parsed_pyproject.dig("project", "optional-dependencies")&.key?(requirement_type)
260
+ elsif pyproject_document.optional_dependency_group?(requirement_type)
260
261
  dynamic_fields.include?("optional-dependencies")
261
262
  else
262
263
  false
@@ -271,15 +272,15 @@ module Dependabot
271
272
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
272
273
 
273
274
  source_types = %w(directory git url)
274
- parsed_lockfile.fetch("package", []).each do |details|
275
- next if source_types.include?(details.dig("source", "type"))
275
+ poetry_lock_document.packages.each do |package|
276
+ next if source_types.include?(package.source_type)
276
277
 
277
- name = normalise(details.fetch("name"))
278
+ name = normalise(package.name)
278
279
 
279
280
  dependencies <<
280
281
  Dependency.new(
281
282
  name: name,
282
- version: details.fetch("version"),
283
+ version: package.version,
283
284
  requirements: [],
284
285
  package_manager: "pip",
285
286
  subdependency_metadata: [{
@@ -302,8 +303,10 @@ module Dependabot
302
303
  sig { returns(T::Array[T.nilable(String)]) }
303
304
  def parse_production_dependency_names
304
305
  SharedHelpers.in_a_temporary_directory do
305
- File.write(T.must(pyproject).name, T.must(pyproject).content)
306
- File.write(lockfile.name, lockfile.content)
306
+ pyproject_file = T.must(pyproject)
307
+ lockfile_file = T.must(lockfile)
308
+ File.write(pyproject_file.name, pyproject_file.content)
309
+ File.write(lockfile_file.name, lockfile_file.content)
307
310
 
308
311
  begin
309
312
  output = SharedHelpers.run_shell_command("pyenv exec poetry show --only main")
@@ -322,18 +325,19 @@ module Dependabot
322
325
  end
323
326
  end
324
327
 
325
- sig { params(dep_name: String).returns(T.untyped) }
328
+ sig { params(dep_name: String).returns(T.nilable(String)) }
326
329
  def version_from_lockfile(dep_name)
327
- return unless parsed_lockfile
330
+ return unless poetry_lock
328
331
 
329
- parsed_lockfile.fetch("package", [])
330
- .find { |p| normalise(p.fetch("name")) == normalise(dep_name) }
331
- &.fetch("version", nil)
332
+ poetry_lock_document.version_for(dep_name) { |name| normalise(name) }
332
333
  end
333
334
 
334
- sig { params(req: T.untyped).returns(T::Array[Dependabot::Python::Requirement]) }
335
+ sig do
336
+ params(req: PyprojectDocument::PoetryRequirement)
337
+ .returns(T::Array[Dependabot::Python::Requirement])
338
+ end
335
339
  def check_requirements(req)
336
- requirement = req.is_a?(String) ? req : req["version"]
340
+ requirement = req.is_a?(String) ? req : requirement_string(req, "version")
337
341
  Python::Requirement.requirements_array(requirement)
338
342
  rescue Gem::Requirement::BadRequirementError => e
339
343
  raise Dependabot::DependencyFileNotEvaluatable, e.message
@@ -344,21 +348,23 @@ module Dependabot
344
348
  NameNormaliser.normalise(name)
345
349
  end
346
350
 
347
- sig { params(source_value: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
351
+ sig do
352
+ params(source_value: T.nilable(Object))
353
+ .returns(T.nilable(Dependabot::DependencyRequirement::Source))
354
+ end
348
355
  def resolve_source(source_value)
349
356
  # Return nil if no source specified
350
357
  return nil if source_value.nil?
351
358
 
352
359
  # If already a hash, return as-is (handles git sources)
353
- return source_value if source_value.is_a?(Hash)
360
+ return PyprojectValueParser.object_hash(source_value, "Poetry dependency source") if source_value.is_a?(Hash)
354
361
 
355
362
  # String sources are references to [[tool.poetry.source]] definitions
356
363
  # Look up the source definition and create a hash
357
364
  return nil unless source_value.is_a?(String)
358
365
 
359
366
  source_name = source_value
360
- poetry_sources = parsed_pyproject.dig("tool", "poetry", "source") || []
361
- source_def = poetry_sources.find { |s| s["name"] == source_name }
367
+ source_def = pyproject_document.poetry_source(source_name)
362
368
 
363
369
  # If source definition not found, return nil
364
370
  return nil unless source_def
@@ -367,26 +373,19 @@ module Dependabot
367
373
  # Use "registry" as the type since these are package index sources
368
374
  {
369
375
  type: "registry",
370
- url: source_def["url"],
376
+ url: source_def.url,
371
377
  name: source_name
372
378
  }
373
379
  end
374
380
 
375
- sig { returns(T::Hash[String, T.untyped]) }
376
- def parsed_pyproject
377
- @parsed_pyproject ||= T.let(TomlRB.parse(T.must(pyproject).content), T.nilable(T::Hash[String, T.untyped]))
378
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
379
- raise Dependabot::DependencyFileNotParseable, T.must(pyproject).path
381
+ sig { returns(PyprojectDocument) }
382
+ def pyproject_document
383
+ @pyproject_document ||= PyprojectDocument.from_file(T.must(pyproject))
380
384
  end
381
385
 
382
- sig { returns(T::Hash[String, T.untyped]) }
383
- def parsed_poetry_lock
384
- @parsed_poetry_lock ||= T.let(
385
- TomlRB.parse(T.must(poetry_lock).content),
386
- T.nilable(T::Hash[String, T.untyped])
387
- )
388
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
389
- raise Dependabot::DependencyFileNotParseable, T.must(poetry_lock).path
386
+ sig { returns(PoetryLock) }
387
+ def poetry_lock_document
388
+ @poetry_lock_document ||= PoetryLock.from_file(T.must(poetry_lock))
390
389
  end
391
390
 
392
391
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -397,20 +396,22 @@ module Dependabot
397
396
  )
398
397
  end
399
398
 
400
- sig { returns(T.untyped) }
399
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
401
400
  def lockfile
402
401
  poetry_lock
403
402
  end
404
403
 
405
- sig { returns(T.untyped) }
404
+ sig { returns(T::Array[PepDependency]) }
406
405
  def parse_pep621_pep735_dependencies
407
406
  SharedHelpers.in_a_temporary_directory do
408
407
  write_temporary_pyproject
409
408
 
410
- SharedHelpers.run_helper_subprocess(
411
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
412
- function: "parse_pep621_pep735_dependencies",
413
- args: [T.must(pyproject).name]
409
+ PepDependency.from_helper_result(
410
+ SharedHelpers.run_helper_subprocess(
411
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
412
+ function: "parse_pep621_pep735_dependencies",
413
+ args: [T.must(pyproject).name]
414
+ )
414
415
  )
415
416
  end
416
417
  end
@@ -422,11 +423,6 @@ module Dependabot
422
423
  File.write(path, T.must(pyproject).content)
423
424
  end
424
425
 
425
- sig { returns(T.untyped) }
426
- def parsed_lockfile
427
- parsed_poetry_lock if poetry_lock
428
- end
429
-
430
426
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
431
427
  def poetry_lock
432
428
  @poetry_lock ||= T.let(
@@ -435,6 +431,16 @@ module Dependabot
435
431
  )
436
432
  end
437
433
 
434
+ sig do
435
+ params(
436
+ requirement: PyprojectValueParser::ObjectHash,
437
+ key: String
438
+ ).returns(T.nilable(String))
439
+ end
440
+ def requirement_string(requirement, key)
441
+ PyprojectValueParser.optional_string(requirement[key], "Poetry dependency #{key}")
442
+ end
443
+
438
444
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
439
445
  def pdm_lock
440
446
  @pdm_lock ||= T.let(
@@ -68,10 +68,19 @@ module Dependabot
68
68
  SharedHelpers.in_a_temporary_directory do
69
69
  write_temporary_dependency_files
70
70
 
71
- requirements = SharedHelpers.run_helper_subprocess(
72
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
73
- function: "parse_setup",
74
- args: [Dir.pwd]
71
+ requirements = T.cast(
72
+ SharedHelpers.run_helper_subprocess(
73
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
74
+ function: "parse_setup",
75
+ args: [Dir.pwd]
76
+ ),
77
+ T.nilable(
78
+ T.any(
79
+ T::Hash[String, T.untyped],
80
+ String,
81
+ T::Array[T::Hash[String, T.untyped]]
82
+ )
83
+ )
75
84
  )
76
85
 
77
86
  check_requirements(requirements)
@@ -90,10 +99,19 @@ module Dependabot
90
99
  SharedHelpers.in_a_temporary_directory do
91
100
  write_sanitized_setup_file
92
101
 
93
- requirements = SharedHelpers.run_helper_subprocess(
94
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
95
- function: "parse_setup",
96
- args: [Dir.pwd]
102
+ requirements = T.cast(
103
+ SharedHelpers.run_helper_subprocess(
104
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
105
+ function: "parse_setup",
106
+ args: [Dir.pwd]
107
+ ),
108
+ T.nilable(
109
+ T.any(
110
+ T::Hash[String, T.untyped],
111
+ String,
112
+ T::Array[T::Hash[String, T.untyped]]
113
+ )
114
+ )
97
115
  )
98
116
 
99
117
  check_requirements(requirements)
@@ -21,6 +21,9 @@ module Dependabot
21
21
  class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/ClassLength
22
22
  extend T::Sig
23
23
 
24
+ require_relative "file_parser/pyproject_document"
25
+ require_relative "file_parser/poetry_lock"
26
+ require_relative "file_parser/pep_dependency"
24
27
  require_relative "file_parser/pipfile_files_parser"
25
28
  require_relative "file_parser/pyproject_files_parser"
26
29
  require_relative "file_parser/setup_file_parser"
@@ -345,10 +345,19 @@ module Dependabot
345
345
  def pipfile_hash_for(pipfile_content)
346
346
  SharedHelpers.in_a_temporary_directory do |dir|
347
347
  File.write(File.join(dir, "Pipfile"), pipfile_content)
348
- SharedHelpers.run_helper_subprocess(
349
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
350
- function: "get_pipfile_hash",
351
- args: [T.cast(dir, Pathname).to_s]
348
+ T.cast(
349
+ SharedHelpers.run_helper_subprocess(
350
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
351
+ function: "get_pipfile_hash",
352
+ args: [T.cast(dir, Pathname).to_s]
353
+ ),
354
+ T.nilable(
355
+ T.any(
356
+ T::Hash[String, T.untyped],
357
+ String,
358
+ T::Array[T::Hash[String, T.untyped]]
359
+ )
360
+ )
352
361
  )
353
362
  end
354
363
  end
@@ -18,6 +18,16 @@ require "dependabot/package/release_cooldown_options"
18
18
 
19
19
  module Dependabot
20
20
  module Python
21
+ PoetryPyprojectHashResult = T.type_alias do
22
+ T.nilable(
23
+ T.any(
24
+ T::Hash[String, T.untyped],
25
+ String,
26
+ T::Array[T::Hash[String, T.untyped]]
27
+ )
28
+ )
29
+ end
30
+
21
31
  class FileUpdater
22
32
  class PoetryFileUpdater
23
33
  require_relative "pyproject_preparer"
@@ -367,17 +377,18 @@ module Dependabot
367
377
  .add_auth_env_vars(credentials)
368
378
  end
369
379
 
370
- sig { params(pyproject_content: String).returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
380
+ sig { params(pyproject_content: String).returns(PoetryPyprojectHashResult) }
371
381
  def pyproject_hash_for(pyproject_content)
372
382
  SharedHelpers.in_a_temporary_directory do |dir|
373
383
  SharedHelpers.with_git_configured(credentials: credentials) do
374
384
  write_temporary_dependency_files(pyproject_content)
375
385
 
376
- SharedHelpers.run_helper_subprocess(
386
+ result = SharedHelpers.run_helper_subprocess(
377
387
  command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
378
388
  function: "get_pyproject_hash",
379
389
  args: [T.cast(dir, Pathname).to_s]
380
390
  )
391
+ T.cast(result, PoetryPyprojectHashResult)
381
392
  end
382
393
  end
383
394
  end