dependabot-uv 0.301.0 → 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 +4 -4
- data/helpers/requirements.txt +1 -1
- data/lib/dependabot/uv/file_updater/lock_file_updater.rb +46 -124
- data/lib/dependabot/uv/package/package_details_fetcher.rb +488 -0
- data/lib/dependabot/uv/{update_checker/index_finder.rb → package/package_registry_finder.rb} +12 -6
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +15 -269
- data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +7 -5
- data/lib/dependabot/uv/update_checker.rb +2 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13de7d80edd84c1e0f706821592cd4f756a2cbc418f16b33f3af2eb58be41942
|
4
|
+
data.tar.gz: 5e0e922260aaa77e3041abb3103501c86be7a24b57d6f1fa25a5e782ec7b5a92
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03fac6d585a7f26b470dda56264b85f3e078b29c561e37a5df3c118298ff2c8a2fe0eb7751efc2a9c127bd6029eed0697a9f2b7dedf0a0f45fb23e597209f346
|
7
|
+
data.tar.gz: 2c1419e9e906e4b7b198f95ae55f032a05ee2fab815672d0c8d80e99edc1923aecbfe02506c73892b058277664bdfaa23c92c77580c2278b6cdd1bc76cda001d
|
data/helpers/requirements.txt
CHANGED
@@ -104,48 +104,36 @@ module Dependabot
|
|
104
104
|
original_requires_python = original_content
|
105
105
|
.match(/requires-python\s*=\s*["']([^"']+)["']/)&.captures&.first
|
106
106
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
result = original_content.sub(dependency_section_pattern) do
|
123
|
-
section_start = Regexp.last_match(1)
|
124
|
-
version_line = "version = \"#{dependency.version}\""
|
125
|
-
section_end = Regexp.last_match(3)
|
126
|
-
next_section_or_end = Regexp.last_match(4)
|
127
|
-
|
128
|
-
"#{section_start}#{version_line}#{section_end}#{next_section_or_end}"
|
129
|
-
end
|
130
|
-
|
131
|
-
# If the content didn't change and we expect it to, something went wrong
|
132
|
-
if result == original_content
|
133
|
-
Dependabot.logger.warn("Package section not found for #{dependency.name}, falling back to raw update")
|
134
|
-
result = new_lockfile
|
135
|
-
end
|
136
|
-
|
137
|
-
# Restore the original requires-python if it exists
|
138
|
-
if original_requires_python
|
139
|
-
result = result.gsub(/requires-python\s*=\s*["'][^"']+["']/,
|
140
|
-
"requires-python = \"#{original_requires_python}\"")
|
141
|
-
end
|
142
|
-
|
143
|
-
result
|
107
|
+
# Store the original Python version requirement for later use
|
108
|
+
@original_python_version = original_requires_python
|
109
|
+
|
110
|
+
new_lockfile = updated_lockfile_content_for(prepared_pyproject)
|
111
|
+
|
112
|
+
# Normalize line endings to ensure proper comparison
|
113
|
+
new_lockfile = normalize_line_endings(new_lockfile, original_content)
|
114
|
+
|
115
|
+
result = new_lockfile
|
116
|
+
|
117
|
+
# Restore the original requires-python if it exists
|
118
|
+
if original_requires_python
|
119
|
+
result = result.gsub(/requires-python\s*=\s*["'][^"']+["']/,
|
120
|
+
"requires-python = \"#{original_requires_python}\"")
|
144
121
|
end
|
122
|
+
|
123
|
+
result
|
145
124
|
end
|
146
125
|
end
|
147
126
|
|
148
|
-
# Helper method to
|
127
|
+
# Helper method to normalize line endings between two strings
|
128
|
+
def normalize_line_endings(content, reference)
|
129
|
+
# Check if reference has escaped newlines like "\n" +
|
130
|
+
if reference.include?("\\n")
|
131
|
+
content.gsub("\n", "\\n")
|
132
|
+
else
|
133
|
+
content
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
149
137
|
def with_original_python_version(original_requires_python)
|
150
138
|
if original_requires_python
|
151
139
|
original_python_version = @original_python_version
|
@@ -164,7 +152,6 @@ module Dependabot
|
|
164
152
|
content = updated_pyproject_content
|
165
153
|
content = sanitize(content)
|
166
154
|
content = freeze_other_dependencies(content)
|
167
|
-
content = update_python_requirement(content)
|
168
155
|
content
|
169
156
|
end
|
170
157
|
end
|
@@ -175,12 +162,6 @@ module Dependabot
|
|
175
162
|
.freeze_top_level_dependencies_except(dependencies)
|
176
163
|
end
|
177
164
|
|
178
|
-
def update_python_requirement(pyproject_content)
|
179
|
-
PyprojectPreparer
|
180
|
-
.new(pyproject_content: pyproject_content)
|
181
|
-
.update_python_requirement(language_version_manager.python_version)
|
182
|
-
end
|
183
|
-
|
184
165
|
def sanitize(pyproject_content)
|
185
166
|
PyprojectPreparer
|
186
167
|
.new(pyproject_content: pyproject_content)
|
@@ -192,14 +173,8 @@ module Dependabot
|
|
192
173
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
193
174
|
write_temporary_dependency_files(pyproject_content)
|
194
175
|
|
195
|
-
#
|
196
|
-
|
197
|
-
|
198
|
-
# Determine the Python version to use after installation
|
199
|
-
python_version = determine_python_version
|
200
|
-
|
201
|
-
# Now write the .python-version file with a version we know is installed
|
202
|
-
File.write(".python-version", python_version)
|
176
|
+
# Set up Python environment using LanguageVersionManager
|
177
|
+
setup_python_environment
|
203
178
|
|
204
179
|
run_update_command
|
205
180
|
|
@@ -209,8 +184,9 @@ module Dependabot
|
|
209
184
|
end
|
210
185
|
|
211
186
|
def run_update_command
|
212
|
-
|
213
|
-
|
187
|
+
# Use pyenv exec to ensure we're using the correct Python environment
|
188
|
+
command = "pyenv exec python -m uv lock --upgrade-package #{dependency.name}"
|
189
|
+
fingerprint = "pyenv exec python -m uv lock --upgrade-package <dependency_name>"
|
214
190
|
|
215
191
|
run_command(command, fingerprint:)
|
216
192
|
end
|
@@ -226,82 +202,28 @@ module Dependabot
|
|
226
202
|
File.write(path, file.content)
|
227
203
|
end
|
228
204
|
|
229
|
-
# Only write the .python-version file after the language version manager has
|
230
|
-
# installed the required Python version to ensure it's available
|
231
205
|
# Overwrite the pyproject with updated content
|
232
206
|
File.write("pyproject.toml", pyproject_content)
|
233
207
|
end
|
234
208
|
|
235
|
-
def
|
236
|
-
#
|
237
|
-
|
238
|
-
begin
|
239
|
-
available_versions = SharedHelpers.run_shell_command("pyenv versions --bare")
|
240
|
-
.split("\n")
|
241
|
-
.map(&:strip)
|
242
|
-
.reject(&:empty?)
|
243
|
-
rescue StandardError => e
|
244
|
-
Dependabot.logger.warn("Error checking available Python versions: #{e}")
|
245
|
-
end
|
246
|
-
|
247
|
-
# Try to find the closest match for our priority order
|
248
|
-
preferred_version = find_preferred_version(available_versions)
|
209
|
+
def setup_python_environment
|
210
|
+
# Use LanguageVersionManager to determine and install the appropriate Python version
|
211
|
+
Dependabot.logger.info("Setting up Python environment using LanguageVersionManager")
|
249
212
|
|
250
|
-
|
251
|
-
#
|
252
|
-
|
253
|
-
else
|
254
|
-
# If all else fails, use "system" which should work with whatever Python is available
|
255
|
-
"system"
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def find_preferred_version(available_versions)
|
260
|
-
return nil unless available_versions&.any?
|
261
|
-
|
262
|
-
# Try each strategy in order of preference
|
263
|
-
try_version_from_file(available_versions) ||
|
264
|
-
try_version_from_requires_python(available_versions) ||
|
265
|
-
try_highest_python3_version(available_versions)
|
266
|
-
end
|
267
|
-
|
268
|
-
def try_version_from_file(available_versions)
|
269
|
-
python_version_file = dependency_files.find { |f| f.name == ".python-version" }
|
270
|
-
return nil unless python_version_file && !python_version_file.content.strip.empty?
|
271
|
-
|
272
|
-
requested_version = python_version_file.content.strip
|
273
|
-
return requested_version if version_available?(available_versions, requested_version)
|
274
|
-
|
275
|
-
Dependabot.logger.info("Python version #{requested_version} from .python-version not available")
|
276
|
-
nil
|
277
|
-
end
|
278
|
-
|
279
|
-
def try_version_from_requires_python(available_versions)
|
280
|
-
return nil unless @original_python_version
|
281
|
-
|
282
|
-
version_match = @original_python_version.match(/(\d+\.\d+)/)
|
283
|
-
return nil unless version_match
|
284
|
-
|
285
|
-
requested_version = version_match[1]
|
286
|
-
return requested_version if version_available?(available_versions, requested_version)
|
287
|
-
|
288
|
-
Dependabot.logger.info("Python version #{requested_version} from requires-python not available")
|
289
|
-
nil
|
290
|
-
end
|
291
|
-
|
292
|
-
def try_highest_python3_version(available_versions)
|
293
|
-
python3_versions = available_versions
|
294
|
-
.select { |v| v.match(/^3\.\d+/) }
|
295
|
-
.sort_by { |v| Gem::Version.new(v.match(/^(\d+\.\d+)/)[1]) }
|
296
|
-
.reverse
|
213
|
+
begin
|
214
|
+
# Install the required Python version
|
215
|
+
language_version_manager.install_required_python
|
297
216
|
|
298
|
-
|
299
|
-
|
217
|
+
# Set the local Python version
|
218
|
+
python_version = language_version_manager.python_version
|
219
|
+
Dependabot.logger.info("Setting Python version to #{python_version}")
|
220
|
+
SharedHelpers.run_shell_command("pyenv local #{language_version_manager.python_major_minor}")
|
300
221
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
222
|
+
# We don't need to install uv as it should be available in the Docker environment
|
223
|
+
Dependabot.logger.info("Using pre-installed uv package")
|
224
|
+
rescue StandardError => e
|
225
|
+
Dependabot.logger.warn("Error setting up Python environment: #{e.message}")
|
226
|
+
Dependabot.logger.info("Falling back to system Python")
|
305
227
|
end
|
306
228
|
end
|
307
229
|
|
@@ -0,0 +1,488 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
require "cgi"
|
7
|
+
require "excon"
|
8
|
+
require "nokogiri"
|
9
|
+
require "sorbet-runtime"
|
10
|
+
require "dependabot/registry_client"
|
11
|
+
require "dependabot/uv/name_normaliser"
|
12
|
+
require "dependabot/package/package_release"
|
13
|
+
require "dependabot/package/package_details"
|
14
|
+
require "dependabot/uv/package/package_registry_finder"
|
15
|
+
|
16
|
+
# Stores metadata for a package, including all its available versions
|
17
|
+
module Dependabot
|
18
|
+
module Uv
|
19
|
+
module Package
|
20
|
+
CREDENTIALS_USERNAME = "username"
|
21
|
+
CREDENTIALS_PASSWORD = "password"
|
22
|
+
|
23
|
+
APPLICATION_JSON = "application/json"
|
24
|
+
APPLICATION_TEXT = "text/html"
|
25
|
+
CPYTHON = "cpython"
|
26
|
+
PYTHON = "python"
|
27
|
+
UNKNOWN = "unknown"
|
28
|
+
|
29
|
+
MAIN_PYPI_INDEXES = %w(
|
30
|
+
https://pypi.python.org/simple/
|
31
|
+
https://pypi.org/simple/
|
32
|
+
).freeze
|
33
|
+
VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
|
34
|
+
|
35
|
+
class PackageDetailsFetcher
|
36
|
+
extend T::Sig
|
37
|
+
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
dependency: Dependabot::Dependency,
|
41
|
+
dependency_files: T::Array[Dependabot::DependencyFile],
|
42
|
+
credentials: T::Array[Dependabot::Credential]
|
43
|
+
).void
|
44
|
+
end
|
45
|
+
def initialize(
|
46
|
+
dependency:,
|
47
|
+
dependency_files:,
|
48
|
+
credentials:
|
49
|
+
)
|
50
|
+
@dependency = dependency
|
51
|
+
@dependency_files = dependency_files
|
52
|
+
@credentials = credentials
|
53
|
+
|
54
|
+
@registry_urls = T.let(nil, T.nilable(T::Array[String]))
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { returns(Dependabot::Dependency) }
|
58
|
+
attr_reader :dependency
|
59
|
+
|
60
|
+
sig { returns(T::Array[T.untyped]) }
|
61
|
+
attr_reader :dependency_files
|
62
|
+
|
63
|
+
sig { returns(T::Array[T.untyped]) }
|
64
|
+
attr_reader :credentials
|
65
|
+
|
66
|
+
sig { returns(Dependabot::Package::PackageDetails) }
|
67
|
+
def fetch
|
68
|
+
package_releases = registry_urls
|
69
|
+
.select { |index_url| validate_index(index_url) } # Ensure only valid URLs
|
70
|
+
.flat_map do |index_url|
|
71
|
+
fetch_from_registry(index_url) || [] # Ensure it always returns an array
|
72
|
+
rescue Excon::Error::Timeout, Excon::Error::Socket
|
73
|
+
raise if MAIN_PYPI_INDEXES.include?(index_url)
|
74
|
+
|
75
|
+
raise PrivateSourceTimedOut, sanitized_url(index_url)
|
76
|
+
rescue URI::InvalidURIError
|
77
|
+
raise DependencyFileNotResolvable, "Invalid URL: #{sanitized_url(index_url)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
Dependabot::Package::PackageDetails.new(
|
81
|
+
dependency: dependency,
|
82
|
+
releases: package_releases.reverse.uniq(&:version)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(index_url: String)
|
90
|
+
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
91
|
+
end
|
92
|
+
def fetch_from_registry(index_url)
|
93
|
+
if Dependabot::Experiments.enabled?(:enable_cooldown_for_uv)
|
94
|
+
metadata = fetch_from_json_registry(index_url)
|
95
|
+
|
96
|
+
return metadata if metadata&.any?
|
97
|
+
|
98
|
+
Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
|
99
|
+
end
|
100
|
+
fetch_from_html_registry(index_url)
|
101
|
+
rescue StandardError => e
|
102
|
+
Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
|
103
|
+
fetch_from_html_registry(index_url)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Example JSON Response Format:
|
107
|
+
#
|
108
|
+
# {
|
109
|
+
# "info": {
|
110
|
+
# "name": "requests",
|
111
|
+
# "summary": "Python HTTP for Humans.",
|
112
|
+
# "author": "Kenneth Reitz",
|
113
|
+
# "license": "Apache-2.0"
|
114
|
+
# },
|
115
|
+
# "releases": {
|
116
|
+
# "2.32.3": [
|
117
|
+
# {
|
118
|
+
# "filename": "requests-2.32.3-py3-none-any.whl",
|
119
|
+
# "version": "2.32.3",
|
120
|
+
# "requires_python": ">=3.8",
|
121
|
+
# "yanked": false,
|
122
|
+
# "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl"
|
123
|
+
# },
|
124
|
+
# {
|
125
|
+
# "filename": "requests-2.32.3.tar.gz",
|
126
|
+
# "version": "2.32.3",
|
127
|
+
# "requires_python": ">=3.8",
|
128
|
+
# "yanked": false,
|
129
|
+
# "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz"
|
130
|
+
# }
|
131
|
+
# ],
|
132
|
+
# "2.27.0": [
|
133
|
+
# {
|
134
|
+
# "filename": "requests-2.27.0-py2.py3-none-any.whl",
|
135
|
+
# "version": "2.27.0",
|
136
|
+
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
137
|
+
# "yanked": false,
|
138
|
+
# "url": "https://files.pythonhosted.org/packages/47/01/f420e7add78110940639a958e5af0e3f8e07a8a8b62049bac55ee117aa91/requests-2.27.0-py2.py3-none-any.whl"
|
139
|
+
# },
|
140
|
+
# {
|
141
|
+
# "filename": "requests-2.27.0.tar.gz",
|
142
|
+
# "version": "2.27.0",
|
143
|
+
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
144
|
+
# "yanked": false,
|
145
|
+
# "url": "https://files.pythonhosted.org/packages/c0/e3/826e27b942352a74b656e8f58b4dc7ed9495ce2d4eeb498181167c615303/requests-2.27.0.tar.gz"
|
146
|
+
# }
|
147
|
+
# ]
|
148
|
+
# }
|
149
|
+
# }
|
150
|
+
sig do
|
151
|
+
params(index_url: String)
|
152
|
+
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
153
|
+
end
|
154
|
+
def fetch_from_json_registry(index_url)
|
155
|
+
json_url = index_url.sub(%r{/simple/?$}i, "/pypi/")
|
156
|
+
|
157
|
+
Dependabot.logger.info(
|
158
|
+
"Fetching release information from json registry at #{sanitized_url(json_url)} for #{dependency.name}"
|
159
|
+
)
|
160
|
+
|
161
|
+
response = registry_json_response_for_dependency(json_url)
|
162
|
+
|
163
|
+
return nil unless response.status == 200
|
164
|
+
|
165
|
+
begin
|
166
|
+
data = JSON.parse(response.body)
|
167
|
+
|
168
|
+
version_releases = data["releases"]
|
169
|
+
|
170
|
+
releases = format_version_releases(version_releases)
|
171
|
+
|
172
|
+
releases.sort_by(&:version).reverse
|
173
|
+
rescue JSON::ParserError
|
174
|
+
Dependabot.logger.warn("JSON parsing error for #{json_url}. Falling back to HTML.")
|
175
|
+
nil
|
176
|
+
rescue StandardError => e
|
177
|
+
Dependabot.logger.warn("Unexpected error while fetching JSON data: #{e.message}.")
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# This URL points to the Simple Index API for the "requests" package on PyPI.
|
183
|
+
# It provides an HTML listing of available package versions following PEP 503 (Simple Repository API).
|
184
|
+
# The information found here is useful for dependency resolution and package version retrieval.
|
185
|
+
#
|
186
|
+
# ✅ Information available in the Simple Index:
|
187
|
+
# - A list of package versions as anchor (`<a>`) elements.
|
188
|
+
# - URLs to distribution files (e.g., `.tar.gz`, `.whl`).
|
189
|
+
# - The `data-requires-python` attribute (if present) specifying the required Python version.
|
190
|
+
# - An optional `data-yanked` attribute indicating a yanked (withdrawn) version.
|
191
|
+
#
|
192
|
+
# ❌ Information NOT available in the Simple Index:
|
193
|
+
# - Release timestamps (upload time).
|
194
|
+
# - File digests (hashes like SHA256, MD5).
|
195
|
+
# - Package metadata such as description, author, or dependencies.
|
196
|
+
# - Download statistics.
|
197
|
+
# - Package type (`sdist` or `bdist_wheel`).
|
198
|
+
#
|
199
|
+
# To obtain full package metadata, use the PyPI JSON API:
|
200
|
+
# - JSON API: https://pypi.org/pypi/requests/json
|
201
|
+
#
|
202
|
+
# More details: https://www.python.org/dev/peps/pep-0503/
|
203
|
+
sig { params(index_url: String).returns(T::Array[Dependabot::Package::PackageRelease]) }
|
204
|
+
def fetch_from_html_registry(index_url)
|
205
|
+
Dependabot.logger.info(
|
206
|
+
"Fetching release information from html registry at #{sanitized_url(index_url)} for #{dependency.name}"
|
207
|
+
)
|
208
|
+
index_response = registry_response_for_dependency(index_url)
|
209
|
+
if index_response.status == 401 || index_response.status == 403
|
210
|
+
registry_index_response = registry_index_response(index_url)
|
211
|
+
|
212
|
+
if registry_index_response.status == 401 || registry_index_response.status == 403
|
213
|
+
raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
version_releases = extract_release_details_json_from_html(index_response.body)
|
218
|
+
releases = format_version_releases(version_releases)
|
219
|
+
|
220
|
+
releases.sort_by(&:version).reverse
|
221
|
+
end
|
222
|
+
|
223
|
+
sig do
|
224
|
+
params(html_body: String)
|
225
|
+
.returns(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]) # Returns JSON-like format
|
226
|
+
end
|
227
|
+
def extract_release_details_json_from_html(html_body)
|
228
|
+
doc = Nokogiri::HTML(html_body)
|
229
|
+
|
230
|
+
releases = {}
|
231
|
+
|
232
|
+
doc.css("a").each do |a_tag|
|
233
|
+
details = version_details_from_link(a_tag.to_s)
|
234
|
+
if details && details["version"]
|
235
|
+
releases[details["version"]] ||= []
|
236
|
+
releases[details["version"]] << details
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
releases
|
241
|
+
end
|
242
|
+
|
243
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
244
|
+
sig do
|
245
|
+
params(link: T.nilable(String))
|
246
|
+
.returns(T.nilable(T::Hash[String, T.untyped]))
|
247
|
+
end
|
248
|
+
def version_details_from_link(link)
|
249
|
+
return unless link
|
250
|
+
|
251
|
+
doc = Nokogiri::XML(link)
|
252
|
+
filename = doc.at_css("a")&.content
|
253
|
+
url = doc.at_css("a")&.attributes&.fetch("href", nil)&.value
|
254
|
+
|
255
|
+
return unless filename&.match?(name_regex) || url&.match?(name_regex)
|
256
|
+
|
257
|
+
version = get_version_from_filename(filename)
|
258
|
+
return unless version_class.correct?(version)
|
259
|
+
|
260
|
+
{
|
261
|
+
"version" => version,
|
262
|
+
"requires_python" => requires_python_from_link(link),
|
263
|
+
"yanked" => link.include?("data-yanked"),
|
264
|
+
"url" => link
|
265
|
+
}
|
266
|
+
end
|
267
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
268
|
+
|
269
|
+
sig do
|
270
|
+
params(
|
271
|
+
releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]])
|
272
|
+
)
|
273
|
+
.returns(T::Array[Dependabot::Package::PackageRelease])
|
274
|
+
end
|
275
|
+
def format_version_releases(releases_json)
|
276
|
+
return [] unless releases_json
|
277
|
+
|
278
|
+
releases_json.each_with_object([]) do |(version, release_data_array), versions|
|
279
|
+
release_data = release_data_array.last
|
280
|
+
|
281
|
+
next unless release_data
|
282
|
+
|
283
|
+
release = format_version_release(version, release_data)
|
284
|
+
|
285
|
+
next unless release
|
286
|
+
|
287
|
+
versions << release
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
sig do
|
292
|
+
params(
|
293
|
+
version: String,
|
294
|
+
release_data: T::Hash[String, T.untyped]
|
295
|
+
)
|
296
|
+
.returns(T.nilable(Dependabot::Package::PackageRelease))
|
297
|
+
end
|
298
|
+
def format_version_release(version, release_data)
|
299
|
+
upload_time = release_data["upload_time"]
|
300
|
+
released_at = Time.parse(upload_time) if upload_time
|
301
|
+
yanked = release_data["yanked"] || false
|
302
|
+
yanked_reason = release_data["yanked_reason"]
|
303
|
+
downloads = release_data["downloads"] || -1
|
304
|
+
url = release_data["url"]
|
305
|
+
package_type = release_data["packagetype"]
|
306
|
+
language = package_language(
|
307
|
+
python_version: release_data["python_version"],
|
308
|
+
requires_python: release_data["requires_python"]
|
309
|
+
)
|
310
|
+
|
311
|
+
release = Dependabot::Package::PackageRelease.new(
|
312
|
+
version: Dependabot::Uv::Version.new(version),
|
313
|
+
released_at: released_at,
|
314
|
+
yanked: yanked,
|
315
|
+
yanked_reason: yanked_reason,
|
316
|
+
downloads: downloads,
|
317
|
+
url: url,
|
318
|
+
package_type: package_type,
|
319
|
+
language: language
|
320
|
+
)
|
321
|
+
release
|
322
|
+
end
|
323
|
+
|
324
|
+
sig do
|
325
|
+
params(
|
326
|
+
python_version: T.nilable(String),
|
327
|
+
requires_python: T.nilable(String)
|
328
|
+
)
|
329
|
+
.returns(T.nilable(Dependabot::Package::PackageLanguage))
|
330
|
+
end
|
331
|
+
def package_language(python_version:, requires_python:)
|
332
|
+
# Extract language name and version
|
333
|
+
language_name, language_version = convert_language_version(python_version)
|
334
|
+
|
335
|
+
# Extract language requirement
|
336
|
+
language_requirement = build_python_requirement(requires_python)
|
337
|
+
|
338
|
+
return nil unless language_version || language_requirement
|
339
|
+
|
340
|
+
# Return a Language object with all details
|
341
|
+
Dependabot::Package::PackageLanguage.new(
|
342
|
+
name: language_name,
|
343
|
+
version: language_version,
|
344
|
+
requirement: language_requirement
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
348
|
+
sig { params(version: T.nilable(String)).returns([String, T.nilable(Dependabot::Version)]) }
|
349
|
+
def convert_language_version(version)
|
350
|
+
return ["python", nil] if version.nil? || version == "source"
|
351
|
+
|
352
|
+
# Extract numeric parts dynamically (e.g., "cp37" -> "3.7", "py38" -> "3.8")
|
353
|
+
extracted_version = version.scan(/\d+/).join(".")
|
354
|
+
|
355
|
+
# Detect the language implementation
|
356
|
+
language_name = if version.start_with?("cp")
|
357
|
+
"cpython" # CPython implementation
|
358
|
+
elsif version.start_with?("py")
|
359
|
+
"python" # General Python compatibility
|
360
|
+
else
|
361
|
+
"unknown" # Fallback for unknown cases
|
362
|
+
end
|
363
|
+
|
364
|
+
# Ensure extracted version is valid before converting
|
365
|
+
language_version =
|
366
|
+
extracted_version.match?(/^\d+(\.\d+)*$/) ? Dependabot::Version.new(extracted_version) : nil
|
367
|
+
|
368
|
+
Dependabot.logger.warn("Skipping invalid language_version: #{version.inspect}") if language_version.nil?
|
369
|
+
|
370
|
+
[language_name, language_version]
|
371
|
+
end
|
372
|
+
|
373
|
+
sig { returns(T::Array[String]) }
|
374
|
+
def registry_urls
|
375
|
+
@registry_urls ||=
|
376
|
+
Package::PackageRegistryFinder.new(
|
377
|
+
dependency_files: dependency_files,
|
378
|
+
credentials: credentials,
|
379
|
+
dependency: dependency
|
380
|
+
).registry_urls
|
381
|
+
end
|
382
|
+
|
383
|
+
sig { returns(String) }
|
384
|
+
def normalised_name
|
385
|
+
NameNormaliser.normalise(dependency.name)
|
386
|
+
end
|
387
|
+
|
388
|
+
sig { params(json_url: String).returns(Excon::Response) }
|
389
|
+
def registry_json_response_for_dependency(json_url)
|
390
|
+
url = "#{json_url.chomp('/')}/#{@dependency.name}/json"
|
391
|
+
Dependabot::RegistryClient.get(
|
392
|
+
url: url,
|
393
|
+
headers: { "Accept" => APPLICATION_JSON }
|
394
|
+
)
|
395
|
+
end
|
396
|
+
|
397
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
398
|
+
def registry_response_for_dependency(index_url)
|
399
|
+
Dependabot::RegistryClient.get(
|
400
|
+
url: index_url + normalised_name + "/",
|
401
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
402
|
+
)
|
403
|
+
end
|
404
|
+
|
405
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
406
|
+
def registry_index_response(index_url)
|
407
|
+
Dependabot::RegistryClient.get(
|
408
|
+
url: index_url,
|
409
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
410
|
+
)
|
411
|
+
end
|
412
|
+
|
413
|
+
sig { params(filename: String).returns(T.nilable(String)) }
|
414
|
+
def get_version_from_filename(filename)
|
415
|
+
filename
|
416
|
+
.gsub(/#{name_regex}-/i, "")
|
417
|
+
.split(/-|\.tar\.|\.zip|\.whl/)
|
418
|
+
.first
|
419
|
+
end
|
420
|
+
|
421
|
+
sig do
|
422
|
+
params(req_string: T.nilable(String))
|
423
|
+
.returns(T.nilable(Dependabot::Requirement))
|
424
|
+
end
|
425
|
+
def build_python_requirement(req_string)
|
426
|
+
return nil unless req_string
|
427
|
+
|
428
|
+
requirement_class.new(CGI.unescapeHTML(req_string))
|
429
|
+
rescue Gem::Requirement::BadRequirementError
|
430
|
+
nil
|
431
|
+
end
|
432
|
+
|
433
|
+
sig { params(link: String).returns(T.nilable(String)) }
|
434
|
+
def requires_python_from_link(link)
|
435
|
+
raw_value = Nokogiri::XML(link)
|
436
|
+
.at_css("a")
|
437
|
+
&.attribute("data-requires-python")
|
438
|
+
&.content
|
439
|
+
|
440
|
+
return nil unless raw_value
|
441
|
+
|
442
|
+
CGI.unescapeHTML(raw_value) # Decodes HTML entities like >=3 → >=3
|
443
|
+
end
|
444
|
+
|
445
|
+
sig { returns(T.class_of(Dependabot::Version)) }
|
446
|
+
def version_class
|
447
|
+
dependency.version_class
|
448
|
+
end
|
449
|
+
|
450
|
+
sig { returns(T.class_of(Dependabot::Requirement)) }
|
451
|
+
def requirement_class
|
452
|
+
dependency.requirement_class
|
453
|
+
end
|
454
|
+
|
455
|
+
sig { params(index_url: String).returns(T::Hash[String, String]) }
|
456
|
+
def auth_headers_for(index_url)
|
457
|
+
credential = @credentials.find { |cred| cred["index-url"] == index_url }
|
458
|
+
return {} unless credential
|
459
|
+
|
460
|
+
{ "Authorization" => "Basic #{Base64.strict_encode64(
|
461
|
+
"#{credential[CREDENTIALS_USERNAME]}:#{credential[CREDENTIALS_PASSWORD]}"
|
462
|
+
)}" }
|
463
|
+
end
|
464
|
+
|
465
|
+
sig { returns(Regexp) }
|
466
|
+
def name_regex
|
467
|
+
parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
|
468
|
+
/#{parts.join("[\s_.-]")}/i
|
469
|
+
end
|
470
|
+
|
471
|
+
sig { params(index_url: T.nilable(String)).returns(T::Boolean) }
|
472
|
+
def validate_index(index_url)
|
473
|
+
return false unless index_url
|
474
|
+
|
475
|
+
return true if index_url.match?(URI::DEFAULT_PARSER.regexp[:ABS_URI])
|
476
|
+
|
477
|
+
raise Dependabot::DependencyFileNotResolvable,
|
478
|
+
"Invalid URL: #{sanitized_url(index_url)}"
|
479
|
+
end
|
480
|
+
|
481
|
+
sig { params(index_url: String).returns(String) }
|
482
|
+
def sanitized_url(index_url)
|
483
|
+
index_url.sub(%r{//([^/@]+)@}, "//redacted@")
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
data/lib/dependabot/uv/{update_checker/index_finder.rb → package/package_registry_finder.rb}
RENAMED
@@ -7,8 +7,9 @@ require "dependabot/errors"
|
|
7
7
|
|
8
8
|
module Dependabot
|
9
9
|
module Uv
|
10
|
-
|
11
|
-
class
|
10
|
+
module Package
|
11
|
+
class PackageRegistryFinder
|
12
|
+
extend T::Sig
|
12
13
|
PYPI_BASE_URL = "https://pypi.org/simple/"
|
13
14
|
ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
|
14
15
|
|
@@ -18,7 +19,8 @@ module Dependabot
|
|
18
19
|
@dependency = dependency
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
+
sig { returns(T::Array[String]) }
|
23
|
+
def registry_urls
|
22
24
|
extra_index_urls =
|
23
25
|
config_variable_index_urls[:extra] +
|
24
26
|
pipfile_index_urls[:extra] +
|
@@ -140,7 +142,7 @@ module Dependabot
|
|
140
142
|
end
|
141
143
|
|
142
144
|
def config_variable_index_urls
|
143
|
-
urls = { main: nil, extra: [] }
|
145
|
+
urls = { main: T.let(nil, T.nilable(String)), extra: [] }
|
144
146
|
|
145
147
|
index_url_creds = credentials
|
146
148
|
.select { |cred| cred["type"] == "python_index" }
|
@@ -158,7 +160,7 @@ module Dependabot
|
|
158
160
|
end
|
159
161
|
|
160
162
|
def clean_check_and_remove_environment_variables(url)
|
161
|
-
url = url.strip.
|
163
|
+
url = url.strip.sub(%r{/+$}, "") + "/"
|
162
164
|
|
163
165
|
return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
|
164
166
|
|
@@ -190,7 +192,11 @@ module Dependabot
|
|
190
192
|
cred = credential_for(base_url)
|
191
193
|
return base_url unless cred
|
192
194
|
|
193
|
-
AuthedUrlBuilder.authed_url(credential: cred)
|
195
|
+
builder = AuthedUrlBuilder.authed_url(credential: cred)
|
196
|
+
|
197
|
+
return base_url unless builder
|
198
|
+
|
199
|
+
builder.gsub(%r{/*$}, "") + "/"
|
194
200
|
end
|
195
201
|
|
196
202
|
def credential_for(url)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
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
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
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
|
@@ -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
|
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(
|
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(
|
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(
|
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
|
@@ -170,6 +170,7 @@ module Dependabot
|
|
170
170
|
credentials: credentials,
|
171
171
|
ignored_versions: ignored_versions,
|
172
172
|
raise_on_ignored: @raise_on_ignored,
|
173
|
+
update_cooldown: @update_cooldown,
|
173
174
|
security_advisories: security_advisories
|
174
175
|
)
|
175
176
|
end
|
@@ -241,6 +242,7 @@ module Dependabot
|
|
241
242
|
credentials: credentials,
|
242
243
|
ignored_versions: ignored_versions,
|
243
244
|
raise_on_ignored: @raise_on_ignored,
|
245
|
+
cooldown_options: @update_cooldown,
|
244
246
|
security_advisories: security_advisories
|
245
247
|
)
|
246
248
|
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.
|
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-
|
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.
|
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.
|
26
|
+
version: 0.302.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: debug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -266,13 +266,14 @@ files:
|
|
266
266
|
- lib/dependabot/uv/metadata_finder.rb
|
267
267
|
- lib/dependabot/uv/name_normaliser.rb
|
268
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
|
269
271
|
- lib/dependabot/uv/package_manager.rb
|
270
272
|
- lib/dependabot/uv/pipenv_runner.rb
|
271
273
|
- lib/dependabot/uv/requirement.rb
|
272
274
|
- lib/dependabot/uv/requirement_parser.rb
|
273
275
|
- lib/dependabot/uv/requirements_file_matcher.rb
|
274
276
|
- lib/dependabot/uv/update_checker.rb
|
275
|
-
- lib/dependabot/uv/update_checker/index_finder.rb
|
276
277
|
- lib/dependabot/uv/update_checker/latest_version_finder.rb
|
277
278
|
- lib/dependabot/uv/update_checker/lock_file_resolver.rb
|
278
279
|
- lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb
|
@@ -284,7 +285,7 @@ licenses:
|
|
284
285
|
- MIT
|
285
286
|
metadata:
|
286
287
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
287
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
288
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.302.0
|
288
289
|
post_install_message:
|
289
290
|
rdoc_options: []
|
290
291
|
require_paths:
|