dependabot-python 0.367.0 → 0.368.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: 5e54cc0660ba9d6007661da639e66151ac7cdab9dafb2ea460ee544b825e7790
4
- data.tar.gz: eaaec236cd949ffa2a446b5056a56378a2bcf954eb32da78e0ec0cb9f7b0bb4c
3
+ metadata.gz: 5b03d84191a73f9cd803b3db1fd32bc70638a4f607c2e9424a3900f143119f5f
4
+ data.tar.gz: 6d04a235c38eb76a1a86eb2f60e2f34c80c82ff5f342d997ff3c8a25011e42f3
5
5
  SHA512:
6
- metadata.gz: 88f0d4fc4032adab9165c2d0187ef56660ebc219472723ad97c61c128e764cfeac49d4a49abfa4999f05ac85f9ccee3189dec442fc5701bf1afd41d18d706fba
7
- data.tar.gz: 931b2880cdfda0184098b61416963e02542a13993752f83e8682f3c25b7de3e2f6c45affe5a80e231a331fc6bcd4b62b0eec3bab33aae065a92d70932e31c844
6
+ metadata.gz: 793a849e50f2162b65f1da65521dfb7450081b84507fbb4134145ab5c72b63a916a59c68602bf41dea530d373cb0d2dd5e9a38fee9b6a5347b17848a8891054f
7
+ data.tar.gz: db96c2fd7be7c4b6ac473f29dc429f3c4163fb2dd38d4b1d48c19ea62727022ff6822166dd10e9c4a680eae24bc5f16f447bf9f8ec55b7909b8ae663698b29c6
@@ -55,33 +55,56 @@ module Dependabot
55
55
 
56
56
  sig { returns(T::Array[Dependabot::DependencyFile]) }
57
57
  def fetch_updated_dependency_files
58
- reqs = dependency.requirements.zip(dependency.previous_requirements || [])
58
+ updated_contents = T.let({}, T::Hash[String, String])
59
+
60
+ unique_requirement_changes.each do |pair|
61
+ new_req = T.must(pair[0])
62
+ old_req = pair[1]
63
+ filename = new_req.fetch(:file)
64
+ content = updated_contents[filename] || T.must(T.must(get_original_file(filename)).content)
65
+ updated_contents[filename] = updated_requirement_or_setup_file_content(content, new_req, old_req)
66
+ end
67
+
68
+ updated_contents.filter_map do |filename, content|
69
+ file = T.must(get_original_file(filename)).dup
70
+ next if content == T.must(file.content)
71
+
72
+ file.content = content
73
+ file
74
+ end
75
+ end
76
+
77
+ # Deduplicates requirements that share the same file and requirement strings.
78
+ # The replacer's regex matches all extras variants at once, so one call per
79
+ # unique (file, old_requirement, new_requirement) is sufficient.
80
+ sig { returns(T::Array[T::Array[T.nilable(T::Hash[Symbol, T.untyped])]]) }
81
+ def unique_requirement_changes
82
+ previous_reqs = dependency.previous_requirements || []
59
83
 
60
- reqs.filter_map do |(new_req, old_req)|
84
+ changes = dependency.requirements.filter_map do |new_req|
85
+ old_req = previous_reqs.find { |r| r[:file] == new_req[:file] && r[:groups] == new_req[:groups] }
61
86
  next if new_req == old_req
62
87
 
63
- file = T.must(get_original_file(new_req.fetch(:file))).dup
64
- updated_content =
65
- updated_requirement_or_setup_file_content(new_req, old_req)
66
- next if updated_content == file.content
88
+ [new_req, old_req]
89
+ end
67
90
 
68
- file.content = updated_content
69
- file
91
+ changes.uniq do |pair|
92
+ new_req = pair[0]
93
+ old_req = pair[1]
94
+ [new_req[:file], old_req&.fetch(:requirement), new_req.fetch(:requirement)]
70
95
  end
71
96
  end
72
97
 
73
98
  sig do
74
99
  params(
100
+ content: String,
75
101
  new_req: T::Hash[Symbol, T.untyped],
76
102
  old_req: T.nilable(T::Hash[Symbol, T.untyped])
77
103
  ).returns(String)
78
104
  end
79
- def updated_requirement_or_setup_file_content(new_req, old_req)
80
- original_file = get_original_file(new_req.fetch(:file))
81
- raise "Could not find a dependency file for #{new_req}" unless original_file
82
-
105
+ def updated_requirement_or_setup_file_content(content, new_req, old_req)
83
106
  RequirementReplacer.new(
84
- content: T.must(original_file.content),
107
+ content: content,
85
108
  dependency_name: dependency.name,
86
109
  old_requirement: old_req&.fetch(:requirement),
87
110
  new_requirement: new_req.fetch(:requirement),
@@ -53,7 +53,7 @@ module Dependabot
53
53
  # ignore it, since it isn't actually a declaration
54
54
  next mtch if Regexp.last_match&.pre_match&.match?(/--.*\z/)
55
55
 
56
- updated_dependency_declaration_string
56
+ updated_matched_declaration(mtch)
57
57
  end
58
58
 
59
59
  raise "Expected content to change!" if old_requirement != new_requirement && content == updated_content
@@ -98,29 +98,30 @@ module Dependabot
98
98
  new_req_string
99
99
  end
100
100
 
101
- sig { returns(String) }
102
- def updated_dependency_declaration_string
103
- old_req = old_requirement
104
- updated_string =
105
- if old_req
106
- original_dependency_declaration_string(old_req)
107
- .sub(RequirementParser::REQUIREMENTS, updated_requirement_string || "")
108
- else
109
- original_dependency_declaration_string(old_req)
110
- .sub(RequirementParser::NAME_WITH_EXTRAS) do |nm|
111
- nm + (updated_requirement_string || "")
112
- end
113
- end
114
-
115
- return updated_string unless update_hashes? && requirement_includes_hashes?(old_req)
116
-
117
- updated_string.sub(
101
+ # Builds updated declaration from the actual matched text, preserving
102
+ # whatever extras (or lack thereof) appeared in the original match.
103
+ sig { params(matched_declaration: String).returns(String) }
104
+ def updated_matched_declaration(matched_declaration)
105
+ updated = if old_requirement
106
+ matched_declaration
107
+ .sub(RequirementParser::REQUIREMENTS, updated_requirement_string || "")
108
+ else
109
+ matched_declaration
110
+ .sub(RequirementParser::NAME_WITH_EXTRAS) { |nm| nm + (updated_requirement_string || "") }
111
+ end
112
+
113
+ return updated unless update_hashes? && matched_declaration.match?(RequirementParser::HASHES)
114
+
115
+ algorithm = T.must(matched_declaration.match(RequirementParser::HASHES))
116
+ .named_captures.fetch("algorithm")
117
+ separator = hash_separator(old_requirement)
118
+ updated.sub(
118
119
  RequirementParser::HASHES,
119
120
  package_hashes_for(
120
121
  name: dependency_name,
121
122
  version: new_hash_version,
122
- algorithm: hash_algorithm(old_req)
123
- ).join(hash_separator(old_req))
123
+ algorithm: algorithm
124
+ ).join(separator)
124
125
  )
125
126
  end
126
127
 
@@ -142,7 +143,14 @@ module Dependabot
142
143
  def original_declaration_replacement_regex
143
144
  original_string =
144
145
  original_dependency_declaration_string(old_requirement)
145
- /(?<![\-\w\.\[])#{Regexp.escape(original_string)}(?![\-\w\.])/
146
+ match_data = T.must(original_string.match(RequirementParser::NAME_WITH_EXTRAS))
147
+ name_escaped = Regexp.escape(T.must(match_data[:name]))
148
+ # Everything after name+extras (the requirement/markers/hashes portion)
149
+ after_name_extras = T.must(original_string[T.must(match_data[0]).length..]).strip
150
+ after_escaped = Regexp.escape(after_name_extras)
151
+ # Match the dependency name with any extras (or none), followed by the requirement.
152
+ # This ensures a single gsub handles all extras variants of the same dependency.
153
+ /(?<![\-\w\.\[])#{name_escaped}\s*\\?\s*(?:\[[^\]]*\])?\s*\\?\s*#{after_escaped}(?![\-\w\.])/
146
154
  end
147
155
 
148
156
  sig { params(requirement: T.nilable(String)).returns(T::Boolean) }
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
5
+ require "openssl"
5
6
  require "uri"
6
7
 
7
8
  require "dependabot/metadata_finders"
@@ -27,6 +28,7 @@ module Dependabot
27
28
  def initialize(dependency:, credentials:)
28
29
  super
29
30
  @pypi_listing = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
31
+ @parsed_source_urls = T.let({}, T::Hash[String, T.nilable(Dependabot::Source)])
30
32
  end
31
33
 
32
34
  sig { returns(T.nilable(String)) }
@@ -37,26 +39,88 @@ module Dependabot
37
39
  super
38
40
  end
39
41
 
42
+ sig { override.returns(T.nilable(String)) }
43
+ def maintainer_changes
44
+ return unless dependency.previous_version
45
+ return unless dependency.version
46
+
47
+ previous_ownership = ownership_for_version(T.must(dependency.previous_version))
48
+ current_ownership = ownership_for_version(T.must(dependency.version))
49
+
50
+ if previous_ownership.nil? || current_ownership.nil?
51
+ Dependabot.logger.info("Unable to determine ownership changes for #{dependency.name}")
52
+ return
53
+ end
54
+
55
+ previous_org = previous_ownership["organization"]
56
+ current_org = current_ownership["organization"]
57
+
58
+ if previous_org != current_org && !(previous_org.nil? && current_org)
59
+ return "The organization that maintains #{dependency.name} on PyPI has " \
60
+ "changed since your current version."
61
+ end
62
+
63
+ previous_users = ownership_users(previous_ownership)
64
+ current_users = ownership_users(current_ownership)
65
+
66
+ # Warn only when there were previous maintainers and none of them remain
67
+ return unless previous_users.any? && !previous_users.intersect?(current_users)
68
+
69
+ "None of the maintainers for your current version of #{dependency.name} are " \
70
+ "listed as maintainers for the new version on PyPI."
71
+ end
72
+
40
73
  private
41
74
 
42
75
  sig { override.returns(T.nilable(Dependabot::Source)) }
43
76
  def look_up_source
77
+ source_url = exact_match_source_url_from_project_urls
78
+ source_url ||= labelled_source_url_from_project_urls
79
+ source_url ||= fallback_source_url
80
+ source_url ||= source_from_description
81
+ source_url ||= source_from_homepage
82
+
83
+ parsed_source_from_url(source_url)
84
+ end
85
+
86
+ sig { returns(T.nilable(String)) }
87
+ def exact_match_source_url_from_project_urls
88
+ project_urls.values.find do |url|
89
+ repo = parsed_source_from_url(url)&.repo
90
+ repo&.downcase&.end_with?(normalised_dependency_name)
91
+ end
92
+ end
93
+
94
+ sig { returns(T.nilable(String)) }
95
+ def labelled_source_url_from_project_urls
96
+ source_urls = source_like_project_url_labels.filter_map do |label|
97
+ project_urls[label]
98
+ end
99
+
100
+ source_urls.find { |url| parsed_source_from_url(url) }
101
+ end
102
+
103
+ sig { returns(T.nilable(String)) }
104
+ def fallback_source_url
44
105
  potential_source_urls = [
45
- pypi_listing.dig("info", "project_urls", "Source"),
46
- pypi_listing.dig("info", "project_urls", "Repository"),
47
106
  pypi_listing.dig("info", "home_page"),
48
107
  pypi_listing.dig("info", "download_url"),
49
108
  pypi_listing.dig("info", "docs_url")
50
109
  ].compact
51
110
 
52
- potential_source_urls +=
53
- (pypi_listing.dig("info", "project_urls") || {}).values
111
+ potential_source_urls += project_urls.values
54
112
 
55
- source_url = potential_source_urls.find { |url| Source.from_url(url) }
56
- source_url ||= source_from_description
57
- source_url ||= source_from_homepage
113
+ potential_source_urls.find { |url| parsed_source_from_url(url) }
114
+ end
58
115
 
59
- Source.from_url(source_url)
116
+ sig { returns(T::Hash[String, String]) }
117
+ def project_urls
118
+ pypi_listing.dig("info", "project_urls") || {}
119
+ end
120
+
121
+ sig { returns(T::Array[String]) }
122
+ def source_like_project_url_labels
123
+ ["Source", "Source Code", "Repository", "Code", "Homepage"]
60
124
  end
61
125
 
62
126
  # rubocop:disable Metrics/PerceivedComplexity
@@ -73,7 +137,7 @@ module Dependabot
73
137
  # Looking for a source where the repo name exactly matches the
74
138
  # dependency name
75
139
  match_url = potential_source_urls.find do |url|
76
- repo = Source.from_url(url)&.repo
140
+ repo = parsed_source_from_url(url)&.repo
77
141
  repo&.downcase&.end_with?(normalised_dependency_name)
78
142
  end
79
143
 
@@ -83,11 +147,14 @@ module Dependabot
83
147
  # mentioned when the link is followed
84
148
  @source_from_description ||= T.let(
85
149
  potential_source_urls.find do |url|
86
- full_url = Source.from_url(url)&.url
150
+ full_url = parsed_source_from_url(url)&.url
87
151
  next unless full_url
88
152
 
89
153
  response = Dependabot::RegistryClient.get(url: full_url)
90
- next unless response.status == 200
154
+ unless response.status == 200
155
+ Dependabot.logger.warn("Error fetching source URL #{full_url}: HTTP #{response.status}")
156
+ next
157
+ end
91
158
 
92
159
  response.body.include?(normalised_dependency_name)
93
160
  end,
@@ -108,7 +175,7 @@ module Dependabot
108
175
  end
109
176
 
110
177
  match_url = potential_source_urls.find do |url|
111
- repo = Source.from_url(url)&.repo
178
+ repo = parsed_source_from_url(url)&.repo
112
179
  repo&.downcase&.end_with?(normalised_dependency_name)
113
180
  end
114
181
 
@@ -116,7 +183,7 @@ module Dependabot
116
183
 
117
184
  @source_from_homepage ||= T.let(
118
185
  potential_source_urls.find do |url|
119
- full_url = Source.from_url(url)&.url
186
+ full_url = parsed_source_from_url(url)&.url
120
187
  next unless full_url
121
188
 
122
189
  response = Dependabot::RegistryClient.get(url: full_url)
@@ -143,7 +210,8 @@ module Dependabot
143
210
  begin
144
211
  Dependabot::RegistryClient.get(url: homepage_url)
145
212
  rescue Excon::Error::Timeout, Excon::Error::Socket,
146
- Excon::Error::TooManyRedirects, ArgumentError
213
+ Excon::Error::TooManyRedirects, OpenSSL::SSL::SSLError, ArgumentError => e
214
+ Dependabot.logger.warn("Error fetching Python homepage URL #{homepage_url}: #{e.class}: #{e.message}")
147
215
  nil
148
216
  end,
149
217
  T.nilable(Excon::Response)
@@ -165,9 +233,8 @@ module Dependabot
165
233
 
166
234
  @pypi_listing = JSON.parse(response.body)
167
235
  return @pypi_listing
168
- rescue JSON::ParserError
169
- next
170
- rescue Excon::Error::Timeout
236
+ rescue JSON::ParserError, Excon::Error::Timeout, Excon::Error::Socket, OpenSSL::SSL::SSLError => e
237
+ Dependabot.logger.warn("Error fetching Python package listing from #{url}: #{e.class}: #{e.message}")
171
238
  next
172
239
  end
173
240
 
@@ -194,23 +261,88 @@ module Dependabot
194
261
 
195
262
  sig { returns(T::Array[String]) }
196
263
  def possible_listing_urls
197
- credential_urls =
264
+ index_credentials =
198
265
  credentials
199
266
  .select { |cred| cred["type"] == "python_index" }
200
- .map { |c| AuthedUrlBuilder.authed_url(credential: c) }
201
267
 
202
- (credential_urls + [MAIN_PYPI_URL]).map do |base_url|
268
+ credential_urls = index_credentials
269
+ .map { |c| AuthedUrlBuilder.authed_url(credential: c) }
270
+ .reject { |url| url.strip.empty? }
271
+
272
+ base_urls = if index_credentials.any?(&:replaces_base?)
273
+ credential_urls
274
+ else
275
+ credential_urls + [MAIN_PYPI_URL]
276
+ end
277
+ base_urls = [MAIN_PYPI_URL] if base_urls.empty?
278
+
279
+ base_urls.map do |base_url|
203
280
  # Convert /simple/ endpoints to /pypi/ for JSON API access
204
281
  json_base_url = base_url.sub(%r{/simple/?$}i, "/pypi")
205
282
  json_base_url.gsub(%r{/$}, "") + "/#{normalised_dependency_name}/json"
206
283
  end
207
284
  end
208
285
 
286
+ sig { params(version: String).returns(T.nilable(T::Hash[String, T.untyped])) }
287
+ def ownership_for_version(version)
288
+ if version.include?("+")
289
+ Dependabot.logger.info("Version #{version} includes a local version identifier, skipping ownership check")
290
+ return nil
291
+ end
292
+
293
+ possible_version_listing_urls(version).each do |url|
294
+ response = fetch_authed_url(url)
295
+ unless response.status == 200
296
+ Dependabot.logger.warn(
297
+ "Error fetching Python package ownership from #{url} for version #{version}: " \
298
+ "HTTP #{response.status}"
299
+ )
300
+ next
301
+ end
302
+
303
+ data = JSON.parse(response.body)
304
+ ownership = data["ownership"]
305
+ Dependabot.logger.debug("Found ownership for #{dependency.name} version #{version}")
306
+ return ownership
307
+ rescue JSON::ParserError, Excon::Error::Timeout, Excon::Error::Socket,
308
+ Excon::Error::TooManyRedirects, OpenSSL::SSL::SSLError, ArgumentError => e
309
+ Dependabot.logger.warn(
310
+ "Error fetching Python package ownership from #{url} for version #{version}: #{e.class}: #{e.message}"
311
+ )
312
+ next
313
+ end
314
+
315
+ nil
316
+ end
317
+
318
+ sig { params(version: String).returns(T::Array[String]) }
319
+ def possible_version_listing_urls(version)
320
+ possible_listing_urls.map do |url|
321
+ url.sub(%r{/json$}, "/#{URI::DEFAULT_PARSER.escape(version)}/json")
322
+ end
323
+ end
324
+
325
+ sig { params(ownership: T::Hash[String, T.untyped]).returns(T::Array[String]) }
326
+ def ownership_users(ownership)
327
+ roles = ownership["roles"]
328
+ return [] unless roles.is_a?(Array)
329
+
330
+ roles.filter_map { |role| role["user"] if role.is_a?(Hash) }
331
+ end
332
+
209
333
  # Strip [extras] from name (dependency_name[extra_dep,other_extra])
210
334
  sig { returns(String) }
211
335
  def normalised_dependency_name
212
336
  NameNormaliser.normalise(dependency.name)
213
337
  end
338
+
339
+ sig { params(url: T.nilable(String)).returns(T.nilable(Dependabot::Source)) }
340
+ def parsed_source_from_url(url)
341
+ return unless url
342
+ return @parsed_source_urls[url] if @parsed_source_urls.key?(url)
343
+
344
+ @parsed_source_urls[url] = Source.from_url(url)
345
+ end
214
346
  end
215
347
  end
216
348
  end
@@ -238,7 +238,8 @@ module Dependabot
238
238
  content = file.content
239
239
  return [] if content.nil?
240
240
 
241
- paths = content.scan(CHILD_REQUIREMENT_REGEX).flatten
241
+ paths = content.scan(CHILD_REQUIREMENT_REGEX).flatten +
242
+ content.scan(CONSTRAINT_REGEX).flatten
242
243
  current_dir = File.dirname(file.name)
243
244
 
244
245
  paths.flat_map do |path|
@@ -268,7 +269,8 @@ module Dependabot
268
269
  sig { returns(T::Array[Dependabot::DependencyFile]) }
269
270
  def constraints_files
270
271
  all_requirement_files = requirements_txt_files +
271
- child_requirement_txt_files
272
+ child_requirement_txt_files +
273
+ requirements_in_files
272
274
 
273
275
  constraints_paths = all_requirement_files.map do |req_file|
274
276
  current_dir = File.dirname(req_file.name)
@@ -283,7 +285,10 @@ module Dependabot
283
285
  end
284
286
  end.flatten.uniq
285
287
 
286
- constraints_paths.map { |path| fetch_file_from_host(path) }
288
+ already_fetched_names = child_requirement_files.map(&:name)
289
+ constraints_paths
290
+ .reject { |path| already_fetched_names.include?(path) }
291
+ .map { |path| fetch_file_from_host(path) }
287
292
  end
288
293
 
289
294
  sig { returns(T::Array[Dependabot::DependencyFile]) }
@@ -355,7 +360,7 @@ module Dependabot
355
360
 
356
361
  uneditable_reqs =
357
362
  content
358
- .scan(/(?<name>^['"]?(?:file:)?(?<path>\.[^\[#'"\n]*))/)
363
+ .scan(/(?<name>^['"]?(?:file:)?(?<path>\.[^\[#'"\n;]*))/)
359
364
  .filter_map do |match_array|
360
365
  n, p = match_array
361
366
  { name: n.to_s.strip, path: p.to_s.strip, file: req_file.name } unless p.to_s.include?("://")
@@ -363,7 +368,7 @@ module Dependabot
363
368
 
364
369
  editable_reqs =
365
370
  content
366
- .scan(/(?<name>^-e\s+['"]?(?:file:)?(?<path>[^\[#'"\n]*))/)
371
+ .scan(/(?<name>^-e\s+['"]?(?:file:)?(?<path>[^\[#'"\n;]*))/)
367
372
  .filter_map do |match_array|
368
373
  n, p = match_array
369
374
  unless p.to_s.include?("://") || p.to_s.include?("git@")
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.367.0
4
+ version: 0.368.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.367.0
18
+ version: 0.368.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.367.0
25
+ version: 0.368.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -294,7 +294,7 @@ licenses:
294
294
  - MIT
295
295
  metadata:
296
296
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
297
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.367.0
297
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.368.0
298
298
  rdoc_options: []
299
299
  require_paths:
300
300
  - lib