dependabot-python 0.301.1 → 0.302.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: 28b4e62ed88202c96564365886f0ffe07f5554d10f77b965030e2285e7e3f63e
4
- data.tar.gz: fed3c7384673233019c27016bac8b786b3ab54b6f0ef8abf7e9a1dee63ba7992
3
+ metadata.gz: be5bb8881f630d585a20e8d38c1458020f9a77e369fc44506a996b87da6b47b1
4
+ data.tar.gz: 327a35b61a9faa2adf137b79d1f49e1b89a8cc3c25e3cc484340d153028cc6dd
5
5
  SHA512:
6
- metadata.gz: fdb559ea1b46ed73f5951c537b809efd9f27c5b740b15186641cc579a906a8e5f80a8779a00d09991b0e12f6d396ab37697ce0c046d0b7b2d530260ba1b0da37
7
- data.tar.gz: 402608f1f8a90234938f732a821fff9729a20c6d4ce3b26aa4ee44fca2794c4c028eb4a8036518a7d0387430dd473cf52fe9f370b132492f3ffef8e2776a3a1c
6
+ metadata.gz: 73a38b119440ef9743235bd9a1b5e68092d822dcb70f0550ac353a9bedc4f64363c40cbbcdadbee50347c199ab2f2b7a7653675bb38c5660f69d68e17f514a6b
7
+ data.tar.gz: f852a27fcc45ec3b4b79bf4c33d0daf0c7e921c258860d55caf79a52102a340db0ac48ba92dba763d7353930922b6d8d0d56aec6b84cceb54aca0857ef0ad540
@@ -53,7 +53,7 @@ module Dependabot
53
53
  # the user-specified range of versions, not the version Dependabot chose to run.
54
54
  python_requirement_parser = FileParser::PythonRequirementParser.new(dependency_files: files)
55
55
  language_version_manager = LanguageVersionManager.new(python_requirement_parser: python_requirement_parser)
56
- Dependabot.logger.info("Dependabot is using Python version '#{language_version_manager.python_major_minor}'.")
56
+ Dependabot.logger.info("Dependabot is using Python version '#{language_version_manager.python_version}'.")
57
57
  {
58
58
  languages: {
59
59
  python: {
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "open3"
@@ -18,41 +18,70 @@ module Dependabot
18
18
  class FileUpdater
19
19
  # rubocop:disable Metrics/ClassLength
20
20
  class PipCompileFileUpdater
21
+ extend T::Sig
21
22
  require_relative "requirement_replacer"
22
23
  require_relative "requirement_file_updater"
23
24
  require_relative "setup_file_sanitizer"
24
25
 
25
- UNSAFE_PACKAGES = %w(setuptools distribute pip).freeze
26
- INCOMPATIBLE_VERSIONS_REGEX = /There are incompatible versions in the resolved dependencies:.*\z/m
27
- WARNINGS = /\s*# WARNING:.*\Z/m
28
- UNSAFE_NOTE = /\s*# The following packages are considered to be unsafe.*\Z/m
29
- RESOLVER_REGEX = /(?<=--resolver=)(\w+)/
30
- NATIVE_COMPILATION_ERROR =
31
- "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1"
32
-
26
+ UNSAFE_PACKAGES = T.let(%w(setuptools distribute pip).freeze, T::Array[String])
27
+ INCOMPATIBLE_VERSIONS_REGEX = T.let(/There are incompatible versions in the resolved dependencies:.*\z/m,
28
+ Regexp)
29
+ WARNINGS = T.let(/\s*# WARNING:.*\Z/m, Regexp)
30
+ UNSAFE_NOTE = T.let(/\s*# The following packages are considered to be unsafe.*\Z/m, Regexp)
31
+ RESOLVER_REGEX = T.let(/(?<=--resolver=)(\w+)/, Regexp)
32
+ NATIVE_COMPILATION_ERROR = T.let(
33
+ "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1",
34
+ String
35
+ )
36
+
37
+ sig { returns(T::Array[Dependabot::Dependency]) }
33
38
  attr_reader :dependencies
39
+
40
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
34
41
  attr_reader :dependency_files
42
+
43
+ sig { returns(T::Array[Dependabot::Credential]) }
35
44
  attr_reader :credentials
36
45
 
37
- def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
38
- @dependencies = dependencies
39
- @dependency_files = dependency_files
40
- @credentials = credentials
41
- @index_urls = index_urls
42
- @build_isolation = true
46
+ sig do
47
+ params(
48
+ dependencies: T::Array[Dependabot::Dependency],
49
+ dependency_files: T::Array[Dependabot::DependencyFile],
50
+ credentials: T::Array[Dependabot::Credential],
51
+ index_urls: T.nilable(T::Array[String])
52
+ ).void
43
53
  end
44
-
54
+ def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
55
+ @dependencies = T.let(dependencies, T::Array[Dependabot::Dependency])
56
+ @dependency_files = T.let(dependency_files, T::Array[Dependabot::DependencyFile])
57
+ @index_urls = T.let(index_urls, T.nilable(T::Array[String]))
58
+ @build_isolation = T.let(true, T::Boolean)
59
+ @sanitized_setup_file_content = T.let({}, T::Hash[String, String])
60
+ @requirement_map = T.let(nil, T.nilable(T::Hash[String, T::Array[String]]))
61
+ @python_requirement_parser = T.let(nil, T.nilable(FileParser::PythonRequirementParser))
62
+ @language_version_manager = T.let(nil, T.nilable(LanguageVersionManager))
63
+ @setup_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
64
+ @setup_cfg_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
65
+ @pip_compile_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
66
+ @compiled_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
67
+ @credentials = T.let(credentials, T::Array[Dependabot::Credential])
68
+ end
69
+
70
+ sig { returns(T.nilable(T::Array[Dependabot::DependencyFile])) }
45
71
  def updated_dependency_files
46
- @updated_dependency_files ||= fetch_updated_dependency_files
72
+ @updated_dependency_files = T.let(fetch_updated_dependency_files,
73
+ T.nilable(T::Array[Dependabot::DependencyFile]))
47
74
  end
48
75
 
49
76
  private
50
77
 
78
+ sig { returns(T.nilable(Dependabot::Dependency)) }
51
79
  def dependency
52
80
  # For now, we'll only ever be updating a single dependency
53
81
  dependencies.first
54
82
  end
55
83
 
84
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
56
85
  def fetch_updated_dependency_files
57
86
  updated_compiled_files = compile_new_requirement_files
58
87
  updated_manifest_files = update_manifest_files
@@ -67,6 +96,7 @@ module Dependabot
67
96
  ]
68
97
  end
69
98
 
99
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
70
100
  def compile_new_requirement_files
71
101
  SharedHelpers.in_a_temporary_directory do
72
102
  write_updated_dependency_files
@@ -93,6 +123,7 @@ module Dependabot
93
123
  end
94
124
  end
95
125
 
126
+ sig { params(filename: String).void }
96
127
  def compile_file(filename)
97
128
  # Shell out to pip-compile, generate a new set of requirements.
98
129
  # This is slow, as pip-compile needs to do installs.
@@ -101,12 +132,12 @@ module Dependabot
101
132
 
102
133
  name_part = "pyenv exec pip-compile " \
103
134
  "#{options} -P " \
104
- "#{dependency.name}"
135
+ "#{T.must(dependency).name}"
105
136
  fingerprint_name_part = "pyenv exec pip-compile " \
106
137
  "#{options_fingerprint} -P " \
107
138
  "<dependency_name>"
108
139
 
109
- version_part = "#{dependency.version} #{filename}"
140
+ version_part = "#{T.must(dependency).version} #{filename}"
110
141
  fingerprint_version_part = "<dependency_version> <filename>"
111
142
 
112
143
  # Don't escape pyenv `dep-name==version` syntax
@@ -127,10 +158,12 @@ module Dependabot
127
158
  raise
128
159
  end
129
160
 
161
+ sig { params(error: SharedHelpers::HelperSubprocessFailed).returns(T::Boolean) }
130
162
  def compilation_error?(error)
131
163
  error.message.include?(NATIVE_COMPILATION_ERROR)
132
164
  end
133
165
 
166
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
134
167
  def update_manifest_files
135
168
  dependency_files.filter_map do |file|
136
169
  next unless file.name.end_with?(".in")
@@ -144,12 +177,15 @@ module Dependabot
144
177
  end
145
178
  end
146
179
 
180
+ sig do
181
+ params(updated_files: T::Array[Dependabot::DependencyFile]).returns(T::Array[Dependabot::DependencyFile])
182
+ end
147
183
  def update_uncompiled_files(updated_files)
148
184
  updated_filenames = updated_files.map(&:name)
149
- old_reqs = dependency.previous_requirements
150
- .reject { |r| updated_filenames.include?(r[:file]) }
151
- new_reqs = dependency.requirements
152
- .reject { |r| updated_filenames.include?(r[:file]) }
185
+ old_reqs = T.must(T.must(dependency).previous_requirements)
186
+ .reject { |r| updated_filenames.include?(r[:file]) }
187
+ new_reqs = T.must(dependency).requirements
188
+ .reject { |r| updated_filenames.include?(r[:file]) }
153
189
 
154
190
  return [] if new_reqs.none?
155
191
 
@@ -162,13 +198,21 @@ module Dependabot
162
198
  args[:previous_requirements] = old_reqs
163
199
 
164
200
  RequirementFileUpdater.new(
165
- dependencies: [Dependency.new(**args)],
201
+ dependencies: [Dependency.new(**T.unsafe(args))],
166
202
  dependency_files: files,
167
203
  credentials: credentials
168
204
  ).updated_dependency_files
169
205
  end
170
206
 
171
- def run_command(cmd, env: python_env, allow_unsafe_shell_command: false, fingerprint:)
207
+ sig do
208
+ params(
209
+ cmd: String,
210
+ fingerprint: String,
211
+ env: T.nilable(T::Hash[String, String]),
212
+ allow_unsafe_shell_command: T::Boolean
213
+ ).returns(String)
214
+ end
215
+ def run_command(cmd, fingerprint:, env: python_env, allow_unsafe_shell_command: false)
172
216
  SharedHelpers.run_shell_command(
173
217
  cmd,
174
218
  env: env,
@@ -186,7 +230,8 @@ module Dependabot
186
230
  raise
187
231
  end
188
232
 
189
- def run_pip_compile_command(command, allow_unsafe_shell_command: false, fingerprint:)
233
+ sig { params(command: String, fingerprint: String, allow_unsafe_shell_command: T::Boolean).returns(String) }
234
+ def run_pip_compile_command(command, fingerprint:, allow_unsafe_shell_command: false)
190
235
  run_command(
191
236
  "pyenv local #{language_version_manager.python_major_minor}",
192
237
  fingerprint: "pyenv local <python_major_minor>"
@@ -199,12 +244,13 @@ module Dependabot
199
244
  )
200
245
  end
201
246
 
247
+ sig { returns(T::Hash[String, T.untyped]) }
202
248
  def python_env
203
249
  env = {}
204
250
 
205
251
  # Handle Apache Airflow 1.10.x installs
206
- if dependency_files.any? { |f| f.content.include?("apache-airflow") }
207
- if dependency_files.any? { |f| f.content.include?("unidecode") }
252
+ if dependency_files.any? { |f| T.must(f.content).include?("apache-airflow") }
253
+ if dependency_files.any? { |f| T.must(f.content).include?("unidecode") }
208
254
  env["AIRFLOW_GPL_UNIDECODE"] = "yes"
209
255
  else
210
256
  env["SLUGIFY_USES_TEXT_UNIDECODE"] = "yes"
@@ -214,6 +260,7 @@ module Dependabot
214
260
  env
215
261
  end
216
262
 
263
+ sig { void }
217
264
  def write_updated_dependency_files
218
265
  dependency_files.each do |file|
219
266
  path = file.name
@@ -237,8 +284,8 @@ module Dependabot
237
284
  end
238
285
  end
239
286
 
287
+ sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
240
288
  def sanitized_setup_file_content(file)
241
- @sanitized_setup_file_content ||= {}
242
289
  return @sanitized_setup_file_content[file.name] if @sanitized_setup_file_content[file.name]
243
290
 
244
291
  @sanitized_setup_file_content[file.name] =
@@ -247,56 +294,61 @@ module Dependabot
247
294
  .sanitized_content
248
295
  end
249
296
 
297
+ sig { params(file: DependencyFile).returns(T.nilable(Dependabot::DependencyFile)) }
250
298
  def setup_cfg(file)
251
299
  dependency_files.find do |f|
252
300
  f.name == file.name.sub(/\.py$/, ".cfg")
253
301
  end
254
302
  end
255
303
 
304
+ sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
256
305
  def freeze_dependency_requirement(file)
257
306
  return file.content unless file.name.end_with?(".in")
258
307
 
259
- old_req = dependency.previous_requirements
260
- .find { |r| r[:file] == file.name }
308
+ old_req = T.must(T.must(dependency).previous_requirements)
309
+ .find { |r| r[:file] == file.name }
261
310
 
262
311
  return file.content unless old_req
263
- return file.content if old_req == "==#{dependency.version}"
312
+ return file.content if old_req == "==#{T.must(dependency).version}"
264
313
 
265
314
  RequirementReplacer.new(
266
315
  content: file.content,
267
- dependency_name: dependency.name,
316
+ dependency_name: T.must(dependency).name,
268
317
  old_requirement: old_req[:requirement],
269
- new_requirement: "==#{dependency.version}",
318
+ new_requirement: "==#{T.must(dependency).version}",
270
319
  index_urls: @index_urls
271
320
  ).updated_content
272
321
  end
273
322
 
323
+ sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
274
324
  def update_dependency_requirement(file)
275
325
  return file.content unless file.name.end_with?(".in")
276
326
 
277
- old_req = dependency.previous_requirements
278
- .find { |r| r[:file] == file.name }
279
- new_req = dependency.requirements
280
- .find { |r| r[:file] == file.name }
327
+ old_req = T.must(T.must(dependency).previous_requirements)
328
+ .find { |r| r[:file] == file.name }
329
+ new_req = T.must(dependency).requirements
330
+ .find { |r| r[:file] == file.name }
281
331
  return file.content unless old_req&.fetch(:requirement)
282
332
  return file.content if old_req == new_req
283
333
 
284
334
  RequirementReplacer.new(
285
335
  content: file.content,
286
- dependency_name: dependency.name,
336
+ dependency_name: T.must(dependency).name,
287
337
  old_requirement: old_req[:requirement],
288
- new_requirement: new_req[:requirement],
338
+ new_requirement: T.must(new_req)[:requirement],
289
339
  index_urls: @index_urls
290
340
  ).updated_content
291
341
  end
292
342
 
343
+ sig { params(updated_content: String, file: Dependabot::DependencyFile).returns(String) }
293
344
  def post_process_compiled_file(updated_content, file)
294
- content = replace_header_with_original(updated_content, file.content)
295
- content = remove_new_warnings(content, file.content)
296
- content = update_hashes_if_required(content, file.content)
297
- replace_absolute_file_paths(content, file.content)
345
+ content = replace_header_with_original(updated_content, T.must(file.content))
346
+ content = remove_new_warnings(content, T.must(file.content))
347
+ content = update_hashes_if_required(content, T.must(file.content))
348
+ replace_absolute_file_paths(content, T.must(file.content))
298
349
  end
299
350
 
351
+ sig { params(updated_content: String, original_content: String).returns(String) }
300
352
  def replace_header_with_original(updated_content, original_content)
301
353
  original_header_lines =
302
354
  original_content.lines.take_while { |l| l.start_with?("#") }
@@ -307,6 +359,7 @@ module Dependabot
307
359
  [*original_header_lines, *updated_content_lines].join
308
360
  end
309
361
 
362
+ sig { params(updated_content: String, original_content: String).returns(String) }
310
363
  def replace_absolute_file_paths(updated_content, original_content)
311
364
  content = updated_content
312
365
 
@@ -328,6 +381,7 @@ module Dependabot
328
381
  content
329
382
  end
330
383
 
384
+ sig { params(updated_content: String, original_content: String).returns(String) }
331
385
  def remove_new_warnings(updated_content, original_content)
332
386
  content = updated_content
333
387
 
@@ -341,6 +395,7 @@ module Dependabot
341
395
  content
342
396
  end
343
397
 
398
+ sig { params(updated_content: String, original_content: String).returns(String) }
344
399
  def update_hashes_if_required(updated_content, original_content)
345
400
  deps_to_update =
346
401
  deps_to_augment_hashes_for(updated_content, original_content)
@@ -353,7 +408,7 @@ module Dependabot
353
408
  name: mtch.named_captures.fetch("name"),
354
409
  version: mtch.named_captures.fetch("version"),
355
410
  algorithm: mtch.named_captures.fetch("algorithm")
356
- ).sort.join(hash_separator(mtch.to_s))
411
+ ).sort.join(T.must(hash_separator(mtch.to_s)))
357
412
  )
358
413
 
359
414
  updated_content_with_hashes = updated_content_with_hashes
@@ -362,6 +417,7 @@ module Dependabot
362
417
  updated_content_with_hashes
363
418
  end
364
419
 
420
+ sig { params(updated_content: String, original_content: String).returns(T::Array[T.untyped]) }
365
421
  def deps_to_augment_hashes_for(updated_content, original_content)
366
422
  regex = /^#{RequirementParser::INSTALL_REQ_WITH_REQUIREMENT}/o
367
423
 
@@ -391,6 +447,7 @@ module Dependabot
391
447
  [*new_deps, *changed_hashes_deps]
392
448
  end
393
449
 
450
+ sig { params(name: String, version: String, algorithm: String).returns(T::Array[String]) }
394
451
  def package_hashes_for(name:, version:, algorithm:)
395
452
  index_urls = @index_urls || [nil]
396
453
  hashes = []
@@ -420,24 +477,24 @@ module Dependabot
420
477
  hashes
421
478
  end
422
479
 
480
+ sig { params(requirement_string: String).returns(T.nilable(String)) }
423
481
  def hash_separator(requirement_string)
424
482
  hash_regex = RequirementParser::HASH
425
483
  return unless requirement_string.match?(hash_regex)
426
484
 
485
+ # rubocop:disable Layout/LineLength
427
486
  current_separator =
428
- requirement_string
429
- .match(/#{hash_regex}((?<separator>\s*\\?\s*?)#{hash_regex})*/)
430
- .named_captures.fetch("separator")
487
+ T.must(requirement_string.match(/#{hash_regex}((?<separator>\s*\\?\s*?)#{hash_regex})*/)).named_captures.fetch("separator")
431
488
 
432
489
  default_separator =
433
- requirement_string
434
- .match(RequirementParser::HASH)
435
- .pre_match.match(/(?<separator>\s*\\?\s*?)\z/)
436
- .named_captures.fetch("separator")
490
+ T.must(T.must(requirement_string
491
+ .match(RequirementParser::HASH)).pre_match.match(/(?<separator>\s*\\?\s*?)\z/)).named_captures.fetch("separator")
437
492
 
493
+ # rubocop:enable Layout/LineLength
438
494
  current_separator || default_separator
439
495
  end
440
496
 
497
+ sig { params(options: String).returns(String) }
441
498
  def pip_compile_options_fingerprint(options)
442
499
  options.sub(
443
500
  /--output-file=\S+/, "--output-file=<output_file>"
@@ -448,6 +505,7 @@ module Dependabot
448
505
  )
449
506
  end
450
507
 
508
+ sig { params(filename: String).returns(String) }
451
509
  def pip_compile_options(filename)
452
510
  options = @build_isolation ? ["--build-isolation"] : ["--no-build-isolation"]
453
511
  options += pip_compile_index_options
@@ -459,30 +517,34 @@ module Dependabot
459
517
  options.join(" ")
460
518
  end
461
519
 
520
+ # rubocop:disable Metrics/AbcSize
521
+ sig { params(requirements_file: T.nilable(Dependabot::DependencyFile)).returns(T::Array[String]) }
462
522
  def pip_compile_options_from_compiled_file(requirements_file)
463
- options = ["--output-file=#{requirements_file.name}"]
523
+ options = ["--output-file=#{T.must(requirements_file).name}"]
464
524
 
465
- options << "--no-emit-index-url" unless requirements_file.content.include?("index-url http")
525
+ options << "--no-emit-index-url" unless T.must(T.must(requirements_file).content).include?("index-url http")
466
526
 
467
- options << "--generate-hashes" if requirements_file.content.include?("--hash=sha")
527
+ options << "--generate-hashes" if T.must(T.must(requirements_file).content).include?("--hash=sha")
468
528
 
469
- options << "--allow-unsafe" if includes_unsafe_packages?(requirements_file.content)
529
+ options << "--allow-unsafe" if includes_unsafe_packages?(T.must(T.must(requirements_file).content))
470
530
 
471
- options << "--no-annotate" unless requirements_file.content.include?("# via ")
531
+ options << "--no-annotate" unless T.must(T.must(requirements_file).content).include?("# via ")
472
532
 
473
- options << "--no-header" unless requirements_file.content.include?("autogenerated by pip-c")
533
+ options << "--no-header" unless T.must(T.must(requirements_file).content).include?("autogenerated by pip-c")
474
534
 
475
- options << "--pre" if requirements_file.content.include?("--pre")
535
+ options << "--pre" if T.must(T.must(requirements_file).content).include?("--pre")
476
536
 
477
- options << "--strip-extras" if requirements_file.content.include?("--strip-extras")
537
+ options << "--strip-extras" if T.must(T.must(requirements_file).content).include?("--strip-extras")
478
538
 
479
- if (resolver = RESOLVER_REGEX.match(requirements_file.content))
539
+ if (resolver = RESOLVER_REGEX.match(T.must(requirements_file).content))
480
540
  options << "--resolver=#{resolver}"
481
541
  end
482
542
 
483
543
  options
484
544
  end
485
545
 
546
+ # rubocop:enable Metrics/AbcSize
547
+ sig { returns(T::Array[String]) }
486
548
  def pip_compile_index_options
487
549
  credentials
488
550
  .select { |cred| cred["type"] == "python_index" }
@@ -497,15 +559,17 @@ module Dependabot
497
559
  end
498
560
  end
499
561
 
562
+ sig { params(content: String).returns(T::Boolean) }
500
563
  def includes_unsafe_packages?(content)
501
564
  UNSAFE_PACKAGES.any? { |n| content.match?(/^#{Regexp.quote(n)}==/) }
502
565
  end
503
566
 
567
+ sig { returns(T::Array[String]) }
504
568
  def filenames_to_compile
505
569
  files_from_reqs =
506
- dependency.requirements
507
- .map { |r| r[:file] }
508
- .select { |fn| fn.end_with?(".in") }
570
+ T.must(dependency).requirements
571
+ .map { |r| r[:file] }
572
+ .select { |fn| fn.end_with?(".in") }
509
573
 
510
574
  files_from_compiled_files =
511
575
  pip_compile_files.map(&:name).select do |fn|
@@ -518,10 +582,11 @@ module Dependabot
518
582
  order_filenames_for_compilation(filenames)
519
583
  end
520
584
 
585
+ sig { params(filename: String).returns(T.nilable(Dependabot::DependencyFile)) }
521
586
  def compiled_file_for_filename(filename)
522
587
  compiled_file =
523
588
  compiled_files
524
- .find { |f| f.content.match?(output_file_regex(filename)) }
589
+ .find { |f| T.must(f.content).match?(output_file_regex(filename)) }
525
590
 
526
591
  compiled_file ||=
527
592
  compiled_files
@@ -530,26 +595,30 @@ module Dependabot
530
595
  compiled_file
531
596
  end
532
597
 
598
+ sig { params(filename: T.any(String, Symbol)).returns(String) }
533
599
  def output_file_regex(filename)
534
600
  "--output-file[=\s]+.*\s#{Regexp.escape(filename)}\s*$"
535
601
  end
536
602
 
603
+ sig { params(compiled_file: T.nilable(Dependabot::DependencyFile)).returns(T::Boolean) }
537
604
  def compiled_file_includes_dependency?(compiled_file)
538
605
  return false unless compiled_file
539
606
 
540
607
  regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
541
608
 
542
609
  matches = []
543
- compiled_file.content.scan(regex) { matches << Regexp.last_match }
544
- matches.any? { |m| normalise(m[:name]) == dependency.name }
610
+ T.must(compiled_file.content).scan(regex) { matches << Regexp.last_match }
611
+ matches.any? { |m| normalise(m[:name]) == T.must(dependency).name }
545
612
  end
546
613
 
614
+ sig { params(name: String).returns(String) }
547
615
  def normalise(name)
548
616
  NameNormaliser.normalise(name)
549
617
  end
550
618
 
551
619
  # If the files we need to update require one another then we need to
552
620
  # update them in the right order
621
+ sig { params(filenames: T::Array[String]).returns(T::Array[String]) }
553
622
  def order_filenames_for_compilation(filenames)
554
623
  ordered_filenames = T.let([], T::Array[String])
555
624
 
@@ -557,7 +626,7 @@ module Dependabot
557
626
  ordered_filenames +=
558
627
  remaining_filenames
559
628
  .reject do |fn|
560
- unupdated_reqs = requirement_map[fn] - ordered_filenames
629
+ unupdated_reqs = (requirement_map[fn] || []) - ordered_filenames
561
630
  unupdated_reqs.intersect?(filenames)
562
631
  end
563
632
  end
@@ -565,11 +634,12 @@ module Dependabot
565
634
  ordered_filenames
566
635
  end
567
636
 
637
+ sig { returns(T::Hash[String, T::Array[String]]) }
568
638
  def requirement_map
569
639
  child_req_regex = Python::FileFetcher::CHILD_REQUIREMENT_REGEX
570
640
  @requirement_map ||=
571
641
  pip_compile_files.each_with_object({}) do |file, req_map|
572
- paths = file.content.scan(child_req_regex).flatten
642
+ paths = T.must(file.content).scan(child_req_regex).flatten
573
643
  current_dir = File.dirname(file.name)
574
644
 
575
645
  req_map[file.name] =
@@ -584,6 +654,7 @@ module Dependabot
584
654
  end
585
655
  end
586
656
 
657
+ sig { returns(Dependabot::Python::FileParser::PythonRequirementParser) }
587
658
  def python_requirement_parser
588
659
  @python_requirement_parser ||=
589
660
  FileParser::PythonRequirementParser.new(
@@ -591,6 +662,7 @@ module Dependabot
591
662
  )
592
663
  end
593
664
 
665
+ sig { returns(Dependabot::Python::LanguageVersionManager) }
594
666
  def language_version_manager
595
667
  @language_version_manager ||=
596
668
  LanguageVersionManager.new(
@@ -598,18 +670,22 @@ module Dependabot
598
670
  )
599
671
  end
600
672
 
673
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
601
674
  def setup_files
602
675
  dependency_files.select { |f| f.name.end_with?("setup.py") }
603
676
  end
604
677
 
678
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
605
679
  def pip_compile_files
606
680
  dependency_files.select { |f| f.name.end_with?(".in") }
607
681
  end
608
682
 
683
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
609
684
  def compiled_files
610
685
  dependency_files.select { |f| f.name.end_with?(".txt") }
611
686
  end
612
687
 
688
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
613
689
  def setup_cfg_files
614
690
  dependency_files.select { |f| f.name.end_with?("setup.cfg") }
615
691
  end
@@ -114,12 +114,12 @@ module Dependabot
114
114
 
115
115
  sig { returns(T::Array[DependencyFile]) }
116
116
  def updated_pip_compile_based_files
117
- PipCompileFileUpdater.new(
117
+ T.must(PipCompileFileUpdater.new(
118
118
  dependencies: dependencies,
119
119
  dependency_files: dependency_files,
120
120
  credentials: credentials,
121
121
  index_urls: pip_compile_index_urls
122
- ).updated_dependency_files
122
+ ).updated_dependency_files)
123
123
  end
124
124
 
125
125
  sig { returns(T::Array[DependencyFile]) }
@@ -11,28 +11,43 @@ module Dependabot
11
11
 
12
12
  class Language < Dependabot::Ecosystem::VersionManager
13
13
  extend T::Sig
14
- # These versions should match the versions specified at the top of `python/Dockerfile`
15
- PYTHON_3_13 = "3.13"
16
- PYTHON_3_12 = "3.12"
17
- PYTHON_3_11 = "3.11"
18
- PYTHON_3_10 = "3.10"
19
- PYTHON_3_9 = "3.9"
20
- PYTHON_3_8 = "3.8"
14
+ # This list must match the versions specified at the top of `python/Dockerfile`
15
+ # ARG PY_3_13=3.13.2
16
+ PRE_INSTALLED_PYTHON_VERSIONS_RAW = %w(
17
+ 3.13.2
18
+ 3.12.9
19
+ 3.11.11
20
+ 3.10.16
21
+ 3.9.21
22
+ ).freeze
21
23
 
22
- DEPRECATED_VERSIONS = T.let([Version.new(PYTHON_3_8)].freeze, T::Array[Dependabot::Version])
24
+ PRE_INSTALLED_PYTHON_VERSIONS = T.let(PRE_INSTALLED_PYTHON_VERSIONS_RAW.map do |v|
25
+ Version.new(v)
26
+ end.sort, T::Array[Dependabot::Python::Version])
23
27
 
24
- # Keep versions in ascending order
25
- SUPPORTED_VERSIONS = T.let([
26
- Version.new(PYTHON_3_9),
27
- Version.new(PYTHON_3_10),
28
- Version.new(PYTHON_3_11),
29
- Version.new(PYTHON_3_12),
30
- Version.new(PYTHON_3_13)
31
- ].freeze, T::Array[Dependabot::Version])
28
+ PRE_INSTALLED_VERSIONS_MAP = T.let(
29
+ PRE_INSTALLED_PYTHON_VERSIONS.to_h do |v|
30
+ [Dependabot::Python::Version.new(T.must(v.segments[0..1]).join(".")), v]
31
+ end,
32
+ T::Hash[Dependabot::Python::Version, Dependabot::Python::Version]
33
+ )
34
+
35
+ PRE_INSTALLED_HIGHEST_VERSION = T.let(T.must(PRE_INSTALLED_PYTHON_VERSIONS.max), Dependabot::Python::Version)
36
+
37
+ SUPPORTED_VERSIONS = T.let(
38
+ PRE_INSTALLED_PYTHON_VERSIONS.map do |v|
39
+ Dependabot::Python::Version.new(T.must(v.segments[0..1]&.join(".")))
40
+ end,
41
+ T::Array[Dependabot::Python::Version]
42
+ )
43
+
44
+ NON_SUPPORTED_HIGHEST_VERSION = "3.8"
45
+
46
+ DEPRECATED_VERSIONS = T.let([Version.new(NON_SUPPORTED_HIGHEST_VERSION)].freeze, T::Array[Dependabot::Version])
32
47
 
33
48
  sig do
34
49
  params(
35
- detected_version: String,
50
+ detected_version: T.nilable(String),
36
51
  raw_version: T.nilable(String),
37
52
  requirement: T.nilable(Requirement)
38
53
  ).void
@@ -40,7 +55,7 @@ module Dependabot
40
55
  def initialize(detected_version:, raw_version: nil, requirement: nil)
41
56
  super(
42
57
  name: LANGUAGE,
43
- detected_version: major_minor_version(detected_version),
58
+ detected_version: detected_version ? major_minor_version(detected_version) : nil,
44
59
  version: raw_version ? Version.new(raw_version) : nil,
45
60
  deprecated_versions: DEPRECATED_VERSIONS,
46
61
  supported_versions: SUPPORTED_VERSIONS,
@@ -48,25 +63,12 @@ module Dependabot
48
63
  )
49
64
  end
50
65
 
51
- sig { override.returns(T::Boolean) }
52
- def deprecated?
53
- return false unless detected_version
54
- return false if unsupported?
55
-
56
- deprecated_versions.include?(detected_version)
57
- end
58
-
59
- sig { override.returns(T::Boolean) }
60
- def unsupported?
61
- return false unless detected_version
62
-
63
- supported_versions.all? { |supported| supported > detected_version }
64
- end
65
-
66
66
  private
67
67
 
68
- sig { params(version: String).returns(Dependabot::Python::Version) }
68
+ sig { params(version: String).returns(T.nilable(Dependabot::Python::Version)) }
69
69
  def major_minor_version(version)
70
+ return nil if version.empty?
71
+
70
72
  major_minor = T.let(T.must(Version.new(version).segments[0..1]&.join(".")), String)
71
73
 
72
74
  Version.new(major_minor)
@@ -9,14 +9,6 @@ module Dependabot
9
9
  module Python
10
10
  class LanguageVersionManager
11
11
  extend T::Sig
12
- # This list must match the versions specified at the top of `python/Dockerfile`
13
- PRE_INSTALLED_PYTHON_VERSIONS = %w(
14
- 3.13.2
15
- 3.12.9
16
- 3.11.11
17
- 3.10.16
18
- 3.9.21
19
- ).freeze
20
12
 
21
13
  sig { params(python_requirement_parser: T.untyped).void }
22
14
  def initialize(python_requirement_parser:)
@@ -62,7 +54,34 @@ module Dependabot
62
54
  user_specified_python_version
63
55
  end
64
56
  else
65
- python_version_matching_imputed_requirements || PRE_INSTALLED_PYTHON_VERSIONS.first
57
+ python_version_matching_imputed_requirements || Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
58
+ end
59
+ end
60
+
61
+ sig { params(requirement_string: T.nilable(String)).returns(T.nilable(String)) }
62
+ def normalize_python_exact_version(requirement_string)
63
+ return requirement_string if requirement_string.nil? || requirement_string.strip.empty?
64
+
65
+ requirement_string = requirement_string.strip
66
+
67
+ # If the requirement already has a wildcard, return nil
68
+ return nil if requirement_string == "*"
69
+
70
+ # If the requirement is not an exact version such as not X.Y.Z, =X.Y.Z, ==X.Y.Z, ===X.Y.Z
71
+ # then return the requirement as is
72
+ return requirement_string unless requirement_string.match?(/^=?={0,2}\s*\d+\.\d+(\.\d+)?(-[a-z0-9.-]+)?$/i)
73
+
74
+ parts = requirement_string.gsub(/^=+/, "").split(".")
75
+
76
+ case parts.length
77
+ when 1 # Only major version (X)
78
+ ">= #{parts[0]}.0.0 < #{parts[0].to_i + 1}.0.0" # Ensure only major version range
79
+ when 2 # Major.Minor (X.Y)
80
+ ">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Ensure only minor version range
81
+ when 3 # Major.Minor.Patch (X.Y.Z)
82
+ ">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Convert to >= X.Y.0
83
+ else
84
+ requirement_string
66
85
  end
67
86
  end
68
87
 
@@ -72,15 +91,22 @@ module Dependabot
72
91
 
73
92
  # If the requirement string isn't already a range (eg ">3.10"), coerce it to "major.minor.*".
74
93
  # The patch version is ignored because a non-matching patch version is unlikely to affect resolution.
75
- requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if requirement_string.start_with?(/\d/)
94
+ requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if /^\d/.match?(requirement_string)
95
+
96
+ requirement_string = normalize_python_exact_version(requirement_string)
97
+
98
+ if requirement_string.nil? || requirement_string.strip.empty?
99
+ return Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
100
+ end
76
101
 
77
102
  # Try to match one of our pre-installed Python versions
78
103
  requirement = T.must(Python::Requirement.requirements_array(requirement_string).first)
79
- version = PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(Python::Version.new(v)) }
80
- return version if version
81
104
 
82
- # Otherwise we have to raise
83
- supported_versions = PRE_INSTALLED_PYTHON_VERSIONS.map { |x| x.gsub(/\.\d+$/, ".*") }.join(", ")
105
+ version = Language::PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(v) }
106
+ return version.to_s if version
107
+
108
+ # Otherwise we have to raise an error
109
+ supported_versions = Language::SUPPORTED_VERSIONS.map { |v| "#{v}.*" }.join(", ")
84
110
  raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
85
111
  end
86
112
 
@@ -100,14 +126,13 @@ module Dependabot
100
126
 
101
127
  sig { params(requirements: T.untyped).returns(T.nilable(String)) }
102
128
  def python_version_matching(requirements)
103
- PRE_INSTALLED_PYTHON_VERSIONS.find do |version_string|
104
- version = Python::Version.new(version_string)
129
+ Language::PRE_INSTALLED_PYTHON_VERSIONS.find do |version|
105
130
  requirements.all? do |req|
106
131
  next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
107
132
 
108
133
  req.satisfied_by?(version)
109
134
  end
110
- end
135
+ end.to_s
111
136
  end
112
137
  end
113
138
  end
@@ -93,7 +93,7 @@ module Dependabot
93
93
  private
94
94
 
95
95
  def convert_python_constraint_to_ruby_constraint(req_string)
96
- return nil if req_string.nil?
96
+ return nil if req_string.nil? || req_string.strip.empty?
97
97
  return nil if req_string == "*"
98
98
 
99
99
  req_string = req_string.gsub("~=", "~>")
@@ -101,6 +101,8 @@ module Dependabot
101
101
 
102
102
  if req_string.match?(/~[^>]/) then convert_tilde_req(req_string)
103
103
  elsif req_string.start_with?("^") then convert_caret_req(req_string)
104
+ elsif req_string.match?(/^=?={0,2}\s*\d+\.\d+(\.\d+)?(-[a-z0-9.-]+)?(\.\*)?$/i)
105
+ convert_exact(req_string)
104
106
  elsif req_string.include?(".*") then convert_wildcard(req_string)
105
107
  else
106
108
  req_string
@@ -155,6 +157,37 @@ module Dependabot
155
157
  .gsub(/\*$/, "0.dev")
156
158
  .tap { |s| exact_op ? s.gsub!(/^(?<!!)=*/, "~>") : s }
157
159
  end
160
+
161
+ def convert_exact(req_string)
162
+ arbitrary_equality = req_string.start_with?("===")
163
+ cleaned_version = req_string.gsub(/^=+/, "").strip
164
+
165
+ return ["=== #{cleaned_version}"] if arbitrary_equality
166
+
167
+ # Handle versions wildcarded with .*, e.g. 1.0.*
168
+ if cleaned_version.include?(".*")
169
+ # Remove all characters after the first .*, and the .*
170
+ cleaned_version = cleaned_version.split(".*").first
171
+ version = Python::Version.new(cleaned_version)
172
+ # Get the release segment parts [major, minor, patch]
173
+ version_parts = version.release_segment
174
+
175
+ if version_parts.length == 1
176
+ major = T.must(version_parts[0])
177
+ [">= #{major}.0.0.dev", "< #{major + 1}.0.0"]
178
+ elsif version_parts.length == 2
179
+ major, minor = version_parts
180
+ "~> #{major}.#{minor}.0.dev"
181
+ elsif version_parts.length == 3
182
+ major, minor, patch = version_parts
183
+ "~> #{major}.#{minor}.#{patch}.dev"
184
+ else
185
+ "= #{cleaned_version}"
186
+ end
187
+ else
188
+ "= #{cleaned_version}"
189
+ end
190
+ end
158
191
  end
159
192
  end
160
193
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-python
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.301.1
4
+ version: 0.302.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-14 00:00:00.000000000 Z
11
+ date: 2025-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.301.1
19
+ version: 0.302.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.301.1
26
+ version: 0.302.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -291,7 +291,7 @@ licenses:
291
291
  - MIT
292
292
  metadata:
293
293
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
294
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.301.1
294
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.302.0
295
295
  post_install_message:
296
296
  rdoc_options: []
297
297
  require_paths: