dependabot-uv 0.300.0 → 0.301.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "cgi"
@@ -12,284 +12,30 @@ require "dependabot/update_checkers/version_filters"
12
12
  require "dependabot/registry_client"
13
13
  require "dependabot/uv/authed_url_builder"
14
14
  require "dependabot/uv/name_normaliser"
15
+ require "dependabot/uv/package/package_registry_finder"
16
+ require "dependabot/uv/package/package_details_fetcher"
17
+ require "dependabot/package/package_latest_version_finder"
15
18
 
16
19
  module Dependabot
17
20
  module Uv
18
21
  class UpdateChecker
19
- class LatestVersionFinder
22
+ class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
20
23
  extend T::Sig
21
24
 
22
- require_relative "index_finder"
23
-
24
- def initialize(dependency:, dependency_files:, credentials:,
25
- ignored_versions:, raise_on_ignored: false,
26
- security_advisories:)
27
- @dependency = dependency
28
- @dependency_files = dependency_files
29
- @credentials = credentials
30
- @ignored_versions = ignored_versions
31
- @raise_on_ignored = raise_on_ignored
32
- @security_advisories = security_advisories
33
- end
34
-
35
- def latest_version(python_version: nil)
36
- @latest_version ||=
37
- fetch_latest_version(python_version: python_version)
38
- end
39
-
40
- def latest_version_with_no_unlock(python_version: nil)
41
- @latest_version_with_no_unlock ||=
42
- fetch_latest_version_with_no_unlock(python_version: python_version)
43
- end
44
-
45
- def lowest_security_fix_version(python_version: nil)
46
- @lowest_security_fix_version ||=
47
- fetch_lowest_security_fix_version(python_version: python_version)
48
- end
49
-
50
- private
51
-
52
- attr_reader :dependency
53
- attr_reader :dependency_files
54
- attr_reader :credentials
55
- attr_reader :ignored_versions
56
- attr_reader :security_advisories
57
-
58
- def fetch_latest_version(python_version:)
59
- versions = available_versions
60
- versions = filter_yanked_versions(versions)
61
- versions = filter_unsupported_versions(versions, python_version)
62
- versions = filter_prerelease_versions(versions)
63
- versions = filter_ignored_versions(versions)
64
- versions.max
65
- end
66
-
67
- def fetch_latest_version_with_no_unlock(python_version:)
68
- versions = available_versions
69
- versions = filter_yanked_versions(versions)
70
- versions = filter_unsupported_versions(versions, python_version)
71
- versions = filter_prerelease_versions(versions)
72
- versions = filter_ignored_versions(versions)
73
- versions = filter_out_of_range_versions(versions)
74
- versions.max
75
- end
76
-
77
- def fetch_lowest_security_fix_version(python_version:)
78
- versions = available_versions
79
- versions = filter_yanked_versions(versions)
80
- versions = filter_unsupported_versions(versions, python_version)
81
- versions = filter_prerelease_versions(versions)
82
- versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(versions,
83
- security_advisories)
84
- versions = filter_ignored_versions(versions)
85
- versions = filter_lower_versions(versions)
86
-
87
- versions.min
88
- end
89
-
90
- sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
91
- def filter_yanked_versions(versions_array)
92
- filtered = versions_array.reject { |details| details.fetch(:yanked) }
93
- if versions_array.count > filtered.count
94
- Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} yanked versions")
95
- end
96
- filtered
97
- end
98
-
99
25
  sig do
100
- params(versions_array: T::Array[T.untyped], python_version: T.nilable(T.any(String, Version)))
101
- .returns(T::Array[T.untyped])
102
- end
103
- def filter_unsupported_versions(versions_array, python_version)
104
- filtered = versions_array.filter_map do |details|
105
- python_requirement = details.fetch(:python_requirement)
106
- next details.fetch(:version) unless python_version
107
- next details.fetch(:version) unless python_requirement
108
- next unless python_requirement.satisfied_by?(python_version)
109
-
110
- details.fetch(:version)
111
- end
112
- if versions_array.count > filtered.count
113
- delta = versions_array.count - filtered.count
114
- Dependabot.logger.info("Filtered out #{delta} unsupported Python #{python_version} versions")
115
- end
116
- filtered
117
- end
118
-
119
- sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
120
- def filter_prerelease_versions(versions_array)
121
- return versions_array if wants_prerelease?
122
-
123
- filtered = versions_array.reject(&:prerelease?)
124
-
125
- if versions_array.count > filtered.count
126
- Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} pre-release versions")
127
- end
128
-
129
- filtered
130
- end
131
-
132
- sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
133
- def filter_ignored_versions(versions_array)
134
- filtered = versions_array
135
- .reject { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
136
- if @raise_on_ignored && filter_lower_versions(filtered).empty? && filter_lower_versions(versions_array).any?
137
- raise Dependabot::AllVersionsIgnored
138
- end
139
-
140
- if versions_array.count > filtered.count
141
- Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} ignored versions")
142
- end
143
- filtered
144
- end
145
-
146
- def filter_lower_versions(versions_array)
147
- return versions_array unless dependency.numeric_version
148
-
149
- versions_array.select { |version| version > dependency.numeric_version }
150
- end
151
-
152
- def filter_out_of_range_versions(versions_array)
153
- reqs = dependency.requirements.filter_map do |r|
154
- requirement_class.requirements_array(r.fetch(:requirement))
155
- end
156
-
157
- versions_array
158
- .select { |v| reqs.all? { |r| r.any? { |o| o.satisfied_by?(v) } } }
159
- end
160
-
161
- def wants_prerelease?
162
- return version_class.new(dependency.version).prerelease? if dependency.version
163
-
164
- dependency.requirements.any? do |req|
165
- reqs = (req.fetch(:requirement) || "").split(",").map(&:strip)
166
- reqs.any? { |r| r.match?(/[A-Za-z]/) }
167
- end
168
- end
169
-
170
- # See https://www.python.org/dev/peps/pep-0503/ for details of the
171
- # Simple Repository API we use here.
172
- def available_versions
173
- @available_versions ||=
174
- index_urls.flat_map do |index_url|
175
- validate_index(index_url)
176
-
177
- sanitized_url = index_url.gsub(%r{(?<=//).*(?=@)}, "redacted")
178
-
179
- index_response = registry_response_for_dependency(index_url)
180
- if index_response.status == 401 || index_response.status == 403
181
- registry_index_response = registry_index_response(index_url)
182
-
183
- if registry_index_response.status == 401 || registry_index_response.status == 403
184
- raise PrivateSourceAuthenticationFailure, sanitized_url
185
- end
186
- end
187
-
188
- version_links = []
189
- index_response.body.scan(%r{<a\s.*?>.*?</a>}m) do
190
- details = version_details_from_link(Regexp.last_match.to_s)
191
- version_links << details if details
192
- end
193
-
194
- version_links.compact
195
- rescue Excon::Error::Timeout, Excon::Error::Socket
196
- raise if MAIN_PYPI_INDEXES.include?(index_url)
197
-
198
- raise PrivateSourceTimedOut, sanitized_url
199
- rescue URI::InvalidURIError
200
- raise DependencyFileNotResolvable, "Invalid URL: #{sanitized_url}"
201
- end
26
+ override.returns(T.nilable(Dependabot::Package::PackageDetails))
202
27
  end
203
-
204
- # rubocop:disable Metrics/PerceivedComplexity
205
- def version_details_from_link(link)
206
- doc = Nokogiri::XML(link)
207
- filename = doc.at_css("a")&.content
208
- url = doc.at_css("a")&.attributes&.fetch("href", nil)&.value
209
- return unless filename&.match?(name_regex) || url&.match?(name_regex)
210
-
211
- version = get_version_from_filename(filename)
212
- return unless version_class.correct?(version)
213
-
214
- {
215
- version: version_class.new(version),
216
- python_requirement: build_python_requirement_from_link(link),
217
- yanked: link&.include?("data-yanked")
218
- }
219
- end
220
- # rubocop:enable Metrics/PerceivedComplexity
221
-
222
- def get_version_from_filename(filename)
223
- filename
224
- .gsub(/#{name_regex}-/i, "")
225
- .split(/-|\.tar\.|\.zip|\.whl/)
226
- .first
227
- end
228
-
229
- def build_python_requirement_from_link(link)
230
- req_string = Nokogiri::XML(link)
231
- .at_css("a")
232
- &.attribute("data-requires-python")
233
- &.content
234
-
235
- return unless req_string
236
-
237
- requirement_class.new(CGI.unescapeHTML(req_string))
238
- rescue Gem::Requirement::BadRequirementError
239
- nil
28
+ def package_details
29
+ @package_details ||= Package::PackageDetailsFetcher.new(
30
+ dependency: dependency,
31
+ dependency_files: dependency_files,
32
+ credentials: credentials
33
+ ).fetch
240
34
  end
241
35
 
242
- def index_urls
243
- @index_urls ||=
244
- IndexFinder.new(
245
- dependency_files: dependency_files,
246
- credentials: credentials,
247
- dependency: dependency
248
- ).index_urls
249
- end
250
-
251
- def registry_response_for_dependency(index_url)
252
- Dependabot::RegistryClient.get(
253
- url: index_url + normalised_name + "/",
254
- headers: { "Accept" => "text/html" }
255
- )
256
- end
257
-
258
- def registry_index_response(index_url)
259
- Dependabot::RegistryClient.get(
260
- url: index_url,
261
- headers: { "Accept" => "text/html" }
262
- )
263
- end
264
-
265
- def ignore_requirements
266
- ignored_versions.flat_map { |req| requirement_class.requirements_array(req) }
267
- end
268
-
269
- def normalised_name
270
- NameNormaliser.normalise(dependency.name)
271
- end
272
-
273
- def name_regex
274
- parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
275
- /#{parts.join("[\s_.-]")}/i
276
- end
277
-
278
- def version_class
279
- dependency.version_class
280
- end
281
-
282
- def requirement_class
283
- dependency.requirement_class
284
- end
285
-
286
- def validate_index(index_url)
287
- sanitized_url = index_url.gsub(%r{(?<=//).*(?=@)}, "redacted")
288
-
289
- return if index_url&.match?(URI::DEFAULT_PARSER.regexp[:ABS_URI])
290
-
291
- raise Dependabot::DependencyFileNotResolvable,
292
- "Invalid URL: #{sanitized_url}"
36
+ sig { override.returns(T::Boolean) }
37
+ def cooldown_enabled?
38
+ Dependabot::Experiments.enabled?(:enable_cooldown_for_uv)
293
39
  end
294
40
  end
295
41
  end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/uv/version"
5
+ require "dependabot/uv/requirement"
6
+ require "dependabot/uv/update_checker"
7
+
8
+ module Dependabot
9
+ module Uv
10
+ class UpdateChecker
11
+ class LockFileResolver
12
+ def initialize(dependency:, dependency_files:, credentials:, repo_contents_path: nil)
13
+ @dependency = dependency
14
+ @dependency_files = dependency_files
15
+ @credentials = credentials
16
+ @repo_contents_path = repo_contents_path
17
+ end
18
+
19
+ def latest_resolvable_version(requirement:)
20
+ return nil unless requirement
21
+
22
+ req = Uv::Requirement.new(requirement)
23
+
24
+ # Get the version from the dependency if available
25
+ version_from_dependency = dependency.version && Uv::Version.new(dependency.version)
26
+ return version_from_dependency if version_from_dependency && req.satisfied_by?(version_from_dependency)
27
+
28
+ nil
29
+ end
30
+
31
+ def resolvable?(*)
32
+ true
33
+ end
34
+
35
+ def lowest_resolvable_security_fix_version
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :dependency
42
+ attr_reader :dependency_files
43
+ attr_reader :credentials
44
+ attr_reader :repo_contents_path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -11,28 +11,29 @@ module Dependabot
11
11
  class UpdateChecker
12
12
  class PipVersionResolver
13
13
  def initialize(dependency:, dependency_files:, credentials:,
14
- ignored_versions:, raise_on_ignored: false,
14
+ ignored_versions:, update_cooldown: nil, raise_on_ignored: false,
15
15
  security_advisories:)
16
- @dependency = dependency
16
+ @dependency = dependency
17
17
  @dependency_files = dependency_files
18
18
  @credentials = credentials
19
19
  @ignored_versions = ignored_versions
20
+ @update_cooldown = update_cooldown
20
21
  @raise_on_ignored = raise_on_ignored
21
22
  @security_advisories = security_advisories
22
23
  end
23
24
 
24
25
  def latest_resolvable_version
25
- latest_version_finder.latest_version(python_version: language_version_manager.python_version)
26
+ latest_version_finder.latest_version(language_version: language_version_manager.python_version)
26
27
  end
27
28
 
28
29
  def latest_resolvable_version_with_no_unlock
29
30
  latest_version_finder
30
- .latest_version_with_no_unlock(python_version: language_version_manager.python_version)
31
+ .latest_version_with_no_unlock(language_version: language_version_manager.python_version)
31
32
  end
32
33
 
33
34
  def lowest_resolvable_security_fix_version
34
35
  latest_version_finder
35
- .lowest_security_fix_version(python_version: language_version_manager.python_version)
36
+ .lowest_security_fix_version(language_version: language_version_manager.python_version)
36
37
  end
37
38
 
38
39
  private
@@ -50,6 +51,7 @@ module Dependabot
50
51
  credentials: credentials,
51
52
  ignored_versions: ignored_versions,
52
53
  raise_on_ignored: @raise_on_ignored,
54
+ cooldown_options: @update_cooldown,
53
55
  security_advisories: security_advisories
54
56
  )
55
57
  end
@@ -21,6 +21,7 @@ module Dependabot
21
21
  require_relative "update_checker/pip_version_resolver"
22
22
  require_relative "update_checker/requirements_updater"
23
23
  require_relative "update_checker/latest_version_finder"
24
+ require_relative "update_checker/lock_file_resolver"
24
25
 
25
26
  MAIN_PYPI_INDEXES = %w(
26
27
  https://pypi.python.org/simple/
@@ -118,6 +119,7 @@ module Dependabot
118
119
  case resolver_type
119
120
  when :pip_compile then pip_compile_version_resolver
120
121
  when :requirements then pip_version_resolver
122
+ when :lock_file then lock_file_resolver
121
123
  else raise "Unexpected resolver type #{resolver_type}"
122
124
  end
123
125
  end
@@ -134,6 +136,7 @@ module Dependabot
134
136
  # which resolver to use based on the filename of its requirements
135
137
  return :requirements if updating_pyproject?
136
138
  return :pip_compile if updating_in_file?
139
+ return :lock_file if updating_uv_lock?
137
140
 
138
141
  if dependency.version && !exact_requirement?(reqs)
139
142
  subdependency_resolver
@@ -167,10 +170,15 @@ module Dependabot
167
170
  credentials: credentials,
168
171
  ignored_versions: ignored_versions,
169
172
  raise_on_ignored: @raise_on_ignored,
173
+ update_cooldown: @update_cooldown,
170
174
  security_advisories: security_advisories
171
175
  )
172
176
  end
173
177
 
178
+ def lock_file_resolver
179
+ @lock_file_resolver ||= LockFileResolver.new(**resolver_args)
180
+ end
181
+
174
182
  def resolver_args
175
183
  {
176
184
  dependency: dependency,
@@ -187,7 +195,7 @@ module Dependabot
187
195
  requirement = reqs.find do |r|
188
196
  file = r[:file]
189
197
 
190
- file == "Pipfile" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt")
198
+ file == "uv.lock" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt")
191
199
  end
192
200
 
193
201
  requirement&.fetch(:requirement)
@@ -234,6 +242,7 @@ module Dependabot
234
242
  credentials: credentials,
235
243
  ignored_versions: ignored_versions,
236
244
  raise_on_ignored: @raise_on_ignored,
245
+ cooldown_options: @update_cooldown,
237
246
  security_advisories: security_advisories
238
247
  )
239
248
  end
@@ -267,6 +276,10 @@ module Dependabot
267
276
  requirement_files.any? { |f| f.end_with?(".in") }
268
277
  end
269
278
 
279
+ def updating_uv_lock?
280
+ requirement_files.any?("uv.lock")
281
+ end
282
+
270
283
  def requirements_text_file?
271
284
  requirement_files.any? { |f| f.end_with?("requirements.txt") }
272
285
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-uv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.300.0
4
+ version: 0.301.1
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-06 00:00:00.000000000 Z
11
+ date: 2025-03-14 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.300.0
19
+ version: 0.301.1
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.300.0
26
+ version: 0.301.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -257,6 +257,7 @@ files:
257
257
  - lib/dependabot/uv/file_parser/setup_file_parser.rb
258
258
  - lib/dependabot/uv/file_updater.rb
259
259
  - lib/dependabot/uv/file_updater/compile_file_updater.rb
260
+ - lib/dependabot/uv/file_updater/lock_file_updater.rb
260
261
  - lib/dependabot/uv/file_updater/pyproject_preparer.rb
261
262
  - lib/dependabot/uv/file_updater/requirement_file_updater.rb
262
263
  - lib/dependabot/uv/file_updater/requirement_replacer.rb
@@ -265,14 +266,16 @@ files:
265
266
  - lib/dependabot/uv/metadata_finder.rb
266
267
  - lib/dependabot/uv/name_normaliser.rb
267
268
  - lib/dependabot/uv/native_helpers.rb
269
+ - lib/dependabot/uv/package/package_details_fetcher.rb
270
+ - lib/dependabot/uv/package/package_registry_finder.rb
268
271
  - lib/dependabot/uv/package_manager.rb
269
272
  - lib/dependabot/uv/pipenv_runner.rb
270
273
  - lib/dependabot/uv/requirement.rb
271
274
  - lib/dependabot/uv/requirement_parser.rb
272
275
  - lib/dependabot/uv/requirements_file_matcher.rb
273
276
  - lib/dependabot/uv/update_checker.rb
274
- - lib/dependabot/uv/update_checker/index_finder.rb
275
277
  - lib/dependabot/uv/update_checker/latest_version_finder.rb
278
+ - lib/dependabot/uv/update_checker/lock_file_resolver.rb
276
279
  - lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb
277
280
  - lib/dependabot/uv/update_checker/pip_version_resolver.rb
278
281
  - lib/dependabot/uv/update_checker/requirements_updater.rb
@@ -282,7 +285,7 @@ licenses:
282
285
  - MIT
283
286
  metadata:
284
287
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
285
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.300.0
288
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.301.1
286
289
  post_install_message:
287
290
  rdoc_options: []
288
291
  require_paths: