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.
@@ -12,6 +12,7 @@ require "dependabot/python/name_normaliser"
12
12
  require "dependabot/package/package_release"
13
13
  require "dependabot/package/package_details"
14
14
  require "dependabot/python/package/package_registry_finder"
15
+ require "dependabot/python/package/simple_api_parser"
15
16
 
16
17
  # Stores metadata for a package, including all its available versions
17
18
  module Dependabot
@@ -21,6 +22,7 @@ module Dependabot
21
22
  CREDENTIALS_PASSWORD = "password"
22
23
 
23
24
  APPLICATION_JSON = "application/json"
25
+ APPLICATION_PYPI_SIMPLE_JSON = "application/vnd.pypi.simple.v1+json"
24
26
  APPLICATION_TEXT = "text/html"
25
27
  CPYTHON = "cpython"
26
28
  PYTHON = "python"
@@ -42,11 +44,7 @@ module Dependabot
42
44
  credentials: T::Array[Dependabot::Credential]
43
45
  ).void
44
46
  end
45
- def initialize(
46
- dependency:,
47
- dependency_files:,
48
- credentials:
49
- )
47
+ def initialize(dependency:, dependency_files:, credentials:)
50
48
  @dependency = dependency
51
49
  @dependency_files = dependency_files
52
50
  @credentials = credentials
@@ -70,7 +68,7 @@ module Dependabot
70
68
  .flat_map do |index_url|
71
69
  fetch_from_registry(index_url) || [] # Ensure it always returns an array
72
70
  rescue Excon::Error::Timeout, Excon::Error::Socket
73
- raise if MAIN_PYPI_INDEXES.include?(index_url)
71
+ raise if main_pypi_index?(index_url)
74
72
 
75
73
  raise PrivateSourceTimedOut, sanitized_url(index_url)
76
74
  rescue URI::InvalidURIError
@@ -90,15 +88,46 @@ module Dependabot
90
88
  .returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
91
89
  end
92
90
  def fetch_from_registry(index_url)
93
- metadata = fetch_from_json_registry(index_url)
91
+ return fetch_from_simple_registry(index_url) unless main_pypi_index?(index_url)
94
92
 
95
- return metadata if metadata&.any?
93
+ begin
94
+ metadata = fetch_from_json_registry(index_url)
95
+
96
+ return metadata if metadata&.any?
96
97
 
97
- Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
98
- fetch_from_html_registry(index_url)
99
- rescue StandardError => e
100
- Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
101
- fetch_from_html_registry(index_url)
98
+ Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
99
+ fetch_from_html_registry(index_url)
100
+ rescue Dependabot::DependencyFileNotResolvable
101
+ raise
102
+ rescue StandardError => e
103
+ Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
104
+ fetch_from_html_registry(index_url)
105
+ end
106
+ end
107
+
108
+ sig { params(index_url: String).returns(T::Array[Dependabot::Package::PackageRelease]) }
109
+ def fetch_from_simple_registry(index_url)
110
+ Dependabot.logger.info(
111
+ "Fetching release information from simple registry at #{sanitized_url(index_url)} for #{dependency.name}"
112
+ )
113
+ response = registry_response_for_dependency(index_url, accept: simple_api_accept)
114
+ project_url = response.path ? Excon::Utils.request_uri(response.data) : index_url + normalised_name + "/"
115
+ check_authentication_response(response, index_url)
116
+
117
+ version_releases = if simple_json_response?(response)
118
+ SimpleApiParser.new(
119
+ dependency: dependency,
120
+ project_url: project_url
121
+ ).parse(response.body)
122
+ else
123
+ extract_release_details_json_from_html(response.body)
124
+ end
125
+
126
+ format_version_releases(version_releases, preserve_distributions: simple_json_response?(response))
127
+ .sort_by(&:version).reverse
128
+ rescue JSON::ParserError
129
+ Dependabot.logger.warn("JSON parsing error for #{sanitized_url(index_url)}.")
130
+ []
102
131
  end
103
132
 
104
133
  # Example JSON Response Format:
@@ -203,14 +232,8 @@ module Dependabot
203
232
  Dependabot.logger.info(
204
233
  "Fetching release information from html registry at #{sanitized_url(index_url)} for #{dependency.name}"
205
234
  )
206
- index_response = registry_response_for_dependency(index_url)
207
- if index_response.status == 401 || index_response.status == 403
208
- registry_index_response = registry_index_response(index_url)
209
-
210
- if registry_index_response.status == 401 || registry_index_response.status == 403
211
- raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
212
- end
213
- end
235
+ index_response = registry_response_for_dependency(index_url, accept: APPLICATION_TEXT)
236
+ check_authentication_response(index_response, index_url)
214
237
 
215
238
  version_releases = extract_release_details_json_from_html(index_response.body)
216
239
  releases = format_version_releases(version_releases)
@@ -218,6 +241,27 @@ module Dependabot
218
241
  releases.sort_by(&:version).reverse
219
242
  end
220
243
 
244
+ sig { params(response: Excon::Response).returns(T::Boolean) }
245
+ def simple_json_response?(response)
246
+ response.headers["Content-Type"].to_s.split(";", 2).first.to_s.strip.downcase == APPLICATION_PYPI_SIMPLE_JSON
247
+ end
248
+
249
+ sig { returns(String) }
250
+ def simple_api_accept
251
+ "application/vnd.pypi.simple.v1+json, " \
252
+ "application/vnd.pypi.simple.v1+html;q=0.2, text/html;q=0.01"
253
+ end
254
+
255
+ sig { params(response: Excon::Response, index_url: String).void }
256
+ def check_authentication_response(response, index_url)
257
+ return unless [401, 403].include?(response.status)
258
+
259
+ index_response = registry_index_response(index_url)
260
+ return unless [401, 403].include?(index_response.status)
261
+
262
+ raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
263
+ end
264
+
221
265
  sig do
222
266
  params(html_body: String)
223
267
  .returns(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]) # Returns JSON-like format
@@ -266,23 +310,17 @@ module Dependabot
266
310
 
267
311
  sig do
268
312
  params(
269
- releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]])
313
+ releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]),
314
+ preserve_distributions: T::Boolean
270
315
  )
271
316
  .returns(T::Array[Dependabot::Package::PackageRelease])
272
317
  end
273
- def format_version_releases(releases_json)
318
+ def format_version_releases(releases_json, preserve_distributions: false)
274
319
  return [] unless releases_json
275
320
 
276
321
  releases_json.each_with_object([]) do |(version, release_data_array), versions|
277
- release_data = release_data_array.last
278
-
279
- next unless release_data
280
-
281
- release = format_version_release(version, release_data)
282
-
283
- next unless release
284
-
285
- versions << release
322
+ release_data_array = [release_data_array.last].compact unless preserve_distributions
323
+ versions.concat(release_data_array.filter_map { |data| format_version_release(version, data) })
286
324
  end
287
325
  end
288
326
 
@@ -397,11 +435,11 @@ module Dependabot
397
435
  )
398
436
  end
399
437
 
400
- sig { params(index_url: String).returns(Excon::Response) }
401
- def registry_response_for_dependency(index_url)
438
+ sig { params(index_url: String, accept: String).returns(Excon::Response) }
439
+ def registry_response_for_dependency(index_url, accept:)
402
440
  Dependabot::RegistryClient.get(
403
441
  url: index_url + normalised_name + "/",
404
- headers: { "Accept" => APPLICATION_TEXT }
442
+ headers: { "Accept" => accept }
405
443
  )
406
444
  end
407
445
 
@@ -487,6 +525,14 @@ module Dependabot
487
525
  index_url.sub(%r{//([^/@]+)@}, "//redacted@")
488
526
  end
489
527
 
528
+ sig { params(index_url: String).returns(T::Boolean) }
529
+ def main_pypi_index?(index_url)
530
+ uri = URI.parse(index_url)
531
+ T.must(uri.scheme.to_s.casecmp("https")).zero? && uri.port == 443 &&
532
+ uri.path.to_s.split("/").reject(&:empty?) == ["simple"] &&
533
+ %w(pypi.org pypi.python.org).include?(uri.host.to_s.downcase)
534
+ end
535
+
490
536
  sig { params(dep_name: String).returns(String) }
491
537
  def remove_optional(dep_name)
492
538
  # Remove any optional dependencies postfix, e.g., "pyvista[io] "
@@ -193,7 +193,7 @@ module Dependabot
193
193
 
194
194
  sig { params(url: String).returns(String) }
195
195
  def clean_check_and_remove_environment_variables(url)
196
- url = url.strip.sub(%r{/+$}, "") + "/"
196
+ url = with_single_trailing_slash(url.strip)
197
197
 
198
198
  return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
199
199
 
@@ -203,7 +203,7 @@ module Dependabot
203
203
  *config_variable_index_urls[:extra]
204
204
  ]
205
205
  .compact
206
- .map { |u| u.strip.gsub(%r{/*$}, "") + "/" }
206
+ .map { |u| with_single_trailing_slash(u.strip) }
207
207
 
208
208
  regexp = url
209
209
  .sub(%r{(?<=://).+@}, "")
@@ -226,7 +226,7 @@ module Dependabot
226
226
  cred = credential_for(base_url)
227
227
  return base_url unless cred
228
228
 
229
- AuthedUrlBuilder.authed_url(credential: cred).gsub(%r{/*$}, "") + "/"
229
+ with_single_trailing_slash(AuthedUrlBuilder.authed_url(credential: cred))
230
230
  end
231
231
 
232
232
  sig { params(url: String).returns(T.nilable(Dependabot::Credential)) }
@@ -234,11 +234,18 @@ module Dependabot
234
234
  credentials
235
235
  .select { |c| c["type"] == "python_index" }
236
236
  .find do |c|
237
- cred_url = c.fetch("index-url").gsub(%r{/*$}, "") + "/"
237
+ cred_url = with_single_trailing_slash(c.fetch("index-url"))
238
238
  cred_url.include?(url)
239
239
  end
240
240
  end
241
241
 
242
+ sig { params(url: String).returns(String) }
243
+ def with_single_trailing_slash(url)
244
+ normalized_url = url.dup
245
+ normalized_url.chop! while normalized_url.end_with?("/")
246
+ normalized_url + "/"
247
+ end
248
+
242
249
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
243
250
  def pip_conf
244
251
  dependency_files.find { |f| f.name == "pip.conf" }
@@ -0,0 +1,91 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "uri"
6
+ require "sorbet-runtime"
7
+ require "dependabot/errors"
8
+ require "dependabot/python/name_normaliser"
9
+
10
+ module Dependabot
11
+ module Python
12
+ module Package
13
+ class SimpleApiParser
14
+ extend T::Sig
15
+
16
+ ReleaseDetail = T.type_alias { T::Hash[String, T.nilable(T.any(String, T::Boolean))] }
17
+
18
+ sig { params(dependency: Dependabot::Dependency, project_url: String).void }
19
+ def initialize(dependency:, project_url:)
20
+ @dependency = dependency
21
+ @project_url = project_url
22
+ end
23
+
24
+ sig do
25
+ params(json_body: String)
26
+ .returns(T::Hash[String, T::Array[ReleaseDetail]])
27
+ end
28
+ def parse(json_body)
29
+ data = JSON.parse(json_body)
30
+ validate_api_version!(data.dig("meta", "api-version").to_s)
31
+
32
+ data.fetch("files", []).each_with_object({}) do |file, releases|
33
+ filename = file["filename"]
34
+ next unless filename&.match?(name_regex)
35
+
36
+ version = version_from_filename(filename)
37
+ next unless dependency.version_class.correct?(version)
38
+
39
+ yanked = file["yanked"] || false
40
+ details = {
41
+ "version" => version,
42
+ "requires_python" => file["requires-python"],
43
+ "yanked" => yanked != false,
44
+ "yanked_reason" => yanked.is_a?(String) ? yanked : nil,
45
+ "upload_time" => file["upload-time"],
46
+ "url" => resolve_url(file["url"])
47
+ }
48
+ releases[version] ||= []
49
+ releases[version] << details
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ sig { returns(Dependabot::Dependency) }
56
+ attr_reader :dependency
57
+
58
+ sig { returns(String) }
59
+ attr_reader :project_url
60
+
61
+ sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
62
+ def resolve_url(url)
63
+ return unless url
64
+
65
+ resolved_url = URI.join(project_url, url)
66
+ resolved_url.user = nil
67
+ resolved_url.password = nil
68
+ resolved_url.to_s
69
+ end
70
+
71
+ sig { params(api_version: String).void }
72
+ def validate_api_version!(api_version)
73
+ return unless api_version.split(".").first.to_i > 1
74
+
75
+ raise Dependabot::DependencyFileNotResolvable, "Unsupported PEP 691 API version: #{api_version}"
76
+ end
77
+
78
+ sig { returns(Regexp) }
79
+ def name_regex
80
+ parts = NameNormaliser.normalise(dependency.name).split(/[\s_.-]/).map { |name| Regexp.quote(name) }
81
+ /#{parts.join("[\s_.-]")}/i
82
+ end
83
+
84
+ sig { params(filename: String).returns(T.nilable(String)) }
85
+ def version_from_filename(filename)
86
+ filename.strip.gsub(/#{name_regex}-/i, "").split(/-|\.tar\.|\.zip|\.whl/).first
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -249,8 +249,7 @@ module Dependabot
249
249
  next if previously_fetched_files.map(&:name).include?(path)
250
250
  next if file.name == path
251
251
 
252
- if Dependabot::Experiments.enabled?(:enable_exclude_paths_subdirectory_manifest_files) &&
253
- !@exclude_paths.empty? && Dependabot::FileFiltering.exclude_path?(path, @exclude_paths)
252
+ if !@exclude_paths.empty? && Dependabot::FileFiltering.exclude_path?(path, @exclude_paths)
254
253
  raise Dependabot::DependencyFileNotEvaluatable,
255
254
  "Cannot process requirements: '#{file.name}' references excluded file '#{path}'. " \
256
255
  "Please either remove the reference from '#{file.name}' " \
@@ -42,6 +42,22 @@ module Dependabot
42
42
  cooldown.semver_minor_days.to_i.positive? ||
43
43
  cooldown.semver_patch_days.to_i.positive?
44
44
  end
45
+
46
+ sig do
47
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
48
+ .returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
49
+ end
50
+ def eligible_releases(language_version: nil)
51
+ releases = available_versions
52
+ return unless releases
53
+
54
+ releases = filter_yanked_versions(releases)
55
+ releases = filter_by_cooldown(releases)
56
+ releases = filter_unsupported_versions(releases, language_version)
57
+ releases = filter_prerelease_versions(releases)
58
+ releases = filter_ignored_versions(releases)
59
+ apply_post_fetch_latest_versions_filter(releases)
60
+ end
45
61
  end
46
62
  end
47
63
  end
@@ -129,7 +129,7 @@ module Dependabot
129
129
 
130
130
  sig { returns(T.nilable(Gem::Version)) }
131
131
  def latest_version_for_git_dependency
132
- latest_git_version_details&.fetch(:version)
132
+ latest_git_version_details&.version
133
133
  end
134
134
 
135
135
  sig { returns(T.nilable(Gem::Version)) }
@@ -152,7 +152,7 @@ module Dependabot
152
152
  def updated_git_source
153
153
  # Update the git tag if a new version is available
154
154
  if git_commit_checker.pinned_ref_looks_like_version? && latest_git_version_details
155
- new_tag = T.must(latest_git_version_details).fetch(:tag)
155
+ new_tag = T.must(latest_git_version_details).tag
156
156
  source_details = dependency.source_details
157
157
  return source_details.transform_keys(&:to_sym).merge(ref: new_tag) if source_details
158
158
  end
@@ -161,11 +161,11 @@ module Dependabot
161
161
  dependency.source_details&.transform_keys(&:to_sym)
162
162
  end
163
163
 
164
- sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
164
+ sig { returns(T.nilable(Dependabot::GitTagDetails)) }
165
165
  def latest_git_version_details
166
166
  @latest_git_version_details ||= T.let(
167
167
  git_commit_checker.local_tag_for_latest_version(@update_cooldown),
168
- T.nilable(T::Hash[Symbol, T.untyped])
168
+ T.nilable(Dependabot::GitTagDetails)
169
169
  )
170
170
  end
171
171
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-python
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.392.0
4
+ version: 0.394.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.392.0
18
+ version: 0.394.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.392.0
25
+ version: 0.394.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -169,14 +169,14 @@ dependencies:
169
169
  requirements:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
- version: '0.22'
172
+ version: '1.1'
173
173
  type: :development
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - "~>"
178
178
  - !ruby/object:Gem::Version
179
- version: '0.22'
179
+ version: '1.1'
180
180
  - !ruby/object:Gem::Dependency
181
181
  name: turbo_tests
182
182
  requirement: !ruby/object:Gem::Requirement
@@ -280,7 +280,10 @@ files:
280
280
  - lib/dependabot/python/dependency_grapher/requirements_layers.rb
281
281
  - lib/dependabot/python/file_fetcher.rb
282
282
  - lib/dependabot/python/file_parser.rb
283
+ - lib/dependabot/python/file_parser/pep_dependency.rb
283
284
  - lib/dependabot/python/file_parser/pipfile_files_parser.rb
285
+ - lib/dependabot/python/file_parser/poetry_lock.rb
286
+ - lib/dependabot/python/file_parser/pyproject_document.rb
284
287
  - lib/dependabot/python/file_parser/pyproject_files_parser.rb
285
288
  - lib/dependabot/python/file_parser/python_requirement_parser.rb
286
289
  - lib/dependabot/python/file_parser/setup_file_parser.rb
@@ -302,6 +305,7 @@ files:
302
305
  - lib/dependabot/python/native_helpers.rb
303
306
  - lib/dependabot/python/package/package_details_fetcher.rb
304
307
  - lib/dependabot/python/package/package_registry_finder.rb
308
+ - lib/dependabot/python/package/simple_api_parser.rb
305
309
  - lib/dependabot/python/package_manager.rb
306
310
  - lib/dependabot/python/pip_compile_file_matcher.rb
307
311
  - lib/dependabot/python/pipenv_runner.rb
@@ -323,7 +327,7 @@ licenses:
323
327
  - MIT
324
328
  metadata:
325
329
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
326
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.392.0
330
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.394.0
327
331
  rdoc_options: []
328
332
  require_paths:
329
333
  - lib