dependabot-python 0.367.0 → 0.369.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +4 -0
  3. data/helpers/requirements.txt +1 -0
  4. data/helpers/test/fixtures/no_dependencies.toml +3 -0
  5. data/helpers/test/fixtures/pep621_arbitrary_equality.toml +7 -0
  6. data/helpers/test/fixtures/pep621_dependencies.toml +21 -0
  7. data/helpers/test/fixtures/pep621_empty_deps.toml +8 -0
  8. data/helpers/test/fixtures/pep621_extras.toml +8 -0
  9. data/helpers/test/fixtures/pep621_markers.toml +7 -0
  10. data/helpers/test/fixtures/pep621_multiple_extras.toml +7 -0
  11. data/helpers/test/fixtures/pep621_no_version.toml +8 -0
  12. data/helpers/test/fixtures/pep621_only_build_system.toml +3 -0
  13. data/helpers/test/fixtures/pep735_cycle.toml +13 -0
  14. data/helpers/test/fixtures/pep735_dependency_groups.toml +18 -0
  15. data/helpers/test/fixtures/requirements/constraints.txt +1 -0
  16. data/helpers/test/fixtures/requirements/markers.txt +1 -0
  17. data/helpers/test/fixtures/requirements/requirements-dev.txt +2 -0
  18. data/helpers/test/fixtures/requirements/requirements.txt +5 -0
  19. data/helpers/test/fixtures/requirements/with_constraints.txt +2 -0
  20. data/helpers/test/fixtures/requirements_empty/.gitkeep +0 -0
  21. data/helpers/test/fixtures/setup_cfg/setup.cfg +16 -0
  22. data/helpers/test/fixtures/setup_py/setup.py +20 -0
  23. data/helpers/test/fixtures/setup_py_comments/setup.py +9 -0
  24. data/helpers/test/test_hasher.py +114 -0
  25. data/helpers/test/test_parse_requirements.py +103 -0
  26. data/helpers/test/test_parse_setup.py +127 -0
  27. data/helpers/test/test_parser.py +184 -0
  28. data/helpers/test/test_run.py +49 -0
  29. data/lib/dependabot/python/file_updater/poetry_file_updater.rb +16 -11
  30. data/lib/dependabot/python/file_updater/pyproject_preparer.rb +109 -0
  31. data/lib/dependabot/python/file_updater/requirement_file_updater.rb +36 -13
  32. data/lib/dependabot/python/file_updater/requirement_replacer.rb +29 -21
  33. data/lib/dependabot/python/metadata_finder.rb +152 -20
  34. data/lib/dependabot/python/shared_file_fetcher.rb +10 -5
  35. data/lib/dependabot/python/update_checker/latest_version_finder.rb +4 -2
  36. metadata +29 -4
@@ -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@")
@@ -9,6 +9,7 @@ require "sorbet-runtime"
9
9
  require "dependabot/dependency"
10
10
  require "dependabot/git_commit_checker"
11
11
  require "dependabot/python/update_checker"
12
+ require "dependabot/update_checkers/cooldown_calculation"
12
13
  require "dependabot/update_checkers/version_filters"
13
14
  require "dependabot/registry_client"
14
15
  require "dependabot/python/authed_url_builder"
@@ -110,8 +111,9 @@ module Dependabot
110
111
  new_version = version_class.new(tag_version_str)
111
112
  days = cooldown_days_for(current_version, new_version)
112
113
 
113
- passed_seconds = Time.now.to_i - release_date_to_seconds(tag_with_detail.release_date)
114
- passed_seconds < days * DAY_IN_SECONDS
114
+ release_time = Time.at(release_date_to_seconds(tag_with_detail.release_date))
115
+ Dependabot::UpdateCheckers::CooldownCalculation
116
+ .within_cooldown_window?(release_time, days)
115
117
  end
116
118
 
117
119
  sig do
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.369.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.369.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.369.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -247,6 +247,31 @@ files:
247
247
  - helpers/lib/parser.py
248
248
  - helpers/requirements.txt
249
249
  - helpers/run.py
250
+ - helpers/test/fixtures/no_dependencies.toml
251
+ - helpers/test/fixtures/pep621_arbitrary_equality.toml
252
+ - helpers/test/fixtures/pep621_dependencies.toml
253
+ - helpers/test/fixtures/pep621_empty_deps.toml
254
+ - helpers/test/fixtures/pep621_extras.toml
255
+ - helpers/test/fixtures/pep621_markers.toml
256
+ - helpers/test/fixtures/pep621_multiple_extras.toml
257
+ - helpers/test/fixtures/pep621_no_version.toml
258
+ - helpers/test/fixtures/pep621_only_build_system.toml
259
+ - helpers/test/fixtures/pep735_cycle.toml
260
+ - helpers/test/fixtures/pep735_dependency_groups.toml
261
+ - helpers/test/fixtures/requirements/constraints.txt
262
+ - helpers/test/fixtures/requirements/markers.txt
263
+ - helpers/test/fixtures/requirements/requirements-dev.txt
264
+ - helpers/test/fixtures/requirements/requirements.txt
265
+ - helpers/test/fixtures/requirements/with_constraints.txt
266
+ - helpers/test/fixtures/requirements_empty/.gitkeep
267
+ - helpers/test/fixtures/setup_cfg/setup.cfg
268
+ - helpers/test/fixtures/setup_py/setup.py
269
+ - helpers/test/fixtures/setup_py_comments/setup.py
270
+ - helpers/test/test_hasher.py
271
+ - helpers/test/test_parse_requirements.py
272
+ - helpers/test/test_parse_setup.py
273
+ - helpers/test/test_parser.py
274
+ - helpers/test/test_run.py
250
275
  - lib/dependabot/python.rb
251
276
  - lib/dependabot/python/authed_url_builder.rb
252
277
  - lib/dependabot/python/dependency_grapher.rb
@@ -294,7 +319,7 @@ licenses:
294
319
  - MIT
295
320
  metadata:
296
321
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
297
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.367.0
322
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.369.0
298
323
  rdoc_options: []
299
324
  require_paths:
300
325
  - lib